From e2b715b53b2c3e9d9d7c07fbe05ef4d52f26b713 Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Tue, 16 Jun 2026 10:13:45 +0100 Subject: [PATCH] fix(parser): support nested generic type arguments TList> and TBox> 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>> 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>, both backends). docs/grammar.ebnf TypeArgList rule updated to recurse via GenericName. All three fixpoints + full suite (3224 tests) pass. --- compiler/src/main/pascal/uParser.pas | 21 +++++++------ compiler/src/test/pascal/cp.test.e2e.misc.pas | 31 +++++++++++++++++++ docs/grammar.ebnf | 4 ++- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/compiler/src/main/pascal/uParser.pas b/compiler/src/main/pascal/uParser.pas index 4cf796b..ff4d028 100644 --- a/compiler/src/main/pascal/uParser.pas +++ b/compiler/src/main/pascal/uParser.pas @@ -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> and + TBox> 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>) — 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 + '>'; diff --git a/compiler/src/test/pascal/cp.test.e2e.misc.pas b/compiler/src/test/pascal/cp.test.e2e.misc.pas index efc880e..146ae5e 100644 --- a/compiler/src/test/pascal/cp.test.e2e.misc.pas +++ b/compiler/src/test/pascal/cp.test.e2e.misc.pas @@ -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>. 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 = class + FV: T; + procedure SetV(V: T); begin FV := V end; + function GetV: T; begin Result := FV end; + end; + var outer: TBox>; inner: TBox; + begin + inner := TBox.Create(); + inner.SetV(7); + outer := TBox>.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); diff --git a/docs/grammar.ebnf b/docs/grammar.ebnf index 9f9fd84..c162d8d 100644 --- a/docs/grammar.ebnf +++ b/docs/grammar.ebnf @@ -711,8 +711,10 @@ SetElement = Expr [ DOTDOT Expr ] ; +(* Each type argument is itself a (possibly-generic) type name, so type + * arguments nest: TList>, TBox>. *) TypeArgList - = IDENT { COMMA IDENT } + = GenericName { COMMA GenericName } ;