← board

TArray<T> is not ambient

program p; {$mode delphi}
type TRec = record A: TArray<byte>; B: Integer; end;
var r: TRec;
begin r.B := 5; WriteLn(r.B); end.

fpc -Mdelphi compiles and prints 5. pxx: unknown type: TArray. Adding uses sysutils; makes pxx agree.

Why it is Track A and not a one-line lib move

lib/rtl/sysutils.pas says it, and it is right: pxx has no lib/rtl/system.pas — its ambient types are compiler-side. So "put it where FPC puts it" is a compiler change, not a unit change, which is the whole reason the original fix chose SysUtils.

The population, and why the original check could not have seen it

corpus files naming TArray<> of those, uses SysUtils
rtl-generics 7 7
fpc-testsuite 6 0

Both numbers are real and the first was measured honestly. The inversion is structural: rtl-generics code uses generic COLLECTIONS, which pull in SysUtils for other reasons, so TArray arrives free and no file has to ask for it. Testsuite files exercise TArray ITSELF and deliberately carry no more uses than the feature under test requires. So the corpus that shows 7/7 is the one where the dependency is invisible, and the corpus that shows 0/6 is the one a conformance run is made of.

A census over the corpus you have is a census over the habits of the code that corpus is made of. Neither number is wrong; the sentence that joined them to "covers every real consumer" is.

Done when

The program above compiles with no uses clause, uses sysutils still works and does not double-declare, and the six testsuite files stop failing on this name. Whether the ambient declaration is a compiler-side builtin or a real lib/rtl/system.pas is the design question and is not settled here.

FIXED 2026-09-06 (frankS), compiler d674faa3ea21

compiler/builtin/sysgenerics.pas — a six-line unit holding the generic types FPC declares in System — is injected ambiently from ParseProgram's token pre-scan when the program's tokens contain the identifier TArray, the same door math, builtinwide, softfloat and wasibackend come through.

A unit rather than a compiler-side registration, because a template is a NAME plus a TOKEN RANGE (Templates[]): registering one without source means synthesising tokens, and a six-line unit is the same thing spelled in the language, with every existing import path working on it unchanged.

SysUtils KEEPS its declaration. The pre-scan sees the PROGRAM's tokens and nothing else — builtinwide's note in pasparser_prog.inc records the same limit — so a UNIT naming TArray while the program does not (rtl-generics' ToArrayImpl, the original report) triggers nothing here. The two populations are disjoint and each is covered by exactly one of the two declarations. Two templates of the same name coexist: verified, both spellings resolve with SysUtils loaded, and a specialization of array of T is a STRUCTURAL type rather than a minted class, so it carries none of the identity trouble a duplicated generic CLASS would ([[bug-p-a-nested-specialization-is-named-by-its-alias-so-one-name-serves-every-outer-specialization]]).

THE HALF THAT WAS NOT THE TEMPLATE

Injecting the unit was not enough, and the failure looked nothing like the cause. With the injection alone, a probe at the injection site printed tmplcount=1 find=0 — the template registered AND nameable — while the same compile answered unknown type: TArray two lines later. An explicit uses sysgenerics worked, which is what isolated it.

Importing a template is two things, and an ambient load gets one. A USE of an imported template — Delphi's bare TArray<T> and objfpc's inline specialize TArray<T> alike — is rewritten into an alias declaration by DelphiRewriteGenericUses, and the sweep that runs it over imported templates, DesugarImportedDelphiGenericUses, is called from the end of ParseUsesClause — which ParseUsesUnitAmbient never enters. The injection site now calls it.

The spelling in the unit is load-bearing

TArray<T> = array of T; under {$MODE PXX} (sysutils' own spelling) serves BOTH consumer spellings. Written as generic TArray<T> = array of T it compiles and then refuses the Delphi spelling at the use site with unknown type: TArray. Measured both ways.

Measured

Fixtures, all three byte-identical against fpc 3.2.2 and none of them with a uses line for this: test_tarray_is_ambient_with_no_uses_clause.pas (result position, record-field position, two argument types, r := nil), test_tarray_is_ambient_in_delphi_mode_and_yields_to_a_local_one.pas, and test_a_program_declaring_its_own_tarray_shadows_the_ambient_one.pas — the control that matters more than the feature, since the injection is parsed before the program's declarations and must lose to them.

Conformance: the wall moved on every row the ticket named. tarray18 37 -> 46, tarrconstr5 14 -> 47, trtti12 -> 22, trtti16 -> 27; tarray12 compiles. None of the four burns yet — each now stops on something else, and their skip reasons are updated to say what.

Log