Resolve variables at parse time (#1429)

This replaces `ResolveVariableNode` and `ResolveMethodNode` with their resolution.
When we build the truffle node tree, we determine whether names resolve to:

* lexical scope
* base module
* implicit this

Then, we use this information to directly construct the underlying nodes (`ReadPropertyNode`, `ReadLocalPropertyNode`, etc).

Additionally, `AstBuilder` determines whether the property access must be const or not.

This introduces a `BaseModuleMembers` registry, which gets generated as part of Java compilation.
This commit is contained in:
Islon Scherer
2026-05-26 23:08:20 +02:00
committed by GitHub
parent b2f005d11d
commit dbf04f6598
45 changed files with 1930 additions and 835 deletions
+1 -1
View File
@@ -193,7 +193,7 @@ Too many lines separate `foo` and `baz`.
Properties that override an existing property shouldn't have doc comments nor type annotations,
unless the type is intentionally overridden via `extends`.
[source%tested,{pkl}]
[source%parsed,{pkl}]
----
amends "myOtherModule.pkl"
+31
View File
@@ -61,6 +61,7 @@ dependencies {
add("generatorImplementation", libs.javaPoet)
add("generatorImplementation", libs.truffleApi)
add("generatorImplementation", libs.jspecify)
add("generatorImplementation", projects.pklParser)
javaExecutableConfiguration(project(":pkl-cli", "javaExecutable"))
}
@@ -140,6 +141,36 @@ tasks.test {
maxHeapSize = "1g"
}
val generateBaseModuleMembers by
tasks.registering(JavaExec::class) {
group = "build"
val outputDir = layout.buildDirectory.dir("generated/sources/baseModuleMembers")
val basePklFile = layout.projectDirectory.file("../stdlib/base.pkl")
inputs
.file(basePklFile)
.withPropertyName("basePkl")
.withPathSensitivity(PathSensitivity.RELATIVE)
outputs.dir(outputDir)
classpath =
generatorSourceSet.get().runtimeClasspath + tasks.processResources.get().outputs.files
mainClass = "org.pkl.core.generator.BaseModuleMembersGenerator"
argumentProviders.add(
CommandLineArgumentProvider {
listOf(basePklFile.asFile.absolutePath, outputDir.get().asFile.absolutePath)
}
)
}
sourceSets.main { java.srcDir(layout.buildDirectory.dir("generated/sources/baseModuleMembers")) }
tasks.compileJava { dependsOn(generateBaseModuleMembers) }
val testJavaExecutable by
tasks.registering(Test::class) {
configureExecutableTest("LanguageSnippetTestsEngine")
@@ -0,0 +1,144 @@
/*
* 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.generator;
import com.palantir.javapoet.CodeBlock;
import com.palantir.javapoet.JavaFile;
import com.palantir.javapoet.MethodSpec;
import com.palantir.javapoet.TypeName;
import com.palantir.javapoet.TypeSpec;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.Modifier;
import org.pkl.parser.Parser;
import org.pkl.parser.syntax.Modifier.ModifierValue;
public final class BaseModuleMembersGenerator {
record Members(Set<String> properties, Set<String> methods) {}
public static void main(String[] args) {
if (args.length < 2) {
throw new IllegalArgumentException(
"Usage: BaseModuleMembersGenerator <path-to-base.pkl> <output-dir>");
}
var members = buildMembers(args[0]);
generateJavaCode(members, args[1]);
}
private static void generateJavaCode(Members members, String outputDir) {
var privateConstructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build();
var hasPropertyMethod =
buildHasMethod("hasProperty", members.properties().stream().sorted().toList());
var hasMethodMethod = buildHasMethod("hasMethod", members.methods().stream().sorted().toList());
var classSpec =
TypeSpec.classBuilder("BaseModuleMembers")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(privateConstructor)
.addMethod(hasPropertyMethod)
.addMethod(hasMethodMethod)
.build();
var javaFile =
JavaFile.builder("org.pkl.core.runtime", classSpec)
.addFileComment("DO NOT EDIT — generated by BaseModuleMembersGenerator")
.build();
try {
javaFile.writeTo(Path.of(outputDir));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static MethodSpec buildHasMethod(String methodName, List<String> names) {
var code = CodeBlock.builder();
code.add("return switch (name) {\n");
code.indent();
code.add("case $S", names.get(0));
if (names.size() == 1) {
code.add(" -> true;\n");
} else {
code.add(",\n");
code.indent();
for (var i = 1; i < names.size() - 1; i++) {
code.add("$S,\n", names.get(i));
}
code.add("$S -> true;\n", names.get(names.size() - 1));
code.unindent();
}
code.add("default -> false;\n");
code.unindent();
code.add("};\n");
return MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(TypeName.BOOLEAN)
.addParameter(String.class, "name")
.addCode(code.build())
.build();
}
private static String getBaseModuleText(String path) {
try {
return Files.readString(Path.of(path));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static Members buildMembers(String basePklPath) {
var text = getBaseModuleText(basePklPath);
var parsed = new Parser().parseModule(text);
var properties = new HashSet<String>();
var methods = new HashSet<String>();
for (var property : parsed.getProperties()) {
if (isLocal(property.getModifiers())) {
continue;
}
properties.add(property.getName().getValue());
}
for (var clazz : parsed.getClasses()) {
if (isLocal(clazz.getModifiers())) {
continue;
}
properties.add(clazz.getName().getValue());
}
for (var typealias : parsed.getTypeAliases()) {
if (isLocal(typealias.getModifiers())) {
continue;
}
properties.add(typealias.getName().getValue());
}
for (var method : parsed.getMethods()) {
if (isLocal(method.getModifiers())) {
continue;
}
methods.add(method.getName().getValue());
}
return new Members(properties, methods);
}
private static boolean isLocal(List<org.pkl.parser.syntax.Modifier> modifiers) {
return modifiers.stream().anyMatch((it) -> it.getValue() == ModifierValue.LOCAL);
}
}
@@ -55,6 +55,8 @@ public final class VmModifier {
public static final int GLOB = 0x1000;
public static final int AMBIGUOUS_LOCALITY = 0x10000;
// modifier sets
public static final int NONE = 0;
@@ -126,6 +128,10 @@ public final class VmModifier {
return (modifiers & CONST) != 0;
}
public static boolean isAmbiguousLocality(int modifiers) {
return (modifiers & AMBIGUOUS_LOCALITY) != 0;
}
public static boolean isElement(int modifiers) {
return (modifiers & ELEMENT) != 0;
}
@@ -154,6 +160,10 @@ public final class VmModifier {
return (modifiers & (CONST | FIXED)) != 0;
}
public static boolean hasSameModifier(int modifiersA, int modifiersB, int modifier) {
return (modifiersA & modifier) == (modifiersB & modifier);
}
public static Set<Modifier> export(int modifiers, boolean isClass) {
var result = EnumSet.noneOf(Modifier.class);
@@ -47,8 +47,18 @@ import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.MemberLookupMode;
import org.pkl.core.ast.PklRootNode;
import org.pkl.core.ast.VmModifier;
import org.pkl.core.ast.builder.MethodResolution.ImplicitBaseMethod;
import org.pkl.core.ast.builder.MethodResolution.ImplicitThisMethod;
import org.pkl.core.ast.builder.MethodResolution.LexicalMethod;
import org.pkl.core.ast.builder.SymbolTable.AnnotationScope;
import org.pkl.core.ast.builder.SymbolTable.ClassScope;
import org.pkl.core.ast.builder.SymbolTable.ModuleScope;
import org.pkl.core.ast.builder.SymbolTable.ObjectScope;
import org.pkl.core.ast.builder.VariableResolution.ForGeneratorVariable;
import org.pkl.core.ast.builder.VariableResolution.ImplicitBaseProperty;
import org.pkl.core.ast.builder.VariableResolution.ImplicitThisProperty;
import org.pkl.core.ast.builder.VariableResolution.LexicalProperty;
import org.pkl.core.ast.builder.VariableResolution.Parameter;
import org.pkl.core.ast.expression.binary.AdditionNodeGen;
import org.pkl.core.ast.expression.binary.DivisionNodeGen;
import org.pkl.core.ast.expression.binary.EqualNodeGen;
@@ -101,12 +111,16 @@ import org.pkl.core.ast.expression.literal.TrueLiteralNode;
import org.pkl.core.ast.expression.member.InferParentWithinMethodNode;
import org.pkl.core.ast.expression.member.InferParentWithinObjectMethodNode;
import org.pkl.core.ast.expression.member.InferParentWithinPropertyNodeGen;
import org.pkl.core.ast.expression.member.InvokeClassMethodNode;
import org.pkl.core.ast.expression.member.InvokeMethodDirectNode;
import org.pkl.core.ast.expression.member.InvokeMethodVirtualNodeGen;
import org.pkl.core.ast.expression.member.InvokeObjectMethodNode;
import org.pkl.core.ast.expression.member.InvokeSuperMethodNodeGen;
import org.pkl.core.ast.expression.member.ReadAmbiguousLocalityPropertyNode;
import org.pkl.core.ast.expression.member.ReadLocalPropertyNode;
import org.pkl.core.ast.expression.member.ReadPropertyNodeGen;
import org.pkl.core.ast.expression.member.ReadSuperEntryNode;
import org.pkl.core.ast.expression.member.ReadSuperPropertyNode;
import org.pkl.core.ast.expression.member.ResolveMethodNode;
import org.pkl.core.ast.expression.primary.GetEnclosingOwnerNode;
import org.pkl.core.ast.expression.primary.GetEnclosingReceiverNode;
import org.pkl.core.ast.expression.primary.GetMemberKeyNode;
@@ -114,7 +128,6 @@ import org.pkl.core.ast.expression.primary.GetModuleNode;
import org.pkl.core.ast.expression.primary.GetOwnerNode;
import org.pkl.core.ast.expression.primary.GetReceiverNode;
import org.pkl.core.ast.expression.primary.OuterNode;
import org.pkl.core.ast.expression.primary.ResolveVariableNode;
import org.pkl.core.ast.expression.primary.ThisNode;
import org.pkl.core.ast.expression.ternary.IfElseNode;
import org.pkl.core.ast.expression.unary.AbstractImportNode;
@@ -131,6 +144,9 @@ import org.pkl.core.ast.expression.unary.ReadOrNullNodeGen;
import org.pkl.core.ast.expression.unary.ThrowNodeGen;
import org.pkl.core.ast.expression.unary.TraceNode;
import org.pkl.core.ast.expression.unary.UnaryMinusNodeGen;
import org.pkl.core.ast.frame.GetEnclosingFrameNode;
import org.pkl.core.ast.frame.ReadExactFrameSlotNodeGen;
import org.pkl.core.ast.frame.ReadFrameSlotNodeGen;
import org.pkl.core.ast.internal.GetBaseModuleClassNode;
import org.pkl.core.ast.internal.GetClassNodeGen;
import org.pkl.core.ast.internal.ToStringNodeGen;
@@ -163,6 +179,7 @@ import org.pkl.core.module.ModuleKeys;
import org.pkl.core.module.ResolvedModuleKey;
import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.runtime.BaseModule;
import org.pkl.core.runtime.FrameDescriptorBuilder;
import org.pkl.core.runtime.ModuleInfo;
import org.pkl.core.runtime.ModuleResolver;
import org.pkl.core.runtime.VmBytes;
@@ -239,7 +256,6 @@ import org.pkl.parser.syntax.ObjectMember.ObjectMethod;
import org.pkl.parser.syntax.ObjectMember.ObjectProperty;
import org.pkl.parser.syntax.ObjectMember.ObjectSpread;
import org.pkl.parser.syntax.ObjectMember.WhenGenerator;
import org.pkl.parser.syntax.Parameter;
import org.pkl.parser.syntax.Parameter.TypedIdentifier;
import org.pkl.parser.syntax.ParameterList;
import org.pkl.parser.syntax.QualifiedIdentifier;
@@ -285,7 +301,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
isBaseModule = ModuleKeys.isBaseModule(moduleKey);
isStdLibModule = ModuleKeys.isStdLibModule(moduleKey);
externalMemberRegistry = MemberRegistryFactory.get(moduleKey);
symbolTable = new SymbolTable(moduleInfo);
symbolTable = new SymbolTable(moduleInfo, isBaseModule);
isMethodReturnTypeChecked = !isStdLibModule || IoUtils.isTestMode();
}
@@ -490,10 +506,10 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
@Override
public GetModuleNode visitModuleExpr(ModuleExpr expr) {
var currentScope = symbolTable.getCurrentScope();
// cannot use unqualified `module` in a const context
if (symbolTable.getCurrentScope().getConstLevel().isConst()
&& !(expr.parent() instanceof QualifiedAccessExpr)) {
var scope = symbolTable.getCurrentScope();
if (currentScope.getConstLevel().isConst() && !(expr.parent() instanceof QualifiedAccessExpr)) {
var scope = currentScope;
while (scope != null
&& !(scope instanceof AnnotationScope)
&& !(scope instanceof ClassScope)) {
@@ -501,7 +517,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
}
if (scope == null) {
throw exceptionBuilder()
.evalError("moduleIsNotConst", symbolTable.getCurrentScope().getName().toString())
.evalError("moduleIsNotConst", currentScope.getName().toString())
.withSourceSection(createSourceSection(expr))
.build();
}
@@ -636,44 +652,161 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
};
}
@Override
public ExpressionNode visitUnqualifiedAccessExpr(UnqualifiedAccessExpr expr) {
var identifier = toIdentifier(expr.getIdentifier().getValue());
var argList = expr.getArgumentList();
if (argList == null) {
return createResolveVariableNode(createSourceSection(expr), identifier);
private ExpressionNode resolveReadVariable(UnqualifiedAccessExpr expr) {
var name = expr.getIdentifier().getValue();
var scope = symbolTable.getCurrentScope();
var sourceSection = createSourceSection(expr);
var constLevel = scope.getConstLevel();
var constDepth = scope.getConstDepth();
var resolution = scope.resolveVariable(name);
if (resolution instanceof LexicalProperty p) {
var needsConst =
switch (constLevel) {
case NONE -> false;
case MODULE -> p.isModuleScope();
case ALL -> p.levelsUp() > constDepth;
};
if (p.isAmbiguousLocality()) {
return new ReadAmbiguousLocalityPropertyNode(
sourceSection, org.pkl.core.runtime.Identifier.get(name), p.levelsUp(), needsConst);
}
if (p.isLocal()) {
return new ReadLocalPropertyNode(
sourceSection,
org.pkl.core.runtime.Identifier.localProperty(name),
p.levelsUp(),
needsConst);
}
return ReadPropertyNodeGen.create(
sourceSection,
org.pkl.core.runtime.Identifier.get(name),
MemberLookupMode.IMPLICIT_LEXICAL,
needsConst,
p.levelsUp() == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(p.levelsUp()));
} else if (resolution instanceof ForGeneratorVariable p) {
// Parameters can possibly write to frame slots actually in a frame that is one level
// higher than what we can tell at parse time. However, for generator variables always
// write to frame slots in the same frame.
//
// function foo(bar) = new Mixin {
// [bar] = 1 <--- actually 1 level, not 0
// for (elem in qux) {
// elem <--- actually 0 level
// }
// }
//
// This is clearer when considering the desugared code:
//
// function foo(bar) = (it) -> (it) {
// res = bar
// for (elem in qux) {
// elem
// }
// }
return p.levelsUp() == 0
? ReadExactFrameSlotNodeGen.create(sourceSection, p.slot())
: ReadFrameSlotNodeGen.create(
sourceSection, p.slot(), new GetEnclosingFrameNode(p.levelsUp()));
} else if (resolution instanceof Parameter p) {
return ReadFrameSlotNodeGen.create(
sourceSection, p.slot(), new GetEnclosingFrameNode(p.levelsUp()));
} else if (resolution instanceof ImplicitBaseProperty) {
return ReadPropertyNodeGen.create(
sourceSection,
org.pkl.core.runtime.Identifier.get(name),
MemberLookupMode.IMPLICIT_BASE,
false,
new ConstantValueNode(BaseModule.getModule()));
} else if (resolution instanceof ImplicitThisProperty) {
var isCustomThisScope = scope.isCustomThisScope();
var needsConst = constLevel == ConstLevel.ALL && constDepth == -1 && !isCustomThisScope;
return ReadPropertyNodeGen.create(
sourceSection,
org.pkl.core.runtime.Identifier.get(name),
MemberLookupMode.IMPLICIT_THIS,
needsConst,
VmUtils.createThisNode(VmUtils.unavailableSourceSection(), isCustomThisScope));
} else {
throw PklBugException.unreachableCode();
}
}
// TODO: make sure that no user-defined List/Set/Map method is in scope
// TODO: support qualified calls (e.g., `import "pkl:base"; x =
// base.List()/Set()/Map()/Bytes()`) for correctness
private ExpressionNode resolvedMethodCall(UnqualifiedAccessExpr expr, ArgumentList argList) {
var name = expr.getIdentifier().getValue();
var scope = symbolTable.getCurrentScope();
var sourceSection = createSourceSection(expr);
var constLevel = scope.getConstLevel();
var constDepth = scope.getConstDepth();
var resolution = scope.resolveMethod(name);
if (resolution instanceof LexicalMethod method) {
var levelsUp = method.levelsUp();
var identifier = org.pkl.core.runtime.Identifier.method(name, method.isLocal());
var args = visitArgumentList(argList);
var needsConst =
switch (constLevel) {
case NONE -> false;
case MODULE -> method.isModuleScope();
case ALL -> method.levelsUp() > constDepth;
};
if (method.isObjectMethod()) {
return new InvokeObjectMethodNode(sourceSection, identifier, levelsUp, args, needsConst);
}
if (method.isOnClosedClass() || method.isLocal() || method.isExternal()) {
return new InvokeClassMethodNode(sourceSection, identifier, levelsUp, args, needsConst);
}
return InvokeMethodVirtualNodeGen.create(
sourceSection,
identifier,
args,
MemberLookupMode.IMPLICIT_LEXICAL,
needsConst,
levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp),
GetClassNodeGen.create(null));
} else if (resolution instanceof ImplicitBaseMethod) {
// TODO: support qualified calls (e.g., `import("pkl:base").List()`) for
// correctness
var identifier = org.pkl.core.runtime.Identifier.get(name);
// Assumption: `pkl:base` does not call List/Set/Map/Bytes constructors.
// `resolveMethod` never returns `ImplicitBase` when resolving within
// pkl:base itself.
if (identifier == org.pkl.core.runtime.Identifier.LIST) {
return doVisitListLiteral(expr, argList);
}
if (identifier == org.pkl.core.runtime.Identifier.SET) {
} else if (identifier == org.pkl.core.runtime.Identifier.SET) {
return doVisitSetLiteral(expr, argList);
}
if (identifier == org.pkl.core.runtime.Identifier.MAP) {
} else if (identifier == org.pkl.core.runtime.Identifier.MAP) {
return doVisitMapLiteral(expr, argList);
}
if (identifier == org.pkl.core.runtime.Identifier.BYTES_CONSTRUCTOR) {
} else if (identifier == org.pkl.core.runtime.Identifier.BYTES_CONSTRUCTOR) {
return doVisitBytesLiteral(expr, argList);
} else {
var baseModule = BaseModule.getModule();
var method = baseModule.getVmClass().getDeclaredMethod(identifier);
assert method != null;
return new InvokeMethodDirectNode(
createSourceSection(expr),
method,
new ConstantValueNode(baseModule),
visitArgumentList(argList));
}
} else if (resolution instanceof ImplicitThisMethod) {
var isCustomThis = scope.isCustomThisScope();
var needsConst = constLevel == ConstLevel.ALL && constDepth == -1 && !isCustomThis;
return InvokeMethodVirtualNodeGen.create(
sourceSection,
org.pkl.core.runtime.Identifier.get(name),
visitArgumentList(argList),
MemberLookupMode.IMPLICIT_THIS,
needsConst,
VmUtils.createThisNode(VmUtils.unavailableSourceSection(), isCustomThis),
GetClassNodeGen.create(null));
} else {
throw PklBugException.unreachableCode();
}
}
var scope = symbolTable.getCurrentScope();
return new ResolveMethodNode(
createSourceSection(expr),
identifier,
visitArgumentList(argList),
isBaseModule,
scope.isCustomThisScope(),
scope.getConstLevel(),
scope.getConstDepth());
@Override
public ExpressionNode visitUnqualifiedAccessExpr(UnqualifiedAccessExpr expr) {
var argList = expr.getArgumentList();
return argList == null ? resolveReadVariable(expr) : resolvedMethodCall(expr, argList);
}
@Override
@@ -980,12 +1113,14 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
public ExpressionNode visitLetExpr(LetExpr letExpr) {
var sourceSection = createSourceSection(letExpr);
var parameter = letExpr.getParameter();
var frameBuilder = FrameDescriptor.newBuilder();
var frameBuilder = new FrameDescriptorBuilder();
UnresolvedTypeNode[] typeNodes;
var bindings = new ArrayList<String>();
if (parameter instanceof TypedIdentifier par) {
typeNodes = new UnresolvedTypeNode[] {visitTypeAnnotation(par.getTypeAnnotation())};
frameBuilder.addSlot(
FrameSlotKind.Illegal, toIdentifier(par.getIdentifier().getValue()), null);
bindings.add(par.getIdentifier().getValue());
} else {
typeNodes = new UnresolvedTypeNode[0];
}
@@ -994,6 +1129,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
UnresolvedFunctionNode functionNode =
symbolTable.enterLambda(
bindings,
frameBuilder,
scope -> {
var expr = visitExpr(letExpr.getExpr());
@@ -1025,9 +1161,12 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
.build();
}
var bindings = getParameterNames(params);
var isCustomThisScope = symbolTable.getCurrentScope().isCustomThisScope();
return symbolTable.enterLambda(
bindings,
descriptorBuilder,
scope -> {
var exprNode = visitExpr(expr.getExpr());
@@ -1168,7 +1307,9 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
@Override
public GeneratorMemberNode visitMemberPredicate(MemberPredicate ctx) {
var keyNode = symbolTable.enterCustomThisScope(scope -> visitExpr(ctx.getPred()));
var keyNode =
symbolTable.enterEagerGenerator(
(scp) -> symbolTable.enterCustomThisScope(scope -> visitExpr(ctx.getPred())));
var member =
doVisitObjectEntryBody(createSourceSection(ctx), keyNode, ctx.getExpr(), ctx.getBodyList());
var isFrameStored =
@@ -1197,7 +1338,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
@Override
public GeneratorMemberNode visitObjectSpread(ObjectSpread member) {
var expr = visitExpr(member.getExpr());
var expr = symbolTable.enterEagerGenerator((ignored) -> visitExpr(member.getExpr()));
return GeneratorSpreadNodeGen.create(createSourceSection(member), expr, member.isNullable());
}
@@ -1210,8 +1351,10 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
? new GeneratorMemberNode[0]
: doVisitForWhenBody(member.getElseClause());
return new GeneratorWhenNode(
sourceSection, visitExpr(member.getPredicate()), thenNodes, elseNodes);
// when predicates cannot see their direct scope
var predicateNode =
symbolTable.enterEagerGenerator((scope) -> visitExpr(member.getPredicate()));
return new GeneratorWhenNode(sourceSection, predicateNode, thenNodes, elseNodes);
}
private GeneratorMemberNode[] doVisitForWhenBody(ObjectBody body) {
@@ -1233,6 +1376,16 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
TypedIdentifier valueTypedIdentifier = null;
if (valueParameter instanceof TypedIdentifier ti) valueTypedIdentifier = ti;
var params = new ArrayList<String>();
if (ctx.getP1() instanceof TypedIdentifier ti) {
params.add(ti.getIdentifier().getValue());
}
if (ctx.getP2() != null) {
if (ctx.getP2() instanceof TypedIdentifier ti) {
params.add(ti.getIdentifier().getValue());
}
}
var keyIdentifier =
keyTypedIdentifier == null
? null
@@ -1249,16 +1402,13 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
}
var currentScope = symbolTable.getCurrentScope();
var generatorDescriptorBuilder = currentScope.newFrameDescriptorBuilder();
var memberDescriptorBuilder = currentScope.newForGeneratorMemberDescriptorBuilder();
var keySlot = -1;
var valueSlot = -1;
if (keyIdentifier != null) {
keySlot = generatorDescriptorBuilder.addSlot(FrameSlotKind.Illegal, keyIdentifier, null);
memberDescriptorBuilder.addSlot(FrameSlotKind.Illegal, keyIdentifier, null);
}
if (valueIdentifier != null) {
valueSlot = generatorDescriptorBuilder.addSlot(FrameSlotKind.Illegal, valueIdentifier, null);
memberDescriptorBuilder.addSlot(FrameSlotKind.Illegal, valueIdentifier, null);
}
var unresolvedKeyTypeNode =
keyTypedIdentifier == null
@@ -1280,12 +1430,10 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
? new TypeNode.UnknownTypeNode(VmUtils.unavailableSourceSection())
.initWriteSlotNode(valueSlot)
: null;
var iterableNode = visitExpr(ctx.getExpr());
var iterableNode = symbolTable.enterEagerGenerator(scope -> visitExpr(ctx.getExpr()));
var memberNodes =
symbolTable.enterForGenerator(
generatorDescriptorBuilder,
memberDescriptorBuilder,
scope -> doVisitForWhenBody(ctx.getBody()));
params, generatorDescriptorBuilder, scope -> doVisitForWhenBody(ctx.getBody()));
return GeneratorForNodeGen.create(
createSourceSection(ctx),
generatorDescriptorBuilder.build(),
@@ -1300,12 +1448,6 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
@Override
public PklRootNode visitModule(Module mod) {
var moduleDecl = mod.getDecl();
var annotationNodes =
moduleDecl != null
? doVisitAnnotations(moduleDecl.getAnnotations())
: new ExpressionNode[] {};
int modifiers;
if (moduleDecl == null) {
modifiers = VmModifier.NONE;
@@ -1324,6 +1466,23 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
}
}
var scope = (ModuleScope) symbolTable.getCurrentScope();
scope.setModifiers(modifiers);
// visit imports first so that we already have the object member name available
var imports = mod.getImports();
var importMembers = new ObjectMember[imports.size()];
for (var i = 0; i < imports.size(); i++) {
importMembers[i] = visitImportClause(imports.get(i));
}
registerModuleScopeNames(mod, importMembers);
var annotationNodes =
moduleDecl != null
? doVisitAnnotations(moduleDecl.getAnnotations(), null)
: new ExpressionNode[] {};
var extendsOrAmendsClause = moduleDecl != null ? moduleDecl.getExtendsOrAmendsDecl() : null;
var supermoduleNode =
@@ -1344,7 +1503,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
new UnresolvedTypeNode.Declared(supermoduleNode.getSourceSection(), supermoduleNode);
var moduleProperties =
doVisitModuleProperties(
mod.getImports(),
importMembers,
mod.getClasses(),
mod.getTypeAliases(),
List.of(),
@@ -1374,7 +1533,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
var moduleProperties =
doVisitModuleProperties(
mod.getImports(),
importMembers,
mod.getClasses(),
mod.getTypeAliases(),
mod.getProperties(),
@@ -1411,18 +1570,17 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
}
private EconomicMap<Object, ObjectMember> doVisitModuleProperties(
List<ImportClause> imports,
ObjectMember[] imports,
List<Class> classes,
List<TypeAlias> typeAliases,
List<ClassProperty> properties,
Set<String> propertyNames,
ModuleInfo moduleInfo) {
var totalSize = imports.size() + classes.size() + typeAliases.size() + properties.size();
var totalSize = imports.length + classes.size() + typeAliases.size() + properties.size();
var result = EconomicMaps.<Object, ObjectMember>create(totalSize);
for (var _import : imports) {
var member = visitImportClause(_import);
for (var member : imports) {
checkDuplicateMember(member.getName(), member.getHeaderSection(), propertyNames);
EconomicMaps.put(result, member.getName(), member);
}
@@ -1492,10 +1650,8 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
importName,
ConstLevel.NONE,
scope -> {
var modifiers = VmModifier.IMPORT | VmModifier.LOCAL | VmModifier.CONST;
if (imp.isGlob()) {
modifiers = modifiers | VmModifier.GLOB;
}
var baseModifiers = VmModifier.IMPORT | VmModifier.LOCAL | VmModifier.CONST;
var modifiers = imp.isGlob() ? baseModifiers | VmModifier.GLOB : baseModifiers;
var result =
new ObjectMember(
importNode.getSourceSection(),
@@ -1512,31 +1668,52 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
});
}
private void registerClassScopeNames(
SymbolTable.Scope scope, List<ClassProperty> properties, List<ClassMethod> methods) {
for (var prop : properties) {
var local = hasLocalModifier(prop.getModifiers());
scope.addProperty(
org.pkl.core.runtime.Identifier.property(prop.getName().getValue(), local),
local ? VmModifier.LOCAL : VmModifier.NONE);
}
for (var method : methods) {
var local = hasLocalModifier(method.getModifiers());
scope.addMethod(
org.pkl.core.runtime.Identifier.method(method.getName().getValue(), local),
local ? VmModifier.LOCAL : VmModifier.NONE);
}
}
@Override
public ObjectMember visitClass(Class clazz) {
var sourceSection = createSourceSection(clazz);
var headerSection = createSourceSection(clazz.getHeaderSpan());
var bodyNode = clazz.getBody();
var typeParameters = visitTypeParameterList(clazz.getTypeParameterList());
List<ClassProperty> properties = bodyNode != null ? bodyNode.getProperties() : List.of();
List<ClassMethod> methods = bodyNode != null ? bodyNode.getMethods() : List.of();
var modifiers =
doVisitModifiers(
clazz.getModifiers(), VmModifier.VALID_CLASS_MODIFIERS, "invalidClassModifier")
| VmModifier.CLASS;
var isLocalClass = VmModifier.isLocal(modifiers);
var className =
org.pkl.core.runtime.Identifier.property(
clazz.getName().getValue(), VmModifier.isLocal(modifiers));
org.pkl.core.runtime.Identifier.property(clazz.getName().getValue(), isLocalClass);
var annotations = doVisitAnnotations(clazz.getAnnotations(), className);
return symbolTable.enterClass(
className,
modifiers,
typeParameters,
scope -> {
var bodyNode = clazz.getBody();
List<ClassProperty> properties = bodyNode != null ? bodyNode.getProperties() : List.of();
List<ClassMethod> methods = bodyNode != null ? bodyNode.getMethods() : List.of();
registerClassScopeNames(scope, properties, methods);
var supertypeCtx = clazz.getSuperClass();
// needs to be inside `enterClass` so that class' type parameters are in scope
@@ -1572,7 +1749,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
sourceSection,
headerSection,
createDocSourceSection(clazz.getDocComment()),
doVisitAnnotations(clazz.getAnnotations()),
annotations,
modifiers,
classInfo,
typeParameters,
@@ -1582,13 +1759,13 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
doVisitClassProperties(properties, propertyNames),
doVisitMethodDefs(methods));
var isLocal = VmModifier.isLocal(modifiers);
var result =
new ObjectMember(
sourceSection,
headerSection,
isLocal ? VmModifier.LOCAL_CLASS_OBJECT_MEMBER : VmModifier.CLASS_OBJECT_MEMBER,
isLocalClass
? VmModifier.LOCAL_CLASS_OBJECT_MEMBER
: VmModifier.CLASS_OBJECT_MEMBER,
scope.getName(),
scope.getQualifiedName());
@@ -1611,6 +1788,10 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
@Override
public Integer visitModifier(Modifier modifier) {
return toModifier(modifier);
}
private int toModifier(Modifier modifier) {
return switch (modifier.getValue()) {
case EXTERNAL -> VmModifier.EXTERNAL;
case ABSTRACT -> VmModifier.ABSTRACT;
@@ -1658,7 +1839,6 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
var expr = entry.getExpr();
var objectBodies = entry.getBodyList();
var docComment = createDocSourceSection(docCom);
var annotationNodes = doVisitAnnotations(annotations);
var sourceSection = createSourceSection(entry);
var headerStart = !modifierList.isEmpty() ? modifierList.get(0).span() : name.span();
var headerEnd = typeAnnotation != null ? typeAnnotation.span() : name.span();
@@ -1670,6 +1850,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
var isLocal = VmModifier.isLocal(modifiers);
var propertyName = org.pkl.core.runtime.Identifier.property(name.getValue(), isLocal);
var annotationNodes = doVisitAnnotations(annotations, propertyName);
return symbolTable.enterProperty(
propertyName,
@@ -1760,9 +1941,14 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
var descriptorBuilder = createFrameDescriptorBuilder(paramListCtx);
var paramCount = paramListCtx.getParameters().size();
var bindings = getParameterNames(paramListCtx);
var annotations = doVisitAnnotations(entry.getAnnotations(), methodName);
return symbolTable.enterMethod(
methodName,
getConstLevel(modifiers),
bindings,
descriptorBuilder,
typeParameters,
scope -> {
@@ -1806,7 +1992,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
headerSection,
scope.buildFrameDescriptor(),
createDocSourceSection(entry.getDocComment()),
doVisitAnnotations(entry.getAnnotations()),
annotations,
modifiers,
methodName,
scope.getQualifiedName(),
@@ -1835,6 +2021,10 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
var name = org.pkl.core.runtime.Identifier.property(typeAlias.getName().getValue(), isLocal);
var typeParameters = visitTypeParameterList(typeAlias.getTypeParameterList());
var objectMemberModifiers =
isLocal ? VmModifier.LOCAL_TYPEALIAS_OBJECT_MEMBER : VmModifier.TYPEALIAS_OBJECT_MEMBER;
var annotations = doVisitAnnotations(typeAlias.getAnnotations(), name);
return symbolTable.enterTypeAlias(
name,
@@ -1846,7 +2036,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
sourceSection,
headerSection,
createDocSourceSection(typeAlias.getDocComment()),
doVisitAnnotations(typeAlias.getAnnotations()),
annotations,
modifiers,
scopeName.toString(),
scope.getQualifiedName(),
@@ -1857,9 +2047,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
new ObjectMember(
sourceSection,
headerSection,
isLocal
? VmModifier.LOCAL_TYPEALIAS_OBJECT_MEMBER
: VmModifier.TYPEALIAS_OBJECT_MEMBER,
objectMemberModifiers,
scopeName,
scope.getQualifiedName());
@@ -1871,8 +2059,8 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
});
}
@Override
public ExpressionNode visitAnnotation(Annotation annotation) {
public ExpressionNode visitAnnotation(
Annotation annotation, org.pkl.core.runtime.@Nullable Identifier annotatedMemberName) {
var verifyNode = new CheckIsAnnotationClassNode(visitType(annotation.getType()));
var bodyCtx = annotation.getBody();
@@ -1890,13 +2078,16 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
verifyNode);
}
return symbolTable.enterAnnotationScope((scope) -> doVisitObjectBody(bodyCtx, verifyNode));
return symbolTable.enterAnnotationScope(
annotatedMemberName, (scope) -> doVisitObjectBody(bodyCtx, verifyNode));
}
private ExpressionNode[] doVisitAnnotations(List<? extends Annotation> annotations) {
private ExpressionNode[] doVisitAnnotations(
List<? extends Annotation> annotations,
org.pkl.core.runtime.@Nullable Identifier annotatedMemberName) {
var nodes = new ExpressionNode[annotations.size()];
for (var i = 0; i < nodes.length; i++) {
nodes[i] = visitAnnotation(annotations.get(i));
nodes[i] = visitAnnotation(annotations.get(i), annotatedMemberName);
}
return nodes;
}
@@ -2004,9 +2195,33 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
return parentNode;
}
private void addObjectNamesToScope(ObjectScope scope, ObjectBody body) {
for (var member : body.getMembers()) {
if (member instanceof ObjectProperty prop) {
var local = hasLocalModifier(prop.getModifiers());
scope.addProperty(
org.pkl.core.runtime.Identifier.property(prop.getIdentifier().getValue(), local),
local ? VmModifier.LOCAL : VmModifier.NONE);
} else if (member instanceof ObjectMethod method) {
var local = hasLocalModifier(method.getModifiers());
scope.addMethod(
org.pkl.core.runtime.Identifier.method(method.getIdentifier().getValue(), local),
local ? VmModifier.LOCAL : VmModifier.NONE);
} else if (member instanceof WhenGenerator whenGenerator) {
addObjectNamesToScope(scope, whenGenerator.getThenClause());
var elseClause = whenGenerator.getElseClause();
if (elseClause != null) {
addObjectNamesToScope(scope, elseClause);
}
}
}
}
private ExpressionNode doVisitObjectBody(ObjectBody body, ExpressionNode parentNode) {
return symbolTable.enterObjectScope(
body,
(scope) -> {
addObjectNamesToScope(scope, body);
var objectMembers = body.getMembers();
if (objectMembers.isEmpty()) {
return EmptyObjectLiteralNodeGen.create(
@@ -2290,8 +2505,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
}
private Pair<ExpressionNode, ObjectMember> doVisitObjectEntry(ObjectEntry entry) {
var keyNode = visitExpr(entry.getKey());
var keyNode = symbolTable.enterEagerGenerator((scp) -> visitExpr(entry.getKey()));
var member =
doVisitObjectEntryBody(
createSourceSection(entry), keyNode, entry.getValue(), entry.getBodyList());
@@ -2371,9 +2585,12 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
var frameDescriptorBuilder = createFrameDescriptorBuilder(paramList);
var bindings = getParameterNames(paramList);
return symbolTable.enterMethod(
methodName,
getConstLevel(modifiers),
bindings,
frameDescriptorBuilder,
List.of(),
scope -> {
@@ -2590,7 +2807,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
return doVisitParameterTypes(paramList.getParameters());
}
private UnresolvedTypeNode[] doVisitParameterTypes(List<Parameter> params) {
private UnresolvedTypeNode[] doVisitParameterTypes(List<org.pkl.parser.syntax.Parameter> params) {
var typeNodes = new UnresolvedTypeNode[params.size()];
for (int i = 0; i < typeNodes.length; i++) {
if (params.get(i) instanceof TypedIdentifier typedIdentifier) {
@@ -2683,8 +2900,17 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
return needsConst;
}
private FrameDescriptor.Builder createFrameDescriptorBuilder(ParameterList params) {
var builder = FrameDescriptor.newBuilder(params.getParameters().size());
private static List<String> getParameterNames(ParameterList parameterList) {
var result = new ArrayList<String>(parameterList.getParameters().size());
for (var param : parameterList.getParameters()) {
var name = param instanceof TypedIdentifier id ? id.getIdentifier().getValue() : "_";
result.add(name);
}
return result;
}
private FrameDescriptorBuilder createFrameDescriptorBuilder(ParameterList params) {
var builder = new FrameDescriptorBuilder(params.getParameters().size());
for (var param : params.getParameters()) {
org.pkl.core.runtime.Identifier identifier = null;
if (param instanceof TypedIdentifier typedIdentifier) {
@@ -2757,18 +2983,6 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
return org.pkl.core.runtime.Identifier.get(text);
}
private ExpressionNode createResolveVariableNode(
SourceSection section, org.pkl.core.runtime.Identifier propertyName) {
var scope = symbolTable.getCurrentScope();
return new ResolveVariableNode(
section,
propertyName,
isBaseModule,
scope.isCustomThisScope(),
scope.getConstLevel(),
scope.getConstDepth());
}
private URI resolveImport(String importUri, StringConstant ctx) {
URI parsedUri;
try {
@@ -2850,4 +3064,47 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
private static SourceSection unavailableSourceSection() {
return VmUtils.unavailableSourceSection();
}
private void registerModuleScopeNames(Module mod, ObjectMember[] importMembers) {
var scope = symbolTable.getCurrentScope();
for (var imp : importMembers) {
scope.addProperty(imp.getName(), imp.getModifiers());
}
for (var clazz : mod.getClasses()) {
var isLocal = hasLocalModifier(clazz.getModifiers());
scope.addProperty(
org.pkl.core.runtime.Identifier.property(clazz.getName().getValue(), isLocal),
isLocal ? VmModifier.LOCAL : VmModifier.NONE);
}
for (var typealias : mod.getTypeAliases()) {
var isLocal = hasLocalModifier(typealias.getModifiers());
scope.addProperty(
org.pkl.core.runtime.Identifier.property(typealias.getName().getValue(), isLocal),
isLocal ? VmModifier.LOCAL : VmModifier.NONE);
}
for (var prop : mod.getProperties()) {
var isLocal = hasLocalModifier(prop.getModifiers());
scope.addProperty(
org.pkl.core.runtime.Identifier.property(prop.getName().getValue(), isLocal),
isLocal ? VmModifier.LOCAL : VmModifier.NONE);
}
for (var method : mod.getMethods()) {
var isLocal = hasLocalModifier(method.getModifiers());
scope.addMethod(
org.pkl.core.runtime.Identifier.method(method.getName().getValue(), isLocal),
isLocal ? VmModifier.LOCAL : VmModifier.NONE);
}
}
private static boolean hasLocalModifier(List<Modifier> modifiers) {
for (var m : modifiers) {
if (m.getValue() == ModifierValue.LOCAL) return true;
}
return false;
}
}
@@ -0,0 +1,40 @@
/*
* 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.ast.builder;
import org.pkl.core.ast.VmModifier;
public sealed interface MethodResolution {
record ImplicitBaseMethod() implements MethodResolution {}
record LexicalMethod(
boolean isObjectMethod,
boolean isOnClosedClass,
boolean isModuleScope,
int modifiers,
int levelsUp)
implements MethodResolution {
public boolean isLocal() {
return VmModifier.isLocal(modifiers);
}
public boolean isExternal() {
return VmModifier.isExternal(modifiers);
}
}
record ImplicitThisMethod() implements MethodResolution {}
}
@@ -16,26 +16,35 @@
package org.pkl.core.ast.builder;
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.FrameDescriptor.Builder;
import java.util.*;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.pkl.core.TypeParameter;
import org.pkl.core.ast.ConstantNode;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.expression.generator.GeneratorMemberNode;
import org.pkl.core.ast.VmModifier;
import org.pkl.core.ast.builder.MethodResolution.ImplicitBaseMethod;
import org.pkl.core.ast.builder.MethodResolution.ImplicitThisMethod;
import org.pkl.core.ast.builder.MethodResolution.LexicalMethod;
import org.pkl.core.ast.builder.VariableResolution.ImplicitBaseProperty;
import org.pkl.core.ast.builder.VariableResolution.LexicalProperty;
import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.runtime.BaseModuleMembers;
import org.pkl.core.runtime.FrameDescriptorBuilder;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.ModuleInfo;
import org.pkl.core.runtime.VmDataSize;
import org.pkl.core.runtime.VmDuration;
import org.pkl.core.util.LateInit;
import org.pkl.parser.Lexer;
import org.pkl.parser.syntax.ObjectBody;
public final class SymbolTable {
private Scope currentScope;
public SymbolTable(ModuleInfo moduleInfo) {
currentScope = new ModuleScope(moduleInfo);
public SymbolTable(ModuleInfo moduleInfo, boolean isBaseModule) {
currentScope = new ModuleScope(moduleInfo, isBaseModule);
}
public Scope getCurrentScope() {
@@ -53,6 +62,7 @@ public final class SymbolTable {
public ObjectMember enterClass(
Identifier name,
int modifiers,
List<TypeParameter> typeParameters,
Function<ClassScope, ObjectMember> nodeFactory) {
return doEnter(
@@ -60,7 +70,8 @@ public final class SymbolTable {
currentScope,
name,
toQualifiedName(name),
FrameDescriptor.newBuilder(),
modifiers,
new FrameDescriptorBuilder(),
typeParameters),
nodeFactory);
}
@@ -74,7 +85,7 @@ public final class SymbolTable {
currentScope,
name,
toQualifiedName(name),
FrameDescriptor.newBuilder(),
new FrameDescriptorBuilder(),
typeParameters),
nodeFactory);
}
@@ -82,7 +93,8 @@ public final class SymbolTable {
public <T> T enterMethod(
Identifier name,
ConstLevel constLevel,
Builder frameDescriptorBuilder,
List<String> bindings,
FrameDescriptorBuilder frameDescriptorBuilder,
List<TypeParameter> typeParameters,
Function<MethodScope, T> nodeFactory) {
return doEnter(
@@ -91,26 +103,30 @@ public final class SymbolTable {
name,
toQualifiedName(name),
constLevel,
bindings,
frameDescriptorBuilder,
typeParameters),
nodeFactory);
}
public <T> T enterEagerGenerator(Function<EagerGeneratorScope, T> nodeFactory) {
return doEnter(new EagerGeneratorScope(currentScope, currentScope.qualifiedName), nodeFactory);
}
public <T> T enterForGenerator(
FrameDescriptor.Builder frameDescriptorBuilder,
FrameDescriptor.Builder memberDescriptorBuilder,
List<String> params,
FrameDescriptorBuilder frameDescriptorBuilder,
Function<ForGeneratorScope, T> nodeFactory) {
return doEnter(
new ForGeneratorScope(
currentScope,
currentScope.qualifiedName,
frameDescriptorBuilder,
memberDescriptorBuilder),
currentScope, currentScope.qualifiedName, params, frameDescriptorBuilder),
nodeFactory);
}
public <T> T enterLambda(
FrameDescriptor.Builder frameDescriptorBuilder, Function<LambdaScope, T> nodeFactory) {
List<String> bindings,
FrameDescriptorBuilder frameDescriptorBuilder,
Function<LambdaScope, T> nodeFactory) {
// flatten names of lambdas nested inside other lambdas for presentation purposes
var parentScope = currentScope;
@@ -122,14 +138,15 @@ public final class SymbolTable {
var qualifiedName = parentScope.qualifiedName + "." + parentScope.getNextLambdaName();
return doEnter(
new LambdaScope(currentScope, qualifiedName, frameDescriptorBuilder), nodeFactory);
new LambdaScope(currentScope, bindings, qualifiedName, frameDescriptorBuilder),
nodeFactory);
}
public <T> T enterProperty(
Identifier name, ConstLevel constLevel, Function<PropertyScope, T> nodeFactory) {
return doEnter(
new PropertyScope(
currentScope, name, toQualifiedName(name), constLevel, FrameDescriptor.newBuilder()),
currentScope, name, toQualifiedName(name), constLevel, new FrameDescriptorBuilder()),
nodeFactory);
}
@@ -140,8 +157,8 @@ public final class SymbolTable {
var qualifiedName = currentScope.getQualifiedName() + currentScope.getNextEntryName(keyNode);
var builder =
currentScope instanceof ForGeneratorScope forScope
? forScope.memberDescriptorBuilder
: FrameDescriptor.newBuilder();
? forScope.frameDescriptorBuilder
: new FrameDescriptorBuilder();
return doEnter(new EntryScope(currentScope, qualifiedName, builder), nodeFactory);
}
@@ -150,13 +167,20 @@ public final class SymbolTable {
new CustomThisScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory);
}
public <T> T enterAnnotationScope(Function<AnnotationScope, T> nodeFactory) {
public <T> T enterAnnotationScope(
@Nullable Identifier annotatedTargetName, Function<AnnotationScope, T> nodeFactory) {
var qualifiedName =
annotatedTargetName == null
? currentScope.getQualifiedName()
: toQualifiedName(annotatedTargetName);
return doEnter(
new AnnotationScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory);
new AnnotationScope(currentScope, qualifiedName, currentScope.frameDescriptorBuilder),
nodeFactory);
}
public <T> T enterObjectScope(Function<ObjectScope, T> nodeFactory) {
return doEnter(new ObjectScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory);
public <T> T enterObjectScope(ObjectBody body, Function<ObjectScope, T> nodeFactory) {
return doEnter(
new ObjectScope(currentScope, body, currentScope.frameDescriptorBuilder), nodeFactory);
}
private <T, S extends Scope> T doEnter(S scope, Function<S, T> nodeFactory) {
@@ -174,25 +198,35 @@ public final class SymbolTable {
return currentScope.qualifiedName + separator + Lexer.maybeQuoteIdentifier(name.toString());
}
public record Member(Identifier name, int modifiers) {}
public abstract static class Scope {
private final @Nullable Scope parent;
private final @Nullable Identifier name;
private final String qualifiedName;
private int lambdaCount = 0;
private int entryCount = 0;
private final FrameDescriptor.Builder frameDescriptorBuilder;
protected final FrameDescriptorBuilder frameDescriptorBuilder;
private final ConstLevel constLevel;
protected boolean isBaseModule;
// The properties defined on this (lexical) scope
protected final Map<String, Member> properties = new HashMap<>();
// The methods defined on this (lexical) scope
protected final Map<String, Member> methods = new HashMap<>();
private Scope(
@Nullable Scope parent,
@Nullable Identifier name,
String qualifiedName,
ConstLevel constLevel,
FrameDescriptor.Builder frameDescriptorBuilder) {
FrameDescriptorBuilder frameDescriptorBuilder) {
this.parent = parent;
this.name = name;
this.qualifiedName = qualifiedName;
this.frameDescriptorBuilder = frameDescriptorBuilder;
if (parent != null) {
this.isBaseModule = parent.isBaseModule;
}
// const level can never decrease
this.constLevel =
parent != null && parent.constLevel.biggerOrEquals(constLevel)
@@ -225,26 +259,10 @@ public final class SymbolTable {
* Returns a new descriptor builder that contains the same slots as the current scope's frame
* descriptor.
*/
public FrameDescriptor.Builder newFrameDescriptorBuilder() {
public FrameDescriptorBuilder newFrameDescriptorBuilder() {
return new FrameDescriptorBuilder(buildFrameDescriptor());
}
/** Returns a new descriptor builder for a {@link GeneratorMemberNode} in the current scope. */
public FrameDescriptor.Builder newForGeneratorMemberDescriptorBuilder() {
return this instanceof ForGeneratorScope forScope
? newFrameDescriptorBuilder(forScope.buildMemberDescriptor())
: FrameDescriptor.newBuilder();
}
private static FrameDescriptor.Builder newFrameDescriptorBuilder(FrameDescriptor descriptor) {
var builder = FrameDescriptor.newBuilder();
for (var i = 0; i < descriptor.getNumberOfSlots(); i++) {
builder.addSlot(
descriptor.getSlotKind(i), descriptor.getSlotName(i), descriptor.getSlotInfo(i));
}
return builder;
}
public @Nullable TypeParameter getTypeParameter(String name) {
return null;
}
@@ -276,7 +294,10 @@ public final class SymbolTable {
var depth = -1;
var lexicalScope = getLexicalScope();
while (lexicalScope.getConstLevel() == ConstLevel.ALL) {
// LambdaScope inherits constLevel but doesn't create a const scope barrier
if (!(lexicalScope instanceof LambdaScope)) {
depth += 1;
}
var parent = lexicalScope.getParent();
if (parent == null) {
return depth;
@@ -354,18 +375,161 @@ public final class SymbolTable {
public ConstLevel getConstLevel() {
return constLevel;
}
public void addProperty(Identifier name, int modifiers) {
var prevProperty = this.properties.put(name.toString(), new Member(name, modifiers));
if (prevProperty != null
&& !VmModifier.hasSameModifier(prevProperty.modifiers, modifiers, VmModifier.LOCAL)) {
// this can happen in when generators:
//
// ```
// when (cond) {
// local prop = 1
// } else {
// prop = 2
// }
// ```
//
// this can't happen with methods; object methods can only be `local`.
this.properties.put(
name.toString(), new Member(name, modifiers | VmModifier.AMBIGUOUS_LOCALITY));
}
}
private interface LexicalScope {}
public void addMethod(Identifier name, int modifiers) {
this.methods.put(name.toString(), new Member(name, modifiers));
}
public final VariableResolution resolveVariable(String name) {
var resolved = resolveLexical((scope, levelUp) -> scope.doResolveProperty(name, levelUp));
if (resolved != null) {
return resolved;
}
if (!isBaseModule) {
if (BaseModuleMembers.hasProperty(name)) {
return new ImplicitBaseProperty();
}
}
return new VariableResolution.ImplicitThisProperty();
}
public final MethodResolution resolveMethod(String name) {
var resolved = resolveLexical((scope, levelUp) -> scope.doResolveMethod(name, levelUp));
if (resolved != null) {
return resolved;
}
if (!isBaseModule) {
if (BaseModuleMembers.hasMethod(name)) {
return new ImplicitBaseMethod();
}
}
return new ImplicitThisMethod();
}
@FunctionalInterface
private interface ResolutionFunction<T> {
@Nullable T apply(LexicalScope scope, int levelUp);
}
private @Nullable <R> R resolveLexical(ResolutionFunction<R> fun) {
var levelsUp = 0;
var shouldSkip = false;
for (var scope = this; scope != null; scope = scope.getParent()) {
// for headers resolve variables one scope up
if (scope instanceof EagerGeneratorScope) {
shouldSkip = true;
continue;
}
// annotations on class members don't level up
if (scope instanceof AnnotationScope && scope.getParent() instanceof ClassScope) {
levelsUp--;
}
if (scope instanceof LexicalScope lex) {
if (shouldSkip && !(scope instanceof ForGeneratorScope)) {
if (scope instanceof ObjectScope objectScope && objectScope.hasParams()) {
levelsUp++;
}
shouldSkip = false;
continue;
}
var result = fun.apply(lex, levelsUp);
if (result != null) return result;
if (scope instanceof MethodScope || scope instanceof ForGeneratorScope) {
// fors and methods don't level up
continue;
}
levelsUp++;
if (scope instanceof ObjectScope objectScope && objectScope.hasParams()) {
levelsUp++;
}
}
}
return null;
}
}
public interface LexicalScope {
@Nullable VariableResolution doResolveProperty(String name, int levelsUp);
@Nullable MethodResolution doResolveMethod(String name, int levelsUp);
}
public static class ObjectScope extends Scope implements LexicalScope {
private ObjectScope(Scope parent, Builder frameDescriptorBuilder) {
private final Map<String, Integer> params;
private ObjectScope(
Scope parent, ObjectBody body, FrameDescriptorBuilder frameDescriptorBuilder) {
super(
parent,
parent.getNameOrNull(),
parent.getQualifiedName(),
ConstLevel.NONE,
frameDescriptorBuilder);
params = collectParams(body);
}
public boolean hasParams() {
return !params.isEmpty();
}
@Override
public @Nullable VariableResolution doResolveProperty(String name, int levelsUp) {
// Underscore is a discard identifier and should not be resolvable
if (name.equals("_")) {
return null;
}
var prop = properties.get(name);
if (prop != null) {
return new VariableResolution.LexicalProperty(false, prop.modifiers, levelsUp);
}
var paramIndex = params.get(name);
if (paramIndex != null) {
// params are on a higher level than the properties
return new VariableResolution.Parameter(paramIndex, levelsUp + 1);
}
return null;
}
@Override
public @Nullable MethodResolution doResolveMethod(String name, int levelsUp) {
var method = methods.get(name);
if (method != null) {
return new LexicalMethod(true, false, false, method.modifiers, levelsUp);
}
return null;
}
private static Map<String, Integer> collectParams(ObjectBody body) {
var params = new HashMap<String, Integer>();
for (var i = 0; i < body.getParameters().size(); i++) {
var param = body.getParameters().get(i);
if (param instanceof org.pkl.parser.syntax.Parameter.TypedIdentifier ti) {
params.put(ti.getIdentifier().getValue(), i);
} else {
params.put("_", i);
}
}
return params;
}
}
@@ -377,7 +541,7 @@ public final class SymbolTable {
Identifier name,
String qualifiedName,
ConstLevel constLevel,
Builder frameDescriptorBuilder,
FrameDescriptorBuilder frameDescriptorBuilder,
List<TypeParameter> typeParameters) {
super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder);
this.typeParameters = typeParameters;
@@ -393,47 +557,112 @@ public final class SymbolTable {
}
public static final class ModuleScope extends Scope implements LexicalScope {
private final ModuleInfo moduleInfo;
@LateInit private boolean isClosed;
private final boolean isAmend;
public ModuleScope(ModuleInfo moduleInfo) {
super(null, null, moduleInfo.getModuleName(), ConstLevel.NONE, FrameDescriptor.newBuilder());
public ModuleScope(ModuleInfo moduleInfo, boolean isBaseModule) {
super(null, null, moduleInfo.getModuleName(), ConstLevel.NONE, new FrameDescriptorBuilder());
this.isBaseModule = isBaseModule;
this.moduleInfo = moduleInfo;
this.isAmend = moduleInfo.isAmend();
}
public void setModifiers(int modifiers) {
this.isClosed = VmModifier.isClosed(modifiers);
}
@Override
public @Nullable VariableResolution doResolveProperty(String name, int levelsUp) {
var member = properties.get(name);
if (member != null) {
return new LexicalProperty(true, member.modifiers, levelsUp);
}
return null;
}
@Override
public @Nullable MethodResolution doResolveMethod(String name, int levelsUp) {
var method = methods.get(name);
if (method == null) return null;
var isObjectMethod = isAmend && VmModifier.isLocal(method.modifiers);
return new LexicalMethod(isObjectMethod, isClosed, true, method.modifiers, levelsUp);
}
}
public static final class MethodScope extends TypeParameterizableScope {
public static final class MethodScope extends TypeParameterizableScope implements LexicalScope {
private final List<String> bindings;
public MethodScope(
Scope parent,
Identifier name,
String qualifiedName,
ConstLevel constLevel,
Builder frameDescriptorBuilder,
List<String> bindings,
FrameDescriptorBuilder frameDescriptorBuilder,
List<TypeParameter> typeParameters) {
super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder, typeParameters);
this.bindings = bindings;
}
@Override
public @Nullable VariableResolution doResolveProperty(String name, int levelsUp) {
return resolveParameter(name, bindings, levelsUp);
}
@Override
public @Nullable MethodResolution doResolveMethod(String name, int levelsUp) {
return null;
}
}
public static final class LambdaScope extends Scope implements LexicalScope {
private final List<String> bindings;
public LambdaScope(
Scope parent, String qualifiedName, FrameDescriptor.Builder frameDescriptorBuilder) {
super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder);
Scope parent,
List<String> bindings,
String qualifiedName,
FrameDescriptorBuilder frameDescriptorBuilder) {
super(parent, null, qualifiedName, parent.getConstLevel(), frameDescriptorBuilder);
this.bindings = bindings;
}
@Override
public @Nullable VariableResolution doResolveProperty(String name, int levelsUp) {
return resolveParameter(name, bindings, levelsUp);
}
@Override
public @Nullable MethodResolution doResolveMethod(String name, int levelsUp) {
return null;
}
}
// A generator scope that is resolved eagerly and one level above
public static final class EagerGeneratorScope extends Scope {
private static FrameDescriptorBuilder getFrameDescriptorBuilder(Scope parent) {
var grandParent = parent.parent;
assert grandParent != null;
return grandParent.frameDescriptorBuilder;
}
private EagerGeneratorScope(Scope parent, String qualifiedName) {
super(parent, null, qualifiedName, ConstLevel.NONE, getFrameDescriptorBuilder(parent));
}
}
public static final class ForGeneratorScope extends Scope implements LexicalScope {
private final FrameDescriptor.Builder memberDescriptorBuilder;
final List<String> params;
public ForGeneratorScope(
Scope parent,
String qualifiedName,
FrameDescriptor.Builder frameDescriptorBuilder,
FrameDescriptor.Builder memberDescriptorBuilder) {
List<String> params,
FrameDescriptorBuilder frameDescriptorBuilder) {
super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder);
this.memberDescriptorBuilder = memberDescriptorBuilder;
}
public FrameDescriptor buildMemberDescriptor() {
return memberDescriptorBuilder.build();
this.params = params;
}
@Override
@@ -442,6 +671,23 @@ public final class SymbolTable {
assert parent != null;
return parent.getNextEntryName(keyNode);
}
@Override
public @Nullable VariableResolution doResolveProperty(String name, int levelsUp) {
if (!params.contains(name)) {
return null;
}
var index = frameDescriptorBuilder.findSlot(Identifier.get(name));
if (index >= 0) {
return new VariableResolution.ForGeneratorVariable(index, levelsUp);
}
return null;
}
@Override
public @Nullable MethodResolution doResolveMethod(String name, int levelsUp) {
return null;
}
}
public static final class PropertyScope extends Scope {
@@ -450,26 +696,46 @@ public final class SymbolTable {
Identifier name,
String qualifiedName,
ConstLevel constLevel,
FrameDescriptor.Builder frameDescriptorBuilder) {
FrameDescriptorBuilder frameDescriptorBuilder) {
super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder);
}
}
public static final class EntryScope extends Scope {
public EntryScope(
Scope parent, String qualifiedName, FrameDescriptor.Builder frameDescriptorBuilder) {
Scope parent, String qualifiedName, FrameDescriptorBuilder frameDescriptorBuilder) {
super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder);
}
}
public static final class ClassScope extends TypeParameterizableScope implements LexicalScope {
private final boolean isClosed;
public ClassScope(
Scope parent,
Identifier name,
String qualifiedName,
Builder frameDescriptorBuilder,
int modifiers,
FrameDescriptorBuilder frameDescriptorBuilder,
List<TypeParameter> typeParameters) {
super(parent, name, qualifiedName, ConstLevel.MODULE, frameDescriptorBuilder, typeParameters);
isClosed = VmModifier.isClosed(modifiers);
}
@Override
public @Nullable VariableResolution doResolveProperty(String name, int levelsUp) {
var member = properties.get(name);
if (member == null) return null;
return new LexicalProperty(false, member.modifiers, levelsUp);
}
@Override
public @Nullable MethodResolution doResolveMethod(String name, int levelsUp) {
var member = methods.get(name);
if (member == null) return null;
return new LexicalMethod(false, isClosed, false, member.modifiers, levelsUp);
}
}
@@ -478,7 +744,7 @@ public final class SymbolTable {
Scope parent,
Identifier name,
String qualifiedName,
FrameDescriptor.Builder frameDescriptorBuilder,
FrameDescriptorBuilder frameDescriptorBuilder,
List<TypeParameter> typeParameters) {
super(parent, name, qualifiedName, ConstLevel.MODULE, frameDescriptorBuilder, typeParameters);
}
@@ -499,7 +765,7 @@ public final class SymbolTable {
}
};
public CustomThisScope(Scope parent, FrameDescriptor.Builder frameDescriptorBuilder) {
public CustomThisScope(Scope parent, FrameDescriptorBuilder frameDescriptorBuilder) {
super(
parent,
parent.getNameOrNull(),
@@ -509,14 +775,23 @@ public final class SymbolTable {
}
}
public static final class AnnotationScope extends Scope implements LexicalScope {
public AnnotationScope(Scope parent, FrameDescriptor.Builder frameDescriptorBuilder) {
public static final class AnnotationScope extends Scope {
public AnnotationScope(
Scope parent, String qualifiedName, FrameDescriptorBuilder frameDescriptorBuilder) {
super(
parent,
parent.getNameOrNull(),
parent.getQualifiedName(),
ConstLevel.MODULE,
frameDescriptorBuilder);
parent, parent.getNameOrNull(), qualifiedName, ConstLevel.MODULE, frameDescriptorBuilder);
}
}
private static @Nullable VariableResolution resolveParameter(
String name, List<String> bindings, int levelsUp) {
if (name.equals("_")) {
return null;
}
var index = bindings.indexOf(name);
if (index != -1) {
return new VariableResolution.Parameter(index, levelsUp);
}
return null;
}
}
@@ -0,0 +1,43 @@
/*
* 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.ast.builder;
import org.pkl.core.ast.VmModifier;
public sealed interface VariableResolution {
record LexicalProperty(boolean isModuleScope, int modifiers, int levelsUp)
implements VariableResolution {
public boolean isLocal() {
return VmModifier.isLocal(modifiers);
}
public boolean isAmbiguousLocality() {
return VmModifier.isAmbiguousLocality(modifiers);
}
}
// let, lambda, object body param
record Parameter(int slot, int levelsUp) implements VariableResolution {}
record ForGeneratorVariable(int slot, int levelsUp) implements VariableResolution {}
// Implicit base module lookup
record ImplicitBaseProperty() implements VariableResolution {}
record ImplicitThisProperty() implements VariableResolution {}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,14 +33,12 @@ public final class RestoreForBindingsNode extends ExpressionNode {
@Override
public Object executeGeneric(VirtualFrame frame) {
var generatorFrame = ObjectData.getGeneratorFrame(frame);
var numSlots = frame.getFrameDescriptor().getNumberOfSlots();
// This value is constant and could be a constructor argument.
var startSlot = generatorFrame.getFrameDescriptor().getNumberOfSlots() - numSlots;
assert startSlot >= 0;
// Copy locals that are for-generator variables into this frame.
// Slots before `startSlot` (if any) are function arguments
// and must not be copied to preserve scoping rules.
VmUtils.copyLocals(generatorFrame, startSlot, frame, 0, numSlots);
// copying all slots includes function arguments, but the capture generator frame
// and the host frame are guaranteed to have the same arguments and number of slots
// (guaranteed by AstBuilder).
assert frame.getFrameDescriptor().getNumberOfSlots()
== generatorFrame.getFrameDescriptor().getNumberOfSlots();
VmUtils.copyLocals(generatorFrame, 0, frame, 0, frame.getFrameDescriptor().getNumberOfSlots());
return child.executeGeneric(frame);
}
}
@@ -27,7 +27,7 @@ import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.PklNode;
import org.pkl.core.ast.PklRootNode;
import org.pkl.core.ast.SimpleRootNode;
import org.pkl.core.ast.frame.ReadFrameSlotNodeGen;
import org.pkl.core.ast.frame.ReadExactFrameSlotNodeGen;
import org.pkl.core.ast.member.FunctionNode;
import org.pkl.core.ast.member.Lambda;
import org.pkl.core.ast.type.TypeNode;
@@ -37,11 +37,13 @@ public final class AmendFunctionNode extends PklNode {
private final boolean isCustomThisScope;
private final PklRootNode initialFunctionRootNode;
@CompilationFinal private int customThisSlot = -1;
private final boolean hasObjectParams;
public AmendFunctionNode(ObjectLiteralNode hostNode, TypeNode[] parameterTypeNodes) {
super(hostNode.getSourceSection());
isCustomThisScope = hostNode.isCustomThisScope;
hasObjectParams = parameterTypeNodes.length > 0;
var builder = FrameDescriptor.newBuilder();
var hostDescriptor = hostNode.parametersDescriptor;
@@ -68,7 +70,7 @@ public final class AmendFunctionNode extends PklNode {
new AmendFunctionBodyNode(
sourceSection,
hostNode.copy(
ReadFrameSlotNodeGen.create(
ReadExactFrameSlotNodeGen.create(
hostNode.getParentNode().getSourceSection(), objectToAmendSlot)),
parameterSlots,
objectToAmendSlot,
@@ -88,7 +90,7 @@ public final class AmendFunctionNode extends PklNode {
new AmendFunctionBodyNode(
sourceSection,
hostNode.copy(
ReadFrameSlotNodeGen.create(
ReadExactFrameSlotNodeGen.create(
hostNode.getParentNode().getSourceSection(), objectToAmendSlot)),
parameterSlots,
objectToAmendSlot,
@@ -108,7 +110,9 @@ public final class AmendFunctionNode extends PklNode {
isCustomThisScope ? frame.getAuxiliarySlot(customThisSlot) : VmUtils.getReceiver(frame),
functionToAmend.getParameterCount(),
initialFunctionRootNode,
new Context(functionToAmend, null));
new Context(functionToAmend, null),
true,
hasObjectParams);
}
private static class AmendFunctionBodyNode extends ExpressionNode {
@@ -0,0 +1,95 @@
/*
* 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.ast.expression.member;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.DirectCallNode;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmObjectLike;
import org.pkl.core.runtime.VmUtils;
public abstract sealed class AbstractInvokeMethodLexicalNode extends ExpressionNode
permits InvokeObjectMethodNode, InvokeClassMethodNode {
protected final Identifier methodName;
protected final int levelsUp;
private final boolean needsConst;
@Children private ExpressionNode[] argumentNodes;
@Child private DirectCallNode callNode;
@CompilationFinal protected boolean isConstChecked;
protected AbstractInvokeMethodLexicalNode(
SourceSection sourceSection,
Identifier methodName,
int levelsUp,
ExpressionNode[] argumentNodes,
boolean needsConst) {
super(sourceSection);
this.methodName = methodName;
this.levelsUp = levelsUp;
this.argumentNodes = argumentNodes;
this.needsConst = needsConst;
this.isConstChecked = false;
}
@Override
@ExplodeLoop
public final Object executeGeneric(VirtualFrame frame) {
var args = new Object[2 + argumentNodes.length];
var capturedFrame = VmUtils.getFrame(frame, levelsUp);
var owner = VmUtils.getOwner(capturedFrame);
var receiver = VmUtils.getReceiver(capturedFrame);
checkConst(owner);
args[0] = receiver;
args[1] = owner;
for (var i = 0; i < argumentNodes.length; i++) {
args[2 + i] = argumentNodes[i].executeGeneric(frame);
}
return getCallNode(owner).call(args);
}
private void checkConst(VmObjectLike owner) {
if (!needsConst || isConstChecked) {
return;
}
CompilerDirectives.transferToInterpreterAndInvalidate();
doCheckConst(owner);
isConstChecked = true;
}
protected abstract CallTarget getCallTarget(VmObjectLike owner);
protected abstract void doCheckConst(VmObjectLike owner);
protected final VmObjectLike getOwner(VirtualFrame frame) {
return VmUtils.getOwner(frame, levelsUp);
}
protected DirectCallNode getCallNode(VmObjectLike owner) {
if (callNode == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
callNode = DirectCallNode.create(getCallTarget(owner));
insert(callNode);
}
return callNode;
}
}
@@ -0,0 +1,55 @@
/*
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.ast.expression.member;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmObjectLike;
/**
* A non-virtual call of closed methods (methods whose enclosing class/module is not open nor
* abstract, and is lexically scoped).
*
* <p>For local methods, use {@link InvokeObjectMethodNode}.
*/
public final class InvokeClassMethodNode extends AbstractInvokeMethodLexicalNode {
public InvokeClassMethodNode(
SourceSection sourceSection,
Identifier methodName,
int levelsUp,
ExpressionNode[] argumentNodes,
boolean needsConst) {
super(sourceSection, methodName, levelsUp, argumentNodes, needsConst);
}
@Override
protected void doCheckConst(VmObjectLike owner) {
var method = owner.getVmClass().getDeclaredMethod(methodName);
assert method != null;
if (!method.isConst()) {
throw exceptionBuilder().evalError("methodMustBeConst", methodName).build();
}
}
@Override
protected CallTarget getCallTarget(VmObjectLike owner) {
var method = owner.getVmClass().getDeclaredMethod(methodName);
assert method != null;
return method.getCallTarget(getSourceSection());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,7 +23,7 @@ import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.member.ClassMethod;
import org.pkl.core.runtime.VmObjectLike;
/** A non-virtual ("direct") method call. */
/** A non-virtual ("direct") method call. Used only for methods on {@code pkl:base}. */
public final class InvokeMethodDirectNode extends ExpressionNode {
private final VmObjectLike owner;
@Child private ExpressionNode receiverNode;
@@ -1,75 +0,0 @@
/*
* Copyright © 2024-2025 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.ast.expression.member;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.frame.Frame;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.DirectCallNode;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.VmUtils;
/**
* A non-virtual method call whose call target is in the lexical scope of the call site. Mainly used
* for calling `local` methods.
*/
public final class InvokeMethodLexicalNode extends ExpressionNode {
@Children private final ExpressionNode[] argumentNodes;
private final int levelsUp;
@Child private DirectCallNode callNode;
InvokeMethodLexicalNode(
SourceSection sourceSection,
CallTarget callTarget,
int levelsUp,
ExpressionNode[] argumentNodes) {
super(sourceSection);
this.levelsUp = levelsUp;
this.argumentNodes = argumentNodes;
callNode = DirectCallNode.create(callTarget);
}
@Override
@ExplodeLoop
public Object executeGeneric(VirtualFrame frame) {
var args = new Object[2 + argumentNodes.length];
var enclosingFrame = getEnclosingFrame(frame);
args[0] = VmUtils.getReceiver(enclosingFrame);
args[1] = VmUtils.getOwner(enclosingFrame);
for (var i = 0; i < argumentNodes.length; i++) {
args[2 + i] = argumentNodes[i].executeGeneric(frame);
}
return callNode.call(args);
}
@ExplodeLoop
private Frame getEnclosingFrame(VirtualFrame frame) {
if (levelsUp == 0) return frame;
var owner = VmUtils.getOwner(frame);
for (var i = 1; i < levelsUp; i++) {
owner = owner.getEnclosingOwner();
assert owner != null;
}
return owner.getEnclosingFrame();
}
}
@@ -0,0 +1,50 @@
/*
* 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.ast.expression.member;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.VmModifier;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmObjectLike;
/** A non-virtual call of a local method. */
public final class InvokeObjectMethodNode extends AbstractInvokeMethodLexicalNode {
public InvokeObjectMethodNode(
SourceSection sourceSection,
Identifier methodName,
int levelsUp,
ExpressionNode[] argumentNodes,
boolean needsConst) {
super(sourceSection, methodName, levelsUp, argumentNodes, needsConst);
}
protected void doCheckConst(VmObjectLike owner) {
var member = owner.getMember(methodName);
assert member != null;
if (!VmModifier.isConst(member.getModifiers())) {
throw exceptionBuilder().evalError("methodMustBeConst", methodName).build();
}
}
@Override
protected CallTarget getCallTarget(VmObjectLike owner) {
var method = owner.getMember(methodName);
assert method != null && method.isLocal();
return (CallTarget) method.getCallTarget().call(owner, owner);
}
}
@@ -0,0 +1,106 @@
/*
* 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.ast.expression.member;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.MemberLookupMode;
import org.pkl.core.ast.expression.primary.GetEnclosingReceiverNode;
import org.pkl.core.ast.expression.primary.GetReceiverNode;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmUtils;
/**
* Reads either a local, or a non-local property.
*
* <p>Sometimes, it is not possible to determine whether a property is local or not at parse time.
* This happens for members declared inside generator bodies. For example:
*
* <pre>{@code
* foo {
* when (cond) {
* local res = 1
* } else {
* res = 2
* }
* // Depending on how the when generator is executed, `res` is either a local property,
* // or a non-local
* bar = res
* }
* }</pre>
*/
public final class ReadAmbiguousLocalityPropertyNode extends ExpressionNode {
private final Identifier name;
private final int levelsUp;
private final boolean needsConst;
private @Child ExpressionNode readLocalPropertyNode;
private @Child ExpressionNode readPropertyNode;
public ReadAmbiguousLocalityPropertyNode(
SourceSection sourceSection, Identifier name, int levelsUp, boolean needsConst) {
super(sourceSection);
this.name = name;
this.levelsUp = levelsUp;
this.needsConst = needsConst;
}
private ExpressionNode getReadLocalPropertyNode() {
if (readLocalPropertyNode == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
readLocalPropertyNode =
insert(
new ReadLocalPropertyNode(
sourceSection, name.toLocalProperty(), levelsUp, needsConst));
}
return readLocalPropertyNode;
}
private ExpressionNode getReadPropertyNode() {
if (readPropertyNode == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
readPropertyNode =
insert(
ReadPropertyNodeGen.create(
sourceSection,
name,
MemberLookupMode.IMPLICIT_LEXICAL,
needsConst,
levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp)));
}
return readPropertyNode;
}
private ExpressionNode getUnderlying(VirtualFrame frame) {
var receiver = VmUtils.getObjectReceiver(frame, levelsUp);
return receiver.hasMember(name) ? getReadPropertyNode() : getReadLocalPropertyNode();
}
// NOTE: `replace()` is actually incorrect (we can't know that the next eval of this node will
// continue resolving to the same node).
// However, Pkl <= 0.31 works this way (ResolveVariableNode always calls `replace()`).
// Also, it's likely that a future version of Pkl will _not_ have generator members be visible
// to the enclosing object body.
// If that scoping rule is implemented, `ReadAmbiguousLocalityPropertyNode` will go away entirely.
// See pkl-core/src/test/files/LanguageSnippetTests/input/basic/localProperty5.pkl
@Override
public Object executeGeneric(VirtualFrame frame) {
return replace(getUnderlying(frame)).executeGeneric(frame);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,64 +16,81 @@
package org.pkl.core.ast.expression.member;
import com.oracle.truffle.api.CompilerAsserts;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.DirectCallNode;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.PklBugException;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmObjectLike;
import org.pkl.core.runtime.VmUtils;
/** Reads a local non-constant property that is known to exist in the lexical scope of this node. */
public final class ReadLocalPropertyNode extends ExpressionNode {
private final ObjectMember property;
private final Identifier name;
private final int levelsUp;
private final boolean needsConst;
@Child private DirectCallNode callNode;
@CompilationFinal private ObjectMember property;
public ReadLocalPropertyNode(SourceSection sourceSection, ObjectMember property, int levelsUp) {
public ReadLocalPropertyNode(
SourceSection sourceSection, Identifier name, int levelsUp, boolean needsConst) {
super(sourceSection);
CompilerAsserts.neverPartOfCompilation();
this.property = property;
this.name = name;
this.levelsUp = levelsUp;
assert property.getNameOrNull() != null;
assert property.getConstantValue() == null : "Use a ConstantNode instead.";
callNode = DirectCallNode.create(property.getCallTarget());
this.needsConst = needsConst;
}
@Override
@ExplodeLoop
public Object executeGeneric(VirtualFrame frame) {
var owner = VmUtils.getOwner(frame);
Object receiver;
if (levelsUp == 0) {
receiver = VmUtils.getReceiver(frame);
} else {
for (int i = 1; i < levelsUp; i++) {
owner = owner.getEnclosingOwner();
assert owner != null;
var owner = VmUtils.getOwner(frame, levelsUp);
var property = getProperty(owner);
var constantValue = property.getConstantValue();
if (constantValue != null) {
return constantValue;
}
receiver = owner.getEnclosingReceiver();
owner = owner.getEnclosingOwner();
}
assert receiver instanceof VmObjectLike
: "Assumption: This node isn't used in Truffle ASTs of `external` pkl.base classes whose values aren't VmObject's.";
var objReceiver = (VmObjectLike) receiver;
var result = objReceiver.getCachedValue(property);
var receiver = (VmObjectLike) VmUtils.getReceiver(frame, levelsUp);
var result = receiver.getCachedValue(property);
if (result == null) {
result = callNode.call(objReceiver, owner, property.getName());
objReceiver.setCachedValue(property, result);
result = getCallNode(property).call(receiver, owner, property.getName());
receiver.setCachedValue(property, result);
}
return result;
}
private ObjectMember getProperty(VmObjectLike owner) {
if (property == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
property = owner.getMember(name);
if (property == null) {
// should never happen
CompilerDirectives.transferToInterpreter();
throw new PklBugException("Couldn't find local variable `" + name + "`.");
}
if (needsConst && !property.isConst()) {
throw exceptionBuilder().evalError("propertyMustBeConst", name.toString()).build();
}
}
return property;
}
public DirectCallNode getCallNode(ObjectMember property) {
if (callNode == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
callNode = DirectCallNode.create(property.getCallTarget());
insert(callNode);
}
return callNode;
}
}
@@ -118,20 +118,27 @@ public abstract class ReadPropertyNode extends ExpressionNode {
.build();
}
// only ever need to check once per node because `needsConst` is only true in the case of implicit
// receivers inside class (and module) bodies, and the const-ness of a resolved property cannot be
// changed by subclasses.
// Only ever need to check once per node because `needsConst` is only true in the case of:
//
// * implicit receiver in class (and module) bodies
// * `local const` object members
//
// and the const-ness of a resolved property cannot be changed by subclasses / amending objects.
private void checkConst(VmObjectLike receiver) {
if (needsConst && !isConstChecked) {
CompilerDirectives.transferToInterpreterAndInvalidate();
var property = receiver.getVmClass().getProperty(propertyName);
if (property == null) {
if (property != null && !property.isConst()) {
throw exceptionBuilder().evalError("propertyMustBeConst", propertyName.toString()).build();
}
var objectMember = receiver.getMember(propertyName);
if (objectMember != null && !objectMember.isConst()) {
throw exceptionBuilder().evalError("propertyMustBeConst", propertyName.toString()).build();
}
if (property == null && objectMember == null) {
// fall through; `cannotFindProperty` gets thrown when we attempt to read the property.
return;
}
if (!property.isConst()) {
throw exceptionBuilder().evalError("propertyMustBeConst", propertyName.toString()).build();
}
isConstChecked = true;
}
}
@@ -1,181 +0,0 @@
/*
* Copyright © 2024-2025 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.ast.expression.member;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.NodeInfo;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ConstantValueNode;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.MemberLookupMode;
import org.pkl.core.ast.builder.ConstLevel;
import org.pkl.core.ast.expression.primary.*;
import org.pkl.core.ast.internal.GetClassNodeGen;
import org.pkl.core.ast.member.Member;
import org.pkl.core.runtime.*;
/**
* Resolves a method name in a method call with implicit receiver, for example `bar` in `x = bar()`
* (but not `foo.bar()`).
*
* <p>A method name can refer to any of the following: - a (potentially `local`) method in the
* lexical scope - a base module method - a method accessible through `this`
*
* <p>This node's task is to make a one-time decision between these alternatives for the call site
* it represents.
*/
// TODO: Consider doing this at parse time (cf. ResolveVariableNode).
@NodeInfo(shortName = "resolveMethod")
public final class ResolveMethodNode extends ExpressionNode {
private final Identifier methodName;
private final ExpressionNode[] argumentNodes;
// Tells if the call site is inside the base module.
private final boolean isBaseModule;
// Tells if the call site is inside a [CustomThisScope].
private final boolean isCustomThisScope;
private final ConstLevel constLevel;
private final int constDepth;
public ResolveMethodNode(
SourceSection sourceSection,
Identifier methodName,
ExpressionNode[] argumentNodes,
boolean isBaseModule,
boolean isCustomThisScope,
ConstLevel constLevel,
int constDepth) {
super(sourceSection);
this.methodName = methodName;
this.argumentNodes = argumentNodes;
this.isBaseModule = isBaseModule;
this.isCustomThisScope = isCustomThisScope;
this.constLevel = constLevel;
this.constDepth = constDepth;
}
@Override
public Object executeGeneric(VirtualFrame frame) {
return replace(doResolve(VmUtils.getOwner(frame))).executeGeneric(frame);
}
@TruffleBoundary
private ExpressionNode doResolve(VmObjectLike initialOwner) {
var levelsUp = 0;
Identifier localMethodName = methodName.toLocalMethod();
// Search lexical scope.
for (var currOwner = initialOwner;
currOwner != null;
currOwner = currOwner.getEnclosingOwner()) {
if (currOwner.isPrototype()) {
var localMethod = currOwner.getVmClass().getDeclaredMethod(localMethodName);
if (localMethod != null) {
assert localMethod.isLocal();
checkConst(currOwner, localMethod, levelsUp);
return new InvokeMethodLexicalNode(
sourceSection, localMethod.getCallTarget(sourceSection), levelsUp, argumentNodes);
}
var method = currOwner.getVmClass().getDeclaredMethod(methodName);
if (method != null) {
assert !method.isLocal();
checkConst(currOwner, method, levelsUp);
if (method.getDeclaringClass().isClosed()) {
return new InvokeMethodLexicalNode(
sourceSection, method.getCallTarget(sourceSection), levelsUp, argumentNodes);
}
//noinspection ConstantConditions
return InvokeMethodVirtualNodeGen.create(
sourceSection,
methodName,
argumentNodes,
MemberLookupMode.IMPLICIT_LEXICAL,
levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp),
GetClassNodeGen.create(null));
}
} else {
var localMethod = currOwner.getMember(localMethodName);
if (localMethod != null) {
assert localMethod.isLocal();
checkConst(currOwner, localMethod, levelsUp);
var methodCallTarget =
// TODO: is it OK to pass owner as receiver here?
// (calls LocalMethodNode, which only resolves types)
(CallTarget) localMethod.getCallTarget().call(currOwner, currOwner);
return new InvokeMethodLexicalNode(
sourceSection, methodCallTarget, levelsUp, argumentNodes);
}
}
levelsUp += 1;
}
// Search base module (unless call site is itself inside base module).
if (!isBaseModule) {
var baseModule = BaseModule.getModule();
// use `getDeclaredMethod()` so as not to resolve to anything declared in class
// pkl.base#Module
var method = baseModule.getVmClass().getDeclaredMethod(methodName);
if (method != null) {
assert !method.isLocal();
return new InvokeMethodDirectNode(
sourceSection, method, new ConstantValueNode(baseModule), argumentNodes);
}
}
// Assuming this method exists at all, it must be a method accessible through `this`.
//
// Calling a method off implicit `this` needs a const check if the node is not in a const scope
// (see ResolveVariableNode for an explanation)
//
// Edge case: always allow method calls for custom `this` scopes (member predicates, type
// constraints)
// because they do not refer to a lexical `this`.
boolean needsConst = constLevel == ConstLevel.ALL && constDepth == -1 && !isCustomThisScope;
//noinspection ConstantConditions
return InvokeMethodVirtualNodeGen.create(
sourceSection,
methodName,
argumentNodes,
MemberLookupMode.IMPLICIT_THIS,
needsConst,
VmUtils.createThisNode(VmUtils.unavailableSourceSection(), isCustomThisScope),
GetClassNodeGen.create(null));
}
@SuppressWarnings("DuplicatedCode")
private void checkConst(VmObjectLike currOwner, Member method, int levelsUp) {
if (!constLevel.isConst()) {
return;
}
var memberIsOutsideConstScope = levelsUp > constDepth;
var invalid =
switch (constLevel) {
case ALL -> memberIsOutsideConstScope && !method.isConst();
case MODULE -> currOwner.isModuleObject() && !method.isConst();
default -> false;
};
if (invalid) {
throw exceptionBuilder().evalError("methodMustBeConst", methodName.toString()).build();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
package org.pkl.core.ast.expression.primary;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.VmUtils;
@@ -29,15 +28,7 @@ public final class GetEnclosingOwnerNode extends ExpressionNode {
assert levelsUp > 0 : "shouldn't be using GetEnclosingOwnerNode for levelsUp == 0";
}
@ExplodeLoop
public Object executeGeneric(VirtualFrame frame) {
var owner = VmUtils.getOwner(frame);
for (var i = 1; i < levelsUp; i++) {
owner = owner.getEnclosingOwner();
assert owner != null;
}
var result = owner.getEnclosingOwner();
assert result != null;
return result;
return VmUtils.getOwner(frame, levelsUp);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
package org.pkl.core.ast.expression.primary;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.VmUtils;
@@ -29,15 +28,7 @@ public final class GetEnclosingReceiverNode extends ExpressionNode {
assert levelsUp > 0 : "shouldn't be using GetEnclosingReceiverNode for levelsUp == 0";
}
@ExplodeLoop
public Object executeGeneric(VirtualFrame frame) {
var owner = VmUtils.getOwner(frame);
for (var i = 1; i < levelsUp; i++) {
owner = owner.getEnclosingOwner();
assert owner != null;
}
var result = owner.getEnclosingReceiver();
assert result != null;
return result;
return VmUtils.getReceiver(frame, levelsUp);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import org.pkl.core.runtime.VmUtils;
@NodeInfo(shortName = "module")
public final class GetModuleNode extends ExpressionNode {
public GetModuleNode(SourceSection sourceSection) {
super(sourceSection);
}
@@ -35,8 +36,10 @@ public final class GetModuleNode extends ExpressionNode {
for (var current = VmUtils.getOwner(frame).getEnclosingOwner();
current != null;
current = current.getEnclosingOwner()) {
if (!current.isParseTimeInvisibleScope()) {
levelsUp += 1;
}
}
return replace(levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp))
.executeGeneric(frame);
@@ -1,228 +0,0 @@
/*
* Copyright © 2024-2025 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.ast.expression.primary;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ConstantValueNode;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.MemberLookupMode;
import org.pkl.core.ast.builder.ConstLevel;
import org.pkl.core.ast.expression.member.ReadLocalPropertyNode;
import org.pkl.core.ast.expression.member.ReadPropertyNodeGen;
import org.pkl.core.ast.frame.ReadEnclosingFrameSlotNodeGen;
import org.pkl.core.ast.frame.ReadFrameSlotNodeGen;
import org.pkl.core.ast.member.Member;
import org.pkl.core.runtime.*;
/**
* Resolves a variable name, for example `foo` in `x = foo`.
*
* <p>A variable name can refer to any of the following:
*
* <ul>
* <li>a method/lambda parameter or for-generator/let-expression variable in the lexical scope
* <li>a (potentially `local`) property in the lexical scope
* <li>a `pkl.base` module property
* <li>a property accessible through `this`
* </ul>
*
* <p>This node's task is to make a one-time decision between these alternatives for the call site
* it represents.
*/
// TODO: Move this to parse time
// * more capable because more information is available
// and AST customization beyond replacing this node is possible
// * useful for runtime AST transformations, for example to implement property-based testing
// * more efficient
//
// TODO: In REPL, undo replace if environment changes to make the following work.
// Perhaps instrumenting this node in REPL would be a good solution.
// x = { y = z }
// :force x // Property not found: z
// z = 1
// :force x // should work but doesn't
public final class ResolveVariableNode extends ExpressionNode {
private final Identifier variableName;
private final boolean isBaseModule;
private final boolean isCustomThisScope;
private final ConstLevel constLevel;
private final int constDepth;
public ResolveVariableNode(
SourceSection sourceSection,
Identifier variableName,
boolean isBaseModule,
boolean isCustomThisScope,
ConstLevel constLevel,
int constDepth) {
super(sourceSection);
this.variableName = variableName;
this.isBaseModule = isBaseModule;
this.isCustomThisScope = isCustomThisScope;
this.constLevel = constLevel;
this.constDepth = constDepth;
}
@Override
public Object executeGeneric(VirtualFrame frame) {
return replace(doResolve(frame)).executeGeneric(frame);
}
private ExpressionNode doResolve(VirtualFrame frame) {
// don't compile this (only runs once)
// invalidation will be done by Node.replace() in the caller
CompilerDirectives.transferToInterpreter();
var localPropertyName = variableName.toLocalProperty();
var currFrame = frame;
var currOwner = VmUtils.getOwner(currFrame);
var levelsUp = 0;
// Search lexical scope for a matching function parameter, for-generator variable, or object
// property.
do {
var slot = findFrameSlot(currFrame, variableName, localPropertyName);
if (slot != -1) {
return levelsUp == 0
? ReadFrameSlotNodeGen.create(getSourceSection(), slot)
: ReadEnclosingFrameSlotNodeGen.create(getSourceSection(), slot, levelsUp);
}
var localMember = currOwner.getMember(localPropertyName);
if (localMember != null) {
assert localMember.isLocal();
checkConst(currOwner, localMember, levelsUp);
var value = localMember.getConstantValue();
if (value != null) {
// This is the only code path that resolves local constant properties.
// Since this code path doesn't call VmObject.getCachedValue(),
// there is no point in calling VmObject.setCachedValue() either.
return new ConstantValueNode(sourceSection, value);
}
return new ReadLocalPropertyNode(sourceSection, localMember, levelsUp);
}
var member = currOwner.getMember(variableName);
if (member != null) {
assert !member.isLocal();
checkConst(currOwner, member, levelsUp);
// Non-local properties are late-bound, which is why we can't ever return ConstantNode here.
//
// Assuming this node isn't used in Truffle ASTs of `external` pkl.base classes whose values
// aren't VmObject's,
// we only ever need VmObject-compatible specializations here.
// We don't exploit this fact here but ReadLocalPropertyNode (used above) does.
return ReadPropertyNodeGen.create(
sourceSection,
variableName,
MemberLookupMode.IMPLICIT_LEXICAL,
// we already checked for const-safety, no need to recheck
false,
levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp));
}
currFrame = currOwner.getEnclosingFrame();
currOwner = VmUtils.getOwnerOrNull(currFrame);
levelsUp += 1;
} while (currOwner != null);
// Search base module, unless call site is inside base module.
if (!isBaseModule) {
var baseModule = BaseModule.getModule();
var cachedValue = baseModule.getCachedValue(variableName);
if (cachedValue != null) {
return new ConstantValueNode(sourceSection, cachedValue);
}
var member = baseModule.getMember(variableName);
if (member != null) {
var constantValue = member.getConstantValue();
if (constantValue != null) {
baseModule.setCachedValue(variableName, constantValue);
return new ConstantValueNode(sourceSection, constantValue);
}
var computedValue = member.getCallTarget().call(baseModule, baseModule);
baseModule.setCachedValue(variableName, computedValue);
return new ConstantValueNode(sourceSection, computedValue);
}
}
// Assuming this property exists at all, it must be a property accessible through `this`.
///
// Reading a property off of implicit `this` needs a const check if this node is not in a const
// scope.
// open class A {
// a = 1
// }
//
// class B extends A {
// const b = a // <-- implicit this lookup of `a`, which is not in a const scope.
// }
//
// A const scope exists if there is an object body, for example.
//
// class B extends A {
// const b = new { a } // <-- `new {}` creates a const scope.
// }
boolean needsConst = constLevel == ConstLevel.ALL && constDepth == -1 && !isCustomThisScope;
return ReadPropertyNodeGen.create(
sourceSection,
variableName,
MemberLookupMode.IMPLICIT_THIS,
needsConst,
VmUtils.createThisNode(VmUtils.unavailableSourceSection(), isCustomThisScope));
}
@SuppressWarnings("DuplicatedCode")
private void checkConst(VmObjectLike currOwner, Member member, int levelsUp) {
if (!constLevel.isConst()) {
return;
}
var memberIsOutsideConstScope = levelsUp > constDepth;
var invalid =
switch (constLevel) {
case ALL -> memberIsOutsideConstScope && !member.isConst();
case MODULE -> currOwner.isModuleObject() && !member.isConst();
default -> false;
};
if (invalid) {
throw exceptionBuilder().evalError("propertyMustBeConst", variableName.toString()).build();
}
}
private static int findFrameSlot(VirtualFrame frame, Object identifier1, Object identifier2) {
var descriptor = frame.getFrameDescriptor();
// Search backwards. The for-generator implementation exploits this
// to shadow a slot by appending a slot with the same name.
for (var i = descriptor.getNumberOfSlots() - 1; i >= 0; i--) {
var slotName = descriptor.getSlotName(i);
if (slotName == identifier1 || slotName == identifier2) {
return i;
}
}
return -1;
}
}
@@ -0,0 +1,38 @@
/*
* 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.ast.frame;
import com.oracle.truffle.api.frame.VirtualFrame;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.VmUtils;
public final class GetEnclosingFrameNode extends ExpressionNode {
private final int levelsUp;
public GetEnclosingFrameNode(int levelsUp) {
this.levelsUp = levelsUp;
}
@Override
public VirtualFrame executeGeneric(VirtualFrame frame) {
return VmUtils.getFrame(frame, levelsUp);
}
@Override
public boolean isInstrumentable() {
return false;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,59 +17,40 @@ package org.pkl.core.ast.frame;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.FrameSlotTypeException;
import com.oracle.truffle.api.frame.MaterializedFrame;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.VmUtils;
public abstract class ReadEnclosingFrameSlotNode extends ExpressionNode {
public abstract class ReadExactFrameSlotNode extends ExpressionNode {
private final int slot;
private final int levelsUp;
protected ReadEnclosingFrameSlotNode(SourceSection sourceSection, int slot, int levelsUp) {
protected ReadExactFrameSlotNode(SourceSection sourceSection, int slot) {
super(sourceSection);
this.slot = slot;
this.levelsUp = levelsUp;
assert levelsUp > 0 : "should be using ReadFrameSlotNode for levelsUp == 0";
}
@Specialization(rewriteOn = FrameSlotTypeException.class)
protected long evalInt(VirtualFrame frame) throws FrameSlotTypeException {
return getCapturedFrame(frame).getLong(slot);
return frame.getLong(slot);
}
@Specialization(rewriteOn = FrameSlotTypeException.class)
protected double evalFloat(VirtualFrame frame) throws FrameSlotTypeException {
return getCapturedFrame(frame).getDouble(slot);
return frame.getDouble(slot);
}
@Specialization(rewriteOn = FrameSlotTypeException.class)
protected boolean evalBoolean(VirtualFrame frame) throws FrameSlotTypeException {
return getCapturedFrame(frame).getBoolean(slot);
return frame.getBoolean(slot);
}
@Specialization(rewriteOn = FrameSlotTypeException.class)
protected Object evalObject(VirtualFrame frame) throws FrameSlotTypeException {
return getCapturedFrame(frame).getObject(slot);
return frame.getObject(slot);
}
@Specialization(replaces = {"evalInt", "evalFloat", "evalBoolean", "evalObject"})
protected Object evalGeneric(VirtualFrame frame) {
return getCapturedFrame(frame).getValue(slot);
}
// could be factored out into a separate node s.t. it
// won't be repeated in case of FrameSlotTypeException
@ExplodeLoop
protected final MaterializedFrame getCapturedFrame(VirtualFrame frame) {
var owner = VmUtils.getOwner(frame);
for (var i = 0; i < levelsUp - 1; i++) {
owner = owner.getEnclosingOwner();
assert owner != null; // guaranteed by AstBuilder
}
return owner.getEnclosingFrame();
return frame.getValue(slot);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,12 +15,14 @@
*/
package org.pkl.core.ast.frame;
import com.oracle.truffle.api.dsl.NodeChild;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.FrameSlotTypeException;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
@NodeChild(value = "enclosingFrame", type = GetEnclosingFrameNode.class)
public abstract class ReadFrameSlotNode extends ExpressionNode {
private final int slot;
@@ -50,6 +50,7 @@ import org.pkl.core.util.AnsiStringBuilder;
import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.MutableReference;
import org.pkl.core.util.Pair;
import org.pkl.core.util.SyntaxHighlighter;
import org.pkl.parser.Parser;
import org.pkl.parser.ParserError;
@@ -58,9 +59,12 @@ import org.pkl.parser.syntax.ClassProperty;
import org.pkl.parser.syntax.Expr;
import org.pkl.parser.syntax.ImportClause;
import org.pkl.parser.syntax.ModuleDecl;
import org.pkl.parser.syntax.Node;
import org.pkl.parser.syntax.ReplInput;
public class ReplServer implements AutoCloseable {
private static final String expressionPreamble = "`THE REPL TEXT EXPR` = ";
private final IndirectCallNode callNode = Truffle.getRuntime().createIndirectCallNode();
private final Context polyglotContext;
private final VmLanguage language;
@@ -72,6 +76,7 @@ public class ReplServer implements AutoCloseable {
private final PackageResolver packageResolver;
private final @Nullable ProjectDependenciesManager projectDependenciesManager;
private final boolean color;
private final Parser parser = new Parser();
public ReplServer(
SecurityManager securityManager,
@@ -178,7 +183,132 @@ public class ReplServer implements AutoCloseable {
.collect(Collectors.toList());
}
@SuppressWarnings({"StatementWithEmptyBody"})
/**
* Create a fake module that declares all the local properties that exist in ReplState, so that
* AstBuilder can correctly resolve variables. Additionally, return a {@link Node} with its span
* calculated in terms of this fake module.
*
* <p>This created module is never executed.
*/
private Pair<String, Node> buildSyntheticModuleText(Node syntaxNode, String srcText) {
if (syntaxNode instanceof ModuleDecl) {
return Pair.of(srcText, syntaxNode);
}
var sb = new StringBuilder();
var nodeText = syntaxNode.text(srcText.toCharArray());
var adjustedNode = syntaxNode;
if (syntaxNode instanceof Expr) {
sb.append(expressionPreamble).append(nodeText).append('\n');
} else {
sb.append(nodeText).append('\n');
}
var mod = parser.parseModule(sb.toString());
if (syntaxNode instanceof Expr) {
adjustedNode = mod.getProperties().get(0).getExpr();
} else if (syntaxNode instanceof ImportClause) {
adjustedNode = mod.getImports().get(0);
} else if (syntaxNode instanceof ClassProperty) {
adjustedNode = mod.getProperties().get(0);
} else if (syntaxNode instanceof Class) {
adjustedNode = mod.getClasses().get(0);
} else if (syntaxNode instanceof org.pkl.parser.syntax.TypeAlias) {
adjustedNode = mod.getTypeAliases().get(0);
} else if (syntaxNode instanceof org.pkl.parser.syntax.ClassMethod) {
adjustedNode = mod.getMethods().get(0);
}
var cursor = EconomicMaps.getEntries(replState.module.getMembers());
while (cursor.advance()) {
var key = cursor.getKey();
var value = cursor.getValue();
if (value.isLocal()) {
sb.append("local ").append(key).append(" = Undefined()\n\n");
}
}
assert adjustedNode != null;
return Pair.of(sb.toString(), adjustedNode);
}
@SuppressWarnings("StatementWithEmptyBody")
private void handleNode(
ReplState replState,
List<Object> results,
URI uri,
Node node,
String srcText,
boolean evalDefinitions,
boolean forceResults) {
var pair = buildSyntheticModuleText(node, srcText);
var syntheticModuleText = pair.first;
var adjustedNode = pair.second;
var module = ModuleKeys.synthetic(uri, workingDir.toUri(), uri, syntheticModuleText, false);
ResolvedModuleKey resolved;
try {
resolved = module.resolve(securityManager);
} catch (SecurityManagerException e) {
throw new VmExceptionBuilder().withCause(e).build();
} catch (IOException e) {
// resolving a synthetic module should never cause IOException
throw new AssertionError(e);
}
var builder =
new AstBuilder(
VmUtils.loadSource(resolved),
language,
replState.module.getModuleInfo(),
moduleResolver);
var mod = parser.parseModule(syntheticModuleText);
try {
builder.visitModule(mod);
if (adjustedNode instanceof Expr expr) {
var exprNode = builder.visitExpr(expr);
evaluateExpr(replState, exprNode, forceResults, results);
} else if (adjustedNode instanceof ImportClause importClause) {
addStaticModuleProperty(builder.visitImportClause(importClause));
} else if (adjustedNode instanceof ClassProperty classProperty) {
var propertyNode = builder.visitClassProperty(classProperty);
var property = addModuleProperty(propertyNode);
if (evalDefinitions) {
evaluateMemberDef(replState, property, forceResults, results);
}
} else if (adjustedNode instanceof Class clazz) {
addStaticModuleProperty(builder.visitClass(clazz));
} else if (adjustedNode instanceof org.pkl.parser.syntax.TypeAlias typeAlias) {
addStaticModuleProperty(builder.visitTypeAlias(typeAlias));
} else if (adjustedNode instanceof org.pkl.parser.syntax.ClassMethod classMethod) {
addModuleMethodDef(builder.visitClassMethod(classMethod));
} else if (adjustedNode instanceof ModuleDecl) {
// do nothing for now
} else {
results.add(
new ReplResponse.InternalError(new IllegalStateException("Unexpected parse result")));
}
} catch (VmException e) {
// TODO: patch stack trace for constants
results.add(new EvalError(renderException(e)));
}
}
/**
* Strip out the {@code `THE REPL TEXT EXPR` = } preamble that we insert in front of expressions;
*/
private String renderException(VmException e) {
var rendered = errorRenderer.render(e);
var sb = new StringBuilder();
var lines = rendered.split("\n");
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line.contains(expressionPreamble)) {
sb.append(line.replace(expressionPreamble, "")).append('\n');
var decoration = lines[i + 1];
sb.append(decoration.replace(" ".repeat(expressionPreamble.length()), "")).append('\n');
i++;
} else {
sb.append(line).append('\n');
}
}
return sb.toString();
}
private List<Object> evaluate(
ReplState replState,
String requestId,
@@ -198,53 +328,9 @@ public class ReplServer implements AutoCloseable {
}
var results = new ArrayList<>();
var module = ModuleKeys.synthetic(uri, workingDir.toUri(), uri, text, false);
ResolvedModuleKey resolved;
try {
resolved = module.resolve(securityManager);
} catch (SecurityManagerException e) {
throw new VmExceptionBuilder().withCause(e).build();
} catch (IOException e) {
// resolving a synthetic module should never cause IOException
throw new AssertionError(e);
}
var builder =
new AstBuilder(
VmUtils.loadSource(resolved),
language,
replState.module.getModuleInfo(),
moduleResolver);
for (var tree : replInputContext.getNodes()) {
try {
if (tree instanceof Expr expr) {
var exprNode = builder.visitExpr(expr);
evaluateExpr(replState, exprNode, forceResults, results);
} else if (tree instanceof ImportClause importClause) {
addStaticModuleProperty(builder.visitImportClause(importClause));
} else if (tree instanceof ClassProperty classProperty) {
var propertyNode = builder.visitClassProperty(classProperty);
var property = addModuleProperty(propertyNode);
if (evalDefinitions) {
evaluateMemberDef(replState, property, forceResults, results);
}
} else if (tree instanceof Class clazz) {
addStaticModuleProperty(builder.visitClass(clazz));
} else if (tree instanceof org.pkl.parser.syntax.TypeAlias typeAlias) {
addStaticModuleProperty(builder.visitTypeAlias(typeAlias));
} else if (tree instanceof org.pkl.parser.syntax.ClassMethod classMethod) {
addModuleMethodDef(builder.visitClassMethod(classMethod));
} else if (tree instanceof ModuleDecl) {
// do nothing for now
} else {
results.add(
new ReplResponse.InternalError(new IllegalStateException("Unexpected parse result")));
}
} catch (VmException e) {
// TODO: patch stack trace for constants
results.add(new EvalError(errorRenderer.render(e)));
}
handleNode(replState, results, uri, tree, text, evalDefinitions, forceResults);
}
return results;
@@ -0,0 +1,84 @@
/*
* 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.runtime;
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.FrameSlotKind;
import java.util.Arrays;
import org.jspecify.annotations.Nullable;
/**
* A wrapper for Truffle's {@link FrameDescriptor.Builder}, but also lets us find the slot of a
* given {@link Identifier}.
*/
public class FrameDescriptorBuilder {
private @Nullable Identifier[] names;
private int size;
private final FrameDescriptor.Builder underlying;
private static final int DEFAULT_CAPACITY = 8;
public FrameDescriptorBuilder() {
this(DEFAULT_CAPACITY);
}
public FrameDescriptorBuilder(int capacity) {
underlying = FrameDescriptor.newBuilder(capacity);
this.names = new Identifier[capacity];
}
public FrameDescriptorBuilder(FrameDescriptor descriptor) {
this(descriptor.getNumberOfSlots());
for (var i = 0; i < descriptor.getNumberOfSlots(); i++) {
addSlot(
descriptor.getSlotKind(i),
(Identifier) descriptor.getSlotName(i),
descriptor.getSlotInfo(i));
}
}
private void ensureCapacity(int count) {
if (names.length < size + count) {
var newLength = Math.max(size + count, size * 2);
names = Arrays.copyOf(names, newLength);
}
}
public int addSlot(FrameSlotKind kind, @Nullable Identifier name, @Nullable Object info) {
ensureCapacity(1);
names[size] = name;
size++;
return underlying.addSlot(kind, name, info);
}
public int findSlot(Identifier identifier) {
// go backwards to account for shadowed variables
// (this happens in the case of nested for generators).
for (var i = size - 1; i >= 0; i--) {
var name = names[i];
if (name != null && name.equals(identifier)) {
return i;
}
}
return -1;
}
public FrameDescriptor build() {
return underlying.build();
}
}
@@ -30,6 +30,8 @@ public final class VmFunction extends VmObjectLike {
private final Object thisValue;
private final int paramCount;
private final PklRootNode rootNode;
private final boolean hasObjectParams;
private final boolean isFunctionAmend;
public VmFunction(
MaterializedFrame enclosingFrame,
@@ -37,11 +39,24 @@ public final class VmFunction extends VmObjectLike {
int paramCount,
PklRootNode rootNode,
@Nullable Object extraStorage) {
this(enclosingFrame, thisValue, paramCount, rootNode, extraStorage, false, false);
}
public VmFunction(
MaterializedFrame enclosingFrame,
Object thisValue,
int paramCount,
PklRootNode rootNode,
@Nullable Object extraStorage,
boolean isFunctionAmend,
boolean hasObjectParams) {
super(enclosingFrame);
this.thisValue = thisValue;
this.paramCount = paramCount;
this.rootNode = rootNode;
this.hasObjectParams = hasObjectParams;
this.extraStorage = extraStorage;
this.isFunctionAmend = isFunctionAmend;
}
public RootCallTarget getCallTarget() {
@@ -71,7 +86,9 @@ public final class VmFunction extends VmObjectLike {
thisValue,
newParamCount,
newRootNode == null ? rootNode : newRootNode,
newExtraStorage);
newExtraStorage,
isFunctionAmend,
hasObjectParams);
}
public Object getThisValue() {
@@ -187,4 +204,9 @@ public final class VmFunction extends VmObjectLike {
public String toString() {
return VmValueRenderer.singleLine(Integer.MAX_VALUE).render(this);
}
@Override
public boolean isParseTimeInvisibleScope() {
return isFunctionAmend && !hasObjectParams;
}
}
@@ -82,6 +82,41 @@ public abstract class VmObjectLike extends VmValue {
/** Returns the declared members of this object. */
public abstract UnmodifiableEconomicMap<Object, ObjectMember> getMembers();
/**
* Tells if it's impossible to determine if this is object describes a scope during parse time.
*
* <p>An amended lambda hides a new lexical scope if there are also no params.
*
* <p>Assuming {@code foo} is a lambda, this snippet:
*
* <pre>{@code
* qux = 3
*
* foo {
* bar = qux
* }
* }</pre>
*
* Desugars to:
*
* <pre>{@code
* qux = 3
*
* foo = () -> (super.foo.apply()) {
* bar = qux
* }
*
* }</pre>
*
* So, {@code qux} is <i>two</i> levels higher, not one. However, it's not possible to figure this
* out at parse time alone.
*
* <p>This method tells if we need to skip this object when traversing up lexical scopes.
*/
public boolean isParseTimeInvisibleScope() {
return false;
}
/**
* Reads from the properties cache for this object. The cache contains the values of all members
* defined in this object or an ancestor thereof which have been requested with this object as the
@@ -143,6 +143,10 @@ public final class VmUtils {
return (VmObjectLike) getReceiver(frame);
}
public static VmObjectLike getObjectReceiver(VirtualFrame frame, int levelsUp) {
return (VmObjectLike) getReceiver(frame, levelsUp);
}
public static VmTyped getTypedObjectReceiver(Frame frame) {
return (VmTyped) getReceiver(frame);
}
@@ -159,6 +163,39 @@ public final class VmUtils {
return result;
}
public static VmObjectLike getOwner(VirtualFrame frame, int levelsUp) {
return getOwner(getFrame(frame, levelsUp));
}
public static Object getReceiver(VirtualFrame frame, int levelsUp) {
return getReceiver(getFrame(frame, levelsUp));
}
public static VirtualFrame getFrame(VirtualFrame frame, int levelsUp) {
frame = skipInvisibleScopes(frame);
if (levelsUp == 0) {
return frame;
}
var owner = getOwner(frame);
for (var i = 0; i < levelsUp; i++) {
frame = owner.getEnclosingFrame();
owner = getOwner(frame);
if (owner.isParseTimeInvisibleScope()) {
i--;
}
}
return frame;
}
private static VirtualFrame skipInvisibleScopes(VirtualFrame frame) {
var owner = getOwner(frame);
while (owner.isParseTimeInvisibleScope()) {
frame = owner.getEnclosingFrame();
owner = getOwner(frame);
}
return frame;
}
/** Returns a `ObjectMember`'s key while executing the corresponding `MemberNode`. */
public static Object getMemberKey(Frame frame) {
return frame.getArguments()[2];
@@ -375,6 +412,7 @@ public final class VmUtils {
int numberOfLocalsToCopy) {
var sourceDescriptor = sourceFrame.getFrameDescriptor();
var targetDescriptor = targetFrame.getFrameDescriptor();
assert sourceDescriptor.getNumberOfSlots() <= targetDescriptor.getNumberOfSlots();
// Alternatively, locals could be copied with `numberOfLocalsToCopy`
// `ReadFrameSlotNode/WriteFrameSlotNode`'s.
for (int i = 0; i < numberOfLocalsToCopy; i++) {
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@ import java.util.HashMap;
import java.util.Map;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.expression.primary.GetReceiverNode;
import org.pkl.core.ast.frame.ReadFrameSlotNodeGen;
import org.pkl.core.ast.frame.ReadExactFrameSlotNodeGen;
import org.pkl.core.runtime.VmException;
import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.runtime.VmUtils;
@@ -115,7 +115,7 @@ public abstract class ExternalMemberRegistry {
if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection);
var sourceSection = VmUtils.unavailableSourceSection();
var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0);
var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0);
return factory.create(new GetReceiverNode(), param1Node);
}
@@ -125,8 +125,8 @@ public abstract class ExternalMemberRegistry {
if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection);
var sourceSection = VmUtils.unavailableSourceSection();
var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0);
var param2Node = ReadFrameSlotNodeGen.create(sourceSection, 1);
var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0);
var param2Node = ReadExactFrameSlotNodeGen.create(sourceSection, 1);
return factory.create(new GetReceiverNode(), param1Node, param2Node);
}
@@ -136,9 +136,9 @@ public abstract class ExternalMemberRegistry {
if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection);
var sourceSection = VmUtils.unavailableSourceSection();
var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0);
var param2Node = ReadFrameSlotNodeGen.create(sourceSection, 1);
var param3Node = ReadFrameSlotNodeGen.create(sourceSection, 2);
var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0);
var param2Node = ReadExactFrameSlotNodeGen.create(sourceSection, 1);
var param3Node = ReadExactFrameSlotNodeGen.create(sourceSection, 2);
return factory.create(new GetReceiverNode(), param1Node, param2Node, param3Node);
}
@@ -148,10 +148,10 @@ public abstract class ExternalMemberRegistry {
if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection);
var sourceSection = VmUtils.unavailableSourceSection();
var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0);
var param2Node = ReadFrameSlotNodeGen.create(sourceSection, 1);
var param3Node = ReadFrameSlotNodeGen.create(sourceSection, 2);
var param4Node = ReadFrameSlotNodeGen.create(sourceSection, 3);
var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0);
var param2Node = ReadExactFrameSlotNodeGen.create(sourceSection, 1);
var param3Node = ReadExactFrameSlotNodeGen.create(sourceSection, 2);
var param4Node = ReadExactFrameSlotNodeGen.create(sourceSection, 3);
return factory.create(new GetReceiverNode(), param1Node, param2Node, param3Node, param4Node);
}
@@ -161,11 +161,11 @@ public abstract class ExternalMemberRegistry {
if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection);
var sourceSection = VmUtils.unavailableSourceSection();
var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0);
var param2Node = ReadFrameSlotNodeGen.create(sourceSection, 1);
var param3Node = ReadFrameSlotNodeGen.create(sourceSection, 2);
var param4Node = ReadFrameSlotNodeGen.create(sourceSection, 3);
var param5Node = ReadFrameSlotNodeGen.create(sourceSection, 4);
var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0);
var param2Node = ReadExactFrameSlotNodeGen.create(sourceSection, 1);
var param3Node = ReadExactFrameSlotNodeGen.create(sourceSection, 2);
var param4Node = ReadExactFrameSlotNodeGen.create(sourceSection, 3);
var param5Node = ReadExactFrameSlotNodeGen.create(sourceSection, 4);
return factory.create(
new GetReceiverNode(), param1Node, param2Node, param3Node, param4Node, param5Node);
}
@@ -0,0 +1,34 @@
import "pkl:reflect"
const function plusThree(it) = it + 3
const function plusFour(it) = it + 4
local const myProp = "myProp"
class MyClass {
@MyAnnotation {
func = (it) -> plusThree(it)
prop = myProp
}
foo: Int
}
@MyAnnotation {
func = (it) -> plusFour(it)
prop = myProp
}
hidden qux: Int
class MyAnnotation extends Annotation {
func: ((Any) -> Any)
prop: Any
}
local nestedAnnotation = reflect.Class(MyClass).properties["foo"].annotations.first as MyAnnotation
local moduleAnnotation = reflect.Module(module).moduleClass.properties["qux"].annotations.first as MyAnnotation
res1 = nestedAnnotation.func.apply(15)
res2 = nestedAnnotation.prop
res3 = moduleAnnotation.func.apply(15)
res4 = moduleAnnotation.prop
@@ -0,0 +1,7 @@
local foo = "hello"
bar {
new Mixin {
[foo] = "world"
}.apply(new Dynamic {})
}
@@ -0,0 +1,8 @@
qux = "outer"
foo {
when (true) {
local qux = "then branch"
prop = qux
}
}
@@ -0,0 +1,29 @@
// `qux` has ambiguous locality; don't know if it should be local read or not
hidden res1 {
cond = true
prop {
when (cond) {
local qux = "then branch"
} else {
qux = "else branch"
}
theBranch = qux
}
}
res2 = (res1) {
cond = false
}
res3 {
cond = true
prop {
when (cond) {
local qux = "then branch"
} else {
qux = cond
}
theBranch = qux
}
}
@@ -0,0 +1,18 @@
foo {
bar = new Listing { "elem" } |> mapEnvWithObjectParam(new Dynamic {
res1Name = "res1Value"
res2Name = "res2Value"
})
}
function mapEnvWithObjectParam(_env: Dynamic) = new Mixin { it ->
new {
res = it
}
for (k, v in _env) {
new {
name = k
value = v
}
}
}
@@ -0,0 +1,15 @@
local function myFunc(foo: String, bar: String, baz: String): Any = new Listing {
for (
qux in new Listing {
for (param1 in List(1)) {
List("\(param1) \(foo) \(bar) \(baz)")
}
}
) {
for (param2 in qux) {
"Hello \(foo) \(bar) \(baz) \(param2)"
}
}
}
res = myFunc("arg1", "arg2", "arg3")
@@ -0,0 +1,4 @@
res1 = 18
res2 = "myProp"
res3 = 19
res4 = "myProp"
@@ -0,0 +1,5 @@
bar {
new {
["hello"] = "world"
}
}
@@ -0,0 +1,4 @@
qux = "outer"
foo {
prop = "then branch"
}
@@ -0,0 +1,13 @@
res2 {
cond = false
prop {
qux = "else branch"
theBranch = "else branch"
}
}
res3 {
cond = true
prop {
theBranch = "then branch"
}
}
@@ -0,0 +1,18 @@
foo {
bar {
"elem"
new {
res {
"elem"
}
}
new {
name = "res1Name"
value = "res1Value"
}
new {
name = "res2Name"
value = "res2Value"
}
}
}
@@ -0,0 +1,3 @@
res {
"Hello arg1 arg2 arg3 1 arg1 arg2 arg3"
}
@@ -223,6 +223,24 @@ class ReplServerTest {
assertThat((response as ReplResponse.EvalSuccess).result).isEqualTo("\u001B[32m5\u001B[0m.ms")
}
@Test
fun `strip expression preamble in error message`() {
val result = makeFailingEvalRequest("foo")
assertThat(result)
.isEqualTo(
"""
Pkl Error
Cannot find property `foo`.
1 | foo
^^^
at (repl:id)
"""
.trimIndent()
)
}
private fun makeEvalRequest(text: String): String {
val responses = server.handleRequest(ReplRequest.Eval("id", text, false, false))