← board

bug: variant record (case fields) do not share storage

Resolution (2026-06-23)

Front-end only (record layout in ParseTypeSection), no codegen. The record field loop had no case handling — case and its branch fields fell to the else Next skip, so the branch field names that lexed as identifiers were laid out SEQUENTIALLY (i at 0, c at 4), not overlapped.

Added a tkCase branch: parses case [tag: ] T of c1,..: (fields); ... end, optionally laying out the discriminant as a real tag field, then computing a single variantBaseOff (the aligned current offset) at which EVERY branch's fields start — a union. The record grows by the largest branch (variantMax). Scalar, fixed-array, and record branch fields are supported.

Verified byte-identical to FPC ({$mode objfpc}): r.i:=65; ord(r.c) -> 65; r.w:=258; r.b -> 2|1 (LE byte overlap); fixed-part + variant (x; case of 0:(a:integer) 1:(bb:array[0..3] of byte)) -> 7 4 3 2 1. No variant records exist in the compiler source, so self-host is unaffected.

Out of scope (separate, pre-existing, not a regression): reading a single branch field aliased over an integer branch yields 0.0 — a single-field reinterpret-load quirk, independent of the (now-correct) union layout. Gate: make test (self-host byte-identical) + FPC oracle. Closes bug-variant-record-no-overlap.

Symptom

Fields in different branches of a record's variant part must occupy the same storage (a union). In pxx they do not — writing one branch and reading another yields 0:

type tr = record case boolean of true: (i: integer); false: (c: char); end;
var r: tr;
begin r.i := 65; writeln(ord(r.c)); end.
{ fpc: 65    pxx: 0 }

Byte-overlap is likewise broken:

type tr = record case integer of 0: (w: word); 1: (b: array[0..1] of byte); end;
var r: tr;
begin r.w := 258; writeln(r.b[0], '|', r.b[1]); end.   { 258 = $0102 }
{ fpc: 2|1 (little-endian)    pxx: 0|0 }

Expected

The variant part is a union: all branch fields share the same offset (the record size is the fixed part plus the largest variant). r.i := 65; r.c reads byte 0 of the integer (65); r.w := $0102; r.b reads [2, 1] on little-endian.

Current behaviour suggests each branch's fields are laid out at separate (non-overlapping) offsets.

Repro

tools/fpc_diff_probe.sh (variant-record).