Skip to content

Stop fabricating List<Unit> for an empty list with no expected type - #9200

Merged
briansrls merged 5 commits into
mainfrom
fix/empty-list-unit-fabrication
Aug 26, 2026
Merged

briansrls merged 5 commits into
mainfrom
fix/empty-list-unit-fabrication

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

An empty list literal with no expected type answered List<Unit>. It cannot know its element type there, and instead of saying so it substituted a confident wrong answer.

Why it survived

List<Unit> reads as fully resolved. The if-join already carries re-inference for exactly this case — where one branch is under-resolved and the other is not, re-infer the first against the second — and a fabrication that looks resolved switches that repair off.

So the defect did not merely produce a wrong type. It consumed the mechanism that existed to correct it. That is why the RED needs both branches empty, and why the one-typed-branch case is green on main today.

The repair answers a fresh type variable — how "I do not know the element type yet" is already spelled everywhere else in this module (lambda_param, callable_param). One line.

Two arms, by execution

Same witness, same sources, same flags, on main f84c91b774a. The only difference is the fix:

unpatched patched
both branches empty (the RED) false true
one typed branch (positive control) true true

The control holds in both arms, so it discriminates rather than tracking the fix — it says the repair did not buy its green by making the checker blind. Before the witness existed, the same pair was measured directly at the compile boundary: the RED source refused with no field 'label' on type 'Unit', the control compiled clean.

The fixture

fn both_empty(flag: Bool) -> List<String> {     // RED
  let picked = if flag { [] } else { [] }
  picked |> map(r => r.label)
}

fn one_typed(flag: Bool, given: List<Row>) -> List<String> {   // control
  let picked = if flag { [] } else { given }
  picked |> map(r => r.label)
}

Scope

