Skip to content

The full SyscallInstrsChip, with full semantics - #122

Open
dtumad wants to merge 121 commits into
dtumad/semantics-gap-2-3-time-unificationfrom
dtumad/syscall-instrs-chip
Open

The full SyscallInstrsChip, with full semantics#122
dtumad wants to merge 121 commits into
dtumad/semantics-gap-2-3-time-unificationfrom
dtumad/syscall-instrs-chip

Conversation

@dtumad

@dtumad dtumad commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #121.

The native ensemble stood in for SP1's SyscallInstrsChip with HaltChip: a one-row table
covering one arm of thirteen, and covering it with two restrictions SP1 does not make. This PR
builds the whole chip with the whole semantics — every arm SP1 dispatches inline, constrained as
upstream constrains it and given a real meaning in the execution model.

121 commits, 105 files, +13,549 / −772, 18 new modules.

What lands

The chip. SyscallInstrsChip with all thirteen inline arms, its RowContract/Spec, and the
whole-chip faithfulness anchor syscallInstrsChip_faithful comparing the complete assertion system
and interaction multiset against the extracted Rust row. The table is registered at ensemble
position 54; the ensemble grows to seven channels.

The semantics, in six layers:

  • an event-indexed foundation (eventTrajectory, eventTimeline) whose clock is the transcript's
    own prefix-summed durations rather than 8 * steps;
  • the truth layer and the grounding walk restated over an arbitrary trajectory, with the Sail
    engine recovered as an instantiation — no consumer moved, which was the acceptance test;
  • the row→event bridge, thirteen per-arm theorems, so a missing arm is a missing theorem;
  • the grounding engine generalised off eight ticks;
  • the State trail's third arm;
  • the event-chain capstone.

The mixed-row walk. The row-generic, trajectory-consuming execution engine, the walked-row
carrier, and all five of walkE's per-row obligations — stepAt, frameAt, rowOKAt, widthAt,
pullAt — proved from the witness for both arms. The trail now projects onto walked rows with
no premise about the syscall table at all.

What this PR does not claim

Two things stated plainly rather than left to be discovered:

  • It adds an interim third conjunct to the headline relation. SupportedCoreNativeRelation
    gains SyscallTableInactive: noActiveRows restricts the certified set to shards whose syscall
    table is inactive, and haltTablePresent keeps HaltChip the sole Exit contributor. The chip is
    proved and registered, but the grounding engine does not yet walk a syscall row's three register
    touches. Shards with an active syscall row are outside the certified set today, and the relation
    says so. AGENTS.md, docs/verification-report.md, docs/audit-surface.md and
    docs/release-audit.md are updated to disclose it — they previously asserted the relation had
    exactly two conjuncts, and verification-report.md said flatly "There is no third conjunct."
  • The Memory balance has both ends built but is not instantiated.
    walkedTouchBalance takes the witness's balance into the refresh elimination's input shape and
    walkedRewrittenMemoryBalance reads its output back out; chaining them at the witness's concrete
    MemoryBump refresh pairs is the remaining step.

SupportedCoreEventRelation exists and is not yet produced by any soundness theorem; the capstone's
conclusion switch is future work.

One census defect fixed on the way

Two axiom-census probes had gone silent. The census names targets in alternation regexes ending
\b, and \b does not match a _core suffix — so renaming sp1ProviderTables_channels_subset to
…_core dropped its probe with no gate objecting. That is the failure mode
gen_axiom_probe.py's own comment warns about. Both _core spellings are named explicitly now
(which also picks up sp1AllTables_channels_subset_core, never probed), the dead
routeOf_reaches_sail is dropped, and the ledger records why the probe's self-check is one-sided:
a wrong name fails to elaborate, but a name that stops matching is simply not emitted.

Verification

  • lake build SP1Clean — 3802 jobs, 0 errors / 0 warnings / no stray info:
  • lake test — 3817 jobs, clean
  • scripts/run_audit.sh --main-onlyAUDIT PASS, all thirteen gates: no sorryAx, no
    compiler-trusted proof constant, no native_decide in the main library, no skipKernelTC, no
    axiom declarations, every elaboration-budget site allowlisted, census matches its snapshot
  • doc gates — check_current_docs, check_release_surface, check_audit_surface,
    check_report_citations, check_root_index, check_layering all PASS
  • zero sorry under SP1Clean/; no set_option maxHeartbeats/maxRecDepth added

🤖 Generated with Claude Code

https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d

dtumad and others added 30 commits September 1, 2026 11:05
S1 of the syscall-chip plan: definitions only, no ensemble membership, so
nothing ripples yet.

syscallChannel is SP1's own bus and reuses the existing SyscallMsg, the
carrier the exact v6.4.0 lists already project -- so both models now name
this bus with one type. Its payload shape came from the extracted send, which
corrected my reading of it: values[0] is clk_high, not a shard index.

publicValuesChannel is the second native-only bus. SP1 reads public_values at
three differently-shaped places; one addressed cell covers all of them without
a variant type, and every message is built from columns a row already has, so
a chip carrying them keeps its upstream column count.

Both are declared without a provider on purpose: balance then forces the
syscall id's table byte and the commit selectors to zero, which is how the
profile carve-outs become theorems instead of assumptions.

Also records the hazard that blocks S4: both names currently fall into
kindOf's else .State bucket, which is inert only while they stay out of the
ensemble, and would otherwise corrupt the State clock telescope silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
S2a: the Inputs struct and the Spec's skeleton plus HALT arm.

The 65 fields are laid out in upstream column order, so the faithfulness
codec will be a re-grouping rather than a permutation, and inputs_size pins
the count by rfl -- which is what checks the column map I read off the
oracle's expression graph against the actual ProvableStruct sizes of
CPUState, RegisterAccessCols, Word, and the three gadget blocks.

ExitCodeValid is the constraint HaltChip omits: SP1 pins a0's upper two
limbs and bounds limb 1 against 0x7F00, the second limb of KoalaBear's
modulus. It is stated structurally so the contract stays field-generic; that
the reduction is therefore below the modulus, and so decodes injectively,
needs the field's actual size and belongs where it is used.

The four non-HALT arms constrain columns this commit leaves unconstrained.
That makes the row weaker than upstream, not unsound, and the anchor is what
will show when it is complete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Reading the row's interaction list rather than only its constraints shows
values[13] sits in the Program message's op_a_0 slot -- the x0 flag, which is
what E27/E29/E31/E33 use to force a written zero -- not a gate for op_a
keeping its value. Also records that t0 is genuinely written: its read-prior
carries the syscall id and its read-back carries op_a_value, where a0 and a1
carry the same word on both sides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
main now covers every one of the 74 scalar asserts and 18 interactions of
Extracted/SystemOracle/SyscallInstrs.lean, plus its eight gadget sub-lists,
composed through gadgets that already existed natively: the identifier byte
split, five IsZeroOperation arm selectors, and two U16CompareOperation
valid-field-element bounds.

Three things the interaction list settles that the constraints alone did not.
t0's memory pair is a write -- read-prior carries the identifier, read-back
carries op_a_value -- where a0 and a1 carry the same word both ways. The
Program message takes its operands from columns rather than literals, with
op_a_0 in the flag slot. And E55 asserts is_real * op_a_0, so a syscall row
never targets x0, which makes the x0 zeroing vacuous on real rows though
still asserted.

The five public-value conjuncts become one message each, built only from
columns the row already has, so the width stays 65: the exit binding on
exitChannel, the two digests and two commit flags on publicValuesChannel.

No padding push on the exit bus. HaltChip has one because its table is
exactly one row, which is what balanced the verifier's ungated pull; a
syscall table has many, so that accounting is redesigned at ensemble wiring
and this row emits only the faithful is_halt-gated push.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
The 65-column row does elaborate, and the cost lands where the plan
predicted. localLength_eq's default tactic times out at whnf on a do-block
with sixty assertions and eight subcircuits, so the field is supplied
explicitly -- the one place a hand-written ElaboratedCircuit field is
warranted rather than a missing circuit_norm lemma. Everything else closes
from circuit_norm alone, including all sixteen channel-membership goals.

Seven buses are declared: Byte arrives through the readers and the three
gadget families, State through CPUState, and Program, Memory, Exit, Syscall
and PublicValues directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Re-reading the oracle's interaction list to its end rather than its first
eighteen entries turned up six more. Four are SP1's slice_range_check_u16 on
op_a_value, and they matter: t0 is written, and on the HINT_LEN arm no
constraint determines the written word, so without them the read-back push
could not discharge the Memory bus's isU64 requirement. With them it can,
and no profile restriction is needed. The other two are the u8-pair checks on
the cached digest word, gated on is_commit.

Found while working the soundness tail, which is what forced the question of
where op_a_value's range comes from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Assumptions is True: every fact the Spec reports is either asserted by the
row or received as a pulled guarantee. Axioms are the standard three.

The halt conjunct is the substantive part. ExitCodeValid is a disjunction
because SP1's bound is one -- the compare gadget decides whether a0's limb 1
is below 0x7F00, and where it is not, the two conditionals force limb 1 to
equal the bound and limb 0 to vanish. Both branches keep the word's reduction
under the modulus, which is what makes the committed exit code decodable.

Two things the requirements tail forced. op_a_value's isU64 comes from the
four byte range pulls, read out through byteRowSpec_range -- the reason those
interactions had to be found first. And next_pc's Spec conjunct is now three
component equations rather than a Vector equality, which avoids an
extensionality detour and is what consumers want anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
RowContract is the completeness precondition and the row's arm-by-arm
contract: gates, reader blocks, arm selection, and one predicate per arm
family, plus the facts the Program and Memory pulls carry. Spec stays the
narrower soundness conclusion, and rowContract_toSpec records that the one
follows from the other -- Clean keeps those two fields apart precisely
because completeness must reconstruct constraints no consumer reads.

Every arm SP1 dispatches inline has a conjunct here, so a missing arm is a
missing conjunct rather than a silent gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
SP1 has two syscall types and I picked the wrong name. SyscallChip
(syscall/chip.rs) is named SyscallCore or SyscallPrecompile and is the handler
table on the far side of the syscall bus -- the one the supported profile
excludes, and whose absence is what makes the unprovisioned channel force the
carve-out. SyscallInstrsChip (syscall/instructions/) is the ECALL instruction
row we actually model, named SyscallInstrs.

So SP1Clean.SyscallChip named our row after the table it deliberately does not
model. Renamed throughout, and the module docstring now records the
distinction so it does not come back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
All 106 completeness obligations discharged, axiom-clean. RowContract gains
the facts a prover must actually supply -- the selector sums, the one-hot
sums, and the Program and Memory pulls' well-formedness.

Two things cost me time and are worth recording. Ordered bullets, not
first-chains: a chain costs goals times alternatives elaborations and blew
the heartbeat budget, while positional bullets cost one each. And
linear_combination is syntactic on atoms, so a Fin-indexed value and its
Nat-indexed twin are different atoms even when defeq -- sub_eq_zero_of_eq or
a norm_num pass bridges them where ring cannot.

The five booleanity gates are reordered so is_commit and is_commit_deferred
sit at positions 2 and 3; the anchor compares lists up to permutation, so
ordering is free here and shallower projections are worth having.

The bundled circuit is not assembled: requirementsChannelsLawful needs a
constraint past the first, and reaching one means normalising the whole
sixty-assertion do-block. The file records the structural fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
The first of the row's arms to become its own FormalAssertion, establishing
the pattern: Inputs and Spec beside the chip's contract, main and elaborated
in Native/.../Arms.lean, soundness and completeness in Proofs/.../Arms.lean.

The point is that each proof sees only its own arm's constraints. PcArm's
soundness and completeness are a dozen lines each and elaborate without
strain, where the same six constraints inside the flat row could only be
reached by normalising sixty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Twenty-three of the row's sixty-one inline constraints -- the one-hot digest
bitmap, its index rule, the two sum rules, and a1's packing -- now sit behind
one FormalAssertion with a six-conjunct Spec.

Both directions went through without strain, which is the evidence the
decomposition is doing its job: the same twenty-three constraints inside the
flat row could not be reached at all without normalising the whole block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Thirteen more constraints behind one FormalAssertion: the x0 rule, the
ENTER_UNCONSTRAINED zeroing, and the rule that every other arm leaves t0
alone. HINT_LEN is the gap the Spec names explicitly -- no arm determines the
written word there, which is why SP1 range-checks it separately.

Three of six arms extracted; 42 of the row's 61 inline constraints now sit
behind bundles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
The last two of the five. FieldBoundArm is the one worth having twice: the
same valid-field-element check bounds a0 for HALT and a1 for
COMMIT_DEFERRED_PROOFS, so it is parameterised by word, bit and gate rather
than written out at both sites.

All five bundles are axiom-clean, and 61 of the row's inline constraints are
now behind six proof boundaries instead of one flat block.

Two shapes that cost me time and are worth knowing. A FormalAssertion's
soundness goal is Spec paired with the composed sub-assertion's requirement
disjunct, not Spec alone -- the pair is refine ⟨⟨...⟩, Or.inl rfl⟩, and
guessing at it produces type errors that look like projection failures. And
a hypothesis stated over a reconstructed record needs its projections
reduced before rw will match, though exact still accepts it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
main now composes WriteArm, PcArm, DispatchArm, FieldBoundArm twice, and
CommitArm instead of carrying their sixty-odd assertions inline, and the
chip's Spec is assembled from the arm Specs rather than restating them --
so the row and its decomposition cannot drift.

Chip soundness is closed and axiom-clean, and it is now mostly forwarding:
supply each arm its Assumptions, get its Spec back. What it still does for
itself is the part that genuinely belongs to the row -- deriving that two
selectors cannot both fire, since one identifier cannot equal two codes,
which is what gives CommitArm and WriteArm their mutual-exclusion premises.

One honest consequence is recorded in the Spec: op_a_0's booleanity reaches
the row over the Program bus, which is gated, while SP1 asserts the x0 rule
ungated. So the write arm's meaning is reported under exactly that
hypothesis rather than pretended unconditional.

Completeness and the bundled circuit are next; the arms make both tractable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Chip completeness is closed and axiom-clean. Before the arms were extracted
it could not be finished at all -- the closers blew the heartbeat budget --
so the decomposition did the work it was meant to.

Most of it is now forwarding: RowContract carries each arm's Assumptions and
Spec, and the chip hands them straight over. What it still does itself is
the row's own business -- the pull guarantees, the byte range checks, and the
IsZero selector semantics.

Two contract corrections the proof forced. The written t0 word's isU64 is a
chip-level prover fact, not a WriteArm one: the byte pulls that establish it
stayed on the row when the arm was split out, and on the HINT_LEN arm nothing
else determines that word. And the two U16Compare Specs left SelectorsValid,
since FieldBoundArm now owns them.

