P: a forward interface declaration is not parsed
Repro — six lines, and its own control
program fi1;
{$mode objfpc}
type
IFoo = interface; { forward }
IFoo = interface
end;
begin
end.
pascal26:4: error: unexpected token
Expected: end, but got: (Kind: 78, Line: 4)
near: fi1 type IFoo interface >>> IFoo
The control is the same declaration one keyword over, and it works:
TBar = class; { forward -- accepted }
TBar = class
end;
This is a double case with one arm missing — the shape
devdocs/dev/normalise-dont-special-case.md is about. pasparser_decl.inc
already detects a bodyless class (UClsForward[ci] := (not hadParens) and (CurTok.Kind = tkSemicolon)) and the interface arm never got the same test, so
the parser walks into the body it assumes is there and asks for the end that
is not coming.
Not a regression
Identical failure on pinned and on HEAD, with only the wording differing
(pinned says expected 'end' before ';'). Nothing recent broke it; it was
never implemented.
Why it is worth more than its size
library_candidates/fpc-testsuite/tests/test/tgenconstraint37.pp is
%NORUN-marked (should compile) and fails on this alone. That test is the
only one in the tgenconstraint set that specializes a generic against
forward-declared types:
TTestObject = class;
ITestInterface = interface;
TGenericTObjectTTestObject = specialize TGenericTObject<TTestObject>;
TGenericIInterfaceITestInterface = specialize TGenericIInterface<ITestInterface>;
TTestObject = class end;
ITestInterface = interface end;
which makes it the natural oracle for exactly the parse-order hazard [[bug-p-generic-constraints-are-checked-before-the-type-section-closes]] is about — constraints checked before the argument's real declaration is reached. It cannot serve as that oracle while it dies at line 18 for an unrelated reason. Fixing this turns one FAIL green AND unblocks the test that would prove the other ticket's fix.
Found while resolving
[[bug-p-generic-type-constraints-are-parsed-and-discarded]]; checked against
pinned before shipping precisely so it would not be mistaken for fallout of
that work.
A second consumer, and a landmine inside it (frankwasm, 2026-08-31)
This gap is what holds tgenconstraint37 at rejected-valid — the only remaining
disagreement in the 40-file tgenconstraint*.pp corpus, now 39/40 after
[[bug-p-generic-constraints-are-checked-before-the-type-section-closes]]. So
closing this ticket is worth one corpus row beyond its own repro.
Do not expect it to be free. 37 declares ITestInterface = interface; and
then specializes TGenericIInterface<ITestInterface> — an interface forward stub
against T: IInterface — and fpc accepts it (%NORUN). As of 2026-08-31 the
constraint checker JUDGES forward stubs rather than skipping them: a class stub
is treated as a class whose ancestry is TObject and which implements nothing yet,
which is what fpc does. The interface mirror of that rule — an interface stub
descends from IInterface — is not written, because nothing can currently
reach it. GCIntfDescends walks a parent chain the stub does not have yet, so
the moment the parse succeeds that line will likely be refused.
The fix, if it is needed, is the same shape as the class one already in
CheckTemplateConstraint: T: TObject is answered as isClass rather than by
walking parents, because every class descends from TObject. Every interface
descends from IInterface the same way.
FIXED 2026-09-05 (frankB) — and the landmine was real, exactly as predicted
Two changes, and the second is the one the ticket warned would not be free.
1. compiler/pasparser_decl.inc, the interface arm. A tkSemicolon right
after the interface keyword mints the UCls row, marks UClsForward, and
stops — the same shape the class arm's UClsForward[ci] := (not hadParens) and (CurTok.Kind = tkSemicolon) has always had. The full declaration then
completes that row in place rather than adding a second one, with the same
member-window re-anchoring and the same UClsUnitIdx = CurrentUnitIdx guard the
class arm carries, and for the reason its comment gives: FindUClass answers
with the first match, so a shadowing second row would take every later use with
it.
That distinction is why the test does not stop at "it compiled". Rows 1 and 2 call a method through the interface and through the class; a stub that was shadowed rather than completed would compile the file and fail there.
2. compiler/pasparser_generic.inc, CheckTemplateConstraint. frankwasm's
prediction was exact: with the parse fixed, specialize TGenericIInterface<ITestInterface> was refused —
generic constraint violated: TGenericIInterface<T> is constrained to
`IInterface`, but ITestInterface does not implement or descend from it
T: IInterface is the exact mirror of T: TObject, which had the same bug
for the same reason and whose fix is already sitting in the else-branch
alongside. An interface's UClsParent is -1 unless it names a parent
explicitly, so GCIntfDescends walks a chain that structurally never reaches
IInterface and answered True only when argCi = conCi. A forward stub has no
chain at all, which is why nothing reached it until forward declarations started
parsing. Answered in the coordinate system where it is expressible: in Pascal
every interface descends from IInterface.
Not a loosening, and the three negative controls say so. All still refuse, and fpc 3.2.2 refuses all three too (different wording — deferred):
| probe | result |
|---|---|
TT<IInterface> against T: ITest1 (ancestor, not descendant — tgenconstraint17) |
refused |
a forward CLASS stub against T: IInterface |
refused ("the stub implements nothing yet") |
a record against T: IInterface |
refused |
The direction still goes through GCIntfDescends unchanged; only the root
constraint short-circuits.
The corpus row cannot be checked from here
library_candidates/ in this checkout holds busybox, html5lib, reportlab,
rtl-generics, tinycss2 and webencodings — there is no fpc-testsuite tree, so
tgenconstraint37.pp could not be run to confirm it turns green. What is
measured is its SHAPE, reproduced from the excerpt in this ticket: two forward
declarations, one class and one interface, specialized against T: TObject and
T: IInterface before either is completed. That compiles and runs here and
under fpc. Whoever has the corpus should confirm the row itself; the claim in
this ticket's body that it fails "on this alone" is now the only untested link.
Gate
make compiler/pascal26 converged; tools/gate.sh quick GREEN with the FPC seed
canary CONCURRENT. test/test_forward_interface_decl.pas matches fpc 3.2.2
row for row and the pin refuses it at its first type line;
test/test_forward_interface_constraint_fail.pas is the negative half, in its
own file because a refusal cannot share a program with rows that must compile.
Log
- 2026-09-05 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit d750d86a4.