Skip to content

gen-verilog: is_lowerable_scalar_struct accepts a named-const array extent that field_type_width sizes as 0 — TernaryWord declared 40 bits, correct is 3712 #2319

Description

@gHashTag

The lowerability predicate accepts a struct the width function cannot size

is_lowerable_scalar_struct_d documents an invariant, in its own comment at
bootstrap/src/compiler.rs:10932-10939:

W681: the SAME cap the width computation uses. When these two drifted -- the
predicate counting struct levels while field_type_width counted
field-then-struct, twice per level -- a struct the predicate accepted could be
one the width function refused to size, and the refusal was a silent 0. They
now share DEPTH_CAP, and the predicate is deliberately the STRICTER of the
two: it consumes one level per nesting step against the width function's two,
so anything it accepts can be sized.

The invariant holds for depth. It does not hold for array extents.

The predicate rejects only an empty extent (W669, :10974-10978):

let inner = trimmed[1..end].trim();
if inner.is_empty() {
    return false;
}
trimmed[end + 1..].trim()

The width function requires the extent to be a numeric literal
(field_type_width, :10180-10185):

let Ok(count) = inner.parse::<u32>() else {
    return 0;
};
let base = t[close + 1..].trim();
return count * self.field_type_width(base, depth + 1);

A named-const extent -- [TRIT_CAPACITY]TritCell -- is non-empty, so the
predicate accepts it, and does not parse as u32, so the width function returns
the poison 0. packed_struct_width (:10199-10204) then does exactly what its
own W681 comment warns about:

fields.iter().map(|(_, ft)| self.field_type_width(ft, depth)).sum()

.sum() swallows the 0. The struct is declared at a plausible width with an
entire field missing, and every field after it is sliced from the wrong bits.
That is verbatim the failure :10159-10171 says the repair must prevent:

the guard fired, this function returned 0, and packed_struct_width's sum()
swallowed it -- so the struct was declared at a plausible-looking width that is
wrong [...] The repair is not a larger cap; it is that
is_lowerable_scalar_struct_d must refuse any struct this function cannot
size, so the failure is loud.

Measured magnitude

specs/base/ternary_memory.t27:21,36,80,149:

const TRIT_CAPACITY : usize = 27;
const WORD_CAPACITY : usize = 1024;

struct TritCell   { value: i32, state: u8, last_access: u64, access_count: u32 }
struct TernaryWord{ trits: [TRIT_CAPACITY]TritCell, state: u8, checksum: u32 }
struct TernaryMemoryBank { words: [WORD_CAPACITY]TernaryWord, allocated: usize,
                           total_accesses: u64 }

TritCell = 32+8+64+32 = 136 bits, and it sizes correctly (no array).

struct emitted today correct field offsets today correct
TernaryWord 40 bits 27x136 + 40 = 3712 state@0, checksum@8 @3672, @3680
TernaryMemoryBank 96 bits 1024x3712 + 96 = 3801184 allocated@0 @3801088

gen_verilog_struct:12905-12916 takes the lowerable branch for both, so it emits
// struct TernaryWord lowered as packed vector (40 bits) and no per-field
registers -- there is no UNSUPPORTED_ICARUS line and nothing is loud.

Blast radius: 7 structs

Across the hand-written corpus, 103 struct fields in 66 structs carry a
non-empty non-numeric array extent. 59 of those structs are already rejected by
the predicate for other reasons, so they leak nothing. Seven are marked
lowerable and are therefore sized with one field omitted:

MACUnit           .pipeline : [PIPELINE_STAGES]TernaryWord
RecallEpisode     .episodes : [POLICY_WINDOW_SIZE]usize
TVCBigInt         .trits    : [MAX_TRITS]i8
TernaryMemoryBank .words    : [WORD_CAPACITY]TernaryWord
TernaryWord       .trits    : [TRIT_CAPACITY]TritCell
WordAddResult     .sum      : [TRITS_PER_WORD]i32
WordSubResult     .diff     : [TRITS_PER_WORD]i32

This affects annotated params, returns and locals -- packed_width:9574,
fn-return width :13031, param width :13003 -- not only inferred ones. It is
live on master now.

Why this blocks the separate local-width defect

