Add pkl:syntax stdlib module (#1821)
This commit is contained in:
1 parent
0cb9843af7
commit
939e844c03
48 files changed
+3149
-214
No files matched your search
@@ -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.
|
||||
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
x = 1
|
||||
|
||||
foo {
|
||||
bar = "baz"
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import "pkl:syntax"
|
||||
|
||||
local parser = new syntax.Parser {}
|
||||
|
||||
res = parser.parseExpression("1 +")
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import "pkl:syntax"
|
||||
|
||||
local parser = new syntax.Parser {}
|
||||
|
||||
res =
|
||||
parser.parseModule(
|
||||
"""
|
||||
foo {
|
||||
bar =
|
||||
}
|
||||
""",
|
||||
)
|
||||
+8
@@ -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
|
||||
}
|
||||
""")
|
||||
}
|
||||
}
|
||||
+1
@@ -27,6 +27,7 @@ pkl:release
|
||||
pkl:semver
|
||||
pkl:settings
|
||||
pkl:shell
|
||||
pkl:syntax
|
||||
pkl:test
|
||||
pkl:xml
|
||||
pkl:yaml
|
||||
|
||||
+16
@@ -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)
|
||||
+16
@@ -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)
|
||||
+16
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user