diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 236e617d9..201287872 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -9000,6 +9000,7 @@ pub struct VerilogCodegen { // the path of the spec being compiled and a source string does not carry // one. Empty everywhere else, which is exactly the old behaviour. imported_enums: Vec<(String, Vec<(String, String)>)>, + imported_structs: Vec<(String, Vec<(String, String)>)>, } #[derive(Clone, Copy, PartialEq)] @@ -9049,6 +9050,7 @@ impl VerilogCodegen { array_param_indices: std::collections::HashMap::new(), array_param_errors: std::collections::HashMap::new(), imported_enums: Vec::new(), + imported_structs: Vec::new(), } } @@ -9056,6 +9058,14 @@ impl VerilogCodegen { /// /// Nothing is emitted for an enum the module never names; see /// `imported_enum_nodes`. + /// #2275: imported structs join `struct_decls` so a `param.field` on an + /// imported struct type resolves to a part-select instead of flattening to + /// an unbound identifier. Local declarations always win: entries are only + /// added for names the module does not declare itself. + pub fn set_imported_structs(&mut self, structs: Vec<(String, Vec<(String, String)>)>) { + self.imported_structs = structs; + } + pub fn set_imported_enums(&mut self, enums: Vec<(String, Vec<(String, String)>)>) { self.imported_enums = enums; } @@ -10668,6 +10678,7 @@ impl VerilogCodegen { array_param_indices: std::collections::HashMap::new(), array_param_errors: std::collections::HashMap::new(), imported_enums: Vec::new(), + imported_structs: Vec::new(), }; tmp.gen_verilog_expr(node); buf.push_str(&tmp.output); @@ -10886,6 +10897,7 @@ impl VerilogCodegen { array_param_indices: std::collections::HashMap::new(), array_param_errors: std::collections::HashMap::new(), imported_enums: Vec::new(), + imported_structs: Vec::new(), }; tmp.emit_packed_array_literal_concat_level( sub, dims, depth + 1, elem_w, elem_type, @@ -11439,6 +11451,12 @@ impl VerilogCodegen { .collect(); self.struct_decls.insert(s.name.clone(), fields); } + // #2275: imported structs fill the gaps -- never shadow a local decl. + for (name, fields) in &self.imported_structs.clone() { + self.struct_decls + .entry(name.clone()) + .or_insert_with(|| fields.clone()); + } // W528: cache module-level const/var type annotations so function-local // and test-bench code can resolve packed array-of-struct accesses. @@ -17931,6 +17949,7 @@ impl Compiler { let mut codegen = VerilogCodegen::with_options(emit_test_assertions); if let Some(path) = spec_path { codegen.set_imported_enums(crate::use_resolve::imported_enums(path, source)); + codegen.set_imported_structs(crate::use_resolve::imported_structs(path, source)); } codegen.gen_verilog(&ast); Ok(codegen.into_string()) diff --git a/bootstrap/src/use_resolve.rs b/bootstrap/src/use_resolve.rs index be8d1495e..01f39041a 100644 --- a/bootstrap/src/use_resolve.rs +++ b/bootstrap/src/use_resolve.rs @@ -467,6 +467,45 @@ pub fn imported_enums(input_path: &Path, source: &str) -> Vec<(String, Vec<(Stri out } +/// The struct declarations of every direct `use` dependency, in the same +/// `(name, fields)` shape `struct_decls` stores for a local struct. Mirrors +/// `imported_enums` (#2275): `word.raw` on an imported-struct param used to +/// fall past the part-select branch (struct_field_offset had no entry) and +/// flatten to the unbound identifier `word_raw`. +pub fn imported_structs(input_path: &Path, source: &str) -> Vec<(String, Vec<(String, String)>)> { + let specs_root = match find_specs_root(input_path) { + Some(r) => r, + None => return Vec::new(), + }; + let mut seen: HashSet = HashSet::new(); + let mut out: Vec<(String, Vec<(String, String)>)> = Vec::new(); + for dep in use_targets(source, &specs_root) { + let text = match std::fs::read_to_string(&dep) { + Ok(t) => t, + Err(_) => continue, + }; + let ast = match crate::compiler::Compiler::parse_ast(&text) { + Ok(a) => a, + Err(_) => continue, + }; + for decl in &ast.children { + if decl.kind != crate::compiler::NodeKind::StructDecl || decl.name.is_empty() { + continue; + } + if !seen.insert(decl.name.clone()) { + continue; + } + let fields: Vec<(String, String)> = decl + .children + .iter() + .map(|f| (f.name.clone(), f.extra_type.clone())) + .collect(); + out.push((decl.name.clone(), fields)); + } + } + out +} + #[cfg(test)] mod tests { use super::*; diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index d3e99f324..835690cb9 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -fba627661c36de08193b310f85c8f19a7e2dfdd77baf3495f12828ae2bd76b0a bootstrap/src/compiler.rs +7c99ba252ee218df2e00bb3708e53f96ea5f64ff66d80542aecf2010682272a9 diff --git a/docs/now/2026-08-22-imported-structs-and-the-phantom-ternaryword.md b/docs/now/2026-08-22-imported-structs-and-the-phantom-ternaryword.md new file mode 100644 index 000000000..e8c07cebe --- /dev/null +++ b/docs/now/2026-08-22-imported-structs-and-the-phantom-ternaryword.md @@ -0,0 +1,21 @@ +# NOW -- imported structs resolve, and mac's phantom TernaryWord is declared (2026-08-22) + +## imported structs join struct_decls; mac.t27 declares the word it always assumed (Refs #2275) + +- `word.raw` on a struct-typed fn param emitted the unbound identifier `word_raw`: + the part-select branch consults `struct_decls`, which held only module-local + structs. Mirroring the imported-enums pass, `use_resolve::imported_structs` + now loads every direct dependency's struct decls and they merge into + `struct_decls` without ever shadowing a local declaration. M5 performed. +- The deeper find: NO file in the corpus declares `TernaryWord{raw}` -- the + `base::ternary_memory` struct of that name is a different shape + (trits/state/checksum). mac.t27 referenced a phantom type and got the + fallback width by coincidence. The spec now declares its own + `struct TernaryWord { raw : u32 }`; `word.raw` lowers to `word[0 +: 32]`, + parse and typecheck stay clean, the full 32-module smoke set lints 32/32. +- Still open in #2275: the `mac_units` array-of-structs state (nested array + field) -- its DECLARATION emits `reg [31:0]` plus a "not yet lowered" TODO, + so element-field access has nothing to bind to. That is an emitter feature, + not a reference fix. +- Two verilog unit tests fail on clean master identically (keyword-escape + local array, for-range) -- pre-existing, measured before blame. diff --git a/specs/fpga/mac.t27 b/specs/fpga/mac.t27 index dc866dfff..befdad962 100644 --- a/specs/fpga/mac.t27 +++ b/specs/fpga/mac.t27 @@ -60,6 +60,14 @@ module ZeroDSP_MAC; // 314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 // MAC unit state + // #2275: the packed word this spec has always assumed. No file in the + // corpus declares TernaryWord{raw} -- the base::ternary_memory struct is a + // different shape (trits/state/checksum) -- so `word.raw` fell past the + // part-select branch and flattened to the unbound identifier `word_raw`. + struct TernaryWord { + raw : u32, + } + struct MACUnit { accumulator : i32, // Accumulator value status : u8, // Current status (READY/BUSY/DONE)