The bundled circuit is still blocked, on requirementsChannelsLawful alone.
The file records what was tried and what to try next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
The blocker was never the size of main. RequirementsChannelsLawful asks a
structural question -- which channels do these operations touch -- and I was
answering it with circuit_norm, which normalises constraint content and so
unfolds every composed circuit's semantics. Measured: the structural simp set
does the same first component in 2 seconds against a heartbeat timeout.

DivRemChip is the worked precedent and the reason the largest chip in the
repo bundles where this one did not: it rewrites with Clean's monadic-append
and per-leaf rfl-lemmas and never mentions circuit_norm. The lever is
shallowInteractions_subcircuit -- a composed subcircuit contributes nothing
shallow -- so the arms, readers and gadgets drop out for free.

subcircuitRequirements_eq lands the first component. The second goes through
the same way. The third has two open cases of twenty-two, where
off_gate_vacuous does not match after the targeted eval rewrite; the file
records that as the remaining gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
The row's three obligations were already proved; what stood between them and the
bundle was `RequirementsChannelsLawful`, whose three components are questions about
the block's *metadata*, not its meaning. Answering them through `circuit_norm`
normalises all seventy-four constraints to decide which channels a list mentions,
and exceeds the elaboration budget doing so. Answering them through the monadic-append
and per-leaf `rfl`-lemmas takes two seconds. `DivRemChip` uses the same shape.

The twenty-two shallow interactions then split two ways: those on a channel the row
guarantees, and the gated pulls whose off-gate `Requirements` is vacuous. The latter
arrive in a form `off_gate_vacuous` does not take — Clean states *both* hypotheses
about the interaction's own multiplicity, so a pull hands over `-x ≠ -1` and `-x ≠ 0`,
not the mixed pair. `off_gate_vacuous_neg` is that form, and saves a rewrite at each
of the two dozen call sites on a row this wide.

`circuit` is axiom-clean: [propext, Classical.choice, Quot.sound].

This completes S2. The chip is not yet in the ensemble and has no faithfulness
anchor — S3 and S4 — so the HaltChip disclosures in the audit and report stand
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
One added line: the chip's `circuit` at [propext, Classical.choice, Quot.sound].

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
921 main / 998 released.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
S3's foundation. The sixty-five-cell codec between the native `Inputs` row and
the extracted flat vector, both round-trips, the oracle, the physical row, and
the structural split of the row's complete `assertZero` list into thirteen
shallow gates plus sixteen composed blocks.

Two things are worth recording, because both cost time:

The cell correspondence is handled once, not sixty-five times.
`syscallInstrsReconfigure_eval` says reconfiguring an evaluated row is
evaluating it cell by cell, so every later statement is phrased over
`syscallInstrsRustColumns` and the Rust oracle's `cols.values[k]` accesses
reduce definitionally to native expressions. The alternative — a sixty-five
conjunct field-equation lemma, as `MemoryBumpChip` uses for fifteen — does not
scale to this width.

`circuit_norm` cannot prove the decomposition: it normalises the content of
seventy-odd constraints to answer a question about their arrangement, and
exceeds the elaboration budget doing so. The structural `Operations.constraints_*`
lemmas answer it in five seconds. The subtlety is that the generic
`Circuit.bind_def` walk descends into the sub-`main`s too, so the blocks are
stated through `X.circuit.main` — folded — rather than `X.main`. That in turn
needs each subcircuit's `localLength` to reduce, which is why
`U16toU8OperationSafe` gains the `circuit_localLength` rfl-lemma every other
operation already had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
One lemma per composed circuit, each over an opaque input of that circuit's own
row type. That is the whole trick: `circuit_norm` applied to the sixty-five
column row at once times out, applied to one arm at a time it is instant. The
five arms, the byte split, and the two reader families are now each pinned to an
explicit evaluated list.

Three of the blocks assert only their own `is_real` gate — the byte split,
`CPUState`, and `RegisterAccessCols`. With the chip's own gate and the two
remaining reader instances that is six copies, which is exactly how many times
SP1's dump repeats `values[64] * (values[64] - 1)`. The correspondence is exact,
not absorbed.

`FieldBoundArm` is the one block with a subcircuit of its own; its `U16Compare`
fragment folds to the extracted Rust list through the shared
`u16compare_assertions_exact`. The bridge has to be stated at `circuit.main`
rather than `main` — they are defeq, but simp matches syntactically.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
SP1's whole-table assertion list holds exactly when the native row's complete
`assertZero` list does, together with the eight public-value conjuncts.

The two sides are the same eighty-one propositions and the correspondence is a
bijection, checked as one: seven composed sub-operation blocks matching block for
block, sixty-six scalars matching one for one, and the eight of
`PublicValueBinding`. Nothing is absorbed and nothing is dropped — in particular
the six copies of the `is_real` boolean gate SP1's dump emits are matched by six
distinct native sources (the chip's own gate, the byte split, `CPUState`, and the
three register readers), not collapsed by `and_self`.

Only two conjuncts are not a direct match, and they are the interesting ones. SP1
compares the *selected public-value digest* against `a1`; the native row compares
its *cached* `digest_word`. The two are interderivable from the digest-byte
bindings without dividing by the commit selector, so each direction is one
`linear_combination` — no case split on whether the row is a commit.

The KoalaBear literals stay confined to `PublicValueBinding`: the native half of
the anchor is field-generic and literal-free, which is the shape external report
Finding 7 asks for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
…call key

The interaction counterpart of the assertion decomposition, proved the same
structural way: the row's complete emitted interaction list split into its
sixteen composed blocks and its own three groups.

The syscall channel is renamed to SP1's own projection key. `Interaction.toAccess`
sends the generated `.raw .syscall` send to `"SP1Raw/syscall"`; with the native
channel named anything else the two project to different `LookupAccess` keys, and
those keys are exactly what a whole-chip anchor compares. Both sides now also
share the `kindOf` fallback classification, which is the same landmine on both
sides rather than a divergence — and is fixed for both at once when
`InteractionKind` gains a `Syscall` constructor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Every composed block's emitted interactions, each over an opaque input of its own
row type. Four of the sixteen emit nothing — `IsZeroOperation` and three of the
five arms are pure `assertZero` blocks — and `FieldBoundArm`'s traffic is entirely
its `U16Compare` fragment's.

One thing worth recording: `Gadgets.Equality.circuit` is a `FormalAssertion`, not
a `FormalCircuit`. Reaching for `FormalCircuit.toSubcircuit_interactions` leaves
the subcircuit unfolded with no error message that says why, and the shape it
leaves behind looks like a missing `rfl` rather than a wrong lemma.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
dtumad and others added 30 commits September 5, 2026 17:58
Stage 1 sub-stage 7. `memoryBalance_of_alignsWith` carries the
`SyscallInstrs` table's produced and consumed Memory messages as a side term
on both sides, exactly as it carries the Halt table's. That is the right
shape for a table whose rows are extracted *after* the walk. It is the wrong
shape for a table whose rows are *walked*: the walk already accounts for a
row's touches through `pushesAt`/`pullsAt`, so carrying them beside it would
double-count.

`syscall_{pushesAt,pullsAt}_eq` are the absorption — the side term *is* the
walked term, so the summand can be dropped from the balance's conclusion
rather than tracked. `memoryFrontierBalance` keeps its syscall summand; it is
the raw ledger form and correct there.

Two supporting pieces. `pushesAt_map_eq_filter_flatMap` and its pulled twin
bridge the walk's per-row view to the ledger's flat view — that identity is
what makes absorption expressible at all. And `syscallRows_{pushesAt,pullsAt}`
state it over *abstract* rows: done directly at the witness's concrete
65-column decode, the same rewrite exceeds the depth budget, which is the
crossing rule biting for the third time this campaign.

