← board

Four sysutils string gaps

Context first: the family is in good shape

Twenty-two programs, one per shape, each diffed against fpc 3.2.2. Eighteen were byte-identical, including every row that usually hides a bug:

So the four below are gaps, not a pattern of rot.

1. Concat is fixed-arity and AnsiString-only

W(Concat('a','b','c'));
pascal26:10: error: no overload of Concat matches these arguments
  argument types: (Char, Char, Char)
  candidates:
    Concat(AnsiString, AnsiString)

FPC's Concat is variadic and accepts chars. Two shapes are missing: more than two arguments, and Char arguments (a Char does not widen to AnsiString at the overload site).

2. AnsiQuotedStr does not exist

undefined variable (AnsiQuotedStr). QuotedStr exists and is correct; AnsiQuotedStr(s, quoteChar) is the general form and is what code that quotes with anything other than ' calls.

3. SameStr does not exist

undefined variable (SameStr). SameText exists (case-insensitive); SameStr is its case-SENSITIVE twin, i.e. CompareStr(a, b) = 0. One line.

4. TryStr* leaves the value untouched on failure

i := -1;
ok := TryStrToInt('q', i);      { fpc: False 0    pxx: False -1 }

FPC's documentation calls the value undefined after a failed Val, so pxx is not strictly wrong — but FPC in practice zeroes it, and a stale value surviving a failed conversion is the shape that bites: if not TryStrToInt(s, n) then is often followed by code that uses n anyway with a default in mind.

The whole family is declared var value, not out value, and all seven behave the same way: TryStrToInt, TryStrToInt64, TryStrToQWord, TryStrToFloat, TryStrToDate, TryStrToTime, TryStrToDateTime.

Note: switching the declarations to out would NOT fix it — pxx does not model out at all (bug-a-an-out-parameter-of-a-managed-type-is-not-cleared), and FPC's out does not clear ordinals either (measured). The fix is one explicit value := 0 on each failure path.

Gate

make lib-test green, plus each of the four shapes above matching fpc 3.2.2.


2026-08-27 — RESOLVED. Two of the four were already fixed; the other two are done.

Re-measured every row against fpc 3.2.2 before touching anything, which is what stopped a wrong change from landing.

# ticket said measured today
1 Concat is fixed-arity and AnsiString-only already fixed, in the COMPILER
2 AnsiQuotedStr does not exist genuinely missing — added
3 SameStr does not exist already there, with AnsiSameStr beside it
4 TryStr* leaves the value untouched date/time three already cleared; the four scalar ones did not — done

1 — and the library fix I nearly landed was the wrong one

Concat('a','b','c') compiles and prints abc today, on the fresh compiler and on v388 pinned. It was fixed in pasparser_expr.inc (compat-pascal-uses-sysutils-withdraws-the-variadic-concat): uses sysutils was withdrawing the variadic intrinsic because a two-argument Concat was in scope, and that arm now folds Concat(s1..sn) to + when no overload matched.

I had already written the obvious library fix — a Concat(const args: array of const) overload rendering each element through FmtArgStr — and it worked. It was still wrong. It has exactly the shape to MATCH Concat('a','b','c') at the overload site, so it would divert those calls away from the intrinsic fold into a TVarRec-building library call, and for the dynamic-array form (Concat(arr1, arr2), which the same intrinsic serves) it would silently produce text instead of an array. Measured cost of the version I reverted: the test binary was 498 bytes larger.

Reverted, and the test carries a comment saying why no such overload exists, because adding one is the natural move for the next reader.

2 — AnsiQuotedStr, and QuotedStr rewritten as its special case

QuotedStr kept its own copy of the quote-doubling loop. It is now AnsiQuotedStr(s, '''') — one rule, one place (devdocs/dev/normalise-dont-special-case.md).

4 — the four scalar TryStr* now zero on failure

TryStrToInt, TryStrToInt64, TryStrToQWord, TryStrToFloat. The ticket said all seven behaved this way; TryStrToDate/Time/DateTime already cleared, with a comment saying why, so only four changed. out would not have done it, for the two reasons the ticket already records.

Callers checked rather than assumed: lib/rtl/variants.pas reads the value only on success, and lib_strtoint / lib_strutil assert it only inside TryStrToInt(...) and (v = ...).

Measured

Log