← board

Variant -> scalar unbox is unavailable on cross targets

Found 2026-07-28 while cross-checking the new class-into-variant boxing ([[bug-a-variant-class-boxing-missing-on-i386-aarch64]]).

Assigning a Variant to a scalar fails to BUILD for i386, aarch64 and arm32:

program crossvar;
type
  TBox = class
    V: Integer;
  end;
var a: Variant; b: TBox; i, out_: Integer; s: AnsiString;
begin
  b := TBox.Create;
  b.V := 7;
  for i := 1 to 100 do
  begin
    a := b;
    a := 'str';
    a := 3.5;
    a := i;
  end;
  s := 'end';
  out_ := a;                 { unbox the Integer 100 }
  writeln(s, ' ', b.V, ' ', out_);
end.

x86-64 prints end 7 100. Every cross target stops at the unbox:

error: variant unbox: VariantToInt64 builtin not loaded

Also with o64: Int64 (o64 := a) and on a program with no class at all — the unbox is what fails, not the boxing.

Cause

IRLowerVariantAsScalar (ir.inc:~3835) lowers the unbox to a call to VariantToInt64 / VariantToDouble / VariantToBool / VariantToChar / VariantToStr, and Errors when FindProc cannot see one. Those live in compiler/builtin/builtin.pas, which is pulled only when something ELSE in the program needs it — so whether a given source builds for a cross target is decided by an unrelated declaration elsewhere in the file. On x86-64 the unit is effectively always present, which is why this never showed natively.

So the fix is about ENSURING the builtin unit is loaded when a variant unbox is lowered (the same "Enable*Runtime" pre-scan shape other runtime helpers use), not about the backends' codegen.

Correctness note — measured, NOT assumed

An earlier draft of this ticket claimed the unbox returns wrong values on i386/arm32 (addresses) and 1 on aarch64. That was a harness error, not a compiler defect: the check ran pxx ... | tail -1 >/dev/null && run_target, whose pipeline exit status hides a failed compile, so run_target executed a STALE binary left from an earlier edit of the source. With the compile status actually checked, all three targets fail to build and nothing wrong is computed. No silent-wrong-value claim survives.

(Lesson worth keeping: never gate a run on compile | tail -N >/dev/null — the pipeline's status is tail's.)

Gate

make test + cross for i386 / aarch64 / arm32 with the program above, whose output must match x86-64's on every target.

Log

Fix

The program-level pre-scan in parser.inc now sets needsBuiltin when the token variant appears anywhere, with the same ESP/xtensa exclusions the Str/Val pull uses — so the unit holding VariantToInt64 and friends is present wherever a variant can be unboxed, instead of arriving as a side effect of an unrelated declaration.

All four targets now print end 7 100 for the program above. test/test_variant_class_cross.pas is registered in test-i386, test-aarch64 and test-arm32, asserting the identical line on each.