`lake build SP1Clean`: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Sub-stage 7's payoff. `memoryBalance_of_alignsWith` carries four summands
beside the instruction rows' touches; `walkedMemoryBalance` restates it over
the combined instruction+syscall carrier with the syscall one **gone**,
because those rows are now walked rather than extracted beside the walk.

`splitPerm` is the only new obligation, and it is the honest statement of
what "the trail is an arbitrary interleaving" means: the walked carrier's
facts are, up to order, the instruction rows' aligned facts together with the
syscall rows' facts. `pushesAt`/`pullsAt` are multiset sums, so the
interleaving itself never has to be reconstructed — `pushesAt_perm` plus the
new `pushesAt_append` do the whole job.

One correction landed with it: `WalkedRow.facts` now takes the instruction
carrier as a parameter instead of fixing `ordinaryRowFacts`. The engine does
not walk the ordinary carrier — it walks the *aligned* one, related to the
ordinary form by `AlignsWith` — so the fixed version produced a carrier the
engine could not actually be fed.

Also noted: `pushesAt_perm`/`pullsAt_perm` already existed in `ChipContracts`.
I wrote copies in `TimedGrounding` before finding them and removed the
duplicates; only the genuinely new `_append` pair stays.

`lake build SP1Clean`: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
The last structural blocker on the engine's conclusion. The plan's Stage 4
treats the shift off `SailChain done.length` as a headline-relation change;
in the engine it is narrower than that, and the reason is worth recording:
`DynamicGroundedRow` never mentioned a chain. Only the quantifier in front of
it did.

`dynamicGrounded_of_weakCurrency` takes a `SailChain steps initial state` and
spends it in exactly three places — `valueOperandsBound_of_pullCurrency`,
`sourceAValueBound_of_pullCurrency`, `memoryPullsBound_of_pullCurrency` — all
of which go through `localValueAt_stepStart_iff` and nothing else. That is
the same single appeal `RowWiring.advance_at` made. Swapping it for
`localValueAtG_stepStart_iff` gives `dynamicGroundedG_of_weakCurrency`, whose
two Sail-shaped hypotheses become `traj n = some state` and `tl.start n` —
precisely the two places a 264-tick row made the old form unusable.

Both libraries build clean: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
The call-site edit's first half. `listAllInl` is today's projection and it is
exactly what `SyscallTableInactive.noActiveRows` exists to make available:
with no syscall rows every trail entry is a `Sum.inl`, so the trail *is* a
list of decoded instruction rows and the engine can walk it.

`listAllWalked` is its successor. It asks only that no **halt** row is
present — which the halt-free branch already establishes from
`realHaltRows witness = []` — and produces a `WalkedRow` list in the same
order, carrying a syscall row through the projection rather than excluding it.

Its result is a `List.Forall₂`, not a `List.map`, and that is forced: the two
row types are related by a *relation* — the syscall arm decodes a raw array —
so there is no function into `WalkedRow` to map with. `Walk.isWalk_forall₂`
is the matching transport, and it is why `isWalk_map` could not be reused.

`walkedRow_pullAt` also takes its walk edge as a parameter now. The trail is
extracted over the *canonicalized* State messages while a row's facts carry
the raw ones, so the two differ by `canonState`; `timeAgree` is the bridge,
and at the call site it is `timeNat_canonState` — a rewriting, not a new
obligation.

`lake build SP1Clean`: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`SyscallRowContext` names the five things a syscall row's meaning needs that
the row does not carry. `operands` came off the Program bus earlier; the
three semantic fields now come off the walk, leaving `pcCarry` as the only
premise — and that one is `StateBumpChip`'s carry fact, genuinely external,
exactly as the pc's `+ 4` is on the AIR side.

The content worth recording is why the three register reads may speak about
the *same* state. Their read times are `+0`, `+3` and `+2`, all below
`regEffectOffset = 4`, so every one is still in the pre-write half of the
row's window and `localValueAtG_regRead_of_traj` sends all three to `traj n`.
That lemma is the generalization of `localValueAtG_stepStart_iff` off the
exact window start, and it is what the `+3`/`+2` operand reads need.

`ecall` then follows from `pcValue` plus the committed `ECALL` fetch — the
second use of `witness_syscallRow_ecallTruth`, after the operand indices.

