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
60 changes: 60 additions & 0 deletions bootstrap/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15044,6 +15044,66 @@ impl VerilogCodegen {
return;
}
let child = &node.children[0];
// #2240: NESTED scalar-struct field access (e.g. state.flags.empty).
// The single-level branch below matches only identifier/index/call
// bases, so a field-access base fell through to name-flattening and
// emitted `state[96 +: 4]_empty` -- an identifier fragment glued onto
// a part-select, invalid Verilog (fifo.v, 6 sites). Resolve the whole
// chain to ONE cumulative part-select on the base identifier.
if child.kind == NodeKind::ExprFieldAccess {
let mut names = vec![node.name.clone()];
let mut cur = child;
while cur.kind == NodeKind::ExprFieldAccess && !cur.children.is_empty() {
names.push(cur.name.clone());
cur = &cur.children[0];
}
if cur.kind == NodeKind::ExprIdentifier {
names.reverse();
let chain_base = cur.name.clone();
if let Some(local_ty) = self.local_types
.get(&chain_base)
.or_else(|| self.param_types.get(&chain_base))
.or_else(|| self.module_types.get(&chain_base))
{
if self.is_lowerable_scalar_struct_type(local_ty)
&& Self::parse_array_type(local_ty).is_none()
{
let mut cur_ty = Self::base_type_name(local_ty);
let mut total_off = 0u32;
let mut fw = 0u32;
let mut ftype = String::new();
let mut ok = true;
for fname in &names {
match self.struct_field_offset(&cur_ty, fname) {
Some((off, w)) => {
total_off += off;
fw = w;
ftype = self.struct_decls.get(&cur_ty)
.and_then(|fs| {
fs.iter()
.find(|(n, _)| n == fname)
.map(|(_, t)| t.clone())
})
.unwrap_or_default();
cur_ty = Self::base_type_name(&ftype);
}
None => { ok = false; break; }
}
}
if ok && fw > 0 {
let signed = Self::scalar_field_is_signed(&ftype);
let slice = format!("{}[{} +: {}]", chain_base, total_off, fw);
if signed && !self.in_lvalue {
self.write(&format!("$signed({})", slice));
} else {
self.write(&slice);
}
return;
}
}
}
}
}
// W533: packed single scalar-struct field access (e.g. p.x).
let base_name = match child.kind {
NodeKind::ExprIdentifier => child.name.clone(),
Expand Down
2 changes: 1 addition & 1 deletion bootstrap/stage0/FROZEN_HASH
Original file line number Diff line number Diff line change
@@ -1 +1 @@
54d19991b0c234c95d83d5348d546f4d91b2732b39bea29f8d4459e4cc248ba2
375b2f88cc2f1c58e5ec26bae8efd1f78fa712491a98216cfd98a69d206ae041
17 changes: 17 additions & 0 deletions docs/NOW.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# NOW -- the honest red has been repaired for real (2026-08-20)

Last updated: 2026-08-20

## fix(gen-verilog): nested struct field resolves to one cumulative part-select (Closes #2240)

- state.flags.empty emitted 'state[96 +: 4]_empty' -- the ExprFieldAccess branch
matched identifier/index/call bases but not a field-access base, so the chain
fell through to name-flattening and glued an identifier onto a part-select
(fifo.v, 6 sites, invalid Verilog; the old warning-only lint absorbed it for
40 minutes after the 32/32 claim)
- The chain now resolves to a single cumulative part-select (state[96 +: 1] /
state[97 +: 1]); signed fields keep $signed() on rvalue reads. M5 performed
- Negative controls: zero glued sites in regenerated fifo.v; yosys parses it;
the full 32-module smoke set lints 32/32 with the repo's own flags -- the
honest lint gate (#2239) can now be GREEN for real

# NOW -- the vacuous greens are gates again (2026-08-19)

Last updated: 2026-08-19
Expand Down
Loading