From a9b7fe3a330f99c2313e76dbee21c58a8554057d Mon Sep 17 00:00:00 2001 From: Daniel Chao Date: Tue, 3 Nov 2026 05:09:40 -0800 Subject: [PATCH] Switch type argument syntax (#1940) This changes type argument syntax to "standard" style, and away from turbofish. The motivation here is that turbofish-style type arguments are hard to read, and developers who are not already deeply knowledgeable about Pkl might struggle to understand such code. In most languages, the grammar for "method call with type args" don't introduce any additional symbols between the identifier and the type argument list (e.g. C++, TypeScript, C#, Scala, Go). --- bench/bench.gradle.kts | 18 +- .../language-reference/pages/index.adoc | 14 +- .../org/pkl/core/ast/MemberLookupMode.java | 11 +- .../generator/GeneratorMemberNode.java | 11 +- .../literal/SpecializedObjectLiteralNode.java | 9 +- .../member/InvokeMethodVirtualNode.java | 14 +- .../member/InvokeSuperMethodNode.java | 10 +- .../expression/member/ReadPropertyNode.java | 20 +- .../org/pkl/core/ast/member/FunctionNode.java | 2 +- .../java/org/pkl/core/ast/type/TypeNode.java | 2 +- .../core/runtime/MemberLookupSuggestions.java | 120 +++++--- .../pkl/core/runtime/VmExceptionBuilder.java | 41 ++- .../org/pkl/core/util/StringSimilarity.java | 2 +- .../org/pkl/core/errorMessages.properties | 8 + .../input/api/benchmarkModule.pkl | 4 +- .../input/api/reference.pkl | 18 +- .../api/referenceWithFinalModuleType.pkl | 2 +- .../input/api/referenceWithOpenModuleType.pkl | 2 +- .../input/api/referenceWithThisType.pkl | 4 +- .../input/basic/reference.pkl | 6 +- .../functionNotFoundMaybeComparison.pkl | 9 + .../functionNotFoundMaybeComparison2.pkl | 9 + .../functionNotFoundMaybeComparison3.pkl | 11 + .../functionNotFoundMaybeComparison4.pkl | 10 + .../functionNotFoundMaybeComparison5.pkl | 19 ++ .../input/errors/reference1.pkl | 2 +- .../input/errors/reference13.pkl | 2 +- .../input/errors/reference14.pkl | 2 +- .../input/errors/reference15.pkl | 2 +- .../input/errors/reference16.pkl | 2 +- .../input/errors/reference17.pkl | 2 +- .../input/errors/reference18.pkl | 2 +- .../input/errors/reference19.pkl | 2 +- .../input/errors/reference2.pkl | 2 +- .../input/errors/reference20.pkl | 2 +- .../input/errors/reference24.pkl | 2 +- .../input/errors/reference25.pkl | 2 +- .../input/errors/reference26.pkl | 2 +- .../input/errors/reference27.pkl | 2 +- .../input/errors/reference3.pkl | 2 +- .../input/errors/reference4.pkl | 2 +- .../input/errors/reference5.pkl | 2 +- .../input/errors/reference6.pkl | 2 +- .../input/errors/reference7.pkl | 2 +- .../input/methods/callWithTypeArguments.pkl | 58 ++-- .../callWithTypeArgumentsConstrainedType.pkl | 28 +- .../methods/callWithTypeArgumentsLexical.pkl | 32 +-- .../callWithTypeArgumentsNestedWithOffset.pkl | 4 +- .../methods/callWithTypeArgumentsSelfType.pkl | 14 +- .../callWithTypeArgumentsStacktrace.pkl | 4 +- .../callWithTypeArgumentsStacktrace2.pkl | 2 +- .../functionNotFoundMaybeComparison.err | 23 ++ .../functionNotFoundMaybeComparison2.err | 23 ++ .../functionNotFoundMaybeComparison3.err | 23 ++ .../functionNotFoundMaybeComparison4.err | 19 ++ .../functionNotFoundMaybeComparison5.err | 23 ++ .../output/errors/reference1.err | 4 +- .../output/errors/reference13.err | 4 +- .../output/errors/reference14.err | 4 +- .../output/errors/reference15.err | 4 +- .../output/errors/reference16.err | 4 +- .../output/errors/reference17.err | 4 +- .../output/errors/reference18.err | 4 +- .../output/errors/reference19.err | 4 +- .../output/errors/reference2.err | 4 +- .../output/errors/reference3.err | 4 +- .../output/errors/reference4.err | 4 +- .../output/errors/reference5.err | 4 +- .../output/errors/reference6.err | 4 +- .../output/errors/reference7.err | 4 +- .../callWithTypeArgumentsStacktrace.err | 16 +- .../callWithTypeArgumentsStacktrace2.err | 4 +- .../test/kotlin/org/pkl/core/EvaluatorTest.kt | 2 +- .../main/java/org/pkl/formatter/Builder.java | 4 +- .../input/method-call-type-args.pkl | 24 +- .../output/method-call-type-args.pkl | 30 +- .../main/java/org/pkl/parser/Constants.java | 28 ++ .../java/org/pkl/parser/GenericParser.java | 13 +- .../org/pkl/parser/GenericParserImpl.java | 82 ++++-- .../src/main/java/org/pkl/parser/Lexer.java | 70 ++++- .../pkl/parser/MaxBacktrackCountError.java | 51 ++++ .../src/main/java/org/pkl/parser/Parser.java | 12 +- .../main/java/org/pkl/parser/ParserImpl.java | 116 +++++--- .../src/main/java/org/pkl/parser/Token.java | 1 - .../parser/TypeArgumentCandidatesScanner.java | 117 ++++++++ .../pkl/parser/syntax/generic/NodeType.java | 1 - .../java/org/pkl/parser/util/IntArray.java | 61 ++++ .../org/pkl/parser/util/SortedIntArray.java | 41 +++ .../org/pkl/parser/errorMessages.properties | 3 + .../org/pkl/parser/GenericParserTest.kt | 47 ++++ .../org/pkl/parser/ParserComparisonTest.kt | 62 ++--- .../test/kotlin/org/pkl/parser/ParserTest.kt | 48 ++++ .../TypeArgumentCandidatesScannerTest.kt | 262 ++++++++++++++++++ .../org/pkl/parser/large_backtracking.pkl.txt | 1 + stdlib/ref.pkl | 4 +- stdlib/syntax.pkl | 1 - 96 files changed, 1448 insertions(+), 376 deletions(-) create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison2.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison3.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison4.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison5.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison2.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison3.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison4.err create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison5.err create mode 100644 pkl-parser/src/main/java/org/pkl/parser/Constants.java create mode 100644 pkl-parser/src/main/java/org/pkl/parser/MaxBacktrackCountError.java create mode 100644 pkl-parser/src/main/java/org/pkl/parser/TypeArgumentCandidatesScanner.java create mode 100644 pkl-parser/src/main/java/org/pkl/parser/util/IntArray.java create mode 100644 pkl-parser/src/main/java/org/pkl/parser/util/SortedIntArray.java create mode 100644 pkl-parser/src/test/kotlin/org/pkl/parser/GenericParserTest.kt create mode 100644 pkl-parser/src/test/kotlin/org/pkl/parser/ParserTest.kt create mode 100644 pkl-parser/src/test/kotlin/org/pkl/parser/TypeArgumentCandidatesScannerTest.kt create mode 100644 pkl-parser/src/test/resources/org/pkl/parser/large_backtracking.pkl.txt diff --git a/bench/bench.gradle.kts b/bench/bench.gradle.kts index a4d68e9f6..d23feb875 100644 --- a/bench/bench.gradle.kts +++ b/bench/bench.gradle.kts @@ -31,13 +31,17 @@ dependencies { } jmh { - // include = ["fib_class_java"] - // include = ["fib_class_constrained1", "fib_class_constrained2"] + // includes = listOf("ParserBenchmark") jmhVersion.set(libs.versions.jmh) // jvmArgsAppend = "-Dgraal.TruffleCompilationExceptionsAreFatal=true " + // "-Dgraal.Dump=Truffle,TruffleTree -Dgraal.TraceTruffleCompilation=true " + // "-Dgraal.TruffleFunctionInlining=false" - jvm.set("${buildInfo.graalVmAmd64.baseDir}/bin/java") + val graalVm = + when (buildInfo.arch) { + Target.Arch.AARCH64 -> buildInfo.graalVmAarch64 + Target.Arch.AMD64 -> buildInfo.graalVmAmd64 + } + jvm.set("${graalVm.baseDir}/bin/java") // see: // https://docs.oracle.com/en/graalvm/enterprise/20/docs/graalvm-as-a-platform/implement-language/#disable-class-path-separation jvmArgs.set( @@ -51,7 +55,13 @@ jmh { // synchronizeIterations = false } -tasks.named("jmh") { dependsOn(":installGraalVmAmd64") } +tasks.named("jmh") { + if (buildInfo.arch == Target.Arch.AARCH64) { + dependsOn(":installGraalVmAarch64") + } else if (buildInfo.arch == Target.Arch.AMD64) { + dependsOn(":installGraalVmAmd64") + } +} // Prevent this error which occurs when building in IntelliJ: // "Entry org/pkl/core/fib_class_typed.pkl is a duplicate but no duplicate handling strategy has diff --git a/docs/modules/language-reference/pages/index.adoc b/docs/modules/language-reference/pages/index.adoc index e166fd07d..5edf09fea 100644 --- a/docs/modules/language-reference/pages/index.adoc +++ b/docs/modules/language-reference/pages/index.adoc @@ -6022,7 +6022,7 @@ class Domain extends ref.AbstractDomain { // <1> typealias Ref = ref.Reference // <2> -const function Ref(data: Any): Ref = ref.Reference::(new Domain {}, data) // <3> +const function Ref(data: Any): Ref = ref.Reference(new Domain {}, data) // <3> abstract class Task { // <4> name: String @@ -6032,7 +6032,7 @@ class TaskA extends Task { input1: String | Ref // <5> input2: Int | Ref - hidden fixed outputs: Ref = Ref::(this) // <6> + hidden fixed outputs: Ref = Ref(this) // <6> } abstract class TaskAOutputs { // <7> @@ -6113,7 +6113,7 @@ tasks { Methods may be declared as "generic", with types that are themselves parameters. Method type parameters may be used within the method anywhere else a type may be used: in a parameter's type, in the return type, or within the method body (e.g. <>, <>, <>). -When calling a generic method, a caller may manually specify type arguments by writing `methodName::(arg1, arg2)`. +When calling a generic method, a caller may manually specify type arguments by writing `methodName(arg1, arg2)`. When no type arguments are provided, type checks of parameter types behave like <> and always succeed. [source,pkl] @@ -6132,11 +6132,11 @@ function makeFlock(names: Listing): Listing = new { } } -pigeonFlock = makeFlock::(new { "Alice"; "Bill"; "Ruth" }) // <1> -hawkFlock = makeFlock::(new { "Florence"; "Holly"; "Jim" }) // <2> +pigeonFlock = makeFlock(new { "Alice"; "Bill"; "Ruth" }) // <1> +hawkFlock = makeFlock(new { "Florence"; "Holly"; "Jim" }) // <2> ---- <1> Result: a `Listing` where every element is a `Pigeon`. <2> Result: a `Listing` where every element is a `Hawk`. -NOTE: The `::<>` syntax is colloquially known as the "turbofish", after https://doc.rust-lang.org/reference/glossary.html#turbofish[the same syntax in Rust]. -Pkl requires this syntax for the same reason as Rust: to avoid parser ambiguity. +NOTE: A `<` following an identifier is parsed as the start of a type argument list when it is well-formed and directly followed by `(`. +Otherwise, it is parsed as the less-than operator; for example, `foo(baz)` is a method call with type arguments, whereas `foo(baz)` is a comparison. diff --git a/pkl-core/src/main/java/org/pkl/core/ast/MemberLookupMode.java b/pkl-core/src/main/java/org/pkl/core/ast/MemberLookupMode.java index 5d9e25743..5c5b44f76 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/MemberLookupMode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/MemberLookupMode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-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. @@ -29,5 +29,12 @@ public enum MemberLookupMode { IMPLICIT_THIS, /** Member lookup with explicit receiver (e.g., {@code foo.bar}). */ - EXPLICIT_RECEIVER + EXPLICIT_RECEIVER, + + /** Member lookup with explicit {@code super.}. */ + EXPLICIT_SUPER; + + public boolean isImplicitReceiver() { + return this != EXPLICIT_RECEIVER && this != EXPLICIT_SUPER; + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorMemberNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorMemberNode.java index ad3698d95..16eb5c04c 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorMemberNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorMemberNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-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. @@ -19,6 +19,7 @@ import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.dsl.Idempotent; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.MemberLookupMode; import org.pkl.core.ast.PklNode; import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.runtime.Identifier; @@ -63,7 +64,13 @@ public abstract class GeneratorMemberNode extends PklNode { if (classProperty == null) { var exception = exceptionBuilder() - .cannotFindProperty(clazz.getPrototype(), memberName, false, false) + .cannotFindProperty( + clazz.getPrototype(), + clazz.getPrototype(), + memberName, + false, + MemberLookupMode.EXPLICIT_RECEIVER, + null) .build(); if (member.getHeaderSection().isAvailable()) { exception diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java index b3cb4e933..fa01f51de 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java @@ -29,6 +29,7 @@ import org.graalvm.collections.EconomicMap; import org.graalvm.collections.UnmodifiableEconomicMap; import org.jspecify.annotations.Nullable; import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.MemberLookupMode; import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.type.UnresolvedTypeNode; import org.pkl.core.runtime.*; @@ -80,7 +81,13 @@ public abstract class SpecializedObjectLiteralNode extends ObjectLiteralNode { var memberName = member.getName(); if (!parentClass.hasProperty(memberName)) { throw exceptionBuilder() - .cannotFindProperty(parentClass.getPrototype(), memberName, false, false) + .cannotFindProperty( + parentClass.getPrototype(), + parentClass.getPrototype(), + memberName, + false, + MemberLookupMode.EXPLICIT_RECEIVER, + null) .withSourceSection(member.getHeaderSection()) .build(); } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodVirtualNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodVirtualNode.java index 718e2f96d..b0006ecb2 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodVirtualNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodVirtualNode.java @@ -37,6 +37,7 @@ import org.pkl.core.ast.type.UnresolvedTypeNode; import org.pkl.core.runtime.Identifier; import org.pkl.core.runtime.VmClass; import org.pkl.core.runtime.VmFunction; +import org.pkl.core.runtime.VmUtils; /** A virtual method call. */ @SuppressWarnings("DuplicatedCode") @@ -89,6 +90,8 @@ public abstract class InvokeMethodVirtualNode extends AbstractInvokeMethodNode { */ public abstract Object executeWith(VirtualFrame frame, Object value, VmClass clazz); + protected abstract ExpressionNode getReceiverNode(); + /** Intrinsifies `FunctionN.apply()` calls. */ @Specialization(guards = {"methodName == APPLY", "receiver.getCallTarget() == cachedCallTarget"}) protected Object evalFunctionCached( @@ -119,7 +122,7 @@ public abstract class InvokeMethodVirtualNode extends AbstractInvokeMethodNode { Object receiver, @SuppressWarnings("unused") VmClass receiverClass, @Cached("receiverClass") @SuppressWarnings("unused") VmClass cachedReceiverClass, - @Cached("resolveMethod(receiverClass)") ClassMethod method, + @Cached("resolveMethod(frame, receiverClass)") ClassMethod method, @Cached("create(method.getCallTarget(sourceSection))") DirectCallNode callNode) { var args = evalArgs(frame, method, method.getOwner(), receiver); return callNode.call(args); @@ -131,7 +134,7 @@ public abstract class InvokeMethodVirtualNode extends AbstractInvokeMethodNode { Object receiver, VmClass receiverClass, @Exclusive @Cached("create()") IndirectCallNode callNode) { - var method = resolveMethod(receiverClass); + var method = resolveMethod(frame, receiverClass); var args = evalArgs(frame, method, method.getOwner(), receiver); // Deprecation should not report here (getCallTarget(sourceSection)), as this happens for each @@ -139,7 +142,7 @@ public abstract class InvokeMethodVirtualNode extends AbstractInvokeMethodNode { return callNode.call(method.getCallTarget(), args); } - protected ClassMethod resolveMethod(VmClass receiverClass) { + protected ClassMethod resolveMethod(VirtualFrame frame, VmClass receiverClass) { var method = receiverClass.getMethod(methodName); if (method != null) { checkConst(method); @@ -151,9 +154,12 @@ public abstract class InvokeMethodVirtualNode extends AbstractInvokeMethodNode { throw exceptionBuilder() .cannotFindMethod( receiverClass.getPrototype(), + VmUtils.getOwner(frame), methodName, argumentNodes.length, - lookupMode != MemberLookupMode.EXPLICIT_RECEIVER) + lookupMode, + unresolvedTypeArgumentNodes != null, + getReceiverNode()) .build(); } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeSuperMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeSuperMethodNode.java index c70f4cbfb..2826661b2 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeSuperMethodNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeSuperMethodNode.java @@ -23,6 +23,7 @@ import com.oracle.truffle.api.nodes.DirectCallNode; import com.oracle.truffle.api.source.SourceSection; import org.jspecify.annotations.Nullable; import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.MemberLookupMode; import org.pkl.core.ast.member.ClassMethod; import org.pkl.core.ast.type.UnresolvedTypeNode; import org.pkl.core.runtime.Identifier; @@ -84,7 +85,14 @@ public abstract class InvokeSuperMethodNode extends AbstractInvokeMethodNode { var parent = owner.getParent(); assert parent != null; throw exceptionBuilder() - .cannotFindMethod(parent, methodName, argumentNodes.length, false) + .cannotFindMethod( + parent, + VmUtils.getOwner(frame), + methodName, + argumentNodes.length, + MemberLookupMode.EXPLICIT_SUPER, + unresolvedTypeArgumentNodes != null, + null) .build(); } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java index 42138dd00..e8aaaa4c8 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java @@ -19,6 +19,7 @@ import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.*; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.DirectCallNode; import com.oracle.truffle.api.nodes.IndirectCallNode; import com.oracle.truffle.api.nodes.NodeInfo; @@ -32,7 +33,7 @@ import org.pkl.core.runtime.VmReference.VmReferenceAccessError; import org.pkl.core.util.ErrorMessages; @NodeInfo(shortName = ".") -@ImportStatic(BaseModule.class) +@ImportStatic({BaseModule.class, VmUtils.class}) @NodeChild(value = "receiverNode", type = ExpressionNode.class) public abstract class ReadPropertyNode extends ExpressionNode { protected final Identifier propertyName; @@ -63,6 +64,8 @@ public abstract class ReadPropertyNode extends ExpressionNode { this(sourceSection, propertyName, MemberLookupMode.EXPLICIT_RECEIVER, false); } + protected abstract ExpressionNode getReceiverNode(); + protected String getReferenceErrorHint(VmReference reference, VmReferenceAccessError err) { var myType = reference.getReferentType(); return switch (err.getErrorType()) { @@ -102,6 +105,7 @@ public abstract class ReadPropertyNode extends ExpressionNode { // https://www.graalvm.org/22.0/graalvm-as-a-platform/language-implementation-framework/TruffleLibraries/#strategy-2-java-interfaces @Specialization(guards = "receiver.getClass() == cachedClass", limit = "99") protected Object evalObject( + VirtualFrame frame, Object receiver, @Cached("getVmObjectSubclassOrNull(receiver)") Class cachedClass, @Cached("create()") IndirectCallNode callNode) { @@ -112,15 +116,16 @@ public abstract class ReadPropertyNode extends ExpressionNode { if (result != null) return result; CompilerDirectives.transferToInterpreter(); - throw cannotFindProperty(object); + throw cannotFindProperty(VmUtils.getOwner(frame), object); } // specializations for all other types @Specialization(guards = "receiver.getClass() == cachedClass", limit = "99") protected Object evalOther( + VirtualFrame frame, Object receiver, @Cached("receiver.getClass()") @SuppressWarnings("unused") Class cachedClass, - @Cached("resolveProperty(receiver)") ClassProperty resolvedProperty, + @Cached("resolveProperty(getOwner(frame), receiver)") ClassProperty resolvedProperty, @Cached("createCallNode(resolvedProperty)") DirectCallNode callNode) { return callNode.call(receiver, resolvedProperty.getOwner(), resolvedProperty.getName()); @@ -131,13 +136,13 @@ public abstract class ReadPropertyNode extends ExpressionNode { return value instanceof VmObjectLike objectLike ? objectLike.getClass() : null; } - protected ClassProperty resolveProperty(Object value) { + protected ClassProperty resolveProperty(VmObjectLike owner, Object value) { var clazz = VmUtils.getClass(value); var propertyDef = clazz.getProperty(propertyName); if (propertyDef != null) return propertyDef; CompilerDirectives.transferToInterpreter(); - throw cannotFindProperty(clazz.getPrototype()); + throw cannotFindProperty(owner, clazz.getPrototype()); } // This method should only be used for standard library properties implemented as nodes. @@ -147,10 +152,9 @@ public abstract class ReadPropertyNode extends ExpressionNode { } @TruffleBoundary - private VmException cannotFindProperty(VmObjectLike receiver) { + private VmException cannotFindProperty(VmObjectLike owner, VmObjectLike receiver) { return exceptionBuilder() - .cannotFindProperty( - receiver, propertyName, true, lookupMode != MemberLookupMode.EXPLICIT_RECEIVER) + .cannotFindProperty(receiver, owner, propertyName, true, lookupMode, getReceiverNode()) .build(); } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/FunctionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/FunctionNode.java index ab0e64e80..006ee846c 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/FunctionNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/FunctionNode.java @@ -152,7 +152,7 @@ public final class FunctionNode extends RegularMemberNode { // this behavior needs to be overridden so the type arg check on return type still runs. // otherwise, this code would succeed instead of failing: // ``` - // res = List("foo", "hello", "a").map::((it) -> it.length) + // res = List("foo", "hello", "a").map((it) -> it.length) // ``` // Assumption: this does not need to account for type args in enclosing frames because // no type parameters exist in the stdlib that would require this. diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java index 341e61039..addf2b030 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java @@ -2300,7 +2300,7 @@ public abstract class TypeNode extends PklNode { // e.g. // ``` // function foo(arg: T) - // res = foo::(new {}) // `Person` is passed in via `typeArgumentOverrides` + // res = foo(new {}) // `Person` is passed in via `typeArgumentOverrides` // ``` if (levelsUp == 0 && isPreCall) { return typeArgumentOverrides == null ? null : typeArgumentOverrides[typeParameter.index()]; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/MemberLookupSuggestions.java b/pkl-core/src/main/java/org/pkl/core/runtime/MemberLookupSuggestions.java index db67f5d77..a62c16954 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/MemberLookupSuggestions.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/MemberLookupSuggestions.java @@ -18,6 +18,8 @@ package org.pkl.core.runtime; import java.util.*; import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; +import org.pkl.core.ast.MemberLookupMode; +import org.pkl.core.ast.PklNode; import org.pkl.core.ast.member.ClassMethod; import org.pkl.core.ast.member.Member; import org.pkl.core.runtime.MemberLookupSuggestions.Candidate.Kind; @@ -30,7 +32,8 @@ public final class MemberLookupSuggestions { // names that differ in their first character similar private static final double SIMILARITY_THRESHOLD = 0.77; - private final VmObjectLike composite; + private final VmObjectLike receiver; + private final VmObjectLike owner; private final Object memberName; // -1 for property, #arguments for function private final int memberArity; @@ -39,8 +42,13 @@ public final class MemberLookupSuggestions { private final Set candidates = new LinkedHashSet<>(); public MemberLookupSuggestions( - VmObjectLike composite, Object memberName, int memberArity, Set memberKinds) { - this.composite = composite; + VmObjectLike receiver, + VmObjectLike owner, + Object memberName, + int memberArity, + Set memberKinds) { + this.receiver = receiver; + this.owner = owner; this.memberName = memberName; this.memberArity = memberArity; this.memberKinds = memberKinds; @@ -49,45 +57,76 @@ public final class MemberLookupSuggestions { // use same search order as in member lookup, // so that in case of members with same name, // (only) the correct one is suggested - public List find(boolean isImplicitReceiver) { + public List find(MemberLookupMode memberLookupMode, @Nullable PklNode receiverNode) { candidates.clear(); - - if (isImplicitReceiver) { - for (var curr = composite; curr != null; curr = curr.getEnclosingOwner()) { - addPropertyCandidates(curr, true); + if (memberLookupMode.isImplicitReceiver()) { + for (var curr = receiver; curr != null; curr = curr.getEnclosingOwner()) { + addPropertyCandidates(curr, true, memberLookupMode, receiverNode); if (curr.isPrototype()) { - addMethodCandidates(curr.getVmClass().getDeclaredMethods(), true); + addMethodCandidates( + curr.getVmClass().getDeclaredMethods(), true, memberLookupMode, receiverNode); } } - addPropertyCandidates(BaseModule.getModule(), false); - addMethodCandidates(BaseModule.getModule().getVmClass().getMethods(), false); + // if we resolved to implicit `this` at parse time, try searching the lexical scope of the + // owner too. + for (var curr = owner; curr != null; curr = curr.getEnclosingOwner()) { + addPropertyCandidates(curr, true, memberLookupMode, receiverNode); + if (curr.isPrototype()) { + addMethodCandidates( + curr.getVmClass().getDeclaredMethods(), true, memberLookupMode, receiverNode); + } + } + addPropertyCandidates(BaseModule.getModule(), false, memberLookupMode, receiverNode); + addMethodCandidates( + BaseModule.getModule().getVmClass().getMethods(), false, memberLookupMode, receiverNode); } - for (var curr = composite; curr != null; curr = curr.getParent()) { - addPropertyCandidates(curr, false); + for (var curr = receiver; curr != null; curr = curr.getParent()) { + addPropertyCandidates(curr, false, memberLookupMode, receiverNode); } - addMethodCandidates(composite.getVmClass().getMethods(), false); + addMethodCandidates(receiver.getVmClass().getMethods(), false, memberLookupMode, receiverNode); return candidates.stream().sorted(Comparator.naturalOrder()).collect(Collectors.toList()); } - private void addPropertyCandidates(VmObjectLike object, boolean includeLocal) { + private void addPropertyCandidates( + VmObjectLike object, + boolean includeLocal, + MemberLookupMode memberLookupMode, + @Nullable PklNode receiverNode) { if (!memberKinds.contains(Kind.PROPERTY)) return; for (var member : EconomicMaps.getValues(object.getMembers())) { - addIfSimilar(member, Candidate.Kind.PROPERTY, -1, includeLocal); + addIfSimilar( + member, Candidate.Kind.PROPERTY, -1, includeLocal, memberLookupMode, receiverNode); } } - private void addMethodCandidates(Iterable methods, boolean includeLocal) { + private void addMethodCandidates( + Iterable methods, + boolean includeLocal, + MemberLookupMode memberLookupMode, + @Nullable PklNode receiverNode) { if (!memberKinds.contains(Kind.METHOD)) return; for (var method : methods) { - addIfSimilar(method, Candidate.Kind.METHOD, method.getParameterCount(), includeLocal); + addIfSimilar( + method, + Candidate.Kind.METHOD, + method.getParameterCount(), + includeLocal, + memberLookupMode, + receiverNode); } } - private void addIfSimilar(Member member, Candidate.Kind kind, int arity, boolean includeLocal) { + private void addIfSimilar( + Member member, + Kind kind, + int arity, + boolean includeLocal, + MemberLookupMode memberLookupMode, + @Nullable PklNode receiverNode) { var memberName = member.getNameOrNull(); if (memberName == null) return; @@ -99,15 +138,26 @@ public final class MemberLookupSuggestions { var arityDifference = Math.abs(arity - memberArity); var signature = member.getCallSignature(); assert signature != null; + var nameWithReceiver = memberName.toString(); + // `receiverNode` is not necessarily null; we use `MemberLookupMode.EXPLICIT_RECEIVER` + // in places that are not property/method calls, e.g. + // `org.pkl.core.ast.expression.literal.SpecializedObjectLiteralNode.checkIsValidTypedAmendment` + if (memberLookupMode == MemberLookupMode.EXPLICIT_RECEIVER && receiverNode != null) { + nameWithReceiver = + receiverNode.getSourceSection().getCharacters().toString() + "." + memberName; + } else if (memberLookupMode == MemberLookupMode.EXPLICIT_SUPER) { + nameWithReceiver = "super." + memberName; + } if (nameSimilarity < 1 || memberArity == 0) { candidates.add( new Candidate( - kind, memberName.toString(), signature, nameSimilarity, arityDifference)); + kind, memberName, nameWithReceiver, signature, nameSimilarity, arityDifference)); } else if (nameSimilarity == 1 && memberArity >= 0) { candidates.add( new Candidate( kind, - memberName.toString(), + memberName, + nameWithReceiver, signature + ".apply(...)", nameSimilarity, arityDifference)); @@ -116,22 +166,14 @@ public final class MemberLookupSuggestions { } } - public static final class Candidate implements Comparable { - private final Kind kind; - private final String name; - private final String callSignature; - private final double nameSimilarity; - private final int arityDifference; - - public Candidate( - Kind kind, String name, String callSignature, double nameSimilarity, int arityDifference) { - this.kind = kind; - this.name = name; - this.callSignature = callSignature; - this.nameSimilarity = nameSimilarity; - this.arityDifference = arityDifference; - } - + public record Candidate( + Kind kind, + Identifier memberName, + String memberNameWithReceiver, + String callSignature, + double nameSimilarity, + int arityDifference) + implements Comparable { // note: not consistent with equals (hence cannot use TreeSet) @Override public int compareTo(Candidate other) { @@ -146,12 +188,12 @@ public final class MemberLookupSuggestions { if (this == obj) return true; if (!(obj instanceof Candidate other)) return false; // member lookup is name rather than signature based (but distinguishes kind) - return kind == other.kind && name.equals(other.name); + return kind == other.kind && memberNameWithReceiver.equals(other.memberNameWithReceiver); } @Override public int hashCode() { - return kind.hashCode() * 31 + name.hashCode(); + return kind.hashCode() * 31 + memberNameWithReceiver.hashCode(); } @Override diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java index cd0fc298b..ac5cdd898 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java @@ -24,6 +24,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; import org.pkl.core.StackFrame; +import org.pkl.core.ast.MemberLookupMode; +import org.pkl.core.ast.PklNode; import org.pkl.core.runtime.MemberLookupSuggestions.Candidate.Kind; import org.pkl.core.runtime.VmException.ProgramValue; import org.pkl.core.util.AnsiStringBuilder; @@ -137,7 +139,8 @@ public final class VmExceptionBuilder { public VmExceptionBuilder cannotFindMember(VmObjectLike receiver, Object memberKey) { if (memberKey instanceof Identifier identifier) { - return cannotFindProperty(receiver, identifier, true, false); + return cannotFindProperty( + receiver, receiver, identifier, true, MemberLookupMode.EXPLICIT_RECEIVER, null); } if (memberKey instanceof String string) { var candidates = KeyLookupSuggestions.forObject(receiver, string); @@ -153,19 +156,21 @@ public final class VmExceptionBuilder { public VmExceptionBuilder cannotFindProperty( VmObjectLike receiver, + VmObjectLike owner, Identifier propertyName, // true -> property read, false -> property definition boolean isRead, - boolean isImplicitReceiver) { + MemberLookupMode memberLookupMode, + @Nullable PklNode receiverNode) { var memberKinds = isRead ? EnumSet.allOf(Kind.class) : EnumSet.of(Kind.PROPERTY); var candidates = - new MemberLookupSuggestions(receiver, propertyName, -1, memberKinds) - .find(isImplicitReceiver); + new MemberLookupSuggestions(receiver, owner, propertyName, -1, memberKinds) + .find(memberLookupMode, receiverNode); if (candidates.isEmpty()) { - if (isImplicitReceiver) { + if (memberLookupMode.isImplicitReceiver()) { // probably wouldn't be useful to list all members in scope (includes all pkl.base members) evalError("cannotFindPropertyInScope", propertyName); } else if (receiver.isModuleObject()) { @@ -183,7 +188,7 @@ public final class VmExceptionBuilder { propertyNames.isEmpty() ? "(none)" : new MultilineValue(propertyNames)); } } else { - if (isImplicitReceiver) { + if (memberLookupMode.isImplicitReceiver()) { evalError( "cannotFindPropertyInScopeListCandidates", propertyName, @@ -208,14 +213,20 @@ public final class VmExceptionBuilder { // `foo.bar()` public VmExceptionBuilder cannotFindMethod( - VmObjectLike receiver, Identifier methodName, int arity, boolean isImplicitReceiver) { + VmObjectLike receiver, + VmObjectLike owner, + Identifier methodName, + int arity, + MemberLookupMode memberLookupMode, + boolean hasTypeArgs, + @Nullable PklNode receiverNode) { var candidates = - new MemberLookupSuggestions(receiver, methodName, arity, EnumSet.allOf(Kind.class)) - .find(isImplicitReceiver); + new MemberLookupSuggestions(receiver, owner, methodName, arity, EnumSet.allOf(Kind.class)) + .find(memberLookupMode, receiverNode); if (candidates.isEmpty()) { - if (isImplicitReceiver) { + if (memberLookupMode.isImplicitReceiver()) { // probably not useful to list all members in scope (includes all pkl.base members) evalError("cannotFindMethodInScope", methodName); } else if (receiver.isModuleObject()) { @@ -232,7 +243,15 @@ public final class VmExceptionBuilder { new MultilineValue(collectMethodNames(receiver.getVmClass()))); } } else { - if (isImplicitReceiver) { + var exactPropertyMatch = + candidates.stream() + .filter((it) -> it.nameSimilarity() == 1 && it.kind() == Kind.PROPERTY) + .findFirst(); + if (hasTypeArgs && exactPropertyMatch.isPresent()) { + var candidate = exactPropertyMatch.get(); + evalError( + "cannotFindMethodMaybeComparisonExpr", methodName, candidate.memberNameWithReceiver()); + } else if (memberLookupMode.isImplicitReceiver()) { evalError( "cannotFindMethodInScopeListCandidates", methodName, new MultilineValue(candidates)); } else if (receiver.isModuleObject()) { diff --git a/pkl-core/src/main/java/org/pkl/core/util/StringSimilarity.java b/pkl-core/src/main/java/org/pkl/core/util/StringSimilarity.java index 3b7350740..ef1f8c5ef 100644 --- a/pkl-core/src/main/java/org/pkl/core/util/StringSimilarity.java +++ b/pkl-core/src/main/java/org/pkl/core/util/StringSimilarity.java @@ -21,7 +21,7 @@ import java.util.Arrays; /** * The Jaro–Winkler distance metric is designed and best suited for short strings such as person * names, and to detect typos; it is (roughly) a variation of Damerau-Levenshtein, where the - * substitution of 2 close characters is considered less important then the substitution of 2 + * substitution of 2 close characters is considered less important than the substitution of 2 * characters that a far from each other. Jaro-Winkler was developed in the area of record linkage * (duplicate detection) (Winkler, 1990). It returns a value in the interval [0.0, 1.0]. The * distance is computed as 1 - Jaro-Winkler similarity. diff --git a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties index eda95bd55..4127c5d68 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -419,6 +419,14 @@ Cannot reference property `{0}` because it is defined inside external class `{1} cannotFindMethodInScope=\ Cannot find method `{0}`. +cannotFindMethodMaybeComparisonExpr=\ +Cannot find method `{0}`.\n\ +\n\ +This code is interpreted as a method call with type arguments.\n\ +If you meant to write a comparison expression instead, wrap the left-hand side with parentheses:\n\ +\n\ +({1}) < ... + cannotFindMethodInScopeListCandidates=\ Cannot find method `{0}`.\n\ \n\ diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/benchmarkModule.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/benchmarkModule.pkl index d4a2cf3ce..df310c113 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/api/benchmarkModule.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/benchmarkModule.pkl @@ -30,8 +30,8 @@ facts { isVerbose = false }.run() - run::().iterations == 3 - run::().iterations == 3 + run().iterations == 3 + run().iterations == 3 } ["run output benchmark (in verbose mode)"] { diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/reference.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/reference.pkl index 39ff79611..7056ebd64 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/api/reference.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/reference.pkl @@ -17,11 +17,11 @@ class D extends ref.AbstractDomain { local const d: D = new {} typealias Ref = ref.Reference -const function Ref(data: Any): Ref = ref.Reference::(d, data) +const function Ref(data: Any): Ref = ref.Reference(d, data) abstract class Resource { name: String - hidden fixed $: Ref = Ref::(this) + hidden fixed $: Ref = Ref(this) } class A extends Resource { @@ -123,32 +123,32 @@ kInterpolation = "\(k)" aValuesJoined = k.aValues.join("\n") // ensure that type arguments that are unions are handled correctly -typeArgs = Ref::(null).prop as Ref> +typeArgs = Ref(null).prop as Ref> class TypeHolder { prop: Listing mappingAnyKey: Mapping mappingTypeArgKey: Mapping, Int> } -typeArgs2 = Ref::>(null) as Ref> +typeArgs2 = Ref>(null) as Ref> class BadResource extends Resource { // return a reference with referent that isn't `this` - fixed $ = Ref::(this) + fixed $ = Ref(this) } badThisRef = test.catch(() -> new BadResource { name = "bad" }.$.toString()) // test that a bare function type toStrings/exports okay -bareFunctionType = test.catch(() -> ref.Reference::(d, null) as Ref) +bareFunctionType = test.catch(() -> ref.Reference(d, null) as Ref) // test for allowing constraints during reference construction -constraintInConstructor = Ref:: 5)>(null) +constraintInConstructor = Ref 5)>(null) -mappingAnyKey = Ref::(null).mappingAnyKey["foo"] +mappingAnyKey = Ref(null).mappingAnyKey["foo"] // this one should be an editor error but can't be caught in the runtime // because VmListing/VmMapping don't track their type args -mappingTypeArgKey = Ref ::(null).mappingTypeArgKey[new Listing { "foo" }] +mappingTypeArgKey = Ref(null).mappingTypeArgKey[new Listing { "foo" }] output { renderer { diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/referenceWithFinalModuleType.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/referenceWithFinalModuleType.pkl index 71efab512..dec2a8a11 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/api/referenceWithFinalModuleType.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/referenceWithFinalModuleType.pkl @@ -14,7 +14,7 @@ hidden children: List typealias R = ref.Reference -res0 = ref.Reference::(new D {}, null) +res0 = ref.Reference(new D {}, null) res1: R = res0.parent res2: R = res0.parent.parent res3: R = res0.children[0] diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/referenceWithOpenModuleType.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/referenceWithOpenModuleType.pkl index 62976ce5d..b578c3088 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/api/referenceWithOpenModuleType.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/referenceWithOpenModuleType.pkl @@ -16,7 +16,7 @@ hidden children: List typealias R = ref.Reference -res0 = ref.Reference::(new D {}, null) +res0 = ref.Reference(new D {}, null) res1: R = res0.parent res2: R = res0.parent.parent res3: R = res0.children[0] diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/referenceWithThisType.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/referenceWithThisType.pkl index 868f011c7..dd437cff9 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/api/referenceWithThisType.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/referenceWithThisType.pkl @@ -22,14 +22,14 @@ open class Node2 { typealias R = ref.Reference typealias R2 = ref.Reference -res0: R = ref.Reference::(new D {}, null) +res0: R = ref.Reference(new D {}, null) res1: R = res0.parent res2: R = res0.parent.parent res3: R = res0.children[0] res4: R = res0.children[0].parent res5: R = res0.children[0].children[0] -res0a: R2 = ref.Reference::(new D {}, null) +res0a: R2 = ref.Reference(new D {}, null) res1a: R2 = res0a.parent res2a: R2 = res0a.parent.parent res3a: R2 = res0a.children[0] diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/reference.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/reference.pkl index 992d56ad4..24d02811c 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/reference.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/reference.pkl @@ -25,9 +25,9 @@ local class OtherHolder { constrainedType: String(isEmpty) } -local holderRef: Ref = ref.Reference::(d, null) -local holderRef2: Ref = ref.Reference::(d, null) -local otherHolderRef: Ref = ref.Reference::(d, null) +local holderRef: Ref = ref.Reference(d, null) +local holderRef2: Ref = ref.Reference(d, null) +local otherHolderRef: Ref = ref.Reference(d, null) facts { ["equality"] { diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison.pkl new file mode 100644 index 000000000..ca5a7aabd --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison.pkl @@ -0,0 +1,9 @@ +abstract class Base { + prop = 10 +} + +class Child extends Base { + result = prop < foo > (1) +} + +result = new Child {}.result diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison2.pkl new file mode 100644 index 000000000..007801c98 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison2.pkl @@ -0,0 +1,9 @@ +abstract class Base { + prop = 10 +} + +class Child extends Base { + result = super.prop < foo > (1) +} + +result = new Child {}.result diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison3.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison3.pkl new file mode 100644 index 000000000..09615dd10 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison3.pkl @@ -0,0 +1,11 @@ +abstract class Base { + prop = 10 +} + +class Child extends Base { + local self = this + + result = self.prop < foo > (1) +} + +result = new Child {}.result diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison4.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison4.pkl new file mode 100644 index 000000000..a6de1071f --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison4.pkl @@ -0,0 +1,10 @@ +// `fooz` and `fool` both fit the string similarity threshold, +// but `fooz` is an exact match. So, still show the specialized error +// message about comparisons instead of generic method call +fooz = true + +fool = false + +res { + fooz < bar > (fool) +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison5.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison5.pkl new file mode 100644 index 000000000..6b2b9fee2 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/functionNotFoundMaybeComparison5.pkl @@ -0,0 +1,19 @@ +fooz = true + +fool = false + +class Foo { + prop1: Int + prop2: Int + prop3: Int + qux: Boolean +} + +res { + // exercise super long receivers + new Foo { + prop1 = 1 + prop2 = 2 + prop3 = 3 + }.qux < bar > (fool) +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference1.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference1.pkl index 5c7ec40be..758869b43 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference1.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference1.pkl @@ -6,4 +6,4 @@ class D extends ref.AbstractDomain { local d: D = new {} typealias Ref = ref.Reference -test = ref.Reference::(d, "") as Ref +test = ref.Reference(d, "") as Ref diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference13.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference13.pkl index b56ccae56..e1f033b49 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference13.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference13.pkl @@ -5,4 +5,4 @@ class D extends ref.AbstractDomain { } local d: D = new {} -test = ref.Reference::(d, "").default +test = ref.Reference(d, "").default diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference14.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference14.pkl index 6e7ee1bd3..c08ccfc9b 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference14.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference14.pkl @@ -5,4 +5,4 @@ class D extends ref.AbstractDomain { } local d: D = new {} -test = ref.Reference::(d, "").default +test = ref.Reference(d, "").default diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference15.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference15.pkl index b639c52c6..3af70c24d 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference15.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference15.pkl @@ -5,4 +5,4 @@ class D extends ref.AbstractDomain { } local d: D = new {} -test = ref.Reference::(d, "").default +test = ref.Reference(d, "").default diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference16.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference16.pkl index 56b98d5e0..dece76b99 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference16.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference16.pkl @@ -7,4 +7,4 @@ class D extends ref.AbstractDomain { } local d: D = new {} -test = ref.Reference::(d, "").output +test = ref.Reference(d, "").output diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference17.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference17.pkl index 104185d84..58f327b0d 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference17.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference17.pkl @@ -7,4 +7,4 @@ class D extends ref.AbstractDomain { } local d: D = new {} -test = ref.Reference::(d, "").output +test = ref.Reference(d, "").output diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference18.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference18.pkl index fea785237..ea9db50e3 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference18.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference18.pkl @@ -9,4 +9,4 @@ class D extends ref.AbstractDomain { } local d: D = new {} -test = ref.Reference::(d, "").output +test = ref.Reference(d, "").output diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference19.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference19.pkl index 0c94fab71..c16aa444a 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference19.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference19.pkl @@ -9,4 +9,4 @@ class D extends ref.AbstractDomain { } local d: D = new {} -test = ref.Reference::(d, "").foo as ref.Reference +test = ref.Reference(d, "").foo as ref.Reference diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference2.pkl index 3831d8083..64c46ce73 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference2.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference2.pkl @@ -6,4 +6,4 @@ class D extends ref.AbstractDomain { local d: D = new {} typealias Ref = ref.Reference -test = ref.Reference::(d, "") as Ref +test = ref.Reference(d, "") as Ref diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference20.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference20.pkl index 2cfc93e6b..62d6f9c76 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference20.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference20.pkl @@ -9,7 +9,7 @@ class D extends ref.AbstractDomain { } local d: D = new {} -local test = ref.Reference::(d, "").foo +local test = ref.Reference(d, "").foo testInterpolation = "test:\(test)" // this tests that the interpolation appears in the output when renderReference throws diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference24.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference24.pkl index 25fa5d958..28d528b1f 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference24.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference24.pkl @@ -8,6 +8,6 @@ class Bird { name: String } -myRef: ref.Reference = ref.Reference::(new D {}, null) +myRef: ref.Reference = ref.Reference(new D {}, null) res = myRef.name diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference25.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference25.pkl index e528f3354..1171b545c 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference25.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference25.pkl @@ -4,6 +4,6 @@ class D extends ref.AbstractDomain { function renderReference(_): String = throw("not implemented") } -local myRef = ref.Reference::>(new D {}, null) +local myRef = ref.Reference>(new D {}, null) res = myRef[0] diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference26.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference26.pkl index b3117c248..c4d920dc1 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference26.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference26.pkl @@ -4,6 +4,6 @@ class D extends ref.AbstractDomain { function renderReference(_): String = throw("not implemented") } -local myRef = ref.Reference::(new D {}, null) +local myRef = ref.Reference(new D {}, null) res = myRef.name diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference27.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference27.pkl index df2c992e4..a8d61494a 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference27.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference27.pkl @@ -4,6 +4,6 @@ class D extends ref.AbstractDomain { function renderReference(_): String = throw("not implemented") } -local myRef = ref.Reference:: Boolean>(new D {}, null) +local myRef = ref.Reference Boolean>(new D {}, null) res = myRef.name diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference3.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference3.pkl index dbbf8150f..d2fa27d7a 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference3.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference3.pkl @@ -6,4 +6,4 @@ class D extends ref.AbstractDomain { local d: D = new {} typealias Ref = ref.Reference -test = ref.Reference::(d, "") as Ref> +test = ref.Reference(d, "") as Ref> diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference4.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference4.pkl index 95ce36cf8..011171b25 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference4.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference4.pkl @@ -6,7 +6,7 @@ class D extends ref.AbstractDomain { local d: D = new {} typealias Ref = ref.Reference -test = ref.Reference::(d, "") as Ref +test = ref.Reference(d, "") as Ref typealias Alias1 = Int | Alias2 typealias Alias2 = Boolean diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference5.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference5.pkl index 31cca0418..9a4ecf4ad 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference5.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference5.pkl @@ -11,4 +11,4 @@ class D2 extends ref.AbstractDomain { } local d2: D2 = new {} -test = ref.Reference::(d2, "") as Ref +test = ref.Reference(d2, "") as Ref diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference6.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference6.pkl index 524800df3..32960badd 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference6.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference6.pkl @@ -10,4 +10,4 @@ class A { b: String } -test = ref.Reference::(d, new A {}).c +test = ref.Reference(d, new A {}).c diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference7.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference7.pkl index 2feb3a5d3..7e4ea0d9e 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference7.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/reference7.pkl @@ -6,4 +6,4 @@ class D extends ref.AbstractDomain { local d: D = new {} typealias Ref = ref.Reference -test = ref.Reference::(d, List())["hi"] +test = ref.Reference(d, List())["hi"] diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArguments.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArguments.pkl index fbd9b6c2e..4190630d8 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArguments.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArguments.pkl @@ -8,14 +8,14 @@ class Domain extends ref.AbstractDomain { } directCall { - pair = Pair::, Set>(List("foo"), Set("bar")) - pairSelf = Pair::(new module {}, new module {}).first.getClass().toString() - pairErr = test.catch(() -> Pair::, Set>(List(0), Set("bar"))) - pairSelfErr = test.catch(() -> Pair::(List(), new module {})) - listMap = List("hi", "foo", "a").map::((it) -> it.length) - listMapErr = test.catch(() -> List("hi", "foo", "a").map::((it) -> it.length)) - wrongTypeArgCount = test.catch(() -> Pair:: ("foo", 10)) - nonGenericMethod = test.catch(() -> Regex:: ("foo")) + pair = Pair, Set>(List("foo"), Set("bar")) + pairSelf = Pair(new module {}, new module {}).first.getClass().toString() + pairErr = test.catch(() -> Pair, Set>(List(0), Set("bar"))) + pairSelfErr = test.catch(() -> Pair(List(), new module {})) + listMap = List("hi", "foo", "a").map((it) -> it.length) + listMapErr = test.catch(() -> List("hi", "foo", "a").map((it) -> it.length)) + wrongTypeArgCount = test.catch(() -> Pair ("foo", 10)) + nonGenericMethod = test.catch(() -> Regex ("foo")) } open class A { @@ -23,12 +23,12 @@ open class A { } class B extends A { - function doIt(arg: List): Boolean = super.doIt::(arg) || super.doIt::>(arg) + function doIt(arg: List): Boolean = super.doIt(arg) || super.doIt>(arg) } superCall { local function doIt(a: A, arg: List): Boolean = - a.doIt::(arg) + a.doIt(arg) // should be true: doIt(new A {}, List("")) @@ -45,16 +45,16 @@ local num = 5 objectMethod { local function foo(): T = "hi" - res = test.catch(() -> foo:: num)>()) + res = test.catch(() -> foo num)>()) } function lazy1(it): Listing = new Listing { it } local function lazy2(it): Listing = new Listing { it } lazyTypeCheck { local function lazy3(it): Listing = new Listing { it } - res1 = test.catch(() -> lazy1::(5)) - res2 = test.catch(() -> lazy2::(5)) - res3 = test.catch(() -> lazy3::(5)) + res1 = test.catch(() -> lazy1(5)) + res2 = test.catch(() -> lazy2(5)) + res3 = test.catch(() -> lazy3(5)) eager { local myListing: Listing = new { "Hello, world!" } @@ -62,7 +62,7 @@ lazyTypeCheck { result = myListing is T } // should be false: - res = myFunc::>().result + res = myFunc>().result } closure { @@ -70,7 +70,7 @@ lazyTypeCheck { local function myFunc() = new { result = myListing as T } - res = test.catch(() -> myFunc::>()) + res = test.catch(() -> myFunc>()) } } @@ -86,13 +86,13 @@ typeDefaults { new { name = val } } local function makeFromParam(val: T): Listing = new { val } - res1 = makeNamed::("bob") + res1 = makeNamed("bob") res1a = res1.getClass().toString() - res2 = makeNamedListing::("bob") + res2 = makeNamedListing("bob") res2a = res2.first.getClass().toString() - res3 = makeFromParam::(new { name = "bob" }) + res3 = makeFromParam(new { name = "bob" }) res3a = res3.first.getClass().toString() - res3b = test.catch(() -> makeFromParam::(new Bird { name = "joe" })) + res3b = test.catch(() -> makeFromParam(new Bird { name = "joe" })) } class Bird { @@ -104,10 +104,10 @@ class Bird { polymorphism { // test that method type parameters are considered non-final local function make(): T = new T { name = "fred" } - res1 = make::() - res2 = make::() + res1 = make() + res2 = make() - local function call(obj: Bird | Person) = obj.greet::() + local function call(obj: Bird | Person) = obj.greet() res3 = call(new Person { name = "fred" }) res4 = test.catch(() -> call(new Bird { name = "pigeon" })) res5 = call(new Person { name = "bob" }) @@ -116,21 +116,21 @@ polymorphism { typeCast { local function cast(l: Listing): Listing = l - res1 = cast::(new Listing { "Bobbert" }) + res1 = cast(new Listing { "Bobbert" }) - res2 = test.catch(() -> cast:: 10)>(res1).toString()) + res2 = test.catch(() -> cast 10)>(res1).toString()) } constraintReadsFrameSlot { local function id(x) = x is T - local function f(n: Int) = id:: n)>("hi") + local function f(n: Int) = id n)>("hi") res1 = f(1) - res2 = let (n = 1) id:: n)>("hi") - res3 = ((n) -> id:: n)>("hi")).apply(1) + res2 = let (n = 1) id n)>("hi") + res3 = ((n) -> id n)>("hi")).apply(1) res4 = new Listing { for (n in List(1)) { - id:: n)>("hi") + id n)>("hi") } } } diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsConstrainedType.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsConstrainedType.pkl index 8bd4fcc7b..42c08bd04 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsConstrainedType.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsConstrainedType.pkl @@ -7,23 +7,23 @@ local function prefix4() = "hi" class Bar { x: Int = 10 - function baz(val): Boolean = foo::(val) - function qux(val): Boolean = foo::(val) + function baz(val): Boolean = foo(val) + function qux(val): Boolean = foo(val) } res { - res1a = foo::("high five") - res1b = foo::("high five") - res1c = foo::("high five") - res1d = foo::("high five") + res1a = foo("high five") + res1b = foo("high five") + res1c = foo("high five") + res1d = foo("high five") - res2a = new Bar {}.baz::("high five") - res2b = new Bar {}.baz::("high five") - res2c = new Bar {}.baz::("high five") - res2d = new Bar {}.baz::("high five") + res2a = new Bar {}.baz("high five") + res2b = new Bar {}.baz("high five") + res2c = new Bar {}.baz("high five") + res2d = new Bar {}.baz("high five") - res3a = new Bar {}.qux::("high five") - res3b = new Bar {}.qux::("high five") - res3c = new Bar {}.qux::("high five") - res3d = new Bar {}.qux::("high five") + res3a = new Bar {}.qux("high five") + res3b = new Bar {}.qux("high five") + res3c = new Bar {}.qux("high five") + res3d = new Bar {}.qux("high five") } diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsLexical.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsLexical.pkl index 108f45df5..5ccb90414 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsLexical.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsLexical.pkl @@ -15,13 +15,13 @@ class Person { lexical { local function f(): Dynamic = new { res = 5 as T } - res1 = test.catch(() -> f::().res) + res1 = test.catch(() -> f().res) local function g(): Dynamic = new { local myProp: T = 5 res = myProp } - res2 = test.catch(() -> g::().res) + res2 = test.catch(() -> g().res) local function nested() = new Dynamic { local function inner(x: Listing, y: U) = x @@ -30,40 +30,40 @@ lexical { local function inner4() = new Dynamic { local function innest1(x: T, y: U, z: V) = x local function innest2(x: T) = x - res1 = innest1::(new { name = "x" }, 1, "hi") + res1 = innest1(new { name = "x" }, 1, "hi") res2 = innest1(new { name = "x" }, 1, "hi") res3 = test.catch(() -> innest2(1)) } - res1 = inner::(new { new { name = "x" } }, 1) - res2 = inner2::(new { name = "x" }, 1) + res1 = inner(new { new { name = "x" } }, 1) + res2 = inner2(new { name = "x" }, 1) res3 = inner(new { new { name = "x" } }, 1) res4 = inner2(new { name = "x" }, 1) res5 = test.catch(() -> inner3(1)) - nested = inner4::() + nested = inner4() } - nested = nested::() + nested = nested() inEagerScope { local function k1() = new { [let (elem = "key" as T) elem] = "value" } - test.catch(() -> k1::()) + test.catch(() -> k1()) local function k2() = new { for (i in List(1, 2, 3)) { [let (elem = "key".repeat(i) as T) elem as T] = "value" } } - k2::() + k2() } reify { local function makeMyObj() = new { - res = ref.Reference::(new {}, null) + res = ref.Reference(new {}, null) } - res = makeMyObj::().res.name + res = makeMyObj().res.name } } @@ -74,18 +74,18 @@ local function nested(foo) = new Dynamic { local function inner4() = new Dynamic { local function innest1(x: T, y: U, z: V) = x local function innest2(x: T) = x - res1 = innest1::(new { name = "x" }, 1, "hi") + res1 = innest1(new { name = "x" }, 1, "hi") res2 = innest1(new { name = "x" }, 1, "hi") res3 = test.catch(() -> innest2(1)) } - res1 = inner::(new { new { name = "x" } }, foo) - res2 = inner2::(new { name = "x" }, 1) + res1 = inner(new { new { name = "x" } }, foo) + res2 = inner2(new { name = "x" }, 1) res3 = inner(new { new { name = "x" } }, 1) res4 = inner2(new { name = "x" }, 1) res5 = test.catch(() -> inner3(1)) - nested = inner4::() + nested = inner4() } -nested = nested::(1) +nested = nested(1) output { renderer { diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsNestedWithOffset.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsNestedWithOffset.pkl index 94b1ce46d..97146ff8b 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsNestedWithOffset.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsNestedWithOffset.pkl @@ -3,6 +3,6 @@ local function make(): Listing = new Listing { 1; 2 } local function amend(l: Listing): Listing = (l) { 3 } as Listing -local parent = make::() -local child = amend::(parent) +local parent = make() +local child = amend(parent) res = child[0] diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsSelfType.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsSelfType.pkl index 5927263d6..454a8243e 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsSelfType.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsSelfType.pkl @@ -1,18 +1,18 @@ open class A { function foo(val): List = List(val is T, val is U, val is V) - function a(val): List = this.foo::(val) + function a(val): List = this.foo(val) } open class B extends A { - function b1(val): List = this.a::(val) - function b2(val): List = super.a::(val) + function b1(val): List = this.a(val) + function b2(val): List = super.a(val) } class C extends B { - function c1(val): List = this.b1::(val) - function c2(val): List = super.b1::(val) - function c3(val): List = this.b2::(val) - function c4(val): List = super.b2::(val) + function c1(val): List = this.b1(val) + function c2(val): List = super.b1(val) + function c3(val): List = this.b2(val) + function c4(val): List = super.b2(val) } resAaA = new A {}.a(new A {}) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsStacktrace.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsStacktrace.pkl index a515758a1..7dfadc313 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsStacktrace.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsStacktrace.pkl @@ -1,5 +1,5 @@ function foo(bar) = bar as T -function baz(qux) = foo::>(qux) +function baz(qux) = foo>(qux) -res = baz::(List("a")) +res = baz(List("a")) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsStacktrace2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsStacktrace2.pkl index 757786f35..bb9d78c47 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsStacktrace2.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/methods/callWithTypeArgumentsStacktrace2.pkl @@ -4,4 +4,4 @@ function bar() = new { prop = foo as T } -res = bar::() +res = bar() diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison.err new file mode 100644 index 000000000..b64662530 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison.err @@ -0,0 +1,23 @@ +–– Pkl Error –– +Cannot find method `prop`. + +x | result = prop < foo > (1) + ^^^^^^^^^^^^^^^^ +at functionNotFoundMaybeComparison#Child.result (file:///$snippetsDir/input/errors/functionNotFoundMaybeComparison.pkl) + +This code is interpreted as a method call with type arguments. +If you meant to write a comparison expression instead, wrap the left-hand side with parentheses: + +(prop) < ... + +x | result = new Child {}.result + ^^^^^^^^^^^^^^^^^^^ +at functionNotFoundMaybeComparison#result (file:///$snippetsDir/input/errors/functionNotFoundMaybeComparison.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison2.err new file mode 100644 index 000000000..240f3be08 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison2.err @@ -0,0 +1,23 @@ +–– Pkl Error –– +Cannot find method `prop`. + +x | result = super.prop < foo > (1) + ^^^^^^^^^^^^^^^^^^^^^^ +at functionNotFoundMaybeComparison2#Child.result (file:///$snippetsDir/input/errors/functionNotFoundMaybeComparison2.pkl) + +This code is interpreted as a method call with type arguments. +If you meant to write a comparison expression instead, wrap the left-hand side with parentheses: + +(super.prop) < ... + +x | result = new Child {}.result + ^^^^^^^^^^^^^^^^^^^ +at functionNotFoundMaybeComparison2#result (file:///$snippetsDir/input/errors/functionNotFoundMaybeComparison2.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison3.err new file mode 100644 index 000000000..21e49bbea --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison3.err @@ -0,0 +1,23 @@ +–– Pkl Error –– +Cannot find method `prop`. + +x | result = self.prop < foo > (1) + ^^^^^^^^^^^^^^^^^^^^^ +at functionNotFoundMaybeComparison3#Child.result (file:///$snippetsDir/input/errors/functionNotFoundMaybeComparison3.pkl) + +This code is interpreted as a method call with type arguments. +If you meant to write a comparison expression instead, wrap the left-hand side with parentheses: + +(self.prop) < ... + +xx | result = new Child {}.result + ^^^^^^^^^^^^^^^^^^^ +at functionNotFoundMaybeComparison3#result (file:///$snippetsDir/input/errors/functionNotFoundMaybeComparison3.pkl) + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison4.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison4.err new file mode 100644 index 000000000..c1a6ea883 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison4.err @@ -0,0 +1,19 @@ +–– Pkl Error –– +Cannot find method `fooz`. + +x | fooz < bar > (fool) + ^^^^^^^^^^^^^^^^^^^ +at functionNotFoundMaybeComparison4#res[#1] (file:///$snippetsDir/input/errors/functionNotFoundMaybeComparison4.pkl) + +This code is interpreted as a method call with type arguments. +If you meant to write a comparison expression instead, wrap the left-hand side with parentheses: + +(fooz) < ... + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison5.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison5.err new file mode 100644 index 000000000..6c373a648 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/functionNotFoundMaybeComparison5.err @@ -0,0 +1,23 @@ +–– Pkl Error –– +Cannot find method `qux`. + +xx | new Foo { + ^^^^^^^^^ +at functionNotFoundMaybeComparison5#res[#1] (file:///$snippetsDir/input/errors/functionNotFoundMaybeComparison5.pkl) + +This code is interpreted as a method call with type arguments. +If you meant to write a comparison expression instead, wrap the left-hand side with parentheses: + +(new Foo { + prop1 = 1 + prop2 = 2 + prop3 = 3 + }.qux) < ... + +xxx | renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base) + +xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8") + ^^^^ +at pkl.base#Module.output.bytes (pkl:base) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference1.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference1.err index 0e0692538..2f79a436c 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference1.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference1.err @@ -6,8 +6,8 @@ x | typealias Ref = ref.Reference ^^^^^^^^^^^^^^^^^^^ at reference1#test (file:///$snippetsDir/input/errors/reference1.pkl) -x | test = ref.Reference::(d, "") as Ref - ^^^^^^^^ +x | test = ref.Reference(d, "") as Ref + ^^^^^^^^ at reference1#test (file:///$snippetsDir/input/errors/reference1.pkl) xxx | renderer.renderDocument(value) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference13.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference13.err index a12b25b05..9d49f38cf 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference13.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference13.err @@ -1,8 +1,8 @@ –– Pkl Error –– Cannot find property `default` in object of type `pkl.ref#Reference`. -x | test = ref.Reference::(d, "").default - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +x | test = ref.Reference(d, "").default + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at reference13#test (file:///$snippetsDir/input/errors/reference13.pkl) Cannot reference property `default` in class `Mapping`. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference14.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference14.err index 4e8004dbb..d5496afbb 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference14.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference14.err @@ -1,8 +1,8 @@ –– Pkl Error –– Cannot find property `default` in object of type `pkl.ref#Reference`. -x | test = ref.Reference::(d, "").default - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +x | test = ref.Reference(d, "").default + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at reference14#test (file:///$snippetsDir/input/errors/reference14.pkl) Cannot reference property `default` in class `Listing`. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference15.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference15.err index 99403efe8..ea80bfeff 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference15.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference15.err @@ -1,8 +1,8 @@ –– Pkl Error –– Cannot find property `default` in object of type `pkl.ref#Reference`. -x | test = ref.Reference::(d, "").default - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +x | test = ref.Reference(d, "").default + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at reference15#test (file:///$snippetsDir/input/errors/reference15.pkl) Cannot reference property `default` in class `Dynamic`. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference16.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference16.err index d13fb50de..866dfe848 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference16.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference16.err @@ -1,8 +1,8 @@ –– Pkl Error –– Cannot find property `output` in object of type `pkl.ref#Reference`. -xx | test = ref.Reference::(d, "").output - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +xx | test = ref.Reference(d, "").output + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at reference16#test (file:///$snippetsDir/input/errors/reference16.pkl) Cannot reference property `output` because it is defined inside external class `Module`. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference17.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference17.err index 6cb83f716..ca9244b9c 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference17.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference17.err @@ -1,8 +1,8 @@ –– Pkl Error –– Cannot find property `output` in object of type `pkl.ref#Reference`. -xx | test = ref.Reference::(d, "").output - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +xx | test = ref.Reference(d, "").output + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at reference17#test (file:///$snippetsDir/input/errors/reference17.pkl) Cannot reference property `output` because it is defined inside external class `Module`. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference18.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference18.err index 0e82888d7..894710682 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference18.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference18.err @@ -1,8 +1,8 @@ –– Pkl Error –– Cannot find property `output` in object of type `pkl.ref#Reference`. -xx | test = ref.Reference::(d, "").output - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +xx | test = ref.Reference(d, "").output + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at reference18#test (file:///$snippetsDir/input/errors/reference18.pkl) Cannot reference property `output` because it is defined inside external class `Module`. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference19.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference19.err index 7fb0dae99..98c333642 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference19.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference19.err @@ -2,8 +2,8 @@ Expected value of type `pkl.ref#Reference`, but got type `pkl.ref#Reference`. Value: Reference(new reference19#D {}, Boolean | Int | String, "").foo -xx | test = ref.Reference::(d, "").foo as ref.Reference - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +xx | test = ref.Reference(d, "").foo as ref.Reference + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at reference19#test (file:///$snippetsDir/input/errors/reference19.pkl) xxx | renderer.renderDocument(value) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference2.err index 5d0132e0a..0aba64178 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference2.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference2.err @@ -6,8 +6,8 @@ x | typealias Ref = ref.Reference ^^^^^^^^^^^^^^^^^^^ at reference2#test (file:///$snippetsDir/input/errors/reference2.pkl) -x | test = ref.Reference::(d, "") as Ref - ^^^^^^^^^^^^^^^^^^ +x | test = ref.Reference(d, "") as Ref + ^^^^^^^^^^^^^^^^^^ at reference2#test (file:///$snippetsDir/input/errors/reference2.pkl) xxx | renderer.renderDocument(value) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference3.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference3.err index 58ee5ec48..a8636d164 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference3.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference3.err @@ -6,8 +6,8 @@ x | typealias Ref = ref.Reference ^^^^^^^^^^^^^^^^^^^ at reference3#test (file:///$snippetsDir/input/errors/reference3.pkl) -x | test = ref.Reference::(d, "") as Ref> - ^^^^^^^^^^^^^^^^^^^^ +x | test = ref.Reference(d, "") as Ref> + ^^^^^^^^^^^^^^^^^^^^ at reference3#test (file:///$snippetsDir/input/errors/reference3.pkl) xxx | renderer.renderDocument(value) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference4.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference4.err index eb826b1d7..865cf3ca9 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference4.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference4.err @@ -6,8 +6,8 @@ x | typealias Ref = ref.Reference ^^^^^^^^^^^^^^^^^^^ at reference4#test (file:///$snippetsDir/input/errors/reference4.pkl) -x | test = ref.Reference::(d, "") as Ref - ^^^^^^^^^^^ +x | test = ref.Reference(d, "") as Ref + ^^^^^^^^^^^ at reference4#test (file:///$snippetsDir/input/errors/reference4.pkl) xxx | renderer.renderDocument(value) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference5.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference5.err index f3ee1c67d..318079dfb 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference5.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference5.err @@ -6,8 +6,8 @@ x | typealias Ref = ref.Reference ^^^^^^^^^^^^^^^^^^^ at reference5#test (file:///$snippetsDir/input/errors/reference5.pkl) -xx | test = ref.Reference::(d2, "") as Ref - ^^^^^^^^^^^ +xx | test = ref.Reference(d2, "") as Ref + ^^^^^^^^^^^ at reference5#test (file:///$snippetsDir/input/errors/reference5.pkl) xxx | renderer.renderDocument(value) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference6.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference6.err index 0e360c3b0..a7d294217 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference6.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference6.err @@ -1,8 +1,8 @@ –– Pkl Error –– Cannot find property `c` in object of type `pkl.ref#Reference`. -xx | test = ref.Reference::(d, new A {}).c - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +xx | test = ref.Reference(d, new A {}).c + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at reference6#test (file:///$snippetsDir/input/errors/reference6.pkl) Cannot find property `c` in type `String`. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference7.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference7.err index 37612a1a7..94deaeadc 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference7.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/reference7.err @@ -3,8 +3,8 @@ Operator `[]` is not defined for operand types `pkl.ref#Reference(d, List())["hi"] - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +x | test = ref.Reference(d, List())["hi"] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at reference7#test (file:///$snippetsDir/input/errors/reference7.pkl) Operator `[]` is not defined for operand types `List` and `String`. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/methods/callWithTypeArgumentsStacktrace.err b/pkl-core/src/test/files/LanguageSnippetTests/output/methods/callWithTypeArgumentsStacktrace.err index d68cabc08..de5d9eb70 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/methods/callWithTypeArgumentsStacktrace.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/methods/callWithTypeArgumentsStacktrace.err @@ -1,24 +1,24 @@ –– Pkl Error –– oops -x | res = baz::(List("a")) - ^^^^^^^^^^^^^ +x | res = baz(List("a")) + ^^^^^^^^^^^^^ at callWithTypeArgumentsStacktrace#res. (file:///$snippetsDir/input/methods/callWithTypeArgumentsStacktrace.pkl) -x | function baz(qux) = foo::>(qux) - ^ +x | function baz(qux) = foo>(qux) + ^ at callWithTypeArgumentsStacktrace#baz. (file:///$snippetsDir/input/methods/callWithTypeArgumentsStacktrace.pkl) x | function foo(bar) = bar as T ^ at callWithTypeArgumentsStacktrace#foo (file:///$snippetsDir/input/methods/callWithTypeArgumentsStacktrace.pkl) -x | function baz(qux) = foo::>(qux) - ^^^^^^^^^^^^^^^^^^^ +x | function baz(qux) = foo>(qux) + ^^^^^^^^^^^^^^^^^ at callWithTypeArgumentsStacktrace#baz (file:///$snippetsDir/input/methods/callWithTypeArgumentsStacktrace.pkl) -x | res = baz::(List("a")) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +x | res = baz(List("a")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at callWithTypeArgumentsStacktrace#res (file:///$snippetsDir/input/methods/callWithTypeArgumentsStacktrace.pkl) xxx | renderer.renderDocument(value) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/methods/callWithTypeArgumentsStacktrace2.err b/pkl-core/src/test/files/LanguageSnippetTests/output/methods/callWithTypeArgumentsStacktrace2.err index efa6a3fa4..b6760f402 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/methods/callWithTypeArgumentsStacktrace2.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/methods/callWithTypeArgumentsStacktrace2.err @@ -2,8 +2,8 @@ Expected value of type `Duration`, but got type `Int`. Value: 5 -x | res = bar::() - ^^^^^^^^ +x | res = bar() + ^^^^^^^^ at callWithTypeArgumentsStacktrace2#bar.prop (file:///$snippetsDir/input/methods/callWithTypeArgumentsStacktrace2.pkl) x | prop = foo as T diff --git a/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorTest.kt index 9d7015733..b626e327a 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorTest.kt @@ -930,7 +930,7 @@ class EvaluatorTest { evaluator.evaluateOutputText( text( """ - res = List(1, 2, 3).map::((it) -> it + 1) + res = List(1, 2, 3).map((it) -> it + 1) """ .trimIndent() ) diff --git a/pkl-formatter/src/main/java/org/pkl/formatter/Builder.java b/pkl-formatter/src/main/java/org/pkl/formatter/Builder.java index 6f6242284..3f5d495cc 100644 --- a/pkl-formatter/src/main/java/org/pkl/formatter/Builder.java +++ b/pkl-formatter/src/main/java/org/pkl/formatter/Builder.java @@ -261,7 +261,7 @@ final class Builder { * {@code foo.bar.baz( * new { qux = 1 } * )} - * {@code foo.bar.baz::< + * {@code foo.bar.baz< * Quux * >( * new { qux = 1 } @@ -368,7 +368,7 @@ final class Builder { var argListIdx = -1; for (var i = 0; i < lastNode.children.size(); i++) { var node = lastNode.children.get(i); - if (node.type == NodeType.DCOLON) { + if (node.type == NodeType.TYPE_ARGUMENT_LIST) { typeArgListIdx = i; continue; } diff --git a/pkl-formatter/src/test/files/FormatterSnippetTests/input/method-call-type-args.pkl b/pkl-formatter/src/test/files/FormatterSnippetTests/input/method-call-type-args.pkl index 0e5fccf27..a9a21e00e 100644 --- a/pkl-formatter/src/test/files/FormatterSnippetTests/input/method-call-type-args.pkl +++ b/pkl-formatter/src/test/files/FormatterSnippetTests/input/method-call-type-args.pkl @@ -1,14 +1,14 @@ -res1 = foo::(baz) -res1a = foo::(baz, baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg) +res1 = foo(baz) +res1a = foo(baz, baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg) -res2 = qux.foo::(baz, baz2) -res2a = qux.foo::(baz, baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg) -res2b = qux.quux::().foo::(baz, baz2) -res2c = qux.quux::().foo::(baz, baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg) -res2d = qux::().foo::(baz, baz2) -res2e = qux::().foo::(baz, baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg) -res2f = grault.qux.quux::().foo::(baz, baz2) -res2g = grault.qux.quux::().foo::(baz, baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg) +res2 = qux.foo(baz, baz2) +res2a = qux.foo(baz, baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg) +res2b = qux.quux().foo(baz, baz2) +res2c = qux.quux().foo(baz, baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg) +res2d = qux().foo(baz, baz2) +res2e = qux().foo(baz, baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg) +res2f = grault.qux.quux().foo(baz, baz2) +res2g = grault.qux.quux().foo(baz, baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg) -res3 = super.foo::(baz, baz2) -res3a = super.foo::(baz, baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg) +res3 = super.foo(baz, baz2) +res3a = super.foo(baz, baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg) diff --git a/pkl-formatter/src/test/files/FormatterSnippetTests/output/method-call-type-args.pkl b/pkl-formatter/src/test/files/FormatterSnippetTests/output/method-call-type-args.pkl index 5b0fc4e60..b3e2b6262 100644 --- a/pkl-formatter/src/test/files/FormatterSnippetTests/output/method-call-type-args.pkl +++ b/pkl-formatter/src/test/files/FormatterSnippetTests/output/method-call-type-args.pkl @@ -1,6 +1,6 @@ -res1 = foo::(baz) +res1 = foo(baz) res1a = - foo::< + foo< Bar, Baz, ReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongType, @@ -10,9 +10,9 @@ res1a = reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg, ) -res2 = qux.foo::(baz, baz2) +res2 = qux.foo(baz, baz2) res2a = - qux.foo::< + qux.foo< Bar, Baz, ReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongType, @@ -21,11 +21,11 @@ res2a = baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg, ) -res2b = qux.quux::().foo::(baz, baz2) +res2b = qux.quux().foo(baz, baz2) res2c = qux - .quux::() - .foo::< + .quux() + .foo< Bar, Baz, ReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongType, @@ -34,10 +34,10 @@ res2c = baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg, ) -res2d = qux::().foo::(baz, baz2) +res2d = qux().foo(baz, baz2) res2e = - qux::() - .foo::< + qux() + .foo< Bar, Baz, ReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongType, @@ -46,11 +46,11 @@ res2e = baz, reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg, ) -res2f = grault.qux.quux::().foo::(baz, baz2) +res2f = grault.qux.quux().foo(baz, baz2) res2g = grault.qux - .quux::() - .foo::< + .quux() + .foo< Bar, Baz, ReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongType, @@ -60,9 +60,9 @@ res2g = reallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongArg, ) -res3 = super.foo::(baz, baz2) +res3 = super.foo(baz, baz2) res3a = - super.foo::< + super.foo< Bar, Baz, ReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLongType, diff --git a/pkl-parser/src/main/java/org/pkl/parser/Constants.java b/pkl-parser/src/main/java/org/pkl/parser/Constants.java new file mode 100644 index 000000000..ccd46794b --- /dev/null +++ b/pkl-parser/src/main/java/org/pkl/parser/Constants.java @@ -0,0 +1,28 @@ +/* + * 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; + +final class Constants { + private Constants() {} + + /** + * Max number of tokens consumed during speculative parsing before failure. + * + *

5 million token budget takes about 300ms before reaching the error case (tested on a MacBook + * Pro M4 Max). + */ + static final int MAX_SPECULATIVE_TOKEN_COUNT = 5_000_000; +} diff --git a/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java b/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java index b6bad75a7..fc457c044 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java +++ b/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java @@ -22,12 +22,21 @@ public final class GenericParser { var parser = new GenericParserImpl(source); try { return parser.parseModule(); - } catch (StackOverflowError e) { + } catch (MaxBacktrackCountError err) { + throw err.toGenericParserError(); + } catch (StackOverflowError err) { throw parser.tooDeeplyNested(); } } public Node parseExpressionInput(String source) { - return new GenericParserImpl(source).parseExpressionInput(); + var parser = new GenericParserImpl(source); + try { + return parser.parseExpressionInput(); + } catch (MaxBacktrackCountError err) { + throw err.toGenericParserError(); + } catch (StackOverflowError err) { + throw parser.tooDeeplyNested(); + } } } diff --git a/pkl-parser/src/main/java/org/pkl/parser/GenericParserImpl.java b/pkl-parser/src/main/java/org/pkl/parser/GenericParserImpl.java index 8718b86b5..e0dd53ac2 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/GenericParserImpl.java +++ b/pkl-parser/src/main/java/org/pkl/parser/GenericParserImpl.java @@ -15,9 +15,13 @@ */ package org.pkl.parser; +import static org.pkl.parser.Constants.MAX_SPECULATIVE_TOKEN_COUNT; + import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Supplier; import org.jspecify.annotations.Nullable; import org.pkl.parser.syntax.Operator; @@ -25,6 +29,7 @@ import org.pkl.parser.syntax.generic.FullSpan; import org.pkl.parser.syntax.generic.Node; import org.pkl.parser.syntax.generic.NodeType; import org.pkl.parser.util.ErrorMessages; +import org.pkl.parser.util.SortedIntArray; @SuppressWarnings("DuplicatedCode") class GenericParserImpl { @@ -35,6 +40,11 @@ class GenericParserImpl { private FullToken _lookahead; private int cursor = 0; private final List tokens = new ArrayList<>(); + private int speculativeTokenCount = 0; + private boolean isSpeculativeParse = false; + private final Set failedTypeArgAttempts = new HashSet<>(); + // char indices of `<` that might start a type argument list + private @Nullable SortedIntArray typeArgCandidates; GenericParserImpl(String source) { this.lexer = new Lexer(source); @@ -745,18 +755,12 @@ class GenericParserImpl { private void parseAccess(List children) { children.add(parseIdentifier()); - if (lookahead() == Token.DCOLON && noSemicolonInbetween() && _lookahead.newLinesBetween == 0) { - children.add(makeTerminal(next())); + var typeArgs = tryParseTypeArgumentList(); + if (typeArgs != null) { + // `tryParseTypeArgumentList` guarantees that an argument list follows + children.add(typeArgs); 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); - } + children.add(parseArgumentList()); } else if (lookahead() == Token.LPAREN && noSemicolonInbetween() && _lookahead.newLinesBetween == 0) { @@ -765,6 +769,48 @@ class GenericParserImpl { } } + private @Nullable Node tryParseTypeArgumentList() { + if (lookahead() != Token.LT + || !noSemicolonInbetween() + || _lookahead.newLinesBetween != 0 + || failedTypeArgAttempts.contains(spanLookahead.charIndex()) + || !isTypeArgCandidate(spanLookahead.charIndex())) { + return null; + } + var savedCursor = cursor; + var savedCharIndex = spanLookahead.charIndex(); + var savedIsSpeculativeParse = isSpeculativeParse; + isSpeculativeParse = true; + try { + var typeArgs = parseTypeArgumentList(); + if (lookahead() == Token.LPAREN + && noSemicolonInbetween() + && _lookahead.newLinesBetween == 0) { + return typeArgs; + } + } catch (GenericParserError ignored) { + // not a type argument list + } finally { + isSpeculativeParse = savedIsSpeculativeParse; + } + failedTypeArgAttempts.add(savedCharIndex); + backtrackTo(savedCursor); + return null; + } + + private boolean isTypeArgCandidate(int charIndex) { + if (typeArgCandidates == null) { + var scanner = new TypeArgumentCandidatesScanner(); + scanner.addPending(charIndex); + for (var i = cursor + 1; i < tokens.size(); i++) { + var tk = tokens.get(i); + scanner.accept(tk.token, tk.span.charIndex()); + } + typeArgCandidates = scanner.candidates(); + } + return typeArgCandidates.contains(charIndex); + } + private Node parseExprAtom(@Nullable String expectation) { var expr = switch (lookahead) { @@ -1506,6 +1552,12 @@ class GenericParserImpl { } private FullToken next() { + if (isSpeculativeParse) { + if (speculativeTokenCount > MAX_SPECULATIVE_TOKEN_COUNT) { + throw new MaxBacktrackCountError(spanLookahead); + } + speculativeTokenCount++; + } var tmp = tokens.get(cursor++); _lookahead = tokens.get(cursor); lookahead = _lookahead.token; @@ -1517,15 +1569,10 @@ class GenericParserImpl { return tokens.get(cursor - 1).token != Token.SEMICOLON; } - private void backtrack() { - var tmp = tokens.get(--cursor); - lookahead = tmp.token; - spanLookahead = tmp.span; - } - private void backtrackTo(int point) { cursor = point; var tmp = tokens.get(cursor); + _lookahead = tmp; lookahead = tmp.token; spanLookahead = tmp.span; } @@ -1611,7 +1658,6 @@ 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; diff --git a/pkl-parser/src/main/java/org/pkl/parser/Lexer.java b/pkl-parser/src/main/java/org/pkl/parser/Lexer.java index 01556d0ab..b1a0ad9ab 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/Lexer.java +++ b/pkl-parser/src/main/java/org/pkl/parser/Lexer.java @@ -85,6 +85,49 @@ public final class Lexer { return new String(source, offset, size); } + /** Captures the lexer's current state so that it can later be rewound via {@link #restore}. */ + public Snapshot snapshot() { + var scopes = new ArrayDeque(); + // `ArrayDeque.push` adds to the front, so iterate in reverse to preserve order. + var it = interpolationStack.descendingIterator(); + while (it.hasNext()) { + scopes.push(it.next().copy()); + } + return new Snapshot( + cursor, + sCursor, + line, + sLine, + col, + sCol, + lookahead, + state, + scopes, + stringEnded, + isEscape, + newLinesBetween); + } + + public void restore(Snapshot snapshot) { + cursor = snapshot.cursor; + sCursor = snapshot.sCursor; + line = snapshot.line; + sLine = snapshot.sLine; + col = snapshot.col; + sCol = snapshot.sCol; + lookahead = snapshot.lookahead; + state = snapshot.state; + interpolationStack.clear(); + // copy again so that the snapshot can be restored more than once + var it = snapshot.interpolationStack.descendingIterator(); + while (it.hasNext()) { + interpolationStack.push(it.next().copy()); + } + stringEnded = snapshot.stringEnded; + isEscape = snapshot.isEscape; + newLinesBetween = snapshot.newLinesBetween; + } + public Token next() { sCursor = cursor; sLine = line; @@ -137,12 +180,7 @@ public final class Lexer { case '}' -> Token.RBRACE; case ',' -> Token.COMMA; case '@' -> Token.AT; - case ':' -> { - if (lookahead == ':') { - nextChar(); - yield Token.DCOLON; - } else yield Token.COLON; - } + case ':' -> Token.COLON; case '+' -> Token.PLUS; case '%' -> Token.MOD; case '[' -> { @@ -864,8 +902,28 @@ public final class Lexer { this.quotes = quotes; this.pounds = pounds; } + + InterpolationScope copy() { + var res = new InterpolationScope(quotes, pounds); + res.parens = parens; + return res; + } } + public record Snapshot( + int cursor, + int sCursor, + int line, + int sLine, + int col, + int sCol, + int lookahead, + State state, + Deque interpolationStack, + boolean stringEnded, + boolean isEscape, + int newLinesBetween) {} + private enum State { DEFAULT, STRING diff --git a/pkl-parser/src/main/java/org/pkl/parser/MaxBacktrackCountError.java b/pkl-parser/src/main/java/org/pkl/parser/MaxBacktrackCountError.java new file mode 100644 index 000000000..2243b4d05 --- /dev/null +++ b/pkl-parser/src/main/java/org/pkl/parser/MaxBacktrackCountError.java @@ -0,0 +1,51 @@ +/* + * 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; + +import org.jspecify.annotations.Nullable; +import org.pkl.parser.syntax.generic.FullSpan; +import org.pkl.parser.util.ErrorMessages; + +final class MaxBacktrackCountError extends RuntimeException { + + private final @Nullable Span span; + private final @Nullable FullSpan fullSpan; + + public MaxBacktrackCountError(Span span) { + this.span = span; + this.fullSpan = null; + } + + public MaxBacktrackCountError(FullSpan span) { + this.span = null; + this.fullSpan = span; + } + + @Override + public String getMessage() { + return ErrorMessages.create("maxBacktrackCount"); + } + + public ParserError toParserError() { + assert span != null; + return new ParserError(getMessage(), span); + } + + public GenericParserError toGenericParserError() { + assert fullSpan != null; + return new GenericParserError(getMessage(), fullSpan); + } +} diff --git a/pkl-parser/src/main/java/org/pkl/parser/Parser.java b/pkl-parser/src/main/java/org/pkl/parser/Parser.java index edfa76203..74ba5cdcd 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/Parser.java +++ b/pkl-parser/src/main/java/org/pkl/parser/Parser.java @@ -26,7 +26,9 @@ public final class Parser { var parser = new ParserImpl(source); try { return parser.parseModule(); - } catch (StackOverflowError e) { + } catch (MaxBacktrackCountError err) { + throw err.toParserError(); + } catch (StackOverflowError err) { throw parser.tooDeeplyNested(); } } @@ -35,7 +37,9 @@ public final class Parser { var parser = new ParserImpl(source); try { return parser.parseExpressionInput(); - } catch (StackOverflowError e) { + } catch (MaxBacktrackCountError err) { + throw err.toParserError(); + } catch (StackOverflowError err) { throw parser.tooDeeplyNested(); } } @@ -44,7 +48,9 @@ public final class Parser { var parser = new ParserImpl(source); try { return parser.parseReplInput(); - } catch (StackOverflowError e) { + } catch (MaxBacktrackCountError err) { + throw err.toParserError(); + } catch (StackOverflowError err) { throw parser.tooDeeplyNested(); } } diff --git a/pkl-parser/src/main/java/org/pkl/parser/ParserImpl.java b/pkl-parser/src/main/java/org/pkl/parser/ParserImpl.java index 614401503..dbc12fb68 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/ParserImpl.java +++ b/pkl-parser/src/main/java/org/pkl/parser/ParserImpl.java @@ -15,10 +15,14 @@ */ package org.pkl.parser; +import static org.pkl.parser.Constants.MAX_SPECULATIVE_TOKEN_COUNT; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Supplier; import org.jspecify.annotations.Nullable; import org.pkl.parser.syntax.Annotation; @@ -86,10 +90,10 @@ import org.pkl.parser.syntax.TypeArgumentList; import org.pkl.parser.syntax.TypeParameter; import org.pkl.parser.syntax.TypeParameterList; import org.pkl.parser.util.ErrorMessages; +import org.pkl.parser.util.SortedIntArray; @SuppressWarnings("DuplicatedCode") final class ParserImpl { - private final Lexer lexer; private Token lookahead; private Span spanLookahead; @@ -97,8 +101,15 @@ final class ParserImpl { private FullToken prev = new FullToken(Token.SOF, new Span(0, 0), 0); private FullToken _lookahead; private boolean precededBySemicolon = false; + private int speculativeTokenCount = 0; + private final Set failedTypeArgAttempts = new HashSet<>(); + private boolean isSpeculativeParse = false; + private final String source; + // char indices of `<` that might start a type argument list + private @Nullable SortedIntArray typeArgCandidates; ParserImpl(String source) { + this.source = source; this.lexer = new Lexer(source); _lookahead = forceNext(); lookahead = _lookahead.token; @@ -831,21 +842,12 @@ 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(); - } + var typeArgumentList = tryParseTypeArgumentList(); 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 = @@ -992,25 +994,14 @@ 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(); - } + var typeArgs = tryParseTypeArgumentList(); if (lookahead == Token.LPAREN && !precededBySemicolon && _lookahead.newLinesBetween == 0) { var args = parseArgumentList(); 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, null, start.endWith(identifier.span())); - } + yield new SuperAccessExpr(identifier, null, null, start.endWith(identifier.span())); } else { expect(Token.LBRACK, "unexpectedToken", "["); var exp = parseExpr("]"); @@ -1052,13 +1043,7 @@ 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(); - } + var typeArgs = tryParseTypeArgumentList(); if (lookahead == Token.LPAREN && !precededBySemicolon && _lookahead.newLinesBetween == 0) { @@ -1066,12 +1051,7 @@ final class ParserImpl { yield new UnqualifiedAccessExpr( 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, null, identifier.span()); - } + yield new UnqualifiedAccessExpr(identifier, null, null, identifier.span()); } case EOF -> throw new ParserError( @@ -1111,17 +1091,10 @@ final class ParserImpl { else 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(); - } + var typeArgumentList = tryParseTypeArgumentList(); 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(); expr = @@ -1603,6 +1576,53 @@ final class ParserImpl { return new TypeArgumentList(pars, start.endWith(end)); } + private @Nullable TypeArgumentList tryParseTypeArgumentList() { + if (lookahead != Token.LT + || precededBySemicolon + || _lookahead.newLinesBetween != 0 + || failedTypeArgAttempts.contains(spanLookahead.charIndex()) + || !isTypeArgCandidate(spanLookahead.charIndex())) { + return null; + } + var lexerSnapshot = lexer.snapshot(); + var savedLookahead = lookahead; + var savedSpanLookahead = spanLookahead; + var savedBacktracking = backtracking; + var savedPrev = prev; + var savedFullLookahead = _lookahead; + var savedPrecededBySemicolon = precededBySemicolon; + var savedIsSpeculativeParse = isSpeculativeParse; + isSpeculativeParse = true; + try { + var typeArgs = parseTypeArgumentList(); + if (lookahead == Token.LPAREN && !precededBySemicolon && _lookahead.newLinesBetween == 0) { + return typeArgs; + } + } catch (ParserError ignored) { + // not a type argument list + } finally { + isSpeculativeParse = savedIsSpeculativeParse; + } + failedTypeArgAttempts.add(savedSpanLookahead.charIndex()); + lexer.restore(lexerSnapshot); + lookahead = savedLookahead; + spanLookahead = savedSpanLookahead; + backtracking = savedBacktracking; + prev = savedPrev; + _lookahead = savedFullLookahead; + precededBySemicolon = savedPrecededBySemicolon; + return null; + } + + private boolean isTypeArgCandidate(int charIndex) { + if (typeArgCandidates == null) { + var snapshot = lexer.snapshot(); + typeArgCandidates = TypeArgumentCandidatesScanner.scanFromPending(lexer); + lexer.restore(snapshot); + } + return typeArgCandidates.contains(charIndex); + } + private ArgumentList parseArgumentList() { var start = expect(Token.LPAREN, "unexpectedToken", "(").span; if (lookahead == Token.RPAREN) { @@ -1877,6 +1897,12 @@ final class ParserImpl { } private FullToken next() { + if (isSpeculativeParse) { + if (speculativeTokenCount > MAX_SPECULATIVE_TOKEN_COUNT) { + throw new MaxBacktrackCountError(spanLookahead); + } + speculativeTokenCount++; + } if (backtracking) { backtracking = false; lookahead = _lookahead.token; diff --git a/pkl-parser/src/main/java/org/pkl/parser/Token.java b/pkl-parser/src/main/java/org/pkl/parser/Token.java index ba0034eae..dcdcfb672 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/Token.java +++ b/pkl-parser/src/main/java/org/pkl/parser/Token.java @@ -89,7 +89,6 @@ public enum Token { NOT, QUESTION, COLON, - DCOLON, ARROW, EQUAL, NOT_EQUAL, diff --git a/pkl-parser/src/main/java/org/pkl/parser/TypeArgumentCandidatesScanner.java b/pkl-parser/src/main/java/org/pkl/parser/TypeArgumentCandidatesScanner.java new file mode 100644 index 000000000..4a6054bbe --- /dev/null +++ b/pkl-parser/src/main/java/org/pkl/parser/TypeArgumentCandidatesScanner.java @@ -0,0 +1,117 @@ +/* + * 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; + +import org.pkl.parser.util.IntArray; +import org.pkl.parser.util.SortedIntArray; + +/** + * Finds all {@code <} tokens might be the start of a method call's type argument list, e.g. the + * {@code } in {@code foo(5)}. + * + *

A candidate must have balanced {@code <} and {@code >} pairs and followed by {@code (}. + * + *

Candidates are not necessarily starts of type argument lists. For example, {@code + * foo<5>()} is not one because {@code 5} is not a type. + * + *

However, this class doesn't know what a type is; this is meant to be a quick scan of the whole + * file to help minimize speculative parsing in the underlying parser. + */ +final class TypeArgumentCandidatesScanner { + // marks a `(`, `[`, `{` or string interpolation on the stack; `<` is stored as its char index + private static final int BRACKET = -100; + + // char index of every candidate, in the order they were discovered + private final IntArray found = new IntArray(); + // `<` that are still open, and the brackets that enclose them + private final IntArray pending = new IntArray(); + // char index of the `<` matching the most recent `>`, if we haven't seen the next token yet + private int currentPending = -1; + + static SortedIntArray scan(Lexer lexer) { + var scanner = new TypeArgumentCandidatesScanner(); + try { + while (true) { + var token = lexer.next(); + if (token == Token.EOF) break; + scanner.accept(token, lexer.getStartCursor()); + } + } catch (ParserError ignored) { + // Lexing error. The parser will report it when it gets there; `<` that are still open at + // this point are simply not candidates. + } + return scanner.candidates(); + } + + static SortedIntArray scanFromPending(Lexer lexer) { + var scanner = new TypeArgumentCandidatesScanner(); + scanner.pending.push(lexer.getStartCursor()); + try { + while (true) { + var token = lexer.next(); + if (token == Token.EOF) break; + scanner.accept(token, lexer.getStartCursor()); + } + } catch (ParserError ignored) { + // Lexing error. The parser will report it when it gets there; `<` that are still open at + // this point are simply not candidates. + } + return scanner.candidates(); + } + + /** + * The indices of the {@code <} tokens that might be type arg calls. + * + *

Must only call this after feeding all tokens. + */ + SortedIntArray candidates() { + return new SortedIntArray(found); + } + + /** Feeds the next token. Call in source order, for every token including affixes. */ + void accept(Token token, int charIndex) { + if (token.isAffix()) return; + if (currentPending >= 0) { + if (token == Token.LPAREN) { + found.push(currentPending); + } + currentPending = -1; + } + switch (token) { + case LT -> pending.push(charIndex); + case GT -> { + // a `>` without a matching `<` at the same level is a comparison operator + if (pending.isNotEmpty() && pending.get(pending.size() - 1) != BRACKET) { + currentPending = pending.pop(); + } + } + case LPAREN, LBRACK, LBRACE, INTERPOLATION_START -> pending.push(BRACKET); + case RPAREN, RBRACK, RBRACE -> { + // discard unmatched `<` (comparison operators) down to the enclosing bracket + while (true) { + if (pending.isEmpty() || pending.pop() == BRACKET) { + break; + } + } + } + default -> {} + } + } + + public void addPending(int charIndex) { + pending.push(charIndex); + } +} diff --git a/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/NodeType.java b/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/NodeType.java index 91ed1440c..f5e10923a 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/NodeType.java +++ b/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/NodeType.java @@ -52,7 +52,6 @@ public enum NodeType { ASSIGN(NodeKind.TERMINAL), AT(NodeKind.TERMINAL), COLON(NodeKind.TERMINAL), - DCOLON(NodeKind.TERMINAL), COMMA(NodeKind.TERMINAL), DOT(NodeKind.TERMINAL), GT(NodeKind.TERMINAL), diff --git a/pkl-parser/src/main/java/org/pkl/parser/util/IntArray.java b/pkl-parser/src/main/java/org/pkl/parser/util/IntArray.java new file mode 100644 index 000000000..2d71d27c1 --- /dev/null +++ b/pkl-parser/src/main/java/org/pkl/parser/util/IntArray.java @@ -0,0 +1,61 @@ +/* + * 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.util; + +import java.util.Arrays; + +public class IntArray { + int[] store = new int[16]; + private int size = 0; + + public void push(int value) { + if (size == store.length) { + store = Arrays.copyOf(store, size * 2); + } + store[size++] = value; + } + + public int pop() { + if (size == 0) return -1; + size--; + return store[size]; + } + + public int size() { + return size; + } + + public void clear() { + this.size = 0; + } + + public void sort() { + Arrays.sort(store, 0, size); + } + + public boolean isEmpty() { + return size == 0; + } + + public boolean isNotEmpty() { + return size != 0; + } + + public int get(int index) { + assert index < size; + return store[index]; + } +} diff --git a/pkl-parser/src/main/java/org/pkl/parser/util/SortedIntArray.java b/pkl-parser/src/main/java/org/pkl/parser/util/SortedIntArray.java new file mode 100644 index 000000000..74b25ffcc --- /dev/null +++ b/pkl-parser/src/main/java/org/pkl/parser/util/SortedIntArray.java @@ -0,0 +1,41 @@ +/* + * 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.util; + +import java.util.Arrays; + +public class SortedIntArray { + private final int[] store; + private final int size; + + public SortedIntArray(IntArray array) { + array.sort(); + this.store = array.store; + this.size = array.size(); + } + + public boolean contains(int value) { + if (size == 0) return false; + if (value < store[0]) return false; + if (value > store[size - 1]) return false; + return Arrays.binarySearch(store, 0, size, value) >= 0; + } + + // for testing only + public int[] toArray() { + return Arrays.copyOf(store, size); + } +} diff --git a/pkl-parser/src/main/resources/org/pkl/parser/errorMessages.properties b/pkl-parser/src/main/resources/org/pkl/parser/errorMessages.properties index 254c94ab0..29c25534c 100644 --- a/pkl-parser/src/main/resources/org/pkl/parser/errorMessages.properties +++ b/pkl-parser/src/main/resources/org/pkl/parser/errorMessages.properties @@ -114,3 +114,6 @@ 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. + +maxBacktrackCount=\ +Too much backtracking occurred during parsing. diff --git a/pkl-parser/src/test/kotlin/org/pkl/parser/GenericParserTest.kt b/pkl-parser/src/test/kotlin/org/pkl/parser/GenericParserTest.kt new file mode 100644 index 000000000..016934355 --- /dev/null +++ b/pkl-parser/src/test/kotlin/org/pkl/parser/GenericParserTest.kt @@ -0,0 +1,47 @@ +/* + * 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 + +import java.nio.charset.StandardCharsets +import java.time.Duration +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.assertTimeoutPreemptively + +class GenericParserTest { + @Test + fun `parser fails if exceeds max backtracking count`() { + val code = + javaClass + .getResourceAsStream("large_backtracking.pkl.txt")!! + .readAllBytes() + .toString(StandardCharsets.UTF_8) + val err = assertThrows { GenericParser().parseModule(code) } + assertThat(err).hasMessageContaining("Too much backtracking occurred during parsing.") + } + + // this is kind of a smoke test to make sure that we parse these cases reasonably quickly. + @Test + fun `parser succeeds if nested backtracking stays within max depth`() { + val code = + """ + result = a(a < a(a < a(a < a(a < a(a < a(a < a(a < a(a < a(a < 1))))))))) + """ + .trimIndent() + assertTimeoutPreemptively(Duration.ofMillis(10)) { GenericParser().parseModule(code) } + } +} diff --git a/pkl-parser/src/test/kotlin/org/pkl/parser/ParserComparisonTest.kt b/pkl-parser/src/test/kotlin/org/pkl/parser/ParserComparisonTest.kt index fe0e2d385..10781aaeb 100644 --- a/pkl-parser/src/test/kotlin/org/pkl/parser/ParserComparisonTest.kt +++ b/pkl-parser/src/test/kotlin/org/pkl/parser/ParserComparisonTest.kt @@ -21,58 +21,42 @@ import kotlin.io.path.extension import kotlin.io.path.pathString import kotlin.io.path.readText import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.SoftAssertions -import org.junit.jupiter.api.Test +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.ExecutionMode import org.pkl.commons.walk @Execution(ExecutionMode.CONCURRENT) class ParserComparisonTest { - - @Test - fun compareSnippetTests() { - SoftAssertions.assertSoftly { softly -> - getSnippets() - .parallelStream() - .map { Pair(it.pathString, it.readText()) } - .forEach { (path, snippet) -> - try { - compare(snippet, path, softly) - } catch (e: GenericParserError) { - softly.fail("path: $path. Message: ${e.message}", e) - } catch (e: ParserError) { - softly.fail("path: $path. Message: ${e.message}", e) - } - } - } - } - - fun getSnippets(): List { - return Path("../pkl-core/src/test/files/LanguageSnippetTests/input") - .walk() - .filter { path -> - val pathStr = path.toString().replace("\\", "/") - path.extension == "pkl" && - !exceptions.any { pathStr.endsWith(it) } && - !regexExceptions.any { it.matches(pathStr) } + @TestFactory + fun compareSnippetTests(): List { + val root = Path("../pkl-core/src/test/files/LanguageSnippetTests/input") + return getSnippets().map { path -> + DynamicTest.dynamicTest(root.relativize(path).pathString, path.toUri()) { + val (sexp, genSexp) = renderBoth(path.readText()) + assertThat(genSexp).isEqualTo(sexp) } - .toList() - } - - fun compare(code: String, path: String? = null, softly: SoftAssertions? = null) { - val (sexp, genSexp) = renderBoth(code) - when { - (path != null && softly != null) -> - softly.assertThat(genSexp).`as`("path: $path").isEqualTo(sexp) - else -> assertThat(genSexp).`as`("path: $path").isEqualTo(sexp) } } - fun renderBoth(code: String): Pair = + private fun renderBoth(code: String): Pair = Pair(renderCode(code), renderGenericCode(code)) companion object { + @JvmStatic + fun getSnippets(): List { + return Path("../pkl-core/src/test/files/LanguageSnippetTests/input") + .walk() + .filter { path -> + val pathStr = path.toString().replace("\\", "/") + path.extension == "pkl" && + !exceptions.any { pathStr.endsWith(it) } && + !regexExceptions.any { it.matches(pathStr) } + } + .toList() + } + private fun renderCode(code: String): String { val parser = Parser() val mod = parser.parseModule(code) diff --git a/pkl-parser/src/test/kotlin/org/pkl/parser/ParserTest.kt b/pkl-parser/src/test/kotlin/org/pkl/parser/ParserTest.kt new file mode 100644 index 000000000..08b7347f6 --- /dev/null +++ b/pkl-parser/src/test/kotlin/org/pkl/parser/ParserTest.kt @@ -0,0 +1,48 @@ +/* + * 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 + +import java.nio.charset.StandardCharsets +import java.time.Duration +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.assertTimeoutPreemptively + +class ParserTest { + @Test + fun `parser fails if exceeds max backtracking count`() { + val code = + javaClass + .getResourceAsStream("large_backtracking.pkl.txt")!! + .readAllBytes() + .toString(StandardCharsets.UTF_8) + val err = assertThrows { Parser().parseModule(code) } + assertThat(err).hasMessageContaining("Too much backtracking occurred during parsing.") + } + + // this is kind of a smoke test to make sure that we parse these cases reasonably quickly. + @Test + fun `parser succeeds if nested backtracking stays within max depth`() { + val code = + """ + result = a(a < a(a < a(a < a(a < a(a < a(a < a(a < a(a < a(a < 1))))))))) + """ + .trimIndent() + // 1 second should still catch if we see exponential regressions + assertTimeoutPreemptively(Duration.ofSeconds(1)) { Parser().parseModule(code) } + } +} diff --git a/pkl-parser/src/test/kotlin/org/pkl/parser/TypeArgumentCandidatesScannerTest.kt b/pkl-parser/src/test/kotlin/org/pkl/parser/TypeArgumentCandidatesScannerTest.kt new file mode 100644 index 000000000..b5452bc78 --- /dev/null +++ b/pkl-parser/src/test/kotlin/org/pkl/parser/TypeArgumentCandidatesScannerTest.kt @@ -0,0 +1,262 @@ +/* + * 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 + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class TypeArgumentCandidatesScannerTest { + private fun candidates(source: String): IntArray = + TypeArgumentCandidatesScanner.scan(Lexer(source)).toArray() + + @Test + fun `empty input`() { + assertThat(candidates("")).isEmpty() + } + + @Test + fun `simple method call with type arguments`() { + assertThat(candidates("x = foo(5)")).isEqualTo(intArrayOf(7)) + } + + @Test + fun `comparison without closing angle bracket`() { + assertThat(candidates("x = a < b")).isEmpty() + } + + @Test + fun `closing angle bracket not followed by open paren`() { + assertThat(candidates("x = foo bar")).isEmpty() + } + + @Test + fun `less-than-or-equal and greater-than-or-equal are not angle brackets`() { + assertThat(candidates("x = a <= b > (c)")).isEmpty() + assertThat(candidates("x = a < b >= (c)")).isEmpty() + } + + @Test + fun `stray greater-than is ignored`() { + assertThat(candidates("x = a > (b)")).isEmpty() + // the first `>` has no matching `<`; the second one matches + assertThat(candidates("x = a > b < c > (d)")).isEqualTo(intArrayOf(10)) + } + + @Test + fun `nested generics - only the outermost is followed by open paren`() { + val source = "x = foo>>(5)" + assertThat(candidates(source)).isEqualTo(intArrayOf(7)) + } + + @Test + fun `multiple candidates`() { + assertThat(candidates("x = foo(bar(1))")).isEqualTo(intArrayOf(7, 16)) + assertThat(candidates("x = foo(1)\ny = bar(2)")).isEqualTo(intArrayOf(7, 23)) + } + + @Test + fun `function types - arrow is not a closing angle bracket`() { + assertThat(candidates("x = foo<(Int) -> String>(f)")).isEqualTo(intArrayOf(7)) + } + + @Test + fun `union, nullable and default types`() { + assertThat(candidates("x = foo<*Int|String?>(f)")).isEqualTo(intArrayOf(7)) + } + + // constrained types + + @Test + fun `greater-than inside a constraint does not close the type argument list`() { + assertThat(candidates("x = foo 3)>(5)")).isEqualTo(intArrayOf(7)) + } + + @Test + fun `less-than inside a constraint is not a candidate`() { + // only the outer `<` is + assertThat(candidates("x = foo(5)")).isEqualTo(intArrayOf(7)) + } + + @Test + fun `comparisons in both directions inside a constraint`() { + assertThat(candidates("x = foo 1)>(5)")).isEqualTo(intArrayOf(7)) + } + + @Test + fun `constraint with a call followed by open paren`() { + val source = "x = foo(this))>(5)" + assertThat(candidates(source)).isEqualTo(intArrayOf(7, 15)) + } + + @Test + fun `constraint in a nested type argument`() { + assertThat(candidates("x = foo 3)>>(5)")).isEqualTo(intArrayOf(7)) + } + + @Test + fun `multiple constrained type arguments`() { + assertThat(candidates("x = foo 3), String(length < 5)>(5)")).isEqualTo(intArrayOf(7)) + } + + @Test + fun `comparison inside parens does not match a greater-than outside`() { + // the `>` after `)` is at a different nesting level than the `<` + assertThat(candidates("x = (a < b) > (c)")).isEmpty() + } + + // brackets and braces + + @Test + fun `comparison inside subscript does not match a greater-than outside`() { + assertThat(candidates("x = list[a < b] > (c)")).isEmpty() + } + + @Test + fun `comparison inside object body does not match a greater-than outside`() { + assertThat(candidates("x = new Foo { y = a < b } > (c)")).isEmpty() + } + + @Test + fun `greater-than inside object body does not close an outer less-than`() { + assertThat(candidates("x = foo 0 }.length > 0)>(5)")) + .isEqualTo(intArrayOf(7)) + } + + @Test + fun `unmatched less-than inside parens is discarded when the paren closes`() { + // `<` pushed after `(` must not be left on the stack for later `>` + assertThat(candidates("x = (a < b) c > (d)")).isEmpty() + } + + @Test + fun `stray closing bracket does not break the scan`() { + assertThat(candidates(") x = foo(5)")).isEqualTo(intArrayOf(9)) + } + + // string interpolation + + @Test + fun `comparison inside string interpolation is not a candidate`() { + assertThat(candidates("""x = "\(a < b)" > (c)""")).isEmpty() + } + + @Test + fun `method call inside string interpolation`() { + assertThat(candidates("""x = "hello \(foo(1)) world"""")).isEqualTo(intArrayOf(16)) + } + + @Test + fun `greater-than inside string interpolation does not close an outer less-than`() { + assertThat(candidates("""x = foo<"\(a > b)">(5)""")).isEqualTo(intArrayOf(7)) + } + + @Test + fun `nested string interpolation`() { + assertThat(candidates("""x = "\("\(foo(1))")"""")).isEqualTo(intArrayOf(13)) + } + + @Test + fun `multiline string with interpolation`() { + val source = "x = \"\"\"\n \\(foo(1))\n \"\"\"" + assertThat(candidates(source)).isEqualTo(intArrayOf(15)) + } + + @Test + fun `angle brackets in string literal text are ignored`() { + assertThat(candidates("""x = "a < b > (c)"""")).isEmpty() + } + + // affixes between the closing angle bracket and open paren + + @Test + fun `comments between closing angle bracket and open paren`() { + assertThat(candidates("x = foo /* c */ (5)")).isEqualTo(intArrayOf(7)) + assertThat(candidates("x = foo // c\n(5)")).isEqualTo(intArrayOf(7)) + } + + @Test + fun `newline and semicolon before open paren - parser re-checks these`() { + // these are intentionally lenient; `ParserImpl` and `GenericParserImpl` reject them. + assertThat(candidates("x = foo\n(5)")).isEqualTo(intArrayOf(7)) + assertThat(candidates("x = foo; (5)")).isEqualTo(intArrayOf(7)) + } + + // robustness + + @Test + fun `lexing error ends scan without throwing`() { + // candidates found before the error are retained + assertThat(candidates("x = foo(1)\ny = \u0001")).isEqualTo(intArrayOf(7)) + // `<` still open when the error occurs is not a candidate + assertThat(candidates("x = foo(1)")).isEmpty() + } + + @Test + fun `deeply nested angle brackets`() { + val depth = 1000 + val source = "x = foo" + "".repeat(depth) + "(1)" + // only the outermost `<` has a `>` that is followed by `(` + assertThat(candidates(source)).isEqualTo(intArrayOf(7)) + } + + @Test + fun `many nested brackets`() { + val depth = 1000 + val source = "x = " + "(".repeat(depth) + "a < b" + ")".repeat(depth) + " > (c)" + assertThat(candidates(source)).isEmpty() + } + + @Test + fun `triply nested brackets`() { + val source = "x = foo < bar < baz < biz >() >() >()" + assertThat(candidates(source)).isEqualTo(intArrayOf(8, 14, 20)) + } + + @Test + fun `triply nested brackets followed by more brackets later`() { + val source = + """ + x = foo < bar < baz < biz > () > () > () + y = foo < bar > () + """ + .trimIndent() + assertThat(candidates(source)).isEqualTo(intArrayOf(8, 14, 20, 49)) + } + + @Test + fun `nested candidate within a non-candidate`() { + val source = + """ + x = a < (foo < A > (1)) > b + // ^ this is the candidate + y = bar(2) + """ + .trimIndent() + assertThat(candidates(source)).isEqualTo(intArrayOf(13, 77)) + } + + @Test + fun `sibling candidates within brackets`() { + val source = + """ + x = a < foo < A > (), bar < B > () > b + // ^ this is the candidate + y = bar(2) + """ + .trimIndent() + assertThat(candidates(source)).isEqualTo(intArrayOf(12, 26, 88)) + } +} diff --git a/pkl-parser/src/test/resources/org/pkl/parser/large_backtracking.pkl.txt b/pkl-parser/src/test/resources/org/pkl/parser/large_backtracking.pkl.txt new file mode 100644 index 000000000..e081b11e0 --- /dev/null +++ b/pkl-parser/src/test/resources/org/pkl/parser/large_backtracking.pkl.txt @@ -0,0 +1 @@ +result = a(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0)>(0) diff --git a/stdlib/ref.pkl b/stdlib/ref.pkl index 6793573a1..ad1ec4bd8 100644 --- a/stdlib/ref.pkl +++ b/stdlib/ref.pkl @@ -35,7 +35,7 @@ module pkl.ref /// /// Example: /// ``` -/// myReference: Reference> = Reference::>(domain, data) +/// myReference: Reference> = Reference>(domain, data) /// ``` external const function Reference( domain: Domain(this is AbstractDomain), @@ -153,7 +153,7 @@ external const function Reference( /// properties: Mapping /// } /// -/// local outputs: FooReference = ref.Reference::(new Foo {}, "outputs") +/// local outputs: FooReference = ref.Reference(new Foo {}, "outputs") /// /// // Dot access; gives a `Reference` /// myFile = outputs.file diff --git a/stdlib/syntax.pkl b/stdlib/syntax.pkl index 68ddea3e3..5466a4afc 100644 --- a/stdlib/syntax.pkl +++ b/stdlib/syntax.pkl @@ -305,7 +305,6 @@ typealias TerminalNodeType = | "assign" | "at" | "colon" - | "dcolon" | "comma" | "dot" | "gt"