From 8382806c04f050cc2fc8549b3ac9667af4cf9b55 Mon Sep 17 00:00:00 2001 From: Islon Scherer Date: Thu, 9 Apr 2026 14:09:16 +0200 Subject: [PATCH 1/4] Add syntax package --- .../org/pkl/core/runtime/ModuleCache.java | 2 + .../org/pkl/core/runtime/SyntaxModule.java | 61 + .../pkl/core/stdlib/syntax/SyntaxNodes.java | 124 ++ .../pkl/core/stdlib/syntax/package-info.java | 4 + .../input/syntax/expressions.pkl | 226 +++ .../input/syntax/moduleStructure.pkl | 221 +++ .../input/syntax/objectMembers.pkl | 170 ++ .../input/syntax/types.pkl | 104 ++ .../output/errors/cannotFindStdLibModule.err | 1 + .../output/syntax/expressions.pcf | 132 ++ .../output/syntax/moduleStructure.pcf | 135 ++ .../output/syntax/objectMembers.pcf | 113 ++ .../output/syntax/types.pcf | 64 + stdlib/syntax.pkl | 1601 +++++++++++++++++ 14 files changed, 2958 insertions(+) create mode 100644 pkl-core/src/main/java/org/pkl/core/runtime/SyntaxModule.java create mode 100644 pkl-core/src/main/java/org/pkl/core/stdlib/syntax/SyntaxNodes.java create mode 100644 pkl-core/src/main/java/org/pkl/core/stdlib/syntax/package-info.java create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/syntax/expressions.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/syntax/moduleStructure.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/syntax/objectMembers.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/syntax/types.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/syntax/expressions.pcf create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/syntax/moduleStructure.pcf create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/syntax/objectMembers.pcf create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/syntax/types.pcf create mode 100644 stdlib/syntax.pkl 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 45b830371..8dbf4cae0 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 @@ -113,6 +113,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..923831e0a --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/SyntaxModule.java @@ -0,0 +1,61 @@ +/* + * 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 getNodeClass() { + return NodeClass.instance; + } + + public static VmClass getSpanClass() { + return SpanClass.instance; + } + + public static VmClass getParserErrorClass() { + return ParserErrorClass.instance; + } + + private static final class NodeClass { + static final VmClass instance = loadClass("Node"); + } + + private static final class SpanClass { + static final VmClass instance = loadClass("Span"); + } + + private static final class ParserErrorClass { + static final VmClass instance = loadClass("ParserError"); + } + + @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/syntax/SyntaxNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/SyntaxNodes.java new file mode 100644 index 000000000..9018bc2ea --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/SyntaxNodes.java @@ -0,0 +1,124 @@ +/* + * 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 com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import java.util.ArrayList; +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.stdlib.ExternalMethod1Node; +import org.pkl.core.stdlib.VmObjectFactory; +import org.pkl.parser.GenericParser; +import org.pkl.parser.GenericParserError; +import org.pkl.parser.syntax.generic.FullSpan; +import org.pkl.parser.syntax.generic.Node; + +public final class SyntaxNodes { + private SyntaxNodes() {} + + /** Extra storage backing a Pkl {@code Node} instance. */ + static final class NodeData { + final Node node; + final char[] source; + VmTyped parentVm; + VmList childrenVm; + VmTyped spanVm; + + NodeData(Node node, char[] source) { + this.node = node; + this.source = source; + } + } + + /** Extra storage backing a Pkl {@code ParserError} instance. */ + static final class ErrorData { + final String text; + final VmTyped spanVm; + + ErrorData(String text, VmTyped spanVm) { + this.text = text; + this.spanVm = spanVm; + } + } + + private static final VmObjectFactory spanFactory = + new VmObjectFactory(SyntaxModule::getSpanClass) + .addIntProperty("lineStart", FullSpan::lineBegin) + .addIntProperty("colStart", FullSpan::colBegin) + .addIntProperty("lineEnd", FullSpan::lineEnd) + .addIntProperty("colEnd", FullSpan::colEnd); + + private static final VmObjectFactory nodeFactory = + new VmObjectFactory(SyntaxModule::getNodeClass) + .addStringProperty("type", nd -> nd.node.type.name().toLowerCase()) + .addListProperty("children", nd -> nd.childrenVm) + .addProperty("parent", nd -> VmNull.lift(nd.parentVm)) + .addProperty( + "text", + nd -> nd.node.children.isEmpty() ? nd.node.text(nd.source) : VmNull.withoutDefault()) + .addTypedProperty("span", nd -> nd.spanVm); + + private static final VmObjectFactory parserErrorFactory = + new VmObjectFactory(SyntaxModule::getParserErrorClass) + .addStringProperty("text", ed -> ed.text) + .addTypedProperty("span", ed -> ed.spanVm); + + public abstract static class parseNodes extends ExternalMethod1Node { + @Specialization + @TruffleBoundary + protected Object eval(VmTyped self, String source) { + var sourceChars = source.toCharArray(); + + try { + var parser = new GenericParser(); + var root = parser.parseModule(source); + return convertNode(root, sourceChars); + } catch (GenericParserError e) { + var errorSpanVm = spanFactory.create(e.getSpan()); + var text = e.getMessage() != null ? e.getMessage() : "Parse error"; + return parserErrorFactory.create(new ErrorData(text, errorSpanVm)); + } + } + + @TruffleBoundary + private static VmTyped convertNode(Node genericNode, char[] sourceChars) { + // Convert children recursively + var childrenList = new ArrayList(genericNode.children.size()); + for (var child : genericNode.children) { + childrenList.add(convertNode(child, sourceChars)); + } + + // Build NodeData + var data = new NodeData(genericNode, sourceChars); + data.childrenVm = VmList.create(childrenList.toArray()); + data.spanVm = spanFactory.create(genericNode.span); + + // Create VmTyped node + var result = nodeFactory.create(data); + + // Set parent back-reference on each child + for (var childVm : childrenList) { + var childData = (NodeData) childVm.getExtraStorage(); + childData.parentVm = result; + } + + return result; + } + } +} 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..6f12d5850 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.stdlib.syntax; + +import org.pkl.core.util.NonnullByDefault; 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..8ca130a2a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/expressions.pkl @@ -0,0 +1,226 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local function parse(source: String) = syntax.parse(source) + +local function expr(source: String) = + let (result = parse("x = \(source)")) + (result as syntax.ModuleNode).properties.first.value + +examples { + ["literals"] { + local boolTrue = expr("true") + boolTrue is syntax.BoolLiteralExprNode + (boolTrue as syntax.BoolLiteralExprNode).value == true + + local boolFalse = expr("false") + boolFalse is syntax.BoolLiteralExprNode + (boolFalse as syntax.BoolLiteralExprNode).value == false + + local intLit = expr("42") + intLit is syntax.IntLiteralExprNode + (intLit as syntax.IntLiteralExprNode).text == "42" + + local hexLit = expr("0xFF") + hexLit is syntax.IntLiteralExprNode + (hexLit as syntax.IntLiteralExprNode).text == "0xFF" + + local floatLit = expr("3.14") + floatLit is syntax.FloatLiteralExprNode + (floatLit as syntax.FloatLiteralExprNode).text == "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 + """ + """#) + multiLine is syntax.MultiLineStringLiteralExprNode + local mlParts = (multiLine as syntax.MultiLineStringLiteralExprNode).parts + mlParts.length >= 1 + } + + ["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.value == "foo" + (unqual as syntax.UnqualifiedAccessExprNode).argumentList == null + + local withArgs = expr("foo(1, 2)") + withArgs is syntax.UnqualifiedAccessExprNode + (withArgs as syntax.UnqualifiedAccessExprNode).identifier.value == "foo" + (withArgs as syntax.UnqualifiedAccessExprNode).argumentList != null + (withArgs as syntax.UnqualifiedAccessExprNode).argumentList!!.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).member.identifier.value == "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.BinaryOpExprNode + (add as syntax.BinaryOpExprNode).operator == "+" + (add as syntax.BinaryOpExprNode).leftExpr is syntax.IntLiteralExprNode + (add as syntax.BinaryOpExprNode).rightExpr is syntax.IntLiteralExprNode + + local eq = expr("a == b") + eq is syntax.BinaryOpExprNode + (eq as syntax.BinaryOpExprNode).operator == "==" + + local pipeline = expr("a |> b") + pipeline is syntax.BinaryOpExprNode + (pipeline as syntax.BinaryOpExprNode).operator == "|>" + + local nullCoalesce = expr("a ?? b") + nullCoalesce is syntax.BinaryOpExprNode + (nullCoalesce as syntax.BinaryOpExprNode).operator == "??" + + local isOp = expr("a is String") + isOp is syntax.BinaryOpExprNode + (isOp as syntax.BinaryOpExprNode).operator == "is" + (isOp as syntax.BinaryOpExprNode).rightType is syntax.DeclaredTypeNode + + local asOp = expr("a as String") + asOp is syntax.BinaryOpExprNode + (asOp as syntax.BinaryOpExprNode).operator == "as" + (asOp as syntax.BinaryOpExprNode).rightType 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!!.value == "y" + (letExpr as syntax.LetExprNode).bindingValue is syntax.IntLiteralExprNode + (letExpr as syntax.LetExprNode).bodyExpr is syntax.BinaryOpExprNode + } + + ["new expression"] { + local newExpr = expr("new Mapping { [\"a\"] = 1 }") + newExpr is syntax.NewExprNode + (newExpr as syntax.NewExprNode).type is syntax.DeclaredTypeNode + (newExpr as syntax.NewExprNode).body is syntax.ObjectBodyNode + } + + ["function literal"] { + local fn = expr("(x, y) -> x + y") + fn is syntax.FunctionLiteralExprNode + (fn as syntax.FunctionLiteralExprNode).parameterList.parameters.length == 2 + (fn as syntax.FunctionLiteralExprNode).parameterList.parameters[0].identifier!!.value == "x" + (fn as syntax.FunctionLiteralExprNode).parameterList.parameters[1].identifier!!.value == "y" + (fn as syntax.FunctionLiteralExprNode).body is syntax.BinaryOpExprNode + } + + ["parenthesized expression"] { + local paren = expr("(1 + 2)") + paren is syntax.ParenthesizedExprNode + (paren as syntax.ParenthesizedExprNode).expression is syntax.BinaryOpExprNode + } + + ["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).isGlob == false + (impExpr as syntax.ImportExprNode).uri == "foo.pkl" + + local impGlob = expr(#"import*("*.pkl")"#) + impGlob is syntax.ImportExprNode + (impGlob as syntax.ImportExprNode).isGlob == true + (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*" + } +} 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..780faf4a4 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/moduleStructure.pkl @@ -0,0 +1,221 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local function parse(source: String) = syntax.parse(source) + +examples { + ["module declaration"] { + local result = parse("module my.app") + result is syntax.ModuleNode + local mod = result as syntax.ModuleNode + mod.declaration != null + mod.declaration!!.name != null + mod.declaration!!.name!!.identifiers.length == 2 + mod.declaration!!.name!!.identifiers[0].value == "my" + mod.declaration!!.name!!.identifiers[1].value == "app" + mod.declaration!!.docComment == null + mod.declaration!!.annotations.length == 0 + mod.declaration!!.modifiers == null + mod.declaration!!.amendsClause == null + mod.declaration!!.extendsClause == null + } + + ["module with modifiers and doc comment"] { + local result = parse(""" + /// This is my module. + @Deprecated { message = "use other" } + open module my.mod + """) + local mod = result as syntax.ModuleNode + mod.declaration != null + mod.declaration!!.docComment != null + mod.declaration!!.docComment!! is syntax.DocCommentNode + mod.declaration!!.docComment!!.lines.length == 1 + + mod.declaration!!.annotations.length == 1 + mod.declaration!!.annotations.first is syntax.AnnotationNode + mod.declaration!!.annotations.first.type is syntax.DeclaredTypeNode + + mod.declaration!!.modifiers != null + mod.declaration!!.modifiers!! is syntax.ModifierListNode + mod.declaration!!.modifiers!!.modifiers == List("open") + } + + ["amends clause"] { + local result = parse(#"amends "base.pkl""#) + local mod = result as syntax.ModuleNode + mod.declaration != null + mod.declaration!!.amendsClause != null + mod.declaration!!.amendsClause!! is syntax.AmendsClauseNode + mod.declaration!!.amendsClause!!.uri == "base.pkl" + mod.declaration!!.extendsClause == null + } + + ["extends clause"] { + local result = parse(#"extends "base.pkl""#) + local mod = result as syntax.ModuleNode + mod.declaration != null + mod.declaration!!.extendsClause != null + mod.declaration!!.extendsClause!! is syntax.ExtendsClauseNode + mod.declaration!!.extendsClause!!.uri == "base.pkl" + mod.declaration!!.amendsClause == null + } + + ["imports"] { + local result = parse(""" + import "foo.pkl" + import "bar.pkl" as myBar + import* "*.pkl" + """) + local mod = result as syntax.ModuleNode + mod.imports.length == 3 + + mod.imports[0] is syntax.ImportNode + mod.imports[0].uri == "foo.pkl" + mod.imports[0].isGlob == false + mod.imports[0].alias == null + + mod.imports[1].uri == "bar.pkl" + mod.imports[1].alias != null + mod.imports[1].alias!!.value == "myBar" + + mod.imports[2].uri == "*.pkl" + mod.imports[2].isGlob == true + } + + ["class declaration"] { + local result = parse(""" + /// A bird class. + abstract class Bird { + name: String + function fly(speed: Int): Boolean = true + } + """) + local mod = result as syntax.ModuleNode + mod.classes.length == 1 + local cls = mod.classes.first + cls is syntax.ClassNode + + cls.docComment != null + cls.docComment!!.lines.length == 1 + + cls.modifiers != null + cls.modifiers!!.modifiers == List("abstract") + + cls.name.value == "Bird" + + cls.typeParameterList == null + cls.extendsClause == null + + cls.body != null + cls.body!!.properties.length == 1 + cls.body!!.properties.first.name.value == "name" + cls.body!!.properties.first.typeAnnotation != null + cls.body!!.properties.first.typeAnnotation!!.type is syntax.DeclaredTypeNode + cls.body!!.properties.first.value == null + + cls.body!!.methods.length == 1 + cls.body!!.methods.first.name.value == "fly" + cls.body!!.methods.first.parameterList.parameters.length == 1 + cls.body!!.methods.first.parameterList.parameters.first.identifier!!.value == "speed" + cls.body!!.methods.first.returnType != null + cls.body!!.methods.first.body != null + cls.body!!.methods.first.body is syntax.BoolLiteralExprNode + } + + ["class with extends and type parameters"] { + local result = parse(""" + class Container extends Base { + item: T + } + """) + local mod = result as syntax.ModuleNode + local cls = mod.classes.first + + cls.name.value == "Container" + cls.typeParameterList != null + cls.typeParameterList!!.typeParameters.length == 1 + cls.typeParameterList!!.typeParameters.first.name.value == "T" + cls.typeParameterList!!.typeParameters.first.variance == null + + cls.extendsClause != null + cls.extendsClause is syntax.DeclaredTypeNode + } + + ["typealias"] { + local result = parse("typealias Positive = Int(this > 0)") + local mod = result as syntax.ModuleNode + mod.typeAliases.length == 1 + local ta = mod.typeAliases.first + ta is syntax.TypeAliasNode + ta.name.value == "Positive" + ta.typeParameterList == null + ta.type is syntax.ConstrainedTypeNode + } + + ["top-level properties and methods"] { + local result = parse(""" + hidden name: String = "pkl" + local count: Int = 42 + function greet(who: String): String = "hi" + """) + local mod = result as syntax.ModuleNode + + mod.properties.length == 2 + mod.properties[0].name.value == "name" + mod.properties[0].modifiers != null + mod.properties[0].modifiers!!.modifiers == List("hidden") + mod.properties[0].value is syntax.SingleLineStringLiteralExprNode + + mod.properties[1].name.value == "count" + mod.properties[1].modifiers != null + mod.properties[1].modifiers!!.modifiers == List("local") + + mod.methods.length == 1 + mod.methods.first.name.value == "greet" + mod.methods.first.parameterList.parameters.length == 1 + mod.methods.first.returnType != null + mod.methods.first.body is syntax.SingleLineStringLiteralExprNode + } + + ["parameter variations"] { + local result = parse("function f(x: Int, _, y): Boolean = true") + local mod = result as syntax.ModuleNode + local params = mod.methods.first.parameterList.parameters + + params.length == 3 + + params[0].identifier != null + params[0].identifier!!.value == "x" + params[0].typeAnnotation != null + params[0].isWildcard == false + + params[1].identifier == null + params[1].isWildcard == true + params[1].typeAnnotation == null + + params[2].identifier != null + params[2].identifier!!.value == "y" + params[2].typeAnnotation == null + params[2].isWildcard == false + } + + ["parser error"] { + local result = parse("x = {{{") + result is syntax.ParserError + (result as syntax.ParserError).text.length > 0 + } + + ["empty module"] { + local result = parse("") + result is syntax.ModuleNode + local mod = result as syntax.ModuleNode + mod.declaration == null + mod.imports.length == 0 + mod.classes.length == 0 + mod.typeAliases.length == 0 + mod.properties.length == 0 + mod.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..7f7baafb2 --- /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) = syntax.parse(source) + +local function body(source: String) = + let (result = parse("x { \(source) }")) + let (mod = result as syntax.ModuleNode) + mod.properties.first.objectBodies.first + +examples { + ["object property"] { + local b = body("name = \"hello\"") + b.properties.length == 1 + b.properties.first is syntax.ObjectPropertyNode + b.properties.first.name.value == "name" + b.properties.first.value is syntax.SingleLineStringLiteralExprNode + b.properties.first.modifiers == null + b.properties.first.typeAnnotation == null + b.properties.first.objectBodies.length == 0 + } + + ["object property with type and modifiers"] { + local b = body("hidden name: String = \"hello\"") + b.properties.length == 1 + b.properties.first.modifiers != null + b.properties.first.modifiers!!.modifiers == List("hidden") + b.properties.first.typeAnnotation != null + b.properties.first.typeAnnotation!!.type is syntax.DeclaredTypeNode + } + + ["object property with amending body"] { + local b = body("inner { x = 1 }") + b.properties.length == 1 + b.properties.first.name.value == "inner" + b.properties.first.value == null + b.properties.first.objectBodies.length == 1 + b.properties.first.objectBodies.first is syntax.ObjectBodyNode + } + + ["object method"] { + local b = body("function greet(who: String): String = \"hi\"") + b.methods.length == 1 + b.methods.first is syntax.ObjectMethodNode + b.methods.first.name.value == "greet" + b.methods.first.parameterList.parameters.length == 1 + b.methods.first.parameterList.parameters.first.identifier!!.value == "who" + b.methods.first.returnType != null + b.methods.first.body is syntax.SingleLineStringLiteralExprNode + } + + ["object element"] { + local b = body("1\n 2\n 3") + b.elements.length == 3 + b.elements[0] is syntax.ObjectElementNode + b.elements[0].expression is syntax.IntLiteralExprNode + b.elements[1].expression is syntax.IntLiteralExprNode + b.elements[2].expression is syntax.IntLiteralExprNode + } + + ["object entry"] { + local b = body("[\"key\"] = 42") + b.entries.length == 1 + b.entries.first is syntax.ObjectEntryNode + 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.members.length == 1 + b.members.first is syntax.ObjectSpreadNode + (b.members.first as syntax.ObjectSpreadNode).isNullable == false + (b.members.first as syntax.ObjectSpreadNode).expression is syntax.UnqualifiedAccessExprNode + } + + ["nullable object spread"] { + local b = body("...?other") + b.members.length == 1 + b.members.first is syntax.ObjectSpreadNode + (b.members.first as syntax.ObjectSpreadNode).isNullable == true + } + + ["member predicate"] { + local b = body("[[name == \"foo\"]] = 1") + b.members.length == 1 + b.members.first is syntax.MemberPredicateNode + (b.members.first as syntax.MemberPredicateNode).condition is syntax.BinaryOpExprNode + (b.members.first as syntax.MemberPredicateNode).value is syntax.IntLiteralExprNode + } + + ["member predicate with amending body"] { + local b = body("[[name == \"foo\"]] { x = 1 }") + local pred = b.members.first as syntax.MemberPredicateNode + pred.condition is syntax.BinaryOpExprNode + pred.value == null + pred.objectBodies.length == 1 + } + + ["for generator"] { + local b = body("for (item in items) { item }") + b.members.length == 1 + b.members.first is syntax.ForGeneratorNode + local gen = b.members.first as syntax.ForGeneratorNode + gen.keyParameter == null + gen.valueParameter.identifier!!.value == "item" + gen.iterable is syntax.UnqualifiedAccessExprNode + gen.body is syntax.ObjectBodyNode + } + + ["for generator with key"] { + local b = body("for (k, v in items) { v }") + local gen = b.members.first as syntax.ForGeneratorNode + gen.keyParameter != null + gen.keyParameter!!.identifier!!.value == "k" + gen.valueParameter.identifier!!.value == "v" + } + + ["when generator"] { + local b = body("when (flag) { 1 }") + b.members.length == 1 + b.members.first is syntax.WhenGeneratorNode + local gen = b.members.first as syntax.WhenGeneratorNode + gen.condition is syntax.UnqualifiedAccessExprNode + gen.thenBody is syntax.ObjectBodyNode + gen.elseBody == null + } + + ["when generator with else"] { + local b = body("when (flag) { 1 } else { 2 }") + local gen = b.members.first as syntax.WhenGeneratorNode + gen.condition is syntax.UnqualifiedAccessExprNode + gen.thenBody is syntax.ObjectBodyNode + gen.elseBody != null + gen.elseBody is syntax.ObjectBodyNode + } + + ["object body with parameters"] { + local result = parse("x = new Listing { a, b -> a }") + local mod = result as syntax.ModuleNode + local propVal = mod.properties.first.value + propVal is syntax.NewExprNode + local newBody = (propVal as syntax.NewExprNode).body + newBody.parameters.length == 2 + newBody.parameters[0].identifier!!.value == "a" + newBody.parameters[1].identifier!!.value == "b" + } + + ["mixed object members"] { + local b = body(""" + name = "pkl" + 1 + ["key"] = 2 + function f() = 3 + """) + b.properties.length == 1 + b.elements.length == 1 + b.entries.length == 1 + b.methods.length == 1 + b.members.length == 4 + } +} 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..9b280e04a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/types.pkl @@ -0,0 +1,104 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local function parse(source: String) = syntax.parse(source) + +local function typeOf(typeSource: String) = + let (result = parse("x: \(typeSource) = 0")) + (result as syntax.ModuleNode).properties.first.typeAnnotation!!.type + +examples { + ["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.identifiers.length == 1 + (simple as syntax.DeclaredTypeNode).name.identifiers.first.value == "String" + (simple as syntax.DeclaredTypeNode).typeArgumentList == null + + local withArgs = typeOf("List") + withArgs is syntax.DeclaredTypeNode + (withArgs as syntax.DeclaredTypeNode).name.identifiers.first.value == "List" + (withArgs as syntax.DeclaredTypeNode).typeArgumentList != null + (withArgs as syntax.DeclaredTypeNode).typeArgumentList!!.typeArguments.length == 1 + (withArgs as syntax.DeclaredTypeNode).typeArgumentList!!.typeArguments.first is syntax.DeclaredTypeNode + + local multiArgs = typeOf("Map") + multiArgs is syntax.DeclaredTypeNode + (multiArgs as syntax.DeclaredTypeNode).typeArgumentList!!.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.BinaryOpExprNode + } + + ["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 as syntax.ModuleNode).typeAliases.first + ta.type is syntax.UnionTypeNode + local members = (ta.type as syntax.UnionTypeNode).members + members.length == 2 + members[0] is syntax.StringConstantTypeNode + (members[0] as syntax.StringConstantTypeNode).value == "bar" + members[1] is syntax.StringConstantTypeNode + (members[1] as syntax.StringConstantTypeNode).value == "baz" + } + + ["type annotation"] { + local result = parse("x: String = \"hello\"") + local prop = (result as syntax.ModuleNode).properties.first + prop.typeAnnotation != null + prop.typeAnnotation!! is syntax.TypeAnnotationNode + prop.typeAnnotation!!.type 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 f2bc6644d..05b1080f9 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err @@ -25,6 +25,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..4a44f87b6 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/expressions.pcf @@ -0,0 +1,132 @@ +examples { + ["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 + } + ["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 + } + ["unary operators"] { + true + true + true + true + true + true + } + ["if expression"] { + true + true + true + true + } + ["let expression"] { + true + true + true + true + } + ["new expression"] { + true + 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 + } +} 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..254b9fbbe --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/moduleStructure.pcf @@ -0,0 +1,135 @@ +examples { + ["module declaration"] { + true + true + true + true + true + true + true + true + true + true + true + } + ["module with modifiers and doc comment"] { + true + true + true + true + true + true + true + true + true + true + } + ["amends clause"] { + true + true + true + true + true + } + ["extends clause"] { + true + true + true + true + true + } + ["imports"] { + true + true + 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 + true + } + ["class with extends and type parameters"] { + true + true + true + true + true + true + true + } + ["typealias"] { + true + 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 + true + true + } + ["parser error"] { + true + true + } + ["empty module"] { + true + 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..963532ff0 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/objectMembers.pcf @@ -0,0 +1,113 @@ +examples { + ["object property"] { + true + true + true + true + true + true + true + } + ["object property with type and modifiers"] { + true + true + true + true + true + } + ["object property with amending body"] { + true + true + true + true + true + } + ["object method"] { + true + true + true + true + true + true + true + } + ["object element"] { + true + true + true + true + true + } + ["object entry"] { + true + true + true + true + } + ["object entry with amending body"] { + true + true + true + } + ["object spread"] { + true + true + true + true + } + ["nullable object spread"] { + true + true + true + } + ["member predicate"] { + true + true + true + true + } + ["member predicate with amending body"] { + true + true + true + } + ["for generator"] { + true + true + true + true + true + true + } + ["for generator with key"] { + true + true + true + } + ["when generator"] { + true + true + true + true + true + } + ["when generator with else"] { + true + true + true + true + } + ["object body with parameters"] { + true + true + true + true + } + ["mixed object members"] { + true + true + true + true + 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..450ba35d2 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/types.pcf @@ -0,0 +1,64 @@ +examples { + ["simple types"] { + true + true + true + } + ["declared type"] { + true + true + 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 + true + } +} diff --git a/stdlib/syntax.pkl b/stdlib/syntax.pkl new file mode 100644 index 000000000..740ab22ea --- /dev/null +++ b/stdlib/syntax.pkl @@ -0,0 +1,1601 @@ +//===----------------------------------------------------------------------===// +// 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 +module pkl.syntax + +/// Parse the string as a Pkl module, returning either a typed AST node or an error. +function parse(source: String): ModuleNode | ParserError = + let (result = parseNodes(source)) + if (result is ParserError) + result + else + new ModuleNode { node = result } + +/// Parse a resource as a Pkl module, returning either a typed AST node or an error. +function parseResource(resourceURI: String): ModuleNode | ParserError = + parse(read(resourceURI).text) + +external local function parseNodes(source: String): Node | ParserError + +class Node { + type: NodeType + children: List + parent: Node? + text: String? + @ConvertSpan + span: Span +} + +class Span { + lineStart: UInt + colStart: UInt + lineEnd: UInt + colEnd: UInt +} + +// noinspection TypeMismatch +class ConvertSpan extends ConvertProperty { + render = (property: Pair, _) -> + let (span = property.value) + Pair(property.key, "\(span.lineStart):\(span.colStart)-\(span.lineEnd):\(span.colEnd)") +} + +class ParserError { + text: String + span: Span +} + +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" + +// Find first child of a given type within a node. +local const function findChild(n: Node, t: NodeType): Node? = + n.children.findOrNull((c) -> c.type == t) + +// Find all children of a given type. +local const function findChildren(n: Node, t: NodeType): List = + n.children.filter((c) -> c.type == t) + +// Find the first child whose type is one of the expression types. +local const function findExprChild(n: Node): Node? = + n.children.findOrNull((c) -> isExprType(c.type)) + +// Find all children whose type is one of the expression types. +local const function findExprChildren(n: Node): List = + n.children.filter((c) -> isExprType(c.type)) + +// Find the first child whose type is one of the type node types. +local const function findTypeChild(n: Node): Node? = + n.children.findOrNull((c) -> isTypeType(c.type)) + +// Find all children whose type is one of the type node types. +local const function findTypeChildren(n: Node): List = + n.children.filter((c) -> isTypeType(c.type)) + +// Check if a NodeType represents an expression. +local const function isExprType(t: NodeType): Boolean = + t == "this_expr" + || t == "outer_expr" + || t == "module_expr" + || t == "null_expr" + || t == "throw_expr" + || t == "trace_expr" + || t == "import_expr" + || t == "read_expr" + || t == "new_expr" + || t == "unary_minus_expr" + || t == "logical_not_expr" + || t == "function_literal_expr" + || t == "parenthesized_expr" + || t == "super_subscript_expr" + || t == "super_access_expr" + || t == "subscript_expr" + || t == "qualified_access_expr" + || t == "if_expr" + || t == "let_expr" + || t == "bool_literal_expr" + || t == "int_literal_expr" + || t == "float_literal_expr" + || t == "single_line_string_literal_expr" + || t == "multi_line_string_literal_expr" + || t == "unqualified_access_expr" + || t == "non_null_expr" + || t == "amends_expr" + || t == "binary_op_expr" + +// Check if a NodeType represents a type node. +local const function isTypeType(t: NodeType): Boolean = + t == "unknown_type" + || t == "nothing_type" + || t == "module_type" + || t == "union_type" + || t == "function_type" + || t == "parenthesized_type" + || t == "declared_type" + || t == "nullable_type" + || t == "string_constant_type" + || t == "constrained_type" + +// Check if a NodeType represents an object member. +local const function isObjectMemberType(t: NodeType): Boolean = + t == "object_element" + || t == "object_property" + || t == "object_method" + || t == "member_predicate" + || t == "object_entry" + || t == "object_spread" + || t == "when_generator" + || t == "for_generator" + +// Wrap a raw Node into the appropriate Expr subclass. +local const function wrapExpr(n: Node): Expr = + if (n.type == "this_expr") + new ThisExprNode { node = n } + else if (n.type == "outer_expr") + new OuterExprNode { node = n } + else if (n.type == "module_expr") + new ModuleExprNode { node = n } + else if (n.type == "null_expr") + new NullLiteralExprNode { node = n } + else if (n.type == "bool_literal_expr") + new BoolLiteralExprNode { node = n } + else if (n.type == "int_literal_expr") + new IntLiteralExprNode { node = n } + else if (n.type == "float_literal_expr") + new FloatLiteralExprNode { node = n } + else if (n.type == "single_line_string_literal_expr") + new SingleLineStringLiteralExprNode { node = n } + else if (n.type == "multi_line_string_literal_expr") + new MultiLineStringLiteralExprNode { node = n } + else if (n.type == "unqualified_access_expr") + new UnqualifiedAccessExprNode { node = n } + else if (n.type == "qualified_access_expr") + new QualifiedAccessExprNode { node = n } + else if (n.type == "subscript_expr") + new SubscriptExprNode { node = n } + else if (n.type == "super_access_expr") + new SuperAccessExprNode { node = n } + else if (n.type == "super_subscript_expr") + new SuperSubscriptExprNode { node = n } + else if (n.type == "if_expr") + new IfExprNode { node = n } + else if (n.type == "let_expr") + new LetExprNode { node = n } + else if (n.type == "throw_expr") + new ThrowExprNode { node = n } + else if (n.type == "trace_expr") + new TraceExprNode { node = n } + else if (n.type == "import_expr") + new ImportExprNode { node = n } + else if (n.type == "read_expr") + new ReadExprNode { node = n } + else if (n.type == "new_expr") + new NewExprNode { node = n } + else if (n.type == "amends_expr") + new AmendsExprNode { node = n } + else if (n.type == "binary_op_expr") + new BinaryOpExprNode { node = n } + else if (n.type == "unary_minus_expr") + new UnaryMinusExprNode { node = n } + else if (n.type == "logical_not_expr") + new LogicalNotExprNode { node = n } + else if (n.type == "non_null_expr") + new NonNullExprNode { node = n } + else if (n.type == "function_literal_expr") + new FunctionLiteralExprNode { node = n } + else if (n.type == "parenthesized_expr") + new ParenthesizedExprNode { node = n } + else + throw("Unknown expression type: \(n.type)") + +// Wrap a raw Node into the appropriate TypeNode subclass. +local const function wrapTypeNode(n: Node): TypeNode = + if (n.type == "unknown_type") + new UnknownTypeNode { node = n } + else if (n.type == "nothing_type") + new NothingTypeNode { node = n } + else if (n.type == "module_type") + new ModuleTypeNode { node = n } + else if (n.type == "declared_type") + new DeclaredTypeNode { node = n } + else if (n.type == "nullable_type") + new NullableTypeNode { node = n } + else if (n.type == "union_type") + new UnionTypeNode { node = n } + else if (n.type == "function_type") + new FunctionTypeNode { node = n } + else if (n.type == "constrained_type") + new ConstrainedTypeNode { node = n } + else if (n.type == "parenthesized_type") + new ParenthesizedTypeNode { node = n } + else if (n.type == "string_constant_type") + new StringConstantTypeNode { node = n } + else + throw("Unknown type node type: \(n.type)") + +// Wrap a raw Node into the appropriate ObjectMemberNode subclass. +local const function wrapObjectMember(n: Node): ObjectMemberNode = + if (n.type == "object_element") + new ObjectElementNode { node = n } + else if (n.type == "object_property") + new ObjectPropertyNode { node = n } + else if (n.type == "object_method") + new ObjectMethodNode { node = n } + else if (n.type == "member_predicate") + new MemberPredicateNode { node = n } + else if (n.type == "object_entry") + new ObjectEntryNode { node = n } + else if (n.type == "object_spread") + new ObjectSpreadNode { node = n } + else if (n.type == "when_generator") + new WhenGeneratorNode { node = n } + else if (n.type == "for_generator") + new ForGeneratorNode { node = n } + else + throw("Unknown object member type: \(n.type)") + +// Extract the string constant from a STRING_CHARS node. +local const function extractStringConstant(n: Node?): String = + if (n == null) + "" + else + let (inner = n.children.filter((c) -> c.type == "terminal").drop(1).dropLast(1)) + inner.map((c) -> c.text ?? "").join("") + +// Find and extract the string_chars of a node. +local const function getStringChars(node: Node): String = + let (sc = findChild(node, "string_chars")) + extractStringConstant(sc) + +/// Base class for all typed syntax nodes. +abstract class SyntaxNode { + hidden node: Node + + hidden children: List = node.children + + hidden text: String? = node.text + + /// The source span of this node. + @ConvertSpan + span: Span = node.span + + /// All terminal children (keywords, punctuation, operators). + hidden terminals: List = children.filter((n) -> n.type == "terminal") + + /// All comment children. + hidden comments: List = + children.filter((n) -> n.type == "line_comment" || n.type == "block_comment") +} + +/// Base class for expression nodes. +abstract class Expr extends SyntaxNode {} + +/// Base class for type nodes. +abstract class TypeNode extends SyntaxNode {} + +/// Base class for object member nodes. +abstract class ObjectMemberNode extends SyntaxNode {} + +/// The top-level module node. +class ModuleNode extends SyntaxNode { + /// The module declaration, if present. + declaration: ModuleDeclarationNode? = + let (n = findChild(node, "module_declaration")) + if (n == null) + null + else + new ModuleDeclarationNode { node = n } + + /// All imports in this module. + imports: List = + let (importList = findChild(node, "import_list")) + if (importList == null) + List() + else + findChildren(importList, "import").map((n) -> new ImportNode { node = n }) + + /// All class declarations in this module. + classes: List = findChildren(node, "class").map((n) -> new ClassNode { node = n }) + + /// All typealias declarations in this module. + typeAliases: List = + findChildren(node, "typealias").map((n) -> new TypeAliasNode { node = n }) + + /// All top-level properties in this module. + properties: List = + findChildren(node, "class_property").map((n) -> new ClassPropertyNode { node = n }) + + /// All top-level methods in this module. + methods: List = + findChildren(node, "class_method").map((n) -> new ClassMethodNode { node = n }) +} + +/// A module declaration (including doc comment, annotations, modifiers, name, amends/extends). +class ModuleDeclarationNode extends SyntaxNode { + local moduleDefinition: Node? = findChild(node, "module_definition") + + /// The doc comment on the module declaration, if present. + docComment: DocCommentNode? = + let (n = findChild(node, "doc_comment")) + if (n == null) + null + else + new DocCommentNode { node = n } + + /// Annotations on the module declaration. + annotations: List = + findChildren(node, "annotation").map((n) -> new AnnotationNode { node = n }) + + /// The modifier list on the module declaration, if present. + modifiers: ModifierListNode? = + if (moduleDefinition == null) + null + else + let (n = findChild(moduleDefinition, "modifier_list")) + if (n == null) + null + else + new ModifierListNode { node = n } + + /// The qualified name of the module, if present. + name: QualifiedIdentifierNode? = + if (moduleDefinition == null) + null + else + let (n = findChild(moduleDefinition, "qualified_identifier")) + if (n == null) + null + else + new QualifiedIdentifierNode { node = n } + + /// The amends clause, if present. + amendsClause: AmendsClauseNode? = + let (n = findChild(node, "amends_clause")) + if (n == null) + null + else + new AmendsClauseNode { node = n } + + /// The extends clause, if present. + extendsClause: ExtendsClauseNode? = + let (n = findChild(node, "extends_clause")) + if (n == null) + null + else + new ExtendsClauseNode { node = n } +} + +/// An `amends "..."` clause. +class AmendsClauseNode extends SyntaxNode { + /// The URI string of the amended module. + uri: String = getStringChars(node) +} + +/// An `extends "..."` clause. +class ExtendsClauseNode extends SyntaxNode { + /// The URI string of the extended module. + uri: String = getStringChars(node) +} + +/// An import declaration. +class ImportNode extends SyntaxNode { + /// Whether this is a glob import (`import*`). + isGlob: Boolean = terminals.firstOrNull?.text == "import*" + + /// The URI string of the import. + uri: String = getStringChars(node) + + /// The alias for this import, if present. + alias: IdentifierNode? = + let (aliasNode = findChild(node, "import_alias")) + if (aliasNode == null) + null + else + let (id = findChild(aliasNode, "identifier")) + if (id == null) + null + else + new IdentifierNode { node = id } +} + +/// A class declaration. +class ClassNode extends SyntaxNode { + local header: Node = findChild(node, "class_header")!! + + /// The doc comment, if present. + docComment: DocCommentNode? = + let (n = findChild(node, "doc_comment")) + if (n == null) + null + else + new DocCommentNode { node = n } + + /// Annotations on the class. + annotations: List = + findChildren(node, "annotation").map((n) -> new AnnotationNode { node = n }) + + /// The modifier list, if present. + modifiers: ModifierListNode? = + let (n = findChild(header, "modifier_list")) + if (n == null) + null + else + new ModifierListNode { node = n } + + /// The class name. + name: IdentifierNode = + let (n = findChild(header, "identifier")) + new IdentifierNode { node = n!! } + + /// The type parameter list, if present. + typeParameterList: TypeParameterListNode? = + let (n = findChild(header, "type_parameter_list")) + if (n == null) + null + else + new TypeParameterListNode { node = n } + + /// The supertype this class extends, if present. + extendsClause: TypeNode? = + let (ext = findChild(header, "class_header_extends")) + if (ext == null) + null + else + let (t = findTypeChild(ext)) + if (t == null) + null + else + wrapTypeNode(t) + + /// The class body, if present. + body: ClassBodyNode? = + let (n = findChild(node, "class_body")) + if (n == null) + null + else + new ClassBodyNode { node = n } +} + +/// A typealias declaration. +class TypeAliasNode extends SyntaxNode { + local header: Node = findChild(node, "typealias_header")!! + + /// The doc comment, if present. + docComment: DocCommentNode? = + let (n = findChild(node, "doc_comment")) + if (n == null) + null + else + new DocCommentNode { node = n } + + /// Annotations on the typealias. + annotations: List = + findChildren(node, "annotation").map((n) -> new AnnotationNode { node = n }) + + /// The modifier list, if present. + modifiers: ModifierListNode? = + let (n = findChild(header, "modifier_list")) + if (n == null) + null + else + new ModifierListNode { node = n } + + /// The typealias name. + name: IdentifierNode = + let (n = findChild(header, "identifier")) + new IdentifierNode { node = n!! } + + /// The type parameter list, if present. + typeParameterList: TypeParameterListNode? = + let (n = findChild(header, "type_parameter_list")) + if (n == null) + null + else + new TypeParameterListNode { node = n } + + /// The type that this alias resolves to. + type: TypeNode = + let (body = findChild(node, "typealias_body")) + let (t = findTypeChild(body!!)) + wrapTypeNode(t!!) +} + +/// A class body delimited by braces. +class ClassBodyNode extends SyntaxNode { + local elements: Node? = findChild(node, "class_body_elements") + + /// Properties declared in this class body. + properties: List = + if (elements == null) + List() + else + findChildren(elements, "class_property").map((n) -> new ClassPropertyNode { node = n }) + + /// Methods declared in this class body. + methods: List = + if (elements == null) + List() + else + findChildren(elements, "class_method").map((n) -> new ClassMethodNode { node = n }) +} + +/// A class property declaration. +class ClassPropertyNode extends SyntaxNode { + local header: Node = findChild(node, "class_property_header")!! + local headerBegin: Node = findChild(header, "class_property_header_begin")!! + + /// The doc comment, if present. + docComment: DocCommentNode? = + let (n = findChild(node, "doc_comment")) + if (n == null) + null + else + new DocCommentNode { node = n } + + /// Annotations on the property. + annotations: List = + findChildren(node, "annotation").map((n) -> new AnnotationNode { node = n }) + + /// The modifier list, if present. + modifiers: ModifierListNode? = + let (n = findChild(headerBegin, "modifier_list")) + if (n == null) + null + else + new ModifierListNode { node = n } + + /// The property name. + name: IdentifierNode = + let (n = findChild(headerBegin, "identifier")) + new IdentifierNode { node = n!! } + + /// The type annotation, if present. + typeAnnotation: TypeAnnotationNode? = + let (n = findChild(header, "type_annotation")) + if (n == null) + null + else + new TypeAnnotationNode { node = n } + + /// The value expression, if present (from `= expr`). + value: Expr? = + let (body = findChild(node, "class_property_body")) + if (body == null) + null + else + let (e = findExprChild(body)) + if (e == null) + null + else + wrapExpr(e) + + /// Object bodies for amending (from `{ ... }` blocks). + objectBodies: List = + findChildren(node, "object_body").map((n) -> new ObjectBodyNode { node = n }) +} + +/// A class method declaration. +class ClassMethodNode extends SyntaxNode { + local methodHeader: Node = findChild(node, "class_method_header")!! + + /// The doc comment, if present. + docComment: DocCommentNode? = + let (n = findChild(node, "doc_comment")) + if (n == null) + null + else + new DocCommentNode { node = n } + + /// Annotations on the method. + annotations: List = + findChildren(node, "annotation").map((n) -> new AnnotationNode { node = n }) + + /// The modifier list, if present. + modifiers: ModifierListNode? = + let (n = findChild(methodHeader, "modifier_list")) + if (n == null) + null + else + new ModifierListNode { node = n } + + /// The method name. + name: IdentifierNode = + let (n = findChild(methodHeader, "identifier")) + new IdentifierNode { node = n!! } + + /// The type parameter list, if present. + typeParameterList: TypeParameterListNode? = + let (n = findChild(node, "type_parameter_list")) + if (n == null) + null + else + new TypeParameterListNode { node = n } + + /// The parameter list. + parameterList: ParameterListNode = + let (n = findChild(node, "parameter_list")) + new ParameterListNode { node = n!! } + + /// The return type annotation, if present. + returnType: TypeAnnotationNode? = + let (n = findChild(node, "type_annotation")) + if (n == null) + null + else + new TypeAnnotationNode { node = n } + + /// The method body expression, if present. + body: Expr? = + let (bodyNode = findChild(node, "class_method_body")) + if (bodyNode == null) + null + else + let (e = findExprChild(bodyNode)) + if (e == null) + null + else + wrapExpr(e) +} + +/// An object body delimited by braces. +class ObjectBodyNode extends SyntaxNode { + local paramList: Node? = findChild(node, "object_parameter_list") + local memberList: Node? = findChild(node, "object_member_list") + + /// Parameters for this object body (e.g., `{ x, y -> ... }`). + parameters: List = + if (paramList == null) + List() + else + findChildren(paramList, "parameter").map((n) -> new ParameterNode { node = n }) + + /// All object members (properties, methods, elements, entries, spreads, generators). + members: List = + if (memberList == null) + List() + else + memberList.children + .filter((c) -> isObjectMemberType(c.type)) + .map((n) -> wrapObjectMember(n)) + + /// Object properties in this body. + properties: List = + if (memberList == null) + List() + else + findChildren(memberList, "object_property").map((n) -> new ObjectPropertyNode { node = n }) + + /// Object methods in this body. + methods: List = + if (memberList == null) + List() + else + findChildren(memberList, "object_method").map((n) -> new ObjectMethodNode { node = n }) + + /// Object elements in this body. + elements: List = + if (memberList == null) + List() + else + findChildren(memberList, "object_element").map((n) -> new ObjectElementNode { node = n }) + + /// Object entries in this body. + entries: List = + if (memberList == null) + List() + else + findChildren(memberList, "object_entry").map((n) -> new ObjectEntryNode { node = n }) +} + +/// An object property declaration. +class ObjectPropertyNode extends ObjectMemberNode { + local header: Node = findChild(node, "object_property_header")!! + local headerBegin: Node = findChild(header, "object_property_header_begin")!! + + /// The modifier list, if present. + modifiers: ModifierListNode? = + let (n = findChild(headerBegin, "modifier_list")) + if (n == null) + null + else + new ModifierListNode { node = n } + + /// The property name. + name: IdentifierNode = + let (n = findChild(headerBegin, "identifier")) + new IdentifierNode { node = n!! } + + /// The type annotation, if present. + typeAnnotation: TypeAnnotationNode? = + let (n = findChild(header, "type_annotation")) + if (n == null) + null + else + new TypeAnnotationNode { node = n } + + /// The value expression, if present (from `= expr`). + value: Expr? = + let (body = findChild(node, "object_property_body")) + if (body == null) + null + else + let (e = findExprChild(body)) + if (e == null) + null + else + wrapExpr(e) + + /// Object bodies for amending. + objectBodies: List = + findChildren(node, "object_body").map((n) -> new ObjectBodyNode { node = n }) +} + +/// An object method declaration. +class ObjectMethodNode extends ObjectMemberNode { + local methodHeader: Node = findChild(node, "class_method_header")!! + + /// The modifier list, if present. + modifiers: ModifierListNode? = + let (n = findChild(methodHeader, "modifier_list")) + if (n == null) + null + else + new ModifierListNode { node = n } + + /// The method name. + name: IdentifierNode = + let (n = findChild(methodHeader, "identifier")) + new IdentifierNode { node = n!! } + + /// The type parameter list, if present. + typeParameterList: TypeParameterListNode? = + let (n = findChild(node, "type_parameter_list")) + if (n == null) + null + else + new TypeParameterListNode { node = n } + + /// The parameter list. + parameterList: ParameterListNode = + let (n = findChild(node, "parameter_list")) + new ParameterListNode { node = n!! } + + /// The return type annotation, if present. + returnType: TypeAnnotationNode? = + let (n = findChild(node, "type_annotation")) + if (n == null) + null + else + new TypeAnnotationNode { node = n } + + /// The method body expression. + body: Expr? = + let (bodyNode = findChild(node, "class_method_body")) + if (bodyNode == null) + null + else + let (e = findExprChild(bodyNode)) + if (e == null) + null + else + wrapExpr(e) +} + +/// An object element (a positional expression in an object body). +class ObjectElementNode extends ObjectMemberNode { + /// The expression value. + expression: Expr = wrapExpr(findExprChild(node)!!) +} + +/// An object entry (`[key] = value` or `[key] { ... }`). +class ObjectEntryNode extends ObjectMemberNode { + local entryHeader: Node = findChild(node, "object_entry_header")!! + + /// The key expression. + key: Expr = wrapExpr(findExprChild(entryHeader)!!) + + /// The value expression, if present (from `[key] = value`). + value: Expr? = + let (e = findExprChildren(node).findOrNull((c) -> c != findExprChild(entryHeader))) + if (e == null) + null + else + wrapExpr(e) + + /// Object bodies for amending. + objectBodies: List = + findChildren(node, "object_body").map((n) -> new ObjectBodyNode { node = n }) +} + +/// An object spread (`...expr` or `...?expr`). +class ObjectSpreadNode extends ObjectMemberNode { + /// Whether this is a nullable spread (`...?`). + isNullable: Boolean = terminals.firstOrNull?.text == "...?" + + /// The spread expression. + expression: Expr = wrapExpr(findExprChild(node)!!) +} + +/// A member predicate (`[[condition]] = value` or `[[condition]] { ... }`). +class MemberPredicateNode extends ObjectMemberNode { + local exprs = findExprChildren(node) + + /// The condition expression. + condition: Expr = wrapExpr(exprs.first) + + /// The value expression, if present. + value: Expr? = + if (exprs.length < 2) + null + else + wrapExpr(exprs[1]) + + /// Object bodies for amending. + objectBodies: List = + findChildren(node, "object_body").map((n) -> new ObjectBodyNode { node = n }) +} + +/// A `for (param in iterable) { ... }` generator. +class ForGeneratorNode extends ObjectMemberNode { + local forHeader: Node = findChild(node, "for_generator_header")!! + local forDef: Node = findChild(forHeader, "for_generator_header_definition")!! + local forDefHeader: Node = findChild(forDef, "for_generator_header_definition_header")!! + local paramNodes: List = findChildren(forDefHeader, "parameter") + + /// The key parameter (first parameter when two are present), if present. + keyParameter: ParameterNode? = + if (paramNodes.length < 2) + null + else + new ParameterNode { node = paramNodes.first } + + /// The value parameter (or the only parameter when just one is present). + valueParameter: ParameterNode = new ParameterNode { node = paramNodes.last } + + /// The iterable expression. + iterable: Expr = + let (e = findExprChild(forDef)) + wrapExpr(e!!) + + /// The body. + body: ObjectBodyNode = + let (n = findChild(node, "object_body")) + new ObjectBodyNode { node = n!! } +} + +/// A `when (condition) { ... }` generator. +class WhenGeneratorNode extends ObjectMemberNode { + local whenHeader: Node = findChild(node, "when_generator_header")!! + local bodyNodes: List = findChildren(node, "object_body") + + /// The condition expression. + condition: Expr = + let (e = findExprChild(whenHeader)) + wrapExpr(e!!) + + /// The "then" body. + thenBody: ObjectBodyNode = new ObjectBodyNode { node = bodyNodes.first } + + /// The "else" body, if present. + elseBody: ObjectBodyNode? = + if (bodyNodes.length < 2) + null + else + new ObjectBodyNode { node = bodyNodes[1] } +} + +/// The `this` expression. +class ThisExprNode extends Expr {} + +/// The `outer` expression. +class OuterExprNode extends Expr {} + +/// The `module` expression. +class ModuleExprNode extends Expr {} + +/// A `null` literal expression. +class NullLiteralExprNode extends Expr {} + +/// A boolean literal expression (`true` or `false`). +class BoolLiteralExprNode extends Expr { + /// The boolean value. + value: Boolean = node.text == "true" +} + +/// An integer literal expression. +class IntLiteralExprNode extends Expr { + /// The raw text of the integer literal. + text: String = node.text ?? "" +} + +/// A float literal expression. +class FloatLiteralExprNode extends Expr { + /// The raw text of the float literal. + text: String = node.text ?? "" +} + +/// A single-line string literal expression. +class SingleLineStringLiteralExprNode extends Expr { + /// The string parts (chars, escapes, interpolations). + parts: List = buildStringParts(children) +} + +/// A multi-line string literal expression. +class MultiLineStringLiteralExprNode extends Expr { + /// The string parts (chars, escapes, interpolations). + parts: List = buildStringParts(children) +} + +/// An unqualified access expression (`name` or `name(args)`). +class UnqualifiedAccessExprNode extends Expr { + /// The identifier being accessed. + identifier: IdentifierNode = + let (n = findChild(node, "identifier")) + new IdentifierNode { node = n!! } + + /// The argument list, if present. + argumentList: ArgumentListNode? = + let (n = findChild(node, "argument_list")) + if (n == null) + null + else + new ArgumentListNode { node = n } +} + +/// A qualified access expression (`receiver.member` or `receiver?.member`). +class QualifiedAccessExprNode extends Expr { + /// The receiver expression. + receiver: Expr = + let (exprs = findExprChildren(node)) + wrapExpr(exprs.first) + + /// Whether this is a null-safe access (`?.`). + isNullSafe: Boolean = findChild(node, "operator")?.text == "?." + + /// The accessed member. + member: UnqualifiedAccessExprNode = + let (n = findChildren(node, "unqualified_access_expr").last) + new UnqualifiedAccessExprNode { node = n } +} + +/// A subscript expression (`receiver[index]`). +class SubscriptExprNode extends Expr { + /// The receiver expression. + receiver: Expr = + let (exprs = findExprChildren(node)) + wrapExpr(exprs.first) + + /// The index expression. + index: Expr = + let (exprs = findExprChildren(node)) + wrapExpr(exprs[1]) +} + +/// A `super.member` access expression. +class SuperAccessExprNode extends Expr {} + +/// A `super[index]` subscript expression. +class SuperSubscriptExprNode extends Expr {} + +/// An `if (condition) thenExpr else elseExpr` expression. +class IfExprNode extends Expr { + local ifHeader: Node = findChild(node, "if_header")!! + local ifCondition: Node = findChild(ifHeader, "if_condition")!! + local ifConditionExpr: Node = findChild(ifCondition, "if_condition_expr")!! + + /// The condition expression. + condition: Expr = + let (e = findExprChild(ifConditionExpr)) + wrapExpr(e!!) + + /// The then-branch expression. + thenExpr: Expr = + let (thenNode = findChild(node, "if_then_expr")) + let (e = findExprChild(thenNode!!)) + wrapExpr(e!!) + + /// The else-branch expression. + elseExpr: Expr = + let (elseNode = findChild(node, "if_else_expr")) + let (e = findExprChild(elseNode!!)) + wrapExpr(e!!) +} + +/// A `let (param = value) body` expression. +class LetExprNode extends Expr { + local letParamDef: Node = findChild(node, "let_parameter_definition")!! + local letParam: Node = findChild(letParamDef, "let_parameter")!! + + /// The let-binding parameter. + parameter: ParameterNode = + let (p = findChild(letParam, "parameter")) + new ParameterNode { node = p!! } + + /// The binding value expression. + bindingValue: Expr = + let (e = findExprChild(letParam)) + wrapExpr(e!!) + + /// The body expression. + bodyExpr: Expr = wrapExpr(findExprChild(node)!!) +} + +/// A `throw(expr)` expression. +class ThrowExprNode extends Expr { + /// The expression being thrown. + expression: Expr = wrapExpr(findExprChild(node)!!) +} + +/// A `trace(expr)` expression. +class TraceExprNode extends Expr { + /// The expression being traced. + expression: Expr = wrapExpr(findExprChild(node)!!) +} + +/// An `import("uri")` or `import*("uri")` expression. +class ImportExprNode extends Expr { + /// Whether this is a glob import expression (`import*`). + isGlob: Boolean = terminals.firstOrNull?.text == "import*" + + /// The import URI string. + uri: String = getStringChars(node) +} + +/// A `read(expr)`, `read*(expr)`, or `read?(expr)` expression. +class ReadExprNode extends Expr { + /// The keyword used (`"read"`, `"read?"`, or `"read*"`). + keyword: String = terminals.firstOrNull?.text ?? "read" + + // The expression to be read + expr: Expr = wrapExpr(findExprChild(node)!!) +} + +/// A `new Type { ... }` expression. +class NewExprNode extends Expr { + local newHeader: Node = findChild(node, "new_header")!! + + /// The type being constructed, if present. + type: TypeNode? = + let (t = findTypeChild(newHeader)) + if (t == null) + null + else + wrapTypeNode(t) + + /// The object body. + body: ObjectBodyNode = + let (n = findChild(node, "object_body")) + new ObjectBodyNode { node = n!! } +} + +/// An `(expr) { ... }` amends expression. +class AmendsExprNode extends Expr { + /// The expression being amended. + parentExpr: Expr = + let (exprs = findExprChildren(node)) + wrapExpr(exprs.first) + + /// The object body. + body: ObjectBodyNode = + let (n = findChild(node, "object_body")) + new ObjectBodyNode { node = n!! } +} + +/// A binary operator expression (`left op right`). +class BinaryOpExprNode extends Expr { + local exprs = findExprChildren(node) + + /// The operator string. + operator: String = findChild(node, "operator")?.text ?? "" + + /// The left-hand expression. + leftExpr: Expr = wrapExpr(exprs.first) + + /// The right-hand expression, if present (not present for `is`/`as` which use a type). + rightExpr: Expr? = + if (exprs.length < 2) + null + else + wrapExpr(exprs[1]) + + /// The right-hand type, if this is an `is` or `as` operation. + rightType: TypeNode? = + let (t = findTypeChild(node)) + if (t == null) + null + else + wrapTypeNode(t) +} + +/// A unary minus expression (`-expr`). +class UnaryMinusExprNode extends Expr { + /// The operand expression. + operand: Expr = wrapExpr(findExprChild(node)!!) +} + +/// A logical not expression (`!expr`). +class LogicalNotExprNode extends Expr { + /// The operand expression. + operand: Expr = wrapExpr(findExprChild(node)!!) +} + +/// A non-null assertion expression (`expr!!`). +class NonNullExprNode extends Expr { + /// The operand expression. + operand: Expr = wrapExpr(findExprChild(node)!!) +} + +/// A function literal expression (`(params) -> body`). +class FunctionLiteralExprNode extends Expr { + /// The parameter list. + parameterList: ParameterListNode = + let (n = findChild(node, "parameter_list")) + new ParameterListNode { node = n!! } + + /// The body expression. + body: Expr = + let (bodyNode = findChild(node, "function_literal_body")) + let (e = findExprChild(bodyNode!!)) + wrapExpr(e!!) +} + +/// A parenthesized expression (`(expr)`). +class ParenthesizedExprNode extends Expr { + /// The inner expression, if present (may be empty for `()`). + expression: Expr? = + let (elems = findChild(node, "parenthesized_expr_elements")) + if (elems == null) + null + else + let (e = findExprChild(elems)) + if (e == null) + null + else + wrapExpr(e) +} + +/// 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. + name: QualifiedIdentifierNode = + let (n = findChild(node, "qualified_identifier")) + new QualifiedIdentifierNode { node = n!! } + + /// The type argument list, if present. + typeArgumentList: TypeArgumentListNode? = + let (n = findChild(node, "type_argument_list")) + if (n == null) + null + else + new TypeArgumentListNode { node = n } +} + +/// A nullable type (`Type?`). +class NullableTypeNode extends TypeNode { + /// The base type. + baseType: TypeNode = wrapTypeNode(findTypeChild(node)!!) +} + +/// A union type (`TypeA|TypeB|TypeC`). +class UnionTypeNode extends TypeNode { + /// The member types. + members: List = findTypeChildren(node).map((n) -> wrapTypeNode(n)) +} + +/// A function type (`(ParamTypes) -> ReturnType`). +class FunctionTypeNode extends TypeNode { + local params: Node = findChild(node, "function_type_parameters")!! + local paramElems: Node? = findChild(params, "parenthesized_type_elements") + + /// The parameter types. + parameterTypes: List = + if (paramElems == null) + List() + else + findTypeChildren(paramElems).map((n) -> wrapTypeNode(n)) + + /// The return type. + returnType: TypeNode = + let (types = findTypeChildren(node)) + wrapTypeNode(types.last) +} + +/// A constrained type (`Type(constraint)`). +class ConstrainedTypeNode extends TypeNode { + /// The base type. + baseType: TypeNode = wrapTypeNode(findTypeChild(node)!!) + + local constraint: Node = findChild(node, "constrained_type_constraint")!! + local constraintElems: Node = findChild(constraint, "constrained_type_elements")!! + + /// The constraint expressions. + constraints: List = findExprChildren(constraintElems).map((n) -> wrapExpr(n)) +} + +/// A parenthesized type (`(Type)`). +class ParenthesizedTypeNode extends TypeNode { + /// The inner type, if present. + type: TypeNode? = + let (elems = findChild(node, "parenthesized_type_elements")) + if (elems == null) + null + else + let (t = findTypeChild(elems)) + if (t == null) + null + else + wrapTypeNode(t) +} + +/// A string constant type (e.g., `"foo"`). +class StringConstantTypeNode extends TypeNode { + /// The string value. + value: String = getStringChars(node) +} + +/// An annotation (`@Type { ... }`). +class AnnotationNode extends SyntaxNode { + /// The annotation type. + type: TypeNode = wrapTypeNode(findTypeChild(node)!!) + + /// The annotation body, if present. + body: ObjectBodyNode? = + let (n = findChild(node, "object_body")) + if (n == null) + null + else + new ObjectBodyNode { node = n } +} + +/// A parameter declaration. +class ParameterNode extends SyntaxNode { + /// Whether this is a wildcard parameter (`_`). + isWildcard: Boolean = + identifier == null + && children.findOrNull((c) -> c.type == "terminal" && c.text == "_") != null + + /// The parameter identifier, if not a wildcard. + identifier: IdentifierNode? = + let (n = findChild(node, "identifier")) + if (n == null) + null + else + new IdentifierNode { node = n } + + /// The type annotation, if present. + typeAnnotation: TypeAnnotationNode? = + let (n = findChild(node, "type_annotation")) + if (n == null) + null + else + new TypeAnnotationNode { node = n } +} + +/// A parameter list (`(param1, param2)`). +class ParameterListNode extends SyntaxNode { + local elems: Node? = findChild(node, "parameter_list_elements") + + /// The parameters in this list. + parameters: List = + if (elems == null) + List() + else + findChildren(elems, "parameter").map((n) -> new ParameterNode { node = n }) +} + +/// An argument list (`(arg1, arg2)`). +class ArgumentListNode extends SyntaxNode { + local elems: Node? = findChild(node, "argument_list_elements") + + /// The argument expressions. + arguments: List = + if (elems == null) + List() + else + findExprChildren(elems).map((n) -> wrapExpr(n)) +} + +/// A type annotation (`: Type`). +class TypeAnnotationNode extends SyntaxNode { + /// The type. + type: TypeNode = wrapTypeNode(findTypeChild(node)!!) +} + +/// A type parameter declaration. +class TypeParameterNode extends SyntaxNode { + /// The variance modifier (`"in"`, `"out"`, or null). + variance: String? = terminals.findOrNull((t) -> t.text == "in" || t.text == "out")?.text + + /// The type parameter name. + name: IdentifierNode = + let (n = findChild(node, "identifier")) + new IdentifierNode { node = n!! } +} + +/// A type parameter list (``). +class TypeParameterListNode extends SyntaxNode { + local elems: Node? = findChild(node, "type_parameter_list_elements") + + /// The type parameters. + typeParameters: List = + if (elems == null) + List() + else + findChildren(elems, "type_parameter").map((n) -> new TypeParameterNode { node = n }) +} + +/// A type argument list (``). +class TypeArgumentListNode extends SyntaxNode { + local elems: Node? = findChild(node, "type_argument_list_elements") + + /// The type arguments. + typeArguments: List = + if (elems == null) + List() + else + findTypeChildren(elems).map((n) -> wrapTypeNode(n)) +} + +/// An identifier node. +class IdentifierNode extends SyntaxNode { + /// The identifier text. + value: String = node.text ?? "" +} + +/// A qualified identifier (`a.b.c`). +class QualifiedIdentifierNode extends SyntaxNode { + /// The identifiers in this qualified name. + identifiers: List = + findChildren(node, "identifier").map((n) -> new IdentifierNode { node = n }) +} + +/// A doc comment. +class DocCommentNode extends SyntaxNode { + /// The text of each doc comment line. + lines: List = findChildren(node, "doc_comment_line").map((n) -> n.text ?? "") +} + +/// A modifier list (e.g., `open`, `abstract external`). +class ModifierListNode extends SyntaxNode { + /// The modifier keywords. + modifiers: List = findChildren(node, "modifier").map((n) -> n.text ?? "") +} + +/// Base class for parts of a string literal. +abstract class StringPartNode extends SyntaxNode {} + +/// A plain text part of a string literal. +class StringCharsNode extends StringPartNode { + /// The text content. + value: String = node.text ?? "" +} + +/// An escape sequence in a string literal. +class StringEscapeNode extends StringPartNode { + /// The escape sequence text. + value: String = node.text ?? "" +} + +/// A newline in a multi-line string literal. +class StringNewlineNode extends StringPartNode {} + +/// An interpolation in a string literal. +class StringInterpolationNode extends StringPartNode { + /// The interpolated expression. + expression: Expr = wrapExpr(node) +} + +/// Build string parts from the children of a string literal node. +local const function buildStringParts(cs: List): List = + // skip opening and closing terminals + let (inner = cs.drop(1).dropLast(1)) + buildStringPartsInner(inner, 0, List()) + +local const function buildStringPartsInner( + cs: List, + i: Int, + acc: List, +): List = + if (i >= cs.length) + acc + else + let (c = cs[i]) + if (c.type == "string_chars") + buildStringPartsInner(cs, i + 1, acc.add(new StringCharsNode { node = c })) + else if (c.type == "string_escape") + buildStringPartsInner(cs, i + 1, acc.add(new StringEscapeNode { node = c })) + else if (c.type == "string_newline") + buildStringPartsInner(cs, i + 1, acc.add(new StringNewlineNode { node = c })) + else if (c.type == "terminal" && isInterpolationStart(c)) + let (exprAndClose = findInterpolationExpr(cs, i + 1)) + buildStringPartsInner( + cs, + exprAndClose.second, + acc.add(new StringInterpolationNode { node = exprAndClose.first }), + ) + else if (c.type == "line_comment" || c.type == "block_comment" || c.type == "semicolon") + // skip affixes + buildStringPartsInner(cs, i + 1, acc) + else + // skip other terminals (shouldn't normally happen) + buildStringPartsInner(cs, i + 1, acc) + +local const function isInterpolationStart(n: Node): Boolean = + let (t = n.text) + if (t == null) + false + else + t.endsWith("(") + && (t.startsWith("\\") || t.startsWith("#")) + +/// Find the interpolation expression and return it along with the index past the closing paren. +local const function findInterpolationExpr(cs: List, startIdx: Int): Pair = + // walk forward to find the expression node (skip affixes) + let (exprIdx = findNextNonAffix(cs, startIdx)) + if (exprIdx >= cs.length) + Pair(cs[startIdx - 1], cs.length) + else + let (exprNode = cs[exprIdx]) + // next should be the closing terminal ")" + let (closeIdx = findNextNonAffix(cs, exprIdx + 1)) + Pair(exprNode, closeIdx + 1) + +local const function findNextNonAffix(cs: List, startIdx: Int): Int = + if (startIdx >= cs.length) + startIdx + else if ( + cs[startIdx].type == "line_comment" + || cs[startIdx].type == "block_comment" + || cs[startIdx].type == "semicolon" + ) + findNextNonAffix(cs, startIdx + 1) + else + startIdx From 129e02ce314021c039826294a614a7baa0f9e6bd Mon Sep 17 00:00:00 2001 From: Islon Scherer Date: Thu, 30 Apr 2026 17:25:19 +0200 Subject: [PATCH 2/4] Add support for full round-trip of Pkl code --- pkl-core/pkl-core.gradle.kts | 1 + .../pkl/core/stdlib/syntax/SyntaxNodes.java | 69 +++- .../input/syntax/format.pkl | 367 ++++++++++++++++++ .../output/syntax/format.pcf | 130 +++++++ .../java/org/pkl/formatter/Formatter.java | 19 + .../org/pkl/parser/syntax/generic/Node.java | 4 + stdlib/syntax.pkl | 15 +- 7 files changed, 595 insertions(+), 10 deletions(-) create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/syntax/format.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/syntax/format.pcf diff --git a/pkl-core/pkl-core.gradle.kts b/pkl-core/pkl-core.gradle.kts index 243e94f42..c421ea9ae 100644 --- a/pkl-core/pkl-core.gradle.kts +++ b/pkl-core/pkl-core.gradle.kts @@ -48,6 +48,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/stdlib/syntax/SyntaxNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/syntax/SyntaxNodes.java index 9018bc2ea..40db0e1fd 100644 --- 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 @@ -18,20 +18,35 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Specialization; import java.util.ArrayList; +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.ExternalMethod1Node; +import org.pkl.core.stdlib.ExternalMethod2Node; import org.pkl.core.stdlib.VmObjectFactory; +import org.pkl.formatter.Formatter; +import org.pkl.formatter.GrammarVersion; import org.pkl.parser.GenericParser; import org.pkl.parser.GenericParserError; 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 Identifier TYPE_ID = Identifier.get("type"); + private static final Identifier CHILDREN_ID = Identifier.get("children"); + private static final Identifier SPAN_ID = Identifier.get("span"); + private static final Identifier LINE_START_ID = Identifier.get("lineStart"); + private static final Identifier COL_START_ID = Identifier.get("colStart"); + private static final Identifier LINE_END_ID = Identifier.get("lineEnd"); + private static final Identifier COL_END_ID = Identifier.get("colEnd"); + private static final char[] EMPTY_SOURCE = new char[0]; + /** Extra storage backing a Pkl {@code Node} instance. */ static final class NodeData { final Node node; @@ -96,23 +111,20 @@ protected Object eval(VmTyped self, String source) { } } - @TruffleBoundary private static VmTyped convertNode(Node genericNode, char[] sourceChars) { - // Convert children recursively + // convert children recursively var childrenList = new ArrayList(genericNode.children.size()); for (var child : genericNode.children) { childrenList.add(convertNode(child, sourceChars)); } - // Build NodeData var data = new NodeData(genericNode, sourceChars); data.childrenVm = VmList.create(childrenList.toArray()); data.spanVm = spanFactory.create(genericNode.span); - // Create VmTyped node var result = nodeFactory.create(data); - // Set parent back-reference on each child + // set parent back-reference on each child for (var childVm : childrenList) { var childData = (NodeData) childVm.getExtraStorage(); childData.parentVm = result; @@ -121,4 +133,51 @@ private static VmTyped convertNode(Node genericNode, char[] sourceChars) { return result; } } + + public abstract static class formatToString extends ExternalMethod2Node { + @Specialization + @TruffleBoundary + protected String eval(VmTyped self, VmTyped nodeVm, String grammarVersion) { + var node = convertVmToNode(nodeVm); + return new Formatter(GrammarVersion.valueOf(grammarVersion)).format(node); + } + } + + private static Node convertVmToNode(VmTyped nodeVm) { + var typeStr = (String) VmUtils.readMember(nodeVm, TYPE_ID); + var nodeType = NodeType.valueOf(typeStr.toUpperCase()); + + var childrenVm = (VmList) VmUtils.readMember(nodeVm, CHILDREN_ID); + var children = new ArrayList(childrenVm.getLength()); + for (var i = 0; i < childrenVm.getLength(); i++) { + children.add(convertVmToNode((VmTyped) childrenVm.get(i))); + } + + var spanVm = (VmTyped) VmUtils.readMember(nodeVm, SPAN_ID); + var lineStart = ((Long) VmUtils.readMember(spanVm, LINE_START_ID)).intValue(); + var colStart = ((Long) VmUtils.readMember(spanVm, COL_START_ID)).intValue(); + var lineEnd = ((Long) VmUtils.readMember(spanVm, LINE_END_ID)).intValue(); + var colEnd = ((Long) VmUtils.readMember(spanVm, COL_END_ID)).intValue(); + var span = new FullSpan(0, 0, lineStart, colStart, lineEnd, colEnd); + + Node node; + if (children.isEmpty()) { + node = new Node(nodeType, span); + } else { + node = new Node(nodeType, span, children); + } + + var textObj = VmUtils.readMember(nodeVm, Identifier.TEXT); + if (textObj instanceof String text) { + node.setText(text); + } else if (nodeType == NodeType.STRING_CHARS) { + var sb = new StringBuilder(); + for (var child : children) { + sb.append(child.text(EMPTY_SOURCE)); + } + node.setText(sb.toString()); + } + + return node; + } } diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/format.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/format.pkl new file mode 100644 index 000000000..96635c630 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/format.pkl @@ -0,0 +1,367 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local function roundTrip(source: String) = syntax.format(parseNode(source)) + +local function parseNode(source: String) = syntax.parse(source).node + +local function replaceLeaf( + node: syntax.Node, + targetType: syntax.NodeType, + oldText: String, + newText: String, +): syntax.Node = + 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.Node, + targetType: syntax.NodeType, + transform: (syntax.Node) -> syntax.Node, +): syntax.Node = + if (node.type == targetType) + transform.apply(node) + else + (node) { + children = node.children.map((c) -> transformFirst(c, targetType, transform)) + } + +examples { + ["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 + """ + + """# + } + + ["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" + } + + ["modify identifier"] { + local root = parseNode("x = 1") + local modified = replaceLeaf(root, "identifier", "x", "y") + syntax.format(modified) == "y = 1\n" + } + + ["modify modifier"] { + local root = parseNode("hidden x = 1") + local modified = replaceLeaf(root, "modifier", "hidden", "local") + syntax.format(modified) == "local x = 1\n" + } + + ["modify string content"] { + local root = parseNode(#"x = "hello""#) + local modified = replaceLeaf(root, "string_chars", "hello", "world") + syntax.format(modified) == #"x = "world"\#n"# + } + + ["modify int literal"] { + local root = parseNode("x = 42") + local modified = replaceLeaf(root, "int_literal_expr", "42", "99") + syntax.format(modified) == "x = 99\n" + } + + ["modify boolean literal"] { + local root = parseNode("x = true") + local modified = replaceLeaf(root, "bool_literal_expr", "true", "false") + syntax.format(modified) == "x = false\n" + } + + ["modify float literal"] { + local root = parseNode("x = 3.14") + local modified = replaceLeaf(root, "float_literal_expr", "3.14", "2.72") + syntax.format(modified) == "x = 2.72\n" + } + + ["add new modifier"] { + local root = parseNode("local x = 1") + local constModifier = new syntax.Node { + type = "modifier" + text = "const" + } + local modified = + transformFirst(root, "modifier_list", (n) -> (n) { + children = List(constModifier) + n.children + }) + // modifier order is switched by the formatter + syntax.format(modified) == """ + local const x = 1 + + """ + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/format.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/format.pcf new file mode 100644 index 000000000..4a5572a28 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/format.pcf @@ -0,0 +1,130 @@ +examples { + ["empty module"] { + true + } + ["simple property"] { + true + } + ["multiple properties"] { + true + } + ["string literals"] { + true + true + true + } + ["multiline string"] { + 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 + } + ["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 + } +} 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/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 index 740ab22ea..62c252b41 100644 --- a/stdlib/syntax.pkl +++ b/stdlib/syntax.pkl @@ -31,20 +31,25 @@ function parseResource(resourceURI: String): ModuleNode | ParserError = external local function parseNodes(source: String): Node | ParserError +/// Format a syntax node back to Pkl source code. +function format(node: Node): String = formatToString(node, "V2") + +external function formatToString(node: Node, grammarVersion: "V1" | "V2"): String + class Node { type: NodeType children: List - parent: Node? + hidden parent: Node? text: String? @ConvertSpan span: Span } class Span { - lineStart: UInt - colStart: UInt - lineEnd: UInt - colEnd: UInt + lineStart: UInt = 0 + colStart: UInt = 0 + lineEnd: UInt = 0 + colEnd: UInt = 0 } // noinspection TypeMismatch From 89c02b4cea2f5b8b7b62cd969b3b2db3a8f5af15 Mon Sep 17 00:00:00 2001 From: Islon Scherer Date: Mon, 18 May 2026 17:00:10 +0200 Subject: [PATCH 3/4] Add ast builders --- .../input/syntax/builders.pkl | 816 ++++++ .../output/syntax/builders.pcf | 244 ++ stdlib/syntax.pkl | 2258 ++++++++++++++++- 3 files changed, 3304 insertions(+), 14 deletions(-) create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/syntax/builders.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/syntax/builders.pcf diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/builders.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/builders.pkl new file mode 100644 index 000000000..095c12062 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/builders.pkl @@ -0,0 +1,816 @@ +amends "../snippetTest.pkl" + +import "pkl:syntax" + +local function formatExpr(builder: syntax.ExprBuilder): String = + syntax.format(builder.build().node) + +local function formatType(builder: syntax.TypeBuilder): String = + syntax.format(builder.build().node) + +local function formatBody(builder: syntax.ObjectBodyBuilder): String = + syntax.format(builder.build().node) + +local function formatModule(builder: syntax.ModuleBuilder): String = + syntax.format(builder.build().node) + +examples { + ["int literal"] { + formatExpr(new syntax.IntLiteralBuilder { value = 42 }) == "42\n" + formatExpr(new syntax.IntLiteralBuilder { value = "0xFF" }) == "0xFF\n" + } + + ["float literal"] { + formatExpr(new syntax.FloatLiteralBuilder { value = 3.14 }) == "3.14\n" + } + + ["bool literal"] { + formatExpr(new syntax.BoolLiteralBuilder { value = true }) == "true\n" + formatExpr(new syntax.BoolLiteralBuilder { value = false }) == "false\n" + } + + ["null literal"] { + formatExpr(new syntax.NullLiteralBuilder {}) == "null\n" + } + + ["this expr"] { + formatExpr(new syntax.ThisExprBuilder {}) == "this\n" + } + + ["outer expr"] { + formatExpr(new syntax.OuterExprBuilder {}) == "outer\n" + } + + ["module expr"] { + formatExpr(new syntax.ModuleExprBuilder {}) == "module\n" + } + + ["identifier expr"] { + formatExpr(new syntax.IdentifierExprBuilder { name = "foo" }) == "foo\n" + } + + ["string literal"] { + formatExpr(new syntax.StringLiteralBuilder { value = "hello" }) == #""hello"\#n"# + } + + ["unary minus"] { + formatExpr(new syntax.UnaryMinusExprBuilder { + operand = new syntax.IntLiteralBuilder { value = 5 } + }) == "-5\n" + } + + ["logical not"] { + formatExpr(new syntax.LogicalNotExprBuilder { + operand = new syntax.IdentifierExprBuilder { name = "flag" } + }) == "!flag\n" + } + + ["non-null"] { + formatExpr(new syntax.NonNullExprBuilder { + operand = new syntax.IdentifierExprBuilder { name = "x" } + }) == "x!!\n" + } + + ["throw"] { + formatExpr(new syntax.ThrowExprBuilder { + expression = new syntax.StringLiteralBuilder { value = "oops" } + }) == #"throw("oops")\#n"# + } + + ["trace"] { + formatExpr(new syntax.TraceExprBuilder { + expression = new syntax.IdentifierExprBuilder { name = "x" } + }) == "trace(x)\n" + } + + ["parenthesized"] { + formatExpr(new syntax.ParenthesizedExprBuilder { + expression = new syntax.IntLiteralBuilder { value = 1 } + }) == "(1)\n" + } + + ["binary op"] { + formatExpr(new syntax.BinaryOpExprBuilder { + left = new syntax.IdentifierExprBuilder { name = "x" } + operator = "+" + right = new syntax.IntLiteralBuilder { value = 1 } + }) == "x + 1\n" + formatExpr(new syntax.BinaryOpExprBuilder { + left = new syntax.IdentifierExprBuilder { name = "a" } + operator = "&&" + right = new syntax.IdentifierExprBuilder { name = "b" } + }) == "a && b\n" + } + + ["is expr"] { + formatExpr(new syntax.IsExprBuilder { + operand = new syntax.IdentifierExprBuilder { name = "x" } + type = new syntax.DeclaredTypeBuilder { name = "String" } + }) == "x is String\n" + } + + ["if expr"] { + formatExpr(new syntax.IfExprBuilder { + condition = new syntax.BinaryOpExprBuilder { + left = new syntax.IdentifierExprBuilder { name = "x" } + operator = ">" + right = new syntax.IntLiteralBuilder { value = 0 } + } + thenExpr = new syntax.StringLiteralBuilder { value = "positive" } + elseExpr = new syntax.StringLiteralBuilder { value = "non-positive" } + }) == #"if (x > 0) "positive" else "non-positive"\#n"# + } + + ["import expr"] { + formatExpr(new syntax.ImportExprBuilder { uri = "pkl:json" }) == #"import("pkl:json")\#n"# + } + + ["read expr"] { + formatExpr(new syntax.ReadExprBuilder { + expression = new syntax.StringLiteralBuilder { value = "file.txt" } + }) == #"read("file.txt")\#n"# + } + + ["let expr"] { + formatExpr(new syntax.LetExprBuilder { + parameterName = "x" + bindingValue = new syntax.IntLiteralBuilder { value = 1 } + body = new syntax.IdentifierExprBuilder { name = "x" } + }) == "let (x = 1) x\n" + formatExpr(new syntax.LetExprBuilder { + parameterName = "x" + parameterType = new syntax.DeclaredTypeBuilder { name = "Int" } + bindingValue = new syntax.IntLiteralBuilder { value = 1 } + body = new syntax.IdentifierExprBuilder { name = "x" } + }) == "let (x: Int = 1) x\n" + } + + ["function literal"] { + formatExpr(new syntax.FunctionLiteralBuilder { + parameters = List(new syntax.ParameterBuilder { name = "x" }) + body = new syntax.BinaryOpExprBuilder { + left = new syntax.IdentifierExprBuilder { name = "x" } + operator = "+" + right = new syntax.IntLiteralBuilder { value = 1 } + } + }) == "(x) -> x + 1\n" + formatExpr(new syntax.FunctionLiteralBuilder { + parameters = List() + body = new syntax.IntLiteralBuilder { value = 0 } + }) == "() -> 0\n" + formatExpr(new syntax.FunctionLiteralBuilder { + parameters = List( + new syntax.ParameterBuilder { name = "x"; typeAnnotation = new syntax.DeclaredTypeBuilder { name = "Int" } }, + new syntax.ParameterBuilder { name = "y" } + ) + body = new syntax.IdentifierExprBuilder { name = "x" } + }) == "(x: Int, y) -> x\n" + } + + ["function call"] { + formatExpr(new syntax.FunctionCallBuilder { + name = "f" + arguments = List() + }) == "f()\n" + formatExpr(new syntax.FunctionCallBuilder { + name = "max" + arguments = List( + new syntax.IntLiteralBuilder { value = 1 }, + new syntax.IntLiteralBuilder { value = 2 } + ) + }) == "max(1, 2)\n" + } + + ["qualified access"] { + formatExpr(new syntax.QualifiedAccessBuilder { + receiver = new syntax.IdentifierExprBuilder { name = "obj" } + member = "field" + }) == "obj.field\n" + formatExpr(new syntax.QualifiedAccessBuilder { + receiver = new syntax.IdentifierExprBuilder { name = "obj" } + member = "field" + isNullSafe = true + }) == "obj?.field\n" + formatExpr(new syntax.QualifiedAccessBuilder { + receiver = new syntax.IdentifierExprBuilder { name = "obj" } + member = "method" + arguments = List(new syntax.IntLiteralBuilder { value = 1 }) + }) == "obj.method(1)\n" + } + + ["subscript"] { + formatExpr(new syntax.SubscriptBuilder { + receiver = new syntax.IdentifierExprBuilder { name = "list" } + index = new syntax.IntLiteralBuilder { value = 0 } + }) == "list[0]\n" + } + + ["unknown type"] { + formatType(new syntax.UnknownTypeBuilder {}) == "unknown\n" + } + + ["nothing type"] { + formatType(new syntax.NothingTypeBuilder {}) == "nothing\n" + } + + ["module type"] { + formatType(new syntax.ModuleTypeBuilder {}) == "module\n" + } + + ["declared type"] { + formatType(new syntax.DeclaredTypeBuilder { name = "String" }) == "String\n" + formatType(new syntax.DeclaredTypeBuilder { + name = "List" + typeArguments = List(new syntax.DeclaredTypeBuilder { name = "Int" }) + }) == "List\n" + formatType(new syntax.DeclaredTypeBuilder { + name = "Map" + typeArguments = List( + new syntax.DeclaredTypeBuilder { name = "String" }, + new syntax.DeclaredTypeBuilder { name = "Int" } + ) + }) == "Map\n" + } + + ["nullable type"] { + formatType(new syntax.NullableTypeBuilder { + baseType = new syntax.DeclaredTypeBuilder { name = "String" } + }) == "String?\n" + } + + ["union type"] { + formatType(new syntax.UnionTypeBuilder { + members = List( + new syntax.DeclaredTypeBuilder { name = "Int" }, + new syntax.DeclaredTypeBuilder { name = "String" } + ) + }) == "Int | String\n" + formatType(new syntax.UnionTypeBuilder { + members = List( + new syntax.DeclaredTypeBuilder { name = "Int" }, + new syntax.DeclaredTypeBuilder { name = "String" }, + new syntax.DeclaredTypeBuilder { name = "Boolean" } + ) + }) == "Int | String | Boolean\n" + } + + ["function type"] { + formatType(new syntax.FunctionTypeBuilder { + parameterTypes = List(new syntax.DeclaredTypeBuilder { name = "Int" }) + returnType = new syntax.DeclaredTypeBuilder { name = "String" } + }) == "(Int) -> String\n" + formatType(new syntax.FunctionTypeBuilder { + parameterTypes = List() + returnType = new syntax.DeclaredTypeBuilder { name = "Int" } + }) == "() -> Int\n" + formatType(new syntax.FunctionTypeBuilder { + parameterTypes = List( + new syntax.DeclaredTypeBuilder { name = "Int" }, + new syntax.DeclaredTypeBuilder { name = "Int" } + ) + returnType = new syntax.DeclaredTypeBuilder { name = "Int" } + }) == "(Int, Int) -> Int\n" + } + + ["constrained type"] { + formatType(new syntax.ConstrainedTypeBuilder { + baseType = new syntax.DeclaredTypeBuilder { name = "Int" } + constraints = List( + new syntax.BinaryOpExprBuilder { + left = new syntax.IdentifierExprBuilder { name = "this" } + operator = ">" + right = new syntax.IntLiteralBuilder { value = 0 } + } + ) + }) == "Int(this > 0)\n" + } + + ["parenthesized type"] { + formatType(new syntax.ParenthesizedTypeBuilder { + type = new syntax.UnionTypeBuilder { + members = List( + new syntax.DeclaredTypeBuilder { name = "Int" }, + new syntax.DeclaredTypeBuilder { name = "String" } + ) + } + }) == "(Int | String)\n" + } + + ["string constant type"] { + formatType(new syntax.StringConstantTypeBuilder { value = "foo" }) == #""foo"\#n"# + } + + ["empty body"] { + formatBody(new syntax.ObjectBodyBuilder {}) == "{}\n" + } + + ["object element"] { + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectElementBuilder { + expression = new syntax.IntLiteralBuilder { value = 1 } + } + ) + }) == "{ 1 }\n" + } + + ["object spread"] { + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectSpreadBuilder { + expression = new syntax.IdentifierExprBuilder { name = "other" } + } + ) + }) == "{ ...other }\n" + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectSpreadBuilder { + expression = new syntax.IdentifierExprBuilder { name = "maybe" } + isNullable = true + } + ) + }) == "{ ...?maybe }\n" + } + + ["object property"] { + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectPropertyBuilder { + name = "x" + value = new syntax.IntLiteralBuilder { value = 1 } + } + ) + }) == "{ x = 1 }\n" + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectPropertyBuilder { + name = "x" + typeAnnotation = new syntax.DeclaredTypeBuilder { name = "Int" } + value = new syntax.IntLiteralBuilder { value = 1 } + } + ) + }) == "{ x: Int = 1 }\n" + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectPropertyBuilder { + modifiers = List("hidden") + name = "x" + value = new syntax.IntLiteralBuilder { value = 1 } + } + ) + }) == "{ hidden x = 1 }\n" + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectPropertyBuilder { + name = "x" + objectBodies = List( + new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectPropertyBuilder { + name = "y" + value = new syntax.IntLiteralBuilder { value = 2 } + } + ) + } + ) + } + ) + }) == "{ x { y = 2 } }\n" + } + + ["object method"] { + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectMethodBuilder { + name = "f" + parameters = List(new syntax.ParameterBuilder { name = "x" }) + body = new syntax.IdentifierExprBuilder { name = "x" } + } + ) + }) == "{ function f(x) = x }\n" + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectMethodBuilder { + name = "f" + parameters = List(new syntax.ParameterBuilder { name = "x"; typeAnnotation = new syntax.DeclaredTypeBuilder { name = "Int" } }) + returnType = new syntax.DeclaredTypeBuilder { name = "Int" } + body = new syntax.IdentifierExprBuilder { name = "x" } + } + ) + }) == "{ function f(x: Int): Int = x }\n" + } + + ["object entry"] { + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectEntryBuilder { + key = new syntax.StringLiteralBuilder { value = "k" } + value = new syntax.IntLiteralBuilder { value = 1 } + } + ) + }) == "{ [\"k\"] = 1 }\n" + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectEntryBuilder { + key = new syntax.StringLiteralBuilder { value = "k" } + objectBodies = List( + new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectPropertyBuilder { + name = "x" + value = new syntax.IntLiteralBuilder { value = 1 } + } + ) + } + ) + } + ) + }) == "{ [\"k\"] { x = 1 } }\n" + } + + ["member predicate"] { + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.MemberPredicateBuilder { + condition = new syntax.IdentifierExprBuilder { name = "cond" } + value = new syntax.IntLiteralBuilder { value = 1 } + } + ) + }) == "{ [[cond]] = 1 }\n" + } + + ["for generator"] { + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ForGeneratorBuilder { + valueParameter = new syntax.ParameterBuilder { name = "x" } + iterable = new syntax.IdentifierExprBuilder { name = "items" } + body = new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectElementBuilder { + expression = new syntax.IdentifierExprBuilder { name = "x" } + } + ) + } + } + ) + }) == "{ for (x in items) { x } }\n" + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.ForGeneratorBuilder { + keyParameter = new syntax.ParameterBuilder { name = "k" } + valueParameter = new syntax.ParameterBuilder { name = "v" } + iterable = new syntax.IdentifierExprBuilder { name = "items" } + body = new syntax.ObjectBodyBuilder {} + } + ) + }) == "{ for (k, v in items) {} }\n" + } + + ["when generator"] { + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.WhenGeneratorBuilder { + condition = new syntax.IdentifierExprBuilder { name = "cond" } + thenBody = new syntax.ObjectBodyBuilder {} + } + ) + }) == "{ when (cond) {} }\n" + formatBody(new syntax.ObjectBodyBuilder { + members = List( + new syntax.WhenGeneratorBuilder { + condition = new syntax.IdentifierExprBuilder { name = "cond" } + thenBody = new syntax.ObjectBodyBuilder {} + elseBody = new syntax.ObjectBodyBuilder {} + } + ) + }) == "{ when (cond) {} else {} }\n" + } + + ["body with parameters"] { + formatBody(new syntax.ObjectBodyBuilder { + parameters = List( + new syntax.ParameterBuilder { name = "x" }, + new syntax.ParameterBuilder { name = "y" } + ) + members = List( + new syntax.ObjectElementBuilder { + expression = new syntax.IdentifierExprBuilder { name = "x" } + } + ) + }) == "{ x, y -> x }\n" + } + + ["new expr"] { + formatExpr(new syntax.NewExprBuilder { + body = new syntax.ObjectBodyBuilder {} + }) == "new {}\n" + formatExpr(new syntax.NewExprBuilder { + type = new syntax.DeclaredTypeBuilder { name = "Foo" } + body = new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectPropertyBuilder { + name = "x" + value = new syntax.IntLiteralBuilder { value = 1 } + } + ) + } + }) == "new Foo { x = 1 }\n" + } + + ["amends expr"] { + formatExpr(new syntax.AmendsExprBuilder { + parentExpr = new syntax.ParenthesizedExprBuilder { + expression = new syntax.IdentifierExprBuilder { name = "base" } + } + body = new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectPropertyBuilder { + name = "x" + value = new syntax.IntLiteralBuilder { value = 1 } + } + ) + } + }) == "(base) { x = 1 }\n" + } + + ["doc comment"] { + syntax.format((new syntax.DocCommentBuilder { + lines = List(" line 1", " line 2") + }).build().node) == "/// line 1\n/// line 2\n" + } + + ["annotation"] { + syntax.format((new syntax.AnnotationBuilder { + type = new syntax.DeclaredTypeBuilder { name = "Deprecated" } + }).build().node) == "@Deprecated\n" + syntax.format((new syntax.AnnotationBuilder { + type = new syntax.DeclaredTypeBuilder { name = "Deprecated" } + body = new syntax.ObjectBodyBuilder { + members = List( + new syntax.ObjectPropertyBuilder { + name = "message" + value = new syntax.StringLiteralBuilder { value = "old" } + } + ) + } + }).build().node) == #"@Deprecated { message = "old" }\#n"# + } + + ["import"] { + syntax.format((new syntax.ImportBuilder { uri = "pkl:json" }).build().node) + == #"import "pkl:json"\#n"# + syntax.format((new syntax.ImportBuilder { uri = "pkl:json"; isGlob = true }).build().node) + == #"import* "pkl:json"\#n"# + syntax.format((new syntax.ImportBuilder { uri = "pkl:json"; alias = "j" }).build().node) + == #"import "pkl:json" as j\#n"# + } + + ["class property (top-level)"] { + syntax.format((new syntax.ClassPropertyBuilder { + name = "x" + value = new syntax.IntLiteralBuilder { value = 1 } + }).build().node) == "x = 1\n" + syntax.format((new syntax.ClassPropertyBuilder { + modifiers = List("hidden") + name = "x" + typeAnnotation = new syntax.DeclaredTypeBuilder { name = "Int" } + value = new syntax.IntLiteralBuilder { value = 1 } + }).build().node) == "hidden x: Int = 1\n" + } + + ["class method (top-level)"] { + syntax.format((new syntax.ClassMethodBuilder { + name = "f" + parameters = List(new syntax.ParameterBuilder { name = "x" }) + body = new syntax.IdentifierExprBuilder { name = "x" } + }).build().node) == "function f(x) = x\n" + syntax.format((new syntax.ClassMethodBuilder { + modifiers = List("abstract") + name = "f" + parameters = List() + returnType = new syntax.DeclaredTypeBuilder { name = "Int" } + }).build().node) == "abstract function f(): Int\n" + } + + ["typealias"] { + syntax.format((new syntax.TypeAliasBuilder { + name = "MyInt" + type = new syntax.DeclaredTypeBuilder { name = "Int" } + }).build().node) == "typealias MyInt = Int\n" + syntax.format((new syntax.TypeAliasBuilder { + name = "Pair" + typeParameters = List( + new syntax.TypeParameterBuilder { name = "A" }, + new syntax.TypeParameterBuilder { name = "B" } + ) + type = new syntax.DeclaredTypeBuilder { name = "Mapping" } + }).build().node) == "typealias Pair = Mapping\n" + } + + ["class"] { + syntax.format((new syntax.ClassBuilder { + name = "Foo" + }).build().node) == "class Foo\n" + syntax.format((new syntax.ClassBuilder { + modifiers = List("open") + name = "Foo" + extendsType = new syntax.DeclaredTypeBuilder { name = "Bar" } + body = new syntax.ClassBodyBuilder { + properties = List( + new syntax.ClassPropertyBuilder { + name = "x" + value = new syntax.IntLiteralBuilder { value = 1 } + } + ) + } + }).build().node) == "open class Foo extends Bar {\n x = 1\n}\n" + } + + ["module declaration"] { + syntax.format((new syntax.ModuleDeclarationBuilder { + name = "my.config" + }).build().node) == "module my.config\n" + syntax.format((new syntax.ModuleDeclarationBuilder { + amendsUri = "pkl:base" + }).build().node) == #"amends "pkl:base"\#n"# + syntax.format((new syntax.ModuleDeclarationBuilder { + modifiers = List("open") + name = "my.config" + extendsUri = "pkl:base" + }).build().node) == #""" + open module my.config + + extends "pkl:base" + + """# + } + + ["module"] { + formatModule(new syntax.ModuleBuilder { + declaration = new syntax.ModuleDeclarationBuilder { name = "my.config" } + imports = List(new syntax.ImportBuilder { uri = "pkl:json" }) + properties = List( + new syntax.ClassPropertyBuilder { + name = "host" + typeAnnotation = new syntax.DeclaredTypeBuilder { name = "String" } + value = new syntax.StringLiteralBuilder { value = "localhost" } + }, + new syntax.ClassPropertyBuilder { + name = "port" + typeAnnotation = new syntax.DeclaredTypeBuilder { name = "Int" } + value = new syntax.IntLiteralBuilder { value = 8080 } + } + ) + }) == #""" + module my.config + import "pkl:json" + host: String = "localhost" + port: Int = 8080 + + """# + } + + ["round-trip: simple property"] { + let (parsed = syntax.parse("x = 1") as syntax.ModuleNode) + let (rebuilt = parsed.toBuilder().build()) + syntax.format(rebuilt.node) == "x = 1\n" + } + + ["round-trip: typed property"] { + let (parsed = syntax.parse("x: Int = 1") as syntax.ModuleNode) + let (rebuilt = parsed.toBuilder().build()) + syntax.format(rebuilt.node) == "x: Int = 1\n" + } + + ["round-trip: if expression"] { + let (parsed = syntax.parse(#"x = if (a > 0) "yes" else "no""#) as syntax.ModuleNode) + let (rebuilt = parsed.toBuilder().build()) + syntax.format(rebuilt.node) == #"x = if (a > 0) "yes" else "no"\#n"# + } + + ["round-trip: function call"] { + let (parsed = syntax.parse("x = max(1, 2)") as syntax.ModuleNode) + let (rebuilt = parsed.toBuilder().build()) + syntax.format(rebuilt.node) == "x = max(1, 2)\n" + } + + ["round-trip: qualified access"] { + let (parsed = syntax.parse("x = obj.field") as syntax.ModuleNode) + let (rebuilt = parsed.toBuilder().build()) + syntax.format(rebuilt.node) == "x = obj.field\n" + } + + ["round-trip: union type"] { + let (parsed = syntax.parse("typealias T = Int|String") as syntax.ModuleNode) + let (rebuilt = parsed.toBuilder().build()) + syntax.format(rebuilt.node) == "typealias T = Int | String\n" + } + + ["round-trip: class with body"] { + let (parsed = syntax.parse(""" + open class Foo extends Bar { + x: Int = 1 + function f(y) = y + } + """) as syntax.ModuleNode) + let (rebuilt = parsed.toBuilder().build()) + syntax.format(rebuilt.node) == #""" + open class Foo extends Bar { + x: Int = 1 + + function f(y) = y + } + + """# + } + + ["round-trip: amend modifies value"] { + let (parsed = syntax.parse("x = 1") as syntax.ModuleNode) + let (modified = (parsed.properties.first.toBuilder()) { + value = new syntax.IntLiteralBuilder { value = 99 } + }) + syntax.format(modified.build().node) == "x = 99\n" + } + + ["multi-line string"] { + formatExpr(new syntax.MultiLineStringLiteralBuilder { + parts = List( + new syntax.StringNewlineBuilder {}, + new syntax.StringCharsBuilder { value = "hello" }, + new syntax.StringNewlineBuilder {}, + new syntax.StringCharsBuilder { value = "world" }, + new syntax.StringNewlineBuilder {} + ) + }) == #""" + """ + hello + world + """ + + """# + } + + ["multi-line string with interpolation"] { + formatExpr(new syntax.MultiLineStringLiteralBuilder { + parts = List( + new syntax.StringNewlineBuilder {}, + new syntax.StringCharsBuilder { value = "hi " }, + new syntax.StringInterpolationBuilder { + expression = new syntax.IdentifierExprBuilder { name = "name" } + }, + new syntax.StringNewlineBuilder {} + ) + }) == #""" + """ + hi \(name) + """ + + """# + } + + ["round-trip: multi-line string with interpolation"] { + let (parsed = syntax.parse(#""" + x = """ + hello \(name) + world + """ + """#) as syntax.ModuleNode) + let (rebuilt = parsed.toBuilder().build()) + syntax.format(rebuilt.node) == #""" + x = + """ + hello \(name) + world + """ + + """# + } + + ["string literal with interpolation"] { + formatExpr(new syntax.StringLiteralBuilder { + parts = List( + new syntax.StringCharsBuilder { value = "hi " }, + new syntax.StringInterpolationBuilder { + expression = new syntax.IdentifierExprBuilder { name = "name" } + } + ) + }) == #""hi \(name)"\#n"# + } + + ["string literal with escape"] { + formatExpr(new syntax.StringLiteralBuilder { + parts = List( + new syntax.StringCharsBuilder { value = "line1" }, + new syntax.StringEscapeBuilder { value = "\\n" }, + new syntax.StringCharsBuilder { value = "line2" } + ) + }) == #""line1\nline2"\#n"# + } + + ["round-trip: single-line string with interpolation"] { + let (parsed = syntax.parse(#"x = "hi \(name)""#) as syntax.ModuleNode) + let (rebuilt = parsed.toBuilder().build()) + syntax.format(rebuilt.node) == #"x = "hi \(name)"\#n"# + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/builders.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/builders.pcf new file mode 100644 index 000000000..7c2f4d818 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/syntax/builders.pcf @@ -0,0 +1,244 @@ +examples { + ["int literal"] { + true + true + } + ["float literal"] { + true + } + ["bool literal"] { + true + true + } + ["null literal"] { + true + } + ["this expr"] { + true + } + ["outer expr"] { + true + } + ["module expr"] { + true + } + ["identifier expr"] { + true + } + ["string literal"] { + true + } + ["unary minus"] { + true + } + ["logical not"] { + true + } + ["non-null"] { + true + } + ["throw"] { + true + } + ["trace"] { + true + } + ["parenthesized"] { + true + } + ["binary op"] { + true + true + } + ["is expr"] { + true + } + ["if expr"] { + true + } + ["import expr"] { + true + } + ["read expr"] { + true + } + ["let expr"] { + true + true + } + ["function literal"] { + true + true + true + } + ["function call"] { + true + true + } + ["qualified access"] { + true + true + true + } + ["subscript"] { + true + } + ["unknown type"] { + true + } + ["nothing type"] { + true + } + ["module type"] { + true + } + ["declared type"] { + true + true + true + } + ["nullable type"] { + true + } + ["union type"] { + true + true + } + ["function type"] { + true + true + true + } + ["constrained type"] { + true + } + ["parenthesized type"] { + true + } + ["string constant type"] { + true + } + ["empty body"] { + true + } + ["object element"] { + true + } + ["object spread"] { + true + true + } + ["object property"] { + true + true + true + true + } + ["object method"] { + true + true + } + ["object entry"] { + true + true + } + ["member predicate"] { + true + } + ["for generator"] { + true + true + } + ["when generator"] { + true + true + } + ["body with parameters"] { + true + } + ["new expr"] { + true + true + } + ["amends expr"] { + true + } + ["doc comment"] { + true + } + ["annotation"] { + true + true + } + ["import"] { + true + true + true + } + ["class property (top-level)"] { + true + true + } + ["class method (top-level)"] { + true + true + } + ["typealias"] { + true + true + } + ["class"] { + true + true + } + ["module declaration"] { + true + true + true + } + ["module"] { + true + } + ["round-trip: simple property"] { + true + } + ["round-trip: typed property"] { + true + } + ["round-trip: if expression"] { + true + } + ["round-trip: function call"] { + true + } + ["round-trip: qualified access"] { + true + } + ["round-trip: union type"] { + true + } + ["round-trip: class with body"] { + true + } + ["round-trip: amend modifies value"] { + true + } + ["multi-line string"] { + true + } + ["multi-line string with interpolation"] { + true + } + ["round-trip: multi-line string with interpolation"] { + true + } + ["string literal with interpolation"] { + true + } + ["string literal with escape"] { + true + } + ["round-trip: single-line string with interpolation"] { + true + } +} diff --git a/stdlib/syntax.pkl b/stdlib/syntax.pkl index 62c252b41..88c75f169 100644 --- a/stdlib/syntax.pkl +++ b/stdlib/syntax.pkl @@ -15,6 +15,7 @@ //===----------------------------------------------------------------------===// /// Utilities for managing Pkl source code +@ModuleInfo { minPklVersion = "0.32.0" } module pkl.syntax /// Parse the string as a Pkl module, returning either a typed AST node or an error. @@ -412,13 +413,23 @@ abstract class SyntaxNode { } /// Base class for expression nodes. -abstract class Expr extends SyntaxNode {} +abstract class Expr extends SyntaxNode { + /// Convert this expression node to its builder. + function toBuilder(): ExprBuilder = throw("toBuilder() not implemented for \(this.getClass())") +} /// Base class for type nodes. -abstract class TypeNode extends SyntaxNode {} +abstract class TypeNode extends SyntaxNode { + /// Convert this type node to its builder. + function toBuilder(): TypeBuilder = throw("toBuilder() not implemented for \(this.getClass())") +} /// Base class for object member nodes. -abstract class ObjectMemberNode extends SyntaxNode {} +abstract class ObjectMemberNode extends SyntaxNode { + /// Convert this object member node to its builder. + function toBuilder(): ObjectMemberBuilder = + throw("toBuilder() not implemented for \(this.getClass())") +} /// The top-level module node. class ModuleNode extends SyntaxNode { @@ -452,6 +463,17 @@ class ModuleNode extends SyntaxNode { /// All top-level methods in this module. methods: List = findChildren(node, "class_method").map((n) -> new ClassMethodNode { node = n }) + + function toBuilder(): ModuleBuilder = + let (self = this) + new ModuleBuilder { + declaration = self.declaration?.toBuilder() + imports = self.imports.map((i) -> i.toBuilder()) + classes = self.classes.map((c) -> c.toBuilder()) + typeAliases = self.typeAliases.map((t) -> t.toBuilder()) + properties = self.properties.map((p) -> p.toBuilder()) + methods = self.methods.map((m) -> m.toBuilder()) + } } /// A module declaration (including doc comment, annotations, modifiers, name, amends/extends). @@ -507,6 +529,21 @@ class ModuleDeclarationNode extends SyntaxNode { null else new ExtendsClauseNode { node = n } + + function toBuilder(): ModuleDeclarationBuilder = + let (self = this) + new ModuleDeclarationBuilder { + docComment = self.docComment?.toBuilder() + annotations = self.annotations.map((a) -> a.toBuilder()) + modifiers = self.modifiers?.modifiers ?? List() + name = + if (self.name == null) + null + else + self.name.identifiers.map((i) -> i.value).join(".") + amendsUri = self.amendsClause?.uri + extendsUri = self.extendsClause?.uri + } } /// An `amends "..."` clause. @@ -540,6 +577,14 @@ class ImportNode extends SyntaxNode { null else new IdentifierNode { node = id } + + function toBuilder(): ImportBuilder = + let (self = this) + new ImportBuilder { + uri = self.uri + isGlob = self.isGlob + alias = self.alias?.value + } } /// A class declaration. @@ -598,6 +643,18 @@ class ClassNode extends SyntaxNode { null else new ClassBodyNode { node = n } + + function toBuilder(): ClassBuilder = + let (self = this) + new ClassBuilder { + docComment = self.docComment?.toBuilder() + annotations = self.annotations.map((a) -> a.toBuilder()) + modifiers = self.modifiers?.modifiers ?? List() + name = self.name.value + typeParameters = self.typeParameterList?.typeParameters?.map((t) -> t.toBuilder()) ?? List() + extendsType = self.extendsClause?.toBuilder() + body = self.body?.toBuilder() + } } /// A typealias declaration. @@ -642,6 +699,17 @@ class TypeAliasNode extends SyntaxNode { let (body = findChild(node, "typealias_body")) let (t = findTypeChild(body!!)) wrapTypeNode(t!!) + + function toBuilder(): TypeAliasBuilder = + let (self = this) + new TypeAliasBuilder { + docComment = self.docComment?.toBuilder() + annotations = self.annotations.map((a) -> a.toBuilder()) + modifiers = self.modifiers?.modifiers ?? List() + name = self.name.value + typeParameters = self.typeParameterList?.typeParameters?.map((t) -> t.toBuilder()) ?? List() + type = self.type.toBuilder() + } } /// A class body delimited by braces. @@ -661,6 +729,13 @@ class ClassBodyNode extends SyntaxNode { List() else findChildren(elements, "class_method").map((n) -> new ClassMethodNode { node = n }) + + function toBuilder(): ClassBodyBuilder = + let (self = this) + new ClassBodyBuilder { + properties = self.properties.map((p) -> p.toBuilder()) + methods = self.methods.map((m) -> m.toBuilder()) + } } /// A class property declaration. @@ -716,6 +791,18 @@ class ClassPropertyNode extends SyntaxNode { /// Object bodies for amending (from `{ ... }` blocks). objectBodies: List = findChildren(node, "object_body").map((n) -> new ObjectBodyNode { node = n }) + + function toBuilder(): ClassPropertyBuilder = + let (self = this) + new ClassPropertyBuilder { + docComment = self.docComment?.toBuilder() + annotations = self.annotations.map((a) -> a.toBuilder()) + modifiers = self.modifiers?.modifiers ?? List() + name = self.name.value + typeAnnotation = self.typeAnnotation?.type?.toBuilder() + value = self.value?.toBuilder() + objectBodies = self.objectBodies.map((b) -> b.toBuilder()) + } } /// A class method declaration. @@ -779,6 +866,19 @@ class ClassMethodNode extends SyntaxNode { null else wrapExpr(e) + + function toBuilder(): ClassMethodBuilder = + let (self = this) + new ClassMethodBuilder { + docComment = self.docComment?.toBuilder() + annotations = self.annotations.map((a) -> a.toBuilder()) + modifiers = self.modifiers?.modifiers ?? List() + name = self.name.value + typeParameters = self.typeParameterList?.typeParameters?.map((t) -> t.toBuilder()) ?? List() + parameters = self.parameterList.parameters.map((p) -> p.toBuilder()) + returnType = self.returnType?.type?.toBuilder() + body = self.body?.toBuilder() + } } /// An object body delimited by braces. @@ -829,6 +929,13 @@ class ObjectBodyNode extends SyntaxNode { List() else findChildren(memberList, "object_entry").map((n) -> new ObjectEntryNode { node = n }) + + function toBuilder(): ObjectBodyBuilder = + let (self = this) + new ObjectBodyBuilder { + parameters = self.parameters.map((p) -> p.toBuilder()) + members = self.members.map((m) -> m.toBuilder()) + } } /// An object property declaration. @@ -872,6 +979,16 @@ class ObjectPropertyNode extends ObjectMemberNode { /// Object bodies for amending. objectBodies: List = findChildren(node, "object_body").map((n) -> new ObjectBodyNode { node = n }) + + function toBuilder(): ObjectPropertyBuilder = + let (self = this) + new ObjectPropertyBuilder { + modifiers = self.modifiers?.modifiers ?? List() + name = self.name.value + typeAnnotation = self.typeAnnotation?.type?.toBuilder() + value = self.value?.toBuilder() + objectBodies = self.objectBodies.map((b) -> b.toBuilder()) + } } /// An object method declaration. @@ -923,12 +1040,27 @@ class ObjectMethodNode extends ObjectMemberNode { null else wrapExpr(e) + + function toBuilder(): ObjectMethodBuilder = + let (self = this) + new ObjectMethodBuilder { + modifiers = self.modifiers?.modifiers ?? List() + name = self.name.value + typeParameters = self.typeParameterList?.typeParameters?.map((t) -> t.toBuilder()) ?? List() + parameters = self.parameterList.parameters.map((p) -> p.toBuilder()) + returnType = self.returnType?.type?.toBuilder() + body = self.body!!.toBuilder() + } } /// An object element (a positional expression in an object body). class ObjectElementNode extends ObjectMemberNode { /// The expression value. expression: Expr = wrapExpr(findExprChild(node)!!) + + function toBuilder(): ObjectElementBuilder = + let (self = this) + new ObjectElementBuilder { expression = self.expression.toBuilder() } } /// An object entry (`[key] = value` or `[key] { ... }`). @@ -949,6 +1081,14 @@ class ObjectEntryNode extends ObjectMemberNode { /// Object bodies for amending. objectBodies: List = findChildren(node, "object_body").map((n) -> new ObjectBodyNode { node = n }) + + function toBuilder(): ObjectEntryBuilder = + let (self = this) + new ObjectEntryBuilder { + key = self.key.toBuilder() + value = self.value?.toBuilder() + objectBodies = self.objectBodies.map((b) -> b.toBuilder()) + } } /// An object spread (`...expr` or `...?expr`). @@ -958,6 +1098,13 @@ class ObjectSpreadNode extends ObjectMemberNode { /// The spread expression. expression: Expr = wrapExpr(findExprChild(node)!!) + + function toBuilder(): ObjectSpreadBuilder = + let (self = this) + new ObjectSpreadBuilder { + expression = self.expression.toBuilder() + isNullable = self.isNullable + } } /// A member predicate (`[[condition]] = value` or `[[condition]] { ... }`). @@ -977,6 +1124,14 @@ class MemberPredicateNode extends ObjectMemberNode { /// Object bodies for amending. objectBodies: List = findChildren(node, "object_body").map((n) -> new ObjectBodyNode { node = n }) + + function toBuilder(): MemberPredicateBuilder = + let (self = this) + new MemberPredicateBuilder { + condition = self.condition.toBuilder() + value = self.value?.toBuilder() + objectBodies = self.objectBodies.map((b) -> b.toBuilder()) + } } /// A `for (param in iterable) { ... }` generator. @@ -1005,6 +1160,15 @@ class ForGeneratorNode extends ObjectMemberNode { body: ObjectBodyNode = let (n = findChild(node, "object_body")) new ObjectBodyNode { node = n!! } + + function toBuilder(): ForGeneratorBuilder = + let (self = this) + new ForGeneratorBuilder { + keyParameter = self.keyParameter?.toBuilder() + valueParameter = self.valueParameter.toBuilder() + iterable = self.iterable.toBuilder() + body = self.body.toBuilder() + } } /// A `when (condition) { ... }` generator. @@ -1026,48 +1190,88 @@ class WhenGeneratorNode extends ObjectMemberNode { null else new ObjectBodyNode { node = bodyNodes[1] } + + function toBuilder(): WhenGeneratorBuilder = + let (self = this) + new WhenGeneratorBuilder { + condition = self.condition.toBuilder() + thenBody = self.thenBody.toBuilder() + elseBody = self.elseBody?.toBuilder() + } } /// The `this` expression. -class ThisExprNode extends Expr {} +class ThisExprNode extends Expr { + function toBuilder(): ThisExprBuilder = new ThisExprBuilder {} +} /// The `outer` expression. -class OuterExprNode extends Expr {} +class OuterExprNode extends Expr { + function toBuilder(): OuterExprBuilder = new OuterExprBuilder {} +} /// The `module` expression. -class ModuleExprNode extends Expr {} +class ModuleExprNode extends Expr { + function toBuilder(): ModuleExprBuilder = new ModuleExprBuilder {} +} /// A `null` literal expression. -class NullLiteralExprNode extends Expr {} +class NullLiteralExprNode extends Expr { + function toBuilder(): NullLiteralBuilder = new NullLiteralBuilder {} +} /// A boolean literal expression (`true` or `false`). class BoolLiteralExprNode extends Expr { /// The boolean value. value: Boolean = node.text == "true" + + function toBuilder(): BoolLiteralBuilder = + let (self = this) + new BoolLiteralBuilder { value = self.value } } /// An integer literal expression. class IntLiteralExprNode extends Expr { /// The raw text of the integer literal. text: String = node.text ?? "" + + function toBuilder(): IntLiteralBuilder = + let (self = this) + new IntLiteralBuilder { value = self.text } } /// A float literal expression. class FloatLiteralExprNode extends Expr { /// The raw text of the float literal. text: String = node.text ?? "" + + function toBuilder(): FloatLiteralBuilder = + let (self = this) + new FloatLiteralBuilder { value = self.text } } /// A single-line string literal expression. class SingleLineStringLiteralExprNode extends Expr { /// The string parts (chars, escapes, interpolations). parts: List = buildStringParts(children) + + function toBuilder(): StringLiteralBuilder = + let (self = this) + new StringLiteralBuilder { + parts = self.parts.map((p) -> p.toBuilder()).toList() + } } /// A multi-line string literal expression. class MultiLineStringLiteralExprNode extends Expr { /// The string parts (chars, escapes, interpolations). parts: List = buildStringParts(children) + + function toBuilder(): MultiLineStringLiteralBuilder = + let (self = this) + new MultiLineStringLiteralBuilder { + parts = self.parts.map((p) -> p.toBuilder()).toList() + } } /// An unqualified access expression (`name` or `name(args)`). @@ -1084,6 +1288,16 @@ class UnqualifiedAccessExprNode extends Expr { null else new ArgumentListNode { node = n } + + function toBuilder(): ExprBuilder = + let (self = this) + if (self.argumentList == null) + new IdentifierExprBuilder { name = self.identifier.value } + else + new FunctionCallBuilder { + name = self.identifier.value + arguments = self.argumentList.arguments.map((a) -> a.toBuilder()).toList() + } } /// A qualified access expression (`receiver.member` or `receiver?.member`). @@ -1100,6 +1314,15 @@ class QualifiedAccessExprNode extends Expr { member: UnqualifiedAccessExprNode = let (n = findChildren(node, "unqualified_access_expr").last) new UnqualifiedAccessExprNode { node = n } + + function toBuilder(): QualifiedAccessBuilder = + let (self = this) + new QualifiedAccessBuilder { + receiver = self.receiver.toBuilder() + member = self.member.identifier.value + isNullSafe = self.isNullSafe + arguments = self.member.argumentList?.arguments?.map((a) -> a.toBuilder()) + } } /// A subscript expression (`receiver[index]`). @@ -1113,6 +1336,13 @@ class SubscriptExprNode extends Expr { index: Expr = let (exprs = findExprChildren(node)) wrapExpr(exprs[1]) + + function toBuilder(): SubscriptBuilder = + let (self = this) + new SubscriptBuilder { + receiver = self.receiver.toBuilder() + index = self.index.toBuilder() + } } /// A `super.member` access expression. @@ -1143,6 +1373,14 @@ class IfExprNode extends Expr { let (elseNode = findChild(node, "if_else_expr")) let (e = findExprChild(elseNode!!)) wrapExpr(e!!) + + function toBuilder(): IfExprBuilder = + let (self = this) + new IfExprBuilder { + condition = self.condition.toBuilder() + thenExpr = self.thenExpr.toBuilder() + elseExpr = self.elseExpr.toBuilder() + } } /// A `let (param = value) body` expression. @@ -1162,18 +1400,35 @@ class LetExprNode extends Expr { /// The body expression. bodyExpr: Expr = wrapExpr(findExprChild(node)!!) + + function toBuilder(): LetExprBuilder = + let (self = this) + new LetExprBuilder { + parameterName = if (self.parameter.isWildcard) "_" else self.parameter.identifier!!.value + parameterType = self.parameter.typeAnnotation?.type?.toBuilder() + bindingValue = self.bindingValue.toBuilder() + body = self.bodyExpr.toBuilder() + } } /// A `throw(expr)` expression. class ThrowExprNode extends Expr { /// The expression being thrown. expression: Expr = wrapExpr(findExprChild(node)!!) + + function toBuilder(): ThrowExprBuilder = + let (self = this) + new ThrowExprBuilder { expression = self.expression.toBuilder() } } /// A `trace(expr)` expression. class TraceExprNode extends Expr { /// The expression being traced. expression: Expr = wrapExpr(findExprChild(node)!!) + + function toBuilder(): TraceExprBuilder = + let (self = this) + new TraceExprBuilder { expression = self.expression.toBuilder() } } /// An `import("uri")` or `import*("uri")` expression. @@ -1183,15 +1438,30 @@ class ImportExprNode extends Expr { /// The import URI string. uri: String = getStringChars(node) + + function toBuilder(): ImportExprBuilder = + let (self = this) + new ImportExprBuilder { + uri = self.uri + isGlob = self.isGlob + } } /// A `read(expr)`, `read*(expr)`, or `read?(expr)` expression. class ReadExprNode extends Expr { /// The keyword used (`"read"`, `"read?"`, or `"read*"`). - keyword: String = terminals.firstOrNull?.text ?? "read" + keyword: "read" | "read?" | "read*" = + (terminals.firstOrNull?.text ?? "read") as "read" | "read?" | "read*" // The expression to be read expr: Expr = wrapExpr(findExprChild(node)!!) + + function toBuilder(): ReadExprBuilder = + let (self = this) + new ReadExprBuilder { + expression = self.expr.toBuilder() + keyword = self.keyword + } } /// A `new Type { ... }` expression. @@ -1210,6 +1480,13 @@ class NewExprNode extends Expr { body: ObjectBodyNode = let (n = findChild(node, "object_body")) new ObjectBodyNode { node = n!! } + + function toBuilder(): NewExprBuilder = + let (self = this) + new NewExprBuilder { + type = self.type?.toBuilder() + body = self.body.toBuilder() + } } /// An `(expr) { ... }` amends expression. @@ -1223,6 +1500,13 @@ class AmendsExprNode extends Expr { body: ObjectBodyNode = let (n = findChild(node, "object_body")) new ObjectBodyNode { node = n!! } + + function toBuilder(): AmendsExprBuilder = + let (self = this) + new AmendsExprBuilder { + parentExpr = self.parentExpr.toBuilder() + body = self.body.toBuilder() + } } /// A binary operator expression (`left op right`). @@ -1249,24 +1533,55 @@ class BinaryOpExprNode extends Expr { null else wrapTypeNode(t) + + function toBuilder(): ExprBuilder = + let (self = this) + if (self.operator == "is") + new IsExprBuilder { + operand = self.leftExpr.toBuilder() + type = self.rightType!!.toBuilder() + } + else if (self.operator == "as") + new AsExprBuilder { + operand = self.leftExpr.toBuilder() + type = self.rightType!!.toBuilder() + } + else + new BinaryOpExprBuilder { + left = self.leftExpr.toBuilder() + operator = self.operator + right = self.rightExpr!!.toBuilder() + } } /// A unary minus expression (`-expr`). class UnaryMinusExprNode extends Expr { /// The operand expression. operand: Expr = wrapExpr(findExprChild(node)!!) + + function toBuilder(): UnaryMinusExprBuilder = + let (self = this) + new UnaryMinusExprBuilder { operand = self.operand.toBuilder() } } /// A logical not expression (`!expr`). class LogicalNotExprNode extends Expr { /// The operand expression. operand: Expr = wrapExpr(findExprChild(node)!!) + + function toBuilder(): LogicalNotExprBuilder = + let (self = this) + new LogicalNotExprBuilder { operand = self.operand.toBuilder() } } /// A non-null assertion expression (`expr!!`). class NonNullExprNode extends Expr { /// The operand expression. operand: Expr = wrapExpr(findExprChild(node)!!) + + function toBuilder(): NonNullExprBuilder = + let (self = this) + new NonNullExprBuilder { operand = self.operand.toBuilder() } } /// A function literal expression (`(params) -> body`). @@ -1281,6 +1596,13 @@ class FunctionLiteralExprNode extends Expr { let (bodyNode = findChild(node, "function_literal_body")) let (e = findExprChild(bodyNode!!)) wrapExpr(e!!) + + function toBuilder(): FunctionLiteralBuilder = + let (self = this) + new FunctionLiteralBuilder { + parameters = self.parameterList.parameters.map((p) -> p.toBuilder()).toList() + body = self.body.toBuilder() + } } /// A parenthesized expression (`(expr)`). @@ -1296,16 +1618,26 @@ class ParenthesizedExprNode extends Expr { null else wrapExpr(e) + + function toBuilder(): ParenthesizedExprBuilder = + let (self = this) + new ParenthesizedExprBuilder { expression = self.expression!!.toBuilder() } } /// The `unknown` type. -class UnknownTypeNode extends TypeNode {} +class UnknownTypeNode extends TypeNode { + function toBuilder(): UnknownTypeBuilder = new UnknownTypeBuilder {} +} /// The `nothing` type. -class NothingTypeNode extends TypeNode {} +class NothingTypeNode extends TypeNode { + function toBuilder(): NothingTypeBuilder = new NothingTypeBuilder {} +} /// The `module` type. -class ModuleTypeNode extends TypeNode {} +class ModuleTypeNode extends TypeNode { + function toBuilder(): ModuleTypeBuilder = new ModuleTypeBuilder {} +} /// A declared type (e.g., `String`, `List`). class DeclaredTypeNode extends TypeNode { @@ -1321,18 +1653,33 @@ class DeclaredTypeNode extends TypeNode { null else new TypeArgumentListNode { node = n } + + function toBuilder(): DeclaredTypeBuilder = + let (self = this) + new DeclaredTypeBuilder { + name = self.name.identifiers.map((i) -> i.value).join(".") + typeArguments = self.typeArgumentList?.typeArguments?.map((t) -> t.toBuilder()) ?? List() + } } /// A nullable type (`Type?`). class NullableTypeNode extends TypeNode { /// The base type. baseType: TypeNode = wrapTypeNode(findTypeChild(node)!!) + + function toBuilder(): NullableTypeBuilder = + let (self = this) + new NullableTypeBuilder { baseType = self.baseType.toBuilder() } } /// A union type (`TypeA|TypeB|TypeC`). class UnionTypeNode extends TypeNode { /// The member types. members: List = findTypeChildren(node).map((n) -> wrapTypeNode(n)) + + function toBuilder(): UnionTypeBuilder = + let (self = this) + new UnionTypeBuilder { members = self.members.map((m) -> m.toBuilder()) } } /// A function type (`(ParamTypes) -> ReturnType`). @@ -1351,6 +1698,13 @@ class FunctionTypeNode extends TypeNode { returnType: TypeNode = let (types = findTypeChildren(node)) wrapTypeNode(types.last) + + function toBuilder(): FunctionTypeBuilder = + let (self = this) + new FunctionTypeBuilder { + parameterTypes = self.parameterTypes.map((t) -> t.toBuilder()) + returnType = self.returnType.toBuilder() + } } /// A constrained type (`Type(constraint)`). @@ -1363,6 +1717,13 @@ class ConstrainedTypeNode extends TypeNode { /// The constraint expressions. constraints: List = findExprChildren(constraintElems).map((n) -> wrapExpr(n)) + + function toBuilder(): ConstrainedTypeBuilder = + let (self = this) + new ConstrainedTypeBuilder { + baseType = self.baseType.toBuilder() + constraints = self.constraints.map((c) -> c.toBuilder()) + } } /// A parenthesized type (`(Type)`). @@ -1378,12 +1739,20 @@ class ParenthesizedTypeNode extends TypeNode { null else wrapTypeNode(t) + + function toBuilder(): ParenthesizedTypeBuilder = + let (self = this) + new ParenthesizedTypeBuilder { type = self.type!!.toBuilder() } } /// A string constant type (e.g., `"foo"`). class StringConstantTypeNode extends TypeNode { /// The string value. value: String = getStringChars(node) + + function toBuilder(): StringConstantTypeBuilder = + let (self = this) + new StringConstantTypeBuilder { value = self.value } } /// An annotation (`@Type { ... }`). @@ -1398,6 +1767,13 @@ class AnnotationNode extends SyntaxNode { null else new ObjectBodyNode { node = n } + + function toBuilder(): AnnotationBuilder = + let (self = this) + new AnnotationBuilder { + type = self.type.toBuilder() + body = self.body?.toBuilder() + } } /// A parameter declaration. @@ -1422,6 +1798,13 @@ class ParameterNode extends SyntaxNode { null else new TypeAnnotationNode { node = n } + + function toBuilder(): ParameterBuilder = + let (self = this) + new ParameterBuilder { + name = if (self.isWildcard) "_" else self.identifier!!.value + typeAnnotation = self.typeAnnotation?.type?.toBuilder() + } } /// A parameter list (`(param1, param2)`). @@ -1457,12 +1840,20 @@ class TypeAnnotationNode extends SyntaxNode { /// A type parameter declaration. class TypeParameterNode extends SyntaxNode { /// The variance modifier (`"in"`, `"out"`, or null). - variance: String? = terminals.findOrNull((t) -> t.text == "in" || t.text == "out")?.text + variance: ("in" | "out")? = + terminals.findOrNull((t) -> t.text == "in" || t.text == "out")?.text as ("in" | "out")? /// The type parameter name. name: IdentifierNode = let (n = findChild(node, "identifier")) new IdentifierNode { node = n!! } + + function toBuilder(): TypeParameterBuilder = + let (self = this) + new TypeParameterBuilder { + name = self.name.value + variance = self.variance + } } /// A type parameter list (``). @@ -1493,6 +1884,7 @@ class TypeArgumentListNode extends SyntaxNode { class IdentifierNode extends SyntaxNode { /// The identifier text. value: String = node.text ?? "" + // this node doesn't have a toBuilder because parent nodes use strings directly } /// A qualified identifier (`a.b.c`). @@ -1500,12 +1892,19 @@ class QualifiedIdentifierNode extends SyntaxNode { /// The identifiers in this qualified name. identifiers: List = findChildren(node, "identifier").map((n) -> new IdentifierNode { node = n }) + // this node doesn't have a toBuilder because parent nodes use strings directly } /// A doc comment. class DocCommentNode extends SyntaxNode { /// The text of each doc comment line. lines: List = findChildren(node, "doc_comment_line").map((n) -> n.text ?? "") + + function toBuilder(): DocCommentBuilder = + let (self = this) + new DocCommentBuilder { + lines = self.lines.map((l) -> if (l.startsWith("///")) l.drop(3) else l) + } } /// A modifier list (e.g., `open`, `abstract external`). @@ -1515,27 +1914,45 @@ class ModifierListNode extends SyntaxNode { } /// Base class for parts of a string literal. -abstract class StringPartNode extends SyntaxNode {} +abstract class StringPartNode extends SyntaxNode { + /// Convert this string part to its builder. + function toBuilder(): StringPartBuilder = + throw("toBuilder() not implemented for \(this.getClass())") +} /// A plain text part of a string literal. class StringCharsNode extends StringPartNode { /// The text content. value: String = node.text ?? "" + + function toBuilder(): StringCharsBuilder = + let (self = this) + new StringCharsBuilder { value = self.value } } /// An escape sequence in a string literal. class StringEscapeNode extends StringPartNode { /// The escape sequence text. value: String = node.text ?? "" + + function toBuilder(): StringEscapeBuilder = + let (self = this) + new StringEscapeBuilder { value = self.value } } /// A newline in a multi-line string literal. -class StringNewlineNode extends StringPartNode {} +class StringNewlineNode extends StringPartNode { + function toBuilder(): StringNewlineBuilder = new StringNewlineBuilder {} +} /// An interpolation in a string literal. class StringInterpolationNode extends StringPartNode { /// The interpolated expression. expression: Expr = wrapExpr(node) + + function toBuilder(): StringInterpolationBuilder = + let (self = this) + new StringInterpolationBuilder { expression = self.expression.toBuilder() } } /// Build string parts from the children of a string literal node. @@ -1604,3 +2021,1816 @@ local const function findNextNonAffix(cs: List, startIdx: Int): Int = findNextNonAffix(cs, startIdx + 1) else startIdx + +// =============== +// Builders +// =============== + +local const terminal: Node = new Node { type = "terminal" } + +local const identifierLeaf: Node = new Node { type = "identifier" } + +local const operatorLeaf: Node = new Node { type = "operator" } + +local const commaTerminal: Node = new Node { type = "terminal"; text = "," } + +// Interleave nodes with commas, producing `[a, ",", b, ",", c]` for `[a, b, c]`. +local const function commaSeparate(items: List): List = + items.fold(List(), (acc: List, item: Node) -> + if (acc.isEmpty) List(item) else acc.add(commaTerminal).add(item) + ) + +// Build a `modifier_list` node from a list of modifier strings. +local const function modifierListNode(mods: List): Node = new Node { + type = "modifier_list" + children = mods.map((m) -> new Node { type = "modifier"; text = m }) +} + +// Build a `qualified_identifier` node from a dotted name like `"a.b.c"`. +local const function qualifiedIdentifierNode(qname: String): Node = new Node { + type = "qualified_identifier" + children = + qname + .split(".") + .fold(List(), (acc: List, part: String) -> + if (acc.isEmpty) + List((identifierLeaf) { text = part }) + else + acc.add((terminal) { text = "." }).add((identifierLeaf) { text = part }) + ) +} + +/// Base class for all syntax builders. +abstract class Builder { + /// Affix nodes (comments, semicolons) to prepend before this node. + prefixes: List + + /// Affix nodes (comments, semicolons) to append after this node. + suffixes: List + + /// Build the typed syntax node. + abstract function build(): SyntaxNode +} + +/// Base class for expression builders. +abstract class ExprBuilder extends Builder { + abstract function build(): Expr +} + +/// Base class for type builders. +abstract class TypeBuilder extends Builder { + abstract function build(): TypeNode +} + +/// Base class for object member builders. +abstract class ObjectMemberBuilder extends Builder { + abstract function build(): ObjectMemberNode +} + +/// Builds an integer literal expression. +class IntLiteralBuilder extends ExprBuilder { + value: Int | String + + function build(): IntLiteralExprNode = new IntLiteralExprNode { + node = new Node { type = "int_literal_expr"; text = value.toString() } + } +} + +/// Builds a float literal expression. +class FloatLiteralBuilder extends ExprBuilder { + value: Float | String + + function build(): FloatLiteralExprNode = new FloatLiteralExprNode { + node = new Node { type = "float_literal_expr"; text = value.toString() } + } +} + +/// Builds a boolean literal expression. +class BoolLiteralBuilder extends ExprBuilder { + value: Boolean + + function build(): BoolLiteralExprNode = new BoolLiteralExprNode { + node = new Node { type = "bool_literal_expr"; text = if (value) "true" else "false" } + } +} + +/// Builds a `null` literal expression. +class NullLiteralBuilder extends ExprBuilder { + function build(): NullLiteralExprNode = new NullLiteralExprNode { + node = new Node { type = "null_expr"; text = "null" } + } +} + +/// Builds a `this` expression. +class ThisExprBuilder extends ExprBuilder { + function build(): ThisExprNode = new ThisExprNode { + node = new Node { type = "this_expr"; text = "this" } + } +} + +/// Builds an `outer` expression. +class OuterExprBuilder extends ExprBuilder { + function build(): OuterExprNode = new OuterExprNode { + node = new Node { type = "outer_expr"; text = "outer" } + } +} + +/// Builds a `module` expression. +class ModuleExprBuilder extends ExprBuilder { + function build(): ModuleExprNode = new ModuleExprNode { + node = new Node { type = "module_expr"; text = "module" } + } +} + +/// Builds an unqualified access expression (identifier or function call). +class IdentifierExprBuilder extends ExprBuilder { + name: String + + function build(): UnqualifiedAccessExprNode = new UnqualifiedAccessExprNode { + node = new Node { + type = "unqualified_access_expr" + children = List((identifierLeaf) { text = name }) + } + } +} + +/// Builds a single-line string literal expression. +/// +/// Set `value` for plain text, or `parts` for strings that contain escapes or interpolations. +/// `parts` defaults to a single chars node for the value, so amending only `value` is the simple path. +class StringLiteralBuilder extends ExprBuilder { + /// Convenience for plain-text strings. Sets `parts` to a single [StringCharsBuilder]. + value: String? + + /// String parts (chars, escapes, interpolations). Defaults to wrapping `value`, or empty. + parts: List = + let (self = this) + if (value != null) + List(new StringCharsBuilder { value = self.value!! }) + else + List() + + function build(): SingleLineStringLiteralExprNode = + let (self = this) + new SingleLineStringLiteralExprNode { + node = new Node { + type = "single_line_string_literal_expr" + children = + List((terminal) { text = "\"" }) + + self.parts.flatMap((p) -> p.buildNodes()) + + List((terminal) { text = "\"" }) + } + } +} + +/// Base class for parts of string literal (text, escapes, newlines, interpolations). +abstract class StringPartBuilder { + /// Build the list of `Node` parts. + abstract function buildNodes(): List +} + +/// Plain text content within a string literal. +class StringCharsBuilder extends StringPartBuilder { + value: String + + function buildNodes(): List = List(new Node { type = "string_chars"; text = value }) +} + +/// An escape sequence in a string literal (e.g., `"\\n"`, `"\\t"`). +class StringEscapeBuilder extends StringPartBuilder { + /// The escape sequence text including the leading backslash (e.g., `"\\n"`). + value: String + + function buildNodes(): List = List(new Node { type = "string_escape"; text = value }) +} + +/// A newline in a multi-line string literal. +class StringNewlineBuilder extends StringPartBuilder { + function buildNodes(): List = List(new Node { type = "string_newline" }) +} + +/// An interpolation in a string literal (`\(expr)`). +class StringInterpolationBuilder extends StringPartBuilder { + expression: ExprBuilder + + function buildNodes(): List = + let (self = this) + List( + (terminal) { text = "\\(" }, + self.expression.build().node, + (terminal) { text = ")" }, + ) +} + +/// Builds a multi-line string literal expression. +/// +/// Use [StringNewlineBuilder] entries in `parts` to separate lines. +class MultiLineStringLiteralBuilder extends ExprBuilder { + parts: List + + function build(): MultiLineStringLiteralExprNode = + let (self = this) + new MultiLineStringLiteralExprNode { + node = new Node { + type = "multi_line_string_literal_expr" + children = + List((terminal) { text = "\"\"\"" }) + + self.parts.flatMap((p) -> p.buildNodes()).toList() + + List(new Node { + type = "terminal" + text = "\"\"\"" + // formatter uses span.colStart of the closing `"""` to determine the + // indentation to strip from each content line. + span = new Span { colStart = 1 } + }) + } + } +} + +/// Builds a unary minus expression (`-expr`). +class UnaryMinusExprBuilder extends ExprBuilder { + operand: ExprBuilder + + function build(): UnaryMinusExprNode = new UnaryMinusExprNode { + node = new Node { + type = "unary_minus_expr" + children = + List( + (terminal) { text = "-" }, + operand.build().node, + ) + } + } +} + +/// Builds a logical not expression (`!expr`). +class LogicalNotExprBuilder extends ExprBuilder { + operand: ExprBuilder + + function build(): LogicalNotExprNode = new LogicalNotExprNode { + node = new Node { + type = "logical_not_expr" + children = + List( + (terminal) { text = "!" }, + operand.build().node, + ) + } + } +} + +/// Builds a non-null assertion expression (`expr!!`). +class NonNullExprBuilder extends ExprBuilder { + operand: ExprBuilder + + function build(): NonNullExprNode = new NonNullExprNode { + node = new Node { + type = "non_null_expr" + children = List(operand.build().node, (operatorLeaf) { text = "!!" }) + } + } +} + +/// Builds a `throw(expr)` expression. +class ThrowExprBuilder extends ExprBuilder { + expression: ExprBuilder + + function build(): ThrowExprNode = new ThrowExprNode { + node = new Node { + type = "throw_expr" + children = + List( + (terminal) { text = "throw" }, + (terminal) { text = "(" }, + expression.build().node, + (terminal) { text = ")" }, + ) + } + } +} + +/// Builds a `trace(expr)` expression. +class TraceExprBuilder extends ExprBuilder { + expression: ExprBuilder + + function build(): TraceExprNode = new TraceExprNode { + node = new Node { + type = "trace_expr" + children = + List( + (terminal) { text = "trace" }, + (terminal) { text = "(" }, + expression.build().node, + (terminal) { text = ")" }, + ) + } + } +} + +/// Builds a parenthesized expression (`(expr)`). +class ParenthesizedExprBuilder extends ExprBuilder { + expression: ExprBuilder + + function build(): ParenthesizedExprNode = new ParenthesizedExprNode { + node = new Node { + type = "parenthesized_expr" + children = + List( + (terminal) { text = "(" }, + new Node { + type = "parenthesized_expr_elements" + children = List(expression.build().node) + }, + (terminal) { text = ")" }, + ) + } + } +} + +/// Builds a binary operator expression (`left op right`). +class BinaryOpExprBuilder extends ExprBuilder { + left: ExprBuilder + operator: String + right: ExprBuilder + + function build(): BinaryOpExprNode = new BinaryOpExprNode { + node = new Node { + type = "binary_op_expr" + children = + List( + left.build().node, + (operatorLeaf) { text = operator }, + right.build().node, + ) + } + } +} + +/// Builds an `expr is Type` expression. +class IsExprBuilder extends ExprBuilder { + operand: ExprBuilder + type: TypeBuilder + + function build(): BinaryOpExprNode = + let (self = this) + new BinaryOpExprNode { + node = new Node { + type = "binary_op_expr" + children = + List( + self.operand.build().node, + (operatorLeaf) { text = "is" }, + self.type.build().node, + ) + } + } +} + +/// Builds an `expr as Type` expression. +class AsExprBuilder extends ExprBuilder { + operand: ExprBuilder + type: TypeBuilder + + function build(): BinaryOpExprNode = + let (self = this) + new BinaryOpExprNode { + node = new Node { + type = "binary_op_expr" + children = + List( + self.operand.build().node, + (operatorLeaf) { text = "as" }, + self.type.build().node, + ) + } + } +} + +/// Builds an `if (condition) thenExpr else elseExpr` expression. +class IfExprBuilder extends ExprBuilder { + condition: ExprBuilder + thenExpr: ExprBuilder + elseExpr: ExprBuilder + + function build(): IfExprNode = new IfExprNode { + node = new Node { + type = "if_expr" + children = + List( + new Node { + type = "if_header" + children = + List( + (terminal) { text = "if" }, + new Node { + type = "if_condition" + children = + List( + (terminal) { text = "(" }, + new Node { + type = "if_condition_expr" + children = List(condition.build().node) + }, + (terminal) { text = ")" }, + ) + }, + ) + }, + new Node { + type = "if_then_expr" + children = List(thenExpr.build().node) + }, + (terminal) { text = "else" }, + new Node { + type = "if_else_expr" + children = List(elseExpr.build().node) + }, + ) + } + } +} + +/// Builds an `import("uri")` or `import*("uri")` expression. +class ImportExprBuilder extends ExprBuilder { + uri: String + isGlob: Boolean = false + + function build(): ImportExprNode = new ImportExprNode { + node = new Node { + type = "import_expr" + children = + List( + (terminal) { text = if (isGlob) "import*" else "import" }, + (terminal) { text = "(" }, + new Node { + type = "string_chars" + children = + List( + (terminal) { text = "\"" }, + (terminal) { text = uri }, + (terminal) { text = "\"" }, + ) + }, + (terminal) { text = ")" }, + ) + } + } +} + +/// Builds a `read(expr)`, `read?(expr)`, or `read*(expr)` expression. +class ReadExprBuilder extends ExprBuilder { + expression: ExprBuilder + keyword: "read" | "read?" | "read*" = "read" + + function build(): ReadExprNode = new ReadExprNode { + node = new Node { + type = "read_expr" + children = + List( + (terminal) { text = keyword }, + (terminal) { text = "(" }, + expression.build().node, + (terminal) { text = ")" }, + ) + } + } +} + +/// Builds a declared type (e.g., `String`, `List`). +class DeclaredTypeBuilder extends TypeBuilder { + name: String + typeArguments: List + + function build(): DeclaredTypeNode = new DeclaredTypeNode { + node = new Node { + type = "declared_type" + children = + if (typeArguments.isEmpty) + List(qualifiedIdentifierNode(name)) + else + List(qualifiedIdentifierNode(name), new Node { + type = "type_argument_list" + children = + List( + (terminal) { text = "<" }, + new Node { + type = "type_argument_list_elements" + children = commaSeparate(typeArguments.map((t) -> t.build().node).toList()) + }, + (terminal) { text = ">" }, + ) + }) + } + } +} + +/// Builds a parameter declaration (`name`, `name: Type`, or `_`). +class ParameterBuilder extends Builder { + /// The parameter name. Use "_" for a wildcard parameter. + name: String + + /// The optional type annotation. + typeAnnotation: TypeBuilder? + + function build(): ParameterNode = + let (self = this) + new ParameterNode { + node = new Node { + type = "parameter" + children = + if (self.name == "_") + List((terminal) { text = "_" }) + else if (self.typeAnnotation == null) + List((identifierLeaf) { text = self.name }) + else + List( + (identifierLeaf) { text = self.name }, + new Node { + type = "type_annotation" + children = + List( + (terminal) { text = ":" }, + self.typeAnnotation.build().node, + ) + }, + ) + } + } +} + +/// Builds a `let (param = value) body` expression. +class LetExprBuilder extends ExprBuilder { + parameterName: String + parameterType: TypeBuilder? + bindingValue: ExprBuilder + body: ExprBuilder + + function build(): LetExprNode = + let (self = this) + new LetExprNode { + node = new Node { + type = "let_expr" + children = + List( + (terminal) { text = "let" }, + new Node { + type = "let_parameter_definition" + children = + List( + (terminal) { text = "(" }, + new Node { + type = "let_parameter" + children = + List( + ( + new ParameterBuilder { + name = self.parameterName + typeAnnotation = self.parameterType + } + ) + .build() + .node, + (terminal) { text = "=" }, + self.bindingValue.build().node, + ) + }, + (terminal) { text = ")" }, + ) + }, + self.body.build().node, + ) + } + } +} + +/// Builds a function literal expression (`(params) -> body`). +class FunctionLiteralBuilder extends ExprBuilder { + parameters: List + body: ExprBuilder + + function build(): FunctionLiteralExprNode = + let (self = this) + new FunctionLiteralExprNode { + node = new Node { + type = "function_literal_expr" + children = + List( + new Node { + type = "parameter_list" + children = + if (self.parameters.isEmpty) + List((terminal) { text = "(" }, (terminal) { text = ")" }) + else + List( + (terminal) { text = "(" }, + new Node { + type = "parameter_list_elements" + children = commaSeparate(self.parameters.map((p) -> p.build().node)) + }, + (terminal) { text = ")" }, + ) + }, + (terminal) { text = "->" }, + new Node { + type = "function_literal_body" + children = List(self.body.build().node) + }, + ) + } + } +} + +/// Builds an unqualified function call expression (`name(args)`). +class FunctionCallBuilder extends ExprBuilder { + name: String + arguments: List + + function build(): UnqualifiedAccessExprNode = + let (self = this) + new UnqualifiedAccessExprNode { + node = new Node { + type = "unqualified_access_expr" + children = + List( + (identifierLeaf) { text = self.name }, + new Node { + type = "argument_list" + children = + if (self.arguments.isEmpty) + List((terminal) { text = "(" }, (terminal) { text = ")" }) + else + List( + (terminal) { text = "(" }, + new Node { + type = "argument_list_elements" + children = commaSeparate(self.arguments.map((a) -> a.build().node)) + }, + (terminal) { text = ")" }, + ) + }, + ) + } + } +} + +/// Builds a qualified access expression (`receiver.member` or `receiver?.member`, +/// optionally with arguments for method calls). +class QualifiedAccessBuilder extends ExprBuilder { + receiver: ExprBuilder + member: String + isNullSafe: Boolean = false + /// If non-null, this is a method call with these arguments. If null, this is a property access. + arguments: List? + + function build(): QualifiedAccessExprNode = + let (self = this) + new QualifiedAccessExprNode { + node = new Node { + type = "qualified_access_expr" + children = + List( + self.receiver.build().node, + (operatorLeaf) { text = if (self.isNullSafe) "?." else "." }, + new Node { + type = "unqualified_access_expr" + children = + if (self.arguments == null) + List((identifierLeaf) { text = self.member }) + else + List( + (identifierLeaf) { text = self.member }, + new Node { + type = "argument_list" + children = + if (self.arguments.isEmpty) + List((terminal) { text = "(" }, (terminal) { text = ")" }) + else + List( + (terminal) { text = "(" }, + new Node { + type = "argument_list_elements" + children = commaSeparate(self.arguments.map((a) -> a.build().node)) + }, + (terminal) { text = ")" }, + ) + }, + ) + }, + ) + } + } +} + +/// Builds a subscript expression (`receiver[index]`). +class SubscriptBuilder extends ExprBuilder { + receiver: ExprBuilder + index: ExprBuilder + + function build(): SubscriptExprNode = + let (self = this) + new SubscriptExprNode { + node = new Node { + type = "subscript_expr" + children = + List( + self.receiver.build().node, + (operatorLeaf) { text = "[" }, + self.index.build().node, + (terminal) { text = "]" }, + ) + } + } +} + +/// Builds the `unknown` type. +class UnknownTypeBuilder extends TypeBuilder { + function build(): UnknownTypeNode = new UnknownTypeNode { + node = new Node { type = "unknown_type"; text = "unknown" } + } +} + +/// Builds the `nothing` type. +class NothingTypeBuilder extends TypeBuilder { + function build(): NothingTypeNode = new NothingTypeNode { + node = new Node { type = "nothing_type"; text = "nothing" } + } +} + +/// Builds the `module` type. +class ModuleTypeBuilder extends TypeBuilder { + function build(): ModuleTypeNode = new ModuleTypeNode { + node = new Node { type = "module_type"; text = "module" } + } +} + +/// Builds a nullable type (`Type?`). +class NullableTypeBuilder extends TypeBuilder { + baseType: TypeBuilder + + function build(): NullableTypeNode = new NullableTypeNode { + node = new Node { + type = "nullable_type" + children = List(baseType.build().node, (terminal) { text = "?" }) + } + } +} + +/// Builds a union type (`A | B | C`). +class UnionTypeBuilder extends TypeBuilder { + members: List + + function build(): UnionTypeNode = + let (self = this) + new UnionTypeNode { + node = new Node { + type = "union_type" + children = + self.members + .map((m) -> m.build().node) + .fold(List(), (acc: List, item: Node) -> + if (acc.isEmpty) List(item) else acc.add((terminal) { text = "|" }).add(item) + ) + } + } +} + +/// Builds a function type (`(ParamTypes) -> ReturnType`). +class FunctionTypeBuilder extends TypeBuilder { + parameterTypes: List + returnType: TypeBuilder + + function build(): FunctionTypeNode = + let (self = this) + new FunctionTypeNode { + node = new Node { + type = "function_type" + children = + List( + new Node { + type = "function_type_parameters" + children = + if (self.parameterTypes.isEmpty) + List((terminal) { text = "(" }, (terminal) { text = ")" }) + else + List( + (terminal) { text = "(" }, + new Node { + type = "parenthesized_type_elements" + children = + commaSeparate(self.parameterTypes.map((t) -> t.build().node).toList()) + }, + (terminal) { text = ")" }, + ) + }, + (terminal) { text = "->" }, + self.returnType.build().node, + ) + } + } +} + +/// Builds a constrained type (`Type(constraint1, constraint2)`). +class ConstrainedTypeBuilder extends TypeBuilder { + baseType: TypeBuilder + constraints: List + + function build(): ConstrainedTypeNode = + let (self = this) + new ConstrainedTypeNode { + node = new Node { + type = "constrained_type" + children = + List(self.baseType.build().node, new Node { + type = "constrained_type_constraint" + children = + List( + (terminal) { text = "(" }, + new Node { + type = "constrained_type_elements" + children = commaSeparate(self.constraints.map((c) -> c.build().node)) + }, + (terminal) { text = ")" }, + ) + }) + } + } +} + +/// Builds a parenthesized type (`(Type)`). +class ParenthesizedTypeBuilder extends TypeBuilder { + type: TypeBuilder + + function build(): ParenthesizedTypeNode = + let (self = this) + new ParenthesizedTypeNode { + node = new Node { + type = "parenthesized_type" + children = + List( + (terminal) { text = "(" }, + new Node { + type = "parenthesized_type_elements" + children = List(self.type.build().node) + }, + (terminal) { text = ")" }, + ) + } + } +} + +/// Builds a string constant type (e.g., `"foo"`). +class StringConstantTypeBuilder extends TypeBuilder { + value: String + + function build(): StringConstantTypeNode = new StringConstantTypeNode { + node = new Node { + type = "string_constant_type" + children = + List(new Node { + type = "string_chars" + children = + List( + (terminal) { text = "\"" }, + (terminal) { text = value }, + (terminal) { text = "\"" }, + ) + }) + } + } +} + +/// Builds a type parameter declaration (`T`, `in T`, or `out T`). +class TypeParameterBuilder extends Builder { + name: String + variance: ("in" | "out")? + + function build(): TypeParameterNode = + let (self = this) + new TypeParameterNode { + node = new Node { + type = "type_parameter" + children = + if (self.variance == null) + List((identifierLeaf) { text = self.name }) + else + List( + (terminal) { text = self.variance }, + (identifierLeaf) { text = self.name }, + ) + } + } +} + +/// Builds an object body delimited by braces. +class ObjectBodyBuilder extends Builder { + parameters: List + members: List + + function build(): ObjectBodyNode = + let (self = this) + new ObjectBodyNode { + node = new Node { + type = "object_body" + children = + List((terminal) { text = "{" }) + + ( + if (self.parameters.isEmpty) + List() + else + List(new Node { + type = "object_parameter_list" + children = + commaSeparate(self.parameters.map((p) -> p.build().node)) + .add((terminal) { text = "->" }) + }) + ) + + ( + if (self.members.isEmpty) + List() + else + List(new Node { + type = "object_member_list" + children = self.members.map((m) -> m.build().node) + }) + ) + + List((terminal) { text = "}" }) + } + } +} + +/// Builds an object element. +class ObjectElementBuilder extends ObjectMemberBuilder { + expression: ExprBuilder + + function build(): ObjectElementNode = + let (self = this) + new ObjectElementNode { + node = new Node { + type = "object_element" + children = List(self.expression.build().node) + } + } +} + +/// Builds an object spread (`...expr` or `...?expr`). +class ObjectSpreadBuilder extends ObjectMemberBuilder { + expression: ExprBuilder + isNullable: Boolean = false + + function build(): ObjectSpreadNode = + let (self = this) + new ObjectSpreadNode { + node = new Node { + type = "object_spread" + children = + List( + (terminal) { text = if (self.isNullable) "...?" else "..." }, + self.expression.build().node, + ) + } + } +} + +/// Builds an object property declaration. +class ObjectPropertyBuilder extends ObjectMemberBuilder { + modifiers: List + name: String + typeAnnotation: TypeBuilder? + value: ExprBuilder? + /// Object bodies for amending. Used when there is no `=` value. + objectBodies: List + + function build(): ObjectPropertyNode = + let (self = this) + new ObjectPropertyNode { + node = new Node { + type = "object_property" + children = + List(new Node { + type = "object_property_header" + children = + List(new Node { + type = "object_property_header_begin" + children = + ( + if (self.modifiers.isEmpty) + List() + else + List(modifierListNode(self.modifiers)) + ) + + List((identifierLeaf) { text = self.name }) + }) + + ( + if (self.typeAnnotation == null) + List() + else + List(new Node { + type = "type_annotation" + children = + List( + (terminal) { text = ":" }, + self.typeAnnotation.build().node, + ) + }) + ) + }) + + ( + if (self.value != null) + List( + (terminal) { text = "=" }, + new Node { + type = "object_property_body" + children = List(self.value.build().node) + }, + ) + else + self.objectBodies.map((b) -> b.build().node) + ) + } + } +} + +/// Builds an object method declaration. +class ObjectMethodBuilder extends ObjectMemberBuilder { + modifiers: List + name: String + typeParameters: List + parameters: List + returnType: TypeBuilder? + body: ExprBuilder + + function build(): ObjectMethodNode = + let (self = this) + new ObjectMethodNode { + node = new Node { + type = "object_method" + children = + List(new Node { + type = "class_method_header" + children = + ( + if (self.modifiers.isEmpty) + List() + else + List(modifierListNode(self.modifiers)) + ) + + List( + (terminal) { text = "function" }, + (identifierLeaf) { text = self.name }, + ) + }) + + ( + if (self.typeParameters.isEmpty) + List() + else + List(new Node { + type = "type_parameter_list" + children = + List( + (terminal) { text = "<" }, + new Node { + type = "type_parameter_list_elements" + children = commaSeparate(self.typeParameters.map((t) -> t.build().node)) + }, + (terminal) { text = ">" }, + ) + }) + ) + + List(new Node { + type = "parameter_list" + children = + if (self.parameters.isEmpty) + List((terminal) { text = "(" }, (terminal) { text = ")" }) + else + List( + (terminal) { text = "(" }, + new Node { + type = "parameter_list_elements" + children = commaSeparate(self.parameters.map((p) -> p.build().node)) + }, + (terminal) { text = ")" }, + ) + }) + + ( + if (self.returnType == null) + List() + else + List(new Node { + type = "type_annotation" + children = + List( + (terminal) { text = ":" }, + self.returnType.build().node, + ) + }) + ) + + List( + (terminal) { text = "=" }, + new Node { + type = "class_method_body" + children = List(self.body.build().node) + }, + ) + } + } +} + +/// Builds an object entry (`[key] = value` or `[key] { ... }`). +class ObjectEntryBuilder extends ObjectMemberBuilder { + key: ExprBuilder + value: ExprBuilder? + objectBodies: List + + function build(): ObjectEntryNode = + let (self = this) + new ObjectEntryNode { + node = new Node { + type = "object_entry" + children = + List(new Node { + type = "object_entry_header" + children = + List( + (terminal) { text = "[" }, + self.key.build().node, + (terminal) { text = "]" }, + ) + + (if (self.value != null) List((terminal) { text = "=" }) else List()) + }) + + ( + if (self.value != null) + List(self.value.build().node) + else + self.objectBodies.map((b) -> b.build().node) + ) + } + } +} + +/// Builds a member predicate (`[[condition]] = value` or `[[condition]] { ... }`). +class MemberPredicateBuilder extends ObjectMemberBuilder { + condition: ExprBuilder + value: ExprBuilder? + objectBodies: List + + function build(): MemberPredicateNode = + let (self = this) + new MemberPredicateNode { + node = new Node { + type = "member_predicate" + children = + List( + (terminal) { text = "[[" }, + self.condition.build().node, + (terminal) { text = "]" }, + (terminal) { text = "]" }, + ) + + ( + if (self.value != null) + List( + (terminal) { text = "=" }, + self.value.build().node, + ) + else + self.objectBodies.map((b) -> b.build().node) + ) + } + } +} + +/// Builds a `for (param in iterable) { ... }` generator. +class ForGeneratorBuilder extends ObjectMemberBuilder { + /// The optional key parameter (when iterating with both key and value). + keyParameter: ParameterBuilder? + valueParameter: ParameterBuilder + iterable: ExprBuilder + body: ObjectBodyBuilder + + function build(): ForGeneratorNode = + let (self = this) + new ForGeneratorNode { + node = new Node { + type = "for_generator" + children = + List( + (terminal) { text = "for" }, + new Node { + type = "for_generator_header" + children = + List( + (terminal) { text = "(" }, + new Node { + type = "for_generator_header_definition" + children = + List( + new Node { + type = "for_generator_header_definition_header" + children = + ( + if (self.keyParameter == null) + List(self.valueParameter.build().node) + else + List( + self.keyParameter.build().node, + (terminal) { text = "," }, + self.valueParameter.build().node, + ) + ) + + List((terminal) { text = "in" }) + }, + self.iterable.build().node, + ) + }, + (terminal) { text = ")" }, + ) + }, + self.body.build().node, + ) + } + } +} + +/// Builds a `when (condition) { ... } else { ... }` generator. +class WhenGeneratorBuilder extends ObjectMemberBuilder { + condition: ExprBuilder + thenBody: ObjectBodyBuilder + elseBody: ObjectBodyBuilder? + + function build(): WhenGeneratorNode = + let (self = this) + new WhenGeneratorNode { + node = new Node { + type = "when_generator" + children = + List( + (terminal) { text = "when" }, + new Node { + type = "when_generator_header" + children = + List( + (terminal) { text = "(" }, + self.condition.build().node, + (terminal) { text = ")" }, + ) + }, + self.thenBody.build().node, + ) + + ( + if (self.elseBody == null) + List() + else + List( + (terminal) { text = "else" }, + self.elseBody.build().node, + ) + ) + } + } +} + +/// Builds a `new Type { ... }` expression. +class NewExprBuilder extends ExprBuilder { + /// The optional type. If null, this is `new { ... }`. + type: TypeBuilder? + body: ObjectBodyBuilder + + function build(): NewExprNode = + let (self = this) + new NewExprNode { + node = new Node { + type = "new_expr" + children = + List( + new Node { + type = "new_header" + children = + if (self.type == null) + List((terminal) { text = "new" }) + else + List((terminal) { text = "new" }, self.type.build().node) + }, + self.body.build().node, + ) + } + } +} + +/// Builds an amends expression (`(expr) { ... }`). +class AmendsExprBuilder extends ExprBuilder { + parentExpr: ExprBuilder + body: ObjectBodyBuilder + + function build(): AmendsExprNode = + let (self = this) + new AmendsExprNode { + node = new Node { + type = "amends_expr" + children = + List( + self.parentExpr.build().node, + self.body.build().node, + ) + } + } +} + +/// Builds a doc comment. +class DocCommentBuilder extends Builder { + /// The body text of each line, without the leading `///`. + lines: List + + function build(): DocCommentNode = + let (self = this) + new DocCommentNode { + node = new Node { + type = "doc_comment" + children = self.lines.map((l) -> new Node { type = "doc_comment_line"; text = "///" + l }) + } + } +} + +/// Builds an annotation (`@Type` or `@Type { ... }`). +class AnnotationBuilder extends Builder { + type: TypeBuilder + body: ObjectBodyBuilder? + + function build(): AnnotationNode = + let (self = this) + new AnnotationNode { + node = new Node { + type = "annotation" + children = + List((terminal) { text = "@" }, self.type.build().node) + + (if (self.body == null) List() else List(self.body.build().node)) + } + } +} + +/// Builds an import declaration. +class ImportBuilder extends Builder { + uri: String + isGlob: Boolean = false + alias: String? + + function build(): ImportNode = + let (self = this) + new ImportNode { + node = new Node { + type = "import" + children = + List( + (terminal) { text = if (self.isGlob) "import*" else "import" }, + new Node { + type = "string_chars" + children = + List( + (terminal) { text = "\"" }, + (terminal) { text = self.uri }, + (terminal) { text = "\"" }, + ) + }, + ) + + ( + if (self.alias == null) + List() + else + List(new Node { + type = "import_alias" + children = + List( + (terminal) { text = "as" }, + (identifierLeaf) { text = self.alias }, + ) + }) + ) + } + } +} + +/// Builds a class body delimited by braces. +class ClassBodyBuilder extends Builder { + properties: List + methods: List + + function build(): ClassBodyNode = + let (self = this) + new ClassBodyNode { + node = new Node { + type = "class_body" + children = + List((terminal) { text = "{" }) + + ( + let ( + members = + self.properties.map((p) -> p.build().node) + + self.methods.map((m) -> m.build().node) + ) + if (members.isEmpty) + List() + else + List(new Node { type = "class_body_elements"; children = members }) + ) + + List((terminal) { text = "}" }) + } + } +} + +/// Builds a class property declaration. +class ClassPropertyBuilder extends Builder { + docComment: DocCommentBuilder? + annotations: List + modifiers: List + name: String + typeAnnotation: TypeBuilder? + value: ExprBuilder? + /// Object bodies for amending. Used when there is no `=` value. + objectBodies: List + + function build(): ClassPropertyNode = + let (self = this) + new ClassPropertyNode { + node = new Node { + type = "class_property" + children = + (if (self.docComment == null) List() else List(self.docComment.build().node)) + + self.annotations.map((a) -> a.build().node) + + List(new Node { + type = "class_property_header" + children = + List(new Node { + type = "class_property_header_begin" + children = + ( + if (self.modifiers.isEmpty) + List() + else + List(modifierListNode(self.modifiers)) + ) + + List((identifierLeaf) { text = self.name }) + }) + + ( + if (self.typeAnnotation == null) + List() + else + List(new Node { + type = "type_annotation" + children = + List( + (terminal) { text = ":" }, + self.typeAnnotation.build().node, + ) + }) + ) + }) + + ( + if (self.value != null) + List( + (terminal) { text = "=" }, + new Node { + type = "class_property_body" + children = List(self.value.build().node) + }, + ) + else + self.objectBodies.map((b) -> b.build().node) + ) + } + } +} + +/// Builds a class method declaration. +class ClassMethodBuilder extends Builder { + docComment: DocCommentBuilder? + annotations: List + modifiers: List + name: String + typeParameters: List + parameters: List + returnType: TypeBuilder? + /// The method body. Null for abstract methods. + body: ExprBuilder? + + function build(): ClassMethodNode = + let (self = this) + new ClassMethodNode { + node = new Node { + type = "class_method" + children = + (if (self.docComment == null) List() else List(self.docComment.build().node)) + + self.annotations.map((a) -> a.build().node) + + List(new Node { + type = "class_method_header" + children = + ( + if (self.modifiers.isEmpty) + List() + else + List(modifierListNode(self.modifiers)) + ) + + List( + (terminal) { text = "function" }, + (identifierLeaf) { text = self.name }, + ) + }) + + ( + if (self.typeParameters.isEmpty) + List() + else + List(new Node { + type = "type_parameter_list" + children = + List( + (terminal) { text = "<" }, + new Node { + type = "type_parameter_list_elements" + children = commaSeparate(self.typeParameters.map((t) -> t.build().node)) + }, + (terminal) { text = ">" }, + ) + }) + ) + + List(new Node { + type = "parameter_list" + children = + if (self.parameters.isEmpty) + List((terminal) { text = "(" }, (terminal) { text = ")" }) + else + List( + (terminal) { text = "(" }, + new Node { + type = "parameter_list_elements" + children = commaSeparate(self.parameters.map((p) -> p.build().node)) + }, + (terminal) { text = ")" }, + ) + }) + + ( + if (self.returnType == null) + List() + else + List(new Node { + type = "type_annotation" + children = + List( + (terminal) { text = ":" }, + self.returnType.build().node, + ) + }) + ) + + ( + if (self.body == null) + List() + else + List( + (terminal) { text = "=" }, + new Node { + type = "class_method_body" + children = List(self.body.build().node) + }, + ) + ) + } + } +} + +/// Builds a typealias declaration. +class TypeAliasBuilder extends Builder { + docComment: DocCommentBuilder? + annotations: List + modifiers: List + name: String + typeParameters: List + type: TypeBuilder + + function build(): TypeAliasNode = + let (self = this) + new TypeAliasNode { + node = new Node { + type = "typealias" + children = + (if (self.docComment == null) List() else List(self.docComment.build().node)) + + self.annotations.map((a) -> a.build().node) + + List(new Node { + type = "typealias_header" + children = + ( + if (self.modifiers.isEmpty) + List() + else + List(modifierListNode(self.modifiers)) + ) + + List( + (terminal) { text = "typealias" }, + (identifierLeaf) { text = self.name }, + ) + + ( + if (self.typeParameters.isEmpty) + List() + else + List(new Node { + type = "type_parameter_list" + children = + List( + (terminal) { text = "<" }, + new Node { + type = "type_parameter_list_elements" + children = + commaSeparate(self.typeParameters.map((t) -> t.build().node)) + }, + (terminal) { text = ">" }, + ) + }) + ) + + List((terminal) { text = "=" }) + }) + + List(new Node { + type = "typealias_body" + children = List(self.type.build().node) + }) + } + } +} + +/// Builds a class declaration. +class ClassBuilder extends Builder { + docComment: DocCommentBuilder? + annotations: List + modifiers: List + name: String + typeParameters: List + extendsType: TypeBuilder? + body: ClassBodyBuilder? + + function build(): ClassNode = + let (self = this) + new ClassNode { + node = new Node { + type = "class" + children = + (if (self.docComment == null) List() else List(self.docComment.build().node)) + + self.annotations.map((a) -> a.build().node) + + List(new Node { + type = "class_header" + children = + ( + if (self.modifiers.isEmpty) + List() + else + List(modifierListNode(self.modifiers)) + ) + + List( + (terminal) { text = "class" }, + (identifierLeaf) { text = self.name }, + ) + + ( + if (self.typeParameters.isEmpty) + List() + else + List(new Node { + type = "type_parameter_list" + children = + List( + (terminal) { text = "<" }, + new Node { + type = "type_parameter_list_elements" + children = + commaSeparate(self.typeParameters.map((t) -> t.build().node)) + }, + (terminal) { text = ">" }, + ) + }) + ) + + ( + if (self.extendsType == null) + List() + else + List(new Node { + type = "class_header_extends" + children = + List( + (terminal) { text = "extends" }, + self.extendsType.build().node, + ) + }) + ) + }) + + (if (self.body == null) List() else List(self.body.build().node)) + } + } +} + +/// Builds a module declaration. +class ModuleDeclarationBuilder extends Builder { + docComment: DocCommentBuilder? + annotations: List + modifiers: List + /// The qualified module name, if a `module` declaration is present. + name: String? + /// The URI string of the amended module, if any. Mutually exclusive with `extendsUri`. + amendsUri: String? + /// The URI string of the extended module, if any. Mutually exclusive with `amendsUri`. + extendsUri: String? + + function build(): ModuleDeclarationNode = + let (self = this) + new ModuleDeclarationNode { + node = new Node { + type = "module_declaration" + children = + (if (self.docComment == null) List() else List(self.docComment.build().node)) + + self.annotations.map((a) -> a.build().node) + + ( + if (self.name != null) + List(new Node { + type = "module_definition" + children = + ( + if (self.modifiers.isEmpty) + List() + else + List(modifierListNode(self.modifiers)) + ) + + List( + (terminal) { text = "module" }, + qualifiedIdentifierNode(self.name!!), + ) + }) + else if (!self.modifiers.isEmpty) + List(modifierListNode(self.modifiers)) + else + List() + ) + + ( + if (self.amendsUri != null) + List(new Node { + type = "amends_clause" + children = + List( + (terminal) { text = "amends" }, + new Node { + type = "string_chars" + children = + List( + (terminal) { text = "\"" }, + (terminal) { text = self.amendsUri }, + (terminal) { text = "\"" }, + ) + }, + ) + }) + else if (self.extendsUri != null) + List(new Node { + type = "extends_clause" + children = + List( + (terminal) { text = "extends" }, + new Node { + type = "string_chars" + children = + List( + (terminal) { text = "\"" }, + (terminal) { text = self.extendsUri }, + (terminal) { text = "\"" }, + ) + }, + ) + }) + else + List() + ) + } + } +} + +/// Builds a module. +class ModuleBuilder extends Builder { + declaration: ModuleDeclarationBuilder? + imports: List + classes: List + typeAliases: List + properties: List + methods: List + + function build(): ModuleNode = + let (self = this) + new ModuleNode { + node = new Node { + type = "module" + children = + (if (self.declaration == null) List() else List(self.declaration.build().node)) + + ( + if (self.imports.isEmpty) + List() + else + List(new Node { + type = "import_list" + children = self.imports.map((i) -> i.build().node) + }) + ) + + self.classes.map((c) -> c.build().node) + + self.typeAliases.map((t) -> t.build().node) + + self.properties.map((p) -> p.build().node) + + self.methods.map((m) -> m.build().node) + } + } +} From 56827fab8418793e6a7c3b49d03eaee55f851f79 Mon Sep 17 00:00:00 2001 From: Islon Scherer Date: Wed, 27 May 2026 17:48:59 +0200 Subject: [PATCH 4/4] Fix build --- .../pkl/core/stdlib/syntax/SyntaxNodes.java | 18 +++++++++++------- .../pkl/core/stdlib/syntax/package-info.java | 4 ++-- .../input/syntax/builders.pkl | 2 ++ 3 files changed, 15 insertions(+), 9 deletions(-) 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 index 40db0e1fd..8a22503c7 100644 --- 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 @@ -18,6 +18,8 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Specialization; import java.util.ArrayList; +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; @@ -51,13 +53,15 @@ private SyntaxNodes() {} static final class NodeData { final Node node; final char[] source; - VmTyped parentVm; + @Nullable VmTyped parentVm; VmList childrenVm; VmTyped spanVm; - NodeData(Node node, char[] source) { + NodeData(Node node, char[] source, VmList childrenVm, VmTyped spanVm) { this.node = node; this.source = source; + this.childrenVm = childrenVm; + this.spanVm = spanVm; } } @@ -81,7 +85,7 @@ static final class ErrorData { private static final VmObjectFactory nodeFactory = new VmObjectFactory(SyntaxModule::getNodeClass) - .addStringProperty("type", nd -> nd.node.type.name().toLowerCase()) + .addStringProperty("type", nd -> nd.node.type.name().toLowerCase(Locale.ROOT)) .addListProperty("children", nd -> nd.childrenVm) .addProperty("parent", nd -> VmNull.lift(nd.parentVm)) .addProperty( @@ -118,9 +122,9 @@ private static VmTyped convertNode(Node genericNode, char[] sourceChars) { childrenList.add(convertNode(child, sourceChars)); } - var data = new NodeData(genericNode, sourceChars); - data.childrenVm = VmList.create(childrenList.toArray()); - data.spanVm = spanFactory.create(genericNode.span); + var childrenVm = VmList.create(childrenList.toArray()); + var spanVm = spanFactory.create(genericNode.span); + var data = new NodeData(genericNode, sourceChars, childrenVm, spanVm); var result = nodeFactory.create(data); @@ -145,7 +149,7 @@ protected String eval(VmTyped self, VmTyped nodeVm, String grammarVersion) { private static Node convertVmToNode(VmTyped nodeVm) { var typeStr = (String) VmUtils.readMember(nodeVm, TYPE_ID); - var nodeType = NodeType.valueOf(typeStr.toUpperCase()); + var nodeType = NodeType.valueOf(typeStr.toUpperCase(Locale.ROOT)); var childrenVm = (VmList) VmUtils.readMember(nodeVm, CHILDREN_ID); var children = new ArrayList(childrenVm.getLength()); 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 index 6f12d5850..14f5426cc 100644 --- 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 @@ -1,4 +1,4 @@ -@NonnullByDefault +@NullMarked package org.pkl.core.stdlib.syntax; -import org.pkl.core.util.NonnullByDefault; +import org.jspecify.annotations.NullMarked; diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/builders.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/builders.pkl index 095c12062..cd02ffec5 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/builders.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/syntax/builders.pkl @@ -664,7 +664,9 @@ examples { ) }) == #""" module my.config + import "pkl:json" + host: String = "localhost" port: Int = 8080