From 8efb17b87f3e8ac85fc011d0b0328118339d3432 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Thu, 20 Aug 2026 00:50:48 +0700 Subject: [PATCH] [GOLD-RING] fix(rust-emitter): an enum member is a path, not a field access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps in the Rust backend, both in how an enum member is spelled. Together they made every enum-returning function ungeneratable. t27 writes a member as `Verdict.escalate` - Zig's spelling, and what the parser produces. Rust spells it `Verdict::escalate`. `ExprFieldAccess` formatted `{}.{}` unconditionally because nothing told it which identifiers name enums, so the generated crate did not compile. The codegen records enum names as it declares them and emits a path when the base is one of them. The shorthand `.escalate` was worse: `ExprEnumValue` printed `format!("{}::{}", node.name, node.extra_field)` with an empty `extra_field`, so the variant landed on the LEFT of the separator - `escalate::`. t27 takes the enum from context the way Zig does and Rust has no such rule, so the emitter has to supply it. The context available at that point is the declared return type of the function being emitted, which is exactly the case the shorthand is written for. Found by generating a real module rather than a fixture: the trios supervisor's decision core - retry, review, merge gate, capacity - the first ring being moved out of Swift. Verified both ways: fn qualified(x: i32) V { if (x > 0) { return V.a; } return V.b; } fn shorthand(x: i32) V { if (x > 0) { return .a; } return .b; } Both now emit `V::a` / `V::b`, and `rustc --crate-type lib` produces an rlib from each. The trios ring compiles the same way (14912 bytes). Seal moved because `bootstrap/src/compiler.rs` changed; new digest recorded in `bootstrap/stage0/FROZEN_HASH` per FROZEN.md §5. `docs/NOW.md` synced per the coordination protocol. Three gaps remain and are NOT addressed here: - `switch` used as a function body emits an empty `match x { };` - the arms are dropped. - `;` comments inside an enum body are parsed as variants. Use `//` in braces. - `pub module name;` is rejected at the top level ("Unexpected top-level token: KwModule"); `module name;` is accepted. `test_highlight.t27` uses the `pub` form, which is presumably why it reads as supported - that file is a highlighting fixture and is never compiled. Nothing under `fpga/` and nothing on the wave branch was touched. Branched from origin/master rather than the local wave branch, which carries 21 unpushed commits that are not mine to publish. --- bootstrap/src/compiler.rs | 38 ++++++++++++++++++++++++++++++++++-- bootstrap/stage0/FROZEN_HASH | 2 +- docs/NOW.md | 25 ++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index a8d70ac15..9a7a9d571 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -19845,6 +19845,13 @@ pub struct RustCodegen { const_types: std::collections::HashMap, /// Declared Rust return type of every function in this module. fn_ret_types: std::collections::HashMap, + /// Names of enums declared in this module. + /// + /// t27 writes an enum member as `Verdict.escalate`, which is Zig's + /// spelling and what the parser produces. Rust spells it + /// `Verdict::escalate`. Without knowing which identifiers name enums the + /// emitter cannot tell that access apart from a struct field. + enum_names: std::collections::HashSet, } #[allow(dead_code)] @@ -19860,6 +19867,7 @@ impl RustCodegen { var_types: std::collections::HashMap::new(), const_types: std::collections::HashMap::new(), fn_ret_types: std::collections::HashMap::new(), + enum_names: std::collections::HashSet::new(), } } @@ -20006,6 +20014,9 @@ impl RustCodegen { } fn gen_enum(&mut self, node: &Node) { + // Recorded before the body is written, so a member referenced inside + // the same module resolves however the declarations are ordered. + self.enum_names.insert(node.name.clone()); self.write_line("#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]"); self.write_line(&format!("pub enum {} {{", node.name)); self.indent += 1; @@ -20725,7 +20736,23 @@ impl RustCodegen { .collect(); format!("{} {{ {} }}", node.name, fields.join(", ")) } - NodeKind::ExprEnumValue => format!("{}::{}", node.name, node.extra_field), + NodeKind::ExprEnumValue => { + if node.extra_field.is_empty() { + // Shorthand `.variant`. t27 takes the enum from context the + // way Zig does; Rust has no such rule, so the emitter must + // supply it. The context available here is the declared + // return type of the function being emitted - exactly the + // case the shorthand is written for. Without it the variant + // was printed on the LEFT of the separator: `escalate::`. + if self.enum_names.contains(&self.fn_ret_type) { + format!("{}::{}", self.fn_ret_type, node.name) + } else { + node.name.clone() + } + } else { + format!("{}::{}", node.name, node.extra_field) + } + } NodeKind::ExprUnary => { if !node.children.is_empty() { let operand = &node.children[0]; @@ -20748,7 +20775,14 @@ impl RustCodegen { } NodeKind::ExprFieldAccess => { if !node.children.is_empty() { - format!("{}.{}", self.expr_to_rust(&node.children[0]), node.name) + let base = self.expr_to_rust(&node.children[0]); + // An enum member is a path in Rust and a field access in + // t27's Zig-shaped syntax. Only the base tells them apart. + if self.enum_names.contains(&base) { + format!("{}::{}", base, node.name) + } else { + format!("{}.{}", base, node.name) + } } else { node.name.clone() } diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index b4b2e4a72..f3c5ae07e 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -315fbe1df4f09eb5a5bd2ac8b9fd748537ccd0b5dc8f0528d602d324a4c48715 bootstrap/src/compiler.rs +cbbfac87dff32a7a8c0fee2331715453566d75f2e338e0c5ea76c089b96a8028 bootstrap/src/compiler.rs diff --git a/docs/NOW.md b/docs/NOW.md index 542421f9a..4ca00556b 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -1,3 +1,28 @@ +# NOW -- an enum member is a path, not a field access (2026-08-20) + +Last updated: 2026-08-20 + +## fix(rust-emitter): enum paths and the shorthand's missing type + +Working on the trios side, moving the supervisor's decision core out of Swift +into a `.t27` ring. Generating it exposed two gaps in the Rust backend, both in +how an enum member is spelled, and together they made every enum-returning +function ungeneratable: + +- `Verdict.escalate` emitted as `Verdict.escalate`; Rust needs `::`. +- The shorthand `.escalate` emitted as `escalate::` -- the variant on the left + of the separator. + +Both fixed in `RustCodegen`. The seal moved because `compiler.rs` changed; +new digest recorded per FROZEN.md §5. + +Not touched: the FPGA stands, `fpga/`, or anything on the wave branch. Three +further gaps are recorded in the commit message and left alone -- `switch` as a +function body, `;` comments inside an enum body, and `pub module` not being +picked up from a `//`-commented file. + +--- + # NOW -- the props read with -formal too (2026-08-20) Last updated: 2026-08-20