← board

A typecast of a Variant reinterprets the record instead of converting

program vcast;
{$mode objfpc}
uses variants;
var v: Variant; i: Int64; d: Double;
begin
  v := 9;
  i := Int64(v);   WriteLn('int64cast=', i);
  v := 2.5;
  d := Double(v);  WriteLn('dblcast=', d:0:2);
end.
FPC 3.x pxx (HEAD 2026-08-12)
Int64(v) with v = 9 9 1
Double(v) with v = 2.5 2.50 SIGSEGV

FPC and Delphi define a typecast of a variant as the variant CONVERSION (the same one i := v performs). pxx lowers it as an ordinary hard cast of the 16-byte variant RECORD, so the value read is the tag word — or, for the float kinds, whatever reinterpreting those bytes as an address leads to.

1 is the worst available answer: it is a plausible integer, it is what a VT_* tag happens to be for several kinds, and every arm of a kind-dispatch chain written with Int64(v) / Integer(v) / Byte(v) gets it.

Why it matters beyond the one spelling

The assignment form (i := v) is correct, so the two spellings of the same intent disagree — which is exactly the double-case shape devdocs/dev/normalise-dont-special-case.md warns about. Anyone porting FPC code (the compat lane's whole business) writes the cast form.

Where to look

The cast is built in the shared type-cast path in parser.inc (the TypeName(expr) conversion arm) and lowered in ir*.inc. The fix is to recognise a VARIANT operand there and emit the variant→scalar conversion the assignment path already emits, rather than a reinterpretation. Grep for the sibling spellings before closing: Integer(v), Boolean(v), Char(v), Single(v), NativeInt(v) — a fix on the Int64 arm alone leaves the rest as they are.

Gate

make test + self-host fixedpoint; a .pas test diffed against FPC covering Int64/Integer/Byte/Boolean/Char/Single/Double casts of a variant holding an int, a float, a bool and a string.

Progress (2026-08-13)

Fixed. Normalised onto the assignment, per the shape the ticket names: VariantCastToTemp (parser.inc, beside PromoDemoteToInt64) rewrites a cast of a variant operand into a hidden temp of the CAST's type plus a store — ir.inc's AN_ASSIGN arm already emits the VariantTo* / pyvar_to_* unbox there. One mechanism, every spelling, all six backends, no new IR op.

The cast sites are FIVE, not one, and the ticket's own "grep the siblings" warning was the load-bearing part:

A second bug fell out of the float arm

Single(expr) / Double(expr) desugar to op := AllocVar('', LastExprTk) — but ParseExpr OVERWRITES LastExprTk with the OPERAND's kind, so the "conversion" allocated a temp of the SOURCE type and no conversion happened:

Capturing the kind before ParseExpr fixes both.

Verified

test/test_variant_typecast.pas (new, wired into make test): all 14 cast spellings over a variant holding an int, a float, a bool and a string, diffed against an FPC build of the same file. Every row matches except two the file flags in-place, both of which are the CONVERSION's own FPC parity and reproduce identically through i := v — [[bug-p-variant-to-int-and-char-conversion-diverges-from-fpc]] (filed): boolean→Int64 gives 1 where FPC gives -1, and Char(65) gives 'A' where FPC renders '65' and takes '6'.

NativeInt(v) is left out of the oracle file: FPC REFUSES it ("Illegal type conversion"), pxx's lax dialect converts it. Nothing to diff.

Not touched: Pointer(v) stays a reinterpret (the record's address is the only sensible reading), and WideChar(v) converts and then keeps its -3 marker by wrapping the converted temp rather than exiting the arm.

Gate: tools/gate.sh quick GREEN (self-host fixedpoint + testmgr quick + FPC seed canary), make test-nilpy GREEN — the nilpy suite because this changes a VARIANT lowering that pylib's Pascal sources compile through.

Log