diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 8958eeed23..d02a1abd4c 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -4210,7 +4210,33 @@ impl Parser { } let op = self.current.lexeme.clone(); self.advance(); + // `a..=b` -- the INCLUSIVE range. 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 specs/math/constants.t27, which 259 of 746 + // specs import. + // + // 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 that + // four emitters must each be taught. + let inclusive = op == ".." && self.current.kind == TokenKind::Equals; + if inclusive { + self.advance(); + } let right = self.parse_expr_bitor()?; + let right = if inclusive { + let mut plus = Node::new(NodeKind::ExprBinary); + plus.extra_op = "+".to_string(); + plus.children.push(right); + let mut one = Node::new(NodeKind::ExprLiteral); + one.value = "1".to_string(); + plus.children.push(one); + plus + } else { + right + }; left = Node { kind: NodeKind::ExprBinary, extra_op: op, @@ -5347,10 +5373,27 @@ impl Parser { fn parse_if_expr(&mut self) -> Result { self.advance(); // consume 'if' - // Condition in parentheses - self.expect(TokenKind::LParen)?; - let cond = self.parse_expr()?; - self.expect(TokenKind::RParen)?; + // Condition, with or without parentheses -- the same rule + // `parse_if_stmt` was given in W578, applied to expression position, + // which was left behind. 14 specs still failed with the identical + // "Expected LParen, got Ident" the comment up there says was fixed, + // so a reader who greps that diagnostic finds a note claiming the + // opposite of what the code does. + // + // Without parentheses, `Name {` opens the THEN branch, so + // struct-literal parsing is suppressed for the condition exactly as + // it is for the statement form. + let cond = if self.current.kind == TokenKind::LParen { + self.advance(); + let c = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + c + } else { + self.no_struct_literal += 1; + let c = self.parse_expr(); + self.no_struct_literal -= 1; + c? + }; // Then expression let then_expr = self.parse_branch_value()?; @@ -21299,18 +21342,37 @@ impl RustCodegen { NodeKind::StmtFor => { self.write_indent(); self.write("for "); - if child.children.len() > 1 { - self.write(&child.children[1].name); - } + // The capture lives in `params` in BOTH shapes the parser builds: + // `for (xs) |x| { }` pushes it there and so does the bare + // `for x in xs { }` collection form. This read `children[1].name`, + // and for the bare form children[1] is the BODY BLOCK, whose name is + // the literal "body" -- so every range loop emitted `for body in ..`. + let capture = child + .params + .first() + .map(|(n, _)| n.clone()) + .unwrap_or_else(|| { + if child.children.len() > 1 { + child.children[1].name.clone() + } else { + String::new() + } + }); + self.write(&capture); self.write(" in "); if !child.children.is_empty() { self.write(&self.expr_to_rust(&child.children[0])); } self.write(" {\n"); self.indent += 1; - if child.children.len() > 2 { - for stmt in &child.children[2].children { - self.gen_rust_stmt(stmt); + // The body is the LAST child, not children[2]. The bare form builds + // two children -- iterable, body -- so the body was dropped entirely + // and the loop emitted empty, with exit code 0. + if child.children.len() > 1 { + if let Some(body) = child.children.last() { + for stmt in &body.children { + self.gen_rust_stmt(stmt); + } } } self.indent -= 1; @@ -21446,18 +21508,37 @@ impl RustCodegen { NodeKind::StmtFor => { self.write_indent(); self.write("for "); - if stmt.children.len() > 1 { - self.write(&stmt.children[1].name); - } + // The capture lives in `params` in BOTH shapes the parser builds: + // `for (xs) |x| { }` pushes it there and so does the bare + // `for x in xs { }` collection form. This read `children[1].name`, + // and for the bare form children[1] is the BODY BLOCK, whose name is + // the literal "body" -- so every range loop emitted `for body in ..`. + let capture = stmt + .params + .first() + .map(|(n, _)| n.clone()) + .unwrap_or_else(|| { + if stmt.children.len() > 1 { + stmt.children[1].name.clone() + } else { + String::new() + } + }); + self.write(&capture); self.write(" in "); if !stmt.children.is_empty() { self.write(&self.expr_to_rust(&stmt.children[0])); } self.write(" {\n"); self.indent += 1; - if stmt.children.len() > 2 { - for s in &stmt.children[2].children { - self.gen_rust_stmt(s); + // The body is the LAST child, not children[2]. The bare form builds + // two children -- iterable, body -- so the body was dropped entirely + // and the loop emitted empty, with exit code 0. + if stmt.children.len() > 1 { + if let Some(body) = stmt.children.last() { + for s in &body.children { + self.gen_rust_stmt(s); + } } } self.indent -= 1; @@ -21906,22 +21987,62 @@ impl RustCodegen { return "/* switch */".to_string(); } let scrutinee = self.expr_to_rust(&node.children[0]); + // The arms carry Zig-shaped `.variant` shorthand, and Rust has + // no rule that resolves it from context. 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 function's return type -- the same rule + // ExprEnumValue uses for the shorthand a few arms above. + let arm_enum = self + .var_types + .get(&scrutinee) + .filter(|t| self.enum_names.contains(*t)) + .cloned() + .or_else(|| { + if self.enum_names.contains(&self.fn_ret_type) { + Some(self.fn_ret_type.clone()) + } else { + None + } + }); let mut s = format!("match {} {{\n", scrutinee); for i in 1..node.children.len() { let arm = &node.children[i]; - if arm.kind == NodeKind::Module { - let pattern = if !arm.name.is_empty() { - arm.name.clone() - } else { - "_".to_string() - }; - let body = if !arm.children.is_empty() { - self.expr_to_rust(&arm.children[0]) - } else { - "()".to_string() - }; - s.push_str(&format!("{} => {},\n", pattern, body)); + // The parser builds EVERY arm as ConstDecl. This tested for + // Module, which no arm is ever built as, so the loop + // matched nothing and the emitter printed `match x { }` -- + // an empty match, with exit code 0, for a construct the + // other three backends lower correctly. 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. + if arm.kind != NodeKind::ConstDecl { + continue; } + // `else` is t27's catch-all; numbers and char literals are + // patterns in their own right and must not be qualified. + let names_a_variant = arm + .name + .chars() + .next() + .is_some_and(|c| c.is_alphabetic() || c == '_'); + let pattern = if arm.name.is_empty() || arm.name == "else" { + "_".to_string() + } else if names_a_variant { + match &arm_enum { + Some(e) => format!("{}::{}", e, arm.name), + None => arm.name.clone(), + } + } else { + arm.name.clone() + }; + let body = if !arm.children.is_empty() { + self.expr_to_rust(&arm.children[0]) + } else { + "()".to_string() + }; + s.push_str(&format!("{} => {},\n", pattern, body)); } s.push('}'); s diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index 29cf489a4a..d302671115 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -e6333575ec1081d16f47b0656ba187bdb3ce5b0e26707653b9cfd0b8d6682d13 +ebf8407b80b6d32bb2ff59549a8203d6a85deb15f289d76af8a433cb49a104b7 diff --git a/docs/now/2026-08-26-four-t27c-defects-and-a-spec-that-finally-runs.md b/docs/now/2026-08-26-four-t27c-defects-and-a-spec-that-finally-runs.md new file mode 100644 index 0000000000..5f90a37eb6 --- /dev/null +++ b/docs/now/2026-08-26-four-t27c-defects-and-a-spec-that-finally-runs.md @@ -0,0 +1,12 @@ +# NOW -- Four t27c defects, and a spec that finally runs (2026-08-26) + +## Four t27c defects, and a spec that finally runs (Refs #2161) + +- Refs #2161. gen-rust emitted an EMPTY match for every `switch`: the arm loop tested `arm.kind == NodeKind::Module` and the parser builds every arm as ConstDecl, so nothing matched and `match a { }` shipped with exit code 0. Three backends agreed and the fourth discarded the function logic while reporting success +- Leaving the pattern unqualified would have been WORSE than the empty match: `match a { neg => .. }` is a BINDING in Rust -- it compiles and matches everything. The scrutinee declared type is the enum and var_types already holds it, so a bare-identifier scrutinee resolves exactly +- parse_if_expr still hard-required parentheses although W578 decided paren-less `if` is legal t27 and paid to make the STATEMENT parser accept it. Specs kept failing with the identical diagnostic the statement parser comment says was fixed. Class 8 -> 0; specs that parse 603 -> 605, because six of the eight carry further defects behind this one +- The inclusive range `a..=b` did not parse: the lexer emits `..` then a separate `=`. Lowered to `a..b + 1` rather than carried as a new operator -- every backend lowers `..` already and none would know `..=`. This unblocked specs/math/constants.t27, the module #2688 calls a ceiling rather than a backlog, which 259 of 746 specs import +- MY FIRST ATTEMPT AT THAT ONE WAS DEAD CODE. I put it in parse_for_range after expect(DotDot); 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. The probe still failed with the fix in place, which is how I found it -- reverted rather than left sitting +- Then, found in passing: BOTH Rust StmtFor handlers read the capture from children[1].name and the body from children[2]. The bare form builds two children -- iterable, body -- so children[1] IS the body block, whose name is the literal string "body". Every range loop in the corpus emitted `for body in (1 .. 3) { }` with the body gone and exit code 0 +- Verified end to end rather than by inspection: generated Rust compiled and RUN. `for i in 1..=3 { total = total + i }` prints 6. First time this campaign a t27 spec reached a running program with the right answer +- Honest deltas, master vs this branch: parse 110 -> 104, PRIMARY corpus 181 -> 179. But parse-no-discard 71 -> 75 and seal-verify 133 -> 141, both mechanical -- a spec that could not parse at all now parses and REVEALS that it discards tokens, and changing emitter output makes stored seals stop matching. Defects moved from invisible-because-blocked to visible-and-counted diff --git a/docs/reports/suite_expectations.json b/docs/reports/suite_expectations.json index aaa20a2ef9..6032e73ea1 100644 --- a/docs/reports/suite_expectations.json +++ b/docs/reports/suite_expectations.json @@ -1,7 +1,7 @@ { "schema_version": 1, "generated_by": "t27c suite --bless-expectations", - "max_entries": 221, + "max_entries": 219, "entries": [ { "path": "specs/account/repo.t27", @@ -59,13 +59,6 @@ "issue": 1959, "expires": "2026-11-30" }, - { - "path": "specs/ar/explainability.t27", - "phase": "parse", - "reason": "parse error in fn near line N: Expected DotDot, got Dot ('.')", - "issue": 1959, - "expires": "2026-11-30" - }, { "path": "specs/ar/ternary_logic.t27", "phase": "parse", @@ -698,9 +691,9 @@ }, { "path": "specs/math/constants.t27", - "phase": "parse", - "reason": "parse error in fn near line N: Expected LParen, got Ident ('negative')", - "issue": 1959, + "phase": "parse-no-discard", + "reason": "parser reaches EOF but DISCARDS top-level tokens (forall-quantified properties)", + "issue": 2474, "expires": "2026-11-30" }, { @@ -733,9 +726,9 @@ }, { "path": "specs/math/radix_economy.t27", - "phase": "parse", - "reason": "parse error in fn near line N: Expected LParen, got Ident ('negative')", - "issue": 1959, + "phase": "parse-no-discard", + "reason": "parser reaches EOF but DISCARDS top-level tokens (forall-quantified properties)", + "issue": 2474, "expires": "2026-11-30" }, { @@ -887,9 +880,9 @@ }, { "path": "specs/numeric/phi_ratio.t27", - "phase": "parse", - "reason": "parse error in fn near line N: Unexpected token in expression: Equals ('=')", - "issue": 1959, + "phase": "parse-no-discard", + "reason": "parser reaches EOF but DISCARDS top-level tokens (forall-quantified properties)", + "issue": 2474, "expires": "2026-11-30" }, { @@ -955,13 +948,6 @@ "issue": 1959, "expires": "2026-11-30" }, - { - "path": "specs/physics/sacred_verification.t27", - "phase": "parse", - "reason": "parse error in fn near line N: Expected LParen, got Ident ('abs_error')", - "issue": 1959, - "expires": "2026-11-30" - }, { "path": "specs/pins/parser.t27", "phase": "parse", @@ -1230,9 +1216,9 @@ }, { "path": "specs/vsa/jones_polynomial.t27", - "phase": "parse", - "reason": "parse error in fn near line N: unexpected token after expression statement: Colon", - "issue": 1959, + "phase": "parse-no-discard", + "reason": "parser reaches EOF but DISCARDS top-level tokens (forall-quantified properties)", + "issue": 2474, "expires": "2026-11-30" }, {