Bumping MAX_PROC_PARAMS 16→32 makes the compiler segfault (self-miscompile?)
- Type: bug (compiler robustness / capacity) — Track A
- Status: backlog
- Opened: 2026-07-02, found while fixing [[bug-c-vararg-vastart-named-fp-stack]].
Symptom
Raising MAX_PROC_PARAMS from 16 to 32 (defs.inc, plus TProc.Params: array[0..31] and the cparser per-param locals) builds and SELF-VERIFIES
byte-identically — but the resulting compiler segfaults (wild jump to
~0x4000bd, corrupt-looking stack) when compiling a C file with a 17-parameter
function definition. With the cap back at 16 the same source gets the new
clean "too many parameters" error and everything is green.
Notes / suspicions
- The wild-jump crash smells like a real self-miscompile interaction with the GROWN TProc record (16 extra TParam entries, each holding a managed AnsiString Name — whole-record copies, managed-field walkers, or a frame size crossing some limit are all candidates). Self-host byte-identical does NOT clear it: the compiler's own source never exercises >16-param C parsing.
- Also note
Params: array[0..31]had to be a literal — const-expr bounds (0..MAX_PROC_PARAMS-1) are not supported in record field declarations (parser gap, possibly worth its own small ticket). - Wanted eventually: sqlite-class C code plausibly exceeds 16 params; [[feature-dynamic-compiler-tables]] is the systemic answer, but a working 32 cap is the cheap interim once this crash is understood.
Acceptance
MAX_PROC_PARAMS = 32 builds a compiler that compiles a 17..32-param C function definition correctly (extend test/cvararg_stack_spill.c); root cause of the wild jump identified and fixed or explained.
Investigation (2026-07-02, rr session — root-cause narrowed, not yet fixed)
Recorded the crash under rr. It is NOT a wild jump: rip 0x4000bd is inside the
emitted AnsiStrRelease stub (decq -0x10(%rax)) with a garbage handle
rax = 0xffffffff. Caller chain: the release fires inside RegisterProc
(called from ParseCSubroutine registering the 17-param function), i.e. the
release-of-old in Procs[ProcCount].Params[i].Name := pnames[i].
Key evidence: the string slot address is byte-misaligned (rdi =
0x31d89e9, ≡1 mod 8), and dumping memory around it shows values shifted by
one byte (a real heap handle stored starting at ...e9 with a stray 00 low
byte at ...e8; repeated 00 ff ff ff ff 00 patterns = int32 -1 writes at
odd offsets). So TParam's field offsets land at odd addresses AND two
access paths appear to disagree about the stride/base by one byte — the
released "handle" 0xffffffff is an int32 SymIdx := -1 read through a
Name-slot offset computed differently by another path.
Suspicion: the compiler's own layout of an ARRAY-of-record FIELD inside a
record (Params: array[0..31] of TParam where TParam has an odd unpadded
size) — stride vs field-offset disagreement between the store path and the
managed-release path, exposed only when i >= 16 is actually reached (the
old silent parameter drop kept every real program at i <= 15, and the
compiler's own source never has >16-param C functions, so self-host
byte-identical proves nothing here). Likely reproducible standalone with a
Pascal record containing arr: array[0..N] of record s: AnsiString; k: Integer; b1, b2: Boolean; end and high-index stores — worth trying BEFORE
touching compiler layout code.
rr trace preserved the session of 2026-07-02 (~/.local/share/rr). Cap stays 16 with the definition-time error until this is fixed.
Standalone repro attempt (same session): a small program with the exact
TParam shape (Name: AnsiString; TypeKind: Byte; SymIdx: Integer; IsRef, IsArray: Boolean) inside a TProc-like record with Params: array[0..31],
nested variable-index stores incl. SymIdx := -1 + Name re-assignment —
works perfectly (SizeOf(TParam)=24 aligned, SizeOf(TProcX)=792, all 32
entries verified, release-of-old fine). So NOT a general
record-array-of-managed-record layout bug; the compiler-context trigger is
something the small repro lacks (the real TProc's ~40 fields, MAX_PROCS-scale
outer array / large BSS offsets, or the specific access pattern). Next probe:
scale the standalone up field-by-field / array-size-by-array-size toward the
real TProc until it breaks.
Correction (same session): the "scaled-up standalone repro" that crashed (32K-element record array + 1000-index loops) was a RED HERRING for this ticket — it was a different bug entirely: the frozen string-concat 272-byte stack carve leaking per loop iteration (fixed in v144, managed concat typing; see test_concat_loop_stack.pas). The MAX_PROC_PARAMS-32 compiler crash (odd-aligned BSS slots, release of an int32 -1 through a Name-slot offset, rsp nowhere near the stack limit) remains unexplained and still reproduces only in the self-hosted-compiler context. The rr evidence above stands.
ROOT CAUSE FOUND + FIXED (2026-07-02, v153)
symtab.inc BuiltinRecFieldArrayCount(REC_TPROC, 4) — the hardcoded builtin
descriptor the compiler uses for its OWN TProc when self-compiling — still
said 16 while defs.inc declared Params: array[0..31]. The descriptor
overrides source layout for the builtin-named records (TProc/TParam/...), so
Procs[] was allocated with the 16-param extent while RegisterProc's code
looped to 32: Params[16].Name aliased BodyAddr (just assigned -1) →
AnsiStrRelease(0xffffffff) = the rr wild-jump. All the rr evidence matches
(int32 -1 patterns = BodyAddr/FramePatch/RetSymIdx = -1 stores; ≡1 mod 8 =
misaligned via descriptor's 8-byte-slot model vs source layout).
Why every standalone repro was negative: user records never hit the builtin descriptor path — only the compiler's own TProc/TParam names do.
Fix: descriptor count → MAX_PROC_PARAMS; ValidateBuiltinRecordLayout TProc
size/offsets now MAX_PROC_PARAMS-derived. All 0..15 param arrays bumped
(defs.inc CTypeFnRetP*, cparser fp*/fnRet*, parser argTypes/pnames/mP* etc.).
BOOTSTRAP NOTE: the descriptor is baked into the COMPILING binary, so growing
MAX_PROC_PARAMS requires the FPC cold bootstrap (or an extra self-host
generation) to converge — make bootstrap handles it; done here.
Diagnostic key: FPC-built -Cr range-checked compiler ran CLEAN (no source-level OOB) but its gen1 crashed → genuine layout-logic divergence, not memory corruption in the compiling process.
Acceptance met: test/cparams_17_32_b150.c (17- and 32-param C defs + calls,
gcc oracle s=153/t=528) in make test. make test + test-lua + test-threads
green, self-host converged, pinned v153.
Pre-existing gaps found en route (filed separately, NOT param-related —
verified identical on pinned v152): bug-c-printf-without-stdio-include-varargs
(explicit int printf(...) prototype → silent NO output; implicit printf
prints the format string unformatted) and the (int)x cast-as-call-argument
parse error (bug-c-cast-as-call-arg-parse-error).