Length() of a pointer-dereferenced frozen string returns 0 on the cross targets
- Type: bug
- Status: done
- Owner: —
- Opened: 2026-06-19 (found while porting streaming/LFM, feature-cross-streaming-lfm)
- Resolved: 2026-06-19
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:
- x86-64 (
ir_codegen.inc:2931) has anelsecatch-all: evaluate the operand to the buffer address and read the length prefix at[buf+0](mov rax,[rax]). The tyPointer-tagged frozen deref lands here → correct. - i386 / aarch64 / arm32 (
ir_codegen386.inc:1875,ir_codegen_aarch64.inc:1285,ir_codegen_arm32.inc:1480) only read[buf+0]when the operand is recognised as a frozen string (operand IRTk = tyString, or anIR_LEAof atyStringsymbol). A tyPointer-tagged frozen deref misses that branch and falls to the dynamic-array/handle default, which reads the element/length header at[value-8]→ garbage → 0.
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
- cross-bootstrap stay byte-identical.
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
- 2026-06-19 — opened. Found during the streaming/LFM cross port; the sibling
string-equality manifestation of the same tyPointer-tag root was fixed in
25eb50d, but
Lengthwas left (not exercised by the streaming RTL). - 2026-06-19 — fixed. Cross
Lengthhandlers read[buf+0]for baretyPointerloads, mirroring x86-64. Regression test added + wired. Resolved.