From 8aa7b04aded40652f4703a96d24744cc9330c3bf Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Fri, 28 Aug 2026 02:26:13 +0700 Subject: [PATCH 1/2] parser: a trailing `;` on a clause no longer empties the whole test body (Refs #2161) A braceless clause may end with a semicolon: test t given p = 0; assert g(1) == 999 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, and `gen_test_block` emits the resulting empty test with no marker. Measured over 746 tracked specs: discarded top-level tokens 35,224 -> 35,070 specs that parse 620 -> 620 t27c tests 1629/6 -> 1629/6 RATCHET CLEAN A SECOND shape in this family is NOT fixed here, deliberately. A body that OPENS with `var`/`const` has no earlier clause to take a column from, so `first_clause_col` is None and the statement arm is skipped. I wrote that fix, measured it recovering 1,914 tokens -- and it regressed specs/memory/notebooklm.t27 from parsing to not parsing. The mechanism is worth recording: seeding the column lets an EARLIER clause take the statement arm, and the parser then reaches `const (notebook, err) = ...` -- a tuple destructure the arm cannot handle -- in a state where the old path would have fallen back for the whole block. It dies with "Expected identifier after 'const', got LParen" instead. The arm's contract is that it may only ADD assertions and never break a file; that version broke one, so it is not in this commit. Filed separately. Isolated by disabling one edit at a time rather than by reading: with the semicolon consumption alone the spec parses, with the column seeding alone it does not. FROZEN_HASH resealed in the same commit (M5). --- bootstrap/src/compiler.rs | 16 ++++++++++++++++ bootstrap/stage0/FROZEN_HASH | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 7729c49034..5601182bdd 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 f475699756..e844d93cdd 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -ff31ebbf36eb1b0ec4a370c8132f27f5d6c79b8d0b2d2c4f8e504b2d42f0f9c2 +ad350950130ab505ebc11b5408f3fc38875c417d9c8abc5cb1527fc78370693b From 5f36741bac28c91f8041028e57ee260350cf801b Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Fri, 28 Aug 2026 02:27:00 +0700 Subject: [PATCH 2/2] docs/now: one character emptied a whole test body (Refs #2161) --- .../2026-08-28-one-character-emptied-a-whole-test-body.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/now/2026-08-28-one-character-emptied-a-whole-test-body.md 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