← board

A generic parameter constraint is counted as a type body

Found by not trusting the sentence I had just written

While fixing [[bug-p-a-method-pointer-type-derails-the-delphi-generic-alias-anchor]] I noticed that <T: class> reaches the same tkClass arm of DGenDeclAnchor and that DGenClassOpensBody answers true for it. Every arm of that fix's test passed, so it was masked, and I recorded it in that ticket as "not measured, not claimed, and not fixed in this commit" rather than either asserting it or dropping it. It is real:

{$MODE DELPHI}
type
  TArg   = class end;
  TBoxA<T: class> = class class function Who: Integer; end;
  TBoxB<T: class> = class class function Who: Integer; end;   { crossed by A's walk }
  TLate  = class end;                                          { A's alias must name this }
pascal26:19: error: expected 'begin' before 'TBoxA$TLate'
  near: Result := 1 ; end ; >>> TBoxA$TLate  specialize

fpc 3.2.2 prints 1 2.

The three spellings are one concept, and only two of them broke

constraint reproduces
<T: class> yes — tkClass, DGenClassOpensBody says it opens a body
<T: record> yes — tkRecord, counted unconditionally
<T: constructor> no — constructor reaches the section-ender arm instead
<T> unconstrained no — nothing to miscount

Ablated rather than reasoned: with the later template's constraint removed the identical program compiles and prints 1 2. A <T: TArg> spelling produces a genuine, correct constraint-violation error and is NOT evidence either way — recorded because it looked like a third data point for a minute and is not one.

Fix

Skip the whole < ... > group. A generic parameter list is not a type body, so nothing inside it may feed the body-depth count — which covers all three spellings, and the argument form (TBox<TArg>) for the same reason.

DGenAngleGroupEnd is deliberately conservative about what it will call a group: preceded by an identifier, containing nothing but identifiers, , : . and the two constraint keywords that are token kinds, nesting counted so TBox<TBox<T>> closes. Anything else — notably a real < comparison — returns -1 and the walk behaves exactly as before.

Special-casing each keyword was the alternative and is worse, for the reason this walk has now demonstrated three times: its keyword list has been wrong three times in three different directions, and the fourth spelling would have been found separately, later, by someone else.

Test

test/test_delphi_generic_constraint_anchor.pas, .expected is fpc 3.2.2's own output. Six arms: class, record and constructor constraints; an unconstrained later template and no later template at all as negative controls; and arm 5 as the control for the fix's own risk — a real anonymous record ... end field whose body must still be counted.

Arm 5 was verified to discriminate rather than assumed to. Built with tkRecord never counted — the tempting wrong fix, since a constraint's record reaches that arm — arm 5 fails:

pascal26:125: error: expected ':'
  near: : Integer ; end ; TBox5$TLate5 >>>  specialize TBox5

the alias spliced into the record. And the whole file fails on the pre-fix compiler at arm 1.

What the test does NOT cover, stated so nobody trusts it to: the skipper being too WIDE about what it calls a < ... > group. Nothing in this file has a < that is not one, so a widened skip passes every arm. That guard lives in DGenAngleGroupEnd's token filter and is argued there, not tested here.

Not a regression

Reproduces on 1ea430c95 and under the pin. b613b5fcf introduced the walk, so this shipped with it; unlike its of object sibling it did not break a corpus rung, which is why it survived undetected.