The frozen-string store body is written twice in three backends
- Type: refactor (codegen) — Track A
- Do NOT start this during the
feature-p-implement-the-real-tyshortstring-byte-prefix-layoutwindow. That work re-typesstring[N]and will rewrite these arms; doing both at once means resolving an extraction against a relayout in hand-encoded backends, which is the worst version of this job. Check first whether the relayout already collapsed them — if it did, close this as done-by.
The duplication, measured
Each of ir_codegen386.inc, ir_codegen_aarch64.inc and ir_codegen_arm32.inc
contains the same body twice: write [len][chars] into a frozen buffer, with
the destination already in the register the caller loaded and the source being a
char ordinal, a managed handle, or a frozen-string address. The only difference
between the copies is the capacity expression.
$ grep -c "char ordinal -> \[len=1\]\[char\]" compiler/ir_codegen{386,_aarch64,_arm32}.inc
2 2 2 # after e4cba526a; it was 1 1 1 before
The duplication grew as a side effect of fixing the bug it caused, which is the cleanest possible statement of why this ticket exists. Nobody did anything wrong: the transplant was the smallest correct change that unblocked Track T's cross fuzz rung, and the ticket it closed said "same file, same registers, transplant not port".
Why it is worth doing rather than tolerating
normalise-dont-special-case.md's claim is not aesthetic — it is that the
second path is the one that stays broken, and here that already happened once
and cost an evening across two sessions. Only the SYM copy ever grew a tyChar
arm; s := c compiled and p^ := c did not, on exactly the three backends
without a second copy of it. x86-64 and riscv32 had it in both paths and were
never affected.
Sharing the body makes that unrepresentable rather than fixed. There is no second arm left to fall behind.
The work is already done and measured
devdocs/dev/parked-patches/frozen-string-store-body-extraction-REFERENCE-ONLY.patch
One EmitFrozenStrStoreBody<arch>(srcTk, strCap) per backend, called by both
arms. Measured before the race was lost, at the then-tip:
| target | corpus binaries byte-identical | changed | newly building |
|---|---|---|---|
| i386 | 32 | 0 | 1 |
| aarch64 | 32 | 0 | 1 |
| arm32 | 32 | 0 | 1 |
over the 35 tree tests containing string[, plus gate.sh quick GREEN and the
self-host fixedpoint converged. Byte-identity is the right control for an
extraction: everything that already compiled must emit the same bytes.
Do not read this as a size estimate. Net lines were -12 / -8 / +7 (i386 / aarch64 / arm32) — arm32 is net POSITIVE. The patch predates the transplant, so in it each backend held the char arm only once and the diff is extraction and the char fix in one. Against today's tree, with a second full copy per file, the extraction deletes more — that number has not been measured and nobody should quote one until the re-derivation produces it.
THE PATCH DOES NOT APPLY. git apply --check returns 1 against four files —
it was cut against the pre-transplant tree and the arms have since changed under
it. It is a REFERENCE for the shape and the helper text, not something to apply.
Re-derive against the tree of the day; the arms will be more similar than they
were, because both now contain the char case.
The safety net already exists
frankC added cross rows for test_char_into_shortstring_via_pointer on
i386/aarch64/arm32/riscv32, executing under qemu, and the native row carries a
comment saying in capitals that it cannot fail for that bug (x86-64 was never
broken). Those cross rows are the guard for this refactor — a regression in
an extracted body shows up as a real cross-target failure rather than as a
silent shape change. Run them, and pair them with the byte-identity control
above, which is the check that catches a change that is wrong but still passes.
Use the Makefile rows, not a sweep harness. Verified 2026-09-02 at both
layers: lines 15394/16238/16950/18727 delegate to tools/run_target.sh, and
neither that script nor tools/expect_same.sh imposes a timeout — it is a bare
exec qemu-<arch>. frankC's own sweep harness wraps the same rows in a
10-second limit and produced a dozen "differences" in one day that were all
that timeout firing under qemu contention; every one was identical at 90s. Do
not read a timeout as a verdict.
The inverse is the cost of the same property, and it is worth knowing before you run these under contention: with no limit, a HANG in an extracted body blocks instead of failing. That is the deliberate trade the nilpy rows make in the other direction (see the Makefile's own comment at ~3082, "the failure mode is a HANG, and a runner that waits forever"). For a wrong-value regression, which is what this refactor risks, unbounded is the right choice.
Position of the transplant's author
Recorded because it is the thing that would otherwise be lost in a message queue, and because a refactor over someone else's recent commit reads very differently with and without it — frankC, 2026-09-02:
I wrote in that commit message that the duplication is normalise-dont-special-case and then transplanted anyway. That was a deliberate call and I still think it was right for THAT hour ... But "smallest correct change now" is not the same claim as "right shape" ... when that window reopens, land it over mine ... I would rather the tree ended up right than that my version stayed in it.
The prose trigger is now an edge, 2026-09-03 (frankA)
This body already said do not start during the P4 window, and to close as
done-by if the relayout collapses the two arms. Nothing watched that
sentence — the ranker offered the ticket regardless. It now carries
blocked-by: feature-p-implement-the-real-tyshortstring-byte-prefix-layout, so
the condition is enforced by the instrument that hands out work rather than by
whoever happens to read down this far.