Two hoists, both the same rule as before: `SyscallRowContext.of_pieces`
assembles over an abstract row, and the `pcBits`-to-event-`pc` coercion moved
inside it. Done at the concrete 65-column row, that coercion alone exceeds
the depth budget — the documented "`have h : <row spelling> := <slice
spelling>` is a defeq coercion that blows up" landmine, hit head-on.

`lake build SP1Clean`: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`CoreProfile.CanonicalSyscallCodes` is the disclosed premise the plan's D9
calls for, carried the way `WithinOrdinaryRowLimit` is: one predicate, with
both directions projecting into it rather than owning separate spellings.

It has to be a premise, and the reason is worth stating where a reader will
find it. SP1's AIR reads bytes 0 and 1 of `x5`; its executor dispatches on
the full `x5 as u32` through `SyscallCode::from_u32`, which *panics* on a
non-enumerated value. So an AIR-valid `HALT` row may carry `x5 = 0x00010000`
— a witness the constraint system admits and no execution produces.
Canonicity is a fact about the executor, not a consequence of the AIR, so the
honest home for it is the profile rather than an invented row constraint that
would make the native chip stricter than the one it models.

Three pieces: the predicate itself; `EventSegmentWitness.syscallEvents` with
`canonicalSyscallCodes_iff`, which proves the segment-level condition *is*
the profile's read at the transcript's syscall events; and `syscallEventsOf`
with `isInlineCanonical_of_profile` on the native side, which is the form
`syscallStepFact_of_advance` consumes.

This is what makes `SyscallTableInactive.noActiveRows` removable rather than
merely unwanted: with no active syscall rows the condition is vacuous, and
with them it is a genuine obligation that had nowhere to live.

`lake build SP1Clean`: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
A0 of the syscall closing plan.  Four `FormalModel/SupportedShard.lean`
definitions rejected a mixed shard *before* any grounding ran, so emitting
per-row syscall events bought nothing until this landed:

  - `executionValid` demanded `execution.AllOrdinary`;
  - `SupportedSP1Transition` begins with `event = .ordinary`, and
    `AllTransitionsSupported` demanded it of every transition;
  - `SupportedHaltingTrace` demanded a dropLast-all-ordinary prefix, so a
    *mid-shard* syscall falsified the halting arm too;
  - `WithinOrdinaryRowLimit execution.steps` charged syscall steps against the
    ordinary instruction-row budget.

The per-transition obligations are now arm-split: `SupportedSP1Transition` is
demanded of `.ordinary` transitions only.  A syscall transition's meaning does
not come from the instruction-routing profile at all -- it travels in
`Machine.EventStep.syscall`'s own `SyscallTransition`, already authenticated by
`CoreShardCase`'s trace validity.  The model layer needed no change: `Clocked`
already dispatches on `ExecutionEvent.StartsAt` and `finalClock` is already a
duration prefix sum.

Two findings worth recording.

`AllOrdinary` was doing two jobs, and only one of them was a profile
restriction.  It was also the **branch discriminator**: `halted_facts` reached
the `.halted` case by refuting the execution branch with `exec.1`.  Dropping it
naively would not merely lose a proof -- it would make that theorem's claim
false, since a mixed non-halting shard is `¬ AllOrdinary` and yet squarely in
the execution branch.  New `EventExecutionTrace.HaltFree` says what the
execution branch actually means (no transition is a canonical HALT); it is
implied by `AllOrdinary` and refutes `HaltsWith` through `List.mem_of_getLast?`,
so the branches stay disjoint.  `halted_facts` now takes `¬ HaltFree`.

The row budget charges ordinary transitions only, via new
`Machine.ordinaryTransitionCount` / `EventExecutionTrace.ordinarySteps`.  That
is what the native side already supplied -- `realDecodedInstructionRows` is an
instruction-row count that never included syscall rows -- so the two sides' row
counts stay matched instead of the semantic side over-charging.

`mem_transitions_of_mem_locatedTransitions` and
`AllTransitionsSupported.all_of_allOrdinary` keep every syscall-free consumer
unchanged; `haltedExecution_of_haltGrounding` keeps publishing the stronger
un-split prefix obligation its callers want.

`lake build SP1Clean` 3801 jobs and `lake test` 3816 jobs, both at 0 errors /
0 warnings / no stray `info:`; layering and root-index gates pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
A1 of the syscall closing plan.  `RowsGrounded.dynamic` indexed its position by
`SailChain done.length initial state`; it now indexes by
`traj done.length = some state`.  `DynamicGroundedRow` never mentioned a chain --
only the quantifier in front of it did -- which is the same shape that made the
walk's own parameterization a rename, and `sailTrajectory_eq_some_iff` is
already proved in both directions, so today's meaning is recovered verbatim at
`Semantics.sailTrajectory initial`.

The generalization buys a position index a 264-tick syscall row can also
occupy.  Note that a row's *duration* never enters here: the index counts steps,
and `eventTrajectory` is step-indexed too, so a syscall row sits at index `k`
exactly like an instruction row.  Duration is a clock-axis concern and stays out
of this structure entirely.

Four adapter lines outside the definition: two `.mpr` at `grounded.at` in
`LocalExecution`, two `.mp` at the `SupportedCoreGrounding` /
`SupportedCoreHaltGrounding` construction sites.  The engine itself still states
its position as a `SailChain`; it moves to the trajectory index later.

Also `pcEdgeOf` and `pcWalk_iff_isWalk`, purely additive.  `PcWalk` is a
redundant copy of `Walk.IsWalk` -- identical nil (`a = b`) and cons
(`(edge x).1 = a`) shapes -- so the bridge holds by `Iff.rfl` at every position.
Stating it as a lemma rather than redefining `PcWalk` inherits `isWalk_append`
(the halt branch is literally a `prefix ++ [one row]` walk), `isWalk_map` and
`isWalk_forall₂` while leaving all six call sites untouched, and it is the
generalization a syscall row needs: a row with no `ChipRow` still has a pc edge.

Acceptance: no pre-existing consumer changed.  `AIR`, `HaltExecution`,
`BootHalt` and the completeness stack all compile as they stood.
`lake build SP1Clean` 3801 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
A2 of the syscall closing plan.  Reading the sites changed this step's shape: an
ordinary chip row genuinely advances eight ticks, so
`eventExecution_of_groundedRows` and `sailRetireChain_of_groundedRows` are the
*ordinary* engine and keep their `8 *` correctly.  What was owed is the
duration-summed form of each building block, with the `8 *` form re-derived from
it -- the same generalize-and-recover discipline as A1.

Most `8 *` occurrences were never targets: `FormalModel/EventExecution.lean`'s
are docstring prose, and `Model/Semantics/Truth.lean`'s are the Sail-specific
`microValue`/`LocalValueAt` that `GenericTruth` already supersedes.

`statePullAlign8_of_durations` was a `True`-valued stub.  It asserted nothing
while its docstring called it the easiest and most necessary of the engine's
three duration generalizations -- a statement that looks load-bearing and is
not.  It is now real, in `GenericWalk.lean` beside the two lemmas its proof
consumes (`statePullTime_of_stateWalk_durations` and `List.dvd_sum`), and
`statePullAlign8_of_stateWalk` is re-derived from it at `duration := fun _ => 8`.
That makes all three generalizations real, where `clockCount_of_stateWalk` and
`statePullTime_of_stateWalk` were already thin delegating wrappers.  Its
`8 ∣ duration` premise is what a 264-tick row discharges through
`eight_dvd_durationAt`, so alignment survives a mixed timeline.

New `transitions_finalClock`: the event clock is the prefix sum of the
transition list's own durations, with no ordinary hypothesis -- plain
`clockAfterEvents` telescoping.  `ordinaryTransitions_finalClock` is now that
composed with `ordinaryTransitions_durationSum`.

`ordinaryTransitions_clocked` is deliberately *not* generalized:
`EventTransitionsClocked` is already the per-position statement, and a syscall
event's `StartsAt` is derivable only from the row's `Spec` limb bounds plus
`2 ^ 24 < p`, which a row-generic engine does not have.  The mixed engine will
supply it per row.

`lake build SP1Clean` 3801 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
A3a of the syscall closing plan.  The eight-tick engine *constructed* its own
position index with `chain.snoc`.  A mixed walk cannot: a syscall step is not a
`SailStep`, so nothing lets the engine extend the trajectory itself.  The new
`executeWalkEventsAux` **consumes** its index instead -- each row's own advance
obligation hands back `traj (k + 1)` -- and stays generic in the row type,
because `Soundness/LocalExecution.lean` sits below the entire syscall campaign in
the import order and so cannot mention a mixed row type.

`WalkAdvancesAt` is that per-position obligation: the row's own semantic step,
the trajectory's next value, the outgoing pc / ROM / configuration facts, and --
on the ordinary arm only -- the instruction-routing obligation.  The arm split
lives in the caller rather than the engine, because `RowsGrounded` still cannot
hold a syscall row: `DynamicGroundedRow` is `chipSpec + advanceReady +
ValueOperandsBound`, all `ChipRow`-shaped.  The routing obligation is guarded by
`eventOf row = .ordinary` because `SupportedSP1Transition` begins with exactly
that conjunct; a syscall transition's meaning travels in `EventStep.syscall`.

The engine reports its trace's events as `suffix.map eventOf` rather than as a
step count.  That one equation carries the length, the per-row durations -- so
the clock is a prefix sum -- and `AllOrdinary` when every row is ordinary, where
the eight-tick engine stated all three separately.  It is what lets
`eventExecution_of_groundedRows` be recovered at `eventOf := fun _ => .ordinary`
with its signature byte-identical, so `AIR` and `HaltExecution` are untouched.

The eight-tick `executePcWalkEventsAux` is deleted; it is now that recovery.
`executePcWalkAux` and `sailRetireChain_of_groundedRows` stay as they were --
they build a `SailRetireChain`, which is ordinary-only by construction.

`lake build SP1Clean` 3801 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Groundwork for the mixed-row instantiation of the row-generic engine.

`LocalStepFactG` deliberately forgets *which* transition produced the
trajectory's successor: `LocalStateTruthG` says only that the trajectory reaches
the next state, which is all the truth layer needs.  An execution engine that
consumes a trajectory needs the `EventStep` itself.

The first instinct -- refactor `ordinaryStepFactG_of_advance` and
`syscallStepFact_of_advance` to also return the step -- is unnecessary.  The
trajectory already remembers: `eventTrajectory_succ` evaluates `executeEvent?`
at the transcript's own event.  So `sailStep_of_eventTrajectory_ordinary`
recovers the step at an ordinary position from the trajectory alone, and neither
of those two large proofs is touched.

Only the ordinary arm needs a lemma.  On the syscall arm the `EventStep` is its
constructor applied to the row's own payload: `SyscallTransition`'s third
component is `handler.run … = some target`, which is definitionally
`ExecutableSyscallHandler.relation`, and the trajectory's successor is that same
`run`.

Underneath it, `sailStep_of_stepOnce` -- the converse of the long-standing
`stepOnce_of_sailStep`.  `stepOnce` succeeds only by `try_step` running to an
`ok`, which is exactly what `SailStep` asserts, so the totalized and relational
presentations are interchangeable.  The forward direction alone sufficed while
the engine *built* its trajectory.

`lake build SP1Clean` 3801 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
New `Soundness/SyscallExecution.lean`: the instantiation of the row-generic
engine at `WalkedRow`.  It lives in its own module because
`Soundness/LocalExecution.lean` -- which holds the engine -- sits below this
whole campaign in the import order and cannot mention `WalkedRow`.

`walkedPcEdge` reads a row's pc edge off its `RowFacts` rather than off a
`ChipRow`, which is what lets a syscall row take part at all: it has no
`ChipRow`, but it supplies the same two State messages through `syscallRowFacts`.

`walkAdvancesAt_of_walk` joins three things, each already proved elsewhere.
`GroundedG` supplies the memory-currency antecedent `LocalStepFactG` demands;
`LocalStepFactG` then supplies the pushed state truth, which *is* the
trajectory's successor together with its pc, ROM and configuration; and
`walkedRow_timeStep` with `start_injective` identify **which** index that
successor sits at.

That last step is the whole content of the campaign's top-ranked risk.  The
engine counts list positions and the transcript counts timeline positions, and
this is the single boundary where they are made the same number -- so a
mismatch fails here, at its cause, rather than deep inside a later clock
obligation.  `start_injective` loses its `private`: it was scoped that way only
to avoid a rebuild below `MicroTime.lean` during the sketch phase, and now has a
real consumer outside its file.

The row's own `EventStep` is deliberately a hypothesis, not a derivation.
`LocalStepFactG` forgets it, and recovering it is arm-split: an ordinary row
needs `¬ AboutToExecuteEcall`, a syscall row the opposite plus its own
`SyscallTransition`.  The caller discharges it per arm.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`;
check_layering PASS (591 modules), check_root_index PASS (588 modules).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`walkedExecution_of_advances` runs the row-generic engine at `WalkedRow`.  The
engine reports its events as `rows.map WalkedRow.event`, which is definitionally
`transcriptOf data rows` -- `transcriptOf_eq_map` is `rfl` -- so this produces
the very transcript the syscall layer is already stated over rather than a
parallel one that would then have to be proved equal to it.  Same discipline as
`walkAdvancesAt_of_walk`, one level up: make the two spellings one term by
construction instead of reconciling them afterwards.

