An integer default on a string parameter compiles, then segfaults
- Type: bug (Pascal frontend — parameter default declarations; memory corruption)
- Opened: 2026-08-10
- Found by: Track A+C+P+N, checking whether string defaults arrive correctly while resolving [[bug-p-float-literal-default-in-a-parameter-list-fails-to-parse]]. The neighbouring correct shapes all passed; this one is what the sweep turned up.
Repro
program n;
procedure R(s: AnsiString = 1); begin writeln('[', s, ']'); end;
begin R(); end.
ok: n [code=47507B ...] { compiles clean, no warning }
[Segmentation fault (core dumped)
Note the partial output: [ is written, then the fault — the crash is at the
point the string is first touched, not at the call, which is the same
"lands far from the cause" shape as
[[bug-p-constructor-with-a-defaulted-variant-param-corrupts-memory]].
What narrows it
| shape | result |
|---|---|
s: AnsiString = 'a' |
ok — value arrives as 'a' |
s: string = 'frozen' |
ok |
s: AnsiString = 1 |
compiles, then segfaults |
d: Double = 2 (integer default, float param) |
ok — the ordinal is converted |
So the type mismatch is tolerated for a numeric parameter (correctly, by conversion) and is silently wrong for a string one, where the ordinal reaches the callee as a pointer.
Cause (mechanism identified, fix not chosen)
ParseParamDefaultValue (compiler/parser.inc) picks the default's shape from
the literal's token kind, never from the parameter's declared type: an
integer literal takes the ordinal arm regardless of what it is defaulting. The
call site then rebuilds it with DefaultArgValueNode, whose ordinal arm emits
an AN_INT_LIT tagged with Procs[mpi].Params[k].TypeKind — i.e. tagged
tyAnsiString. Nothing converts, so 1 is passed where a string pointer is
expected.
Note DefaultArgValueNode already carries a special case of exactly this
shape for tyVariant (it retags the ordinal tyInteger so the value gets
boxed). That is the second instance of "the literal's kind and the parameter's
kind disagree" — which argues the fix belongs at the declaration, as a
compatibility CHECK, rather than as a third retag downstream.
Recommended fix
Reject it at the declaration, as FPC does: after
ParseParamDefaultValue returns, verify the default's shape against the
parameter's TypeKind and Error on a mismatch that cannot be converted
(ordinal → string/class/pointer). That closes the whole family at one site
instead of one arm at a time, and turns a segfault into a compile error.
Check the corpora before landing: lib/** may contain a = 0 on a string
parameter that has always "worked" because the argument was never omitted, and
a new error would break the build. If so, warn under the default dialect and
error under --strict-fpc (see meta-dialect-extensions-and-fpc-strict).
Gate
The repro becoming a compile error; test/test_default_params_methods.pas
extended (it is the home for this concept); tools/gate.sh quick. If a
{%FAIL}-style conformance case is the right home for the diagnostic, prefer
that over a runtime test.
Log
- 2026-08-10 — resolved, commit b91a10bb0.
Resolution (2026-08-10)
Fixed as recommended: rejected at the declaration, in the one shared
ParseParamDefaultValue that the four parameter parsers now go through
(unified by [[bug-p-float-literal-default-in-a-parameter-list-fails-to-parse]]
immediately before this) — so the check lands once and covers free routines,
class methods, interface methods and record methods together.
Both arms of the mismatch are refused:
s: AnsiString = 1 -> a string parameter's default must be a string literal
k: Integer = 'a' -> a string literal cannot be the default for a non-string parameter
Exemptions, deliberate: tyVariant (every shape is legitimately boxed into
it — the existing retag in DefaultArgValueNode is what makes that work), and
tyChar taking an ordinal, which is ordinary Pascal.
No dialect flag. This is not FPC-parity strictness that belongs behind
--strict-fpc: the accepted form produced a segfault, so it falls under the
compat-tag escape rule — silent wrong behaviour is a plain bug, not a laxness
knob.
Corpus check (the ticket's stated risk — a new hard error breaking code
that used to compile): all 131 units under lib/rtl, lib/pcl and
lib/* were compiled with the new binary. Zero new failures. Four units
(palthread, palpthread, palparallel, palthreadobj) fail identically on
the pinned compiler — they need --threadsafe — so they are a control, not
a regression. Note make lib-test could not have answered this: Track B builds
with $(PXX_STABLE), which does not contain the change.
Regression tests: test/test_ordinal_default_on_string_param_fail.pas and
test/test_string_default_on_ordinal_param_fail.pas, both wired into the
Makefile's negative-test block asserting the exact diagnostic text.
Gate: tools/gate.sh quick GREEN (self-host fixedpoint + testmgr quick).