← board

What was fixed

Every parameter-indexed buffer now says MAX_PROC_PARAMS: TProc.Params (defs.inc), the thirteen cparser.inc locals, and the argUndecl call-argument mirror with its two <= 31 guard literals.

pptrdims was genuinely out of bounds and nobody had hit it: it is indexed nparams * MAX_ARR_DIMS + pmi — up to 31*6+5 = 191 — and was declared array[0..31 * MAX_ARR_DIMS - 1], i.e. 0..185. Six elements short, for a pointer-to-multidimensional-array parameter in one of the last slots. Found by widening, not by a test.

CORRECTION (2026-09-10, same day): the comment was right and I was wrong

I filed this saying record-field bounds cannot fold the constant, then "disproved" that with a two-line probe and landed e9599a1b0 writing array[0..MAX_PROC_PARAMS-1] into the field. The probe was drawn from the wrong population -- a nine-line program with the constant declared eight lines above -- and it folds there. In defs.inc the field keeps 32 slots whatever the bound says, a bare array[0..255] included, so my change was a no-op that deleted a correct warning and left source that reads as coupled. Reverted to the literal with the measurements written beside it; the layout defect is now bug-a-fourteen-compiler-internal-record-names-shadow-any-user-type — and it is a NAME-SHADOWING bug, not an array-bound one: fourteen internal record names shadow any user type of the same name, so type TProc = record ... end in an ordinary program silently gets the compiler's layout.

The original author's comment was accurate about the SYMPTOM and wrong only about the mechanism (they said const-expr; it ignores a literal too). That is CLAUDE.md's "comment vs code" rule going the other way: I decided the comment was wrong, and the deciding evidence was a control that could not fail.

The old section, kept because its premise is what broke

Params : array[0..31] of TParam;   { literal 31: record-field bounds cannot
                                     use MAX_PROC_PARAMS-1 (const-expr gap) }

There is no const-expr gap. Measured 2026-09-10 with a two-line probe against both the live compiler and stable_linux_amd64/default/pinned: a record field declared array[0..MAXP-1] of TP compiles and reports 32 elements on each. defs.inc:4617-4625CTypeFnRetPTypes and eight siblings — have been written array[0..MAX_PROC_PARAMS-1] all along, a few thousand lines below the comment saying it cannot be done.

Both halves of that paragraph are true and the conclusion drawn from them was wrong. Vars and locals do fold the constant; this record field does not size itself from its bound at all, expression or literal. The siblings were a real observation about a different construct.

{$if} over a Pascal const does not survive the pinned seed — do not guard this way

The first attempt at enforcing the coupling was {$if MAX_PROC_PARAMS <> 32} {$error ...} {$endif} in defs.inc. It builds locally: make compiler/pascal26 converges, --tier quick is green, and its positive control fires correctly when the constant is changed. The pinned compiler refuses it outright:

pascal26:0: error: conditional directive: `MAX_PROC_PARAMS` has no integer
value here, so it cannot be compared (an undefined symbol, or one defined
without a value)

{$if} evaluates preprocessor symbols; the live compiler has grown the ability to see a Pascal const there and the pin has not. So the only row that caught it was gate.sh quick's self-host fixedpoint, reporting round 1 — seed could not compile the compiler and nothing else. Exactly the defect class CLAUDE.md says that canary exists for. A compile-time guard in compiler/** must be expressed in something the PIN accepts.

What is still broken

The original body blamed the segfault on the field holding 32 slots. That was right. What was wrong was my belief that writing the constant into the bound would widen it. With every staging array derived and MAX_PROC_PARAMS = 64:

params result
32 compiles and runs
33 SIGSEGV (rc=139)
40, 64, 65 SIGSEGV

Measured on two independently built compilers — one seeded from pinned, one from compiler/pascal26 — so it is a source-level defect, not a miscompile. bss grows by only 2848 bytes at 64, which is the abi.inc/codegen local vectors and nothing else: a widened TProc.Params would have added 16384 * 32 * 40 = 21MB. That delta is the clearest single sign the field did NOT widen, and I read it as confirmation that it had — the number was in front of me from the first build.

The crash needs no body and no call site. All three of these die:

procedure wide(a0: Integer; ... a32: Integer); external;   { declaration alone }
procedure wide(...); begin WriteLn(a0); end;               { defined, never called }
wide(1, ..., 1);                                           { defined and called }

and the C frontend fails identically at the same boundary, so it is below both parsers — the staging arrays in pasparser_proc.inc and cparser.inc are all derived and all sized 64 here. RegisterProc's own guard (nParams > MAX_PROC_PARAMS) is correct and not reached.

Next step for whoever takes this: build with -g -O2, source tools/pxx-gdb.py, and get a real frame. Every cheap narrowing above is spent; the remaining question is one backtrace wide.

What it blocks

import "/usr/include/SDL2/SDL.h" reaches gcc's <immintrin.h> via SDL_cpuinfo.h/HAVE_IMMINTRIN_H. Current wall, measured today:

pascal26:3913: error: C function definition: more than 32 parameters not
supported (MAX_PROC_PARAMS)
  near: char __q01  char __q00  >>>  return

_mm512_set_epi8 takes 64 — the structural maximum over gcc's x86 intrinsic headers, since AVX-512 is 512 bits.

The fork worth measuring before doing the work

Past the parameter list sits __m512i, a vector type pxx does not have, so raising the limit may only move the wall a few tokens. The alternative is a pxx-owned immintrin.h that declares nothing and fails by NAME — honest, since pxx implements no AVX-512 intrinsics either way. Measure which wall comes next before choosing. The 33-param crash above is worth fixing regardless: it is a segfault on ordinary Pascal, independent of intrinsics.