26 of 32 emitted modules reference struct fields under a prefix declared nowhere
fpga-conformance has been red on every master run since fd245d2aa (#2242) turned
the iverilog step from ::warning into ::error + exit 1. The red is correct and the
gate is correct. What is broken is the Verilog emitter: it writes function/task bodies
that reference flattened struct-field identifiers never declared in any enclosing
scope, and marks the very structs it is about to mis-emit with its own
UNSUPPORTED_ICARUS comment before emitting them anyway.
This is not #2241 (nothing executes the vectors — no vvp anywhere). This is that
28 of 32 modules will not even compile, so #2241's lane has nothing to run.
Measurement
Source: the fpga-verilog artifact of FPGA E2E Build run #1037
(id 32464326319, head bba06449, branch master, 80,443 bytes, 32 .v files).
Locally under Icarus Verilog 13.0 (stable) (v13_0) with the workflow's exact flags:
pass=4, fail=28 — byte-for-byte the count CI reports in its own annotation on that
run: 28/32 modules failed iverilog compilation.
The 4 that compile: hw_types.v, spi.v, top_level.v, uart.v.
Error families across the 28:
342 error: Unable to bind wire/reg/memory (r-value reads)
178 error: Unable to elaborate condition expression. (downstream of the above)
28 error: Could not find variable (l-value writes)
5 error: Unknown module type (all 5 in zerodsp_top.v)
3 error: Enable of unknown task (all 3 in bridge.v)
2 error: No function named (both in bridge.v)
Unable to bind and Could not find variable are the same defect seen from two sides:
iverilog reports an unresolvable read as the former and an unresolvable write as
the latter.
Scope split — 26 emitter, 2 harness
| class |
count |
files |
| flattened struct-field prefix (this issue) |
26 |
all failing except the two below |
| cross-file references under a per-file compile |
2 |
bridge.v (calls mac_*/spi_* tasks defined in other files), zerodsp_top.v (instantiates 5 modules defined in other files) |
| compile clean |
4 |
hw_types.v, spi.v, top_level.v, uart.v |
The 2 in the middle row are a different problem — the gate compiles each .v in
isolation, so a genuine cross-module reference cannot resolve. Do not fold them in here.
The correlation is exact. All 26 flatten-class failures contain at least one
// UNSUPPORTED_ICARUS: struct <Name> contains non-lowerable fields line. All 4 passing
files contain zero. The emitter already knows which structs it cannot lower.
Per-file error counts (28 failing):
axi4 57 ternary_isa 51 hir 41 fifo 36 gf16_accel 35 formal 32 memory 30
placement 26 apb_bridge 24 mac 22 assembler 22 stdlib 18 dft 18
vcd_trace 16 linker 15 timing 14 testbench 12 e2e_demo 12 clock_domain 11
simulator 10 bootrom 10 router 9 cts 9 partition 8 bridge 6
zerodsp_top 5 crossopt 5 power 4
Verbatim samples:
mac.v:212: error: Could not find variable ``mac_units_status'' in ``ZeroDSP_MAC.mac_cycle.mac_cycle_body''
fifo.v:174: error: Unable to bind wire/reg/memory `cfg_depth' in `Fifo.addr_width.addr_width_body'
gf16_accel.v:292: error: Unable to bind wire/reg/memory `cfg_has_fft' in `Gf16Accel.bram_count.bram_count_body'
memory.v:206: error: Unable to bind wire/reg/memory `result_port_count' in `Memory.add_read_port.add_read_port_body'
Reproduction
No build required. Needs gh (authenticated) and iverilog >= 11.
mkdir -p /tmp/t27vlog && cd /tmp/t27vlog
gh run download 32464326319 --repo gHashTag/t27 --name fpga-verilog --dir .
pass=0; fail=0
for v in ./*.v; do
name=$(sed -n 's/^module \([A-Za-z0-9_]*\).*/\1/p' "$v" | head -1)
if iverilog -o "/tmp/t27vlog/${name}_tb.vvp" -g2012 -DSIMULATION "$v" 2>"/tmp/t27vlog/$(basename "$v" .v).log"; then
pass=$((pass+1))
else
fail=$((fail+1))
fi
done
echo "pass=$pass fail=$fail total=$((pass+fail))" # => pass=4 fail=28 total=32
The loop body is copied from .github/workflows/fpga-build.yml:716-732 (step
"Compile conformance testbenches (iverilog)"); only the output paths differ.
Error-family histogram:
cat /tmp/t27vlog/*.log | grep -o "error: [A-Za-z][^\`']*" | sed 's/ *$//' | sort | uniq -c | sort -rn
Confirm the UNSUPPORTED_ICARUS correlation:
grep -c UNSUPPORTED_ICARUS /tmp/t27vlog/*.v | grep -v ':0$' | wc -l # => 26
grep -c UNSUPPORTED_ICARUS /tmp/t27vlog/{hw_types,spi,top_level,uart}.v # => all 0
Artifact retention is 7 days from 2026-08-21. To regenerate from source instead:
cargo build --release -p t27c
t27c fpga-build --smoke # writes build/fpga/generated/*.v
then run the same loop over build/fpga/generated/*.v.
Mechanism, in one file
fifo.v is the whole defect in 40 lines. Module-scope registers, emitted by
gen_verilog_struct (artifact fifo.v:55-67):
// struct FifoConfig
// UNSUPPORTED_ICARUS: struct FifoConfig contains non-lowerable fields
reg [31:0] fifoconfig_name; // FifoConfig.name
reg signed [7:0] fifoconfig_kind; // FifoConfig.kind
reg [31:0] fifoconfig_depth; // FifoConfig.depth
reg [31:0] fifoconfig_data_width; // FifoConfig.data_width
...
// struct FifoState
// struct FifoState lowered as packed vector (100 bits)
FifoConfig took the flatten branch, prefixed with the lowercased type name.
FifoState took the packed branch and gets no registers at all.
Now the use site (fifo.v:286-306):
// function: push
function [99:0] push; // -> FifoState
input [99:0] state;
input [31:0] cfg;
begin : push_body
reg [31:0] result;
reg __t27_ret;
__t27_ret = 1'b0;
result = state;
if (state[97 +: 1]) begin
push = result;
__t27_ret = 1'b1;
end else begin
result_tail_ptr = ((state[64 +: 32] + 1) % cfg_depth);
result_fill_count = (state[0 +: 32] + 1);
result_flags_empty = 1'b0;
if ((result_fill_count == cfg_depth)) begin
result_flags_full = 1'b1;
end
push = result;
Three prefixes for two struct types, in one function:
state — FifoState on the packed path. state[64 +: 32] resolves. Correct.
result — the same FifoState, on the flatten path. result_tail_ptr is
declared nowhere. reg [31:0] result exists but is a different, unrelated name.
cfg — FifoConfig on the flatten path, so cfg_depth; the registers were emitted
as fifoconfig_depth.
The unbound identifiers in fifo.v: cfg_almost_empty_threshold,
cfg_almost_full_threshold, cfg_data_width, cfg_depth, cfg_has_almost_empty,
cfg_has_almost_full, cfg_kind, cfg_name, result_fill_count, plus the l-value
class result_tail_ptr, result_head_ptr, result_flags_empty, result_flags_full.
Note the second-order damage even if the names were fixed: result = state assigns a
100-bit value into reg [31:0] result, and the writes to result_tail_ptr are to a
name unrelated to result, so push = result returns the truncated, unmodified copy.
Renaming alone does not restore the dataflow.
Why no correct prefix exists
gen_verilog_struct emits one register set per struct type, at module scope, under
one prefix. But a struct type can be bound under several names simultaneously in a
single scope — push above holds FifoState as both state and result at the same
time — and each binding needs its own storage.
So the prefix decision is unsatisfiable as posed:
- Prefix by type name (
fifoconfig_) — what the fallback does — matches no use site,
because bodies emit <binding>_<field>.
- Prefix by binding name — what
struct_var tries — can only pick one binding per
type. In push it must be state and result.
- Prefix by binding, emitted per binding, would put function-local storage at module
scope, aliasing every call site and destroying re-entrancy.
There is no third option that keeps per-type module-scope registers. The flatten path
is wrong by construction, not by an off-by-one in prefix selection, which is why the
existing struct_var map cannot be patched into correctness.
Proposal, not a decision
The packed path already works — state[64 +: 32] compiles and carries correct
semantics. The plausible direction is to make it the only path: declare every struct
binding as input [W-1:0] base (or a local reg [W-1:0]) and lower every base.field
to a base[off +: w] part-select, deleting the flatten branch and the
UNSUPPORTED_ICARUS marker with it. That requires is_lowerable_scalar_struct to
accept everything it currently rejects, which is a real piece of work and interacts with
#2319 (extents the width function cannot size). I am not asserting this is the right
design — it is the direction the working half of the emitter suggests. The decision is
open.
Compiler references, checked against origin/master (fead099c2)
bootstrap/src/compiler.rs is 37,141 lines on master. Reading it from a stale checkout
produces wrong line numbers, so these were re-derived from git show origin/master: today.
Holds — the fallback, compiler.rs:11892-11895:
for s in &structs {
let prefix = struct_var
.get(&s.name)
.cloned()
.unwrap_or_else(|| s.name.to_lowercase());
self.gen_verilog_struct(s, &prefix);
}
Holds — the comment that condemns that exact fallback, compiler.rs:11877-11881:
// Declare struct field regs under the module-level `var` name that
// holds the struct (e.g. `uart_state_status`), matching exactly what
// gen_verilog_expr emits for field access. Falling back to the struct
// type name (`uartstate_status`) leaves every field reference an
// undeclared identifier and breaks iverilog compilation.
It is at :11877-11881, i.e. eleven lines above the let prefix, not four — the ten
lines between it build the struct_var map. The comment documents the failure mode the
code immediately below it still takes, and fifo.v's fifoconfig_depth is that comment's
uartstate_status example realized.
Holds — the two branches, gen_verilog_struct at compiler.rs:12946, dispatching on
is_lowerable_scalar_struct at :12949, UNSUPPORTED_ICARUS emitted at :12962-12965,
per-field reg {prefix}_{field} at :12988-12996.
Does not hold as a scope figure for this defect — the repo's 489 of 618 /
79.1% at compiler.rs:13026-13028. That comment is real, but it belongs to a
different cause (W657/T103): param_types was cleared in gen_verilog_fn and never
repopulated, so struct-typed function parameters fell to the flatten fallback. That
repair has landed — :13033-13035 now populates the map:
for (pname, ptype) in &node.params {
self.param_types.insert(pname.clone(), ptype.clone());
}
Citing 489/618 as this issue's scope would be wrong on two counts: wrong defect, and a
figure whose fix is already in the tree. The scope measured today is 26 of 32 emitted
modules (81.3%) on the artifact above.
Do not soften the gate
Before fd245d2aa (2026-08-19, #2242) this step ran bare -g2005, compiled 0 of 32,
and reported ::warning — a permanent green that measured nothing. That commit changed
it to -g2012 -DSIMULATION with exit 1, which is what surfaced this. Reverting to a
warning restores the false green. The red should stay until the emitter is fixed. If the
lane must move meanwhile, exclude by explicit named list with a link back here — never by
downgrading the annotation level.
Related
26 of 32 emitted modules reference struct fields under a prefix declared nowhere
fpga-conformancehas been red on everymasterrun sincefd245d2aa(#2242) turnedthe iverilog step from
::warninginto::error+exit 1. The red is correct and thegate is correct. What is broken is the Verilog emitter: it writes function/task bodies
that reference flattened struct-field identifiers never declared in any enclosing
scope, and marks the very structs it is about to mis-emit with its own
UNSUPPORTED_ICARUScomment before emitting them anyway.This is not #2241 (nothing executes the vectors — no
vvpanywhere). This is that28 of 32 modules will not even compile, so #2241's lane has nothing to run.
Measurement
Source: the
fpga-verilogartifact of FPGA E2E Build run #1037(id
32464326319, headbba06449, branchmaster, 80,443 bytes, 32.vfiles).Locally under Icarus Verilog 13.0 (stable) (v13_0) with the workflow's exact flags:
pass=4, fail=28 — byte-for-byte the count CI reports in its own annotation on that
run:
28/32 modules failed iverilog compilation.The 4 that compile:
hw_types.v,spi.v,top_level.v,uart.v.Error families across the 28:
Unable to bindandCould not find variableare the same defect seen from two sides:iverilog reports an unresolvable read as the former and an unresolvable write as
the latter.
Scope split — 26 emitter, 2 harness
bridge.v(callsmac_*/spi_*tasks defined in other files),zerodsp_top.v(instantiates 5 modules defined in other files)hw_types.v,spi.v,top_level.v,uart.vThe 2 in the middle row are a different problem — the gate compiles each
.vinisolation, so a genuine cross-module reference cannot resolve. Do not fold them in here.
The correlation is exact. All 26 flatten-class failures contain at least one
// UNSUPPORTED_ICARUS: struct <Name> contains non-lowerable fieldsline. All 4 passingfiles contain zero. The emitter already knows which structs it cannot lower.
Per-file error counts (28 failing):
Verbatim samples:
Reproduction
No build required. Needs
gh(authenticated) andiverilog>= 11.The loop body is copied from
.github/workflows/fpga-build.yml:716-732(step"Compile conformance testbenches (iverilog)"); only the output paths differ.
Error-family histogram:
Confirm the
UNSUPPORTED_ICARUScorrelation:Artifact retention is 7 days from 2026-08-21. To regenerate from source instead:
cargo build --release -p t27c t27c fpga-build --smoke # writes build/fpga/generated/*.vthen run the same loop over
build/fpga/generated/*.v.Mechanism, in one file
fifo.vis the whole defect in 40 lines. Module-scope registers, emitted bygen_verilog_struct(artifactfifo.v:55-67):FifoConfigtook the flatten branch, prefixed with the lowercased type name.FifoStatetook the packed branch and gets no registers at all.Now the use site (
fifo.v:286-306):Three prefixes for two struct types, in one function:
state—FifoStateon the packed path.state[64 +: 32]resolves. Correct.result— the sameFifoState, on the flatten path.result_tail_ptrisdeclared nowhere.
reg [31:0] resultexists but is a different, unrelated name.cfg—FifoConfigon the flatten path, socfg_depth; the registers were emittedas
fifoconfig_depth.The unbound identifiers in
fifo.v:cfg_almost_empty_threshold,cfg_almost_full_threshold,cfg_data_width,cfg_depth,cfg_has_almost_empty,cfg_has_almost_full,cfg_kind,cfg_name,result_fill_count, plus the l-valueclass
result_tail_ptr,result_head_ptr,result_flags_empty,result_flags_full.Note the second-order damage even if the names were fixed:
result = stateassigns a100-bit value into
reg [31:0] result, and the writes toresult_tail_ptrare to aname unrelated to
result, sopush = resultreturns the truncated, unmodified copy.Renaming alone does not restore the dataflow.
Why no correct prefix exists
gen_verilog_structemits one register set per struct type, at module scope, underone prefix. But a struct type can be bound under several names simultaneously in a
single scope —
pushabove holdsFifoStateas bothstateandresultat the sametime — and each binding needs its own storage.
So the prefix decision is unsatisfiable as posed:
fifoconfig_) — what the fallback does — matches no use site,because bodies emit
<binding>_<field>.struct_vartries — can only pick one binding pertype. In
pushit must bestateandresult.scope, aliasing every call site and destroying re-entrancy.
There is no third option that keeps per-type module-scope registers. The flatten path
is wrong by construction, not by an off-by-one in prefix selection, which is why the
existing
struct_varmap cannot be patched into correctness.Proposal, not a decision
The packed path already works —
state[64 +: 32]compiles and carries correctsemantics. The plausible direction is to make it the only path: declare every struct
binding as
input [W-1:0] base(or a localreg [W-1:0]) and lower everybase.fieldto a
base[off +: w]part-select, deleting the flatten branch and theUNSUPPORTED_ICARUSmarker with it. That requiresis_lowerable_scalar_structtoaccept everything it currently rejects, which is a real piece of work and interacts with
#2319 (extents the width function cannot size). I am not asserting this is the right
design — it is the direction the working half of the emitter suggests. The decision is
open.
Compiler references, checked against
origin/master(fead099c2)bootstrap/src/compiler.rsis 37,141 lines on master. Reading it from a stale checkoutproduces wrong line numbers, so these were re-derived from
git show origin/master:today.Holds — the fallback,
compiler.rs:11892-11895:Holds — the comment that condemns that exact fallback,
compiler.rs:11877-11881:It is at
:11877-11881, i.e. eleven lines above thelet prefix, not four — the tenlines between it build the
struct_varmap. The comment documents the failure mode thecode immediately below it still takes, and
fifo.v'sfifoconfig_depthis that comment'suartstate_statusexample realized.Holds — the two branches,
gen_verilog_structatcompiler.rs:12946, dispatching onis_lowerable_scalar_structat:12949,UNSUPPORTED_ICARUSemitted at:12962-12965,per-field
reg {prefix}_{field}at:12988-12996.Does not hold as a scope figure for this defect — the repo's
489 of 618/79.1%atcompiler.rs:13026-13028. That comment is real, but it belongs to adifferent cause (W657/T103):
param_typeswas cleared ingen_verilog_fnand neverrepopulated, so struct-typed function parameters fell to the flatten fallback. That
repair has landed —
:13033-13035now populates the map:Citing 489/618 as this issue's scope would be wrong on two counts: wrong defect, and a
figure whose fix is already in the tree. The scope measured today is 26 of 32 emitted
modules (81.3%) on the artifact above.
Do not soften the gate
Before
fd245d2aa(2026-08-19, #2242) this step ran bare-g2005, compiled 0 of 32,and reported
::warning— a permanent green that measured nothing. That commit changedit to
-g2012 -DSIMULATIONwithexit 1, which is what surfaced this. Reverting to awarning restores the false green. The red should stay until the emitter is fixed. If the
lane must move meanwhile, exclude by explicit named list with a link back here — never by
downgrading the annotation level.
Related
vvpstep). Downstream of this; its laneneeds these 28 to compile first. Cross-reference, not a duplicate.
mac.vspecifically: imported-enum members never declared, plus a struct-typedfn param flattened. Same emitter, narrower slice;
mac.vcontributes 22 of the 558errors counted here.
is_lowerable_scalar_structaccepts named-const array extents thatfield_type_widthsizes as0. Directly blocks the packed-path proposal above: makingpacked the only path requires the width function to size everything the predicate accepts.
ZeroDSP_TopLevelsynthesizes to 0 cells.