Skip to content

gen-c: a fixed-array struct field is storage, not a pointer - #3447

Merged
gHashTag merged 1 commit into
masterfrom
parity/all-positions
Sep 8, 2026
Merged

gen-c: a fixed-array struct field is storage, not a pointer#3447
gHashTag merged 1 commit into
masterfrom
parity/all-positions

Conversation

@gHashTag

@gHashTag gHashTag commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Closes #3446 · Refs #3445, #3443

struct Holder { f : [4]u8, g : i32, }
backend field struct size
C uint8_t* f; 16
Rust pub f: [u8; 4], 8
Zig f: [4]u8, 8
Verilog packed, 4×8 + 32 64 bits

The field held no storage. h.f[0] = 1 wrote through an uninitialised
pointer, and the same struct had two layouts depending on which backend
produced it — which is the assumption the whole project rests on. Sizes
measured by compiling and printing sizeof, not inferred.

65 fields across 31 specs. The array-field path already existed for fields
whose size the parser puts in extra_size; this spelling never reached it.

Measured — 582 generated C translation units

before after
errors 3849 3825
files that compile 296 301

Per file: six better, one worse, and the regression is named rather than
netted away. specs/physics/zamolodchikov_4d_conjecture.t27 gains one error
because a field that is now a real array is initialised from a function
returning double*. That function is #3445 — fixing the field made the other
defect visible.

Five files go from failing to clean: fpga/hir (9→0), fpga/memory (5→0),
tri/trees/octree (3→0), tri/trees/quadtree (3→0), fpga/stdlib (2→0).

Three positions, three answers

The narrowing is the design:

