Skip to content

t27c: four defects, three of them silent (Refs #2161) - #2720

Merged
gHashTag merged 6 commits into
masterfrom
w699-t27c-fixes
Aug 27, 2026
Merged

t27c: four defects, three of them silent (Refs #2161)#2720
gHashTag merged 6 commits into
masterfrom
w699-t27c-fixes

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

Four defects in t27c, each measured before and after, each in its own commit with FROZEN_HASH resealed alongside (M5).

Two of them are the same failure mode the project explicitly guards against: a green exit that is not a result.

1. gen-rust emitted an empty match for every switch

The arm loop tested arm.kind == NodeKind::Module. The parser builds every arm as ConstDecl, so the loop matched nothing:

pub fn trit_negate(a: Trit) -> Trit {
    return match a {
};
}

Exit code 0. gen-c and gen-verilog lower the same construct correctly — three backends agreed and the fourth silently discarded the function's logic while reporting success, on the sentence the language is sold with.

Leaving the pattern unqualified would have been worse than the empty match. match a { neg => .. } is a binding in Rust: it compiles, and it matches everything. The scrutinee's declared type is the enum, and var_types already holds every parameter and local with its Rust type, so a bare-identifier scrutinee resolves exactly; anything else falls back to the return type — the rule ExprEnumValue already uses for the same shorthand a few arms above.

master this branch
match arms emitted, specs/base/ops.t27 0 3
rustc errors on that output 43 42

The remaining 42 are Zig builtins leaking into Rust — a separate defect this does not touch.

2. Paren-less if was legal in statement position only

W578 decided if cond { .. } is legal t27 and paid to make parse_if_stmt accept it. parse_if_expr was left on a hard expect(LParen), so specs kept failing with the identical "Expected LParen, got Ident" that the statement parser's own comment says was fixed — a reader who greps that diagnostic finds a note claiming the opposite of what the code does.

master this branch
specs failing with that diagnostic 8 0
specs that parse 603 605

The class is gone; the count moves by 2 because six of the eight carry further defects behind this one. That is the shape t27c backlog already documents — removing the single most frequent cause once moved the compiling count 151 → 151 — and it is why the class count, not the spec count, is the number this commit is about.

3. The inclusive range a..=b

The lexer emits .. and then a separate =, so the right-operand parser met =b. Lowered to a..b + 1 rather than carried as a new operator: every backend already lowers .., none would know ..=, and the alternative is a fifth spelling four emitters must each be taught.

This unblocks specs/math/constants.t27 — the module #2688 calls "a ceiling, not a backlog" — which 259 of 746 specs import.

My first attempt at this was dead code. I put it in parse_for_range after expect(DotDot). That function parses its start bound with the full expression grammar, and that grammar already consumes .. as a binary operator, so control never reaches the expect. The probe still failed with the fix in place, which is how I found it. Reverted rather than left sitting.

master this branch
specs that parse 603 609
regressions 0

Newly parsing: specs/math/constants.t27, specs/math/radix_economy.t27, specs/numeric/phi_ratio.t27, specs/vsa/jones_polynomial.t27.

4. Found in passing: every for loop body was dropped in gen-rust

Both StmtFor handlers read the capture from children[1].name and the body from children[2].children. The bare for x in xs { } form builds two children — iterable, body — and puts the capture in params. So children[1] is the body block, whose name is the literal string "body", and children[2] does not exist:

for body in (1 .. 3) {
}

Exit code 0, induction variable renamed to body, loop body gone. Same class as #1.

The capture is in params in both shapes and the body is the last child in both, so one read serves each form.

Verified by running it, not by reading it

for i in 1..=3 { total = total + i }   →  compiles, prints 6
for i in 1..3  { total = total + i }   →  body present
for x in xs    { n = n + 1 }           →  `for x in xs`, body present

First time in this campaign a t27 spec reached a running program with the right answer.

Honest deltas across the whole suite

master this branch
parse 110 104
PRIMARY (corpus) 181 179
parse-no-discard 71 75
seal-verify 133 + 128 141 + 129

Two numbers got worse and both are mechanical. A spec that could not parse at all now parses and reveals that it discards tokens — it moves from parse into parse-no-discard. And changing an emitter's output makes the stored seals stop matching; that is the "emitter drift" class the seal audit already separates from real staleness.

Nothing regressed: no spec that parsed on master fails to parse here, checked at each of the three parser commits.

The suite exits 1 on master and exits 1 here, with the same ACCEPTABLE: no verdict.

Refs #2161

The Rust emitter's arm loop tested `arm.kind == NodeKind::Module`. The
parser builds every arm as `ConstDecl`, so the loop matched nothing and
the emitter printed

    pub fn trit_negate(a: Trit) -> Trit {
        return match a {
    };
    }

an empty match, with exit code 0, for a construct gen-c and gen-verilog
lower correctly. Three backends agreed and the fourth silently discarded
the function's logic while reporting success -- on the sentence the
language is sold with.

Leaving the pattern unqualified would have been worse than the empty
match: `match a { neg => .. }` is a BINDING in Rust. It compiles, and it
matches everything. The scrutinee's declared type is the enum, and
`var_types` already holds every parameter and local with its Rust type,
so a bare-identifier scrutinee resolves exactly; anything else falls back
to the return type, which is the rule ExprEnumValue already uses for the
same shorthand a few arms above.

Numbers and char literals are patterns in their own right and are not
qualified; `else` becomes `_`.

Measured on specs/base/ops.t27, master binary vs this one:
    match arms emitted   0 -> 3
    rustc errors        43 -> 42
The remaining 42 are Zig builtins leaking into the Rust output, a
separate defect this does not touch.

FROZEN_HASH resealed in the same commit (M5).
W578 decided paren-less `if cond { .. }` is legal t27 and paid to make
`parse_if_stmt` accept it. `parse_if_expr` was left behind on a hard
`expect(LParen)`, so specs kept failing with the identical
"Expected LParen, got Ident" that the statement parser's comment says was
fixed -- a reader who greps that diagnostic finds a note claiming the
opposite of what the code does.

The fix is the same six lines, including the struct-literal suppression:
without parentheses `Name {` opens the THEN branch, not a struct literal.

Measured, master binary vs this one, over all 747 tracked specs:

    specs failing with "Expected LParen, got Ident"    8 -> 0
    specs that parse                                 603 -> 605
    specs that regressed                                     0

The class is gone; the spec count moves by 2 because six of the eight
carry further defects behind this one. That is the shape `t27c backlog`
already documents -- removing the single most frequent cause once moved
the compiling count 151 -> 151 -- and it is why the class count, not the
spec count, is the number this commit is about.

FROZEN_HASH resealed in the same commit (M5).
The lexer emits `..` and then a separate `=`, so the right-operand parser
met `=b` and answered "Unexpected token in expression: Equals". 12 specs
write this form. One of them is specs/math/constants.t27, which 259 of
746 specs import -- the module #2688 calls a "ceiling, not a backlog".

Lowered to the exclusive range over `b + 1` rather than carried as a new
operator: every backend already lowers `..` and none would know `..=`.
For the integer loops this grammar has the two are the same range, and
the alternative is a fifth spelling four emitters must each be taught.

The fix belongs in the comparison chain, not in `parse_for_range`. My
first attempt put it after `expect(DotDot)` there, which is dead code for
this input: `parse_for_range` parses its start bound with the FULL
expression grammar, and that grammar already consumes `..` as a binary
operator, so control never reaches the `expect`. Found by measuring --
the probe still failed with the fix in place -- and reverted rather than
left sitting.

Measured over all 747 tracked specs, master binary vs this one:

    specs that parse    603 -> 609
    regressions                  0

Newly parsing: specs/math/constants.t27, specs/math/radix_economy.t27,
specs/numeric/phi_ratio.t27, specs/vsa/jones_polynomial.t27.

FROZEN_HASH resealed in the same commit (M5).
…2161)

Both Rust `StmtFor` handlers read the capture from `children[1].name` and
the body from `children[2].children`. The parser's bare `for x in xs { }`
form builds TWO children -- iterable, body -- and puts the capture in
`params`. So `children[1]` is the body block, whose name is the literal
string "body", and `children[2]` does not exist.

Every range loop in the corpus emitted

    for body in (1 .. 3) {
    }

with exit code 0: the induction variable renamed to "body" and the loop
body gone. Same class as the empty `match` two commits ago -- the fourth
backend silently discarding the program's logic while reporting success.

The capture is in `params` in BOTH shapes (`for (xs) |x| { }` pushes it
there too), and the body is the LAST child in both, so one read serves
each form.

Verified end to end rather than by inspection -- generated Rust compiled
and run:

    for i in 1..=3 { total = total + i }   ->  prints 6
    for i in 1..3  { total = total + i }   ->  body present
    for x in xs    { n = n + 1 }           ->  `for x in xs`, body present

Parse count unchanged at 609/747; this is an emitter fix.

FROZEN_HASH resealed in the same commit (M5).
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-08-26 17:13:32 UTC

Summary

Status Count
Total Open PRs 8
PRs with Failing Checks 6
PRs with All Checks Green 2
READY 1
FAILING 6
PENDING 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=ebf8407b80b6 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

The ratchet named both halves itself:

    UNEXPECTED PASSES  : 6
      - specs/ar/explainability.t27 [parse] (fixed -- remove from the ledger)
      - specs/math/constants.t27 [parse]
      - specs/math/radix_economy.t27 [parse]
      - specs/numeric/phi_ratio.t27 [parse]
      - specs/physics/sacred_verification.t27 [parse]
      - specs/vsa/jones_polynomial.t27 [parse]
    UNEXPECTED FAILURES: 4
      + specs/math/constants.t27 [parse-no-discard]
      + specs/math/radix_economy.t27 [parse-no-discard]
      + specs/numeric/phi_ratio.t27 [parse-no-discard]
      + specs/vsa/jones_polynomial.t27 [parse-no-discard]

Those are the same four specs on both lists, and that is the whole story:
a spec that could not parse at all now parses and REVEALS that the parser
discards its `forall`-quantified properties. The defect moved from
invisible-because-blocked to visible-and-counted; it did not appear.

The four new entries cite #2474, the discard census, rather than a fresh
issue -- they are that population, not a new one.

Ledger 181 -> 179, max_entries 221 -> 219. Net LOWER, so the cap moves in
the direction the doc says it may move on its own; the two entries of
slack are gone rather than banked.

RATCHET: CLEAN -- 0 unexpected failures, 0 unexpected passes, 0 expiries.
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-08-27 12:43:25 UTC

Summary

Status Count
Total Open PRs 8
PRs with Failing Checks 7
PRs with All Checks Green 1
READY 0
FAILING 7
PENDING 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=ebf8407b80b6 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@gHashTag
gHashTag merged commit 6c8d218 into master Aug 27, 2026
33 of 34 checks passed
@gHashTag
gHashTag deleted the w699-t27c-fixes branch August 27, 2026 12:56
gHashTag added a commit that referenced this pull request Aug 27, 2026
* specs: restore the 2544 lines commit 4639b38 overwrote with a counter (Refs #2161)

`4639b38cd` -- "fix(l3-purity): replace all Unicode with ASCII in 160 .t27
files" -- did not transliterate. It replaced the i-th non-ASCII character
of each file with the ASCII digits of `i`:

    -    fn ternary_not(a: i32) → i32 {
    +    fn ternary_not(a: i32) 257 i32 {

That transform is exactly reproducible from the pre-image, which is what
makes the repair safe rather than a guess. The model was verified to
reproduce the commit's output BYTE FOR BYTE before anything was written.

A line is restored only when the line at HEAD is byte-identical to the
transform of its pre-image line, so every line edited in the four months
since is left untouched. 141 files, 2544 lines.

Measured with the MASTER binary, so this is isolated from the compiler
fixes in #2720:

    specs that parse    603 -> 610
    regressions                  0

The seven are exactly the "Expected LBrace, got Number ('257')" cluster.
They failed not because they LOST an arrow -- `→` was never a token, the
lexer discards it -- but because they GAINED an integer literal.

One restored line, as a sample of what the other 2543 are:

    -    assert jones_trefoil_at_phi() 1222 PHI + PHI * PHI within 0.1
    +    assert jones_trefoil_at_phi() ≈ PHI + PHI * PHI within 0.1

The comparison operator was that file's 1222nd non-ASCII character.

This takes the RESTORE branch of the question in #2713 rather than
transliterating, on two grounds. The pre-image is exact and a
transliteration is a fresh authoring decision on 2544 lines. And the L3
ASCII rule is enforced by nothing today: 320 of 747 tracked .t27 files
carry 88,033 non-ASCII characters at HEAD and no gate objects -- while
TASK.md:128 records "0 non-ASCII remaining".

scripts/repair/unscar_4639b38cd.py is committed, so the transliterate
branch stays available and the operation is reproducible.

* ratchet + width baseline: seven specs reach the gates for the first time (Refs #2161, #2713)

Restoring the source moved seven specs from "does not parse" to "parses",
and both ledgers noticed. Neither is new damage: these files had never
reached either gate.

RATCHET. The same seven appear on both of its lists -- out of `parse`,
into `parse-no-discard`:

    UNEXPECTED PASSES  : 7   specs/{base/ternary_encoding, base/ternary_memory,
    UNEXPECTED FAILURES: 7   isa/ternary_arithmetic, isa/ternary_bitwise,
                             isa/ternary_deque, isa/ternary_gates,
                             isa/ternary_shift}.t27

They parse and discard top-level tokens, which is the #2474 population.
Proof that restoration REVEALED rather than CAUSED it: each restored file
is byte-identical to its pre-image, checked with `diff`. Ledger 181 -> 181,
`max_entries` untouched, because the move is one-for-one.

WIDTH BASELINE. specs/base/ternary_memory.t27 emits `[3801183:0]`, and it
is not an underflow -- derived independently and matching the emitter bit
for bit:

    TritCell           32 + 8 + 64 + 32          =       136
    TernaryWord        27*136 + 8 + 32           =     3,712
    TernaryMemoryBank  1024*3712 + 32 + 64       = 3,801,184

464 KiB in one packed register. The backend computed it faithfully; the
spec asked for something with no hardware form. TRIT_CAPACITY is 27 = 3^3,
so the size is deliberate. Same class as the stdlib.t27 line already
there, and recorded with the derivation for the same reason: so the next
reader does not go looking for a subtraction that is not there.
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