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, Properties that override an existing property shouldn't have doc comments nor type annotations,
unless the type is intentionally overridden via `extends`. unless the type is intentionally overridden via `extends`.
[source%tested,{pkl}] [source%parsed,{pkl}]
---- ----
amends "myOtherModule.pkl" amends "myOtherModule.pkl"
+31
View File
@@ -61,6 +61,7 @@ dependencies {
add("generatorImplementation", libs.javaPoet) add("generatorImplementation", libs.javaPoet)
add("generatorImplementation", libs.truffleApi) add("generatorImplementation", libs.truffleApi)
add("generatorImplementation", libs.jspecify) add("generatorImplementation", libs.jspecify)
add("generatorImplementation", projects.pklParser)
javaExecutableConfiguration(project(":pkl-cli", "javaExecutable")) javaExecutableConfiguration(project(":pkl-cli", "javaExecutable"))
} }
@@ -140,6 +141,36 @@ tasks.test {
maxHeapSize = "1g" 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 val testJavaExecutable by
tasks.registering(Test::class) { tasks.registering(Test::class) {
configureExecutableTest("LanguageSnippetTestsEngine") 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 GLOB = 0x1000;
public static final int AMBIGUOUS_LOCALITY = 0x10000;
// modifier sets // modifier sets
public static final int NONE = 0; public static final int NONE = 0;
@@ -126,6 +128,10 @@ public final class VmModifier {
return (modifiers & CONST) != 0; return (modifiers & CONST) != 0;
} }
public static boolean isAmbiguousLocality(int modifiers) {
return (modifiers & AMBIGUOUS_LOCALITY) != 0;
}
public static boolean isElement(int modifiers) { public static boolean isElement(int modifiers) {
return (modifiers & ELEMENT) != 0; return (modifiers & ELEMENT) != 0;
} }
@@ -154,6 +160,10 @@ public final class VmModifier {
return (modifiers & (CONST | FIXED)) != 0; 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) { public static Set<Modifier> export(int modifiers, boolean isClass) {
var result = EnumSet.noneOf(Modifier.class); 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.MemberLookupMode;
import org.pkl.core.ast.PklRootNode; import org.pkl.core.ast.PklRootNode;
import org.pkl.core.ast.VmModifier; 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.AnnotationScope;
import org.pkl.core.ast.builder.SymbolTable.ClassScope; 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.AdditionNodeGen;
import org.pkl.core.ast.expression.binary.DivisionNodeGen; import org.pkl.core.ast.expression.binary.DivisionNodeGen;
import org.pkl.core.ast.expression.binary.EqualNodeGen; 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.InferParentWithinMethodNode;
import org.pkl.core.ast.expression.member.InferParentWithinObjectMethodNode; import org.pkl.core.ast.expression.member.InferParentWithinObjectMethodNode;
import org.pkl.core.ast.expression.member.InferParentWithinPropertyNodeGen; 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.InvokeMethodVirtualNodeGen;
import org.pkl.core.ast.expression.member.InvokeObjectMethodNode;
import org.pkl.core.ast.expression.member.InvokeSuperMethodNodeGen; 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.ReadPropertyNodeGen;
import org.pkl.core.ast.expression.member.ReadSuperEntryNode; import org.pkl.core.ast.expression.member.ReadSuperEntryNode;
import org.pkl.core.ast.expression.member.ReadSuperPropertyNode; 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.GetEnclosingOwnerNode;
import org.pkl.core.ast.expression.primary.GetEnclosingReceiverNode; import org.pkl.core.ast.expression.primary.GetEnclosingReceiverNode;
import org.pkl.core.ast.expression.primary.GetMemberKeyNode; 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.GetOwnerNode;
import org.pkl.core.ast.expression.primary.GetReceiverNode; import org.pkl.core.ast.expression.primary.GetReceiverNode;
import org.pkl.core.ast.expression.primary.OuterNode; 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.primary.ThisNode;
import org.pkl.core.ast.expression.ternary.IfElseNode; import org.pkl.core.ast.expression.ternary.IfElseNode;
import org.pkl.core.ast.expression.unary.AbstractImportNode; 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.ThrowNodeGen;
import org.pkl.core.ast.expression.unary.TraceNode; import org.pkl.core.ast.expression.unary.TraceNode;
import org.pkl.core.ast.expression.unary.UnaryMinusNodeGen; 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.GetBaseModuleClassNode;
import org.pkl.core.ast.internal.GetClassNodeGen; import org.pkl.core.ast.internal.GetClassNodeGen;
import org.pkl.core.ast.internal.ToStringNodeGen; 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.module.ResolvedModuleKey;
import org.pkl.core.packages.PackageLoadError; import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.runtime.BaseModule; import org.pkl.core.runtime.BaseModule;
import org.pkl.core.runtime.FrameDescriptorBuilder;
import org.pkl.core.runtime.ModuleInfo; import org.pkl.core.runtime.ModuleInfo;
import org.pkl.core.runtime.ModuleResolver; import org.pkl.core.runtime.ModuleResolver;
import org.pkl.core.runtime.VmBytes; 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.ObjectProperty;
import org.pkl.parser.syntax.ObjectMember.ObjectSpread; import org.pkl.parser.syntax.ObjectMember.ObjectSpread;
import org.pkl.parser.syntax.ObjectMember.WhenGenerator; 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.Parameter.TypedIdentifier;
import org.pkl.parser.syntax.ParameterList; import org.pkl.parser.syntax.ParameterList;
import org.pkl.parser.syntax.QualifiedIdentifier; import org.pkl.parser.syntax.QualifiedIdentifier;
@@ -285,7 +301,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
isBaseModule = ModuleKeys.isBaseModule(moduleKey); isBaseModule = ModuleKeys.isBaseModule(moduleKey);
isStdLibModule = ModuleKeys.isStdLibModule(moduleKey); isStdLibModule = ModuleKeys.isStdLibModule(moduleKey);
externalMemberRegistry = MemberRegistryFactory.get(moduleKey); externalMemberRegistry = MemberRegistryFactory.get(moduleKey);
symbolTable = new SymbolTable(moduleInfo); symbolTable = new SymbolTable(moduleInfo, isBaseModule);
isMethodReturnTypeChecked = !isStdLibModule || IoUtils.isTestMode(); isMethodReturnTypeChecked = !isStdLibModule || IoUtils.isTestMode();
} }
@@ -490,10 +506,10 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
@Override @Override
public GetModuleNode visitModuleExpr(ModuleExpr expr) { public GetModuleNode visitModuleExpr(ModuleExpr expr) {
var currentScope = symbolTable.getCurrentScope();
// cannot use unqualified `module` in a const context // cannot use unqualified `module` in a const context
if (symbolTable.getCurrentScope().getConstLevel().isConst() if (currentScope.getConstLevel().isConst() && !(expr.parent() instanceof QualifiedAccessExpr)) {
&& !(expr.parent() instanceof QualifiedAccessExpr)) { var scope = currentScope;
var scope = symbolTable.getCurrentScope();
while (scope != null while (scope != null
&& !(scope instanceof AnnotationScope) && !(scope instanceof AnnotationScope)
&& !(scope instanceof ClassScope)) { && !(scope instanceof ClassScope)) {
@@ -501,7 +517,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
} }
if (scope == null) { if (scope == null) {
throw exceptionBuilder() throw exceptionBuilder()
.evalError("moduleIsNotConst", symbolTable.getCurrentScope().getName().toString()) .evalError("moduleIsNotConst", currentScope.getName().toString())
.withSourceSection(createSourceSection(expr)) .withSourceSection(createSourceSection(expr))
.build(); .build();
} }
@@ -636,44 +652,161 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
}; };
} }
@Override private ExpressionNode resolveReadVariable(UnqualifiedAccessExpr expr) {
public ExpressionNode visitUnqualifiedAccessExpr(UnqualifiedAccessExpr expr) { var name = expr.getIdentifier().getValue();
var identifier = toIdentifier(expr.getIdentifier().getValue()); var scope = symbolTable.getCurrentScope();
var argList = expr.getArgumentList(); var sourceSection = createSourceSection(expr);
var constLevel = scope.getConstLevel();
if (argList == null) { var constDepth = scope.getConstDepth();
return createResolveVariableNode(createSourceSection(expr), identifier); 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 private ExpressionNode resolvedMethodCall(UnqualifiedAccessExpr expr, ArgumentList argList) {
// TODO: support qualified calls (e.g., `import "pkl:base"; x = var name = expr.getIdentifier().getValue();
// base.List()/Set()/Map()/Bytes()`) for correctness 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) { if (identifier == org.pkl.core.runtime.Identifier.LIST) {
return doVisitListLiteral(expr, argList); return doVisitListLiteral(expr, argList);
} } else if (identifier == org.pkl.core.runtime.Identifier.SET) {
if (identifier == org.pkl.core.runtime.Identifier.SET) {
return doVisitSetLiteral(expr, argList); return doVisitSetLiteral(expr, argList);
} } else if (identifier == org.pkl.core.runtime.Identifier.MAP) {
if (identifier == org.pkl.core.runtime.Identifier.MAP) {
return doVisitMapLiteral(expr, argList); return doVisitMapLiteral(expr, argList);
} } else if (identifier == org.pkl.core.runtime.Identifier.BYTES_CONSTRUCTOR) {
if (identifier == org.pkl.core.runtime.Identifier.BYTES_CONSTRUCTOR) {
return doVisitBytesLiteral(expr, argList); 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(); @Override
public ExpressionNode visitUnqualifiedAccessExpr(UnqualifiedAccessExpr expr) {
return new ResolveMethodNode( var argList = expr.getArgumentList();
createSourceSection(expr), return argList == null ? resolveReadVariable(expr) : resolvedMethodCall(expr, argList);
identifier,
visitArgumentList(argList),
isBaseModule,
scope.isCustomThisScope(),
scope.getConstLevel(),
scope.getConstDepth());
} }
@Override @Override
@@ -980,12 +1113,14 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
public ExpressionNode visitLetExpr(LetExpr letExpr) { public ExpressionNode visitLetExpr(LetExpr letExpr) {
var sourceSection = createSourceSection(letExpr); var sourceSection = createSourceSection(letExpr);
var parameter = letExpr.getParameter(); var parameter = letExpr.getParameter();
var frameBuilder = FrameDescriptor.newBuilder(); var frameBuilder = new FrameDescriptorBuilder();
UnresolvedTypeNode[] typeNodes; UnresolvedTypeNode[] typeNodes;
var bindings = new ArrayList<String>();
if (parameter instanceof TypedIdentifier par) { if (parameter instanceof TypedIdentifier par) {
typeNodes = new UnresolvedTypeNode[] {visitTypeAnnotation(par.getTypeAnnotation())}; typeNodes = new UnresolvedTypeNode[] {visitTypeAnnotation(par.getTypeAnnotation())};
frameBuilder.addSlot( frameBuilder.addSlot(
FrameSlotKind.Illegal, toIdentifier(par.getIdentifier().getValue()), null); FrameSlotKind.Illegal, toIdentifier(par.getIdentifier().getValue()), null);
bindings.add(par.getIdentifier().getValue());
} else { } else {
typeNodes = new UnresolvedTypeNode[0]; typeNodes = new UnresolvedTypeNode[0];
} }
@@ -994,6 +1129,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
UnresolvedFunctionNode functionNode = UnresolvedFunctionNode functionNode =
symbolTable.enterLambda( symbolTable.enterLambda(
bindings,
frameBuilder, frameBuilder,
scope -> { scope -> {
var expr = visitExpr(letExpr.getExpr()); var expr = visitExpr(letExpr.getExpr());
@@ -1025,9 +1161,12 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
.build(); .build();
} }
var bindings = getParameterNames(params);
var isCustomThisScope = symbolTable.getCurrentScope().isCustomThisScope(); var isCustomThisScope = symbolTable.getCurrentScope().isCustomThisScope();
return symbolTable.enterLambda( return symbolTable.enterLambda(
bindings,
descriptorBuilder, descriptorBuilder,
scope -> { scope -> {
var exprNode = visitExpr(expr.getExpr()); var exprNode = visitExpr(expr.getExpr());
@@ -1168,7 +1307,9 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
@Override @Override
public GeneratorMemberNode visitMemberPredicate(MemberPredicate ctx) { 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 = var member =
doVisitObjectEntryBody(createSourceSection(ctx), keyNode, ctx.getExpr(), ctx.getBodyList()); doVisitObjectEntryBody(createSourceSection(ctx), keyNode, ctx.getExpr(), ctx.getBodyList());
var isFrameStored = var isFrameStored =
@@ -1197,7 +1338,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
@Override @Override
public GeneratorMemberNode visitObjectSpread(ObjectSpread member) { 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()); return GeneratorSpreadNodeGen.create(createSourceSection(member), expr, member.isNullable());
} }
@@ -1210,8 +1351,10 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
? new GeneratorMemberNode[0] ? new GeneratorMemberNode[0]
: doVisitForWhenBody(member.getElseClause()); : doVisitForWhenBody(member.getElseClause());
return new GeneratorWhenNode( // when predicates cannot see their direct scope
sourceSection, visitExpr(member.getPredicate()), thenNodes, elseNodes); var predicateNode =
symbolTable.enterEagerGenerator((scope) -> visitExpr(member.getPredicate()));
return new GeneratorWhenNode(sourceSection, predicateNode, thenNodes, elseNodes);
} }
private GeneratorMemberNode[] doVisitForWhenBody(ObjectBody body) { private GeneratorMemberNode[] doVisitForWhenBody(ObjectBody body) {
@@ -1233,6 +1376,16 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
TypedIdentifier valueTypedIdentifier = null; TypedIdentifier valueTypedIdentifier = null;
if (valueParameter instanceof TypedIdentifier ti) valueTypedIdentifier = ti; 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 = var keyIdentifier =
keyTypedIdentifier == null keyTypedIdentifier == null
? null ? null
@@ -1249,16 +1402,13 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
} }
var currentScope = symbolTable.getCurrentScope(); var currentScope = symbolTable.getCurrentScope();
var generatorDescriptorBuilder = currentScope.newFrameDescriptorBuilder(); var generatorDescriptorBuilder = currentScope.newFrameDescriptorBuilder();
var memberDescriptorBuilder = currentScope.newForGeneratorMemberDescriptorBuilder();
var keySlot = -1; var keySlot = -1;
var valueSlot = -1; var valueSlot = -1;
if (keyIdentifier != null) { if (keyIdentifier != null) {
keySlot = generatorDescriptorBuilder.addSlot(FrameSlotKind.Illegal, keyIdentifier, null); keySlot = generatorDescriptorBuilder.addSlot(FrameSlotKind.Illegal, keyIdentifier, null);
memberDescriptorBuilder.addSlot(FrameSlotKind.Illegal, keyIdentifier, null);
} }
if (valueIdentifier != null) { if (valueIdentifier != null) {
valueSlot = generatorDescriptorBuilder.addSlot(FrameSlotKind.Illegal, valueIdentifier, null); valueSlot = generatorDescriptorBuilder.addSlot(FrameSlotKind.Illegal, valueIdentifier, null);
memberDescriptorBuilder.addSlot(FrameSlotKind.Illegal, valueIdentifier, null);
} }
var unresolvedKeyTypeNode = var unresolvedKeyTypeNode =
keyTypedIdentifier == null keyTypedIdentifier == null
@@ -1280,12 +1430,10 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
? new TypeNode.UnknownTypeNode(VmUtils.unavailableSourceSection()) ? new TypeNode.UnknownTypeNode(VmUtils.unavailableSourceSection())
.initWriteSlotNode(valueSlot) .initWriteSlotNode(valueSlot)
: null; : null;
var iterableNode = visitExpr(ctx.getExpr()); var iterableNode = symbolTable.enterEagerGenerator(scope -> visitExpr(ctx.getExpr()));
var memberNodes = var memberNodes =
symbolTable.enterForGenerator( symbolTable.enterForGenerator(
generatorDescriptorBuilder, params, generatorDescriptorBuilder, scope -> doVisitForWhenBody(ctx.getBody()));
memberDescriptorBuilder,
scope -> doVisitForWhenBody(ctx.getBody()));
return GeneratorForNodeGen.create( return GeneratorForNodeGen.create(
createSourceSection(ctx), createSourceSection(ctx),
generatorDescriptorBuilder.build(), generatorDescriptorBuilder.build(),
@@ -1300,12 +1448,6 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
@Override @Override
public PklRootNode visitModule(Module mod) { public PklRootNode visitModule(Module mod) {
var moduleDecl = mod.getDecl(); var moduleDecl = mod.getDecl();
var annotationNodes =
moduleDecl != null
? doVisitAnnotations(moduleDecl.getAnnotations())
: new ExpressionNode[] {};
int modifiers; int modifiers;
if (moduleDecl == null) { if (moduleDecl == null) {
modifiers = VmModifier.NONE; 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 extendsOrAmendsClause = moduleDecl != null ? moduleDecl.getExtendsOrAmendsDecl() : null;
var supermoduleNode = var supermoduleNode =
@@ -1344,7 +1503,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
new UnresolvedTypeNode.Declared(supermoduleNode.getSourceSection(), supermoduleNode); new UnresolvedTypeNode.Declared(supermoduleNode.getSourceSection(), supermoduleNode);
var moduleProperties = var moduleProperties =
doVisitModuleProperties( doVisitModuleProperties(
mod.getImports(), importMembers,
mod.getClasses(), mod.getClasses(),
mod.getTypeAliases(), mod.getTypeAliases(),
List.of(), List.of(),
@@ -1374,7 +1533,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
var moduleProperties = var moduleProperties =
doVisitModuleProperties( doVisitModuleProperties(
mod.getImports(), importMembers,
mod.getClasses(), mod.getClasses(),
mod.getTypeAliases(), mod.getTypeAliases(),
mod.getProperties(), mod.getProperties(),
@@ -1411,18 +1570,17 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
} }
private EconomicMap<Object, ObjectMember> doVisitModuleProperties( private EconomicMap<Object, ObjectMember> doVisitModuleProperties(
List<ImportClause> imports, ObjectMember[] imports,
List<Class> classes, List<Class> classes,
List<TypeAlias> typeAliases, List<TypeAlias> typeAliases,
List<ClassProperty> properties, List<ClassProperty> properties,
Set<String> propertyNames, Set<String> propertyNames,
ModuleInfo moduleInfo) { 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); var result = EconomicMaps.<Object, ObjectMember>create(totalSize);
for (var _import : imports) { for (var member : imports) {
var member = visitImportClause(_import);
checkDuplicateMember(member.getName(), member.getHeaderSection(), propertyNames); checkDuplicateMember(member.getName(), member.getHeaderSection(), propertyNames);
EconomicMaps.put(result, member.getName(), member); EconomicMaps.put(result, member.getName(), member);
} }
@@ -1492,10 +1650,8 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
importName, importName,
ConstLevel.NONE, ConstLevel.NONE,
scope -> { scope -> {
var modifiers = VmModifier.IMPORT | VmModifier.LOCAL | VmModifier.CONST; var baseModifiers = VmModifier.IMPORT | VmModifier.LOCAL | VmModifier.CONST;
if (imp.isGlob()) { var modifiers = imp.isGlob() ? baseModifiers | VmModifier.GLOB : baseModifiers;
modifiers = modifiers | VmModifier.GLOB;
}
var result = var result =
new ObjectMember( new ObjectMember(
importNode.getSourceSection(), 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 @Override
public ObjectMember visitClass(Class clazz) { public ObjectMember visitClass(Class clazz) {
var sourceSection = createSourceSection(clazz); var sourceSection = createSourceSection(clazz);
var headerSection = createSourceSection(clazz.getHeaderSpan()); var headerSection = createSourceSection(clazz.getHeaderSpan());
var bodyNode = clazz.getBody();
var typeParameters = visitTypeParameterList(clazz.getTypeParameterList()); 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 = var modifiers =
doVisitModifiers( doVisitModifiers(
clazz.getModifiers(), VmModifier.VALID_CLASS_MODIFIERS, "invalidClassModifier") clazz.getModifiers(), VmModifier.VALID_CLASS_MODIFIERS, "invalidClassModifier")
| VmModifier.CLASS; | VmModifier.CLASS;
var isLocalClass = VmModifier.isLocal(modifiers);
var className = var className =
org.pkl.core.runtime.Identifier.property( org.pkl.core.runtime.Identifier.property(clazz.getName().getValue(), isLocalClass);
clazz.getName().getValue(), VmModifier.isLocal(modifiers));
var annotations = doVisitAnnotations(clazz.getAnnotations(), className);
return symbolTable.enterClass( return symbolTable.enterClass(
className, className,
modifiers,
typeParameters, typeParameters,
scope -> { 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(); var supertypeCtx = clazz.getSuperClass();
// needs to be inside `enterClass` so that class' type parameters are in scope // needs to be inside `enterClass` so that class' type parameters are in scope
@@ -1572,7 +1749,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
sourceSection, sourceSection,
headerSection, headerSection,
createDocSourceSection(clazz.getDocComment()), createDocSourceSection(clazz.getDocComment()),
doVisitAnnotations(clazz.getAnnotations()), annotations,
modifiers, modifiers,
classInfo, classInfo,
typeParameters, typeParameters,
@@ -1582,13 +1759,13 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
doVisitClassProperties(properties, propertyNames), doVisitClassProperties(properties, propertyNames),
doVisitMethodDefs(methods)); doVisitMethodDefs(methods));
var isLocal = VmModifier.isLocal(modifiers);
var result = var result =
new ObjectMember( new ObjectMember(
sourceSection, sourceSection,
headerSection, headerSection,
isLocal ? VmModifier.LOCAL_CLASS_OBJECT_MEMBER : VmModifier.CLASS_OBJECT_MEMBER, isLocalClass
? VmModifier.LOCAL_CLASS_OBJECT_MEMBER
: VmModifier.CLASS_OBJECT_MEMBER,
scope.getName(), scope.getName(),
scope.getQualifiedName()); scope.getQualifiedName());
@@ -1611,6 +1788,10 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
@Override @Override
public Integer visitModifier(Modifier modifier) { public Integer visitModifier(Modifier modifier) {
return toModifier(modifier);
}
private int toModifier(Modifier modifier) {
return switch (modifier.getValue()) { return switch (modifier.getValue()) {
case EXTERNAL -> VmModifier.EXTERNAL; case EXTERNAL -> VmModifier.EXTERNAL;
case ABSTRACT -> VmModifier.ABSTRACT; case ABSTRACT -> VmModifier.ABSTRACT;
@@ -1658,7 +1839,6 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
var expr = entry.getExpr(); var expr = entry.getExpr();
var objectBodies = entry.getBodyList(); var objectBodies = entry.getBodyList();
var docComment = createDocSourceSection(docCom); var docComment = createDocSourceSection(docCom);
var annotationNodes = doVisitAnnotations(annotations);
var sourceSection = createSourceSection(entry); var sourceSection = createSourceSection(entry);
var headerStart = !modifierList.isEmpty() ? modifierList.get(0).span() : name.span(); var headerStart = !modifierList.isEmpty() ? modifierList.get(0).span() : name.span();
var headerEnd = typeAnnotation != null ? typeAnnotation.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 isLocal = VmModifier.isLocal(modifiers);
var propertyName = org.pkl.core.runtime.Identifier.property(name.getValue(), isLocal); var propertyName = org.pkl.core.runtime.Identifier.property(name.getValue(), isLocal);
var annotationNodes = doVisitAnnotations(annotations, propertyName);
return symbolTable.enterProperty( return symbolTable.enterProperty(
propertyName, propertyName,
@@ -1760,9 +1941,14 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
var descriptorBuilder = createFrameDescriptorBuilder(paramListCtx); var descriptorBuilder = createFrameDescriptorBuilder(paramListCtx);
var paramCount = paramListCtx.getParameters().size(); var paramCount = paramListCtx.getParameters().size();
var bindings = getParameterNames(paramListCtx);
var annotations = doVisitAnnotations(entry.getAnnotations(), methodName);
return symbolTable.enterMethod( return symbolTable.enterMethod(
methodName, methodName,
getConstLevel(modifiers), getConstLevel(modifiers),
bindings,
descriptorBuilder, descriptorBuilder,
typeParameters, typeParameters,
scope -> { scope -> {
@@ -1806,7 +1992,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
headerSection, headerSection,
scope.buildFrameDescriptor(), scope.buildFrameDescriptor(),
createDocSourceSection(entry.getDocComment()), createDocSourceSection(entry.getDocComment()),
doVisitAnnotations(entry.getAnnotations()), annotations,
modifiers, modifiers,
methodName, methodName,
scope.getQualifiedName(), 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 name = org.pkl.core.runtime.Identifier.property(typeAlias.getName().getValue(), isLocal);
var typeParameters = visitTypeParameterList(typeAlias.getTypeParameterList()); 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( return symbolTable.enterTypeAlias(
name, name,
@@ -1846,7 +2036,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
sourceSection, sourceSection,
headerSection, headerSection,
createDocSourceSection(typeAlias.getDocComment()), createDocSourceSection(typeAlias.getDocComment()),
doVisitAnnotations(typeAlias.getAnnotations()), annotations,
modifiers, modifiers,
scopeName.toString(), scopeName.toString(),
scope.getQualifiedName(), scope.getQualifiedName(),
@@ -1857,9 +2047,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
new ObjectMember( new ObjectMember(
sourceSection, sourceSection,
headerSection, headerSection,
isLocal objectMemberModifiers,
? VmModifier.LOCAL_TYPEALIAS_OBJECT_MEMBER
: VmModifier.TYPEALIAS_OBJECT_MEMBER,
scopeName, scopeName,
scope.getQualifiedName()); scope.getQualifiedName());
@@ -1871,8 +2059,8 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
}); });
} }
@Override public ExpressionNode visitAnnotation(
public ExpressionNode visitAnnotation(Annotation annotation) { Annotation annotation, org.pkl.core.runtime.@Nullable Identifier annotatedMemberName) {
var verifyNode = new CheckIsAnnotationClassNode(visitType(annotation.getType())); var verifyNode = new CheckIsAnnotationClassNode(visitType(annotation.getType()));
var bodyCtx = annotation.getBody(); var bodyCtx = annotation.getBody();
@@ -1890,13 +2078,16 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
verifyNode); 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()]; var nodes = new ExpressionNode[annotations.size()];
for (var i = 0; i < nodes.length; i++) { for (var i = 0; i < nodes.length; i++) {
nodes[i] = visitAnnotation(annotations.get(i)); nodes[i] = visitAnnotation(annotations.get(i), annotatedMemberName);
} }
return nodes; return nodes;
} }
@@ -2004,9 +2195,33 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
return parentNode; 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) { private ExpressionNode doVisitObjectBody(ObjectBody body, ExpressionNode parentNode) {
return symbolTable.enterObjectScope( return symbolTable.enterObjectScope(
body,
(scope) -> { (scope) -> {
addObjectNamesToScope(scope, body);
var objectMembers = body.getMembers(); var objectMembers = body.getMembers();
if (objectMembers.isEmpty()) { if (objectMembers.isEmpty()) {
return EmptyObjectLiteralNodeGen.create( return EmptyObjectLiteralNodeGen.create(
@@ -2290,8 +2505,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
} }
private Pair<ExpressionNode, ObjectMember> doVisitObjectEntry(ObjectEntry entry) { private Pair<ExpressionNode, ObjectMember> doVisitObjectEntry(ObjectEntry entry) {
var keyNode = visitExpr(entry.getKey()); var keyNode = symbolTable.enterEagerGenerator((scp) -> visitExpr(entry.getKey()));
var member = var member =
doVisitObjectEntryBody( doVisitObjectEntryBody(
createSourceSection(entry), keyNode, entry.getValue(), entry.getBodyList()); createSourceSection(entry), keyNode, entry.getValue(), entry.getBodyList());
@@ -2371,9 +2585,12 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
var frameDescriptorBuilder = createFrameDescriptorBuilder(paramList); var frameDescriptorBuilder = createFrameDescriptorBuilder(paramList);
var bindings = getParameterNames(paramList);
return symbolTable.enterMethod( return symbolTable.enterMethod(
methodName, methodName,
getConstLevel(modifiers), getConstLevel(modifiers),
bindings,
frameDescriptorBuilder, frameDescriptorBuilder,
List.of(), List.of(),
scope -> { scope -> {
@@ -2590,7 +2807,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
return doVisitParameterTypes(paramList.getParameters()); 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()]; var typeNodes = new UnresolvedTypeNode[params.size()];
for (int i = 0; i < typeNodes.length; i++) { for (int i = 0; i < typeNodes.length; i++) {
if (params.get(i) instanceof TypedIdentifier typedIdentifier) { if (params.get(i) instanceof TypedIdentifier typedIdentifier) {
@@ -2683,8 +2900,17 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
return needsConst; return needsConst;
} }
private FrameDescriptor.Builder createFrameDescriptorBuilder(ParameterList params) { private static List<String> getParameterNames(ParameterList parameterList) {
var builder = FrameDescriptor.newBuilder(params.getParameters().size()); 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()) { for (var param : params.getParameters()) {
org.pkl.core.runtime.Identifier identifier = null; org.pkl.core.runtime.Identifier identifier = null;
if (param instanceof TypedIdentifier typedIdentifier) { if (param instanceof TypedIdentifier typedIdentifier) {
@@ -2757,18 +2983,6 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
return org.pkl.core.runtime.Identifier.get(text); 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) { private URI resolveImport(String importUri, StringConstant ctx) {
URI parsedUri; URI parsedUri;
try { try {
@@ -2850,4 +3064,47 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
private static SourceSection unavailableSourceSection() { private static SourceSection unavailableSourceSection() {
return VmUtils.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; package org.pkl.core.ast.builder;
import com.oracle.truffle.api.frame.FrameDescriptor; import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.FrameDescriptor.Builder;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.pkl.core.TypeParameter; import org.pkl.core.TypeParameter;
import org.pkl.core.ast.ConstantNode; import org.pkl.core.ast.ConstantNode;
import org.pkl.core.ast.ExpressionNode; 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.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.Identifier;
import org.pkl.core.runtime.ModuleInfo; import org.pkl.core.runtime.ModuleInfo;
import org.pkl.core.runtime.VmDataSize; import org.pkl.core.runtime.VmDataSize;
import org.pkl.core.runtime.VmDuration; import org.pkl.core.runtime.VmDuration;
import org.pkl.core.util.LateInit;
import org.pkl.parser.Lexer; import org.pkl.parser.Lexer;
import org.pkl.parser.syntax.ObjectBody;
public final class SymbolTable { public final class SymbolTable {
private Scope currentScope; private Scope currentScope;
public SymbolTable(ModuleInfo moduleInfo) { public SymbolTable(ModuleInfo moduleInfo, boolean isBaseModule) {
currentScope = new ModuleScope(moduleInfo); currentScope = new ModuleScope(moduleInfo, isBaseModule);
} }
public Scope getCurrentScope() { public Scope getCurrentScope() {
@@ -53,6 +62,7 @@ public final class SymbolTable {
public ObjectMember enterClass( public ObjectMember enterClass(
Identifier name, Identifier name,
int modifiers,
List<TypeParameter> typeParameters, List<TypeParameter> typeParameters,
Function<ClassScope, ObjectMember> nodeFactory) { Function<ClassScope, ObjectMember> nodeFactory) {
return doEnter( return doEnter(
@@ -60,7 +70,8 @@ public final class SymbolTable {
currentScope, currentScope,
name, name,
toQualifiedName(name), toQualifiedName(name),
FrameDescriptor.newBuilder(), modifiers,
new FrameDescriptorBuilder(),
typeParameters), typeParameters),
nodeFactory); nodeFactory);
} }
@@ -74,7 +85,7 @@ public final class SymbolTable {
currentScope, currentScope,
name, name,
toQualifiedName(name), toQualifiedName(name),
FrameDescriptor.newBuilder(), new FrameDescriptorBuilder(),
typeParameters), typeParameters),
nodeFactory); nodeFactory);
} }
@@ -82,7 +93,8 @@ public final class SymbolTable {
public <T> T enterMethod( public <T> T enterMethod(
Identifier name, Identifier name,
ConstLevel constLevel, ConstLevel constLevel,
Builder frameDescriptorBuilder, List<String> bindings,
FrameDescriptorBuilder frameDescriptorBuilder,
List<TypeParameter> typeParameters, List<TypeParameter> typeParameters,
Function<MethodScope, T> nodeFactory) { Function<MethodScope, T> nodeFactory) {
return doEnter( return doEnter(
@@ -91,26 +103,30 @@ public final class SymbolTable {
name, name,
toQualifiedName(name), toQualifiedName(name),
constLevel, constLevel,
bindings,
frameDescriptorBuilder, frameDescriptorBuilder,
typeParameters), typeParameters),
nodeFactory); nodeFactory);
} }
public <T> T enterEagerGenerator(Function<EagerGeneratorScope, T> nodeFactory) {
return doEnter(new EagerGeneratorScope(currentScope, currentScope.qualifiedName), nodeFactory);
}
public <T> T enterForGenerator( public <T> T enterForGenerator(
FrameDescriptor.Builder frameDescriptorBuilder, List<String> params,
FrameDescriptor.Builder memberDescriptorBuilder, FrameDescriptorBuilder frameDescriptorBuilder,
Function<ForGeneratorScope, T> nodeFactory) { Function<ForGeneratorScope, T> nodeFactory) {
return doEnter( return doEnter(
new ForGeneratorScope( new ForGeneratorScope(
currentScope, currentScope, currentScope.qualifiedName, params, frameDescriptorBuilder),
currentScope.qualifiedName,
frameDescriptorBuilder,
memberDescriptorBuilder),
nodeFactory); nodeFactory);
} }
public <T> T enterLambda( 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 // flatten names of lambdas nested inside other lambdas for presentation purposes
var parentScope = currentScope; var parentScope = currentScope;
@@ -122,14 +138,15 @@ public final class SymbolTable {
var qualifiedName = parentScope.qualifiedName + "." + parentScope.getNextLambdaName(); var qualifiedName = parentScope.qualifiedName + "." + parentScope.getNextLambdaName();
return doEnter( return doEnter(
new LambdaScope(currentScope, qualifiedName, frameDescriptorBuilder), nodeFactory); new LambdaScope(currentScope, bindings, qualifiedName, frameDescriptorBuilder),
nodeFactory);
} }
public <T> T enterProperty( public <T> T enterProperty(
Identifier name, ConstLevel constLevel, Function<PropertyScope, T> nodeFactory) { Identifier name, ConstLevel constLevel, Function<PropertyScope, T> nodeFactory) {
return doEnter( return doEnter(
new PropertyScope( new PropertyScope(
currentScope, name, toQualifiedName(name), constLevel, FrameDescriptor.newBuilder()), currentScope, name, toQualifiedName(name), constLevel, new FrameDescriptorBuilder()),
nodeFactory); nodeFactory);
} }
@@ -140,8 +157,8 @@ public final class SymbolTable {
var qualifiedName = currentScope.getQualifiedName() + currentScope.getNextEntryName(keyNode); var qualifiedName = currentScope.getQualifiedName() + currentScope.getNextEntryName(keyNode);
var builder = var builder =
currentScope instanceof ForGeneratorScope forScope currentScope instanceof ForGeneratorScope forScope
? forScope.memberDescriptorBuilder ? forScope.frameDescriptorBuilder
: FrameDescriptor.newBuilder(); : new FrameDescriptorBuilder();
return doEnter(new EntryScope(currentScope, qualifiedName, builder), nodeFactory); return doEnter(new EntryScope(currentScope, qualifiedName, builder), nodeFactory);
} }
@@ -150,13 +167,20 @@ public final class SymbolTable {
new CustomThisScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory); 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( return doEnter(
new AnnotationScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory); new AnnotationScope(currentScope, qualifiedName, currentScope.frameDescriptorBuilder),
nodeFactory);
} }
public <T> T enterObjectScope(Function<ObjectScope, T> nodeFactory) { public <T> T enterObjectScope(ObjectBody body, Function<ObjectScope, T> nodeFactory) {
return doEnter(new ObjectScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory); return doEnter(
new ObjectScope(currentScope, body, currentScope.frameDescriptorBuilder), nodeFactory);
} }
private <T, S extends Scope> T doEnter(S scope, Function<S, T> 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()); return currentScope.qualifiedName + separator + Lexer.maybeQuoteIdentifier(name.toString());
} }
public record Member(Identifier name, int modifiers) {}
public abstract static class Scope { public abstract static class Scope {
private final @Nullable Scope parent; private final @Nullable Scope parent;
private final @Nullable Identifier name; private final @Nullable Identifier name;
private final String qualifiedName; private final String qualifiedName;
private int lambdaCount = 0; private int lambdaCount = 0;
private int entryCount = 0; private int entryCount = 0;
private final FrameDescriptor.Builder frameDescriptorBuilder; protected final FrameDescriptorBuilder frameDescriptorBuilder;
private final ConstLevel constLevel; 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( private Scope(
@Nullable Scope parent, @Nullable Scope parent,
@Nullable Identifier name, @Nullable Identifier name,
String qualifiedName, String qualifiedName,
ConstLevel constLevel, ConstLevel constLevel,
FrameDescriptor.Builder frameDescriptorBuilder) { FrameDescriptorBuilder frameDescriptorBuilder) {
this.parent = parent; this.parent = parent;
this.name = name; this.name = name;
this.qualifiedName = qualifiedName; this.qualifiedName = qualifiedName;
this.frameDescriptorBuilder = frameDescriptorBuilder; this.frameDescriptorBuilder = frameDescriptorBuilder;
if (parent != null) {
this.isBaseModule = parent.isBaseModule;
}
// const level can never decrease // const level can never decrease
this.constLevel = this.constLevel =
parent != null && parent.constLevel.biggerOrEquals(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 * Returns a new descriptor builder that contains the same slots as the current scope's frame
* descriptor. * descriptor.
*/ */
public FrameDescriptor.Builder newFrameDescriptorBuilder() { public FrameDescriptorBuilder newFrameDescriptorBuilder() {
return new FrameDescriptorBuilder(buildFrameDescriptor()); 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) { public @Nullable TypeParameter getTypeParameter(String name) {
return null; return null;
} }
@@ -276,7 +294,10 @@ public final class SymbolTable {
var depth = -1; var depth = -1;
var lexicalScope = getLexicalScope(); var lexicalScope = getLexicalScope();
while (lexicalScope.getConstLevel() == ConstLevel.ALL) { while (lexicalScope.getConstLevel() == ConstLevel.ALL) {
// LambdaScope inherits constLevel but doesn't create a const scope barrier
if (!(lexicalScope instanceof LambdaScope)) {
depth += 1; depth += 1;
}
var parent = lexicalScope.getParent(); var parent = lexicalScope.getParent();
if (parent == null) { if (parent == null) {
return depth; return depth;
@@ -354,18 +375,161 @@ public final class SymbolTable {
public ConstLevel getConstLevel() { public ConstLevel getConstLevel() {
return constLevel; 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 { 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( super(
parent, parent,
parent.getNameOrNull(), parent.getNameOrNull(),
parent.getQualifiedName(), parent.getQualifiedName(),
ConstLevel.NONE, ConstLevel.NONE,
frameDescriptorBuilder); 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, Identifier name,
String qualifiedName, String qualifiedName,
ConstLevel constLevel, ConstLevel constLevel,
Builder frameDescriptorBuilder, FrameDescriptorBuilder frameDescriptorBuilder,
List<TypeParameter> typeParameters) { List<TypeParameter> typeParameters) {
super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder); super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder);
this.typeParameters = typeParameters; this.typeParameters = typeParameters;
@@ -393,47 +557,112 @@ public final class SymbolTable {
} }
public static final class ModuleScope extends Scope implements LexicalScope { public static final class ModuleScope extends Scope implements LexicalScope {
private final ModuleInfo moduleInfo; private final ModuleInfo moduleInfo;
@LateInit private boolean isClosed;
private final boolean isAmend;
public ModuleScope(ModuleInfo moduleInfo) { public ModuleScope(ModuleInfo moduleInfo, boolean isBaseModule) {
super(null, null, moduleInfo.getModuleName(), ConstLevel.NONE, FrameDescriptor.newBuilder()); super(null, null, moduleInfo.getModuleName(), ConstLevel.NONE, new FrameDescriptorBuilder());
this.isBaseModule = isBaseModule;
this.moduleInfo = moduleInfo; 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( public MethodScope(
Scope parent, Scope parent,
Identifier name, Identifier name,
String qualifiedName, String qualifiedName,
ConstLevel constLevel, ConstLevel constLevel,
Builder frameDescriptorBuilder, List<String> bindings,
FrameDescriptorBuilder frameDescriptorBuilder,
List<TypeParameter> typeParameters) { List<TypeParameter> typeParameters) {
super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder, 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 { public static final class LambdaScope extends Scope implements LexicalScope {
private final List<String> bindings;
public LambdaScope( public LambdaScope(
Scope parent, String qualifiedName, FrameDescriptor.Builder frameDescriptorBuilder) { Scope parent,
super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder); 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 { public static final class ForGeneratorScope extends Scope implements LexicalScope {
private final FrameDescriptor.Builder memberDescriptorBuilder; final List<String> params;
public ForGeneratorScope( public ForGeneratorScope(
Scope parent, Scope parent,
String qualifiedName, String qualifiedName,
FrameDescriptor.Builder frameDescriptorBuilder, List<String> params,
FrameDescriptor.Builder memberDescriptorBuilder) { FrameDescriptorBuilder frameDescriptorBuilder) {
super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder); super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder);
this.memberDescriptorBuilder = memberDescriptorBuilder; this.params = params;
}
public FrameDescriptor buildMemberDescriptor() {
return memberDescriptorBuilder.build();
} }
@Override @Override
@@ -442,6 +671,23 @@ public final class SymbolTable {
assert parent != null; assert parent != null;
return parent.getNextEntryName(keyNode); 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 { public static final class PropertyScope extends Scope {
@@ -450,26 +696,46 @@ public final class SymbolTable {
Identifier name, Identifier name,
String qualifiedName, String qualifiedName,
ConstLevel constLevel, ConstLevel constLevel,
FrameDescriptor.Builder frameDescriptorBuilder) { FrameDescriptorBuilder frameDescriptorBuilder) {
super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder); super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder);
} }
} }
public static final class EntryScope extends Scope { public static final class EntryScope extends Scope {
public EntryScope( public EntryScope(
Scope parent, String qualifiedName, FrameDescriptor.Builder frameDescriptorBuilder) { Scope parent, String qualifiedName, FrameDescriptorBuilder frameDescriptorBuilder) {
super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder); super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder);
} }
} }
public static final class ClassScope extends TypeParameterizableScope implements LexicalScope { public static final class ClassScope extends TypeParameterizableScope implements LexicalScope {
private final boolean isClosed;
public ClassScope( public ClassScope(
Scope parent, Scope parent,
Identifier name, Identifier name,
String qualifiedName, String qualifiedName,
Builder frameDescriptorBuilder, int modifiers,
FrameDescriptorBuilder frameDescriptorBuilder,
List<TypeParameter> typeParameters) { List<TypeParameter> typeParameters) {
super(parent, name, qualifiedName, ConstLevel.MODULE, frameDescriptorBuilder, 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, Scope parent,
Identifier name, Identifier name,
String qualifiedName, String qualifiedName,
FrameDescriptor.Builder frameDescriptorBuilder, FrameDescriptorBuilder frameDescriptorBuilder,
List<TypeParameter> typeParameters) { List<TypeParameter> typeParameters) {
super(parent, name, qualifiedName, ConstLevel.MODULE, frameDescriptorBuilder, 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( super(
parent, parent,
parent.getNameOrNull(), parent.getNameOrNull(),
@@ -509,14 +775,23 @@ public final class SymbolTable {
} }
} }
public static final class AnnotationScope extends Scope implements LexicalScope { public static final class AnnotationScope extends Scope {
public AnnotationScope(Scope parent, FrameDescriptor.Builder frameDescriptorBuilder) { public AnnotationScope(
Scope parent, String qualifiedName, FrameDescriptorBuilder frameDescriptorBuilder) {
super( super(
parent, parent, parent.getNameOrNull(), qualifiedName, ConstLevel.MODULE, frameDescriptorBuilder);
parent.getNameOrNull(),
parent.getQualifiedName(),
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 @Override
public Object executeGeneric(VirtualFrame frame) { public Object executeGeneric(VirtualFrame frame) {
var generatorFrame = ObjectData.getGeneratorFrame(frame); var generatorFrame = ObjectData.getGeneratorFrame(frame);
var numSlots = frame.getFrameDescriptor().getNumberOfSlots(); // copying all slots includes function arguments, but the capture generator frame
// This value is constant and could be a constructor argument. // and the host frame are guaranteed to have the same arguments and number of slots
var startSlot = generatorFrame.getFrameDescriptor().getNumberOfSlots() - numSlots; // (guaranteed by AstBuilder).
assert startSlot >= 0; assert frame.getFrameDescriptor().getNumberOfSlots()
// Copy locals that are for-generator variables into this frame. == generatorFrame.getFrameDescriptor().getNumberOfSlots();
// Slots before `startSlot` (if any) are function arguments VmUtils.copyLocals(generatorFrame, 0, frame, 0, frame.getFrameDescriptor().getNumberOfSlots());
// and must not be copied to preserve scoping rules.
VmUtils.copyLocals(generatorFrame, startSlot, frame, 0, numSlots);
return child.executeGeneric(frame); 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.PklNode;
import org.pkl.core.ast.PklRootNode; import org.pkl.core.ast.PklRootNode;
import org.pkl.core.ast.SimpleRootNode; 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.FunctionNode;
import org.pkl.core.ast.member.Lambda; import org.pkl.core.ast.member.Lambda;
import org.pkl.core.ast.type.TypeNode; import org.pkl.core.ast.type.TypeNode;
@@ -37,11 +37,13 @@ public final class AmendFunctionNode extends PklNode {
private final boolean isCustomThisScope; private final boolean isCustomThisScope;
private final PklRootNode initialFunctionRootNode; private final PklRootNode initialFunctionRootNode;
@CompilationFinal private int customThisSlot = -1; @CompilationFinal private int customThisSlot = -1;
private final boolean hasObjectParams;
public AmendFunctionNode(ObjectLiteralNode hostNode, TypeNode[] parameterTypeNodes) { public AmendFunctionNode(ObjectLiteralNode hostNode, TypeNode[] parameterTypeNodes) {
super(hostNode.getSourceSection()); super(hostNode.getSourceSection());
isCustomThisScope = hostNode.isCustomThisScope; isCustomThisScope = hostNode.isCustomThisScope;
hasObjectParams = parameterTypeNodes.length > 0;
var builder = FrameDescriptor.newBuilder(); var builder = FrameDescriptor.newBuilder();
var hostDescriptor = hostNode.parametersDescriptor; var hostDescriptor = hostNode.parametersDescriptor;
@@ -68,7 +70,7 @@ public final class AmendFunctionNode extends PklNode {
new AmendFunctionBodyNode( new AmendFunctionBodyNode(
sourceSection, sourceSection,
hostNode.copy( hostNode.copy(
ReadFrameSlotNodeGen.create( ReadExactFrameSlotNodeGen.create(
hostNode.getParentNode().getSourceSection(), objectToAmendSlot)), hostNode.getParentNode().getSourceSection(), objectToAmendSlot)),
parameterSlots, parameterSlots,
objectToAmendSlot, objectToAmendSlot,
@@ -88,7 +90,7 @@ public final class AmendFunctionNode extends PklNode {
new AmendFunctionBodyNode( new AmendFunctionBodyNode(
sourceSection, sourceSection,
hostNode.copy( hostNode.copy(
ReadFrameSlotNodeGen.create( ReadExactFrameSlotNodeGen.create(
hostNode.getParentNode().getSourceSection(), objectToAmendSlot)), hostNode.getParentNode().getSourceSection(), objectToAmendSlot)),
parameterSlots, parameterSlots,
objectToAmendSlot, objectToAmendSlot,
@@ -108,7 +110,9 @@ public final class AmendFunctionNode extends PklNode {
isCustomThisScope ? frame.getAuxiliarySlot(customThisSlot) : VmUtils.getReceiver(frame), isCustomThisScope ? frame.getAuxiliarySlot(customThisSlot) : VmUtils.getReceiver(frame),
functionToAmend.getParameterCount(), functionToAmend.getParameterCount(),
initialFunctionRootNode, initialFunctionRootNode,
new Context(functionToAmend, null)); new Context(functionToAmend, null),
true,
hasObjectParams);
} }
private static class AmendFunctionBodyNode extends ExpressionNode { 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.ast.member.ClassMethod;
import org.pkl.core.runtime.VmObjectLike; 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 { public final class InvokeMethodDirectNode extends ExpressionNode {
private final VmObjectLike owner; private final VmObjectLike owner;
@Child private ExpressionNode receiverNode; @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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.pkl.core.ast.expression.member;
import com.oracle.truffle.api.CompilerAsserts; 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.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.DirectCallNode; import com.oracle.truffle.api.nodes.DirectCallNode;
import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.source.SourceSection; import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.PklBugException;
import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmObjectLike; import org.pkl.core.runtime.VmObjectLike;
import org.pkl.core.runtime.VmUtils; import org.pkl.core.runtime.VmUtils;
/** Reads a local non-constant property that is known to exist in the lexical scope of this node. */ /** Reads a local non-constant property that is known to exist in the lexical scope of this node. */
public final class ReadLocalPropertyNode extends ExpressionNode { public final class ReadLocalPropertyNode extends ExpressionNode {
private final ObjectMember property; private final Identifier name;
private final int levelsUp; private final int levelsUp;
private final boolean needsConst;
@Child private DirectCallNode callNode; @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); super(sourceSection);
CompilerAsserts.neverPartOfCompilation(); CompilerAsserts.neverPartOfCompilation();
this.property = property; this.name = name;
this.levelsUp = levelsUp; this.levelsUp = levelsUp;
this.needsConst = needsConst;
assert property.getNameOrNull() != null;
assert property.getConstantValue() == null : "Use a ConstantNode instead.";
callNode = DirectCallNode.create(property.getCallTarget());
} }
@Override @Override
@ExplodeLoop @ExplodeLoop
public Object executeGeneric(VirtualFrame frame) { public Object executeGeneric(VirtualFrame frame) {
var owner = VmUtils.getOwner(frame); var owner = VmUtils.getOwner(frame, levelsUp);
Object receiver; var property = getProperty(owner);
var constantValue = property.getConstantValue();
if (levelsUp == 0) { if (constantValue != null) {
receiver = VmUtils.getReceiver(frame); return constantValue;
} else {
for (int i = 1; i < levelsUp; i++) {
owner = owner.getEnclosingOwner();
assert owner != null;
} }
receiver = owner.getEnclosingReceiver(); var receiver = (VmObjectLike) VmUtils.getReceiver(frame, levelsUp);
owner = owner.getEnclosingOwner(); var result = receiver.getCachedValue(property);
}
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);
if (result == null) { if (result == null) {
result = callNode.call(objReceiver, owner, property.getName()); result = getCallNode(property).call(receiver, owner, property.getName());
objReceiver.setCachedValue(property, result); receiver.setCachedValue(property, result);
} }
return 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(); .build();
} }
// only ever need to check once per node because `needsConst` is only true in the case of implicit // Only ever need to check once per node because `needsConst` is only true in the case of:
// receivers inside class (and module) bodies, and the const-ness of a resolved property cannot be //
// changed by subclasses. // * 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) { private void checkConst(VmObjectLike receiver) {
if (needsConst && !isConstChecked) { if (needsConst && !isConstChecked) {
CompilerDirectives.transferToInterpreterAndInvalidate(); CompilerDirectives.transferToInterpreterAndInvalidate();
var property = receiver.getVmClass().getProperty(propertyName); 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. // fall through; `cannotFindProperty` gets thrown when we attempt to read the property.
return; return;
} }
if (!property.isConst()) {
throw exceptionBuilder().evalError("propertyMustBeConst", propertyName.toString()).build();
}
isConstChecked = true; 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.pkl.core.ast.expression.primary;
import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.VmUtils; 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"; assert levelsUp > 0 : "shouldn't be using GetEnclosingOwnerNode for levelsUp == 0";
} }
@ExplodeLoop
public Object executeGeneric(VirtualFrame frame) { public Object executeGeneric(VirtualFrame frame) {
var owner = VmUtils.getOwner(frame); return VmUtils.getOwner(frame, levelsUp);
for (var i = 1; i < levelsUp; i++) {
owner = owner.getEnclosingOwner();
assert owner != null;
}
var result = owner.getEnclosingOwner();
assert result != null;
return result;
} }
} }
@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.pkl.core.ast.expression.primary;
import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.VmUtils; 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"; assert levelsUp > 0 : "shouldn't be using GetEnclosingReceiverNode for levelsUp == 0";
} }
@ExplodeLoop
public Object executeGeneric(VirtualFrame frame) { public Object executeGeneric(VirtualFrame frame) {
var owner = VmUtils.getOwner(frame); return VmUtils.getReceiver(frame, levelsUp);
for (var i = 1; i < levelsUp; i++) {
owner = owner.getEnclosingOwner();
assert owner != null;
}
var result = owner.getEnclosingReceiver();
assert result != null;
return result;
} }
} }
@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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") @NodeInfo(shortName = "module")
public final class GetModuleNode extends ExpressionNode { public final class GetModuleNode extends ExpressionNode {
public GetModuleNode(SourceSection sourceSection) { public GetModuleNode(SourceSection sourceSection) {
super(sourceSection); super(sourceSection);
} }
@@ -35,8 +36,10 @@ public final class GetModuleNode extends ExpressionNode {
for (var current = VmUtils.getOwner(frame).getEnclosingOwner(); for (var current = VmUtils.getOwner(frame).getEnclosingOwner();
current != null; current != null;
current = current.getEnclosingOwner()) { current = current.getEnclosingOwner()) {
if (!current.isParseTimeInvisibleScope()) {
levelsUp += 1; levelsUp += 1;
} }
}
return replace(levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp)) return replace(levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp))
.executeGeneric(frame); .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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.dsl.Specialization;
import com.oracle.truffle.api.frame.FrameSlotTypeException; 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.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.source.SourceSection; import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode; 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 slot;
private final int levelsUp;
protected ReadEnclosingFrameSlotNode(SourceSection sourceSection, int slot, int levelsUp) { protected ReadExactFrameSlotNode(SourceSection sourceSection, int slot) {
super(sourceSection); super(sourceSection);
this.slot = slot; this.slot = slot;
this.levelsUp = levelsUp;
assert levelsUp > 0 : "should be using ReadFrameSlotNode for levelsUp == 0";
} }
@Specialization(rewriteOn = FrameSlotTypeException.class) @Specialization(rewriteOn = FrameSlotTypeException.class)
protected long evalInt(VirtualFrame frame) throws FrameSlotTypeException { protected long evalInt(VirtualFrame frame) throws FrameSlotTypeException {
return getCapturedFrame(frame).getLong(slot); return frame.getLong(slot);
} }
@Specialization(rewriteOn = FrameSlotTypeException.class) @Specialization(rewriteOn = FrameSlotTypeException.class)
protected double evalFloat(VirtualFrame frame) throws FrameSlotTypeException { protected double evalFloat(VirtualFrame frame) throws FrameSlotTypeException {
return getCapturedFrame(frame).getDouble(slot); return frame.getDouble(slot);
} }
@Specialization(rewriteOn = FrameSlotTypeException.class) @Specialization(rewriteOn = FrameSlotTypeException.class)
protected boolean evalBoolean(VirtualFrame frame) throws FrameSlotTypeException { protected boolean evalBoolean(VirtualFrame frame) throws FrameSlotTypeException {
return getCapturedFrame(frame).getBoolean(slot); return frame.getBoolean(slot);
} }
@Specialization(rewriteOn = FrameSlotTypeException.class) @Specialization(rewriteOn = FrameSlotTypeException.class)
protected Object evalObject(VirtualFrame frame) throws FrameSlotTypeException { protected Object evalObject(VirtualFrame frame) throws FrameSlotTypeException {
return getCapturedFrame(frame).getObject(slot); return frame.getObject(slot);
} }
@Specialization(replaces = {"evalInt", "evalFloat", "evalBoolean", "evalObject"}) @Specialization(replaces = {"evalInt", "evalFloat", "evalBoolean", "evalObject"})
protected Object evalGeneric(VirtualFrame frame) { protected Object evalGeneric(VirtualFrame frame) {
return getCapturedFrame(frame).getValue(slot); return 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();
} }
} }
@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -15,12 +15,14 @@
*/ */
package org.pkl.core.ast.frame; 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.dsl.Specialization;
import com.oracle.truffle.api.frame.FrameSlotTypeException; import com.oracle.truffle.api.frame.FrameSlotTypeException;
import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.source.SourceSection; import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.ExpressionNode;
@NodeChild(value = "enclosingFrame", type = GetEnclosingFrameNode.class)
public abstract class ReadFrameSlotNode extends ExpressionNode { public abstract class ReadFrameSlotNode extends ExpressionNode {
private final int slot; 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.EconomicMaps;
import org.pkl.core.util.IoUtils; import org.pkl.core.util.IoUtils;
import org.pkl.core.util.MutableReference; import org.pkl.core.util.MutableReference;
import org.pkl.core.util.Pair;
import org.pkl.core.util.SyntaxHighlighter; import org.pkl.core.util.SyntaxHighlighter;
import org.pkl.parser.Parser; import org.pkl.parser.Parser;
import org.pkl.parser.ParserError; 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.Expr;
import org.pkl.parser.syntax.ImportClause; import org.pkl.parser.syntax.ImportClause;
import org.pkl.parser.syntax.ModuleDecl; import org.pkl.parser.syntax.ModuleDecl;
import org.pkl.parser.syntax.Node;
import org.pkl.parser.syntax.ReplInput; import org.pkl.parser.syntax.ReplInput;
public class ReplServer implements AutoCloseable { public class ReplServer implements AutoCloseable {
private static final String expressionPreamble = "`THE REPL TEXT EXPR` = ";
private final IndirectCallNode callNode = Truffle.getRuntime().createIndirectCallNode(); private final IndirectCallNode callNode = Truffle.getRuntime().createIndirectCallNode();
private final Context polyglotContext; private final Context polyglotContext;
private final VmLanguage language; private final VmLanguage language;
@@ -72,6 +76,7 @@ public class ReplServer implements AutoCloseable {
private final PackageResolver packageResolver; private final PackageResolver packageResolver;
private final @Nullable ProjectDependenciesManager projectDependenciesManager; private final @Nullable ProjectDependenciesManager projectDependenciesManager;
private final boolean color; private final boolean color;
private final Parser parser = new Parser();
public ReplServer( public ReplServer(
SecurityManager securityManager, SecurityManager securityManager,
@@ -178,7 +183,132 @@ public class ReplServer implements AutoCloseable {
.collect(Collectors.toList()); .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( private List<Object> evaluate(
ReplState replState, ReplState replState,
String requestId, String requestId,
@@ -198,53 +328,9 @@ public class ReplServer implements AutoCloseable {
} }
var results = new ArrayList<>(); 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()) { for (var tree : replInputContext.getNodes()) {
try { handleNode(replState, results, uri, tree, text, evalDefinitions, forceResults);
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)));
}
} }
return results; 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 Object thisValue;
private final int paramCount; private final int paramCount;
private final PklRootNode rootNode; private final PklRootNode rootNode;
private final boolean hasObjectParams;
private final boolean isFunctionAmend;
public VmFunction( public VmFunction(
MaterializedFrame enclosingFrame, MaterializedFrame enclosingFrame,
@@ -37,11 +39,24 @@ public final class VmFunction extends VmObjectLike {
int paramCount, int paramCount,
PklRootNode rootNode, PklRootNode rootNode,
@Nullable Object extraStorage) { @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); super(enclosingFrame);
this.thisValue = thisValue; this.thisValue = thisValue;
this.paramCount = paramCount; this.paramCount = paramCount;
this.rootNode = rootNode; this.rootNode = rootNode;
this.hasObjectParams = hasObjectParams;
this.extraStorage = extraStorage; this.extraStorage = extraStorage;
this.isFunctionAmend = isFunctionAmend;
} }
public RootCallTarget getCallTarget() { public RootCallTarget getCallTarget() {
@@ -71,7 +86,9 @@ public final class VmFunction extends VmObjectLike {
thisValue, thisValue,
newParamCount, newParamCount,
newRootNode == null ? rootNode : newRootNode, newRootNode == null ? rootNode : newRootNode,
newExtraStorage); newExtraStorage,
isFunctionAmend,
hasObjectParams);
} }
public Object getThisValue() { public Object getThisValue() {
@@ -187,4 +204,9 @@ public final class VmFunction extends VmObjectLike {
public String toString() { public String toString() {
return VmValueRenderer.singleLine(Integer.MAX_VALUE).render(this); 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. */ /** Returns the declared members of this object. */
public abstract UnmodifiableEconomicMap<Object, ObjectMember> getMembers(); 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 * 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 * 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); return (VmObjectLike) getReceiver(frame);
} }
public static VmObjectLike getObjectReceiver(VirtualFrame frame, int levelsUp) {
return (VmObjectLike) getReceiver(frame, levelsUp);
}
public static VmTyped getTypedObjectReceiver(Frame frame) { public static VmTyped getTypedObjectReceiver(Frame frame) {
return (VmTyped) getReceiver(frame); return (VmTyped) getReceiver(frame);
} }
@@ -159,6 +163,39 @@ public final class VmUtils {
return result; 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`. */ /** Returns a `ObjectMember`'s key while executing the corresponding `MemberNode`. */
public static Object getMemberKey(Frame frame) { public static Object getMemberKey(Frame frame) {
return frame.getArguments()[2]; return frame.getArguments()[2];
@@ -375,6 +412,7 @@ public final class VmUtils {
int numberOfLocalsToCopy) { int numberOfLocalsToCopy) {
var sourceDescriptor = sourceFrame.getFrameDescriptor(); var sourceDescriptor = sourceFrame.getFrameDescriptor();
var targetDescriptor = targetFrame.getFrameDescriptor(); var targetDescriptor = targetFrame.getFrameDescriptor();
assert sourceDescriptor.getNumberOfSlots() <= targetDescriptor.getNumberOfSlots();
// Alternatively, locals could be copied with `numberOfLocalsToCopy` // Alternatively, locals could be copied with `numberOfLocalsToCopy`
// `ReadFrameSlotNode/WriteFrameSlotNode`'s. // `ReadFrameSlotNode/WriteFrameSlotNode`'s.
for (int i = 0; i < numberOfLocalsToCopy; i++) { 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 java.util.Map;
import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.expression.primary.GetReceiverNode; 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.VmException;
import org.pkl.core.runtime.VmExceptionBuilder; import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.runtime.VmUtils; import org.pkl.core.runtime.VmUtils;
@@ -115,7 +115,7 @@ public abstract class ExternalMemberRegistry {
if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection); if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection);
var sourceSection = VmUtils.unavailableSourceSection(); var sourceSection = VmUtils.unavailableSourceSection();
var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0); var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0);
return factory.create(new GetReceiverNode(), param1Node); return factory.create(new GetReceiverNode(), param1Node);
} }
@@ -125,8 +125,8 @@ public abstract class ExternalMemberRegistry {
if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection); if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection);
var sourceSection = VmUtils.unavailableSourceSection(); var sourceSection = VmUtils.unavailableSourceSection();
var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0); var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0);
var param2Node = ReadFrameSlotNodeGen.create(sourceSection, 1); var param2Node = ReadExactFrameSlotNodeGen.create(sourceSection, 1);
return factory.create(new GetReceiverNode(), param1Node, param2Node); return factory.create(new GetReceiverNode(), param1Node, param2Node);
} }
@@ -136,9 +136,9 @@ public abstract class ExternalMemberRegistry {
if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection); if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection);
var sourceSection = VmUtils.unavailableSourceSection(); var sourceSection = VmUtils.unavailableSourceSection();
var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0); var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0);
var param2Node = ReadFrameSlotNodeGen.create(sourceSection, 1); var param2Node = ReadExactFrameSlotNodeGen.create(sourceSection, 1);
var param3Node = ReadFrameSlotNodeGen.create(sourceSection, 2); var param3Node = ReadExactFrameSlotNodeGen.create(sourceSection, 2);
return factory.create(new GetReceiverNode(), param1Node, param2Node, param3Node); return factory.create(new GetReceiverNode(), param1Node, param2Node, param3Node);
} }
@@ -148,10 +148,10 @@ public abstract class ExternalMemberRegistry {
if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection); if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection);
var sourceSection = VmUtils.unavailableSourceSection(); var sourceSection = VmUtils.unavailableSourceSection();
var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0); var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0);
var param2Node = ReadFrameSlotNodeGen.create(sourceSection, 1); var param2Node = ReadExactFrameSlotNodeGen.create(sourceSection, 1);
var param3Node = ReadFrameSlotNodeGen.create(sourceSection, 2); var param3Node = ReadExactFrameSlotNodeGen.create(sourceSection, 2);
var param4Node = ReadFrameSlotNodeGen.create(sourceSection, 3); var param4Node = ReadExactFrameSlotNodeGen.create(sourceSection, 3);
return factory.create(new GetReceiverNode(), param1Node, param2Node, param3Node, param4Node); return factory.create(new GetReceiverNode(), param1Node, param2Node, param3Node, param4Node);
} }
@@ -161,11 +161,11 @@ public abstract class ExternalMemberRegistry {
if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection); if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection);
var sourceSection = VmUtils.unavailableSourceSection(); var sourceSection = VmUtils.unavailableSourceSection();
var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0); var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0);
var param2Node = ReadFrameSlotNodeGen.create(sourceSection, 1); var param2Node = ReadExactFrameSlotNodeGen.create(sourceSection, 1);
var param3Node = ReadFrameSlotNodeGen.create(sourceSection, 2); var param3Node = ReadExactFrameSlotNodeGen.create(sourceSection, 2);
var param4Node = ReadFrameSlotNodeGen.create(sourceSection, 3); var param4Node = ReadExactFrameSlotNodeGen.create(sourceSection, 3);
var param5Node = ReadFrameSlotNodeGen.create(sourceSection, 4); var param5Node = ReadExactFrameSlotNodeGen.create(sourceSection, 4);
return factory.create( return factory.create(
new GetReceiverNode(), param1Node, param2Node, param3Node, param4Node, param5Node); 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") 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 { private fun makeEvalRequest(text: String): String {
val responses = server.handleRequest(ReplRequest.Eval("id", text, false, false)) val responses = server.handleRequest(ReplRequest.Eval("id", text, false, false))