diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index d5399ce29..b56090f21 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -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(), diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index a0232017d..8f0ec1b67 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -54d19991b0c234c95d83d5348d546f4d91b2732b39bea29f8d4459e4cc248ba2 +375b2f88cc2f1c58e5ec26bae8efd1f78fa712491a98216cfd98a69d206ae041 diff --git a/docs/NOW.md b/docs/NOW.md index 9e4b59e0e..abe5da261 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -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