diff --git a/pkl-core/pkl-core.gradle.kts b/pkl-core/pkl-core.gradle.kts index 5d2b2920d..762818ef8 100644 --- a/pkl-core/pkl-core.gradle.kts +++ b/pkl-core/pkl-core.gradle.kts @@ -59,6 +59,7 @@ dependencies { compileOnly(projects.pklExecutor) implementation(projects.pklParser) + implementation(projects.pklFormatter) implementation(libs.msgpack) implementation(libs.truffleApi) implementation(libs.graalSdk) diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java b/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java index 9009ccf17..1c799e8b4 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java @@ -172,6 +172,18 @@ public final class Identifier implements Comparable { // common in lambdas etc public static final Identifier IT = get("it"); + // members of pkl.syntax#Node, pkl.syntax#Span and pkl.syntax#SourceLocation + public static final Identifier TYPE = get("type"); + public static final Identifier CHILDREN = get("children"); + public static final Identifier SPAN = get("span"); + public static final Identifier START = get("start"); + public static final Identifier END = get("end"); + public static final Identifier LINE = get("line"); + public static final Identifier COLUMN = get("column"); + + // members of pkl.syntax#Renderer + public static final Identifier GRAMMAR_VERSION = get("grammarVersion"); + private final String name; private Identifier(String name) { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java index 3467a381c..b5060019c 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java @@ -115,6 +115,8 @@ public synchronized VmTyped getOrLoad( case "settings": // always needed if ~/.pkl/settings.pkl is present return SettingsModule.getModule(); + case "syntax": + return SyntaxModule.getModule(); case "test": return TestModule.getModule(); case "xml": diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/SyntaxModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/SyntaxModule.java new file mode 100644 index 000000000..3b4e4a4e3 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/SyntaxModule.java @@ -0,0 +1,733 @@ +/* + * Copyright © 2025-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.core.runtime; + +import com.oracle.truffle.api.CompilerDirectives; +import java.net.URI; + +public final class SyntaxModule extends StdLibModule { + private static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(URI.create("pkl:syntax"), instance); + } + + public static VmTyped getModule() { + return instance; + } + + public static VmClass getGenericNodeClass() { + return GenericNodeClass.instance; + } + + public static VmClass getSpanClass() { + return SpanClass.instance; + } + + public static VmClass getSourceLocationClass() { + return SourceLocationClass.instance; + } + + public static VmClass getModuleNodeClass() { + return ModuleNodeClass.instance; + } + + public static VmClass getModuleDeclarationNodeClass() { + return ModuleDeclarationNodeClass.instance; + } + + public static VmClass getExtendsOrAmendsClauseNodeClass() { + return ExtendsOrAmendsClauseNodeClass.instance; + } + + public static VmClass getImportNodeClass() { + return ImportNodeClass.instance; + } + + public static VmClass getIdentifierNodeClass() { + return IdentifierNodeClass.instance; + } + + public static VmClass getQualifiedIdentifierNodeClass() { + return QualifiedIdentifierNodeClass.instance; + } + + public static VmClass getDocCommentNodeClass() { + return DocCommentNodeClass.instance; + } + + public static VmClass getAnnotationNodeClass() { + return AnnotationNodeClass.instance; + } + + public static VmClass getClassNodeClass() { + return ClassNodeClass.instance; + } + + public static VmClass getTypeAliasNodeClass() { + return TypeAliasNodeClass.instance; + } + + public static VmClass getClassBodyNodeClass() { + return ClassBodyNodeClass.instance; + } + + public static VmClass getClassPropertyNodeClass() { + return ClassPropertyNodeClass.instance; + } + + public static VmClass getClassMethodNodeClass() { + return ClassMethodNodeClass.instance; + } + + public static VmClass getObjectBodyNodeClass() { + return ObjectBodyNodeClass.instance; + } + + public static VmClass getParameterNodeClass() { + return ParameterNodeClass.instance; + } + + public static VmClass getObjectPropertyNodeClass() { + return ObjectPropertyNodeClass.instance; + } + + public static VmClass getObjectMethodNodeClass() { + return ObjectMethodNodeClass.instance; + } + + public static VmClass getMemberPredicateNodeClass() { + return MemberPredicateNodeClass.instance; + } + + public static VmClass getObjectEntryNodeClass() { + return ObjectEntryNodeClass.instance; + } + + public static VmClass getObjectSpreadNodeClass() { + return ObjectSpreadNodeClass.instance; + } + + public static VmClass getWhenGeneratorNodeClass() { + return WhenGeneratorNodeClass.instance; + } + + public static VmClass getForGeneratorNodeClass() { + return ForGeneratorNodeClass.instance; + } + + public static VmClass getStringCharsNodeClass() { + return StringCharsNodeClass.instance; + } + + public static VmClass getStringEscapeNodeClass() { + return StringEscapeNodeClass.instance; + } + + public static VmClass getStringNewlineNodeClass() { + return StringNewlineNodeClass.instance; + } + + public static VmClass getStringInterpolationNodeClass() { + return StringInterpolationNodeClass.instance; + } + + public static VmClass getTypeParameterNodeClass() { + return TypeParameterNodeClass.instance; + } + + public static VmClass getUnknownTypeNodeClass() { + return UnknownTypeNodeClass.instance; + } + + public static VmClass getNothingTypeNodeClass() { + return NothingTypeNodeClass.instance; + } + + public static VmClass getModuleTypeNodeClass() { + return ModuleTypeNodeClass.instance; + } + + public static VmClass getDeclaredTypeNodeClass() { + return DeclaredTypeNodeClass.instance; + } + + public static VmClass getNullableTypeNodeClass() { + return NullableTypeNodeClass.instance; + } + + public static VmClass getUnionTypeNodeClass() { + return UnionTypeNodeClass.instance; + } + + public static VmClass getFunctionTypeNodeClass() { + return FunctionTypeNodeClass.instance; + } + + public static VmClass getConstrainedTypeNodeClass() { + return ConstrainedTypeNodeClass.instance; + } + + public static VmClass getParenthesizedTypeNodeClass() { + return ParenthesizedTypeNodeClass.instance; + } + + public static VmClass getStringLiteralTypeNodeClass() { + return StringLiteralTypeNodeClass.instance; + } + + public static VmClass getExprNodeClass() { + return ExprNodeClass.instance; + } + + public static VmClass getThisExprNodeClass() { + return ThisExprNodeClass.instance; + } + + public static VmClass getOuterExprNodeClass() { + return OuterExprNodeClass.instance; + } + + public static VmClass getModuleExprNodeClass() { + return ModuleExprNodeClass.instance; + } + + public static VmClass getNullLiteralExprNodeClass() { + return NullLiteralExprNodeClass.instance; + } + + public static VmClass getBooleanLiteralExprNodeClass() { + return BooleanLiteralExprNodeClass.instance; + } + + public static VmClass getIntLiteralExprNodeClass() { + return IntLiteralExprNodeClass.instance; + } + + public static VmClass getFloatLiteralExprNodeClass() { + return FloatLiteralExprNodeClass.instance; + } + + public static VmClass getSingleLineStringLiteralExprNodeClass() { + return SingleLineStringLiteralExprNodeClass.instance; + } + + public static VmClass getMultiLineStringLiteralExprNodeClass() { + return MultiLineStringLiteralExprNodeClass.instance; + } + + public static VmClass getUnqualifiedAccessExprNodeClass() { + return UnqualifiedAccessExprNodeClass.instance; + } + + public static VmClass getQualifiedAccessExprNodeClass() { + return QualifiedAccessExprNodeClass.instance; + } + + public static VmClass getSubscriptExprNodeClass() { + return SubscriptExprNodeClass.instance; + } + + public static VmClass getSuperAccessExprNodeClass() { + return SuperAccessExprNodeClass.instance; + } + + public static VmClass getSuperSubscriptExprNodeClass() { + return SuperSubscriptExprNodeClass.instance; + } + + public static VmClass getIfExprNodeClass() { + return IfExprNodeClass.instance; + } + + public static VmClass getLetExprNodeClass() { + return LetExprNodeClass.instance; + } + + public static VmClass getThrowExprNodeClass() { + return ThrowExprNodeClass.instance; + } + + public static VmClass getTraceExprNodeClass() { + return TraceExprNodeClass.instance; + } + + public static VmClass getImportExprNodeClass() { + return ImportExprNodeClass.instance; + } + + public static VmClass getReadExprNodeClass() { + return ReadExprNodeClass.instance; + } + + public static VmClass getNewExprNodeClass() { + return NewExprNodeClass.instance; + } + + public static VmClass getAmendsExprNodeClass() { + return AmendsExprNodeClass.instance; + } + + public static VmClass getExponentiationExprNodeClass() { + return ExponentiationExprNodeClass.instance; + } + + public static VmClass getMultiplicationExprNodeClass() { + return MultiplicationExprNodeClass.instance; + } + + public static VmClass getDivisionExprNodeClass() { + return DivisionExprNodeClass.instance; + } + + public static VmClass getIntegerDivisionExprNodeClass() { + return IntegerDivisionExprNodeClass.instance; + } + + public static VmClass getRemainderExprNodeClass() { + return RemainderExprNodeClass.instance; + } + + public static VmClass getAdditionExprNodeClass() { + return AdditionExprNodeClass.instance; + } + + public static VmClass getSubtractionExprNodeClass() { + return SubtractionExprNodeClass.instance; + } + + public static VmClass getLessThanExprNodeClass() { + return LessThanExprNodeClass.instance; + } + + public static VmClass getLessThanOrEqualExprNodeClass() { + return LessThanOrEqualExprNodeClass.instance; + } + + public static VmClass getGreaterThanExprNodeClass() { + return GreaterThanExprNodeClass.instance; + } + + public static VmClass getGreaterThanOrEqualExprNodeClass() { + return GreaterThanOrEqualExprNodeClass.instance; + } + + public static VmClass getEqualExprNodeClass() { + return EqualExprNodeClass.instance; + } + + public static VmClass getNotEqualExprNodeClass() { + return NotEqualExprNodeClass.instance; + } + + public static VmClass getLogicalAndExprNodeClass() { + return LogicalAndExprNodeClass.instance; + } + + public static VmClass getLogicalOrExprNodeClass() { + return LogicalOrExprNodeClass.instance; + } + + public static VmClass getPipeExprNodeClass() { + return PipeExprNodeClass.instance; + } + + public static VmClass getNullCoalescingExprNodeClass() { + return NullCoalescingExprNodeClass.instance; + } + + public static VmClass getTypeCheckExprNodeClass() { + return TypeCheckExprNodeClass.instance; + } + + public static VmClass getTypeCastExprNodeClass() { + return TypeCastExprNodeClass.instance; + } + + public static VmClass getUnaryMinusExprNodeClass() { + return UnaryMinusExprNodeClass.instance; + } + + public static VmClass getLogicalNotExprNodeClass() { + return LogicalNotExprNodeClass.instance; + } + + public static VmClass getNonNullExprNodeClass() { + return NonNullExprNodeClass.instance; + } + + public static VmClass getFunctionLiteralExprNodeClass() { + return FunctionLiteralExprNodeClass.instance; + } + + public static VmClass getParenthesizedExprNodeClass() { + return ParenthesizedExprNodeClass.instance; + } + + private static final class GenericNodeClass { + static final VmClass instance = loadClass("GenericNode"); + } + + private static final class SpanClass { + static final VmClass instance = loadClass("Span"); + } + + private static final class SourceLocationClass { + static final VmClass instance = loadClass("SourceLocation"); + } + + private static final class ModuleNodeClass { + static final VmClass instance = loadClass("ModuleNode"); + } + + private static final class ModuleDeclarationNodeClass { + static final VmClass instance = loadClass("ModuleDeclarationNode"); + } + + private static final class ExtendsOrAmendsClauseNodeClass { + static final VmClass instance = loadClass("ExtendsOrAmendsClauseNode"); + } + + private static final class ImportNodeClass { + static final VmClass instance = loadClass("ImportNode"); + } + + private static final class IdentifierNodeClass { + static final VmClass instance = loadClass("IdentifierNode"); + } + + private static final class QualifiedIdentifierNodeClass { + static final VmClass instance = loadClass("QualifiedIdentifierNode"); + } + + private static final class DocCommentNodeClass { + static final VmClass instance = loadClass("DocCommentNode"); + } + + private static final class AnnotationNodeClass { + static final VmClass instance = loadClass("AnnotationNode"); + } + + private static final class ClassNodeClass { + static final VmClass instance = loadClass("ClassNode"); + } + + private static final class TypeAliasNodeClass { + static final VmClass instance = loadClass("TypeAliasNode"); + } + + private static final class ClassBodyNodeClass { + static final VmClass instance = loadClass("ClassBodyNode"); + } + + private static final class ClassPropertyNodeClass { + static final VmClass instance = loadClass("ClassPropertyNode"); + } + + private static final class ClassMethodNodeClass { + static final VmClass instance = loadClass("ClassMethodNode"); + } + + private static final class ObjectBodyNodeClass { + static final VmClass instance = loadClass("ObjectBodyNode"); + } + + private static final class ParameterNodeClass { + static final VmClass instance = loadClass("ParameterNode"); + } + + private static final class ObjectPropertyNodeClass { + static final VmClass instance = loadClass("ObjectPropertyNode"); + } + + private static final class ObjectMethodNodeClass { + static final VmClass instance = loadClass("ObjectMethodNode"); + } + + private static final class MemberPredicateNodeClass { + static final VmClass instance = loadClass("MemberPredicateNode"); + } + + private static final class ObjectEntryNodeClass { + static final VmClass instance = loadClass("ObjectEntryNode"); + } + + private static final class ObjectSpreadNodeClass { + static final VmClass instance = loadClass("ObjectSpreadNode"); + } + + private static final class WhenGeneratorNodeClass { + static final VmClass instance = loadClass("WhenGeneratorNode"); + } + + private static final class ForGeneratorNodeClass { + static final VmClass instance = loadClass("ForGeneratorNode"); + } + + private static final class StringCharsNodeClass { + static final VmClass instance = loadClass("StringCharsNode"); + } + + private static final class StringEscapeNodeClass { + static final VmClass instance = loadClass("StringEscapeNode"); + } + + private static final class StringNewlineNodeClass { + static final VmClass instance = loadClass("StringNewlineNode"); + } + + private static final class StringInterpolationNodeClass { + static final VmClass instance = loadClass("StringInterpolationNode"); + } + + private static final class TypeParameterNodeClass { + static final VmClass instance = loadClass("TypeParameterNode"); + } + + private static final class UnknownTypeNodeClass { + static final VmClass instance = loadClass("UnknownTypeNode"); + } + + private static final class NothingTypeNodeClass { + static final VmClass instance = loadClass("NothingTypeNode"); + } + + private static final class ModuleTypeNodeClass { + static final VmClass instance = loadClass("ModuleTypeNode"); + } + + private static final class DeclaredTypeNodeClass { + static final VmClass instance = loadClass("DeclaredTypeNode"); + } + + private static final class NullableTypeNodeClass { + static final VmClass instance = loadClass("NullableTypeNode"); + } + + private static final class UnionTypeNodeClass { + static final VmClass instance = loadClass("UnionTypeNode"); + } + + private static final class FunctionTypeNodeClass { + static final VmClass instance = loadClass("FunctionTypeNode"); + } + + private static final class ConstrainedTypeNodeClass { + static final VmClass instance = loadClass("ConstrainedTypeNode"); + } + + private static final class ParenthesizedTypeNodeClass { + static final VmClass instance = loadClass("ParenthesizedTypeNode"); + } + + private static final class StringLiteralTypeNodeClass { + static final VmClass instance = loadClass("StringLiteralTypeNode"); + } + + private static final class ExprNodeClass { + static final VmClass instance = loadClass("ExprNode"); + } + + private static final class ThisExprNodeClass { + static final VmClass instance = loadClass("ThisExprNode"); + } + + private static final class OuterExprNodeClass { + static final VmClass instance = loadClass("OuterExprNode"); + } + + private static final class ModuleExprNodeClass { + static final VmClass instance = loadClass("ModuleExprNode"); + } + + private static final class NullLiteralExprNodeClass { + static final VmClass instance = loadClass("NullLiteralExprNode"); + } + + private static final class BooleanLiteralExprNodeClass { + static final VmClass instance = loadClass("BooleanLiteralExprNode"); + } + + private static final class IntLiteralExprNodeClass { + static final VmClass instance = loadClass("IntLiteralExprNode"); + } + + private static final class FloatLiteralExprNodeClass { + static final VmClass instance = loadClass("FloatLiteralExprNode"); + } + + private static final class SingleLineStringLiteralExprNodeClass { + static final VmClass instance = loadClass("SingleLineStringLiteralExprNode"); + } + + private static final class MultiLineStringLiteralExprNodeClass { + static final VmClass instance = loadClass("MultiLineStringLiteralExprNode"); + } + + private static final class UnqualifiedAccessExprNodeClass { + static final VmClass instance = loadClass("UnqualifiedAccessExprNode"); + } + + private static final class QualifiedAccessExprNodeClass { + static final VmClass instance = loadClass("QualifiedAccessExprNode"); + } + + private static final class SubscriptExprNodeClass { + static final VmClass instance = loadClass("SubscriptExprNode"); + } + + private static final class SuperAccessExprNodeClass { + static final VmClass instance = loadClass("SuperAccessExprNode"); + } + + private static final class SuperSubscriptExprNodeClass { + static final VmClass instance = loadClass("SuperSubscriptExprNode"); + } + + private static final class IfExprNodeClass { + static final VmClass instance = loadClass("IfExprNode"); + } + + private static final class LetExprNodeClass { + static final VmClass instance = loadClass("LetExprNode"); + } + + private static final class ThrowExprNodeClass { + static final VmClass instance = loadClass("ThrowExprNode"); + } + + private static final class TraceExprNodeClass { + static final VmClass instance = loadClass("TraceExprNode"); + } + + private static final class ImportExprNodeClass { + static final VmClass instance = loadClass("ImportExprNode"); + } + + private static final class ReadExprNodeClass { + static final VmClass instance = loadClass("ReadExprNode"); + } + + private static final class NewExprNodeClass { + static final VmClass instance = loadClass("NewExprNode"); + } + + private static final class AmendsExprNodeClass { + static final VmClass instance = loadClass("AmendsExprNode"); + } + + private static final class ExponentiationExprNodeClass { + static final VmClass instance = loadClass("ExponentiationExprNode"); + } + + private static final class MultiplicationExprNodeClass { + static final VmClass instance = loadClass("MultiplicationExprNode"); + } + + private static final class DivisionExprNodeClass { + static final VmClass instance = loadClass("DivisionExprNode"); + } + + private static final class IntegerDivisionExprNodeClass { + static final VmClass instance = loadClass("IntegerDivisionExprNode"); + } + + private static final class RemainderExprNodeClass { + static final VmClass instance = loadClass("RemainderExprNode"); + } + + private static final class AdditionExprNodeClass { + static final VmClass instance = loadClass("AdditionExprNode"); + } + + private static final class SubtractionExprNodeClass { + static final VmClass instance = loadClass("SubtractionExprNode"); + } + + private static final class LessThanExprNodeClass { + static final VmClass instance = loadClass("LessThanExprNode"); + } + + private static final class LessThanOrEqualExprNodeClass { + static final VmClass instance = loadClass("LessThanOrEqualExprNode"); + } + + private static final class GreaterThanExprNodeClass { + static final VmClass instance = loadClass("GreaterThanExprNode"); + } + + private static final class GreaterThanOrEqualExprNodeClass { + static final VmClass instance = loadClass("GreaterThanOrEqualExprNode"); + } + + private static final class EqualExprNodeClass { + static final VmClass instance = loadClass("EqualExprNode"); + } + + private static final class NotEqualExprNodeClass { + static final VmClass instance = loadClass("NotEqualExprNode"); + } + + private static final class LogicalAndExprNodeClass { + static final VmClass instance = loadClass("LogicalAndExprNode"); + } + + private static final class LogicalOrExprNodeClass { + static final VmClass instance = loadClass("LogicalOrExprNode"); + } + + private static final class PipeExprNodeClass { + static final VmClass instance = loadClass("PipeExprNode"); + } + + private static final class NullCoalescingExprNodeClass { + static final VmClass instance = loadClass("NullCoalescingExprNode"); + } + + private static final class TypeCheckExprNodeClass { + static final VmClass instance = loadClass("TypeCheckExprNode"); + } + + private static final class TypeCastExprNodeClass { + static final VmClass instance = loadClass("TypeCastExprNode"); + } + + private static final class UnaryMinusExprNodeClass { + static final VmClass instance = loadClass("UnaryMinusExprNode"); + } + + private static final class LogicalNotExprNodeClass { + static final VmClass instance = loadClass("LogicalNotExprNode"); + } + + private static final class NonNullExprNodeClass { + static final VmClass instance = loadClass("NonNullExprNode"); + } + + private static final class FunctionLiteralExprNodeClass { + static final VmClass instance = loadClass("FunctionLiteralExprNode"); + } + + private static final class ParenthesizedExprNodeClass { + static final VmClass instance = loadClass("ParenthesizedExprNode"); + } + + @CompilerDirectives.TruffleBoundary + private static VmClass loadClass(String className) { + var theModule = getModule(); + return (VmClass) VmUtils.readMember(theModule, Identifier.get(className)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/VmObjectFactory.java b/pkl-core/src/main/java/org/pkl/core/stdlib/VmObjectFactory.java index 377a86709..dadb8e0ae 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/VmObjectFactory.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/VmObjectFactory.java @@ -68,6 +68,10 @@ public VmObjectFactory addListProperty(String name, Property impl) return doAddProperty(name, new PropertyNode<>(impl)); } + public VmObjectFactory addListingProperty(String name, Property impl) { + return doAddProperty(name, new PropertyNode<>(impl)); + } + public VmObjectFactory addSetProperty(String name, Property impl) { return doAddProperty(name, new PropertyNode<>(impl)); } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/GenericNodeNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/GenericNodeNodes.java new file mode 100644 index 000000000..d8f214f7d --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/GenericNodeNodes.java @@ -0,0 +1,108 @@ +/* + * 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.core.stdlib.syntax; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import java.util.ArrayDeque; +import org.pkl.core.ast.lambda.ApplyVmFunction1Node; +import org.pkl.core.ast.lambda.ApplyVmFunction2Node; +import org.pkl.core.ast.lambda.ApplyVmFunction2NodeGen; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmFunction; +import org.pkl.core.runtime.VmList; +import org.pkl.core.runtime.VmPair; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.stdlib.ExternalMethod1Node; +import org.pkl.core.stdlib.ExternalMethod2Node; +import org.pkl.core.stdlib.syntax.SyntaxNodes.GenericNodeData; + +/** Backs {@code pkl.syntax#GenericNode.fold} and {@code pkl.syntax#GenericNode.transform}. */ +public final class GenericNodeNodes { + private GenericNodeNodes() {} + + public abstract static class fold extends ExternalMethod2Node { + @Child private ApplyVmFunction2Node applyAccumulate = ApplyVmFunction2NodeGen.create(); + + @Specialization + @TruffleBoundary + protected Object eval(VmTyped self, Object initial, VmFunction operator) { + var pending = new ArrayDeque(); + pending.push(self); + var result = initial; + while (!pending.isEmpty()) { + var node = pending.pop(); + result = applyAccumulate.execute(operator, result, node); + var children = (VmList) VmUtils.readMember(node, Identifier.CHILDREN); + for (var i = children.getLength() - 1; i >= 0; i--) { + pending.push((VmTyped) children.get(i)); + } + } + return result; + } + } + + public abstract static class transform extends ExternalMethod1Node { + @Child private ApplyVmFunction1Node applyOperator = ApplyVmFunction1Node.create(); + + @Specialization + @TruffleBoundary + protected VmTyped eval(VmTyped self, VmFunction operator) { + var result = transformNode(self, operator); + // the root of the returned tree has no parent + if (result.hasExtraStorage()) { + ((GenericNodeData) result.getExtraStorage()).parentVm = null; + } + return result; + } + + private VmTyped transformNode(VmTyped nodeVm, VmFunction operator) { + var transformed = applyOperator.execute(operator, nodeVm); + + VmTyped node; + boolean descend; + if (transformed instanceof VmPair pair) { + node = (VmTyped) pair.getFirst(); + descend = (Boolean) pair.getSecond(); + } else { + // `null`: leave this node unchanged and keep descending + node = nodeVm; + descend = true; + } + if (!descend) { + return node; + } + + var childrenVm = (VmList) VmUtils.readMember(node, Identifier.CHILDREN); + var length = childrenVm.getLength(); + if (length == 0) { + return node; + } + + var newChildren = new Object[length]; + var changed = false; + for (var i = 0; i < length; i++) { + var child = (VmTyped) childrenVm.get(i); + var newChild = transformNode(child, operator); + newChildren[i] = newChild; + changed |= newChild != child; + } + // reuse the node (and its extra storage) untouched when nothing below changed + return changed ? SyntaxNodes.rebuild(node, newChildren) : node; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/NodeNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/NodeNodes.java new file mode 100644 index 000000000..10e04ad52 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/NodeNodes.java @@ -0,0 +1,911 @@ +/* + * 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.core.stdlib.syntax; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import java.util.ArrayList; +import java.util.List; +import org.jspecify.annotations.Nullable; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.SyntaxModule; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.runtime.VmList; +import org.pkl.core.runtime.VmListing; +import org.pkl.core.runtime.VmObjectBuilder; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.stdlib.ExternalPropertyNode; +import org.pkl.core.stdlib.PklName; +import org.pkl.core.stdlib.syntax.SyntaxNodes.SpanData; +import org.pkl.parser.Lexer; +import org.pkl.parser.syntax.generic.FullSpan; + +public final class NodeNodes { + private NodeNodes() {} + + @PklName("builtNode") + public abstract static class builtNode extends ExternalPropertyNode { + @Specialization + @TruffleBoundary + protected Object eval(VmTyped self) { + return build(self); + } + } + + private static VmTyped build(VmTyped self) { + if (self.hasExtraStorage() && self.getExtraStorage() instanceof VmTyped genericNode) { + return genericNode; + } + return switch (self.getVmClass().getSimpleName()) { + case "ModuleNode" -> buildModule(self); + case "ModuleDeclarationNode" -> buildModuleDeclaration(self); + case "ExtendsOrAmendsClauseNode" -> { + var keyword = str(self, "keyword"); + yield branch( + keyword.equals("amends") ? "amends_clause" : "extends_clause", + List.of(terminal(keyword), stringCharsNode(str(self, "uri")))); + } + case "ImportNode" -> buildImport(self); + case "ClassNode" -> buildClass(self); + case "TypeAliasNode" -> buildTypeAlias(self); + case "ClassBodyNode" -> buildClassBody(self); + case "ClassPropertyNode" -> buildClassProperty(self); + case "ClassMethodNode" -> buildClassMethod(self); + case "ObjectBodyNode" -> buildObjectBody(self); + case "ObjectPropertyNode" -> buildObjectProperty(self); + case "ObjectMethodNode" -> buildObjectMethod(self); + case "ObjectEntryNode" -> buildObjectEntry(self); + case "ObjectSpreadNode" -> + branch( + "object_spread", + List.of(terminal(str(self, "keyword")), build(reqNode(self, "expression")))); + case "MemberPredicateNode" -> buildMemberPredicate(self); + case "ForGeneratorNode" -> buildForGenerator(self); + case "WhenGeneratorNode" -> buildWhenGenerator(self); + case "ThisExprNode" -> leaf("this_expr", "this"); + case "OuterExprNode" -> leaf("outer_expr", "outer"); + case "ModuleExprNode" -> leaf("module_expr", "module"); + case "NullLiteralExprNode" -> leaf("null_expr", "null"); + case "BooleanLiteralExprNode" -> + leaf("bool_literal_expr", Boolean.toString(bool(self, "value"))); + case "IntLiteralExprNode" -> leaf("int_literal_expr", numText(member(self, "value"))); + case "FloatLiteralExprNode" -> leaf("float_literal_expr", numText(member(self, "value"))); + case "SingleLineStringLiteralExprNode" -> buildSingleLineString(self); + case "MultiLineStringLiteralExprNode" -> buildMultiLineString(self); + case "UnqualifiedAccessExprNode" -> buildUnqualifiedAccess(self); + case "QualifiedAccessExprNode" -> buildQualifiedAccess(self); + case "SubscriptExprNode" -> + branch( + "subscript_expr", + List.of( + build(reqNode(self, "receiver")), + operatorLeaf("["), + build(reqNode(self, "index")), + terminal("]"))); + case "SuperAccessExprNode" -> buildSuperAccess(self); + case "SuperSubscriptExprNode" -> + branch( + "super_subscript_expr", + List.of( + terminal("super"), terminal("["), build(reqNode(self, "index")), terminal("]"))); + case "IfExprNode" -> buildIf(self); + case "LetExprNode" -> buildLet(self); + case "ThrowExprNode" -> buildCall("throw_expr", "throw", build(reqNode(self, "expression"))); + case "TraceExprNode" -> buildCall("trace_expr", "trace", build(reqNode(self, "expression"))); + case "ImportExprNode" -> + buildCall("import_expr", str(self, "keyword"), stringCharsNode(str(self, "uri"))); + case "ReadExprNode" -> + buildCall("read_expr", str(self, "keyword"), build(reqNode(self, "expression"))); + case "NewExprNode" -> buildNew(self); + case "AmendsExprNode" -> + branch( + "amends_expr", + List.of(build(reqNode(self, "parentExpr")), build(reqNode(self, "body")))); + case "ExponentiationExprNode" -> buildBinaryOp(self, "**"); + case "MultiplicationExprNode" -> buildBinaryOp(self, "*"); + case "DivisionExprNode" -> buildBinaryOp(self, "/"); + case "IntegerDivisionExprNode" -> buildBinaryOp(self, "~/"); + case "RemainderExprNode" -> buildBinaryOp(self, "%"); + case "AdditionExprNode" -> buildBinaryOp(self, "+"); + case "SubtractionExprNode" -> buildBinaryOp(self, "-"); + case "LessThanExprNode" -> buildBinaryOp(self, "<"); + case "LessThanOrEqualExprNode" -> buildBinaryOp(self, "<="); + case "GreaterThanExprNode" -> buildBinaryOp(self, ">"); + case "GreaterThanOrEqualExprNode" -> buildBinaryOp(self, ">="); + case "EqualExprNode" -> buildBinaryOp(self, "=="); + case "NotEqualExprNode" -> buildBinaryOp(self, "!="); + case "LogicalAndExprNode" -> buildBinaryOp(self, "&&"); + case "LogicalOrExprNode" -> buildBinaryOp(self, "||"); + case "PipeExprNode" -> buildBinaryOp(self, "|>"); + case "NullCoalescingExprNode" -> buildBinaryOp(self, "??"); + case "TypeCheckExprNode" -> buildTypeOp(self, "is"); + case "TypeCastExprNode" -> buildTypeOp(self, "as"); + case "UnaryMinusExprNode" -> + branch("unary_minus_expr", List.of(terminal("-"), build(reqNode(self, "operand")))); + case "LogicalNotExprNode" -> + branch("logical_not_expr", List.of(terminal("!"), build(reqNode(self, "operand")))); + case "NonNullExprNode" -> + branch("non_null_expr", List.of(build(reqNode(self, "operand")), operatorLeaf("!!"))); + case "FunctionLiteralExprNode" -> buildFunctionLiteral(self); + case "ParenthesizedExprNode" -> + branch( + "parenthesized_expr", + List.of( + terminal("("), + branch( + "parenthesized_expr_elements", List.of(build(reqNode(self, "expression")))), + terminal(")"))); + case "UnknownTypeNode" -> leaf("unknown_type", "unknown"); + case "NothingTypeNode" -> leaf("nothing_type", "nothing"); + case "ModuleTypeNode" -> leaf("module_type", "module"); + case "DeclaredTypeNode" -> buildDeclaredType(self); + case "NullableTypeNode" -> + branch("nullable_type", List.of(build(reqNode(self, "baseType")), terminal("?"))); + case "UnionTypeNode" -> + branch( + "union_type", interleave(buildAll(listMember(self, "members")), () -> terminal("|"))); + case "FunctionTypeNode" -> buildFunctionType(self); + case "ConstrainedTypeNode" -> buildConstrainedType(self); + case "ParenthesizedTypeNode" -> + branch( + "parenthesized_type", + List.of( + terminal("("), + branch("parenthesized_type_elements", List.of(build(reqNode(self, "type")))), + terminal(")"))); + case "StringLiteralTypeNode" -> + branch("string_constant_type", List.of(stringCharsNode(str(self, "value")))); + case "AnnotationNode" -> buildAnnotation(self); + case "ParameterNode" -> buildParameter(self); + case "TypeParameterNode" -> buildTypeParameter(self); + case "IdentifierNode" -> leaf("identifier", identifierText(self)); + case "QualifiedIdentifierNode" -> + branch( + "qualified_identifier", + interleave(buildAll(listMember(self, "identifiers")), () -> terminal("."))); + case "DocCommentNode" -> buildDocComment(self); + default -> + throw new VmExceptionBuilder() + .bug("Unexpected syntax node: " + self.getVmClass().getSimpleName()) + .build(); + }; + } + + private static VmTyped buildModule(VmTyped self) { + var children = new ArrayList<>(); + var declaration = optNode(self, "declaration"); + if (declaration != null) { + children.add(build(declaration)); + } + var imports = listMember(self, "imports"); + if (!imports.isEmpty()) { + children.add(branch("import_list", buildAll(imports))); + } + children.addAll(buildAll(listMember(self, "members"))); + return branch("module", children); + } + + private static VmTyped buildModuleDeclaration(VmTyped self) { + var children = docAndAnnotations(self); + var name = optNode(self, "name"); + var modifiers = listMember(self, "modifiers"); + if (name != null) { + var definition = new ArrayList<>(); + if (!modifiers.isEmpty()) { + definition.add(modifierListNode(modifiers)); + } + definition.add(terminal("module")); + definition.add(build(name)); + children.add(branch("module_definition", definition)); + } else if (!modifiers.isEmpty()) { + children.add(modifierListNode(modifiers)); + } + var clause = optNode(self, "extendsOrAmendsClause"); + if (clause != null) { + children.add(build(clause)); + } + return branch("module_declaration", children); + } + + private static VmTyped buildImport(VmTyped self) { + var children = new ArrayList<>(); + children.add(terminal(str(self, "keyword"))); + children.add(stringCharsNode(str(self, "uri"))); + var alias = optNode(self, "alias"); + if (alias != null) { + children.add(branch("import_alias", List.of(terminal("as"), build(alias)))); + } + return branch("import", children); + } + + private static VmTyped buildClass(VmTyped self) { + var children = docAndAnnotations(self); + var header = new ArrayList<>(); + var modifiers = listMember(self, "modifiers"); + if (!modifiers.isEmpty()) { + header.add(modifierListNode(modifiers)); + } + header.add(terminal("class")); + header.add(build(reqNode(self, "identifier"))); + header.addAll(typeParameterListNodes(listMember(self, "typeParameters"))); + var superType = optNode(self, "superType"); + if (superType != null) { + header.add(branch("class_header_extends", List.of(terminal("extends"), build(superType)))); + } + children.add(branch("class_header", header)); + var body = optNode(self, "body"); + if (body != null) { + children.add(build(body)); + } + return branch("class", children); + } + + private static VmTyped buildTypeAlias(VmTyped self) { + var children = docAndAnnotations(self); + var header = new ArrayList<>(); + var modifiers = listMember(self, "modifiers"); + if (!modifiers.isEmpty()) { + header.add(modifierListNode(modifiers)); + } + header.add(terminal("typealias")); + header.add(build(reqNode(self, "identifier"))); + header.addAll(typeParameterListNodes(listMember(self, "typeParameters"))); + header.add(terminal("=")); + children.add(branch("typealias_header", header)); + children.add(branch("typealias_body", List.of(build(reqNode(self, "type"))))); + return branch("typealias", children); + } + + private static VmTyped buildClassBody(VmTyped self) { + var members = buildAll(listMember(self, "members")); + var children = new ArrayList<>(); + children.add(terminal("{")); + if (!members.isEmpty()) { + children.add(branch("class_body_elements", members)); + } + children.add(terminal("}")); + return branch("class_body", children); + } + + private static VmTyped buildClassProperty(VmTyped self) { + var children = docAndAnnotations(self); + var headerBegin = new ArrayList<>(); + var modifiers = listMember(self, "modifiers"); + if (!modifiers.isEmpty()) { + headerBegin.add(modifierListNode(modifiers)); + } + headerBegin.add(build(reqNode(self, "identifier"))); + var header = new ArrayList<>(); + header.add(branch("class_property_header_begin", headerBegin)); + header.addAll(typeAnnotationNodes(optNode(self, "typeAnnotation"))); + children.add(branch("class_property_header", header)); + var value = optNode(self, "value"); + if (value != null) { + children.add(terminal("=")); + children.add(branch("class_property_body", List.of(build(value)))); + } else { + children.addAll(buildAll(listMember(self, "objectBodies"))); + } + return branch("class_property", children); + } + + private static VmTyped buildClassMethod(VmTyped self) { + var children = docAndAnnotations(self); + var header = new ArrayList<>(); + var modifiers = listMember(self, "modifiers"); + if (!modifiers.isEmpty()) { + header.add(modifierListNode(modifiers)); + } + header.add(terminal("function")); + header.add(build(reqNode(self, "identifier"))); + children.add(branch("class_method_header", header)); + children.addAll(typeParameterListNodes(listMember(self, "typeParameters"))); + children.add(parameterListNode(listMember(self, "parameters"))); + children.addAll(typeAnnotationNodes(optNode(self, "returnType"))); + var body = optNode(self, "body"); + if (body != null) { + children.add(terminal("=")); + children.add(branch("class_method_body", List.of(build(body)))); + } + return branch("class_method", children); + } + + private static VmTyped buildObjectBody(VmTyped self) { + var children = new ArrayList<>(); + children.add(terminal("{")); + var parameters = listMember(self, "parameters"); + if (!parameters.isEmpty()) { + var elements = interleave(buildAll(parameters), NodeNodes::comma); + elements.add(terminal("->")); + children.add(branch("object_parameter_list", elements)); + } + var members = new ArrayList<>(); + for (var member : listMember(self, "members")) { + members.add(buildObjectMember((VmTyped) member)); + } + if (!members.isEmpty()) { + children.add(branch("object_member_list", members)); + } + children.add(terminal("}")); + return branch("object_body", children); + } + + private static VmTyped buildObjectMember(VmTyped member) { + var built = build(member); + return member.getVmClass().isSubclassOf(SyntaxModule.getExprNodeClass()) + ? branch("object_element", List.of(built)) + : built; + } + + private static VmTyped buildObjectProperty(VmTyped self) { + var headerBegin = new ArrayList<>(); + var modifiers = listMember(self, "modifiers"); + if (!modifiers.isEmpty()) { + headerBegin.add(modifierListNode(modifiers)); + } + headerBegin.add(build(reqNode(self, "identifier"))); + var header = new ArrayList<>(); + header.add(branch("object_property_header_begin", headerBegin)); + header.addAll(typeAnnotationNodes(optNode(self, "typeAnnotation"))); + var children = new ArrayList<>(); + children.add(branch("object_property_header", header)); + var value = optNode(self, "value"); + if (value != null) { + children.add(terminal("=")); + children.add(branch("object_property_body", List.of(build(value)))); + } else { + children.addAll(buildAll(listMember(self, "objectBodies"))); + } + return branch("object_property", children); + } + + private static VmTyped buildObjectMethod(VmTyped self) { + var header = new ArrayList<>(); + var modifiers = listMember(self, "modifiers"); + if (!modifiers.isEmpty()) { + header.add(modifierListNode(modifiers)); + } + header.add(terminal("function")); + header.add(build(reqNode(self, "identifier"))); + var children = new ArrayList<>(); + children.add(branch("class_method_header", header)); + children.addAll(typeParameterListNodes(listMember(self, "typeParameters"))); + children.add(parameterListNode(listMember(self, "parameters"))); + children.addAll(typeAnnotationNodes(optNode(self, "returnType"))); + children.add(terminal("=")); + children.add(branch("class_method_body", List.of(build(reqNode(self, "body"))))); + return branch("object_method", children); + } + + private static VmTyped buildObjectEntry(VmTyped self) { + var header = new ArrayList<>(); + header.add(terminal("[")); + header.add(build(reqNode(self, "key"))); + header.add(terminal("]")); + var value = optNode(self, "value"); + if (value != null) { + header.add(terminal("=")); + } + var children = new ArrayList<>(); + children.add(branch("object_entry_header", header)); + if (value != null) { + children.add(build(value)); + } else { + children.addAll(buildAll(listMember(self, "objectBodies"))); + } + return branch("object_entry", children); + } + + private static VmTyped buildMemberPredicate(VmTyped self) { + var children = new ArrayList<>(); + children.add(terminal("[[")); + children.add(build(reqNode(self, "condition"))); + children.add(terminal("]")); + children.add(terminal("]")); + var value = optNode(self, "value"); + if (value != null) { + children.add(terminal("=")); + children.add(build(value)); + } else { + children.addAll(buildAll(listMember(self, "objectBodies"))); + } + return branch("member_predicate", children); + } + + private static VmTyped buildForGenerator(VmTyped self) { + var definitionHeader = new ArrayList<>(); + var keyParameter = optNode(self, "keyParameter"); + if (keyParameter == null) { + definitionHeader.add(build(reqNode(self, "valueParameter"))); + } else { + definitionHeader.add(build(keyParameter)); + definitionHeader.add(terminal(",")); + definitionHeader.add(build(reqNode(self, "valueParameter"))); + } + definitionHeader.add(terminal("in")); + List definition = + List.of( + branch("for_generator_header_definition_header", definitionHeader), + build(reqNode(self, "iterable"))); + List header = + List.of( + terminal("("), branch("for_generator_header_definition", definition), terminal(")")); + return branch( + "for_generator", + List.of( + terminal("for"), branch("for_generator_header", header), build(reqNode(self, "body")))); + } + + private static VmTyped buildWhenGenerator(VmTyped self) { + var children = new ArrayList<>(); + children.add(terminal("when")); + children.add( + branch( + "when_generator_header", + List.of(terminal("("), build(reqNode(self, "condition")), terminal(")")))); + children.add(build(reqNode(self, "thenBody"))); + var elseBody = optNode(self, "elseBody"); + if (elseBody != null) { + children.add(terminal("else")); + children.add(build(elseBody)); + } + return branch("when_generator", children); + } + + private static VmTyped buildSingleLineString(VmTyped self) { + var children = new ArrayList<>(); + children.add(terminal("\"")); + children.addAll(buildStringParts(listMember(self, "parts"))); + children.add(terminal("\"")); + return branch("single_line_string_literal_expr", children); + } + + private static VmTyped buildMultiLineString(VmTyped self) { + var children = new ArrayList<>(); + children.add(terminal("\"\"\"")); + children.addAll(buildStringParts(listMember(self, "parts"))); + // The formatter uses the start column of the closing `"""` to determine the indentation to + // strip from each content line. + var closingSpan = + SyntaxNodes.spanFactory.create(new SpanData(new FullSpan(0, 0, 0, 1, 0, 0), null)); + children.add(makeNode("terminal", null, "\"\"\"", closingSpan)); + return branch("multi_line_string_literal_expr", children); + } + + private static List buildStringParts(List parts) { + var result = new ArrayList<>(); + for (var part : parts) { + result.addAll(buildStringPart((VmTyped) part)); + } + return result; + } + + // String parts are not `Node`s, so they are handled here rather than through `build`. + private static List buildStringPart(VmTyped part) { + return switch (part.getVmClass().getSimpleName()) { + case "StringCharsNode" -> List.of(leaf("string_chars", str(part, "value"))); + case "StringEscapeNode" -> List.of(leaf("string_escape", str(part, "value"))); + case "StringNewlineNode" -> List.of(typeOnly("string_newline")); + case "StringInterpolationNode" -> + List.of(terminal("\\("), build(reqNode(part, "expression")), terminal(")")); + default -> + throw new VmExceptionBuilder() + .bug("Unexpected string-part node: " + part.getVmClass().getSimpleName()) + .build(); + }; + } + + private static VmTyped buildUnqualifiedAccess(VmTyped self) { + var children = new ArrayList<>(); + children.add(build(reqNode(self, "identifier"))); + var arguments = optList(self, "arguments"); + if (arguments != null) { + children.add(argumentListNode(arguments)); + } + return branch("unqualified_access_expr", children); + } + + private static VmTyped buildQualifiedAccess(VmTyped self) { + var member = new ArrayList<>(); + member.add(build(reqNode(self, "identifier"))); + var arguments = optList(self, "arguments"); + if (arguments != null) { + member.add(argumentListNode(arguments)); + } + return branch( + "qualified_access_expr", + List.of( + build(reqNode(self, "receiver")), + operatorLeaf(bool(self, "isNullSafe") ? "?." : "."), + branch("unqualified_access_expr", member))); + } + + private static VmTyped buildSuperAccess(VmTyped self) { + var children = new ArrayList<>(); + children.add(terminal("super")); + children.add(terminal(".")); + children.add(build(reqNode(self, "identifier"))); + var arguments = optList(self, "arguments"); + if (arguments != null) { + children.add(argumentListNode(arguments)); + } + return branch("super_access_expr", children); + } + + private static VmTyped buildIf(VmTyped self) { + return branch( + "if_expr", + List.of( + branch( + "if_header", + List.of( + terminal("if"), + branch( + "if_condition", + List.of( + terminal("("), + branch("if_condition_expr", List.of(build(reqNode(self, "condition")))), + terminal(")"))))), + branch("if_then_expr", List.of(build(reqNode(self, "thenExpr")))), + terminal("else"), + branch("if_else_expr", List.of(build(reqNode(self, "elseExpr")))))); + } + + private static VmTyped buildLet(VmTyped self) { + return branch( + "let_expr", + List.of( + terminal("let"), + branch( + "let_parameter_definition", + List.of( + terminal("("), + branch( + "let_parameter", + List.of( + build(reqNode(self, "parameter")), + terminal("="), + build(reqNode(self, "bindingValue")))), + terminal(")"))), + build(reqNode(self, "body")))); + } + + private static VmTyped buildNew(VmTyped self) { + var type = optNode(self, "type"); + var header = + type == null + ? List.of(terminal("new")) + : List.of(terminal("new"), build(type)); + return branch("new_expr", List.of(branch("new_header", header), build(reqNode(self, "body")))); + } + + private static VmTyped buildBinaryOp(VmTyped self, String operator) { + return branch( + "binary_op_expr", + List.of( + build(reqNode(self, "left")), operatorLeaf(operator), build(reqNode(self, "right")))); + } + + private static VmTyped buildTypeOp(VmTyped self, String operator) { + return branch( + "binary_op_expr", + List.of( + build(reqNode(self, "expression")), + operatorLeaf(operator), + build(reqNode(self, "type")))); + } + + private static VmTyped buildFunctionLiteral(VmTyped self) { + return branch( + "function_literal_expr", + List.of( + parameterListNode(listMember(self, "parameters")), + terminal("->"), + branch("function_literal_body", List.of(build(reqNode(self, "body")))))); + } + + private static VmTyped buildDeclaredType(VmTyped self) { + var name = build(reqNode(self, "name")); + var typeArguments = listMember(self, "typeArguments"); + if (typeArguments.isEmpty()) { + return branch("declared_type", List.of(name)); + } + return branch( + "declared_type", + List.of( + name, + branch( + "type_argument_list", + List.of( + terminal("<"), + branch( + "type_argument_list_elements", + interleave(buildAll(typeArguments), NodeNodes::comma)), + terminal(">"))))); + } + + private static VmTyped buildFunctionType(VmTyped self) { + var parameterTypes = listMember(self, "parameterTypes"); + var parameters = + parameterTypes.isEmpty() + ? List.of(terminal("("), terminal(")")) + : List.of( + terminal("("), + branch( + "parenthesized_type_elements", + interleave(buildAll(parameterTypes), NodeNodes::comma)), + terminal(")")); + return branch( + "function_type", + List.of( + branch("function_type_parameters", parameters), + terminal("->"), + build(reqNode(self, "returnType")))); + } + + private static VmTyped buildConstrainedType(VmTyped self) { + return branch( + "constrained_type", + List.of( + build(reqNode(self, "baseType")), + branch( + "constrained_type_constraint", + List.of( + terminal("("), + branch( + "constrained_type_elements", + interleave(buildAll(listMember(self, "constraints")), NodeNodes::comma)), + terminal(")"))))); + } + + private static VmTyped buildAnnotation(VmTyped self) { + var children = new ArrayList<>(); + children.add(terminal("@")); + children.add(build(reqNode(self, "type"))); + var body = optNode(self, "body"); + if (body != null) { + children.add(build(body)); + } + return branch("annotation", children); + } + + private static VmTyped buildParameter(VmTyped self) { + var identifier = optNode(self, "identifier"); + if (identifier == null) { + return branch("parameter", List.of(terminal("_"))); + } + var typeAnnotation = optNode(self, "typeAnnotation"); + if (typeAnnotation == null) { + return branch("parameter", List.of(build(identifier))); + } + var children = new ArrayList<>(); + children.add(build(identifier)); + children.addAll(typeAnnotationNodes(typeAnnotation)); + return branch("parameter", children); + } + + private static VmTyped buildTypeParameter(VmTyped self) { + var children = new ArrayList<>(); + // Unlike other modifiers, variance modifiers are plain terminals rather than a `modifier_list`. + for (var modifier : listMember(self, "modifiers")) { + children.add(terminal((String) modifier)); + } + children.add(build(reqNode(self, "identifier"))); + return branch("type_parameter", children); + } + + private static VmTyped buildDocComment(VmTyped self) { + var value = str(self, "value"); + var children = new ArrayList<>(); + for (var line : value.split("\n", -1)) { + children.add(leaf("doc_comment_line", "/// " + line)); + } + return branch("doc_comment", children); + } + + private static VmTyped buildCall(String type, String keyword, VmTyped inner) { + return branch(type, List.of(terminal(keyword), terminal("("), inner, terminal(")"))); + } + + private static String identifierText(VmTyped self) { + return Lexer.maybeQuoteIdentifier(str(self, "name")); + } + + // The doc comment (if any) followed by the annotations of a declaration. + private static List docAndAnnotations(VmTyped self) { + var result = new ArrayList<>(); + var docComment = optNode(self, "docComment"); + if (docComment != null) { + result.add(build(docComment)); + } + result.addAll(buildAll(listMember(self, "annotations"))); + return result; + } + + // Node construction helpers + + private static VmTyped modifierListNode(List modifiers) { + var children = new ArrayList<>(); + for (var modifier : modifiers) { + children.add(leaf("modifier", (String) modifier)); + } + return branch("modifier_list", children); + } + + // A quoted `string_chars` node for a string constant like `"foo"`. + private static VmTyped stringCharsNode(String value) { + return makeNode( + "string_chars", + List.of(terminal("\""), terminal(value), terminal("\"")), + "\"" + value + "\"", + null); + } + + private static VmTyped parameterListNode(List parameters) { + if (parameters.isEmpty()) { + return branch("parameter_list", List.of(terminal("("), terminal(")"))); + } + return branch( + "parameter_list", + List.of( + terminal("("), + branch("parameter_list_elements", interleave(buildAll(parameters), NodeNodes::comma)), + terminal(")"))); + } + + private static VmTyped argumentListNode(List arguments) { + if (arguments.isEmpty()) { + return branch("argument_list", List.of(terminal("("), terminal(")"))); + } + return branch( + "argument_list", + List.of( + terminal("("), + branch("argument_list_elements", interleave(buildAll(arguments), NodeNodes::comma)), + terminal(")"))); + } + + private static List typeParameterListNodes(List typeParameters) { + if (typeParameters.isEmpty()) { + return List.of(); + } + return List.of( + branch( + "type_parameter_list", + List.of( + terminal("<"), + branch( + "type_parameter_list_elements", + interleave(buildAll(typeParameters), NodeNodes::comma)), + terminal(">")))); + } + + private static List typeAnnotationNodes(@Nullable VmTyped type) { + if (type == null) { + return List.of(); + } + return List.of(branch("type_annotation", List.of(terminal(":"), build(type)))); + } + + private static VmTyped comma() { + return terminal(","); + } + + // Interleave `items` with fresh separators. + private static ArrayList interleave( + List items, java.util.function.Supplier separator) { + var result = new ArrayList<>(items.isEmpty() ? 0 : items.size() * 2 - 1); + for (var item : items) { + if (!result.isEmpty()) { + result.add(separator.get()); + } + result.add(item); + } + return result; + } + + private static VmTyped terminal(String text) { + return leaf("terminal", text); + } + + private static VmTyped operatorLeaf(String text) { + return leaf("operator", text); + } + + private static VmTyped branch(String type, List children) { + return makeNode(type, children, null, null); + } + + private static VmTyped leaf(String type, String text) { + return makeNode(type, null, text, null); + } + + private static VmTyped typeOnly(String type) { + return makeNode(type, null, null, null); + } + + // Build a `GenericNode`, setting only the members that differ from the class + // defaults (`children` defaults to empty, `text` to null, `span`/`parent` to their defaults). + private static VmTyped makeNode( + String type, @Nullable List children, @Nullable String text, @Nullable VmTyped span) { + var builder = new VmObjectBuilder(4); + builder.addProperty(Identifier.TYPE, type); + if (children != null) { + builder.addProperty(Identifier.CHILDREN, VmList.create(children.toArray())); + } + if (text != null) { + builder.addProperty(Identifier.TEXT, text); + } + if (span != null) { + builder.addProperty(Identifier.SPAN, span); + } + return builder.toTyped(SyntaxModule.getGenericNodeClass()); + } + + // =============== + // Member readers + // =============== + + private static List buildAll(List nodes) { + var result = new ArrayList<>(); + for (var node : nodes) { + result.add(build((VmTyped) node)); + } + return result; + } + + private static Object member(VmTyped self, String name) { + return VmUtils.readMember(self, Identifier.get(name)); + } + + private static VmTyped reqNode(VmTyped self, String name) { + return (VmTyped) member(self, name); + } + + private static @Nullable VmTyped optNode(VmTyped self, String name) { + return member(self, name) instanceof VmTyped node ? node : null; + } + + private static List listMember(VmTyped self, String name) { + return elementsOf((VmListing) member(self, name)); + } + + private static @Nullable List optList(VmTyped self, String name) { + return member(self, name) instanceof VmListing listing ? elementsOf(listing) : null; + } + + // The elements of `listing`, in order. + private static List elementsOf(VmListing listing) { + var result = new ArrayList<>(listing.getLength()); + for (var i = 0; i < listing.getLength(); i++) { + result.add(VmUtils.readMember(listing, (long) i)); + } + return result; + } + + private static String str(VmTyped self, String name) { + return (String) member(self, name); + } + + private static boolean bool(VmTyped self, String name) { + return (Boolean) member(self, name); + } + + private static String numText(Object value) { + return value instanceof String s ? s : value.toString(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/ParserNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/ParserNodes.java new file mode 100644 index 000000000..96363519b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/ParserNodes.java @@ -0,0 +1,1657 @@ +/* + * 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.core.stdlib.syntax; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.SyntaxModule; +import org.pkl.core.runtime.VmClass; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.runtime.VmList; +import org.pkl.core.runtime.VmListing; +import org.pkl.core.runtime.VmNull; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.stdlib.ExternalMethod1Node; +import org.pkl.core.stdlib.VmObjectFactory; +import org.pkl.core.stdlib.syntax.SyntaxNodes.GenericNodeData; +import org.pkl.core.stdlib.syntax.SyntaxNodes.SpanData; +import org.pkl.parser.GenericParser; +import org.pkl.parser.GenericParserError; +import org.pkl.parser.syntax.generic.Node; +import org.pkl.parser.syntax.generic.NodeType; + +public class ParserNodes { + private ParserNodes() {} + + private static final VmObjectFactory genericNodeFactory = + new VmObjectFactory(SyntaxModule::getGenericNodeClass) + .addStringProperty("type", nd -> nd.node.type.name().toLowerCase(Locale.ROOT)) + .addListProperty("children", nd -> nd.childrenVm) + .addProperty("parent", nd -> VmNull.lift(nd.parentVm)) + .addProperty( + "text", + nd -> + nd.node.children.isEmpty() || nd.node.type == NodeType.STRING_CHARS + ? nd.node.text(nd.source) + : VmNull.withoutDefault()) + .addProperty("span", nd -> VmNull.lift(nd.spanVm)); + + private static final VmObjectFactory identifierNodeFactory = + new VmObjectFactory(SyntaxModule::getIdentifierNodeClass) + .addProperty("genericNode", vm -> vm) + .addStringProperty("text", ParserNodes::identifierText); + + private static VmObjectFactory genericNodeOnlyFactory(Supplier classSupplier) { + return new VmObjectFactory(classSupplier).addProperty("genericNode", vm -> vm); + } + + private static final VmObjectFactory qualifiedIdentifierNodeFactory = + new VmObjectFactory(SyntaxModule::getQualifiedIdentifierNodeClass) + .addProperty("genericNode", vm -> vm) + .addListingProperty("identifiers", ParserNodes::qualifiedIdentifierIdentifiers) + .addStringProperty("value", ParserNodes::qualifiedIdentifierValue); + private static final VmObjectFactory docCommentNodeFactory = + new VmObjectFactory(SyntaxModule::getDocCommentNodeClass) + .addProperty("genericNode", vm -> vm) + .addStringProperty("value", ParserNodes::docCommentValue); + private static final VmObjectFactory annotationNodeFactory = + new VmObjectFactory(SyntaxModule::getAnnotationNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("type", ParserNodes::annotationType) + .addProperty("body", ParserNodes::annotationBody); + private static final VmObjectFactory typeParameterNodeFactory = + new VmObjectFactory(SyntaxModule::getTypeParameterNodeClass) + .addProperty("genericNode", vm -> vm) + .addListingProperty("modifiers", ParserNodes::typeParameterModifiers) + .addTypedProperty("identifier", ParserNodes::identifierNodeOf); + private static final VmObjectFactory objectBodyNodeFactory = + new VmObjectFactory(SyntaxModule::getObjectBodyNodeClass) + .addProperty("genericNode", vm -> vm) + .addListingProperty("parameters", ParserNodes::objectBodyParameters) + .addListingProperty("members", ParserNodes::objectBodyMembers); + private static final VmObjectFactory parameterNodeFactory = + new VmObjectFactory(SyntaxModule::getParameterNodeClass) + .addProperty("genericNode", vm -> vm) + .addBooleanProperty("isBlankIdentifier", ParserNodes::parameterIsBlankIdentifier) + .addProperty("identifier", ParserNodes::parameterIdentifier) + .addProperty("typeAnnotation", ParserNodes::parameterTypeAnnotation); + + private static final VmObjectFactory objectPropertyNodeFactory = + new VmObjectFactory(SyntaxModule::getObjectPropertyNodeClass) + .addProperty("genericNode", vm -> vm) + .addListingProperty("modifiers", ParserNodes::objectPropertyModifiers) + .addTypedProperty("identifier", ParserNodes::objectPropertyIdentifier) + .addProperty("typeAnnotation", ParserNodes::objectPropertyTypeAnnotation) + .addProperty("value", ParserNodes::objectPropertyValue) + .addListingProperty("objectBodies", ParserNodes::objectPropertyObjectBodies); + private static final VmObjectFactory objectMethodNodeFactory = + new VmObjectFactory(SyntaxModule::getObjectMethodNodeClass) + .addProperty("genericNode", vm -> vm) + .addListingProperty("modifiers", ParserNodes::classMethodModifiers) + .addTypedProperty("identifier", ParserNodes::classMethodIdentifier) + .addListingProperty("typeParameters", ParserNodes::classMethodTypeParameters) + .addListingProperty("parameters", ParserNodes::classMethodParameters) + .addProperty("returnType", ParserNodes::classMethodReturnType) + .addTypedProperty("body", ParserNodes::objectMethodBody); + private static final VmObjectFactory memberPredicateNodeFactory = + new VmObjectFactory(SyntaxModule::getMemberPredicateNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("condition", ParserNodes::memberPredicateCondition) + .addProperty("value", ParserNodes::memberPredicateValue) + .addListingProperty("objectBodies", ParserNodes::memberPredicateObjectBodies); + private static final VmObjectFactory objectEntryNodeFactory = + new VmObjectFactory(SyntaxModule::getObjectEntryNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("key", ParserNodes::objectEntryKey) + .addProperty("value", ParserNodes::objectEntryValue) + .addListingProperty("objectBodies", ParserNodes::objectEntryObjectBodies); + private static final VmObjectFactory objectSpreadNodeFactory = + new VmObjectFactory(SyntaxModule::getObjectSpreadNodeClass) + .addProperty("genericNode", vm -> vm) + .addStringProperty("keyword", ParserNodes::objectSpreadKeyword) + .addTypedProperty("expression", ParserNodes::soleExpr); + private static final VmObjectFactory whenGeneratorNodeFactory = + new VmObjectFactory(SyntaxModule::getWhenGeneratorNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("condition", ParserNodes::whenCondition) + .addTypedProperty("thenBody", ParserNodes::whenThenBody) + .addProperty("elseBody", ParserNodes::whenElseBody); + private static final VmObjectFactory forGeneratorNodeFactory = + new VmObjectFactory(SyntaxModule::getForGeneratorNodeClass) + .addProperty("genericNode", vm -> vm) + .addProperty("keyParameter", ParserNodes::forKeyParameter) + .addTypedProperty("valueParameter", ParserNodes::forValueParameter) + .addTypedProperty("iterable", ParserNodes::forIterable) + .addTypedProperty("body", ParserNodes::forBody); + + private static final VmObjectFactory unknownTypeNodeFactory = + genericNodeOnlyFactory(SyntaxModule::getUnknownTypeNodeClass); + private static final VmObjectFactory nothingTypeNodeFactory = + genericNodeOnlyFactory(SyntaxModule::getNothingTypeNodeClass); + private static final VmObjectFactory moduleTypeNodeFactory = + genericNodeOnlyFactory(SyntaxModule::getModuleTypeNodeClass); + private static final VmObjectFactory declaredTypeNodeFactory = + new VmObjectFactory(SyntaxModule::getDeclaredTypeNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("name", ParserNodes::declaredTypeName) + .addListingProperty("typeArguments", ParserNodes::declaredTypeArguments); + private static final VmObjectFactory nullableTypeNodeFactory = + new VmObjectFactory(SyntaxModule::getNullableTypeNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("baseType", ParserNodes::nullableTypeBaseType); + private static final VmObjectFactory unionTypeNodeFactory = + new VmObjectFactory(SyntaxModule::getUnionTypeNodeClass) + .addProperty("genericNode", vm -> vm) + .addListingProperty("members", ParserNodes::unionTypeMembers); + private static final VmObjectFactory functionTypeNodeFactory = + new VmObjectFactory(SyntaxModule::getFunctionTypeNodeClass) + .addProperty("genericNode", vm -> vm) + .addListingProperty("parameterTypes", ParserNodes::functionTypeParameterTypes) + .addTypedProperty("returnType", ParserNodes::functionTypeReturnType); + private static final VmObjectFactory constrainedTypeNodeFactory = + new VmObjectFactory(SyntaxModule::getConstrainedTypeNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("baseType", ParserNodes::constrainedTypeBaseType) + .addListingProperty("constraints", ParserNodes::constrainedTypeConstraints); + private static final VmObjectFactory parenthesizedTypeNodeFactory = + new VmObjectFactory(SyntaxModule::getParenthesizedTypeNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("type", ParserNodes::parenthesizedTypeType); + private static final VmObjectFactory stringLiteralTypeNodeFactory = + new VmObjectFactory(SyntaxModule::getStringLiteralTypeNodeClass) + .addProperty("genericNode", vm -> vm) + .addStringProperty("value", ParserNodes::stringLiteralTypeValue); + + private static final VmObjectFactory thisExprNodeFactory = + genericNodeOnlyFactory(SyntaxModule::getThisExprNodeClass); + private static final VmObjectFactory outerExprNodeFactory = + genericNodeOnlyFactory(SyntaxModule::getOuterExprNodeClass); + private static final VmObjectFactory moduleExprNodeFactory = + genericNodeOnlyFactory(SyntaxModule::getModuleExprNodeClass); + private static final VmObjectFactory nullLiteralExprNodeFactory = + genericNodeOnlyFactory(SyntaxModule::getNullLiteralExprNodeClass); + private static final VmObjectFactory booleanLiteralExprNodeFactory = + new VmObjectFactory(SyntaxModule::getBooleanLiteralExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addBooleanProperty("value", ParserNodes::booleanLiteralValue); + private static final VmObjectFactory intLiteralExprNodeFactory = + new VmObjectFactory(SyntaxModule::getIntLiteralExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addProperty("value", ParserNodes::literalText); + private static final VmObjectFactory floatLiteralExprNodeFactory = + new VmObjectFactory(SyntaxModule::getFloatLiteralExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addProperty("value", ParserNodes::literalText); + private static final VmObjectFactory singleLineStringLiteralExprNodeFactory = + new VmObjectFactory(SyntaxModule::getSingleLineStringLiteralExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addListingProperty("parts", ParserNodes::buildStringParts); + private static final VmObjectFactory multiLineStringLiteralExprNodeFactory = + new VmObjectFactory(SyntaxModule::getMultiLineStringLiteralExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addListingProperty("parts", ParserNodes::buildStringParts); + private static final VmObjectFactory unqualifiedAccessExprNodeFactory = + new VmObjectFactory(SyntaxModule::getUnqualifiedAccessExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("identifier", ParserNodes::identifierNodeOf) + .addProperty("arguments", ParserNodes::unqualifiedAccessArguments); + private static final VmObjectFactory qualifiedAccessExprNodeFactory = + new VmObjectFactory(SyntaxModule::getQualifiedAccessExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("receiver", ParserNodes::qualifiedAccessReceiver) + .addBooleanProperty("isNullSafe", ParserNodes::qualifiedAccessIsNullSafe) + .addTypedProperty("identifier", ParserNodes::qualifiedAccessIdentifier) + .addProperty("arguments", ParserNodes::qualifiedAccessArguments); + private static final VmObjectFactory subscriptExprNodeFactory = + new VmObjectFactory(SyntaxModule::getSubscriptExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("receiver", ParserNodes::subscriptReceiver) + .addTypedProperty("index", ParserNodes::subscriptIndex); + private static final VmObjectFactory superAccessExprNodeFactory = + new VmObjectFactory(SyntaxModule::getSuperAccessExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("identifier", ParserNodes::identifierNodeOf) + .addProperty("arguments", ParserNodes::argumentsOrNull); + private static final VmObjectFactory superSubscriptExprNodeFactory = + new VmObjectFactory(SyntaxModule::getSuperSubscriptExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("index", ParserNodes::soleExpr); + private static final VmObjectFactory ifExprNodeFactory = + new VmObjectFactory(SyntaxModule::getIfExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("condition", ParserNodes::ifCondition) + .addTypedProperty("thenExpr", ParserNodes::ifThenExpr) + .addTypedProperty("elseExpr", ParserNodes::ifElseExpr); + private static final VmObjectFactory letExprNodeFactory = + new VmObjectFactory(SyntaxModule::getLetExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("parameter", ParserNodes::letParameter) + .addTypedProperty("bindingValue", ParserNodes::letBindingValue) + .addTypedProperty("body", ParserNodes::letBody); + private static final VmObjectFactory throwExprNodeFactory = + new VmObjectFactory(SyntaxModule::getThrowExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("expression", ParserNodes::soleExpr); + private static final VmObjectFactory traceExprNodeFactory = + new VmObjectFactory(SyntaxModule::getTraceExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("expression", ParserNodes::soleExpr); + private static final VmObjectFactory importExprNodeFactory = + new VmObjectFactory(SyntaxModule::getImportExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addStringProperty("keyword", ParserNodes::importKeyword) + .addStringProperty("uri", ParserNodes::importUri); + private static final VmObjectFactory readExprNodeFactory = + new VmObjectFactory(SyntaxModule::getReadExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addStringProperty("keyword", ParserNodes::readKeyword) + .addTypedProperty("expression", ParserNodes::soleExpr); + private static final VmObjectFactory newExprNodeFactory = + new VmObjectFactory(SyntaxModule::getNewExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addProperty("type", ParserNodes::newExprType) + .addTypedProperty("body", ParserNodes::newExprBody); + private static final VmObjectFactory amendsExprNodeFactory = + new VmObjectFactory(SyntaxModule::getAmendsExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("parentExpr", ParserNodes::amendsParentExpr) + .addTypedProperty("body", ParserNodes::amendsBody); + + private static VmObjectFactory binaryOpExprNodeFactory(Supplier classSupplier) { + return new VmObjectFactory(classSupplier) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("left", ParserNodes::binaryOpLeft) + .addTypedProperty("right", ParserNodes::binaryOpRight); + } + + private static VmObjectFactory typeOpExprNodeFactory(Supplier classSupplier) { + return new VmObjectFactory(classSupplier) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("expression", ParserNodes::binaryOpLeft) + .addTypedProperty("type", ParserNodes::typeOpType); + } + + private static final VmObjectFactory exponentiationExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getExponentiationExprNodeClass); + private static final VmObjectFactory multiplicationExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getMultiplicationExprNodeClass); + private static final VmObjectFactory divisionExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getDivisionExprNodeClass); + private static final VmObjectFactory integerDivisionExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getIntegerDivisionExprNodeClass); + private static final VmObjectFactory remainderExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getRemainderExprNodeClass); + private static final VmObjectFactory additionExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getAdditionExprNodeClass); + private static final VmObjectFactory subtractionExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getSubtractionExprNodeClass); + private static final VmObjectFactory lessThanExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getLessThanExprNodeClass); + private static final VmObjectFactory lessThanOrEqualExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getLessThanOrEqualExprNodeClass); + private static final VmObjectFactory greaterThanExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getGreaterThanExprNodeClass); + private static final VmObjectFactory greaterThanOrEqualExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getGreaterThanOrEqualExprNodeClass); + private static final VmObjectFactory equalExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getEqualExprNodeClass); + private static final VmObjectFactory notEqualExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getNotEqualExprNodeClass); + private static final VmObjectFactory logicalAndExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getLogicalAndExprNodeClass); + private static final VmObjectFactory logicalOrExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getLogicalOrExprNodeClass); + private static final VmObjectFactory pipeExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getPipeExprNodeClass); + private static final VmObjectFactory nullCoalescingExprNodeFactory = + binaryOpExprNodeFactory(SyntaxModule::getNullCoalescingExprNodeClass); + private static final VmObjectFactory typeCheckExprNodeFactory = + typeOpExprNodeFactory(SyntaxModule::getTypeCheckExprNodeClass); + private static final VmObjectFactory typeCastExprNodeFactory = + typeOpExprNodeFactory(SyntaxModule::getTypeCastExprNodeClass); + private static final VmObjectFactory unaryMinusExprNodeFactory = + new VmObjectFactory(SyntaxModule::getUnaryMinusExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("operand", ParserNodes::soleExpr); + private static final VmObjectFactory logicalNotExprNodeFactory = + new VmObjectFactory(SyntaxModule::getLogicalNotExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("operand", ParserNodes::soleExpr); + private static final VmObjectFactory nonNullExprNodeFactory = + new VmObjectFactory(SyntaxModule::getNonNullExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("operand", ParserNodes::soleExpr); + private static final VmObjectFactory functionLiteralExprNodeFactory = + new VmObjectFactory(SyntaxModule::getFunctionLiteralExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addListingProperty("parameters", ParserNodes::functionLiteralParameters) + .addTypedProperty("body", ParserNodes::functionLiteralBody); + private static final VmObjectFactory parenthesizedExprNodeFactory = + new VmObjectFactory(SyntaxModule::getParenthesizedExprNodeClass) + .addProperty("genericNode", vm -> vm) + .addTypedProperty("expression", ParserNodes::parenthesizedExpression); + + // String-part factories, produced by `buildStringParts`. String parts are not `Node`s and don't + // carry a `genericNode`, but the generic node is still their factory input. + private static final VmObjectFactory stringCharsNodeFactory = + new VmObjectFactory(SyntaxModule::getStringCharsNodeClass) + .addStringProperty("value", ParserNodes::literalText); + private static final VmObjectFactory stringEscapeNodeFactory = + new VmObjectFactory(SyntaxModule::getStringEscapeNodeClass) + .addStringProperty("value", ParserNodes::literalText); + private static final VmObjectFactory stringNewlineNodeFactory = + new VmObjectFactory(SyntaxModule::getStringNewlineNodeClass); + private static final VmObjectFactory stringInterpolationNodeFactory = + new VmObjectFactory(SyntaxModule::getStringInterpolationNodeClass) + .addTypedProperty("expression", ParserNodes::wrapExpr); + + private static final VmObjectFactory importNodeFactory = + new VmObjectFactory(SyntaxModule::getImportNodeClass) + .addProperty("genericNode", vm -> vm) + .addStringProperty("keyword", ParserNodes::importKeyword) + .addStringProperty("uri", ParserNodes::importUri) + .addProperty("alias", ParserNodes::importAlias); + + private static final VmObjectFactory moduleDeclarationNodeFactory = + new VmObjectFactory(SyntaxModule::getModuleDeclarationNodeClass) + .addProperty("genericNode", vm -> vm) + .addProperty("docComment", ParserNodes::docCommentOf) + .addListingProperty("annotations", ParserNodes::annotationsOf) + .addListingProperty("modifiers", ParserNodes::moduleDeclModifiers) + .addProperty("name", ParserNodes::moduleDeclName) + .addProperty("extendsOrAmendsClause", ParserNodes::moduleDeclExtendsOrAmendsClause); + + private static final VmObjectFactory extendsOrAmendsClauseNodeFactory = + new VmObjectFactory(SyntaxModule::getExtendsOrAmendsClauseNodeClass) + .addProperty("genericNode", vm -> vm) + .addStringProperty("keyword", ParserNodes::extendsOrAmendsClauseKeyword) + .addStringProperty("uri", ParserNodes::stringCharsOf); + + private static final VmObjectFactory classNodeFactory = + new VmObjectFactory(SyntaxModule::getClassNodeClass) + .addProperty("genericNode", vm -> vm) + .addProperty("docComment", ParserNodes::docCommentOf) + .addListingProperty("annotations", ParserNodes::annotationsOf) + .addListingProperty("modifiers", ParserNodes::classModifiers) + .addTypedProperty("identifier", ParserNodes::classIdentifier) + .addListingProperty("typeParameters", ParserNodes::classTypeParameters) + .addProperty("superType", ParserNodes::classSuperType) + .addProperty("body", ParserNodes::classBody); + + private static final VmObjectFactory typeAliasNodeFactory = + new VmObjectFactory(SyntaxModule::getTypeAliasNodeClass) + .addProperty("genericNode", vm -> vm) + .addProperty("docComment", ParserNodes::docCommentOf) + .addListingProperty("annotations", ParserNodes::annotationsOf) + .addListingProperty("modifiers", ParserNodes::typeAliasModifiers) + .addTypedProperty("identifier", ParserNodes::typeAliasIdentifier) + .addListingProperty("typeParameters", ParserNodes::typeAliasTypeParameters) + .addTypedProperty("type", ParserNodes::typeAliasType); + + private static final VmObjectFactory classPropertyNodeFactory = + new VmObjectFactory(SyntaxModule::getClassPropertyNodeClass) + .addProperty("genericNode", vm -> vm) + .addProperty("docComment", ParserNodes::docCommentOf) + .addListingProperty("annotations", ParserNodes::annotationsOf) + .addListingProperty("modifiers", ParserNodes::classPropertyModifiers) + .addTypedProperty("identifier", ParserNodes::classPropertyIdentifier) + .addProperty("typeAnnotation", ParserNodes::classPropertyTypeAnnotation) + .addProperty("value", ParserNodes::classPropertyValue) + .addListingProperty("objectBodies", ParserNodes::classPropertyObjectBodies); + + private static final VmObjectFactory classMethodNodeFactory = + new VmObjectFactory(SyntaxModule::getClassMethodNodeClass) + .addProperty("genericNode", vm -> vm) + .addProperty("docComment", ParserNodes::docCommentOf) + .addListingProperty("annotations", ParserNodes::annotationsOf) + .addListingProperty("modifiers", ParserNodes::classMethodModifiers) + .addTypedProperty("identifier", ParserNodes::classMethodIdentifier) + .addListingProperty("typeParameters", ParserNodes::classMethodTypeParameters) + .addListingProperty("parameters", ParserNodes::classMethodParameters) + .addProperty("returnType", ParserNodes::classMethodReturnType) + .addProperty("body", ParserNodes::classMethodBody); + + private static final VmObjectFactory classBodyNodeFactory = + new VmObjectFactory(SyntaxModule::getClassBodyNodeClass) + .addProperty("genericNode", vm -> vm) + .addListingProperty("members", ParserNodes::classBodyMembers); + + private static final VmObjectFactory moduleNodeFactory = + new VmObjectFactory(SyntaxModule::getModuleNodeClass) + .addProperty("genericNode", vm -> vm) + .addProperty("declaration", ParserNodes::moduleDeclaration) + .addListingProperty("imports", ParserNodes::moduleImports) + .addListingProperty("members", ParserNodes::memberChildren); + + private static Object moduleDeclaration(VmTyped moduleVm) { + var declVm = findChildVm(moduleVm, NodeType.MODULE_DECLARATION); + return declVm == null ? VmNull.withoutDefault() : moduleDeclarationNodeFactory.create(declVm); + } + + private static Object docCommentOf(VmTyped ownerVm) { + var dc = findChildVm(ownerVm, NodeType.DOC_COMMENT); + return dc == null ? VmNull.withoutDefault() : docCommentNodeFactory.create(dc); + } + + private static VmListing annotationsOf(VmTyped ownerVm) { + return wrapAll(findChildrenVm(ownerVm, NodeType.ANNOTATION), annotationNodeFactory); + } + + private static VmTyped annotationType(VmTyped annotationVm) { + var type = findTypeChildVm(annotationVm); + if (type == null) { + throw new VmExceptionBuilder().bug("An annotation always has a type.").build(); + } + return wrapType(type); + } + + private static Object annotationBody(VmTyped annotationVm) { + var body = findChildVm(annotationVm, NodeType.OBJECT_BODY); + return body == null ? VmNull.withoutDefault() : objectBodyNodeFactory.create(body); + } + + private static String docCommentValue(VmTyped docCommentVm) { + var lineVms = findChildrenVm(docCommentVm, NodeType.DOC_COMMENT_LINE); + var builder = new StringBuilder(); + for (var i = 0; i < lineVms.size(); i++) { + if (i > 0) { + builder.append('\n'); + } + var data = (GenericNodeData) lineVms.get(i).getExtraStorage(); + var text = data.node.text(data.source); + if (text.startsWith("/// ")) { + builder.append(text, 4, text.length()); + } else if (text.startsWith("///")) { + builder.append(text, 3, text.length()); + } else { + builder.append(text); + } + } + return builder.toString(); + } + + private static VmTyped declaredTypeName(VmTyped typeVm) { + var name = findChildVm(typeVm, NodeType.QUALIFIED_IDENTIFIER); + if (name == null) { + throw new VmExceptionBuilder() + .bug("A declared type always has a qualified identifier.") + .build(); + } + return qualifiedIdentifierNodeFactory.create(name); + } + + private static VmListing declaredTypeArguments(VmTyped typeVm) { + var list = findChildVm(typeVm, NodeType.TYPE_ARGUMENT_LIST); + if (list == null) { + return VmListing.empty(); + } + var elems = findChildVm(list, NodeType.TYPE_ARGUMENT_LIST_ELEMENTS); + if (elems == null) { + return VmListing.empty(); + } + return wrapTypes(findTypeChildrenVm(elems)); + } + + private static VmTyped nullableTypeBaseType(VmTyped typeVm) { + return wrapType(requireTypeChild(typeVm)); + } + + private static VmListing unionTypeMembers(VmTyped typeVm) { + return wrapTypes(findTypeChildrenVm(typeVm)); + } + + private static VmListing functionTypeParameterTypes(VmTyped typeVm) { + var params = findChildVm(typeVm, NodeType.FUNCTION_TYPE_PARAMETERS); + if (params == null) { + return VmListing.empty(); + } + var elems = findChildVm(params, NodeType.PARENTHESIZED_TYPE_ELEMENTS); + if (elems == null) { + return VmListing.empty(); + } + return wrapTypes(findTypeChildrenVm(elems)); + } + + private static VmTyped functionTypeReturnType(VmTyped typeVm) { + var types = findTypeChildrenVm(typeVm); + if (types.isEmpty()) { + throw new VmExceptionBuilder().bug("A function type always has a return type.").build(); + } + return wrapType(types.get(types.size() - 1)); + } + + private static VmTyped constrainedTypeBaseType(VmTyped typeVm) { + return wrapType(requireTypeChild(typeVm)); + } + + private static VmListing constrainedTypeConstraints(VmTyped typeVm) { + var constraint = findChildVm(typeVm, NodeType.CONSTRAINED_TYPE_CONSTRAINT); + if (constraint == null) { + return VmListing.empty(); + } + var elems = findChildVm(constraint, NodeType.CONSTRAINED_TYPE_ELEMENTS); + if (elems == null) { + return VmListing.empty(); + } + return wrapExprs(findExprChildrenVm(elems)); + } + + private static VmTyped parenthesizedTypeType(VmTyped typeVm) { + var elems = findChildVm(typeVm, NodeType.PARENTHESIZED_TYPE_ELEMENTS); + var type = elems == null ? null : findTypeChildVm(elems); + if (type == null) { + throw new VmExceptionBuilder().bug("A parenthesized type always has an inner type.").build(); + } + return wrapType(type); + } + + private static String stringLiteralTypeValue(VmTyped typeVm) { + var data = (GenericNodeData) typeVm.getExtraStorage(); + return extractStringChars(data.node, data.source); + } + + private static VmTyped requireTypeChild(VmTyped genericVm) { + var type = findTypeChildVm(genericVm); + if (type == null) { + throw new VmExceptionBuilder().bug("Expected a type-node child.").build(); + } + return type; + } + + private static String literalText(VmTyped exprVm) { + var text = nodeText((GenericNodeData) exprVm.getExtraStorage()); + return text == null ? "" : text; + } + + private static boolean booleanLiteralValue(VmTyped exprVm) { + return "true".equals(nodeText((GenericNodeData) exprVm.getExtraStorage())); + } + + private static Object unqualifiedAccessArguments(VmTyped exprVm) { + return argumentsOrNull(exprVm); + } + + private static VmTyped qualifiedAccessReceiver(VmTyped exprVm) { + return wrapExpr(requireExprChild(exprVm)); + } + + private static boolean qualifiedAccessIsNullSafe(VmTyped exprVm) { + var op = findChildVm(exprVm, NodeType.OPERATOR); + if (op == null) { + return false; + } + var data = (GenericNodeData) op.getExtraStorage(); + return "?.".equals(data.node.text(data.source)); + } + + private static VmTyped qualifiedAccessIdentifier(VmTyped exprVm) { + return identifierNodeOf(lastChildVm(exprVm, NodeType.UNQUALIFIED_ACCESS_EXPR)); + } + + private static Object qualifiedAccessArguments(VmTyped exprVm) { + var member = lastChildVm(exprVm, NodeType.UNQUALIFIED_ACCESS_EXPR); + return member == null ? VmNull.withoutDefault() : argumentsOrNull(member); + } + + private static VmTyped subscriptReceiver(VmTyped exprVm) { + return wrapExpr(findExprChildrenVm(exprVm).get(0)); + } + + private static VmTyped subscriptIndex(VmTyped exprVm) { + return wrapExpr(findExprChildrenVm(exprVm).get(1)); + } + + private static VmTyped ifCondition(VmTyped exprVm) { + var header = requireChild(exprVm, NodeType.IF_HEADER); + var condition = requireChild(header, NodeType.IF_CONDITION); + var conditionExpr = requireChild(condition, NodeType.IF_CONDITION_EXPR); + return wrapExpr(requireExprChild(conditionExpr)); + } + + private static VmTyped ifThenExpr(VmTyped exprVm) { + return wrapExpr(requireExprChild(requireChild(exprVm, NodeType.IF_THEN_EXPR))); + } + + private static VmTyped ifElseExpr(VmTyped exprVm) { + return wrapExpr(requireExprChild(requireChild(exprVm, NodeType.IF_ELSE_EXPR))); + } + + private static VmTyped letParamNode(VmTyped exprVm) { + return requireChild( + requireChild(exprVm, NodeType.LET_PARAMETER_DEFINITION), NodeType.LET_PARAMETER); + } + + private static VmTyped letParameter(VmTyped exprVm) { + return parameterNodeFactory.create(requireChild(letParamNode(exprVm), NodeType.PARAMETER)); + } + + private static VmTyped letBindingValue(VmTyped exprVm) { + return wrapExpr(requireExprChild(letParamNode(exprVm))); + } + + private static VmTyped letBody(VmTyped exprVm) { + return wrapExpr(requireExprChild(exprVm)); + } + + // The sole expression child of `exprVm` (throw/trace/unary-minus/logical-not/non-null operand). + private static VmTyped soleExpr(VmTyped exprVm) { + return wrapExpr(requireExprChild(exprVm)); + } + + private static String readKeyword(VmTyped exprVm) { + return firstTerminalText(exprVm, "read"); + } + + private static Object newExprType(VmTyped exprVm) { + var header = requireChild(exprVm, NodeType.NEW_HEADER); + var type = findTypeChildVm(header); + return type == null ? VmNull.withoutDefault() : wrapType(type); + } + + private static VmTyped newExprBody(VmTyped exprVm) { + return objectBodyNodeFactory.create(requireChild(exprVm, NodeType.OBJECT_BODY)); + } + + private static VmTyped amendsParentExpr(VmTyped exprVm) { + return wrapExpr(requireExprChild(exprVm)); + } + + private static VmTyped amendsBody(VmTyped exprVm) { + return objectBodyNodeFactory.create(requireChild(exprVm, NodeType.OBJECT_BODY)); + } + + private static String binaryOpOperator(VmTyped exprVm) { + var op = findChildVm(exprVm, NodeType.OPERATOR); + if (op == null) { + return ""; + } + var data = (GenericNodeData) op.getExtraStorage(); + return data.node.text(data.source); + } + + private static VmTyped binaryOpLeft(VmTyped exprVm) { + return wrapExpr(findExprChildrenVm(exprVm).get(0)); + } + + private static VmTyped binaryOpRight(VmTyped exprVm) { + return wrapExpr(findExprChildrenVm(exprVm).get(1)); + } + + private static VmTyped typeOpType(VmTyped exprVm) { + return wrapType(requireTypeChild(exprVm)); + } + + private static VmObjectFactory binaryOpFactory(VmTyped exprVm) { + var operator = binaryOpOperator(exprVm); + return switch (operator) { + case "**" -> exponentiationExprNodeFactory; + case "*" -> multiplicationExprNodeFactory; + case "/" -> divisionExprNodeFactory; + case "~/" -> integerDivisionExprNodeFactory; + case "%" -> remainderExprNodeFactory; + case "+" -> additionExprNodeFactory; + case "-" -> subtractionExprNodeFactory; + case "<" -> lessThanExprNodeFactory; + case "<=" -> lessThanOrEqualExprNodeFactory; + case ">" -> greaterThanExprNodeFactory; + case ">=" -> greaterThanOrEqualExprNodeFactory; + case "==" -> equalExprNodeFactory; + case "!=" -> notEqualExprNodeFactory; + case "&&" -> logicalAndExprNodeFactory; + case "||" -> logicalOrExprNodeFactory; + case "|>" -> pipeExprNodeFactory; + case "??" -> nullCoalescingExprNodeFactory; + case "is" -> typeCheckExprNodeFactory; + case "as" -> typeCastExprNodeFactory; + default -> + throw new VmExceptionBuilder().bug("Unexpected binary operator: " + operator).build(); + }; + } + + private static VmListing functionLiteralParameters(VmTyped exprVm) { + return parametersOf(exprVm); + } + + private static VmTyped functionLiteralBody(VmTyped exprVm) { + return wrapExpr(requireExprChild(requireChild(exprVm, NodeType.FUNCTION_LITERAL_BODY))); + } + + private static VmTyped parenthesizedExpression(VmTyped exprVm) { + var elems = requireChild(exprVm, NodeType.PARENTHESIZED_EXPR_ELEMENTS); + return wrapExpr(requireExprChild(elems)); + } + + private static Object argumentsOrNull(VmTyped ownerVm) { + var argList = findChildVm(ownerVm, NodeType.ARGUMENT_LIST); + return argList == null ? VmNull.withoutDefault() : argumentsOf(argList); + } + + private static VmListing argumentsOf(VmTyped argListVm) { + var elems = findChildVm(argListVm, NodeType.ARGUMENT_LIST_ELEMENTS); + if (elems == null) { + return VmListing.empty(); + } + return wrapExprs(findExprChildrenVm(elems)); + } + + private static VmTyped requireChild(VmTyped genericVm, NodeType type) { + var child = findChildVm(genericVm, type); + if (child == null) { + throw new VmExceptionBuilder().bug("Expected a `" + type + "` child.").build(); + } + return child; + } + + private static VmTyped requireExprChild(VmTyped genericVm) { + var expr = findExprChildVm(genericVm); + if (expr == null) { + throw new VmExceptionBuilder().bug("Expected an expression child.").build(); + } + return expr; + } + + private static @Nullable VmTyped lastChildVm(VmTyped genericVm, NodeType type) { + var data = (GenericNodeData) genericVm.getExtraStorage(); + var children = data.node.children; + VmTyped result = null; + for (var i = 0; i < children.size(); i++) { + if (children.get(i).type == type) { + result = (VmTyped) data.childrenVm.get(i); + } + } + return result; + } + + private static VmListing objectBodyParameters(VmTyped bodyVm) { + var paramList = findChildVm(bodyVm, NodeType.OBJECT_PARAMETER_LIST); + if (paramList == null) { + return VmListing.empty(); + } + return wrapAll(findChildrenVm(paramList, NodeType.PARAMETER), parameterNodeFactory); + } + + private static VmListing objectBodyMembers(VmTyped bodyVm) { + var memberList = findChildVm(bodyVm, NodeType.OBJECT_MEMBER_LIST); + if (memberList == null) { + return VmListing.empty(); + } + var data = (GenericNodeData) memberList.getExtraStorage(); + var children = data.node.children; + var result = new ArrayList<>(); + for (var i = 0; i < children.size(); i++) { + var childVm = (VmTyped) data.childrenVm.get(i); + var member = + switch (children.get(i).type) { + case OBJECT_PROPERTY -> objectPropertyNodeFactory.create(childVm); + case OBJECT_METHOD -> objectMethodNodeFactory.create(childVm); + // an element is represented by its expression + case OBJECT_ELEMENT -> soleExpr(childVm); + case OBJECT_ENTRY -> objectEntryNodeFactory.create(childVm); + case OBJECT_SPREAD -> objectSpreadNodeFactory.create(childVm); + case MEMBER_PREDICATE -> memberPredicateNodeFactory.create(childVm); + case FOR_GENERATOR -> forGeneratorNodeFactory.create(childVm); + case WHEN_GENERATOR -> whenGeneratorNodeFactory.create(childVm); + default -> null; + }; + if (member != null) { + result.add(member); + } + } + return listingOf(result.toArray()); + } + + private static @Nullable VmTyped objectPropertyHeaderBegin(VmTyped propertyVm) { + var header = findChildVm(propertyVm, NodeType.OBJECT_PROPERTY_HEADER); + return header == null ? null : findChildVm(header, NodeType.OBJECT_PROPERTY_HEADER_BEGIN); + } + + private static VmListing objectPropertyModifiers(VmTyped propertyVm) { + var headerBegin = objectPropertyHeaderBegin(propertyVm); + return headerBegin == null ? VmListing.empty() : modifiersOf(headerBegin); + } + + private static VmTyped objectPropertyIdentifier(VmTyped propertyVm) { + return identifierNodeOf(objectPropertyHeaderBegin(propertyVm)); + } + + private static Object objectPropertyTypeAnnotation(VmTyped propertyVm) { + return typeAnnotationOf(findChildVm(propertyVm, NodeType.OBJECT_PROPERTY_HEADER)); + } + + private static Object objectPropertyValue(VmTyped propertyVm) { + return exprInChild(propertyVm, NodeType.OBJECT_PROPERTY_BODY); + } + + private static VmListing objectPropertyObjectBodies(VmTyped propertyVm) { + return objectBodiesOf(propertyVm); + } + + private static VmTyped objectMethodBody(VmTyped methodVm) { + return wrapExpr(requireExprChild(requireChild(methodVm, NodeType.CLASS_METHOD_BODY))); + } + + private static VmTyped memberPredicateCondition(VmTyped predicateVm) { + return wrapExpr(findExprChildrenVm(predicateVm).get(0)); + } + + private static Object memberPredicateValue(VmTyped predicateVm) { + var exprs = findExprChildrenVm(predicateVm); + return exprs.size() < 2 ? VmNull.withoutDefault() : wrapExpr(exprs.get(1)); + } + + private static VmListing memberPredicateObjectBodies(VmTyped predicateVm) { + return objectBodiesOf(predicateVm); + } + + private static VmTyped objectEntryKey(VmTyped entryVm) { + var header = requireChild(entryVm, NodeType.OBJECT_ENTRY_HEADER); + return wrapExpr(requireExprChild(header)); + } + + private static Object objectEntryValue(VmTyped entryVm) { + var expr = findExprChildVm(entryVm); + return expr == null ? VmNull.withoutDefault() : wrapExpr(expr); + } + + private static VmListing objectEntryObjectBodies(VmTyped entryVm) { + return objectBodiesOf(entryVm); + } + + private static String objectSpreadKeyword(VmTyped spreadVm) { + return firstTerminalText(spreadVm, "..."); + } + + private static VmTyped whenCondition(VmTyped whenVm) { + var header = requireChild(whenVm, NodeType.WHEN_GENERATOR_HEADER); + return wrapExpr(requireExprChild(header)); + } + + private static VmTyped whenThenBody(VmTyped whenVm) { + return objectBodyNodeFactory.create(findChildrenVm(whenVm, NodeType.OBJECT_BODY).get(0)); + } + + private static Object whenElseBody(VmTyped whenVm) { + var bodies = findChildrenVm(whenVm, NodeType.OBJECT_BODY); + return bodies.size() < 2 + ? VmNull.withoutDefault() + : objectBodyNodeFactory.create(bodies.get(1)); + } + + private static List forGeneratorParams(VmTyped forVm) { + var header = findChildVm(forVm, NodeType.FOR_GENERATOR_HEADER); + var def = header == null ? null : findChildVm(header, NodeType.FOR_GENERATOR_HEADER_DEFINITION); + var defHeader = + def == null ? null : findChildVm(def, NodeType.FOR_GENERATOR_HEADER_DEFINITION_HEADER); + return defHeader == null ? new ArrayList<>() : findChildrenVm(defHeader, NodeType.PARAMETER); + } + + private static Object forKeyParameter(VmTyped forVm) { + var params = forGeneratorParams(forVm); + return params.size() < 2 ? VmNull.withoutDefault() : parameterNodeFactory.create(params.get(0)); + } + + private static VmTyped forValueParameter(VmTyped forVm) { + var params = forGeneratorParams(forVm); + return parameterNodeFactory.create(params.get(params.size() - 1)); + } + + private static VmTyped forIterable(VmTyped forVm) { + var header = requireChild(forVm, NodeType.FOR_GENERATOR_HEADER); + var def = requireChild(header, NodeType.FOR_GENERATOR_HEADER_DEFINITION); + return wrapExpr(requireExprChild(def)); + } + + private static VmTyped forBody(VmTyped forVm) { + return objectBodyNodeFactory.create(requireChild(forVm, NodeType.OBJECT_BODY)); + } + + private static VmListing qualifiedIdentifierIdentifiers(VmTyped qualifiedVm) { + return wrapAll(findChildrenVm(qualifiedVm, NodeType.IDENTIFIER), identifierNodeFactory); + } + + private static String qualifiedIdentifierValue(VmTyped qualifiedVm) { + var idVms = findChildrenVm(qualifiedVm, NodeType.IDENTIFIER); + var builder = new StringBuilder(); + for (var i = 0; i < idVms.size(); i++) { + if (i > 0) { + builder.append('.'); + } + var text = nodeText((GenericNodeData) idVms.get(i).getExtraStorage()); + builder.append(text == null ? "" : text); + } + return builder.toString(); + } + + // Unlike other modifiers, variance modifiers are plain terminals rather than `modifier` nodes. + private static VmListing typeParameterModifiers(VmTyped typeParameterVm) { + var data = (GenericNodeData) typeParameterVm.getExtraStorage(); + var result = new ArrayList<>(); + for (var child : data.node.children) { + if (child.type == NodeType.TERMINAL) { + var text = child.text(data.source); + if ("in".equals(text) || "out".equals(text)) { + result.add(text); + } + } + } + return listingOf(result.toArray()); + } + + private static boolean parameterIsBlankIdentifier(VmTyped parameterVm) { + return findChildVm(parameterVm, NodeType.IDENTIFIER) == null; + } + + private static Object parameterIdentifier(VmTyped parameterVm) { + var id = findChildVm(parameterVm, NodeType.IDENTIFIER); + return id == null ? VmNull.withoutDefault() : identifierNodeFactory.create(id); + } + + private static Object parameterTypeAnnotation(VmTyped parameterVm) { + return typeAnnotationOf(parameterVm); + } + + private static VmListing buildStringParts(VmTyped stringVm) { + var data = (GenericNodeData) stringVm.getExtraStorage(); + var children = data.node.children; + var childrenVm = data.childrenVm; + var parts = new ArrayList<>(); + // skip the opening and closing quote terminals + var end = children.size() - 1; + var i = 1; + while (i < end) { + var child = children.get(i); + switch (child.type) { + case STRING_CHARS -> { + parts.add(stringCharsNodeFactory.create((VmTyped) childrenVm.get(i))); + i++; + } + case STRING_ESCAPE -> { + parts.add(stringEscapeNodeFactory.create((VmTyped) childrenVm.get(i))); + i++; + } + case STRING_NEWLINE -> { + parts.add(stringNewlineNodeFactory.create((VmTyped) childrenVm.get(i))); + i++; + } + case TERMINAL -> { + if (isInterpolationStart(child, data.source)) { + var exprIdx = nextNonAffix(children, i + 1, end); + if (exprIdx >= end) { + i = end; + } else { + parts.add(stringInterpolationNodeFactory.create((VmTyped) childrenVm.get(exprIdx))); + i = nextNonAffix(children, exprIdx + 1, end) + 1; + } + } else { + i++; + } + } + default -> i++; // affixes and stray terminals + } + } + return listingOf(parts.toArray()); + } + + private static boolean isInterpolationStart(Node terminal, char[] source) { + var text = terminal.text(source); + return text.endsWith("(") && (text.startsWith("\\") || text.startsWith("#")); + } + + // The next index in `[start, end)` whose node is not an affix (comment/semicolon), else `end`. + private static int nextNonAffix(List children, int start, int end) { + var i = start; + while (i < end) { + var type = children.get(i).type; + if (type == NodeType.LINE_COMMENT + || type == NodeType.BLOCK_COMMENT + || type == NodeType.SEMICOLON) { + i++; + } else { + return i; + } + } + return i; + } + + private static VmListing moduleDeclModifiers(VmTyped declVm) { + var moduleDefinition = findChildVm(declVm, NodeType.MODULE_DEFINITION); + return moduleDefinition == null ? VmListing.empty() : modifiersOf(moduleDefinition); + } + + private static Object moduleDeclName(VmTyped declVm) { + var moduleDefinition = findChildVm(declVm, NodeType.MODULE_DEFINITION); + if (moduleDefinition == null) { + return VmNull.withoutDefault(); + } + var name = findChildVm(moduleDefinition, NodeType.QUALIFIED_IDENTIFIER); + return name == null ? VmNull.withoutDefault() : qualifiedIdentifierNodeFactory.create(name); + } + + private static Object moduleDeclExtendsOrAmendsClause(VmTyped declVm) { + var clause = findChildVm(declVm, NodeType.AMENDS_CLAUSE); + if (clause == null) { + clause = findChildVm(declVm, NodeType.EXTENDS_CLAUSE); + } + return clause == null + ? VmNull.withoutDefault() + : extendsOrAmendsClauseNodeFactory.create(clause); + } + + private static String extendsOrAmendsClauseKeyword(VmTyped clauseVm) { + var data = (GenericNodeData) clauseVm.getExtraStorage(); + return data.node.type == NodeType.AMENDS_CLAUSE ? "amends" : "extends"; + } + + // The typed classes, typealiases, properties, and methods among `ownerVm`'s children, + // in source order. + private static VmListing memberChildren(VmTyped ownerVm) { + var data = (GenericNodeData) ownerVm.getExtraStorage(); + var children = data.node.children; + var result = new ArrayList<>(); + for (var i = 0; i < children.size(); i++) { + var factory = + switch (children.get(i).type) { + case CLASS -> classNodeFactory; + case TYPEALIAS -> typeAliasNodeFactory; + case CLASS_PROPERTY -> classPropertyNodeFactory; + case CLASS_METHOD -> classMethodNodeFactory; + default -> null; + }; + if (factory != null) { + result.add(factory.create((VmTyped) data.childrenVm.get(i))); + } + } + return listingOf(result.toArray()); + } + + private static VmListing classModifiers(VmTyped classVm) { + var header = findChildVm(classVm, NodeType.CLASS_HEADER); + return header == null ? VmListing.empty() : modifiersOf(header); + } + + private static VmTyped classIdentifier(VmTyped classVm) { + return identifierNodeOf(findChildVm(classVm, NodeType.CLASS_HEADER)); + } + + private static VmListing classTypeParameters(VmTyped classVm) { + return typeParametersOf(findChildVm(classVm, NodeType.CLASS_HEADER)); + } + + private static Object classSuperType(VmTyped classVm) { + var header = findChildVm(classVm, NodeType.CLASS_HEADER); + if (header == null) { + return VmNull.withoutDefault(); + } + var ext = findChildVm(header, NodeType.CLASS_HEADER_EXTENDS); + if (ext == null) { + return VmNull.withoutDefault(); + } + var type = findTypeChildVm(ext); + return type == null ? VmNull.withoutDefault() : wrapType(type); + } + + private static Object classBody(VmTyped classVm) { + var body = findChildVm(classVm, NodeType.CLASS_BODY); + return body == null ? VmNull.withoutDefault() : classBodyNodeFactory.create(body); + } + + private static VmListing typeAliasModifiers(VmTyped typeAliasVm) { + var header = findChildVm(typeAliasVm, NodeType.TYPEALIAS_HEADER); + return header == null ? VmListing.empty() : modifiersOf(header); + } + + private static VmTyped typeAliasIdentifier(VmTyped typeAliasVm) { + return identifierNodeOf(findChildVm(typeAliasVm, NodeType.TYPEALIAS_HEADER)); + } + + private static VmListing typeAliasTypeParameters(VmTyped typeAliasVm) { + return typeParametersOf(findChildVm(typeAliasVm, NodeType.TYPEALIAS_HEADER)); + } + + private static VmTyped typeAliasType(VmTyped typeAliasVm) { + var body = findChildVm(typeAliasVm, NodeType.TYPEALIAS_BODY); + var type = body == null ? null : findTypeChildVm(body); + if (type == null) { + throw new VmExceptionBuilder().bug("A parsed `typealias` always has a body type.").build(); + } + return wrapType(type); + } + + private static @Nullable VmTyped classPropertyHeaderBegin(VmTyped propertyVm) { + var propHeader = findChildVm(propertyVm, NodeType.CLASS_PROPERTY_HEADER); + return propHeader == null + ? null + : findChildVm(propHeader, NodeType.CLASS_PROPERTY_HEADER_BEGIN); + } + + private static VmListing classPropertyModifiers(VmTyped propertyVm) { + var headerBegin = classPropertyHeaderBegin(propertyVm); + return headerBegin == null ? VmListing.empty() : modifiersOf(headerBegin); + } + + private static VmTyped classPropertyIdentifier(VmTyped propertyVm) { + return identifierNodeOf(classPropertyHeaderBegin(propertyVm)); + } + + private static Object classPropertyTypeAnnotation(VmTyped propertyVm) { + return typeAnnotationOf(findChildVm(propertyVm, NodeType.CLASS_PROPERTY_HEADER)); + } + + private static Object classPropertyValue(VmTyped propertyVm) { + return exprInChild(propertyVm, NodeType.CLASS_PROPERTY_BODY); + } + + private static VmListing classPropertyObjectBodies(VmTyped propertyVm) { + return objectBodiesOf(propertyVm); + } + + private static VmListing classMethodModifiers(VmTyped methodVm) { + var header = findChildVm(methodVm, NodeType.CLASS_METHOD_HEADER); + return header == null ? VmListing.empty() : modifiersOf(header); + } + + private static VmTyped classMethodIdentifier(VmTyped methodVm) { + return identifierNodeOf(findChildVm(methodVm, NodeType.CLASS_METHOD_HEADER)); + } + + private static VmListing classMethodTypeParameters(VmTyped methodVm) { + return typeParametersOf(methodVm); + } + + private static VmListing classMethodParameters(VmTyped methodVm) { + return parametersOf(methodVm); + } + + private static Object classMethodReturnType(VmTyped methodVm) { + return typeAnnotationOf(methodVm); + } + + private static Object classMethodBody(VmTyped methodVm) { + return exprInChild(methodVm, NodeType.CLASS_METHOD_BODY); + } + + private static VmListing classBodyMembers(VmTyped classBodyVm) { + var elements = findChildVm(classBodyVm, NodeType.CLASS_BODY_ELEMENTS); + if (elements == null) { + return VmListing.empty(); + } + return memberChildren(elements); + } + + private static VmListing moduleImports(VmTyped moduleVm) { + var importListVm = findChildVm(moduleVm, NodeType.IMPORT_LIST); + if (importListVm == null) { + return VmListing.empty(); + } + return wrapAll(findChildrenVm(importListVm, NodeType.IMPORT), importNodeFactory); + } + + private static String importKeyword(VmTyped importVm) { + return firstTerminalText(importVm, "import"); + } + + private static String importUri(VmTyped importVm) { + var data = (GenericNodeData) importVm.getExtraStorage(); + return extractStringChars(data.node, data.source); + } + + private static Object importAlias(VmTyped importVm) { + var aliasVm = findChildVm(importVm, NodeType.IMPORT_ALIAS); + if (aliasVm == null) { + return VmNull.withoutDefault(); + } + // an `import_alias` node always contains an `identifier` + return identifierNodeFactory.create(findChildVm(aliasVm, NodeType.IDENTIFIER)); + } + + private static String identifierText(VmTyped identifierVm) { + var text = nodeText((GenericNodeData) identifierVm.getExtraStorage()); + return text == null ? "" : text; + } + + private static @Nullable String nodeText(GenericNodeData data) { + return data.node.children.isEmpty() || data.node.type == NodeType.STRING_CHARS + ? data.node.text(data.source) + : null; + } + + // The text of the node's first terminal child, or `fallback` if it has none + private static String firstTerminalText(VmTyped nodeVm, String fallback) { + var data = (GenericNodeData) nodeVm.getExtraStorage(); + for (var child : data.node.children) { + if (child.type == NodeType.TERMINAL) { + return child.text(data.source); + } + } + return fallback; + } + + // Extract the string constant from a node's `string_chars` child (dropping the enclosing quotes) + private static String extractStringChars(Node node, char[] source) { + var stringChars = node.findChildByType(NodeType.STRING_CHARS); + if (stringChars == null) { + return ""; + } + var terminals = new ArrayList(); + for (var child : stringChars.children) { + if (child.type == NodeType.TERMINAL) { + terminals.add(child); + } + } + var builder = new StringBuilder(); + for (var i = 1; i < terminals.size() - 1; i++) { + var text = terminals.get(i).text(source); + builder.append(text); + } + return builder.toString(); + } + + // A `Listing` holding `elements`, in order. + private static VmListing listingOf(Object[] elements) { + return VmList.create(elements).toListing(); + } + + private static VmListing modifiersOf(VmTyped ownerVm) { + var modifierList = findChildVm(ownerVm, NodeType.MODIFIER_LIST); + if (modifierList == null) { + return VmListing.empty(); + } + var modifierVms = findChildrenVm(modifierList, NodeType.MODIFIER); + var result = new Object[modifierVms.size()]; + for (var i = 0; i < modifierVms.size(); i++) { + var data = (GenericNodeData) modifierVms.get(i).getExtraStorage(); + result[i] = data.node.text(data.source); + } + return listingOf(result); + } + + private static String stringCharsOf(VmTyped clauseVm) { + var data = (GenericNodeData) clauseVm.getExtraStorage(); + return extractStringChars(data.node, data.source); + } + + // The first child of `genericVm` with the given type, as a generic-node `VmTyped`, or null. + private static @Nullable VmTyped findChildVm(VmTyped genericVm, NodeType type) { + var data = (GenericNodeData) genericVm.getExtraStorage(); + var children = data.node.children; + for (var i = 0; i < children.size(); i++) { + if (children.get(i).type == type) { + return (VmTyped) data.childrenVm.get(i); + } + } + return null; + } + + // The first type-node child of `genericVm`, as a generic-node `VmTyped`, or null. + private static @Nullable VmTyped findTypeChildVm(VmTyped genericVm) { + var data = (GenericNodeData) genericVm.getExtraStorage(); + var children = data.node.children; + for (var i = 0; i < children.size(); i++) { + if (children.get(i).type.isType()) { + return (VmTyped) data.childrenVm.get(i); + } + } + return null; + } + + // The first expression-node child of `genericVm`, as a generic-node `VmTyped`, or null. + private static @Nullable VmTyped findExprChildVm(VmTyped genericVm) { + var data = (GenericNodeData) genericVm.getExtraStorage(); + var children = data.node.children; + for (var i = 0; i < children.size(); i++) { + if (children.get(i).type.isExpression()) { + return (VmTyped) data.childrenVm.get(i); + } + } + return null; + } + + // All type-node children of `genericVm`, as generic-node `VmTyped`s. + private static List findTypeChildrenVm(VmTyped genericVm) { + var data = (GenericNodeData) genericVm.getExtraStorage(); + var children = data.node.children; + var result = new ArrayList(); + for (var i = 0; i < children.size(); i++) { + if (children.get(i).type.isType()) { + result.add((VmTyped) data.childrenVm.get(i)); + } + } + return result; + } + + // All expression-node children of `genericVm`, as generic-node `VmTyped`s. + private static List findExprChildrenVm(VmTyped genericVm) { + var data = (GenericNodeData) genericVm.getExtraStorage(); + var children = data.node.children; + var result = new ArrayList(); + for (var i = 0; i < children.size(); i++) { + if (children.get(i).type.isExpression()) { + result.add((VmTyped) data.childrenVm.get(i)); + } + } + return result; + } + + // Wrap each generic type node into its `TypeNode` subclass, as a `VmListing`. + private static VmListing wrapTypes(List typeVms) { + var result = new Object[typeVms.size()]; + for (var i = 0; i < typeVms.size(); i++) { + result[i] = wrapType(typeVms.get(i)); + } + return listingOf(result); + } + + // Wrap each generic expression node into its `ExprNode` subclass, as a `VmListing`. + private static VmListing wrapExprs(List exprVms) { + var result = new Object[exprVms.size()]; + for (var i = 0; i < exprVms.size(); i++) { + result[i] = wrapExpr(exprVms.get(i)); + } + return listingOf(result); + } + + private static Object typeAnnotationOf(@Nullable VmTyped ownerVm) { + if (ownerVm == null) { + return VmNull.withoutDefault(); + } + var annotation = findChildVm(ownerVm, NodeType.TYPE_ANNOTATION); + if (annotation == null) { + return VmNull.withoutDefault(); + } + var type = findTypeChildVm(annotation); + if (type == null) { + throw new VmExceptionBuilder().bug("A `type_annotation` always contains a type.").build(); + } + return wrapType(type); + } + + // The expression inside a `containerType` child of `ownerVm`, as an ExprNode, or null when + // absent. + private static Object exprInChild(VmTyped ownerVm, NodeType containerType) { + var container = findChildVm(ownerVm, containerType); + if (container == null) { + return VmNull.withoutDefault(); + } + var expr = findExprChildVm(container); + return expr == null ? VmNull.withoutDefault() : wrapExpr(expr); + } + + private static VmListing objectBodiesOf(VmTyped ownerVm) { + return wrapAll(findChildrenVm(ownerVm, NodeType.OBJECT_BODY), objectBodyNodeFactory); + } + + private static VmListing parametersOf(VmTyped ownerVm) { + var list = findChildVm(ownerVm, NodeType.PARAMETER_LIST); + if (list == null) { + return VmListing.empty(); + } + var elems = findChildVm(list, NodeType.PARAMETER_LIST_ELEMENTS); + if (elems == null) { + return VmListing.empty(); + } + return wrapAll(findChildrenVm(elems, NodeType.PARAMETER), parameterNodeFactory); + } + + // Wrap each generic-node `VmTyped` into a typed node via `factory`, as a `VmListing`. + private static VmListing wrapAll(List genericVms, VmObjectFactory factory) { + var result = new Object[genericVms.size()]; + for (var i = 0; i < genericVms.size(); i++) { + result[i] = factory.create(genericVms.get(i)); + } + return listingOf(result); + } + + private static VmTyped identifierNodeOf(@Nullable VmTyped ownerVm) { + var id = ownerVm == null ? null : findChildVm(ownerVm, NodeType.IDENTIFIER); + return identifierNodeFactory.create(id); + } + + private static VmListing typeParametersOf(@Nullable VmTyped ownerVm) { + if (ownerVm == null) { + return VmListing.empty(); + } + var list = findChildVm(ownerVm, NodeType.TYPE_PARAMETER_LIST); + if (list == null) { + return VmListing.empty(); + } + var elems = findChildVm(list, NodeType.TYPE_PARAMETER_LIST_ELEMENTS); + if (elems == null) { + return VmListing.empty(); + } + return wrapAll(findChildrenVm(elems, NodeType.TYPE_PARAMETER), typeParameterNodeFactory); + } + + // Wrap a generic type node into its specific `TypeNode` subclass + private static VmTyped wrapType(VmTyped typeVm) { + var data = (GenericNodeData) typeVm.getExtraStorage(); + return switch (data.node.type) { + case UNKNOWN_TYPE -> unknownTypeNodeFactory.create(typeVm); + case NOTHING_TYPE -> nothingTypeNodeFactory.create(typeVm); + case MODULE_TYPE -> moduleTypeNodeFactory.create(typeVm); + case DECLARED_TYPE -> declaredTypeNodeFactory.create(typeVm); + case NULLABLE_TYPE -> nullableTypeNodeFactory.create(typeVm); + case UNION_TYPE -> unionTypeNodeFactory.create(typeVm); + case FUNCTION_TYPE -> functionTypeNodeFactory.create(typeVm); + case CONSTRAINED_TYPE -> constrainedTypeNodeFactory.create(typeVm); + case PARENTHESIZED_TYPE -> parenthesizedTypeNodeFactory.create(typeVm); + case STRING_CONSTANT_TYPE -> stringLiteralTypeNodeFactory.create(typeVm); + default -> + throw new VmExceptionBuilder().bug("Unexpected type node: " + data.node.type).build(); + }; + } + + // Wrap a generic expression node into its specific `ExprNode` subclass + private static VmTyped wrapExpr(VmTyped exprVm) { + var data = (GenericNodeData) exprVm.getExtraStorage(); + return switch (data.node.type) { + case THIS_EXPR -> thisExprNodeFactory.create(exprVm); + case OUTER_EXPR -> outerExprNodeFactory.create(exprVm); + case MODULE_EXPR -> moduleExprNodeFactory.create(exprVm); + case NULL_EXPR -> nullLiteralExprNodeFactory.create(exprVm); + case BOOL_LITERAL_EXPR -> booleanLiteralExprNodeFactory.create(exprVm); + case INT_LITERAL_EXPR -> intLiteralExprNodeFactory.create(exprVm); + case FLOAT_LITERAL_EXPR -> floatLiteralExprNodeFactory.create(exprVm); + case SINGLE_LINE_STRING_LITERAL_EXPR -> singleLineStringLiteralExprNodeFactory.create(exprVm); + case MULTI_LINE_STRING_LITERAL_EXPR -> multiLineStringLiteralExprNodeFactory.create(exprVm); + case UNQUALIFIED_ACCESS_EXPR -> unqualifiedAccessExprNodeFactory.create(exprVm); + case QUALIFIED_ACCESS_EXPR -> qualifiedAccessExprNodeFactory.create(exprVm); + case SUBSCRIPT_EXPR -> subscriptExprNodeFactory.create(exprVm); + case SUPER_ACCESS_EXPR -> superAccessExprNodeFactory.create(exprVm); + case SUPER_SUBSCRIPT_EXPR -> superSubscriptExprNodeFactory.create(exprVm); + case IF_EXPR -> ifExprNodeFactory.create(exprVm); + case LET_EXPR -> letExprNodeFactory.create(exprVm); + case THROW_EXPR -> throwExprNodeFactory.create(exprVm); + case TRACE_EXPR -> traceExprNodeFactory.create(exprVm); + case IMPORT_EXPR -> importExprNodeFactory.create(exprVm); + case READ_EXPR -> readExprNodeFactory.create(exprVm); + case NEW_EXPR -> newExprNodeFactory.create(exprVm); + case AMENDS_EXPR -> amendsExprNodeFactory.create(exprVm); + case BINARY_OP_EXPR -> binaryOpFactory(exprVm).create(exprVm); + case UNARY_MINUS_EXPR -> unaryMinusExprNodeFactory.create(exprVm); + case LOGICAL_NOT_EXPR -> logicalNotExprNodeFactory.create(exprVm); + case NON_NULL_EXPR -> nonNullExprNodeFactory.create(exprVm); + case FUNCTION_LITERAL_EXPR -> functionLiteralExprNodeFactory.create(exprVm); + case PARENTHESIZED_EXPR -> parenthesizedExprNodeFactory.create(exprVm); + default -> + throw new VmExceptionBuilder() + .bug("Unexpected expression node: " + data.node.type) + .build(); + }; + } + + // All children of `genericVm` with the given type, as generic-node `VmTyped`s. + private static List findChildrenVm(VmTyped genericVm, NodeType type) { + var data = (GenericNodeData) genericVm.getExtraStorage(); + var children = data.node.children; + var result = new ArrayList(); + for (var i = 0; i < children.size(); i++) { + if (children.get(i).type == type) { + result.add((VmTyped) data.childrenVm.get(i)); + } + } + return result; + } + + public abstract static class parseModule extends ExternalMethod1Node { + @Specialization + @TruffleBoundary + protected Object evalString(@SuppressWarnings("unused") VmTyped self, String source) { + return doParse(source, null); + } + + @Specialization + @TruffleBoundary + protected Object evalResource(@SuppressWarnings("unused") VmTyped self, VmTyped source) { + // `source` is a `pkl.base#Resource` + var text = (String) VmUtils.readMember(source, Identifier.TEXT); + return doParse(text, sourceUri(source)); + } + } + + public abstract static class parseModuleOrNull extends ExternalMethod1Node { + @Specialization + @TruffleBoundary + protected Object evalString(@SuppressWarnings("unused") VmTyped self, String source) { + return doParseOrNull(source, null); + } + + @Specialization + @TruffleBoundary + protected Object evalResource(@SuppressWarnings("unused") VmTyped self, VmTyped source) { + // `source` is a `pkl.base#Resource` + var text = (String) VmUtils.readMember(source, Identifier.TEXT); + return doParseOrNull(text, sourceUri(source)); + } + } + + public abstract static class parseExpression extends ExternalMethod1Node { + @Specialization + @TruffleBoundary + protected Object evalString(@SuppressWarnings("unused") VmTyped self, String source) { + return doParseExpression(source, null); + } + + @Specialization + @TruffleBoundary + protected Object evalResource(@SuppressWarnings("unused") VmTyped self, VmTyped source) { + // `source` is a `pkl.base#Resource` + var text = (String) VmUtils.readMember(source, Identifier.TEXT); + return doParseExpression(text, sourceUri(source)); + } + } + + public abstract static class parseExpressionOrNull extends ExternalMethod1Node { + @Specialization + @TruffleBoundary + protected Object evalString(@SuppressWarnings("unused") VmTyped self, String source) { + return doParseExpressionOrNull(source, null); + } + + @Specialization + @TruffleBoundary + protected Object evalResource(@SuppressWarnings("unused") VmTyped self, VmTyped source) { + // `source` is a `pkl.base#Resource` + var text = (String) VmUtils.readMember(source, Identifier.TEXT); + return doParseExpressionOrNull(text, sourceUri(source)); + } + } + + private static String sourceUri(VmTyped resource) { + return VmUtils.readMember(resource, Identifier.URI).toString(); + } + + private static Object doParse(String src, @Nullable String sourceUri) { + var sourceChars = src.toCharArray(); + try { + var parser = new GenericParser(); + var root = parser.parseModule(src); + var genericNode = convertNode(root, sourceChars, sourceUri); + return moduleNodeFactory.create(genericNode); + } catch (GenericParserError e) { + throw new VmExceptionBuilder().evalError("parserError").withHint(e.toString()).build(); + } + } + + private static Object doParseOrNull(String src, @Nullable String sourceUri) { + var sourceChars = src.toCharArray(); + try { + var parser = new GenericParser(); + var root = parser.parseModule(src); + var genericNode = convertNode(root, sourceChars, sourceUri); + return moduleNodeFactory.create(genericNode); + } catch (GenericParserError e) { + return VmNull.withoutDefault(); + } + } + + private static Object doParseExpression(String src, @Nullable String sourceUri) { + var sourceChars = src.toCharArray(); + try { + var parser = new GenericParser(); + var root = parser.parseExpressionInput(src); + return wrapExpr(convertNode(root, sourceChars, sourceUri)); + } catch (GenericParserError e) { + throw new VmExceptionBuilder().evalError("parserError").withHint(e.toString()).build(); + } + } + + private static Object doParseExpressionOrNull(String src, @Nullable String sourceUri) { + var sourceChars = src.toCharArray(); + try { + var parser = new GenericParser(); + var root = parser.parseExpressionInput(src); + return wrapExpr(convertNode(root, sourceChars, sourceUri)); + } catch (GenericParserError e) { + return VmNull.withoutDefault(); + } + } + + private static VmTyped convertNode( + Node genericNode, char[] sourceChars, @Nullable String sourceUri) { + // convert children recursively + var childrenList = new ArrayList(genericNode.children.size()); + for (var child : genericNode.children) { + childrenList.add(convertNode(child, sourceChars, sourceUri)); + } + + // materialize text now so that nodes reused verbatim by `transform`/`format` are + // self-contained + if (genericNode.children.isEmpty() || genericNode.type == NodeType.STRING_CHARS) { + genericNode.text(sourceChars); + } + + var childrenVm = VmList.create(childrenList.toArray()); + var spanVm = SyntaxNodes.spanFactory.create(new SpanData(genericNode.span, sourceUri)); + var data = new GenericNodeData(genericNode, sourceChars, childrenVm, spanVm); + + var result = genericNodeFactory.create(data); + + // set parent back-reference on each child + for (var childVm : childrenList) { + var childData = (GenericNodeData) childVm.getExtraStorage(); + childData.parentVm = result; + } + + return result; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/RendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/RendererNodes.java new file mode 100644 index 000000000..5ef65b8b2 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/RendererNodes.java @@ -0,0 +1,39 @@ +/* + * 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.core.stdlib.syntax; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.stdlib.ExternalMethod1Node; +import org.pkl.formatter.Formatter; +import org.pkl.formatter.GrammarVersion; + +public final class RendererNodes { + private RendererNodes() {} + + public abstract static class render extends ExternalMethod1Node { + @Specialization + @TruffleBoundary + protected String eval(VmTyped self, VmTyped nodeVm) { + var grammarVersion = (String) VmUtils.readMember(self, Identifier.GRAMMAR_VERSION); + var node = SyntaxNodes.convertVmToNode(nodeVm, SyntaxNodes.ZERO_SPAN); + return new Formatter(GrammarVersion.valueOf(grammarVersion)).format(node); + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/SyntaxNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/SyntaxNodes.java new file mode 100644 index 000000000..d139a31f9 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/SyntaxNodes.java @@ -0,0 +1,203 @@ +/* + * Copyright © 2025-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.core.stdlib.syntax; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import org.jspecify.annotations.Nullable; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.SyntaxModule; +import org.pkl.core.runtime.VmList; +import org.pkl.core.runtime.VmNull; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.stdlib.VmObjectFactory; +import org.pkl.parser.syntax.generic.FullSpan; +import org.pkl.parser.syntax.generic.Node; +import org.pkl.parser.syntax.generic.NodeType; + +public final class SyntaxNodes { + private SyntaxNodes() {} + + private static final char[] EMPTY_SOURCE = new char[0]; + static final FullSpan ZERO_SPAN = new FullSpan(0, 0, 0, 0, 0, 0); + + record SpanData(FullSpan span, @Nullable String sourceUri) {} + + private record SourceLocationData(int line, int column, @Nullable String sourceUri) {} + + private static final VmObjectFactory sourceLocationFactory = + new VmObjectFactory(SyntaxModule::getSourceLocationClass) + .addIntProperty("line", SourceLocationData::line) + .addIntProperty("column", SourceLocationData::column) + .addStringProperty( + "displayUri", sl -> displayUri(sl.sourceUri(), position(sl.line(), sl.column()))); + + static final VmObjectFactory spanFactory = + new VmObjectFactory(SyntaxModule::getSpanClass) + .addTypedProperty( + "start", + sd -> + sourceLocationFactory.create( + new SourceLocationData( + sd.span().lineBegin(), sd.span().colBegin(), sd.sourceUri()))) + .addTypedProperty( + "end", + sd -> + sourceLocationFactory.create( + new SourceLocationData( + sd.span().lineEnd(), sd.span().colEnd(), sd.sourceUri()))) + .addStringProperty( + "displayUri", + sd -> + displayUri( + sd.sourceUri(), + position(sd.span().lineBegin(), sd.span().colBegin()) + + "-" + + position(sd.span().lineEnd(), sd.span().colEnd()))); + + private static String position(int line, int column) { + return line + ":" + column; + } + + private static String displayUri(@Nullable String sourceUri, String position) { + return sourceUri == null ? position : sourceUri + "#" + position; + } + + /** Extra storage backing a Pkl {@code GenericNode} instance. */ + static final class GenericNodeData { + final Node node; + final char[] source; + @Nullable VmTyped parentVm; + VmList childrenVm; + @Nullable VmTyped spanVm; + + GenericNodeData(Node node, char[] source, VmList childrenVm, @Nullable VmTyped spanVm) { + this.node = node; + this.source = source; + this.childrenVm = childrenVm; + this.spanVm = spanVm; + } + } + + private static final VmObjectFactory genericNodeFactory = + new VmObjectFactory(SyntaxModule::getGenericNodeClass) + .addStringProperty("type", nd -> nd.node.type.name().toLowerCase(Locale.ROOT)) + .addListProperty("children", nd -> nd.childrenVm) + .addProperty("parent", nd -> VmNull.lift(nd.parentVm)) + .addProperty( + "text", + nd -> + nd.node.children.isEmpty() || nd.node.type == NodeType.STRING_CHARS + ? nd.node.text(nd.source) + : VmNull.withoutDefault()) + .addProperty("span", nd -> VmNull.lift(nd.spanVm)); + + /** Rebuild a node from {@code template} (its type, span, text) with new children. */ + static VmTyped rebuild(VmTyped template, Object[] newChildrenVm) { + var nodeType = + NodeType.valueOf( + ((String) VmUtils.readMember(template, Identifier.TYPE)).toUpperCase(Locale.ROOT)); + var spanVm = optSpan(template); + var span = readSpan(spanVm); + + var childJavaNodes = new ArrayList(newChildrenVm.length); + for (var child : newChildrenVm) { + // constructed (storage-less) children have no meaningful span; anchor them to this + // node's span so the formatter's line-break heuristics stay consistent with reused + // siblings (which keep their original spans). + childJavaNodes.add(convertVmToNode((VmTyped) child, span)); + } + var javaNode = + makeJavaNode(nodeType, span, childJavaNodes, VmUtils.readMember(template, Identifier.TEXT)); + + var childrenVm = VmList.create(newChildrenVm); + var result = + genericNodeFactory.create(new GenericNodeData(javaNode, EMPTY_SOURCE, childrenVm, spanVm)); + + // wire up the parent back-reference + for (var child : newChildrenVm) { + var childVm = (VmTyped) child; + if (childVm.hasExtraStorage()) { + ((GenericNodeData) childVm.getExtraStorage()).parentVm = result; + } + } + return result; + } + + /** + * Convert a Pkl {@code GenericNode} to a generic {@link Node}, reusing the parse-time node when + * present. + * + *

{@code fallbackSpan} is used for constructed nodes (and their descendants) that carry no + * meaningful span of their own, so that a subtree spliced into reused siblings lines up with + * them. + */ + static Node convertVmToNode(VmTyped nodeVm, FullSpan fallbackSpan) { + // a node still carrying its parse-time storage is verbatim from `parse`: reuse it wholesale + if (nodeVm.hasExtraStorage()) { + return ((GenericNodeData) nodeVm.getExtraStorage()).node; + } + + var typeStr = (String) VmUtils.readMember(nodeVm, Identifier.TYPE); + var nodeType = NodeType.valueOf(typeStr.toUpperCase(Locale.ROOT)); + + var ownSpan = readSpan(optSpan(nodeVm)); + // a constructed node that did not set its own span inherits the insertion point's span + var span = ownSpan.equals(ZERO_SPAN) ? fallbackSpan : ownSpan; + + var childrenVm = (VmList) VmUtils.readMember(nodeVm, Identifier.CHILDREN); + var children = new ArrayList(childrenVm.getLength()); + for (var i = 0; i < childrenVm.getLength(); i++) { + children.add(convertVmToNode((VmTyped) childrenVm.get(i), span)); + } + + return makeJavaNode(nodeType, span, children, VmUtils.readMember(nodeVm, Identifier.TEXT)); + } + + private static @Nullable VmTyped optSpan(VmTyped nodeVm) { + return VmUtils.readMember(nodeVm, Identifier.SPAN) instanceof VmTyped spanVm ? spanVm : null; + } + + private static FullSpan readSpan(@Nullable VmTyped spanVm) { + if (spanVm == null) { + return ZERO_SPAN; + } + var start = (VmTyped) VmUtils.readMember(spanVm, Identifier.START); + var end = (VmTyped) VmUtils.readMember(spanVm, Identifier.END); + return new FullSpan( + 0, + 0, + readPosition(start, Identifier.LINE), + readPosition(start, Identifier.COLUMN), + readPosition(end, Identifier.LINE), + readPosition(end, Identifier.COLUMN)); + } + + private static int readPosition(VmTyped sourceLocationVm, Identifier name) { + return ((Long) VmUtils.readMember(sourceLocationVm, name)).intValue(); + } + + private static Node makeJavaNode( + NodeType nodeType, FullSpan span, List children, Object textObj) { + var node = children.isEmpty() ? new Node(nodeType, span) : new Node(nodeType, span, children); + if (textObj instanceof String text) { + node.setText(text); + } + return node; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/package-info.java b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/package-info.java new file mode 100644 index 000000000..14f5426cc --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.pkl.core.stdlib.syntax; + +import org.jspecify.annotations.NullMarked; 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 b1e52a977..08b95965c 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -1206,5 +1206,8 @@ Redirected to: `{1}` invalidReferenceTypeAnnotationWithConstraint=\ `Reference` referent type argument may not include type constraints. +parserError=\ +Could not parse Pkl source. + cannotInstallPackageWithNoCache=\ Cannot install package to module cache dir when module cache is disabled. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/syntax/spans/sample.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/syntax/spans/sample.pkl new file mode 100644 index 000000000..1cd9df9f5 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/syntax/spans/sample.pkl @@ -0,0 +1,5 @@ +x = 1 + +foo { + bar = "baz" +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/expressions.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/expressions.pkl new file mode 100644 index 000000000..4b32a6ac7 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/expressions.pkl @@ -0,0 +1,285 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local function expr(source: String) = new syntax.Parser {}.parseExpression(source) + +facts { + ["literals"] { + local boolTrue = expr("true") + boolTrue is syntax.BooleanLiteralExprNode + (boolTrue as syntax.BooleanLiteralExprNode).value == true + + local boolFalse = expr("false") + boolFalse is syntax.BooleanLiteralExprNode + (boolFalse as syntax.BooleanLiteralExprNode).value == false + + local intLit = expr("42") + intLit is syntax.IntLiteralExprNode + (intLit as syntax.IntLiteralExprNode).value == "42" + + local hexLit = expr("0xFF") + hexLit is syntax.IntLiteralExprNode + (hexLit as syntax.IntLiteralExprNode).value == "0xFF" + + local floatLit = expr("3.14") + floatLit is syntax.FloatLiteralExprNode + (floatLit as syntax.FloatLiteralExprNode).value == "3.14" + + local nullLit = expr("null") + nullLit is syntax.NullLiteralExprNode + } + + ["strings"] { + local simple = expr(#""hello""#) + simple is syntax.SingleLineStringLiteralExprNode + (simple as syntax.SingleLineStringLiteralExprNode).parts.length == 1 + (simple as syntax.SingleLineStringLiteralExprNode).parts.first is syntax.StringCharsNode + ((simple as syntax.SingleLineStringLiteralExprNode).parts.first as syntax.StringCharsNode).value + == "hello" + + local withEscape = expr(#""hello\nworld""#) + withEscape is syntax.SingleLineStringLiteralExprNode + local escapeParts = (withEscape as syntax.SingleLineStringLiteralExprNode).parts + escapeParts.length == 3 + escapeParts[0] is syntax.StringCharsNode + escapeParts[1] is syntax.StringEscapeNode + (escapeParts[1] as syntax.StringEscapeNode).value == #"\n"# + + local withInterp = expr(#""hello \(name)""#) + withInterp is syntax.SingleLineStringLiteralExprNode + local interpParts = (withInterp as syntax.SingleLineStringLiteralExprNode).parts + interpParts.length == 2 + interpParts[0] is syntax.StringCharsNode + interpParts[1] is syntax.StringInterpolationNode + (interpParts[1] as syntax.StringInterpolationNode).expression + is syntax.UnqualifiedAccessExprNode + + local multiLine = + expr( + #""" + """ + hello + world + """ + """#, + ) + multiLine is syntax.MultiLineStringLiteralExprNode + local mlParts = (multiLine as syntax.MultiLineStringLiteralExprNode).parts + // each content line is preceded by a newline part + mlParts.length == 5 + mlParts[0] is syntax.StringNewlineNode + (mlParts[1] as syntax.StringCharsNode).value == "hello" + mlParts[2] is syntax.StringNewlineNode + (mlParts[3] as syntax.StringCharsNode).value == "world" + mlParts[4] is syntax.StringNewlineNode + } + + ["keyword expressions"] { + local thisExpr = expr("this") + thisExpr is syntax.ThisExprNode + + local outerExpr = expr("outer") + outerExpr is syntax.OuterExprNode + + local moduleExpr = expr("module") + moduleExpr is syntax.ModuleExprNode + } + + ["access expressions"] { + local unqual = expr("foo") + unqual is syntax.UnqualifiedAccessExprNode + (unqual as syntax.UnqualifiedAccessExprNode).identifier.name == "foo" + (unqual as syntax.UnqualifiedAccessExprNode).arguments == null + + local withArgs = expr("foo(1, 2)") + withArgs is syntax.UnqualifiedAccessExprNode + (withArgs as syntax.UnqualifiedAccessExprNode).identifier.name == "foo" + (withArgs as syntax.UnqualifiedAccessExprNode).arguments != null + (withArgs as syntax.UnqualifiedAccessExprNode).arguments.length == 2 + + local qual = expr("foo.bar") + qual is syntax.QualifiedAccessExprNode + (qual as syntax.QualifiedAccessExprNode).receiver is syntax.UnqualifiedAccessExprNode + (qual as syntax.QualifiedAccessExprNode).isNullSafe == false + (qual as syntax.QualifiedAccessExprNode).identifier.name == "bar" + + local nullSafe = expr("foo?.bar") + nullSafe is syntax.QualifiedAccessExprNode + (nullSafe as syntax.QualifiedAccessExprNode).isNullSafe == true + + local subscript = expr("foo[0]") + subscript is syntax.SubscriptExprNode + (subscript as syntax.SubscriptExprNode).receiver is syntax.UnqualifiedAccessExprNode + (subscript as syntax.SubscriptExprNode).index is syntax.IntLiteralExprNode + } + + ["binary operators"] { + local add = expr("1 + 2") + add is syntax.AdditionExprNode + (add as syntax.AdditionExprNode).left is syntax.IntLiteralExprNode + (add as syntax.AdditionExprNode).right is syntax.IntLiteralExprNode + + expr("1 ** 2") is syntax.ExponentiationExprNode + expr("1 * 2") is syntax.MultiplicationExprNode + expr("1 / 2") is syntax.DivisionExprNode + expr("1 ~/ 2") is syntax.IntegerDivisionExprNode + expr("1 % 2") is syntax.RemainderExprNode + expr("1 - 2") is syntax.SubtractionExprNode + expr("a < b") is syntax.LessThanExprNode + expr("a <= b") is syntax.LessThanOrEqualExprNode + expr("a > b") is syntax.GreaterThanExprNode + expr("a >= b") is syntax.GreaterThanOrEqualExprNode + expr("a == b") is syntax.EqualExprNode + expr("a != b") is syntax.NotEqualExprNode + expr("a && b") is syntax.LogicalAndExprNode + expr("a || b") is syntax.LogicalOrExprNode + expr("a |> b") is syntax.PipeExprNode + expr("a ?? b") is syntax.NullCoalescingExprNode + + local isOp = expr("a is String") + isOp is syntax.TypeCheckExprNode + (isOp as syntax.TypeCheckExprNode).expression is syntax.UnqualifiedAccessExprNode + (isOp as syntax.TypeCheckExprNode).type is syntax.DeclaredTypeNode + + local asOp = expr("a as String") + asOp is syntax.TypeCastExprNode + (asOp as syntax.TypeCastExprNode).expression is syntax.UnqualifiedAccessExprNode + (asOp as syntax.TypeCastExprNode).type is syntax.DeclaredTypeNode + } + + ["unary operators"] { + local neg = expr("-42") + neg is syntax.UnaryMinusExprNode + (neg as syntax.UnaryMinusExprNode).operand is syntax.IntLiteralExprNode + + local notExpr = expr("!flag") + notExpr is syntax.LogicalNotExprNode + (notExpr as syntax.LogicalNotExprNode).operand is syntax.UnqualifiedAccessExprNode + + local nonNull = expr("x!!") + nonNull is syntax.NonNullExprNode + (nonNull as syntax.NonNullExprNode).operand is syntax.UnqualifiedAccessExprNode + } + + ["if expression"] { + local ifExpr = expr("if (x) 1 else 2") + ifExpr is syntax.IfExprNode + (ifExpr as syntax.IfExprNode).condition is syntax.UnqualifiedAccessExprNode + (ifExpr as syntax.IfExprNode).thenExpr is syntax.IntLiteralExprNode + (ifExpr as syntax.IfExprNode).elseExpr is syntax.IntLiteralExprNode + } + + ["let expression"] { + local letExpr = expr("let (y = 1) y + 1") + letExpr is syntax.LetExprNode + (letExpr as syntax.LetExprNode).parameter.identifier!!.name == "y" + (letExpr as syntax.LetExprNode).bindingValue is syntax.IntLiteralExprNode + (letExpr as syntax.LetExprNode).body is syntax.AdditionExprNode + } + + ["new expression"] { + local newExpr = expr("new Mapping { [\"a\"] = 1 }") + newExpr is syntax.NewExprNode + (newExpr as syntax.NewExprNode).type is syntax.DeclaredTypeNode + } + + ["function literal"] { + local fn = expr("(x, y) -> x + y") + fn is syntax.FunctionLiteralExprNode + (fn as syntax.FunctionLiteralExprNode).parameters.length == 2 + (fn as syntax.FunctionLiteralExprNode).parameters[0].identifier!!.name == "x" + (fn as syntax.FunctionLiteralExprNode).parameters[1].identifier!!.name == "y" + (fn as syntax.FunctionLiteralExprNode).body is syntax.AdditionExprNode + } + + ["parenthesized expression"] { + local paren = expr("(1 + 2)") + paren is syntax.ParenthesizedExprNode + (paren as syntax.ParenthesizedExprNode).expression is syntax.AdditionExprNode + } + + ["throw and trace"] { + local throwExpr = expr(#"throw("error")"#) + throwExpr is syntax.ThrowExprNode + (throwExpr as syntax.ThrowExprNode).expression is syntax.SingleLineStringLiteralExprNode + + local traceExpr = expr("trace(42)") + traceExpr is syntax.TraceExprNode + (traceExpr as syntax.TraceExprNode).expression is syntax.IntLiteralExprNode + } + + ["import expression"] { + local impExpr = expr(#"import("foo.pkl")"#) + impExpr is syntax.ImportExprNode + (impExpr as syntax.ImportExprNode).keyword == "import" + (impExpr as syntax.ImportExprNode).uri == "foo.pkl" + + local impGlob = expr(#"import*("*.pkl")"#) + impGlob is syntax.ImportExprNode + (impGlob as syntax.ImportExprNode).keyword == "import*" + (impGlob as syntax.ImportExprNode).uri == "*.pkl" + } + + ["read expression"] { + local readExpr = expr(#"read("file.txt")"#) + readExpr is syntax.ReadExprNode + (readExpr as syntax.ReadExprNode).keyword == "read" + + local readOpt = expr(#"read?("file.txt")"#) + readOpt is syntax.ReadExprNode + (readOpt as syntax.ReadExprNode).keyword == "read?" + + local readGlob = expr(#"read*("*.txt")"#) + readGlob is syntax.ReadExprNode + (readGlob as syntax.ReadExprNode).keyword == "read*" + } + + ["class declaration"] { + local mod = + new syntax.Parser {}.parseModule( + """ + /// A person. + @Deprecated { message = "old" } + abstract open class Person extends Being { + name: String + function greet() = "hi" + } + """, + ) + mod.classes.length == 1 + local cls = mod.classes["Person"] + cls.identifier.name == "Person" + cls.modifiers == new Listing { "abstract"; "open" } + cls.docComment != null + cls.docComment!!.value == "A person." + cls.annotations.length == 1 + cls.annotations.first.type is syntax.DeclaredTypeNode + cls.superType is syntax.DeclaredTypeNode + cls.body != null + cls.body!!.properties.length == 1 + cls.body!!.methods.length == 1 + cls.typeParameters.isEmpty + } + + ["generic class"] { + local mod = new syntax.Parser {}.parseModule("class Box { value: T }") + local cls = mod.classes["Box"] + cls.identifier.name == "Box" + cls.typeParameters.length == 1 + cls.typeParameters.first.identifier.name == "T" + cls.typeParameters.first.modifiers == new Listing { "out" } + } + + ["minimal class"] { + local mod = new syntax.Parser {}.parseModule("class Empty") + local cls = mod.classes["Empty"] + cls.identifier.name == "Empty" + cls.modifiers.isEmpty + cls.docComment == null + cls.annotations.isEmpty + cls.superType == null + cls.body == null + cls.typeParameters.isEmpty + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/moduleStructure.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/moduleStructure.pkl new file mode 100644 index 000000000..bbe76daae --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/moduleStructure.pkl @@ -0,0 +1,314 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local parser = new syntax.Parser {} + +local function parse(source: String) = parser.parseModule(source) + +facts { + ["module declaration"] { + local mod = parse("module my.app") + mod.declaration != null + mod.declaration!!.name!!.value == "my.app" + mod.declaration!!.docComment == null + mod.declaration!!.annotations.length == 0 + mod.declaration!!.modifiers.isEmpty + mod.declaration!!.extendsOrAmendsClause == null + } + + ["module with modifiers and doc comment"] { + local mod = + parse( + """ + /// This is my module. + @Deprecated { message = "use other" } + open module my.mod + """, + ) + mod.declaration != null + mod.declaration!!.docComment != null + mod.declaration!!.docComment!!.value == "This is my module." + + mod.declaration!!.annotations.length == 1 + mod.declaration!!.annotations.first.type is syntax.DeclaredTypeNode + + mod.declaration!!.modifiers != null + mod.declaration!!.modifiers == new Listing { "open" } + } + + ["amends clause"] { + local mod = parse(#"amends "base.pkl""#) + mod.declaration != null + mod.declaration!!.extendsOrAmendsClause != null + mod.declaration!!.extendsOrAmendsClause!!.keyword == "amends" + mod.declaration!!.extendsOrAmendsClause!!.uri == "base.pkl" + } + + ["extends clause"] { + local mod = parse(#"extends "base.pkl""#) + mod.declaration != null + mod.declaration!!.extendsOrAmendsClause != null + mod.declaration!!.extendsOrAmendsClause!!.keyword == "extends" + mod.declaration!!.extendsOrAmendsClause!!.uri == "base.pkl" + } + + ["imports"] { + local mod = + parse( + """ + import "foo.pkl" + import "bar.pkl" as myBar + import* "*.pkl" + """, + ) + mod.imports.length == 3 + + mod.imports[0].uri == "foo.pkl" + mod.imports[0].keyword == "import" + mod.imports[0].alias == null + + mod.imports[1].uri == "bar.pkl" + mod.imports[1].alias!!.name == "myBar" + + mod.imports[2].uri == "*.pkl" + mod.imports[2].keyword == "import*" + } + + ["class declaration"] { + local mod = + parse( + """ + /// A bird class. + abstract class Bird { + name: String + function fly(speed: Int): Boolean = true + } + """, + ) + mod.classes.length == 1 + local cls = mod.classes["Bird"] + + cls.docComment != null + cls.docComment!!.value == "A bird class." + + cls.modifiers != null + cls.modifiers == new Listing { "abstract" } + + cls.identifier.name == "Bird" + + cls.typeParameters.isEmpty + cls.superType == null + + cls.body != null + cls.body!!.properties.length == 1 + cls.body!!.properties["name"].identifier.name == "name" + cls.body!!.properties["name"].typeAnnotation != null + cls.body!!.properties["name"].typeAnnotation is syntax.DeclaredTypeNode + cls.body!!.properties["name"].value == null + + cls.body!!.methods.length == 1 + cls.body!!.methods["fly"].identifier.name == "fly" + cls.body!!.methods["fly"].parameters.length == 1 + cls.body!!.methods["fly"].parameters.first.identifier!!.name == "speed" + cls.body!!.methods["fly"].returnType != null + cls.body!!.methods["fly"].body != null + cls.body!!.methods["fly"].body is syntax.BooleanLiteralExprNode + } + + ["class with extends and type parameters"] { + local mod = + parse( + """ + class Container extends Base { + item: T + } + """, + ) + local cls = mod.classes["Container"] + + cls.identifier.name == "Container" + cls.typeParameters != null + cls.typeParameters.length == 1 + cls.typeParameters.first.identifier.name == "T" + cls.typeParameters.first.modifiers.isEmpty + + cls.superType != null + cls.superType is syntax.DeclaredTypeNode + } + + ["typealias"] { + local mod = parse("typealias Positive = Int(this > 0)") + mod.typeAliases.length == 1 + local ta = mod.typeAliases["Positive"] + ta.identifier.name == "Positive" + ta.typeParameters.isEmpty + ta.type is syntax.ConstrainedTypeNode + } + + ["top-level properties and methods"] { + local mod = + parse( + """ + hidden name: String = "pkl" + local count: Int = 42 + function greet(who: String): String = "hi" + """, + ) + + mod.properties.length == 2 + mod.properties["name"].identifier.name == "name" + mod.properties["name"].modifiers != null + mod.properties["name"].modifiers == new Listing { "hidden" } + mod.properties["name"].value is syntax.SingleLineStringLiteralExprNode + + mod.properties["count"].identifier.name == "count" + mod.properties["count"].modifiers != null + mod.properties["count"].modifiers == new Listing { "local" } + + mod.methods.length == 1 + mod.methods["greet"].identifier.name == "greet" + mod.methods["greet"].parameters.length == 1 + mod.methods["greet"].returnType != null + mod.methods["greet"].body is syntax.SingleLineStringLiteralExprNode + } + + ["parameter variations"] { + local mod = parse("function f(x: Int, _, y): Boolean = true") + local params = mod.methods["f"].parameters + + params.length == 3 + + params[0].identifier!!.name == "x" + params[0].typeAnnotation != null + params[0].isBlankIdentifier == false + + params[1].identifier == null + params[1].isBlankIdentifier == true + params[1].typeAnnotation == null + + params[2].identifier!!.name == "y" + params[2].typeAnnotation == null + params[2].isBlankIdentifier == false + } + + // `value` carries the identifier verbatim, so quoted identifiers keep their backticks + ["quoted identifiers"] { + local mod = + parse( + """ + module `my mod`.`sub pkg` + + import "foo.pkl" as `my alias` + + class `My Class` { + `a prop`: String + function `do it`(`the arg`: Int) = `the arg` + } + + typealias `My Alias` = `My Class` + + const `my prop` = 0 + """, + ) + + mod.declaration!!.name!!.value == "`my mod`.`sub pkg`" + mod.declaration!!.name!!.identifiers.toList().map((i) -> i.text) + == List("`my mod`", "`sub pkg`") + mod.declaration!!.name!!.identifiers.toList().map((i) -> i.name) + == List("my mod", "sub pkg") + + mod.imports.first.alias!!.text == "`my alias`" + mod.imports.first.alias!!.name == "my alias" + + local cls = mod.classes["My Class"] + cls.identifier.text == "`My Class`" + cls.identifier.name == "My Class" + cls.body!!.properties["a prop"].identifier.name == "a prop" + cls.body!!.methods["do it"].identifier.name == "do it" + cls.body!!.methods["do it"].parameters.first.identifier!!.name == "the arg" + + local ta = mod.typeAliases["My Alias"] + ta.identifier.name == "My Alias" + (ta.type as syntax.DeclaredTypeNode).name.value == "`My Class`" + + mod.properties["my prop"].identifier.name == "my prop" + mod.properties["my prop"].modifiers == new Listing { "const" } + } + + ["quoted identifiers in object bodies and accesses"] { + local mod = parse("x { `inner prop` = `some`.`path` }") + local prop = mod.properties["x"].objectBodies.first.properties.first + prop.identifier.text == "`inner prop`" + prop.identifier.name == "inner prop" + + local access = prop.value as syntax.QualifiedAccessExprNode + access.identifier.name == "path" + (access.receiver as syntax.UnqualifiedAccessExprNode).identifier.name == "some" + } + + ["parser error"] { + local result = new syntax.Parser {}.parseModuleOrNull("x = {{{") + result == null + } + + ["members keep source order"] { + local mod = + parse( + """ + a = 1 + typealias A = Int + function f() = 0 + class C { + z: Int + } + b = 2 + """, + ) + + mod.members.length == 5 + mod.members[0] is syntax.ClassPropertyNode + mod.members[1] is syntax.TypeAliasNode + mod.members[2] is syntax.ClassMethodNode + mod.members[3] is syntax.ClassNode + mod.members[4] is syntax.ClassPropertyNode + + // the specific-member views index `members` by name + mod.properties.keys == Set("a", "b") + mod.typeAliases.keys == Set("A") + mod.methods.keys == Set("f") + mod.classes.keys == Set("C") + mod.classes["C"] == mod.members[3] + } + + ["building a module keeps member order"] { + local mod = + parse( + """ + a = 1 + typealias A = Int + function f() = 0 + b = 2 + """, + ) + + new syntax.Renderer {}.render(mod.builtNode) + == """ + a = 1 + typealias A = Int + function f() = 0 + b = 2 + + """ + } + + ["empty module"] { + local result = parse("") + result.declaration == null + result.imports.length == 0 + result.classes.length == 0 + result.typeAliases.length == 0 + result.properties.length == 0 + result.methods.length == 0 + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/objectMembers.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/objectMembers.pkl new file mode 100644 index 000000000..117f7090d --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/objectMembers.pkl @@ -0,0 +1,170 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local function parse(source: String) = new syntax.Parser {}.parseModule(source) + +local function body(source: String) = + let (result = parse("x { \(source) }")) + result.properties["x"].objectBodies.first + +facts { + ["object property"] { + local b = body("name = \"hello\"") + b.properties.length == 1 + local prop = b.properties.first + prop.identifier.name == "name" + prop.value is syntax.SingleLineStringLiteralExprNode + prop.modifiers.isEmpty + prop.typeAnnotation == null + prop.objectBodies.length == 0 + } + + ["object property with type and modifiers"] { + local b = body("local name: String = \"hello\"") + b.properties.length == 1 + local prop = b.properties.first + prop.modifiers == new Listing { "local" } + prop.typeAnnotation != null + prop.typeAnnotation is syntax.DeclaredTypeNode + } + + ["object property with amending body"] { + local b = body("inner { x = 1 }") + b.properties.length == 1 + local prop = b.properties.first + prop.identifier.name == "inner" + prop.value == null + prop.objectBodies.length == 1 + } + + ["object method"] { + local b = body("function greet(who: String): String = \"hi\"") + b.methods.length == 1 + local method = b.methods.first + method.identifier.name == "greet" + method.parameters.length == 1 + method.parameters.first.identifier!!.name == "who" + method.returnType != null + method.body is syntax.SingleLineStringLiteralExprNode + } + + ["object element"] { + local b = body("1\n 2\n 3") + b.elements.length == 3 + b.elements[0] is syntax.IntLiteralExprNode + b.elements[1] is syntax.IntLiteralExprNode + b.elements[2] is syntax.IntLiteralExprNode + } + + ["object entry"] { + local b = body("[\"key\"] = 42") + b.entries.length == 1 + b.entries.first.key is syntax.SingleLineStringLiteralExprNode + b.entries.first.value is syntax.IntLiteralExprNode + } + + ["object entry with amending body"] { + local b = body("[\"key\"] { x = 1 }") + b.entries.length == 1 + b.entries.first.key is syntax.SingleLineStringLiteralExprNode + b.entries.first.objectBodies.length == 1 + } + + ["object spread"] { + local b = body("...other") + b.spreads.length == 1 + b.spreads.first.keyword == "..." + b.spreads.first.expression is syntax.UnqualifiedAccessExprNode + } + + ["nullable object spread"] { + local b = body("...?other") + b.spreads.length == 1 + b.spreads.first.keyword == "...?" + } + + ["member predicate"] { + local b = body("[[name == \"foo\"]] = 1") + b.memberPredicates.length == 1 + b.memberPredicates.first.condition is syntax.EqualExprNode + b.memberPredicates.first.value is syntax.IntLiteralExprNode + } + + ["member predicate with amending body"] { + local b = body("[[name == \"foo\"]] { x = 1 }") + local pred = b.memberPredicates.first + pred.condition is syntax.EqualExprNode + pred.value == null + pred.objectBodies.length == 1 + } + + ["for generator"] { + local b = body("for (item in items) { item }") + b.forGenerators.length == 1 + local gen = b.forGenerators.first + gen.keyParameter == null + gen.valueParameter.identifier!!.name == "item" + gen.iterable is syntax.UnqualifiedAccessExprNode + } + + ["for generator with key"] { + local b = body("for (k, v in items) { v }") + local gen = b.forGenerators.first + gen.keyParameter != null + gen.keyParameter!!.identifier!!.name == "k" + gen.valueParameter.identifier!!.name == "v" + } + + ["when generator"] { + local b = body("when (flag) { 1 }") + b.whenGenerators.length == 1 + local gen = b.whenGenerators.first + gen.condition is syntax.UnqualifiedAccessExprNode + gen.elseBody == null + } + + ["when generator with else"] { + local b = body("when (flag) { 1 } else { 2 }") + local gen = b.whenGenerators.first + gen.condition is syntax.UnqualifiedAccessExprNode + gen.elseBody != null + gen.elseBody is syntax.ObjectBodyNode + } + + ["object body with parameters"] { + local mod = parse("x = new Listing { a, b -> a }") + local propVal = mod.properties["x"].value + propVal is syntax.NewExprNode + local newBody = (propVal as syntax.NewExprNode).body + newBody.parameters.length == 2 + newBody.parameters[0].identifier!!.name == "a" + newBody.parameters[1].identifier!!.name == "b" + } + + ["mixed object members"] { + local b = body(""" + name = "pkl" + 1 + ["key"] = 2 + function f() = 3 + """) + + // `members` keeps source order across member kinds + b.members.length == 4 + b.members[0] is syntax.ObjectPropertyNode + b.members[1] is syntax.IntLiteralExprNode + b.members[2] is syntax.ObjectEntryNode + b.members[3] is syntax.ObjectMethodNode + + // the specific-member views are derived from `members` + b.properties.length == 1 + b.properties.first == b.members[0] + b.elements.length == 1 + b.elements.first == b.members[1] + b.entries.length == 1 + b.entries.first == b.members[2] + b.methods.length == 1 + b.methods.first == b.members[3] + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/render.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/render.pkl new file mode 100644 index 000000000..4d377ca1a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/render.pkl @@ -0,0 +1,457 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local renderer = new syntax.Renderer {} + +local function roundTrip(source: String) = renderer.render(parseNode(source)!!) + +local function parseNode(source: String) = new syntax.Parser {}.parseModule(source).genericNode + +local function renderExpr(node: syntax.ExprNode) = renderer.render(node.builtNode).trim() + +local function replaceLeaf( + node: syntax.GenericNode, + targetType: syntax.NodeType, + oldText: String, + newText: String, +): syntax.GenericNode = + if (node.type == targetType && node.text == oldText) + (node) { text = newText } + else + (node) { + children = node.children.map((c) -> replaceLeaf(c, targetType, oldText, newText)) + } + +local function transformFirst( + node: syntax.GenericNode, + targetType: syntax.NodeType, + transform: (syntax.GenericNode) -> syntax.GenericNode, +): syntax.GenericNode = + if (node.type == targetType) + transform.apply(node) + else + (node) { + children = node.children.map((c) -> transformFirst(c, targetType, transform)) + } + +facts { + ["empty module"] { + roundTrip("") == "\n" + } + + ["simple property"] { + roundTrip("x = 1") == "x = 1\n" + } + + ["multiple properties"] { + roundTrip( + """ + x = 1 + y = 2 + """, + ) + == """ + x = 1 + y = 2 + + """ + } + + ["string literals"] { + roundTrip(#"x = "hello""#) == #"x = "hello"\#n"# + roundTrip(#"x = "hello\nworld""#) == #"x = "hello\nworld"\#n"# + roundTrip(#"x = "hello \(name)""#) == #"x = "hello \(name)"\#n"# + } + + ["multiline string"] { + roundTrip( + #""" + x = """ + hello + world + """ + """#, + ) + == #""" + x = + """ + hello + world + """ + + """# + } + + ["single-line string literal from a value"] { + renderExpr(syntax.singleLineStringLiteral("hello")) == #""hello""# + renderExpr(syntax.singleLineStringLiteral("")) == "\"\"" + renderExpr(syntax.singleLineStringLiteral("a\\b")) == #""a\\b""# + renderExpr(syntax.singleLineStringLiteral(#"he said "hi""#)) == #""he said \"hi\"""# + renderExpr(syntax.singleLineStringLiteral("a\nb\tc\rd")) == #""a\nb\tc\rd""# + renderExpr(syntax.singleLineStringLiteral(#"\(foo)"#)) == #""\\(foo)""# + } + + ["multi-line string literal from a value"] { + renderExpr(syntax.multiLineStringLiteral("hello\nworld")) == #""" + """ + hello + world + """ + """# + renderExpr(syntax.multiLineStringLiteral("")) == "\"\"\"\n\n\"\"\"" + renderExpr(syntax.multiLineStringLiteral("foo\n")) == #""" + """ + foo + + """ + """# + renderExpr(syntax.multiLineStringLiteral(" indented")) == #""" + """ + indented + """ + """# + renderExpr(syntax.multiLineStringLiteral("a\n\nb")) == #""" + """ + a + + b + """ + """# + renderExpr(syntax.multiLineStringLiteral("a\\b\tc\rd")) == #""" + """ + a\\b\tc\rd + """ + """# + renderExpr(syntax.multiLineStringLiteral(#"say """ now"#)) == #""" + """ + say \""" now + """ + """# + // in a longer run, every quote that would start a `"""` sequence is escaped + renderExpr(syntax.multiLineStringLiteral(#"quad """" quotes"#)) == #""" + """ + quad \"\""" quotes + """ + """# + } + + ["comments"] { + roundTrip( + """ + // this is a comment + x = 1 + """, + ) + == """ + // this is a comment + x = 1 + + """ + } + + ["doc comments"] { + roundTrip( + """ + /// A documented property. + x = 1 + """, + ) + == """ + /// A documented property. + x = 1 + + """ + } + + ["class declaration"] { + roundTrip( + """ + class Bird { + name: String + canFly: Boolean = true + } + """, + ) + == """ + class Bird { + name: String + canFly: Boolean = true + } + + """ + } + + ["module declaration"] { + roundTrip("module my.app") == "module my.app\n" + } + + ["imports"] { + roundTrip( + """ + import "foo.pkl" + import "bar.pkl" as myBar + """, + ) + == """ + import "bar.pkl" as myBar + import "foo.pkl" + + """ + } + + ["typealias"] { + roundTrip("typealias Positive = Int(this>0)") == "typealias Positive = Int(this > 0)\n" + } + + ["boolean literals"] { + roundTrip("x = true") == "x = true\n" + roundTrip("x = false") == "x = false\n" + } + + ["numeric literals"] { + roundTrip("x = 42") == "x = 42\n" + roundTrip("x = 3.14") == "x = 3.14\n" + roundTrip("x = 0xFF") == "x = 0xFF\n" + } + + ["null literal"] { + roundTrip("x = null") == "x = null\n" + } + + ["keyword expressions"] { + roundTrip("x = this") == "x = this\n" + roundTrip("x = outer") == "x = outer\n" + roundTrip("x = module") == "x = module\n" + } + + ["binary operators"] { + roundTrip("x = 1 + 2") == "x = 1 + 2\n" + roundTrip("x = a && b") == "x = a && b\n" + roundTrip("x = a is String") == "x = a is String\n" + } + + ["unary operators"] { + roundTrip("x = -1") == "x = -1\n" + roundTrip("x = !flag") == "x = !flag\n" + roundTrip("x = value!!") == "x = value!!\n" + } + + ["if expression"] { + roundTrip("x = if(a) b else c") == "x = if (a) b else c\n" + } + + ["function literal"] { + roundTrip("x = (a) -> a + 1") == "x = (a) -> a + 1\n" + } + + ["method declaration"] { + roundTrip("function greet(name: String): String = name") + == "function greet(name: String): String = name\n" + } + + ["modifiers"] { + roundTrip("hidden x = 1") == "hidden x = 1\n" + roundTrip("local x = 1") == "local x = 1\n" + } + + ["object body"] { + roundTrip( + """ + x { z -> + a = 1 + b = 2 + } + """, + ) + == """ + x { z -> + a = 1 + b = 2 + } + + """ + } + + ["annotations"] { + roundTrip( + """ + @Deprecated { message = "use other" } + x = 1 + """, + ) + == """ + @Deprecated { message = "use other" } + x = 1 + + """ + } + + ["type annotations"] { + roundTrip("x: String = \"hello\"") == "x: String = \"hello\"\n" + roundTrip("x: Int? = null") == "x: Int? = null\n" + roundTrip("x: String|Int = 1") == "x: String | Int = 1\n" + } + + ["for generator"] { + roundTrip( + """ + x { + for (k, v in items) { + [k] = v + } + } + """, + ) + == """ + x { + for (k, v in items) { + [k] = v + } + } + + """ + } + + ["when generator"] { + roundTrip( + """ + x { + when (flag) { + a = 1 + } else { + b = 2 + } + } + """, + ) + == """ + x { + when (flag) { + a = 1 + } else { + b = 2 + } + } + + """ + } + + ["let expression"] { + roundTrip("x = let (y = 1) y + 1") == "x = let (y = 1) y + 1\n" + } + + ["new expression"] { + roundTrip( + """ + x = new Dynamic { + a = 1 + } + """, + ) + == """ + x = new Dynamic { + a = 1 + } + + """ + } + + ["qualified access"] { + roundTrip("x = foo.bar.baz") == "x = foo.bar.baz\n" + } + + ["subscript"] { + roundTrip("x = list[0]") == "x = list[0]\n" + } + + ["parenthesized expression"] { + roundTrip("x = (1 + 2)") == "x = (1 + 2)\n" + } + + ["quoted identifiers"] { + roundTrip("const `my prop` = 0") == "const `my prop` = 0\n" + roundTrip("x = `some`.`path`") == "x = `some`.`path`\n" + roundTrip("module `my mod`.`sub pkg`") == "module `my mod`.`sub pkg`\n" + roundTrip("function `do it`(`the arg`: Int) = `the arg`") + == "function `do it`(`the arg`: Int) = `the arg`\n" + roundTrip( + """ + class `My Class` { + `a prop`: String + } + """, + ) + == """ + class `My Class` { + `a prop`: String + } + + """ + } + + ["modify identifier"] { + local root = parseNode("x = 1") + local modified = replaceLeaf(root!!, "identifier", "x", "y") + renderer.render(modified) == "y = 1\n" + } + + ["modify modifier"] { + local root = parseNode("hidden x = 1") + local modified = replaceLeaf(root!!, "modifier", "hidden", "local") + renderer.render(modified) == "local x = 1\n" + } + + ["modify string content"] { + local root = parseNode(#"x = "hello""#) + local modified = replaceLeaf(root!!, "string_chars", "hello", "world") + renderer.render(modified) == #"x = "world"\#n"# + } + + ["modify int literal"] { + local root = parseNode("x = 42") + local modified = replaceLeaf(root!!, "int_literal_expr", "42", "99") + renderer.render(modified) == "x = 99\n" + } + + ["modify boolean literal"] { + local root = parseNode("x = true") + local modified = replaceLeaf(root!!, "bool_literal_expr", "true", "false") + renderer.render(modified) == "x = false\n" + } + + ["modify float literal"] { + local root = parseNode("x = 3.14") + local modified = replaceLeaf(root!!, "float_literal_expr", "3.14", "2.72") + renderer.render(modified) == "x = 2.72\n" + } + + ["add new modifier"] { + local root = parseNode("local x = 1") + local constModifier = new syntax.GenericNode { + type = "modifier" + text = "const" + } + local modified = + transformFirst(root!!, "modifier_list", (n) -> (n) { + children = List(constModifier) + n.children + }) + // modifier order is switched by the formatter + renderer.render(modified) == """ + local const x = 1 + + """ + } + + ["a default renderer targets the V2 grammar"] { + local node = parseNode("x = 1") + renderer.render(node!!) == new syntax.Renderer { grammarVersion = "V2" }.render(node!!) + } + + ["renderer honors the grammar version"] { + local node = + parseNode("x = fn(argument1, argument2, argument3, argument4, argument5, argument6, argument7, argument8, argument9)") + new syntax.Renderer { grammarVersion = "V1" }.render(node!!) + != new syntax.Renderer { grammarVersion = "V2" }.render(node!!) + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/spans.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/spans.pkl new file mode 100644 index 000000000..50c175f8a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/spans.pkl @@ -0,0 +1,85 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local parser = new syntax.Parser {} + +local stringModule = parser.parseModule(""" + x = 1 + + foo { + bar = "baz" + } + """) + +local resourceModule = parser.parseModule(read(".../input-helper/syntax/spans/sample.pkl")) + +local function nestedProperty(mod: syntax.ModuleNode): syntax.GenericNode = + mod.genericNode!! + .fold(List(), (acc: List, n) -> if (n.type == "object_property") acc.add(n) else acc) + .last + +examples { + ["span of a module parsed from a string"] { + stringModule.genericNode.span + } + + ["span of a leaf parsed from a string"] { + nestedProperty(stringModule).span + } + + ["span of a module parsed from a resource carries the resource URI"] { + resourceModule.genericNode.span + } + + ["span of a leaf parsed from a resource carries the resource URI"] { + nestedProperty(resourceModule).span + } +} + +facts { + ["span positions are 1-based"] { + stringModule.genericNode.span!!.start.line == 1 + stringModule.genericNode.span!!.start.column == 1 + } + + ["end is exclusive"] { + local span = nestedProperty(stringModule).span!! + span.start.line == 4 + span.start.column == 3 + span.end.line == 4 + span.end.column == 14 + } + + ["a span's displayUri combines those of its start and end"] { + local span = nestedProperty(stringModule).span!! + span.displayUri == "\(span.start.displayUri)-\(span.end.displayUri)" + } + + ["a child's span is contained in its parent's"] { + local child = nestedProperty(stringModule) + local parent = child.parent!! + parent.span!!.start.line <= child.span!!.start.line + parent.span!!.end.line >= child.span!!.end.line + } + + ["nodes constructed from scratch have no span"] { + new syntax.IntLiteralExprNode { value = 42 }?.genericNode?.span == null + new syntax.IntLiteralExprNode { value = 42 }.builtNode.span == null + } + + ["a spanless constructed node still renders"] { + new syntax.Renderer {}.render(new syntax.IntLiteralExprNode { value = 42 }.builtNode).trim() + == "42" + } + + ["parsing the same text from a string and a resource differs only in displayUri"] { + local fromString = stringModule.genericNode.span!! + local fromResource = resourceModule.genericNode.span!! + fromString.start.line == fromResource.start.line + fromString.start.column == fromResource.start.column + fromString.end.line == fromResource.end.line + fromString.end.column == fromResource.end.column + fromString.displayUri != fromResource.displayUri + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/transform.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/transform.pkl new file mode 100644 index 000000000..298c168ef --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/transform.pkl @@ -0,0 +1,155 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local function mod(source: String): syntax.ModuleNode = new syntax.Parser {}.parseModule(source) + +local renderer = new syntax.Renderer {} + +local function fmt(source: String): String = renderer.render(mod(source).genericNode!!) + +local function transformFormat( + source: String, + operator: (syntax.GenericNode) -> Pair?, +): String = renderer.render(mod(source).genericNode!!.transform(operator)) + +local function renameIdentifier(source: String, oldText: String, newName: String): String = + transformFormat(source, (n) -> + if (n.type == "identifier" && n.text == oldText) + Pair(new syntax.IdentifierNode { name = newName }.builtNode, false) + else + null + ) + +local function accessNamed(newName: String): String = + transformFormat("x = 0", (n) -> + if (n.type == "int_literal_expr") + Pair( + new syntax.UnqualifiedAccessExprNode { + identifier = new syntax.IdentifierNode { name = newName } + arguments = null + }.builtNode, + false, + ) + else + null + ) + +facts { + ["read-only transform leaves the tree unchanged"] { + // returning `null` everywhere keeps every node and keeps descending + transformFormat("x = 1", (_) -> null) == fmt("x = 1") + transformFormat("x = if (cond) 1 else 2", (_) -> null) == fmt("x = if (cond) 1 else 2") + } + + ["rename identifiers everywhere"] { + transformFormat("foo = foo + 1", (n) -> + if (n.type == "identifier" && n.text == "foo") + Pair((n) { text = "bar" }, true) + else + null + ) == fmt("bar = bar + 1") + } + + ["replace a leaf via a typed node"] { + transformFormat("x = 0", (n) -> + if (n.type == "int_literal_expr" && n.text == "0") + Pair(new syntax.IntLiteralExprNode { value = 100 }.builtNode, false) + else + null + ) == fmt("x = 100") + } + + ["rebuilds ancestors of a changed node"] { + transformFormat("x = if (cond) 41 else 0", (n) -> + if (n.type == "int_literal_expr" && n.text == "41") + Pair(new syntax.IntLiteralExprNode { value = 42 }.builtNode, false) + else if (n.type == "int_literal_expr" && n.text == "0") + Pair(new syntax.IntLiteralExprNode { value = 100 }.builtNode, false) + else + null + ) == fmt("x = if (cond) 42 else 100") + } + + ["descend reprocesses emitted nodes"] { + transformFormat("x = 0", (n) -> + if (n.type == "int_literal_expr" && n.text == "0") + Pair( + new syntax.ParenthesizedExprNode { + expression = new syntax.IntLiteralExprNode { value = 1 } + }.builtNode, + true, + ) + else if (n.type == "int_literal_expr" && n.text == "1") + Pair(new syntax.IntLiteralExprNode { value = 2 }.builtNode, true) + else + null + ) == fmt("x = (2)") + } + + ["descend = false leaves the emitted subtree untouched"] { + transformFormat("x = 0", (n) -> + if (n.type == "int_literal_expr" && n.text == "0") + Pair( + new syntax.ParenthesizedExprNode { + expression = new syntax.IntLiteralExprNode { value = 1 } + }.builtNode, + false, + ) + else if (n.type == "int_literal_expr" && n.text == "1") + Pair(new syntax.IntLiteralExprNode { value = 2 }.builtNode, true) + else + null + ) == fmt("x = (1)") + } + + ["build super access from scratch"] { + transformFormat("x = 0", (n) -> + if (n.type == "int_literal_expr") + Pair( + new syntax.SuperAccessExprNode { + identifier = new syntax.IdentifierNode { name = "foo" } + arguments = null + }.builtNode, + false, + ) + else + null + ) == fmt("x = super.foo") + + transformFormat("x = 0", (n) -> + if (n.type == "int_literal_expr") + Pair( + new syntax.SuperAccessExprNode { + identifier = new syntax.IdentifierNode { name = "foo" } + arguments { new syntax.IntLiteralExprNode { value = 1 } } + }.builtNode, + false, + ) + else + null + ) == fmt("x = super.foo(1)") + } + + ["build super subscript from scratch"] { + transformFormat("x = 0", (n) -> + if (n.type == "int_literal_expr") + Pair( + new syntax.SuperSubscriptExprNode { + index = new syntax.IntLiteralExprNode { value = 0 } + }.builtNode, + false, + ) + else + null + ) == fmt("x = super[0]") + } + + ["an identifier name is quoted as needed"] { + accessNamed("foo") == fmt("x = foo") + accessNamed("my ## foo") == fmt("x = `my ## foo`") + accessNamed("class") == fmt("x = `class`") + renameIdentifier("x = someProp", "someProp", "other prop") == fmt("x = `other prop`") + renameIdentifier("x = `some prop`", "`some prop`", "otherProp") == fmt("x = otherProp") + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/traversal.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/traversal.pkl new file mode 100644 index 000000000..3d6f04564 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/traversal.pkl @@ -0,0 +1,57 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local function mod(source: String): syntax.ModuleNode = new syntax.Parser {}.parseModule(source) + +local sample: syntax.GenericNode = + mod( + """ + import "foo.pkl" + import "bar.pkl" as baz + + class Point { + x: Int + y: Int + } + + origin = if (useOrigin) new Point { x = 0; y = 0 } else null + + scaled = origin.x * 2 + """, + ).genericNode!! + +facts { + ["fold counts nodes by predicate"] { + sample.fold(0, (acc, n) -> if (n.type == "import") acc + 1 else acc) == 2 + + sample.fold(0, (acc, n) -> if (n.type == "if_expr") acc + 1 else acc) == 1 + + sample.fold(0, (acc, n) -> if (n.type == "when_generator") acc + 1 else acc) == 0 + } + + ["fold accumulates into a collection"] { + sample.fold(List(), (acc, n) -> if (n.type == "identifier") acc.add(n.text) else acc) + == List( + "baz", + "Point", + "x", + "Int", + "y", + "Int", + "origin", + "useOrigin", + "Point", + "x", + "y", + "scaled", + "origin", + "x", + ) + } + + ["fold visits a node before its children (pre-order)"] { + // the module node is visited first + sample.fold(List(), (acc, n) -> acc.add(n.type)).first == "module" + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/types.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/types.pkl new file mode 100644 index 000000000..be0a15762 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/types.pkl @@ -0,0 +1,100 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local function parse(source: String): syntax.ModuleNode = new syntax.Parser {}.parseModule(source) + +local function typeOf(typeSource: String) = + parse("x: \(typeSource) = 0").properties["x"].typeAnnotation + +facts { + ["simple types"] { + local unknownT = typeOf("unknown") + unknownT is syntax.UnknownTypeNode + + local nothingT = typeOf("nothing") + nothingT is syntax.NothingTypeNode + + local moduleT = typeOf("module") + moduleT is syntax.ModuleTypeNode + } + + ["declared type"] { + local simple = typeOf("String") + simple is syntax.DeclaredTypeNode + (simple as syntax.DeclaredTypeNode).name.value == "String" + (simple as syntax.DeclaredTypeNode).typeArguments.isEmpty + + local withArgs = typeOf("List") + withArgs is syntax.DeclaredTypeNode + (withArgs as syntax.DeclaredTypeNode).name.value == "List" + (withArgs as syntax.DeclaredTypeNode).typeArguments.length == 1 + (withArgs as syntax.DeclaredTypeNode).typeArguments.first is syntax.DeclaredTypeNode + + local multiArgs = typeOf("Map") + multiArgs is syntax.DeclaredTypeNode + (multiArgs as syntax.DeclaredTypeNode).typeArguments.length == 2 + } + + ["nullable type"] { + local nullable = typeOf("String?") + nullable is syntax.NullableTypeNode + (nullable as syntax.NullableTypeNode).baseType is syntax.DeclaredTypeNode + } + + ["union type"] { + local union = typeOf("String|Int|Boolean") + union is syntax.UnionTypeNode + (union as syntax.UnionTypeNode).members.length == 3 + (union as syntax.UnionTypeNode).members[0] is syntax.DeclaredTypeNode + (union as syntax.UnionTypeNode).members[1] is syntax.DeclaredTypeNode + (union as syntax.UnionTypeNode).members[2] is syntax.DeclaredTypeNode + } + + ["function type"] { + local fnType = typeOf("(Int, String) -> Boolean") + fnType is syntax.FunctionTypeNode + (fnType as syntax.FunctionTypeNode).parameterTypes.length == 2 + (fnType as syntax.FunctionTypeNode).parameterTypes[0] is syntax.DeclaredTypeNode + (fnType as syntax.FunctionTypeNode).parameterTypes[1] is syntax.DeclaredTypeNode + (fnType as syntax.FunctionTypeNode).returnType is syntax.DeclaredTypeNode + + local noParams = typeOf("() -> String") + noParams is syntax.FunctionTypeNode + (noParams as syntax.FunctionTypeNode).parameterTypes.length == 0 + (noParams as syntax.FunctionTypeNode).returnType is syntax.DeclaredTypeNode + } + + ["constrained type"] { + local constrained = typeOf("Int(this >= 0)") + constrained is syntax.ConstrainedTypeNode + (constrained as syntax.ConstrainedTypeNode).baseType is syntax.DeclaredTypeNode + (constrained as syntax.ConstrainedTypeNode).constraints.length == 1 + (constrained as syntax.ConstrainedTypeNode).constraints.first is syntax.GreaterThanOrEqualExprNode + } + + ["parenthesized type"] { + local paren = typeOf("(String)") + paren is syntax.ParenthesizedTypeNode + (paren as syntax.ParenthesizedTypeNode).type is syntax.DeclaredTypeNode + } + + ["string constant type"] { + local result = parse(#"typealias Foo = "bar"|"baz""#) + local ta = result.typeAliases["Foo"] + ta.type is syntax.UnionTypeNode + local members = (ta.type as syntax.UnionTypeNode).members + members.length == 2 + members[0] is syntax.StringLiteralTypeNode + (members[0] as syntax.StringLiteralTypeNode).value == "bar" + members[1] is syntax.StringLiteralTypeNode + (members[1] as syntax.StringLiteralTypeNode).value == "baz" + } + + ["type annotation"] { + local result = parse("x: String = \"hello\"") + local prop = result.properties["x"] + prop.typeAnnotation != null + prop.typeAnnotation!! is syntax.DeclaredTypeNode + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err index 8e835110d..236355ef6 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err @@ -26,6 +26,7 @@ pkl:release pkl:semver pkl:settings pkl:shell +pkl:syntax pkl:test pkl:xml pkl:yaml diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/expressions.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/expressions.pcf new file mode 100644 index 000000000..4d309931d --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/expressions.pcf @@ -0,0 +1,174 @@ +facts { + ["literals"] { + true + true + true + true + true + true + true + true + true + true + true + } + ["strings"] { + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + } + ["keyword expressions"] { + true + true + true + } + ["access expressions"] { + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + } + ["binary operators"] { + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + } + ["unary operators"] { + true + true + true + true + true + true + } + ["if expression"] { + true + true + true + true + } + ["let expression"] { + true + true + true + true + } + ["new expression"] { + true + true + } + ["function literal"] { + true + true + true + true + true + } + ["parenthesized expression"] { + true + true + } + ["throw and trace"] { + true + true + true + true + } + ["import expression"] { + true + true + true + true + true + true + } + ["read expression"] { + true + true + true + true + true + true + } + ["class declaration"] { + true + true + true + true + true + true + true + true + true + true + true + true + } + ["generic class"] { + true + true + true + true + } + ["minimal class"] { + true + true + true + true + true + true + true + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/moduleStructure.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/moduleStructure.pcf new file mode 100644 index 000000000..814770cf6 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/moduleStructure.pcf @@ -0,0 +1,155 @@ +facts { + ["module declaration"] { + true + true + true + true + true + true + } + ["module with modifiers and doc comment"] { + true + true + true + true + true + true + true + } + ["amends clause"] { + true + true + true + true + } + ["extends clause"] { + true + true + true + true + } + ["imports"] { + true + true + true + true + true + true + true + true + } + ["class declaration"] { + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + } + ["class with extends and type parameters"] { + true + true + true + true + true + true + true + } + ["typealias"] { + true + true + true + true + } + ["top-level properties and methods"] { + true + true + true + true + true + true + true + true + true + true + true + true + true + } + ["parameter variations"] { + true + true + true + true + true + true + true + true + true + true + } + ["quoted identifiers"] { + true + true + true + true + true + true + true + true + true + true + true + true + true + true + } + ["quoted identifiers in object bodies and accesses"] { + true + true + true + true + } + ["parser error"] { + true + } + ["members keep source order"] { + true + true + true + true + true + true + true + true + true + true + true + } + ["building a module keeps member order"] { + true + } + ["empty module"] { + true + true + true + true + true + true + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/objectMembers.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/objectMembers.pcf new file mode 100644 index 000000000..680894e8c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/objectMembers.pcf @@ -0,0 +1,107 @@ +facts { + ["object property"] { + true + true + true + true + true + true + } + ["object property with type and modifiers"] { + true + true + true + true + } + ["object property with amending body"] { + true + true + true + true + } + ["object method"] { + true + true + true + true + true + true + } + ["object element"] { + true + true + true + true + } + ["object entry"] { + true + true + true + } + ["object entry with amending body"] { + true + true + true + } + ["object spread"] { + true + true + true + } + ["nullable object spread"] { + true + true + } + ["member predicate"] { + true + true + true + } + ["member predicate with amending body"] { + true + true + true + } + ["for generator"] { + true + true + true + true + } + ["for generator with key"] { + true + true + true + } + ["when generator"] { + true + true + true + } + ["when generator with else"] { + true + true + true + } + ["object body with parameters"] { + true + true + true + true + } + ["mixed object members"] { + true + true + true + true + true + true + true + true + true + true + true + true + true + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/render.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/render.pcf new file mode 100644 index 000000000..3133d0670 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/render.pcf @@ -0,0 +1,161 @@ +facts { + ["empty module"] { + true + } + ["simple property"] { + true + } + ["multiple properties"] { + true + } + ["string literals"] { + true + true + true + } + ["multiline string"] { + true + } + ["single-line string literal from a value"] { + true + true + true + true + true + true + } + ["multi-line string literal from a value"] { + true + true + true + true + true + true + true + true + } + ["comments"] { + true + } + ["doc comments"] { + true + } + ["class declaration"] { + true + } + ["module declaration"] { + true + } + ["imports"] { + true + } + ["typealias"] { + true + } + ["boolean literals"] { + true + true + } + ["numeric literals"] { + true + true + true + } + ["null literal"] { + true + } + ["keyword expressions"] { + true + true + true + } + ["binary operators"] { + true + true + true + } + ["unary operators"] { + true + true + true + } + ["if expression"] { + true + } + ["function literal"] { + true + } + ["method declaration"] { + true + } + ["modifiers"] { + true + true + } + ["object body"] { + true + } + ["annotations"] { + true + } + ["type annotations"] { + true + true + true + } + ["for generator"] { + true + } + ["when generator"] { + true + } + ["let expression"] { + true + } + ["new expression"] { + true + } + ["qualified access"] { + true + } + ["subscript"] { + true + } + ["parenthesized expression"] { + true + } + ["quoted identifiers"] { + true + true + true + true + true + } + ["modify identifier"] { + true + } + ["modify modifier"] { + true + } + ["modify string content"] { + true + } + ["modify int literal"] { + true + } + ["modify boolean literal"] { + true + } + ["modify float literal"] { + true + } + ["add new modifier"] { + true + } + ["a default renderer targets the V2 grammar"] { + true + } + ["renderer honors the grammar version"] { + true + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/spans.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/spans.pcf new file mode 100644 index 000000000..eda558e88 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/spans.pcf @@ -0,0 +1,95 @@ +facts { + ["span positions are 1-based"] { + true + true + } + ["end is exclusive"] { + true + true + true + true + } + ["a span's displayUri combines those of its start and end"] { + true + } + ["a child's span is contained in its parent's"] { + true + true + } + ["nodes constructed from scratch have no span"] { + true + true + } + ["a spanless constructed node still renders"] { + true + } + ["parsing the same text from a string and a resource differs only in displayUri"] { + true + true + true + true + true + } +} +examples { + ["span of a module parsed from a string"] { + new { + start { + line = X + column = 1 + displayUri = "1:1" + } + end { + line = X + column = 2 + displayUri = "5:2" + } + displayUri = "1:1-5:2" + } + } + ["span of a leaf parsed from a string"] { + new { + start { + line = X + column = 3 + displayUri = "4:3" + } + end { + line = X + column = 14 + displayUri = "4:14" + } + displayUri = "4:3-4:14" + } + } + ["span of a module parsed from a resource carries the resource URI"] { + new { + start { + line = X + column = 1 + displayUri = "file:///$snippetsDir/input-helper/syntax/spans/sample.pkl#1:1" + } + end { + line = X + column = 2 + displayUri = "file:///$snippetsDir/input-helper/syntax/spans/sample.pkl#5:2" + } + displayUri = "file:///$snippetsDir/input-helper/syntax/spans/sample.pkl#1:1-5:2" + } + } + ["span of a leaf parsed from a resource carries the resource URI"] { + new { + start { + line = X + column = 3 + displayUri = "file:///$snippetsDir/input-helper/syntax/spans/sample.pkl#4:3" + } + end { + line = X + column = 14 + displayUri = "file:///$snippetsDir/input-helper/syntax/spans/sample.pkl#4:14" + } + displayUri = "file:///$snippetsDir/input-helper/syntax/spans/sample.pkl#4:3-4:14" + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/transform.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/transform.pcf new file mode 100644 index 000000000..595098b1c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/transform.pcf @@ -0,0 +1,35 @@ +facts { + ["read-only transform leaves the tree unchanged"] { + true + true + } + ["rename identifiers everywhere"] { + true + } + ["replace a leaf via a typed node"] { + true + } + ["rebuilds ancestors of a changed node"] { + true + } + ["descend reprocesses emitted nodes"] { + true + } + ["descend = false leaves the emitted subtree untouched"] { + true + } + ["build super access from scratch"] { + true + true + } + ["build super subscript from scratch"] { + true + } + ["an identifier name is quoted as needed"] { + true + true + true + true + true + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/traversal.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/traversal.pcf new file mode 100644 index 000000000..a8b02f8ba --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/traversal.pcf @@ -0,0 +1,13 @@ +facts { + ["fold counts nodes by predicate"] { + true + true + true + } + ["fold accumulates into a collection"] { + true + } + ["fold visits a node before its children (pre-order)"] { + true + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/types.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/types.pcf new file mode 100644 index 000000000..1a0ff2176 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/types.pcf @@ -0,0 +1,61 @@ +facts { + ["simple types"] { + true + true + true + } + ["declared type"] { + true + true + true + true + true + true + true + true + true + } + ["nullable type"] { + true + true + } + ["union type"] { + true + true + true + true + true + } + ["function type"] { + true + true + true + true + true + true + true + true + } + ["constrained type"] { + true + true + true + true + } + ["parenthesized type"] { + true + true + } + ["string constant type"] { + true + true + true + true + true + true + } + ["type annotation"] { + true + true + } +} 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 247ee8a04..c7b1f60ae 100644 --- a/pkl-formatter/src/main/java/org/pkl/formatter/Builder.java +++ b/pkl-formatter/src/main/java/org/pkl/formatter/Builder.java @@ -1028,7 +1028,6 @@ private FormatNode formatNewHeader(Node node) { } private FormatNode formatParenthesizedExpr(Node node) { - if (node.children.size() == 2) return new Text("()"); var nodes = formatGenericWithGen( node.children, diff --git a/pkl-formatter/src/main/java/org/pkl/formatter/Formatter.java b/pkl-formatter/src/main/java/org/pkl/formatter/Formatter.java index 6e9487a76..543a991dd 100644 --- a/pkl-formatter/src/main/java/org/pkl/formatter/Formatter.java +++ b/pkl-formatter/src/main/java/org/pkl/formatter/Formatter.java @@ -21,6 +21,7 @@ import java.nio.file.Path; import java.util.List; import org.pkl.parser.GenericParser; +import org.pkl.parser.syntax.generic.Node; /** * A formatter for Pkl files that applies canonical formatting rules. @@ -111,6 +112,24 @@ public void format(Reader input, Appendable output) throws IOException { format(sb.toString(), output); } + /** + * Format the given Pkl AST as text. + * + *

The AST terminal nodes should have their text property set before calling format, as this + * method does not have access to the original source. + * + * @param ast the Pkl module node to format + * @return the formatted Pkl source code + */ + public String format(Node ast) { + var formatAst = new Builder("", grammarVersion).format(ast); + // force a line at the end of the file + var nodes = new Nodes(List.of(formatAst, ForceLine.INSTANCE)); + var output = new StringBuilder(); + new Generator(output).generate(nodes); + return output.toString(); + } + private void format(String input, Appendable output) { var ast = new GenericParser().parseModule(input); var formatAst = new Builder(input, grammarVersion).format(ast); 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 3cb01dc64..a770c6a47 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java +++ b/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java @@ -21,4 +21,8 @@ public final class GenericParser { public Node parseModule(String source) { return new GenericParserImpl(source).parseModule(); } + + public Node parseExpressionInput(String source) { + return new GenericParserImpl(source).parseExpressionInput(); + } } 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 0dd9b9a65..7b6fa5699 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/GenericParserImpl.java +++ b/pkl-parser/src/main/java/org/pkl/parser/GenericParserImpl.java @@ -102,6 +102,16 @@ Node parseModule() { return new Node(NodeType.MODULE, nodes); } + Node parseExpressionInput() { + ff(); + var expr = parseExpr(); + ff(); + if (lookahead != Token.EOF) { + throw parserError("unexpectedToken", _lookahead.text(lexer), "end of file"); + } + return expr; + } + private Node parseModuleDecl(List preChildren) { var headerParts = getHeaderParts(preChildren); var children = new ArrayList<>(headerParts.preffixes); @@ -937,7 +947,9 @@ private boolean isFunctionLiteral() { var token = next().token; ff(); if (token == Token.RPAREN) { - return lookahead == Token.ARROW; + // `()` is only valid as an empty parameter list + // let `parseFunctionLiteral` report the missing `->` + return true; } if (token == Token.UNDERSCORE) { return true; @@ -1072,11 +1084,6 @@ private void validateStringIndentation(List nodes) { private Node parseParenthesizedExpr() { var children = new ArrayList(); expect(Token.LPAREN, children, "unexpectedToken", "("); - if (lookahead() == Token.RPAREN) { - ff(children); - children.add(makeTerminal(next())); - return new Node(NodeType.PARENTHESIZED_EXPR, children); - } var elements = new ArrayList(); ff(elements); elements.add(parseExpr(")")); diff --git a/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/Node.java b/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/Node.java index bc481b15d..d15cd7308 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/Node.java +++ b/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/Node.java @@ -51,6 +51,10 @@ public String text(char[] source) { return text; } + public void setText(String text) { + this.text = text; + } + /** Returns the first child of type {@code type} or {@code null}. */ public @Nullable Node findChildByType(NodeType type) { for (var child : children) { diff --git a/stdlib/syntax.pkl b/stdlib/syntax.pkl new file mode 100644 index 000000000..1dca78806 --- /dev/null +++ b/stdlib/syntax.pkl @@ -0,0 +1,1116 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// + +/// Utilities for managing Pkl source code +@ModuleInfo { minPklVersion = "0.33.0" } +module pkl.syntax + +/// Utilities to parse Pkl source code to a typed AST. +class Parser { + /// Parse the string or resource as a Pkl module, returning either a typed AST node or + /// throwing an error. + external function parseModule(source: String | Resource): ModuleNode + + /// Parse the string or resource as a Pkl module, returning either a typed AST node or null + /// in case of an error. + external function parseModuleOrNull(source: String | Resource): ModuleNode? + + /// Parse the string or resource as a Pkl expression, returning either a typed AST node or + /// throwing an error. + external function parseExpression(source: String | Resource): ExprNode + + /// Parse the string or resource as a Pkl expression, returning either a typed AST node or null + /// in case of an error. + external function parseExpressionOrNull(source: String | Resource): ExprNode? +} + +/// Renders a [GenericNode] back to Pkl source code. +class Renderer { + /// The grammar version to target. + /// + /// `"V1"` matches Pkl 0.25 - 0.29, `"V2"` matches Pkl 0.30+. + grammarVersion: "V1" | "V2" = "V2" + + /// Render [node] as Pkl source code. + external function render(node: GenericNode): String +} + +/// A generic, untyped node in the Pkl syntax tree. +/// +/// A node is either a *leaf*, carrying source [text] and no +/// [children], or a *branch*, carrying [children] and no [text]. +class GenericNode { + /// The kind of this node. + type: NodeType + + /// The child nodes, in source order. Empty for leaf nodes. + children: List + + /// The parent node, or `null` for the root and for nodes not attached to a tree. + hidden parent: GenericNode? + + /// The verbatim source text of a leaf node (identifier, literal, terminal, etc.), + /// or `null` for a branch node whose content is its [children]. + text: String? + + /// The source location of this node or `null`. + span: Span? + + /// Walk this node and its descendants top-down, applying [operator] to each node and + /// returning the (possibly rewritten) tree. + /// + /// For each node, [operator] returns either: + /// - `null` to leave the node unchanged and continue walking into its children. + /// - `Pair(replacement, descend)` to replace the node with `replacement`. When + /// `descend` is `true`, the children of `replacement` are visited in turn, so + /// nodes emitted by a rewrite are themselves processed further down. When + /// `descend` is `false`, `replacement` and its subtree are left as-is. + /// + /// Constructed nodes must set their own [text] where it applies. + /// [span] is carried through unchanged unless set explicitly. + /// [parent] is populated on the returned tree for nodes originating from [Parser.parseModule]; + /// nodes constructed from scratch retain their given `parent`. + external function transform(operator: (GenericNode) -> Pair?): GenericNode + + /// Fold [operator] over this node and its descendants, top-down in pre-order. + /// + /// ``` + /// // count the if-expressions in a module + /// module.genericNode.fold(0, (acc, n) -> if (n.type == "if_expr") acc + 1 else acc) + /// ``` + external function fold(initial: Result, operator: (Result, GenericNode) -> Result): Result +} + +/// A range of source code, spanning [start] (inclusive) to [end] (exclusive). +class Span { + /// The start of this span. + start: SourceLocation + + /// The end of this span. + end: SourceLocation + + /// The display URI of this span. + /// + /// For nodes parsed from a [Resource], this is the resource's URI with the span appended + /// as a fragment, for example `file:///foo/bar.pkl#3:5-3:12`. + /// For nodes parsed from a [String], the URI of the source is unknown, so this is just the + /// span itself, for example `3:5-3:12`. + displayUri: String +} + +/// A position in source code, given as a 1-based line and column. +class SourceLocation { + /// The 1-based line number of this source location. + line: UInt + + /// The 1-based column number of this source location. + column: UInt + + /// The display URI of this source location. + /// + /// For nodes parsed from a [Resource], this is the resource's URI with the location appended + /// as a fragment, for example `file:///foo/bar.pkl#3:5`. + /// For nodes parsed from a [String], the URI of the source is unknown, so this is just the + /// location itself, for example `3:5`. + displayUri: String +} + +typealias NodeType = + // terminals and affixes + "terminal" + | "shebang" + | "line_comment" + | "block_comment" + | "semicolon" + // module structure + | "module" + | "doc_comment" + | "doc_comment_line" + | "modifier" + | "modifier_list" + | "amends_clause" + | "extends_clause" + | "module_declaration" + | "module_definition" + | "annotation" + | "identifier" + | "qualified_identifier" + | "import" + | "import_alias" + | "import_list" + | "typealias" + | "typealias_header" + | "typealias_body" + | "class" + | "class_header" + | "class_header_extends" + | "class_body" + | "class_body_elements" + | "class_method" + | "class_method_header" + | "class_method_body" + | "class_property" + | "class_property_header" + | "class_property_header_begin" + | "class_property_body" + | "object_body" + | "object_member_list" + | "parameter" + | "type_annotation" + | "parameter_list" + | "parameter_list_elements" + | "type_parameter_list" + | "type_parameter_list_elements" + | "argument_list" + | "argument_list_elements" + | "type_argument_list" + | "type_argument_list_elements" + | "object_parameter_list" + | "type_parameter" + | "string_chars" + | "operator" + | "string_newline" + | "string_escape" + // members + | "object_element" + | "object_property" + | "object_property_header" + | "object_property_header_begin" + | "object_property_body" + | "object_method" + | "member_predicate" + | "object_entry" + | "object_entry_header" + | "object_spread" + | "when_generator" + | "when_generator_header" + | "for_generator" + | "for_generator_header" + | "for_generator_header_definition" + | "for_generator_header_definition_header" + // expressions + | "this_expr" + | "outer_expr" + | "module_expr" + | "null_expr" + | "throw_expr" + | "trace_expr" + | "import_expr" + | "read_expr" + | "new_expr" + | "new_header" + | "unary_minus_expr" + | "logical_not_expr" + | "function_literal_expr" + | "function_literal_body" + | "parenthesized_expr" + | "parenthesized_expr_elements" + | "super_subscript_expr" + | "super_access_expr" + | "subscript_expr" + | "qualified_access_expr" + | "if_expr" + | "if_header" + | "if_condition" + | "if_condition_expr" + | "if_then_expr" + | "if_else_expr" + | "let_expr" + | "let_parameter_definition" + | "let_parameter" + | "bool_literal_expr" + | "int_literal_expr" + | "float_literal_expr" + | "single_line_string_literal_expr" + | "multi_line_string_literal_expr" + | "unqualified_access_expr" + | "non_null_expr" + | "amends_expr" + | "binary_op_expr" + // types + | "unknown_type" + | "nothing_type" + | "module_type" + | "union_type" + | "function_type" + | "function_type_parameters" + | "parenthesized_type" + | "parenthesized_type_elements" + | "declared_type" + | "nullable_type" + | "string_constant_type" + | "constrained_type" + | "constrained_type_constraint" + | "constrained_type_elements" + +/// Base class for all typed syntax nodes. +/// +/// A typed node may be *backed* by a parsed [GenericNode] (available via [genericNode]) or +/// constructed from scratch (in which case [genericNode] is `null`). Its typed fields +/// are read from [genericNode] when backed, or supplied directly when constructed. +abstract class Node { + /// The original parsed node, or `null` when this was built from scratch. + hidden genericNode: GenericNode? = null + + /// This node rebuilt into a [GenericNode]. + /// + /// A node that is verbatim from [Parser.parseModule()] yields its [genericNode] unchanged, as + /// does any part of this node's subtree that was not modified. Modified and constructed nodes are + /// rebuilt from their fields. + external fixed builtNode: GenericNode +} + +/// Base class for expression nodes. +abstract class ExprNode extends Node + +/// Base class for type nodes. +abstract class TypeNode extends Node + +/// A member of a [ModuleNode]. +typealias ModuleMember = ClassNode | TypeAliasNode | ClassPropertyNode | ClassMethodNode + +/// Indexes [members] by [IdentifierNode.name]. +local const function byIdentifier(members: List) = + members.toMap((it) -> it.identifier.name, (it) -> it).toMapping() + +// noinspection TypeMismatch +/// The top-level module node. +class ModuleNode extends Node { + /// The module declaration. + declaration: ModuleDeclarationNode? + + /// All imports in this module. + imports: Listing + + /// The members of this module, in source order. + members: Listing + + /// All class declarations in this module, keyed by class name. + fixed classes: Mapping = + byIdentifier(members.toList().filterIsInstance(ClassNode)) + + /// All typealias declarations in this module, keyed by typealias name. + fixed typeAliases: Mapping = + byIdentifier(members.toList().filterIsInstance(TypeAliasNode)) + + /// All top-level properties in this module, keyed by property name. + fixed properties: Mapping = + byIdentifier(members.toList().filterIsInstance(ClassPropertyNode)) + + /// All top-level methods in this module, keyed by method name. + fixed methods: Mapping = + byIdentifier(members.toList().filterIsInstance(ClassMethodNode)) +} + +/// A module declaration (including doc comment, annotations, modifiers, name, amends/extends). +class ModuleDeclarationNode extends Node { + /// The doc comment on the module declaration. + docComment: DocCommentNode? + + /// Annotations on the module declaration. + annotations: Listing + + /// The modifiers on the module declaration. + /// + /// An amending module cannot have any modifiers. + modifiers: Listing<"abstract" | "open">(isDistinct) + + /// The qualified name of the module. + name: QualifiedIdentifierNode? + + /// The `extends` or `amends` clause. + extendsOrAmendsClause: ExtendsOrAmendsClauseNode? +} + +/// The `extends` or `amends` clause of a module declaration. +class ExtendsOrAmendsClauseNode extends Node { + /// The keyword used (`"extends"` or `"amends"`). + keyword: "extends" | "amends" + + /// The URI string of the amended or extended module. + uri: String +} + +/// An import declaration. +class ImportNode extends Node { + /// The keyword used (`"import"` or `"import*"`). + keyword: "import" | "import*" + + /// The URI string of the import. + uri: String + + /// The alias for this import. + alias: IdentifierNode? +} + +/// A class declaration. +class ClassNode extends Node { + /// The doc comment. + docComment: DocCommentNode? + + /// Annotations on the class. + annotations: Listing + + /// The modifiers on the class. + modifiers: Listing<"abstract" | "open" | "local" | "external">(isDistinct) + + /// The class name. + identifier: IdentifierNode + + /// The type parameters. + typeParameters: Listing + + /// The supertype this class extends. + superType: TypeNode? + + /// The class body. + body: ClassBodyNode? +} + +/// A typealias declaration. +class TypeAliasNode extends Node { + /// The doc comment. + docComment: DocCommentNode? + + /// Annotations on the typealias. + annotations: Listing + + /// The modifiers on the typealias. + modifiers: Listing<"local" | "external">(isDistinct) + + /// The typealias name. + identifier: IdentifierNode + + /// The type parameters. + typeParameters: Listing + + /// The type that this alias resolves to. + type: TypeNode +} + +/// A member of a [ClassBodyNode]. +typealias ClassMember = ClassPropertyNode | ClassMethodNode + +// noinspection TypeMismatch +/// A class body delimited by braces. +class ClassBodyNode extends Node { + /// The members of this class body, in source order. + members: Listing + + /// Properties declared in this class body, keyed by property name. + fixed properties: Mapping = + byIdentifier(members.toList().filterIsInstance(ClassPropertyNode)) + + /// Methods declared in this class body, keyed by method name. + fixed methods: Mapping = + byIdentifier(members.toList().filterIsInstance(ClassMethodNode)) +} + +/// A class property declaration. +class ClassPropertyNode extends Node { + /// The doc comment. + docComment: DocCommentNode? + + /// Annotations on the property. + annotations: Listing + + /// The modifiers on the property. + /// + /// The `abstract` modifier is accepted for backwards compatibility, but has no effect. + modifiers: Listing<"abstract" | "local" | "hidden" | "external" | "fixed" | "const">(isDistinct) + + /// The property name. + identifier: IdentifierNode + + /// The type annotation. + typeAnnotation: TypeNode? + + /// The value expression (from `= expr`). + value: ExprNode(objectBodies.isEmpty)? + + /// Object bodies for amending (from `{ ... }` blocks). + objectBodies: Listing +} + +/// A class method declaration. +class ClassMethodNode extends Node { + /// The doc comment. + docComment: DocCommentNode? + + /// Annotations on the method. + annotations: Listing + + /// The modifiers on the method. + modifiers: Listing<"abstract" | "local" | "external" | "const">(isDistinct) + + /// The method name. + identifier: IdentifierNode + + /// The type parameters. + typeParameters: Listing + + /// The parameters. + parameters: Listing + + /// The return type annotation. + returnType: TypeNode? + + /// The method body expression. Null for abstract methods. + body: ExprNode?((this == null) == modifiers.contains("abstract")) +} + +/// A member of an [ObjectBodyNode]. +/// +/// An [ExprNode] member is an *element*, that is, a positional value such as the `1` in `{ 1 }`. +typealias ObjectMemberNode = + ObjectPropertyNode + | ObjectMethodNode + | ExprNode + | ObjectEntryNode + | ObjectSpreadNode + | MemberPredicateNode + | ForGeneratorNode + | WhenGeneratorNode + +/// An object body delimited by braces. +class ObjectBodyNode extends Node { + /// Parameters for this object body (e.g., `{ x, y -> ... }`). + parameters: Listing + + /// The members of this object body, in source order. + members: Listing + + /// Properties declared in this object body, in source order. + fixed properties: Listing = + members.toList().filterIsInstance(ObjectPropertyNode).toListing() + + /// Methods declared in this object body, in source order. + fixed methods: Listing = + members.toList().filterIsInstance(ObjectMethodNode).toListing() + + /// Elements declared in this object body, in source order. + fixed elements: Listing = members.toList().filterIsInstance(ExprNode).toListing() + + /// Entries declared in this object body, in source order. + fixed entries: Listing = + members.toList().filterIsInstance(ObjectEntryNode).toListing() + + /// Spreads declared in this object body, in source order. + fixed spreads: Listing = + members.toList().filterIsInstance(ObjectSpreadNode).toListing() + + /// Member predicates declared in this object body, in source order. + fixed memberPredicates: Listing = + members.toList().filterIsInstance(MemberPredicateNode).toListing() + + /// `for` generators declared in this object body, in source order. + fixed forGenerators: Listing = + members.toList().filterIsInstance(ForGeneratorNode).toListing() + + /// `when` generators declared in this object body, in source order. + fixed whenGenerators: Listing = + members.toList().filterIsInstance(WhenGeneratorNode).toListing() +} + +/// An object property declaration. +class ObjectPropertyNode extends Node { + /// The modifiers on the property. + modifiers: Listing<"local" | "const">(isDistinct) + + /// The property name. + identifier: IdentifierNode + + /// The type annotation. + typeAnnotation: TypeNode? + + /// The value expression (from `= expr`). + value: ExprNode(objectBodies.isEmpty)? + + /// Object bodies for amending. + objectBodies: Listing +} + +/// An object method declaration. +class ObjectMethodNode extends Node { + /// The modifiers on the method. + modifiers: Listing<"local" | "const">(isDistinct) + + /// The method name. + identifier: IdentifierNode + + /// The type parameters. + typeParameters: Listing + + /// The parameters. + parameters: Listing + + /// The return type annotation. + returnType: TypeNode? + + /// The method body expression. + body: ExprNode +} + +/// An object entry (`[key] = value` or `[key] { ... }`). +class ObjectEntryNode extends Node { + /// The key expression. + key: ExprNode + + /// The value expression (from `[key] = value`). + value: ExprNode(objectBodies.isEmpty)? + + /// Object bodies for amending. + objectBodies: Listing +} + +/// An object spread (`...expr` or `...?expr`). +class ObjectSpreadNode extends Node { + /// The keyword used (`"..."` or `"...?"`). + keyword: "..." | "...?" + + /// The spread expression. + expression: ExprNode +} + +/// A member predicate (`[[condition]] = value` or `[[condition]] { ... }`). +class MemberPredicateNode extends Node { + /// The condition expression. + condition: ExprNode + + /// The value expression. + value: ExprNode(objectBodies.isEmpty)? + + /// Object bodies for amending. + objectBodies: Listing +} + +/// A `for (param in iterable) { ... }` generator. +class ForGeneratorNode extends Node { + /// The key parameter (first parameter when two are present). + keyParameter: ParameterNode? + + /// The value parameter (or the only parameter when just one is present). + valueParameter: ParameterNode + + /// The iterable expression. + iterable: ExprNode + + /// The body. + body: ObjectBodyNode +} + +/// A `when (condition) { ... }` generator. +class WhenGeneratorNode extends Node { + /// The condition expression. + condition: ExprNode + + /// The "then" body. + thenBody: ObjectBodyNode + + /// The "else" body. + elseBody: ObjectBodyNode? +} + +/// The `this` expression. +class ThisExprNode extends ExprNode + +/// The `outer` expression. +class OuterExprNode extends ExprNode + +/// The `module` expression. +class ModuleExprNode extends ExprNode + +/// A `null` literal expression. +class NullLiteralExprNode extends ExprNode + +/// A boolean literal expression (`true` or `false`). +class BooleanLiteralExprNode extends ExprNode { + /// The boolean value. + value: Boolean +} + +/// An integer literal expression. +class IntLiteralExprNode extends ExprNode { + /// The integer literal (e.g. `42`, `"0xFF"`). + value: Int | String +} + +/// A float literal expression. +class FloatLiteralExprNode extends ExprNode { + /// The float literal (e.g. `3.14`, `"1.0e10"`). + value: Float | String(toFloatOrNull() != null) +} + +/// A part of a single-line string literal. +typealias SingleLineStringPartNode = StringCharsNode | StringEscapeNode | StringInterpolationNode + +/// A part of a multi-line string literal. +typealias MultiLineStringPartNode = + StringCharsNode | StringEscapeNode | StringInterpolationNode | StringNewlineNode + +/// A single-line string literal expression. +class SingleLineStringLiteralExprNode extends ExprNode { + /// The string parts (chars, escapes, interpolations). + parts: Listing +} + +/// A multi-line string literal expression. +/// +/// Use [StringNewlineNode] entries in [parts] to separate lines. +class MultiLineStringLiteralExprNode extends ExprNode { + /// The string parts (chars, escapes, newlines, interpolations). + parts: Listing +} + +/// Splits a string into runs of characters that can appear verbatim in a single-line string +/// literal (group 1), and single characters that must be escaped (group 2). +local const singleLineStringChunks = Regex(#"([^\\"\n\r\t]+)|([\s\S])"#) + +/// Same as [singleLineStringChunks], but for a multi-line string literal. +local const multiLineStringChunks = Regex(#"((?:(?!""")[^\\\n\r\t])+)|([\s\S])"#) + +/// The escape sequence for each character that cannot appear verbatim in a string literal. +local const stringEscapes: Map = + Map( + "\\", #"\\"#, + "\"", #"\""#, + "\n", #"\n"#, + "\r", #"\r"#, + "\t", #"\t"#, + ) + +/// Turns a chunk matched by [singleLineStringChunks] or [multiLineStringChunks] into a string +/// literal part. +local const function stringPart(match: RegexMatch): SingleLineStringPartNode = + if (match.groups[1] != null) + new StringCharsNode { value = match.value } + else + new StringEscapeNode { value = stringEscapes[match.value] } + +/// Same as [stringPart()], but turns a newline into a [StringNewlineNode]. +local const function multiLineStringPart(match: RegexMatch): MultiLineStringPartNode = + if (match.value == "\n") new StringNewlineNode {} else stringPart(match) + +/// Creates a single-line string literal expression that evaluates to [value]. +/// +/// Characters that cannot appear verbatim in a single-line string literal +/// (`\`, `"`, newline, carriage return, and tab) become [StringEscapeNode] parts. +/// +/// To interpolate expressions, set [SingleLineStringLiteralExprNode.parts] directly, +/// using [StringInterpolationNode] parts. +const function singleLineStringLiteral(value: String): SingleLineStringLiteralExprNode = new { + parts { ...singleLineStringChunks.findMatchesIn(value).map((match) -> stringPart(match)) } +} + +/// Creates a multi-line string literal expression that evaluates to [value]. +/// +/// Newlines in [value] become [StringNewlineNode] parts. +/// Characters that cannot appear verbatim in a multi-line string literal +/// (`\`, carriage return, tab, and quotes that would otherwise form `"""`) +/// become [StringEscapeNode] parts. +/// +/// To interpolate expressions, set [MultiLineStringLiteralExprNode.parts] directly, +/// using [StringInterpolationNode] parts. +const function multiLineStringLiteral(value: String): MultiLineStringLiteralExprNode = new { + parts { + new StringNewlineNode {} + ...multiLineStringChunks.findMatchesIn(value).map((match) -> multiLineStringPart(match)) + new StringNewlineNode {} + } +} + +/// An unqualified access expression (`name` or `name(args)`). +class UnqualifiedAccessExprNode extends ExprNode { + /// The identifier being accessed. + identifier: IdentifierNode + + /// The arguments, if this is a function call. Null for a plain identifier access. + arguments: Listing? +} + +/// A qualified access expression (`receiver.member` or `receiver?.member`, +/// optionally with arguments for method calls). +class QualifiedAccessExprNode extends ExprNode { + /// The receiver expression. + receiver: ExprNode + + /// Whether this is a null-safe access (`?.`). + isNullSafe: Boolean + + /// The accessed member name. + identifier: IdentifierNode + + /// The arguments, if this is a method call. Null for a property access. + arguments: Listing? +} + +/// A subscript expression (`receiver[index]`). +class SubscriptExprNode extends ExprNode { + /// The receiver expression. + receiver: ExprNode + + /// The index expression. + index: ExprNode +} + +/// A `super.member` access expression (`super.member` or `super.member(args)`). +class SuperAccessExprNode extends ExprNode { + /// The accessed member name. + identifier: IdentifierNode + + /// The arguments, if this is a method call. Null for a property access. + arguments: Listing? +} + +/// A `super[index]` subscript expression. +class SuperSubscriptExprNode extends ExprNode { + /// The index expression. + index: ExprNode +} + +/// An `if (condition) thenExpr else elseExpr` expression. +class IfExprNode extends ExprNode { + /// The condition expression. + condition: ExprNode + + /// The then-branch expression. + thenExpr: ExprNode + + /// The else-branch expression. + elseExpr: ExprNode +} + +/// A `let (param = value) body` expression. +class LetExprNode extends ExprNode { + /// The let-binding parameter. + parameter: ParameterNode + + /// The binding value expression. + bindingValue: ExprNode + + /// The body expression. + body: ExprNode +} + +/// A `throw(expr)` expression. +class ThrowExprNode extends ExprNode { + /// The expression being thrown. + expression: ExprNode +} + +/// A `trace(expr)` expression. +class TraceExprNode extends ExprNode { + /// The expression being traced. + expression: ExprNode +} + +/// An `import("uri")` or `import*("uri")` expression. +class ImportExprNode extends ExprNode { + /// The keyword used (`"import"` or `"import*"`). + keyword: "import" | "import*" + + /// The import URI string. + uri: String +} + +/// A `read(expr)`, `read*(expr)`, or `read?(expr)` expression. +class ReadExprNode extends ExprNode { + /// The keyword used (`"read"`, `"read?"`, or `"read*"`). + keyword: "read" | "read?" | "read*" + + /// The expression to be read. + expression: ExprNode +} + +/// A `new Type { ... }` expression. +class NewExprNode extends ExprNode { + /// The type being constructed. + type: TypeNode? + + /// The object body. + body: ObjectBodyNode +} + +/// An `(expr) { ... }` amends expression. +class AmendsExprNode extends ExprNode { + /// The expression being amended. + /// + /// Takes one of a few forms: + /// * [ParenthesizedExprNode]: `(expr) { ... }` + /// * [AmendsExprNode]: `(expr) { ... } { ... }` (where `(expr) { ... }` is the parent) + /// * [NewExprNode]: `new { ... } { ... }` (where `new { ... }` is the parent) + parentExpr: ParenthesizedExprNode | AmendsExprNode | NewExprNode + + /// The object body. + body: ObjectBodyNode +} + +/// Base class for binary operator expressions (`left op right`). +abstract class BinaryOpExprNode extends ExprNode { + /// The left-hand expression. + left: ExprNode + + /// The right-hand expression. + right: ExprNode +} + +/// An exponentiation expression (`left ** right`). +class ExponentiationExprNode extends BinaryOpExprNode + +/// A multiplication expression (`left * right`). +class MultiplicationExprNode extends BinaryOpExprNode + +/// A division expression (`left / right`). +class DivisionExprNode extends BinaryOpExprNode + +/// An integer division expression (`left ~/ right`). +class IntegerDivisionExprNode extends BinaryOpExprNode + +/// A remainder expression (`left % right`). +class RemainderExprNode extends BinaryOpExprNode + +/// An addition expression (`left + right`). +class AdditionExprNode extends BinaryOpExprNode + +/// A subtraction expression (`left - right`). +class SubtractionExprNode extends BinaryOpExprNode + +/// A less-than comparison expression (`left < right`). +class LessThanExprNode extends BinaryOpExprNode + +/// A less-than-or-equal comparison expression (`left <= right`). +class LessThanOrEqualExprNode extends BinaryOpExprNode + +/// A greater-than comparison expression (`left > right`). +class GreaterThanExprNode extends BinaryOpExprNode + +/// A greater-than-or-equal comparison expression (`left >= right`). +class GreaterThanOrEqualExprNode extends BinaryOpExprNode + +/// An equality expression (`left == right`). +class EqualExprNode extends BinaryOpExprNode + +/// An inequality expression (`left != right`). +class NotEqualExprNode extends BinaryOpExprNode + +/// A logical and expression (`left && right`). +class LogicalAndExprNode extends BinaryOpExprNode + +/// A logical or expression (`left || right`). +class LogicalOrExprNode extends BinaryOpExprNode + +/// A pipe expression (`left |> right`). +class PipeExprNode extends BinaryOpExprNode + +/// A null coalescing expression (`left ?? right`). +class NullCoalescingExprNode extends BinaryOpExprNode + +/// A type check expression (`expression is Type`). +class TypeCheckExprNode extends ExprNode { + /// The left-hand expression. + expression: ExprNode + + /// The right-hand type. + type: TypeNode +} + +/// A type cast expression (`expression as Type`). +class TypeCastExprNode extends ExprNode { + /// The left-hand expression. + expression: ExprNode + + /// The right-hand type. + type: TypeNode +} + +/// A unary minus expression (`-expr`). +class UnaryMinusExprNode extends ExprNode { + /// The operand expression. + operand: ExprNode +} + +/// A logical not expression (`!expr`). +class LogicalNotExprNode extends ExprNode { + /// The operand expression. + operand: ExprNode +} + +/// A non-null assertion expression (`expr!!`). +class NonNullExprNode extends ExprNode { + /// The operand expression. + operand: ExprNode +} + +/// A function literal expression (`(params) -> body`). +class FunctionLiteralExprNode extends ExprNode { + /// The parameters. + parameters: Listing + + /// The body expression. + body: ExprNode +} + +/// A parenthesized expression (`(expr)`). +class ParenthesizedExprNode extends ExprNode { + /// The inner expression. + expression: ExprNode +} + +/// The `unknown` type. +class UnknownTypeNode extends TypeNode + +/// The `nothing` type. +class NothingTypeNode extends TypeNode + +/// The `module` type. +class ModuleTypeNode extends TypeNode + +/// A declared type (e.g., `String`, `List`). +class DeclaredTypeNode extends TypeNode { + /// The type name (dotted, e.g. `List` or `foo.Bar`). + name: QualifiedIdentifierNode(identifiers.length.isBetween(1, 2)) + + /// The type arguments. + typeArguments: Listing +} + +/// A nullable type (`Type?`). +class NullableTypeNode extends TypeNode { + /// The base type. + baseType: TypeNode +} + +/// A union type (`TypeA|TypeB|TypeC`). +class UnionTypeNode extends TypeNode { + /// The member types. + members: Listing +} + +/// A function type (`(ParamTypes) -> ReturnType`). +class FunctionTypeNode extends TypeNode { + /// The parameter types. + parameterTypes: Listing + + /// The return type. + returnType: TypeNode +} + +/// A constrained type (`Type(constraint)`). +class ConstrainedTypeNode extends TypeNode { + /// The base type. + baseType: TypeNode + + /// The constraint expressions. + constraints: Listing +} + +/// A parenthesized type (`(Type)`). +class ParenthesizedTypeNode extends TypeNode { + /// The inner type. + type: TypeNode +} + +/// A string literal type (e.g., `"foo"`). +class StringLiteralTypeNode extends TypeNode { + /// The string value. + value: String +} + +/// An annotation (`@Type { ... }`). +class AnnotationNode extends Node { + /// The annotation type. + type: TypeNode + + /// The annotation body. + body: ObjectBodyNode? +} + +/// A parameter declaration (`name`, `name: Type`, or `_`). +class ParameterNode extends Node { + /// Whether this is a blank identifier parameter (`_`). + isBlankIdentifier: Boolean(this == (identifier == null), implies(typeAnnotation == null)) + + /// The parameter name, or `null` for a wildcard parameter (`_`). + identifier: IdentifierNode? + + /// The type annotation. + typeAnnotation: TypeNode? +} + +/// A type parameter declaration (`T`, `in T`, or `out T`). +class TypeParameterNode extends Node { + /// The variance modifiers on the type parameter. + modifiers: Listing<"in" | "out">(isDistinct) + + /// The type parameter name. + identifier: IdentifierNode +} + +/// Strips the enclosing backticks of a quoted identifier. +local const function unquote(identifier: String): String = + if (identifier.startsWith("`")) identifier.drop(1).dropLast(1) else identifier + +/// An identifier (a name occurring in the source, e.g. a property or class name). +class IdentifierNode extends Node { + /// The verbatim identifier text, including the enclosing backticks of a quoted identifier. + /// + /// For example, the text of `` `my ## foo` `` is ``"`my ## foo`"``. + text: String + + /// The identifier name, with the enclosing backticks of a quoted identifier stripped. + /// + /// For example, the name of `` `my ## foo` `` is `"my ## foo"`. + /// + /// This is the name used when rendering this node back to source code; it is quoted again + /// if it is not a regular Pkl identifier. + name: String = unquote(text) +} + +/// A qualified (dotted) identifier, e.g. `foo.bar.baz`. +class QualifiedIdentifierNode extends Node { + /// The parts of the qualified identifier. + identifiers: Listing + + /// The dotted name (e.g. `"foo.bar.baz"`). + value: String +} + +/// A doc comment. +class DocCommentNode extends Node { + /// The body text of the comment, with the leading `///` stripped from each line. + value: String +} + +/// A plain text part of a string literal. +class StringCharsNode { + /// The text content. + value: String +} + +/// An escape sequence in a string literal (e.g., `"\\n"`, `"\\t"`). +class StringEscapeNode { + /// The escape sequence text including the leading backslash. + value: String +} + +/// A newline in a multi-line string literal. +class StringNewlineNode + +/// An interpolation in a string literal (`\(expr)`). +class StringInterpolationNode { + /// The interpolated expression. + expression: ExprNode +}