`walkedExecution_finalClock` is where the 264-tick row is actually paid for.
`transitions_finalClock` sums the trace's own event durations with no ordinary
hypothesis, and the new pointwise `WalkedRow.duration_event` turns each event
duration back into the row width the walk was stated with.  Nothing here knows
that 8 is a special number.

`walkExecution_of_advances` exposes the engine publicly, specialized from the
suffix-indexed auxiliary to the whole row list; `eventExecution_of_groundedRows`
now goes through it too, so the ordinary and mixed paths share one entry point.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`eventTransitionsClocked_of_starts` states the schedule obligation index-wise:
every event must start at the prefix sum of the durations before it.  Stating it
that way is what lets a mixed trace establish `Clocked` with no all-ordinary
hypothesis, where `ordinaryTransitions_clocked` needed one.

`walkedExecution_clocked` applies it to the walked carrier.  The obligation is
arm-split by construction and cannot be otherwise: an instruction row owes
nothing, because an ordinary event's `StartsAt` is `True`; a syscall row owes
`event.clock =` its own prefix sum, which `syscallEvent_startsAt` derives from
the row's `Spec` limb bounds and `2 ^ 24 < p`.  That is exactly the fact a
row-generic engine cannot produce, which is why it enters as a hypothesis rather
than a conclusion -- the same reason `ordinaryTransitions_clocked` was left
alone rather than generalized.

`walkedExecution_durations` is factored out of the final-clock proof so the
clock and the schedule share one statement of "the trace's per-transition widths
are the walk's own row widths".

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
The last mechanical piece of the mixed-row instantiation.  `stepOf_of_arms`
turns each walked row into the `EventStep` the engine asks for, and the two arms
genuinely need different things -- which is why no generic argument could have
covered both.

An ordinary row needs `¬ AboutToExecuteEcall`, which comes from its own routing
evidence rather than from the trajectory, plus a determinism step: its `advance`
picks some target, the trajectory picks some target, and `stepOnce` being a
function identifies them.  A syscall row needs the *opposite* ecall fact, from
`SyscallRowContext`, plus its payload's `SyscallTransition`; there determinism is
free, because `ExecutableSyscallHandler.relation` is definitionally `run = some`
and the trajectory's successor is that same `run`.  New
`handlerRun_of_eventTrajectory_syscall` reads it out, and needs no analogue of
`sailStep_of_stepOnce` for exactly that reason.

The instruction arm's premise is stated as "this row advances and routes" rather
than as a `GroundedRow`, so this lemma stays clear of the wiring and carrier
layers; the caller supplies it from `GroundedRow.advance` and
`GroundedRow.supportedSP1Transition`.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`walkedRow_widthAt` is the first of `walkE`'s five per-row inputs proved from
the ensemble over the mixed carrier, and the first place the two arms'
genuinely different clock contracts appear as one statement: `+8` for an
instruction row against `+264` for a syscall row.  Both were already proved
(`witness_realDecodedInstructionRows_timeStep`,
`witness_realSyscallInstrsRows_timeStep`); what is new is presenting them under
one index.

The aligned carrier costs nothing here: `AlignsWith` fixes both State messages
on the nose and reorders only the memory lists.

One landmine paid for and recorded inline: the syscall arm must *rewrite* with
`syscallRowFacts_statePush`/`_statePull` rather than let `exact` reach the
messages by defeq.  `syscallInstrsRow` unfolds into the table's element
construction, and crossing that at a witness row exceeds the depth budget --
`whnf` timeout, no heartbeat setting would have helped, and the fix is the
campaign's standing crossing rule rather than a bigger budget.  With the rewrite
the file elaborates in 2.9s.

The witness-level obligations live in their own section: they need `2 ^ 24 < p`
for the syscall row's clock recombination, while the engine plumbing above is
content with `2 ^ 17`, and a file-wide bump would have handed the plumbing a
hypothesis it does not use.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`syscallRowOKCore_of_witness` gathers nine of `syscallRowOKCore`'s ten premises
from facts already proved elsewhere: `is_real` from table membership, `Spec`
through the currency break, the two clock-byte bounds from the composed
`CPUState` reader, the three operand indices from the committed `ECALL` Program
row, and the `+264` step.

The tenth stays a hypothesis, and the reason is worth naming rather than
working around: `align8` relates the row's pull to the **shard's** initial
clock, which no single row can see.  Only the walk establishes it, inductively,
so no quantity of witness facts would discharge it here.

`syscallInstrsRow_cpuState_bounds` loses its `private` -- the same "private only
until a real consumer lands outside the file" situation as `start_injective`.
Its halt-row twin stays private, since its only consumer is still in-file.

