Skip to content

Route kernel-decidable Lean verify cases through decide +kernel - #812

Merged
jasisz merged 3 commits into
mainfrom
feat/kernel-decide-verify-cases
Aug 8, 2026
Merged

Route kernel-decidable Lean verify cases through decide +kernel#812
jasisz merged 3 commits into
mainfrom
feat/kernel-decide-verify-cases

Conversation

@jasisz

@jasisz jasisz commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Sampled verify cases all emitted native_decide, so every case's axiom closure carried Lean.ofReduceBool — including concrete SHA-256 examples, whose exported model is total and kernel-reducible.

Each case is now classified individually. decide +kernel is emitted only when the emitter can positively establish that the case's whole closure reduces in the kernel; anything unknown stays on native_decide.

What declines a case:

  • kernel-opaque emissions, read off the text this export actually produced (partial def, unsafe, opaque, sorry, a fuel wrapper's panic! arm, any mutual group)
  • any Float reachable through the closure (no DecidableEq Float; the prelude shim is an @[implemented_by] opaque constant), and recursive user types, which carry the same shim
  • a builtin outside the table pinned against Lean 4.32 — the match is exhaustive, so a new Builtin variant is a compile error rather than a silent reclassification
  • no VM ground-truth literal on the expected side: panic! returns default silently under kernel reduction where native evaluation prints the PANIC at … line aver proof --check charges as a failure
  • an emitted term above a 1 KiB data budget — kernel reduction has no heartbeat limit, so an oversized case would only make lake build slow

Only proof mode classifies; the standard emit is byte-identical.

Evidence on tests/fixtures/stdlib_bytes_app.av: the SHA-256, byte-range and digest-length cases emit decide +kernel and build green (1.3–7.4 s per case; entry module 0.3 s → 6.3 s, whole project 5.4 s → 7.8 s), with #print axioms reporting only propext / Quot.sound. The hex cases stay on native_decide because they route through the partial def hex parser. Both verdicts live in one file, so a classifier that collapses either way fails the new regression.

Tests: two additions in tests/proof_spec/lean_kernel.rs (crypto routing + axiom-closure audit via real lake, and a Float pin on the new tests/fixtures/kernel_decide_split.av). Full proof_spec suite green locally with real lake — 233 passed, 0 failed, 171 s. The large_domain_law golden moves one tripleSum case to the new tactic.

🤖 Generated with Claude Code

jasisz and others added 3 commits August 8, 2026 15:22
Sampled `verify` cases all emitted `native_decide`, which puts
`Lean.ofReduceBool` into every case's axiom closure — the proof rests on
native evaluation rather than on the kernel. Concrete SHA-256 examples
carried that trust axiom even though the exported crypto model is total
and kernel-reducible.

Classify each case instead. A case emits `decide +kernel` only when the
emitter can positively establish that its whole closure reduces in the
kernel, and `native_decide` whenever anything is unknown:

- Kernel-opaque emissions are read off the text this export actually
  produced (`partial def`, `unsafe`, `opaque`, `sorry`, a fuel wrapper's
  `panic!` arm, any mutual group), not re-derived from the source shape.
- Any Float reachable through the closure disqualifies a case: Lean has
  no `DecidableEq Float`, and the prelude's shim is an `@[implemented_by]`
  `opaque` constant. Recursive user types carry the same shim.
- Builtins are allowed from an explicit table pinned against Lean 4.32;
  a new `Builtin` variant is a compile error there, not a silent
  reclassification.
- A case needs the VM ground-truth literal on its expected side. Lean's
  `panic!` returns `default` silently under kernel reduction where native
  evaluation prints the `PANIC at …` line `aver proof --check` charges as
  a failure, so a literal expected side keeps that gate meaningful.
- Oversized terms keep `native_decide`: kernel reduction has no heartbeat
  limit, so the emitted term text carries a 1 KiB data budget calibrated
  against the measured SHA-256 curve.

Only proof mode classifies; the standard emit is byte-identical.

On `tests/fixtures/stdlib_bytes_app.av` the SHA-256 and byte-range cases
now build green under `decide +kernel` (1.3-7.4 s each, entry module
0.3 s to 6.3 s) with axiom closures inside Lean's core three, while the
hex cases stay native because they route through the `partial def` hex
parser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The literal expected side was carrying the whole anti-vacuity argument,
and it does not hold on its own. Lean's `panic!` returns `default`, and
under kernel reduction it does so with no diagnostic, so a model that
panics still satisfies the equation whenever `default` equals the value
the VM recorded — `false` for any `Bool` case, which is half of them.

The reviewer's counterexample reaches that in two steps, neither of
which a single-builtin audit catches:

    s = match Vector.get(Vector.fromList(["x"]), 0 - 1)
        Option.None -> "B"
        Option.Some(_) -> ""
    Char.toCode(s) == 65

`Vector.get` lowers to `arr[Int.toNat i]?`, and `Int.toNat` maps every
negative index to `0`, so the model takes the `Some` arm the runtime
never took, lands on `Char.toCode ""`, and defaults that panic to `0`.
`0 == 65` is `false` — the literal the VM recorded. It built green and
silent under `decide +kernel`; on `native_decide` the same case prints
`PANIC at Char.toCode` and `aver proof --check` fails the run.

So classify the faithfulness of each builtin's lowering next to its
kernel-reducibility, in a second exhaustive table a new `Builtin`
variant cannot skip. Declined: `Char.toCode` (reaches `panic!` on the
empty string), `Vector.get` and `Vector.set` (narrow a negative index
the runtime rejects — and `Array.set!` panics outright on an empty
vector). Kept, with the reason recorded: `List.take` / `List.drop`
narrow through the same `Int.toNat` but the runtime clamps identically,
`String.slice` and `String.charAt` guard theirs, and SHA-256's
`Array.get!` / `set!` indices are fixed by the definition rather than by
any argument. Across the example and proof corpora 37 sampled cases move
back to `native_decide`, all in `examples/games/wumpus.av` (Vector.get)
and `examples/data/json.av` (Char.toCode).

Two more fixes in the same area:

- The term budget measured only the left-hand side, so a three-token
  call returning kilobytes of literal data passed it. The kernel reduces
  the equation, so budget the equation.
- The named-type walk keyed its field scan on the bare type name, which
  unions two dependency modules' same-named records: a `Metrics.Reading`
  of one Int inherited the Float of a `Sensors.Reading` and lost kernel
  coverage. Follow the stamped `TypeId` instead, with the source name
  only as the fallback for references the symbol table never bound.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jasisz
jasisz merged commit 33beb50 into main Aug 8, 2026
35 checks passed
@jasisz
jasisz deleted the feat/kernel-decide-verify-cases branch August 8, 2026 15:01
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