← board

SizeOf(string) disagrees with the storage a string actually gets

Repro

program sinc;
type TA4 = array[0..3] of string;  TRS = record s: string; end;
var v: string; a: TA4; p, q: ^string;
begin
  WriteLn(SizeOf(string));            { the TYPE     -> 8   WRONG }
  WriteLn(SizeOf(v));                 { a VARIABLE   -> 4   right }
  WriteLn(SizeOf(TA4) div 4);         { array stride -> 4   right }
  WriteLn(SizeOf(TRS));               { record field -> 4   right }
  WriteLn(SizeOf(Pointer));           {              -> 4         }
  p := @a[0]; q := @a[1];
  WriteLn(PtrUInt(q) - PtrUInt(p));   { addr stride  -> 4   right }
end.

pascal26 --target=riscv32 (also i386, arm32) printed 8 4 4 4 4 4. x86-64 and aarch64 printed 8 throughout and were correct by coincidence — the hardcode happens to equal the pointer width there, which is why this survived.

Why it matters

GetMem(SizeOf(string) * n) merely over-allocates. Move(src, dst, SizeOf(string)) copies 8 bytes out of a 4-byte slot — it reads and writes a neighbouring field. Silent, and only on the targets where it is least likely to be noticed. That is verbatim the consequence recorded in the Real ticket below.

Cause

Two paths answer "what is a bare string", and only one of them asked:

BuiltinTypeNameTk's own header says "One table, so the next builtin type cannot be present in half the compiler", and the arm immediately below it (ansistring/unicodestring/widestring) already consults the define. The one entry that still disagreed was the one the function was written to fix.

This is the sibling of bug-a-sizeof-real-disagrees-with-the-storage-real-actually-gets, whose fix — Result := RealTypeKind — is five lines above in the same chain, and whose comment describes this bug word for word with Real in place of string. CLAUDE.md's rule is if you fix a bug on one arm of a double case, grep for the sibling before closing the ticket; that grep was not done, so the string arm sat there while the Real arm's comment explained exactly what was wrong with it.

Fix

BareStringKind moved from pasparser_decl.inc to util.inc, beside RealTypeKind — the sibling one-answer helper, upstream of every caller — and the string arm now calls it. Three sites were separately short-circuiting 'string' before reaching this table (RTTI in pasparser_expr.inc, operator overloading in pasparser_call.inc, plus the declaration path): two mechanisms is a smell and three is a design flaw. The two remaining short-circuits are deliberately kept, and both are genuinely different questions rather than copies:

Verified — fixedpoint bab147eec504

Log