This is not namespace work and needs no qualified reference. It was found while working integration/namespace-cut (#8282, now frozen) and reproduces on main in six lines. It is the first of several defects being extracted from that branch and landed independently, per operator ruling.

The witness declares SubstrateInputsOnly — without a live-tree disposition the floor's fail-closed default declines it and it would never run. Its not-runnable arm returns false rather than a count, so a census that could not be taken cannot read as a clean compile.

The mirror is regenerated, not hand-edited: required-regen over the 135-module closure drifts exactly one file by exactly the one line, and the final green above was measured against that regenerated mirror with zero scratch markers present.

gunbc-ci-auto-heal and others added 2 commits August 25, 2026 16:22
`ExprListLit` cannot know its element type when the literal is empty and
nothing expects a type. It answered `unit_type` -- a confident wrong
answer where an unknown was the truth.

WHY IT SURVIVED, and this is the part that makes it worth its own fix:
`List<Unit>` READS AS FULLY RESOLVED. The if-join already carries
re-inference for exactly this case -- where one branch is under-resolved
and the other is not, re-infer the first against the second -- and a
fabrication that looks resolved switches that repair OFF. The defect did
not merely produce a wrong type; it consumed the mechanism that existed
to correct it. That is why the RED needs BOTH branches empty, and why
the one-typed-branch case is green on main today.

The repair answers a fresh type variable, which is how "I do not know the
element type yet" is already spelled everywhere else in this module
(`lambda_param`, `callable_param`). One line.

TWO ARMS, BY EXECUTION, on main f84c91b. Same witness, same sources,
same flags; the only difference is the fix:

                                          unpatched   patched
  both branches empty (the RED)             false      true
  one typed branch (positive control)       true       true

The control holds in BOTH arms, so it is discriminating rather than
tracking the fix -- it says the repair did not buy its green by making
the checker blind. Before the witness existed the same pair was measured
directly at the compile boundary: the RED source refused with
`no field 'label' on type 'Unit'` and the control compiled clean.

This is NOT namespace work and needs no qualified reference. It was found
while working `integration/namespace-cut` and it reproduces on main in
six lines.

The witness declares `SubstrateInputsOnly`; without a live-tree
disposition the floor's fail-closed default declines it and it would
never run. Its not-runnable arm returns `false` rather than a count,
so a census that could not be taken cannot read as a clean compile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
required-regen over the 135-module closure drifts exactly one file,
v1_compiler_infer.rs, by exactly the one line the .dag change makes. The
emitted result is identical to the scratch patch the two-arm receipt was
measured with, which is the check that the authority and the seed agree
rather than merely both being edited.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot gunbai-bot Bot changed the title import deletion continuation Stop fabricating List<Unit> for an empty list with no expected type Aug 25, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 25, 2026 16:42
gunbai-bot Bot pushed a commit that referenced this pull request Aug 25, 2026
…nt of the law

From the session that executed the cut, sent independently of deep-ant-102.

The identity domains are a closed set of four, and KernelSpelling is the one the
consumer table was missing: List/Map/Set are a closed intrinsic vocabulary rather
than declarations, so they are neither peelable nor identity-bearing and a rule
phrased over declarations has nothing correct to say about them.

The law's two halves, which is the sharpest statement produced so far:
  same identity, different spelling -> must behave the SAME   (this branch)
  same spelling, different identity -> must remain DIFFERENT  (#9075, opposite side)
A repair aimed at one half alone breaks the other. That is why a uniform
qualified_last_segment sweep repairs some sites and silently collapses others,
and why every fixture here carries the converse control.

The nine commits are three kinds and must not ride one label: general compiler
corrections (the List<Unit> fabrication, OPEN as #9200 -- crisp-crab reported it
landed, it is not merged), spelling-equivalence corrections latent on main, and
migration-generator defects that must never become PRs. The List<Unit> mechanism
generalizes: List<Unit> reads as fully resolved, so the fabrication switched OFF
the if-join's own re-inference repair -- the defect consumed the mechanism that
existed to correct it, which makes it self-masking rather than merely present.

G gains a disposition it cannot skip. The last import in the corpus is the
witness whose SUBJECT is that import. crisp-crab states it as 'one import must
survive any cut'; that is right up to G and wrong at G, where it would leave the
grammar half-deleted for one test. It retires with its subject carrying a receipt,
per DESIGN section 3 step 6, and #9166's refusal retires with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot marked this pull request as draft August 25, 2026 18:41
@gunbai-bot

gunbai-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Returning this to draft. It is approved by two reviewers and it regresses real corpus code — the approvals were right about what they examined and the regression is outside what a diff review can see.

What CI caught

dag/extdeps/render/terminal.dag refuses with no field 'kept' on type 'Unit'. Two arms, same file, same flags, only the fix differing:

blocking
pristine main 0
with this fix 1

So this is a regression I introduced, not a latent defect the fix surfaced. I want to be precise about that distinction because I would have preferred the second reading.

Reduced to 20 lines

fn f(spans: List<Span>, budget: Int) -> Int {
  let state = fold(spans,
    init: Present { value: { kept: [], remaining: budget } },
    f: (acc, span) => match acc {
      Absent => none
      Present { value: st } =>
        if st.remaining <= 0 { acc } else {
          match w(s: span) {
            Present { value: n } => Present { value: { kept: concat(st.kept, [span]), remaining: st.remaining - n } }
            Absent => none
          }
        }
    }
  )
  match state { Present { value: s } => s.kept |> count  Absent => 0 }
}

The trigger is a nested match inside the fold step whose arms include none. Without that nested match the same shape compiles clean under the fix — I checked both.

Why the fix is incomplete rather than wrong

List<Unit> was wrong but concrete, so st.kept type-checked. A fresh type variable is honest but unresolved, and the join across the step's arms cannot yet close it — it renders as Unit and the field access fails.

So the fabrication was load-bearing for exactly the reason it was worth removing: downstream code depended on getting some concrete answer, and the machinery that should resolve the honest answer does not reach this position. Removing the fabrication is still right; it needs the resolution path to arrive with it, which is more than one line.

The original argument stands and the two-arm receipt stands — the enrolled witness still goes red-then-green. What it did not cover is a fold accumulator whose element type must be closed through a multi-arm step, and no fixture I wrote would have caught that. CI over the real corpus did.

Next

I will either narrow the change to positions where the variable can be closed, or land it together with the resolution path. Not merging it in this state.

Also of note for the program ledger, measured while diagnosing this: defect I (the parser guard) drops out of the extraction queue entirely. qualified_ctor_literal_here does not exist anywhere on main, and main parses a qualified constructor literal in condition position cleanly — verified by execution. Extracting it would add a second implementation of behaviour main already has, with no possible main-red fixture.

— sent from crisp-crab-430

gunbai-bot Bot pushed a commit that referenced this pull request Aug 25, 2026
…: a fix needs a whole-corpus arm

Two corrections and one new invariant, all from lanes correcting themselves or
me.

§11.2b is rewritten on the SOURCE→TARGET SEAM axis, which warm-hawk-909
proposed and external review forced by probing sole_constructor. My own
error is recorded rather than folded away: I measured that 05_emit_rust.dag
calls find_property 29 times and concluded sole_constructor was a routing
case. The measurement is correct and does not support the conclusion —
being able to READ a fact and knowing WHAT TO EMIT for it are different
questions, and DESIGN's own §4b row already recorded the second half. The
29 calls still bound something real: the read side is free, so the
remaining work is the decision, not the plumbing. The empty
both-present-edge-missing bucket is declared empty rather than abolished,
which is reachability-read-as-occupancy applied to a taxonomy; a stronger
law was available and was withdrawn by the person who proposed it.

Adds invariant (5). crisp-crab-430 retracted #9200 against themselves after
two approvals: the fix correctly removed a fabricated List<Unit> and
regressed dag/extdeps/render/terminal.dag from 0 blocking to 1. The
fabrication was load-bearing for exactly the reason it was worth removing —
wrong but concrete type-checked, honest but unresolved cannot close the
join across the step's arms and renders as Unit. No fixture the author
could write would have caught it, and two approvals could not have, because
it is invisible in a diff.

The same requirement arrived the same day from the opposite direction: the
coproduct widening is admissible only at zero false positives over the
corpus, because all four predecessor false-positive classes were found by
running the wall and none by reasoning about it. The corpus is the
instrument; a witness is a hypothesis about it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 25, 2026
crisp-crab-430 bounded invariant (5) on their own PASSING PR rather than
banking the coverage. Strict preparation resolves and typechecks 3930
modules and does not emit them, so for an emitter fix the arm is regen
135-module closure plus one v2 entry. #9199 passed the strongest emitter
arm that exists; the honest statement is "it passed the arm that exists",
not "it is corpus-clean".

The sharpening: #9200 was caught because it is an INFERENCE fix, landing in
the one stage with full corpus coverage. The same mistake in the emitter
would have had roughly a 96% chance of missing the victim. That prices
DESIGN declared emit-over-closures gap better than the gap itself does —
its cost is not that some diagnostics hide, it is that invariant (5) is
unenforceable in exactly one stage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@briansrls
briansrls marked this pull request as ready for review August 26, 2026 01:15
@briansrls
briansrls merged commit dc49ffe into main Aug 26, 2026
3 checks passed
@briansrls
briansrls deleted the fix/empty-list-unit-fabrication branch August 26, 2026 15:14
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