Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 149 additions & 28 deletions bootstrap/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -5347,10 +5373,27 @@ impl Parser {
fn parse_if_expr(&mut self) -> Result<Node, String> {
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()?;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bootstrap/stage0/FROZEN_HASH
Original file line number Diff line number Diff line change
@@ -1 +1 @@
e6333575ec1081d16f47b0656ba187bdb3ce5b0e26707653b9cfd0b8d6682d13
ebf8407b80b6d32bb2ff59549a8203d6a85deb15f289d76af8a433cb49a104b7
Original file line number Diff line number Diff line change
@@ -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
40 changes: 13 additions & 27 deletions docs/reports/suite_expectations.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"
},
{
Expand Down Expand Up @@ -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"
},
{
Expand Down Expand Up @@ -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"
},
{
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"
},
{
Expand Down
Loading