← board

sizeof of a partial index answers the element, not the row

Why prio 80 — above the segfaults in the sweep

A segfault is loud. This truncates a copy and returns.

int src[3][4], dst[3][4];
memcpy(dst[1], src[1], sizeof(src[1]));
gcc pxx
dst[1] after the memcpy 10 11 12 13 10 -1 -1 -1
same through a field, s.m[2] 20 21 22 23 20 -1 -1 -1
sizeof(src[0]) / sizeof(src[0][0]) 4 1

Both are the idiomatic C spellings — size a row copy from the row, count a row's elements by dividing the sizes. Neither involves a cast, a pointer, or a struct. The last line is the one that will quietly halve a loop bound somewhere in a corpus.

Two different defects, found by varying the shape

The census reported one wrong cell; an isolated probe disagreed with it, and separating the parenthesis from the spelling showed two mechanisms with two different wrong answers:

form gcc pxx
sizeof m[0] (bare, ident) 16 16 — correct
sizeof(m[0]) (parens, ident) 16 4
sizeof gs.m[0] (bare, field) 16 8
sizeof(gs.m[0]) (parens, field) 16 4
sizeof m / sizeof(m) (whole array) 48 48 — correct

Why nothing found this before — and it is the point of the sweep's method

This is the case a grep for the duplicated-arm pattern is structurally unable to find. There is no correct sibling arm to notice the absence against: sizeof(m[0]) is wrong for an ident and a local and a field and p->m and a nested field. A search for divergent copies cannot find logic that is missing everywhere (frankS, 2026-08-29, from the xtensa ABIParamSlotHoldsValueAddr case — the backend that needed converting was the one with zero copies of the pattern).

It fell out of the enumeration on the first run, because a behavioural cell is wrong whether the code that should handle it is divergent or absent.

Note on the measurement

The census harness spells every cell parenthesised, so its sizeof-row row reported 16!=4 across all six spellings and read as one universal defect. An isolated probe using the bare form contradicted it. Both were right about different things; the grid above is what separating the two variables shows. A census cell that disagrees with a hand probe is a signal to vary the shape, not to pick a winner.

Root — there were TWO sizeof implementations, and the good one had already published this bug

ParseCSizeof served the two spellings from two different mechanisms:

spelling mechanism multi-dim rule
sizeof a[0] (no parens) a type-descriptor walk"walked as a type descriptor rather than per-shape special cases, so the chain composes to any depth" yes
sizeof(a[0]) (parens) its own per-shape copy: a .-chain walk and a [ arm none

So one parenthesis decided whether memcpy copied a row or an int.

And the descriptor walk's own comment names the exact failure its twin had:

"Without the spans sizeof m[0] would answer the element size and ARRAY_SIZE(m) would count 15 rather than 3."

Written by someone who understood the bug completely, in the arm that did not have it. Grep for the incumbent before building — and note this is the worst version of that rule: a search for the symptom lands on that comment and reads it as handled, because it genuinely was handled, just not on the path the user's parenthesis takes.

The second defect had the same shape one level down. The walk bailed to the pointer-size default for a multi-dim record field, on the stated grounds that "the spans of a multidim RECORD FIELD are not reachable by symbol index"a correct sentence justifying a wrong default. True of symbol indices; RecFieldArrDimSpanAt already existed. That bail is the 8-for-int-char-double.

Fix (DONE)

  1. The descriptor walk asks RecFieldArrDimSpanAt for a record field's spans instead of bailing.
  2. The walk is extracted as CSizeofDescriptorWalk and the parenthesised path routed through it, deleting 107 lines — the whole second implementation.

compiler/cparser.inc is ~100 lines shorter and one sizeof implementation lighter. The overhaul was the smaller job, as devdocs/dev/root-cause-over-microfix.md says it usually is.

Gate — MET

Log