← board

A frozen string compared to an AnsiString is FALSE under the flag on x86-64

type TS = string[8];
var a: TS; m: AnsiString;
begin
  a := 'abcde'; m := 'abcde';
  WriteLn(a = m);        { x86-64 -dPXX_SHORTSTRING: FALSE.  Everything else: TRUE }
  WriteLn(a = 'abcde');  { TRUE everywhere — the row that hides it }
end.

Measured 2026-09-03. Bisected: first bad commit eadf214725a, feat(P): phase 2 — byte-prefix codegen, x86-64 complete and FPC-identical. That commit introduced the layout, so there is no earlier state in which this worked and no change of anyone else's to look for.

default -dPXX_SHORTSTRING
HEAD x86-64 TRUE FALSE
HEAD i386/arm32/aarch64/riscv32/xtensa/wasm32 TRUE TRUE

THE PIN IS NOT A CONTROL FOR THIS, and reading it as one is how this was

first filed as a regression

The pinned compiler (v401) answers TRUE under the flag — and it answers SizeOf(string[8]) = 16 while doing so. The pin predates the byte prefix, so -dPXX_SHORTSTRING is a NO-OP in it: both pinned rows are the same 8-byte-prefix program and the flag row is the default row wearing a flag. A control drawn from a population where the feature does not exist cannot fail, and this one passed and certified a regression that never happened.

The tell is in the control's own output. sizeof 16 in a mode that must print 9 says the flag did nothing. Any pinned measurement of byte-prefix behaviour has to print the size beside the answer, or it is measuring the other layout.

Why it matters more than one row

It is the CROSS-REPRESENTATION compare — one operand with an inline prefix, one with a heap handle — wrong only in the mode phase 4 makes the DEFAULT, on the backend everything else is measured on. The default-mode row beside it is green, so a suite that runs one mode certifies it. The literal row is green too, which is the comparand a repro naturally reaches for.

test/test_frozen_compare_operand_shapes.pas is wired for x86-64 in the DEFAULT mode only, with this ticket named at the point the flag row was left out. Wire it when this lands.

[[bug-a-i386-comparing-two-elements-of-an-array-of-frozen-strings-is-false]] [[feature-p-implement-the-real-tyshortstring-byte-prefix-layout]]

Resolution (2026-09-03, frankB)

One line, and the file's own comments named the rule it broke twice over. ir_codegen.inc's equality block for a comparison with a managed operand did

lhsTk := IntToTypeKind(IRTk[left]);
rhsTk := IntToTypeKind(IRTk[right]);
...
EmitAnsiStrCmpReg(op = tkEq, lhsTk, rhsTk);

while the ORDERING branch immediately below it and the frozen-only branch after it both pass IRStrTkOf, each with a comment saying why: the IR tags every frozen string tyString generically. So EmitAnsiStrCmpReg — which is correct at either width and takes the width from the kind — was told 8 for a string[N] whose prefix is one byte, and read the length byte plus seven characters as a length. Every equality against a managed string failed; inequality mostly passed, which is the same signature EmitStrCmpReg was fixed for earlier.

Three call sites of the same helper family, two converted and one not. Changed to IRStrTkOf on both sides; the Char arms are unaffected because IRStrTkOf returns tyChar for a Char node and tyAnsiString for a managed one, resolving only the frozen kinds this block had wrong.

Verified in test/test_frozen_compare_operand_shapes.pas (var_ans row, with its must-be-FALSE partner) on all seven targets in both modes, byte-identical to FPC 3.2.2. Positive control: fix reverted, compiler rebuilt, the row returns to FALSE.

Log