Skip to content

The B4 door was one capability, not 40 diagnostics: the statement-form let has no body to lower from its own node - #8828

Merged
briansrls merged 1 commit into
mainfrom
session/silent-tern-888
Aug 22, 2026
Merged

briansrls merged 1 commit into
mainfrom
session/silent-tern-888

Conversation

@briansrls

@briansrls briansrls commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

The door was never 40 diagnostics. It was one missing capability.

Boundary 4 of the seven-boundary product receipt refused on the compiler's own 107-module closure with 40 diagnostics. b4 causal versus carried had already established, by reading the producers' return constructors, that 39 of those are produced on Accepted paths and merely carried into the Rejected list by rejected_with_pending — so exactly one rejects. What that reading could not do is name which one: body_lowering_reason_unsupported_form has sixteen producer sites and the receipt renders only a tally of reason symbols.

It is let lm = dag_language_model(). More precisely, it is the class that line belongs to: the ordinary statement-form let, which occurs at 104 sites in the tree.

What it refuses, read from the producers rather than from message text

v2.compiler.body_lowering_fold body_lower_let_expr calls body_lower_try_let_in_from_captured, whose first move is body_lower_let_in_body_capture_optional — a walk for the dag_token_kw_in atom. With no in it returns Accepted { value: Absent }, and body_lower_let_expr rendered that absence as unsupported_form.

Two independent confirmations that this is the statement form and not a failed spine walk, because the two have different repairs:

  • The grammar says so by declaration. dag_grammar_let_expr is a choice of two alternatives: let n = e in e, which carries its own body, and let n [: T] = e, which does not.
  • The parse tree says so by execution. fn f() -> Int { let x = 1 \n x } dumped through the real grammar gives a right-nested statement spine — stmt_seq.captured holds the let at grammar_sequence_left_node_projection and the continuation at grammar_sequence_right_node_projection.

So the statement form's Bind body is a sibling of the let, not a descendant of it. body_lower_let_expr was being handed a node that cannot in principle carry its own body. A refusal that fires on the ordinary case is a missing capability wearing a malformed-input costume.

What changed

1. The capability (body_lower_stmt_spine, body_lower_stmt_seq, reached from both body_lower_fold and the fn-body walker through one shared authority). A let-headed statement spine lowers to lower_bind(key, value, <remainder lowered>). It lives at dag_surface_stmt_seq because that is the only node from which both halves of a Bind are reachable — a structural argument, not a convenience one.

The guard is the whole overlap story: a spine is taken over only when it is headed by a statement-form let and has a following statement, which is exactly the case that has no lowering today. The in form is untouched and keeps its own path. A non-final statement that binds nothing refuses with its own cause (body_lowering_reason_statement_precedes_without_binding) rather than fabricating a continuation.

2. The cause split, which is worth landing on its own and would be even if the capability were bigger than it looked. body_lower_let_expr had one reason standing over "malformed let" and "well-formed let whose body this node cannot see". That conflation is what gave every reader the wrong first hypothesis and is why this took a session to find. It is #8801's split on the neighbouring form — spelled as two reasons rather than a coproduct-plus-projection because this result has exactly one consumer and it does not branch on the distinction; only the diagnostic does, and a coproduct whose sole consumer immediately collapses it would be a second representation of what the reason symbol already carries.

3. The instrument that made the finding reachable. The transaction now writes <txn>/b4-diagnostic-ledger.txt beside the retention census: every diagnostic the refusal carries, in carriage order, attributed through the same locus channel and production-identity vocabulary the census already used, plus a bounded rendering of the refused subtree's atoms. Written from inside the transaction for the same reason the census is. CensusOutcome is renamed B4EvidenceOutcome because boundary 4 now writes two evidence artifacts and one outcome type per artifact would be two names for one concept.

A note on why the atoms and not a file: a Node deliberately carries no filename — std.occurrence_identity occurrence_identity_scope_law names filename as a forbidden identity input — so nothing here proposes putting one on it. The atoms are a locator, not an identity.

The door moved — MOVED, and what it moved to

Both measurements are the same producer (run_seven_boundary_product_receipt), same subject (src/v2/compiler/00_compile.dag, 107 files), same transport.

before after
diagnostics 40 45
parse_grammar_choice_overlap_residue 7 7
body_lowering_reason_wrapper_retained_emitted 32 37
the one that rejects body_lowering_reason_unsupported_form @ dag_surface_let_expr normalized_tree_reason_wrapper_retention_not_normalized @ dag_surface_module

The 7 residue invariant holds, as its mechanism predicts. The next first failure is at a different producer, not one rank down a queue of lets.

And it changes the standing of the wrapper-retained population, which is the part a reader should not miss. The new holder is the normalized-tree admission refusing because retention is present. So the 37 are no longer merely carried beside the refusal — they are upstream of it. "Closing the 32 advances the door by zero boundaries" was true of the door as it stood and is not true of the door as it now stands; both prior probe documents are corrected in place rather than left to be reconciled by the next reader.

Evidence

src/v2/test/claim/body_lowering/statement_let_bind_test.dag, four claims written as invariants rather than as observations of the present frontier:

  • statement_let_then_reference_lowers_holds — the positive control
  • statement_let_chain_lowers_holds — nested binds
  • single_statement_body_unchanged_holds — the path that already worked, unmoved
  • unbound_statement_prefix_refuses_holds — the discriminating RED. A body whose non-final statement binds nothing has no continuation to bind and must refuse with its own cause. If this ever greens, the sequence lowering has begun inventing a body for a statement that produced none. It does not retire when the capability lands; it is the standing control that the capability stayed honest.

Executed before enrolment, through the real grammar and the real normalize: single-statement ACCEPTED; let x = 1 \n x ACCEPTED; let x = 1 \n let y = x \n y ACCEPTED; 1 \n 2 REJECTED: body_lowering_reason_statement_precedes_without_binding. The first pre-repair run of the same four returned unsupported_form on the two let cases.

Subject discipline, which cost another lane a run tonight

Every measurement here was taken through ctrl-build --remote, which fetches current main and applies the local worktree as a patch on top. The base commit is therefore not this branch's HEAD and differs between dispatches (three consecutive runs based on 08be45b6, 47ea349f, a9582f84). Pinning a sha would name a tree no run used, so the subject is stated as "current main at dispatch time, plus this branch's diff". A corollary worth carrying: a historical two-ref comparison is not available on this transport at all — positioning the worktree beforehand does not help, because the patch lands on whatever main is current.

…m let has no body to lower from its own node

Boundary 4 of the seven-boundary product receipt refused with 40 diagnostics,
of which exactly one rejects. A per-diagnostic ledger written by the
transaction identifies it as dag_surface_let_expr -- the ordinary statement
form, whose Bind body is the enclosing statement spine's remainder and so is
not reachable from the let node at all. It is lowered at dag_surface_stmt_seq,
the only node from which both halves of a Bind are visible.

Executed: the door moves from body_lowering_reason_unsupported_form to
normalized_tree_reason_wrapper_retention_not_normalized.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gunbai-bot gunbai-bot Bot changed the title B4 PRODUCT DOOR, serial owner (zesty-carp-454 and clever-pike-504 both archived; #8785 and #8801 MERGED so the subject correction has landed). B4 gates B5/B6/B7: no authentic product assembly, no authority Cargo manifest, no rustc over the actual v2-produced compiler until the FIRST failing module a The B4 door was one capability, not 40 diagnostics: the statement-form let has no body to lower from its own node Aug 21, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 21, 2026 22:57
@briansrls
briansrls merged commit e58e4b3 into main Aug 22, 2026
1 of 2 checks passed
@briansrls
briansrls deleted the session/silent-tern-888 branch August 22, 2026 00:37
gunbai-bot Bot pushed a commit that referenced this pull request Aug 22, 2026
…loor failures

The floor red on 77c9121 was 16 FAILs, all in v2.test.claim.fold_lowering (12) and
v2.test.claim.body_lowering.statement_let_bind (4), every one 'non-exhaustive pattern
match on OccurrenceId'. None of it is reachable from this branch's subject -- the diff
touches the systemd unit-file grammar and the build-cache renderers.

Diagnosed rather than assumed: main was GREEN at the same head class, and the two
failing families map exactly to two commits this branch did not contain -- #8818, which
routed the fold-lowering witnesses out of long/ and into the executing floor, and #8828,
which is the statement-form let lowering. This branch carried the test files at their
pre-#8818 home while the merged main-side routing executed them, so it ran witnesses
against a compiler half a lane behind them.

The remedy is the merge, not an edit: no file of mine is implicated, and the merge is
clean (#8818 deletes src/v2/test/claim/long/fold_lowering_test.dag outright).

Re-verified after merging: build_cache_ensure witness green, my grammar and renderer
changes intact, no body-level annotation in the diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
briansrls pushed a commit that referenced this pull request Aug 22, 2026
…ad ever obeyed until one did (#8854)

* The declaration was the lie: two OccurrenceId params that no caller had ever obeyed until one did

MAIN IS RED at 67437fc — 16 witnesses (12 fold_lowering, 4 body_lowering.statement_let_bind)
fail with `non-exhaustive pattern match on: OccurrenceId { value: 79 }`. Green PR, green base,
red union: #8833's branch did not contain #8828, and neither head ever existed in the
combination that fails.

The mechanism is one layer below the obvious one. #8833 changed one call in `02_parse`
`parse_expr_terminal` from `node_occurrence_minted(id: minted.id)` to `minted.id` — and it did
that to OBEY the signature it was calling. `v2.extdeps.languages.dag` declares

  fn dag_int_literal_node_from_lexeme(lexeme: String, occurrence_id: OccurrenceId)
  fn dag_int_literal_node_from_magnitude(magnitude: DecimalMagnitude, occurrence_id: OccurrenceId)

and stores that parameter straight into `Node.occurrence_id`, which is declared
`NodeOccurrenceId`. Both signatures have said `OccurrenceId` since #6558. Every caller in the
tree contradicted them by passing `SyntheticOccurrence`, which is why it stayed invisible.
#8833's author was the first person to take the signature at its word.

So the wrap is correct and the declaration is the defect. Fixed at the declaration, not just at
the call site — repairing only the caller leaves the lie standing for the next caller to obey:

  - dag.dag: both parameters redeclared `NodeOccurrenceId`; the now-unused
    `import std.occurrence_identity { OccurrenceId }` dropped, `NodeOccurrenceId` added to the
    existing `v2.std.node` import.
  - 02_parse.dag: `node_occurrence_minted(id: minted.id)` restored.

Caller census, read at call grain rather than by same-line grep — 17 sites across the two
functions: 15 pass `SyntheticOccurrence`, 1 passes `node.occurrence_id`, 1 is dag.dag's own
internal pass-through, and 1 passes the raw `minted.id`. Exactly one obedient caller existed and
it is the one that reddened main, so no second detonation is waiting. Changing the declared type
inverts which callers are correct, and by the finding below the typechecker cannot verify that
for us — hence reading, not building.

Green by execution, not by reasoning that the types now match: all 16 fold_lowering witnesses and
all 4 statement_let_bind witnesses PASS. The discriminating RED is main itself.

Also files one guarantee row, deliberately not fixed here. `v1.compiler.04_infer` gates
`arg_compat_diags` on `module_skips_direct_call_arg_check`, which is true for every module named
`v2.*`, keyed on the CALLER's module — so the direct-call argument-TYPE judgment is switched off
across the entire active v2 corpus. That is why nothing complained in EITHER direction for two
months. The row records the inversion that makes this class hard to see: a declaration that lies
is inert while every caller contradicts it in the same direction, and detonates on the first
caller who obeys it, so the at-risk population is callers who might get it right.

* The row is a correction to DESIGN §4b, not an observation beside it: confinement was measured and then treated as safety

DESIGN §4b names module_skips_direct_call_arg_check and reports it scoped entirely to the
direct-call argument-type judgment, not reaching sole_constructor — a positive finding, axis
retired. That finding is correct. It asked whether the exemption LEAKS; it never asked what the
exemption COSTS inside the judgment it is scoped to, and that judgment is the argument-type check
for the whole active v2 corpus. A reader who finds DESIGN's sentence and this row separately would
conclude the question was already answered, so the row now says which half it is.

Also records, against my own case: the arm is NOT unreasoned. direct_call_shape_wall_note states
the TYPE judgment's false-positive classes are representation gaps (brand aliases, optionality's
two forms, anonymous literals, expansion depth). That is a real stated reason, and it is why this
row asks for a measurement rather than a deletion. What is unmeasured is whether those classes
still fire in v2 and at what rate.

* The corrected declaration is TRUE but INERT: the judgment that would enforce it is the one switched off

Two changes, neither to the repair.

RUNG HONESTY, and it is the point. Correcting the parameter to NodeOccurrenceId reads like the §5
construction move -- fix the type and the wrong call stops compiling -- but the judgment that would
refuse the wrong call is `module_skips_direct_call_arg_check`, which exempts every `v2.*` caller
from the direct-call argument-TYPE check. So this PR lands a wall that CANNOT FIRE. The declaration
is now true, which is worth having on its own terms, but as enforcement it is inert, and shipping
it while believing the class is closed is exactly the rung inflation §4b calls worse than sitting
low. The class stays at its old rung; the next-rung trigger is the exemption's deletion, which the
guarantee row already names. (Product-direction ruling, swift-badger-524.)

ANNOTATIONS, grafted from gunbc#8856 (author's wording kept) with two additions of my own. With the
type wall inert, a §4c annotation is the only thing standing at these two sites -- it is not
evidence, and nothing mechanical reads it, which is precisely why it has to say why the shape is
what it is. Added to the callee: a bare OccurrenceId is the PAYLOAD INSIDE one of NodeOccurrenceId's
arms, never a member of the coproduct -- the modeling error in one line (smart-ram-730's phrasing,
the clearest statement of it anyone produced). Added to the caller: that the exemption is why no
mechanism stands there.

Green by execution after the annotations: 16/16 fold_lowering witnesses PASS, 0 FAIL.

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
briansrls added a commit that referenced this pull request Aug 22, 2026
…on family (#8827)

* Build-cache vertical: the systemd unit becomes a modeled value, not INI text gunbc spells itself

gunbc.build_cache_unit hand-concatenated "[Unit]", "[Service]", "Environment=..."
and "WantedBy=..." as string literals, which made gunbc a SECOND AUTHORITY for
systemd's unit-file syntax beside extdeps.systemd.unit_file -- the §3 nicknaming
that module's own header says it exists to prevent. Its systemd_medium_as_string_note
declared the debt and named the wrong exit: the dissolve-on pointed at
extdeps.languages, while the grammar had already landed at extdeps.systemd.unit_file.
The trigger had effectively fired and nothing noticed.

extdeps.systemd.unit_file grows the directives this consumer needs, each cited:
Before=/Requires= in [Unit] (systemd.unit(5), already in scope) and Slice= plus a
[Slice] section with MemoryMax=/MemoryHigh= (systemd.resource-control(5), a scope
the module did not previously cite and now does). SliceUnitFile joins the unit-type
coproduct, so a [Service] in a slice stays unwritable.

Drop-ins get their own type rather than a unit file with empty sections: a fragment
has no unit type, so SystemdDropIn is a LIST of known sections -- absence is absence
from the list, and an empty [Service] stays distinct from no [Service] at all.

The line view and the bytes are now one assembly read at two grains
(systemd_unit_file_lines / serialize_systemd_unit_file), which is what let the
build-cache render drop its own List<String> renderer instead of keeping one to
serve consumers that want lines.

The admissions stop carrying rendered text. EnsureBuildCacheAdmitted carried
unit_lines: List<String> and EnsureCompilePoolAdmitted carried slice_lines --
a decision type holding a serialized artifact of one realization. Both now carry
the modeled unit value, so bytes are produced by the single serializer at the
moment of writing rather than frozen into the decision.

NO HOST BYTES CHANGE, measured rather than reasoned. An earlier draft declared a
one-byte behavioural change (a trailing newline); that was wrong -- the heredoc
already supplied it. build_cache_ensure_daemon_body was rendered on this tree and
on origin/main with one compiler, for the CI instance (absent capacity) and the
session instance (present capacity), byte-identical both times, with a control
confirming the two trees genuinely differ. The write site concatenates the heredoc
terminator rather than joining it, which is what keeps that true.

95 witnesses across the six affected claim modules pass. Six new witnesses cover
the added grammar, including exact bytes for a slice unit and the empty-vs-absent
drop-in section. Three mutations were executed against the new walls -- a wrong
MemoryHigh spelling, a line view that stops sharing the serializer's assembly, and
a drop-in that always emits [Unit] -- each producing targeted reds.

Does NOT wire realize_provision_build_cache onto the admission; that stays open and
is zesty-wolf-464's lane on #8825.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Move the heredoc rationale to module-item grain: a body-level // refuses at the floor

CI's floor phase refused this branch with four typed, located diagnostics --
"source annotation sits inside a declaration body. Only module-item grain is
modeled" -- against the four-line comment I put inside build_cache_ensure_daemon_body's
list literal. DESIGN section 4c says exactly this: the .dag realization admits only
standalone leading // blocks attached to module-scope declarations, and body,
trailing, unattached and block-comment forms refuse until separately modeled.

The rationale is unchanged in substance and now sits above the declaration it
describes, which is where section 4c puts it.

WHY IT WAS NOT CAUGHT LOCALLY, recorded because the next person will reach for the
same tool: `gunbc run` is eval-only and never applies the annotation-grain gate, so
all 95 witnesses passed over source the floor refuses. `gunbc compile` is the strict
path, but a whole-corpus compile does not finish inside a single local window
(reconcile alone is 8-10 minutes, and the attempt timed out mid-emit), so the
known-positive probe for the gate was INCONCLUSIVE rather than green -- stated that
way rather than as a local check that passed.

What was actually verified before pushing is narrower and is the decidable proxy for
this class: the diff carries no indented // anywhere, checked mechanically across all
seven changed files. That is a syntactic property, not a run of the gate, and CI
remains the authority.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Merge origin/main: pick up #8818/#8828, whose witnesses were the 16 floor failures

The floor red on 77c9121 was 16 FAILs, all in v2.test.claim.fold_lowering (12) and
v2.test.claim.body_lowering.statement_let_bind (4), every one 'non-exhaustive pattern
match on OccurrenceId'. None of it is reachable from this branch's subject -- the diff
touches the systemd unit-file grammar and the build-cache renderers.

Diagnosed rather than assumed: main was GREEN at the same head class, and the two
failing families map exactly to two commits this branch did not contain -- #8818, which
routed the fold-lowering witnesses out of long/ and into the executing floor, and #8828,
which is the statement-form let lowering. This branch carried the test files at their
pre-#8818 home while the merged main-side routing executed them, so it ran witnesses
against a compiler half a lane behind them.

The remedy is the merge, not an edit: no file of mine is implicated, and the merge is
clean (#8818 deletes src/v2/test/claim/long/fold_lowering_test.dag outright).

Re-verified after merging: build_cache_ensure witness green, my grammar and renderer
changes intact, no body-level annotation in the diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Delete two _text projections that never gained a consumer

runner_ordering_drop_in_text and compile_pool_slice_text were authored as the
natural peers of their _lines projections and nothing ever called them -- not
in this module, not in the tests, not in the realize path. Under DESIGN §6
that is "a new artifact with no final consumer", and the honest fix is a
deletion rather than a note explaining why the surface exists.

The _lines peers stay: render_compile_pool_slice reaches compile_pool_ensure's
admission and runner_ordering_dropin_lines reaches the tests. build_cache_unit_text
stays: host_effect_realize writes it into the heredoc. serialize_systemd_drop_in
stays in the grammar module -- its byte-exact witnesses consume it -- and only
this module's now-unused import of it is dropped.

Found by review 54749 on #8827.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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