position spelling why
parameter T x[static N] the only form that checks the caller (#3435)
field T f[N] ordinary C storage
return C cannot return an array

A test pins the parameter position, because a field rewrite leaking into
parameters would silently undo #3435.

#3445 is filed, not fixed — because the obvious repair is worse

Normalising [N]T into the existing [T; N] machinery was built and measured:

before attempt
errors 3849 3878
files that compile 296 287
-Wreturn-stack-address 18 15

The type becomes a struct while the indexing (a[i] vs a.v[i]) and the call
sites do not: +56 incompatible pointer types passing 't27_arr_int32_t_3 *',
+15 subscripted value is not an array. Built, measured, reverted.

A mutant survived, and it is written down

Deleting the ; guard in c_array_field changes nothing any test catches —
[u8; 4] reaches the emptiness check below it anyway. The guard is redundant
today, is kept as intent with a comment saying so, and the position it was
meant to protect now has its own test.

mutant dead tests
field path disabled 3
zero-length guard removed 1
nested dimensions dropped 1
; guard removed 0 — equivalent, documented

Full suite: 2609 passed, 0 failed.

The tool

tools/backend_parity_table.py gains return and field position. The parameter
table alone hid both of these findings.

Its own Verilog column was empty for six field rows until the matcher learned
that a struct has two Verilog shapes — one packed vector, or a reg per
field. A gap in the tool printed as a fact about the backend, which is the
third time this session; the self-check now covers all three positions.

77 seals refreshed in the same commit.

Closes #3446
Refs #3445, #3443

    struct Holder { f : [4]u8, g : i32, }

    C      uint8_t* f;        sizeof(Holder) = 16
    Rust   pub f: [u8; 4],    size_of        =  8
    Zig    f: [4]u8,
    Verilog                   packed, 4*8 + 32 bits

The field held NO STORAGE. `h.f[0] = 1` wrote through an uninitialised
pointer, and the same struct had two different layouts depending on which
backend produced it -- which is the assumption the whole project rests on.
Sizes measured by compiling and printing, not inferred. 65 fields in 31 specs.

The array-field path already existed for fields whose size the parser puts in
`extra_size`; this spelling never reached it.

MEASURED, over 582 generated C translation units:

    errors              3849 -> 3825
    files that compile   296 ->  301

Per file: six better, one worse. The regression is named rather than netted
away -- `specs/physics/zamolodchikov_4d_conjecture.t27` gains one error because
a field that is now a real array is initialised from a function returning
`double*`. That function is #3445: a fixed-array RETURN emits the address of a
stack local, 18 sites in 14 files. Fixing the field made the other defect
visible.

THREE POSITIONS, THREE ANSWERS, and the narrowing is the design. A parameter
wants `T x[static N]`, the spelling that checks the caller (#3435) -- pinned by
a test here, because a field rewrite leaking into parameters would silently
undo it. A field wants `T f[N]`. A return cannot be an array in C at all.

#3445 is filed rather than fixed because the obvious repair was tried and
measured WORSE: normalising `[N]T` into the `[T; N]` machinery took errors to
3878 and compiling files to 287, since the type becomes a struct while the
indexing and the call sites do not. Built, measured, reverted.

A MUTANT SURVIVED. Deleting the `;` guard in `c_array_field` changes nothing
any test catches -- `[u8; 4]` reaches the emptiness check below it anyway. The
guard is redundant today, kept as intent, and the position it was meant to
protect now has its own test. Four other mutants died.

tools/backend_parity_table.py gains return and field position. The parameter
table alone hid both of these, and its own Verilog column was empty for six
field rows until the matcher learned that a struct has two Verilog shapes --
one packed vector, or a `reg` per field. A gap in the tool, printed as a fact
about the backend, which is the third time this session.

77 seals are refreshed in the same commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gHashTag
gHashTag enabled auto-merge (squash) September 8, 2026 01:37
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-09-08 01:37:17 UTC

Summary

Status Count
Total Open PRs 16
PRs with Failing Checks 13
PRs with All Checks Green 3
READY 2
FAILING 13
PENDING 0
NO CHECKS YET 0

These columns do not partition: 2 + 13 + 0 + 0 = 15, and there are 16 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=7ca9bc868124 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@gHashTag
gHashTag merged commit b716454 into master Sep 8, 2026
29 of 33 checks passed
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

gHashTag added a commit that referenced this pull request Sep 8, 2026
Closes #3448

    var x : [4]GF16;   ->   GF16 x[4];
                            error: use of undeclared identifier 'GF16'

The same element is `uint16_t` in a parameter (`uint16_t a[static 4]`) and in
a struct field (`uint16_t f[4];`). Only the local path passed the t27 text
through, because it gated on `is_primitive` -- and the note retiring that very
gate sits two hundred lines below, on `param_type_to_c`:

    W583: this used to be gated on `is_primitive`, which lists only the
    integer scalars -- so `f32`, `f64`, `str`, `string` and `gf16` took the
    pass-through arm and reached C unmapped even after `type_to_c` learned
    them.

Same sentence, same file, other position.

Two more defects live in the same eight lines. `var x : []u8;` emitted
`uint8_t x[];`, which is not a definition; and `var x : []const u8;` emitted
`const u8* x`, because the qualifier lives INSIDE the element text and this
path did not strip it.

The first slice repair was wrong, and the corpus said so: rewriting every
`[]T` local to a pointer broke `T x[] = { ... }`, which is legal C and takes
its size from the initialiser list. +5 errors, nothing better, in the one file
carrying the shape. `size.is_empty() && node.children.is_empty()` is that
measurement kept as code.

MEASURED, and this is the part that matters beyond this commit.

CLANG'S DEFAULT -ferror-limit=20 HAS BEEN CENSORING EVERY TOTAL I HAVE
PUBLISHED THIS SESSION. 141 files reach the limit. The corpus emits 15133
errors, not the 3849 reported. The per-file splits were sound, because they
were counted per file; the totals were floors.

    this change      15133 -> 15126   3 files better, 0 worse
    #3447, re-measured   15188 -> 15133   published as 3849 -> 3825

The direction and the per-file conclusions stand; the magnitudes were
understated. It surfaced because a repair that removes real errors showed a
ZERO delta -- three files sitting at exactly 20 before and after.

tools/backend_parity_table.py gains the fourth position. Its first probe there
measured the wrong thing: `var x : T;` with no initialiser is declared by C and
Rust and emitted by NEITHER Zig nor Verilog, so the table was reading their
dead-code removal. Initialising from a parameter of the same type fixed it, and
the self-check now covers all four positions.

Four mutants, four dead. 14 seals refreshed in the same commit.

Co-authored-by: lab <lab@example.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@gHashTag

gHashTag commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

Correction to the figures in this PR.

The error totals above — 3849 → 3825 — are floors, not counts. Clang's default -ferror-limit=20 stops at twenty errors per file, and 141 files reach it, so the aggregate was censored.

Re-measured with -ferror-limit=0:

before after
errors 15188 15133
files that compile 296 301

So this change removed 55 errors, not 24. The direction, the per-file split (six better, one worse) and the named regression were all counted per file and are unaffected.

Found while measuring #3448, where a repair that removes real errors showed a zero delta — three files sitting at exactly 20 both before and after.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gen-c: a struct field declared [N]T becomes a pointer, so the same struct is 16 bytes in C and 8 in Rust

1 participant