There is a second, distinct defect: an unannotated local gets
reg [31:0] regardless of its real type. parse_var_decl:2264 fills
extra_type only under TokenKind::Colon and nothing infers it, so
var result = state; (specs/fpga/fifo.t27:168,182) leaves extra_type == "";
emit_local:14482 then calls type_to_width(""), hits _ => 32 (:9270), and
emits reg [31:0] result; for a 100-bit FifoState. Worse, local_types holds
"", so result.tail_ptr falls back to the flat name result_tail_ptr, which
gen_verilog_struct declares nowhere now that FifoState is lowerable.

The obvious repair is to infer the local's type from its initializer and route it
into the already-shipping W533 path at :14286-14307. Measured scope: 17 sites
in 7 specs
.

spec sites struct today after the repair
specs/fpga/vcd_conformance_compare.t27 8 CompareResult [31:0] [127:0] correct
specs/fpga/fifo.t27 2 FifoState [31:0] [99:0] correct
specs/isa/registers.t27 3 TernaryWord [31:0] [39:0] still wrong
specs/isa/ternary_memory.t27 1 TernaryWord [31:0] [39:0] still wrong
specs/base/debounce.t27 1 Debouncer [31:0] [128:0] correct
specs/igla/coder/eval.t27 1 SimResult [31:0] [135:0] correct
specs/server/api.t27 1 Usage [31:0] [63:0] correct

4 of the 17 sites are TernaryWord. Landing the local-width repair while this
issue is open changes them from 32 to 40 bits -- still short of 3712, but now
looking fixed. A reviewer checking "the width moved off 32" would sign it off.
That is the T124 trade in its worst form, so this issue should land first.

What the repair is

Not a wider cap and not a struct-aware type_to_width. Measured over the corpus,
type_to_width's _ => 32 arm absorbs 6,638 of 14,428 width-reaching type
occurrences (46%), and 82% of those are not struct names -- strings, enums,
Option<T>, *Parser, std.mem.Allocator. Of the 264 struct names that do
reach it, only 39 are lowerable; 225 have no correct width to substitute. And
0 has no representation downstream: range_decl:9538 is
format!("[{}:0]", width - 1) on a u32 with no zero guard, so a poison 0
panics in debug and wraps to [4294967295:0] in the release build CI uses. No
caller anywhere tests a width for 0.

The repair that matches the precedent is the one :10169-10171 already names:
make the predicate refuse what the width function cannot size. Either

  1. reject a non-numeric extent in is_lowerable_scalar_struct_d (loud
    UNSUPPORTED_ICARUS, consistent with W669's treatment of []T), or
  2. resolve const extents into the codegen registry so both agree and the real
    width is emitted.

(2) is the better end state -- these are legitimate fixed-size arrays, and 3712
bits is a real number -- but it is a compiler feature, not a patch. (1) is the
bounded step and restores the invariant immediately.

Notes for whoever lands this

  • bootstrap/build.rs:235 verifies sha256(bootstrap/src/compiler.rs) against
    bootstrap/stage0/FROZEN_HASH on every cargo build. Any edit to
    compiler.rs must reseal in the same commit or master stops building --
    that is exactly what fix(verilog): declare the enums a spec imports (Closes #2316) #2317 -> fix(freeze): reseal FROZEN_HASH -- master does not build (Closes #2316) #2318 had to repair 2026-08-20.
  • The ratchet cannot see this. docs/reports/suite_expectations.json is 221
    entries at max_entries: 221, phases parse 157 and parse-no-discard 64 --
    zero gen-verilog entries, and the schema has no tags field. The
    gen-verilog phase fails only on a compiler Err/panic, never on wrong
    output, so every width above is currently invisible to it. The exposure of a
    change here is UNEXPECTED FAILURE, not UNEXPECTED PASS.
  • docs/reports/gen_verilog_smoke_baseline.json has "expected_failures": [] --
    zero amnesty over the 27 specs in igla_clean_specs() (suite.rs:1165), and
    specs/igla/coder/eval.t27 is on that list.

Provenance

Read from origin/master blobs only; no local build. The compiler.rs copy
analysed hashes c3ec9fba947b9d5845af10d1270619f6e49298698139d0e227ab669f93868bbf
under shasum -a 256, which is byte-for-byte the operational line of
bootstrap/stage0/FROZEN_HASH on master, so the line numbers above are master's.
Nothing was landed: this environment has 355 MB free and a release build needs
~1.1 GB, so the fix could not be compiled, let alone proven on generated output.

Adjacent but distinct: #2275 (struct-field part-select does not consult fn
param
types) and #2241.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions