← board

A string Variant never converts to a Boolean

Found 2026-08-23 by the Variant differential family (fpc 3.2.2 -Mobjfpc -O1 vs pxx d014cc21a).

var a: Variant; b: Boolean;
begin
  a := 'True';  b := a;   { fpc: True   pxx: EVariantError }
end.

Loud, not silent — but it kills a program on a value FPC converts happily, and the string form is exactly how a Boolean arrives from a config file, a query result or a parsed field, which is the whole reason a variant holds it.

The existing comment was right about the case it measured, and wrong in general

VariantToBool (compiler/builtin/builtin.pas) says:

FPC RAISES for a string here (b := v with v='' is EVariantError, measured) rather than treating '' as false

'' does raise. So does 'zz'. But those are the only kind that does, and the arm was written to refuse every VT_STRING. A one-value measurement became a whole-tag rule — the failure mode devdocs/dev/root-cause-over-microfix.md describes as varying the shape too little.

FPC's actual rule, measured across 21 spellings

input fpc note
'True' 'true' 'TRUE' True case-insensitive, EXACT — no trimming
'False' 'FaLsE' False same
' true' 'true ' raises whitespace is not trimmed
'1' '2' '-1' '2.5' True numeric text, non-zero
'0' '0.0' '-0' False numeric text, zero
'' 'zz' 'yes' raises neither a keyword nor a number
1 0 2.5 0.0 (non-string) True/False/True/False unchanged

So: try the two keywords case-insensitively, then try to parse a number and test it against zero, then raise. Nothing else.

Fix

That rule, in VariantToBool's string arm. The number half is the same ValValFloat ladder PXXVarNumCoerce already uses twenty lines above, so the two agree about what "numeric text" means by construction.

VT_CHAR (tag 5) is folded in as its one-character text — pxx has a char variant and FPC does not (v := c gives FPC a varString), so there is no oracle for it; treating it as the 1-character string is the coherent reading and is what PXXVarNumCoerce already does with VT_CHAR. '0' is False, '1' is True, 'x' raises.

VT_OBJECT keeps raising, unchanged.

NilPy is not affected

NilPy routes to pylib's pyvar_to_bool at the lowering seam (ir.inc:5364), so Python's truthiness — where '' is False and 'zz' is True — is untouched.

Gate

Track A's, plus the 21 rows above matching fpc 3.2.2 (raise vs value, and which value) on x86-64 and a cross target, and a .npy row proving bool('') is still False and bool('zz') still True.

FIXED 2026-08-23 (claude-A)

All 21 measured rows now match fpc 3.2.2 -Mobjfpc -O1 exactly — including which ones RAISE, which is half the rule and the half a lenient fix would have broken.

VariantToBool's string arm now: __pxxSameNameCI against 'true'/'false', then ValValFloat (the same ladder PXXVarNumCoerce uses twenty lines above, so the two agree about numeric text by construction) tested against zero, then PXXVariantError. VT_CHAR folded in as its one-character text; VT_OBJECT unchanged.

One mechanical note: __pxxSameNameCI is implemented ~1000 lines below VariantToBool, so it gained an interface declaration beside __pxxUpCase rather than being duplicated.

Verified

Gate

make compiler/pascal26 converged + the 21-row differential + tools/gate.sh quick.

Log