Skip to content

gen-c: carry a fixed-size array parameter's length into C - #3435

Merged
gHashTag merged 1 commit into
masterfrom
c/static-array-params
Sep 7, 2026
Merged

gen-c: carry a fixed-size array parameter's length into C#3435
gHashTag merged 1 commit into
masterfrom
c/static-array-params

Conversation

@gHashTag

@gHashTag gHashTag commented Sep 7, 2026

Copy link
Copy Markdown
Owner

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.

The obvious spelling is wrong

Apple clang 21, -Wall -Wextra, a 2-element array into a 4-element parameter:

prototype result
void f(uint64_t a[4]); silent -- the parameter 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]

This mattered for the test, not just the fix. A test asserting only on the
emitted text passes on [4], which checks nothing. So
a_short_caller_is_diagnosed_and_a_correct_one_is_not hands both a short
caller and a correct one to cc and requires the diagnostic on the first and
its 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:

old new
errors 3849 3849
warnings 2705 2705
distinct diagnostic kinds 881 881

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_layer inside the real generated corpus raised
array argument is too small; contains 1 elements, callee requires at least 8
and the comparator reported it -- so the parity above is a measurement, not a
blind spot.

Mutation -- 3 mutants, 3 killed:

mutant dead tests
new path disabled 2
[static N] -> [N] 2, including the behavioural one
zero-length guard removed 1

Full suite: 2588 passed, 0 failed.

What this does not do

It finds no existing defect -- the generated corpus compiles to 0
-Warray-bounds today. Its value is that the next short call is rejected
instead of accepted, which matters most for hand-written callers outside this
repo.

Narrowing

  • Parameter position only. param_type_to_c also spells struct fields, where
    [static N] is a syntax error -- the same narrowing the *T -> &mut T
    lowering 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]T parameter keeps the pointer.
  • A const-named size is emitted by NAME ([static N]), which is only safe
    because the file #defines it earlier -- asserted by the test.
  • A slice []u8 has no compile-time length and stays a pointer.

Withdrawn

Two figures I published in earlier passes are wrong and are retracted here:

  1. "fiat-crypto's uint64_t out1[4] cannot be passed short without a compiler
    diagnostic." False -- plain [4] is silent, as the table above shows.
  2. "150 such parameters across 120 functions." That was a line-based regex
    that cannot see a multi-line parameter list. The emitter says 51 in 24.

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>
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-09-07 23:47:31 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)=78efe5d475af != 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).

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

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

@gHashTag
gHashTag enabled auto-merge (squash) September 7, 2026 23:47
@gHashTag
gHashTag merged commit c01ab67 into master Sep 7, 2026
27 of 33 checks passed
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>
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>
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>
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 fixed-size [N]T parameter loses its size and the caller is never checked

1 participant