Document the Variant conversion rules, and fix the wrong note about them
- Type: doc — Track D (
docs/**only; prose, nocompiler/**, nolib/**) - Why now: the behaviour was undefined-in-practice until 2026-08-13 and is now settled and tested, so it can be written down without guessing. Shipped in pin v268: [[bug-p-a-typecast-of-a-variant-reinterprets-it-instead-of-converting]] and [[bug-p-variant-to-int-and-char-conversion-diverges-from-fpc]].
1. There is a WRONG statement in the docs today — fix it first
docs/language/types.md (the ## Variants section, the > [!NOTE] block):
In this dialect, boolean values stored in a
Variantprint as0forFalseand1forTruewhen usingwriteln.
Both halves are wrong. Measured against pinned v268 and FPC 3.2.2:
var v: Variant; b: Boolean;
v := True; writeln(v); { True — NOT 1 }
v := False; writeln(v); { False — NOT 0 }
b := True; writeln(b); { TRUE — a plain Boolean is upper-case }
FPC prints exactly the same three lines. So it is not a divergence at all, and
"in this dialect" is doubly misleading. Worth keeping the real quirk the note
was probably reaching for: a boolean variant prints True/False while a
plain Boolean prints TRUE/FALSE — in both compilers.
2. Document the conversion rules (the new content)
A Variant in a scalar context — i := v and Int64(v) alike, which are the
SAME operation as of the fix above — converts rather than reinterpreting. Two
rows are worth stating explicitly because they surprise people:
Boolean -> number is -1, not 1. A boolean variant converts to OLE's
VARIANT_TRUE, matching FPC and every COM/OLE consumer:
| expression | value |
|---|---|
Int64(v) with v := True |
-1 |
Byte(v) with v := True |
255 |
Double(v) with v := True |
-1.0 |
Ord(True), Integer(someBooleanVar) |
1 — unchanged |
The contrast in the last row is the point: the -1 belongs to the VARIANT
conversion, not to booleans generally. Same in FPC.
Variant -> Char diverges from FPC on purpose. PXX answers Chr(n); FPC
renders the variant to its string form and takes character 1:
v |
PXX (default) | FPC, and PXX under --strict-fpc |
|---|---|---|
65 |
A |
6 |
122 |
z |
1 |
2.5 |
#0 |
2 |
True |
#1 |
T |
'hi' |
h |
h |
Explain WHY, briefly — it is the rare case where "we differ from FPC" is the defensible side, and readers porting code deserve the reason:
- FPC contradicts itself here: the same numeric variant converts as a NUMBER
for
Byte/Word/Int64(Byte(v)is 65) and as a STRING forChar. - The rule comes from OLE Automation's
VARIANT, which has no char type, so Delphi defined a Char target as "a string of length 1" and FPC inherited it. - The intermediate is not even writable by hand:
c := someAnsiStringis a type error in FPC.
--strict-fpc reproduces FPC's rule exactly, including the edges (an empty
string yields #0; a Null variant raises, with FPC's own message, which names
String rather than Char).
3. The umbrella's description needs widening
docs/reference/modes.md describes --strict-fpc as turning on "the strict
checks" and lists four (--strict-case, --strict-operator,
--strict-visibility, --require-forward). That is now incomplete in kind, not
just in count: the umbrella also changes semantics in two places — FPC's
shift widths (StrictShiftWidth, see
[[bug-a-strict-fpc-does-not-reproduce-fpc-shift-widths]]) and now the
Variant->Char rule. A reader who takes "checks" literally will not expect
--strict-fpc to change a value. Same for docs/reference/cli.md's table row
and docs/reference/directives.md's {$STRICT_FPC} row.
Verify, do not assume
Track D's rule: compile every snippet with $(PXX_STABLE) (v268 or later — an
older pin predates this behaviour and will show the old wrong answers). The
Char table above is worth pasting into a scratch file and running under both
modes rather than transcribing from this ticket.
Gate
Docs stay internally consistent; every snippet compiles and produces the stated
output under $(PXX_STABLE). No compiler or library changes.
Log
- 2026-08-14 — done. All three parts landed, everything measured against pinned
v303 with FPC 3.2.2 as the oracle rather than transcribed from this ticket.
§1: the wrong
> [!NOTE]indocs/language/types.mdis replaced — confirmedwriteln(v)on a boolean variant printsTrue/Falseand a plain Boolean printsTRUE/FALSE, identically in both compilers, so the note now states the real quirk instead of a non-existent divergence. §2: new "Converting a Variant to a scalar" section; the eight boolean values (-1 / 255 / -1.0, vs Ord(True)=1) match FPC exactly, and every row of the Char table reproduces — default givesA z #0 #1 h,--strict-fpcgives6 1 2 T h, byte-identical to FPC's own output. §3:--strict-fpcre-described as checks plus two semantic rules inmodes.md,cli.mdanddirectives.md. One ticket claim dropped as unreachable: theNull-variant edge (raises with FPC's String-naming message) cannot be written in PXX today —NullandVarClearare bothundefined variableon the pinned compiler, so there is no user program that observes it. Documented the empty-string edge instead (#0, verified in both modes and in FPC). WhetherNullshould exist at all is a language question, not a docs one. - 2026-08-14 — resolved, commit 28c6f6c75.