The proof needed the depth-blow-up remedy, and this instance is worth recording
because it bisects to nothing: the statement alone elaborates in 2s, every
premise derivation alone is fine, and applying `syscallRowOKCore` at an abstract
row is fine -- yet the combination times out, and clearing the context does not
help because the cost is not in the context.  `set_option diagnostics true`
names it directly: 372k `Vector.mapRange` unfoldings driven by
`[def_eq] sp1Ensemble`, i.e. `whnf` normalizing the table's element construction
because a derived premise and the lemma's expectation of it differ just enough.
Interposing an opaque variable -- `generalize <row> = r at <premises> ⊢` --
brings it back to ~2s.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`syscallStepFact_of_witness` and `syscallFrameFact_of_witness` gather the
witness-derivable premises of their `_of_advance` counterparts: `is_real`,
`Spec` through the currency break, `SelectorsValid`, `PulledFacts`, D9's
canonicity, the clock-byte bounds, `op_a`'s `isU64`, the `x5` index, and the
`+264` step.  The frame twin needs neither the clock bounds nor the `isU64`,
because a frame claim never touches the written value.

Three premises remain on each, and they are the same three every time: the
handler's own semantics (`payload`), the fact that this row's window is the
transcript slot carrying *this* row's event (`positioned` -- about the walk
order, not the row), and `rowContext`, which needs the trajectory's state at the
row's index plus `StateBumpChip`'s pc-carry.  That mirrors `align8` in the row
contract: the irreducible premises are consistently the ones reaching outside a
single row, which is a good sign the decomposition is cutting where the
semantics actually joins.

Both proofs interpose the opaque row variable prophylactically, now that the
failure mode is known.

Also `SyscallInstrsChip.Spec.selectorsValid`, a named projection for `Spec`'s
twelfth conjunct.  Consumers were reaching it through an eleven-deep chain of
`.2`s that silently re-points to a different conjunct the moment anyone inserts
one -- and `SelectorsValid` is exactly the conjunct every per-arm semantic
theorem turns on, so a silent re-point would fail quietly.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`RowWiring.readTime` pins every pull to the row's window start, so it holds at
the **ordinary** carrier and not at the aligned one.  That is why an instruction
row's step and frame facts are proved at the ordinary carrier and then
transported -- and why the mixed walk cannot consume them without the
trajectory-indexed form of that transport.

Four lemmas, each the `G` twin of an existing Sail one:
`localStateTruthG_congr`, `ordinaryPullCurrencyG_of_valueAligned`,
`localStepFactG_valueAligned_of_ordinary` and `frameFactG_valueAligned_of_ordinary`.

They are near-verbatim ports, which is the same result D10 reported for the walk
itself: the reasoning was already trajectory-agnostic and only the types said
otherwise.  The one real difference is that the Sail shift-window lemma derives
its window from the state truth, while `localValueAtG_shift_window` is indexed
by an explicit step -- so the port has to name the index the pulled state truth
already carries.

With these in place an instruction row and a syscall row can reach the mixed
walk at the same carrier, which is what `walkE` requires.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`engineFactsG_of_kind` and `ChipGroundingContracts.engineFactsG` are the
trajectory-indexed twins of `engineFacts_of_kind` and
`ChipGroundingContracts.engineFacts`.  With them the instruction arm reaches
`LocalStepFactG`/`FrameFactG` from the 25 chips' existing contract bundle, which
is what the mixed walk needs from an ordinary row.

The bundle's three currency-conditional producers -- `wiring`, `chipSpec`,
`readiness` -- are reused verbatim.  None of them inspects the index, so the D0
circularity break carries over untouched: the row's open Memory inputs still
come from the *assumed* pull currency, never from the walk's own output.  That is
D10's observation about the walk arriving one layer down.

The one genuinely new premise is `positioned`.  On the Sail side a transcript
slot could not be anything but ordinary, so the fact was free; on a mixed
transcript it is the statement that this row is not sitting where a syscall
event sits, which no row-local datum can decide.

That completes the per-row obligations for both arms: `widthAt` and `rowOKAt`
were already in place, `stepAt`/`frameAt` land here for instruction rows and in
the previous commit for syscall rows, and `pullAt` follows from the State walk.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`walkedTrail_of_haltFree` is the structural replacement for `listAllInl`.  That
projection needs *both* the halt and the syscall table empty, which is exactly
what `SyscallTableInactive.noActiveRows` was buying; this one needs only the
halt table empty, and hands back a `WalkedRow` list in the same order -- so a
syscall row is carried **through** the projection rather than excluded by it.

The correspondence is a `List.Forall₂` and not a `List.map`, because the syscall
arm decodes a raw array: the two row types are related by a relation, not a
function.  `Walk.isWalk_forall₂` is the transport that shape needs, and the edge
agreement it asks for is `rfl` arm by arm against the new `walkedCanonEdge` --
`trailCanonEdge` with the halt arm gone.

So on any halt-free shard there is now a mixed walk over the canonicalized State
edges, with no premise about the syscall table at all.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`walkedTrail_pullAt` is the last of `walkE`'s five per-row inputs, and the one
that ties the walk order to the transcript: a walked row at trail index `k`
pulls at the transcript's own `tl.start k`.  It holds for a mixed shard for the
same reason it holds for an ordinary one -- `walkedRow_pullAt` telescopes the
State walk against each row's *own* width, and `durationAt_transcriptOf` says
the transcript reports that same width at that same index.  Nothing here knows
that 8 is special.

Underneath it, `walkedCanonEdge_steps` and `walkedCanonEdge_timeAgree`.
Canonicalization preserves `timeNat` once the endpoint's `clk_high` is genuinely
24-bit, which the goodness filter already supplies for both arms; the widths are
the two `timeStep` lemmas; and the instruction arm's carrier agreement is
`AlignsWith`, which fixes the State messages on the nose and reorders only the
memory lists.

With this, all five per-row obligations -- `stepAt`, `frameAt`, `rowOKAt`,
`widthAt`, `pullAt` -- are provable from the witness over the mixed carrier, for
both arms.  What the walked grounding certificate still needs is the two
balances, which is bookkeeping over `walkedMemoryBalance` and the State walk's
endpoints rather than new semantics.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Assembling the mixed walk turned up a design fact worth naming: the syscall
arm's State messages must be re-spelled to canonical, exactly as the instruction
arm's are.  The walk runs on *canonicalized* edges -- that is what makes the
StateBump rows cancel as self-loops -- and a syscall row's pushed message is
genuinely non-canonical: `clk_low + 264` may pass `2 ^ 24`, and
`next_pc[0] = pc[0] + 4` carries nowhere.  Both are upstream's own deliberate
non-canonicalities, left for `StateBumpChip`, and recorded in this campaign's
ground-truth notes from the start.

`ValueAligned` cannot carry that transport.  It demands `ordTime` -- every pull
sitting at the row's window start -- and a syscall row's three reads
deliberately sit at `+0`, `+3` and `+2`, forced by `TouchOK`'s two disjuncts.  A
bare re-spelling asks for none of that, which is all a canonicalized State edge
needs.  Hence `localStepFactG_stateRespell`, `frameFactG_stateRespell` and
`rowOKCore_stateRespell` beside the `ValueAligned` family rather than as
instances of it -- and `rowOKCore_` rather than `rowOK_` for the parallel reason
that `RowOK.time8` is an exact eight ticks, which a 264-tick row never meets.

The frame transport needs one hypothesis fewer than the step transport: a frame
claim reads the pushed message's time and never its pc.

One mechanical note: passing `localStateTruthG_congr` its congruence in the
wrong direction *while* relying on defeq through `stateRespell`'s projection
blows the recursion depth rather than reporting a type error.  Making the
projections syntactic with `rw` first is what surfaces the real mistake.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`trailWalk_grounded` now takes the canonicalized State edge plus four
`timeNat`/`pcBits` agreements, re-spells the carrier internally, walks, and
transports the `GroundedG` output back to the rows' own facts.

