Skip to content

IxVM: handle wrapped recursor telescopes and theorem-headed iota majors - #545

Merged
arthurpaulino merged 2 commits into
mainfrom
ap/ixvm2
Aug 10, 2026
Merged

IxVM: handle wrapped recursor telescopes and theorem-headed iota majors#545
arthurpaulino merged 2 commits into
mainfrom
ap/ixvm2

Conversation

@arthurpaulino

Copy link
Copy Markdown
Member

Summary

Fix two WHNF gaps that caused IxVM to reject valid Lean constants involving recursors:

  • Whnf index telescopes while reconstructing canonical recursors, so index binders hidden behind definitional wrappers are discovered.
  • Unfold theorem-headed recursor majors at the iota site, while keeping theorem bodies stuck in general WHNF.
  • Run K-recursor synthesis on the raw major before major WHNF, matching the reference kernel and avoiding expensive evaluation of proof-producing decision procedures.
  • Guard the hoisted K path against partially applied recursors before reading the major from the spine.

Together these changes fix all 34 previously reported mathlib failures, including Rat.instEncodable and the family of failures involving wrapped inductive types.

Details

Canonical recursor reconstruction previously peeled an inductive's declared type structurally. For declarations whose index telescope is hidden behind an abbrev, this encountered an application instead of a Forall and aborted. collect_index_doms now preserves the cheap structural fast path, falls back to WHNF when necessary, and stops tolerantly in the same way as the reference implementation. The earlier validation of the complete parameter and index telescope remains strict, so this does not weaken the kernel check.

Separately, a recursor major may legitimately be headed by an auto-generated theorem after Lean's abstractNestedProofs pass. General IxVM WHNF intentionally leaves theorem heads stuck, preventing iota reduction in this case. The new whnf_iota_major unfolds theorem heads only while reducing an iota major. This gives the required reduction without globally expanding every proof term.

For K-like recursors, synthesis now runs against the raw major before attempting major WHNF, as in the reference kernel. This avoids evaluating potentially large decision procedures merely to reduce proof-valued majors. Because this moves the K gate ahead of try_iota, it now checks that the spine actually contains the major before calling list_lookup; partially applied recursors simply decline the iota step.

Tests and cost

  • Added HiddenIdx/PredOver coverage for index binders hidden by a definitional wrapper.
  • Added thmMajorUse coverage for theorem-headed iota majors.
  • Added partialKRec coverage for partially applied K recursors.
  • Both IxVM test suites pass.
  • All 34 previously failing mathlib constants pass; Rat.instEncodable completes in 119 seconds.
  • Performance guards remain flat: Std.Time...ofDays._proof_1 5.1s, bitblast goCache equality 41s, goCache_Inv_of_Inv 112s, and UInt8.ofBitVec_not 2.3s.
  • FFT pins were re-harvested. The index-telescope change moves affected pins by at most 0.0002%; the iota/K change has a largest shift of +0.03%, with some recursor-heavy cases improving by avoiding major WHNF.

The canonical recursor reconstruction collected index domains off the
inductive's declared type with a structural-only peel. When that type is
a definitional wrapper — an `abbrev` such as mathlib's
`MorphismProperty C := ...` — the index binder only appears after whnf,
so the peel met an `App` node, fell off the match, and aborted
mid-execution. Every recursor for such an inductive was wrongly
rejected: 33 of the 34 reported mathlib failures, all with
"no match case for value 3" (tag 3 = App).

collect_index_doms now mirrors the reference's loop
(inductive.rs build_motive_type_flat, `for _ in 0..n_indices { whnf;
All => push; _ => break }`): structural first — whnf of a Forall is
that Forall, so the orders agree and the common case pays nothing —
whnf on the fallback, and a tolerant stop. The empty whnf context
mirrors the reference, which performs this walk with no ctx pushes; a
context-poorer whnf can only under-reduce, never invent a reduction.

The tolerant stop is not a weakened gate: get_result_sort_level already
peels n_params + n_indices Foralls off the same declared type
intolerantly (and whnf'd) before any of this runs, so reaching the
index walk with fewer binders is impossible for a block that validated.

Only this one site changed. The five sibling telescope walks were left
alone — no evidence implicates them, and each whnf costs.

Fixture IxVMInd.HiddenIdx pins the shape (an inductive whose declared
type is an abbrev over a predicate), verified to abort before the fix.
All kernel FFT pins re-harvested: 59 of 68 shifted, every one by
<= 0.0002% — the fixed cost of the added call site.

Regression guard, all flat vs this base: bitblast goCache eq_def 41.1s,
goCache_Inv_of_Inv 112s, UInt8.ofBitVec_not 2.3s, Std.Time ofDays
5.1s. Both test suites green. Rat.instEncodable still fails — a
separate root cause (theorem-headed iota major), fixed separately.
A recursor can only fire once its major reaches constructor form, but a
major can legitimately be a THEOREM: `abstractNestedProofs` lifts inline
proofs into auto-theorems, so `And.rec motive minor MAJOR` gets a
`..._proof_1` there. The general reducer keeps `Thm` heads stuck, so the
iota never fired and `Rat.instEncodable` was rejected with "inferred type
is not def-eq to the expected type".

Unfolding theorems globally (as the reference's delta loop does) fixes
the verdict but costs `Std.Time...ofDays._proof_1` 14x, 5.1s -> 69.8s:
every proof term the check touches then gets driven through its body.
Theorem unfolding is only needed to drive an iota major, so it lives at
that one site, `whnf_iota_major`, called from `try_iota`. Under-reducing
elsewhere is the safe direction — it can only leave a term stuck, never
manufacture a reduction.

That alone still regressed ofDays, because a K major is typically a proof
and whnf'ing it evaluates whatever decision procedure produced it
(ofDays' major is an `Eq.rec` over a `decide +kernel` proof, so reducing
it runs the whole Rat/Nat procedure over 14-digit literals). The
reference synthesizes the K constructor from the RAW major first
(whnf.rs:1349-1356), so `try_k_synth_iota` now runs above `try_iota`.
The gate infers the type of the raw major, so its verdict does not depend
on the major having been whnf'd; this is ordering only.

Hoisting the gate broke its precondition: it reads the major with
`list_lookup`, whose `Cons` binding is irrefutable, and its only previous
caller was `try_iota`'s stuck branch, which had already established
`major_idx < spine_len`. A partially applied recursor then walked off the
end of the spine and aborted on `Nil`, which surfaced as "no match case
for value 1" on two `Std.Tactic.BVDecide` constants. The gate now
re-establishes the same length guard itself; a recursor short of its
major cannot iota at all, so declining is also the right verdict.

Both halves get a two-sided fixture: `thmMajorUse` is rejected without
`whnf_iota_major` and accepted with it, `partialKRec` aborts without the
length guard and passes with it.

Guard set flat: ofDays 5.1s, goCache._mutual.eq_def 41s,
goCache_Inv_of_Inv._mutual 112s, UInt8.ofBitVec_not 2.3s. All 34
previously-failing mathlib constants pass, `Rat.instEncodable` in 119s.
Pins re-harvested for the added call site; the largest shift is +0.03%
and several recursor-heavy entries drop, which is the avoided major whnf.
@arthurpaulino
arthurpaulino enabled auto-merge (squash) August 10, 2026 21:12
@arthurpaulino
arthurpaulino merged commit fc4f43a into main Aug 10, 2026
11 checks passed
@arthurpaulino
arthurpaulino deleted the ap/ixvm2 branch August 10, 2026 21:12
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.

2 participants