← board

Bug: const array[0..N-1] of AnsiString = (...) literal fails "too many array constant elements" despite correct count

Repro

Not yet minimally isolated in this session (time-boxed in favor of the workaround, see below) — the working assumption based on what changed between "fails" and "compiles" is a const <name>: array[0..N-1] of AnsiString = ('a', 'b', ..., N literals); declaration, N in the 8-16 range, tried both as a unit-level const block and as a function-local const block. A future pickup should start by re-deriving a minimal repro (the git history around this ticket's opening commit has the original failing declarations in compiler/asmdisasm_x64.inc, since reverted to a case-statement workaround, as reference material).

Suspected shape

ParseTypeSection/ParseVarSection's array-constant-initializer parser (compiler/parser.inc, the cElem >= cFlatLen check, ~line 9789) computes cFlatLen from the array's declared bounds via AllocArray. Given the initializer counts were hand-verified correct multiple times, the likely culprit is cFlatLen/cLo/cHi being computed wrong for an AnsiString-element array specifically (a stride/size miscalculation unique to managed types, matching the general "array + AnsiString" fragility theme already surfacing this session — see the sibling bug-var-array-of-ansistring-param-loses-writes ticket, though that one is about var parameters, a different code path, not const declarations).

Impact

Hard parse error (loud, not silent) — but a confusing one, since the error message ("too many array constant elements") strongly implies a user counting mistake, sending anyone who hits this down the wrong debugging path first (this session spent real time re-counting correct initializers before suspecting the compiler itself).

Workaround used

Replaced every const array[..] of AnsiString = (...) literal in compiler/asmdisasm_x64.inc with a plain function Foo(idx: Integer): AnsiString; begin case idx of 0: Result := '...'; ... end; end; lookup — proven-safe, used pervasively elsewhere in this codebase already. Slightly more verbose but functionally identical and avoids the parser gap entirely.

Suggested fix

Needs a minimal isolated repro first (see above), then a trace through AllocArray's bounds computation and the const-array-initializer parsing loop (compiler/parser.inc ~9760-9800) specifically for tyAnsiString element type, comparing against the same logic for a scalar element type (e.g. Integer) which presumably works (no existing failures reported for const array[..] of Integer = (...), a much more common pattern in this codebase).

Also worth checking while in there

Fixed (2026-07-01, Track A)

Root cause (found via a dispatched research agent, verified by direct code reading): compiler/parser.inc's array-const element loop (~9781-9812) consumed each element via ParseInitVal -> ConstEval -> ConstEvalFactor. ConstEvalFactor has exactly ONE branch that matches a tkString token — a single-character literal, treated as an ordinal (Ord(CurTok.SVal[1]), for Char-constant contexts) — and calls Next to consume it. A multi-character string literal matches NO branch in ConstEvalFactor; execution falls through to Result := 0 WITHOUT calling Next, leaving the token stream un-advanced. The outer loop's Inc(cElem) runs unconditionally regardless of whether a token was actually consumed, so it re-examines the same un-consumed string token on the next iteration, incrementing cElem again with zero real progress — cElem reaches cFlatLen (the correct element count) long before the real token stream reaches the closing ), firing the misleading "too many array constant elements" error on a perfectly correctly-counted initializer. The single-char case (e.g. array[0..15] of AnsiString = ('a','b',...)) doesn't hit the parse error at all — ConstEvalFactor matches it, cElem/token-stream stay in lockstep — but silently stores Ord(char) as a plain AN_INT_LIT where a managed- string handle is expected, segfaulting at runtime instead. Confirmed: this is a genuine unimplemented-feature gap, not a miscount — a doc comment right above the array-const path already said so ("string/float/record initializers remain a follow-up"), and PendingInitKind/LocalInit* had no string-literal wiring for array elements (a separate Kind=1 string-literal mechanism already existed and is used by the C frontend for globals, just never wired up from the Pascal array-const parser).

Fix: when the array's element type is a string kind (tyString/tyAnsiString/tyFixedString/tyShortString), the element loop now bypasses ParseInitVal/ConstEval entirely and captures the literal's span directly (Tokens[TokPos-1].SOffset/SLen, the same pattern the existing scalar typed-string-const path already uses), recording it as a Kind=1 (string literal) init instead of a plain int. For GLOBAL consts this reuses the pre-existing PendingInitKind=1 mechanism (already consumed correctly by CompilePendingGlobalInits, building an AN_STR_LIT node — zero changes needed there). For LOCAL (routine-scoped) typed consts, added a matching LocalInitKind array (compiler/defs.inc, mirroring PendingInitKind) plus a new AN_STR_LIT branch in CompilePendingLocalInits, since that path had no string-literal support at all before this fix. Both emit a normal arr[elem] := AN_STR_LIT assignment — the exact same managed-string coercion/allocation any ordinary s := 'literal' already gets, so no new ARC logic was needed.

Verified: multi-char and single-char elements, global AND local typed array consts, plus a reassignment-after-init check (Multi[1] := 'zzz' after reading the original value into a separate var) proving the element is a real independently-refcounted managed string, not an aliased literal. Matches FPC output exactly (fpc build of the same source). New test/test_const_array_of_string.pas in make test-core. Full make test green, self-host bootstrap byte-identical (pinned v115).

Log