fix(parser): support nested generic type arguments

TList<TList<Integer>> and TBox<TPair<Integer, string>> failed to parse:
each type argument was read as a single bare identifier, with no recursion
into a nested <...>, so a nested '<' produced "Expected '>' but got '<'"
(type position) or "Expected '.' or '(' after generic type arguments"
(constructor/expression position).

Both type-argument parse sites now recurse through ParseTypeName, so a type
argument may itself be a generic specialisation, to arbitrary depth
(TList<TList<TList<Integer>>> works). The expression-position heuristic also
accepts tkLessThan as the lookahead-2 token (the first arg being itself
generic). The comparison-operator disambiguation (a < b, (a < b) and (b < c))
is unaffected.

Found by the e2e generics hardening sweep. Regression:
TE2EMiscTests.TestRun_NestedGenericTypeArgs (TBox<TBox<Integer>>, both
backends). docs/grammar.ebnf TypeArgList rule updated to recurse via
GenericName.

All three fixpoints + full suite (3224 tests) pass.
This commit is contained in:
Graeme Geldenhuys 2026-06-16 10:13:45 +01:00
parent 236736a3be
commit e2b715b53b
3 changed files with 46 additions and 10 deletions

View file

@ -273,8 +273,10 @@ begin
raise EParseError.Create(Format(
'Expected type argument after ''<'' at line %d col %d in %s',
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
Result := Result + FCurrent.Value;
Advance();
{ Each type argument is itself a (possibly-generic) type name, so recurse
this is what makes nested generics like TList<TList<Integer>> and
TBox<TPair<Integer, string>> parse. }
Result := Result + Self.ParseTypeName();
while Check(tkComma) do
begin
Advance();
@ -282,8 +284,7 @@ begin
raise EParseError.Create(Format(
'Expected type argument after '','' at line %d col %d in %s',
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
Result := Result + ',' + FCurrent.Value;
Advance();
Result := Result + ',' + Self.ParseTypeName();
end;
Expect(tkGreaterThan);
Result := Result + '>';
@ -3970,16 +3971,18 @@ begin
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
end
else if Check(tkLessThan) and (PeekKind() = tkIdent) and
(PeekKind2() in [tkGreaterThan, tkComma]) then
(PeekKind2() in [tkGreaterThan, tkComma, tkLessThan]) then
begin
{ Generic type args. PeekKind2 = '<' means the first arg is itself
generic (TList<TList<Integer>>) recurse via ParseTypeName so
nested generics parse. Plain idents also go through ParseTypeName
(which simply returns the ident when no '<' follows). }
Advance(); { consume '<' }
Name := Name + '<' + FCurrent.Value;
Advance();
Name := Name + '<' + Self.ParseTypeName();
while Check(tkComma) do
begin
Advance();
Name := Name + ',' + FCurrent.Value;
Advance();
Name := Name + ',' + Self.ParseTypeName();
end;
Expect(tkGreaterThan);
Name := Name + '>';

View file

@ -108,6 +108,10 @@ type
{ (expr as T).Field := value a parenthesised cast as an assignment
TARGET. Was a parser gap (statements could not start with '('). }
procedure TestRun_ParenCastAsAssignmentTarget;
{ Nested generic type arguments: TList<TList<Integer>>. Was a parser gap
(type-arg list did not recurse, in both type and constructor position). }
procedure TestRun_NestedGenericTypeArgs;
end;
implementation
@ -1146,6 +1150,33 @@ begin
AssertRunsOnAll(SrcParenCastTarget, '42' + LE, 0);
end;
const
{ Nested generic type args, in both var-type and constructor position. }
SrcNestedGeneric = '''
program Prg;
type TBox<T> = class
FV: T;
procedure SetV(V: T); begin FV := V end;
function GetV: T; begin Result := FV end;
end;
var outer: TBox<TBox<Integer>>; inner: TBox<Integer>;
begin
inner := TBox<Integer>.Create();
inner.SetV(7);
outer := TBox<TBox<Integer>>.Create();
outer.SetV(inner);
WriteLn(outer.GetV().GetV());
outer.Free();
inner.Free()
end.
''';
procedure TE2EMiscTests.TestRun_NestedGenericTypeArgs;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcNestedGeneric, '7' + LE, 0);
end;
initialization
RegisterTest(TE2EMiscTests);

View file

@ -711,8 +711,10 @@ SetElement
= Expr [ DOTDOT Expr ]
;
(* Each type argument is itself a (possibly-generic) type name, so type
* arguments nest: TList<TList<Integer>>, TBox<TPair<Integer, string>>. *)
TypeArgList
= IDENT { COMMA IDENT }
= GenericName { COMMA GenericName }
;