← board

VarIsStr is False for a one-character string variant

Measured

uses SysUtils, Variants;
var v: Variant;
begin
  v := 'x';  WriteLn(VarIsStr(v), ' tag=', VarType(v));
  v := 'xy'; WriteLn(VarIsStr(v), ' tag=', VarType(v));
  v := '';   WriteLn(VarIsStr(v), ' tag=', VarType(v));
end.
FPC 3.2.2 pxx
'x' True False (tag 5)
'xy' True True (tag 6)
'' True True (tag 6)

The length of the string decides the answer, which no caller could guess.

Root cause, and the shape it is an instance of

function VarIsStr(const V: Variant): Boolean;
begin
  Result := VarType(V) = 6;          { VT_STRING only }
end;

{ text tag: a char and a string are one kind here ... }
function IsTextTag(t: TVarType): Boolean;
begin
  Result := (t = VT_STRING) or (t = VT_CHAR);
end;

Those two sit three lines apart in the same file and answer the same question differently. IsTextTag is the correct one — its own comment says so outright — and VarCompareValue uses it, which is why comparing a one-char variant as text works while asking whether it IS text does not.

Two mechanisms serving one concept is the smell devdocs/dev/normalise-dont-special-case.md is about, and this is its cheapest possible form. Note also that VarIsStr spells the tag as a bare 6 where the whole rest of the unit uses the named VT_STRING, which is how it escaped the notice that VT_CHAR exists.

Fix

Result := IsTextTag(VarType(V));

IsTextTag is declared in the implementation section below VarIsStr, so either move it up or forward-declare it.

Check the siblings in the same pass, since the same bare-number habit could have hit them: VarIsNumeric (does it accept VT_INT64 and VT_DOUBLE as well as VT_INT?) and any other VarType(V) = <literal> in the unit. Grep for VarType(V) = and make every one of them use a named constant or a predicate.

Not part of this

VarType's numbers do not match FPC's varXxx — that is deliberate and documented in the unit header, and [[decide-variant-tag-space-is-a-language-wide-commitment]] settled it (the tag set is ours, closed, and not a durable format). FPC code comparing against varString fails to COMPILE here rather than silently mis-branching, because this unit does not declare those constants. Do not "fix" that.

Gate

Track B's: build with $(PXX_STABLE) (working again as of the v373 pin), make lib-test. Plus the three rows above matching fpc 3.2.2, one-char case included.

RESOLVED 2026-08-28 (frankB) — landed as part of one job

Done together with bug-b-the-fpc-vartype-constants-are-missing, which carries the full write-up. The three tickets were one defect and one already-fixed twin, and doing the headline one alone would have broken this one rather than left it stale: after VarType translates, a comparison against an internal VT_ tag is false for every value of that kind, not just the reported case.

Landed in lib/rtl/variants.pas only, no compiler change: FPC's varXxx constants exported (measured from fpc 3.2.2, not transcribed), a private RawTag for the eight internal call sites, and VarType translating at the facade seam. 11 of 12 rows now match FPC exactly; the documented divergence is v := 1 (FPC narrows the literal to varShortInt).

Gated by test/lib_variants_vartype_codes.pas in make lib-test, sentinel VARTYPECODES OK, negative-controlled.

Log