← board

SetLength on a frozen string traps on wasm32

Found 2026-09-02 — first hit by frankc-af while probing candidates for a test-wasm32 target, and it is the cause of three of the five red rows in that sweep (test_cross_sets, test_frozen_string_cross_b305, test_static_string_literal, all dying at rc 134). Diagnosed here.

Repro

var s: string[10];
begin
  s := 'abcde';
  SetLength(s, 1);
  WriteLn('[', s, ']');
end.

wasm32: wasm trap: unreachable, rc 134. Native: fine.

The cause, and it is not the length prefix

WasmEmitSetLenStr in compiler/ir_codegen_wasm32.inc is four lines of body and has no frozen arm:

WasmEmitValueAs(IRA[node], WT_I32, 'slot address of SetLength');
WasmEmitValueAs(IRB[node], WT_I32, 'SetLength count');
if not WasmCallRtl('PXXStrSetLen') then Exit;

PXXStrSetLen is the MANAGED string routine — it expects a slot holding a heap handle whose header sits below it. A frozen slot holds [len][chars] inline, so it is handed a buffer where a handle is required.

For a frozen string the operation is not a runtime call at all: it is a clamp to the capacity and a store of the new length into the prefix — the same prefix the conversion now writes through WasmFrozenStoreLen, so the machinery to do it correctly at either width already exists in the file.

Pre-existing, with the control

Traps identically under stable_linux_amd64/default/pinned (1eec4dc5e0a74c69), and traps at default as well as under -dPXX_SHORTSTRING. So it is neither caused by nor related to the byte-prefix work: it is a lowering that was never written.

Why p25

It fails LOUD — a trap with no wrong value produced — and the frozen SetLength shape is not on any current milestone. The three sweep rows it blocks are real but are excluded from the wired test-wasm32 set with a comment, so their absence is recorded rather than silent.

FIXED before 2026-09-04, and NOT by the change that noticed it

Re-measured 2026-09-04 while closing the two frozen-deref tickets in this family. This ticket's repro verbatim, on wasm32, at default AND under -dPXX_SHORTSTRING: [a], rc 0. Three more rows, wasm32 against native, all identical: shrink [a] 1, grow to 5, clamp at the capacity 10, and cap 256 (the wide prefix) [ab] 2.

Not this session's change. The same four rows are green on a compiler built from the parent commit as well, so nothing here is claimed for the deref fix — the control was run precisely to avoid claiming it.

The residual question, answered rather than left open. WasmEmitSetLenStr STILL has no frozen arm and still calls PXXStrSetLen unconditionally, so the body this ticket quotes is unchanged and the ticket reads as if it should still trap. It does not, because a frozen string no longer REACHES that op: compiler/ir.inc's SetLength lowering routes IR_SETLEN_STR only for a managed tyAnsiString target, with the comment "Only tyAnsiString reaches here; frozen inline strings take the -101 store-length path." The emitter was never given the frozen arm the ticket asks for; the caller stopped calling it.

So the diagnosis in this ticket is correct about the emitter and stale about the route. Left as a record of both, since the emitter is still a live trap for anyone who reintroduces a frozen caller.

Log