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..36cf0666b 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,6 +45,10 @@ import org.pkl.core.ast.MemberLookupMode; import org.pkl.core.ast.PklRootNode; import org.pkl.core.ast.VmModifier; +import org.pkl.core.ast.builder.PropertyResolution.ConstantProperty; +import org.pkl.core.ast.builder.PropertyResolution.LetOrLambdaProperty; +import org.pkl.core.ast.builder.PropertyResolution.LocalClassProperty; +import org.pkl.core.ast.builder.PropertyResolution.NormalClassProperty; import org.pkl.core.ast.builder.SymbolTable.AnnotationScope; import org.pkl.core.ast.builder.SymbolTable.ClassScope; import org.pkl.core.ast.expression.binary.AdditionNodeGen; @@ -99,6 +103,7 @@ 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.InvokeMethodDirectNode; import org.pkl.core.ast.expression.member.InvokeMethodVirtualNodeGen; import org.pkl.core.ast.expression.member.InvokeSuperMethodNodeGen; import org.pkl.core.ast.expression.member.ReadPropertyNodeGen; @@ -112,6 +117,10 @@ 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.PartiallyResolvedMethod; +import org.pkl.core.ast.expression.primary.PartiallyResolvedVariable; +import org.pkl.core.ast.expression.primary.ResolveParseTimeMethodNode; +import org.pkl.core.ast.expression.primary.ResolveParseTimeVariableNode; import org.pkl.core.ast.expression.primary.ResolveVariableNode; import org.pkl.core.ast.expression.primary.ThisNode; import org.pkl.core.ast.expression.ternary.IfElseNode; @@ -272,20 +281,39 @@ public class AstBuilder extends AbstractAstBuilder { private final ExternalMemberRegistry externalMemberRegistry; private final SymbolTable symbolTable; private final boolean isMethodReturnTypeChecked; + // we resolve variables at runtime in repl mode + private final boolean isReplMode; public AstBuilder( - Source source, VmLanguage language, ModuleInfo moduleInfo, ModuleResolver moduleResolver) { + Source source, + VmLanguage language, + ModuleInfo moduleInfo, + @Nullable Module module, + ModuleResolver moduleResolver) { super(source); this.language = language; this.moduleInfo = moduleInfo; + // force loading of pkl:base for parse time variable resolution + var baseModule = BaseModule.getModule(); moduleKey = moduleInfo.getModuleKey(); this.moduleResolver = moduleResolver; isBaseModule = ModuleKeys.isBaseModule(moduleKey); isStdLibModule = ModuleKeys.isStdLibModule(moduleKey); externalMemberRegistry = MemberRegistryFactory.get(moduleKey); - symbolTable = new SymbolTable(moduleInfo); + if (isBaseModule) { + symbolTable = new SymbolTable(moduleInfo, module); + } else { + symbolTable = new SymbolTable(moduleInfo, module, baseModule); + } isMethodReturnTypeChecked = !isStdLibModule || IoUtils.isTestMode(); + isReplMode = !isStdLibModule && "repl".equals(moduleKey.getUri().getHost()); + } + + // Constructor for REPL/expression parsing where no Module syntax tree is available + public AstBuilder( + Source source, VmLanguage language, ModuleInfo moduleInfo, ModuleResolver moduleResolver) { + this(source, language, moduleInfo, null, moduleResolver); } public static AstBuilder create( @@ -331,7 +359,7 @@ public static AstBuilder create( isAmend); } - return new AstBuilder(source, language, moduleInfo, moduleResolver); + return new AstBuilder(source, language, moduleInfo, ctx, moduleResolver); } @Override @@ -635,35 +663,81 @@ public AbstractReadNode visitReadExpr(ReadExpr expr) { }; } + private static final boolean shouldResolveVariables = true; + @Override public ExpressionNode visitUnqualifiedAccessExpr(UnqualifiedAccessExpr expr) { var identifier = toIdentifier(expr.getIdentifier().getValue()); var argList = expr.getArgumentList(); + var scope = symbolTable.getCurrentScope(); + var sourceSection = createSourceSection(expr); + if (argList == null) { + if (shouldResolveVariables && !isReplMode && !isStdLibModule) { + var node = + scope.resolveProperty( + identifier, + res -> { + if (res instanceof ConstantProperty c) { + return new PartiallyResolvedVariable.ConstantVar(c.constant()); + } else if (res instanceof LocalClassProperty p) { + return new PartiallyResolvedVariable.LocalPropertyVar(p.name(), p.isConst()); + } else if (res instanceof NormalClassProperty p) { + return new PartiallyResolvedVariable.PropertyVar(p.name(), p.isConst()); + } else if (res instanceof LetOrLambdaProperty) { + return new PartiallyResolvedVariable.FrameSlotVar(identifier); + } + return null; + }); + if (node != null) { + return new ResolveParseTimeVariableNode( + node, sourceSection, scope.getConstLevel(), scope.getConstDepth(), identifier); + } + } + // fall back to resolve variable node 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); + if (!isReplMode) { + var result = + scope.resolveMethod( + identifier, + res -> { + if (res instanceof MethodResolution.DirectMethod m) { + if (m.isBase() && identifier == org.pkl.core.runtime.Identifier.LIST) { + return doVisitListLiteral(expr, argList); + } else if (m.isBase() && identifier == org.pkl.core.runtime.Identifier.SET) { + return doVisitSetLiteral(expr, argList); + } else if (m.isBase() && identifier == org.pkl.core.runtime.Identifier.MAP) { + return doVisitMapLiteral(expr, argList); + } else if (m.isBase() + && identifier == org.pkl.core.runtime.Identifier.BYTES_CONSTRUCTOR) { + return doVisitBytesLiteral(expr, argList); + } else { + var args = visitArgumentList(argList); + return new InvokeMethodDirectNode( + createSourceSection(expr), m.method(), m.receiver(), args); + } + } else if (shouldResolveVariables && !isStdLibModule) { + if (res instanceof MethodResolution.LexicalMethod lm) { + return new PartiallyResolvedMethod.LexicalMethodVar(identifier, lm.isConst()); + } else if (res instanceof MethodResolution.VirtualMethod vm) { + return new PartiallyResolvedMethod.VirtualMethodVar(identifier, vm.isConst()); + } + } + return null; + }); + if (result instanceof ExpressionNode n) return n; + if (result instanceof PartiallyResolvedMethod pm) { + var args = visitArgumentList(argList); + return new ResolveParseTimeMethodNode( + pm, sourceSection, args, scope.getConstLevel(), scope.getConstDepth(), identifier); + } } - var scope = symbolTable.getCurrentScope(); + // TODO: support qualified calls (e.g., `import "pkl:base"; x = + // base.List()/Set()/Map()/Bytes()`) for correctness return new ResolveMethodNode( createSourceSection(expr), @@ -981,10 +1055,14 @@ public ExpressionNode visitLetExpr(LetExpr letExpr) { var parameter = letExpr.getParameter(); var frameBuilder = FrameDescriptor.newBuilder(); UnresolvedTypeNode[] typeNodes; + var bindings = new ArrayList(); + var slot = -1; if (parameter instanceof TypedIdentifier par) { typeNodes = new UnresolvedTypeNode[] {visitTypeAnnotation(par.getTypeAnnotation())}; - frameBuilder.addSlot( - FrameSlotKind.Illegal, toIdentifier(par.getIdentifier().getValue()), null); + slot = + frameBuilder.addSlot( + FrameSlotKind.Illegal, toIdentifier(par.getIdentifier().getValue()), null); + bindings.add(par.getIdentifier().getValue()); } else { typeNodes = new UnresolvedTypeNode[0]; } @@ -993,6 +1071,8 @@ public ExpressionNode visitLetExpr(LetExpr letExpr) { UnresolvedFunctionNode functionNode = symbolTable.enterLambda( + bindings, + slot, frameBuilder, scope -> { var expr = visitExpr(letExpr.getExpr()); @@ -1016,6 +1096,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 +1105,19 @@ 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, + -1, descriptorBuilder, scope -> { var exprNode = visitExpr(expr.getExpr()); @@ -1167,7 +1258,9 @@ public GeneratorMemberNode visitObjectMethod(ObjectMethod memberNode) { @Override public GeneratorMemberNode visitMemberPredicate(MemberPredicate ctx) { - var keyNode = symbolTable.enterCustomThisScope(scope -> visitExpr(ctx.getPred())); + var keyNode = + symbolTable.enterForEager( + (scp) -> symbolTable.enterCustomThisScope(scope -> visitExpr(ctx.getPred()))); var member = doVisitObjectEntryBody(createSourceSection(ctx), keyNode, ctx.getExpr(), ctx.getBodyList()); var isFrameStored = @@ -1209,8 +1302,9 @@ 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.enterForEager((scope) -> visitExpr(member.getPredicate())); + return new GeneratorWhenNode(sourceSection, predicateNode, thenNodes, elseNodes); } private GeneratorMemberNode[] doVisitForWhenBody(ObjectBody body) { @@ -1232,6 +1326,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 @@ -1251,12 +1355,15 @@ public GeneratorMemberNode visitForGenerator(ForGenerator ctx) { var memberDescriptorBuilder = currentScope.newForGeneratorMemberDescriptorBuilder(); var keySlot = -1; var valueSlot = -1; + var nestLevel = -1; if (keyIdentifier != null) { keySlot = generatorDescriptorBuilder.addSlot(FrameSlotKind.Illegal, keyIdentifier, null); + nestLevel = keySlot; memberDescriptorBuilder.addSlot(FrameSlotKind.Illegal, keyIdentifier, null); } if (valueIdentifier != null) { valueSlot = generatorDescriptorBuilder.addSlot(FrameSlotKind.Illegal, valueIdentifier, null); + if (nestLevel == -1) nestLevel = valueSlot; memberDescriptorBuilder.addSlot(FrameSlotKind.Illegal, valueIdentifier, null); } var unresolvedKeyTypeNode = @@ -1279,9 +1386,11 @@ public GeneratorMemberNode visitForGenerator(ForGenerator ctx) { ? new TypeNode.UnknownTypeNode(VmUtils.unavailableSourceSection()) .initWriteSlotNode(valueSlot) : null; - var iterableNode = visitExpr(ctx.getExpr()); + var iterableNode = symbolTable.enterForEager(scope -> visitExpr(ctx.getExpr())); var memberNodes = symbolTable.enterForGenerator( + params, + nestLevel, generatorDescriptorBuilder, memberDescriptorBuilder, scope -> doVisitForWhenBody(ctx.getBody())); @@ -1534,6 +1643,7 @@ public ObjectMember visitClass(Class clazz) { return symbolTable.enterClass( className, + clazz, typeParameters, scope -> { var supertypeCtx = clazz.getSuperClass(); @@ -1610,6 +1720,10 @@ private ExpressionNode resolveBaseModuleClass( @Override public Integer visitModifier(Modifier modifier) { + return toModifier(modifier); + } + + public static int toModifier(Modifier modifier) { return switch (modifier.getValue()) { case EXTERNAL -> VmModifier.EXTERNAL; case ABSTRACT -> VmModifier.ABSTRACT; @@ -1759,9 +1873,19 @@ 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 Parameter.TypedIdentifier id) { + bindings.add(id.getIdentifier().getValue()); + } else { + bindings.add("_"); + } + } + return symbolTable.enterMethod( methodName, getConstLevel(modifiers), + bindings, descriptorBuilder, typeParameters, scope -> { @@ -2005,6 +2129,7 @@ private ExpressionNode doVisitObjectBody( private ExpressionNode doVisitObjectBody(ObjectBody body, ExpressionNode parentNode) { return symbolTable.enterObjectScope( + body, (scope) -> { var objectMembers = body.getMembers(); if (objectMembers.isEmpty()) { @@ -2289,7 +2414,8 @@ private ObjectMember doVisitObjectProperty( } private Pair doVisitObjectEntry(ObjectEntry entry) { - var keyNode = visitExpr(entry.getKey()); + var keyNode = symbolTable.enterForEager((scp) -> visitExpr(entry.getKey())); + // var keyNode = visitExpr(entry.getKey()); var member = doVisitObjectEntryBody( @@ -2370,9 +2496,19 @@ private ObjectMember doVisitObjectMethod( var frameDescriptorBuilder = createFrameDescriptorBuilder(paramList); + var bindings = new ArrayList(); + for (var param : paramList.getParameters()) { + if (param instanceof Parameter.TypedIdentifier id) { + bindings.add(id.getIdentifier().getValue()); + } else { + bindings.add("_"); + } + } + return symbolTable.enterMethod( methodName, getConstLevel(modifiers), + bindings, frameDescriptorBuilder, List.of(), scope -> { 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..03bb7de62 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/MethodResolution.java @@ -0,0 +1,29 @@ +/* + * 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.ExpressionNode; +import org.pkl.core.ast.member.ClassMethod; + +public sealed interface MethodResolution { + + record DirectMethod(ClassMethod method, ExpressionNode receiver, boolean isBase) + implements MethodResolution {} + + record LexicalMethod(boolean isConst) implements MethodResolution {} + + record VirtualMethod(boolean isConst) implements MethodResolution {} +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/PropertyResolution.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/PropertyResolution.java new file mode 100644 index 000000000..af52de1c4 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/PropertyResolution.java @@ -0,0 +1,29 @@ +/* + * 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.runtime.Identifier; + +public sealed interface PropertyResolution { + + record ConstantProperty(Object constant) implements PropertyResolution {} + + record LocalClassProperty(Identifier name, boolean isConst) implements PropertyResolution {} + + record NormalClassProperty(Identifier name, boolean isConst) implements PropertyResolution {} + + record LetOrLambdaProperty(Identifier name) implements PropertyResolution {} +} 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..fe3a8dd68 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 @@ -21,21 +21,44 @@ import java.util.function.Function; import org.pkl.core.TypeParameter; import org.pkl.core.ast.ConstantNode; +import org.pkl.core.ast.ConstantValueNode; import org.pkl.core.ast.ExpressionNode; +import org.pkl.core.ast.VmModifier; +import org.pkl.core.ast.builder.MethodResolution.DirectMethod; +import org.pkl.core.ast.builder.MethodResolution.LexicalMethod; +import org.pkl.core.ast.builder.MethodResolution.VirtualMethod; +import org.pkl.core.ast.builder.PropertyResolution.ConstantProperty; +import org.pkl.core.ast.builder.PropertyResolution.LetOrLambdaProperty; +import org.pkl.core.ast.builder.PropertyResolution.LocalClassProperty; +import org.pkl.core.ast.builder.PropertyResolution.NormalClassProperty; import org.pkl.core.ast.expression.generator.GeneratorMemberNode; +import org.pkl.core.ast.expression.primary.PartiallyResolvedVariable; import org.pkl.core.ast.member.ObjectMember; 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.runtime.VmTyped; import org.pkl.core.util.Nullable; import org.pkl.parser.Lexer; +import org.pkl.parser.syntax.Class; +import org.pkl.parser.syntax.ClassMethod; +import org.pkl.parser.syntax.Modifier; +import org.pkl.parser.syntax.Modifier.ModifierValue; +import org.pkl.parser.syntax.ObjectBody; +import org.pkl.parser.syntax.Parameter; public final class SymbolTable { private Scope currentScope; - public SymbolTable(ModuleInfo moduleInfo) { - currentScope = new ModuleScope(moduleInfo); + public SymbolTable(ModuleInfo moduleInfo, @Nullable org.pkl.parser.syntax.Module module) { + currentScope = new ModuleScope(moduleInfo, module); + } + + public SymbolTable( + ModuleInfo moduleInfo, @Nullable org.pkl.parser.syntax.Module module, VmTyped base) { + var baseScope = new BaseScope(base); + currentScope = new ModuleScope(moduleInfo, module, baseScope); } public Scope getCurrentScope() { @@ -53,6 +76,7 @@ public Scope getCurrentScope() { public ObjectMember enterClass( Identifier name, + Class clazz, List typeParameters, Function nodeFactory) { return doEnter( @@ -60,6 +84,7 @@ public ObjectMember enterClass( currentScope, name, toQualifiedName(name), + clazz, FrameDescriptor.newBuilder(), typeParameters), nodeFactory); @@ -82,6 +107,7 @@ public ObjectMember enterTypeAlias( public T enterMethod( Identifier name, ConstLevel constLevel, + List bindings, Builder frameDescriptorBuilder, List typeParameters, Function nodeFactory) { @@ -91,12 +117,19 @@ public T enterMethod( name, toQualifiedName(name), constLevel, + bindings, frameDescriptorBuilder, typeParameters), nodeFactory); } + public T enterForEager(Function nodeFactory) { + return doEnter(new ForEagerScope(currentScope, currentScope.qualifiedName), nodeFactory); + } + public T enterForGenerator( + List params, + int nestLevel, FrameDescriptor.Builder frameDescriptorBuilder, FrameDescriptor.Builder memberDescriptorBuilder, Function nodeFactory) { @@ -104,13 +137,18 @@ public T enterForGenerator( new ForGeneratorScope( currentScope, currentScope.qualifiedName, + params, + nestLevel, frameDescriptorBuilder, memberDescriptorBuilder), nodeFactory); } public T enterLambda( - FrameDescriptor.Builder frameDescriptorBuilder, Function nodeFactory) { + List bindings, + int slot, + FrameDescriptor.Builder frameDescriptorBuilder, + Function nodeFactory) { // flatten names of lambdas nested inside other lambdas for presentation purposes var parentScope = currentScope; @@ -122,7 +160,8 @@ public T enterLambda( var qualifiedName = parentScope.qualifiedName + "." + parentScope.getNextLambdaName(); return doEnter( - new LambdaScope(currentScope, qualifiedName, frameDescriptorBuilder), nodeFactory); + new LambdaScope(currentScope, bindings, slot, qualifiedName, frameDescriptorBuilder), + nodeFactory); } public T enterProperty( @@ -155,8 +194,9 @@ public T enterAnnotationScope(Function nodeFactory) { new AnnotationScope(currentScope, 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) { @@ -276,7 +316,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 +397,155 @@ public final boolean isForGeneratorScope() { public ConstLevel getConstLevel() { return constLevel; } + + @FunctionalInterface + private interface ResolutionFunction { + @Nullable + T apply(LexicalScope scope, int levelUp); + } + + public @Nullable PartiallyResolvedVariable resolveProperty( + Identifier name, Function fun) { + return resolve((scope, levelUp) -> scope.doResolveProperty(name, levelUp, fun)); + } + + public @Nullable Object resolveMethod(Identifier name, Function fun) { + return resolve((scope, levelUp) -> scope.doResolveMethod(name, levelUp, fun)); + } + + private @Nullable T resolve(ResolutionFunction fun) { + var levelUp = 0; + var shouldSkip = false; + for (var scope = this; scope != null; scope = scope.getParent()) { + // for headers resolve variables one scope up + if (scope instanceof ForEagerScope) { + shouldSkip = true; + continue; + } + if (scope instanceof LexicalScope lex) { + if (shouldSkip && !(scope instanceof ForGeneratorScope)) { + shouldSkip = false; + continue; + } + var result = fun.apply(lex, levelUp); + if (result != null) return result; + if (scope instanceof MethodScope || scope instanceof ForGeneratorScope) { + // fors and methods don't level up + continue; + } + levelUp++; + } + } + return null; + } } - private interface LexicalScope {} + protected interface LexicalScope { + @Nullable + PartiallyResolvedVariable doResolveProperty( + Identifier name, + int levelUp, + Function callback); + + @Nullable + Object doResolveMethod( + Identifier name, int levelUp, Function callback); + } public static class ObjectScope extends Scope implements LexicalScope { - private ObjectScope(Scope parent, Builder frameDescriptorBuilder) { + private final Map params; + private final Map props; + private final Map methods; + + private ObjectScope(Scope parent, ObjectBody body, Builder frameDescriptorBuilder) { super( parent, parent.getNameOrNull(), parent.getQualifiedName(), ConstLevel.NONE, frameDescriptorBuilder); + params = collectParams(body); + props = collectProps(body); + methods = collectMethods(body); + } + + @Override + public @Nullable PartiallyResolvedVariable doResolveProperty( + Identifier name, + int levelUp, + Function callback) { + var strName = name.toString(); + // Underscore is a discard identifier and should not be resolvable + if (strName.equals("_")) { + return null; + } + var prop = props.get(strName); + if (prop != null) { + if (VmModifier.isLocal(prop)) { + return callback.apply( + new LocalClassProperty(name.toLocalProperty(), VmModifier.isConst(prop))); + } else { + return callback.apply(new NormalClassProperty(name, VmModifier.isConst(prop))); + } + } + var paramIndex = params.get(strName); + if (paramIndex != null) { + // params are on a higher level than the properties + return callback.apply(new LetOrLambdaProperty(name)); + } + return null; + } + + @Override + public @Nullable Object doResolveMethod( + Identifier name, int levelUp, Function callback) { + var method = methods.get(name.toString()); + if (method != null) { + // Object methods are always local + return callback.apply(new LexicalMethod(VmModifier.isConst(method))); + } + 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 Parameter.TypedIdentifier ti) { + params.put(ti.getIdentifier().getValue(), i); + } else { + params.put("_", i); + } + } + return params; + } + + private static Map collectProps(ObjectBody body) { + var props = new HashMap(); + for (var member : body.getMembers()) { + if (member instanceof org.pkl.parser.syntax.ObjectMember.ObjectProperty prop) { + props.put(prop.getIdentifier().getValue(), modifiers(prop.getModifiers())); + } + } + return props; + } + + private static Map collectMethods(ObjectBody body) { + var methods = new HashMap(); + for (var member : body.getMembers()) { + if (member instanceof org.pkl.parser.syntax.ObjectMember.ObjectMethod method) { + methods.put(method.getIdentifier().getValue(), modifiers(method.getModifiers())); + } + } + return methods; + } + + private static int modifiers(List modifiers) { + int res = 0; + for (var mod : modifiers) { + res += AstBuilder.toModifier(mod); + } + return res; } } @@ -394,42 +574,231 @@ public TypeParameterizableScope( public static final class ModuleScope extends Scope implements LexicalScope { private final ModuleInfo moduleInfo; + private final Map> properties; + private final Map methods; - public ModuleScope(ModuleInfo moduleInfo) { + public ModuleScope(ModuleInfo moduleInfo, @Nullable org.pkl.parser.syntax.Module module) { super(null, null, moduleInfo.getModuleName(), ConstLevel.NONE, FrameDescriptor.newBuilder()); this.moduleInfo = moduleInfo; + this.properties = module != null ? collectProperties(module) : Map.of(); + this.methods = module != null ? collectMethods(module) : Map.of(); + } + + // modules other than base have pkl:base as their parent + public ModuleScope( + ModuleInfo moduleInfo, @Nullable org.pkl.parser.syntax.Module module, BaseScope base) { + super(base, null, moduleInfo.getModuleName(), ConstLevel.NONE, FrameDescriptor.newBuilder()); + this.moduleInfo = moduleInfo; + this.properties = module != null ? collectProperties(module) : Map.of(); + this.methods = module != null ? collectMethods(module) : Map.of(); + } + + @Override + public @Nullable PartiallyResolvedVariable doResolveProperty( + Identifier name, + int levelUp, + Function callback) { + var modifiers = properties.get(name); + if (modifiers == null) return null; + if (isLocal(modifiers)) { + return callback.apply(new LocalClassProperty(name.toLocalProperty(), isConst(modifiers))); + } else { + return callback.apply(new NormalClassProperty(name, isConst(modifiers))); + } + } + + @Override + public @Nullable Object doResolveMethod( + Identifier name, int levelUp, Function callback) { + var method = methods.get(name); + if (method == null) return null; + var methodIsConst = isConst(method.getModifiers()); + if (isLocal(method.getModifiers())) { + return callback.apply(new LexicalMethod(methodIsConst)); + } else { + return callback.apply(new VirtualMethod(methodIsConst)); + } + } + + private static Map> collectProperties( + org.pkl.parser.syntax.Module module) { + var map = new HashMap>(); + for (var prop : module.getProperties()) { + map.put(Identifier.get(prop.getName().getValue()), prop.getModifiers()); + } + return map; + } + + private static Map collectMethods( + org.pkl.parser.syntax.Module module) { + var map = new HashMap(); + for (var method : module.getMethods()) { + map.put(Identifier.get(method.getName().getValue()), method); + } + return map; + } + + private static boolean isLocal(List modifiers) { + for (var modifier : modifiers) { + if (modifier.getValue() == ModifierValue.LOCAL) return true; + } + return false; + } + + private static boolean isConst(List modifiers) { + for (var modifier : modifiers) { + if (modifier.getValue() == ModifierValue.CONST) return true; + } + return false; } } - public static final class MethodScope extends TypeParameterizableScope { + // The scope of pkl:base, implicitly imported in every file + public static final class BaseScope extends Scope implements LexicalScope { + private final VmTyped base; + + public BaseScope(VmTyped base) { + super(null, null, "base", ConstLevel.NONE, FrameDescriptor.newBuilder()); + this.base = base; + } + + @Override + public @Nullable PartiallyResolvedVariable doResolveProperty( + Identifier name, + int levelUp, + Function callback) { + var cachedValue = base.getCachedValue(name); + if (cachedValue != null) { + return callback.apply(new ConstantProperty(cachedValue)); + } + + var member = base.getMember(name); + + if (member != null) { + var constantValue = member.getConstantValue(); + if (constantValue != null) { + base.setCachedValue(name, constantValue); + return callback.apply(new ConstantProperty(constantValue)); + } + + var computedValue = member.getCallTarget().call(base, base); + base.setCachedValue(name, computedValue); + return callback.apply(new ConstantProperty(computedValue)); + } + return null; + } + + @Override + public @Nullable Object doResolveMethod( + Identifier name, int levelUp, Function callback) { + var method = base.getVmClass().getDeclaredMethod(name); + if (method != null) { + assert !method.isLocal(); + return callback.apply(new DirectMethod(method, new ConstantValueNode(base), true)); + } + return null; + } + } + + public static final class MethodScope extends TypeParameterizableScope implements LexicalScope { + private final List bindings; + public MethodScope( Scope parent, Identifier name, String qualifiedName, ConstLevel constLevel, + List bindings, Builder frameDescriptorBuilder, List typeParameters) { super(parent, name, qualifiedName, constLevel, frameDescriptorBuilder, typeParameters); + this.bindings = bindings; + } + + @Override + public @Nullable PartiallyResolvedVariable doResolveProperty( + Identifier name, + int levelUp, + Function callback) { + var nameStr = name.toString(); + if (nameStr.equals("_")) { + return null; + } + var index = bindings.indexOf(nameStr); + if (index != -1) { + return callback.apply(new LetOrLambdaProperty(name)); + } + return null; + } + + @Override + public @Nullable Object doResolveMethod( + Identifier name, int levelUp, Function callback) { + return null; } } public static final class LambdaScope extends Scope implements LexicalScope { + private final List bindings; + private final int slot; + public LambdaScope( - Scope parent, String qualifiedName, FrameDescriptor.Builder frameDescriptorBuilder) { - super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder); + Scope parent, + List bindings, + int slot, + String qualifiedName, + FrameDescriptor.Builder frameDescriptorBuilder) { + super(parent, null, qualifiedName, parent.getConstLevel(), frameDescriptorBuilder); + this.bindings = bindings; + this.slot = slot; + } + + @Override + public @Nullable PartiallyResolvedVariable doResolveProperty( + Identifier name, + int levelUp, + Function callback) { + var nameStr = name.toString(); + if (nameStr.equals("_")) { + return null; + } + var index = bindings.indexOf(nameStr); + if (index != -1) { + return callback.apply(new LetOrLambdaProperty(name)); + } + return null; + } + + @Override + public @Nullable Object doResolveMethod( + Identifier name, int levelUp, Function callback) { + return null; + } + } + + // A scope used only for variable resolution + public static final class ForEagerScope extends Scope { + private ForEagerScope(@Nullable Scope parent, String qualifiedName) { + super(parent, null, qualifiedName, ConstLevel.NONE, FrameDescriptor.newBuilder()); } } public static final class ForGeneratorScope extends Scope implements LexicalScope { private final FrameDescriptor.Builder memberDescriptorBuilder; + final List params; + private final int nestLevel; public ForGeneratorScope( Scope parent, String qualifiedName, + List params, + int nestLevel, FrameDescriptor.Builder frameDescriptorBuilder, FrameDescriptor.Builder memberDescriptorBuilder) { super(parent, null, qualifiedName, ConstLevel.NONE, frameDescriptorBuilder); this.memberDescriptorBuilder = memberDescriptorBuilder; + this.params = params; + this.nestLevel = nestLevel; } public FrameDescriptor buildMemberDescriptor() { @@ -442,6 +811,28 @@ protected String getNextEntryName(@Nullable ExpressionNode keyNode) { assert parent != null; return parent.getNextEntryName(keyNode); } + + @Override + public @Nullable PartiallyResolvedVariable doResolveProperty( + Identifier name, + int levelUp, + Function callback) { + var nameStr = name.toString(); + if (nameStr.equals("_")) { + return null; + } + var index = params.indexOf(nameStr); + if (index >= 0) { + return callback.apply(new LetOrLambdaProperty(name)); + } + return null; + } + + @Override + public @Nullable Object doResolveMethod( + Identifier name, int levelUp, Function callback) { + return null; + } } public static final class PropertyScope extends Scope { @@ -463,13 +854,94 @@ public EntryScope( } public static final class ClassScope extends TypeParameterizableScope implements LexicalScope { + private final Map> properties; + private final Map methods; + private final boolean isClosed; + public ClassScope( Scope parent, Identifier name, String qualifiedName, + Class clazz, Builder frameDescriptorBuilder, List typeParameters) { super(parent, name, qualifiedName, ConstLevel.MODULE, frameDescriptorBuilder, typeParameters); + properties = collectProperties(clazz); + methods = collectMethods(clazz); + isClosed = isClosed(clazz); + } + + @Override + public @Nullable PartiallyResolvedVariable doResolveProperty( + Identifier name, + int levelUp, + Function callback) { + + var modifiers = properties.get(name); + if (modifiers == null) return null; + if (isLocal(modifiers)) { + return callback.apply(new LocalClassProperty(name.toLocalProperty(), isConst(modifiers))); + } else { + return callback.apply(new NormalClassProperty(name, isConst(modifiers))); + } + } + + @Override + public @Nullable Object doResolveMethod( + Identifier name, int levelUp, Function callback) { + + var method = methods.get(name); + if (method == null) return null; + var methodIsConst = isConst(method.getModifiers()); + if (isClosed || isLocal(method.getModifiers())) { + return callback.apply(new LexicalMethod(methodIsConst)); + } else { + return callback.apply(new VirtualMethod(methodIsConst)); + } + } + + private static Map> collectProperties(Class clazz) { + var map = new HashMap>(); + var body = clazz.getBody(); + if (body == null) return map; + for (var prop : body.getProperties()) { + map.put(Identifier.get(prop.getName().getValue()), prop.getModifiers()); + } + return map; + } + + private static Map collectMethods(Class clazz) { + var map = new HashMap(); + var body = clazz.getBody(); + if (body == null) return map; + for (var prop : body.getMethods()) { + map.put(Identifier.get(prop.getName().getValue()), prop); + } + return map; + } + + private static boolean isLocal(List modifiers) { + for (var modifier : modifiers) { + if (modifier.getValue() == ModifierValue.LOCAL) return true; + } + return false; + } + + private static boolean isConst(List modifiers) { + for (var modifier : modifiers) { + if (modifier.getValue() == ModifierValue.CONST) return true; + } + return false; + } + + private static boolean isClosed(Class clazz) { + for (var modifier : clazz.getModifiers()) { + if (modifier.getValue() == ModifierValue.OPEN + || modifier.getValue() == ModifierValue.ABSTRACT) { + return false; + } + } + return true; } } @@ -518,5 +990,20 @@ public AnnotationScope(Scope parent, FrameDescriptor.Builder frameDescriptorBuil ConstLevel.MODULE, frameDescriptorBuilder); } + + @Override + public @Nullable PartiallyResolvedVariable doResolveProperty( + Identifier name, + int levelUp, + Function callback) { + // annotation scopes don't have variables, the inner object scope might have + return null; + } + + @Override + public @Nullable Object doResolveMethod( + Identifier name, int levelUp, Function callback) { + return null; + } } } 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 index e859bf06c..c7ec3750b 100644 --- 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 @@ -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. @@ -34,7 +34,7 @@ public final class InvokeMethodLexicalNode extends ExpressionNode { @Child private DirectCallNode callNode; - InvokeMethodLexicalNode( + public InvokeMethodLexicalNode( SourceSection sourceSection, CallTarget callTarget, int levelsUp, 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..6e07885f4 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,33 +16,33 @@ package org.pkl.core.ast.expression.member; import com.oracle.truffle.api.CompilerAsserts; +import com.oracle.truffle.api.CompilerDirectives; 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; +import org.pkl.core.util.Nullable; /** 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 ObjectMember property; @Child private DirectCallNode callNode; - public ReadLocalPropertyNode(SourceSection sourceSection, ObjectMember property, int levelsUp) { + public ReadLocalPropertyNode(SourceSection sourceSection, Identifier name, int levelsUp) { 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()); } @Override @@ -61,6 +61,18 @@ public Object executeGeneric(VirtualFrame frame) { receiver = owner.getEnclosingReceiver(); owner = owner.getEnclosingOwner(); + assert owner != null; + } + var constantValue = getProperty(frame); + if (constantValue != null) { + return constantValue; + } + + var property = owner.getMember(name); + if (property == null) { + // should never happen + CompilerDirectives.transferToInterpreter(); + throw new PklBugException("Couldn't find local variable `" + name + "`."); } assert receiver instanceof VmObjectLike @@ -76,4 +88,32 @@ public Object executeGeneric(VirtualFrame frame) { return result; } + + private @Nullable Object getProperty(VirtualFrame frame) { + if (property != null) return property.getConstantValue(); + + var currFrame = frame; + var currOwner = VmUtils.getOwner(currFrame); + + do { + var localMember = currOwner.getMember(name); + if (localMember != null) { + assert localMember.isLocal(); + + var value = localMember.getConstantValue(); + if (value != null) { + return value; + } + + property = localMember; + callNode = DirectCallNode.create(property.getCallTarget()); + insert(callNode); + break; + } + + currFrame = currOwner.getEnclosingFrame(); + currOwner = VmUtils.getOwnerOrNull(currFrame); + } while (currOwner != null); + return null; + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/PartiallyResolvedMethod.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/PartiallyResolvedMethod.java new file mode 100644 index 000000000..3f996d9bb --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/PartiallyResolvedMethod.java @@ -0,0 +1,25 @@ +/* + * 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.expression.primary; + +import org.pkl.core.runtime.Identifier; + +public sealed interface PartiallyResolvedMethod { + + record LexicalMethodVar(Identifier name, boolean isConst) implements PartiallyResolvedMethod {} + + record VirtualMethodVar(Identifier name, boolean isConst) implements PartiallyResolvedMethod {} +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/PartiallyResolvedVariable.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/PartiallyResolvedVariable.java new file mode 100644 index 000000000..26baf458b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/PartiallyResolvedVariable.java @@ -0,0 +1,29 @@ +/* + * 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.expression.primary; + +import org.pkl.core.runtime.Identifier; + +public sealed interface PartiallyResolvedVariable { + + record ConstantVar(Object value) implements PartiallyResolvedVariable {} + + record LocalPropertyVar(Identifier name, boolean isConst) implements PartiallyResolvedVariable {} + + record PropertyVar(Identifier name, boolean isConst) implements PartiallyResolvedVariable {} + + record FrameSlotVar(Identifier name) implements PartiallyResolvedVariable {} +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveParseTimeMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveParseTimeMethodNode.java new file mode 100644 index 000000000..61a13b0e6 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveParseTimeMethodNode.java @@ -0,0 +1,173 @@ +/* + * 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.expression.primary; + +import com.oracle.truffle.api.CallTarget; +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.PklBugException; +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.InvokeMethodLexicalNode; +import org.pkl.core.ast.expression.member.InvokeMethodVirtualNodeGen; +import org.pkl.core.ast.internal.GetClassNodeGen; +import org.pkl.core.ast.member.Member; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmObjectLike; +import org.pkl.core.runtime.VmUtils; + +/** + * Resolves a method that was partially resolved at parse time. This node is needed because mixins + * and function amending may introduce further level ups that cannot be calculated at parse time. + */ +public final class ResolveParseTimeMethodNode extends ExpressionNode { + private final PartiallyResolvedMethod pmeth; + @Children private final ExpressionNode[] argumentNodes; + private final ConstLevel constLevel; + private final int constDepth; + private final Identifier methodName; + + public ResolveParseTimeMethodNode( + PartiallyResolvedMethod pmeth, + SourceSection sourceSection, + ExpressionNode[] argumentNodes, + ConstLevel constLevel, + int constDepth, + Identifier methodName) { + super(sourceSection); + this.pmeth = pmeth; + this.argumentNodes = argumentNodes; + this.constLevel = constLevel; + this.constDepth = constDepth; + this.methodName = methodName; + } + + @Override + public Object executeGeneric(VirtualFrame frame) { + return replace(doResolve(frame)).executeGeneric(frame); + } + + private ExpressionNode doResolve(VirtualFrame frame) { + CompilerDirectives.transferToInterpreter(); + + if (pmeth instanceof PartiallyResolvedMethod.LexicalMethodVar) { + return resolveLexicalMethod(frame); + } + + if (pmeth instanceof PartiallyResolvedMethod.VirtualMethodVar) { + return resolveVirtualMethod(frame); + } + + throw PklBugException.unreachableCode(); + } + + private ExpressionNode resolveLexicalMethod(VirtualFrame frame) { + var lmvar = (PartiallyResolvedMethod.LexicalMethodVar) pmeth; + var localMethodName = methodName.toLocalMethod(); + var currOwner = VmUtils.getOwner(frame); + var levelsUp = 0; + + do { + if (currOwner.isPrototype()) { + var localMethod = currOwner.getVmClass().getDeclaredMethod(localMethodName); + if (localMethod != null) { + assert localMethod.isLocal(); + if (!lmvar.isConst()) { + 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(); + if (!lmvar.isConst()) { + checkConst(currOwner, method, levelsUp); + } + return new InvokeMethodLexicalNode( + sourceSection, method.getCallTarget(sourceSection), levelsUp, argumentNodes); + } + } else { + var localMethod = currOwner.getMember(localMethodName); + if (localMethod != null) { + assert localMethod.isLocal(); + if (!lmvar.isConst()) { + checkConst(currOwner, localMethod, levelsUp); + } + var methodCallTarget = + (CallTarget) localMethod.getCallTarget().call(currOwner, currOwner); + return new InvokeMethodLexicalNode( + sourceSection, methodCallTarget, levelsUp, argumentNodes); + } + } + + currOwner = currOwner.getEnclosingOwner(); + levelsUp += 1; + } while (currOwner != null); + + throw PklBugException.unreachableCode(); + } + + private ExpressionNode resolveVirtualMethod(VirtualFrame frame) { + var vmvar = (PartiallyResolvedMethod.VirtualMethodVar) pmeth; + var currOwner = VmUtils.getOwner(frame); + var levelsUp = 0; + + do { + if (currOwner.isPrototype()) { + var method = currOwner.getVmClass().getDeclaredMethod(methodName); + if (method != null) { + assert !method.isLocal(); + if (!vmvar.isConst()) { + checkConst(currOwner, method, levelsUp); + } + //noinspection ConstantConditions + return InvokeMethodVirtualNodeGen.create( + sourceSection, + methodName, + argumentNodes, + MemberLookupMode.IMPLICIT_LEXICAL, + levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp), + GetClassNodeGen.create(null)); + } + } + + currOwner = currOwner.getEnclosingOwner(); + levelsUp += 1; + } while (currOwner != null); + + throw PklBugException.unreachableCode(); + } + + @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/ResolveParseTimeVariableNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveParseTimeVariableNode.java new file mode 100644 index 000000000..316f3abd4 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/primary/ResolveParseTimeVariableNode.java @@ -0,0 +1,183 @@ +/* + * 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.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.PklBugException; +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.Identifier; +import org.pkl.core.runtime.VmObjectLike; +import org.pkl.core.runtime.VmUtils; + +/** + * Resolves a variable that was partially resolved at parse time. This node is needed mainly because + * of two reasons: + * + *
    + *
  1. Frame slot nodes cannot always be resolved at parse time. + *
  2. Mixins/function amending may introduce further level ups that cannot be calculated at parse + * time + *
+ */ +public final class ResolveParseTimeVariableNode extends ExpressionNode { + private final PartiallyResolvedVariable pvar; + private final ConstLevel constLevel; + private final int constDepth; + private final Identifier variableName; + + public ResolveParseTimeVariableNode( + PartiallyResolvedVariable pvar, + SourceSection sourceSection, + ConstLevel constLevel, + int constDepth, + Identifier variableName) { + super(sourceSection); + this.pvar = pvar; + this.constLevel = constLevel; + this.constDepth = constDepth; + this.variableName = variableName; + } + + @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(); + + if (pvar instanceof PartiallyResolvedVariable.ConstantVar cvar) { + return new ConstantValueNode(sourceSection, cvar.value()); + } + + if (pvar instanceof PartiallyResolvedVariable.LocalPropertyVar lpvar) { + var name = lpvar.name(); + var currFrame = frame; + var currOwner = VmUtils.getOwner(currFrame); + var levelsUp = 0; + + do { + var localMember = currOwner.getMember(name); + if (localMember != null) { + assert localMember.isLocal(); + + if (!lpvar.isConst()) { + checkConst(currOwner, localMember, levelsUp); + } + + var value = localMember.getConstantValue(); + if (value != null) { + return new ConstantValueNode(sourceSection, value); + } + + return new ReadLocalPropertyNode(sourceSection, name, levelsUp); + } + + currFrame = currOwner.getEnclosingFrame(); + currOwner = VmUtils.getOwnerOrNull(currFrame); + levelsUp += 1; + } while (currOwner != null); + + throw PklBugException.unreachableCode(); + } + + if (pvar instanceof PartiallyResolvedVariable.PropertyVar ppvar) { + var name = ppvar.name(); + var currFrame = frame; + var currOwner = VmUtils.getOwner(currFrame); + var levelsUp = 0; + + do { + var member = currOwner.getMember(name); + if (member != null) { + assert !member.isLocal(); + + if (!ppvar.isConst()) { + checkConst(currOwner, member, levelsUp); + } + + return ReadPropertyNodeGen.create( + sourceSection, + name, + MemberLookupMode.IMPLICIT_LEXICAL, + false, + levelsUp == 0 ? new GetReceiverNode() : new GetEnclosingReceiverNode(levelsUp)); + } + + currFrame = currOwner.getEnclosingFrame(); + currOwner = VmUtils.getOwnerOrNull(currFrame); + levelsUp += 1; + } while (currOwner != null); + + throw PklBugException.unreachableCode(); + } + + if (pvar instanceof PartiallyResolvedVariable.FrameSlotVar flvar) { + var fsvName = flvar.name(); + var localPropertyName = fsvName.toLocalProperty(); + var currFrame = frame; + var currOwner = VmUtils.getOwner(currFrame); + var levelsUp = 0; + + do { + var slot = ResolveVariableNode.findFrameSlot(currFrame, fsvName, localPropertyName); + if (slot != -1) { + return levelsUp == 0 + ? ReadFrameSlotNodeGen.create(getSourceSection(), slot) + : ReadEnclosingFrameSlotNodeGen.create(getSourceSection(), slot, levelsUp); + } + + currFrame = currOwner.getEnclosingFrame(); + currOwner = VmUtils.getOwnerOrNull(currFrame); + levelsUp += 1; + } while (currOwner != null); + + // if this variable was resolved at parse time, this should never happen + throw PklBugException.unreachableCode(); + } + + throw PklBugException.unreachableCode(); + } + + @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(); + } + } +} 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 index e4b38e285..d6302f5bd 100644 --- 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 @@ -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. @@ -117,7 +117,7 @@ private ExpressionNode doResolve(VirtualFrame frame) { return new ConstantValueNode(sourceSection, value); } - return new ReadLocalPropertyNode(sourceSection, localMember, levelsUp); + return new ReadLocalPropertyNode(sourceSection, localPropertyName, levelsUp); } var member = currOwner.getMember(variableName); @@ -213,7 +213,7 @@ private void checkConst(VmObjectLike currOwner, Member member, int levelsUp) { } } - private static int findFrameSlot(VirtualFrame frame, Object identifier1, Object identifier2) { + public 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.