Skip to content

audit(decode): 3 false accepts from unconsumed fields; disprove the EAnnot comment - #19

Merged
Ch4s3 merged 3 commits into
mainfrom
claude/unconsumed-audit
Aug 8, 2026
Merged

Ch4s3 merged 3 commits into
mainfrom
claude/unconsumed-audit

Conversation

@Ch4s3

@Ch4s3 Ch4s3 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Audit: decoded-but-unconsumed fields — 3 more false accepts, 1 false comment

A systematic sweep for a specific defect signature that produced the three worst bugs found in this project:

The decoder populates a field, something downstream reads it, but the layer that should act on it silently drops it — behind a comment making the omission look deliberate.

That signature previously produced: retAnnot (function return types were never checked — 8 false accepts), println (dead builtin-table entry — 8 false rejects), and ELet (discarded a let RHS, blinding all four capability walks).

The audit traced every decoder-populated field to its consumers, distinguishing "read by" from "acted on by" — the distinction is the whole point. retAnnot was read in several places and acted on in none that mattered, so a "is this referenced?" grep would have cleared it.

Confirmed divergences (all verified against march's actual diagnostic)

Probe march rejects because before after
fn f[a : NoSuchThing](x : Int) : Int do x end Bound `NoSuchThing` is not a known ADT or interface name. accept skip
fn f[a : Int -> Int](…) Bound `Int -> Int` … must be an ADT name, interface name, or `Nat`. accept skip
cap no_panic + let bad = 10 / 0 division by zero literal in `cap no_panic` module. accept reject
cap no_panic/cap pure + app block calling panic/println `__app_init__` … calls `panic` skip (child lost) reject

The first two share one cause: fn.bounds was never decoded at all — march emits it, we never looked. The third is the recurring dfn-only assumption: division safety scanned function bodies but not top-level let bodies.

The false comment

Elab.decodeTerm asserted: "EAnnot/EHole/EResultRef … no parser production reaches them here."

The premise is true and the conclusion is false. Desugar synthesizes EAnnot for every app block (desugar.ml:929, annotating the spec field so the type checker verifies it returns SupervisorSpec), inside a DFn __app_init__ that is emitted. Confirmed on specs/lang/grammar/parse/p19_app_on_start_supervisor_spec.march, which emits EAnnot — the only unhandled expression kind across a sweep of all 490 emittable corpus files.

Parser coverage ≠ emitter coverage. Desugar sits between them. That lesson is now written into the code.

Claims that held under the same scrutiny: PatVar carries no lin; EPipe/ESigil are never emitted; ELetFn never decodes; Check-8's shared-blind-spot note.

Notable non-gaps

  • DLet.binding.ty (a top-level let x : String = 42) — march ignores it too (0/0). Decoding it would have created false rejects.
  • In-fn let annotations, lambda param annotations, retAnnot, and match-arm guards all verified as genuinely acted on.

Verification

Build clean 24/24, no warnings, no sorry/unsafe/import Mathlib; all pre-existing fixtures hold. New fixtures for all three fixes, including four near-misses pinning that the other cap gates stay dfn-only.

Regression sweep over 275 march-accepted files (specs/**/accept, examples/, stdlib/): verdict tuples byte-identical before and after — (0,0)=34, (0,2)=213, (1,2)=28. Zero false rejects, zero false accepts introduced.

total files: 334   MATCH 86   MISMATCH 0   ERROR 0
SKIP 246 (145 accept / 101 reject)   SKIP-LEDGER OK   RESULT PASS

Unchanged by these fixes — the corpus exercises none of these shapes. Fifth consecutive time.

Where the next defect of this shape lives (reported, not fixed)

  1. Decl.dlet in the remaining cap gates. They are dfn-only, and correct only because march's checkers are DFn-only today — a dependency on a moving target, and march is widening them one at a time.
  2. The six other decl forms march's division walk covers (DImpl/DActor/DTest/DApp/DDescribe/DSetup*). The opaque_ "carry the children anyway" move has been made for terms but never for declarations.
  3. vis is unmodelled (masked today by the qualified-call gap); Module.insts is decode-only, its docstring naming a consumer that was never written (costs exit-4 precision only, never soundness).

Cheapest standing defence

A corpus kind-histogram diffed against decodeTerm/decodeDecl's handled sets would have caught both ELet and EAnnot for free — mechanically, with no probing. Worth building: it converts this entire defect class from "hope someone audits" into a check.

Ch4s3 added 3 commits August 8, 2026 12:42
…just `dfn`

march's `refinecheck/division_safety.ml` walk is exhaustive over `A.decl`
with no wildcard and names `A.DLet (_, b, _) -> expr b.bind_expr` (:574) —
an arm added because omitting it was a march bug that shipped (see
specs/lang/types/reject/t120's header). `checkOneModule` scanned `dfn`
bodies only, reproducing march's OLD behavior.

Verified FALSE ACCEPT:
  mod M do cap no_panic  let bad = 10 / 0  ... end
march exit 1 ("division by zero literal in `cap no_panic` module."),
this checker exit 0. Now exit 1.

Scope is deliberately division-safety ONLY. Verified directly that march
ACCEPTS `cap pure` + `let bad = println("leak")` and `cap no_alloc` +
`let bad = (1, 2)` — `check_pure_module`/`check_deterministic_module`/
`check_no_panic_module`/`no_alloc.ml` all filter `Ast.DFn` alone. Both
near-misses are pinned as fixtures so a future widening breaks the build.

275 march-accepted files under specs/**/accept, examples/, stdlib/: verdicts
byte-identical before and after; zero rejects among them.
…fragment

`fn_def.bounds` (`fn f[a : SomeADT](...)`, parser.mly:386/414, ast.ml:231,
emitted at ast_json.ml:830 as `[{name, ty}]`) was never read by the decoder.
march does not merely record it: `typecheck.ml:6926-6959` VALIDATES every
bound and errors when it names neither a known ADT, a known interface, nor
`Nat`.

Two verified FALSE ACCEPTS (march exit 1, this checker exit 0):
  fn f[a : NoSuchThing](x : Int) : Int do x end
    -> "Bound `NoSuchThing` is not a known ADT or interface name."
  fn f[a : Int -> Int](x : Int) : Int do x end
    -> "Bound `Int -> Int` on type variable `a` must be an ADT name,
        interface name, or `Nat`."

A bound also pre-registers a type variable for param annotations to
reference, which this fragment cannot represent at all (`decodeSurfaceTy`
maps every `TyVar` to `Ty.unsupported`). So the honest answer is the same
one a guarded clause already gets: `Decl.unsupported`, i.e. an honest
whole-file skip. Fails conservative by construction — this can only move a
verdict toward skip, never toward a confident one.

Costs nothing: zero of the 490 emittable .march files under march's specs/,
examples/ and stdlib/ carry a non-empty `bounds`.
…cap layer

The `decodeTerm` docstring claimed `EAnnot` was safe to omit because "no
parser production reaches" it. The premise is true and the conclusion is
FALSE: `Desugar` synthesizes an `EAnnot` for every `app` block
(`desugar.ml:929` wraps the app body in `EAnnot (body', SupervisorSpec, _)`),
and the resulting `DFn __app_init__` IS emitted. `EAnnot` is in fact the
ONLY unhandled expression `kind` present anywhere in a sweep of all 490
emittable .march files under march's specs/, examples/ and stdlib/.

Falling to `| _ => Term.unsupported` discarded the child expression, so a
`cap` violation inside an app body was invisible to every `CapCheck` walk —
the exact shape of the `ELet` defect. Verified divergences:
  cap no_panic + app body `panic("boom")`  -> march 1 (`__app_init__` ...
    calls `panic`), this checker 2
  cap pure     + app body `println(...)`   -> march 1 (`__app_init__` ...
    calls `println`), this checker 2
Both are exit 1 now.

Zero false-reject exposure: `opaque_` models no typing rule and
`Term.hasUnsupported` is hard-coded `true` for it, so `Infer` and
`Linearity` still never see the node — identical contract to the nine
`opaque_` kinds already decoded this way. Only the child expression is
carried; the ascribed type is deliberately dropped.

The docstring is corrected in place, including the general lesson: "no
parser production reaches X" is not the claim "X is not emitted", because
Desugar runs between them and manufactures nodes of its own.
@Ch4s3
Ch4s3 merged commit ca087a1 into main Aug 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant