Add pkl:syntax stdlib module (#1821)

This commit is contained in:
Islon Scherer authored and GitHub committed 2026-10-23 15:51:22 +02:00
1 parent 0cb9843af7
commit 939e844c03
48 files changed
+3149 -214

No files matched your search

@@ -33,7 +33,6 @@ import org.pkl.core.util.IoUtils
import org.pkl.formatter.Formatter
import org.pkl.formatter.GrammarVersion
import org.pkl.parser.GenericParserError
import org.pkl.parser.ParserError
class CliFormatterCommand
@JvmOverloads
@@ -128,10 +127,7 @@ constructor(
consoleWriter.write(formatted)
consoleWriter.flush()
}
} catch (pe: ParserError) { // thrown by the lexer
writeErrLine("Could not format `$pathStr`: $pe")
status.update(ERROR)
} catch (pe: GenericParserError) { // thrown by the generic parser
} catch (pe: GenericParserError) {
writeErrLine("Could not format `$pathStr`: $pe")
status.update(ERROR)
} catch (e: IOException) {
+1
View File
@@ -61,6 +61,7 @@ dependencies {
compileOnly(projects.pklExecutor)
implementation(projects.pklParser)
implementation(projects.pklFormatter)
implementation(libs.msgpack)
implementation(libs.truffleApi)
implementation(libs.graalSdk)
@@ -279,7 +279,7 @@ import org.pkl.parser.syntax.Type.ModuleType;
import org.pkl.parser.syntax.Type.NothingType;
import org.pkl.parser.syntax.Type.NullableType;
import org.pkl.parser.syntax.Type.ParenthesizedType;
import org.pkl.parser.syntax.Type.StringConstantType;
import org.pkl.parser.syntax.Type.StringLiteralType;
import org.pkl.parser.syntax.Type.ThisType;
import org.pkl.parser.syntax.Type.UnionType;
import org.pkl.parser.syntax.Type.UnknownType;
@@ -474,7 +474,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
}
@Override
public UnresolvedTypeNode visitStringConstantType(StringConstantType type) {
public UnresolvedTypeNode visitStringLiteralType(StringLiteralType type) {
return new UnresolvedTypeNode.StringLiteral(
createSourceSection(type), type.getStr().getString());
}
@@ -565,7 +565,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
boolean isUnionOfStringLiterals = true;
for (var typ : elementTypes) {
if (!(typ instanceof StringConstantType)) {
if (!(typ instanceof StringLiteralType)) {
isUnionOfStringLiterals = false;
break;
}
@@ -576,7 +576,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
return new UnresolvedTypeNode.UnionOfStringLiterals(
createSourceSection(type),
defaultIndex,
elementTypes.stream().map(it -> ((StringConstantType) it).getStr().getString()).toList());
elementTypes.stream().map(it -> ((StringLiteralType) it).getStr().getString()).toList());
}
var elements = new UnresolvedTypeNode[elementTypes.size()];
@@ -181,6 +181,18 @@ public final class Identifier implements Comparable<Identifier> {
// common in lambdas etc
public static final Identifier IT = get("it");
// members of pkl.syntax#Node, pkl.syntax#Span and pkl.syntax#SourceLocation
public static final Identifier TYPE = get("type");
public static final Identifier CHILDREN = get("children");
public static final Identifier SPAN = get("span");
public static final Identifier START = get("start");
public static final Identifier END = get("end");
public static final Identifier LINE = get("line");
public static final Identifier COLUMN = get("column");
// members of pkl.syntax#FormattingRenderer
public static final Identifier GRAMMAR_VERSION = get("grammarVersion");
private final String name;
private Identifier(String name) {
@@ -107,7 +107,7 @@ public final class MirrorFactories {
new VmObjectFactory<>(ReflectModule::getNothingTypeClass);
public static final VmObjectFactory<SourceSection> sourceLocationFactory =
new VmObjectFactory<>(ReflectModule::getSourceLocationClass);
new VmObjectFactory<>(SyntaxModule::getSourceLocationClass);
static {
moduleFactory
@@ -117,6 +117,8 @@ public final class ModuleCache {
case "settings":
// always needed if ~/.config/pkl/settings.pkl is present
return SettingsModule.getModule();
case "syntax":
return SyntaxModule.getModule();
case "test":
return TestModule.getModule();
case "xml":
@@ -104,10 +104,6 @@ public final class ReflectModule extends StdLibModule {
return TypeVariableClass.instance;
}
public static VmClass getSourceLocationClass() {
return SourceLocationClass.instance;
}
private static final class ModuleClass {
static final VmClass instance = loadClass("Module");
}
@@ -176,10 +172,6 @@ public final class ReflectModule extends StdLibModule {
static final VmClass instance = loadClass("TypeVariable");
}
private static final class SourceLocationClass {
static final VmClass instance = loadClass("SourceLocation");
}
@TruffleBoundary
private static VmClass loadClass(String className) {
var theModule = getModule();
@@ -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.TruffleBoundary;
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 getSourceLocationClass() {
return SourceLocationClass.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 SourceLocationClass {
static final VmClass instance = loadClass("SourceLocation");
}
@TruffleBoundary
private static VmClass loadClass(String className) {
var theModule = getModule();
return (VmClass) VmUtils.readMember(theModule, Identifier.get(className));
}
}
@@ -947,22 +947,28 @@ public final class VmUtils {
}
public static String getDisplayUri(SourceSection section, StackFrameTransformer transformer) {
var sourceUri = section.getSource().getURI();
var frame =
new StackFrame(
sourceUri.toString(),
null,
List.of(),
section.getStartLine(),
section.getStartColumn(),
section.getEndLine(),
section.getEndColumn());
StackFrame transformed = transformer.apply(frame);
return transformed.getModuleUri();
return getDisplayUri(
section.getSource().getURI().toString(),
section.getStartLine(),
section.getStartColumn(),
section.getEndLine(),
section.getEndColumn(),
transformer);
}
public static String getDisplayUri(URI moduleUri, StackFrameTransformer transformer) {
var frame = new StackFrame(moduleUri.toString(), null, List.of(), 1, 1, 1, 1);
return getDisplayUri(moduleUri.toString(), 1, 1, 1, 1, transformer);
}
public static String getDisplayUri(
String moduleUri,
int startLine,
int startColumn,
int endLine,
int endColumn,
StackFrameTransformer transformer) {
var frame =
new StackFrame(moduleUri, null, List.of(), startLine, startColumn, endLine, endColumn);
StackFrame transformed = transformer.apply(frame);
return transformed.getModuleUri();
}
@@ -118,6 +118,15 @@ public final class VmObjectFactory<E> {
return result;
}
@TruffleBoundary
public VmTyped create(@Nullable E extraStorage, VmTyped parent) {
var clazz = classSupplier.get();
assert parent.getVmClass() == clazz;
var result = new VmTyped(VmUtils.createEmptyMaterializedFrame(), parent, clazz, members);
result.setExtraStorage(extraStorage);
return result;
}
@FunctionalInterface
public interface IntProperty<E> {
long evaluate(E extraStorage);
@@ -0,0 +1,55 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.stdlib.syntax;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.IndirectCallNode;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmTyped;
import org.pkl.core.runtime.VmUtils;
import org.pkl.core.stdlib.ExternalMethod1Node;
import org.pkl.formatter.Formatter;
import org.pkl.formatter.GrammarVersion;
import org.pkl.parser.syntax.generic.Node;
public final class FormattingRendererNodes {
private FormattingRendererNodes() {}
public abstract static class render extends ExternalMethod1Node {
@Specialization
protected String eval(
VmTyped self, VmTyped nodeVm, @Cached("create()") IndirectCallNode callNode) {
var grammarVersion = (String) VmUtils.readMember(self, Identifier.GRAMMAR_VERSION, callNode);
var node = SyntaxNodes.convertVmToNode(nodeVm, SyntaxNodes.ZERO_SPAN, callNode);
try {
return format(grammarVersion, node);
} catch (RuntimeException e) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder()
.evalError("cannotRenderSyntaxNode", VmUtils.readMember(nodeVm, Identifier.TYPE))
.build();
}
}
@TruffleBoundary
private static String format(String grammarVersion, Node node) {
return new Formatter(GrammarVersion.valueOf(grammarVersion)).format(node);
}
}
}
@@ -0,0 +1,410 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.stdlib.syntax;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.IndirectCallNode;
import com.oracle.truffle.api.nodes.LoopNode;
import com.oracle.truffle.api.nodes.Node;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.pkl.core.ast.lambda.ApplyVmFunction1Node;
import org.pkl.core.ast.lambda.ApplyVmFunction2Node;
import org.pkl.core.ast.lambda.ApplyVmFunction2NodeGen;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmFunction;
import org.pkl.core.runtime.VmList;
import org.pkl.core.runtime.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.syntax.SyntaxNodes.Rebuild;
/** Backs the methods of {@code pkl.syntax#Node}. */
public final class NodeNodes {
private NodeNodes() {}
public abstract static class fold extends ExternalMethod2Node {
@Child private ApplyVmFunction2Node applyAccumulate = ApplyVmFunction2NodeGen.create();
@Child private IndirectCallNode callNode = IndirectCallNode.create();
@Specialization
protected Object eval(VmTyped self, Object initial, VmFunction operator) {
var pending = new ArrayDeque<VmTyped>();
pending.push(self);
var result = initial;
var visited = 0;
while (!pending.isEmpty()) {
var node = pending.pop();
result = applyAccumulate.execute(operator, result, node);
var children = (VmList) VmUtils.readMember(node, Identifier.CHILDREN, callNode);
for (var i = children.getLength() - 1; i >= 0; i--) {
pending.push((VmTyped) children.get(i));
}
visited += 1;
}
LoopNode.reportLoopCount(this, visited);
return result;
}
}
public abstract static class findChild extends ExternalMethod1Node {
@Child private ApplyVmFunction1Node applyPredicate = ApplyVmFunction1Node.create();
@Child private IndirectCallNode callNode = IndirectCallNode.create();
@Specialization
protected VmTyped eval(VmTyped self, VmFunction predicate) {
var result = findFirstChild(self, new PredicateMatcher(predicate, applyPredicate), callNode);
if (result == null) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder().evalError("cannotFindMatchingChildNode").build();
}
return result;
}
}
public abstract static class findChildOrNull extends ExternalMethod1Node {
@Child private ApplyVmFunction1Node applyPredicate = ApplyVmFunction1Node.create();
@Child private IndirectCallNode callNode = IndirectCallNode.create();
@Specialization
protected Object eval(VmTyped self, VmFunction predicate) {
return VmNull.lift(
findFirstChild(self, new PredicateMatcher(predicate, applyPredicate), callNode));
}
}
public abstract static class findChildren extends ExternalMethod1Node {
@Child private ApplyVmFunction1Node applyPredicate = ApplyVmFunction1Node.create();
@Child private IndirectCallNode callNode = IndirectCallNode.create();
@Specialization
protected VmList eval(VmTyped self, VmFunction predicate) {
return VmList.create(
findMatchingChildren(
this, self, new PredicateMatcher(predicate, applyPredicate), callNode));
}
}
public abstract static class findChildOfType extends ExternalMethod1Node {
@Child private IndirectCallNode callNode = IndirectCallNode.create();
@Specialization
protected VmTyped eval(VmTyped self, String type) {
var result = findFirstChild(self, new TypeMatcher(type), callNode);
if (result == null) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder().evalError("cannotFindChildNodeOfType", type).build();
}
return result;
}
}
public abstract static class findChildOfTypeOrNull extends ExternalMethod1Node {
@Child private IndirectCallNode callNode = IndirectCallNode.create();
@Specialization
protected Object eval(VmTyped self, String type) {
return VmNull.lift(findFirstChild(self, new TypeMatcher(type), callNode));
}
}
public abstract static class findChildrenOfType extends ExternalMethod1Node {
@Child private IndirectCallNode callNode = IndirectCallNode.create();
@Specialization
protected VmList eval(VmTyped self, String type) {
return VmList.create(findMatchingChildren(this, self, new TypeMatcher(type), callNode));
}
}
public abstract static class replaceChild extends ExternalMethod2Node {
@Child private ApplyVmFunction1Node applyPredicate = ApplyVmFunction1Node.create();
@Child private IndirectCallNode callNode = IndirectCallNode.create();
@Specialization
protected VmTyped eval(VmTyped self, VmFunction predicate, VmFunction replacer) {
var matcher = new PredicateMatcher(predicate, applyPredicate);
return replaceMatchingChildren(self, matcher, true, replacer, callNode);
}
}
public abstract static class replaceChildren extends ExternalMethod2Node {
@Child private ApplyVmFunction1Node applyPredicate = ApplyVmFunction1Node.create();
@Child private IndirectCallNode callNode = IndirectCallNode.create();
@Specialization
protected VmTyped eval(VmTyped self, VmFunction predicate, VmFunction replacer) {
var matcher = new PredicateMatcher(predicate, applyPredicate);
return replaceMatchingChildren(self, matcher, false, replacer, callNode);
}
}
public abstract static class replaceChildOfType extends ExternalMethod2Node {
@Child private IndirectCallNode callNode = IndirectCallNode.create();
@Specialization
protected VmTyped eval(VmTyped self, String type, VmFunction replacer) {
return replaceMatchingChildren(self, new TypeMatcher(type), true, replacer, callNode);
}
}
public abstract static class replaceChildrenOfType extends ExternalMethod2Node {
@Child private IndirectCallNode callNode = IndirectCallNode.create();
@Specialization
protected VmTyped eval(VmTyped self, String type, VmFunction replacer) {
return replaceMatchingChildren(self, new TypeMatcher(type), false, replacer, callNode);
}
}
public abstract static class findParent extends ExternalMethod1Node {
@Child private ApplyVmFunction1Node applyPredicate = ApplyVmFunction1Node.create();
@Specialization
protected VmTyped eval(VmTyped self, VmFunction predicate) {
var result = findFirstParent(this, self, new PredicateMatcher(predicate, applyPredicate));
if (result == null) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder().evalError("cannotFindMatchingParentNode").build();
}
return result;
}
}
public abstract static class findParentOrNull extends ExternalMethod1Node {
@Child private ApplyVmFunction1Node applyPredicate = ApplyVmFunction1Node.create();
@Specialization
protected Object eval(VmTyped self, VmFunction predicate) {
return VmNull.lift(
findFirstParent(this, self, new PredicateMatcher(predicate, applyPredicate)));
}
}
public abstract static class findParentOfType extends ExternalMethod1Node {
@Specialization
protected VmTyped eval(VmTyped self, String type) {
var result = findFirstParent(this, self, new TypeMatcher(type));
if (result == null) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder().evalError("cannotFindParentNodeOfType", type).build();
}
return result;
}
}
public abstract static class findParentOfTypeOrNull extends ExternalMethod1Node {
@Specialization
protected Object eval(VmTyped self, String type) {
return VmNull.lift(findFirstParent(this, self, new TypeMatcher(type)));
}
}
public abstract static class findParents extends ExternalMethod1Node {
@Child private ApplyVmFunction1Node applyPredicate = ApplyVmFunction1Node.create();
@Specialization
protected VmList eval(VmTyped self, VmFunction predicate) {
var matcher = new PredicateMatcher(predicate, applyPredicate);
var matches = VmList.EMPTY.builder();
var visited = 0;
for (var node = parentOf(self); node != null; node = parentOf(node)) {
visited += 1;
if (matcher.matches(node)) {
matches.add(node);
}
}
LoopNode.reportLoopCount(this, visited);
return matches.build();
}
}
public abstract static class hasParent extends ExternalMethod1Node {
@Child private ApplyVmFunction1Node applyPredicate = ApplyVmFunction1Node.create();
@Specialization
protected boolean eval(VmTyped self, VmFunction predicate) {
return findFirstParent(this, self, new PredicateMatcher(predicate, applyPredicate)) != null;
}
}
public abstract static class hasParentOfType extends ExternalMethod1Node {
@Specialization
protected boolean eval(VmTyped self, String type) {
return findFirstParent(this, self, new TypeMatcher(type)) != null;
}
}
/** Decides whether a node is a match for a search. */
private interface NodeMatcher {
boolean matches(VmTyped node);
}
/** Matches the nodes a Pkl predicate accepts. */
private record PredicateMatcher(VmFunction predicate, ApplyVmFunction1Node applyPredicate)
implements NodeMatcher {
@Override
public boolean matches(VmTyped node) {
return applyPredicate.executeBoolean(predicate, node);
}
}
/** Matches the nodes of a given type. */
private record TypeMatcher(String type) implements NodeMatcher {
@Override
public boolean matches(VmTyped node) {
return type.equals(VmUtils.readMember(node, Identifier.TYPE));
}
}
private static @Nullable VmTyped findFirstChild(
VmTyped self, NodeMatcher matcher, IndirectCallNode callNode) {
var pending = new ArrayDeque<VmTyped>();
pushChildren(pending, self, callNode);
while (!pending.isEmpty()) {
var node = pending.pop();
if (matcher.matches(node)) {
return node;
}
pushChildren(pending, node, callNode);
}
return null;
}
/**
* Collect the descendants of {@code self} matching {@code matcher}, searching depth-first in
* pre-order.
*
* <p>A match is searched for further matches, so a match may contain another.
*/
private static List<VmTyped> findMatchingChildren(
Node owner, VmTyped self, NodeMatcher matcher, IndirectCallNode callNode) {
List<VmTyped> matches = new ArrayList<>();
var pending = new ArrayDeque<VmTyped>();
pushChildren(pending, self, callNode);
var visited = 0;
while (!pending.isEmpty()) {
var node = pending.pop();
visited += 1;
if (matcher.matches(node)) {
matches.add(node);
}
pushChildren(pending, node, callNode);
}
LoopNode.reportLoopCount(owner, visited);
return matches;
}
private static void pushChildren(
ArrayDeque<VmTyped> pending, VmTyped node, IndirectCallNode callNode) {
var children = (VmList) VmUtils.readMember(node, Identifier.CHILDREN, callNode);
for (var i = children.getLength() - 1; i >= 0; i--) {
pending.push((VmTyped) children.get(i));
}
}
/**
* Returns {@code self} rebuilt with the descendants matching {@code matcher} replaced by {@code
* replacer}'s results, searching depth-first in pre-order and stopping at the first match if
* {@code firstOnly}.
*
* <p>A match is not searched for further matches.
*/
private static VmTyped replaceMatchingChildren(
VmTyped self,
NodeMatcher matcher,
boolean firstOnly,
VmFunction replacer,
IndirectCallNode callNode) {
var search = new ReplaceSearch(matcher, firstOnly, replacer, callNode);
return SyntaxNodes.rebuild(self, search.replaceBelow(self));
}
/** Finds and replaces the matches of a {@code Node.replaceChild*} call. */
private static final class ReplaceSearch {
private final NodeMatcher matcher;
private final boolean firstOnly;
private final VmFunction replacer;
private final IndirectCallNode callNode;
private boolean done;
private ReplaceSearch(
NodeMatcher matcher, boolean firstOnly, VmFunction replacer, IndirectCallNode callNode) {
this.matcher = matcher;
this.firstOnly = firstOnly;
this.replacer = replacer;
this.callNode = callNode;
}
/**
* Returns the children of {@code node} with the matches below it replaced, or {@code null} if
* nothing below it matches.
*
* <p>Each element is either a {@link Rebuild} or a node to keep as it is.
*/
@TruffleBoundary
private Object @Nullable [] replaceBelow(VmTyped node) {
var children = (VmList) VmUtils.readMember(node, Identifier.CHILDREN, callNode);
Object @Nullable [] result = null;
for (var i = 0; i < children.getLength() && !done; i++) {
var child = (VmTyped) children.get(i);
Object replacement;
if (matcher.matches(child)) {
replacement = replacer.apply(child);
done = firstOnly;
} else {
var grandchildren = replaceBelow(child);
replacement = grandchildren == null ? null : new Rebuild(child, grandchildren);
}
if (replacement != null) {
if (result == null) {
result = children.toArray();
}
result[i] = replacement;
}
}
return result;
}
}
private static @Nullable VmTyped findFirstParent(Node owner, VmTyped self, NodeMatcher matcher) {
VmTyped result = null;
var visited = 0;
for (var node = parentOf(self); node != null; node = parentOf(node)) {
visited += 1;
if (matcher.matches(node)) {
result = node;
break;
}
}
LoopNode.reportLoopCount(owner, visited);
return result;
}
private static @Nullable VmTyped parentOf(VmTyped node) {
return (VmTyped) VmNull.unwrap(VmUtils.readMember(node, Identifier.PARENT));
}
}
@@ -0,0 +1,146 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.stdlib.syntax;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.IndirectCallNode;
import org.jspecify.annotations.Nullable;
import org.pkl.core.runtime.Identifier;
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.syntax.SyntaxNodes.NodeData;
import org.pkl.parser.GenericParser;
import org.pkl.parser.GenericParserError;
/** Backs the {@code parse*} methods of {@code pkl.syntax#Parser}. */
public final class ParserNodes {
private ParserNodes() {}
private static GenericParser parser = new GenericParser();
public abstract static class parseModule extends ExternalMethod1Node {
@Specialization
protected Object evalString(VmTyped ignored, String source) {
try {
return parseModuleNode(source, null);
} catch (GenericParserError e) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder().evalError("parserError").withHint(e.toString()).build();
}
}
@Specialization
protected Object evalResource(
VmTyped ignored, VmTyped source, @Cached("create()") IndirectCallNode callNode) {
var text = (String) VmUtils.readMember(source, Identifier.TEXT, callNode);
var uri = (String) VmUtils.readMember(source, Identifier.URI, callNode);
try {
return parseModuleNode(text, uri);
} catch (GenericParserError e) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder().evalError("parserError").withHint(e.toString()).build();
}
}
}
public abstract static class parseModuleOrNull extends ExternalMethod1Node {
@Specialization
protected Object evalString(VmTyped ignored, String source) {
try {
return parseModuleNode(source, null);
} catch (GenericParserError e) {
return VmNull.withoutDefault();
}
}
@Specialization
protected Object evalResource(
VmTyped ignored, VmTyped source, @Cached("create()") IndirectCallNode callNode) {
var text = (String) VmUtils.readMember(source, Identifier.TEXT, callNode);
var uri = (String) VmUtils.readMember(source, Identifier.URI, callNode);
try {
return parseModuleNode(text, uri);
} catch (GenericParserError e) {
return VmNull.withoutDefault();
}
}
}
public abstract static class parseExpression extends ExternalMethod1Node {
@Specialization
protected Object evalString(VmTyped ignored, String source) {
try {
return parseExpressionNode(source, null);
} catch (GenericParserError e) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder().evalError("parserError").withHint(e.toString()).build();
}
}
@Specialization
protected Object evalResource(
VmTyped ignored, VmTyped source, @Cached("create()") IndirectCallNode callNode) {
var text = (String) VmUtils.readMember(source, Identifier.TEXT, callNode);
var uri = (String) VmUtils.readMember(source, Identifier.URI, callNode);
try {
return parseExpressionNode(text, uri);
} catch (GenericParserError e) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder().evalError("parserError").withHint(e.toString()).build();
}
}
}
public abstract static class parseExpressionOrNull extends ExternalMethod1Node {
@Specialization
protected Object evalString(VmTyped ignored, String source) {
try {
return parseExpressionNode(source, null);
} catch (GenericParserError e) {
return VmNull.withoutDefault();
}
}
@Specialization
protected Object evalResource(
VmTyped ignored, VmTyped source, @Cached("create()") IndirectCallNode callNode) {
var text = (String) VmUtils.readMember(source, Identifier.TEXT, callNode);
var uri = (String) VmUtils.readMember(source, Identifier.URI, callNode);
try {
return parseExpressionNode(text, uri);
} catch (GenericParserError e) {
return VmNull.withoutDefault();
}
}
}
@TruffleBoundary
private static VmTyped parseModuleNode(String src, @Nullable String sourceUri) {
var root = parser.parseModule(src);
return SyntaxNodes.createNode(new NodeData(root, src.toCharArray(), sourceUri, null));
}
@TruffleBoundary
private static VmTyped parseExpressionNode(String src, @Nullable String sourceUri) {
var root = parser.parseExpressionInput(src);
return SyntaxNodes.createNode(new NodeData(root, src.toCharArray(), sourceUri, null));
}
}
@@ -0,0 +1,305 @@
/*
* 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.nodes.ExplodeLoop;
import com.oracle.truffle.api.nodes.IndirectCallNode;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.jspecify.annotations.Nullable;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.SyntaxModule;
import org.pkl.core.runtime.VmContext;
import org.pkl.core.runtime.VmList;
import org.pkl.core.runtime.VmNull;
import org.pkl.core.runtime.VmTyped;
import org.pkl.core.runtime.VmUtils;
import org.pkl.core.stdlib.VmObjectFactory;
import org.pkl.parser.syntax.generic.FullSpan;
import org.pkl.parser.syntax.generic.Node;
import org.pkl.parser.syntax.generic.NodeType;
public final class SyntaxNodes {
private SyntaxNodes() {}
static final FullSpan ZERO_SPAN = new FullSpan(0, 0, 0, 0, 0, 0);
record SpanData(FullSpan span, @Nullable String sourceUri) {}
private record SourceLocationData(int line, int column, @Nullable String sourceUri) {}
private static final VmObjectFactory<SourceLocationData> sourceLocationFactory =
new VmObjectFactory<SourceLocationData>(SyntaxModule::getSourceLocationClass)
.addIntProperty("line", SourceLocationData::line)
.addIntProperty("column", SourceLocationData::column)
.addStringProperty(
"displayUri", sl -> displayUri(sl.sourceUri(), sl.line(), sl.column()));
static final VmObjectFactory<SpanData> spanFactory =
new VmObjectFactory<SpanData>(SyntaxModule::getSpanClass)
.addTypedProperty(
"start",
sd ->
sourceLocationFactory.create(
new SourceLocationData(
sd.span().lineBegin(), sd.span().colBegin(), sd.sourceUri())))
.addTypedProperty(
"end",
sd ->
sourceLocationFactory.create(
new SourceLocationData(
sd.span().lineEnd(), sd.span().colEnd(), sd.sourceUri())))
.addStringProperty(
"displayUri",
sd ->
displayUri(
sd.sourceUri(),
sd.span().lineBegin(),
sd.span().colBegin(),
sd.span().lineEnd(),
sd.span().colEnd()));
private static String displayUri(@Nullable String sourceUri, int line, int column) {
return displayUri(sourceUri, line, column, line, column);
}
@TruffleBoundary
private static String displayUri(
@Nullable String sourceUri, int startLine, int startColumn, int endLine, int endColumn) {
if (sourceUri == null) {
return "";
}
return VmUtils.getDisplayUri(
sourceUri,
startLine,
startColumn,
endLine,
endColumn,
VmContext.get(null).getFrameTransformer());
}
/** Extra storage backing a Pkl {@code Node} parsed from source. */
static final class NodeData {
final Node node;
// the source {@link #node} was parsed from
final char[] source;
// the URI of {@link #source}, or {@code null} if unknown
private final @Nullable String sourceUri;
private final @Nullable VmTyped parentVm;
// the object this storage backs, so that lazily created children can be parented at it
private @Nullable VmTyped selfVm;
NodeData(Node node, char[] source, @Nullable String sourceUri, @Nullable VmTyped parentVm) {
this.node = node;
this.source = source;
this.sourceUri = sourceUri;
this.parentVm = parentVm;
}
String text() {
return node.text(source);
}
@ExplodeLoop
VmList children() {
var childNodes = node.children;
if (childNodes.isEmpty()) {
return VmList.EMPTY;
}
var builder = VmList.EMPTY.builder();
for (var childNode : childNodes) {
builder.add(createNode(new NodeData(childNode, source, sourceUri, selfVm)));
}
return builder.build();
}
VmTyped span() {
return spanFactory.create(new SpanData(node.span, sourceUri));
}
}
static final VmObjectFactory<NodeData> genericNodeFactory =
new VmObjectFactory<NodeData>(SyntaxModule::getNodeClass)
.addStringProperty("type", nd -> nd.node.type.name().toLowerCase(Locale.ROOT))
.addListProperty("children", NodeData::children)
.addProperty("parent", nd -> VmNull.lift(nd.parentVm))
.addProperty("text", NodeData::text)
.addProperty("span", NodeData::span);
/** Create the Pkl {@code Node} backed by {@code data}. */
static VmTyped createNode(NodeData data) {
var result = genericNodeFactory.create(data);
data.selfVm = result;
return result;
}
/**
* A node to rebuild with new children.
*
* <p>Each of {@code children} is either a {@code Rebuild} or a node to keep as it is.
*/
record Rebuild(VmTyped basis, Object[] children) {}
/**
* Extra storage backing a Pkl {@code Node} rebuilt from {@link #build(VmTyped, Object[],
* VmTyped)}.
*/
private static final class BuiltNodeData {
private final @Nullable VmTyped parentVm;
// set right after the node is created, so that the children can be parented at it
private VmList children = VmList.EMPTY;
private BuiltNodeData(@Nullable VmTyped parentVm) {
this.parentVm = parentVm;
}
}
private static final VmObjectFactory<BuiltNodeData> builtNodeFactory =
new VmObjectFactory<BuiltNodeData>(SyntaxModule::getNodeClass)
.addListProperty("children", bd -> bd.children)
.addProperty("parent", bd -> VmNull.lift(bd.parentVm));
/**
* Build the tree rooted at {@code root}, with its children replaced by {@code children} if not
* {@code null}.
*
* <p>The returned root has no parent, and every other node is parented in the returned tree. The
* tree {@code root} is left untouched.
*/
@TruffleBoundary
static VmTyped rebuild(VmTyped root, Object @Nullable [] children) {
return children == null ? reparent(root, null) : build(root, children, null);
}
private static VmTyped build(VmTyped basis, Object[] children, @Nullable VmTyped parent) {
var data = new BuiltNodeData(parent);
var result = builtNodeFactory.create(data, basis);
var listBuilder = VmList.EMPTY.builder();
for (var child : children) {
var newChild =
child instanceof Rebuild rebuild
? build(rebuild.basis(), rebuild.children(), result)
: reparent((VmTyped) child, result);
listBuilder.add(newChild);
}
// A node and its children point at each other, so neither can be complete before the other is
// created. This node is therefore created first, without children, so its children can be built
// with it as their parent, and only then are they set here.
// This is safe because factory properties are evaluated lazily, on first read, and no Pkl code
// can see this node before `build` returns, so `children` can't be read before it's set.
data.children = listBuilder.build();
return result;
}
/** Copy {@code node} and its subtree to sit below {@code parent}. */
private static VmTyped reparent(VmTyped node, @Nullable VmTyped parent) {
// a parsed node's subtree is created lazily from its parse-time node, so only the node is
// copied
if (node.hasExtraStorage() && node.getExtraStorage() instanceof NodeData data) {
return createNode(new NodeData(data.node, data.source, data.sourceUri, parent));
}
var children = (VmList) VmUtils.readMember(node, Identifier.CHILDREN);
return build(node, children.toArray(), parent);
}
/**
* Convert a Pkl {@code Node} to a generic {@link Node}, reusing the parse-time node when present.
*
* <p>{@code fallbackSpan} is used for constructed nodes (and their descendants) that carry no
* meaningful span of their own, so that a subtree spliced into reused siblings lines up with
* them.
*/
@TruffleBoundary
static Node convertVmToNode(VmTyped nodeVm, FullSpan fallbackSpan, IndirectCallNode callNode) {
if (nodeVm.hasExtraStorage()) {
var storage = nodeVm.getExtraStorage();
// a node still carrying its parse-time storage is verbatim from `parse`: reuse it wholesale
if (storage instanceof NodeData data) {
materializeText(data.node, data.source);
return data.node;
}
}
var typeStr = (String) VmUtils.readMember(nodeVm, Identifier.TYPE, callNode);
var nodeType = NodeType.valueOf(typeStr.toUpperCase(Locale.ROOT));
var ownSpan = readSpan(optSpan(nodeVm, callNode), callNode);
// a constructed node that did not set its own span inherits the insertion point's span
var span = ownSpan.equals(ZERO_SPAN) ? fallbackSpan : ownSpan;
var childrenVm = (VmList) VmUtils.readMember(nodeVm, Identifier.CHILDREN, callNode);
var children = new ArrayList<Node>(childrenVm.getLength());
for (var i = 0; i < childrenVm.getLength(); i++) {
children.add(convertVmToNode((VmTyped) childrenVm.get(i), span, callNode));
}
return makeJavaNode(
nodeType, span, children, VmUtils.readMember(nodeVm, Identifier.TEXT, callNode));
}
private static @Nullable VmTyped optSpan(VmTyped nodeVm, IndirectCallNode callNode) {
return (VmTyped) VmNull.unwrap(VmUtils.readMember(nodeVm, Identifier.SPAN, callNode));
}
private static FullSpan readSpan(@Nullable VmTyped spanVm, IndirectCallNode callNode) {
if (spanVm == null) {
return ZERO_SPAN;
}
var start = (VmTyped) VmUtils.readMember(spanVm, Identifier.START, callNode);
var end = (VmTyped) VmUtils.readMember(spanVm, Identifier.END, callNode);
return new FullSpan(
0,
0,
readPosition(start, Identifier.LINE, callNode),
readPosition(start, Identifier.COLUMN, callNode),
readPosition(end, Identifier.LINE, callNode),
readPosition(end, Identifier.COLUMN, callNode));
}
private static int readPosition(
VmTyped sourceLocationVm, Identifier name, IndirectCallNode callNode) {
return ((Long) VmUtils.readMember(sourceLocationVm, name, callNode)).intValue();
}
private static Node makeJavaNode(
NodeType nodeType, FullSpan span, List<Node> children, Object textObj) {
var node = children.isEmpty() ? new Node(nodeType, span) : new Node(nodeType, span, children);
if (textObj instanceof String text) {
node.setText(text);
}
return node;
}
/**
* Materialize the text of the nodes in {@code node}'s subtree that the formatter reads directly.
*
* <p>{@link org.pkl.formatter.Formatter#format(Node)} has no access to the source, so a subtree
* reused verbatim from a parse must carry its own text by the time it is handed over.
*/
private static void materializeText(Node node, char[] source) {
// `string_constant` is read by the formatter but is not a leaf
if (node.children.isEmpty() || node.type == NodeType.STRING_CONSTANT) {
node.text(source);
}
for (var child : node.children) {
materializeText(child, source);
}
}
}
@@ -0,0 +1,4 @@
@NullMarked
package org.pkl.core.stdlib.syntax;
import org.jspecify.annotations.NullMarked;
@@ -1226,6 +1226,24 @@ Redirected to: `{1}`
invalidReferenceTypeAnnotationWithConstraint=\
`Reference` referent type argument may not include type constraints.
parserError=\
Could not parse Pkl source.
cannotRenderSyntaxNode=\
Could not render node of type `{0}` as Pkl source because it is not a well-formed syntax tree.
cannotFindMatchingChildNode=\
Cannot find a child node matching the predicate.
cannotFindChildNodeOfType=\
Cannot find a child node of type `{0}`.
cannotFindMatchingParentNode=\
Cannot find a parent node matching the predicate.
cannotFindParentNodeOfType=\
Cannot find a parent node of type `{0}`.
cannotInstallPackageWithNoCache=\
Cannot install package to module cache dir when module cache is disabled.
@@ -0,0 +1,5 @@
x = 1
foo {
bar = "baz"
}
@@ -0,0 +1,5 @@
import "pkl:syntax"
local parser = new syntax.Parser {}
res = parser.parseExpression("1 +")
@@ -0,0 +1,12 @@
import "pkl:syntax"
local parser = new syntax.Parser {}
res =
parser.parseModule(
"""
foo {
bar =
}
""",
)
@@ -0,0 +1,8 @@
import "pkl:syntax"
mod =
new syntax.Parser {}.parseModule(
"""
import "foo\nbar"
""",
)
@@ -0,0 +1,257 @@
amends "../snippetTest.pkl"
import "pkl:syntax"
import "pkl:test"
local parser = new syntax.Parser {}
local renderer = new syntax.FormattingRenderer {}
local function mod(source: String): syntax.Node = parser.parseModule(source)
local function intLiteral(value: Int): syntax.Node = new syntax.Node {
type = "int_literal_expr"
text = value.toString()
}
// a node built from scratch, whose children have no parent of their own
local constructedAccess = new syntax.Node {
type = "unqualified_access_expr"
children = List(new syntax.Node { type = "identifier"; text = "foo" })
}
local sample: syntax.Node =
mod(
"""
class Point {
x: Int
y: Int
}
origin = new Point { x = 0; y = 0 }
""",
)
facts {
["findChild returns the first match in pre-order"] {
sample.findChild((n) -> n.type == "identifier").text == "Point"
sample.findChild((n) -> n.type == "int_literal_expr").text == "0"
}
["findChild throws when nothing matches"] {
test
.catch(() -> sample.findChild((n) -> n.type == "when_generator"))
.contains("Cannot find a child node matching the predicate")
}
["findChildOrNull returns null when nothing matches"] {
sample.findChildOrNull((n) -> n.type == "when_generator") == null
sample.findChildOrNull((n) -> n.type == "identifier")!!.text == "Point"
}
["findChild does not consider this node"] {
sample.findChildOrNull((n) -> n.type == "module") == null
sample.type == "module"
}
["findChildren returns every match in pre-order"] {
sample
.findChildren((n) -> n.type == "identifier")
.map((n) -> n.text)
== List("Point", "x", "Int", "y", "Int", "origin", "Point", "x", "y")
sample.findChildren((n) -> n.type == "when_generator") == List()
}
["findChildren searches a match for further matches"] {
// the outer object body contains the two entries, which are themselves matches
mod("foo { bar { baz = 1 } }").findChildren((n) -> n.type == "object_body").length == 2
}
["findChildOfType agrees with findChild"] {
sample.findChildOfType("class").text == sample.findChild((n) -> n.type == "class").text
sample.findChildrenOfType("identifier").length == 9
}
["findChildOfType throws when nothing matches"] {
test
.catch(() -> sample.findChildOfType("when_generator"))
.contains("Cannot find a child node of type `when_generator`")
}
["findChildOfTypeOrNull returns null when nothing matches"] {
sample.findChildOfTypeOrNull("when_generator") == null
sample.findChildOfTypeOrNull("class")!!.text == sample.findChildOfType("class").text
}
["findParent returns the nearest match"] {
local inner = mod("foo { bar { baz = 1 } }").findChild((n) -> n.text == "baz")
inner.findParent((n) -> n.type == "object_body").text == "{ baz = 1 }"
inner.findParent((n) -> n.type == "module").type == "module"
test
.catch(() -> inner.findParent((n) -> n.type == "class"))
.contains("Cannot find a parent node matching the predicate")
}
["findParentOrNull returns null when nothing matches"] {
local inner = mod("foo { bar { baz = 1 } }").findChild((n) -> n.text == "baz")
inner.findParentOrNull((n) -> n.type == "class") == null
inner.findParentOrNull((n) -> n.type == "object_body")!!.text == "{ baz = 1 }"
}
["findParentOfType agrees with findParent"] {
local inner = mod("foo { bar { baz = 1 } }").findChild((n) -> n.text == "baz")
inner.findParentOfType("object_body").text == "{ baz = 1 }"
test
.catch(() -> inner.findParentOfType("class"))
.contains("Cannot find a parent node of type `class`")
}
["findParentOfTypeOrNull returns null when nothing matches"] {
local inner = mod("foo { bar { baz = 1 } }").findChild((n) -> n.text == "baz")
inner.findParentOfTypeOrNull("class") == null
inner.findParentOfTypeOrNull("object_body")!!.text == "{ baz = 1 }"
}
["findParents returns every match, nearest first"] {
local inner = mod("foo { bar { baz = 1 } }").findChild((n) -> n.text == "baz")
inner
.findParents((n) -> n.type == "object_body")
.map((n) -> n.text)
== List("{ baz = 1 }", "{ bar { baz = 1 } }")
}
["parent lookups do not consider this node"] {
sample.findParentOrNull((n) -> n.type == "module") == null
sample.findParents((n) -> true) == List()
!sample.hasParentOfType("module")
}
["hasParent and hasParentOfType follow findParent"] {
local inner = mod("foo { bar = 1 }").findChild((n) -> n.text == "bar")
inner.hasParent((n) -> n.type == "object_body")
inner.hasParentOfType("class_property")
!inner.hasParentOfType("class")
}
["replaceChild replaces only the first match"] {
renderer.render(
mod("x = 1 + 1").replaceChild((n) -> n.type == "int_literal_expr", (_n) -> intLiteral(9)),
) == "x = 9 + 1\n"
}
["replaceChild leaves the tree unchanged when nothing matches"] {
renderer.render(
mod("x = 1").replaceChild((n) -> n.type == "when_generator", (_n) -> intLiteral(9)),
) == "x = 1\n"
}
["replaceChildren replaces every match"] {
renderer.render(
mod("x = 1 + 1").replaceChildren((n) -> n.type == "int_literal_expr", (_n) -> intLiteral(9)),
) == "x = 9 + 9\n"
}
["a replacer sees the node it replaces"] {
renderer.render(
mod("x = 1 + 2")
.replaceChildren(
(n) -> n.type == "int_literal_expr",
(n) -> intLiteral(n.text!!.toInt() * 10),
),
) == "x = 10 + 20\n"
}
["a replacement is not searched for further matches"] {
// replacing an identifier with an identifier terminates instead of looping
renderer.render(
mod("foo = 1").replaceChildrenOfType("identifier", (n) -> (n) { text = "\(n.text!!)Bar" }),
) == "fooBar = 1\n"
}
["replaceChildOfType agrees with replaceChild"] {
renderer.render(mod("x = 1 + 1").replaceChildOfType("int_literal_expr", (_n) -> intLiteral(9)))
== renderer.render(
mod("x = 1 + 1").replaceChild((n) -> n.type == "int_literal_expr", (_n) -> intLiteral(9)),
)
}
["a replacer sees where its node sits in the tree being built"] {
local result =
mod("x = 1")
.replaceChildOfType("int_literal_expr", (n) ->
if (n.hasParentOfType("class_property")) intLiteral(9) else intLiteral(0)
)
renderer.render(result) == "x = 9\n"
}
["a replace does not change the tree it walks"] {
local original = mod("x = 1")
local rewritten = original.replaceChildrenOfType("int_literal_expr", (_n) -> intLiteral(9))
let (rendered = renderer.render(rewritten))
rendered == "x = 9\n" && renderer.render(original) == "x = 1\n"
}
["the root of a replaced tree has no parent"] {
mod("x = 1").replaceChildrenOfType("int_literal_expr", (_n) -> intLiteral(9)).parent == null
mod("x = 1").replaceChildrenOfType("when_generator", (_n) -> intLiteral(9)).parent == null
}
["only the root of a replaced tree has no parent"] {
mod("x = 1")
.replaceChildrenOfType("int_literal_expr", (_n) -> intLiteral(9))
.fold(0, (acc, n) -> if (n.parent == null) acc + 1 else acc) == 1
// the constructed node's child has no parent of its own, so the returned tree must supply one
mod("x = 1")
.replaceChildrenOfType("int_literal_expr", (_) -> constructedAccess)
.fold(0, (acc, n) -> if (n.parent == null) acc + 1 else acc) == 1
}
["replacing below a node does not detach it from its own tree"] {
local property = mod("x = 1").findChildOfType("class_property")
local replaced = property.replaceChildrenOfType("int_literal_expr", (_) -> intLiteral(9))
replaced.parent == null && property.parent != null
}
["a replacer sees this tree, not the one being built"] {
// both literals see their unreplaced sibling
local sumOfSiblings = (n: syntax.Node) ->
intLiteral(
n.parent!!
.findChildrenOfType("int_literal_expr")
.fold(0, (acc, c) -> acc + c.text!!.toInt()),
)
renderer.render(mod("x = 1 + 2").replaceChildrenOfType("int_literal_expr", sumOfSiblings))
== "x = 3 + 3\n"
}
["the children of an amended replacement are parented at the replacement"] {
mod("x = 1 + 2")
.replaceChildOfType("binary_op_expr", (n) -> (n) { text = "changed" })
.findChildOfType("int_literal_expr")
.parent!!.text
== "changed"
}
["rebuilds ancestors of a replacement"] {
renderer.render(
mod("x = if (cond) 41 else 0")
.replaceChildrenOfType("int_literal_expr", (n) -> intLiteral(n.text!!.toInt() + 1)),
) == "x = if (cond) 42 else 1\n"
}
["a match is not searched for further matches"] {
local predicate = (n: syntax.Node) ->
if (n.type == "int_literal_expr")
throw("searched inside a match")
else
n.type == "parenthesized_expr"
renderer.render(mod("x = (1)").replaceChildren(predicate, (_) -> intLiteral(9))) == "x = 9\n"
}
["replacement is eager"] {
test
.catch(() -> mod("x = 1").replaceChildrenOfType("int_literal_expr", (_) -> throw("boom")))
.contains("boom")
}
}
@@ -0,0 +1,413 @@
amends "../snippetTest.pkl"
import "pkl:syntax"
import "pkl:test"
local renderer = new syntax.FormattingRenderer {}
local function roundTrip(source: String) = renderer.render(parseNode(source))
local function parseNode(source: String): syntax.Node =
new syntax.Parser {}.parseModule(source)
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))
}
facts {
["empty module"] {
roundTrip("") == "\n"
}
["simple property"] {
roundTrip("x = 1") == "x = 1\n"
}
["multiple properties"] {
roundTrip(
"""
x = 1
y = 2
""",
)
== """
x = 1
y = 2
"""
}
["string literals"] {
roundTrip(#"x = "hello""#) == #"x = "hello"\#n"#
roundTrip(#"x = "hello\nworld""#) == #"x = "hello\nworld"\#n"#
roundTrip(#"x = "hello \(name)""#) == #"x = "hello \(name)"\#n"#
}
["multiline string"] {
roundTrip(
#"""
x = """
hello
world
"""
"""#,
)
== #"""
x =
"""
hello
world
"""
"""#
}
["comments"] {
roundTrip(
"""
// this is a comment
x = 1
""",
)
== """
// this is a comment
x = 1
"""
}
["doc comments"] {
roundTrip(
"""
/// A documented property.
x = 1
""",
)
== """
/// A documented property.
x = 1
"""
}
["class declaration"] {
roundTrip(
"""
class Bird {
name: String
canFly: Boolean = true
}
""",
)
== """
class Bird {
name: String
canFly: Boolean = true
}
"""
}
["module declaration"] {
roundTrip("module my.app") == "module my.app\n"
}
["imports"] {
roundTrip(
"""
import "foo.pkl"
import "bar.pkl" as myBar
""",
)
== """
import "bar.pkl" as myBar
import "foo.pkl"
"""
}
["typealias"] {
roundTrip("typealias Positive = Int(this>0)") == "typealias Positive = Int(this > 0)\n"
}
["boolean literals"] {
roundTrip("x = true") == "x = true\n"
roundTrip("x = false") == "x = false\n"
}
["numeric literals"] {
roundTrip("x = 42") == "x = 42\n"
roundTrip("x = 3.14") == "x = 3.14\n"
roundTrip("x = 0xFF") == "x = 0xFF\n"
}
["null literal"] {
roundTrip("x = null") == "x = null\n"
}
["keyword expressions"] {
roundTrip("x = this") == "x = this\n"
roundTrip("x = outer") == "x = outer\n"
roundTrip("x = module") == "x = module\n"
}
["binary operators"] {
roundTrip("x = 1 + 2") == "x = 1 + 2\n"
roundTrip("x = a && b") == "x = a && b\n"
roundTrip("x = a is String") == "x = a is String\n"
}
["unary operators"] {
roundTrip("x = -1") == "x = -1\n"
roundTrip("x = !flag") == "x = !flag\n"
roundTrip("x = value!!") == "x = value!!\n"
}
["if expression"] {
roundTrip("x = if(a) b else c") == "x = if (a) b else c\n"
}
["function literal"] {
roundTrip("x = (a) -> a + 1") == "x = (a) -> a + 1\n"
}
["method declaration"] {
roundTrip("function greet(name: String): String = name")
== "function greet(name: String): String = name\n"
}
["modifiers"] {
roundTrip("hidden x = 1") == "hidden x = 1\n"
roundTrip("local x = 1") == "local x = 1\n"
}
["object body"] {
roundTrip(
"""
x { z ->
a = 1
b = 2
}
""",
)
== """
x { z ->
a = 1
b = 2
}
"""
}
["annotations"] {
roundTrip(
"""
@Deprecated { message = "use other" }
x = 1
""",
)
== """
@Deprecated { message = "use other" }
x = 1
"""
}
["type annotations"] {
roundTrip("x: String = \"hello\"") == "x: String = \"hello\"\n"
roundTrip("x: Int? = null") == "x: Int? = null\n"
roundTrip("x: String|Int = 1") == "x: String | Int = 1\n"
}
["for generator"] {
roundTrip(
"""
x {
for (k, v in items) {
[k] = v
}
}
""",
)
== """
x {
for (k, v in items) {
[k] = v
}
}
"""
}
["when generator"] {
roundTrip(
"""
x {
when (flag) {
a = 1
} else {
b = 2
}
}
""",
)
== """
x {
when (flag) {
a = 1
} else {
b = 2
}
}
"""
}
["let expression"] {
roundTrip("x = let (y = 1) y + 1") == "x = let (y = 1) y + 1\n"
}
["new expression"] {
roundTrip(
"""
x = new Dynamic {
a = 1
}
""",
)
== """
x = new Dynamic {
a = 1
}
"""
}
["qualified access"] {
roundTrip("x = foo.bar.baz") == "x = foo.bar.baz\n"
}
["subscript"] {
roundTrip("x = list[0]") == "x = list[0]\n"
}
["parenthesized expression"] {
roundTrip("x = (1 + 2)") == "x = (1 + 2)\n"
}
["quoted identifiers"] {
roundTrip("const `my prop` = 0") == "const `my prop` = 0\n"
roundTrip("x = `some`.`path`") == "x = `some`.`path`\n"
roundTrip("module `my mod`.`sub pkg`") == "module `my mod`.`sub pkg`\n"
roundTrip("function `do it`(`the arg`: Int) = `the arg`")
== "function `do it`(`the arg`: Int) = `the arg`\n"
roundTrip(
"""
class `My Class` {
`a prop`: String
}
""",
)
== """
class `My Class` {
`a prop`: String
}
"""
}
["modify identifier"] {
local root = parseNode("x = 1")
local modified = replaceLeaf(root, "identifier", "x", "y")
renderer.render(modified) == "y = 1\n"
}
["modify modifier"] {
local root = parseNode("hidden x = 1")
local modified = replaceLeaf(root, "modifier", "hidden", "local")
renderer.render(modified) == "local x = 1\n"
}
["modify string content"] {
local root = parseNode(#"x = "hello""#)
local modified = replaceLeaf(root, "string_chars", "hello", "world")
renderer.render(modified) == #"x = "world"\#n"#
}
["modify int literal"] {
local root = parseNode("x = 42")
local modified = replaceLeaf(root, "int_literal_expr", "42", "99")
renderer.render(modified) == "x = 99\n"
}
["modify boolean literal"] {
local root = parseNode("x = true")
local modified = replaceLeaf(root, "bool_literal_expr", "true", "false")
renderer.render(modified) == "x = false\n"
}
["modify float literal"] {
local root = parseNode("x = 3.14")
local modified = replaceLeaf(root, "float_literal_expr", "3.14", "2.72")
renderer.render(modified) == "x = 2.72\n"
}
["add new modifier"] {
local root = parseNode("local x = 1")
local constModifier = new syntax.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
renderer.render(modified) == """
local const x = 1
"""
}
["a default renderer targets the V2 grammar"] {
local node = parseNode("x = 1")
renderer.render(node) == new syntax.FormattingRenderer { grammarVersion = "V2" }.render(node)
}
["renderer honors the grammar version"] {
local node =
parseNode("x = fn(argument1, argument2, argument3, argument4, argument5, argument6, argument7, argument8, argument9)")
new syntax.FormattingRenderer { grammarVersion = "V1" }.render(node)
!= new syntax.FormattingRenderer { grammarVersion = "V2" }.render(node)
}
["a malformed tree is reported as such"] {
local malformed = new syntax.Node {
type = "object_element"
children = List()
}
test.catch(() -> renderer.render(malformed))
.contains("Could not render node of type `object_element` as Pkl source")
}
}
@@ -0,0 +1,80 @@
amends "../snippetTest.pkl"
import "pkl:syntax"
local parser = new syntax.Parser {}
local stringModule = parser.parseModule("""
x = 1
foo {
bar = "baz"
}
""")
local resourceModule = parser.parseModule(read(".../input-helper/syntax/spans/sample.pkl"))
local function nestedProperty(mod: syntax.Node): syntax.Node =
mod
.fold(List(), (acc: List<syntax.Node>, n) -> if (n.type == "object_property") acc.add(n) else acc)
.last
examples {
["span of a module parsed from a string"] {
stringModule.span
}
["span of a leaf parsed from a string"] {
nestedProperty(stringModule).span
}
["span of a module parsed from a resource carries the resource URI"] {
resourceModule.span
}
["span of a leaf parsed from a resource carries the resource URI"] {
nestedProperty(resourceModule).span
}
}
facts {
["span positions are 1-based"] {
stringModule.span!!.start.line == 1
stringModule.span!!.start.column == 1
}
["end is exclusive"] {
local span = nestedProperty(stringModule).span!!
span.start.line == 4
span.start.column == 3
span.end.line == 4
span.end.column == 14
}
["a child's span is contained in its parent's"] {
local child = nestedProperty(stringModule)
local parent = child.parent!!
parent.span!!.start.line <= child.span!!.start.line
parent.span!!.end.line >= child.span!!.end.line
}
["nodes constructed from scratch have no span"] {
new syntax.Node { type = "int_literal_expr"; text = "42" }.span == null
}
["a spanless constructed node still renders"] {
new syntax.FormattingRenderer {}
.render(new syntax.Node { type = "int_literal_expr"; text = "42" })
.trim() == "42"
}
["parsing the same text from a string and a resource differs only in displayUri"] {
local fromString = stringModule.span!!
local fromResource = resourceModule.span!!
fromString.start.line == fromResource.start.line
fromString.start.column == fromResource.start.column
fromString.end.line == fromResource.end.line
fromString.end.column == fromResource.end.column
fromString.displayUri != fromResource.displayUri
}
}
@@ -0,0 +1,101 @@
amends "../snippetTest.pkl"
import "pkl:syntax"
local function mod(source: String): syntax.Node = new syntax.Parser {}.parseModule(source)
local sample: syntax.Node =
mod(
"""
import "foo.pkl"
import "bar.pkl" as baz
class Point {
x: Int
y: Int
}
origin = if (useOrigin) new Point { x = 0; y = 0 } else null
scaled = origin.x * 2
""",
)
facts {
["fold counts nodes by predicate"] {
sample.fold(0, (acc, n) -> if (n.type == "import") acc + 1 else acc) == 2
sample.fold(0, (acc, n) -> if (n.type == "if_expr") acc + 1 else acc) == 1
sample.fold(0, (acc, n) -> if (n.type == "when_generator") acc + 1 else acc) == 0
}
["fold accumulates into a collection"] {
sample.fold(List(), (acc, n) -> if (n.type == "identifier") acc.add(n.text) else acc)
== List(
"baz",
"Point",
"x",
"Int",
"y",
"Int",
"origin",
"useOrigin",
"Point",
"x",
"y",
"scaled",
"origin",
"x",
)
}
["fold visits a node before its children (pre-order)"] {
// the module node is visited first
sample.fold(List(), (acc, n) -> acc.add(n.type)).first == "module"
}
["terminals have no children"] {
sample.fold(true, (acc, n) -> if (n.isTerminal) acc && n.children.isEmpty else acc)
}
["a parsed module is a branch"] {
!sample.isTerminal
}
["identifiers are terminals"] {
sample.fold(true, (acc, n) -> if (n.type == "identifier") acc && n.isTerminal else acc)
}
["terminals carry text"] {
sample.fold(true, (acc, n) -> if (n.isTerminal) acc && n.text != null else acc)
}
["branches carry the text of their entire subtree"] {
sample.fold(true, (acc, n) -> acc && n.text != null)
// the module node spans the whole source
sample.text == """
import "foo.pkl"
import "bar.pkl" as baz
class Point {
x: Int
y: Int
}
origin = if (useOrigin) new Point { x = 0; y = 0 } else null
scaled = origin.x * 2
"""
// a branch's text includes the whitespace and comments between its children
sample.fold(List(), (acc, n) -> if (n.type == "class_body") acc.add(n.text) else acc)
== List("""
{
x: Int
y: Int
}
""")
}
}
@@ -27,6 +27,7 @@ pkl:release
pkl:semver
pkl:settings
pkl:shell
pkl:syntax
pkl:test
pkl:xml
pkl:yaml
@@ -0,0 +1,16 @@
–– Pkl Error ––
Could not parse Pkl source.
x | res = parser.parseExpression("1 +")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at syntaxParseExpression#res (file:///$snippetsDir/input/errors/syntaxParseExpression.pkl)
Unexpected end of file. at (1:4)
xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -0,0 +1,16 @@
–– Pkl Error ––
Could not parse Pkl source.
x | parser.parseModule(
^^^^^^^^^^^^^^^^^^^
at syntaxParseModule#res (file:///$snippetsDir/input/errors/syntaxParseModule.pkl)
Unexpected token `}`. Expected `}`. at (3:1 - 3:2)
xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -0,0 +1,16 @@
–– Pkl Error ––
Could not parse Pkl source.
x | new syntax.Parser {}.parseModule(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at syntaxParseModuleLexerError#mod (file:///$snippetsDir/input/errors/syntaxParseModuleLexerError.pkl)
Missing `"` delimiter. at (1:9 - 2:1)
xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -0,0 +1,115 @@
facts {
["findChild returns the first match in pre-order"] {
true
true
}
["findChild throws when nothing matches"] {
true
}
["findChildOrNull returns null when nothing matches"] {
true
true
}
["findChild does not consider this node"] {
true
true
}
["findChildren returns every match in pre-order"] {
true
true
}
["findChildren searches a match for further matches"] {
true
}
["findChildOfType agrees with findChild"] {
true
true
}
["findChildOfType throws when nothing matches"] {
true
}
["findChildOfTypeOrNull returns null when nothing matches"] {
true
true
}
["findParent returns the nearest match"] {
true
true
true
}
["findParentOrNull returns null when nothing matches"] {
true
true
}
["findParentOfType agrees with findParent"] {
true
true
}
["findParentOfTypeOrNull returns null when nothing matches"] {
true
true
}
["findParents returns every match, nearest first"] {
true
}
["parent lookups do not consider this node"] {
true
true
true
}
["hasParent and hasParentOfType follow findParent"] {
true
true
true
}
["replaceChild replaces only the first match"] {
true
}
["replaceChild leaves the tree unchanged when nothing matches"] {
true
}
["replaceChildren replaces every match"] {
true
}
["a replacer sees the node it replaces"] {
true
}
["a replacement is not searched for further matches"] {
true
}
["replaceChildOfType agrees with replaceChild"] {
true
}
["a replacer sees where its node sits in the tree being built"] {
true
}
["a replace does not change the tree it walks"] {
true
}
["the root of a replaced tree has no parent"] {
true
true
}
["only the root of a replaced tree has no parent"] {
true
true
}
["replacing below a node does not detach it from its own tree"] {
true
}
["a replacer sees this tree, not the one being built"] {
true
}
["the children of an amended replacement are parented at the replacement"] {
true
}
["rebuilds ancestors of a replacement"] {
true
}
["a match is not searched for further matches"] {
true
}
["replacement is eager"] {
true
}
}
@@ -0,0 +1,146 @@
facts {
["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
}
["quoted identifiers"] {
true
true
true
true
true
}
["modify identifier"] {
true
}
["modify modifier"] {
true
}
["modify string content"] {
true
}
["modify int literal"] {
true
}
["modify boolean literal"] {
true
}
["modify float literal"] {
true
}
["add new modifier"] {
true
}
["a default renderer targets the V2 grammar"] {
true
}
["renderer honors the grammar version"] {
true
}
["a malformed tree is reported as such"] {
true
}
}
@@ -0,0 +1,91 @@
facts {
["span positions are 1-based"] {
true
true
}
["end is exclusive"] {
true
true
true
true
}
["a child's span is contained in its parent's"] {
true
true
}
["nodes constructed from scratch have no span"] {
true
}
["a spanless constructed node still renders"] {
true
}
["parsing the same text from a string and a resource differs only in displayUri"] {
true
true
true
true
true
}
}
examples {
["span of a module parsed from a string"] {
new {
start {
line = X
column = 1
displayUri = ""
}
end {
line = X
column = 2
displayUri = ""
}
displayUri = ""
}
}
["span of a leaf parsed from a string"] {
new {
start {
line = X
column = 3
displayUri = ""
}
end {
line = X
column = 14
displayUri = ""
}
displayUri = ""
}
}
["span of a module parsed from a resource carries the resource URI"] {
new {
start {
line = X
column = 1
displayUri = "file:///$snippetsDir/input-helper/syntax/spans/sample.pkl"
}
end {
line = X
column = 2
displayUri = "file:///$snippetsDir/input-helper/syntax/spans/sample.pkl"
}
displayUri = "file:///$snippetsDir/input-helper/syntax/spans/sample.pkl"
}
}
["span of a leaf parsed from a resource carries the resource URI"] {
new {
start {
line = X
column = 3
displayUri = "file:///$snippetsDir/input-helper/syntax/spans/sample.pkl"
}
end {
line = X
column = 14
displayUri = "file:///$snippetsDir/input-helper/syntax/spans/sample.pkl"
}
displayUri = "file:///$snippetsDir/input-helper/syntax/spans/sample.pkl"
}
}
}
@@ -0,0 +1,30 @@
facts {
["fold counts nodes by predicate"] {
true
true
true
}
["fold accumulates into a collection"] {
true
}
["fold visits a node before its children (pre-order)"] {
true
}
["terminals have no children"] {
true
}
["a parsed module is a branch"] {
true
}
["identifiers are terminals"] {
true
}
["terminals carry text"] {
true
}
["branches carry the text of their entire subtree"] {
true
true
true
}
}
@@ -42,6 +42,7 @@ final class Builder {
}
FormatNode format(Node node) {
if (node.type.isTerminal()) return new Text(node.text(source));
return switch (node.type) {
case MODULE -> formatModule(node);
case DOC_COMMENT,
@@ -56,10 +57,10 @@ final class Builder {
case DOC_COMMENT_LINE -> formatDocComment(node);
case LINE_COMMENT,
BLOCK_COMMENT,
TERMINAL,
MODIFIER,
IDENTIFIER,
STRING_CHARS,
STRING_CONSTANT,
STRING_ESCAPE,
INT_LITERAL_EXPR,
FLOAT_LITERAL_EXPR,
@@ -115,11 +116,11 @@ final class Builder {
formatClassPropertyHeaderBegin(node);
case CLASS_PROPERTY_BODY, OBJECT_PROPERTY_BODY -> formatClassPropertyBody(node);
case CLASS_METHOD, OBJECT_METHOD -> formatClassMethod(node);
case CLASS_METHOD_HEADER -> formatClassMethodHeader(node);
case CLASS_METHOD_BODY -> formatClassMethodBody(node);
case METHOD_HEADER -> formatClassMethodHeader(node);
case METHOD_BODY -> formatClassMethodBody(node);
case OBJECT_BODY -> formatObjectBody(node);
case OBJECT_ELEMENT -> format(node.children.get(0)); // has a single element
case OBJECT_ENTRY_HEADER -> formatObjectEntryHeader(node);
case OBJECT_ENTRY_HEADER, MEMBER_PREDICATE_HEADER -> formatObjectEntryHeader(node);
case FOR_GENERATOR -> formatForGenerator(node);
case FOR_GENERATOR_HEADER -> formatForGeneratorHeader(node);
case FOR_GENERATOR_HEADER_DEFINITION -> formatForGeneratorHeaderDefinition(node);
@@ -147,7 +148,6 @@ final class Builder {
case SUBSCRIPT_EXPR, SUPER_SUBSCRIPT_EXPR -> formatSubscriptExpr(node);
case TRACE_EXPR, THROW_EXPR, READ_EXPR -> formatTraceThrowReadExpr(node);
case PARENTHESIZED_EXPR -> formatParenthesizedExpr(node);
case PARENTHESIZED_EXPR_ELEMENTS -> formatParenthesizedExprElements(node);
case LET_EXPR -> formatLetExpr(node);
case LET_PARAMETER_DEFINITION -> formatLetParameterDefinition(node);
case LET_PARAMETER -> formatLetParameter(node);
@@ -156,7 +156,7 @@ final class Builder {
case CONSTRAINED_TYPE -> formatConstrainedType(node);
case UNION_TYPE -> formatUnionType(node);
case FUNCTION_TYPE -> formatFunctionType(node);
case STRING_CONSTANT_TYPE -> format(node.children.get(0));
case STRING_LITERAL_TYPE -> format(node.children.get(0));
case PARENTHESIZED_TYPE -> formatParenthesizedType(node);
case PARENTHESIZED_TYPE_ELEMENTS -> formatParenthesizedTypeElements(node);
default -> throw new RuntimeException("Unknown node type: " + node.type);
@@ -231,7 +231,7 @@ final class Builder {
first.add(format(node.children.get(0)));
first.add(line());
var rest = node.children.subList(1, node.children.size());
var nodes = formatGeneric(rest, (n1, next) -> n1.type == NodeType.TERMINAL ? null : line());
var nodes = formatGeneric(rest, (n1, next) -> n1.type.isTerminal() ? null : line());
first.add(new Indent(nodes));
return new Group(newId(), first);
}
@@ -397,7 +397,7 @@ final class Builder {
formatGenericWithGen(
node.children,
spaceOrLine(),
(n, next) -> isTerminal(n, "import") ? format(n) : indent(format(n))));
(n, next) -> n.type == NodeType.IMPORT_KEYWORD ? format(n) : indent(format(n))));
}
private FormatNode formatAnnotation(Node node) {
@@ -504,12 +504,12 @@ final class Builder {
private FormatNode formatClassMethod(Node node) {
var prefixes = new ArrayList<FormatNode>();
List<Node> methodNodes;
if (node.children.get(0).type == NodeType.CLASS_METHOD_HEADER) {
if (node.children.get(0).type == NodeType.METHOD_HEADER) {
methodNodes = node.children;
} else {
var idx = -1;
for (var i = 0; i < node.children.size(); i++) {
if (node.children.get(i).type == NodeType.CLASS_METHOD_HEADER) {
if (node.children.get(i).type == NodeType.METHOD_HEADER) {
idx = i;
break;
}
@@ -525,7 +525,7 @@ final class Builder {
// Separate header (before =) and body (= and after)
var bodyIdx = -1;
for (var i = 0; i < methodNodes.size(); i++) {
if (methodNodes.get(i).type == NodeType.CLASS_METHOD_BODY) {
if (methodNodes.get(i).type == NodeType.METHOD_BODY) {
bodyIdx = i - 1;
break;
}
@@ -594,8 +594,8 @@ final class Builder {
formatGeneric(
node.children,
(prev, next) -> {
if (isTerminal(prev, "(") || isTerminal(next, ")")) {
if (isTerminal(next, ")")) {
if (insideParens(prev, next)) {
if (next.type == NodeType.RPAREN) {
// trailing comma
if (grammarVersion == GrammarVersion.V1) {
return line();
@@ -618,9 +618,9 @@ final class Builder {
formatGenericWithGen(
node.children,
(prev, next) -> {
if (isTerminal(prev, "(") || isTerminal(next, ")")) {
if (insideParens(prev, next)) {
var lineNode = hasTrailingLambda ? Empty.INSTANCE : line();
if (isTerminal(next, ")") && !hasTrailingLambda) {
if (next.type == NodeType.RPAREN && !hasTrailingLambda) {
// trailing comma
if (grammarVersion == GrammarVersion.V1) {
return lineNode;
@@ -642,7 +642,7 @@ final class Builder {
private FormatNode formatArgumentListElements(
Node node, boolean hasTrailingLambda, boolean twoBy2) {
var children = node.children;
var shouldMultiline = shouldMultilineNodes(node, n -> isTerminal(n, ","));
var shouldMultiline = shouldMultilineNodes(node, n -> n.type == NodeType.COMMA);
BiFunction<Node, Node, @Nullable FormatNode> sep =
(prev, next) -> shouldMultiline ? forceSpaceyLine() : spaceOrLine();
if (twoBy2) {
@@ -698,7 +698,7 @@ final class Builder {
while (!node.children.isEmpty()) {
node = node.children.get(node.children.size() - 1);
}
return isTerminalSingle(node, "}");
return node.type == NodeType.RBRACE;
}
/**
@@ -741,7 +741,8 @@ final class Builder {
return false;
}
}
return properArgCount <= 2 || !shouldMultilineNodes(elementsNode, n -> isTerminal(n, ","));
return properArgCount <= 2
|| !shouldMultilineNodes(elementsNode, n -> n.type == NodeType.COMMA);
}
private List<Node> pairArguments(List<Node> nodes) {
@@ -749,7 +750,7 @@ final class Builder {
var tmp = new ArrayList<Node>();
var commas = 0;
for (var node : nodes) {
if (isTerminalSingle(node, ",")) {
if (node.type == NodeType.COMMA) {
commas++;
if (commas == 2) {
var suffixes = new ArrayList<Node>();
@@ -791,8 +792,8 @@ final class Builder {
formatGeneric(
node.children,
(prev, next) -> {
if (isTerminal(prev, "<") || isTerminal(next, ">")) {
if (isTerminal(next, ">")) {
if (prev.type == NodeType.LT || next.type == NodeType.GT) {
if (next.type == NodeType.GT) {
// trailing comma
if (grammarVersion == GrammarVersion.V1) {
return new Line();
@@ -825,14 +826,14 @@ final class Builder {
node.children,
(prev, next) -> {
if (next.type == NodeType.OBJECT_PARAMETER_LIST) return Empty.INSTANCE;
if (isTerminal(prev, "{") || isTerminal(next, "}")) {
if (prev.type == NodeType.LBRACE || next.type == NodeType.RBRACE) {
var lines = linesBetween(prev, next);
return lines == 0 ? spaceOrLine() : forceSpaceyLine();
}
return spaceOrLine();
},
(n, next) ->
n.type == NodeType.OBJECT_MEMBER_LIST
n.type == NodeType.OBJECT_MEMBER_ELEMENTS
? formatObjectMemberList(n, groupId)
: format(n));
return new Group(groupId, nodes);
@@ -869,9 +870,7 @@ final class Builder {
private FormatNode formatForGeneratorHeader(Node node) {
var nodes =
formatGeneric(
node.children,
(prev, next) -> isTerminal(prev, "(") || isTerminal(next, ")") ? line() : null);
formatGeneric(node.children, (prev, next) -> insideParens(prev, next) ? line() : null);
return new Group(newId(), nodes);
}
@@ -897,8 +896,9 @@ final class Builder {
node.children,
(prev, next) ->
prev.type == NodeType.WHEN_GENERATOR_HEADER
|| isTerminal(prev, "when", "else")
|| isTerminal(next, "else")
|| prev.type == NodeType.WHEN_KEYWORD
|| prev.type == NodeType.ELSE_KEYWORD
|| next.type == NodeType.ELSE_KEYWORD
? Space.INSTANCE
: spaceOrLine());
return new Group(newId(), nodes);
@@ -908,9 +908,8 @@ final class Builder {
var nodes =
formatGenericWithGen(
node.children,
(prev, next) -> isTerminal(prev, "(") || isTerminal(next, ")") ? line() : spaceOrLine(),
(n, next) ->
!n.type.isAffix() && n.type != NodeType.TERMINAL ? indent(format(n)) : format(n));
(prev, next) -> insideParens(prev, next) ? line() : spaceOrLine(),
(n, next) -> isProper(n) ? indent(format(n)) : format(n));
return new Group(newId(), nodes);
}
@@ -932,7 +931,7 @@ final class Builder {
while (cursor.hasNext()) {
if (isInStringInterpolation) {
var prevNoNewlines = noNewlines;
var elems = cursor.takeUntilBefore(n -> isTerminalSingle(n, ")"));
var elems = cursor.takeUntilBefore(n -> n.type == NodeType.RPAREN);
noNewlines = !isMultilineList(elems);
//noinspection ConstantValue
assert prev != null;
@@ -946,7 +945,7 @@ final class Builder {
continue;
}
var elem = cursor.next();
if (elem.type == NodeType.TERMINAL && text(elem).endsWith("(")) {
if (elem.type == NodeType.INTERPOLATION_START) {
isInStringInterpolation = true;
}
var formatted = format(elem);
@@ -996,9 +995,7 @@ final class Builder {
private FormatNode formatIfCondition(Node node) {
var nodes =
formatGeneric(
node.children,
(prev, next) ->
isTerminal(prev, "(") || isTerminal(next, ")") ? line() : spaceOrLine());
node.children, (prev, next) -> insideParens(prev, next) ? line() : spaceOrLine());
return new Group(newId(), nodes);
}
@@ -1029,21 +1026,19 @@ final class Builder {
}
private FormatNode formatParenthesizedExpr(Node node) {
if (node.children.size() == 2) return new Text("()");
var nodes =
formatGenericWithGen(
node.children,
(prev, next) -> isTerminal(prev, "(") || isTerminal(next, ")") ? line() : spaceOrLine(),
(n, next) -> n.type.isExpression() ? indent(format(n)) : format(n));
(prev, next) -> insideParens(prev, next) ? line() : spaceOrLine(),
(n, next) ->
(n.type == NodeType.LPAREN || n.type == NodeType.RPAREN)
? format(n)
: indent(format(n)));
return new Group(newId(), nodes);
}
private FormatNode formatParenthesizedExprElements(Node node) {
return indent(new Group(newId(), formatGeneric(node.children, (FormatNode) null)));
}
private FormatNode formatFunctionLiteralExpr(Node node) {
var splitResult = splitOn(node.children, n -> isTerminalSingle(n, "->"));
var splitResult = splitOn(node.children, n -> n.type == NodeType.ARROW);
var params = splitResult[0];
var rest = splitResult[1];
Node bodyNode = null;
@@ -1112,9 +1107,7 @@ final class Builder {
private FormatNode formatLetParameterDefinition(Node node) {
var nodes =
formatGeneric(
node.children,
(prev, next) ->
isTerminal(prev, "(") || isTerminal(next, ")") ? line() : spaceOrLine());
node.children, (prev, next) -> insideParens(prev, next) ? line() : spaceOrLine());
return new Group(newId(), nodes);
}
@@ -1160,7 +1153,7 @@ final class Builder {
var nodes =
formatGenericWithGen(
node.children,
(prev, next) -> isTerminal(prev, "(") || isTerminal(next, ")") ? line() : null,
(prev, next) -> insideParens(prev, next) ? line() : null,
(n, next) -> n.type.isExpression() ? indent(format(n)) : format(n));
return new Group(newId(), nodes);
}
@@ -1183,8 +1176,8 @@ final class Builder {
formatGeneric(
node.children,
(prev, next) -> {
if (isTerminal(next, "|")) return spaceOrLine();
if (isTerminal(prev, "|")) return Space.INSTANCE;
if (next.type == NodeType.UNION) return spaceOrLine();
if (prev.type == NodeType.UNION) return Space.INSTANCE;
return null;
});
return new Group(newId(), indentAfterFirstNewline(nodes, false));
@@ -1194,7 +1187,7 @@ final class Builder {
var nodes =
formatGenericWithGen(
node.children,
(prev, next) -> isTerminal(prev, "(") || isTerminal(next, ")") ? line() : spaceOrLine(),
(prev, next) -> insideParens(prev, next) ? line() : spaceOrLine(),
(n, next) -> next == null ? indent(format(n)) : format(n));
return new Group(newId(), nodes);
}
@@ -1204,9 +1197,7 @@ final class Builder {
var groupId = newId();
var nodes =
formatGeneric(
node.children,
(prev, next) ->
isTerminal(prev, "(") || isTerminal(next, ")") ? line() : spaceOrLine());
node.children, (prev, next) -> insideParens(prev, next) ? line() : spaceOrLine());
return new Group(groupId, nodes);
}
@@ -1288,9 +1279,7 @@ final class Builder {
var regularImports = new ArrayList<ImportWithComments>();
var globImports = new ArrayList<ImportWithComments>();
for (var entry : allImportsWithComments) {
var terminalNode = entry.importNode.findChildByType(NodeType.TERMINAL);
var terminalText = terminalNode != null ? terminalNode.text(source) : null;
if ("import*".equals(terminalText)) {
if (entry.importNode.findChildByType(NodeType.IMPORT_STAR_KEYWORD) != null) {
globImports.add(entry);
} else {
regularImports.add(entry);
@@ -1417,7 +1406,7 @@ final class Builder {
}
private boolean isSemicolon(Node node) {
return node.type.isAffix() && text(node).equals(";");
return node.type == NodeType.SEMICOLON;
}
/** Groups all non prefixes (comments, doc comments, annotations) of this node together. */
@@ -1446,9 +1435,9 @@ final class Builder {
}
private String getImportUrl(Node node) {
var strChars = node.findChildByType(NodeType.STRING_CHARS);
assert strChars != null;
var txt = strChars.text(source);
var strConst = node.findChildByType(NodeType.STRING_CONSTANT);
assert strConst != null;
var txt = strConst.text(source);
return txt.substring(1, txt.length() - 1);
}
@@ -1468,7 +1457,7 @@ final class Builder {
return linesBetween(prev, next) > 1 ? TWO_NEWLINES : mustForceLine();
}
if (hasTrailingAffix(prev, next)) {
if (isTerminal(prev, "(") && startsWithBlockComment(next)) return line();
if (prev.type == NodeType.LPAREN && startsWithBlockComment(next)) return line();
return Space.INSTANCE;
}
if (prev.type == NodeType.DOC_COMMENT) return mustForceLine();
@@ -1479,13 +1468,11 @@ final class Builder {
if (prev.type == NodeType.BLOCK_COMMENT) {
return linesBetween(prev, next) > 0 ? forceSpaceyLine() : Space.INSTANCE;
}
if (EMPTY_SUFFIXES.contains(next.type)
|| isTerminal(prev, "[", "!", "@", "[[")
|| isTerminal(next, "]", "?", ",")) {
if (EMPTY_SUFFIXES.contains(next.type) || EMPTY_PREFIXES.contains(prev.type)) {
return Empty.INSTANCE;
}
if (isTerminal(prev, "class", "function", "new")
|| isTerminal(next, "=", "{", "->", "class", "function")
if (SPACE_PREFIXES.contains(prev.type)
|| SPACE_SUFFIXES.contains(next.type)
|| next.type == NodeType.OBJECT_BODY
|| prev.type == NodeType.MODIFIER_LIST) {
return Space.INSTANCE;
@@ -1659,17 +1646,8 @@ final class Builder {
return node.text(source);
}
private boolean isTerminal(Node node, String... texts) {
if (node.type != NodeType.TERMINAL) return false;
var t = node.text(source);
for (var text : texts) {
if (t.equals(text)) return true;
}
return false;
}
private boolean isTerminalSingle(Node node, String text) {
return node.type == NodeType.TERMINAL && node.text(source).equals(text);
private static boolean insideParens(Node prev, Node next) {
return prev.type == NodeType.LPAREN || next.type == NodeType.RPAREN;
}
private int newId() {
@@ -1704,7 +1682,7 @@ final class Builder {
// returns true if this node is not an affix or terminal
private static boolean isProper(Node node) {
return !node.type.isAffix() && node.type != NodeType.TERMINAL;
return !node.type.isAffix() && !node.type.isTerminal();
}
private static boolean isMultiline(Node node) {
@@ -1805,7 +1783,24 @@ final class Builder {
NodeType.TYPE_ARGUMENT_LIST,
NodeType.TYPE_ANNOTATION,
NodeType.TYPE_PARAMETER_LIST,
NodeType.PARAMETER_LIST);
NodeType.PARAMETER_LIST,
NodeType.RBRACK,
NodeType.QUESTION,
NodeType.COMMA);
private static final EnumSet<NodeType> EMPTY_PREFIXES =
EnumSet.of(NodeType.LBRACK, NodeType.NOT, NodeType.AT, NodeType.LPRED);
private static final EnumSet<NodeType> SPACE_PREFIXES =
EnumSet.of(NodeType.CLASS_KEYWORD, NodeType.FUNCTION_KEYWORD, NodeType.NEW_KEYWORD);
private static final EnumSet<NodeType> SPACE_SUFFIXES =
EnumSet.of(
NodeType.ASSIGN,
NodeType.LBRACE,
NodeType.ARROW,
NodeType.CLASS_KEYWORD,
NodeType.FUNCTION_KEYWORD);
private static final EnumSet<NodeType> SAME_LINE_EXPRS =
EnumSet.of(NodeType.NEW_EXPR, NodeType.AMENDS_EXPR, NodeType.FUNCTION_LITERAL_EXPR);
@@ -1819,8 +1814,8 @@ final class Builder {
@Override
public int compare(Node o1, Node o2) {
var import1 = o1.findChildByType(NodeType.STRING_CHARS);
var import2 = o2.findChildByType(NodeType.STRING_CHARS);
var import1 = o1.findChildByType(NodeType.STRING_CONSTANT);
var import2 = o2.findChildByType(NodeType.STRING_CONSTANT);
if (import1 == null || import2 == null) {
// should never happen
throw new RuntimeException("ImportComparator: not an import");
@@ -21,6 +21,7 @@ import java.nio.file.Files;
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,9 +112,43 @@ public class Formatter {
format(sb.toString(), output);
}
/**
* Format the given Pkl AST as text.
*
* <p>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 output = new StringBuilder();
format(ast, output);
return output.toString();
}
/**
* Formats the given Pkl AST as text.
*
* <p>The AST terminal nodes should have their text property set before calling format, as this
* method does not have access to the original source.
*
* <p>It is the caller's responsibility to close {@code output}, if applicable.
*
* @param ast the Pkl module node to format
* @param output the formatted Pkl source code
* @throws java.io.UncheckedIOException if an I/O error occurs during writing
*/
public void format(Node ast, Appendable output) {
generate(new Builder("", grammarVersion).format(ast), output);
}
private void format(String input, Appendable output) {
var ast = new GenericParser().parseModule(input);
var formatAst = new Builder(input, grammarVersion).format(ast);
generate(new Builder(input, grammarVersion).format(ast), output);
}
private static void generate(FormatNode formatAst, Appendable output) {
// force a line at the end of the file
var nodes = new Nodes(List.of(formatAst, ForceLine.INSTANCE));
new Generator(output).generate(nodes);
@@ -25,7 +25,9 @@ import org.junit.jupiter.api.assertThrows
import org.pkl.commons.readString
import org.pkl.commons.test.FileTestUtils
import org.pkl.commons.test.failWithDiff
import org.pkl.parser.GenericParser
import org.pkl.parser.GenericParserError
import org.pkl.parser.syntax.generic.Node
class FormatterTest {
@@ -128,4 +130,21 @@ class FormatterTest {
Formatter().format(input, output)
assertThat(output.toString()).isEqualTo("x = 42\n")
}
@Test
fun `format AST into Appendable`() {
val source = " x = 42"
val ast = GenericParser().parseModule(source)
fun setText(node: Node) {
if (node.children.isEmpty()) {
node.text(source.toCharArray())
} else {
node.children.forEach(::setText)
}
}
setText(ast)
val output = StringBuilder()
Formatter().format(ast, output)
assertThat(output.toString()).isEqualTo("x = 42\n")
}
}
@@ -81,7 +81,7 @@ import org.pkl.parser.syntax.Type.ModuleType;
import org.pkl.parser.syntax.Type.NothingType;
import org.pkl.parser.syntax.Type.NullableType;
import org.pkl.parser.syntax.Type.ParenthesizedType;
import org.pkl.parser.syntax.Type.StringConstantType;
import org.pkl.parser.syntax.Type.StringLiteralType;
import org.pkl.parser.syntax.Type.ThisType;
import org.pkl.parser.syntax.Type.UnionType;
import org.pkl.parser.syntax.Type.UnknownType;
@@ -114,7 +114,7 @@ public abstract class BaseParserVisitor<T> implements ParserVisitor<T> {
}
@Override
public T visitStringConstantType(StringConstantType type) {
public T visitStringLiteralType(StringLiteralType type) {
return visitChildren(type);
}
@@ -21,4 +21,8 @@ public final class GenericParser {
public Node parseModule(String source) {
return new GenericParserImpl(source).parseModule();
}
public Node parseExpressionInput(String source) {
return new GenericParserImpl(source).parseExpressionInput();
}
}
@@ -17,6 +17,7 @@ package org.pkl.parser;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.pkl.parser.syntax.Operator;
@@ -37,10 +38,14 @@ class GenericParserImpl {
GenericParserImpl(String source) {
this.lexer = new Lexer(source);
while (true) {
var ft = new FullToken(lexer.next(), lexer.fullSpan(), lexer.getNewLinesBetween());
tokens.add(ft);
if (ft.token == Token.EOF) break;
try {
while (true) {
var ft = new FullToken(lexer.next(), lexer.fullSpan(), lexer.getNewLinesBetween());
tokens.add(ft);
if (ft.token == Token.EOF) break;
}
} catch (ParserError e) {
throw parserError(Objects.requireNonNull(e.getMessage()), lexer.fullSpan());
}
_lookahead = tokens.get(cursor);
lookahead = _lookahead.token;
@@ -102,6 +107,16 @@ class GenericParserImpl {
return new Node(NodeType.MODULE, nodes);
}
Node parseExpressionInput() {
ff();
var expr = parseExpr();
ff();
if (lookahead != Token.EOF) {
throw parserError("unexpectedToken", _lookahead.text(lexer), "end of file");
}
return expr;
}
private Node parseModuleDecl(List<Node> preChildren) {
var headerParts = getHeaderParts(preChildren);
var children = new ArrayList<>(headerParts.preffixes);
@@ -139,7 +154,7 @@ class GenericParserImpl {
children.add(parseIdentifier());
while (lookahead() == Token.DOT) {
ff(children);
children.add(new Node(NodeType.TERMINAL, next().span));
children.add(makeTerminal(next()));
ff(children);
children.add(parseIdentifier());
}
@@ -222,13 +237,13 @@ class GenericParserImpl {
headers.add(makeTerminal(next()));
ff(headers);
headers.add(parseIdentifier());
ff(headers);
if (lookahead == Token.LT) {
headers.add(parseTypeParameterList());
if (lookahead() == Token.LT) {
ff(headers);
headers.add(parseTypeParameterList());
}
expect(Token.ASSIGN, headers, "unexpectedToken", "=");
children.add(new Node(NodeType.TYPEALIAS_HEADER, headers));
ff(children);
expect(Token.ASSIGN, children, "unexpectedToken", "=");
var body = new ArrayList<Node>();
ff(body);
body.add(parseType());
@@ -307,13 +322,13 @@ class GenericParserImpl {
}
headerBegin.add(parseIdentifier());
header.add(new Node(NodeType.CLASS_PROPERTY_HEADER_BEGIN, headerBegin));
children.add(new Node(NodeType.CLASS_PROPERTY_HEADER, header));
var hasTypeAnnotation = false;
if (lookahead() == Token.COLON) {
ff(header);
header.add(parseTypeAnnotation());
ff(children);
children.add(parseTypeAnnotation());
hasTypeAnnotation = true;
}
children.add(new Node(NodeType.CLASS_PROPERTY_HEADER, header));
if (lookahead() == Token.ASSIGN) {
ff(children);
children.add(makeTerminal(next()));
@@ -343,7 +358,7 @@ class GenericParserImpl {
expect(Token.FUNCTION, headers, "unexpectedToken", "function");
ff(headers);
headers.add(parseIdentifier());
children.add(new Node(NodeType.CLASS_METHOD_HEADER, headers));
children.add(new Node(NodeType.METHOD_HEADER, headers));
ff(children);
if (lookahead == Token.LT) {
children.add(parseTypeParameterList());
@@ -360,7 +375,7 @@ class GenericParserImpl {
var body = new ArrayList<Node>();
ff(body);
body.add(parseExpr());
children.add(new Node(NodeType.CLASS_METHOD_BODY, body));
children.add(new Node(NodeType.METHOD_BODY, body));
}
return new Node(NodeType.CLASS_METHOD, children);
}
@@ -391,7 +406,7 @@ class GenericParserImpl {
ff(members);
}
if (!members.isEmpty()) {
children.add(new Node(NodeType.OBJECT_MEMBER_LIST, members));
children.add(new Node(NodeType.OBJECT_MEMBER_ELEMENTS, members));
}
children.add(makeTerminal(next())); // RBRACE
return new Node(NodeType.OBJECT_BODY, children);
@@ -484,13 +499,13 @@ class GenericParserImpl {
}
headerBegin.add(parseIdentifier());
header.add(new Node(NodeType.OBJECT_PROPERTY_HEADER_BEGIN, headerBegin));
children.add(new Node(NodeType.OBJECT_PROPERTY_HEADER, header));
var hasTypeAnnotation = false;
if (lookahead() == Token.COLON) {
ff(header);
header.add(parseTypeAnnotation());
ff(children);
children.add(parseTypeAnnotation());
hasTypeAnnotation = true;
}
children.add(new Node(NodeType.OBJECT_PROPERTY_HEADER, header));
if (hasTypeAnnotation || lookahead() == Token.ASSIGN) {
ff(children);
expect(Token.ASSIGN, children, "unexpectedToken", "=");
@@ -515,7 +530,7 @@ class GenericParserImpl {
expect(Token.FUNCTION, headers, "unexpectedToken", "function");
ff(headers);
headers.add(parseIdentifier());
children.add(new Node(NodeType.CLASS_METHOD_HEADER, headers));
children.add(new Node(NodeType.METHOD_HEADER, headers));
ff(children);
if (lookahead == Token.LT) {
children.add(parseTypeParameterList());
@@ -531,20 +546,22 @@ class GenericParserImpl {
var body = new ArrayList<Node>();
ff(body);
body.add(parseExpr());
children.add(new Node(NodeType.CLASS_METHOD_BODY, body));
children.add(new Node(NodeType.METHOD_BODY, body));
return new Node(NodeType.OBJECT_METHOD, children);
}
private Node parseMemberPredicate() {
var children = new ArrayList<Node>();
children.add(makeTerminal(next()));
ff(children);
children.add(parseExpr());
ff(children);
var header = new ArrayList<Node>();
header.add(makeTerminal(next()));
ff(header);
header.add(parseExpr());
ff(header);
var firstBrack = expect(Token.RBRACK, "unexpectedToken", "]]");
children.add(makeTerminal(firstBrack));
header.add(makeTerminal(firstBrack));
var secondbrack = expect(Token.RBRACK, "unexpectedToken", "]]");
children.add(makeTerminal(secondbrack));
header.add(makeTerminal(secondbrack));
children.add(new Node(NodeType.MEMBER_PREDICATE_HEADER, header));
if (firstBrack.span.charIndex() != secondbrack.span.charIndex() - 1) {
// There shouldn't be any whitespace between the first and second ']'.
var span = firstBrack.span.endWith(secondbrack.span);
@@ -569,16 +586,14 @@ class GenericParserImpl {
ff(header);
header.add(parseExpr());
expect(Token.RBRACK, header, "unexpectedToken", "]");
if (lookahead() == Token.ASSIGN) {
ff(header);
header.add(makeTerminal(next()));
children.add(new Node(NodeType.OBJECT_ENTRY_HEADER, header));
children.add(new Node(NodeType.OBJECT_ENTRY_HEADER, header));
ff(children);
if (lookahead == Token.ASSIGN) {
children.add(makeTerminal(next()));
ff(children);
children.add(parseExpr());
return new Node(NodeType.OBJECT_ENTRY, children);
}
children.add(new Node(NodeType.OBJECT_ENTRY_HEADER, header));
ff(children);
children.addAll(parseBodyList());
return new Node(NodeType.OBJECT_ENTRY, children);
}
@@ -937,7 +952,9 @@ class GenericParserImpl {
var token = next().token;
ff();
if (token == Token.RPAREN) {
return lookahead == Token.ARROW;
// `()` is only valid as an empty parameter list
// let `parseFunctionLiteral` report the missing `->`
return true;
}
if (token == Token.UNDERSCORE) {
return true;
@@ -1072,16 +1089,9 @@ class GenericParserImpl {
private Node parseParenthesizedExpr() {
var children = new ArrayList<Node>();
expect(Token.LPAREN, children, "unexpectedToken", "(");
if (lookahead() == Token.RPAREN) {
ff(children);
children.add(makeTerminal(next()));
return new Node(NodeType.PARENTHESIZED_EXPR, children);
}
var elements = new ArrayList<Node>();
ff(elements);
elements.add(parseExpr(")"));
ff(elements);
children.add(new Node(NodeType.PARENTHESIZED_EXPR_ELEMENTS, elements));
ff(children);
children.add(parseExpr(")"));
ff(children);
expect(Token.RPAREN, children, "unexpectedToken", ")");
return new Node(NodeType.PARENTHESIZED_EXPR, children);
}
@@ -1211,7 +1221,7 @@ class GenericParserImpl {
yield new Node(NodeType.DECLARED_TYPE, children);
}
case STRING_START ->
new Node(NodeType.STRING_CONSTANT_TYPE, List.of(parseStringConstant()));
new Node(NodeType.STRING_LITERAL_TYPE, List.of(parseStringConstant()));
default -> {
var text = _lookahead.text(lexer);
if (expectation != null) {
@@ -1381,14 +1391,14 @@ class GenericParserImpl {
children.add(makeTerminal(startTk));
while (lookahead != Token.STRING_END) {
switch (lookahead) {
case STRING_PART,
STRING_ESCAPE_NEWLINE,
case STRING_PART -> children.add(make(NodeType.STRING_CHARS, next().span));
case STRING_ESCAPE_NEWLINE,
STRING_ESCAPE_TAB,
STRING_ESCAPE_QUOTE,
STRING_ESCAPE_BACKSLASH,
STRING_ESCAPE_RETURN,
STRING_ESCAPE_UNICODE ->
children.add(makeTerminal(next()));
children.add(make(NodeType.STRING_ESCAPE, next().span));
case EOF -> {
var delimiter = new StringBuilder(startTk.text(lexer)).reverse().toString();
throw parserError("missingDelimiter", delimiter);
@@ -1399,7 +1409,7 @@ class GenericParserImpl {
}
}
children.add(makeTerminal(next())); // string end
return new Node(NodeType.STRING_CHARS, children);
return new Node(NodeType.STRING_CONSTANT, children);
}
private FullToken expect(Token type, String errorKey, Object... messageArgs) {
@@ -1542,7 +1552,64 @@ class GenericParserImpl {
}
private Node makeTerminal(FullToken tk) {
return new Node(NodeType.TERMINAL, tk.span);
var nodeType =
switch (tk.token) {
// keywords
case AMENDS -> NodeType.AMENDS_KEYWORD;
case AS -> NodeType.AS_KEYWORD;
case CLASS -> NodeType.CLASS_KEYWORD;
case ELSE -> NodeType.ELSE_KEYWORD;
case EXTENDS -> NodeType.EXTENDS_KEYWORD;
case FOR -> NodeType.FOR_KEYWORD;
case FUNCTION -> NodeType.FUNCTION_KEYWORD;
case IF -> NodeType.IF_KEYWORD;
case IMPORT -> NodeType.IMPORT_KEYWORD;
case IMPORT_STAR -> NodeType.IMPORT_STAR_KEYWORD;
case IN -> NodeType.IN_KEYWORD;
case LET -> NodeType.LET_KEYWORD;
case MODULE -> NodeType.MODULE_KEYWORD;
case NEW -> NodeType.NEW_KEYWORD;
case OUT -> NodeType.OUT_KEYWORD;
case READ -> NodeType.READ_KEYWORD;
case READ_QUESTION -> NodeType.READ_QUESTION_KEYWORD;
case READ_STAR -> NodeType.READ_STAR_KEYWORD;
case SUPER -> NodeType.SUPER_KEYWORD;
case THROW -> NodeType.THROW_KEYWORD;
case TRACE -> NodeType.TRACE_KEYWORD;
case TYPE_ALIAS -> NodeType.TYPEALIAS_KEYWORD;
case WHEN -> NodeType.WHEN_KEYWORD;
// punctuation
case ARROW -> NodeType.ARROW;
case ASSIGN -> NodeType.ASSIGN;
case AT -> NodeType.AT;
case COLON -> NodeType.COLON;
case COMMA -> NodeType.COMMA;
case DOT -> NodeType.DOT;
case GT -> NodeType.GT;
case LBRACE -> NodeType.LBRACE;
case LBRACK -> NodeType.LBRACK;
case LPAREN -> NodeType.LPAREN;
case LPRED -> NodeType.LPRED;
case LT -> NodeType.LT;
case MINUS -> NodeType.MINUS;
case NOT -> NodeType.NOT;
case QSPREAD -> NodeType.QSPREAD;
case QUESTION -> NodeType.QUESTION;
case RBRACE -> NodeType.RBRACE;
case RBRACK -> NodeType.RBRACK;
case RPAREN -> NodeType.RPAREN;
case SPREAD -> NodeType.SPREAD;
case STAR -> NodeType.STAR;
case UNDERSCORE -> NodeType.UNDERSCORE;
case UNION -> NodeType.UNION;
// string delimiters
case INTERPOLATION_START -> NodeType.INTERPOLATION_START;
case STRING_START -> NodeType.STRING_START;
case STRING_MULTI_START -> NodeType.STRING_MULTI_START;
case STRING_END -> NodeType.STRING_END;
default -> throw new RuntimeException("Unreacheable code");
};
return new Node(nodeType, tk.span);
}
// fast-forward over affix tokens
@@ -79,7 +79,7 @@ import org.pkl.parser.syntax.StringPart.StringChars;
import org.pkl.parser.syntax.Type;
import org.pkl.parser.syntax.Type.DeclaredType;
import org.pkl.parser.syntax.Type.ParenthesizedType;
import org.pkl.parser.syntax.Type.StringConstantType;
import org.pkl.parser.syntax.Type.StringLiteralType;
import org.pkl.parser.syntax.TypeAlias;
import org.pkl.parser.syntax.TypeAnnotation;
import org.pkl.parser.syntax.TypeArgumentList;
@@ -1427,7 +1427,7 @@ final class ParserImpl {
}
case STRING_START -> {
var str = parseStringConstant();
typ = new StringConstantType(str, str.span());
typ = new StringLiteralType(str, str.span());
}
default -> {
var text = _lookahead.text(lexer);
@@ -84,7 +84,7 @@ public interface ParserVisitor<Result> {
Result visitThisType(Type.ThisType type);
Result visitStringConstantType(Type.StringConstantType type);
Result visitStringLiteralType(Type.StringLiteralType type);
Result visitDeclaredType(Type.DeclaredType type);
@@ -72,14 +72,14 @@ public abstract sealed class Type extends AbstractNode {
}
}
public static final class StringConstantType extends Type {
public StringConstantType(StringConstant str, Span span) {
public static final class StringLiteralType extends Type {
public StringLiteralType(StringConstant str, Span span) {
super(span, List.of(str));
}
@Override
public <T> T accept(ParserVisitor<T> visitor) {
return visitor.visitStringConstantType(this);
return visitor.visitStringLiteralType(this);
}
public StringConstant getStr() {
@@ -1,5 +1,5 @@
/*
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
* 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.
@@ -38,6 +38,9 @@ public record FullSpan(
@Override
public String toString() {
if (lineBegin == lineEnd && colBegin == colEnd) {
return "(%d:%d)".formatted(lineBegin, colBegin);
}
return "(%d:%d - %d:%d)".formatted(lineBegin, colBegin, lineEnd, colEnd);
}
}
@@ -51,6 +51,10 @@ public final class Node {
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) {
@@ -16,13 +16,68 @@
package org.pkl.parser.syntax.generic;
public enum NodeType {
TERMINAL,
SHEBANG,
// affixes,
LINE_COMMENT(NodeKind.AFFIX),
BLOCK_COMMENT(NodeKind.AFFIX),
SEMICOLON(NodeKind.AFFIX),
// terminals: keywords
AMENDS_KEYWORD(NodeKind.TERMINAL),
AS_KEYWORD(NodeKind.TERMINAL),
CLASS_KEYWORD(NodeKind.TERMINAL),
ELSE_KEYWORD(NodeKind.TERMINAL),
EXTENDS_KEYWORD(NodeKind.TERMINAL),
FOR_KEYWORD(NodeKind.TERMINAL),
FUNCTION_KEYWORD(NodeKind.TERMINAL),
IF_KEYWORD(NodeKind.TERMINAL),
IMPORT_KEYWORD(NodeKind.TERMINAL),
IMPORT_STAR_KEYWORD(NodeKind.TERMINAL),
IN_KEYWORD(NodeKind.TERMINAL),
LET_KEYWORD(NodeKind.TERMINAL),
MODULE_KEYWORD(NodeKind.TERMINAL),
NEW_KEYWORD(NodeKind.TERMINAL),
OUT_KEYWORD(NodeKind.TERMINAL),
READ_KEYWORD(NodeKind.TERMINAL),
READ_QUESTION_KEYWORD(NodeKind.TERMINAL),
READ_STAR_KEYWORD(NodeKind.TERMINAL),
SUPER_KEYWORD(NodeKind.TERMINAL),
THROW_KEYWORD(NodeKind.TERMINAL),
TRACE_KEYWORD(NodeKind.TERMINAL),
TYPEALIAS_KEYWORD(NodeKind.TERMINAL),
WHEN_KEYWORD(NodeKind.TERMINAL),
// terminals: punctuation
ARROW(NodeKind.TERMINAL),
ASSIGN(NodeKind.TERMINAL),
AT(NodeKind.TERMINAL),
COLON(NodeKind.TERMINAL),
COMMA(NodeKind.TERMINAL),
DOT(NodeKind.TERMINAL),
GT(NodeKind.TERMINAL),
LBRACE(NodeKind.TERMINAL),
LBRACK(NodeKind.TERMINAL),
LPAREN(NodeKind.TERMINAL),
LPRED(NodeKind.TERMINAL),
LT(NodeKind.TERMINAL),
MINUS(NodeKind.TERMINAL),
NOT(NodeKind.TERMINAL),
QSPREAD(NodeKind.TERMINAL),
QUESTION(NodeKind.TERMINAL),
RBRACE(NodeKind.TERMINAL),
RBRACK(NodeKind.TERMINAL),
RPAREN(NodeKind.TERMINAL),
SPREAD(NodeKind.TERMINAL),
STAR(NodeKind.TERMINAL),
UNDERSCORE(NodeKind.TERMINAL),
UNION(NodeKind.TERMINAL),
// terminals: string delimiters
INTERPOLATION_START(NodeKind.TERMINAL),
STRING_START(NodeKind.TERMINAL),
STRING_MULTI_START(NodeKind.TERMINAL),
STRING_END(NodeKind.TERMINAL),
MODULE,
DOC_COMMENT,
DOC_COMMENT_LINE,
@@ -47,14 +102,14 @@ public enum NodeType {
CLASS_BODY,
CLASS_BODY_ELEMENTS,
CLASS_METHOD,
CLASS_METHOD_HEADER,
CLASS_METHOD_BODY,
METHOD_HEADER,
METHOD_BODY,
CLASS_PROPERTY,
CLASS_PROPERTY_HEADER,
CLASS_PROPERTY_HEADER_BEGIN,
CLASS_PROPERTY_BODY,
OBJECT_BODY,
OBJECT_MEMBER_LIST,
OBJECT_MEMBER_ELEMENTS,
PARAMETER,
TYPE_ANNOTATION,
PARAMETER_LIST,
@@ -67,7 +122,8 @@ public enum NodeType {
TYPE_ARGUMENT_LIST_ELEMENTS,
OBJECT_PARAMETER_LIST,
TYPE_PARAMETER,
STRING_CHARS,
STRING_CHARS(NodeKind.TERMINAL),
STRING_CONSTANT,
OPERATOR,
STRING_NEWLINE,
STRING_CONTINUATION,
@@ -81,6 +137,7 @@ public enum NodeType {
OBJECT_PROPERTY_BODY,
OBJECT_METHOD,
MEMBER_PREDICATE,
MEMBER_PREDICATE_HEADER,
OBJECT_ENTRY,
OBJECT_ENTRY_HEADER,
OBJECT_SPREAD,
@@ -107,7 +164,6 @@ public enum NodeType {
FUNCTION_LITERAL_EXPR(NodeKind.EXPR),
FUNCTION_LITERAL_BODY,
PARENTHESIZED_EXPR(NodeKind.EXPR),
PARENTHESIZED_EXPR_ELEMENTS,
SUPER_SUBSCRIPT_EXPR(NodeKind.EXPR),
SUPER_ACCESS_EXPR(NodeKind.EXPR),
SUBSCRIPT_EXPR(NodeKind.EXPR),
@@ -143,7 +199,7 @@ public enum NodeType {
PARENTHESIZED_TYPE_ELEMENTS,
DECLARED_TYPE(NodeKind.TYPE),
NULLABLE_TYPE(NodeKind.TYPE),
STRING_CONSTANT_TYPE(NodeKind.TYPE),
STRING_LITERAL_TYPE(NodeKind.TYPE),
CONSTRAINED_TYPE(NodeKind.TYPE),
CONSTRAINED_TYPE_CONSTRAINT,
CONSTRAINED_TYPE_ELEMENTS;
@@ -162,6 +218,10 @@ public enum NodeType {
return kind == NodeKind.AFFIX;
}
public boolean isTerminal() {
return kind == NodeKind.TERMINAL;
}
public boolean isExpression() {
return kind == NodeKind.EXPR;
}
@@ -174,6 +234,7 @@ public enum NodeType {
TYPE,
EXPR,
AFFIX,
TERMINAL,
NONE;
}
}
@@ -55,13 +55,13 @@ class GenericSexpRenderer(code: String) {
buf.append("(")
buf.append(name(node))
val oldTab = increaseTab()
var previousTerminal: Node? = null
var isDefault = false
for (child in node.children) {
if (child.type == NodeType.TERMINAL) previousTerminal = child
if (child.type in IGNORED_CHILDREN) continue
if (child.type == NodeType.STAR) isDefault = true
if (child.type.isIgnored()) continue
buf.append('\n')
if (previousTerminal != null && previousTerminal.text(source) == "*") {
previousTerminal = null
if (isDefault) {
isDefault = false
renderDefaultUnionType(child)
} else {
innerRender(child)
@@ -83,9 +83,9 @@ class GenericSexpRenderer(code: String) {
private fun collectChildren(node: Node): List<Node> =
when (node.type) {
NodeType.MULTI_LINE_STRING_LITERAL_EXPR ->
node.children.filter { it.type !in IGNORED_CHILDREN && !it.type.isStringData() }
node.children.filter { !it.type.isIgnored() && !it.type.isStringData() }
NodeType.SINGLE_LINE_STRING_LITERAL_EXPR -> {
val children = node.children.filter { it.type !in IGNORED_CHILDREN }
val children = node.children.filter { !it.type.isIgnored() }
val res = mutableListOf<Node>()
var prev: Node? = null
for (child in children) {
@@ -97,11 +97,13 @@ class GenericSexpRenderer(code: String) {
}
res
}
// a string constant is rendered as a single leaf
NodeType.STRING_CONSTANT,
NodeType.DOC_COMMENT -> listOf()
else -> {
val nodes = mutableListOf<Node>()
for (child in node.children) {
if (child.type in IGNORED_CHILDREN) continue
if (child.type.isIgnored()) continue
if (child.type in UNPACK_CHILDREN) {
nodes += collectChildren(child)
} else {
@@ -112,6 +114,9 @@ class GenericSexpRenderer(code: String) {
}
}
private fun NodeType.isIgnored(): Boolean =
this != NodeType.STRING_CHARS && (isTerminal || this in IGNORED_CHILDREN)
private fun NodeType.isStringData(): Boolean =
this == NodeType.STRING_CHARS || this == NodeType.STRING_ESCAPE
@@ -125,19 +130,18 @@ class GenericSexpRenderer(code: String) {
NodeType.EXTENDS_CLAUSE,
NodeType.AMENDS_CLAUSE -> "extendsOrAmendsClause"
NodeType.TYPEALIAS -> "typeAlias"
NodeType.STRING_CONSTANT,
NodeType.STRING_ESCAPE -> "stringChars"
NodeType.QUALIFIED_ACCESS_EXPR -> {
val op = node.findChildByType(NodeType.OPERATOR)!!
if (op.text(source) == ".") "qualifiedAccessExpr" else "nullableQualifiedAccessExpr"
}
NodeType.READ_EXPR -> {
val terminal = node.children.find { it.type == NodeType.TERMINAL }!!.text(source)
when (terminal) {
"read*" -> "readGlobExpr"
"read?" -> "readNullExpr"
NodeType.READ_EXPR ->
when {
node.findChildByType(NodeType.READ_STAR_KEYWORD) != null -> "readGlobExpr"
node.findChildByType(NodeType.READ_QUESTION_KEYWORD) != null -> "readNullExpr"
else -> "readExpr"
}
}
else -> {
val names = node.type.name.split('_').map { it.lowercase() }
if (names.size > 1) {
@@ -148,9 +152,9 @@ class GenericSexpRenderer(code: String) {
}
private fun importName(node: Node, isExpr: Boolean): String {
val terminal = node.children.find { it.type == NodeType.TERMINAL }!!.text(source)
val isGlob = node.findChildByType(NodeType.IMPORT_STAR_KEYWORD) != null
val suffix = if (isExpr) "Expr" else "Clause"
return if (terminal == "import*") "importGlob$suffix" else "import$suffix"
return if (isGlob) "importGlob$suffix" else "import$suffix"
}
private fun binopName(node: Node): String {
@@ -194,7 +198,6 @@ class GenericSexpRenderer(code: String) {
NodeType.BLOCK_COMMENT,
NodeType.SHEBANG,
NodeType.SEMICOLON,
NodeType.TERMINAL,
NodeType.OPERATOR,
NodeType.STRING_NEWLINE,
NodeType.STRING_CONTINUATION,
@@ -210,15 +213,16 @@ class GenericSexpRenderer(code: String) {
NodeType.CLASS_PROPERTY_HEADER,
NodeType.CLASS_PROPERTY_HEADER_BEGIN,
NodeType.CLASS_PROPERTY_BODY,
NodeType.CLASS_METHOD_HEADER,
NodeType.CLASS_METHOD_BODY,
NodeType.METHOD_HEADER,
NodeType.METHOD_BODY,
NodeType.CLASS_HEADER,
NodeType.CLASS_HEADER_EXTENDS,
NodeType.CLASS_BODY_ELEMENTS,
NodeType.MODIFIER_LIST,
NodeType.NEW_HEADER,
NodeType.OBJECT_MEMBER_LIST,
NodeType.OBJECT_MEMBER_ELEMENTS,
NodeType.OBJECT_ENTRY_HEADER,
NodeType.MEMBER_PREDICATE_HEADER,
NodeType.OBJECT_PROPERTY_HEADER,
NodeType.OBJECT_PROPERTY_HEADER_BEGIN,
NodeType.OBJECT_PROPERTY_BODY,
@@ -241,7 +245,6 @@ class GenericSexpRenderer(code: String) {
NodeType.TYPE_ARGUMENT_LIST_ELEMENTS,
NodeType.LET_PARAMETER_DEFINITION,
NodeType.LET_PARAMETER,
NodeType.PARENTHESIZED_EXPR_ELEMENTS,
NodeType.PARENTHESIZED_TYPE_ELEMENTS,
NodeType.FUNCTION_TYPE_PARAMETERS,
)
@@ -777,7 +777,7 @@ class SexpRenderer {
buf.append(tab)
buf.append("(thisType)")
}
is StringConstantType -> renderStringConstantType(type)
is StringLiteralType -> renderStringLiteralType(type)
is DeclaredType -> renderDeclaredType(type)
is ParenthesizedType -> renderParenthesizedType(type)
is NullableType -> renderNullableType(type)
@@ -787,9 +787,9 @@ class SexpRenderer {
}
}
fun renderStringConstantType(type: StringConstantType) {
fun renderStringLiteralType(type: StringLiteralType) {
buf.append(tab)
buf.append("(stringConstantType")
buf.append("(stringLiteralType")
val oldTab = increaseTab()
buf.append('\n')
renderStringConstant(type.str)
+4 -11
View File
@@ -30,6 +30,7 @@
module pkl.reflect
import "pkl:base"
import "pkl:syntax"
/// A mirror for the [Any] type.
const anyType: DeclaredType = DeclaredType(Class(Any))
@@ -166,7 +167,7 @@ external const function TypeVariable(referent: TypeParameter): TypeVariable
/// A program declaration.
abstract external class Declaration {
/// The source location of this declaration.
location: SourceLocation
location: syntax.SourceLocation
/// The documentation comment for this declaration, if any.
docComment: String?
@@ -457,13 +458,5 @@ external class TypeVariable extends Type {
}
/// A source location within a module.
external class SourceLocation {
/// The 1-based line number of this source location.
line: UInt
/// The 1-based column number of this source location.
column: UInt
/// The display URI of this source location.
displayUri: String
}
@Deprecated { since = "0.33.0"; replaceWith = "syntax.SourceLocation" }
typealias SourceLocation = syntax.SourceLocation
+422
View File
@@ -0,0 +1,422 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
/// Utilities for managing Pkl source code.
@ModuleInfo { minPklVersion = "0.33.0" }
@Since { version = "0.33.0" }
module pkl.syntax
/// Utilities to parse Pkl source code to a syntax tree.
class Parser {
/// Parse the string or resource as a Pkl module, returning either a syntax node or
/// throwing an error.
external function parseModule(source: String | Resource): Node
/// Parse the string or resource as a Pkl module, returning either a syntax node or null
/// in case of an error.
external function parseModuleOrNull(source: String | Resource): Node?
/// Parse the string or resource as a Pkl expression, returning either a syntax node or
/// throwing an error.
external function parseExpression(source: String | Resource): Node
/// Parse the string or resource as a Pkl expression, returning either a syntax node or null
/// in case of an error.
external function parseExpressionOrNull(source: String | Resource): Node?
}
/// Renders a [Node] back to Pkl source code, using the canonical Pkl formatter.
class FormattingRenderer {
/// The grammar version to target.
///
/// `"V1"` matches Pkl 0.25 - 0.29, `"V2"` matches Pkl 0.30+.
grammarVersion: "V1" | "V2" = "V2"
/// Render [node] as Pkl source code.
external function render(node: Node): String
}
/// A generic, untyped node in the Pkl syntax tree.
///
/// A node is either a *leaf*, carrying no [children], or a *branch*, whose content is its
/// [children]. Either way, [text] is the source code the node spans.
class Node {
/// The kind of this node.
type: NodeType
/// The child nodes, in source order. Empty for leaf nodes.
children: List<Node>(isTerminal.implies(isEmpty))
/// The parent node, or [null] for the root and for nodes not attached to a tree.
hidden parent: Node?
/// The verbatim source text for this node.
///
/// [FormattingRenderer] only renders this text if this is a [terminal][isTerminal] node.
text: String?(isTerminal.implies(this != null))
/// The source location of this node.
span: Span?
/// Tells if this is a terminal node; i.e. a node that spans a single token of the grammar.
///
/// A terminal never has [children], but not every node without [children] is a terminal.
fixed isTerminal: Boolean = type is TerminalNodeType
/// Fold [operator] over this node and its descendants, top-down in pre-order.
///
/// Example:
/// ```
/// // count the if-expressions in a module
/// module.fold(0, (acc, n) -> if (n.type == "if_expr") acc + 1 else acc)
/// ```
external function fold<Result>(initial: Result, operator: (Result, Node) -> Result): Result
/// Returns the first child matching [predicate], searching depth-first in pre-order.
///
/// *Child* means any node below this one, at any depth.
/// This node itself is not considered.
/// The search stops at the first match.
///
/// Throws if no child matches [predicate].
///
/// Example:
/// ```
/// // the name of the first class declared in a module
/// module.findChild((n) -> n.type == "class").findChildOfType("identifier").text
/// ```
external function findChild(predicate: (Node) -> Boolean): Node
/// Same as [findChild()] but returns [null] if no child matches [predicate].
external function findChildOrNull(predicate: (Node) -> Boolean): Node?
/// Returns every child matching [predicate], in depth-first pre-order.
///
/// *Child* means any node below this one, at any depth.
/// This node itself is not considered.
/// A matching node is searched for further matches, so a match may contain another.
external function findChildren(predicate: (Node) -> Boolean): List<Node>
/// Returns the first child whose [type] is [type].
///
/// Throws if no child has this type.
external function findChildOfType(type: NodeType): Node
/// Same as [findChildOfType()] but returns [null] if no child has this type.
external function findChildOfTypeOrNull(type: NodeType): Node?
/// Returns every child whose [type] is [type], in depth-first pre-order.
external function findChildrenOfType(type: NodeType): List<Node>
/// Returns this node with the first child matching [predicate] replaced by [replacer]'s result.
///
/// The child replaced is the one [findChild()] returns. If there is none, the tree is
/// returned unchanged.
/// The replacement is left as it is, so it is not searched for further matches.
external function replaceChild(predicate: (Node) -> Boolean, replacer: (Node) -> Node): Node
/// Returns this node with every child matching [predicate] replaced by [replacer]'s result.
///
/// The children replaced are those [findChildren()] returns, except that a replacement is left
/// as it is, so a match nested in another match is left to [replacer].
external function replaceChildren(predicate: (Node) -> Boolean, replacer: (Node) -> Node): Node
/// Returns this node with the first child whose [type] is [type] replaced by [replacer]'s result.
///
/// If there is no such child, the tree is returned unchanged.
external function replaceChildOfType(type: NodeType, replacer: (Node) -> Node): Node
/// Returns this node with every child whose [type] is [type] replaced by [replacer]'s result.
external function replaceChildrenOfType(type: NodeType, replacer: (Node) -> Node): Node
/// Returns the nearest parent matching [predicate].
///
/// Throws if no parent matches [predicate].
external function findParent(predicate: (Node) -> Boolean): Node
/// Same as [findParent()] but returns [null] if no parent matches [predicate].
external function findParentOrNull(predicate: (Node) -> Boolean): Node?
/// Returns the nearest parent whose [type] is [type].
///
/// Throws if no parent has this type.
external function findParentOfType(type: NodeType): Node
/// Same as [findParentOfType()] but returns [null] if no parent has this type.
external function findParentOfTypeOrNull(type: NodeType): Node?
/// Returns every parent matching [predicate], nearest first.
external function findParents(predicate: (Node) -> Boolean): List<Node>
/// Tells whether any parent matches [predicate].
external function hasParent(predicate: (Node) -> Boolean): Boolean
/// Tells whether any parent's [type] is [type].
external function hasParentOfType(type: NodeType): Boolean
}
/// A range of source code, spanning [start] (inclusive) to [end] (exclusive).
class Span {
/// The start of this span.
start: SourceLocation
/// The end of this span.
end: SourceLocation
/// The display URI of this span.
///
/// Rendered for the editor configured in the Pkl settings file, the same way a location is
/// rendered in a stack trace.
displayUri: String
}
/// A position in source code, given as a 1-based line and column.
class SourceLocation {
/// The 1-based line number of this source location.
line: UInt
/// The 1-based column number of this source location.
column: UInt
/// A URI identifying this source location, suitable for display.
///
/// The exact format depends on where this location came from and on the editor configured in the
/// Pkl settings file.
displayUri: String
}
/// The kinds of node in the Pkl syntax tree.
typealias NodeType =
TerminalNodeType
| ExprNodeType
| TypeNodeType
| ObjectMemberNodeType
| ClassMemberNodeType
| ModuleMemberNodeType
// module structure
| "module"
| "doc_comment"
| "modifier_list"
| "amends_clause"
| "extends_clause"
| "module_declaration"
| "module_definition"
| "annotation"
| "qualified_identifier"
| "import"
| "import_alias"
| "import_list"
| "typealias_header"
| "typealias_body"
| "class_header"
| "class_header_extends"
| "class_body"
| "class_body_elements"
| "method_header"
| "method_body"
| "class_property_header"
| "class_property_header_begin"
| "class_property_body"
| "object_body"
| "object_member_elements"
| "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_constant"
// parts of members
| "object_property_header"
| "object_property_header_begin"
| "object_property_body"
| "object_entry_header"
| "member_predicate_header"
| "when_generator_header"
| "for_generator_header"
| "for_generator_header_definition"
| "for_generator_header_definition_header"
// parts of expressions
| "new_header"
| "function_literal_body"
| "if_header"
| "if_condition"
| "if_condition_expr"
| "if_then_expr"
| "if_else_expr"
| "let_parameter_definition"
| "let_parameter"
// parts of types
| "function_type_parameters"
| "parenthesized_type_elements"
| "constrained_type_constraint"
| "constrained_type_elements"
/// The [NodeType]s that describe a terminal: a node that spans a single token of the grammar, such
/// as a keyword, a punctuation character, an identifier or a literal.
///
/// A terminal never has [Node.children].
typealias TerminalNodeType =
// keywords
"amends_keyword"
| "as_keyword"
| "class_keyword"
| "else_keyword"
| "extends_keyword"
| "for_keyword"
| "function_keyword"
| "if_keyword"
| "import_keyword"
| "import_star_keyword"
| "in_keyword"
| "let_keyword"
| "module_keyword"
| "new_keyword"
| "out_keyword"
| "read_keyword"
| "read_question_keyword"
| "read_star_keyword"
| "super_keyword"
| "throw_keyword"
| "trace_keyword"
| "typealias_keyword"
| "when_keyword"
// punctuation
| "arrow"
| "assign"
| "at"
| "colon"
| "comma"
| "dot"
| "gt"
| "lbrace"
| "lbrack"
| "lparen"
| "lpred"
| "lt"
| "minus"
| "not"
| "qspread"
| "question"
| "rbrace"
| "rbrack"
| "rparen"
| "spread"
| "star"
| "underscore"
| "union"
// string delimiters
| "interpolation_start"
| "string_start"
| "string_multi_start"
| "string_end"
// string content
| "string_chars"
| "string_newline"
| "string_continuation"
| "string_escape"
// affixes
| "shebang"
| "line_comment"
| "block_comment"
| "semicolon"
| "doc_comment_line"
// names, modifiers and operators
| "identifier"
| "modifier"
| "operator"
// literals
| "bool_literal_expr"
| "int_literal_expr"
| "float_literal_expr"
// expressions without operands
| "this_expr"
| "outer_expr"
| "module_expr"
| "null_expr"
// types without operands
| "unknown_type"
| "nothing_type"
| "module_type"
| "this_type"
/// The [NodeType]s that describe an expression.
typealias ExprNodeType =
"this_expr"
| "outer_expr"
| "module_expr"
| "null_expr"
| "throw_expr"
| "trace_expr"
| "import_expr"
| "read_expr"
| "new_expr"
| "unary_minus_expr"
| "logical_not_expr"
| "function_literal_expr"
| "parenthesized_expr"
| "super_subscript_expr"
| "super_access_expr"
| "subscript_expr"
| "qualified_access_expr"
| "if_expr"
| "let_expr"
| "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"
/// The [NodeType]s that describe a type.
typealias TypeNodeType =
"unknown_type"
| "nothing_type"
| "module_type"
| "this_type"
| "union_type"
| "function_type"
| "parenthesized_type"
| "declared_type"
| "nullable_type"
| "string_literal_type"
| "constrained_type"
/// The [NodeType]s that describe a member of an object body.
typealias ObjectMemberNodeType =
"object_element"
| "object_property"
| "object_method"
| "member_predicate"
| "object_entry"
| "object_spread"
| "when_generator"
| "for_generator"
/// The [NodeType]s that describe a member of a class body.
typealias ClassMemberNodeType = "class_property" | "class_method"
/// The [NodeType]s that describe a member of a module.
typealias ModuleMemberNodeType = "class_property" | "class_method" | "class" | "typealias"