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
19 changes: 19 additions & 0 deletions bootstrap/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -9049,13 +9050,22 @@ impl VerilogCodegen {
array_param_indices: std::collections::HashMap::new(),
array_param_errors: std::collections::HashMap::new(),
imported_enums: Vec::new(),
imported_structs: Vec::new(),
}
}

/// Hand the backend the enums it reaches through `use`.
///
/// 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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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())
Expand Down
39 changes: 39 additions & 0 deletions bootstrap/src/use_resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = 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::*;
Expand Down
2 changes: 1 addition & 1 deletion bootstrap/stage0/FROZEN_HASH
Original file line number Diff line number Diff line change
@@ -1 +1 @@
fba627661c36de08193b310f85c8f19a7e2dfdd77baf3495f12828ae2bd76b0a bootstrap/src/compiler.rs
7c99ba252ee218df2e00bb3708e53f96ea5f64ff66d80542aecf2010682272a9
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions specs/fpga/mac.t27
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading