← board

The constraint check rejects a class declared in the same type section

Regression, found by Track T (seven), full tier. test-fgl passed at a8947307fa98 and went NEW-RED at 719bef10ea68 — so this is fresh, not pre-existing.

test-fgl#src:compiler/.pascal26.fixedpoint
  PASS map_str.pas | FAIL objectlist.pas -- compile error:
  pascal26:9: error: generic constraint violated: TFPGObjectList<T> is
    constrained to `TObject`, but TThing is not TObject or a descendant of it
  near: = specialize TFPGObjectList < TThing > >>> ; constructor TThing

The repro is already in the tree: test/fgl/objectlist.pas

type
  TThing = class            { <- implicit TObject descendant }
    v: Integer;
    constructor Create(av: Integer);
  end;
  TThingList = specialize TFPGObjectList<TThing>;   { <- same type section }

TThing is a bare class, which is a TObject descendant. The rejection is wrong on the language, and near: shows the check firing while the type section is still open — the specialization sits between TThing's end; and the constructor TThing implementation.

Almost certainly [[bug-p-generic-constraints-are-checked-before-the-type-section-closes]]

That ticket [P] describes exactly this defect and predates the check that now exposes it. Raised 40 → 70: it was a low-prio one-liner while nothing read a constraint, and it became a live regression on real corpus code the moment f4fb9d31b made constraints load-bearing.

The cheap discriminating test — run this FIRST, before reading any code

Split the type section:

type
  TThing = class v: Integer; constructor Create(av: Integer); end;
type
  TThingList = specialize TFPGObjectList<TThing>;

Compiles ⇒ the mechanism is confirmed as "checked before the section closes", and the fix is to defer constraint checking to section close (or to resolve ancestry lazily at the check). Still fails ⇒ the mechanism is something else — ancestry resolution for an implicit TObject base, not section timing — and this ticket's framing is wrong. Do not skip this; the two produce the identical message.

What is NOT wrong here

f4fb9d31b is a correct and valuable fix and must not be reverted: TFoo<T: class> and every other constraint form was parsed and discarded, so no specialization was ever checked and all 40 FAIL-marked tgenconstraint fpc-testsuite tests were wrongly ACCEPTED. The check is right; its timing is wrong. Fix the timing, keep the check.

Gate

test/fgl/objectlist.pas compiles and runs, map_str.pas stays green, and the 35 tgenconstraint tests stay correctly rejected — that last one is the arm a naive "loosen the check" fix would silently undo.


FIXED (frankS, 2026-08-30) — the framing was wrong: not timing, not implicit ancestry

The discriminating test, run first, killed both hypotheses

Splitting the type section so the class closes before the specialization still fails. So it is not section timing. The coordinator's stated alternative — implicit-TObject ancestry — is also dead: an explicit class(TObject) fails identically. A self-contained repro (no fgl corpus needed) mapped it:

shape before
constraint TObject, arg TObject ok
constraint TObject, arg class (implicit), same section FAIL
constraint TObject, arg class (implicit), split section FAIL
constraint TObject, arg class(TObject) (explicit) FAIL
constraint TObject, arg three levels down FAIL
constraint user base class, arg its descendant, same section ok
ordinary descendant → ancestor assignment ok

The parent-chain walk is sound — a user base class works, and works in the same type section, which independently re-falsifies timing. Only TObject as the constraint fails, and only for descendants.

The mechanism: two representations of "descends from TObject"

pasparser_decl.inc:4594-4606 deliberately refuses to link TObject as a parent:

{ TObject is a registered builtin class row now, so exclude it from
  the parent lookup by name — a real-parent link would relocate
  every `class(TObject)` VMT and break the implicit-root model
  (and the COM/ARC synthesis for TInterfacedObject). }
if CaseEqual(CurTok.SVal, 'TObject') then tmpCi := -1;

So UClsParent is -1 at the root for both class(TObject) and a plain class. Meanwhile CheckTemplateConstraint does conCi := FindUClass('TObject'), which does find the registered builtin row, then calls GCIsDescOrSelf(argCi, conCi) — a walk up a chain that terminates at -1 and structurally can never reach that row. TBox<TObject> passed only because argCi = conCi matched on the first iteration, before any walk happened.

The parent chain says -1; the class table says a row. The check asked the question in the coordinate system where the answer is unreachable. Neither side is wrong on its own — they were never reconciled.

The fix — pasparser_generic.inc only

Answer the root constraint in the other coordinate system: in Pascal every class descends from TObject, so T: TObject means exactly isClass, which the function has already computed. Three lines at one call site.

This is not a loosening. A non-class still fails isClass, so every FAIL-marked test stays rejected — proven by control below, not by argument.

Why the three-part plan was not needed

The relayed plan was a pending-constraint list in defs.inc, recording instead of checking at pasparser_generic.inc:2551, and an unconditional drain at TypeSectionDepth = 0 in pasparser_decl.inc. All three parts implement deferral, and deferral does not fix this: the split-section row already defers the check past the class's own section close and still fails. Building it would have left the regression live, touched two files held by other agents, and made the check fire later in a coordinate system that still cannot answer it.

bug-p-generic-constraints-are-checked-before-the-type-section-closes [P p40] is therefore not this ticket's root cause and the two should not merge. It describes a real third state (a class whose declaration has started but whose ancestors/IMT are not yet populated) and tgenconstraint4/5 are still its evidence — it stands unchanged, and it is still worth doing on its own merits.

Gate — all three rows, each with a pre-fix control

Corpus note: neither fgl.pp nor the fpc-testsuite is installed in this checkout, so both were run against read-only copies from sibling checkouts (/home/neo/frankA/..., /home/neo/frank1/...). Nothing in either was written.

Regression test

test/test_generic_constraint_tobject_root.pas, self-contained so it runs on a box without the fgl corpus (this one skips test-fgl entirely). Six rows: implicit, explicit, three-deep, TObject itself, a separate type section, and a user-base constraint. .expected generated by FPC 3.2.2 compiling the file; pxx matches byte for byte, and the pre-fix compiler fails it at the first row.

The section-splitting rows are deliberate: they are what falsified the timing diagnosis, and keeping them stops a future timing-flavoured change from quietly re-opening it.

Log