← board

Fell out of bug-a-a-variant-converted-to-ansistring-leaks-whenever-the-result-is-a-temporary as a residual, and is deliberately NOT closed by it. Filed so the question has an owner rather than sitting as a caveat in a test header.

What was measured

test/test_variant_string_temp_leaks.pas, 400 trips over nine arms, live at exit, before and after that fix:

target before after
x86-64 1549 2
aarch64 1549 2
i386 3616 1
arm32 3856 2
riscv32 364 1

The fix cleared every target, so nothing here is currently leaking and this is not urgent. What it exposes is that the targets did not agree BEFORE it, and they had no reason to disagree: the defect it fixed lives in shared IR, so all five should have leaked the same amount. Two of them leaked more than twice as much and one leaked a quarter as much.

The allocation COUNT differs too, which a leak alone does not explain: i386 and riscv32 ran 8671 allocations before the fix and 6850 after, arm32 7707 -> 5411, while x86-64 and aarch64 sat at 5411 both times. A pure ownership fix should not move how many times the program allocates.

Why it matters even though everything is green

The nearby release-matrix work (bug-a-scope-exit-release-matrix-has-four-holes-left-on-i386-and-arm32) found that i386 had no variant, promotable-int or record arm at all. These numbers are consistent with more of that class still being present and currently MASKED — masked because the arms in this program now balance, not because the holes were filled. A masked hole reappears the moment a shape reaches it by another route.

Where to start

Not from this program, which mixes nine arms. Split it: run each arm alone on each target and find which arm produces the divergence. The arms are already separate procedures. riscv32 being LOW rather than high is the most informative row — a target that leaks less than the shared-IR defect can explain is either freeing something it should not, or not allocating something the others do, and the allocation-count column says the second is in play.

Do not treat "all five are under the bound now" as an answer. That is the condition under which this is cheap to investigate, not evidence there is nothing there.

Narrowed by a negative result (frankB, 2026-09-01) — NOT the general release matrix

frankB swept 20 shapes that measure clean on x86-64 across all five targets, 1000 trips each, at 42507851cdde: interfaces (local, reassigned, function result, in a dyn array, as a parameter), 2-D dyn arrays including shrink and regrow, for..in over strings and over records, a class with a destructor, try/finally, an open-array parameter, a concat loop, variant+promo in one record, 3-deep nesting, inherited managed fields, and dyn-array-of-variant and dyn-array-of-promo as record members.

Every cell identical across x86-64 / i386 / arm32 / riscv32 / aarch64, and every one clean (1 to 10). No spread at all.

That matters because it removes the hypothesis this ticket was filed under. If the scope-exit release matrix were broadly short on i386 and arm32, at least one of twenty shapes covering interfaces, nested dyn arrays, destructors, for..in and finally should have shown it. So the spread is specific to the variant string-temp family rather than general to managed memory on those targets, and the search should start there rather than in the release matrix.

The harness was shown to reflect the target rather than falling back, with a positive instance rather than an assertion: a member-array run produced 12 on i386 and riscv32 against 7 on x86-64 and aarch64 — a real 32-vs-64-bit split from the promo stride. So the identical rows are identical because the counts match, not because one binary ran five times. That is the check that makes a 20-shape zero mean something.

Closed 2026-10-03 (frankuser) -- the last divergence was i386 boxing a LITERAL

Re-measured at HEAD, the full program, -dPXX_ALLOC_CENSUS: x86-64, arm32, aarch64 and riscv32 all allocs=5411 live=2; i386 allocs=6088 live=1. Split by arm (the ticket's own advice), every arm matched across x86-64 and i386 except ArmLit: 0 allocations on x86-64, ~2 per trip on i386. Narrowed further to one statement each:

v := 'literal';                 x86-64 0   i386 1 per store
if v = 'literal' then ...       x86-64 0   i386 1 per compare

Not a leak (frees matched) -- a COPY. The IR is identical on both targets (const_str -> var_store); the i386 backend's IR_VAR_STORE and IR_VAR_BOX arms called PXXStrFromLit for a tyString source unconditionally, while EmitStaticLitHandle386 -- the i386 port of x86-64's static-literal handle, already used at three other i386 sites -- was never asked there. Both arms ask it first now; no retain, for the reason that function documents.

After: i386 allocs=5411 frees=5409 live=2, the same as the other four, same output (hits=2800, the tail payload intact). This also explains the ticket's original allocation-COUNT column for i386: that spread was this copy, which a pure ownership fix could not move.

arm32's and riscv32's earlier spreads had already closed (their literal paths were ported under perf-a-every-string-literal-assignment-heap-copies-on-i386-arm32-riscv32-and-xtensa).