← board

Bumping MAX_PROC_PARAMS 16→32 makes the compiler segfault (self-miscompile?)

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

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).