← board

Variant-boxing temporaries are shared globals (thread-unsafe)

Symptom

When a variant comparison/operation needs to box a non-variant operand, the IR lowering allocates the box temp as a program global:

{ ir.inc, variant operand boxing (two sites) }
savedProc := CurProc;
CurProc := -1;                       { force global scope }
value := AllocVar('', tyVariant);    { one shared BSS slot per source site }
CurProc := savedProc;
left := IRAppend(IR_VAR_BOX, IRAppend(IR_LEA, value, ...), ...);

So every runtime execution of that comparison site writes the same BSS slot. Two threads evaluating the same site race on it. (Single-threaded is safe: the box and its use are in one expression with no intervening call, so recursion does not clobber it — unlike the frozen-string Result, this is thread-only, not a reentrancy bug.)

Fix

Allocate the variant box temp as a routine local (the normal CurProc >= 0 path) instead of forcing global scope, so each call/thread gets its own slot. The CurProc := -1 here looks like a copy of the frozen-string-Result idiom and is probably unnecessary — a stack local works for a transient box. Verify the temp's lifetime does not outlive the frame (it is consumed within the comparison, so a local is fine). Keep self-host byte-identical (variants are rare in the compiler's own source) + cross green.

Acceptance

Notes

Resolution (2026-06-30, commit a7d5d413, pin v97)

Fixed. Dropped the savedProc := CurProc; CurProc := -1; ... := savedProc forcing at both variant-operand-boxing sites in ir.inc; the box temp is now a plain AllocVar('', tyVariant) routine-local (per-call/thread slot). It is consumed within the one comparison and never outlives the frame, so a local is correct. savedProc removed (now unused). Single-threaded behaviour unchanged.

Verified: self-host byte-identical; make test + all four cross suites (i386/arm32/aarch64/riscv32) green — variant + variant-single output identical to x86-64. The arithmetic-result variant temp (separate BSS slot at the non-comparison branch) is a different allocation and out of scope here; it is a transient result, not an operand box.