gen-c: carry a fixed-size array parameter's length into C - #3435
Merged
Conversation
Closes #3434 `[4]u8` reached C as `uint8_t*`. The size was gone, so a caller passing a two-element array compiled in silence. `gen-rust` keeps `[u8; 4]` and Zig keeps `[4]u8`; C was the one backend that dropped it -- 51 parameters in 24 functions across 12 files. The spelling had to be measured rather than assumed, and the obvious one is wrong. Apple clang 21, `-Wall -Wextra`, a 2-element array into a 4-element parameter: void f(uint64_t a[4]); /* silent -- decays to a pointer */ void f(uint64_t a[static 4]); /* warning: array argument is too small; contains 2 elements, callee requires at least 4 [-Warray-bounds] */ So `c_static_array_param` emits `T name[static N]`, and returns None -- leaving the existing pointer lowering -- for everything it cannot render safely: a zero length (`[static 0]` is not C), an array of arrays, and a size that is neither a literal nor a const this file `#define`s. PARAMETER POSITION ONLY. `param_type_to_c` also spells struct fields, where `[static N]` is a syntax error; the two call sites rewritten here are the prototype and definition emitters and nothing else. That is the same narrowing the `*T` -> `&mut T` lowering needed. Measured on the whole corpus, old binary against new, 582 translation units each: 881 distinct diagnostic kinds, every count identical -- 3849 errors and 2705 warnings on both sides. All 48 changed lines contain `[static `; nothing else moved. A planted short call inside the real generated corpus does raise the warning, and the comparator was checked against that planted change, so the parity above is a measurement and not a blind spot. This finds no existing defect: the corpus compiles to 0 `-Warray-bounds` today. It is a guard for the next caller. Two figures published earlier are withdrawn. "fiat-crypto's `uint64_t out1[4]` cannot be passed short without a diagnostic" is false -- plain `[4]` is silent. And "150 such parameters across 120 functions" was a line-based regex that cannot see a multi-line parameter list; the emitter's own count is 51 in 24 functions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
gHashTag
enabled auto-merge (squash)
September 7, 2026 23:47
This was referenced Sep 8, 2026
gHashTag
added a commit
that referenced
this pull request
Sep 8, 2026
Refs #3440 `Spec Guards` has been red on master since #3435 and red again after #3437. Both merged, because it is not a required context -- so `--auto` never looked at it, and neither did I. It was wired one pass earlier precisely because three checks only ran when someone typed them. The guard is right. #3435 changed C parameter emission and #3437 changed the Rust `Copy` derive, so the seals recording what those backends emit no longer described the output. Measured on b519f90: seals scanned 1318 current 953 STALE generated-code hash 102 across 57 specs sealed with gen_hash=none 169 spec file no longer present 94 By field: 88 gen_hash_rust, 19 gen_hash_c. `spec_hash` stale for ZERO, which is why every coverage and freshness gate stayed green -- the same shape as #3415, one merge later. This is a cycle rather than rot: the previous `sealed_at` on the affected files was 2026-09-07T21:07:04Z, hours old. Every backend change invalidates seals and nothing in the merge path refreshes them or blocks on them. The diff is 102 files and exactly three keys move: `sealed_at` (102), `gen_hash_rust` (88), `gen_hash_c` (19). `spec_hash`, `gen_hash_verilog` and `gen_hash_zig` are untouched. After: 0 stale, the gate exits 0. Two neighbouring populations are deliberately left alone and named in #3440 rather than folded in: 94 seals whose spec file no longer exists, and 169 recording `gen_hash=none`. Conflating them is how the last batch stayed invisible. The cure for the cycle is the owner's call and is not in this commit. Co-authored-by: lab <lab@example.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 8, 2026
gHashTag
added a commit
that referenced
this pull request
Sep 8, 2026
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: lab <lab@example.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 8, 2026
gHashTag
added a commit
that referenced
this pull request
Sep 8, 2026
) Closes #3457 const A : [4]u8 = [1,2,3,4]; static const [4]u8 A = { 1, 2, 3, 4 }; error: brackets are not allowed here; to declare an array, place the brackets after the identifier The MUTABLE TWIN four lines above the site the probe found carried the same defect -- `var A : [4]u8` gives `static [4]u8 A` -- and only a grep for every site that builds a declarator from a type and a name turned it up. A slice constant was broken before either: `const A : []u8` gave `static const []u8 A`, where `T name[] = { ... }` is legal C and takes its size from the list. Fourth position for one rule, built from the field helper rather than a fourth copy of it: parameter T x[static N] #3435 struct field T f[N]; #3446 local T x[N]; #3448 module const T name[N] here MEASURED, whole corpus, -ferror-limit=0: errors 15126 -> 15021 files that compile 301 -> 302 files better 11 `math/e8_lie_algebra` 44 -> 4, `fpga/mac` 101 -> 85, `queen/lotus` 29 -> 16. ONE FILE'S COUNT ROSE AND IT IS NOT A REGRESSION. `isa/registers` goes 34 -> 44 because the declarations now PARSE: the errors that disappear are `brackets are not allowed here` and two `expected ';'`, and what appears behind them is semantic -- `TernaryWord{.raw=0}` is not C, a separate defect the parse cascade had been hiding. A raw error count is not monotone under a repair that fixes a parse error; the per-file split is what makes that visible. MY OWN TEST ASSERTED SOMETHING THAT NEVER EXISTED. It required a slice const to keep "the old pointer lowering", and there was no pointer lowering -- the type went through verbatim. The test was wrong AND the code was wrong, and the test failing is what showed both. A MUTANT SURVIVED AND THE ANSWER WAS TO DELETE. A `has_init` parameter guarded a case neither caller can produce, so the mutant deleting its check changed nothing. A guard nothing can reach is not protection: the parameter is removed rather than annotated. Two other mutants died. Round six of the parity table, the const-initialiser position it had never covered. The previous round found only latent defects; this one found a live one on its first form, so the table does not stop yet. 28 seals refreshed in the same commit. Co-authored-by: lab <lab@example.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3434
[4]u8reached C asuint8_t*. The size was gone, so a caller passing atwo-element array compiled in silence.
gen-rustkeeps[u8; 4]and Zig keeps[4]u8; C was the one backend that dropped it.The obvious spelling is wrong
Apple clang 21,
-Wall -Wextra, a 2-element array into a 4-element parameter:void f(uint64_t a[4]);void f(uint64_t a[static 4]);warning: array argument is too small; contains 2 elements, callee requires at least 4 [-Warray-bounds]This mattered for the test, not just the fix. A test asserting only on the
emitted text passes on
[4], which checks nothing. Soa_short_caller_is_diagnosed_and_a_correct_one_is_nothands both a shortcaller and a correct one to
ccand requires the diagnostic on the first andits absence on the second.
Measurements
Population -- counted by the emitter, not a grep: 51 parameters, 24
functions, 12 files (of 651 specs, 583 of which emit C).
Corpus parity, old binary against new, 582 translation units each:
Every count in the 881-kind multiset is identical. All 48 changed lines contain
[static; nothing else moved.The comparator was checked. A short call planted into
specs/ternary/bitnet_layerinside the real generated corpus raisedarray argument is too small; contains 1 elements, callee requires at least 8and the comparator reported it -- so the parity above is a measurement, not a
blind spot.
Mutation -- 3 mutants, 3 killed:
[static N]->[N]Full suite: 2588 passed, 0 failed.
What this does not do
It finds no existing defect -- the generated corpus compiles to 0
-Warray-boundstoday. Its value is that the next short call is rejectedinstead of accepted, which matters most for hand-written callers outside this
repo.
Narrowing
param_type_to_calso spells struct fields, where[static N]is a syntax error -- the same narrowing the*T->&mut Tlowering needed in fix(rust-backend): prefix the dereference, and give an out-pointer a safe type #3423.
[static 0]is not valid C, so a[0]Tparameter keeps the pointer.[static N]), which is only safebecause the file
#defines it earlier -- asserted by the test.[]u8has no compile-time length and stays a pointer.Withdrawn
Two figures I published in earlier passes are wrong and are retracted here:
uint64_t out1[4]cannot be passed short without a compilerdiagnostic." False -- plain
[4]is silent, as the table above shows.that cannot see a multi-line parameter list. The emitter says 51 in 24.