← board

A bodiless generic class with abstract and a generic parent is rejected

Repro

unit u_c;
{$MODE DELPHI}
interface
type
  TBase = class
  end;
  TEnumBase<T> = class
  end;
  TD<T> = class abstract(TEnumBase<T>);     { <-- rejected }
implementation
end.
pascal26:11: error: unexpected token in a unit interface section:
             it starts no declaration (a mistyped section header?)

The error points at end. — the interface section was silently over-consumed, so the reported line is nowhere near the cause.

All three ingredients are required — measured

shape result
TD<T> = class abstract(TEnumBase<T>); FAIL
TD<T> = class(TEnumBase<T>); (no abstract) pass
TD = class abstract(TBase); (not generic, non-generic parent) pass
TD<T> = class(TBase); (generic, non-generic parent) pass
TD<T> = class(TEnumBase<T>) end; (with a body) pass

So it is not "bodiless classes", not "abstract", and not "generic parents" — it is their intersection. FPC compiles the failing row.

Why it matters

rtl-generics writes exactly this, and it is the first declaration of its kind in generics.collections:

TCustomPointersEnumerator<T, PT> = class abstract(TEnumerator<PT>);

It is on the path of [[feature-pascal-corpus-generics]] (rung 3).

Relationship to the bound-name scan fix

Found while fixing a different defect in the same shape's neighbourhood — CollectNestedTypeNames mis-tracked nesting depth on bodiless classes and swallowed 11,312 tokens of generics.collections. That is fixed. This one is the parser rejecting the declaration outright, which is a separate mechanism and is why it is a separate ticket: the scan fix made the corpus advance past it, and this repro isolates it standalone.

Where to start

The class-header parse path, where abstract/sealed modifiers are consumed relative to the parenthesised parent list — and specifically what happens when the parent list contains a <...> group. The passing rows say each half is handled; only the combination is not.

Log

Closed 2026-09-04 (frankB) — measured fixed, with the instrument checked first

Re-measured at binary 62e6e63cd05b while picking this rung up. All three rows compile:

TD<T>                      = class abstract(TEnumBase<T>);   { the ticket's repro }
TCustomPointersEnumerator<T, PT> = class abstract(TEnumerator<PT>);  { rtl-generics' own }
TSealedish<T>              = class sealed(TEnumerator<T>);

and var d: TD<Integer>; in the importing program binds, so the declaration is not merely being skipped.

The instrument was checked before the negative was believed. A file that compiles is exactly what a parser silently swallowing a declaration also produces. Substituting @@@ into the same slot of the same unit reproduces this ticket's error verbatim — pascal26:11: error: unexpected token in a unit interface section: it starts no declaration (a mistyped section header?) — so the pass is a real negative and not a stray-token arm eating the row.

Fixed by the bodiless-modifier work in test/test_generic_bodiless_class_modifier.pas, whose header lists TDerived<T> = class abstract(TBase<T>); FAILS before / ok after as one of its measured rows. That test's existence is not what closes this; the measurement above is. The test is why it stays closed.