From a4b01790cef8a7bdeafe094b24ac3505ff09ac8d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 19:33:49 +0700 Subject: [PATCH] fix(verilog): declare the enums a spec imports (Closes #2316) `gen_verilog_expr` lowers `Enum.variant` to the flat identifier `Enum_variant` whichever spec declared the enum; `gen_verilog` declared the matching `localparam` only for enums a spec declares itself. An enum arriving through `use` therefore produced a reference with nothing declaring it, which is not valid Verilog under any standard: gen/mac.v:100: error: Unable to bind wire/reg/memory `Trit_neg' in `ZeroDSP_MAC.extract_trit.extract_trit_body' `use_resolve::imported_enums` returns `(enum, [(variant, value)])` for the specs a file imports -- enums only, no splicing. `resolve()` pulls whole declarations the Verilog backend cannot lower, and 492 of the 650 specs carry a `use` line; an enum is the one case where the backend already does everything except emit the declaration. The backend emits them through the same `gen_verilog_enum` a same-spec enum goes through. Two filters. Referenced only: an enum the module never names emits nothing, so `specs/fpga/uart.t27` -- which imports both modules that declare `Trit` and never mentions it -- is byte-identical to before. Never shadow: a name the module already declares is dropped, since a redeclaration is a compile error and this pass may only ever add a declaration nothing else provides. Measured with the prebuilt `t27c` at `fb88da234`, which regenerates `mac.v` byte-identical to the `fpga-verilog` artifact of run 32358697899: `iverilog -g2012 -DSIMULATION gen/mac.v` goes from 28 errors to 22, none of them mentioning `Trit_`. Across all 650 specs, exactly six import an enum they name and do not declare, and all six want `Trit`. `fpga-conformance` stays red at 4/32. The other 22 errors in `mac.v`, and 26 of the 28 failing modules, are a different defect: a struct-typed function parameter is declared as one scalar and then referenced through `_` names nothing declares (`word_raw`, `mac_units_status`, `cfg_addr_width` -- 202 distinct identifiers). Choosing one struct representation is a design decision, not a repair, and it is not made here. Closes #2316 --- bootstrap/src/compiler.rs | 136 ++++++++++++- bootstrap/src/main.rs | 5 +- bootstrap/src/use_resolve.rs | 55 ++++++ bootstrap/tests/verilog_imported_enum.rs | 231 +++++++++++++++++++++++ docs/NOW.md | 81 ++++++++ 5 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 bootstrap/tests/verilog_imported_enum.rs diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 9a7a9d571..598b8f1e6 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -8948,6 +8948,15 @@ pub struct VerilogCodegen { array_param_bindings: std::collections::HashMap>, array_param_indices: std::collections::HashMap>, array_param_errors: std::collections::HashMap, + // Enums declared by a spec this one `use`s, as (enum, [(variant, value)]). + // `Enum.variant` has always lowered to the identifier `Enum_variant`, and a + // `localparam` was declared for it only when the enum was declared in the + // SAME spec -- so an imported enum produced a reference with no + // declaration, which no Verilog standard accepts. Populated only through + // `Compiler::compile_verilog_at`, since resolving `use base::ops;` needs + // 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)>)>, } #[derive(Clone, Copy, PartialEq)] @@ -8996,9 +9005,18 @@ impl VerilogCodegen { array_param_bindings: std::collections::HashMap::new(), array_param_indices: std::collections::HashMap::new(), array_param_errors: std::collections::HashMap::new(), + imported_enums: 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`. + pub fn set_imported_enums(&mut self, enums: Vec<(String, Vec<(String, String)>)>) { + self.imported_enums = enums; + } + // Restored from Wave Loop 455 (same botched merge). fn is_simple_tuple_type(ty: &str) -> bool { ty.starts_with('(') @@ -10606,6 +10624,7 @@ impl VerilogCodegen { array_param_bindings: std::collections::HashMap::new(), array_param_indices: std::collections::HashMap::new(), array_param_errors: std::collections::HashMap::new(), + imported_enums: Vec::new(), }; tmp.gen_verilog_expr(node); buf.push_str(&tmp.output); @@ -10823,6 +10842,7 @@ impl VerilogCodegen { array_param_bindings: std::collections::HashMap::new(), array_param_indices: std::collections::HashMap::new(), array_param_errors: std::collections::HashMap::new(), + imported_enums: Vec::new(), }; tmp.emit_packed_array_literal_concat_level( sub, dims, depth + 1, elem_w, elem_type, @@ -11741,13 +11761,23 @@ impl VerilogCodegen { } // Section: Enum parameters - if !enums.is_empty() { + // + // An enum reached through `use` is emitted by the SAME function as a + // same-spec enum, because the body already spells the reference the + // same way -- `Enum.variant` lowers to `Enum_variant` whichever spec + // declared it. Imported ones come first: they are what the local + // declarations may depend on, never the other way round. + let imported_enum_nodes = self.imported_enum_nodes(ast, &enums); + if !enums.is_empty() || !imported_enum_nodes.is_empty() { self.write_indent(); self.write_line("// -------------------------------------------------------"); self.write_indent(); self.write_line("// Enum constants"); self.write_indent(); self.write_line("// -------------------------------------------------------"); + for e in &imported_enum_nodes { + self.gen_verilog_enum(e); + } for e in &enums { self.gen_verilog_enum(e); } @@ -12773,6 +12803,84 @@ impl VerilogCodegen { self.write_line(""); } + /// The enums this module imports AND actually names, as `EnumDecl` nodes + /// so they go through the same emitter as a same-spec enum. + /// + /// Two filters, both deliberate: + /// + /// * **Referenced only.** An unused `localparam` is harmless to a + /// simulator, but emitting one per imported enum would rewrite the + /// generated Verilog of every spec that merely imports a module with an + /// enum in it. Only `Enum.variant` references pull anything in. + /// * **Never shadow.** If the module already declares that name, or + /// already declares the `Enum_variant` identifier by another route, the + /// import is dropped. A redeclaration is a compile error, so this pass + /// must only ever ADD a declaration that nothing else provides. + fn imported_enum_nodes(&self, ast: &Node, local_enums: &[&Node]) -> Vec { + if self.imported_enums.is_empty() { + return Vec::new(); + } + let mut referenced: std::collections::HashSet = + std::collections::HashSet::new(); + Self::collect_field_access_bases(ast, &mut referenced); + if referenced.is_empty() { + return Vec::new(); + } + + let mut taken: std::collections::HashSet = std::collections::HashSet::new(); + for decl in &ast.children { + if !decl.name.is_empty() { + taken.insert(decl.name.clone()); + } + } + for e in local_enums { + for v in &e.children { + taken.insert(format!("{}_{}", e.name, v.name)); + } + } + + let mut out: Vec = Vec::new(); + for (name, variants) in &self.imported_enums { + if !referenced.contains(name) || taken.contains(name) { + continue; + } + if variants + .iter() + .any(|(variant, _)| taken.contains(&format!("{}_{}", name, variant))) + { + continue; + } + let mut node = Node::new(NodeKind::EnumDecl); + node.name = name.clone(); + for (variant, value) in variants { + let mut child = Node::new(NodeKind::EnumVariant); + child.name = variant.clone(); + child.value = value.clone(); + taken.insert(format!("{}_{}", name, variant)); + node.children.push(child); + } + out.push(node); + } + out + } + + /// The `Enum` half of every `Enum.variant`-shaped reference in the tree. + /// + /// This is the same shape `gen_verilog_expr` lowers to `Enum_variant`, so + /// asking it what the body names is asking exactly the right question. + fn collect_field_access_bases(node: &Node, out: &mut std::collections::HashSet) { + if node.kind == NodeKind::ExprFieldAccess { + if let Some(base) = node.children.first() { + if base.kind == NodeKind::ExprIdentifier { + out.insert(base.name.clone()); + } + } + } + for child in &node.children { + Self::collect_field_access_bases(child, out); + } + } + fn gen_verilog_enum(&mut self, node: &Node) { self.write_indent(); self.write_line(&format!("// enum {}", node.name)); @@ -17742,14 +17850,31 @@ impl Compiler { } pub fn compile_verilog(source: &str) -> Result { - Self::compile_verilog_with_options(source, false) + Self::compile_verilog_with_options(source, false, None) + } + + /// `compile_verilog`, told where the spec lives. + /// + /// Only a caller holding the path can resolve `use base::ops;` to a file, + /// and until it is resolved the backend cannot know that `Trit.neg` -- + /// which it lowers to the identifier `Trit_neg` -- needs a `localparam`. + /// Callers holding only a source string keep the old behaviour. + pub fn compile_verilog_at( + source: &str, + spec_path: &std::path::Path, + ) -> Result { + Self::compile_verilog_with_options(source, false, Some(spec_path)) } pub fn compile_verilog_for_simulation(source: &str) -> Result { - Self::compile_verilog_with_options(source, true) + Self::compile_verilog_with_options(source, true, None) } - fn compile_verilog_with_options(source: &str, emit_test_assertions: bool) -> Result { + fn compile_verilog_with_options( + source: &str, + emit_test_assertions: bool, + spec_path: Option<&std::path::Path>, + ) -> Result { let lexer = Lexer::new(source); let mut parser = Parser::new(lexer); let mut ast = parser.parse()?; @@ -17761,6 +17886,9 @@ impl Compiler { Self::detect_unsupported_verilog_locals(&ast, &structs)?; optimize(&mut ast, &OptConfig::default()); 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.gen_verilog(&ast); Ok(codegen.into_string()) } diff --git a/bootstrap/src/main.rs b/bootstrap/src/main.rs index af7acfc53..f63e3c3d0 100644 --- a/bootstrap/src/main.rs +++ b/bootstrap/src/main.rs @@ -4009,7 +4009,10 @@ fn run_gen(input_path: &str) -> anyhow::Result<()> { let path = Path::new(input_path); let source = fs::read_to_string(path)?; - match compiler::Compiler::compile_verilog(&source) { + // The path, not just the source: `use base::ops;` names a file, and until + // it is read the backend emits `Trit_neg` with nothing declaring it. See + // `use_resolve::imported_enums` -- enums only, no splicing. + match compiler::Compiler::compile_verilog_at(&source, path) { Ok(verilog_code) => { print!("{}", verilog_code); if with_sva { diff --git a/bootstrap/src/use_resolve.rs b/bootstrap/src/use_resolve.rs index b1992d005..be8d1495e 100644 --- a/bootstrap/src/use_resolve.rs +++ b/bootstrap/src/use_resolve.rs @@ -412,6 +412,61 @@ pub fn resolve(input_path: &Path, source: &str) -> String { out } +/// The enums declared by the specs this one imports, as +/// `(enum, [(variant, value)])` in `use` order. +/// +/// This is deliberately NOT `resolve`. Splicing pulls whole declarations -- +/// functions, structs, constants -- and the Verilog backend cannot lower most +/// of them, so widening its input is a change of behaviour for 492 specs. An +/// enum is different: the backend ALREADY lowers `Enum.variant` to the +/// identifier `Enum_variant`, and it already declares a `localparam` for every +/// enum a spec declares itself. The only thing missing when the enum arrives +/// through `use` is the declaration. That is what this returns, and nothing +/// else. +/// +/// Direct imports only, and only dependencies that parse on their own -- the +/// same contract `resolve` carries. `specs/base/types.t27` does not parse and +/// declares `Trit`; a spec that also imports `base::ops` still gets `Trit`, +/// because ops declares the same enum. The first declaration of a name wins, +/// so one file can never resolve one name two ways. +pub fn imported_enums(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::EnumDecl || decl.name.is_empty() { + continue; + } + if !seen.insert(decl.name.clone()) { + continue; + } + // The value is carried verbatim, including the empty string, so the + // backend applies the same "no value means the ordinal" rule to an + // imported enum that it applies to a local one. + let variants: Vec<(String, String)> = decl + .children + .iter() + .filter(|v| v.kind == crate::compiler::NodeKind::EnumVariant) + .map(|v| (v.name.clone(), v.value.clone())) + .collect(); + out.push((decl.name.clone(), variants)); + } + } + out +} + #[cfg(test)] mod tests { use super::*; diff --git a/bootstrap/tests/verilog_imported_enum.rs b/bootstrap/tests/verilog_imported_enum.rs new file mode 100644 index 000000000..8d07600f3 --- /dev/null +++ b/bootstrap/tests/verilog_imported_enum.rs @@ -0,0 +1,231 @@ +// ============================================================================= +// An enum reached through `use` must be DECLARED in the Verilog that +// references it. +// +// `gen_verilog_expr` lowers `Enum.variant` to the flat identifier +// `Enum_variant` no matter which spec declared the enum, but the declaration +// -- `localparam Enum_variant = ...;` -- was emitted only for enums the spec +// declares ITSELF. So `specs/fpga/mac.t27`, which does `use base::ops;` and +// returns `Trit.neg`, generated Verilog naming `Trit_neg` with nothing +// anywhere declaring it. That is not valid Verilog under any standard: +// +// gen/mac.v:100: error: Unable to bind wire/reg/memory `Trit_neg' +// in `ZeroDSP_MAC.extract_trit.extract_trit_body' +// +// Yosys did not object, because these modules hold no `always` block and the +// emitted functions are dead code it never elaborates -- so the `fpga-lint` +// gate read 32/32 green while every one of the 32 files was failing iverilog. +// The gate that looked is the one that was right. +// +// These tests assert the property directly rather than a byte pattern: every +// `Enum_variant`-shaped identifier the output USES is one the output +// DECLARES. They shell out to the built `t27c` so the path threading +// (`gen-verilog ` -> `compile_verilog_at`) is exercised too; a unit test +// on a source string cannot see that half of it. +// +// phi^2 + 1/phi^2 = 3 | TRINITY +// ============================================================================= + +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn gen_verilog(spec: &Path) -> String { + let bin = env!("CARGO_BIN_EXE_t27c"); + let out = Command::new(bin) + .args(["gen-verilog", spec.to_str().expect("spec path is utf8")]) + .output() + .expect("t27c gen-verilog should run"); + assert!( + out.status.success(), + "t27c gen-verilog failed on {}:\nstderr: {}", + spec.display(), + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8_lossy(&out.stdout).into_owned() +} + +/// Every name declared by a `localparam = ...;` line. +fn declared_localparams(src: &str) -> HashSet { + let mut out = HashSet::new(); + for line in src.lines() { + let Some(rest) = line.trim_start().strip_prefix("localparam ") else { + continue; + }; + // `localparam [7:0] NAME = 0;` -- skip an optional range first. + let rest = match rest.trim_start().strip_prefix('[') { + Some(after) => match after.find(']') { + Some(i) => &after[i + 1..], + None => continue, + }, + None => rest, + }; + let name: String = rest + .trim_start() + .chars() + .take_while(|c| c.is_alphanumeric() || *c == '_') + .collect(); + if !name.is_empty() { + out.insert(name); + } + } + out +} + +/// Every identifier in the code (comments stripped) that starts with +/// `_`, i.e. every flattened enum-variant reference. +fn variant_refs(src: &str, enum_name: &str) -> HashSet { + let prefix = format!("{}_", enum_name); + let mut out = HashSet::new(); + for line in src.lines() { + let code = match line.find("//") { + Some(i) => &line[..i], + None => line, + }; + let mut cur = String::new(); + for c in code.chars().chain(std::iter::once(' ')) { + if c.is_alphanumeric() || c == '_' { + cur.push(c); + continue; + } + if cur.starts_with(&prefix) && cur.len() > prefix.len() { + out.insert(cur.clone()); + } + cur.clear(); + } + } + out +} + +fn assert_every_variant_declared(src: &str, enum_name: &str, where_: &str) { + let declared = declared_localparams(src); + let used = variant_refs(src, enum_name); + assert!( + !used.is_empty(), + "{}: expected the generated Verilog to reference {}_; \ + if the spec stopped using the enum this test needs a new subject", + where_, + enum_name + ); + let undeclared: Vec<&String> = used.iter().filter(|u| !declared.contains(*u)).collect(); + assert!( + undeclared.is_empty(), + "{}: {} identifier(s) referenced with no declaration: {:?}. \ + iverilog reports these as `Unable to bind wire/reg/memory`.", + where_, + undeclared.len(), + undeclared + ); +} + +fn repo_root() -> PathBuf { + let mut cur: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + loop { + if cur.join("specs").join("fpga").join("mac.t27").is_file() { + return cur; + } + assert!(cur.pop(), "could not locate a repo root holding specs/fpga/mac.t27"); + } +} + +// ----------------------------------------------------------------------------- +// 1. The spec the FPGA gate compiles: `use base::ops;`, returns `Trit.neg`. +// ----------------------------------------------------------------------------- + +#[test] +fn mac_spec_declares_the_trit_enum_it_imports() { + let spec = repo_root().join("specs").join("fpga").join("mac.t27"); + let src = gen_verilog(&spec); + assert_every_variant_declared(&src, "Trit", "specs/fpga/mac.t27"); + + // The values come from the imported declaration, not from the ordinal: + // `base/ops.t27` declares `Trit = enum(i8) { neg = -1, zero = 0, pos = 1 }`. + for expected in [ + "localparam Trit_neg = -1;", + "localparam Trit_zero = 0;", + "localparam Trit_pos = 1;", + ] { + assert!( + src.contains(expected), + "specs/fpga/mac.t27: missing `{}` -- an imported enum must carry \ + its declared values, exactly as a same-spec enum does", + expected + ); + } +} + +// ----------------------------------------------------------------------------- +// 2. Importing a module that declares an enum is NOT enough. +// +// `specs/fpga/uart.t27` does `use base::types;` and `use base::ops;` -- both +// declare `Trit` -- and never names it. Its generated Verilog must be exactly +// what it was before. Without this the pass would rewrite the output of every +// spec that merely imports a module with an enum in it, and the 492 specs that +// carry a `use` line would all move at once. +// ----------------------------------------------------------------------------- + +#[test] +fn an_unreferenced_imported_enum_emits_nothing() { + let spec = repo_root().join("specs").join("fpga").join("uart.t27"); + let src = gen_verilog(&spec); + assert!( + variant_refs(&src, "Trit").is_empty(), + "specs/fpga/uart.t27 is the control case: it must not reference Trit_*" + ); + assert!( + !src.contains("localparam Trit"), + "specs/fpga/uart.t27: an imported enum the module never names must not \ + be emitted.\n--- generated Verilog ---\n{}", + src + ); +} + +// ----------------------------------------------------------------------------- +// 3. The same property on a spec tree this test owns, so the guarantee does +// not depend on what the FPGA specs happen to import next month. +// ----------------------------------------------------------------------------- + +const PALETTE: &str = "module tb-palette; + +pub const Hue = enum(i8) { + low = -1, + mid = 0, + high = 1, +}; +"; + +const IMPORTER: &str = "module ImportedEnumProbe; + +use tb::palette; + +fn pick(selector: u32) -> u32 { + if (selector == 0) { + return Hue.low; + } else if (selector == 1) { + return Hue.high; + } else { + return Hue.mid; + } +} +"; + +#[test] +fn a_synthetic_import_resolves_through_the_specs_root() { + let base = std::env::temp_dir().join(format!("t27_imported_enum_{}", std::process::id())); + let dir = base.join("specs").join("tb"); + std::fs::create_dir_all(&dir).expect("create temp spec tree"); + std::fs::write(dir.join("palette.t27"), PALETTE).expect("write palette.t27"); + let importer = dir.join("importer.t27"); + std::fs::write(&importer, IMPORTER).expect("write importer.t27"); + + let src = gen_verilog(&importer); + let _ = std::fs::remove_dir_all(&base); + + assert_every_variant_declared(&src, "Hue", "synthetic tb::palette importer"); + assert!( + src.contains("localparam Hue_low = -1;"), + "synthetic importer: expected `localparam Hue_low = -1;`\n\ + --- generated Verilog ---\n{}", + src + ); +} diff --git a/docs/NOW.md b/docs/NOW.md index dc2fa6aed..a8d26b017 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -1,3 +1,84 @@ +# NOW -- an enum reached through `use` was referenced but never declared (2026-08-20) + +Last updated: 2026-08-20 + +## fix(verilog): declare the enums a spec imports (Closes #2316) + +`fpga-conformance` went hard-red at `fd245d2aa` and its log cannot say why: the +step redirects every compiler error to `/dev/null` and prints one count. Rerun +by hand against the `fpga-verilog` artifact of run 32358697899, the loop is +`pass=4 fail=28 total=32` -- and it has been 32/32 failing since the step was +born on 2026-04-14 in `544479121`, warning-only, eight sampled greens all +carrying `##[warning]32/32 modules failed iverilog compilation`. Nothing +regressed; arming an always-failing gate made a four-month-old emitter defect +visible. + +Of the four causes behind those 28, exactly one is mechanical and this is it. + +### The defect + +`gen_verilog_expr` lowers `Enum.variant` to the flat identifier `Enum_variant` +whichever spec declared the enum. `gen_verilog` declared +`localparam Enum_variant = ...;` only for enums a spec declares itself, so an +enum arriving through `use` produced a reference with no declaration: + +``` +gen/mac.v:100: error: Unable to bind wire/reg/memory `Trit_neg' in `ZeroDSP_MAC.extract_trit.extract_trit_body' +``` + +`specs/fpga/mac.t27` does `use base::ops;`, and `base/ops.t27` declares +`Trit = enum(i8) { neg = -1, zero = 0, pos = 1 }`. + +### Enums only -- not `use_resolve::resolve` + +Splicing whole declarations is the wrong instrument here: `resolve` pulls +functions, structs and constants, the Verilog backend cannot lower most of them, +and 492 of the 650 specs carry a `use` line. An enum is the one case where the +backend already does everything except emit the declaration. So +`use_resolve::imported_enums` returns `(enum, [(variant, value)])` and nothing +else, and the backend emits it through the same `gen_verilog_enum` a same-spec +enum goes through. + +Two filters keep the blast radius at what it must be. **Referenced only**: an +enum the module never names emits nothing, so `uart.t27` -- which imports both +modules that declare `Trit` and never mentions it -- is byte-identical to before. +**Never shadow**: a name the module already declares is dropped, because a +redeclaration is a compile error and this pass may only ever add a declaration +nothing else provides. + +Measured across all 650 specs, the ones that import an enum, name it, and do not +declare it locally are six, and all six want `Trit`. Two of them +(`demos/jones_topology_*`) import only `base::types`, which is `NOPARSE`, so they +are unchanged; the other four also import `base::ops`. + +### What it moves, and what it does not + +The prebuilt `t27c` at `fb88da234` regenerates `mac.v` byte-identical to the CI +artifact, so it is a faithful stand-in. With the three localparams in place: + +| | `iverilog -g2012 -DSIMULATION gen/mac.v` | +|---|---| +| as generated | 28 errors | +| after | 22 errors, **0** mentioning `Trit_` | + +`mac.v` still fails, so **`fpga-conformance` stays red at 4/32**. The remaining +22 -- and 26 of the 28 failing modules -- are one different defect: a +struct-typed function parameter is declared as one scalar and then referenced +through `_` names nothing declares (`word_raw`, +`mac_units_status`, `cfg_addr_width`, 202 distinct identifiers). That is a +design decision about how structs lower, not a repair, and it is not made here. +`bridge.v` (cross-spec calls to functions defined inside other modules -- illegal +in Verilog whatever file set you compile) and `zerodsp_top.v` (a structural +wrapper the loop compiles without its five dependencies) are the other two. + +### Two premise defects in the gate itself, unfixed + +Named so a future green here is not mistaken for proof: the step is called +*"Compile conformance testbenches"* but compiles design files -- no testbench +exists in the artifact, `vvp` never runs, and the `conformance/fpga_*.json` +vectors the previous step enumerates are fed to nothing. And `2>/dev/null` +guarantees that when this gate fails, its log cannot say why. That is why it sat +unread for four months. # NOW -- tri ci baseline: PR gates that have never run on the branch they gate (2026-08-20) Last updated: 2026-08-20