diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 9932a30505..beab8d26a9 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -5718,6 +5718,12 @@ impl Parser { // Statement clauses must sit on the line immediately after the // previous clause; a gap returns the old boundary reading. let adjacent = self.current.line <= self.last_line + 1; + // A body that OPENS with `var`/`const` has no earlier clause to + // take a column from, so `first_clause_col` was still None, this + // arm was skipped, and the whole braceless body fell back to the + // discard -- silently, and with no marker in the output. Seed the + // column from this statement itself: when it is the first thing in + // the block, it IS the first clause. if matches!(self.current.kind, TokenKind::KwConst | TokenKind::KwVar) && adjacent && first_clause_col.map_or(false, |c| c > 1 && self.current.col >= c) @@ -6221,6 +6227,16 @@ impl Parser { Some(c) if c <= clause_col => c, _ => clause_col, }); + // A clause may end with a semicolon: `given p = 0;`. Nothing + // consumed it, so the next loop turn met `;` where it expects a + // clause head, read that as "stopped mid-clause", and restored the + // fallback -- discarding the WHOLE block over one character. The + // same body without the semicolon lowered fine, which is what made + // it invisible: two spellings of one clause, one of them silently + // emptying every assertion after it. + if self.current.kind == TokenKind::Semicolon { + self.advance(); + } lowered += 1; } diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index fed9ae90ee..d50cdefc45 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -78bc6d9b03a1ab33729361e558030cd5c67f82d9acdd782912317eaa6d0ea46f +024051760b49a8fae2a3b56cbd362d2bba1dbc6bdc61594f7adfba67c1e3f9fa diff --git a/docs/now/2026-08-28-one-character-emptied-a-whole-test-body.md b/docs/now/2026-08-28-one-character-emptied-a-whole-test-body.md new file mode 100644 index 0000000000..b47da74384 --- /dev/null +++ b/docs/now/2026-08-28-one-character-emptied-a-whole-test-body.md @@ -0,0 +1,8 @@ +# NOW -- One character emptied a whole test body (2026-08-28) + +## One character emptied a whole test body (Refs #2161) + +- Refs #2161. A braceless clause may end with a semicolon: `given p = 0;`. Nothing consumed it, so the next loop turn met `;` where it expects a clause head, read that as stopped-mid-clause, and restored the fallback -- discarding the WHOLE block over one character. The identical body without the semicolon lowered fine, which is what kept it invisible: two spellings of one clause, one of them silently emptying every assertion after it +- Measured: discarded tokens 35224 -> 35070, parse 620 -> 620, tests unchanged, RATCHET CLEAN +- A SECOND shape in the same family is not shipped, and the reason is worth keeping. A body OPENING with var/const has no earlier clause to take a column from, so the statement arm is skipped. I wrote that fix and measured it recovering 1914 tokens -- and it regressed specs/memory/notebooklm.t27 from parsing to not parsing. Seeding the column lets an EARLIER clause take the arm, and the parser then reaches `const (notebook, err) = ...` in a state where the old path would have fallen back for the whole block; instead it dies hard +- Isolated by disabling one edit at a time, not by reading: with the semicolon consumption alone the spec parses, with the column seeding alone it does not. parse_bdd_clauses carries the contract "may only ADD assertions, never break a file" in its own doc comment, and that version broke one. Filed as #2735 with the containment fix it actually needs