The reason it must is narrow, which is what makes the fix cheap: only
`stateBalance` reads the State messages as *messages*.  Every other obligation
reads them through `timeNat`, and `walkedRow_pullAt` was already written with
its edge as a parameter for exactly this reason -- its docstring says the trail
is extracted over the canonicalized messages while the row's facts carry the raw
ones.  So the mismatch is confined to one hypothesis, and internalizing the
re-spelling leaves all 49 `WalkedRow.facts` call sites untouched.

The alternative -- re-spelling the carrier itself -- would have rippled through
four files, and would have put a canonicalization into the definition of what a
walked row's facts *are*, which is the wrong place for it: the canonicalization
belongs to the trail's extraction, not to the row.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`walkedCanonEdge_agrees` supplies `trailWalk_grounded`'s four re-spelling
agreements in one statement: both endpoints' `timeNat` and `pcBits` match the
rows' own facts.  `timeNat_canonState` and `pcBits_canonState` do the work, the
goodness filter already supplies their `clk_high`/`pc1`/`pc2` bounds for both
arms, and the instruction arm additionally leans on `AlignsWith`, which fixes
the aligned carrier's State messages to the ordinary ones on the nose.

`walkedTrail_stateBalance` is the obligation that forced the re-spelling in the
first place -- the only one that reads the State messages as *messages* -- and
at the canonicalized edge it is `endpointBalance_of_stateWalk` applied to the
walk, nothing more.

`walkedCanonEdge_timeAgree` is deleted: it is now literally the first projection
of `walkedCanonEdge_agrees`, and keeping both would be the kind of quiet
duplication this campaign has already paid for twice.

The remaining `trailWalk_grounded` hypothesis is the Memory balance.  That one
is not wiring: `walkedMemoryBalance`'s conclusion still carries the MemoryBump
and Halt side terms, so it needs the bump-refresh elimination the ordinary
engine performs -- named work, not a gap.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`RefreshWiring`'s `touchPairsAt` bridges are already row-generic --
`pushesAt_of_touchLists` and `pullsAt_of_touchLists` take `{α : Type*}
(rows : List α)` -- so the Memory balance over walked rows is an instantiation
rather than a port.  What they need is a per-row touch list and the two facts
that a row's push and pull lists are its projections.

`walkedTouches` supplies the list: the caller's aligned touches for an
instruction row (the same `touchesOf` the ordinary engine chooses), and the
syscall row's own three register touches, which are already paired by
construction.

`walkedTouches_projections` supplies the three obligations, and the syscall arm
gets all three from `RowOKCore` itself -- nothing extra is assumed about the
row.  `RowOKCore.touches` is a `List.Forall₂` over the pull and push lists, so
it pins their lengths, which is exactly what makes the `zip` lossless; and it
carries `TouchOK.loc_eq` per touch, which is the per-touch location agreement
`pullsAt_of_touchLists` asks for.  That the shape obligation and the balance
obligation are served by the same field is a small piece of evidence the row
contract is cut in the right place.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`walkedTouchBalance` converts the balance's *shape*, not its provenance:
`walkedMemoryBalance` supplies the `pushesAt`/`pullsAt` form, and
`RefreshWiring`'s two bridges -- already row-generic -- turn it into the
per-location touch pairs `exists_refreshFreeTouchLists` consumes.

Two terms disappear on the way.  On a halt-free shard the Halt table's Memory
messages are literally nil (`halt_producedMessages_nil_of_haltFree` and its
twin), so both Halt side terms vanish outright.  What is left is the MemoryBump
refresh pair, which is the elimination's own input rather than a side term --
and nothing about a syscall row survives as a side term at all, because its
touches are absorbed into the batch.  That absorption is the move the ordinary
engine already performs for the halt row, now doing the same job for syscall
rows, which is exactly what the plan predicted would replace `noActiveRows`'s
memory obligation.

`RefreshWiring` is imported directly rather than reached transitively; there is
no cycle, since it sits below the whole syscall campaign.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`;
check_layering PASS (591 modules), check_root_index PASS (588 modules).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`walkedRewrittenMemoryBalance` is the other end of the touch-pair conversion.
`exists_refreshFreeTouchLists` hands back a rewritten touch batch and a weakened
finalize frontier whose per-location balance carries no refresh edge; rebuilding
each row's carrier from its own rewritten touch list turns that straight back
into `pushesAt`/`pullsAt`, which is `walkE`'s `memoryBalance` shape.

It is cheap for a structural reason worth recording: `alignedOf`'s two memory
projections *are* the touch list's two projections, by definition.  So both
bridge obligations are `rfl`, and the same two row-generic lemmas that carried
the balance into touch-pair form carry it back out.  The only genuine premise is
the per-touch location agreement, which the rewrite preserves -- a `PullRewrite`
moves a pull to a same-location, same-value, no-later ancestor.

With this the Memory balance has both ends: `walkedTouchBalance` takes the
witness's balance into the elimination's input shape, and this takes the
elimination's output back to the walk's.  What remains is instantiating the
chain at the witness's concrete MemoryBump refresh pairs and frontiers, which is
cutover material rather than new machinery.

`lake build SP1Clean` 3802 jobs, 0 errors / 0 warnings / no stray `info:`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Two honesty defects the campaign introduced, both found by running the release
gates rather than by the build.

**The relation grew a third conjunct and four documents still said it had two.**
`SupportedCoreNativeRelation` carries `SyscallTableInactive`, and `AGENTS.md`,
`docs/verification-report.md` ("There is no third conjunct."),
`docs/audit-surface.md` and `docs/release-audit.md` all asserted otherwise.  The
Lean docstring disclosed the reversal in place; the documents did not, and a doc
that contradicts the code is worse than one that is merely stale.  All four now
name the premise, say it is a placeholder rather than a derived fact, and say
what each field is waiting on -- `noActiveRows` on the engine's mixed-row
carrier, `haltTablePresent` on D8's successor Exit table.  None of the gates
caught this: they check currency and declaration resolution, not whether a claim
is true.

**Two axiom-census probes had gone silent.** The census names its targets in
alternation regexes ending `\b`, and `\b` does not match a `_core` suffix -- so
when the syscall wave renamed `sp1ProviderTables_channels_subset` to
`…_core` (the syscall chip speaks on seven buses, so the statement gained a
disjunct), the probe stopped matching and simply generated one line fewer.  That
is exactly the failure mode `gen_axiom_probe.py`'s own comment warns about for
release headlines.  Both `_core` spellings are now named, which also adds
`sp1AllTables_channels_subset_core` -- never probed at all.

While there, `routeOf_reaches_sail` is dropped from the coverage alternation: it
has matched nothing since the `reaches_sail` path was retired, so it was dead
weight making the alternation look like it covered something it did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
`run_audit.sh --main-only --update` from a clean tree, plus the ledger's two
counts (923 main, 1000 total).  The delta is the two channel-discipline probes
the alternation rename had silenced, and the ledger now records *why* they went
silent -- the probe's self-check is one-sided.  A wrong fully qualified name
fails to elaborate and stops a stale census passing; a name that no longer
matches an alternation is simply not emitted, and the census shrinks quietly.
Prefer exact names over shared prefixes when adding to one.

`scripts/run_audit.sh --main-only` now reports AUDIT PASS across all thirteen
gates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6qohiaMMLVxznWLifuJ2d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant