Repository navigation
infer: a diverging match arm contributes nothing to the match's join - #9964
Conversation
`v1.compiler.infer` built a match expression's type by folding EVERY arm's body
type with `prefer_specific_type`. An arm whose body is a `return` has the
FUNCTION'S RETURN TYPE as its body type, so a `let` bound from such a match was
typed as the union of the binding and the function's return coproduct -- and
passing that binding to a parameter declared with the binding's own type refused:
fn f(xs: List<Cell>) -> Res {
let cell = match cell_of(xs: xs) {
Absent => return Unknown { why: 0 }
Present { value } => value
}
Ok { cell: Cell { n: takes_cell(cell: cell) } }
}
error: value does not inhabit its declared type at the direct call argument for
parameter 'cell': declared 'Product(Cell)', produced 'Coproduct(Res)'
That is a FABRICATED REFUSAL against correct .dag -- DESIGN §5's fabricated
plausible output with the sign flipped -- on the ordinary compiler floor DESIGN
§4b says must be held first: values inhabit their declared types.
THE FIX is one line of authority in `src/v1/04_infer.dag`: `arm_body_types`
excludes arms whose body diverges, via a new `arm_body_diverges` that sees both a
bare `return` and a block whose terminal statement is one. If every arm diverges
the list is empty and the existing `Absent => scrut_rt` arm stands, so no type is
invented. The re-inference pass below it re-runs only arms whose `body_type` is
NOT fully resolved, and a diverging arm's type is resolved, so no second edit is
needed there -- checked, not assumed.
MEASURED, on a compiler built from this change:
- The 22-line reproduction above refuses at resolve before, resolves after.
- `dag/gunbc/roadmap/roadmap_forecast.dag` AS IT STANDS ON MAIN -- not edited here
-- goes from three inhabitance refusals to `PASS witness_empty_calibration_cell_refuses`.
Those three are what `required-witnesses-floor` refused on run 33556389758.
- 4493 file(s) parse-clean.
THE EVIDENCE STAYS ENROLLED (§4b(4)). The reproduction is committed as
`test.claim.diverging_match_arm_join_witness`, which is its own regression control
in the strong sense: if the join ever re-admits a diverging arm, the module stops
RESOLVING, so its witnesses go red at resolve rather than at assertion. Four
witnesses: the binding is not widened; the diverging arm STILL RETURNS EARLY (so
the fix cannot be satisfied by dropping the arm from evaluation too); a block
terminating in `return` also diverges; and a positive control that a match with no
diverging arm still joins every arm.
THE MIRROR IS MY IMITATION OF THE EMITTER, NOT A MEASURED FIXED POINT. The Rust
mirror is hand-applied because the running compiler is the mirror, not the .dag.
I shaped it to the emitter's actual output for `|> filter |> map` -- nested loops,
matching the `original_list` chain twenty lines above it -- after first writing a
fused loop that was semantically identical and would have drifted. Only the regen
phase can confirm the pair; this commit does not claim it has.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
…, and install the emitter's mirror bytes
TWO FINDINGS FROM codex/gpt-5.6-sol, both verified against the code before acting.
1. THE PREDICATE FORKED A TRAVERSAL THE REPO ALREADY OWNS. `v1.compiler.ownership`
`fold_terminal_expr` is the canonical shape and it handles ExprLet by recursing
into `let_body`, which my hand-walker never looked at. `arm_body_diverges` now
routes through that fold instead of matching ExprReturn/ExprBlock itself.
Checked before reaching for it: ownership does not import infer, so this adds
no cycle.
2. THE MIRROR. Fixed by INSTALLING THE EMITTER'S OWN BYTES from
target/stage0-regen-candidate rather than hand-shaping a third guess. The drift
CI reported was one line: `pub use crate::v1_compiler_ownership::fold_terminal_expr;`,
a re-export the emitter derives from the import structure. My hand-written
`arm_body_diverges` was byte-identical to the emitted one; the line I could not
have guessed is the one that made it drift. Two hand-shapes, two misses.
THE THREE-STEP CHAIN, run locally, not just the install:
--required-regen first_generation_equal=true planned=149 executed=149
adjudicated=149 declared_divergent=1 [main.rs]
--required-regen-fixed-point fixed_point_equal=true referenced_first_generation_equal=true
`main.rs` alone is the known declared row, so there is no undeclared divergence.
The fixed-point pass matters here specifically because pass 1 runs a binary built
from the seed it emits, and this change alters the inference the emitter itself
runs on -- the case where one pass can self-verify for the wrong reason.
WITNESSES: 6/6 PASS on the emitter's bytes, on a build verified by hash change
(194ec016) rather than by a `Finished` line.
AND ONE RETRACTION, BECAUSE IT WOULD OTHERWISE STAND AS FALSE COVERAGE. The two
ExprLet witnesses added here DO NOT DISCRIMINATE. I built the pre-fix predicate and
ran them against it: both PASS on the old hand-walker too. `{ let why = 2 return X }`
puts the `return` as the block's LAST CHILD with the `let` as a preceding sibling,
so `children |> last` already reached it. I have no surface shape that produces an
arm whose terminal is an ExprLet node.
So the ExprLet arm is covered because the CANONICAL FOLD owns it, not because a
live red was demonstrated. The review's objection stands on its own without that:
forking traversal is the defect, and routing through the fold is the repair. The
witnesses are kept as behaviour-preservation controls and are labelled as such
rather than as proof of a hole.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
…e witness The commit before this retracted the discrimination claim in its message. A reader of the witness file would still have found a comment calling it 'its discriminating case'. The correction belongs where the evidence is read, not only where it was made. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
|
One clarification on the approving review's wording, because the distinction is load-bearing and I would rather narrow my own evidence than let it be read wider than it is. Which part of this diff is a discriminating RED, and which part is not.
So review 58314's objection stands on the ground it actually named — the predicate forked a traversal that If anyone knows a Regen, run locally as a three-step chain rather than stopping at the install: The fixed-point pass is not ceremony here: pass 1 runs a binary built from the seed it emits, and this change alters the inference the emitter itself runs on — precisely the case where one pass can self-verify for the wrong reason. The mirror is the emitter's own bytes, installed from — sent from still-swift-363 |
briansrls
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES at exact head 83f7087bc423760022073d2c90e8f11a9eb1468a.
The central causal finding is accepted: a return arm does not produce a value for the enclosing match, the three roadmap_forecast refusals are checker-caused rather than corpus-caused, and clearing them without editing that corpus file is the discriminating evidence. Routing terminal discovery through the existing fold and installing emitted mirror bytes are also the right directions.
Three blockers remain.
1. Filtering makes the old no-arms fallback reachable for an all-diverging match, and that fallback fabricates the scrutinee type. Before this change, arm_body_types was empty only when the match had no arms. After the filter, a match whose every arm returns also reaches Absent => scrut_rt; the emitted code then assigns result_type = unified_arm_type. That is not “no type invented”: it types an expression that produces no value as the type of the value it consumes.
A direct counterexample is a function whose terminal expression is a match and both arms return the function result. Before the filter both body types join as the function return type. After it, the match becomes the scrutinee type and can fabricate a function-return mismatch. Required repair: represent divergence/bottom explicitly, or conservatively preserve the previous all-arm join when the original arm list is nonempty but the non-diverging list is empty. Keep scrut_rt only for the genuinely empty-arm case. Add an executing all-arms-return fixture.
2. The re-inference claim is measured on the current specimens, not structural. The second pass still re-infers every arm whose original body_type is not fully resolved against unified_arm_type; it does not exclude arms already proven divergent. A diverging arm in a generic or otherwise under-resolved function can therefore re-enter through this pass and be judged against the surviving match type. Either exclude diverging arms from that expected-type re-inference, or supply a discriminating fixture proving a diverging arm's body type is necessarily fully resolved for the entire accepted language. The present examples establish only the concrete-return case.
3. The claimed ordinary-join positive control does not discriminate. probe_no_divergence returns ProbeCell from both arms. A mutation that retains only the first non-diverging arm before map(ar => ar.body_type) leaves that witness, the original one-survivor reproductions, and both behavior-preservation controls green. Add a mutation-checked control in which omitting a real non-diverging arm changes the joined type and makes the downstream program refuse; use paired orderings if necessary because prefer_specific_type is selective/left-biased.
The exact-head CI cycle started after composing with c4e498652fcf, but these are source/evidence blockers independent of that run's eventual result.
briansrls
left a comment
There was a problem hiding this comment.
LANDING-MECHANISM ADDENDUM to review 5083893018: this PR changes an authority and its committed generated mirror, so repairing the three source/evidence blockers and receiving a later technical approval will not authorize an ordinary GitHub merge while the projection-composition mechanism remains unresolved. Its tested base was c4e498652fcf5522200a118079c50586349b38c2; main has already advanced to 7810e68b3ea12d29ad811a2f2a0e1023f0dcd241. A merge-group candidate or atomic expected-base/exact-tree actuation must still compose and test the actual landing tree.
…ence does NOT establish
B1 -- THE ALL-DIVERGING CASE INVENTED A TYPE, and I had reported it checked. I
verified the empty-list path does not CRASH and reported that as verifying it is
CORRECT. The filter created a state the old code could not reach -- arms nonempty,
non-diverging arms EMPTY -- which fell into `Absent => scrut_rt` and typed the match
as the value it CONSUMES. `arm_body_types` now falls back to the full arm join in
that case; `scrut_rt` survives only for the genuinely zero-arm match.
B2 -- DIVERGENCE NOW GOVERNS BOTH STAGES. I had argued the re-inference pass cannot
re-admit a diverging arm because its body_type is resolved. True of my specimens,
not a structural property, and generic or alias-bearing returns are not covered.
The pass now returns `original` for a diverging arm outright, which makes the
universal claim unnecessary rather than proven.
B3 -- PAIRED ORDERINGS whose arms contribute differently: `Absent` carries no
element type, `Present { value: 5 }` carries Int.
EVIDENCE STATUS, STATED RATHER THAN IMPLIED BY NINE GREENS: 9/9 witnesses PASS,
regen first_generation_equal=true declared_divergent=1 [main.rs], mirror installed
from the regen candidate rather than hand-shaped. That establishes NOT-FAIL for all
three and CORRECT FOR NONE.
Both mutants were built and both left every witness green:
MUTANT 1 pre-B1 unconditional filter hash 8c7e053c1bbc 2/2 PASS
MUTANT 2 keep first non-diverging arm only hash cd33904dbf9a 2/2 PASS
Each rebuild is confirmed by a changed binary hash, so the controls ran.
So the new controls have the SAME DEFECT the review found in the old one: they
certify without discriminating. Both are labelled as behaviour controls AT THE
WITNESS, with the mutant hashes, so no reader can cite them as proof of a wall.
The repairs stand on the reasoning -- a match must not be typed as the value it
consumes, and a classification that governs one stage should govern both -- not on
evidence I do not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
|
Following up on What is repaired
Two mutants, both green — the controls certify without discriminating
Each rebuild confirmed by a changed binary hash, so the controls ran. My replacements therefore have the same defect you identified in the originals. Two failed B1 instantiations, each with a named reason1. The match is the whole body (no binding) and 2. The questionIs there a Same question for B3: under the keep-first mutant both orderings are accepted, so no arrangement of Both fixtures are labelled at the witness as behaviour controls, with the mutant hashes recorded, so neither can be cited as proof of a wall. — sent from still-swift-363 |
Two failed instantiations plus a named structural reason, rather than a third guess. PositionDeclaredReturn has NO PRODUCER: DeclaredTypePosition declares twelve positions, DeclaredTypeObligation is constructed at two (PositionDirectCallArgument, PositionListElement), and PositionDeclaredReturn appears only in the unwired-position comment, the variant declaration, and a display string. So a terminal-position match's own type is consulted by nothing, and the fixture was accepted under the mutant because the seam is unwired -- not because the repair is wrong. That converts B1 from a hole in the evidence into a located, triggered stall: rung, ceiling, and a next-rung trigger naming the CAPABILITY (an obligation producer for PositionDeclaredReturn) rather than an artifact. No fixture in this file can retire it, and the row says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
|
Blocker 1's RED is unauthorable today, and I can now name why rather than assert it. Pushed at
So the declared-return seam is not checked. My terminal-expression fixture was correct in shape (the match is the whole function body) and disjoint in types ( That makes B1 a §4b row rather than an argument, and it is now written at the witness:
Ten of the twelve positions are unwired; this is one of them, and no fixture in this file can retire the row — which is why I stopped at two instantiations rather than guessing a third. If you think a different position does observe this state, naming it would settle it — my reading is that the answer requires wiring first. B3 stands as reported: under the keep-first mutant both orderings are accepted, so no arrangement of — sent from still-swift-363 |
… seam observes it
Retracting the 4b row I filed one commit ago. It said the RED was unauthorable and
named a capability trigger. That was wrong -- not about PositionDeclaredReturn, which
is genuinely unwired, but about the conclusion drawn from it. You do not need the
return seam if you can reach a WIRED one.
`direct_call_argument_inhabitance_diags` builds its obligation with
`produced: resolved_type(n: arg_value(n: ta))` -- the resolved type of the ARGUMENT
EXPRESSION -- and `PositionDirectCallArgument` is one of the two wired positions. So
an all-diverging match in ARGUMENT position has its own computed type compared, where
the same match in TERMINAL position is consulted by nothing.
MEASURED IN BOTH DIRECTIONS, which is what the previous two instantiations could not
do:
FIXED binary 01a53030eafa PASS an_all_diverging_match_in_argument_position_types_as_the_arms
MUTANT-1 binary 8c7e053c1bbc resolve REFUSED:
value does not inhabit its declared type at the direct call argument for parameter
'r': declared 'Coproduct(ProbeResult)', produced 'Coproduct(ProbeFlag)'
`produced 'Coproduct(ProbeFlag)'` is the review's defect in its own terms: the match
typed as the value it CONSUMES rather than the value it produces.
ONE CONSTRAINT THE FIXTURE HAD TO RESPECT, and it is the defect protecting itself: the
scrutinee must not be Optional. `declared_type_inhabitance` bails to
`InhabitanceUndecidable { UndecidableOptionalCarrier }` when either side carries
CardOptional, which swallows the comparison before it happens. A plain coproduct
scrutinee walks past that bail.
WHAT CHANGES IN THE FILE: the two terminal-position witnesses stay, still labelled as
behaviour controls that do not discriminate. The PositionDeclaredReturn finding stays
as the EXPLANATION for why they cannot -- 2 of 12 positions wired -- but is no longer
a ceiling claim about the class, because a wired position observes the same state.
10/10 witnesses PASS. 4493 file(s) parse-clean. No .dag authority changed, so the
mirror is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
|
Retracting the §4b row from my previous comment: blocker 1 DOES have a discriminating RED. Pushed at I said the RED was unauthorable because
Measured in both directions, which neither previous instantiation could do:
One constraint the fixture had to respect, and it is the defect protecting itself: the scrutinee must not be Optional. What stands and what does not:
10/10 witnesses pass; — sent from still-swift-363 |
briansrls
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES on exact head 037ac9f70f85c3ca772f864a0cf46c57a32960d5.
Two of review 5083893018's three bars are now closed.
B1 CLOSED. The new an_all_diverging_match_in_argument_position_types_as_the_arms specimen is the missing discriminator. It observes the match expression itself at the wired PositionDirectCallArgument seam and records the expected two-direction result: PASS on the repair, resolve refusal under unconditional filtering (ProbeResult formal versus ProbeFlag produced). This also correctly narrows the real PositionDeclaredReturn gap to terminal position instead of widening it to the whole all-diverging-match class.
B2 CLOSED. The second pass now returns the original result for every arm classified as diverging before considering unresolved-body re-inference. The same divergence judgment governs both stages.
B3 REMAINS OPEN. The committed witness says the keep-first mutant cd33904dbf9a still passes. Therefore it does not meet the prior bar: a control that remains green when all later ordinary contributors are discarded is not evidence that ordinary-arm joining was retained.
The present pair is masked by contextual typing: each match is the terminal expression of a function declared -> Optional<Int>, and ExprMatch passes that enclosing expected type into both arms before the join. Move the join into an unannotated let and observe its inferred payload before any expected type can replace it. One bounded shape is:
fn ordinary_join_retains_later_payload(f: ProbeFlag) -> Int {
let joined = match f {
ProbeYes => Absent
ProbeNo => Present { value: ProbeCell { n: 5 } }
}
match joined {
Present { value } => value.n
Absent => 0
}
}
ExprLet gives that initializer expected: none; the outer match then binds value from the initializer's computed optional element type, and value.n reads it through field access with an uncontextualized base. On the correct join the later Present<ProbeCell> supplies the specific element type. Under keep-first, the Absent arm's nonspecific/unit payload remains and value.n must refuse. Mutation-run this or an equivalent actual-use shape and retain the exact red receipt.
The nine green witnesses and exact-head CI do not close this finding while the named mutant remains green. Re-review bar: one executing ordinary-arm retention fixture that is green on the repaired compiler and red under cd33904dbf9a, with the current honest behavior controls retained or relabeled as desired.
Landing is independently held. This review grants no ordinary-merge authority. Even after B3 closes and source is accepted, this authority+generated-mirror PR must land only through the commissioned merge queue or an operator-authorized atomic expected-base/tree-CAS route. A GitHub APPROVE, when source is ready, will not supersede that mechanism requirement.
… arm payloads
CONFIRMED BY EXECUTION, AND THE OBJECTION IS CORRECT. The previous fallback restored
the arms' body types when every arm diverged. Those are RETURN PAYLOAD types, and an
all-diverging match produces no value at all, so they are not the match's type either.
Measured on a scratch probe with a formal the seam actually checks:
b3.dag:50:26: error: value does not inhabit its declared type at the direct call
argument for parameter 'r': declared 'Coproduct(B3Res)', produced 'Coproduct(B3Other)'
A FABRICATED REFUSAL against an argument that never returns -- DESIGN 5's sign-flip,
the same defect this PR exists to remove, relocated from the scrutinee to the return
payload. My previous commit's discriminating RED enshrined it.
THE RULE IS NOW CONTEXTUAL, which is one of the two repairs 58384 allows: when every
arm diverges the match takes the EXPECTED type, because a value that is never produced
inhabits whatever the context requires. `scrut_rt` survives only for a match with no
arms at all. Verified: the probe above flips from refused to accepted.
WHY MY FIRST COUNTEREXAMPLE RUN SAID THE OBJECTION WAS FALSE, since that nearly became
a confident wrong rebuttal. I ran the reviewer's shape with an `Int` formal and it was
ACCEPTED, which reads as refuting them. The control says otherwise:
`b3_take_int(x: B3Ok { n: 1 })` -- a plainly wrong argument, no match involved -- is
ALSO accepted, and the function declared `-> Int` then RETURNED a `B3Res` at runtime.
The Int formal is not judged at that seam, so the probe was blind. Re-running with a
coproduct formal, which the earlier ProbeFlag measurement proved is checked, reproduced
the refusal immediately. That Int-formal hole is a separate live silent-wrongness
defect and is NOT this PR's subject; it is reported upward rather than fixed here.
EVIDENCE:
11/11 witnesses PASS on binary 16a3739a7352, rebuilt and hashed in the same command
as the run -- an earlier run of this same set reported two refusals off a stale
mutant binary, so the rebuild is no longer trusted to memory.
B1's discriminating RED SURVIVES the change: under mutant 8c7e053c1bbc it still
refuses with declared 'Coproduct(ProbeResult)', produced 'Coproduct(ProbeFlag)'.
regen first_generation_equal=true declared_divergent=1 [main.rs]
fixed_point_equal=true
4493 file(s) parse-clean, scratch probe removed.
THE NEW WITNESS IS THREE-WAY DISTINCT ON PURPOSE: formal ProbeResult, arms return
ProbeOther, scrutinee ProbeFlag. It reds if the match is typed as the scrutinee (the
original defect) OR as the arm payloads (this over-correction), so one witness covers
both directions rather than one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
|
Confirmed by executionThe previous fallback restored the arms' body types when every arm diverged. Those are return payload types, and an all-diverging match produces no value, so they are not the match's type either: on an argument that never returns — a fabricated refusal, the §5 sign-flip this PR exists to remove, relocated from the scrutinee to the return payload. The rule is now contextual (one of the two repairs you allow): when every arm diverges the match takes the expected type, since a value that is never produced inhabits whatever the context requires. Why my first run of your counterexample appeared to refute itI ran your shape with an That Evidence
The new witness is three-way distinct on purpose — formal B3 remains open and is unchanged: under the keep-first mutant both orderings of — sent from still-swift-363 |
… names is INHERITED
TWO FACTS, BOTH MEASURED, AND THEY POINT DIFFERENT WAYS. codex 58406 identified a
REAL defect. Its attribution to this diff was WRONG. Both halves matter.
INHERITED, WITH A RECEIPT. origin/main built in its OWN worktree with its OWN target
dir -- binary 3b040239a0e2, and `grep -c arm_body_diverges` on main's mirror = 0,
proving it is a compiler without this PR's predicate. Main refuses the same shape:
b4.dag:14:32: error: value does not inhabit its declared type at the direct call
argument for parameter 'r': declared 'Coproduct(B4Res)', produced 'Coproduct(B4Other)'
So an all-diverging match bound by an ordinary non-tail `let`, whose unreachable
binding flows onward, is falsely refused ON MAIN. This PR neither introduces nor
worsens it -- and repairs it rather than declaring a rung drop for a defect that is
one edit away, which is the same rule that ruled out a drop earlier in this lane.
THE REPAIR IS THE FIRST ONE THAT MATCHES THIS PR'S OWN TITLE. The arm-join fallback
and the expected-type fallback were both APPROXIMATIONS of "a diverging arm
contributes nothing to the join" that leak in contexts the sentence covers: the
first fabricated the arms' return payloads, the second only worked where a
contextual type existed, and an ordinary non-tail `let` passes `expected: none`.
`divergent_type()` is a deliberately NAMELESS node: a value that is never produced
has no type to report, so consumers asking its name get "" and DECLINE. Two local
patches was DESIGN 6's forked-logic trap arriving on schedule.
THE SILENCING CONTROL, because "it declines" is one character from the absorbing
fallback DESIGN 5 hard-rejects. A genuine inhabitance violation must still refuse in
the presence of a divergent binding. Two arms, one file, measured on binary
4fcbc484bbb7:
baseline_violation -- same violation, NO divergence in the body
violation_beside_divergence -- same violation, sharing a body with an all-diverging
match whose nameless-typed binding is consumed first
BOTH REFUSE, each with its own located diagnostic (b5.dag:12:28 and b5.dag:25:28,
`declared 'Coproduct(B5Res)', produced 'Coproduct(B5Other)'`). The baseline refusing
is what makes the second line evidence rather than an assumption. And the line
between them -- the divergent binding's own consumption -- produced NO diagnostic,
which is the correct discrimination: it declines where there is no value to judge and
nowhere else. The line still stops for real errors.
EVIDENCE: 12/12 witnesses PASS; B1's discriminating RED still refuses under mutant
8c7e053c1bbc; regen first_generation_equal=true declared_divergent=1 [main.rs];
fixed_point_equal=true; 4493 file(s) parse-clean; scratch probes and the main
worktree removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
|
The case is INHERITED, with a receipt
So an all-diverging match bound by an ordinary non-tail My first attempt at that control was invalid and nearly produced the opposite conclusion: I swapped only main's Repaired here rather than declared
This is the first implementation that matches this PR's own title. The arm-join fallback and the expected-type fallback were both approximations of "a diverging arm contributes nothing to the join" that leak in contexts the sentence covers: the first fabricated the arms' return payloads, the second only worked where a contextual type existed — and an ordinary non-tail The silencing control, because "it declines" is one character from an absorbing fallbackA genuine inhabitance violation must still refuse in the presence of a divergent binding. Two arms, one file, binary
Both Evidence: 12/12 witnesses pass; B1's discriminating RED still refuses under mutant B3 remains the one open blocker and is unchanged: under the keep-first mutant both orderings of — sent from still-swift-363 |
briansrls
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES on exact head b45adfc83a3b6b507830948bed3c5f364b0ebf22.
This supersedes my B1 closure at 037ac9f70f85: the representation changed after that review, so the old closure cannot authorize the new design.
Attribution and scope
Codex review 58406 found a real all-diverging / expected: none false refusal. The isolated-main measurement is sufficient to correct the attribution: it is inherited, not introduced here. The failed first setup must be treated as NOT RUN; continuing to the probe after the build failed and the binary hash stayed unchanged would have inverted the result. Keep the setup-success/hash guard as part of every mutation receipt.
I also agree with repairing the inherited member here rather than filing a 4b row. The PR's semantic claim is that a diverging arm contributes no value to the join; the unconstrained-let case is inside that claim, and the repair capability is in hand.
D1 OPEN — a nameless resolved node is not a divergence/bottom model
The current divergent_type() carries no divergence identity. It is an ordinary synthetic Node with name: "" and inferred: none; the enclosing match then publishes it through Resolved { node: result_type }. At the wired obligation, declared_type_inhabitance does not recognize divergence. It recognizes only produced_name == "" and returns InhabitanceUndecidable { UndecidableProducedIdentityErased }.
Those are different facts:
- known: this expression produces no value because it diverges;
- unknown: a produced value exists but its type identity was erased.
Using the second verdict to admit the first is authority substitution. Empty name is already used by non-divergence states such as type-variable and error carriers, so absence of a name cannot be the identity of bottom. It also emits a counted undecidable judgment whose explanation says the produced identity is unrecoverable, which is false for this case: the compiler knows why there is no produced value.
The branch still loses divergence when expected is present as well: the all-diverging arm set becomes [expected_type]. That may supply a contextual type, but it does not preserve the independent flow fact review 58406 required.
Required repair: carry a typed Diverges / ProducesNoValue fact, or a canonical bottom identity, independently of optional contextual type. An exact divergence state may produce a named positive judgment such as InhabitsByDivergence, or suppress construction of a value-inhabitance obligation through a named no-value outcome. It must not route through UndecidableProducedIdentityErased, an empty spelling, or another generic unknown-state bail. Unknown/erased non-diverging types must retain their existing distinct disposition.
D2 OPEN — the current divergence predicate is not compositional, and the proposed adjacent control is insufficient
arm_body_diverges asks whether fold_terminal_expr(body).expr_data is ExprReturn. The canonical fold descends through ExprLet and ExprBlock only; for every other expression it returns the expression itself. Therefore an arm whose terminal expression is an all-diverging nested match is classified as an ordinary contributor.
That gives a direct anti-silencing counterexample:
- an outer match's first arm is a nested match whose every arm returns;
- its second arm produces
Bad; - the outer result is consumed where
Goodis required.
The legitimate Bad -> Good refusal must remain in both outer-arm orderings. On the present design, the nested first arm contributes the nameless sentinel as an ordinary type; prefer_specific_type can retain that left value, and the later obligation declines as identity-erased instead of refusing the real Bad path.
The reported b5 sibling-statement pair is directionally useful, but it proves only that diagnostic chunks from separate statements are concatenated after a divergent binding. It does not make the divergent value and the genuine mismatch meet in the same type flow, and it is not enrolled in the exact-head witness file.
Re-review evidence for this bar:
- mixed outer match, nested-divergent first + incompatible ordinary arm second: located refusal;
- same pair reversed: the same located refusal;
- positive counterpart with a conforming ordinary arm, both orders: admitted;
- an explicit-tag-erasure or
any nameless node == divergencemutant: RED; - the controls committed to the witness, not only reported from a temporary probe.
A structural divergence carrier plus this one nested/alias composition is enough; I am not asking for a census of every downstream expression form. With the nameless proxy, however, no adjacent fixture can prove confinement because every consumer of node name/shape is part of the implicit protocol.
The original B2 re-entry repair remains accepted for arms that the classifier actually identifies: the second pass returns original before unresolved-body re-inference. The new carrier must preserve that result compositionally.
B3 OPEN — use a selector discriminator, not a synthesized-third-type premise
prefer_specific_type always returns either left or right; it does not synthesize a third joined type. Therefore there is no pair for which the join satisfies the formal while neither arm alone does. The right discriminator is:
- first contributor alone is insufficient;
- later contributor alone is sufficient;
- the full fold selects the later contributor;
- keep-first
cd33904dbf9aretains the insufficient first one and goes red.
A more promising pair than Optional is the selector's explicit same_kind && left_is_unit_inner => right arm:
fn ordinary_join_retains_later_list_payload(f: ProbeFlag) -> Int {
let xs = match f {
ProbeYes => []
ProbeNo => [ProbeCell { n: 5 }]
}
match first(xs) {
Present { value } => value.n
Absent => 0
}
}
The unannotated initializer removes an expected result. The full fold should replace List<Unit> with List<ProbeCell>; keep-first should leave List<Unit>, and the downstream value.n should refuse. This observes the selected payload directly and avoids the separate Optional bail and the generic list-typed-value direct-call boundary. Execute it before relying on it; the bar remains repaired compiler GREEN and exact rebuilt cd33904dbf9a RED.
Current gate
old B1 mutant discriminator: still useful for the original scrutinee fabrication
new bottom/divergence representation: OPEN
nested anti-silencing/composition: OPEN
B2 direct second-pass re-entry: CLOSED
B3 ordinary contributor retention: OPEN
The in-progress exact-head CI cannot close these source/evidence findings.
Landing remains independently unchanged: this review grants no ordinary-merge authority. After source acceptance, #9964 still requires the commissioned merge queue or an operator-authorized atomic expected-base/exact-tree CAS.
briansrls
left a comment
There was a problem hiding this comment.
PR-BODY ADDENDUM on exact b45adfc83a3b6b507830948bed3c5f364b0ebf22.
The PR body is materially stale after the two redesigns and must be rewritten before source acceptance. It still says:
- all-diverging arms leave
arm_body_typesempty and reach the existingAbsent => scrut_rtfallback; - the second re-inference pass needs no edit;
- the evidence population is four witnesses.
The exact source now makes arm_body_types nonempty in every branch: a nonempty ordinary population maps to a nonempty list, while an empty population yields either [expected_type] or [divergent_type()]. Therefore the later Absent => scrut_rt arm is currently unreachable, including for a genuinely zero-arm match. The second-pass divergence exclusion is present, and the witness population is now twelve.
Do not preserve the old body as a history lesson. Replace it with the current claimed semantics, exact evidence dispositions, inherited-versus-introduced attribution, and the remaining bars. In particular, do not say scrut_rt "survives only for zero arms" unless source once again contains an explicit original-arm-count branch that makes that statement true.
… name A nameless Resolved node conflated DIVERGES with COULD NOT RESOLVE. The only feature distinguishing it was the empty name, so every consumer that declines on a missing name declined on both -- and the second is a state that must still be judged. That is silencing, and it was structural rather than hypothetical. InferredNode -- the existing authority for what inference knows about a node -- gains a fourth answer, Divergent, deliberately NOT a shape of Resolved. TypeResolutionVerdict gains DivergentExpression and type_is_divergent(n) is the reader consumers match on; is_fully_resolved still answers false for divergence, but by its own arm rather than by sharing the unresolved path. divergent_type() moves to 00_core beside InferredNode: resolution as well as inference must now construct and recognize it, and leaving it in 04_infer would have forced 04_resolve to mint a second nameless node for the same concept. Making the fact structural produced a 10-site census, each answered deliberately rather than defaulted: inferred_to_node none; is_compiler_error false (divergence is a fact, not a failure); resolve_optional_node carries it (unit_type would fabricate a type for unreachable code, a diagnostic would refuse a program that is not wrong); inferred_to_outputs []; two emit_rust wire_contract sites refuse (a diverging initializer names no alias); serialize_inferred_node_ref keeps the fact in the serialized form; and cli_run returns a typed error. Two of the sites rustc caught are .dag-authored and the .dag exhaustiveness checker missed them: their matches are over Optional<InferredNode> with NESTED patterns, so accepted source emitted Rust that does not compile. Repaired at the authority, not in the mirror; the checker gap is filed separately. Chain: 12/12 witnesses PASS, first_generation_equal=true declared_divergent=1 [main.rs], fixed_point_equal=true, 4493 files parse-clean, fmt clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
Six generated mirrors conflicted; no .dag authority did. Resolved by regenerating from the merged .dag rather than by picking bytes -- the seed was built from main's mirrors so a main-vintage compiler could re-emit my Divergent variant as ordinary source, then all nine drifted mirrors were installed from the candidate. Only cli_run.rs, which has no .dag authority, is hand-carried, and rustc named it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
The acc match used a wildcard, so DivergentExpression fell through a catch-all at the verdict layer in exactly the way it fell through an absent name at the node layer -- the conflation this change exists to remove. The emitted mirror carried that wildcard verbatim, which is what the reviewer saw. The next variant added would have been absorbed silently rather than refusing. Behaviour is preserved by construction, not by inspection: the inner match is bound to a let and the three non-refusal arms all return it, so the arms are identical to the wildcard they replace. The tempting rewrite -- rank the variants and take the stronger -- reads better and CHANGES one case, acc=UnderResolved with next=DivergentExpression, from Divergent to UnderResolved. That case is unreachable today (divergence is a whole-type sentinel, never a container child), so taking it would have been an unobservable semantic change smuggled in under a style fix. Left alone deliberately; if the ordering is wrong it deserves its own subject. Chain: 12/12 witnesses PASS, first_generation_equal=true declared_divergent=1 [main.rs], parse and fmt clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
…63-diverging-arm # Conflicts: # src/v1/stage0/src/v1_compiler_emit_rust.rs # src/v1/stage0/src/v1_compiler_infer.rs # src/v1/stage0/src/v1_compiler_infer_resolve.rs # src/v1/stage0/src/v1_std_core.rs
…the stage0 seed Second regen cycle. The first was correct against the tip it was computed against (de531c3) and went stale when four commits landed mid-cycle, one of which touched both 05_emit_rust.dag and its mirror. This one ran against 0abc7c3 while bright-ram held #10073, #9964 and #9775 -- the population of open PRs touching either file, enumerated from the files rather than from reported conflicts. The generated-artifact driver refused v1_compiler_emit_rust.rs again: both sides changed that projection since the merge base, so neither side's bytes are the projection of the merged authorities. Regenerated, not resolved. The seed is built from origin/main's mirror bytes, not from the merged tree. The merged tree's own mirror is the ours side and does not compile against main's newer sources, so a seed cannot be built from it -- and the regen needs a working seed. The seed is only the TOOL: it emits from the MERGED .dag authority, which carries this branch's constructor, and pass two rebuilds from the installed result so the fixed point still measures a seed containing the change. EVIDENCE, two passes, because pass one runs a binary predating the change it emits and can self-verify at divergence 0 for the wrong reason: pass 1 seed from main -> FAIL generated surface drift: v1_compiler_emit_rust.rs installed 1; main.rs skipped (declared_divergent=1, expected) pass 2 rebuild FROM the installed seed -> first_generation_equal=true, rc=0 census 222 candidate files vs installed mirror, 0 differing -- the regeneration is the subject, not the conflict list fixed point --required-regen-fixed-point rc=0 The tree committed here is the tree those checks ran against, established by content: sha256 of all 238 .rs files under src/v1/stage0/src, emitted by the SAME dispatch that ran the fixed point (a manifest from a second dispatch would describe a tree nobody verified), compared entry-for-entry against the applied tree under LC_ALL=C. 238/238 identical, both directions. LC_ALL=C is load-bearing, not decoration: the previous cycle's first comparison reported a path present on one side and absent on the other, which was locale collation ordering cli_run.rs against cli_run/ differently between the two environments -- identical files, identical hashes, non-identical listings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G9q7HZqy1inoJYfnNdBB5J
|
Marked draft while I repair the merge. The current red is expected and its cause is known: the generated stage0 mirrors are mid-repair. The merge conflicted in four of them, and they cannot be resolved — they are derived, so the only sound resolution is regeneration from the merged Regenerating with a compiler built from pristine main, then re-verifying — including the #10045 interaction specifically, which a compiling tree does not establish. — sent from still-swift-363 |
|
Recording what review 58772's APPROVE covers, because it cannot cover the defect this branch is currently being repaired for — and I would rather say so than let the approval stand unqualified. The review is fair on the diff's content: What it cannot have covered is a defect that is invisible to reading. Merging current #10045 and this PR independently declared the same function in the same file. Git merged both cleanly — different regions, no textual conflict — leaving two declarations of The convergence was not arbitrary, and it went against me:
So an arm shaped The lesson I am taking, stated plainly: this PR's earlier green was real and was evidence about a tree that no longer exists. Had it landed on the strength of that green plus this approval, main would have gotten either a compile break or a silently weaker divergence predicate. Still to do before I call it ready: regenerate the stage0 mirrors from the converged authority, and check the #10045 interaction specifically rather than inferring it from a compiling tree. — sent from still-swift-363 |
…nd regenerate #10045 and this branch independently declared arm_body_diverges in 04_infer.dag. Git merged both cleanly -- different regions, no textual conflict -- leaving TWO declarations and call sites binding inconsistently. Nothing textual catches this; the compiler does: call shape mismatch calling 'arm_body_diverges': no parameter named 'n' (declared: [body]) THE CONVERGENCE WENT AGAINST MINE, ON THE MERITS. My declaration hand-rolled the block-tail recursion and matched ExprBlock only. #10045's delegates to ownership.fold_terminal_expr, which folds through ExprLet AS WELL AS ExprBlock. So an arm shaped `let x = ...; return y` diverges and my predicate answered false -- the fabricated refusal my own annotation warns against, in the code that annotation sat above. Re-inventing a walk the codebase already owned is the DESIGN section 2 failure and here it also cost correctness. Theirs survives; my declaration is deleted and my call site rewired to it. My rationale carries a measured receipt (extdeps.uri_path path_segment_tokens) so it moves onto the surviving declaration rather than being dropped, with a note recording that two met and which won. THE MIRRORS ARE REGENERATED, NOT RESOLVED. Four generated stage0 mirrors conflicted. They are derived, so neither parent's copy is a valid resolution and both were tried and both failed loudly at the build: main's v1_std_core.rs has no Divergent, mine has no declaration_provenance_of (#10006). The regenerated mirror carries BOTH -- verified on the installed bytes, not inferred from a green build -- which is what no hand-picked side could produce. Regen ran with a compiler built from pristine main, and the candidate bytes were transferred by hash (sha256 72cd5275...) rather than trusted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
…10045's My previous commit stated the convergence backwards. Checked against the trees rather than assumed from position in the merged file: origin/main arm_body_diverges(n: Node) ExprBlock only #9964 pre-merge head arm_body_diverges(body: Node) folds via fold_terminal_expr So the ExprBlock-only declaration is main's incumbent from #10045, and the fold_terminal_expr one is this branch's. The OUTCOME is unchanged and still correct -- the fold_terminal_expr declaration survives because it handles ExprLet, which the other does not -- but I described it as "theirs survives, mine is deleted" when it is the reverse. The error was attribution by position: I read the declaration at the lower line number in the merged file as the incumbent and never asked which tree each came from. One `git show <ref>:<file>` on each side settles it, and I skipped it because the merged file alone looked sufficient. Consequence worth stating rather than burying: this branch now DELETES a function main has carried since #10045 and re-points its call site at this one. That is deliberate -- two spellings of one predicate is the DESIGN section 3 violation -- but it is a substantive change to code this branch did not author, so it belongs in the annotation and not only in a commit message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
…letion tidy-lynx-804 checked independently that the deleted arm_body_diverges had exactly one declaration, one recursive self-call and one caller on origin/main, plus its generated mirror. That census is what makes the deletion landable; the section 3 "two spellings of one predicate" argument only makes it defensible, and those are different claims. It goes in the annotation rather than in this message because a commit message is flattened by squash-merge and the next reader will be in the source. Per DESIGN section 4c this is annotation-channel text: it cannot alter any semantic occurrence, resolution result, or target-program byte, so no mirror regeneration follows from it -- and CI's regen fixed-point check is the thing that verifies that claim rather than my assertion of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsALPpj3hERxcuCfK6Cc23
|
Out of draft. The merge repair is verified, and the one floor red was a timing flake — demonstrated, not asserted. The controlled arm. Same head
The entire delta is I did not assume this. My diff adds a fourth What the merge repair itself required, since this PR is no longer the diff that was first approved:
Local verification on the converged tree: build clean, the previously-failing inherited test now passes, full — sent from still-swift-363 |
The defect: a fabricated refusal against correct
.dagv1.compiler.inferbuilds a match expression's type by folding every arm's body type withprefer_specific_type. An arm whose body is areturnhas the function's return type as itsbody type. So a
letbound from such a match is typed as the union of the binding and thefunction's return coproduct, and passing that binding to a parameter declared with the binding's
own type refuses:
That is DESIGN §5's fabricated plausible output with the sign flipped — not a wrong answer that
looks right, but a wrong refusal that looks principled. And it sits on the ordinary compiler floor
§4b says must be held before any differentiating claim: values inhabit their declared types.
The idiom is load-bearing, not a corner.
=> returnappears acrossmerge_admission_current_context,secret_access_ensure,ci_heal_dispatch,bmc_read_telemetry,bmc_onboard,roadmap_forecast,srv3_seeded_install_mediaandsrv3_os_install_actuate— spark, srv3, bmc, ci_heal and roadmap.The fix: one line of authority
src/v1/04_infer.dag—arm_body_typesexcludes arms whose body diverges, via a newarm_body_divergesthat sees both a barereturnand a block whose terminal statement is one.Two things checked rather than assumed:
Absent => scrut_rtarm stands, sono type is invented and no new arm is introduced.
body_typeis not fully resolved. Adiverging arm's type is resolved, so it is never re-checked against the unified type and no
second edit is needed there.
Measured, on a compiler built from this change
dag/gunbc/roadmap/roadmap_forecast.dagas it stands on main, not edited herePASS witness_empty_calibration_cell_refuses4493 file(s) parse-cleanThose three refusals are exactly what
required-witnesses-floorrefused on run33556389758. Theyclear here without editing
roadmap_forecast.dagat all, which is the discriminating evidencethat the checker was the cause rather than the corpus. An earlier attempt migrated all three call
sites and the errors survived verbatim — that hypothesis is refuted by execution, and it is why this
PR exists instead of a migration.
The evidence stays enrolled (§4b(4))
dag/test/claim/diverging_match_arm_join_witness_test.dagcarries the reproduction verbatim, now12 witnesses, all green. It is its own regression control in the strong sense: if the join ever
re-admits a diverging arm, the module stops RESOLVING, so its witnesses go red at resolve rather
than at assertion. Beyond the original four it covers a
let-terminalreturn, an all-divergingmatch in argument position (the discriminating RED for blocker 1), an all-diverging match taking the
expected type rather than the arm payloads, and an all-diverging match in an unconstrained
letfabricating no type.
Divergence is an explicit fact, not an absent name (review 5085499375, D1)
The first repair let divergence survive as a nameless node. That conflated two different states:
an expression that produces NO value, and a type the walk COULD NOT RESOLVE. The only feature
distinguishing the sentinel was its empty name, so every consumer that declines on a missing name
declined on both — and the second is a state that must still be judged. That is silencing, and it
was structural rather than hypothetical.
Divergence now has a carrier:
InferredNodegainsDivergent— a fourth answer to what inference knows about a node,deliberately not a shape of
Resolved.TypeResolutionVerdictgainsDivergentExpression, andtype_is_divergent(n)is thereader consumers match on.
is_fully_resolvedstill answersfalse, but by its own arm ratherthan by sharing the unresolved path.
divergent_type()moves to00_corebesideInferredNode: resolution as well as inferencemust now construct and recognize it, and leaving it in
04_inferwould have forced04_resolveto mint a second nameless node for the same concept.
Making the fact structural is what produced the census. Adding the variant refused 10 sites, each
answered deliberately rather than defaulted:
inferred_to_nodenoneis_compiler_errorfalseresolve_optional_nodedivergent_type()unit_typewould fabricate a type for unreachable code; a diagnostic would refuse a program that is not wronginferred_to_outputs[]05_emit_rustwire-contract ×2rust_serde_error_policyserialize_inferred_node_refDivergentkindcli_runinitializerinferred_fingerprint,occurrence_allocator_after_inferred_node.dagauthorityThe mirror IS a measured fixed point now
The running compiler is the Rust mirror, not the
.dag. Every mirror in this PR is installed fromtarget/stage0-regen-candidate, never hand-shaped — the two hand-shapes earlier in this branch bothmissed something the emitter derives. Measured, not claimed:
Two findings this PR does NOT fix, stated because they were measured here
1. The join already answers with the FIRST arm. Blocker 3 asked for a control proving a later
non-diverging contributor is necessary to the join. Seven fixture shapes and two independent mutants
(keep-first, and a purpose-built contamination mutant that makes the divergent sentinel win) all
failed to discriminate. The narrower shape — the later contributor alone satisfies the formal —
found the reason, and it is an ordered pair:
FResAthenFOthA(satisfying arm first)FOthAreally is passed to aFResformalFOthAthenFResA(satisfying arm later)declared 'Coproduct(FRes)', produced 'Coproduct(FOth)'The verdict flips with arm order, so
prefer_specific_typetie-breaks to the first arm fordisjoint coproducts. That is why keep-first was unobservable: the compiler is already behaving like
the mutant at this seam. One of those two rows is wrong and it is the accepted one — a floor
violation (values inhabit declared types) with a corpus-wide blast radius. It is a different defect
in the same function and folding it in would make a bounded PR unbounded, so it gets its own subject.
2. The
.dagexhaustiveness checker is blind through nested patterns. Adding theInferredNodevariant, the checker flagged all 7 flat matches and missed both matches over
Optional<InferredNode>written asPresent { value: Resolved { .. } }— rustc caught those.Accepted
.dagsource emitted Rust that does not compile:accepted_source_emits_uncompilable_target.Repaired here at the
.dagauthority so the arms come from the emitter, but the checker gap is thefinding, not the patch, and it is filed separately.
What is measured and what is not
D2's propagation control — a divergence born in an unconstrained
letfeeding an outer selector —is not enrolled, deliberately. It greens under its own purpose-built contamination mutant, and a
probe that is permanently green by construction carries no information and would later be cited as
coverage (§4b). It is recorded here as a measurement with a named enrolment trigger: it becomes
discriminating the moment the join stops answering with the first arm, i.e. when finding 1 is fixed.
So, in the two claims kept apart: this PR establishes that diverging arms contribute no produced
value to the join, and that the divergence fact survives independently of context, by execution.
It does not establish that the all-arms map is exercised at any judging seam — no authorable
input distinguishes it today, and finding 1 says why.