Add user generic methods (#1849)

This commit is contained in:
Jen Basch authored and GitHub committed 2026-10-27 07:17:08 -07:00
1 parent 939e844c03
commit 21d278eba5
160 files changed
+2597 -880

No files matched your search

@@ -237,9 +237,10 @@ class GenericParserImpl {
headers.add(makeTerminal(next()));
ff(headers);
headers.add(parseIdentifier());
if (lookahead() == Token.LT) {
ff(headers);
if (lookahead == Token.LT) {
headers.add(parseTypeParameterList(true));
ff(headers);
headers.add(parseTypeParameterList());
}
children.add(new Node(NodeType.TYPEALIAS_HEADER, headers));
ff(children);
@@ -264,7 +265,7 @@ class GenericParserImpl {
headers.add(parseIdentifier());
if (lookahead() == Token.LT) {
ff(headers);
headers.add(parseTypeParameterList());
headers.add(parseTypeParameterList(true));
}
if (lookahead() == Token.EXTENDS) {
var extend = new ArrayList<Node>();
@@ -361,7 +362,7 @@ class GenericParserImpl {
children.add(new Node(NodeType.METHOD_HEADER, headers));
ff(children);
if (lookahead == Token.LT) {
children.add(parseTypeParameterList());
children.add(parseTypeParameterList(false));
ff(children);
}
children.add(parseParameterList());
@@ -533,7 +534,7 @@ class GenericParserImpl {
children.add(new Node(NodeType.METHOD_HEADER, headers));
ff(children);
if (lookahead == Token.LT) {
children.add(parseTypeParameterList());
children.add(parseTypeParameterList(false));
ff(children);
}
children.add(parseParameterList());
@@ -738,12 +739,30 @@ class GenericParserImpl {
private Node parseUnqualifiedAccessExpr() {
var children = new ArrayList<Node>();
parseAccess(children);
return new Node(NodeType.UNQUALIFIED_ACCESS_EXPR, children);
}
private void parseAccess(List<Node> children) {
children.add(parseIdentifier());
if (lookahead() == Token.LPAREN && noSemicolonInbetween() && _lookahead.newLinesBetween == 0) {
if (lookahead() == Token.DCOLON && noSemicolonInbetween() && _lookahead.newLinesBetween == 0) {
children.add(makeTerminal(next()));
ff(children);
children.add(parseTypeArgumentList());
if (lookahead() == Token.LPAREN
&& noSemicolonInbetween()
&& _lookahead.newLinesBetween == 0) {
ff(children);
children.add(parseArgumentList());
} else {
throw parserError(ErrorMessages.create("unexpectedToken", "("), spanLookahead);
}
} else if (lookahead() == Token.LPAREN
&& noSemicolonInbetween()
&& _lookahead.newLinesBetween == 0) {
ff(children);
children.add(parseArgumentList());
}
return new Node(NodeType.UNQUALIFIED_ACCESS_EXPR, children);
}
private Node parseExprAtom(@Nullable String expectation) {
@@ -843,11 +862,7 @@ class GenericParserImpl {
if (lookahead == Token.DOT) {
children.add(makeTerminal(next()));
ff(children);
children.add(parseIdentifier());
if (lookahead() == Token.LPAREN) {
ff(children);
children.add(parseArgumentList());
}
parseAccess(children);
yield new Node(NodeType.SUPER_ACCESS_EXPR, children);
} else {
expect(Token.LBRACK, children, "unexpectedToken", "[");
@@ -1317,12 +1332,12 @@ class GenericParserImpl {
return bodies;
}
private Node parseTypeParameterList() {
private Node parseTypeParameterList(boolean allowVarianceModifiers) {
var children = new ArrayList<Node>();
expect(Token.LT, children, "unexpectedToken", "<");
ff(children);
var elements = new ArrayList<Node>();
parseListOf(Token.GT, elements, this::parseTypeParameter);
parseListOf(Token.GT, elements, () -> parseTypeParameter(allowVarianceModifiers));
children.add(new Node(NodeType.TYPE_PARAMETER_LIST_ELEMENTS, elements));
expect(Token.GT, children, "unexpectedToken2", ",", ">");
return new Node(NodeType.TYPE_PARAMETER_LIST, children);
@@ -1356,11 +1371,17 @@ class GenericParserImpl {
return new Node(NodeType.ARGUMENT_LIST, children);
}
private Node parseTypeParameter() {
private Node parseTypeParameter(boolean allowVarianceModifier) {
var children = new ArrayList<Node>();
if (lookahead == Token.IN) {
if (!allowVarianceModifier) {
throw parserError("typeParameterVarianceNotAllowed");
}
children.add(makeTerminal(next()));
} else if (lookahead == Token.OUT) {
if (!allowVarianceModifier) {
throw parserError("typeParameterVarianceNotAllowed");
}
children.add(makeTerminal(next()));
}
children.add(parseIdentifier());
@@ -1583,6 +1604,7 @@ class GenericParserImpl {
case ASSIGN -> NodeType.ASSIGN;
case AT -> NodeType.AT;
case COLON -> NodeType.COLON;
case DCOLON -> NodeType.DCOLON;
case COMMA -> NodeType.COMMA;
case DOT -> NodeType.DOT;
case GT -> NodeType.GT;
@@ -137,7 +137,12 @@ public final class Lexer {
case '}' -> Token.RBRACE;
case ',' -> Token.COMMA;
case '@' -> Token.AT;
case ':' -> Token.COLON;
case ':' -> {
if (lookahead == ':') {
nextChar();
yield Token.DCOLON;
} else yield Token.COLON;
}
case '+' -> Token.PLUS;
case '%' -> Token.MOD;
case '[' -> {
@@ -365,7 +365,7 @@ final class ParserImpl {
var identifier = parseIdentifier();
TypeParameterList typePars = null;
if (lookahead == Token.LT) {
typePars = parseTypeParameterList();
typePars = parseTypeParameterList(true);
}
expect(Token.ASSIGN, "unexpectedToken", "=");
var type = parseType();
@@ -398,7 +398,7 @@ final class ParserImpl {
TypeParameterList typePars = null;
var end = name.span();
if (lookahead == Token.LT) {
typePars = parseTypeParameterList();
typePars = parseTypeParameterList(true);
end = typePars.span();
}
children.add(typePars);
@@ -500,7 +500,7 @@ final class ParserImpl {
children.add(name);
TypeParameterList typePars = null;
if (lookahead == Token.LT) {
typePars = parseTypeParameterList();
typePars = parseTypeParameterList(false);
}
children.add(typePars);
var parameterList = parseParameterList();
@@ -693,7 +693,7 @@ final class ParserImpl {
var identifier = parseIdentifier();
TypeParameterList params = null;
if (lookahead == Token.LT) {
params = parseTypeParameterList();
params = parseTypeParameterList(false);
}
var args = parseParameterList();
TypeAnnotation typeAnnotation = null;
@@ -831,16 +831,31 @@ final class ParserImpl {
case DOT, QDOT -> {
var rhs = parseIdentifier();
var isNullable = op == Operator.QDOT;
TypeArgumentList typeArgumentList = null;
if (lookahead == Token.DCOLON
&& !precededBySemicolon
&& _lookahead.newLinesBetween == 0) {
next();
typeArgumentList = parseTypeArgumentList();
}
ArgumentList argumentList = null;
if (lookahead == Token.LPAREN
&& !precededBySemicolon
&& _lookahead.newLinesBetween == 0) {
argumentList = parseArgumentList();
} else if (typeArgumentList != null) {
// type args but no args
throw new ParserError(ErrorMessages.create("unexpectedToken", "("), spanLookahead);
}
var lastSpan = argumentList != null ? argumentList.span() : rhs.span();
expr =
new QualifiedAccessExpr(
expr, rhs, isNullable, argumentList, expr.span().endWith(lastSpan));
expr,
rhs,
isNullable,
typeArgumentList,
argumentList,
expr.span().endWith(lastSpan));
}
default -> {
var nextMinPrec = op.isLeftAssoc() ? op.getPrec() + 1 : op.getPrec();
@@ -977,13 +992,24 @@ final class ParserImpl {
if (lookahead == Token.DOT) {
next();
var identifier = parseIdentifier();
TypeArgumentList typeArgs = null;
if (lookahead == Token.DCOLON
&& !precededBySemicolon
&& _lookahead.newLinesBetween == 0) {
next();
typeArgs = parseTypeArgumentList();
}
if (lookahead == Token.LPAREN
&& !precededBySemicolon
&& _lookahead.newLinesBetween == 0) {
var args = parseArgumentList();
yield new SuperAccessExpr(identifier, args, start.endWith(args.span()));
yield new SuperAccessExpr(identifier, typeArgs, args, start.endWith(args.span()));
}
if (typeArgs != null) {
// type args but no args
throw new ParserError(ErrorMessages.create("unexpectedToken", "("), spanLookahead);
} else {
yield new SuperAccessExpr(identifier, null, start.endWith(identifier.span()));
yield new SuperAccessExpr(identifier, null, null, start.endWith(identifier.span()));
}
} else {
expect(Token.LBRACK, "unexpectedToken", "[");
@@ -1026,14 +1052,25 @@ final class ParserImpl {
case STRING_MULTI_START -> parseMultiLineStringLiteralExpr();
case IDENTIFIER -> {
var identifier = parseIdentifier();
TypeArgumentList typeArgs = null;
if (lookahead == Token.DCOLON
&& !precededBySemicolon
&& _lookahead.newLinesBetween == 0) {
next();
typeArgs = parseTypeArgumentList();
}
if (lookahead == Token.LPAREN
&& !precededBySemicolon
&& _lookahead.newLinesBetween == 0) {
var args = parseArgumentList();
yield new UnqualifiedAccessExpr(
identifier, args, identifier.span().endWith(args.span()));
identifier, typeArgs, args, identifier.span().endWith(args.span()));
}
if (typeArgs != null) {
// type args but no args
throw new ParserError(ErrorMessages.create("unexpectedToken", "("), spanLookahead);
} else {
yield new UnqualifiedAccessExpr(identifier, null, identifier.span());
yield new UnqualifiedAccessExpr(identifier, null, null, identifier.span());
}
}
case EOF ->
@@ -1072,14 +1109,27 @@ final class ParserImpl {
if (lookahead == Token.DOT || lookahead == Token.QDOT) {
var isNullable = next().token == Token.QDOT;
var identifier = parseIdentifier();
TypeArgumentList typeArgumentList = null;
if (lookahead == Token.DCOLON && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
next();
typeArgumentList = parseTypeArgumentList();
}
ArgumentList argumentList = null;
if (lookahead == Token.LPAREN && !precededBySemicolon && _lookahead.newLinesBetween == 0) {
argumentList = parseArgumentList();
} else if (typeArgumentList != null) {
// type args but no args
throw new ParserError(ErrorMessages.create("unexpectedToken", "("), spanLookahead);
}
var lastSpan = argumentList != null ? argumentList.span() : identifier.span();
var res =
new QualifiedAccessExpr(
expr, identifier, isNullable, argumentList, expr.span().endWith(lastSpan));
expr,
identifier,
isNullable,
typeArgumentList,
argumentList,
expr.span().endWith(lastSpan));
return parseExprRest(res);
}
// subscript (needs to be in the same line as the expression)
@@ -1317,7 +1367,7 @@ final class ParserImpl {
var paramList = new ParameterList(params, start.endWith(end));
yield new FunctionLiteralExpr(paramList, expr, start.endWith(expr.span()));
} else {
var exp = new UnqualifiedAccessExpr(identifier, null, identifier.span());
var exp = new UnqualifiedAccessExpr(identifier, null, null, identifier.span());
yield new ParenthesizedExpr(exp, start.endWith(end));
}
}
@@ -1532,9 +1582,9 @@ final class ParserImpl {
return bodies;
}
private TypeParameterList parseTypeParameterList() {
private TypeParameterList parseTypeParameterList(boolean allowVarianceModifiers) {
var start = expect(Token.LT, "unexpectedToken", "<").span;
var pars = parseListOf(Token.COMMA, Token.GT, this::parseTypeParameter);
var pars = parseListOf(Token.COMMA, Token.GT, () -> parseTypeParameter(allowVarianceModifiers));
var end = expect(Token.GT, "unexpectedToken2", ",", ">").span;
return new TypeParameterList(pars, start.endWith(end));
}
@@ -1556,13 +1606,19 @@ final class ParserImpl {
return new ArgumentList(exprs, start.endWith(end));
}
private TypeParameter parseTypeParameter() {
private TypeParameter parseTypeParameter(boolean allowVarianceModifier) {
TypeParameter.Variance variance = null;
var start = spanLookahead;
if (lookahead == Token.IN) {
if (!allowVarianceModifier) {
throw parserError("typeParameterVarianceNotAllowed");
}
next();
variance = TypeParameter.Variance.IN;
} else if (lookahead == Token.OUT) {
if (!allowVarianceModifier) {
throw parserError("typeParameterVarianceNotAllowed");
}
next();
variance = TypeParameter.Variance.OUT;
}
@@ -89,6 +89,7 @@ public enum Token {
NOT,
QUESTION,
COLON,
DCOLON,
ARROW,
EQUAL,
NOT_EQUAL,
@@ -20,7 +20,7 @@ import org.jspecify.annotations.Nullable;
import org.pkl.parser.ParserVisitor;
import org.pkl.parser.Span;
public final class ClassMethod extends AbstractNode {
public final class ClassMethod extends AbstractNode implements Method {
private final int modifiersOffset;
private final int nameOffset;
private final Span headerSpan;
@@ -291,8 +291,13 @@ public abstract sealed class Expr extends AbstractNode {
public static final class UnqualifiedAccessExpr extends Expr {
public UnqualifiedAccessExpr(
Identifier identifier, @Nullable ArgumentList argumentList, Span span) {
super(span, Arrays.asList(identifier, argumentList));
Identifier identifier,
@Nullable TypeArgumentList typeArgumentList,
@Nullable ArgumentList argumentList,
Span span) {
super(span, Arrays.asList(identifier, typeArgumentList, argumentList));
assert typeArgumentList == null || argumentList != null
: "UnqualifiedAccessExpr created with typeArgumentList but no argumentList";
}
@Override
@@ -306,8 +311,12 @@ public abstract sealed class Expr extends AbstractNode {
return ret;
}
public @Nullable TypeArgumentList getTypeArgumentList() {
return (TypeArgumentList) children.get(1);
}
public @Nullable ArgumentList getArgumentList() {
return (ArgumentList) children.get(1);
return (ArgumentList) children.get(2);
}
}
@@ -318,9 +327,12 @@ public abstract sealed class Expr extends AbstractNode {
Expr expr,
Identifier identifier,
boolean isNullable,
@Nullable TypeArgumentList typeArgumentList,
@Nullable ArgumentList argumentList,
Span span) {
super(span, Arrays.asList(expr, identifier, argumentList));
super(span, Arrays.asList(expr, identifier, typeArgumentList, argumentList));
assert typeArgumentList == null || argumentList != null
: "QualifiedAccessExpr created with typeArgumentList but no argumentList";
this.isNullable = isNullable;
}
@@ -345,14 +357,24 @@ public abstract sealed class Expr extends AbstractNode {
return isNullable;
}
public @Nullable TypeArgumentList getTypeArgumentList() {
return (TypeArgumentList) children.get(2);
}
public @Nullable ArgumentList getArgumentList() {
return (ArgumentList) children.get(2);
return (ArgumentList) children.get(3);
}
}
public static final class SuperAccessExpr extends Expr {
public SuperAccessExpr(Identifier identifier, @Nullable ArgumentList argumentList, Span span) {
super(span, Arrays.asList(identifier, argumentList));
public SuperAccessExpr(
Identifier identifier,
@Nullable TypeArgumentList typeArgumentList,
@Nullable ArgumentList argumentList,
Span span) {
super(span, Arrays.asList(identifier, typeArgumentList, argumentList));
assert typeArgumentList == null || argumentList != null
: "SuperAccessExpr created with typeArgumentList but no argumentList";
}
@Override
@@ -366,8 +388,12 @@ public abstract sealed class Expr extends AbstractNode {
return ret;
}
public @Nullable TypeArgumentList getTypeArgumentList() {
return (TypeArgumentList) children.get(1);
}
public @Nullable ArgumentList getArgumentList() {
return (ArgumentList) children.get(1);
return (ArgumentList) children.get(2);
}
}
@@ -0,0 +1,24 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.parser.syntax;
import org.jspecify.annotations.Nullable;
public interface Method {
@Nullable Expr getExpr();
@Nullable TypeAnnotation getTypeAnnotation();
}
@@ -84,7 +84,7 @@ public abstract sealed class ObjectMember extends AbstractNode {
}
}
public static final class ObjectMethod extends ObjectMember {
public static final class ObjectMethod extends ObjectMember implements Method {
private final int identifierOffset;
public ObjectMethod(List<@Nullable Node> nodes, int identifierOffset, Span span) {
@@ -52,6 +52,7 @@ public enum NodeType {
ASSIGN(NodeKind.TERMINAL),
AT(NodeKind.TERMINAL),
COLON(NodeKind.TERMINAL),
DCOLON(NodeKind.TERMINAL),
COMMA(NodeKind.TERMINAL),
DOT(NodeKind.TERMINAL),
GT(NodeKind.TERMINAL),
@@ -108,3 +108,6 @@ Documentation comments must be attached to modules, classes, typealiases, method
wrongShebangPosition=\
Shebangs are only allowed at the very beginning of a file.
typeParameterVarianceNotAllowed=\
Variance modifiers may only be applied to type parameters on classes or typealiases.
@@ -99,6 +99,8 @@ class ParserComparisonTest {
"errors/invalidCharacterEscape.pkl",
"errors/invalidCharacterEscape2.pkl",
"errors/invalidUnicodeEscape.pkl",
"errors/invalidVarianceModifierClassMethod.pkl",
"errors/invalidVarianceModifierObjectMethod.pkl",
"errors/letExpressionError3.pkl",
"errors/unterminatedUnicodeEscape.pkl",
"errors/keywordNotAllowedHere1.pkl",
@@ -507,6 +507,10 @@ class SexpRenderer {
buf.append('\n')
buf.append(tab)
buf.append("(identifier)")
if (expr.typeArgumentList !== null) {
buf.append('\n')
renderTypeArgumentList(expr.typeArgumentList!!)
}
if (expr.argumentList !== null) {
buf.append('\n')
renderArgumentList(expr.argumentList!!)
@@ -522,6 +526,10 @@ class SexpRenderer {
buf.append('\n')
buf.append(tab)
buf.append("(identifier)")
if (expr.typeArgumentList !== null) {
buf.append('\n')
renderTypeArgumentList(expr.typeArgumentList!!)
}
if (expr.argumentList !== null) {
buf.append('\n')
renderArgumentList(expr.argumentList!!)
@@ -549,6 +557,10 @@ class SexpRenderer {
buf.append('\n')
buf.append(tab)
buf.append("(identifier)")
if (expr.typeArgumentList !== null) {
buf.append('\n')
renderTypeArgumentList(expr.typeArgumentList!!)
}
if (expr.argumentList !== null) {
buf.append('\n')
renderArgumentList(expr.argumentList!!)
@@ -958,6 +970,7 @@ class SexpRenderer {
buf.append("(identifier)")
val tparList = method.typeParameterList
if (tparList !== null) {
buf.append('\n')
renderTypeParameterList(tparList)
}
buf.append('\n')