array of string is unsupported in the frozen-string model
- Type: feature (compiler core) — Track A
- Opened: 2026-08-21, split out of [[feature-dynarray-insert-delete-managed-elements]] while closing its last items.
What happens
program frz;
var a: array of string;
begin
SetLength(a, 3); { ok in the managed model }
end.
$ ./compiler/pascal26 -uPXX_MANAGED_STRING frz.pas out
pascal26:5: error: SetLength: dynamic array of record/string not yet supported
Managed mode (tyAnsiString elements, the default) is fine — the element is a
handle, one pointer wide. The frozen model stores the string INLINE, so the
element stride is the declared capacity plus its length header, and that number
is not what TypeSize(tyString) answers. ir_codegen.inc's specialId = 102
arm refuses rather than striding by a wrong width, which is the right call:
the wrong stride here is not a crash, it is a buffer of silently overlapping
strings.
Why this ticket exists separately
feature-dynarray-insert-delete-managed-elements carried "frozen-string
elements" as an open item on Delete/Insert. It is not their gap. They refuse it
because nothing supports it: a program cannot even SetLength such an array,
so Delete/Insert could never have been reached with one. Fixing it there would
have been a microfix on the symptom
(devdocs/dev/root-cause-over-microfix.md).
Scope
- Record the element's frozen capacity on the dyn-array symbol (the
SymTR[].ElemTk = tyStringcase needs anElemStrCapalongside it — the record arm already carriesElemRecfor exactly this reason and computesRecSize). - Use it as the stride in every SetLength / index / copy path, per backend —
the same list
SymDynElemRowLenalready threads through. - Then DELETE the frozen-string exclusions in the Delete/Insert parser gates
(
pasparser_stmt.inc, both spellings of "frozen-string element type not yet supported"), and add anarray of stringsection totest/test_dynarray_insert_delete.pasthat runs under-uPXX_MANAGED_STRING.
Priority note
Low (30). The frozen model exists for the self-host build, and the compiler's
own source does not use array of string — it would have hit this. Ordinary
user code runs in the managed model, where the shape already works.
Re-measured 2026-09-03 (frankA) — the scope moved, in both directions
Still refused under -uPXX_MANAGED_STRING at HEAD. But the population it
refuses is not what this body describes, and the half that WAS the hard part is
already done.
| declaration | -uPXX_MANAGED_STRING |
|---|---|
array of string[10] |
works — 4 elements, values correct, no overlap |
array of string |
SetLength: dynamic array of record/string not yet supported |
array of AnsiString |
same refusal |
THE CAPPED SPELLING NOW WORKS AND THIS TICKET DID NOT KNOW. array of string[10] allocates and strides correctly in all three modes (default,
-u, -dPXX_SHORTSTRING), asserted with four 10-char elements read back —
the shape that shows overlap if the stride is wrong. So "the element stride is
the declared capacity plus its header and that number is not what
TypeSize(tyString) answers" has been solved for every element that HAS a
declared capacity. What is left is the element that has none.
AND array of AnsiString FAILS FOR THE SAME ONE REASON, WHICH THIS BODY
IMPLIES IT WOULD NOT. It says "managed mode (tyAnsiString elements, the
default) is fine", which reads as though an explicitly-declared AnsiString
element stays managed under -u. It does not: the flag freezes the whole
string family, not just bare string. Measured — appending 'x' 300 times:
default ansi=300 plain=300
-uPXX_MANAGED_STRING ansi=255 plain=255
So under -u an AnsiString is a 255-cap frozen buffer that truncates
silently, and array of AnsiString hits the same ElemTk = tyString guard.
One refusal, two spellings — not two gaps. (Whether the silent truncation of an
explicitly-declared AnsiString is chosen or a defect is a separate question
and not this ticket's; the mode's premise is "no heap strings", so it is at
least deliberate in direction.)
WHAT IS ACTUALLY LEFT, AND IT IS A DECISION BEFORE IT IS A PATCH. Both IR
paths are identical (PXXDBG=a.ir: the capped and uncapped spellings produce
byte-identical IR down to the -102 call), so the whole difference is the
guard and the elemSize line beside it. Giving a plain frozen element the
stride it would have as a VARIABLE means FrozenStrSlotSize(tyString, DEFAULT_STR_CAP) — 264 bytes per element, so SetLength(a, 1000) is
264 KB. That is consistent with what a frozen string local costs and it is
still a number somebody should choose rather than inherit from a default,
which is why this was left rather than patched: the same 0-means-unset
encoding is the subject of
[[bug-a-a-plain-frozen-string-records-capacity-zero-so-eleven-clamp-sites-cannot-say-unset]],
and deciding it there decides it here.
Not started for a second reason too: this is the -uPXX_MANAGED_STRING axis,
which the phase-4 byte-prefix flip does not touch, so it neither blocks the
flip nor is released by it.