IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface - #457

Merged
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor
Jul 2, 2026
Merged

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface#457
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor

Conversation

@arthurpaulino

Copy link
Copy Markdown
Member

Three commits on top of main that cut ingress FFT cost on heavy shards and land a documented IxVM IOBuffer interface.

Measurement methodology — read first

Shard 51 with unstubbed blake3 currently OOMs at all practical ulimit settings on the dev machine. To get past memory exhaustion and produce a comparative measurement, the kernel's blake3 verification paths in load_verified_constant / load_verified_blob / load_verified_claim were stubbed to no-ops for profiling. The stubs are not part of any committed code in this PR — they were applied locally during measurement and reverted.

What this means for the numbers below:

  • The delta between two stubbed runs is real for the non-blake3 portion of the trace. Both endpoints elide the same verification work, so the comparison isolates the effect of the kernel changes in this PR.
  • The absolute stubbed cost (e.g. "93.94G FFT") is not the production shard-51 typecheck cost. Production includes blake3 work (~16–20% of the trace per earlier profiles) that the stubs omit.

Every shard-51 number below is a "blake3-stubbed trace" measurement. Per-const numbers from lake test -- --ignored ixvm (no stubs) are unaffected and reflect real production cost.

Headline

MetricBeforeAfterΔ
Shard 51 (blake3-stubbed trace)119_152_044_06293_939_967_063−21.1% on the non-blake3 portion
Vector.append (production, unstubbed)2_661_244_8452_607_925_682−2.00%
Array.append_assoc (production)2_588_157_5382_537_478_644−1.96%
_private...extractMainModule._unsafe_rec (production)1_091_354_0311_064_762_809−2.44%

Per-const wins are end-to-end on the real (unstubbed) kernel; the shard-51 delta is real on the non-blake3 portion of the trace.

Commits

1. 120001d — IxVM kernel: augment addr_pos_map with blob-ref sentinels

lookup_addr_pos's rbtree only contained const addresses. Every blob ref (literal-blob payload pointers in Constant.refs) probed the map, missed, and fell through to the O(N) linear scan over all_addrs which also returned 0. Wasted work proportional to (blob refs × shard closure size).

This commit adds a third value class to the same rbtree: a sentinel 4294967295 (beyond any honest pos+1), inserted by walking each const's refs once via augment_with_blob_refs. lookup_addr_pos becomes a 3-way match: 0 → fall back to linear scan (now only fires under the de-intern soundness corner-case, ~never in practice); SENTINEL → return 0 directly (known blob); pos+1 → return pos.

Shard 51 (blake3-stubbed): 119_152_044_062 → 102_190_540_489 — −14.2% on the non-blake3 portion.

address_eq row count drops from ~10.3M to ~3.4M (−67%); lookup_addr_pos_linear falls out of the top-25 cost table. The augment-walk's added rbtree probes (~430M FFT in the stubbed trace) are paid back many times over by the ~17B FFT saved on lookups.

Soundness: every probe still relies on the positive direction ptr_val equality ⇒ content equality (Aiur Store content-addressing invariant). The linear fallback stays for the (still theoretical) malicious de-intern, where it uses content-based address_eq.

2. 9acbd58 — IxVM ingress: plumbing helpers + sidecar→rbtree migrations + ptr-passing

A 5-reviewer synthesis of ingress accidental complexity drove this consolidated refactor. Three families of change.

Plumbing helpers:

  • verify_bytes_against(bytes, expected) + bytes_to_addr(bytes) + blake3_flat(input) centralise the 24-line [h[i][j]] digest reshape that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
  • Channel I/O helpers factor the io_get_info + #read_byte_stream boilerplate at 6 sites. load_verified_blob no longer double-load(addr)s.
  • sentinel_blob_ref() named const replaces one bare 4294967295.

Sidecar lists → rbtree:

  • lookup_canon_addr: O(N) parallel-list walk → O(log N) rbtree probe via new build_canon_addr_map. canon_addrs: List<Addr> arg replaced by canon_addr_map: &RBTreeMap<Addr> (one threaded ptr).
  • lookup_block_start: O(N) parallel-list walk → O(log N) rbtree probe via new build_block_start_map. block_addrs / block_starts args replaced by block_start_map: &RBTreeMap<G> across ~12 fn signatures.
  • build_aux_recr_ctor_idxs returns (idxs, block_addr) so the standalone-Recr caller doesn't redo the rec_typ_to_inductive_addr + load_verified_constant chain to recover block_addr.
  • build_ref_idxs_and_blobs fuses the two separate walks (build_ref_idxs_mapped + build_lit_blobs) into one — single rbtree probe per ref produces both ref_idxs and lit_blobs. 5 caller sites simplified.

Pointer-passing (per Aiur's inputSize→width cost model):

  • convert_univ(u: &Univ): per-row input width drops from 5-variant union to one G column. Caller convert_univ_idxs already produces the &Univ via list_lookup(univs, idx) — no extra store.
  • convert_one(input: &ConvertInput): caller convert_all already has &input from the ListNode.Cons destructure.

Dead code deleted:address_in_list, apply_ctor_overrides, lookup_override, build_lit_blobs, build_ref_idxs_mapped, is_blob, ctx_convert_expr.

Shard 51 (blake3-stubbed): 102_190_540_489 → 93_939_967_063 — −8.07% on the non-blake3 portion on top of 120001d.

Per-const wins on heavy production targets (unstubbed lake test):

ConstantBeforeAfterΔ
Nat.add_comm54_369_74554_049_773−0.59%
Nat.sub_le_of_le_add515_331_420510_843_459−0.87%
Nat.decLe191_471_719189_723_325−0.91%
Array.append_assoc2_567_087_8932_537_360_311−1.16%
Vector.append2_639_286_0782_607_800_745−1.19%
IxVMInd.Even.rec31_659_49331_434_525−0.71%
IxVMInd.Odd.rec31_658_59831_433_622−0.71%
String.Internal.append718_803_075708_296_270−1.46%
_private...extractMainModule._unsafe_rec1_081_617_7051_064_689_765−1.57%

lake test -- --ignored ixvm green; 42 pins re-pinned.

3. 9c2eb23 — IxVM: per-kind IOBuffer channels with documented interface

The pre-existing IOBuffer multiplexed claim bytes, assumption tree bytes, constant wire bytes, and a per-blob-addr empty-marker on a single channel 0, distinguished only by blake3 content-hash collision-impossibility. A new contributor reading load_payload_const(key) couldn't tell from the helper whether they were getting a const, a claim, or a tree — only the caller knew the intent.

This commit splits channel 0 into per-kind channels and lands a documented IxVM IOBuffer interface block (mirrored on both Aiur and Lean sides).

TierChannelPurposeKeyValue
Ctrl0claim wire bytesblake3(claim_bytes)claim bytes
Ctrl1assumption tree bytestree.roottree bytes
Const2constant wire bytesconst addrconst bytes
Const3Defn reducibility hintDefn addrsingle G
Blob4blob discriminatoraddrone byte (1=const, 0=blob)
Blob5blob raw bytesblob addrraw bytes

Tier 1 fires once per verify_claim. Tier 2 fires per constant traversed during load_with_deps. Tier 3 fires per blob ref encountered during build_ref_idxs_and_blobs.

The blob discriminator (ch 4) replaces the previous io_get_info(0, addr).len == 0 ⟹ blob hack inside load_with_deps with an explicit per-addr one-byte payload. No more len-as-meaning overload.

Every per-channel io_get_info + #read_byte_stream pair is inlined at the single consumer of each channel — no fn-call row gets added for the reorganization. Channel reorganization is cost-neutral by design, confirmed by measurement: per-const pin shifts vs 9acbd58 land within ±0.010% (noise-tier; the ch-4 discriminator's one read_byte trades against the eliminated io_get_info(0, addr).len check).

The win here is review-time clarity, not FFT — io_get_info(channel, key) now has one value shape per channel and a doc block both kernel and harness reference.

Soundness: ch 0/1/2/5 byte streams are blake3-verified by the kernel against their content-addressed keys. ch 3 hint is semantically optional (controls WHNF reduction heuristic only; def-eq is sound either way). ch 4 discriminator is sound by erasure-correctness — a lying byte flips the const/blob decision and the wrong-path load downstream fails (a "const" blob triggers a ch 2 read that returns empty → blake3 verify against the non-empty addr fails; a "blob" const dangles references → typecheck fail).

Cumulative measurement (vs main)

ConstantBeforeAfterΔ
Nat.add_comm54_504_714 (approx.)54_047_004~−0.84%
Nat.decLe191_354_793189_722_204−0.85%
Nat.sub_le_of_le_add515_056_158510_870_092−0.81%
Array.append_assoc2_588_157_5382_537_478_644−1.96%
Vector.append2_661_244_8452_607_925_682−2.00%
String.Internal.append725_415_461708_332_683−2.36%
_private...extractMainModule._unsafe_rec1_091_354_0311_064_762_809−2.44%
Shard 51 (blake3-stubbed trace, non-blake3 portion)119_152_044_06293_939_967_063−21.1%

Test plan

  • lake build clean
  • lake test -- --ignored ixvm green at every commit
  • Shard 51 comparative measurement with blake3 stubs applied locally to both endpoints (stubs reverted; not part of any committed code in this PR)

`lookup_addr_pos`'s rbtree (`addr_pos_map`) only contained const
addresses. Every blob ref (literal-blob payload pointers in
`Constant.refs`) probed the map, missed, and fell through to the O(N)
linear scan over `all_addrs` which also returned 0. Wasted work
proportional to (blob refs × shard closure size).
Add a third value class to the same rbtree: the sentinel `4294967295`
(beyond any honest `pos+1`), inserted by walking each const's `refs`
once via `augment_with_blob_refs`. Any ref not already mapped to a
const-position gets the sentinel.
`lookup_addr_pos` becomes a 3-way match: `0` → fall back to linear
scan (now only fires under the de-intern soundness corner-case, ~never
in practice); `SENTINEL` → return 0 directly (known blob); `pos+1` →
return `pos`. `is_blob` mirrors the same match.
Soundness preserved: every probe still relies on the positive direction
`ptr_val` equality ⇒ content equality (Aiur Store's content-addressing
invariant). The linear fallback stays put for the (still theoretical)
malicious de-intern, where it uses content-based `address_eq`.
`lake exe ix check --ixe init.ixe --ixes init.ixes --shard 51`
(`ulimit -v 23000000`, with the kernel's blake3 paths in
`load_verified_constant` / `load_verified_blob` / `load_verified_claim`
stubbed for profiling):
119_152_044_062 → 102_190_540_489 FFT (-14.2%).
Tests/Ix/IxVM.lean: 42 pin shifts absorbing the per-ingress
augment-walk overhead.
A consolidated refactor of the ingress pipeline guided by a 5-reviewer
synthesis of accidental complexity sites. Three families of change:
PLUMBING
* `Blake3.lean`: `verify_bytes_against(bytes, expected)` + `bytes_to_addr(bytes)`
+ `blake3_flat(input)` centralise the 24-line `[h[i][j]]` digest reshape
that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
* `Ingress.lean`: `load_payload_const`/`blob`/`hint` + `ch_const`/`blob`/`hint`
factor the `io_get_info` + `#read_byte_stream` boilerplate at 6 sites.
`load_verified_blob` no longer double-`load(addr)`s.
* `sentinel_blob_ref()` named const replaces one bare `4294967295`.
SIDECAR LISTS → RBTREE
* `lookup_canon_addr`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_canon_addr_map`. `canon_addrs: List<Addr>` arg replaced
by `canon_addr_map: &RBTreeMap<Addr>` (one threaded ptr).
* `lookup_block_start`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_block_start_map`. `block_addrs` / `block_starts` args
replaced by `block_start_map: &RBTreeMap<G>` across ~12 fn signatures.
* `build_aux_recr_ctor_idxs` returns `(idxs, block_addr)` so the
standalone-Recr caller in `build_convert_inputs_walk` doesn't redo the
`rec_typ_to_inductive_addr` + `load_verified_constant` chain to recover
`block_addr` — `build_aux_recr_ctor_idxs` already computed it internally.
* `build_ref_idxs_and_blobs` fuses the two separate walks
(`build_ref_idxs_mapped` + `build_lit_blobs`) into one — single rbtree
probe per ref produces both `ref_idxs` and `lit_blobs`. 5 caller sites
simplified.
PTR-PASSING (per `reference_aiur_pass_pointer_not_value`)
* `convert_univ(u: &Univ)`: per-row input width drops from 5-variant
union to one G column. Caller `convert_univ_idxs` already produces the
`&Univ` via `list_lookup(univs, idx)` — no extra `store`.
* `convert_one(input: &ConvertInput)`: caller `convert_all` already has
`&input` from the `ListNode.Cons` destructure.
DEAD CODE DELETED
`address_in_list`, `apply_ctor_overrides`, `lookup_override`, `build_lit_blobs`,
`build_ref_idxs_mapped`, `is_blob`, `ctx_convert_expr` — all unused after
the migrations above.
TRIED + REVERTED (left as notes in code where instructive):
* Fused `build_addr_pos_map` + `augment_with_blob_refs` single-pass.
Lost to Aiur's per-row width tax — two narrower passes cheaper than
one merged wide pass.
* `convert_expr(ctx: &ConvertCtx)` ptr. Per-arm `load(ctx)` cost > raw-arg
threading.
* `compute_layout_walk` seen-mptrs/seen-poses → rbtree. Small regression
on typical shards (few enough Muts blocks that O(N) list scan is
already cheap; rbtree value column tax > savings).
* `block_members_map` to skip per-projection `load_verified_constant`.
Lost because Aiur memoises `load_verified_constant` automatically — the
new rbtree probe's width tax exceeded the savings from the eliminated
match cascade.
* `ctx` everywhere as `&ConvertCtx` (with `store(...)` at 5 construction
sites). The added `store` rows offset the per-row savings — wash.
`lake test -- --ignored ixvm`: 42 pin shifts, all measurable wins vs the
`ap/blob-ref-rbtree-augment` tip. Cumulative on heavy consts:
* `Nat.add_comm`: 54_369_745 → 54_049_773 FFT (-0.59%)
* `Nat.sub_le_of_le_add`: 515_331_420 → 510_843_459 FFT (-0.87%)
* `Array.append_assoc`: 2_567_087_893 → 2_537_360_311 FFT (-1.16%)
* `Vector.append`: 2_639_286_078 → 2_607_800_745 FFT (-1.19%)
* `String.Internal.append`: 718_803_075 → 708_296_270 FFT (-1.46%)
* `IxVMInd.Even.rec`: 31_659_493 → 31_434_525 FFT (-0.71%)
* `_private....extractMainModule._unsafe_rec`: 1_081_617_705 → 1_064_689_765 FFT (-1.57%)
Replaces channel 0's tagged-union (claim / tree / const bytes / empty-
blob-marker, distinguished only by key content-hash collision-
impossibility) with one channel per value kind. Channel reorganization
is cost-neutral — all six per-channel `io_get_info` + `#read_byte_stream`
pairs are inlined at the single consumer of each channel, so no fn-call
row gets added.
New layout, tiered by access pattern:
| Tier | Channel | Purpose | Key | Value |
|-------|---------|--------------------------|-------------------------|----------------------|
| Ctrl | 0 | claim wire bytes | `blake3(claim_bytes)` | claim bytes |
| Ctrl | 1 | assumption tree bytes | `tree.root` | tree bytes |
| Const | 2 | constant wire bytes | const addr | const bytes |
| Const | 3 | Defn reducibility hint | Defn addr | single G |
| Blob | 4 | blob discriminator | addr | one byte (1=const, 0=blob) |
| Blob | 5 | blob raw bytes | blob addr | raw bytes |
Tier 1 = one-per-`verify_claim`. Tier 2 = per-const. Tier 3 = per-blob.
`io_get_info(channel, key)` is now unambiguous by channel alone.
Aiur side (`Ingress.lean`, `Kernel/Claim.lean`):
* `load_verified_constant` inlines `io_get_info(2, raw)` +
`#read_byte_stream(2, ...)` (was ch 0).
* `load_verified_blob` inlines ch 5 read (was ch 1).
* `load_constant_hint` inlines ch 3 read (was ch 2).
* `load_with_deps` inlines a ch 4 discriminator probe replacing the
former `io_get_info(0, addr).len == 0 ⟹ blob` hack on ch 0. One
read_byte for the per-addr discriminator byte.
* `load_verified_claim` inlines ch 0 (was using a shared `load_payload_const`
helper on ch 0).
* `load_assumption_tree` inlines ch 1 (was using same `load_payload_const`
helper on ch 0).
* Helper wrappers (`load_payload_claim/tree/const/hint/blob`,
`load_discriminator`, `ch_*` constants) all removed — every channel
has exactly one consumer site and the inlined dispatch costs no fn
call.
* Top-of-file `IxVM IOBuffer interface` doc block documents the layout
+ soundness model.
Lean side (`ClaimHarness.lean`):
* `addEntries` writes per channel — consts → ch 2 + per-addr
discriminator `[1]` on ch 4; blobs → ch 5 + per-addr discriminator
`[0]` on ch 4; Defn hints → ch 3.
* `seedTreeAt` writes tree bytes to ch 1 (was ch 0).
* `buildClaimWitness` writes claim bytes to ch 0 (unchanged).
* Drops the empty-marker write on ch 0 for blob addrs — the ch 4
discriminator covers blob/const classification explicitly.
* Top-of-section `IxVM IOBuffer interface` doc block mirrors the
Aiur-side comment.
Soundness model unchanged. ch 0/1/2/5 byte streams are blake3-verified
by the kernel against their content-addressed keys. ch 3 hint is
semantically optional (controls WHNF reduction heuristic only; def-eq
is sound either way). ch 4 discriminator is sound by erasure-
correctness — a lying byte flips the const/blob decision and the
wrong-path load downstream fails (a "const" blob triggers a ch 2 read
that returns empty → blake3 verify against the non-empty addr fails;
a "blob" const dangles references → typecheck fail).
`lake test -- --ignored ixvm` green; 42 pins shifted within ±0.010%
(noise-tier) vs the pre-shuffle baseline — channel reorganization is
cost-neutral as required, the discriminator's one read_byte trades
against the eliminated `io_get_info(0, ...).len` check.
`blake3_flat` was a pure array-reshaping wrapper around `blake3` used
by `verify_bytes_against` and `bytes_to_addr`. Inline the reshape at
both call sites. `blake3`'s memoization key is the input ByteStream, so
the 74 cache hits that `blake3_flat` recorded migrate cleanly to
`blake3` — dedup preserved.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,898,064 → 12,430,154,083 (−743,981)
- Total width: 33,755 → 33,714 (−41)
- `blake3_flat` (41w × 1692h, 74 hits, 743,981 FFT) — removed
- `blake3` (92w × 1692h, 0 hits, ...) → now 74 hits
Same experiment for `bytes_to_addr` inline was neutral (−5k FFT, +92
width across 4 call sites) — kept as a wrapper.
@arthurpaulino
arthurpaulino marked this pull request as ready for review July 2, 2026 15:06
@arthurpaulino
arthurpaulino enabled auto-merge (squash) July 2, 2026 15:06
Mirror the Expr port pattern (`KExpr = &KExprNode`) for universe levels.
Kernel-side universe reps flip from unboxed enum values to pointer
aliases:
Before:
enum KLevel {
Zero,
Succ(&KLevel),
Max(&KLevel, &KLevel),
IMax(&KLevel, &KLevel),
Param(G)
}
After:
enum KLevelNode {
Zero,
Succ(KLevel),
Max(KLevel, KLevel),
IMax(KLevel, KLevel),
Param(G)
}
type KLevel = &KLevelNode
Callers switch from passing whole enum values to passing pointers.
Every match on a KLevel arg gains a `load()`; every value construction
that fills a KLevel arg or return slot gets a `store()`. `KExprNode.Srt`,
`KConstantInfo.{Ctor,Axiom,Defn,Thm,Opaque,Quot,Induct,Rec}` universe
fields all use the aliased `KLevel` now.
Motivation: pass-by-pointer shrinks per-row input width on every level-
manipulating function (`level_eq`, `level_leq`, `level_reduce`,
`level_inst_params`, ...), same trick as
`reference_aiur_pass_pointer_not_value`. Compared to the Expr port the
delta is smaller because level fns are already narrow, but it stacks.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,154,083 -> 12,418,344,860 (-11,809,223)
- Total width: 33,714 -> 33,532 (-182)
Re-pin every FFT cost shifted by the port.
@arthurpaulino
arthurpaulino merged commit 110f28f into mainJul 2, 2026
15 of 16 checks passed
@arthurpaulino
arthurpaulino deleted the ap/ingress-refactor branch July 2, 2026 19:04
johnchandlerburnham pushed a commit that referenced this pull request Jul 21, 2026
…505)
* IxVM: drop dead KValNode/KVal/KValEnv
From ap/kernel 828fb85 (Arthur Paulino): the NbE value domain is defined
but referenced nowhere — the live kernel runs on de-Bruijn KExpr;
vestigial from an abandoned NbE direction. (The closed-term context
normalization from that commit was measured separately and not taken:
the per-call expr_lbr probe cost +2.9% FFT on recursor loops for a 0.5%
record reduction.)
* IxVM: memoized prim_family dispatch + width-safe offset-stuck placement
Three coordinated changes to Const-head whnf dispatch (cherry-pick of
130f30b, adapted to post-#450/#457 main):
1. prim_family(addr) classifies a head address into the one reducer
family that could fire on it (nat/str/bitvec/native/decidable; the
sets are disjoint). Keyed on the ADDRESS ALONE it memoizes to one row
per distinct constant address per run, and whnf_const_head calls at
most one family reducer. The previous gauntlet ran every reducer in
sequence for guaranteed misses.
2. The symbolic-Nat offset-stuck check moves from a delta-arm probe into
try_nat_dispatch's miss path as a cold function
(try_nat_offset_dispatch, verdict 2 = "already stuck, do not
re-whnf"), with the offset construction shared via
mk_nat_offset_stuck (also used by the linear-rec collapse).
3. nat_lit_to_ctor_or_self exposes ONE constructor layer
(n -> succ(Lit(n-1))) instead of materializing the full succ chain.
Adaptations vs the original patch:
- whnf_nd_const_head (no-delta WHNF, added on main after the patch)
converted to the same family dispatch.
- Kept main's cold-extracted try_nat_binop_dispatch and routed the
symbolic-base case to try_nat_offset_dispatch from its miss arm.
Measured (lake exe ix check Nat.add_comm): total width 34820 -> 34806,
FFT cost 49571210 -> 48860647 (-1.43%). All 53 ixvm-suite FFT pins
decreased (-0.19%..-1.43%); parity and claim smokes pass. Pins updated;
crates/ixvm-codegen/src/aiur_ixvm.rs regenerated via `lake exe ix
codegen`.
* IxVM: port jcb/fixes H-14 — ptr_val skip map + lockstep addr cursor
Two quadratic/constant-factor fixes to check_all_skipping, ported from
jcb/fixes (3763356, John C. Burnham); cherry-pick of 2d83be1 adapted to
post-#457 main:
- The assumption-leaf skip set keys on ptr_val instead of the first 4
address bytes: one tree lookup per constant, no per-lookup address
load, no confirming address_eq. Sound by the build_addr_pos_map
interning invariant (one pointer, one content): a ptr hit implies the
address IS a leaf; a de-interned pointer reads as absent and the
constant just gets checked — fail-closed.
- The iterator walks the addrs list in LOCKSTEP with consts (cur_addrs
suffix) instead of list_lookup(addrs, pos) per constant, which
re-walked the prefix every iteration — a standalone O(closure^2).
Adaptations: kept main's two-arg check_canonical_block_sort call;
addr_key retained (the Inductive.lean block-membership table added on
main after this patch still uses it), comment updated.
Measured (lake exe ix check Nat.add_comm): total width 34806 -> 34781,
FFT cost unchanged (plain checks never take the skip path). Full ixvm
suite green incl. the frontier-assumption claim smoke; all FFT pins
unchanged. crates/ixvm-codegen/src/aiur_ixvm.rs regenerated.
---------
Co-authored-by: samuelburnham <45365069+samuelburnham@users.noreply.github.com>
Sign up for freeto 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

@arthurpaulino@gabriel-barrett
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface - #457

Merged
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor
Jul 2, 2026
Merged

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface#457
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor

Conversation

@arthurpaulino

Copy link
Copy Markdown
Member

Three commits on top of main that cut ingress FFT cost on heavy shards and land a documented IxVM IOBuffer interface.

Measurement methodology — read first

Shard 51 with unstubbed blake3 currently OOMs at all practical ulimit settings on the dev machine. To get past memory exhaustion and produce a comparative measurement, the kernel's blake3 verification paths in load_verified_constant / load_verified_blob / load_verified_claim were stubbed to no-ops for profiling. The stubs are not part of any committed code in this PR — they were applied locally during measurement and reverted.

What this means for the numbers below:

  • The delta between two stubbed runs is real for the non-blake3 portion of the trace. Both endpoints elide the same verification work, so the comparison isolates the effect of the kernel changes in this PR.
  • The absolute stubbed cost (e.g. "93.94G FFT") is not the production shard-51 typecheck cost. Production includes blake3 work (~16–20% of the trace per earlier profiles) that the stubs omit.

Every shard-51 number below is a "blake3-stubbed trace" measurement. Per-const numbers from lake test -- --ignored ixvm (no stubs) are unaffected and reflect real production cost.

Headline

MetricBeforeAfterΔ
Shard 51 (blake3-stubbed trace)119_152_044_06293_939_967_063−21.1% on the non-blake3 portion
Vector.append (production, unstubbed)2_661_244_8452_607_925_682−2.00%
Array.append_assoc (production)2_588_157_5382_537_478_644−1.96%
_private...extractMainModule._unsafe_rec (production)1_091_354_0311_064_762_809−2.44%

Per-const wins are end-to-end on the real (unstubbed) kernel; the shard-51 delta is real on the non-blake3 portion of the trace.

Commits

1. 120001d — IxVM kernel: augment addr_pos_map with blob-ref sentinels

lookup_addr_pos's rbtree only contained const addresses. Every blob ref (literal-blob payload pointers in Constant.refs) probed the map, missed, and fell through to the O(N) linear scan over all_addrs which also returned 0. Wasted work proportional to (blob refs × shard closure size).

This commit adds a third value class to the same rbtree: a sentinel 4294967295 (beyond any honest pos+1), inserted by walking each const's refs once via augment_with_blob_refs. lookup_addr_pos becomes a 3-way match: 0 → fall back to linear scan (now only fires under the de-intern soundness corner-case, ~never in practice); SENTINEL → return 0 directly (known blob); pos+1 → return pos.

Shard 51 (blake3-stubbed): 119_152_044_062 → 102_190_540_489 — −14.2% on the non-blake3 portion.

address_eq row count drops from ~10.3M to ~3.4M (−67%); lookup_addr_pos_linear falls out of the top-25 cost table. The augment-walk's added rbtree probes (~430M FFT in the stubbed trace) are paid back many times over by the ~17B FFT saved on lookups.

Soundness: every probe still relies on the positive direction ptr_val equality ⇒ content equality (Aiur Store content-addressing invariant). The linear fallback stays for the (still theoretical) malicious de-intern, where it uses content-based address_eq.

2. 9acbd58 — IxVM ingress: plumbing helpers + sidecar→rbtree migrations + ptr-passing

A 5-reviewer synthesis of ingress accidental complexity drove this consolidated refactor. Three families of change.

Plumbing helpers:

  • verify_bytes_against(bytes, expected) + bytes_to_addr(bytes) + blake3_flat(input) centralise the 24-line [h[i][j]] digest reshape that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
  • Channel I/O helpers factor the io_get_info + #read_byte_stream boilerplate at 6 sites. load_verified_blob no longer double-load(addr)s.
  • sentinel_blob_ref() named const replaces one bare 4294967295.

Sidecar lists → rbtree:

  • lookup_canon_addr: O(N) parallel-list walk → O(log N) rbtree probe via new build_canon_addr_map. canon_addrs: List<Addr> arg replaced by canon_addr_map: &RBTreeMap<Addr> (one threaded ptr).
  • lookup_block_start: O(N) parallel-list walk → O(log N) rbtree probe via new build_block_start_map. block_addrs / block_starts args replaced by block_start_map: &RBTreeMap<G> across ~12 fn signatures.
  • build_aux_recr_ctor_idxs returns (idxs, block_addr) so the standalone-Recr caller doesn't redo the rec_typ_to_inductive_addr + load_verified_constant chain to recover block_addr.
  • build_ref_idxs_and_blobs fuses the two separate walks (build_ref_idxs_mapped + build_lit_blobs) into one — single rbtree probe per ref produces both ref_idxs and lit_blobs. 5 caller sites simplified.

Pointer-passing (per Aiur's inputSize→width cost model):

  • convert_univ(u: &Univ): per-row input width drops from 5-variant union to one G column. Caller convert_univ_idxs already produces the &Univ via list_lookup(univs, idx) — no extra store.
  • convert_one(input: &ConvertInput): caller convert_all already has &input from the ListNode.Cons destructure.

Dead code deleted:address_in_list, apply_ctor_overrides, lookup_override, build_lit_blobs, build_ref_idxs_mapped, is_blob, ctx_convert_expr.

Shard 51 (blake3-stubbed): 102_190_540_489 → 93_939_967_063 — −8.07% on the non-blake3 portion on top of 120001d.

Per-const wins on heavy production targets (unstubbed lake test):

ConstantBeforeAfterΔ
Nat.add_comm54_369_74554_049_773−0.59%
Nat.sub_le_of_le_add515_331_420510_843_459−0.87%
Nat.decLe191_471_719189_723_325−0.91%
Array.append_assoc2_567_087_8932_537_360_311−1.16%
Vector.append2_639_286_0782_607_800_745−1.19%
IxVMInd.Even.rec31_659_49331_434_525−0.71%
IxVMInd.Odd.rec31_658_59831_433_622−0.71%
String.Internal.append718_803_075708_296_270−1.46%
_private...extractMainModule._unsafe_rec1_081_617_7051_064_689_765−1.57%

lake test -- --ignored ixvm green; 42 pins re-pinned.

3. 9c2eb23 — IxVM: per-kind IOBuffer channels with documented interface

The pre-existing IOBuffer multiplexed claim bytes, assumption tree bytes, constant wire bytes, and a per-blob-addr empty-marker on a single channel 0, distinguished only by blake3 content-hash collision-impossibility. A new contributor reading load_payload_const(key) couldn't tell from the helper whether they were getting a const, a claim, or a tree — only the caller knew the intent.

This commit splits channel 0 into per-kind channels and lands a documented IxVM IOBuffer interface block (mirrored on both Aiur and Lean sides).

TierChannelPurposeKeyValue
Ctrl0claim wire bytesblake3(claim_bytes)claim bytes
Ctrl1assumption tree bytestree.roottree bytes
Const2constant wire bytesconst addrconst bytes
Const3Defn reducibility hintDefn addrsingle G
Blob4blob discriminatoraddrone byte (1=const, 0=blob)
Blob5blob raw bytesblob addrraw bytes

Tier 1 fires once per verify_claim. Tier 2 fires per constant traversed during load_with_deps. Tier 3 fires per blob ref encountered during build_ref_idxs_and_blobs.

The blob discriminator (ch 4) replaces the previous io_get_info(0, addr).len == 0 ⟹ blob hack inside load_with_deps with an explicit per-addr one-byte payload. No more len-as-meaning overload.

Every per-channel io_get_info + #read_byte_stream pair is inlined at the single consumer of each channel — no fn-call row gets added for the reorganization. Channel reorganization is cost-neutral by design, confirmed by measurement: per-const pin shifts vs 9acbd58 land within ±0.010% (noise-tier; the ch-4 discriminator's one read_byte trades against the eliminated io_get_info(0, addr).len check).

The win here is review-time clarity, not FFT — io_get_info(channel, key) now has one value shape per channel and a doc block both kernel and harness reference.

Soundness: ch 0/1/2/5 byte streams are blake3-verified by the kernel against their content-addressed keys. ch 3 hint is semantically optional (controls WHNF reduction heuristic only; def-eq is sound either way). ch 4 discriminator is sound by erasure-correctness — a lying byte flips the const/blob decision and the wrong-path load downstream fails (a "const" blob triggers a ch 2 read that returns empty → blake3 verify against the non-empty addr fails; a "blob" const dangles references → typecheck fail).

Cumulative measurement (vs main)

ConstantBeforeAfterΔ
Nat.add_comm54_504_714 (approx.)54_047_004~−0.84%
Nat.decLe191_354_793189_722_204−0.85%
Nat.sub_le_of_le_add515_056_158510_870_092−0.81%
Array.append_assoc2_588_157_5382_537_478_644−1.96%
Vector.append2_661_244_8452_607_925_682−2.00%
String.Internal.append725_415_461708_332_683−2.36%
_private...extractMainModule._unsafe_rec1_091_354_0311_064_762_809−2.44%
Shard 51 (blake3-stubbed trace, non-blake3 portion)119_152_044_06293_939_967_063−21.1%

Test plan

  • lake build clean
  • lake test -- --ignored ixvm green at every commit
  • Shard 51 comparative measurement with blake3 stubs applied locally to both endpoints (stubs reverted; not part of any committed code in this PR)

`lookup_addr_pos`'s rbtree (`addr_pos_map`) only contained const
addresses. Every blob ref (literal-blob payload pointers in
`Constant.refs`) probed the map, missed, and fell through to the O(N)
linear scan over `all_addrs` which also returned 0. Wasted work
proportional to (blob refs × shard closure size).
Add a third value class to the same rbtree: the sentinel `4294967295`
(beyond any honest `pos+1`), inserted by walking each const's `refs`
once via `augment_with_blob_refs`. Any ref not already mapped to a
const-position gets the sentinel.
`lookup_addr_pos` becomes a 3-way match: `0` → fall back to linear
scan (now only fires under the de-intern soundness corner-case, ~never
in practice); `SENTINEL` → return 0 directly (known blob); `pos+1` →
return `pos`. `is_blob` mirrors the same match.
Soundness preserved: every probe still relies on the positive direction
`ptr_val` equality ⇒ content equality (Aiur Store's content-addressing
invariant). The linear fallback stays put for the (still theoretical)
malicious de-intern, where it uses content-based `address_eq`.
`lake exe ix check --ixe init.ixe --ixes init.ixes --shard 51`
(`ulimit -v 23000000`, with the kernel's blake3 paths in
`load_verified_constant` / `load_verified_blob` / `load_verified_claim`
stubbed for profiling):
119_152_044_062 → 102_190_540_489 FFT (-14.2%).
Tests/Ix/IxVM.lean: 42 pin shifts absorbing the per-ingress
augment-walk overhead.
A consolidated refactor of the ingress pipeline guided by a 5-reviewer
synthesis of accidental complexity sites. Three families of change:
PLUMBING
* `Blake3.lean`: `verify_bytes_against(bytes, expected)` + `bytes_to_addr(bytes)`
+ `blake3_flat(input)` centralise the 24-line `[h[i][j]]` digest reshape
that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
* `Ingress.lean`: `load_payload_const`/`blob`/`hint` + `ch_const`/`blob`/`hint`
factor the `io_get_info` + `#read_byte_stream` boilerplate at 6 sites.
`load_verified_blob` no longer double-`load(addr)`s.
* `sentinel_blob_ref()` named const replaces one bare `4294967295`.
SIDECAR LISTS → RBTREE
* `lookup_canon_addr`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_canon_addr_map`. `canon_addrs: List<Addr>` arg replaced
by `canon_addr_map: &RBTreeMap<Addr>` (one threaded ptr).
* `lookup_block_start`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_block_start_map`. `block_addrs` / `block_starts` args
replaced by `block_start_map: &RBTreeMap<G>` across ~12 fn signatures.
* `build_aux_recr_ctor_idxs` returns `(idxs, block_addr)` so the
standalone-Recr caller in `build_convert_inputs_walk` doesn't redo the
`rec_typ_to_inductive_addr` + `load_verified_constant` chain to recover
`block_addr` — `build_aux_recr_ctor_idxs` already computed it internally.
* `build_ref_idxs_and_blobs` fuses the two separate walks
(`build_ref_idxs_mapped` + `build_lit_blobs`) into one — single rbtree
probe per ref produces both `ref_idxs` and `lit_blobs`. 5 caller sites
simplified.
PTR-PASSING (per `reference_aiur_pass_pointer_not_value`)
* `convert_univ(u: &Univ)`: per-row input width drops from 5-variant
union to one G column. Caller `convert_univ_idxs` already produces the
`&Univ` via `list_lookup(univs, idx)` — no extra `store`.
* `convert_one(input: &ConvertInput)`: caller `convert_all` already has
`&input` from the `ListNode.Cons` destructure.
DEAD CODE DELETED
`address_in_list`, `apply_ctor_overrides`, `lookup_override`, `build_lit_blobs`,
`build_ref_idxs_mapped`, `is_blob`, `ctx_convert_expr` — all unused after
the migrations above.
TRIED + REVERTED (left as notes in code where instructive):
* Fused `build_addr_pos_map` + `augment_with_blob_refs` single-pass.
Lost to Aiur's per-row width tax — two narrower passes cheaper than
one merged wide pass.
* `convert_expr(ctx: &ConvertCtx)` ptr. Per-arm `load(ctx)` cost > raw-arg
threading.
* `compute_layout_walk` seen-mptrs/seen-poses → rbtree. Small regression
on typical shards (few enough Muts blocks that O(N) list scan is
already cheap; rbtree value column tax > savings).
* `block_members_map` to skip per-projection `load_verified_constant`.
Lost because Aiur memoises `load_verified_constant` automatically — the
new rbtree probe's width tax exceeded the savings from the eliminated
match cascade.
* `ctx` everywhere as `&ConvertCtx` (with `store(...)` at 5 construction
sites). The added `store` rows offset the per-row savings — wash.
`lake test -- --ignored ixvm`: 42 pin shifts, all measurable wins vs the
`ap/blob-ref-rbtree-augment` tip. Cumulative on heavy consts:
* `Nat.add_comm`: 54_369_745 → 54_049_773 FFT (-0.59%)
* `Nat.sub_le_of_le_add`: 515_331_420 → 510_843_459 FFT (-0.87%)
* `Array.append_assoc`: 2_567_087_893 → 2_537_360_311 FFT (-1.16%)
* `Vector.append`: 2_639_286_078 → 2_607_800_745 FFT (-1.19%)
* `String.Internal.append`: 718_803_075 → 708_296_270 FFT (-1.46%)
* `IxVMInd.Even.rec`: 31_659_493 → 31_434_525 FFT (-0.71%)
* `_private....extractMainModule._unsafe_rec`: 1_081_617_705 → 1_064_689_765 FFT (-1.57%)
Replaces channel 0's tagged-union (claim / tree / const bytes / empty-
blob-marker, distinguished only by key content-hash collision-
impossibility) with one channel per value kind. Channel reorganization
is cost-neutral — all six per-channel `io_get_info` + `#read_byte_stream`
pairs are inlined at the single consumer of each channel, so no fn-call
row gets added.
New layout, tiered by access pattern:
| Tier | Channel | Purpose | Key | Value |
|-------|---------|--------------------------|-------------------------|----------------------|
| Ctrl | 0 | claim wire bytes | `blake3(claim_bytes)` | claim bytes |
| Ctrl | 1 | assumption tree bytes | `tree.root` | tree bytes |
| Const | 2 | constant wire bytes | const addr | const bytes |
| Const | 3 | Defn reducibility hint | Defn addr | single G |
| Blob | 4 | blob discriminator | addr | one byte (1=const, 0=blob) |
| Blob | 5 | blob raw bytes | blob addr | raw bytes |
Tier 1 = one-per-`verify_claim`. Tier 2 = per-const. Tier 3 = per-blob.
`io_get_info(channel, key)` is now unambiguous by channel alone.
Aiur side (`Ingress.lean`, `Kernel/Claim.lean`):
* `load_verified_constant` inlines `io_get_info(2, raw)` +
`#read_byte_stream(2, ...)` (was ch 0).
* `load_verified_blob` inlines ch 5 read (was ch 1).
* `load_constant_hint` inlines ch 3 read (was ch 2).
* `load_with_deps` inlines a ch 4 discriminator probe replacing the
former `io_get_info(0, addr).len == 0 ⟹ blob` hack on ch 0. One
read_byte for the per-addr discriminator byte.
* `load_verified_claim` inlines ch 0 (was using a shared `load_payload_const`
helper on ch 0).
* `load_assumption_tree` inlines ch 1 (was using same `load_payload_const`
helper on ch 0).
* Helper wrappers (`load_payload_claim/tree/const/hint/blob`,
`load_discriminator`, `ch_*` constants) all removed — every channel
has exactly one consumer site and the inlined dispatch costs no fn
call.
* Top-of-file `IxVM IOBuffer interface` doc block documents the layout
+ soundness model.
Lean side (`ClaimHarness.lean`):
* `addEntries` writes per channel — consts → ch 2 + per-addr
discriminator `[1]` on ch 4; blobs → ch 5 + per-addr discriminator
`[0]` on ch 4; Defn hints → ch 3.
* `seedTreeAt` writes tree bytes to ch 1 (was ch 0).
* `buildClaimWitness` writes claim bytes to ch 0 (unchanged).
* Drops the empty-marker write on ch 0 for blob addrs — the ch 4
discriminator covers blob/const classification explicitly.
* Top-of-section `IxVM IOBuffer interface` doc block mirrors the
Aiur-side comment.
Soundness model unchanged. ch 0/1/2/5 byte streams are blake3-verified
by the kernel against their content-addressed keys. ch 3 hint is
semantically optional (controls WHNF reduction heuristic only; def-eq
is sound either way). ch 4 discriminator is sound by erasure-
correctness — a lying byte flips the const/blob decision and the
wrong-path load downstream fails (a "const" blob triggers a ch 2 read
that returns empty → blake3 verify against the non-empty addr fails;
a "blob" const dangles references → typecheck fail).
`lake test -- --ignored ixvm` green; 42 pins shifted within ±0.010%
(noise-tier) vs the pre-shuffle baseline — channel reorganization is
cost-neutral as required, the discriminator's one read_byte trades
against the eliminated `io_get_info(0, ...).len` check.
`blake3_flat` was a pure array-reshaping wrapper around `blake3` used
by `verify_bytes_against` and `bytes_to_addr`. Inline the reshape at
both call sites. `blake3`'s memoization key is the input ByteStream, so
the 74 cache hits that `blake3_flat` recorded migrate cleanly to
`blake3` — dedup preserved.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,898,064 → 12,430,154,083 (−743,981)
- Total width: 33,755 → 33,714 (−41)
- `blake3_flat` (41w × 1692h, 74 hits, 743,981 FFT) — removed
- `blake3` (92w × 1692h, 0 hits, ...) → now 74 hits
Same experiment for `bytes_to_addr` inline was neutral (−5k FFT, +92
width across 4 call sites) — kept as a wrapper.
@arthurpaulino
arthurpaulino marked this pull request as ready for review July 2, 2026 15:06
@arthurpaulino
arthurpaulino enabled auto-merge (squash) July 2, 2026 15:06
Mirror the Expr port pattern (`KExpr = &KExprNode`) for universe levels.
Kernel-side universe reps flip from unboxed enum values to pointer
aliases:
Before:
enum KLevel {
Zero,
Succ(&KLevel),
Max(&KLevel, &KLevel),
IMax(&KLevel, &KLevel),
Param(G)
}
After:
enum KLevelNode {
Zero,
Succ(KLevel),
Max(KLevel, KLevel),
IMax(KLevel, KLevel),
Param(G)
}
type KLevel = &KLevelNode
Callers switch from passing whole enum values to passing pointers.
Every match on a KLevel arg gains a `load()`; every value construction
that fills a KLevel arg or return slot gets a `store()`. `KExprNode.Srt`,
`KConstantInfo.{Ctor,Axiom,Defn,Thm,Opaque,Quot,Induct,Rec}` universe
fields all use the aliased `KLevel` now.
Motivation: pass-by-pointer shrinks per-row input width on every level-
manipulating function (`level_eq`, `level_leq`, `level_reduce`,
`level_inst_params`, ...), same trick as
`reference_aiur_pass_pointer_not_value`. Compared to the Expr port the
delta is smaller because level fns are already narrow, but it stacks.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,154,083 -> 12,418,344,860 (-11,809,223)
- Total width: 33,714 -> 33,532 (-182)
Re-pin every FFT cost shifted by the port.
@arthurpaulino
arthurpaulino merged commit 110f28f into mainJul 2, 2026
15 of 16 checks passed
@arthurpaulino
arthurpaulino deleted the ap/ingress-refactor branch July 2, 2026 19:04
johnchandlerburnham pushed a commit that referenced this pull request Jul 21, 2026
…505)
* IxVM: drop dead KValNode/KVal/KValEnv
From ap/kernel 828fb85 (Arthur Paulino): the NbE value domain is defined
but referenced nowhere — the live kernel runs on de-Bruijn KExpr;
vestigial from an abandoned NbE direction. (The closed-term context
normalization from that commit was measured separately and not taken:
the per-call expr_lbr probe cost +2.9% FFT on recursor loops for a 0.5%
record reduction.)
* IxVM: memoized prim_family dispatch + width-safe offset-stuck placement
Three coordinated changes to Const-head whnf dispatch (cherry-pick of
130f30b, adapted to post-#450/#457 main):
1. prim_family(addr) classifies a head address into the one reducer
family that could fire on it (nat/str/bitvec/native/decidable; the
sets are disjoint). Keyed on the ADDRESS ALONE it memoizes to one row
per distinct constant address per run, and whnf_const_head calls at
most one family reducer. The previous gauntlet ran every reducer in
sequence for guaranteed misses.
2. The symbolic-Nat offset-stuck check moves from a delta-arm probe into
try_nat_dispatch's miss path as a cold function
(try_nat_offset_dispatch, verdict 2 = "already stuck, do not
re-whnf"), with the offset construction shared via
mk_nat_offset_stuck (also used by the linear-rec collapse).
3. nat_lit_to_ctor_or_self exposes ONE constructor layer
(n -> succ(Lit(n-1))) instead of materializing the full succ chain.
Adaptations vs the original patch:
- whnf_nd_const_head (no-delta WHNF, added on main after the patch)
converted to the same family dispatch.
- Kept main's cold-extracted try_nat_binop_dispatch and routed the
symbolic-base case to try_nat_offset_dispatch from its miss arm.
Measured (lake exe ix check Nat.add_comm): total width 34820 -> 34806,
FFT cost 49571210 -> 48860647 (-1.43%). All 53 ixvm-suite FFT pins
decreased (-0.19%..-1.43%); parity and claim smokes pass. Pins updated;
crates/ixvm-codegen/src/aiur_ixvm.rs regenerated via `lake exe ix
codegen`.
* IxVM: port jcb/fixes H-14 — ptr_val skip map + lockstep addr cursor
Two quadratic/constant-factor fixes to check_all_skipping, ported from
jcb/fixes (3763356, John C. Burnham); cherry-pick of 2d83be1 adapted to
post-#457 main:
- The assumption-leaf skip set keys on ptr_val instead of the first 4
address bytes: one tree lookup per constant, no per-lookup address
load, no confirming address_eq. Sound by the build_addr_pos_map
interning invariant (one pointer, one content): a ptr hit implies the
address IS a leaf; a de-interned pointer reads as absent and the
constant just gets checked — fail-closed.
- The iterator walks the addrs list in LOCKSTEP with consts (cur_addrs
suffix) instead of list_lookup(addrs, pos) per constant, which
re-walked the prefix every iteration — a standalone O(closure^2).
Adaptations: kept main's two-arg check_canonical_block_sort call;
addr_key retained (the Inductive.lean block-membership table added on
main after this patch still uses it), comment updated.
Measured (lake exe ix check Nat.add_comm): total width 34806 -> 34781,
FFT cost unchanged (plain checks never take the skip path). Full ixvm
suite green incl. the frontier-assumption claim smoke; all FFT pins
unchanged. crates/ixvm-codegen/src/aiur_ixvm.rs regenerated.
---------
Co-authored-by: samuelburnham <45365069+samuelburnham@users.noreply.github.com>
Sign up for freeto 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

@arthurpaulino@gabriel-barrett
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface - #457

Merged
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor
Jul 2, 2026
Merged

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface#457
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor

Conversation

@arthurpaulino

Copy link
Copy Markdown
Member

Three commits on top of main that cut ingress FFT cost on heavy shards and land a documented IxVM IOBuffer interface.

Measurement methodology — read first

Shard 51 with unstubbed blake3 currently OOMs at all practical ulimit settings on the dev machine. To get past memory exhaustion and produce a comparative measurement, the kernel's blake3 verification paths in load_verified_constant / load_verified_blob / load_verified_claim were stubbed to no-ops for profiling. The stubs are not part of any committed code in this PR — they were applied locally during measurement and reverted.

What this means for the numbers below:

  • The delta between two stubbed runs is real for the non-blake3 portion of the trace. Both endpoints elide the same verification work, so the comparison isolates the effect of the kernel changes in this PR.
  • The absolute stubbed cost (e.g. "93.94G FFT") is not the production shard-51 typecheck cost. Production includes blake3 work (~16–20% of the trace per earlier profiles) that the stubs omit.

Every shard-51 number below is a "blake3-stubbed trace" measurement. Per-const numbers from lake test -- --ignored ixvm (no stubs) are unaffected and reflect real production cost.

Headline

MetricBeforeAfterΔ
Shard 51 (blake3-stubbed trace)119_152_044_06293_939_967_063−21.1% on the non-blake3 portion
Vector.append (production, unstubbed)2_661_244_8452_607_925_682−2.00%
Array.append_assoc (production)2_588_157_5382_537_478_644−1.96%
_private...extractMainModule._unsafe_rec (production)1_091_354_0311_064_762_809−2.44%

Per-const wins are end-to-end on the real (unstubbed) kernel; the shard-51 delta is real on the non-blake3 portion of the trace.

Commits

1. 120001d — IxVM kernel: augment addr_pos_map with blob-ref sentinels

lookup_addr_pos's rbtree only contained const addresses. Every blob ref (literal-blob payload pointers in Constant.refs) probed the map, missed, and fell through to the O(N) linear scan over all_addrs which also returned 0. Wasted work proportional to (blob refs × shard closure size).

This commit adds a third value class to the same rbtree: a sentinel 4294967295 (beyond any honest pos+1), inserted by walking each const's refs once via augment_with_blob_refs. lookup_addr_pos becomes a 3-way match: 0 → fall back to linear scan (now only fires under the de-intern soundness corner-case, ~never in practice); SENTINEL → return 0 directly (known blob); pos+1 → return pos.

Shard 51 (blake3-stubbed): 119_152_044_062 → 102_190_540_489 — −14.2% on the non-blake3 portion.

address_eq row count drops from ~10.3M to ~3.4M (−67%); lookup_addr_pos_linear falls out of the top-25 cost table. The augment-walk's added rbtree probes (~430M FFT in the stubbed trace) are paid back many times over by the ~17B FFT saved on lookups.

Soundness: every probe still relies on the positive direction ptr_val equality ⇒ content equality (Aiur Store content-addressing invariant). The linear fallback stays for the (still theoretical) malicious de-intern, where it uses content-based address_eq.

2. 9acbd58 — IxVM ingress: plumbing helpers + sidecar→rbtree migrations + ptr-passing

A 5-reviewer synthesis of ingress accidental complexity drove this consolidated refactor. Three families of change.

Plumbing helpers:

  • verify_bytes_against(bytes, expected) + bytes_to_addr(bytes) + blake3_flat(input) centralise the 24-line [h[i][j]] digest reshape that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
  • Channel I/O helpers factor the io_get_info + #read_byte_stream boilerplate at 6 sites. load_verified_blob no longer double-load(addr)s.
  • sentinel_blob_ref() named const replaces one bare 4294967295.

Sidecar lists → rbtree:

  • lookup_canon_addr: O(N) parallel-list walk → O(log N) rbtree probe via new build_canon_addr_map. canon_addrs: List<Addr> arg replaced by canon_addr_map: &RBTreeMap<Addr> (one threaded ptr).
  • lookup_block_start: O(N) parallel-list walk → O(log N) rbtree probe via new build_block_start_map. block_addrs / block_starts args replaced by block_start_map: &RBTreeMap<G> across ~12 fn signatures.
  • build_aux_recr_ctor_idxs returns (idxs, block_addr) so the standalone-Recr caller doesn't redo the rec_typ_to_inductive_addr + load_verified_constant chain to recover block_addr.
  • build_ref_idxs_and_blobs fuses the two separate walks (build_ref_idxs_mapped + build_lit_blobs) into one — single rbtree probe per ref produces both ref_idxs and lit_blobs. 5 caller sites simplified.

Pointer-passing (per Aiur's inputSize→width cost model):

  • convert_univ(u: &Univ): per-row input width drops from 5-variant union to one G column. Caller convert_univ_idxs already produces the &Univ via list_lookup(univs, idx) — no extra store.
  • convert_one(input: &ConvertInput): caller convert_all already has &input from the ListNode.Cons destructure.

Dead code deleted:address_in_list, apply_ctor_overrides, lookup_override, build_lit_blobs, build_ref_idxs_mapped, is_blob, ctx_convert_expr.

Shard 51 (blake3-stubbed): 102_190_540_489 → 93_939_967_063 — −8.07% on the non-blake3 portion on top of 120001d.

Per-const wins on heavy production targets (unstubbed lake test):

ConstantBeforeAfterΔ
Nat.add_comm54_369_74554_049_773−0.59%
Nat.sub_le_of_le_add515_331_420510_843_459−0.87%
Nat.decLe191_471_719189_723_325−0.91%
Array.append_assoc2_567_087_8932_537_360_311−1.16%
Vector.append2_639_286_0782_607_800_745−1.19%
IxVMInd.Even.rec31_659_49331_434_525−0.71%
IxVMInd.Odd.rec31_658_59831_433_622−0.71%
String.Internal.append718_803_075708_296_270−1.46%
_private...extractMainModule._unsafe_rec1_081_617_7051_064_689_765−1.57%

lake test -- --ignored ixvm green; 42 pins re-pinned.

3. 9c2eb23 — IxVM: per-kind IOBuffer channels with documented interface

The pre-existing IOBuffer multiplexed claim bytes, assumption tree bytes, constant wire bytes, and a per-blob-addr empty-marker on a single channel 0, distinguished only by blake3 content-hash collision-impossibility. A new contributor reading load_payload_const(key) couldn't tell from the helper whether they were getting a const, a claim, or a tree — only the caller knew the intent.

This commit splits channel 0 into per-kind channels and lands a documented IxVM IOBuffer interface block (mirrored on both Aiur and Lean sides).

TierChannelPurposeKeyValue
Ctrl0claim wire bytesblake3(claim_bytes)claim bytes
Ctrl1assumption tree bytestree.roottree bytes
Const2constant wire bytesconst addrconst bytes
Const3Defn reducibility hintDefn addrsingle G
Blob4blob discriminatoraddrone byte (1=const, 0=blob)
Blob5blob raw bytesblob addrraw bytes

Tier 1 fires once per verify_claim. Tier 2 fires per constant traversed during load_with_deps. Tier 3 fires per blob ref encountered during build_ref_idxs_and_blobs.

The blob discriminator (ch 4) replaces the previous io_get_info(0, addr).len == 0 ⟹ blob hack inside load_with_deps with an explicit per-addr one-byte payload. No more len-as-meaning overload.

Every per-channel io_get_info + #read_byte_stream pair is inlined at the single consumer of each channel — no fn-call row gets added for the reorganization. Channel reorganization is cost-neutral by design, confirmed by measurement: per-const pin shifts vs 9acbd58 land within ±0.010% (noise-tier; the ch-4 discriminator's one read_byte trades against the eliminated io_get_info(0, addr).len check).

The win here is review-time clarity, not FFT — io_get_info(channel, key) now has one value shape per channel and a doc block both kernel and harness reference.

Soundness: ch 0/1/2/5 byte streams are blake3-verified by the kernel against their content-addressed keys. ch 3 hint is semantically optional (controls WHNF reduction heuristic only; def-eq is sound either way). ch 4 discriminator is sound by erasure-correctness — a lying byte flips the const/blob decision and the wrong-path load downstream fails (a "const" blob triggers a ch 2 read that returns empty → blake3 verify against the non-empty addr fails; a "blob" const dangles references → typecheck fail).

Cumulative measurement (vs main)

ConstantBeforeAfterΔ
Nat.add_comm54_504_714 (approx.)54_047_004~−0.84%
Nat.decLe191_354_793189_722_204−0.85%
Nat.sub_le_of_le_add515_056_158510_870_092−0.81%
Array.append_assoc2_588_157_5382_537_478_644−1.96%
Vector.append2_661_244_8452_607_925_682−2.00%
String.Internal.append725_415_461708_332_683−2.36%
_private...extractMainModule._unsafe_rec1_091_354_0311_064_762_809−2.44%
Shard 51 (blake3-stubbed trace, non-blake3 portion)119_152_044_06293_939_967_063−21.1%

Test plan

  • lake build clean
  • lake test -- --ignored ixvm green at every commit
  • Shard 51 comparative measurement with blake3 stubs applied locally to both endpoints (stubs reverted; not part of any committed code in this PR)

`lookup_addr_pos`'s rbtree (`addr_pos_map`) only contained const
addresses. Every blob ref (literal-blob payload pointers in
`Constant.refs`) probed the map, missed, and fell through to the O(N)
linear scan over `all_addrs` which also returned 0. Wasted work
proportional to (blob refs × shard closure size).
Add a third value class to the same rbtree: the sentinel `4294967295`
(beyond any honest `pos+1`), inserted by walking each const's `refs`
once via `augment_with_blob_refs`. Any ref not already mapped to a
const-position gets the sentinel.
`lookup_addr_pos` becomes a 3-way match: `0` → fall back to linear
scan (now only fires under the de-intern soundness corner-case, ~never
in practice); `SENTINEL` → return 0 directly (known blob); `pos+1` →
return `pos`. `is_blob` mirrors the same match.
Soundness preserved: every probe still relies on the positive direction
`ptr_val` equality ⇒ content equality (Aiur Store's content-addressing
invariant). The linear fallback stays put for the (still theoretical)
malicious de-intern, where it uses content-based `address_eq`.
`lake exe ix check --ixe init.ixe --ixes init.ixes --shard 51`
(`ulimit -v 23000000`, with the kernel's blake3 paths in
`load_verified_constant` / `load_verified_blob` / `load_verified_claim`
stubbed for profiling):
119_152_044_062 → 102_190_540_489 FFT (-14.2%).
Tests/Ix/IxVM.lean: 42 pin shifts absorbing the per-ingress
augment-walk overhead.
A consolidated refactor of the ingress pipeline guided by a 5-reviewer
synthesis of accidental complexity sites. Three families of change:
PLUMBING
* `Blake3.lean`: `verify_bytes_against(bytes, expected)` + `bytes_to_addr(bytes)`
+ `blake3_flat(input)` centralise the 24-line `[h[i][j]]` digest reshape
that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
* `Ingress.lean`: `load_payload_const`/`blob`/`hint` + `ch_const`/`blob`/`hint`
factor the `io_get_info` + `#read_byte_stream` boilerplate at 6 sites.
`load_verified_blob` no longer double-`load(addr)`s.
* `sentinel_blob_ref()` named const replaces one bare `4294967295`.
SIDECAR LISTS → RBTREE
* `lookup_canon_addr`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_canon_addr_map`. `canon_addrs: List<Addr>` arg replaced
by `canon_addr_map: &RBTreeMap<Addr>` (one threaded ptr).
* `lookup_block_start`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_block_start_map`. `block_addrs` / `block_starts` args
replaced by `block_start_map: &RBTreeMap<G>` across ~12 fn signatures.
* `build_aux_recr_ctor_idxs` returns `(idxs, block_addr)` so the
standalone-Recr caller in `build_convert_inputs_walk` doesn't redo the
`rec_typ_to_inductive_addr` + `load_verified_constant` chain to recover
`block_addr` — `build_aux_recr_ctor_idxs` already computed it internally.
* `build_ref_idxs_and_blobs` fuses the two separate walks
(`build_ref_idxs_mapped` + `build_lit_blobs`) into one — single rbtree
probe per ref produces both `ref_idxs` and `lit_blobs`. 5 caller sites
simplified.
PTR-PASSING (per `reference_aiur_pass_pointer_not_value`)
* `convert_univ(u: &Univ)`: per-row input width drops from 5-variant
union to one G column. Caller `convert_univ_idxs` already produces the
`&Univ` via `list_lookup(univs, idx)` — no extra `store`.
* `convert_one(input: &ConvertInput)`: caller `convert_all` already has
`&input` from the `ListNode.Cons` destructure.
DEAD CODE DELETED
`address_in_list`, `apply_ctor_overrides`, `lookup_override`, `build_lit_blobs`,
`build_ref_idxs_mapped`, `is_blob`, `ctx_convert_expr` — all unused after
the migrations above.
TRIED + REVERTED (left as notes in code where instructive):
* Fused `build_addr_pos_map` + `augment_with_blob_refs` single-pass.
Lost to Aiur's per-row width tax — two narrower passes cheaper than
one merged wide pass.
* `convert_expr(ctx: &ConvertCtx)` ptr. Per-arm `load(ctx)` cost > raw-arg
threading.
* `compute_layout_walk` seen-mptrs/seen-poses → rbtree. Small regression
on typical shards (few enough Muts blocks that O(N) list scan is
already cheap; rbtree value column tax > savings).
* `block_members_map` to skip per-projection `load_verified_constant`.
Lost because Aiur memoises `load_verified_constant` automatically — the
new rbtree probe's width tax exceeded the savings from the eliminated
match cascade.
* `ctx` everywhere as `&ConvertCtx` (with `store(...)` at 5 construction
sites). The added `store` rows offset the per-row savings — wash.
`lake test -- --ignored ixvm`: 42 pin shifts, all measurable wins vs the
`ap/blob-ref-rbtree-augment` tip. Cumulative on heavy consts:
* `Nat.add_comm`: 54_369_745 → 54_049_773 FFT (-0.59%)
* `Nat.sub_le_of_le_add`: 515_331_420 → 510_843_459 FFT (-0.87%)
* `Array.append_assoc`: 2_567_087_893 → 2_537_360_311 FFT (-1.16%)
* `Vector.append`: 2_639_286_078 → 2_607_800_745 FFT (-1.19%)
* `String.Internal.append`: 718_803_075 → 708_296_270 FFT (-1.46%)
* `IxVMInd.Even.rec`: 31_659_493 → 31_434_525 FFT (-0.71%)
* `_private....extractMainModule._unsafe_rec`: 1_081_617_705 → 1_064_689_765 FFT (-1.57%)
Replaces channel 0's tagged-union (claim / tree / const bytes / empty-
blob-marker, distinguished only by key content-hash collision-
impossibility) with one channel per value kind. Channel reorganization
is cost-neutral — all six per-channel `io_get_info` + `#read_byte_stream`
pairs are inlined at the single consumer of each channel, so no fn-call
row gets added.
New layout, tiered by access pattern:
| Tier | Channel | Purpose | Key | Value |
|-------|---------|--------------------------|-------------------------|----------------------|
| Ctrl | 0 | claim wire bytes | `blake3(claim_bytes)` | claim bytes |
| Ctrl | 1 | assumption tree bytes | `tree.root` | tree bytes |
| Const | 2 | constant wire bytes | const addr | const bytes |
| Const | 3 | Defn reducibility hint | Defn addr | single G |
| Blob | 4 | blob discriminator | addr | one byte (1=const, 0=blob) |
| Blob | 5 | blob raw bytes | blob addr | raw bytes |
Tier 1 = one-per-`verify_claim`. Tier 2 = per-const. Tier 3 = per-blob.
`io_get_info(channel, key)` is now unambiguous by channel alone.
Aiur side (`Ingress.lean`, `Kernel/Claim.lean`):
* `load_verified_constant` inlines `io_get_info(2, raw)` +
`#read_byte_stream(2, ...)` (was ch 0).
* `load_verified_blob` inlines ch 5 read (was ch 1).
* `load_constant_hint` inlines ch 3 read (was ch 2).
* `load_with_deps` inlines a ch 4 discriminator probe replacing the
former `io_get_info(0, addr).len == 0 ⟹ blob` hack on ch 0. One
read_byte for the per-addr discriminator byte.
* `load_verified_claim` inlines ch 0 (was using a shared `load_payload_const`
helper on ch 0).
* `load_assumption_tree` inlines ch 1 (was using same `load_payload_const`
helper on ch 0).
* Helper wrappers (`load_payload_claim/tree/const/hint/blob`,
`load_discriminator`, `ch_*` constants) all removed — every channel
has exactly one consumer site and the inlined dispatch costs no fn
call.
* Top-of-file `IxVM IOBuffer interface` doc block documents the layout
+ soundness model.
Lean side (`ClaimHarness.lean`):
* `addEntries` writes per channel — consts → ch 2 + per-addr
discriminator `[1]` on ch 4; blobs → ch 5 + per-addr discriminator
`[0]` on ch 4; Defn hints → ch 3.
* `seedTreeAt` writes tree bytes to ch 1 (was ch 0).
* `buildClaimWitness` writes claim bytes to ch 0 (unchanged).
* Drops the empty-marker write on ch 0 for blob addrs — the ch 4
discriminator covers blob/const classification explicitly.
* Top-of-section `IxVM IOBuffer interface` doc block mirrors the
Aiur-side comment.
Soundness model unchanged. ch 0/1/2/5 byte streams are blake3-verified
by the kernel against their content-addressed keys. ch 3 hint is
semantically optional (controls WHNF reduction heuristic only; def-eq
is sound either way). ch 4 discriminator is sound by erasure-
correctness — a lying byte flips the const/blob decision and the
wrong-path load downstream fails (a "const" blob triggers a ch 2 read
that returns empty → blake3 verify against the non-empty addr fails;
a "blob" const dangles references → typecheck fail).
`lake test -- --ignored ixvm` green; 42 pins shifted within ±0.010%
(noise-tier) vs the pre-shuffle baseline — channel reorganization is
cost-neutral as required, the discriminator's one read_byte trades
against the eliminated `io_get_info(0, ...).len` check.
`blake3_flat` was a pure array-reshaping wrapper around `blake3` used
by `verify_bytes_against` and `bytes_to_addr`. Inline the reshape at
both call sites. `blake3`'s memoization key is the input ByteStream, so
the 74 cache hits that `blake3_flat` recorded migrate cleanly to
`blake3` — dedup preserved.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,898,064 → 12,430,154,083 (−743,981)
- Total width: 33,755 → 33,714 (−41)
- `blake3_flat` (41w × 1692h, 74 hits, 743,981 FFT) — removed
- `blake3` (92w × 1692h, 0 hits, ...) → now 74 hits
Same experiment for `bytes_to_addr` inline was neutral (−5k FFT, +92
width across 4 call sites) — kept as a wrapper.
@arthurpaulino
arthurpaulino marked this pull request as ready for review July 2, 2026 15:06
@arthurpaulino
arthurpaulino enabled auto-merge (squash) July 2, 2026 15:06
Mirror the Expr port pattern (`KExpr = &KExprNode`) for universe levels.
Kernel-side universe reps flip from unboxed enum values to pointer
aliases:
Before:
enum KLevel {
Zero,
Succ(&KLevel),
Max(&KLevel, &KLevel),
IMax(&KLevel, &KLevel),
Param(G)
}
After:
enum KLevelNode {
Zero,
Succ(KLevel),
Max(KLevel, KLevel),
IMax(KLevel, KLevel),
Param(G)
}
type KLevel = &KLevelNode
Callers switch from passing whole enum values to passing pointers.
Every match on a KLevel arg gains a `load()`; every value construction
that fills a KLevel arg or return slot gets a `store()`. `KExprNode.Srt`,
`KConstantInfo.{Ctor,Axiom,Defn,Thm,Opaque,Quot,Induct,Rec}` universe
fields all use the aliased `KLevel` now.
Motivation: pass-by-pointer shrinks per-row input width on every level-
manipulating function (`level_eq`, `level_leq`, `level_reduce`,
`level_inst_params`, ...), same trick as
`reference_aiur_pass_pointer_not_value`. Compared to the Expr port the
delta is smaller because level fns are already narrow, but it stacks.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,154,083 -> 12,418,344,860 (-11,809,223)
- Total width: 33,714 -> 33,532 (-182)
Re-pin every FFT cost shifted by the port.
@arthurpaulino
arthurpaulino merged commit 110f28f into mainJul 2, 2026
15 of 16 checks passed
@arthurpaulino
arthurpaulino deleted the ap/ingress-refactor branch July 2, 2026 19:04
johnchandlerburnham pushed a commit that referenced this pull request Jul 21, 2026
…505)
* IxVM: drop dead KValNode/KVal/KValEnv
From ap/kernel 828fb85 (Arthur Paulino): the NbE value domain is defined
but referenced nowhere — the live kernel runs on de-Bruijn KExpr;
vestigial from an abandoned NbE direction. (The closed-term context
normalization from that commit was measured separately and not taken:
the per-call expr_lbr probe cost +2.9% FFT on recursor loops for a 0.5%
record reduction.)
* IxVM: memoized prim_family dispatch + width-safe offset-stuck placement
Three coordinated changes to Const-head whnf dispatch (cherry-pick of
130f30b, adapted to post-#450/#457 main):
1. prim_family(addr) classifies a head address into the one reducer
family that could fire on it (nat/str/bitvec/native/decidable; the
sets are disjoint). Keyed on the ADDRESS ALONE it memoizes to one row
per distinct constant address per run, and whnf_const_head calls at
most one family reducer. The previous gauntlet ran every reducer in
sequence for guaranteed misses.
2. The symbolic-Nat offset-stuck check moves from a delta-arm probe into
try_nat_dispatch's miss path as a cold function
(try_nat_offset_dispatch, verdict 2 = "already stuck, do not
re-whnf"), with the offset construction shared via
mk_nat_offset_stuck (also used by the linear-rec collapse).
3. nat_lit_to_ctor_or_self exposes ONE constructor layer
(n -> succ(Lit(n-1))) instead of materializing the full succ chain.
Adaptations vs the original patch:
- whnf_nd_const_head (no-delta WHNF, added on main after the patch)
converted to the same family dispatch.
- Kept main's cold-extracted try_nat_binop_dispatch and routed the
symbolic-base case to try_nat_offset_dispatch from its miss arm.
Measured (lake exe ix check Nat.add_comm): total width 34820 -> 34806,
FFT cost 49571210 -> 48860647 (-1.43%). All 53 ixvm-suite FFT pins
decreased (-0.19%..-1.43%); parity and claim smokes pass. Pins updated;
crates/ixvm-codegen/src/aiur_ixvm.rs regenerated via `lake exe ix
codegen`.
* IxVM: port jcb/fixes H-14 — ptr_val skip map + lockstep addr cursor
Two quadratic/constant-factor fixes to check_all_skipping, ported from
jcb/fixes (3763356, John C. Burnham); cherry-pick of 2d83be1 adapted to
post-#457 main:
- The assumption-leaf skip set keys on ptr_val instead of the first 4
address bytes: one tree lookup per constant, no per-lookup address
load, no confirming address_eq. Sound by the build_addr_pos_map
interning invariant (one pointer, one content): a ptr hit implies the
address IS a leaf; a de-interned pointer reads as absent and the
constant just gets checked — fail-closed.
- The iterator walks the addrs list in LOCKSTEP with consts (cur_addrs
suffix) instead of list_lookup(addrs, pos) per constant, which
re-walked the prefix every iteration — a standalone O(closure^2).
Adaptations: kept main's two-arg check_canonical_block_sort call;
addr_key retained (the Inductive.lean block-membership table added on
main after this patch still uses it), comment updated.
Measured (lake exe ix check Nat.add_comm): total width 34806 -> 34781,
FFT cost unchanged (plain checks never take the skip path). Full ixvm
suite green incl. the frontier-assumption claim smoke; all FFT pins
unchanged. crates/ixvm-codegen/src/aiur_ixvm.rs regenerated.
---------
Co-authored-by: samuelburnham <45365069+samuelburnham@users.noreply.github.com>
Sign up for freeto 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

@arthurpaulino@gabriel-barrett
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface - #457

Merged
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor
Jul 2, 2026
Merged

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface#457
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor

Conversation

@arthurpaulino

Copy link
Copy Markdown
Member

Three commits on top of main that cut ingress FFT cost on heavy shards and land a documented IxVM IOBuffer interface.

Measurement methodology — read first

Shard 51 with unstubbed blake3 currently OOMs at all practical ulimit settings on the dev machine. To get past memory exhaustion and produce a comparative measurement, the kernel's blake3 verification paths in load_verified_constant / load_verified_blob / load_verified_claim were stubbed to no-ops for profiling. The stubs are not part of any committed code in this PR — they were applied locally during measurement and reverted.

What this means for the numbers below:

  • The delta between two stubbed runs is real for the non-blake3 portion of the trace. Both endpoints elide the same verification work, so the comparison isolates the effect of the kernel changes in this PR.
  • The absolute stubbed cost (e.g. "93.94G FFT") is not the production shard-51 typecheck cost. Production includes blake3 work (~16–20% of the trace per earlier profiles) that the stubs omit.

Every shard-51 number below is a "blake3-stubbed trace" measurement. Per-const numbers from lake test -- --ignored ixvm (no stubs) are unaffected and reflect real production cost.

Headline

MetricBeforeAfterΔ
Shard 51 (blake3-stubbed trace)119_152_044_06293_939_967_063−21.1% on the non-blake3 portion
Vector.append (production, unstubbed)2_661_244_8452_607_925_682−2.00%
Array.append_assoc (production)2_588_157_5382_537_478_644−1.96%
_private...extractMainModule._unsafe_rec (production)1_091_354_0311_064_762_809−2.44%

Per-const wins are end-to-end on the real (unstubbed) kernel; the shard-51 delta is real on the non-blake3 portion of the trace.

Commits

1. 120001d — IxVM kernel: augment addr_pos_map with blob-ref sentinels

lookup_addr_pos's rbtree only contained const addresses. Every blob ref (literal-blob payload pointers in Constant.refs) probed the map, missed, and fell through to the O(N) linear scan over all_addrs which also returned 0. Wasted work proportional to (blob refs × shard closure size).

This commit adds a third value class to the same rbtree: a sentinel 4294967295 (beyond any honest pos+1), inserted by walking each const's refs once via augment_with_blob_refs. lookup_addr_pos becomes a 3-way match: 0 → fall back to linear scan (now only fires under the de-intern soundness corner-case, ~never in practice); SENTINEL → return 0 directly (known blob); pos+1 → return pos.

Shard 51 (blake3-stubbed): 119_152_044_062 → 102_190_540_489 — −14.2% on the non-blake3 portion.

address_eq row count drops from ~10.3M to ~3.4M (−67%); lookup_addr_pos_linear falls out of the top-25 cost table. The augment-walk's added rbtree probes (~430M FFT in the stubbed trace) are paid back many times over by the ~17B FFT saved on lookups.

Soundness: every probe still relies on the positive direction ptr_val equality ⇒ content equality (Aiur Store content-addressing invariant). The linear fallback stays for the (still theoretical) malicious de-intern, where it uses content-based address_eq.

2. 9acbd58 — IxVM ingress: plumbing helpers + sidecar→rbtree migrations + ptr-passing

A 5-reviewer synthesis of ingress accidental complexity drove this consolidated refactor. Three families of change.

Plumbing helpers:

  • verify_bytes_against(bytes, expected) + bytes_to_addr(bytes) + blake3_flat(input) centralise the 24-line [h[i][j]] digest reshape that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
  • Channel I/O helpers factor the io_get_info + #read_byte_stream boilerplate at 6 sites. load_verified_blob no longer double-load(addr)s.
  • sentinel_blob_ref() named const replaces one bare 4294967295.

Sidecar lists → rbtree:

  • lookup_canon_addr: O(N) parallel-list walk → O(log N) rbtree probe via new build_canon_addr_map. canon_addrs: List<Addr> arg replaced by canon_addr_map: &RBTreeMap<Addr> (one threaded ptr).
  • lookup_block_start: O(N) parallel-list walk → O(log N) rbtree probe via new build_block_start_map. block_addrs / block_starts args replaced by block_start_map: &RBTreeMap<G> across ~12 fn signatures.
  • build_aux_recr_ctor_idxs returns (idxs, block_addr) so the standalone-Recr caller doesn't redo the rec_typ_to_inductive_addr + load_verified_constant chain to recover block_addr.
  • build_ref_idxs_and_blobs fuses the two separate walks (build_ref_idxs_mapped + build_lit_blobs) into one — single rbtree probe per ref produces both ref_idxs and lit_blobs. 5 caller sites simplified.

Pointer-passing (per Aiur's inputSize→width cost model):

  • convert_univ(u: &Univ): per-row input width drops from 5-variant union to one G column. Caller convert_univ_idxs already produces the &Univ via list_lookup(univs, idx) — no extra store.
  • convert_one(input: &ConvertInput): caller convert_all already has &input from the ListNode.Cons destructure.

Dead code deleted:address_in_list, apply_ctor_overrides, lookup_override, build_lit_blobs, build_ref_idxs_mapped, is_blob, ctx_convert_expr.

Shard 51 (blake3-stubbed): 102_190_540_489 → 93_939_967_063 — −8.07% on the non-blake3 portion on top of 120001d.

Per-const wins on heavy production targets (unstubbed lake test):

ConstantBeforeAfterΔ
Nat.add_comm54_369_74554_049_773−0.59%
Nat.sub_le_of_le_add515_331_420510_843_459−0.87%
Nat.decLe191_471_719189_723_325−0.91%
Array.append_assoc2_567_087_8932_537_360_311−1.16%
Vector.append2_639_286_0782_607_800_745−1.19%
IxVMInd.Even.rec31_659_49331_434_525−0.71%
IxVMInd.Odd.rec31_658_59831_433_622−0.71%
String.Internal.append718_803_075708_296_270−1.46%
_private...extractMainModule._unsafe_rec1_081_617_7051_064_689_765−1.57%

lake test -- --ignored ixvm green; 42 pins re-pinned.

3. 9c2eb23 — IxVM: per-kind IOBuffer channels with documented interface

The pre-existing IOBuffer multiplexed claim bytes, assumption tree bytes, constant wire bytes, and a per-blob-addr empty-marker on a single channel 0, distinguished only by blake3 content-hash collision-impossibility. A new contributor reading load_payload_const(key) couldn't tell from the helper whether they were getting a const, a claim, or a tree — only the caller knew the intent.

This commit splits channel 0 into per-kind channels and lands a documented IxVM IOBuffer interface block (mirrored on both Aiur and Lean sides).

TierChannelPurposeKeyValue
Ctrl0claim wire bytesblake3(claim_bytes)claim bytes
Ctrl1assumption tree bytestree.roottree bytes
Const2constant wire bytesconst addrconst bytes
Const3Defn reducibility hintDefn addrsingle G
Blob4blob discriminatoraddrone byte (1=const, 0=blob)
Blob5blob raw bytesblob addrraw bytes

Tier 1 fires once per verify_claim. Tier 2 fires per constant traversed during load_with_deps. Tier 3 fires per blob ref encountered during build_ref_idxs_and_blobs.

The blob discriminator (ch 4) replaces the previous io_get_info(0, addr).len == 0 ⟹ blob hack inside load_with_deps with an explicit per-addr one-byte payload. No more len-as-meaning overload.

Every per-channel io_get_info + #read_byte_stream pair is inlined at the single consumer of each channel — no fn-call row gets added for the reorganization. Channel reorganization is cost-neutral by design, confirmed by measurement: per-const pin shifts vs 9acbd58 land within ±0.010% (noise-tier; the ch-4 discriminator's one read_byte trades against the eliminated io_get_info(0, addr).len check).

The win here is review-time clarity, not FFT — io_get_info(channel, key) now has one value shape per channel and a doc block both kernel and harness reference.

Soundness: ch 0/1/2/5 byte streams are blake3-verified by the kernel against their content-addressed keys. ch 3 hint is semantically optional (controls WHNF reduction heuristic only; def-eq is sound either way). ch 4 discriminator is sound by erasure-correctness — a lying byte flips the const/blob decision and the wrong-path load downstream fails (a "const" blob triggers a ch 2 read that returns empty → blake3 verify against the non-empty addr fails; a "blob" const dangles references → typecheck fail).

Cumulative measurement (vs main)

ConstantBeforeAfterΔ
Nat.add_comm54_504_714 (approx.)54_047_004~−0.84%
Nat.decLe191_354_793189_722_204−0.85%
Nat.sub_le_of_le_add515_056_158510_870_092−0.81%
Array.append_assoc2_588_157_5382_537_478_644−1.96%
Vector.append2_661_244_8452_607_925_682−2.00%
String.Internal.append725_415_461708_332_683−2.36%
_private...extractMainModule._unsafe_rec1_091_354_0311_064_762_809−2.44%
Shard 51 (blake3-stubbed trace, non-blake3 portion)119_152_044_06293_939_967_063−21.1%

Test plan

  • lake build clean
  • lake test -- --ignored ixvm green at every commit
  • Shard 51 comparative measurement with blake3 stubs applied locally to both endpoints (stubs reverted; not part of any committed code in this PR)

`lookup_addr_pos`'s rbtree (`addr_pos_map`) only contained const
addresses. Every blob ref (literal-blob payload pointers in
`Constant.refs`) probed the map, missed, and fell through to the O(N)
linear scan over `all_addrs` which also returned 0. Wasted work
proportional to (blob refs × shard closure size).
Add a third value class to the same rbtree: the sentinel `4294967295`
(beyond any honest `pos+1`), inserted by walking each const's `refs`
once via `augment_with_blob_refs`. Any ref not already mapped to a
const-position gets the sentinel.
`lookup_addr_pos` becomes a 3-way match: `0` → fall back to linear
scan (now only fires under the de-intern soundness corner-case, ~never
in practice); `SENTINEL` → return 0 directly (known blob); `pos+1` →
return `pos`. `is_blob` mirrors the same match.
Soundness preserved: every probe still relies on the positive direction
`ptr_val` equality ⇒ content equality (Aiur Store's content-addressing
invariant). The linear fallback stays put for the (still theoretical)
malicious de-intern, where it uses content-based `address_eq`.
`lake exe ix check --ixe init.ixe --ixes init.ixes --shard 51`
(`ulimit -v 23000000`, with the kernel's blake3 paths in
`load_verified_constant` / `load_verified_blob` / `load_verified_claim`
stubbed for profiling):
119_152_044_062 → 102_190_540_489 FFT (-14.2%).
Tests/Ix/IxVM.lean: 42 pin shifts absorbing the per-ingress
augment-walk overhead.
A consolidated refactor of the ingress pipeline guided by a 5-reviewer
synthesis of accidental complexity sites. Three families of change:
PLUMBING
* `Blake3.lean`: `verify_bytes_against(bytes, expected)` + `bytes_to_addr(bytes)`
+ `blake3_flat(input)` centralise the 24-line `[h[i][j]]` digest reshape
that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
* `Ingress.lean`: `load_payload_const`/`blob`/`hint` + `ch_const`/`blob`/`hint`
factor the `io_get_info` + `#read_byte_stream` boilerplate at 6 sites.
`load_verified_blob` no longer double-`load(addr)`s.
* `sentinel_blob_ref()` named const replaces one bare `4294967295`.
SIDECAR LISTS → RBTREE
* `lookup_canon_addr`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_canon_addr_map`. `canon_addrs: List<Addr>` arg replaced
by `canon_addr_map: &RBTreeMap<Addr>` (one threaded ptr).
* `lookup_block_start`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_block_start_map`. `block_addrs` / `block_starts` args
replaced by `block_start_map: &RBTreeMap<G>` across ~12 fn signatures.
* `build_aux_recr_ctor_idxs` returns `(idxs, block_addr)` so the
standalone-Recr caller in `build_convert_inputs_walk` doesn't redo the
`rec_typ_to_inductive_addr` + `load_verified_constant` chain to recover
`block_addr` — `build_aux_recr_ctor_idxs` already computed it internally.
* `build_ref_idxs_and_blobs` fuses the two separate walks
(`build_ref_idxs_mapped` + `build_lit_blobs`) into one — single rbtree
probe per ref produces both `ref_idxs` and `lit_blobs`. 5 caller sites
simplified.
PTR-PASSING (per `reference_aiur_pass_pointer_not_value`)
* `convert_univ(u: &Univ)`: per-row input width drops from 5-variant
union to one G column. Caller `convert_univ_idxs` already produces the
`&Univ` via `list_lookup(univs, idx)` — no extra `store`.
* `convert_one(input: &ConvertInput)`: caller `convert_all` already has
`&input` from the `ListNode.Cons` destructure.
DEAD CODE DELETED
`address_in_list`, `apply_ctor_overrides`, `lookup_override`, `build_lit_blobs`,
`build_ref_idxs_mapped`, `is_blob`, `ctx_convert_expr` — all unused after
the migrations above.
TRIED + REVERTED (left as notes in code where instructive):
* Fused `build_addr_pos_map` + `augment_with_blob_refs` single-pass.
Lost to Aiur's per-row width tax — two narrower passes cheaper than
one merged wide pass.
* `convert_expr(ctx: &ConvertCtx)` ptr. Per-arm `load(ctx)` cost > raw-arg
threading.
* `compute_layout_walk` seen-mptrs/seen-poses → rbtree. Small regression
on typical shards (few enough Muts blocks that O(N) list scan is
already cheap; rbtree value column tax > savings).
* `block_members_map` to skip per-projection `load_verified_constant`.
Lost because Aiur memoises `load_verified_constant` automatically — the
new rbtree probe's width tax exceeded the savings from the eliminated
match cascade.
* `ctx` everywhere as `&ConvertCtx` (with `store(...)` at 5 construction
sites). The added `store` rows offset the per-row savings — wash.
`lake test -- --ignored ixvm`: 42 pin shifts, all measurable wins vs the
`ap/blob-ref-rbtree-augment` tip. Cumulative on heavy consts:
* `Nat.add_comm`: 54_369_745 → 54_049_773 FFT (-0.59%)
* `Nat.sub_le_of_le_add`: 515_331_420 → 510_843_459 FFT (-0.87%)
* `Array.append_assoc`: 2_567_087_893 → 2_537_360_311 FFT (-1.16%)
* `Vector.append`: 2_639_286_078 → 2_607_800_745 FFT (-1.19%)
* `String.Internal.append`: 718_803_075 → 708_296_270 FFT (-1.46%)
* `IxVMInd.Even.rec`: 31_659_493 → 31_434_525 FFT (-0.71%)
* `_private....extractMainModule._unsafe_rec`: 1_081_617_705 → 1_064_689_765 FFT (-1.57%)
Replaces channel 0's tagged-union (claim / tree / const bytes / empty-
blob-marker, distinguished only by key content-hash collision-
impossibility) with one channel per value kind. Channel reorganization
is cost-neutral — all six per-channel `io_get_info` + `#read_byte_stream`
pairs are inlined at the single consumer of each channel, so no fn-call
row gets added.
New layout, tiered by access pattern:
| Tier | Channel | Purpose | Key | Value |
|-------|---------|--------------------------|-------------------------|----------------------|
| Ctrl | 0 | claim wire bytes | `blake3(claim_bytes)` | claim bytes |
| Ctrl | 1 | assumption tree bytes | `tree.root` | tree bytes |
| Const | 2 | constant wire bytes | const addr | const bytes |
| Const | 3 | Defn reducibility hint | Defn addr | single G |
| Blob | 4 | blob discriminator | addr | one byte (1=const, 0=blob) |
| Blob | 5 | blob raw bytes | blob addr | raw bytes |
Tier 1 = one-per-`verify_claim`. Tier 2 = per-const. Tier 3 = per-blob.
`io_get_info(channel, key)` is now unambiguous by channel alone.
Aiur side (`Ingress.lean`, `Kernel/Claim.lean`):
* `load_verified_constant` inlines `io_get_info(2, raw)` +
`#read_byte_stream(2, ...)` (was ch 0).
* `load_verified_blob` inlines ch 5 read (was ch 1).
* `load_constant_hint` inlines ch 3 read (was ch 2).
* `load_with_deps` inlines a ch 4 discriminator probe replacing the
former `io_get_info(0, addr).len == 0 ⟹ blob` hack on ch 0. One
read_byte for the per-addr discriminator byte.
* `load_verified_claim` inlines ch 0 (was using a shared `load_payload_const`
helper on ch 0).
* `load_assumption_tree` inlines ch 1 (was using same `load_payload_const`
helper on ch 0).
* Helper wrappers (`load_payload_claim/tree/const/hint/blob`,
`load_discriminator`, `ch_*` constants) all removed — every channel
has exactly one consumer site and the inlined dispatch costs no fn
call.
* Top-of-file `IxVM IOBuffer interface` doc block documents the layout
+ soundness model.
Lean side (`ClaimHarness.lean`):
* `addEntries` writes per channel — consts → ch 2 + per-addr
discriminator `[1]` on ch 4; blobs → ch 5 + per-addr discriminator
`[0]` on ch 4; Defn hints → ch 3.
* `seedTreeAt` writes tree bytes to ch 1 (was ch 0).
* `buildClaimWitness` writes claim bytes to ch 0 (unchanged).
* Drops the empty-marker write on ch 0 for blob addrs — the ch 4
discriminator covers blob/const classification explicitly.
* Top-of-section `IxVM IOBuffer interface` doc block mirrors the
Aiur-side comment.
Soundness model unchanged. ch 0/1/2/5 byte streams are blake3-verified
by the kernel against their content-addressed keys. ch 3 hint is
semantically optional (controls WHNF reduction heuristic only; def-eq
is sound either way). ch 4 discriminator is sound by erasure-
correctness — a lying byte flips the const/blob decision and the
wrong-path load downstream fails (a "const" blob triggers a ch 2 read
that returns empty → blake3 verify against the non-empty addr fails;
a "blob" const dangles references → typecheck fail).
`lake test -- --ignored ixvm` green; 42 pins shifted within ±0.010%
(noise-tier) vs the pre-shuffle baseline — channel reorganization is
cost-neutral as required, the discriminator's one read_byte trades
against the eliminated `io_get_info(0, ...).len` check.
`blake3_flat` was a pure array-reshaping wrapper around `blake3` used
by `verify_bytes_against` and `bytes_to_addr`. Inline the reshape at
both call sites. `blake3`'s memoization key is the input ByteStream, so
the 74 cache hits that `blake3_flat` recorded migrate cleanly to
`blake3` — dedup preserved.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,898,064 → 12,430,154,083 (−743,981)
- Total width: 33,755 → 33,714 (−41)
- `blake3_flat` (41w × 1692h, 74 hits, 743,981 FFT) — removed
- `blake3` (92w × 1692h, 0 hits, ...) → now 74 hits
Same experiment for `bytes_to_addr` inline was neutral (−5k FFT, +92
width across 4 call sites) — kept as a wrapper.
@arthurpaulino
arthurpaulino marked this pull request as ready for review July 2, 2026 15:06
@arthurpaulino
arthurpaulino enabled auto-merge (squash) July 2, 2026 15:06
Mirror the Expr port pattern (`KExpr = &KExprNode`) for universe levels.
Kernel-side universe reps flip from unboxed enum values to pointer
aliases:
Before:
enum KLevel {
Zero,
Succ(&KLevel),
Max(&KLevel, &KLevel),
IMax(&KLevel, &KLevel),
Param(G)
}
After:
enum KLevelNode {
Zero,
Succ(KLevel),
Max(KLevel, KLevel),
IMax(KLevel, KLevel),
Param(G)
}
type KLevel = &KLevelNode
Callers switch from passing whole enum values to passing pointers.
Every match on a KLevel arg gains a `load()`; every value construction
that fills a KLevel arg or return slot gets a `store()`. `KExprNode.Srt`,
`KConstantInfo.{Ctor,Axiom,Defn,Thm,Opaque,Quot,Induct,Rec}` universe
fields all use the aliased `KLevel` now.
Motivation: pass-by-pointer shrinks per-row input width on every level-
manipulating function (`level_eq`, `level_leq`, `level_reduce`,
`level_inst_params`, ...), same trick as
`reference_aiur_pass_pointer_not_value`. Compared to the Expr port the
delta is smaller because level fns are already narrow, but it stacks.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,154,083 -> 12,418,344,860 (-11,809,223)
- Total width: 33,714 -> 33,532 (-182)
Re-pin every FFT cost shifted by the port.
@arthurpaulino
arthurpaulino merged commit 110f28f into mainJul 2, 2026
15 of 16 checks passed
@arthurpaulino
arthurpaulino deleted the ap/ingress-refactor branch July 2, 2026 19:04
johnchandlerburnham pushed a commit that referenced this pull request Jul 21, 2026
…505)
* IxVM: drop dead KValNode/KVal/KValEnv
From ap/kernel 828fb85 (Arthur Paulino): the NbE value domain is defined
but referenced nowhere — the live kernel runs on de-Bruijn KExpr;
vestigial from an abandoned NbE direction. (The closed-term context
normalization from that commit was measured separately and not taken:
the per-call expr_lbr probe cost +2.9% FFT on recursor loops for a 0.5%
record reduction.)
* IxVM: memoized prim_family dispatch + width-safe offset-stuck placement
Three coordinated changes to Const-head whnf dispatch (cherry-pick of
130f30b, adapted to post-#450/#457 main):
1. prim_family(addr) classifies a head address into the one reducer
family that could fire on it (nat/str/bitvec/native/decidable; the
sets are disjoint). Keyed on the ADDRESS ALONE it memoizes to one row
per distinct constant address per run, and whnf_const_head calls at
most one family reducer. The previous gauntlet ran every reducer in
sequence for guaranteed misses.
2. The symbolic-Nat offset-stuck check moves from a delta-arm probe into
try_nat_dispatch's miss path as a cold function
(try_nat_offset_dispatch, verdict 2 = "already stuck, do not
re-whnf"), with the offset construction shared via
mk_nat_offset_stuck (also used by the linear-rec collapse).
3. nat_lit_to_ctor_or_self exposes ONE constructor layer
(n -> succ(Lit(n-1))) instead of materializing the full succ chain.
Adaptations vs the original patch:
- whnf_nd_const_head (no-delta WHNF, added on main after the patch)
converted to the same family dispatch.
- Kept main's cold-extracted try_nat_binop_dispatch and routed the
symbolic-base case to try_nat_offset_dispatch from its miss arm.
Measured (lake exe ix check Nat.add_comm): total width 34820 -> 34806,
FFT cost 49571210 -> 48860647 (-1.43%). All 53 ixvm-suite FFT pins
decreased (-0.19%..-1.43%); parity and claim smokes pass. Pins updated;
crates/ixvm-codegen/src/aiur_ixvm.rs regenerated via `lake exe ix
codegen`.
* IxVM: port jcb/fixes H-14 — ptr_val skip map + lockstep addr cursor
Two quadratic/constant-factor fixes to check_all_skipping, ported from
jcb/fixes (3763356, John C. Burnham); cherry-pick of 2d83be1 adapted to
post-#457 main:
- The assumption-leaf skip set keys on ptr_val instead of the first 4
address bytes: one tree lookup per constant, no per-lookup address
load, no confirming address_eq. Sound by the build_addr_pos_map
interning invariant (one pointer, one content): a ptr hit implies the
address IS a leaf; a de-interned pointer reads as absent and the
constant just gets checked — fail-closed.
- The iterator walks the addrs list in LOCKSTEP with consts (cur_addrs
suffix) instead of list_lookup(addrs, pos) per constant, which
re-walked the prefix every iteration — a standalone O(closure^2).
Adaptations: kept main's two-arg check_canonical_block_sort call;
addr_key retained (the Inductive.lean block-membership table added on
main after this patch still uses it), comment updated.
Measured (lake exe ix check Nat.add_comm): total width 34806 -> 34781,
FFT cost unchanged (plain checks never take the skip path). Full ixvm
suite green incl. the frontier-assumption claim smoke; all FFT pins
unchanged. crates/ixvm-codegen/src/aiur_ixvm.rs regenerated.
---------
Co-authored-by: samuelburnham <45365069+samuelburnham@users.noreply.github.com>
Sign up for freeto 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

@arthurpaulino@gabriel-barrett
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface - #457

Merged
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor
Jul 2, 2026
Merged

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface#457
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor

Conversation

@arthurpaulino

Copy link
Copy Markdown
Member

Three commits on top of main that cut ingress FFT cost on heavy shards and land a documented IxVM IOBuffer interface.

Measurement methodology — read first

Shard 51 with unstubbed blake3 currently OOMs at all practical ulimit settings on the dev machine. To get past memory exhaustion and produce a comparative measurement, the kernel's blake3 verification paths in load_verified_constant / load_verified_blob / load_verified_claim were stubbed to no-ops for profiling. The stubs are not part of any committed code in this PR — they were applied locally during measurement and reverted.

What this means for the numbers below:

  • The delta between two stubbed runs is real for the non-blake3 portion of the trace. Both endpoints elide the same verification work, so the comparison isolates the effect of the kernel changes in this PR.
  • The absolute stubbed cost (e.g. "93.94G FFT") is not the production shard-51 typecheck cost. Production includes blake3 work (~16–20% of the trace per earlier profiles) that the stubs omit.

Every shard-51 number below is a "blake3-stubbed trace" measurement. Per-const numbers from lake test -- --ignored ixvm (no stubs) are unaffected and reflect real production cost.

Headline

MetricBeforeAfterΔ
Shard 51 (blake3-stubbed trace)119_152_044_06293_939_967_063−21.1% on the non-blake3 portion
Vector.append (production, unstubbed)2_661_244_8452_607_925_682−2.00%
Array.append_assoc (production)2_588_157_5382_537_478_644−1.96%
_private...extractMainModule._unsafe_rec (production)1_091_354_0311_064_762_809−2.44%

Per-const wins are end-to-end on the real (unstubbed) kernel; the shard-51 delta is real on the non-blake3 portion of the trace.

Commits

1. 120001d — IxVM kernel: augment addr_pos_map with blob-ref sentinels

lookup_addr_pos's rbtree only contained const addresses. Every blob ref (literal-blob payload pointers in Constant.refs) probed the map, missed, and fell through to the O(N) linear scan over all_addrs which also returned 0. Wasted work proportional to (blob refs × shard closure size).

This commit adds a third value class to the same rbtree: a sentinel 4294967295 (beyond any honest pos+1), inserted by walking each const's refs once via augment_with_blob_refs. lookup_addr_pos becomes a 3-way match: 0 → fall back to linear scan (now only fires under the de-intern soundness corner-case, ~never in practice); SENTINEL → return 0 directly (known blob); pos+1 → return pos.

Shard 51 (blake3-stubbed): 119_152_044_062 → 102_190_540_489 — −14.2% on the non-blake3 portion.

address_eq row count drops from ~10.3M to ~3.4M (−67%); lookup_addr_pos_linear falls out of the top-25 cost table. The augment-walk's added rbtree probes (~430M FFT in the stubbed trace) are paid back many times over by the ~17B FFT saved on lookups.

Soundness: every probe still relies on the positive direction ptr_val equality ⇒ content equality (Aiur Store content-addressing invariant). The linear fallback stays for the (still theoretical) malicious de-intern, where it uses content-based address_eq.

2. 9acbd58 — IxVM ingress: plumbing helpers + sidecar→rbtree migrations + ptr-passing

A 5-reviewer synthesis of ingress accidental complexity drove this consolidated refactor. Three families of change.

Plumbing helpers:

  • verify_bytes_against(bytes, expected) + bytes_to_addr(bytes) + blake3_flat(input) centralise the 24-line [h[i][j]] digest reshape that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
  • Channel I/O helpers factor the io_get_info + #read_byte_stream boilerplate at 6 sites. load_verified_blob no longer double-load(addr)s.
  • sentinel_blob_ref() named const replaces one bare 4294967295.

Sidecar lists → rbtree:

  • lookup_canon_addr: O(N) parallel-list walk → O(log N) rbtree probe via new build_canon_addr_map. canon_addrs: List<Addr> arg replaced by canon_addr_map: &RBTreeMap<Addr> (one threaded ptr).
  • lookup_block_start: O(N) parallel-list walk → O(log N) rbtree probe via new build_block_start_map. block_addrs / block_starts args replaced by block_start_map: &RBTreeMap<G> across ~12 fn signatures.
  • build_aux_recr_ctor_idxs returns (idxs, block_addr) so the standalone-Recr caller doesn't redo the rec_typ_to_inductive_addr + load_verified_constant chain to recover block_addr.
  • build_ref_idxs_and_blobs fuses the two separate walks (build_ref_idxs_mapped + build_lit_blobs) into one — single rbtree probe per ref produces both ref_idxs and lit_blobs. 5 caller sites simplified.

Pointer-passing (per Aiur's inputSize→width cost model):

  • convert_univ(u: &Univ): per-row input width drops from 5-variant union to one G column. Caller convert_univ_idxs already produces the &Univ via list_lookup(univs, idx) — no extra store.
  • convert_one(input: &ConvertInput): caller convert_all already has &input from the ListNode.Cons destructure.

Dead code deleted:address_in_list, apply_ctor_overrides, lookup_override, build_lit_blobs, build_ref_idxs_mapped, is_blob, ctx_convert_expr.

Shard 51 (blake3-stubbed): 102_190_540_489 → 93_939_967_063 — −8.07% on the non-blake3 portion on top of 120001d.

Per-const wins on heavy production targets (unstubbed lake test):

ConstantBeforeAfterΔ
Nat.add_comm54_369_74554_049_773−0.59%
Nat.sub_le_of_le_add515_331_420510_843_459−0.87%
Nat.decLe191_471_719189_723_325−0.91%
Array.append_assoc2_567_087_8932_537_360_311−1.16%
Vector.append2_639_286_0782_607_800_745−1.19%
IxVMInd.Even.rec31_659_49331_434_525−0.71%
IxVMInd.Odd.rec31_658_59831_433_622−0.71%
String.Internal.append718_803_075708_296_270−1.46%
_private...extractMainModule._unsafe_rec1_081_617_7051_064_689_765−1.57%

lake test -- --ignored ixvm green; 42 pins re-pinned.

3. 9c2eb23 — IxVM: per-kind IOBuffer channels with documented interface

The pre-existing IOBuffer multiplexed claim bytes, assumption tree bytes, constant wire bytes, and a per-blob-addr empty-marker on a single channel 0, distinguished only by blake3 content-hash collision-impossibility. A new contributor reading load_payload_const(key) couldn't tell from the helper whether they were getting a const, a claim, or a tree — only the caller knew the intent.

This commit splits channel 0 into per-kind channels and lands a documented IxVM IOBuffer interface block (mirrored on both Aiur and Lean sides).

TierChannelPurposeKeyValue
Ctrl0claim wire bytesblake3(claim_bytes)claim bytes
Ctrl1assumption tree bytestree.roottree bytes
Const2constant wire bytesconst addrconst bytes
Const3Defn reducibility hintDefn addrsingle G
Blob4blob discriminatoraddrone byte (1=const, 0=blob)
Blob5blob raw bytesblob addrraw bytes

Tier 1 fires once per verify_claim. Tier 2 fires per constant traversed during load_with_deps. Tier 3 fires per blob ref encountered during build_ref_idxs_and_blobs.

The blob discriminator (ch 4) replaces the previous io_get_info(0, addr).len == 0 ⟹ blob hack inside load_with_deps with an explicit per-addr one-byte payload. No more len-as-meaning overload.

Every per-channel io_get_info + #read_byte_stream pair is inlined at the single consumer of each channel — no fn-call row gets added for the reorganization. Channel reorganization is cost-neutral by design, confirmed by measurement: per-const pin shifts vs 9acbd58 land within ±0.010% (noise-tier; the ch-4 discriminator's one read_byte trades against the eliminated io_get_info(0, addr).len check).

The win here is review-time clarity, not FFT — io_get_info(channel, key) now has one value shape per channel and a doc block both kernel and harness reference.

Soundness: ch 0/1/2/5 byte streams are blake3-verified by the kernel against their content-addressed keys. ch 3 hint is semantically optional (controls WHNF reduction heuristic only; def-eq is sound either way). ch 4 discriminator is sound by erasure-correctness — a lying byte flips the const/blob decision and the wrong-path load downstream fails (a "const" blob triggers a ch 2 read that returns empty → blake3 verify against the non-empty addr fails; a "blob" const dangles references → typecheck fail).

Cumulative measurement (vs main)

ConstantBeforeAfterΔ
Nat.add_comm54_504_714 (approx.)54_047_004~−0.84%
Nat.decLe191_354_793189_722_204−0.85%
Nat.sub_le_of_le_add515_056_158510_870_092−0.81%
Array.append_assoc2_588_157_5382_537_478_644−1.96%
Vector.append2_661_244_8452_607_925_682−2.00%
String.Internal.append725_415_461708_332_683−2.36%
_private...extractMainModule._unsafe_rec1_091_354_0311_064_762_809−2.44%
Shard 51 (blake3-stubbed trace, non-blake3 portion)119_152_044_06293_939_967_063−21.1%

Test plan

  • lake build clean
  • lake test -- --ignored ixvm green at every commit
  • Shard 51 comparative measurement with blake3 stubs applied locally to both endpoints (stubs reverted; not part of any committed code in this PR)

`lookup_addr_pos`'s rbtree (`addr_pos_map`) only contained const
addresses. Every blob ref (literal-blob payload pointers in
`Constant.refs`) probed the map, missed, and fell through to the O(N)
linear scan over `all_addrs` which also returned 0. Wasted work
proportional to (blob refs × shard closure size).
Add a third value class to the same rbtree: the sentinel `4294967295`
(beyond any honest `pos+1`), inserted by walking each const's `refs`
once via `augment_with_blob_refs`. Any ref not already mapped to a
const-position gets the sentinel.
`lookup_addr_pos` becomes a 3-way match: `0` → fall back to linear
scan (now only fires under the de-intern soundness corner-case, ~never
in practice); `SENTINEL` → return 0 directly (known blob); `pos+1` →
return `pos`. `is_blob` mirrors the same match.
Soundness preserved: every probe still relies on the positive direction
`ptr_val` equality ⇒ content equality (Aiur Store's content-addressing
invariant). The linear fallback stays put for the (still theoretical)
malicious de-intern, where it uses content-based `address_eq`.
`lake exe ix check --ixe init.ixe --ixes init.ixes --shard 51`
(`ulimit -v 23000000`, with the kernel's blake3 paths in
`load_verified_constant` / `load_verified_blob` / `load_verified_claim`
stubbed for profiling):
119_152_044_062 → 102_190_540_489 FFT (-14.2%).
Tests/Ix/IxVM.lean: 42 pin shifts absorbing the per-ingress
augment-walk overhead.
A consolidated refactor of the ingress pipeline guided by a 5-reviewer
synthesis of accidental complexity sites. Three families of change:
PLUMBING
* `Blake3.lean`: `verify_bytes_against(bytes, expected)` + `bytes_to_addr(bytes)`
+ `blake3_flat(input)` centralise the 24-line `[h[i][j]]` digest reshape
that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
* `Ingress.lean`: `load_payload_const`/`blob`/`hint` + `ch_const`/`blob`/`hint`
factor the `io_get_info` + `#read_byte_stream` boilerplate at 6 sites.
`load_verified_blob` no longer double-`load(addr)`s.
* `sentinel_blob_ref()` named const replaces one bare `4294967295`.
SIDECAR LISTS → RBTREE
* `lookup_canon_addr`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_canon_addr_map`. `canon_addrs: List<Addr>` arg replaced
by `canon_addr_map: &RBTreeMap<Addr>` (one threaded ptr).
* `lookup_block_start`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_block_start_map`. `block_addrs` / `block_starts` args
replaced by `block_start_map: &RBTreeMap<G>` across ~12 fn signatures.
* `build_aux_recr_ctor_idxs` returns `(idxs, block_addr)` so the
standalone-Recr caller in `build_convert_inputs_walk` doesn't redo the
`rec_typ_to_inductive_addr` + `load_verified_constant` chain to recover
`block_addr` — `build_aux_recr_ctor_idxs` already computed it internally.
* `build_ref_idxs_and_blobs` fuses the two separate walks
(`build_ref_idxs_mapped` + `build_lit_blobs`) into one — single rbtree
probe per ref produces both `ref_idxs` and `lit_blobs`. 5 caller sites
simplified.
PTR-PASSING (per `reference_aiur_pass_pointer_not_value`)
* `convert_univ(u: &Univ)`: per-row input width drops from 5-variant
union to one G column. Caller `convert_univ_idxs` already produces the
`&Univ` via `list_lookup(univs, idx)` — no extra `store`.
* `convert_one(input: &ConvertInput)`: caller `convert_all` already has
`&input` from the `ListNode.Cons` destructure.
DEAD CODE DELETED
`address_in_list`, `apply_ctor_overrides`, `lookup_override`, `build_lit_blobs`,
`build_ref_idxs_mapped`, `is_blob`, `ctx_convert_expr` — all unused after
the migrations above.
TRIED + REVERTED (left as notes in code where instructive):
* Fused `build_addr_pos_map` + `augment_with_blob_refs` single-pass.
Lost to Aiur's per-row width tax — two narrower passes cheaper than
one merged wide pass.
* `convert_expr(ctx: &ConvertCtx)` ptr. Per-arm `load(ctx)` cost > raw-arg
threading.
* `compute_layout_walk` seen-mptrs/seen-poses → rbtree. Small regression
on typical shards (few enough Muts blocks that O(N) list scan is
already cheap; rbtree value column tax > savings).
* `block_members_map` to skip per-projection `load_verified_constant`.
Lost because Aiur memoises `load_verified_constant` automatically — the
new rbtree probe's width tax exceeded the savings from the eliminated
match cascade.
* `ctx` everywhere as `&ConvertCtx` (with `store(...)` at 5 construction
sites). The added `store` rows offset the per-row savings — wash.
`lake test -- --ignored ixvm`: 42 pin shifts, all measurable wins vs the
`ap/blob-ref-rbtree-augment` tip. Cumulative on heavy consts:
* `Nat.add_comm`: 54_369_745 → 54_049_773 FFT (-0.59%)
* `Nat.sub_le_of_le_add`: 515_331_420 → 510_843_459 FFT (-0.87%)
* `Array.append_assoc`: 2_567_087_893 → 2_537_360_311 FFT (-1.16%)
* `Vector.append`: 2_639_286_078 → 2_607_800_745 FFT (-1.19%)
* `String.Internal.append`: 718_803_075 → 708_296_270 FFT (-1.46%)
* `IxVMInd.Even.rec`: 31_659_493 → 31_434_525 FFT (-0.71%)
* `_private....extractMainModule._unsafe_rec`: 1_081_617_705 → 1_064_689_765 FFT (-1.57%)
Replaces channel 0's tagged-union (claim / tree / const bytes / empty-
blob-marker, distinguished only by key content-hash collision-
impossibility) with one channel per value kind. Channel reorganization
is cost-neutral — all six per-channel `io_get_info` + `#read_byte_stream`
pairs are inlined at the single consumer of each channel, so no fn-call
row gets added.
New layout, tiered by access pattern:
| Tier | Channel | Purpose | Key | Value |
|-------|---------|--------------------------|-------------------------|----------------------|
| Ctrl | 0 | claim wire bytes | `blake3(claim_bytes)` | claim bytes |
| Ctrl | 1 | assumption tree bytes | `tree.root` | tree bytes |
| Const | 2 | constant wire bytes | const addr | const bytes |
| Const | 3 | Defn reducibility hint | Defn addr | single G |
| Blob | 4 | blob discriminator | addr | one byte (1=const, 0=blob) |
| Blob | 5 | blob raw bytes | blob addr | raw bytes |
Tier 1 = one-per-`verify_claim`. Tier 2 = per-const. Tier 3 = per-blob.
`io_get_info(channel, key)` is now unambiguous by channel alone.
Aiur side (`Ingress.lean`, `Kernel/Claim.lean`):
* `load_verified_constant` inlines `io_get_info(2, raw)` +
`#read_byte_stream(2, ...)` (was ch 0).
* `load_verified_blob` inlines ch 5 read (was ch 1).
* `load_constant_hint` inlines ch 3 read (was ch 2).
* `load_with_deps` inlines a ch 4 discriminator probe replacing the
former `io_get_info(0, addr).len == 0 ⟹ blob` hack on ch 0. One
read_byte for the per-addr discriminator byte.
* `load_verified_claim` inlines ch 0 (was using a shared `load_payload_const`
helper on ch 0).
* `load_assumption_tree` inlines ch 1 (was using same `load_payload_const`
helper on ch 0).
* Helper wrappers (`load_payload_claim/tree/const/hint/blob`,
`load_discriminator`, `ch_*` constants) all removed — every channel
has exactly one consumer site and the inlined dispatch costs no fn
call.
* Top-of-file `IxVM IOBuffer interface` doc block documents the layout
+ soundness model.
Lean side (`ClaimHarness.lean`):
* `addEntries` writes per channel — consts → ch 2 + per-addr
discriminator `[1]` on ch 4; blobs → ch 5 + per-addr discriminator
`[0]` on ch 4; Defn hints → ch 3.
* `seedTreeAt` writes tree bytes to ch 1 (was ch 0).
* `buildClaimWitness` writes claim bytes to ch 0 (unchanged).
* Drops the empty-marker write on ch 0 for blob addrs — the ch 4
discriminator covers blob/const classification explicitly.
* Top-of-section `IxVM IOBuffer interface` doc block mirrors the
Aiur-side comment.
Soundness model unchanged. ch 0/1/2/5 byte streams are blake3-verified
by the kernel against their content-addressed keys. ch 3 hint is
semantically optional (controls WHNF reduction heuristic only; def-eq
is sound either way). ch 4 discriminator is sound by erasure-
correctness — a lying byte flips the const/blob decision and the
wrong-path load downstream fails (a "const" blob triggers a ch 2 read
that returns empty → blake3 verify against the non-empty addr fails;
a "blob" const dangles references → typecheck fail).
`lake test -- --ignored ixvm` green; 42 pins shifted within ±0.010%
(noise-tier) vs the pre-shuffle baseline — channel reorganization is
cost-neutral as required, the discriminator's one read_byte trades
against the eliminated `io_get_info(0, ...).len` check.
`blake3_flat` was a pure array-reshaping wrapper around `blake3` used
by `verify_bytes_against` and `bytes_to_addr`. Inline the reshape at
both call sites. `blake3`'s memoization key is the input ByteStream, so
the 74 cache hits that `blake3_flat` recorded migrate cleanly to
`blake3` — dedup preserved.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,898,064 → 12,430,154,083 (−743,981)
- Total width: 33,755 → 33,714 (−41)
- `blake3_flat` (41w × 1692h, 74 hits, 743,981 FFT) — removed
- `blake3` (92w × 1692h, 0 hits, ...) → now 74 hits
Same experiment for `bytes_to_addr` inline was neutral (−5k FFT, +92
width across 4 call sites) — kept as a wrapper.
@arthurpaulino
arthurpaulino marked this pull request as ready for review July 2, 2026 15:06
@arthurpaulino
arthurpaulino enabled auto-merge (squash) July 2, 2026 15:06
Mirror the Expr port pattern (`KExpr = &KExprNode`) for universe levels.
Kernel-side universe reps flip from unboxed enum values to pointer
aliases:
Before:
enum KLevel {
Zero,
Succ(&KLevel),
Max(&KLevel, &KLevel),
IMax(&KLevel, &KLevel),
Param(G)
}
After:
enum KLevelNode {
Zero,
Succ(KLevel),
Max(KLevel, KLevel),
IMax(KLevel, KLevel),
Param(G)
}
type KLevel = &KLevelNode
Callers switch from passing whole enum values to passing pointers.
Every match on a KLevel arg gains a `load()`; every value construction
that fills a KLevel arg or return slot gets a `store()`. `KExprNode.Srt`,
`KConstantInfo.{Ctor,Axiom,Defn,Thm,Opaque,Quot,Induct,Rec}` universe
fields all use the aliased `KLevel` now.
Motivation: pass-by-pointer shrinks per-row input width on every level-
manipulating function (`level_eq`, `level_leq`, `level_reduce`,
`level_inst_params`, ...), same trick as
`reference_aiur_pass_pointer_not_value`. Compared to the Expr port the
delta is smaller because level fns are already narrow, but it stacks.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,154,083 -> 12,418,344,860 (-11,809,223)
- Total width: 33,714 -> 33,532 (-182)
Re-pin every FFT cost shifted by the port.
@arthurpaulino
arthurpaulino merged commit 110f28f into mainJul 2, 2026
15 of 16 checks passed
@arthurpaulino
arthurpaulino deleted the ap/ingress-refactor branch July 2, 2026 19:04
johnchandlerburnham pushed a commit that referenced this pull request Jul 21, 2026
…505)
* IxVM: drop dead KValNode/KVal/KValEnv
From ap/kernel 828fb85 (Arthur Paulino): the NbE value domain is defined
but referenced nowhere — the live kernel runs on de-Bruijn KExpr;
vestigial from an abandoned NbE direction. (The closed-term context
normalization from that commit was measured separately and not taken:
the per-call expr_lbr probe cost +2.9% FFT on recursor loops for a 0.5%
record reduction.)
* IxVM: memoized prim_family dispatch + width-safe offset-stuck placement
Three coordinated changes to Const-head whnf dispatch (cherry-pick of
130f30b, adapted to post-#450/#457 main):
1. prim_family(addr) classifies a head address into the one reducer
family that could fire on it (nat/str/bitvec/native/decidable; the
sets are disjoint). Keyed on the ADDRESS ALONE it memoizes to one row
per distinct constant address per run, and whnf_const_head calls at
most one family reducer. The previous gauntlet ran every reducer in
sequence for guaranteed misses.
2. The symbolic-Nat offset-stuck check moves from a delta-arm probe into
try_nat_dispatch's miss path as a cold function
(try_nat_offset_dispatch, verdict 2 = "already stuck, do not
re-whnf"), with the offset construction shared via
mk_nat_offset_stuck (also used by the linear-rec collapse).
3. nat_lit_to_ctor_or_self exposes ONE constructor layer
(n -> succ(Lit(n-1))) instead of materializing the full succ chain.
Adaptations vs the original patch:
- whnf_nd_const_head (no-delta WHNF, added on main after the patch)
converted to the same family dispatch.
- Kept main's cold-extracted try_nat_binop_dispatch and routed the
symbolic-base case to try_nat_offset_dispatch from its miss arm.
Measured (lake exe ix check Nat.add_comm): total width 34820 -> 34806,
FFT cost 49571210 -> 48860647 (-1.43%). All 53 ixvm-suite FFT pins
decreased (-0.19%..-1.43%); parity and claim smokes pass. Pins updated;
crates/ixvm-codegen/src/aiur_ixvm.rs regenerated via `lake exe ix
codegen`.
* IxVM: port jcb/fixes H-14 — ptr_val skip map + lockstep addr cursor
Two quadratic/constant-factor fixes to check_all_skipping, ported from
jcb/fixes (3763356, John C. Burnham); cherry-pick of 2d83be1 adapted to
post-#457 main:
- The assumption-leaf skip set keys on ptr_val instead of the first 4
address bytes: one tree lookup per constant, no per-lookup address
load, no confirming address_eq. Sound by the build_addr_pos_map
interning invariant (one pointer, one content): a ptr hit implies the
address IS a leaf; a de-interned pointer reads as absent and the
constant just gets checked — fail-closed.
- The iterator walks the addrs list in LOCKSTEP with consts (cur_addrs
suffix) instead of list_lookup(addrs, pos) per constant, which
re-walked the prefix every iteration — a standalone O(closure^2).
Adaptations: kept main's two-arg check_canonical_block_sort call;
addr_key retained (the Inductive.lean block-membership table added on
main after this patch still uses it), comment updated.
Measured (lake exe ix check Nat.add_comm): total width 34806 -> 34781,
FFT cost unchanged (plain checks never take the skip path). Full ixvm
suite green incl. the frontier-assumption claim smoke; all FFT pins
unchanged. crates/ixvm-codegen/src/aiur_ixvm.rs regenerated.
---------
Co-authored-by: samuelburnham <45365069+samuelburnham@users.noreply.github.com>
Sign up for freeto 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

@arthurpaulino@gabriel-barrett
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface - #457

Merged
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor
Jul 2, 2026
Merged

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface#457
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor

Conversation

@arthurpaulino

Copy link
Copy Markdown
Member

Three commits on top of main that cut ingress FFT cost on heavy shards and land a documented IxVM IOBuffer interface.

Measurement methodology — read first

Shard 51 with unstubbed blake3 currently OOMs at all practical ulimit settings on the dev machine. To get past memory exhaustion and produce a comparative measurement, the kernel's blake3 verification paths in load_verified_constant / load_verified_blob / load_verified_claim were stubbed to no-ops for profiling. The stubs are not part of any committed code in this PR — they were applied locally during measurement and reverted.

What this means for the numbers below:

  • The delta between two stubbed runs is real for the non-blake3 portion of the trace. Both endpoints elide the same verification work, so the comparison isolates the effect of the kernel changes in this PR.
  • The absolute stubbed cost (e.g. "93.94G FFT") is not the production shard-51 typecheck cost. Production includes blake3 work (~16–20% of the trace per earlier profiles) that the stubs omit.

Every shard-51 number below is a "blake3-stubbed trace" measurement. Per-const numbers from lake test -- --ignored ixvm (no stubs) are unaffected and reflect real production cost.

Headline

MetricBeforeAfterΔ
Shard 51 (blake3-stubbed trace)119_152_044_06293_939_967_063−21.1% on the non-blake3 portion
Vector.append (production, unstubbed)2_661_244_8452_607_925_682−2.00%
Array.append_assoc (production)2_588_157_5382_537_478_644−1.96%
_private...extractMainModule._unsafe_rec (production)1_091_354_0311_064_762_809−2.44%

Per-const wins are end-to-end on the real (unstubbed) kernel; the shard-51 delta is real on the non-blake3 portion of the trace.

Commits

1. 120001d — IxVM kernel: augment addr_pos_map with blob-ref sentinels

lookup_addr_pos's rbtree only contained const addresses. Every blob ref (literal-blob payload pointers in Constant.refs) probed the map, missed, and fell through to the O(N) linear scan over all_addrs which also returned 0. Wasted work proportional to (blob refs × shard closure size).

This commit adds a third value class to the same rbtree: a sentinel 4294967295 (beyond any honest pos+1), inserted by walking each const's refs once via augment_with_blob_refs. lookup_addr_pos becomes a 3-way match: 0 → fall back to linear scan (now only fires under the de-intern soundness corner-case, ~never in practice); SENTINEL → return 0 directly (known blob); pos+1 → return pos.

Shard 51 (blake3-stubbed): 119_152_044_062 → 102_190_540_489 — −14.2% on the non-blake3 portion.

address_eq row count drops from ~10.3M to ~3.4M (−67%); lookup_addr_pos_linear falls out of the top-25 cost table. The augment-walk's added rbtree probes (~430M FFT in the stubbed trace) are paid back many times over by the ~17B FFT saved on lookups.

Soundness: every probe still relies on the positive direction ptr_val equality ⇒ content equality (Aiur Store content-addressing invariant). The linear fallback stays for the (still theoretical) malicious de-intern, where it uses content-based address_eq.

2. 9acbd58 — IxVM ingress: plumbing helpers + sidecar→rbtree migrations + ptr-passing

A 5-reviewer synthesis of ingress accidental complexity drove this consolidated refactor. Three families of change.

Plumbing helpers:

  • verify_bytes_against(bytes, expected) + bytes_to_addr(bytes) + blake3_flat(input) centralise the 24-line [h[i][j]] digest reshape that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
  • Channel I/O helpers factor the io_get_info + #read_byte_stream boilerplate at 6 sites. load_verified_blob no longer double-load(addr)s.
  • sentinel_blob_ref() named const replaces one bare 4294967295.

Sidecar lists → rbtree:

  • lookup_canon_addr: O(N) parallel-list walk → O(log N) rbtree probe via new build_canon_addr_map. canon_addrs: List<Addr> arg replaced by canon_addr_map: &RBTreeMap<Addr> (one threaded ptr).
  • lookup_block_start: O(N) parallel-list walk → O(log N) rbtree probe via new build_block_start_map. block_addrs / block_starts args replaced by block_start_map: &RBTreeMap<G> across ~12 fn signatures.
  • build_aux_recr_ctor_idxs returns (idxs, block_addr) so the standalone-Recr caller doesn't redo the rec_typ_to_inductive_addr + load_verified_constant chain to recover block_addr.
  • build_ref_idxs_and_blobs fuses the two separate walks (build_ref_idxs_mapped + build_lit_blobs) into one — single rbtree probe per ref produces both ref_idxs and lit_blobs. 5 caller sites simplified.

Pointer-passing (per Aiur's inputSize→width cost model):

  • convert_univ(u: &Univ): per-row input width drops from 5-variant union to one G column. Caller convert_univ_idxs already produces the &Univ via list_lookup(univs, idx) — no extra store.
  • convert_one(input: &ConvertInput): caller convert_all already has &input from the ListNode.Cons destructure.

Dead code deleted:address_in_list, apply_ctor_overrides, lookup_override, build_lit_blobs, build_ref_idxs_mapped, is_blob, ctx_convert_expr.

Shard 51 (blake3-stubbed): 102_190_540_489 → 93_939_967_063 — −8.07% on the non-blake3 portion on top of 120001d.

Per-const wins on heavy production targets (unstubbed lake test):

ConstantBeforeAfterΔ
Nat.add_comm54_369_74554_049_773−0.59%
Nat.sub_le_of_le_add515_331_420510_843_459−0.87%
Nat.decLe191_471_719189_723_325−0.91%
Array.append_assoc2_567_087_8932_537_360_311−1.16%
Vector.append2_639_286_0782_607_800_745−1.19%
IxVMInd.Even.rec31_659_49331_434_525−0.71%
IxVMInd.Odd.rec31_658_59831_433_622−0.71%
String.Internal.append718_803_075708_296_270−1.46%
_private...extractMainModule._unsafe_rec1_081_617_7051_064_689_765−1.57%

lake test -- --ignored ixvm green; 42 pins re-pinned.

3. 9c2eb23 — IxVM: per-kind IOBuffer channels with documented interface

The pre-existing IOBuffer multiplexed claim bytes, assumption tree bytes, constant wire bytes, and a per-blob-addr empty-marker on a single channel 0, distinguished only by blake3 content-hash collision-impossibility. A new contributor reading load_payload_const(key) couldn't tell from the helper whether they were getting a const, a claim, or a tree — only the caller knew the intent.

This commit splits channel 0 into per-kind channels and lands a documented IxVM IOBuffer interface block (mirrored on both Aiur and Lean sides).

TierChannelPurposeKeyValue
Ctrl0claim wire bytesblake3(claim_bytes)claim bytes
Ctrl1assumption tree bytestree.roottree bytes
Const2constant wire bytesconst addrconst bytes
Const3Defn reducibility hintDefn addrsingle G
Blob4blob discriminatoraddrone byte (1=const, 0=blob)
Blob5blob raw bytesblob addrraw bytes

Tier 1 fires once per verify_claim. Tier 2 fires per constant traversed during load_with_deps. Tier 3 fires per blob ref encountered during build_ref_idxs_and_blobs.

The blob discriminator (ch 4) replaces the previous io_get_info(0, addr).len == 0 ⟹ blob hack inside load_with_deps with an explicit per-addr one-byte payload. No more len-as-meaning overload.

Every per-channel io_get_info + #read_byte_stream pair is inlined at the single consumer of each channel — no fn-call row gets added for the reorganization. Channel reorganization is cost-neutral by design, confirmed by measurement: per-const pin shifts vs 9acbd58 land within ±0.010% (noise-tier; the ch-4 discriminator's one read_byte trades against the eliminated io_get_info(0, addr).len check).

The win here is review-time clarity, not FFT — io_get_info(channel, key) now has one value shape per channel and a doc block both kernel and harness reference.

Soundness: ch 0/1/2/5 byte streams are blake3-verified by the kernel against their content-addressed keys. ch 3 hint is semantically optional (controls WHNF reduction heuristic only; def-eq is sound either way). ch 4 discriminator is sound by erasure-correctness — a lying byte flips the const/blob decision and the wrong-path load downstream fails (a "const" blob triggers a ch 2 read that returns empty → blake3 verify against the non-empty addr fails; a "blob" const dangles references → typecheck fail).

Cumulative measurement (vs main)

ConstantBeforeAfterΔ
Nat.add_comm54_504_714 (approx.)54_047_004~−0.84%
Nat.decLe191_354_793189_722_204−0.85%
Nat.sub_le_of_le_add515_056_158510_870_092−0.81%
Array.append_assoc2_588_157_5382_537_478_644−1.96%
Vector.append2_661_244_8452_607_925_682−2.00%
String.Internal.append725_415_461708_332_683−2.36%
_private...extractMainModule._unsafe_rec1_091_354_0311_064_762_809−2.44%
Shard 51 (blake3-stubbed trace, non-blake3 portion)119_152_044_06293_939_967_063−21.1%

Test plan

  • lake build clean
  • lake test -- --ignored ixvm green at every commit
  • Shard 51 comparative measurement with blake3 stubs applied locally to both endpoints (stubs reverted; not part of any committed code in this PR)

`lookup_addr_pos`'s rbtree (`addr_pos_map`) only contained const
addresses. Every blob ref (literal-blob payload pointers in
`Constant.refs`) probed the map, missed, and fell through to the O(N)
linear scan over `all_addrs` which also returned 0. Wasted work
proportional to (blob refs × shard closure size).
Add a third value class to the same rbtree: the sentinel `4294967295`
(beyond any honest `pos+1`), inserted by walking each const's `refs`
once via `augment_with_blob_refs`. Any ref not already mapped to a
const-position gets the sentinel.
`lookup_addr_pos` becomes a 3-way match: `0` → fall back to linear
scan (now only fires under the de-intern soundness corner-case, ~never
in practice); `SENTINEL` → return 0 directly (known blob); `pos+1` →
return `pos`. `is_blob` mirrors the same match.
Soundness preserved: every probe still relies on the positive direction
`ptr_val` equality ⇒ content equality (Aiur Store's content-addressing
invariant). The linear fallback stays put for the (still theoretical)
malicious de-intern, where it uses content-based `address_eq`.
`lake exe ix check --ixe init.ixe --ixes init.ixes --shard 51`
(`ulimit -v 23000000`, with the kernel's blake3 paths in
`load_verified_constant` / `load_verified_blob` / `load_verified_claim`
stubbed for profiling):
119_152_044_062 → 102_190_540_489 FFT (-14.2%).
Tests/Ix/IxVM.lean: 42 pin shifts absorbing the per-ingress
augment-walk overhead.
A consolidated refactor of the ingress pipeline guided by a 5-reviewer
synthesis of accidental complexity sites. Three families of change:
PLUMBING
* `Blake3.lean`: `verify_bytes_against(bytes, expected)` + `bytes_to_addr(bytes)`
+ `blake3_flat(input)` centralise the 24-line `[h[i][j]]` digest reshape
that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
* `Ingress.lean`: `load_payload_const`/`blob`/`hint` + `ch_const`/`blob`/`hint`
factor the `io_get_info` + `#read_byte_stream` boilerplate at 6 sites.
`load_verified_blob` no longer double-`load(addr)`s.
* `sentinel_blob_ref()` named const replaces one bare `4294967295`.
SIDECAR LISTS → RBTREE
* `lookup_canon_addr`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_canon_addr_map`. `canon_addrs: List<Addr>` arg replaced
by `canon_addr_map: &RBTreeMap<Addr>` (one threaded ptr).
* `lookup_block_start`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_block_start_map`. `block_addrs` / `block_starts` args
replaced by `block_start_map: &RBTreeMap<G>` across ~12 fn signatures.
* `build_aux_recr_ctor_idxs` returns `(idxs, block_addr)` so the
standalone-Recr caller in `build_convert_inputs_walk` doesn't redo the
`rec_typ_to_inductive_addr` + `load_verified_constant` chain to recover
`block_addr` — `build_aux_recr_ctor_idxs` already computed it internally.
* `build_ref_idxs_and_blobs` fuses the two separate walks
(`build_ref_idxs_mapped` + `build_lit_blobs`) into one — single rbtree
probe per ref produces both `ref_idxs` and `lit_blobs`. 5 caller sites
simplified.
PTR-PASSING (per `reference_aiur_pass_pointer_not_value`)
* `convert_univ(u: &Univ)`: per-row input width drops from 5-variant
union to one G column. Caller `convert_univ_idxs` already produces the
`&Univ` via `list_lookup(univs, idx)` — no extra `store`.
* `convert_one(input: &ConvertInput)`: caller `convert_all` already has
`&input` from the `ListNode.Cons` destructure.
DEAD CODE DELETED
`address_in_list`, `apply_ctor_overrides`, `lookup_override`, `build_lit_blobs`,
`build_ref_idxs_mapped`, `is_blob`, `ctx_convert_expr` — all unused after
the migrations above.
TRIED + REVERTED (left as notes in code where instructive):
* Fused `build_addr_pos_map` + `augment_with_blob_refs` single-pass.
Lost to Aiur's per-row width tax — two narrower passes cheaper than
one merged wide pass.
* `convert_expr(ctx: &ConvertCtx)` ptr. Per-arm `load(ctx)` cost > raw-arg
threading.
* `compute_layout_walk` seen-mptrs/seen-poses → rbtree. Small regression
on typical shards (few enough Muts blocks that O(N) list scan is
already cheap; rbtree value column tax > savings).
* `block_members_map` to skip per-projection `load_verified_constant`.
Lost because Aiur memoises `load_verified_constant` automatically — the
new rbtree probe's width tax exceeded the savings from the eliminated
match cascade.
* `ctx` everywhere as `&ConvertCtx` (with `store(...)` at 5 construction
sites). The added `store` rows offset the per-row savings — wash.
`lake test -- --ignored ixvm`: 42 pin shifts, all measurable wins vs the
`ap/blob-ref-rbtree-augment` tip. Cumulative on heavy consts:
* `Nat.add_comm`: 54_369_745 → 54_049_773 FFT (-0.59%)
* `Nat.sub_le_of_le_add`: 515_331_420 → 510_843_459 FFT (-0.87%)
* `Array.append_assoc`: 2_567_087_893 → 2_537_360_311 FFT (-1.16%)
* `Vector.append`: 2_639_286_078 → 2_607_800_745 FFT (-1.19%)
* `String.Internal.append`: 718_803_075 → 708_296_270 FFT (-1.46%)
* `IxVMInd.Even.rec`: 31_659_493 → 31_434_525 FFT (-0.71%)
* `_private....extractMainModule._unsafe_rec`: 1_081_617_705 → 1_064_689_765 FFT (-1.57%)
Replaces channel 0's tagged-union (claim / tree / const bytes / empty-
blob-marker, distinguished only by key content-hash collision-
impossibility) with one channel per value kind. Channel reorganization
is cost-neutral — all six per-channel `io_get_info` + `#read_byte_stream`
pairs are inlined at the single consumer of each channel, so no fn-call
row gets added.
New layout, tiered by access pattern:
| Tier | Channel | Purpose | Key | Value |
|-------|---------|--------------------------|-------------------------|----------------------|
| Ctrl | 0 | claim wire bytes | `blake3(claim_bytes)` | claim bytes |
| Ctrl | 1 | assumption tree bytes | `tree.root` | tree bytes |
| Const | 2 | constant wire bytes | const addr | const bytes |
| Const | 3 | Defn reducibility hint | Defn addr | single G |
| Blob | 4 | blob discriminator | addr | one byte (1=const, 0=blob) |
| Blob | 5 | blob raw bytes | blob addr | raw bytes |
Tier 1 = one-per-`verify_claim`. Tier 2 = per-const. Tier 3 = per-blob.
`io_get_info(channel, key)` is now unambiguous by channel alone.
Aiur side (`Ingress.lean`, `Kernel/Claim.lean`):
* `load_verified_constant` inlines `io_get_info(2, raw)` +
`#read_byte_stream(2, ...)` (was ch 0).
* `load_verified_blob` inlines ch 5 read (was ch 1).
* `load_constant_hint` inlines ch 3 read (was ch 2).
* `load_with_deps` inlines a ch 4 discriminator probe replacing the
former `io_get_info(0, addr).len == 0 ⟹ blob` hack on ch 0. One
read_byte for the per-addr discriminator byte.
* `load_verified_claim` inlines ch 0 (was using a shared `load_payload_const`
helper on ch 0).
* `load_assumption_tree` inlines ch 1 (was using same `load_payload_const`
helper on ch 0).
* Helper wrappers (`load_payload_claim/tree/const/hint/blob`,
`load_discriminator`, `ch_*` constants) all removed — every channel
has exactly one consumer site and the inlined dispatch costs no fn
call.
* Top-of-file `IxVM IOBuffer interface` doc block documents the layout
+ soundness model.
Lean side (`ClaimHarness.lean`):
* `addEntries` writes per channel — consts → ch 2 + per-addr
discriminator `[1]` on ch 4; blobs → ch 5 + per-addr discriminator
`[0]` on ch 4; Defn hints → ch 3.
* `seedTreeAt` writes tree bytes to ch 1 (was ch 0).
* `buildClaimWitness` writes claim bytes to ch 0 (unchanged).
* Drops the empty-marker write on ch 0 for blob addrs — the ch 4
discriminator covers blob/const classification explicitly.
* Top-of-section `IxVM IOBuffer interface` doc block mirrors the
Aiur-side comment.
Soundness model unchanged. ch 0/1/2/5 byte streams are blake3-verified
by the kernel against their content-addressed keys. ch 3 hint is
semantically optional (controls WHNF reduction heuristic only; def-eq
is sound either way). ch 4 discriminator is sound by erasure-
correctness — a lying byte flips the const/blob decision and the
wrong-path load downstream fails (a "const" blob triggers a ch 2 read
that returns empty → blake3 verify against the non-empty addr fails;
a "blob" const dangles references → typecheck fail).
`lake test -- --ignored ixvm` green; 42 pins shifted within ±0.010%
(noise-tier) vs the pre-shuffle baseline — channel reorganization is
cost-neutral as required, the discriminator's one read_byte trades
against the eliminated `io_get_info(0, ...).len` check.
`blake3_flat` was a pure array-reshaping wrapper around `blake3` used
by `verify_bytes_against` and `bytes_to_addr`. Inline the reshape at
both call sites. `blake3`'s memoization key is the input ByteStream, so
the 74 cache hits that `blake3_flat` recorded migrate cleanly to
`blake3` — dedup preserved.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,898,064 → 12,430,154,083 (−743,981)
- Total width: 33,755 → 33,714 (−41)
- `blake3_flat` (41w × 1692h, 74 hits, 743,981 FFT) — removed
- `blake3` (92w × 1692h, 0 hits, ...) → now 74 hits
Same experiment for `bytes_to_addr` inline was neutral (−5k FFT, +92
width across 4 call sites) — kept as a wrapper.
@arthurpaulino
arthurpaulino marked this pull request as ready for review July 2, 2026 15:06
@arthurpaulino
arthurpaulino enabled auto-merge (squash) July 2, 2026 15:06
Mirror the Expr port pattern (`KExpr = &KExprNode`) for universe levels.
Kernel-side universe reps flip from unboxed enum values to pointer
aliases:
Before:
enum KLevel {
Zero,
Succ(&KLevel),
Max(&KLevel, &KLevel),
IMax(&KLevel, &KLevel),
Param(G)
}
After:
enum KLevelNode {
Zero,
Succ(KLevel),
Max(KLevel, KLevel),
IMax(KLevel, KLevel),
Param(G)
}
type KLevel = &KLevelNode
Callers switch from passing whole enum values to passing pointers.
Every match on a KLevel arg gains a `load()`; every value construction
that fills a KLevel arg or return slot gets a `store()`. `KExprNode.Srt`,
`KConstantInfo.{Ctor,Axiom,Defn,Thm,Opaque,Quot,Induct,Rec}` universe
fields all use the aliased `KLevel` now.
Motivation: pass-by-pointer shrinks per-row input width on every level-
manipulating function (`level_eq`, `level_leq`, `level_reduce`,
`level_inst_params`, ...), same trick as
`reference_aiur_pass_pointer_not_value`. Compared to the Expr port the
delta is smaller because level fns are already narrow, but it stacks.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,154,083 -> 12,418,344,860 (-11,809,223)
- Total width: 33,714 -> 33,532 (-182)
Re-pin every FFT cost shifted by the port.
@arthurpaulino
arthurpaulino merged commit 110f28f into mainJul 2, 2026
15 of 16 checks passed
@arthurpaulino
arthurpaulino deleted the ap/ingress-refactor branch July 2, 2026 19:04
johnchandlerburnham pushed a commit that referenced this pull request Jul 21, 2026
…505)
* IxVM: drop dead KValNode/KVal/KValEnv
From ap/kernel 828fb85 (Arthur Paulino): the NbE value domain is defined
but referenced nowhere — the live kernel runs on de-Bruijn KExpr;
vestigial from an abandoned NbE direction. (The closed-term context
normalization from that commit was measured separately and not taken:
the per-call expr_lbr probe cost +2.9% FFT on recursor loops for a 0.5%
record reduction.)
* IxVM: memoized prim_family dispatch + width-safe offset-stuck placement
Three coordinated changes to Const-head whnf dispatch (cherry-pick of
130f30b, adapted to post-#450/#457 main):
1. prim_family(addr) classifies a head address into the one reducer
family that could fire on it (nat/str/bitvec/native/decidable; the
sets are disjoint). Keyed on the ADDRESS ALONE it memoizes to one row
per distinct constant address per run, and whnf_const_head calls at
most one family reducer. The previous gauntlet ran every reducer in
sequence for guaranteed misses.
2. The symbolic-Nat offset-stuck check moves from a delta-arm probe into
try_nat_dispatch's miss path as a cold function
(try_nat_offset_dispatch, verdict 2 = "already stuck, do not
re-whnf"), with the offset construction shared via
mk_nat_offset_stuck (also used by the linear-rec collapse).
3. nat_lit_to_ctor_or_self exposes ONE constructor layer
(n -> succ(Lit(n-1))) instead of materializing the full succ chain.
Adaptations vs the original patch:
- whnf_nd_const_head (no-delta WHNF, added on main after the patch)
converted to the same family dispatch.
- Kept main's cold-extracted try_nat_binop_dispatch and routed the
symbolic-base case to try_nat_offset_dispatch from its miss arm.
Measured (lake exe ix check Nat.add_comm): total width 34820 -> 34806,
FFT cost 49571210 -> 48860647 (-1.43%). All 53 ixvm-suite FFT pins
decreased (-0.19%..-1.43%); parity and claim smokes pass. Pins updated;
crates/ixvm-codegen/src/aiur_ixvm.rs regenerated via `lake exe ix
codegen`.
* IxVM: port jcb/fixes H-14 — ptr_val skip map + lockstep addr cursor
Two quadratic/constant-factor fixes to check_all_skipping, ported from
jcb/fixes (3763356, John C. Burnham); cherry-pick of 2d83be1 adapted to
post-#457 main:
- The assumption-leaf skip set keys on ptr_val instead of the first 4
address bytes: one tree lookup per constant, no per-lookup address
load, no confirming address_eq. Sound by the build_addr_pos_map
interning invariant (one pointer, one content): a ptr hit implies the
address IS a leaf; a de-interned pointer reads as absent and the
constant just gets checked — fail-closed.
- The iterator walks the addrs list in LOCKSTEP with consts (cur_addrs
suffix) instead of list_lookup(addrs, pos) per constant, which
re-walked the prefix every iteration — a standalone O(closure^2).
Adaptations: kept main's two-arg check_canonical_block_sort call;
addr_key retained (the Inductive.lean block-membership table added on
main after this patch still uses it), comment updated.
Measured (lake exe ix check Nat.add_comm): total width 34806 -> 34781,
FFT cost unchanged (plain checks never take the skip path). Full ixvm
suite green incl. the frontier-assumption claim smoke; all FFT pins
unchanged. crates/ixvm-codegen/src/aiur_ixvm.rs regenerated.
---------
Co-authored-by: samuelburnham <45365069+samuelburnham@users.noreply.github.com>
Sign up for freeto 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

@arthurpaulino@gabriel-barrett
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface - #457

Merged
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor
Jul 2, 2026
Merged

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface#457
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor

Conversation

@arthurpaulino

Copy link
Copy Markdown
Member

Three commits on top of main that cut ingress FFT cost on heavy shards and land a documented IxVM IOBuffer interface.

Measurement methodology — read first

Shard 51 with unstubbed blake3 currently OOMs at all practical ulimit settings on the dev machine. To get past memory exhaustion and produce a comparative measurement, the kernel's blake3 verification paths in load_verified_constant / load_verified_blob / load_verified_claim were stubbed to no-ops for profiling. The stubs are not part of any committed code in this PR — they were applied locally during measurement and reverted.

What this means for the numbers below:

  • The delta between two stubbed runs is real for the non-blake3 portion of the trace. Both endpoints elide the same verification work, so the comparison isolates the effect of the kernel changes in this PR.
  • The absolute stubbed cost (e.g. "93.94G FFT") is not the production shard-51 typecheck cost. Production includes blake3 work (~16–20% of the trace per earlier profiles) that the stubs omit.

Every shard-51 number below is a "blake3-stubbed trace" measurement. Per-const numbers from lake test -- --ignored ixvm (no stubs) are unaffected and reflect real production cost.

Headline

MetricBeforeAfterΔ
Shard 51 (blake3-stubbed trace)119_152_044_06293_939_967_063−21.1% on the non-blake3 portion
Vector.append (production, unstubbed)2_661_244_8452_607_925_682−2.00%
Array.append_assoc (production)2_588_157_5382_537_478_644−1.96%
_private...extractMainModule._unsafe_rec (production)1_091_354_0311_064_762_809−2.44%

Per-const wins are end-to-end on the real (unstubbed) kernel; the shard-51 delta is real on the non-blake3 portion of the trace.

Commits

1. 120001d — IxVM kernel: augment addr_pos_map with blob-ref sentinels

lookup_addr_pos's rbtree only contained const addresses. Every blob ref (literal-blob payload pointers in Constant.refs) probed the map, missed, and fell through to the O(N) linear scan over all_addrs which also returned 0. Wasted work proportional to (blob refs × shard closure size).

This commit adds a third value class to the same rbtree: a sentinel 4294967295 (beyond any honest pos+1), inserted by walking each const's refs once via augment_with_blob_refs. lookup_addr_pos becomes a 3-way match: 0 → fall back to linear scan (now only fires under the de-intern soundness corner-case, ~never in practice); SENTINEL → return 0 directly (known blob); pos+1 → return pos.

Shard 51 (blake3-stubbed): 119_152_044_062 → 102_190_540_489 — −14.2% on the non-blake3 portion.

address_eq row count drops from ~10.3M to ~3.4M (−67%); lookup_addr_pos_linear falls out of the top-25 cost table. The augment-walk's added rbtree probes (~430M FFT in the stubbed trace) are paid back many times over by the ~17B FFT saved on lookups.

Soundness: every probe still relies on the positive direction ptr_val equality ⇒ content equality (Aiur Store content-addressing invariant). The linear fallback stays for the (still theoretical) malicious de-intern, where it uses content-based address_eq.

2. 9acbd58 — IxVM ingress: plumbing helpers + sidecar→rbtree migrations + ptr-passing

A 5-reviewer synthesis of ingress accidental complexity drove this consolidated refactor. Three families of change.

Plumbing helpers:

  • verify_bytes_against(bytes, expected) + bytes_to_addr(bytes) + blake3_flat(input) centralise the 24-line [h[i][j]] digest reshape that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
  • Channel I/O helpers factor the io_get_info + #read_byte_stream boilerplate at 6 sites. load_verified_blob no longer double-load(addr)s.
  • sentinel_blob_ref() named const replaces one bare 4294967295.

Sidecar lists → rbtree:

  • lookup_canon_addr: O(N) parallel-list walk → O(log N) rbtree probe via new build_canon_addr_map. canon_addrs: List<Addr> arg replaced by canon_addr_map: &RBTreeMap<Addr> (one threaded ptr).
  • lookup_block_start: O(N) parallel-list walk → O(log N) rbtree probe via new build_block_start_map. block_addrs / block_starts args replaced by block_start_map: &RBTreeMap<G> across ~12 fn signatures.
  • build_aux_recr_ctor_idxs returns (idxs, block_addr) so the standalone-Recr caller doesn't redo the rec_typ_to_inductive_addr + load_verified_constant chain to recover block_addr.
  • build_ref_idxs_and_blobs fuses the two separate walks (build_ref_idxs_mapped + build_lit_blobs) into one — single rbtree probe per ref produces both ref_idxs and lit_blobs. 5 caller sites simplified.

Pointer-passing (per Aiur's inputSize→width cost model):

  • convert_univ(u: &Univ): per-row input width drops from 5-variant union to one G column. Caller convert_univ_idxs already produces the &Univ via list_lookup(univs, idx) — no extra store.
  • convert_one(input: &ConvertInput): caller convert_all already has &input from the ListNode.Cons destructure.

Dead code deleted:address_in_list, apply_ctor_overrides, lookup_override, build_lit_blobs, build_ref_idxs_mapped, is_blob, ctx_convert_expr.

Shard 51 (blake3-stubbed): 102_190_540_489 → 93_939_967_063 — −8.07% on the non-blake3 portion on top of 120001d.

Per-const wins on heavy production targets (unstubbed lake test):

ConstantBeforeAfterΔ
Nat.add_comm54_369_74554_049_773−0.59%
Nat.sub_le_of_le_add515_331_420510_843_459−0.87%
Nat.decLe191_471_719189_723_325−0.91%
Array.append_assoc2_567_087_8932_537_360_311−1.16%
Vector.append2_639_286_0782_607_800_745−1.19%
IxVMInd.Even.rec31_659_49331_434_525−0.71%
IxVMInd.Odd.rec31_658_59831_433_622−0.71%
String.Internal.append718_803_075708_296_270−1.46%
_private...extractMainModule._unsafe_rec1_081_617_7051_064_689_765−1.57%

lake test -- --ignored ixvm green; 42 pins re-pinned.

3. 9c2eb23 — IxVM: per-kind IOBuffer channels with documented interface

The pre-existing IOBuffer multiplexed claim bytes, assumption tree bytes, constant wire bytes, and a per-blob-addr empty-marker on a single channel 0, distinguished only by blake3 content-hash collision-impossibility. A new contributor reading load_payload_const(key) couldn't tell from the helper whether they were getting a const, a claim, or a tree — only the caller knew the intent.

This commit splits channel 0 into per-kind channels and lands a documented IxVM IOBuffer interface block (mirrored on both Aiur and Lean sides).

TierChannelPurposeKeyValue
Ctrl0claim wire bytesblake3(claim_bytes)claim bytes
Ctrl1assumption tree bytestree.roottree bytes
Const2constant wire bytesconst addrconst bytes
Const3Defn reducibility hintDefn addrsingle G
Blob4blob discriminatoraddrone byte (1=const, 0=blob)
Blob5blob raw bytesblob addrraw bytes

Tier 1 fires once per verify_claim. Tier 2 fires per constant traversed during load_with_deps. Tier 3 fires per blob ref encountered during build_ref_idxs_and_blobs.

The blob discriminator (ch 4) replaces the previous io_get_info(0, addr).len == 0 ⟹ blob hack inside load_with_deps with an explicit per-addr one-byte payload. No more len-as-meaning overload.

Every per-channel io_get_info + #read_byte_stream pair is inlined at the single consumer of each channel — no fn-call row gets added for the reorganization. Channel reorganization is cost-neutral by design, confirmed by measurement: per-const pin shifts vs 9acbd58 land within ±0.010% (noise-tier; the ch-4 discriminator's one read_byte trades against the eliminated io_get_info(0, addr).len check).

The win here is review-time clarity, not FFT — io_get_info(channel, key) now has one value shape per channel and a doc block both kernel and harness reference.

Soundness: ch 0/1/2/5 byte streams are blake3-verified by the kernel against their content-addressed keys. ch 3 hint is semantically optional (controls WHNF reduction heuristic only; def-eq is sound either way). ch 4 discriminator is sound by erasure-correctness — a lying byte flips the const/blob decision and the wrong-path load downstream fails (a "const" blob triggers a ch 2 read that returns empty → blake3 verify against the non-empty addr fails; a "blob" const dangles references → typecheck fail).

Cumulative measurement (vs main)

ConstantBeforeAfterΔ
Nat.add_comm54_504_714 (approx.)54_047_004~−0.84%
Nat.decLe191_354_793189_722_204−0.85%
Nat.sub_le_of_le_add515_056_158510_870_092−0.81%
Array.append_assoc2_588_157_5382_537_478_644−1.96%
Vector.append2_661_244_8452_607_925_682−2.00%
String.Internal.append725_415_461708_332_683−2.36%
_private...extractMainModule._unsafe_rec1_091_354_0311_064_762_809−2.44%
Shard 51 (blake3-stubbed trace, non-blake3 portion)119_152_044_06293_939_967_063−21.1%

Test plan

  • lake build clean
  • lake test -- --ignored ixvm green at every commit
  • Shard 51 comparative measurement with blake3 stubs applied locally to both endpoints (stubs reverted; not part of any committed code in this PR)

`lookup_addr_pos`'s rbtree (`addr_pos_map`) only contained const
addresses. Every blob ref (literal-blob payload pointers in
`Constant.refs`) probed the map, missed, and fell through to the O(N)
linear scan over `all_addrs` which also returned 0. Wasted work
proportional to (blob refs × shard closure size).
Add a third value class to the same rbtree: the sentinel `4294967295`
(beyond any honest `pos+1`), inserted by walking each const's `refs`
once via `augment_with_blob_refs`. Any ref not already mapped to a
const-position gets the sentinel.
`lookup_addr_pos` becomes a 3-way match: `0` → fall back to linear
scan (now only fires under the de-intern soundness corner-case, ~never
in practice); `SENTINEL` → return 0 directly (known blob); `pos+1` →
return `pos`. `is_blob` mirrors the same match.
Soundness preserved: every probe still relies on the positive direction
`ptr_val` equality ⇒ content equality (Aiur Store's content-addressing
invariant). The linear fallback stays put for the (still theoretical)
malicious de-intern, where it uses content-based `address_eq`.
`lake exe ix check --ixe init.ixe --ixes init.ixes --shard 51`
(`ulimit -v 23000000`, with the kernel's blake3 paths in
`load_verified_constant` / `load_verified_blob` / `load_verified_claim`
stubbed for profiling):
119_152_044_062 → 102_190_540_489 FFT (-14.2%).
Tests/Ix/IxVM.lean: 42 pin shifts absorbing the per-ingress
augment-walk overhead.
A consolidated refactor of the ingress pipeline guided by a 5-reviewer
synthesis of accidental complexity sites. Three families of change:
PLUMBING
* `Blake3.lean`: `verify_bytes_against(bytes, expected)` + `bytes_to_addr(bytes)`
+ `blake3_flat(input)` centralise the 24-line `[h[i][j]]` digest reshape
that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
* `Ingress.lean`: `load_payload_const`/`blob`/`hint` + `ch_const`/`blob`/`hint`
factor the `io_get_info` + `#read_byte_stream` boilerplate at 6 sites.
`load_verified_blob` no longer double-`load(addr)`s.
* `sentinel_blob_ref()` named const replaces one bare `4294967295`.
SIDECAR LISTS → RBTREE
* `lookup_canon_addr`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_canon_addr_map`. `canon_addrs: List<Addr>` arg replaced
by `canon_addr_map: &RBTreeMap<Addr>` (one threaded ptr).
* `lookup_block_start`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_block_start_map`. `block_addrs` / `block_starts` args
replaced by `block_start_map: &RBTreeMap<G>` across ~12 fn signatures.
* `build_aux_recr_ctor_idxs` returns `(idxs, block_addr)` so the
standalone-Recr caller in `build_convert_inputs_walk` doesn't redo the
`rec_typ_to_inductive_addr` + `load_verified_constant` chain to recover
`block_addr` — `build_aux_recr_ctor_idxs` already computed it internally.
* `build_ref_idxs_and_blobs` fuses the two separate walks
(`build_ref_idxs_mapped` + `build_lit_blobs`) into one — single rbtree
probe per ref produces both `ref_idxs` and `lit_blobs`. 5 caller sites
simplified.
PTR-PASSING (per `reference_aiur_pass_pointer_not_value`)
* `convert_univ(u: &Univ)`: per-row input width drops from 5-variant
union to one G column. Caller `convert_univ_idxs` already produces the
`&Univ` via `list_lookup(univs, idx)` — no extra `store`.
* `convert_one(input: &ConvertInput)`: caller `convert_all` already has
`&input` from the `ListNode.Cons` destructure.
DEAD CODE DELETED
`address_in_list`, `apply_ctor_overrides`, `lookup_override`, `build_lit_blobs`,
`build_ref_idxs_mapped`, `is_blob`, `ctx_convert_expr` — all unused after
the migrations above.
TRIED + REVERTED (left as notes in code where instructive):
* Fused `build_addr_pos_map` + `augment_with_blob_refs` single-pass.
Lost to Aiur's per-row width tax — two narrower passes cheaper than
one merged wide pass.
* `convert_expr(ctx: &ConvertCtx)` ptr. Per-arm `load(ctx)` cost > raw-arg
threading.
* `compute_layout_walk` seen-mptrs/seen-poses → rbtree. Small regression
on typical shards (few enough Muts blocks that O(N) list scan is
already cheap; rbtree value column tax > savings).
* `block_members_map` to skip per-projection `load_verified_constant`.
Lost because Aiur memoises `load_verified_constant` automatically — the
new rbtree probe's width tax exceeded the savings from the eliminated
match cascade.
* `ctx` everywhere as `&ConvertCtx` (with `store(...)` at 5 construction
sites). The added `store` rows offset the per-row savings — wash.
`lake test -- --ignored ixvm`: 42 pin shifts, all measurable wins vs the
`ap/blob-ref-rbtree-augment` tip. Cumulative on heavy consts:
* `Nat.add_comm`: 54_369_745 → 54_049_773 FFT (-0.59%)
* `Nat.sub_le_of_le_add`: 515_331_420 → 510_843_459 FFT (-0.87%)
* `Array.append_assoc`: 2_567_087_893 → 2_537_360_311 FFT (-1.16%)
* `Vector.append`: 2_639_286_078 → 2_607_800_745 FFT (-1.19%)
* `String.Internal.append`: 718_803_075 → 708_296_270 FFT (-1.46%)
* `IxVMInd.Even.rec`: 31_659_493 → 31_434_525 FFT (-0.71%)
* `_private....extractMainModule._unsafe_rec`: 1_081_617_705 → 1_064_689_765 FFT (-1.57%)
Replaces channel 0's tagged-union (claim / tree / const bytes / empty-
blob-marker, distinguished only by key content-hash collision-
impossibility) with one channel per value kind. Channel reorganization
is cost-neutral — all six per-channel `io_get_info` + `#read_byte_stream`
pairs are inlined at the single consumer of each channel, so no fn-call
row gets added.
New layout, tiered by access pattern:
| Tier | Channel | Purpose | Key | Value |
|-------|---------|--------------------------|-------------------------|----------------------|
| Ctrl | 0 | claim wire bytes | `blake3(claim_bytes)` | claim bytes |
| Ctrl | 1 | assumption tree bytes | `tree.root` | tree bytes |
| Const | 2 | constant wire bytes | const addr | const bytes |
| Const | 3 | Defn reducibility hint | Defn addr | single G |
| Blob | 4 | blob discriminator | addr | one byte (1=const, 0=blob) |
| Blob | 5 | blob raw bytes | blob addr | raw bytes |
Tier 1 = one-per-`verify_claim`. Tier 2 = per-const. Tier 3 = per-blob.
`io_get_info(channel, key)` is now unambiguous by channel alone.
Aiur side (`Ingress.lean`, `Kernel/Claim.lean`):
* `load_verified_constant` inlines `io_get_info(2, raw)` +
`#read_byte_stream(2, ...)` (was ch 0).
* `load_verified_blob` inlines ch 5 read (was ch 1).
* `load_constant_hint` inlines ch 3 read (was ch 2).
* `load_with_deps` inlines a ch 4 discriminator probe replacing the
former `io_get_info(0, addr).len == 0 ⟹ blob` hack on ch 0. One
read_byte for the per-addr discriminator byte.
* `load_verified_claim` inlines ch 0 (was using a shared `load_payload_const`
helper on ch 0).
* `load_assumption_tree` inlines ch 1 (was using same `load_payload_const`
helper on ch 0).
* Helper wrappers (`load_payload_claim/tree/const/hint/blob`,
`load_discriminator`, `ch_*` constants) all removed — every channel
has exactly one consumer site and the inlined dispatch costs no fn
call.
* Top-of-file `IxVM IOBuffer interface` doc block documents the layout
+ soundness model.
Lean side (`ClaimHarness.lean`):
* `addEntries` writes per channel — consts → ch 2 + per-addr
discriminator `[1]` on ch 4; blobs → ch 5 + per-addr discriminator
`[0]` on ch 4; Defn hints → ch 3.
* `seedTreeAt` writes tree bytes to ch 1 (was ch 0).
* `buildClaimWitness` writes claim bytes to ch 0 (unchanged).
* Drops the empty-marker write on ch 0 for blob addrs — the ch 4
discriminator covers blob/const classification explicitly.
* Top-of-section `IxVM IOBuffer interface` doc block mirrors the
Aiur-side comment.
Soundness model unchanged. ch 0/1/2/5 byte streams are blake3-verified
by the kernel against their content-addressed keys. ch 3 hint is
semantically optional (controls WHNF reduction heuristic only; def-eq
is sound either way). ch 4 discriminator is sound by erasure-
correctness — a lying byte flips the const/blob decision and the
wrong-path load downstream fails (a "const" blob triggers a ch 2 read
that returns empty → blake3 verify against the non-empty addr fails;
a "blob" const dangles references → typecheck fail).
`lake test -- --ignored ixvm` green; 42 pins shifted within ±0.010%
(noise-tier) vs the pre-shuffle baseline — channel reorganization is
cost-neutral as required, the discriminator's one read_byte trades
against the eliminated `io_get_info(0, ...).len` check.
`blake3_flat` was a pure array-reshaping wrapper around `blake3` used
by `verify_bytes_against` and `bytes_to_addr`. Inline the reshape at
both call sites. `blake3`'s memoization key is the input ByteStream, so
the 74 cache hits that `blake3_flat` recorded migrate cleanly to
`blake3` — dedup preserved.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,898,064 → 12,430,154,083 (−743,981)
- Total width: 33,755 → 33,714 (−41)
- `blake3_flat` (41w × 1692h, 74 hits, 743,981 FFT) — removed
- `blake3` (92w × 1692h, 0 hits, ...) → now 74 hits
Same experiment for `bytes_to_addr` inline was neutral (−5k FFT, +92
width across 4 call sites) — kept as a wrapper.
@arthurpaulino
arthurpaulino marked this pull request as ready for review July 2, 2026 15:06
@arthurpaulino
arthurpaulino enabled auto-merge (squash) July 2, 2026 15:06
Mirror the Expr port pattern (`KExpr = &KExprNode`) for universe levels.
Kernel-side universe reps flip from unboxed enum values to pointer
aliases:
Before:
enum KLevel {
Zero,
Succ(&KLevel),
Max(&KLevel, &KLevel),
IMax(&KLevel, &KLevel),
Param(G)
}
After:
enum KLevelNode {
Zero,
Succ(KLevel),
Max(KLevel, KLevel),
IMax(KLevel, KLevel),
Param(G)
}
type KLevel = &KLevelNode
Callers switch from passing whole enum values to passing pointers.
Every match on a KLevel arg gains a `load()`; every value construction
that fills a KLevel arg or return slot gets a `store()`. `KExprNode.Srt`,
`KConstantInfo.{Ctor,Axiom,Defn,Thm,Opaque,Quot,Induct,Rec}` universe
fields all use the aliased `KLevel` now.
Motivation: pass-by-pointer shrinks per-row input width on every level-
manipulating function (`level_eq`, `level_leq`, `level_reduce`,
`level_inst_params`, ...), same trick as
`reference_aiur_pass_pointer_not_value`. Compared to the Expr port the
delta is smaller because level fns are already narrow, but it stacks.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,154,083 -> 12,418,344,860 (-11,809,223)
- Total width: 33,714 -> 33,532 (-182)
Re-pin every FFT cost shifted by the port.
@arthurpaulino
arthurpaulino merged commit 110f28f into mainJul 2, 2026
15 of 16 checks passed
@arthurpaulino
arthurpaulino deleted the ap/ingress-refactor branch July 2, 2026 19:04
johnchandlerburnham pushed a commit that referenced this pull request Jul 21, 2026
…505)
* IxVM: drop dead KValNode/KVal/KValEnv
From ap/kernel 828fb85 (Arthur Paulino): the NbE value domain is defined
but referenced nowhere — the live kernel runs on de-Bruijn KExpr;
vestigial from an abandoned NbE direction. (The closed-term context
normalization from that commit was measured separately and not taken:
the per-call expr_lbr probe cost +2.9% FFT on recursor loops for a 0.5%
record reduction.)
* IxVM: memoized prim_family dispatch + width-safe offset-stuck placement
Three coordinated changes to Const-head whnf dispatch (cherry-pick of
130f30b, adapted to post-#450/#457 main):
1. prim_family(addr) classifies a head address into the one reducer
family that could fire on it (nat/str/bitvec/native/decidable; the
sets are disjoint). Keyed on the ADDRESS ALONE it memoizes to one row
per distinct constant address per run, and whnf_const_head calls at
most one family reducer. The previous gauntlet ran every reducer in
sequence for guaranteed misses.
2. The symbolic-Nat offset-stuck check moves from a delta-arm probe into
try_nat_dispatch's miss path as a cold function
(try_nat_offset_dispatch, verdict 2 = "already stuck, do not
re-whnf"), with the offset construction shared via
mk_nat_offset_stuck (also used by the linear-rec collapse).
3. nat_lit_to_ctor_or_self exposes ONE constructor layer
(n -> succ(Lit(n-1))) instead of materializing the full succ chain.
Adaptations vs the original patch:
- whnf_nd_const_head (no-delta WHNF, added on main after the patch)
converted to the same family dispatch.
- Kept main's cold-extracted try_nat_binop_dispatch and routed the
symbolic-base case to try_nat_offset_dispatch from its miss arm.
Measured (lake exe ix check Nat.add_comm): total width 34820 -> 34806,
FFT cost 49571210 -> 48860647 (-1.43%). All 53 ixvm-suite FFT pins
decreased (-0.19%..-1.43%); parity and claim smokes pass. Pins updated;
crates/ixvm-codegen/src/aiur_ixvm.rs regenerated via `lake exe ix
codegen`.
* IxVM: port jcb/fixes H-14 — ptr_val skip map + lockstep addr cursor
Two quadratic/constant-factor fixes to check_all_skipping, ported from
jcb/fixes (3763356, John C. Burnham); cherry-pick of 2d83be1 adapted to
post-#457 main:
- The assumption-leaf skip set keys on ptr_val instead of the first 4
address bytes: one tree lookup per constant, no per-lookup address
load, no confirming address_eq. Sound by the build_addr_pos_map
interning invariant (one pointer, one content): a ptr hit implies the
address IS a leaf; a de-interned pointer reads as absent and the
constant just gets checked — fail-closed.
- The iterator walks the addrs list in LOCKSTEP with consts (cur_addrs
suffix) instead of list_lookup(addrs, pos) per constant, which
re-walked the prefix every iteration — a standalone O(closure^2).
Adaptations: kept main's two-arg check_canonical_block_sort call;
addr_key retained (the Inductive.lean block-membership table added on
main after this patch still uses it), comment updated.
Measured (lake exe ix check Nat.add_comm): total width 34806 -> 34781,
FFT cost unchanged (plain checks never take the skip path). Full ixvm
suite green incl. the frontier-assumption claim smoke; all FFT pins
unchanged. crates/ixvm-codegen/src/aiur_ixvm.rs regenerated.
---------
Co-authored-by: samuelburnham <45365069+samuelburnham@users.noreply.github.com>
Sign up for freeto 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

@arthurpaulino@gabriel-barrett
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface - #457

Merged
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor
Jul 2, 2026
Merged

IxVM ingress: blob-ref index, sidecar→rbtree, IOBuffer interface#457
arthurpaulino merged 5 commits into
mainfrom
ap/ingress-refactor

Conversation

@arthurpaulino

Copy link
Copy Markdown
Member

Three commits on top of main that cut ingress FFT cost on heavy shards and land a documented IxVM IOBuffer interface.

Measurement methodology — read first

Shard 51 with unstubbed blake3 currently OOMs at all practical ulimit settings on the dev machine. To get past memory exhaustion and produce a comparative measurement, the kernel's blake3 verification paths in load_verified_constant / load_verified_blob / load_verified_claim were stubbed to no-ops for profiling. The stubs are not part of any committed code in this PR — they were applied locally during measurement and reverted.

What this means for the numbers below:

  • The delta between two stubbed runs is real for the non-blake3 portion of the trace. Both endpoints elide the same verification work, so the comparison isolates the effect of the kernel changes in this PR.
  • The absolute stubbed cost (e.g. "93.94G FFT") is not the production shard-51 typecheck cost. Production includes blake3 work (~16–20% of the trace per earlier profiles) that the stubs omit.

Every shard-51 number below is a "blake3-stubbed trace" measurement. Per-const numbers from lake test -- --ignored ixvm (no stubs) are unaffected and reflect real production cost.

Headline

MetricBeforeAfterΔ
Shard 51 (blake3-stubbed trace)119_152_044_06293_939_967_063−21.1% on the non-blake3 portion
Vector.append (production, unstubbed)2_661_244_8452_607_925_682−2.00%
Array.append_assoc (production)2_588_157_5382_537_478_644−1.96%
_private...extractMainModule._unsafe_rec (production)1_091_354_0311_064_762_809−2.44%

Per-const wins are end-to-end on the real (unstubbed) kernel; the shard-51 delta is real on the non-blake3 portion of the trace.

Commits

1. 120001d — IxVM kernel: augment addr_pos_map with blob-ref sentinels

lookup_addr_pos's rbtree only contained const addresses. Every blob ref (literal-blob payload pointers in Constant.refs) probed the map, missed, and fell through to the O(N) linear scan over all_addrs which also returned 0. Wasted work proportional to (blob refs × shard closure size).

This commit adds a third value class to the same rbtree: a sentinel 4294967295 (beyond any honest pos+1), inserted by walking each const's refs once via augment_with_blob_refs. lookup_addr_pos becomes a 3-way match: 0 → fall back to linear scan (now only fires under the de-intern soundness corner-case, ~never in practice); SENTINEL → return 0 directly (known blob); pos+1 → return pos.

Shard 51 (blake3-stubbed): 119_152_044_062 → 102_190_540_489 — −14.2% on the non-blake3 portion.

address_eq row count drops from ~10.3M to ~3.4M (−67%); lookup_addr_pos_linear falls out of the top-25 cost table. The augment-walk's added rbtree probes (~430M FFT in the stubbed trace) are paid back many times over by the ~17B FFT saved on lookups.

Soundness: every probe still relies on the positive direction ptr_val equality ⇒ content equality (Aiur Store content-addressing invariant). The linear fallback stays for the (still theoretical) malicious de-intern, where it uses content-based address_eq.

2. 9acbd58 — IxVM ingress: plumbing helpers + sidecar→rbtree migrations + ptr-passing

A 5-reviewer synthesis of ingress accidental complexity drove this consolidated refactor. Three families of change.

Plumbing helpers:

  • verify_bytes_against(bytes, expected) + bytes_to_addr(bytes) + blake3_flat(input) centralise the 24-line [h[i][j]] digest reshape that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
  • Channel I/O helpers factor the io_get_info + #read_byte_stream boilerplate at 6 sites. load_verified_blob no longer double-load(addr)s.
  • sentinel_blob_ref() named const replaces one bare 4294967295.

Sidecar lists → rbtree:

  • lookup_canon_addr: O(N) parallel-list walk → O(log N) rbtree probe via new build_canon_addr_map. canon_addrs: List<Addr> arg replaced by canon_addr_map: &RBTreeMap<Addr> (one threaded ptr).
  • lookup_block_start: O(N) parallel-list walk → O(log N) rbtree probe via new build_block_start_map. block_addrs / block_starts args replaced by block_start_map: &RBTreeMap<G> across ~12 fn signatures.
  • build_aux_recr_ctor_idxs returns (idxs, block_addr) so the standalone-Recr caller doesn't redo the rec_typ_to_inductive_addr + load_verified_constant chain to recover block_addr.
  • build_ref_idxs_and_blobs fuses the two separate walks (build_ref_idxs_mapped + build_lit_blobs) into one — single rbtree probe per ref produces both ref_idxs and lit_blobs. 5 caller sites simplified.

Pointer-passing (per Aiur's inputSize→width cost model):

  • convert_univ(u: &Univ): per-row input width drops from 5-variant union to one G column. Caller convert_univ_idxs already produces the &Univ via list_lookup(univs, idx) — no extra store.
  • convert_one(input: &ConvertInput): caller convert_all already has &input from the ListNode.Cons destructure.

Dead code deleted:address_in_list, apply_ctor_overrides, lookup_override, build_lit_blobs, build_ref_idxs_mapped, is_blob, ctx_convert_expr.

Shard 51 (blake3-stubbed): 102_190_540_489 → 93_939_967_063 — −8.07% on the non-blake3 portion on top of 120001d.

Per-const wins on heavy production targets (unstubbed lake test):

ConstantBeforeAfterΔ
Nat.add_comm54_369_74554_049_773−0.59%
Nat.sub_le_of_le_add515_331_420510_843_459−0.87%
Nat.decLe191_471_719189_723_325−0.91%
Array.append_assoc2_567_087_8932_537_360_311−1.16%
Vector.append2_639_286_0782_607_800_745−1.19%
IxVMInd.Even.rec31_659_49331_434_525−0.71%
IxVMInd.Odd.rec31_658_59831_433_622−0.71%
String.Internal.append718_803_075708_296_270−1.46%
_private...extractMainModule._unsafe_rec1_081_617_7051_064_689_765−1.57%

lake test -- --ignored ixvm green; 42 pins re-pinned.

3. 9c2eb23 — IxVM: per-kind IOBuffer channels with documented interface

The pre-existing IOBuffer multiplexed claim bytes, assumption tree bytes, constant wire bytes, and a per-blob-addr empty-marker on a single channel 0, distinguished only by blake3 content-hash collision-impossibility. A new contributor reading load_payload_const(key) couldn't tell from the helper whether they were getting a const, a claim, or a tree — only the caller knew the intent.

This commit splits channel 0 into per-kind channels and lands a documented IxVM IOBuffer interface block (mirrored on both Aiur and Lean sides).

TierChannelPurposeKeyValue
Ctrl0claim wire bytesblake3(claim_bytes)claim bytes
Ctrl1assumption tree bytestree.roottree bytes
Const2constant wire bytesconst addrconst bytes
Const3Defn reducibility hintDefn addrsingle G
Blob4blob discriminatoraddrone byte (1=const, 0=blob)
Blob5blob raw bytesblob addrraw bytes

Tier 1 fires once per verify_claim. Tier 2 fires per constant traversed during load_with_deps. Tier 3 fires per blob ref encountered during build_ref_idxs_and_blobs.

The blob discriminator (ch 4) replaces the previous io_get_info(0, addr).len == 0 ⟹ blob hack inside load_with_deps with an explicit per-addr one-byte payload. No more len-as-meaning overload.

Every per-channel io_get_info + #read_byte_stream pair is inlined at the single consumer of each channel — no fn-call row gets added for the reorganization. Channel reorganization is cost-neutral by design, confirmed by measurement: per-const pin shifts vs 9acbd58 land within ±0.010% (noise-tier; the ch-4 discriminator's one read_byte trades against the eliminated io_get_info(0, addr).len check).

The win here is review-time clarity, not FFT — io_get_info(channel, key) now has one value shape per channel and a doc block both kernel and harness reference.

Soundness: ch 0/1/2/5 byte streams are blake3-verified by the kernel against their content-addressed keys. ch 3 hint is semantically optional (controls WHNF reduction heuristic only; def-eq is sound either way). ch 4 discriminator is sound by erasure-correctness — a lying byte flips the const/blob decision and the wrong-path load downstream fails (a "const" blob triggers a ch 2 read that returns empty → blake3 verify against the non-empty addr fails; a "blob" const dangles references → typecheck fail).

Cumulative measurement (vs main)

ConstantBeforeAfterΔ
Nat.add_comm54_504_714 (approx.)54_047_004~−0.84%
Nat.decLe191_354_793189_722_204−0.85%
Nat.sub_le_of_le_add515_056_158510_870_092−0.81%
Array.append_assoc2_588_157_5382_537_478_644−1.96%
Vector.append2_661_244_8452_607_925_682−2.00%
String.Internal.append725_415_461708_332_683−2.36%
_private...extractMainModule._unsafe_rec1_091_354_0311_064_762_809−2.44%
Shard 51 (blake3-stubbed trace, non-blake3 portion)119_152_044_06293_939_967_063−21.1%

Test plan

  • lake build clean
  • lake test -- --ignored ixvm green at every commit
  • Shard 51 comparative measurement with blake3 stubs applied locally to both endpoints (stubs reverted; not part of any committed code in this PR)

`lookup_addr_pos`'s rbtree (`addr_pos_map`) only contained const
addresses. Every blob ref (literal-blob payload pointers in
`Constant.refs`) probed the map, missed, and fell through to the O(N)
linear scan over `all_addrs` which also returned 0. Wasted work
proportional to (blob refs × shard closure size).
Add a third value class to the same rbtree: the sentinel `4294967295`
(beyond any honest `pos+1`), inserted by walking each const's `refs`
once via `augment_with_blob_refs`. Any ref not already mapped to a
const-position gets the sentinel.
`lookup_addr_pos` becomes a 3-way match: `0` → fall back to linear
scan (now only fires under the de-intern soundness corner-case, ~never
in practice); `SENTINEL` → return 0 directly (known blob); `pos+1` →
return `pos`. `is_blob` mirrors the same match.
Soundness preserved: every probe still relies on the positive direction
`ptr_val` equality ⇒ content equality (Aiur Store's content-addressing
invariant). The linear fallback stays put for the (still theoretical)
malicious de-intern, where it uses content-based `address_eq`.
`lake exe ix check --ixe init.ixe --ixes init.ixes --shard 51`
(`ulimit -v 23000000`, with the kernel's blake3 paths in
`load_verified_constant` / `load_verified_blob` / `load_verified_claim`
stubbed for profiling):
119_152_044_062 → 102_190_540_489 FFT (-14.2%).
Tests/Ix/IxVM.lean: 42 pin shifts absorbing the per-ingress
augment-walk overhead.
A consolidated refactor of the ingress pipeline guided by a 5-reviewer
synthesis of accidental complexity sites. Three families of change:
PLUMBING
* `Blake3.lean`: `verify_bytes_against(bytes, expected)` + `bytes_to_addr(bytes)`
+ `blake3_flat(input)` centralise the 24-line `[h[i][j]]` digest reshape
that was hand-unrolled at 7 sites. ~100 lines of boilerplate deleted.
* `Ingress.lean`: `load_payload_const`/`blob`/`hint` + `ch_const`/`blob`/`hint`
factor the `io_get_info` + `#read_byte_stream` boilerplate at 6 sites.
`load_verified_blob` no longer double-`load(addr)`s.
* `sentinel_blob_ref()` named const replaces one bare `4294967295`.
SIDECAR LISTS → RBTREE
* `lookup_canon_addr`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_canon_addr_map`. `canon_addrs: List<Addr>` arg replaced
by `canon_addr_map: &RBTreeMap<Addr>` (one threaded ptr).
* `lookup_block_start`: O(N) parallel-list walk → O(log N) rbtree probe
via new `build_block_start_map`. `block_addrs` / `block_starts` args
replaced by `block_start_map: &RBTreeMap<G>` across ~12 fn signatures.
* `build_aux_recr_ctor_idxs` returns `(idxs, block_addr)` so the
standalone-Recr caller in `build_convert_inputs_walk` doesn't redo the
`rec_typ_to_inductive_addr` + `load_verified_constant` chain to recover
`block_addr` — `build_aux_recr_ctor_idxs` already computed it internally.
* `build_ref_idxs_and_blobs` fuses the two separate walks
(`build_ref_idxs_mapped` + `build_lit_blobs`) into one — single rbtree
probe per ref produces both `ref_idxs` and `lit_blobs`. 5 caller sites
simplified.
PTR-PASSING (per `reference_aiur_pass_pointer_not_value`)
* `convert_univ(u: &Univ)`: per-row input width drops from 5-variant
union to one G column. Caller `convert_univ_idxs` already produces the
`&Univ` via `list_lookup(univs, idx)` — no extra `store`.
* `convert_one(input: &ConvertInput)`: caller `convert_all` already has
`&input` from the `ListNode.Cons` destructure.
DEAD CODE DELETED
`address_in_list`, `apply_ctor_overrides`, `lookup_override`, `build_lit_blobs`,
`build_ref_idxs_mapped`, `is_blob`, `ctx_convert_expr` — all unused after
the migrations above.
TRIED + REVERTED (left as notes in code where instructive):
* Fused `build_addr_pos_map` + `augment_with_blob_refs` single-pass.
Lost to Aiur's per-row width tax — two narrower passes cheaper than
one merged wide pass.
* `convert_expr(ctx: &ConvertCtx)` ptr. Per-arm `load(ctx)` cost > raw-arg
threading.
* `compute_layout_walk` seen-mptrs/seen-poses → rbtree. Small regression
on typical shards (few enough Muts blocks that O(N) list scan is
already cheap; rbtree value column tax > savings).
* `block_members_map` to skip per-projection `load_verified_constant`.
Lost because Aiur memoises `load_verified_constant` automatically — the
new rbtree probe's width tax exceeded the savings from the eliminated
match cascade.
* `ctx` everywhere as `&ConvertCtx` (with `store(...)` at 5 construction
sites). The added `store` rows offset the per-row savings — wash.
`lake test -- --ignored ixvm`: 42 pin shifts, all measurable wins vs the
`ap/blob-ref-rbtree-augment` tip. Cumulative on heavy consts:
* `Nat.add_comm`: 54_369_745 → 54_049_773 FFT (-0.59%)
* `Nat.sub_le_of_le_add`: 515_331_420 → 510_843_459 FFT (-0.87%)
* `Array.append_assoc`: 2_567_087_893 → 2_537_360_311 FFT (-1.16%)
* `Vector.append`: 2_639_286_078 → 2_607_800_745 FFT (-1.19%)
* `String.Internal.append`: 718_803_075 → 708_296_270 FFT (-1.46%)
* `IxVMInd.Even.rec`: 31_659_493 → 31_434_525 FFT (-0.71%)
* `_private....extractMainModule._unsafe_rec`: 1_081_617_705 → 1_064_689_765 FFT (-1.57%)
Replaces channel 0's tagged-union (claim / tree / const bytes / empty-
blob-marker, distinguished only by key content-hash collision-
impossibility) with one channel per value kind. Channel reorganization
is cost-neutral — all six per-channel `io_get_info` + `#read_byte_stream`
pairs are inlined at the single consumer of each channel, so no fn-call
row gets added.
New layout, tiered by access pattern:
| Tier | Channel | Purpose | Key | Value |
|-------|---------|--------------------------|-------------------------|----------------------|
| Ctrl | 0 | claim wire bytes | `blake3(claim_bytes)` | claim bytes |
| Ctrl | 1 | assumption tree bytes | `tree.root` | tree bytes |
| Const | 2 | constant wire bytes | const addr | const bytes |
| Const | 3 | Defn reducibility hint | Defn addr | single G |
| Blob | 4 | blob discriminator | addr | one byte (1=const, 0=blob) |
| Blob | 5 | blob raw bytes | blob addr | raw bytes |
Tier 1 = one-per-`verify_claim`. Tier 2 = per-const. Tier 3 = per-blob.
`io_get_info(channel, key)` is now unambiguous by channel alone.
Aiur side (`Ingress.lean`, `Kernel/Claim.lean`):
* `load_verified_constant` inlines `io_get_info(2, raw)` +
`#read_byte_stream(2, ...)` (was ch 0).
* `load_verified_blob` inlines ch 5 read (was ch 1).
* `load_constant_hint` inlines ch 3 read (was ch 2).
* `load_with_deps` inlines a ch 4 discriminator probe replacing the
former `io_get_info(0, addr).len == 0 ⟹ blob` hack on ch 0. One
read_byte for the per-addr discriminator byte.
* `load_verified_claim` inlines ch 0 (was using a shared `load_payload_const`
helper on ch 0).
* `load_assumption_tree` inlines ch 1 (was using same `load_payload_const`
helper on ch 0).
* Helper wrappers (`load_payload_claim/tree/const/hint/blob`,
`load_discriminator`, `ch_*` constants) all removed — every channel
has exactly one consumer site and the inlined dispatch costs no fn
call.
* Top-of-file `IxVM IOBuffer interface` doc block documents the layout
+ soundness model.
Lean side (`ClaimHarness.lean`):
* `addEntries` writes per channel — consts → ch 2 + per-addr
discriminator `[1]` on ch 4; blobs → ch 5 + per-addr discriminator
`[0]` on ch 4; Defn hints → ch 3.
* `seedTreeAt` writes tree bytes to ch 1 (was ch 0).
* `buildClaimWitness` writes claim bytes to ch 0 (unchanged).
* Drops the empty-marker write on ch 0 for blob addrs — the ch 4
discriminator covers blob/const classification explicitly.
* Top-of-section `IxVM IOBuffer interface` doc block mirrors the
Aiur-side comment.
Soundness model unchanged. ch 0/1/2/5 byte streams are blake3-verified
by the kernel against their content-addressed keys. ch 3 hint is
semantically optional (controls WHNF reduction heuristic only; def-eq
is sound either way). ch 4 discriminator is sound by erasure-
correctness — a lying byte flips the const/blob decision and the
wrong-path load downstream fails (a "const" blob triggers a ch 2 read
that returns empty → blake3 verify against the non-empty addr fails;
a "blob" const dangles references → typecheck fail).
`lake test -- --ignored ixvm` green; 42 pins shifted within ±0.010%
(noise-tier) vs the pre-shuffle baseline — channel reorganization is
cost-neutral as required, the discriminator's one read_byte trades
against the eliminated `io_get_info(0, ...).len` check.
`blake3_flat` was a pure array-reshaping wrapper around `blake3` used
by `verify_bytes_against` and `bytes_to_addr`. Inline the reshape at
both call sites. `blake3`'s memoization key is the input ByteStream, so
the 74 cache hits that `blake3_flat` recorded migrate cleanly to
`blake3` — dedup preserved.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,898,064 → 12,430,154,083 (−743,981)
- Total width: 33,755 → 33,714 (−41)
- `blake3_flat` (41w × 1692h, 74 hits, 743,981 FFT) — removed
- `blake3` (92w × 1692h, 0 hits, ...) → now 74 hits
Same experiment for `bytes_to_addr` inline was neutral (−5k FFT, +92
width across 4 call sites) — kept as a wrapper.
@arthurpaulino
arthurpaulino marked this pull request as ready for review July 2, 2026 15:06
@arthurpaulino
arthurpaulino enabled auto-merge (squash) July 2, 2026 15:06
Mirror the Expr port pattern (`KExpr = &KExprNode`) for universe levels.
Kernel-side universe reps flip from unboxed enum values to pointer
aliases:
Before:
enum KLevel {
Zero,
Succ(&KLevel),
Max(&KLevel, &KLevel),
IMax(&KLevel, &KLevel),
Param(G)
}
After:
enum KLevelNode {
Zero,
Succ(KLevel),
Max(KLevel, KLevel),
IMax(KLevel, KLevel),
Param(G)
}
type KLevel = &KLevelNode
Callers switch from passing whole enum values to passing pointers.
Every match on a KLevel arg gains a `load()`; every value construction
that fills a KLevel arg or return slot gets a `store()`. `KExprNode.Srt`,
`KConstantInfo.{Ctor,Axiom,Defn,Thm,Opaque,Quot,Induct,Rec}` universe
fields all use the aliased `KLevel` now.
Motivation: pass-by-pointer shrinks per-row input width on every level-
manipulating function (`level_eq`, `level_leq`, `level_reduce`,
`level_inst_params`, ...), same trick as
`reference_aiur_pass_pointer_not_value`. Compared to the Expr port the
delta is smaller because level fns are already narrow, but it stacks.
Measured with `lake exe ix check Std.Time.Week.Offset.ofMilliseconds`:
- Total FFT cost: 12,430,154,083 -> 12,418,344,860 (-11,809,223)
- Total width: 33,714 -> 33,532 (-182)
Re-pin every FFT cost shifted by the port.
@arthurpaulino
arthurpaulino merged commit 110f28f into mainJul 2, 2026
15 of 16 checks passed
@arthurpaulino
arthurpaulino deleted the ap/ingress-refactor branch July 2, 2026 19:04
johnchandlerburnham pushed a commit that referenced this pull request Jul 21, 2026
…505)
* IxVM: drop dead KValNode/KVal/KValEnv
From ap/kernel 828fb85 (Arthur Paulino): the NbE value domain is defined
but referenced nowhere — the live kernel runs on de-Bruijn KExpr;
vestigial from an abandoned NbE direction. (The closed-term context
normalization from that commit was measured separately and not taken:
the per-call expr_lbr probe cost +2.9% FFT on recursor loops for a 0.5%
record reduction.)
* IxVM: memoized prim_family dispatch + width-safe offset-stuck placement
Three coordinated changes to Const-head whnf dispatch (cherry-pick of
130f30b, adapted to post-#450/#457 main):
1. prim_family(addr) classifies a head address into the one reducer
family that could fire on it (nat/str/bitvec/native/decidable; the
sets are disjoint). Keyed on the ADDRESS ALONE it memoizes to one row
per distinct constant address per run, and whnf_const_head calls at
most one family reducer. The previous gauntlet ran every reducer in
sequence for guaranteed misses.
2. The symbolic-Nat offset-stuck check moves from a delta-arm probe into
try_nat_dispatch's miss path as a cold function
(try_nat_offset_dispatch, verdict 2 = "already stuck, do not
re-whnf"), with the offset construction shared via
mk_nat_offset_stuck (also used by the linear-rec collapse).
3. nat_lit_to_ctor_or_self exposes ONE constructor layer
(n -> succ(Lit(n-1))) instead of materializing the full succ chain.
Adaptations vs the original patch:
- whnf_nd_const_head (no-delta WHNF, added on main after the patch)
converted to the same family dispatch.
- Kept main's cold-extracted try_nat_binop_dispatch and routed the
symbolic-base case to try_nat_offset_dispatch from its miss arm.
Measured (lake exe ix check Nat.add_comm): total width 34820 -> 34806,
FFT cost 49571210 -> 48860647 (-1.43%). All 53 ixvm-suite FFT pins
decreased (-0.19%..-1.43%); parity and claim smokes pass. Pins updated;
crates/ixvm-codegen/src/aiur_ixvm.rs regenerated via `lake exe ix
codegen`.
* IxVM: port jcb/fixes H-14 — ptr_val skip map + lockstep addr cursor
Two quadratic/constant-factor fixes to check_all_skipping, ported from
jcb/fixes (3763356, John C. Burnham); cherry-pick of 2d83be1 adapted to
post-#457 main:
- The assumption-leaf skip set keys on ptr_val instead of the first 4
address bytes: one tree lookup per constant, no per-lookup address
load, no confirming address_eq. Sound by the build_addr_pos_map
interning invariant (one pointer, one content): a ptr hit implies the
address IS a leaf; a de-interned pointer reads as absent and the
constant just gets checked — fail-closed.
- The iterator walks the addrs list in LOCKSTEP with consts (cur_addrs
suffix) instead of list_lookup(addrs, pos) per constant, which
re-walked the prefix every iteration — a standalone O(closure^2).
Adaptations: kept main's two-arg check_canonical_block_sort call;
addr_key retained (the Inductive.lean block-membership table added on
main after this patch still uses it), comment updated.
Measured (lake exe ix check Nat.add_comm): total width 34806 -> 34781,
FFT cost unchanged (plain checks never take the skip path). Full ixvm
suite green incl. the frontier-assumption claim smoke; all FFT pins
unchanged. crates/ixvm-codegen/src/aiur_ixvm.rs regenerated.
---------
Co-authored-by: samuelburnham <45365069+samuelburnham@users.noreply.github.com>
Sign up for freeto 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

@arthurpaulino@gabriel-barrett