diff --git a/docs/modules/style-guide/pages/index.adoc b/docs/modules/style-guide/pages/index.adoc index 740795b78..0c7ebebbf 100644 --- a/docs/modules/style-guide/pages/index.adoc +++ b/docs/modules/style-guide/pages/index.adoc @@ -193,7 +193,7 @@ Too many lines separate `foo` and `baz`. Properties that override an existing property shouldn't have doc comments nor type annotations, unless the type is intentionally overridden via `extends`. -[source%tested,{pkl}] +[source%parsed,{pkl}] ---- amends "myOtherModule.pkl" diff --git a/pkl-core/pkl-core.gradle.kts b/pkl-core/pkl-core.gradle.kts index 9ffbe0d22..86a6f83b2 100644 --- a/pkl-core/pkl-core.gradle.kts +++ b/pkl-core/pkl-core.gradle.kts @@ -57,6 +57,7 @@ dependencies { add("generatorImplementation", libs.javaPoet) add("generatorImplementation", libs.truffleApi) + add("generatorImplementation", projects.pklParser) javaExecutableConfiguration(project(":pkl-cli", "javaExecutable")) } @@ -124,6 +125,34 @@ tasks.test { maxHeapSize = "1g" } +val generateBaseModuleMemberRegistry by + tasks.registering(JavaExec::class) { + 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.BaseModuleMemberRegistryGenerator" + + argumentProviders.add( + CommandLineArgumentProvider { + listOf(basePklFile.asFile.absolutePath, outputDir.get().asFile.absolutePath) + } + ) + } + +sourceSets.main { java.srcDir(layout.buildDirectory.dir("generated/sources/baseModuleMembers")) } + +tasks.compileJava { dependsOn(generateBaseModuleMemberRegistry) } + val testJavaExecutable by tasks.registering(Test::class) { configureExecutableTest("LanguageSnippetTestsEngine") diff --git a/pkl-core/src/generator/java/org/pkl/core/generator/BaseModuleMemberRegistryGenerator.java b/pkl-core/src/generator/java/org/pkl/core/generator/BaseModuleMemberRegistryGenerator.java new file mode 100644 index 000000000..218dd99df --- /dev/null +++ b/pkl-core/src/generator/java/org/pkl/core/generator/BaseModuleMemberRegistryGenerator.java @@ -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 BaseModuleMemberRegistryGenerator { + record Members(Set properties, Set methods) {} + + public static void main(String[] args) { + if (args.length < 2) { + throw new IllegalArgumentException( + "Usage: BaseModuleMemberRegistryGenerator "); + } + 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 BaseModuleMemberRegistryGenerator") + .build(); + + try { + javaFile.writeTo(Path.of(outputDir)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static MethodSpec buildHasMethod(String methodName, List 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(); + var methods = new HashSet(); + 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 modifiers) { + return modifiers.stream().anyMatch((it) -> it.getValue() == ModifierValue.LOCAL); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/VmModifier.java b/pkl-core/src/main/java/org/pkl/core/ast/VmModifier.java index c000cc350..3908387e9 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/VmModifier.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/VmModifier.java @@ -55,6 +55,8 @@ private VmModifier() {} public static final int GLOB = 0x1000; + public static final int AMBIGUOUS_LOCALITY = 0x10000; + // modifier sets public static final int NONE = 0; @@ -126,6 +128,10 @@ public static boolean isConst(int modifiers) { return (modifiers & CONST) != 0; } + public static boolean isAmbiguousLocality(int modifiers) { + return (modifiers & AMBIGUOUS_LOCALITY) != 0; + } + public static boolean isElement(int modifiers) { return (modifiers & ELEMENT) != 0; } @@ -154,6 +160,10 @@ public static boolean isConstOrFixed(int modifiers) { return (modifiers & (CONST | FIXED)) != 0; } + public static boolean hasSameModifier(int modifiersA, int modifiersB, int modifier) { + return (modifiersA & modifier) == (modifiersB & modifier); + } + public static Set export(int modifiers, boolean isClass) { var result = EnumSet.noneOf(Modifier.class); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java index 244f2870b..e8fc6f272 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java @@ -45,8 +45,18 @@ import org.pkl.core.ast.MemberLookupMode; import org.pkl.core.ast.PklRootNode; import org.pkl.core.ast.VmModifier; +import org.pkl.core.ast.builder.MethodResolution.ImplicitBaseMethod; +import org.pkl.core.ast.builder.MethodResolution.ImplicitThisMethod; +import org.pkl.core.ast.builder.MethodResolution.LexicalMethod; import org.pkl.core.ast.builder.SymbolTable.AnnotationScope; import org.pkl.core.ast.builder.SymbolTable.ClassScope; +import org.pkl.core.ast.builder.SymbolTable.ModuleScope; +import org.pkl.core.ast.builder.SymbolTable.ObjectScope; +import org.pkl.core.ast.builder.VariableResolution.ForGeneratorVariable; +import org.pkl.core.ast.builder.VariableResolution.ImplicitBaseProperty; +import org.pkl.core.ast.builder.VariableResolution.ImplicitThisProperty; +import org.pkl.core.ast.builder.VariableResolution.LexicalProperty; +import org.pkl.core.ast.builder.VariableResolution.Parameter; import org.pkl.core.ast.expression.binary.AdditionNodeGen; import org.pkl.core.ast.expression.binary.DivisionNodeGen; import org.pkl.core.ast.expression.binary.EqualNodeGen; @@ -99,12 +109,16 @@ import org.pkl.core.ast.expression.member.InferParentWithinMethodNode; import org.pkl.core.ast.expression.member.InferParentWithinObjectMethodNode; import org.pkl.core.ast.expression.member.InferParentWithinPropertyNodeGen; +import org.pkl.core.ast.expression.member.InvokeClassMethodNode; +import org.pkl.core.ast.expression.member.InvokeMethodDirectNode; import org.pkl.core.ast.expression.member.InvokeMethodVirtualNodeGen; +import org.pkl.core.ast.expression.member.InvokeObjectMethodNode; import org.pkl.core.ast.expression.member.InvokeSuperMethodNodeGen; +import org.pkl.core.ast.expression.member.ReadAmbiguousLocalityPropertyNode; +import org.pkl.core.ast.expression.member.ReadLocalPropertyNode; import org.pkl.core.ast.expression.member.ReadPropertyNodeGen; import org.pkl.core.ast.expression.member.ReadSuperEntryNode; import org.pkl.core.ast.expression.member.ReadSuperPropertyNode; -import org.pkl.core.ast.expression.member.ResolveMethodNode; import org.pkl.core.ast.expression.primary.GetEnclosingOwnerNode; import org.pkl.core.ast.expression.primary.GetEnclosingReceiverNode; import org.pkl.core.ast.expression.primary.GetMemberKeyNode; @@ -112,7 +126,6 @@ import org.pkl.core.ast.expression.primary.GetOwnerNode; import org.pkl.core.ast.expression.primary.GetReceiverNode; import org.pkl.core.ast.expression.primary.OuterNode; -import org.pkl.core.ast.expression.primary.ResolveVariableNode; import org.pkl.core.ast.expression.primary.ThisNode; import org.pkl.core.ast.expression.ternary.IfElseNode; import org.pkl.core.ast.expression.unary.AbstractImportNode; @@ -129,6 +142,9 @@ import org.pkl.core.ast.expression.unary.ThrowNodeGen; import org.pkl.core.ast.expression.unary.TraceNode; import org.pkl.core.ast.expression.unary.UnaryMinusNodeGen; +import org.pkl.core.ast.frame.GetEnclosingFrameNode; +import org.pkl.core.ast.frame.ReadExactFrameSlotNodeGen; +import org.pkl.core.ast.frame.ReadFrameSlotNodeGen; import org.pkl.core.ast.internal.GetBaseModuleClassNode; import org.pkl.core.ast.internal.GetClassNodeGen; import org.pkl.core.ast.internal.ToStringNodeGen; @@ -161,6 +177,7 @@ import org.pkl.core.module.ResolvedModuleKey; import org.pkl.core.packages.PackageLoadError; import org.pkl.core.runtime.BaseModule; +import org.pkl.core.runtime.FrameDescriptorBuilder; import org.pkl.core.runtime.ModuleInfo; import org.pkl.core.runtime.ModuleResolver; import org.pkl.core.runtime.VmBytes; @@ -238,7 +255,6 @@ import org.pkl.parser.syntax.ObjectMember.ObjectProperty; import org.pkl.parser.syntax.ObjectMember.ObjectSpread; import org.pkl.parser.syntax.ObjectMember.WhenGenerator; -import org.pkl.parser.syntax.Parameter; import org.pkl.parser.syntax.Parameter.TypedIdentifier; import org.pkl.parser.syntax.ParameterList; import org.pkl.parser.syntax.QualifiedIdentifier; @@ -284,7 +300,7 @@ public AstBuilder( isBaseModule = ModuleKeys.isBaseModule(moduleKey); isStdLibModule = ModuleKeys.isStdLibModule(moduleKey); externalMemberRegistry = MemberRegistryFactory.get(moduleKey); - symbolTable = new SymbolTable(moduleInfo); + symbolTable = new SymbolTable(moduleInfo, isBaseModule); isMethodReturnTypeChecked = !isStdLibModule || IoUtils.isTestMode(); } @@ -489,10 +505,10 @@ public OuterNode visitOuterExpr(OuterExpr expr) { @Override public GetModuleNode visitModuleExpr(ModuleExpr expr) { + var currentScope = symbolTable.getCurrentScope(); // cannot use unqualified `module` in a const context - if (symbolTable.getCurrentScope().getConstLevel().isConst() - && !(expr.parent() instanceof QualifiedAccessExpr)) { - var scope = symbolTable.getCurrentScope(); + if (currentScope.getConstLevel().isConst() && !(expr.parent() instanceof QualifiedAccessExpr)) { + var scope = currentScope; while (scope != null && !(scope instanceof AnnotationScope) && !(scope instanceof ClassScope)) { @@ -500,7 +516,7 @@ public GetModuleNode visitModuleExpr(ModuleExpr expr) { } if (scope == null) { throw exceptionBuilder() - .evalError("moduleIsNotConst", symbolTable.getCurrentScope().getName().toString()) + .evalError("moduleIsNotConst", currentScope.getName().toString()) .withSourceSection(createSourceSection(expr)) .build(); } @@ -635,44 +651,161 @@ public AbstractReadNode visitReadExpr(ReadExpr expr) { }; } - @Override - public ExpressionNode visitUnqualifiedAccessExpr(UnqualifiedAccessExpr expr) { - var identifier = toIdentifier(expr.getIdentifier().getValue()); - var argList = expr.getArgumentList(); - - if (argList == null) { - return createResolveVariableNode(createSourceSection(expr), identifier); - } - - // TODO: make sure that no user-defined List/Set/Map method is in scope - // TODO: support qualified calls (e.g., `import "pkl:base"; x = - // base.List()/Set()/Map()/Bytes()`) for correctness - if (identifier == org.pkl.core.runtime.Identifier.LIST) { - return doVisitListLiteral(expr, argList); - } - - if (identifier == org.pkl.core.runtime.Identifier.SET) { - return doVisitSetLiteral(expr, argList); - } - - if (identifier == org.pkl.core.runtime.Identifier.MAP) { - return doVisitMapLiteral(expr, argList); - } - - if (identifier == org.pkl.core.runtime.Identifier.BYTES_CONSTRUCTOR) { - return doVisitBytesLiteral(expr, argList); + private ExpressionNode resolveReadVariable(UnqualifiedAccessExpr expr) { + var name = expr.getIdentifier().getValue(); + var scope = symbolTable.getCurrentScope(); + var sourceSection = createSourceSection(expr); + var constLevel = scope.getConstLevel(); + var constDepth = scope.getConstDepth(); + var resolution = scope.resolveVariable(name); + if (resolution instanceof LexicalProperty p) { + var needsConst = + switch (constLevel) { + case NONE -> false; + case MODULE -> p.isModuleScope(); + case ALL -> p.levelsUp() > constDepth; + }; + if (p.isAmbiguousLocality()) { + return new ReadAmbiguousLocalityPropertyNode( + sourceSection, org.pkl.core.runtime.Identifier.get(name), p.levelsUp(), needsConst); + } + if (p.isLocal()) { + return new ReadLocalPropertyNode( + sourceSection, + org.pkl.core.runtime.Identifier.localProperty(name), + p.levelsUp(), + needsConst); + } + return ReadPropertyNodeGen.create( + sourceSection, + org.pkl.core.runtime.Identifier.get(name), + MemberLookupMode.IMPLICIT_LEXICAL, + needsConst, + p.levelsUp() == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(p.levelsUp())); + } else if (resolution instanceof ForGeneratorVariable p) { + // Parameters can possibly write to frame slots actually in a frame that is one level + // higher than what we can tell at parse time. However, for generator variables always + // write to frame slots in the same frame. + // + // function foo(bar) = new Mixin { + // [bar] = 1 <--- actually 1 level, not 0 + // for (elem in qux) { + // elem <--- actually 0 level + // } + // } + // + // This is clearer when considering the desugared code: + // + // function foo(bar) = (it) -> (it) { + // res = bar + // for (elem in qux) { + // elem + // } + // } + return p.levelsUp() == 0 + ? ReadExactFrameSlotNodeGen.create(sourceSection, p.slot()) + : ReadFrameSlotNodeGen.create( + sourceSection, p.slot(), new GetEnclosingFrameNode(p.levelsUp())); + } else if (resolution instanceof Parameter p) { + return ReadFrameSlotNodeGen.create( + sourceSection, p.slot(), new GetEnclosingFrameNode(p.levelsUp())); + } else if (resolution instanceof ImplicitBaseProperty) { + return ReadPropertyNodeGen.create( + sourceSection, + org.pkl.core.runtime.Identifier.get(name), + MemberLookupMode.IMPLICIT_BASE, + false, + new ConstantValueNode(BaseModule.getModule())); + } else if (resolution instanceof ImplicitThisProperty) { + var isCustomThisScope = scope.isCustomThisScope(); + var needsConst = constLevel == ConstLevel.ALL && constDepth == -1 && !isCustomThisScope; + return ReadPropertyNodeGen.create( + sourceSection, + org.pkl.core.runtime.Identifier.get(name), + MemberLookupMode.IMPLICIT_THIS, + needsConst, + VmUtils.createThisNode(VmUtils.unavailableSourceSection(), isCustomThisScope)); + } else { + throw PklBugException.unreachableCode(); } + } + private ExpressionNode resolvedMethodCall(UnqualifiedAccessExpr expr, ArgumentList argList) { + var name = expr.getIdentifier().getValue(); var scope = symbolTable.getCurrentScope(); + var sourceSection = createSourceSection(expr); + var constLevel = scope.getConstLevel(); + var constDepth = scope.getConstDepth(); + var resolution = scope.resolveMethod(name); + if (resolution instanceof LexicalMethod method) { + var levelsUp = method.levelsUp(); + var identifier = org.pkl.core.runtime.Identifier.method(name, method.isLocal()); + var args = visitArgumentList(argList); + var needsConst = + switch (constLevel) { + case NONE -> false; + case MODULE -> method.isModuleScope(); + case ALL -> method.levelsUp() > constDepth; + }; + if (method.isObjectMethod()) { + return new InvokeObjectMethodNode(sourceSection, identifier, levelsUp, args, needsConst); + } + if (method.isOnClosedClass() || method.isLocal() || method.isExternal()) { + return new InvokeClassMethodNode(sourceSection, identifier, levelsUp, args, needsConst); + } + return InvokeMethodVirtualNodeGen.create( + sourceSection, + identifier, + args, + MemberLookupMode.IMPLICIT_LEXICAL, + needsConst, + levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp), + GetClassNodeGen.create(null)); + } else if (resolution instanceof ImplicitBaseMethod) { + // TODO: support qualified calls (e.g., `import("pkl:base").List()`) for + // correctness + var identifier = org.pkl.core.runtime.Identifier.get(name); + // Assumption: `pkl:base` does not call List/Set/Map/Bytes constructors. + // `resolveMethod` never returns `ImplicitBase` when resolving within + // pkl:base itself. + if (identifier == org.pkl.core.runtime.Identifier.LIST) { + return doVisitListLiteral(expr, argList); + } else if (identifier == org.pkl.core.runtime.Identifier.SET) { + return doVisitSetLiteral(expr, argList); + } else if (identifier == org.pkl.core.runtime.Identifier.MAP) { + return doVisitMapLiteral(expr, argList); + } else if (identifier == org.pkl.core.runtime.Identifier.BYTES_CONSTRUCTOR) { + return doVisitBytesLiteral(expr, argList); + } else { + var baseModule = BaseModule.getModule(); + var method = baseModule.getVmClass().getDeclaredMethod(identifier); + assert method != null; + return new InvokeMethodDirectNode( + createSourceSection(expr), + method, + new ConstantValueNode(baseModule), + visitArgumentList(argList)); + } + } else if (resolution instanceof ImplicitThisMethod) { + var isCustomThis = scope.isCustomThisScope(); + var needsConst = constLevel == ConstLevel.ALL && constDepth == -1 && !isCustomThis; + return InvokeMethodVirtualNodeGen.create( + sourceSection, + org.pkl.core.runtime.Identifier.get(name), + visitArgumentList(argList), + MemberLookupMode.IMPLICIT_THIS, + needsConst, + VmUtils.createThisNode(VmUtils.unavailableSourceSection(), isCustomThis), + GetClassNodeGen.create(null)); + } else { + throw PklBugException.unreachableCode(); + } + } - return new ResolveMethodNode( - createSourceSection(expr), - identifier, - visitArgumentList(argList), - isBaseModule, - scope.isCustomThisScope(), - scope.getConstLevel(), - scope.getConstDepth()); + @Override + public ExpressionNode visitUnqualifiedAccessExpr(UnqualifiedAccessExpr expr) { + var argList = expr.getArgumentList(); + return argList == null ? resolveReadVariable(expr) : resolvedMethodCall(expr, argList); } @Override @@ -979,12 +1112,14 @@ public ExpressionNode visitIfExpr(IfExpr expr) { public ExpressionNode visitLetExpr(LetExpr letExpr) { var sourceSection = createSourceSection(letExpr); var parameter = letExpr.getParameter(); - var frameBuilder = FrameDescriptor.newBuilder(); + var frameBuilder = new FrameDescriptorBuilder(); UnresolvedTypeNode[] typeNodes; + var bindings = new ArrayList(); if (parameter instanceof TypedIdentifier par) { typeNodes = new UnresolvedTypeNode[] {visitTypeAnnotation(par.getTypeAnnotation())}; frameBuilder.addSlot( FrameSlotKind.Illegal, toIdentifier(par.getIdentifier().getValue()), null); + bindings.add(par.getIdentifier().getValue()); } else { typeNodes = new UnresolvedTypeNode[0]; } @@ -993,6 +1128,7 @@ public ExpressionNode visitLetExpr(LetExpr letExpr) { UnresolvedFunctionNode functionNode = symbolTable.enterLambda( + bindings, frameBuilder, scope -> { var expr = visitExpr(letExpr.getExpr()); @@ -1016,6 +1152,7 @@ public ExpressionNode visitFunctionLiteralExpr(FunctionLiteralExpr expr) { var params = expr.getParameterList(); var descriptorBuilder = createFrameDescriptorBuilder(params); var paramCount = params.getParameters().size(); + var bindings = new ArrayList(); if (paramCount > 5) { throw exceptionBuilder() @@ -1024,9 +1161,18 @@ public ExpressionNode visitFunctionLiteralExpr(FunctionLiteralExpr expr) { .build(); } + for (var par : params.getParameters()) { + if (par instanceof TypedIdentifier ti) { + bindings.add(ti.getIdentifier().getValue()); + } else { + bindings.add("_"); + } + } + var isCustomThisScope = symbolTable.getCurrentScope().isCustomThisScope(); return symbolTable.enterLambda( + bindings, descriptorBuilder, scope -> { var exprNode = visitExpr(expr.getExpr()); @@ -1167,7 +1313,9 @@ public GeneratorMemberNode visitObjectMethod(ObjectMethod memberNode) { @Override public GeneratorMemberNode visitMemberPredicate(MemberPredicate ctx) { - var keyNode = symbolTable.enterCustomThisScope(scope -> visitExpr(ctx.getPred())); + var keyNode = + symbolTable.enterEagerGenerator( + (scp) -> symbolTable.enterCustomThisScope(scope -> visitExpr(ctx.getPred()))); var member = doVisitObjectEntryBody(createSourceSection(ctx), keyNode, ctx.getExpr(), ctx.getBodyList()); var isFrameStored = @@ -1196,7 +1344,7 @@ public GeneratorMemberNode visitObjectEntry(ObjectEntry member) { @Override public GeneratorMemberNode visitObjectSpread(ObjectSpread member) { - var expr = visitExpr(member.getExpr()); + var expr = symbolTable.enterEagerGenerator((ignored) -> visitExpr(member.getExpr())); return GeneratorSpreadNodeGen.create(createSourceSection(member), expr, member.isNullable()); } @@ -1209,8 +1357,10 @@ public GeneratorMemberNode visitWhenGenerator(WhenGenerator member) { ? new GeneratorMemberNode[0] : doVisitForWhenBody(member.getElseClause()); - return new GeneratorWhenNode( - sourceSection, visitExpr(member.getPredicate()), thenNodes, elseNodes); + // when predicates cannot see their direct scope + var predicateNode = + symbolTable.enterEagerGenerator((scope) -> visitExpr(member.getPredicate())); + return new GeneratorWhenNode(sourceSection, predicateNode, thenNodes, elseNodes); } private GeneratorMemberNode[] doVisitForWhenBody(ObjectBody body) { @@ -1232,6 +1382,16 @@ public GeneratorMemberNode visitForGenerator(ForGenerator ctx) { TypedIdentifier valueTypedIdentifier = null; if (valueParameter instanceof TypedIdentifier ti) valueTypedIdentifier = ti; + var params = new ArrayList(); + if (ctx.getP1() instanceof TypedIdentifier ti) { + params.add(ti.getIdentifier().getValue()); + } + if (ctx.getP2() != null) { + if (ctx.getP2() instanceof TypedIdentifier ti) { + params.add(ti.getIdentifier().getValue()); + } + } + var keyIdentifier = keyTypedIdentifier == null ? null @@ -1248,16 +1408,13 @@ public GeneratorMemberNode visitForGenerator(ForGenerator ctx) { } var currentScope = symbolTable.getCurrentScope(); var generatorDescriptorBuilder = currentScope.newFrameDescriptorBuilder(); - var memberDescriptorBuilder = currentScope.newForGeneratorMemberDescriptorBuilder(); var keySlot = -1; var valueSlot = -1; if (keyIdentifier != null) { keySlot = generatorDescriptorBuilder.addSlot(FrameSlotKind.Illegal, keyIdentifier, null); - memberDescriptorBuilder.addSlot(FrameSlotKind.Illegal, keyIdentifier, null); } if (valueIdentifier != null) { valueSlot = generatorDescriptorBuilder.addSlot(FrameSlotKind.Illegal, valueIdentifier, null); - memberDescriptorBuilder.addSlot(FrameSlotKind.Illegal, valueIdentifier, null); } var unresolvedKeyTypeNode = keyTypedIdentifier == null @@ -1279,12 +1436,10 @@ public GeneratorMemberNode visitForGenerator(ForGenerator ctx) { ? new TypeNode.UnknownTypeNode(VmUtils.unavailableSourceSection()) .initWriteSlotNode(valueSlot) : null; - var iterableNode = visitExpr(ctx.getExpr()); + var iterableNode = symbolTable.enterEagerGenerator(scope -> visitExpr(ctx.getExpr())); var memberNodes = symbolTable.enterForGenerator( - generatorDescriptorBuilder, - memberDescriptorBuilder, - scope -> doVisitForWhenBody(ctx.getBody())); + params, generatorDescriptorBuilder, scope -> doVisitForWhenBody(ctx.getBody())); return GeneratorForNodeGen.create( createSourceSection(ctx), generatorDescriptorBuilder.build(), @@ -1299,12 +1454,6 @@ public GeneratorMemberNode visitForGenerator(ForGenerator ctx) { @Override public PklRootNode visitModule(Module mod) { var moduleDecl = mod.getDecl(); - - var annotationNodes = - moduleDecl != null - ? doVisitAnnotations(moduleDecl.getAnnotations()) - : new ExpressionNode[] {}; - int modifiers; if (moduleDecl == null) { modifiers = VmModifier.NONE; @@ -1323,6 +1472,23 @@ public PklRootNode visitModule(Module mod) { } } + var scope = (ModuleScope) symbolTable.getCurrentScope(); + scope.setModifiers(modifiers); + + // visit imports first so that we already have the object member name available + var imports = mod.getImports(); + var importMembers = new ObjectMember[imports.size()]; + for (var i = 0; i < imports.size(); i++) { + importMembers[i] = visitImportClause(imports.get(i)); + } + + registerModuleScopeNames(mod, importMembers); + + var annotationNodes = + moduleDecl != null + ? doVisitAnnotations(moduleDecl.getAnnotations(), null) + : new ExpressionNode[] {}; + var extendsOrAmendsClause = moduleDecl != null ? moduleDecl.getExtendsOrAmendsDecl() : null; var supermoduleNode = @@ -1343,7 +1509,7 @@ public PklRootNode visitModule(Module mod) { new UnresolvedTypeNode.Declared(supermoduleNode.getSourceSection(), supermoduleNode); var moduleProperties = doVisitModuleProperties( - mod.getImports(), + importMembers, mod.getClasses(), mod.getTypeAliases(), List.of(), @@ -1373,7 +1539,7 @@ public PklRootNode visitModule(Module mod) { var moduleProperties = doVisitModuleProperties( - mod.getImports(), + importMembers, mod.getClasses(), mod.getTypeAliases(), mod.getProperties(), @@ -1410,18 +1576,17 @@ public PklRootNode visitModule(Module mod) { } private EconomicMap doVisitModuleProperties( - List imports, + ObjectMember[] imports, List classes, List typeAliases, List properties, Set propertyNames, ModuleInfo moduleInfo) { - var totalSize = imports.size() + classes.size() + typeAliases.size() + properties.size(); + var totalSize = imports.length + classes.size() + typeAliases.size() + properties.size(); var result = EconomicMaps.create(totalSize); - for (var _import : imports) { - var member = visitImportClause(_import); + for (var member : imports) { checkDuplicateMember(member.getName(), member.getHeaderSection(), propertyNames); EconomicMaps.put(result, member.getName(), member); } @@ -1491,10 +1656,8 @@ public ObjectMember visitImportClause(ImportClause imp) { importName, ConstLevel.NONE, scope -> { - var modifiers = VmModifier.IMPORT | VmModifier.LOCAL | VmModifier.CONST; - if (imp.isGlob()) { - modifiers = modifiers | VmModifier.GLOB; - } + var baseModifiers = VmModifier.IMPORT | VmModifier.LOCAL | VmModifier.CONST; + var modifiers = imp.isGlob() ? baseModifiers | VmModifier.GLOB : baseModifiers; var result = new ObjectMember( importNode.getSourceSection(), @@ -1511,31 +1674,52 @@ public ObjectMember visitImportClause(ImportClause imp) { }); } + private void registerClassScopeNames( + SymbolTable.Scope scope, List properties, List methods) { + for (var prop : properties) { + var local = hasLocalModifier(prop.getModifiers()); + scope.addProperty( + org.pkl.core.runtime.Identifier.property(prop.getName().getValue(), local), + local ? VmModifier.LOCAL : VmModifier.NONE); + } + for (var method : methods) { + var local = hasLocalModifier(method.getModifiers()); + scope.addMethod( + org.pkl.core.runtime.Identifier.method(method.getName().getValue(), local), + local ? VmModifier.LOCAL : VmModifier.NONE); + } + } + @Override public ObjectMember visitClass(Class clazz) { var sourceSection = createSourceSection(clazz); var headerSection = createSourceSection(clazz.getHeaderSpan()); - var bodyNode = clazz.getBody(); - var typeParameters = visitTypeParameterList(clazz.getTypeParameterList()); - List properties = bodyNode != null ? bodyNode.getProperties() : List.of(); - List methods = bodyNode != null ? bodyNode.getMethods() : List.of(); - var modifiers = doVisitModifiers( clazz.getModifiers(), VmModifier.VALID_CLASS_MODIFIERS, "invalidClassModifier") | VmModifier.CLASS; + var isLocalClass = VmModifier.isLocal(modifiers); + var className = - org.pkl.core.runtime.Identifier.property( - clazz.getName().getValue(), VmModifier.isLocal(modifiers)); + org.pkl.core.runtime.Identifier.property(clazz.getName().getValue(), isLocalClass); + + var annotations = doVisitAnnotations(clazz.getAnnotations(), className); return symbolTable.enterClass( className, + modifiers, typeParameters, scope -> { + var bodyNode = clazz.getBody(); + + List properties = bodyNode != null ? bodyNode.getProperties() : List.of(); + List methods = bodyNode != null ? bodyNode.getMethods() : List.of(); + registerClassScopeNames(scope, properties, methods); + var supertypeCtx = clazz.getSuperClass(); // needs to be inside `enterClass` so that class' type parameters are in scope @@ -1571,7 +1755,7 @@ public ObjectMember visitClass(Class clazz) { sourceSection, headerSection, createDocSourceSection(clazz.getDocComment()), - doVisitAnnotations(clazz.getAnnotations()), + annotations, modifiers, classInfo, typeParameters, @@ -1581,13 +1765,13 @@ public ObjectMember visitClass(Class clazz) { doVisitClassProperties(properties, propertyNames), doVisitMethodDefs(methods)); - var isLocal = VmModifier.isLocal(modifiers); - var result = new ObjectMember( sourceSection, headerSection, - isLocal ? VmModifier.LOCAL_CLASS_OBJECT_MEMBER : VmModifier.CLASS_OBJECT_MEMBER, + isLocalClass + ? VmModifier.LOCAL_CLASS_OBJECT_MEMBER + : VmModifier.CLASS_OBJECT_MEMBER, scope.getName(), scope.getQualifiedName()); @@ -1610,6 +1794,10 @@ private ExpressionNode resolveBaseModuleClass( @Override public Integer visitModifier(Modifier modifier) { + return toModifier(modifier); + } + + private int toModifier(Modifier modifier) { return switch (modifier.getValue()) { case EXTERNAL -> VmModifier.EXTERNAL; case ABSTRACT -> VmModifier.ABSTRACT; @@ -1657,7 +1845,6 @@ public UnresolvedPropertyNode visitClassProperty(ClassProperty entry) { var expr = entry.getExpr(); var objectBodies = entry.getBodyList(); var docComment = createDocSourceSection(docCom); - var annotationNodes = doVisitAnnotations(annotations); var sourceSection = createSourceSection(entry); var headerStart = !modifierList.isEmpty() ? modifierList.get(0).span() : name.span(); var headerEnd = typeAnnotation != null ? typeAnnotation.span() : name.span(); @@ -1669,6 +1856,7 @@ public UnresolvedPropertyNode visitClassProperty(ClassProperty entry) { var isLocal = VmModifier.isLocal(modifiers); var propertyName = org.pkl.core.runtime.Identifier.property(name.getValue(), isLocal); + var annotationNodes = doVisitAnnotations(annotations, propertyName); return symbolTable.enterProperty( propertyName, @@ -1759,9 +1947,21 @@ public UnresolvedMethodNode visitClassMethod(ClassMethod entry) { var descriptorBuilder = createFrameDescriptorBuilder(paramListCtx); var paramCount = paramListCtx.getParameters().size(); + var bindings = new ArrayList(); + for (var param : paramListCtx.getParameters()) { + if (param instanceof org.pkl.parser.syntax.Parameter.TypedIdentifier id) { + bindings.add(id.getIdentifier().getValue()); + } else { + bindings.add("_"); + } + } + + var annotations = doVisitAnnotations(entry.getAnnotations(), methodName); + return symbolTable.enterMethod( methodName, getConstLevel(modifiers), + bindings, descriptorBuilder, typeParameters, scope -> { @@ -1805,7 +2005,7 @@ public UnresolvedMethodNode visitClassMethod(ClassMethod entry) { headerSection, scope.buildFrameDescriptor(), createDocSourceSection(entry.getDocComment()), - doVisitAnnotations(entry.getAnnotations()), + annotations, modifiers, methodName, scope.getQualifiedName(), @@ -1834,6 +2034,10 @@ public ObjectMember visitTypeAlias(TypeAlias typeAlias) { var name = org.pkl.core.runtime.Identifier.property(typeAlias.getName().getValue(), isLocal); var typeParameters = visitTypeParameterList(typeAlias.getTypeParameterList()); + var objectMemberModifiers = + isLocal ? VmModifier.LOCAL_TYPEALIAS_OBJECT_MEMBER : VmModifier.TYPEALIAS_OBJECT_MEMBER; + + var annotations = doVisitAnnotations(typeAlias.getAnnotations(), name); return symbolTable.enterTypeAlias( name, @@ -1845,7 +2049,7 @@ public ObjectMember visitTypeAlias(TypeAlias typeAlias) { sourceSection, headerSection, createDocSourceSection(typeAlias.getDocComment()), - doVisitAnnotations(typeAlias.getAnnotations()), + annotations, modifiers, scopeName.toString(), scope.getQualifiedName(), @@ -1856,9 +2060,7 @@ public ObjectMember visitTypeAlias(TypeAlias typeAlias) { new ObjectMember( sourceSection, headerSection, - isLocal - ? VmModifier.LOCAL_TYPEALIAS_OBJECT_MEMBER - : VmModifier.TYPEALIAS_OBJECT_MEMBER, + objectMemberModifiers, scopeName, scope.getQualifiedName()); @@ -1870,8 +2072,8 @@ public ObjectMember visitTypeAlias(TypeAlias typeAlias) { }); } - @Override - public ExpressionNode visitAnnotation(Annotation annotation) { + public ExpressionNode visitAnnotation( + Annotation annotation, @Nullable org.pkl.core.runtime.Identifier annotatedMemberName) { var verifyNode = new CheckIsAnnotationClassNode(visitType(annotation.getType())); var bodyCtx = annotation.getBody(); @@ -1889,13 +2091,16 @@ public ExpressionNode visitAnnotation(Annotation annotation) { verifyNode); } - return symbolTable.enterAnnotationScope((scope) -> doVisitObjectBody(bodyCtx, verifyNode)); + return symbolTable.enterAnnotationScope( + annotatedMemberName, (scope) -> doVisitObjectBody(bodyCtx, verifyNode)); } - private ExpressionNode[] doVisitAnnotations(List annotations) { + private ExpressionNode[] doVisitAnnotations( + List annotations, + @Nullable org.pkl.core.runtime.Identifier annotatedMemberName) { var nodes = new ExpressionNode[annotations.size()]; for (var i = 0; i < nodes.length; i++) { - nodes[i] = visitAnnotation(annotations.get(i)); + nodes[i] = visitAnnotation(annotations.get(i), annotatedMemberName); } return nodes; } @@ -2003,9 +2208,33 @@ private ExpressionNode doVisitObjectBody( return parentNode; } + private void addObjectNamesToScope(ObjectScope scope, ObjectBody body) { + for (var member : body.getMembers()) { + if (member instanceof ObjectProperty prop) { + var local = hasLocalModifier(prop.getModifiers()); + scope.addProperty( + org.pkl.core.runtime.Identifier.property(prop.getIdentifier().getValue(), local), + local ? VmModifier.LOCAL : VmModifier.NONE); + } else if (member instanceof ObjectMethod method) { + var local = hasLocalModifier(method.getModifiers()); + scope.addMethod( + org.pkl.core.runtime.Identifier.method(method.getIdentifier().getValue(), local), + local ? VmModifier.LOCAL : VmModifier.NONE); + } else if (member instanceof WhenGenerator whenGenerator) { + addObjectNamesToScope(scope, whenGenerator.getThenClause()); + var elseClause = whenGenerator.getElseClause(); + if (elseClause != null) { + addObjectNamesToScope(scope, elseClause); + } + } + } + } + private ExpressionNode doVisitObjectBody(ObjectBody body, ExpressionNode parentNode) { return symbolTable.enterObjectScope( + body, (scope) -> { + addObjectNamesToScope(scope, body); var objectMembers = body.getMembers(); if (objectMembers.isEmpty()) { return EmptyObjectLiteralNodeGen.create( @@ -2289,8 +2518,7 @@ private ObjectMember doVisitObjectProperty( } private Pair doVisitObjectEntry(ObjectEntry entry) { - var keyNode = visitExpr(entry.getKey()); - + var keyNode = symbolTable.enterEagerGenerator((scp) -> visitExpr(entry.getKey())); var member = doVisitObjectEntryBody( createSourceSection(entry), keyNode, entry.getValue(), entry.getBodyList()); @@ -2370,9 +2598,19 @@ private ObjectMember doVisitObjectMethod( var frameDescriptorBuilder = createFrameDescriptorBuilder(paramList); + var bindings = new ArrayList(); + for (var param : paramList.getParameters()) { + if (param instanceof org.pkl.parser.syntax.Parameter.TypedIdentifier id) { + bindings.add(id.getIdentifier().getValue()); + } else { + bindings.add("_"); + } + } + return symbolTable.enterMethod( methodName, getConstLevel(modifiers), + bindings, frameDescriptorBuilder, List.of(), scope -> { @@ -2589,7 +2827,7 @@ private UnresolvedTypeNode[] doVisitParameterTypes(ParameterList paramList) { return doVisitParameterTypes(paramList.getParameters()); } - private UnresolvedTypeNode[] doVisitParameterTypes(List params) { + private UnresolvedTypeNode[] doVisitParameterTypes(List params) { var typeNodes = new UnresolvedTypeNode[params.size()]; for (int i = 0; i < typeNodes.length; i++) { if (params.get(i) instanceof TypedIdentifier typedIdentifier) { @@ -2682,8 +2920,8 @@ private boolean needsConst(ExpressionNode receiver) { return needsConst; } - private FrameDescriptor.Builder createFrameDescriptorBuilder(ParameterList params) { - var builder = FrameDescriptor.newBuilder(params.getParameters().size()); + private FrameDescriptorBuilder createFrameDescriptorBuilder(ParameterList params) { + var builder = new FrameDescriptorBuilder(params.getParameters().size()); for (var param : params.getParameters()) { org.pkl.core.runtime.Identifier identifier = null; if (param instanceof TypedIdentifier typedIdentifier) { @@ -2756,18 +2994,6 @@ private org.pkl.core.runtime.Identifier toIdentifier(String 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) { URI parsedUri; try { @@ -2849,4 +3075,47 @@ private VmException missingLocalPropertyValue(TypeAnnotation typeAnn) { private static SourceSection unavailableSourceSection() { return VmUtils.unavailableSourceSection(); } + + private void registerModuleScopeNames(Module mod, ObjectMember[] importMembers) { + var scope = symbolTable.getCurrentScope(); + + for (var imp : importMembers) { + scope.addProperty(imp.getName(), imp.getModifiers()); + } + + for (var clazz : mod.getClasses()) { + var isLocal = hasLocalModifier(clazz.getModifiers()); + scope.addProperty( + org.pkl.core.runtime.Identifier.property(clazz.getName().getValue(), isLocal), + isLocal ? VmModifier.LOCAL : VmModifier.NONE); + } + + for (var typealias : mod.getTypeAliases()) { + var isLocal = hasLocalModifier(typealias.getModifiers()); + scope.addProperty( + org.pkl.core.runtime.Identifier.property(typealias.getName().getValue(), isLocal), + isLocal ? VmModifier.LOCAL : VmModifier.NONE); + } + + for (var prop : mod.getProperties()) { + var isLocal = hasLocalModifier(prop.getModifiers()); + scope.addProperty( + org.pkl.core.runtime.Identifier.property(prop.getName().getValue(), isLocal), + isLocal ? VmModifier.LOCAL : VmModifier.NONE); + } + + for (var method : mod.getMethods()) { + var isLocal = hasLocalModifier(method.getModifiers()); + scope.addMethod( + org.pkl.core.runtime.Identifier.method(method.getName().getValue(), isLocal), + isLocal ? VmModifier.LOCAL : VmModifier.NONE); + } + } + + private static boolean hasLocalModifier(List modifiers) { + for (var m : modifiers) { + if (m.getValue() == ModifierValue.LOCAL) return true; + } + return false; + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/MethodResolution.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/MethodResolution.java new file mode 100644 index 000000000..8f3a7fdd1 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/MethodResolution.java @@ -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 {} +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/SymbolTable.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/SymbolTable.java index 4f2eb64fc..b048f88a4 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/builder/SymbolTable.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/SymbolTable.java @@ -16,26 +16,35 @@ package org.pkl.core.ast.builder; import com.oracle.truffle.api.frame.FrameDescriptor; -import com.oracle.truffle.api.frame.FrameDescriptor.Builder; import java.util.*; import java.util.function.Function; import org.pkl.core.TypeParameter; import org.pkl.core.ast.ConstantNode; import org.pkl.core.ast.ExpressionNode; -import org.pkl.core.ast.expression.generator.GeneratorMemberNode; +import org.pkl.core.ast.VmModifier; +import org.pkl.core.ast.builder.MethodResolution.ImplicitBaseMethod; +import org.pkl.core.ast.builder.MethodResolution.ImplicitThisMethod; +import org.pkl.core.ast.builder.MethodResolution.LexicalMethod; +import org.pkl.core.ast.builder.VariableResolution.ImplicitBaseProperty; +import org.pkl.core.ast.builder.VariableResolution.LexicalProperty; import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.BaseModuleMembers; +import org.pkl.core.runtime.FrameDescriptorBuilder; import org.pkl.core.runtime.Identifier; import org.pkl.core.runtime.ModuleInfo; import org.pkl.core.runtime.VmDataSize; import org.pkl.core.runtime.VmDuration; +import org.pkl.core.util.LateInit; import org.pkl.core.util.Nullable; import org.pkl.parser.Lexer; +import org.pkl.parser.syntax.ObjectBody; public final class SymbolTable { + private Scope currentScope; - public SymbolTable(ModuleInfo moduleInfo) { - currentScope = new ModuleScope(moduleInfo); + public SymbolTable(ModuleInfo moduleInfo, boolean isBaseModule) { + currentScope = new ModuleScope(moduleInfo, isBaseModule); } public Scope getCurrentScope() { @@ -53,6 +62,7 @@ public Scope getCurrentScope() { public ObjectMember enterClass( Identifier name, + int modifiers, List typeParameters, Function nodeFactory) { return doEnter( @@ -60,7 +70,8 @@ public ObjectMember enterClass( currentScope, name, toQualifiedName(name), - FrameDescriptor.newBuilder(), + modifiers, + new FrameDescriptorBuilder(), typeParameters), nodeFactory); } @@ -74,7 +85,7 @@ public ObjectMember enterTypeAlias( currentScope, name, toQualifiedName(name), - FrameDescriptor.newBuilder(), + new FrameDescriptorBuilder(), typeParameters), nodeFactory); } @@ -82,7 +93,8 @@ public ObjectMember enterTypeAlias( public T enterMethod( Identifier name, ConstLevel constLevel, - Builder frameDescriptorBuilder, + List bindings, + FrameDescriptorBuilder frameDescriptorBuilder, List typeParameters, Function nodeFactory) { return doEnter( @@ -91,26 +103,30 @@ public T enterMethod( name, toQualifiedName(name), constLevel, + bindings, frameDescriptorBuilder, typeParameters), nodeFactory); } + public T enterEagerGenerator(Function nodeFactory) { + return doEnter(new EagerGeneratorScope(currentScope, currentScope.qualifiedName), nodeFactory); + } + public T enterForGenerator( - FrameDescriptor.Builder frameDescriptorBuilder, - FrameDescriptor.Builder memberDescriptorBuilder, + List params, + FrameDescriptorBuilder frameDescriptorBuilder, Function nodeFactory) { return doEnter( new ForGeneratorScope( - currentScope, - currentScope.qualifiedName, - frameDescriptorBuilder, - memberDescriptorBuilder), + currentScope, currentScope.qualifiedName, params, frameDescriptorBuilder), nodeFactory); } public T enterLambda( - FrameDescriptor.Builder frameDescriptorBuilder, Function nodeFactory) { + List bindings, + FrameDescriptorBuilder frameDescriptorBuilder, + Function nodeFactory) { // flatten names of lambdas nested inside other lambdas for presentation purposes var parentScope = currentScope; @@ -122,14 +138,15 @@ public T enterLambda( var qualifiedName = parentScope.qualifiedName + "." + parentScope.getNextLambdaName(); return doEnter( - new LambdaScope(currentScope, qualifiedName, frameDescriptorBuilder), nodeFactory); + new LambdaScope(currentScope, bindings, qualifiedName, frameDescriptorBuilder), + nodeFactory); } public T enterProperty( Identifier name, ConstLevel constLevel, Function nodeFactory) { return doEnter( new PropertyScope( - currentScope, name, toQualifiedName(name), constLevel, FrameDescriptor.newBuilder()), + currentScope, name, toQualifiedName(name), constLevel, new FrameDescriptorBuilder()), nodeFactory); } @@ -140,8 +157,8 @@ public T enterEntry( var qualifiedName = currentScope.getQualifiedName() + currentScope.getNextEntryName(keyNode); var builder = currentScope instanceof ForGeneratorScope forScope - ? forScope.memberDescriptorBuilder - : FrameDescriptor.newBuilder(); + ? forScope.frameDescriptorBuilder + : new FrameDescriptorBuilder(); return doEnter(new EntryScope(currentScope, qualifiedName, builder), nodeFactory); } @@ -150,13 +167,20 @@ public T enterCustomThisScope(Function nodeFactory) { new CustomThisScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory); } - public T enterAnnotationScope(Function nodeFactory) { + public T enterAnnotationScope( + @Nullable Identifier annotatedTargetName, Function nodeFactory) { + var qualifiedName = + annotatedTargetName == null + ? currentScope.getQualifiedName() + : toQualifiedName(annotatedTargetName); return doEnter( - new AnnotationScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory); + new AnnotationScope(currentScope, qualifiedName, currentScope.frameDescriptorBuilder), + nodeFactory); } - public T enterObjectScope(Function nodeFactory) { - return doEnter(new ObjectScope(currentScope, currentScope.frameDescriptorBuilder), nodeFactory); + public T enterObjectScope(ObjectBody body, Function nodeFactory) { + return doEnter( + new ObjectScope(currentScope, body, currentScope.frameDescriptorBuilder), nodeFactory); } private T doEnter(S scope, Function nodeFactory) { @@ -174,25 +198,35 @@ private String toQualifiedName(Identifier name) { return currentScope.qualifiedName + separator + Lexer.maybeQuoteIdentifier(name.toString()); } + public record Member(Identifier name, int modifiers) {} + public abstract static class Scope { private final @Nullable Scope parent; private final @Nullable Identifier name; private final String qualifiedName; private int lambdaCount = 0; private int entryCount = 0; - private final FrameDescriptor.Builder frameDescriptorBuilder; + protected final FrameDescriptorBuilder frameDescriptorBuilder; private final ConstLevel constLevel; + protected boolean isBaseModule; + // The properties defined on this (lexical) scope + protected final Map properties = new HashMap<>(); + // The methods defined on this (lexical) scope + protected final Map methods = new HashMap<>(); private Scope( @Nullable Scope parent, @Nullable Identifier name, String qualifiedName, ConstLevel constLevel, - FrameDescriptor.Builder frameDescriptorBuilder) { + FrameDescriptorBuilder frameDescriptorBuilder) { this.parent = parent; this.name = name; this.qualifiedName = qualifiedName; this.frameDescriptorBuilder = frameDescriptorBuilder; + if (parent != null) { + this.isBaseModule = parent.isBaseModule; + } // const level can never decrease this.constLevel = parent != null && parent.constLevel.biggerOrEquals(constLevel) @@ -225,24 +259,8 @@ public FrameDescriptor buildFrameDescriptor() { * Returns a new descriptor builder that contains the same slots as the current scope's frame * descriptor. */ - public FrameDescriptor.Builder newFrameDescriptorBuilder() { - return newFrameDescriptorBuilder(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 FrameDescriptorBuilder newFrameDescriptorBuilder() { + return new FrameDescriptorBuilder(buildFrameDescriptor()); } public @Nullable TypeParameter getTypeParameter(String name) { @@ -276,7 +294,10 @@ public int getConstDepth() { var depth = -1; var lexicalScope = getLexicalScope(); while (lexicalScope.getConstLevel() == ConstLevel.ALL) { - depth += 1; + // LambdaScope inherits constLevel but doesn't create a const scope barrier + if (!(lexicalScope instanceof LambdaScope)) { + depth += 1; + } var parent = lexicalScope.getParent(); if (parent == null) { return depth; @@ -354,18 +375,164 @@ public final boolean isForGeneratorScope() { public ConstLevel getConstLevel() { return constLevel; } + + public void addProperty(Identifier name, int modifiers) { + var prevProperty = this.properties.put(name.toString(), new Member(name, modifiers)); + if (prevProperty != null + && !VmModifier.hasSameModifier(prevProperty.modifiers, modifiers, VmModifier.LOCAL)) { + // this can happen in when generators: + // + // ``` + // when (cond) { + // local prop = 1 + // } else { + // prop = 2 + // } + // ``` + // + // this can't happen with methods; object methods can only be `local`. + this.properties.put( + name.toString(), new Member(name, modifiers | VmModifier.AMBIGUOUS_LOCALITY)); + } + } + + 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 { + @Nullable + T apply(LexicalScope scope, int levelUp); + } + + private @Nullable R resolveLexical(ResolutionFunction 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; + } } - private interface LexicalScope {} + public interface LexicalScope { + @Nullable + VariableResolution doResolveProperty(String name, int levelsUp); + + @Nullable + MethodResolution doResolveMethod(String name, int levelsUp); + } public static class ObjectScope extends Scope implements LexicalScope { - private ObjectScope(Scope parent, Builder frameDescriptorBuilder) { + private final Map params; + + private ObjectScope( + Scope parent, ObjectBody body, FrameDescriptorBuilder frameDescriptorBuilder) { super( parent, parent.getNameOrNull(), parent.getQualifiedName(), ConstLevel.NONE, frameDescriptorBuilder); + params = collectParams(body); + } + + public boolean hasParams() { + return !params.isEmpty(); + } + + @Override + public @Nullable VariableResolution doResolveProperty(String name, int levelsUp) { + // Underscore is a discard identifier and should not be resolvable + if (name.equals("_")) { + return null; + } + var prop = properties.get(name); + if (prop != null) { + return new VariableResolution.LexicalProperty(false, prop.modifiers, levelsUp); + } + var paramIndex = params.get(name); + if (paramIndex != null) { + // params are on a higher level than the properties + return new VariableResolution.Parameter(paramIndex, levelsUp + 1); + } + return null; + } + + @Override + public @Nullable MethodResolution doResolveMethod(String name, int levelsUp) { + var method = methods.get(name); + if (method != null) { + return new LexicalMethod(true, false, false, method.modifiers, levelsUp); + } + return null; + } + + private static Map collectParams(ObjectBody body) { + var params = new HashMap(); + 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 +544,7 @@ public TypeParameterizableScope( Identifier name, String qualifiedName, ConstLevel constLevel, - Builder frameDescriptorBuilder, + FrameDescriptorBuilder frameDescriptorBuilder, List typeParameters) { super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder); this.typeParameters = typeParameters; @@ -393,47 +560,112 @@ public TypeParameterizableScope( } public static final class ModuleScope extends Scope implements LexicalScope { + private final ModuleInfo moduleInfo; + @LateInit private boolean isClosed; + private final boolean isAmend; - public ModuleScope(ModuleInfo moduleInfo) { - super(null, null, moduleInfo.getModuleName(), ConstLevel.NONE, FrameDescriptor.newBuilder()); + public ModuleScope(ModuleInfo moduleInfo, boolean isBaseModule) { + super(null, null, moduleInfo.getModuleName(), ConstLevel.NONE, new FrameDescriptorBuilder()); + this.isBaseModule = isBaseModule; this.moduleInfo = moduleInfo; + this.isAmend = moduleInfo.isAmend(); + } + + public void setModifiers(int modifiers) { + this.isClosed = VmModifier.isClosed(modifiers); + } + + @Override + public @Nullable VariableResolution doResolveProperty(String name, int levelsUp) { + var member = properties.get(name); + if (member != null) { + return new LexicalProperty(true, member.modifiers, levelsUp); + } + return null; + } + + @Override + public @Nullable MethodResolution doResolveMethod(String name, int levelsUp) { + var method = methods.get(name); + if (method == null) return null; + var isObjectMethod = isAmend && VmModifier.isLocal(method.modifiers); + return new LexicalMethod(isObjectMethod, isClosed, true, method.modifiers, levelsUp); } } - public static final class MethodScope extends TypeParameterizableScope { + public static final class MethodScope extends TypeParameterizableScope implements LexicalScope { + private final List bindings; + public MethodScope( Scope parent, Identifier name, String qualifiedName, ConstLevel constLevel, - Builder frameDescriptorBuilder, + List bindings, + FrameDescriptorBuilder frameDescriptorBuilder, List typeParameters) { super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder, typeParameters); + this.bindings = bindings; + } + + @Override + public @Nullable VariableResolution doResolveProperty(String name, int levelsUp) { + return resolveParameter(name, bindings, levelsUp); + } + + @Override + public @Nullable MethodResolution doResolveMethod(String name, int levelsUp) { + return null; } } public static final class LambdaScope extends Scope implements LexicalScope { + private final List bindings; + public LambdaScope( - Scope parent, String qualifiedName, FrameDescriptor.Builder frameDescriptorBuilder) { - super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder); + Scope parent, + List bindings, + String qualifiedName, + FrameDescriptorBuilder frameDescriptorBuilder) { + super(parent, null, qualifiedName, parent.getConstLevel(), frameDescriptorBuilder); + this.bindings = bindings; + } + + @Override + public @Nullable VariableResolution doResolveProperty(String name, int levelsUp) { + return resolveParameter(name, bindings, levelsUp); + } + + @Override + public @Nullable MethodResolution doResolveMethod(String name, int levelsUp) { + return null; + } + } + + // A generator scope that is resolved eagerly and one level above + public static final class EagerGeneratorScope extends Scope { + private static FrameDescriptorBuilder getFrameDescriptorBuilder(Scope parent) { + var grandParent = parent.parent; + assert grandParent != null; + return grandParent.frameDescriptorBuilder; + } + + private EagerGeneratorScope(Scope parent, String qualifiedName) { + super(parent, null, qualifiedName, ConstLevel.NONE, getFrameDescriptorBuilder(parent)); } } public static final class ForGeneratorScope extends Scope implements LexicalScope { - private final FrameDescriptor.Builder memberDescriptorBuilder; + final List params; public ForGeneratorScope( Scope parent, String qualifiedName, - FrameDescriptor.Builder frameDescriptorBuilder, - FrameDescriptor.Builder memberDescriptorBuilder) { + List params, + FrameDescriptorBuilder frameDescriptorBuilder) { super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder); - this.memberDescriptorBuilder = memberDescriptorBuilder; - } - - public FrameDescriptor buildMemberDescriptor() { - return memberDescriptorBuilder.build(); + this.params = params; } @Override @@ -442,6 +674,23 @@ protected String getNextEntryName(@Nullable ExpressionNode keyNode) { assert parent != null; return parent.getNextEntryName(keyNode); } + + @Override + public @Nullable VariableResolution doResolveProperty(String name, int levelsUp) { + if (!params.contains(name)) { + return null; + } + var index = frameDescriptorBuilder.findSlot(Identifier.get(name)); + if (index >= 0) { + return new VariableResolution.ForGeneratorVariable(index, levelsUp); + } + return null; + } + + @Override + public @Nullable MethodResolution doResolveMethod(String name, int levelsUp) { + return null; + } } public static final class PropertyScope extends Scope { @@ -450,26 +699,46 @@ public PropertyScope( Identifier name, String qualifiedName, ConstLevel constLevel, - FrameDescriptor.Builder frameDescriptorBuilder) { + FrameDescriptorBuilder frameDescriptorBuilder) { super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder); } } public static final class EntryScope extends Scope { public EntryScope( - Scope parent, String qualifiedName, FrameDescriptor.Builder frameDescriptorBuilder) { + Scope parent, String qualifiedName, FrameDescriptorBuilder frameDescriptorBuilder) { super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder); } } public static final class ClassScope extends TypeParameterizableScope implements LexicalScope { + private final boolean isClosed; + public ClassScope( Scope parent, Identifier name, String qualifiedName, - Builder frameDescriptorBuilder, + int modifiers, + FrameDescriptorBuilder frameDescriptorBuilder, List 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 +747,7 @@ public TypeAliasScope( Scope parent, Identifier name, String qualifiedName, - FrameDescriptor.Builder frameDescriptorBuilder, + FrameDescriptorBuilder frameDescriptorBuilder, List typeParameters) { super(parent, name, qualifiedName, ConstLevel.MODULE, frameDescriptorBuilder, typeParameters); } @@ -499,7 +768,7 @@ public String toString() { } }; - public CustomThisScope(Scope parent, FrameDescriptor.Builder frameDescriptorBuilder) { + public CustomThisScope(Scope parent, FrameDescriptorBuilder frameDescriptorBuilder) { super( parent, parent.getNameOrNull(), @@ -509,14 +778,23 @@ public CustomThisScope(Scope parent, FrameDescriptor.Builder frameDescriptorBuil } } - public static final class AnnotationScope extends Scope implements LexicalScope { - public AnnotationScope(Scope parent, FrameDescriptor.Builder frameDescriptorBuilder) { + public static final class AnnotationScope extends Scope { + public AnnotationScope( + Scope parent, String qualifiedName, FrameDescriptorBuilder frameDescriptorBuilder) { super( - parent, - parent.getNameOrNull(), - parent.getQualifiedName(), - ConstLevel.MODULE, - frameDescriptorBuilder); + parent, parent.getNameOrNull(), qualifiedName, ConstLevel.MODULE, frameDescriptorBuilder); + } + } + + private static @Nullable VariableResolution resolveParameter( + String name, List bindings, int levelsUp) { + if (name.equals("_")) { + return null; } + var index = bindings.indexOf(name); + if (index != -1) { + return new VariableResolution.Parameter(index, levelsUp); + } + return null; } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/VariableResolution.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/VariableResolution.java new file mode 100644 index 000000000..c78916757 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/VariableResolution.java @@ -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 {} +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/RestoreForBindingsNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/RestoreForBindingsNode.java index 1e6ee8016..82a4d423d 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/RestoreForBindingsNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/RestoreForBindingsNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,14 +33,12 @@ public RestoreForBindingsNode(ExpressionNode child) { @Override public Object executeGeneric(VirtualFrame frame) { var generatorFrame = ObjectData.getGeneratorFrame(frame); - var numSlots = frame.getFrameDescriptor().getNumberOfSlots(); - // This value is constant and could be a constructor argument. - var startSlot = generatorFrame.getFrameDescriptor().getNumberOfSlots() - numSlots; - assert startSlot >= 0; - // Copy locals that are for-generator variables into this frame. - // Slots before `startSlot` (if any) are function arguments - // and must not be copied to preserve scoping rules. - VmUtils.copyLocals(generatorFrame, startSlot, frame, 0, numSlots); + // copying all slots includes function arguments, but the capture generator frame + // and the host frame are guaranteed to have the same arguments and number of slots + // (guaranteed by AstBuilder). + assert frame.getFrameDescriptor().getNumberOfSlots() + == generatorFrame.getFrameDescriptor().getNumberOfSlots(); + VmUtils.copyLocals(generatorFrame, 0, frame, 0, frame.getFrameDescriptor().getNumberOfSlots()); return child.executeGeneric(frame); } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendFunctionNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendFunctionNode.java index fc8b8fcfd..b79849fec 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendFunctionNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendFunctionNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import org.pkl.core.ast.PklNode; import org.pkl.core.ast.PklRootNode; import org.pkl.core.ast.SimpleRootNode; -import org.pkl.core.ast.frame.ReadFrameSlotNodeGen; +import org.pkl.core.ast.frame.ReadExactFrameSlotNodeGen; import org.pkl.core.ast.member.FunctionNode; import org.pkl.core.ast.member.Lambda; import org.pkl.core.ast.type.TypeNode; @@ -37,11 +37,13 @@ public final class AmendFunctionNode extends PklNode { private final boolean isCustomThisScope; private final PklRootNode initialFunctionRootNode; @CompilationFinal private int customThisSlot = -1; + private final boolean hasObjectParams; public AmendFunctionNode(ObjectLiteralNode hostNode, TypeNode[] parameterTypeNodes) { super(hostNode.getSourceSection()); isCustomThisScope = hostNode.isCustomThisScope; + hasObjectParams = parameterTypeNodes.length > 0; var builder = FrameDescriptor.newBuilder(); var hostDescriptor = hostNode.parametersDescriptor; @@ -68,7 +70,7 @@ public AmendFunctionNode(ObjectLiteralNode hostNode, TypeNode[] parameterTypeNod new AmendFunctionBodyNode( sourceSection, hostNode.copy( - ReadFrameSlotNodeGen.create( + ReadExactFrameSlotNodeGen.create( hostNode.getParentNode().getSourceSection(), objectToAmendSlot)), parameterSlots, objectToAmendSlot, @@ -88,7 +90,7 @@ public AmendFunctionNode(ObjectLiteralNode hostNode, TypeNode[] parameterTypeNod new AmendFunctionBodyNode( sourceSection, hostNode.copy( - ReadFrameSlotNodeGen.create( + ReadExactFrameSlotNodeGen.create( hostNode.getParentNode().getSourceSection(), objectToAmendSlot)), parameterSlots, objectToAmendSlot, @@ -108,7 +110,9 @@ public VmFunction execute(VirtualFrame frame, VmFunction functionToAmend) { isCustomThisScope ? frame.getAuxiliarySlot(customThisSlot) : VmUtils.getReceiver(frame), functionToAmend.getParameterCount(), initialFunctionRootNode, - new Context(functionToAmend, null)); + new Context(functionToAmend, null), + true, + hasObjectParams); } private static class AmendFunctionBodyNode extends ExpressionNode { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/AbstractInvokeMethodLexicalNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/AbstractInvokeMethodLexicalNode.java new file mode 100644 index 000000000..def29a31e --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/AbstractInvokeMethodLexicalNode.java @@ -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; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeClassMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeClassMethodNode.java new file mode 100644 index 000000000..d18831702 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeClassMethodNode.java @@ -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). + * + *

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()); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodDirectNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodDirectNode.java index 1ca4c29f6..c11d33b29 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodDirectNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodDirectNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import org.pkl.core.ast.member.ClassMethod; import org.pkl.core.runtime.VmObjectLike; -/** A non-virtual ("direct") method call. */ +/** A non-virtual ("direct") method call. Used only for methods on {@code pkl:base}. */ public final class InvokeMethodDirectNode extends ExpressionNode { private final VmObjectLike owner; @Child private ExpressionNode receiverNode; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodLexicalNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodLexicalNode.java deleted file mode 100644 index e859bf06c..000000000 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeMethodLexicalNode.java +++ /dev/null @@ -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(); - } -} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeObjectMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeObjectMethodNode.java new file mode 100644 index 000000000..8537881d6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InvokeObjectMethodNode.java @@ -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); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadAmbiguousLocalityPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadAmbiguousLocalityPropertyNode.java new file mode 100644 index 000000000..1c7d9a7cd --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadAmbiguousLocalityPropertyNode.java @@ -0,0 +1,83 @@ +/* + * 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; + +public 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; + } + + // shouldn't `replace()` because this same node can resolve to either a local or non-local + // property depending on the receiver; see + // pkl-core/src/test/files/LanguageSnippetTests/input/basic/localProperty5.pkl + private ExpressionNode getUnderlying(VirtualFrame frame) { + var receiver = VmUtils.getObjectReceiver(frame, levelsUp); + return receiver.hasMember(name) ? getReadPropertyNode() : getReadLocalPropertyNode(); + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + return getUnderlying(frame).executeGeneric(frame); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java index e7c18a8b2..a7d2fe658 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadLocalPropertyNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,64 +16,81 @@ package org.pkl.core.ast.expression.member; import com.oracle.truffle.api.CompilerAsserts; +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.DirectCallNode; import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.PklBugException; import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.Identifier; import org.pkl.core.runtime.VmObjectLike; import org.pkl.core.runtime.VmUtils; /** Reads a local non-constant property that is known to exist in the lexical scope of this node. */ public final class ReadLocalPropertyNode extends ExpressionNode { - private final ObjectMember property; + private final Identifier name; private final int levelsUp; + private final boolean needsConst; @Child private DirectCallNode callNode; + @CompilationFinal private ObjectMember property; - public ReadLocalPropertyNode(SourceSection sourceSection, ObjectMember property, int levelsUp) { + public ReadLocalPropertyNode( + SourceSection sourceSection, Identifier name, int levelsUp, boolean needsConst) { super(sourceSection); CompilerAsserts.neverPartOfCompilation(); - this.property = property; + this.name = name; this.levelsUp = levelsUp; - - assert property.getNameOrNull() != null; - assert property.getConstantValue() == null : "Use a ConstantNode instead."; - - callNode = DirectCallNode.create(property.getCallTarget()); + this.needsConst = needsConst; } @Override @ExplodeLoop public Object executeGeneric(VirtualFrame frame) { - var owner = VmUtils.getOwner(frame); - Object receiver; - - if (levelsUp == 0) { - receiver = VmUtils.getReceiver(frame); - } else { - for (int i = 1; i < levelsUp; i++) { - owner = owner.getEnclosingOwner(); - assert owner != null; - } - - receiver = owner.getEnclosingReceiver(); - owner = owner.getEnclosingOwner(); + var owner = VmUtils.getOwner(frame, levelsUp); + var property = getProperty(owner); + var constantValue = property.getConstantValue(); + if (constantValue != null) { + return constantValue; } - assert receiver instanceof VmObjectLike - : "Assumption: This node isn't used in Truffle ASTs of `external` pkl.base classes whose values aren't VmObject's."; - - var objReceiver = (VmObjectLike) receiver; - var result = objReceiver.getCachedValue(property); + var receiver = (VmObjectLike) VmUtils.getReceiver(frame, levelsUp); + var result = receiver.getCachedValue(property); if (result == null) { - result = callNode.call(objReceiver, owner, property.getName()); - objReceiver.setCachedValue(property, result); + result = getCallNode(property).call(receiver, owner, property.getName()); + receiver.setCachedValue(property, result); } return result; } + + private ObjectMember getProperty(VmObjectLike owner) { + if (property == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + property = owner.getMember(name); + if (property == null) { + // should never happen + CompilerDirectives.transferToInterpreter(); + throw new PklBugException("Couldn't find local variable `" + name + "`."); + } + if (needsConst && !property.isConst()) { + throw exceptionBuilder().evalError("propertyMustBeConst", name.toString()).build(); + } + } + return property; + } + + public DirectCallNode getCallNode(ObjectMember property) { + if (callNode == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + callNode = DirectCallNode.create(property.getCallTarget()); + insert(callNode); + } + return callNode; + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java index e9cda6af3..8eb28d8e7 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ReadPropertyNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -118,20 +118,27 @@ private VmException cannotFindProperty(VmObjectLike receiver) { .build(); } - // only ever need to check once per node because `needsConst` is only true in the case of implicit - // receivers inside class (and module) bodies, and the const-ness of a resolved property cannot be - // changed by subclasses. + // Only ever need to check once per node because `needsConst` is only true in the case of: + // + // * implicit receiver in class (and module) bodies + // * `local const` object members + // + // and the const-ness of a resolved property cannot be changed by subclasses / amending objects. private void checkConst(VmObjectLike receiver) { if (needsConst && !isConstChecked) { CompilerDirectives.transferToInterpreterAndInvalidate(); var property = receiver.getVmClass().getProperty(propertyName); - if (property == null) { - // fall through; `cannotFindProperty` gets thrown when we attempt to read the property. - return; + if (property != null && !property.isConst()) { + throw exceptionBuilder().evalError("propertyMustBeConst", propertyName.toString()).build(); } - if (!property.isConst()) { + var objectMember = receiver.getMember(propertyName); + if (objectMember != null && !objectMember.isConst()) { throw exceptionBuilder().evalError("propertyMustBeConst", propertyName.toString()).build(); } + if (property == null && objectMember == null) { + // fall through; `cannotFindProperty` gets thrown when we attempt to read the property. + return; + } isConstChecked = true; } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ResolveMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ResolveMethodNode.java deleted file mode 100644 index 1cd90ab7e..000000000 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/ResolveMethodNode.java +++ /dev/null @@ -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()`). - * - *

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` - * - *

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(); - } - } -} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingOwnerNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingOwnerNode.java index 2b553bd0d..a8580c58d 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingOwnerNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingOwnerNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,13 +31,6 @@ public GetEnclosingOwnerNode(int levelsUp) { @ExplodeLoop public Object executeGeneric(VirtualFrame frame) { - var owner = VmUtils.getOwner(frame); - for (var i = 1; i < levelsUp; i++) { - owner = owner.getEnclosingOwner(); - assert owner != null; - } - var result = owner.getEnclosingOwner(); - assert result != null; - return result; + return VmUtils.getOwner(frame, levelsUp); } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingReceiverNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingReceiverNode.java index 9382d0dfc..1de68d498 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingReceiverNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetEnclosingReceiverNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.pkl.core.ast.expression.primary; import com.oracle.truffle.api.frame.VirtualFrame; -import com.oracle.truffle.api.nodes.ExplodeLoop; import org.pkl.core.ast.ExpressionNode; import org.pkl.core.runtime.VmUtils; @@ -29,15 +28,7 @@ public GetEnclosingReceiverNode(int levelsUp) { assert levelsUp > 0 : "shouldn't be using GetEnclosingReceiverNode for levelsUp == 0"; } - @ExplodeLoop public Object executeGeneric(VirtualFrame frame) { - var owner = VmUtils.getOwner(frame); - for (var i = 1; i < levelsUp; i++) { - owner = owner.getEnclosingOwner(); - assert owner != null; - } - var result = owner.getEnclosingReceiver(); - assert result != null; - return result; + return VmUtils.getReceiver(frame, levelsUp); } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetModuleNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetModuleNode.java index e3a287115..8bea07663 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetModuleNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/GetModuleNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ @NodeInfo(shortName = "module") public final class GetModuleNode extends ExpressionNode { + public GetModuleNode(SourceSection sourceSection) { super(sourceSection); } @@ -35,7 +36,9 @@ public Object executeGeneric(VirtualFrame frame) { for (var current = VmUtils.getOwner(frame).getEnclosingOwner(); current != null; current = current.getEnclosingOwner()) { - levelsUp += 1; + if (!current.isParseTimeInvisibleScope()) { + levelsUp += 1; + } } return replace(levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp)) diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveVariableNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveVariableNode.java deleted file mode 100644 index e4b38e285..000000000 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveVariableNode.java +++ /dev/null @@ -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`. - * - *

A variable name can refer to any of the following: - * - *

    - *
  • a method/lambda parameter or for-generator/let-expression variable in the lexical scope - *
  • a (potentially `local`) property in the lexical scope - *
  • a `pkl.base` module property - *
  • a property accessible through `this` - *
- * - *

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; - } -} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/frame/GetEnclosingFrameNode.java b/pkl-core/src/main/java/org/pkl/core/ast/frame/GetEnclosingFrameNode.java new file mode 100644 index 000000000..c9a0e4731 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/frame/GetEnclosingFrameNode.java @@ -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; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/frame/ReadEnclosingFrameSlotNode.java b/pkl-core/src/main/java/org/pkl/core/ast/frame/ReadExactFrameSlotNode.java similarity index 57% rename from pkl-core/src/main/java/org/pkl/core/ast/frame/ReadEnclosingFrameSlotNode.java rename to pkl-core/src/main/java/org/pkl/core/ast/frame/ReadExactFrameSlotNode.java index d2cce0bd6..a0940a2c1 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/frame/ReadEnclosingFrameSlotNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/frame/ReadExactFrameSlotNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,59 +17,40 @@ import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.FrameSlotTypeException; -import com.oracle.truffle.api.frame.MaterializedFrame; import com.oracle.truffle.api.frame.VirtualFrame; -import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.ast.ExpressionNode; -import org.pkl.core.runtime.VmUtils; -public abstract class ReadEnclosingFrameSlotNode extends ExpressionNode { +public abstract class ReadExactFrameSlotNode extends ExpressionNode { private final int slot; - private final int levelsUp; - protected ReadEnclosingFrameSlotNode(SourceSection sourceSection, int slot, int levelsUp) { + protected ReadExactFrameSlotNode(SourceSection sourceSection, int slot) { super(sourceSection); this.slot = slot; - this.levelsUp = levelsUp; - - assert levelsUp > 0 : "should be using ReadFrameSlotNode for levelsUp == 0"; } @Specialization(rewriteOn = FrameSlotTypeException.class) protected long evalInt(VirtualFrame frame) throws FrameSlotTypeException { - return getCapturedFrame(frame).getLong(slot); + return frame.getLong(slot); } @Specialization(rewriteOn = FrameSlotTypeException.class) protected double evalFloat(VirtualFrame frame) throws FrameSlotTypeException { - return getCapturedFrame(frame).getDouble(slot); + return frame.getDouble(slot); } @Specialization(rewriteOn = FrameSlotTypeException.class) protected boolean evalBoolean(VirtualFrame frame) throws FrameSlotTypeException { - return getCapturedFrame(frame).getBoolean(slot); + return frame.getBoolean(slot); } @Specialization(rewriteOn = FrameSlotTypeException.class) protected Object evalObject(VirtualFrame frame) throws FrameSlotTypeException { - return getCapturedFrame(frame).getObject(slot); + return frame.getObject(slot); } @Specialization(replaces = {"evalInt", "evalFloat", "evalBoolean", "evalObject"}) protected Object evalGeneric(VirtualFrame frame) { - return getCapturedFrame(frame).getValue(slot); - } - - // could be factored out into a separate node s.t. it - // won't be repeated in case of FrameSlotTypeException - @ExplodeLoop - protected final MaterializedFrame getCapturedFrame(VirtualFrame frame) { - var owner = VmUtils.getOwner(frame); - for (var i = 0; i < levelsUp - 1; i++) { - owner = owner.getEnclosingOwner(); - assert owner != null; // guaranteed by AstBuilder - } - return owner.getEnclosingFrame(); + return frame.getValue(slot); } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/frame/ReadFrameSlotNode.java b/pkl-core/src/main/java/org/pkl/core/ast/frame/ReadFrameSlotNode.java index 192fe82c0..8c3083a31 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/frame/ReadFrameSlotNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/frame/ReadFrameSlotNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,14 @@ */ package org.pkl.core.ast.frame; +import com.oracle.truffle.api.dsl.NodeChild; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.FrameSlotTypeException; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.ast.ExpressionNode; +@NodeChild(value = "enclosingFrame", type = GetEnclosingFrameNode.class) public abstract class ReadFrameSlotNode extends ExpressionNode { private final int slot; diff --git a/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java b/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java index 4cbe56085..6b4061823 100644 --- a/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java +++ b/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java @@ -50,6 +50,7 @@ import org.pkl.core.util.IoUtils; import org.pkl.core.util.MutableReference; import org.pkl.core.util.Nullable; +import org.pkl.core.util.Pair; import org.pkl.core.util.SyntaxHighlighter; import org.pkl.parser.Parser; import org.pkl.parser.ParserError; @@ -58,9 +59,12 @@ import org.pkl.parser.syntax.Expr; import org.pkl.parser.syntax.ImportClause; import org.pkl.parser.syntax.ModuleDecl; +import org.pkl.parser.syntax.Node; import org.pkl.parser.syntax.ReplInput; public class ReplServer implements AutoCloseable { + private static final String expressionPreamble = "`THE REPL TEXT EXPR` = "; + private final IndirectCallNode callNode = Truffle.getRuntime().createIndirectCallNode(); private final Context polyglotContext; private final VmLanguage language; @@ -72,6 +76,7 @@ public class ReplServer implements AutoCloseable { private final PackageResolver packageResolver; private final @Nullable ProjectDependenciesManager projectDependenciesManager; private final boolean color; + private final Parser parser = new Parser(); public ReplServer( SecurityManager securityManager, @@ -178,7 +183,132 @@ private List handleEval(Eval request) { .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. + * + *

This created module is never executed. + */ + private Pair 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 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 evaluate( ReplState replState, String requestId, @@ -198,53 +328,9 @@ private List evaluate( } var results = new ArrayList<>(); - var module = ModuleKeys.synthetic(uri, workingDir.toUri(), uri, text, false); - ResolvedModuleKey resolved; - try { - resolved = module.resolve(securityManager); - } catch (SecurityManagerException e) { - throw new VmExceptionBuilder().withCause(e).build(); - } catch (IOException e) { - // resolving a synthetic module should never cause IOException - throw new AssertionError(e); - } - - var builder = - new AstBuilder( - VmUtils.loadSource(resolved), - language, - replState.module.getModuleInfo(), - moduleResolver); for (var tree : replInputContext.getNodes()) { - try { - if (tree instanceof Expr expr) { - var exprNode = builder.visitExpr(expr); - evaluateExpr(replState, exprNode, forceResults, results); - } else if (tree instanceof ImportClause importClause) { - addStaticModuleProperty(builder.visitImportClause(importClause)); - } else if (tree instanceof ClassProperty classProperty) { - var propertyNode = builder.visitClassProperty(classProperty); - var property = addModuleProperty(propertyNode); - if (evalDefinitions) { - evaluateMemberDef(replState, property, forceResults, results); - } - } else if (tree instanceof Class clazz) { - addStaticModuleProperty(builder.visitClass(clazz)); - } else if (tree instanceof org.pkl.parser.syntax.TypeAlias typeAlias) { - addStaticModuleProperty(builder.visitTypeAlias(typeAlias)); - } else if (tree instanceof org.pkl.parser.syntax.ClassMethod classMethod) { - addModuleMethodDef(builder.visitClassMethod(classMethod)); - } else if (tree instanceof ModuleDecl) { - // do nothing for now - } else { - results.add( - new ReplResponse.InternalError(new IllegalStateException("Unexpected parse result"))); - } - } catch (VmException e) { - // TODO: patch stack trace for constants - results.add(new EvalError(errorRenderer.render(e))); - } + handleNode(replState, results, uri, tree, text, evalDefinitions, forceResults); } return results; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/FrameDescriptorBuilder.java b/pkl-core/src/main/java/org/pkl/core/runtime/FrameDescriptorBuilder.java new file mode 100644 index 000000000..d4cb19910 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/FrameDescriptorBuilder.java @@ -0,0 +1,83 @@ +/* + * 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.pkl.core.util.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 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--) { + if (names[i] != null && names[i].equals(identifier)) { + return i; + } + } + return -1; + } + + public FrameDescriptor build() { + return underlying.build(); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java index 6358f783c..6dec43e22 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java @@ -60,6 +60,7 @@ private void renderBugException(VmBugException exception, AnsiStringBuilder out) // if a cause exists, it's more useful to report just that var exceptionToReport = exception.getCause() != null ? exception.getCause() : exception; var exceptionUrl = URLEncoder.encode(exceptionToReport.toString(), StandardCharsets.UTF_8); + System.out.println(exception.getCause().toString()); out.append("An unexpected error has occurred. Would you mind filing a bug report?") .append('\n') diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java index c3b13c0ba..be84fbfd6 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java @@ -30,6 +30,8 @@ public final class VmFunction extends VmObjectLike { private final Object thisValue; private final int paramCount; private final PklRootNode rootNode; + private boolean hasObjectParams; + private final boolean isFunctionAmend; public VmFunction( MaterializedFrame enclosingFrame, @@ -37,11 +39,24 @@ public VmFunction( int paramCount, PklRootNode rootNode, @Nullable Object extraStorage) { + this(enclosingFrame, thisValue, paramCount, rootNode, extraStorage, false, false); + } + + public VmFunction( + MaterializedFrame enclosingFrame, + Object thisValue, + int paramCount, + PklRootNode rootNode, + @Nullable Object extraStorage, + boolean isFunctionAmend, + boolean hasObjectParams) { super(enclosingFrame); this.thisValue = thisValue; this.paramCount = paramCount; this.rootNode = rootNode; + this.hasObjectParams = hasObjectParams; this.extraStorage = extraStorage; + this.isFunctionAmend = isFunctionAmend; } public RootCallTarget getCallTarget() { @@ -71,7 +86,9 @@ public VmFunction copy( thisValue, newParamCount, newRootNode == null ? rootNode : newRootNode, - newExtraStorage); + newExtraStorage, + isFunctionAmend, + hasObjectParams); } public Object getThisValue() { @@ -187,4 +204,9 @@ public int hashCode() { public String toString() { return VmValueRenderer.singleLine(Integer.MAX_VALUE).render(this); } + + @Override + public boolean isParseTimeInvisibleScope() { + return isFunctionAmend && !hasObjectParams; + } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java index b6f087aa1..64ef9d1aa 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,6 +82,42 @@ public boolean isModuleObject() { /** Returns the declared members of this object. */ public abstract UnmodifiableEconomicMap getMembers(); + /** + * Tells if this it's impossible to determine if this is object describes a scope during parse + * time. + * + *

An amended lambda hides a new lexical scope if there are also no params. + * + *

Assuming {@code foo} is a lambda, this snippet: + * + *

{@code
+   * qux = 3
+   *
+   * foo {
+   *   bar = qux
+   * }
+   * }
+ * + * Desugars to: + * + *
{@code
+   * qux = 3
+   *
+   * foo = () -> (super.foo.apply()) {
+   *   bar = qux
+   * }
+   *
+   * }
+ * + * So, {@code qux} is two levels higher, not one. However, it's not possible to figure this + * out at parse time alone. + * + *

This method tells if we need to skip this object when traversing up lexical scopes. + */ + public boolean isParseTimeInvisibleScope() { + return false; + } + /** * Reads from the properties cache for this object. The cache contains the values of all members * defined in this object or an ancestor thereof which have been requested with this object as the diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java index 880be9d9d..f6e875584 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java @@ -143,6 +143,10 @@ public static VmObjectLike getObjectReceiver(Frame frame) { return (VmObjectLike) getReceiver(frame); } + public static VmObjectLike getObjectReceiver(VirtualFrame frame, int levelsUp) { + return (VmObjectLike) getReceiver(frame, levelsUp); + } + public static VmTyped getTypedObjectReceiver(Frame frame) { return (VmTyped) getReceiver(frame); } @@ -159,6 +163,40 @@ public static VmObjectLike getOwner(Frame frame) { 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)); + } + + @ExplodeLoop + public static VirtualFrame getFrame(VirtualFrame frame, int levelsUp) { + frame = skipInvisibleScopes(frame); + if (levelsUp == 0) { + return frame; + } + var owner = getOwner(frame); + for (var i = 0; i < levelsUp; i++) { + frame = owner.getEnclosingFrame(); + owner = getOwner(frame); + if (owner.isParseTimeInvisibleScope()) { + i--; + } + } + return frame; + } + + private static VirtualFrame skipInvisibleScopes(VirtualFrame frame) { + var owner = getOwner(frame); + while (owner.isParseTimeInvisibleScope()) { + frame = owner.getEnclosingFrame(); + owner = getOwner(frame); + } + return frame; + } + /** Returns a `ObjectMember`'s key while executing the corresponding `MemberNode`. */ public static Object getMemberKey(Frame frame) { return frame.getArguments()[2]; @@ -375,6 +413,7 @@ public static void copyLocals( int numberOfLocalsToCopy) { var sourceDescriptor = sourceFrame.getFrameDescriptor(); var targetDescriptor = targetFrame.getFrameDescriptor(); + assert sourceDescriptor.getNumberOfSlots() <= targetDescriptor.getNumberOfSlots(); // Alternatively, locals could be copied with `numberOfLocalsToCopy` // `ReadFrameSlotNode/WriteFrameSlotNode`'s. for (int i = 0; i < numberOfLocalsToCopy; i++) { diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/registry/ExternalMemberRegistry.java b/pkl-core/src/main/java/org/pkl/core/stdlib/registry/ExternalMemberRegistry.java index a0f7b3d59..2f31d1bff 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/registry/ExternalMemberRegistry.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/registry/ExternalMemberRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.util.Map; import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.expression.primary.GetReceiverNode; -import org.pkl.core.ast.frame.ReadFrameSlotNodeGen; +import org.pkl.core.ast.frame.ReadExactFrameSlotNodeGen; import org.pkl.core.runtime.VmException; import org.pkl.core.runtime.VmExceptionBuilder; import org.pkl.core.runtime.VmUtils; @@ -115,7 +115,7 @@ private ExpressionNode getFunction1Body(String qualifiedName, SourceSection head if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection); var sourceSection = VmUtils.unavailableSourceSection(); - var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0); + var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0); return factory.create(new GetReceiverNode(), param1Node); } @@ -125,8 +125,8 @@ private ExpressionNode getFunction2Body(String qualifiedName, SourceSection head if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection); var sourceSection = VmUtils.unavailableSourceSection(); - var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0); - var param2Node = ReadFrameSlotNodeGen.create(sourceSection, 1); + var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0); + var param2Node = ReadExactFrameSlotNodeGen.create(sourceSection, 1); return factory.create(new GetReceiverNode(), param1Node, param2Node); } @@ -136,9 +136,9 @@ private ExpressionNode getFunction3Body(String qualifiedName, SourceSection head if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection); var sourceSection = VmUtils.unavailableSourceSection(); - var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0); - var param2Node = ReadFrameSlotNodeGen.create(sourceSection, 1); - var param3Node = ReadFrameSlotNodeGen.create(sourceSection, 2); + var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0); + var param2Node = ReadExactFrameSlotNodeGen.create(sourceSection, 1); + var param3Node = ReadExactFrameSlotNodeGen.create(sourceSection, 2); return factory.create(new GetReceiverNode(), param1Node, param2Node, param3Node); } @@ -148,10 +148,10 @@ private ExpressionNode getFunction4Body(String qualifiedName, SourceSection head if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection); var sourceSection = VmUtils.unavailableSourceSection(); - var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0); - var param2Node = ReadFrameSlotNodeGen.create(sourceSection, 1); - var param3Node = ReadFrameSlotNodeGen.create(sourceSection, 2); - var param4Node = ReadFrameSlotNodeGen.create(sourceSection, 3); + var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0); + var param2Node = ReadExactFrameSlotNodeGen.create(sourceSection, 1); + var param3Node = ReadExactFrameSlotNodeGen.create(sourceSection, 2); + var param4Node = ReadExactFrameSlotNodeGen.create(sourceSection, 3); return factory.create(new GetReceiverNode(), param1Node, param2Node, param3Node, param4Node); } @@ -161,11 +161,11 @@ private ExpressionNode getFunction5Body(String qualifiedName, SourceSection head if (factory == null) throw cannotFindMemberImpl(qualifiedName, headerSection); var sourceSection = VmUtils.unavailableSourceSection(); - var param1Node = ReadFrameSlotNodeGen.create(sourceSection, 0); - var param2Node = ReadFrameSlotNodeGen.create(sourceSection, 1); - var param3Node = ReadFrameSlotNodeGen.create(sourceSection, 2); - var param4Node = ReadFrameSlotNodeGen.create(sourceSection, 3); - var param5Node = ReadFrameSlotNodeGen.create(sourceSection, 4); + var param1Node = ReadExactFrameSlotNodeGen.create(sourceSection, 0); + var param2Node = ReadExactFrameSlotNodeGen.create(sourceSection, 1); + var param3Node = ReadExactFrameSlotNodeGen.create(sourceSection, 2); + var param4Node = ReadExactFrameSlotNodeGen.create(sourceSection, 3); + var param5Node = ReadExactFrameSlotNodeGen.create(sourceSection, 4); return factory.create( new GetReceiverNode(), param1Node, param2Node, param3Node, param4Node, param5Node); } diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/annotation/annotation7.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/annotation/annotation7.pkl new file mode 100644 index 000000000..d6459f328 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/annotation/annotation7.pkl @@ -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 diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/localProperty3.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/localProperty3.pkl new file mode 100644 index 000000000..7b89c9fd0 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/localProperty3.pkl @@ -0,0 +1,7 @@ +local foo = "hello" + +bar { + new Mixin { + [foo] = "world" + }.apply(new Dynamic {}) +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/localProperty4.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/localProperty4.pkl new file mode 100644 index 000000000..c7c4ba3f3 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/localProperty4.pkl @@ -0,0 +1,8 @@ +qux = "outer" + +foo { + when (true) { + local qux = "then branch" + prop = qux + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/localProperty5.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/localProperty5.pkl new file mode 100644 index 000000000..fbcafb4a1 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/localProperty5.pkl @@ -0,0 +1,17 @@ +// `qux` has ambiguous locality; don't know if it should be local read or not + +res1 { + cond = true + prop { + when (cond) { + local qux = "then branch" + } else { + qux = "else branch" + } + theBranch = qux + } +} + +res2 = (res1) { + cond = false +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/generators/forGeneratorInMixinWithObjectParam.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/generators/forGeneratorInMixinWithObjectParam.pkl new file mode 100644 index 000000000..65ff3cb63 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/generators/forGeneratorInMixinWithObjectParam.pkl @@ -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 + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/generators/forGeneratorsInMethods.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/generators/forGeneratorsInMethods.pkl new file mode 100644 index 000000000..5f81114f2 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/generators/forGeneratorsInMethods.pkl @@ -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") diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/annotation/annotation7.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/annotation/annotation7.pcf new file mode 100644 index 000000000..7e7b0a6f1 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/annotation/annotation7.pcf @@ -0,0 +1,4 @@ +res1 = 18 +res2 = "myProp" +res3 = 19 +res4 = "myProp" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/basic/localProperty3.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/localProperty3.pcf new file mode 100644 index 000000000..d52af886d --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/localProperty3.pcf @@ -0,0 +1,5 @@ +bar { + new { + ["hello"] = "world" + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/basic/localProperty4.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/localProperty4.pcf new file mode 100644 index 000000000..68c91d0a5 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/localProperty4.pcf @@ -0,0 +1,4 @@ +qux = "outer" +foo { + prop = "then branch" +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/basic/localProperty5.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/localProperty5.pcf new file mode 100644 index 000000000..847df47b5 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/localProperty5.pcf @@ -0,0 +1,13 @@ +res1 { + cond = true + prop { + theBranch = "then branch" + } +} +res2 { + cond = false + prop { + qux = "else branch" + theBranch = "else branch" + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/generators/forGeneratorInMixinWithObjectParam.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/generators/forGeneratorInMixinWithObjectParam.pcf new file mode 100644 index 000000000..b16367fc1 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/generators/forGeneratorInMixinWithObjectParam.pcf @@ -0,0 +1,18 @@ +foo { + bar { + "elem" + new { + res { + "elem" + } + } + new { + name = "res1Name" + value = "res1Value" + } + new { + name = "res2Name" + value = "res2Value" + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/generators/forGeneratorsInMethods.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/generators/forGeneratorsInMethods.pcf new file mode 100644 index 000000000..48f5ddcd2 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/generators/forGeneratorsInMethods.pcf @@ -0,0 +1,3 @@ +res { + "Hello arg1 arg2 arg3 1 arg1 arg2 arg3" +} diff --git a/pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt index 373ddcc7a..b76ca3500 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt @@ -223,6 +223,24 @@ class ReplServerTest { assertThat((response as ReplResponse.EvalSuccess).result).isEqualTo("\u001B[32m5\u001B[0m.ms") } + @Test + fun `strip expression preamble in error message`() { + val result = makeFailingEvalRequest("foo") + assertThat(result) + .isEqualTo( + """ + –– Pkl Error –– + Cannot find property `foo`. + + 1 | foo + ^^^ + at (repl:id) + + """ + .trimIndent() + ) + } + private fun makeEvalRequest(text: String): String { val responses = server.handleRequest(ReplRequest.Eval("id", text, false, false))