← board

Length() of a pointer-dereferenced frozen string returns 0 on the cross targets

Symptom

Length(p^) / Length(rec.pf^) where the dereferenced value is a frozen string returns 0 on i386 / aarch64 / arm32. x86-64 returns the correct length. A direct Length(s) over a plain string variable works on all four.

type PStr = ^string;
     TRec = record np: PStr; end;
var s: string; ps: PStr; r: TRec;
begin
  s := 'TRoot'; ps := @s; r.np := @s;
  writeln(Length(s));      { 5 everywhere — baseline OK }
  writeln(Length(ps^));    { 5 on x86-64; 0 on i386/aarch64/arm32 }
  writeln(Length(r.np^));  { 5 on x86-64; 0 on i386/aarch64/arm32 }
end.

Repro: /tmp/lenscope.pas shape above; compile per target and run under tools/run_target.sh.

Root cause

Same root as the frozen-string-equality cross bug fixed in 25eb50d: a frozen string reached through a pointer deref or pointer field is lowered to the inner pointer-load node, which keeps IRTk = tyPointer (its value is the buffer address, but the tag is not tyString). The Length codegen dispatches on the operand's IR shape/type:

This did not affect streaming/LFM (the RTL compares such strings via =, which is now fixed, and never calls Length on a deref'd frozen string), so it is latent, not a regression.

Fix direction

Mirror the equality fix on the three cross Length handlers: treat a tyPointer-tagged operand whose value is a frozen-string buffer address as a frozen string and read the length at [buf+0] (like x86-64's else branch), instead of the [value-8] handle path. Take care to keep genuine dynamic-array / managed-handle Length (which legitimately read [value-8]) working — the discriminator is the operand's source type, so this likely wants the same "tyPointer-as-frozen when context is a string" decode used in the string-eq branches, or a properly threaded type tag. Cross-bootstrap must stay byte-identical.

Acceptance

Length(p^) and Length(rec.pf^) over a frozen string return the correct length on i386 / aarch64 / arm32 (output-equal to x86-64); a regression test covers the local-pointer and pointer-field forms on all four targets; bootstrap

Resolution

Fixed in the three cross Length handlers (ir_codegen386.inc, ir_codegen_aarch64.inc, ir_codegen_arm32.inc). The frozen-string [buf+0] branch now also fires when the operand is a bare pointer load (IR_LOAD_SYM or IR_LOAD_MEM) tagged tyPointer — exactly the deref'd-frozen-string shape (ps^ lowers to a load_sym of the pointer var; r.np^ to a load_mem of the pointer field), whose value already IS the buffer address. This mirrors x86-64, where those same operands fall to the else catch-all that reads [buf+0]. The genuine dynamic-array / managed-handle cases keep their [value-8] path: they arrive as tyAnsiString, IR_LEA-of-IsArray, IR_INDEX/IR_FIELD, or a by-ref AnsiString param, none of which match the new bare-tyPointer-load guard. No frontend / shared-IR change was needed (an earlier attempt to retag the value node to tyString broke its emission — the value-producing load then took a string-load path). make test, the three cross suites, and cross-bootstrap all stay byte-identical; new regression test test/test_cross_frozen_strlen_deref.pas wired into all three cross suites.

Log