Improve evaluation of typealiases (#1796)

This simplifies the resolution of names in typealias constraints.
This removes the existing logic around swapping out the frame's
owner/receiver, and instead favors resolving the variable at parse time.

* During variable resolution, create read variable nodes that read off of
  a receiver node
* Introduce `GetTypeAliasModuleNode` that provides the typealias's
  enclosing module
* Inject the enclosing module during typealias instantiation
This commit is contained in:
Daniel Chao
2026-07-30 17:22:45 +00:00
committed by GitHub
parent 2ca2aaf9ec
commit a55a3d7c33
39 changed files with 634 additions and 139 deletions
@@ -112,14 +112,17 @@ import org.pkl.core.ast.expression.literal.TrueLiteralNode;
import org.pkl.core.ast.expression.member.InferParentWithinMethodNode; import org.pkl.core.ast.expression.member.InferParentWithinMethodNode;
import org.pkl.core.ast.expression.member.InferParentWithinObjectMethodNode; import org.pkl.core.ast.expression.member.InferParentWithinObjectMethodNode;
import org.pkl.core.ast.expression.member.InferParentWithinPropertyNodeGen; import org.pkl.core.ast.expression.member.InferParentWithinPropertyNodeGen;
import org.pkl.core.ast.expression.member.InvokeClassMethodNode; import org.pkl.core.ast.expression.member.InvokeLexicalClassMethodNode;
import org.pkl.core.ast.expression.member.InvokeLexicalObjectMethodNode;
import org.pkl.core.ast.expression.member.InvokeMethodDirectNode; import org.pkl.core.ast.expression.member.InvokeMethodDirectNode;
import org.pkl.core.ast.expression.member.InvokeMethodVirtualNodeGen; import org.pkl.core.ast.expression.member.InvokeMethodVirtualNodeGen;
import org.pkl.core.ast.expression.member.InvokeObjectMethodNode; import org.pkl.core.ast.expression.member.InvokeQualifiedClassMethodNode;
import org.pkl.core.ast.expression.member.InvokeQualifiedObjectMethodNode;
import org.pkl.core.ast.expression.member.InvokeSuperMethodNodeGen; import org.pkl.core.ast.expression.member.InvokeSuperMethodNodeGen;
import org.pkl.core.ast.expression.member.ReadAmbiguousLocalityPropertyNode; import org.pkl.core.ast.expression.member.ReadAmbiguousLocalityPropertyNode;
import org.pkl.core.ast.expression.member.ReadLocalPropertyNode; import org.pkl.core.ast.expression.member.ReadLexicalLocalPropertyNode;
import org.pkl.core.ast.expression.member.ReadPropertyNodeGen; import org.pkl.core.ast.expression.member.ReadPropertyNodeGen;
import org.pkl.core.ast.expression.member.ReadQualifiedLocalPropertyNode;
import org.pkl.core.ast.expression.member.ReadSuperEntryNode; import org.pkl.core.ast.expression.member.ReadSuperEntryNode;
import org.pkl.core.ast.expression.member.ReadSuperPropertyNode; import org.pkl.core.ast.expression.member.ReadSuperPropertyNode;
import org.pkl.core.ast.expression.primary.ExecuteCustomThisWithRootNode; import org.pkl.core.ast.expression.primary.ExecuteCustomThisWithRootNode;
@@ -128,6 +131,7 @@ import org.pkl.core.ast.expression.primary.GetMemberKeyNode;
import org.pkl.core.ast.expression.primary.GetModuleNode; import org.pkl.core.ast.expression.primary.GetModuleNode;
import org.pkl.core.ast.expression.primary.GetOwnerNode; import org.pkl.core.ast.expression.primary.GetOwnerNode;
import org.pkl.core.ast.expression.primary.GetReceiverNode; import org.pkl.core.ast.expression.primary.GetReceiverNode;
import org.pkl.core.ast.expression.primary.GetTypeAliasModuleNode;
import org.pkl.core.ast.expression.primary.OuterNode; import org.pkl.core.ast.expression.primary.OuterNode;
import org.pkl.core.ast.expression.primary.ThisNode; import org.pkl.core.ast.expression.primary.ThisNode;
import org.pkl.core.ast.expression.ternary.IfElseNode; import org.pkl.core.ast.expression.ternary.IfElseNode;
@@ -526,7 +530,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
} }
@Override @Override
public GetModuleNode visitModuleExpr(ModuleExpr expr) { public ExpressionNode visitModuleExpr(ModuleExpr expr) {
var currentScope = symbolTable.getCurrentScope(); var currentScope = symbolTable.getCurrentScope();
// cannot use unqualified `module` in a const context // cannot use unqualified `module` in a const context
if (currentScope.getConstLevel().isConst() && !(expr.parent() instanceof QualifiedAccessExpr)) { if (currentScope.getConstLevel().isConst() && !(expr.parent() instanceof QualifiedAccessExpr)) {
@@ -549,7 +553,9 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
.withSourceSection(createSourceSection(expr)) .withSourceSection(createSourceSection(expr))
.build(); .build();
} }
return new GetModuleNode(createSourceSection(expr)); return symbolTable.isInTypeAliasScope
? new GetTypeAliasModuleNode(createSourceSection(expr))
: new GetModuleNode(createSourceSection(expr));
} }
@Override @Override
@@ -687,12 +693,30 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
case MODULE -> p.isModuleScope(); case MODULE -> p.isModuleScope();
case ALL -> p.levelsUp() > constDepth; case ALL -> p.levelsUp() > constDepth;
}; };
// Assumption: typealiases can only be declared on the module.
// If we ever allow typealiases in classes, this code needs to change.
if (symbolTable.isInTypeAliasScope && p.isModuleScope()) {
var getModuleNode = new GetTypeAliasModuleNode(sourceSection);
if (p.isLocal()) {
return new ReadQualifiedLocalPropertyNode(
sourceSection,
org.pkl.core.runtime.Identifier.localProperty(name),
needsConst,
getModuleNode);
}
return ReadPropertyNodeGen.create(
sourceSection,
org.pkl.core.runtime.Identifier.get(name),
MemberLookupMode.IMPLICIT_LEXICAL,
needsConst,
getModuleNode);
}
if (p.isAmbiguousLocality()) { if (p.isAmbiguousLocality()) {
return new ReadAmbiguousLocalityPropertyNode( return new ReadAmbiguousLocalityPropertyNode(
sourceSection, org.pkl.core.runtime.Identifier.get(name), p.levelsUp(), needsConst); sourceSection, org.pkl.core.runtime.Identifier.get(name), p.levelsUp(), needsConst);
} }
if (p.isLocal()) { if (p.isLocal()) {
return new ReadLocalPropertyNode( return new ReadLexicalLocalPropertyNode(
sourceSection, sourceSection,
org.pkl.core.runtime.Identifier.localProperty(name), org.pkl.core.runtime.Identifier.localProperty(name),
p.levelsUp(), p.levelsUp(),
@@ -769,11 +793,34 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
case MODULE -> method.isModuleScope(); case MODULE -> method.isModuleScope();
case ALL -> method.levelsUp() > constDepth; case ALL -> method.levelsUp() > constDepth;
}; };
// Assumption: typealiases can only be declared on the module.
// If we ever allow typealiases in classes, this code needs to change.
if (symbolTable.isInTypeAliasScope && method.isModuleScope()) {
var getModuleNode = new GetTypeAliasModuleNode(sourceSection);
if (method.isObjectMethod()) {
return new InvokeQualifiedObjectMethodNode(
sourceSection, identifier, args, needsConst, getModuleNode);
}
if (method.isOnClosedClass() || method.isLocal() || method.isExternal()) {
return new InvokeQualifiedClassMethodNode(
sourceSection, identifier, args, needsConst, getModuleNode);
}
return InvokeMethodVirtualNodeGen.create(
sourceSection,
identifier,
args,
MemberLookupMode.IMPLICIT_LEXICAL,
needsConst,
getModuleNode,
GetClassNodeGen.create(null));
}
if (method.isObjectMethod()) { if (method.isObjectMethod()) {
return new InvokeObjectMethodNode(sourceSection, identifier, levelsUp, args, needsConst); return new InvokeLexicalObjectMethodNode(
sourceSection, identifier, levelsUp, args, needsConst);
} }
if (method.isOnClosedClass() || method.isLocal() || method.isExternal()) { if (method.isOnClosedClass() || method.isLocal() || method.isExternal()) {
return new InvokeClassMethodNode(sourceSection, identifier, levelsUp, args, needsConst); return new InvokeLexicalClassMethodNode(
sourceSection, identifier, levelsUp, args, needsConst);
} }
return InvokeMethodVirtualNodeGen.create( return InvokeMethodVirtualNodeGen.create(
sourceSection, sourceSection,
@@ -47,6 +47,8 @@ import org.pkl.parser.Lexer;
public final class SymbolTable { public final class SymbolTable {
private Scope currentScope; private Scope currentScope;
// consider having each scope keep track of this individually rather than set on SymbolTable.
public boolean isInTypeAliasScope;
public SymbolTable(ModuleInfo moduleInfo, boolean isBaseModule) { public SymbolTable(ModuleInfo moduleInfo, boolean isBaseModule) {
currentScope = new ModuleScope(moduleInfo, isBaseModule); currentScope = new ModuleScope(moduleInfo, isBaseModule);
@@ -85,14 +87,19 @@ public final class SymbolTable {
Identifier name, Identifier name,
List<TypeParameter> typeParameters, List<TypeParameter> typeParameters,
Function<TypeAliasScope, ObjectMember> nodeFactory) { Function<TypeAliasScope, ObjectMember> nodeFactory) {
return doEnter( try {
new TypeAliasScope( this.isInTypeAliasScope = true;
currentScope, return doEnter(
name, new TypeAliasScope(
toQualifiedName(name), currentScope,
new FrameDescriptorBuilder(), name,
typeParameters), toQualifiedName(name),
nodeFactory); new FrameDescriptorBuilder(),
typeParameters),
nodeFactory);
} finally {
this.isInTypeAliasScope = false;
}
} }
public <T> T enterMethod( public <T> T enterMethod(
@@ -0,0 +1,45 @@
/*
* 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.frame.VirtualFrame;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmUtils;
public abstract sealed class AbstractInvokeLexicalMethodNode extends AbstractInvokeMethodNode
permits InvokeLexicalClassMethodNode, InvokeLexicalObjectMethodNode {
private final int levelsUp;
public AbstractInvokeLexicalMethodNode(
SourceSection sourceSection,
Identifier methodName,
int levelsUp,
ExpressionNode[] argumentNodes,
boolean needsConst) {
super(sourceSection, methodName, argumentNodes, needsConst);
this.levelsUp = levelsUp;
}
@Override
public final Object executeGeneric(VirtualFrame frame) {
var capturedFrame = VmUtils.getFrame(frame, levelsUp);
var owner = VmUtils.getOwner(capturedFrame);
var receiver = VmUtils.getReceiver(capturedFrame);
return invoke(frame, owner, receiver);
}
}
@@ -26,40 +26,40 @@ import org.jspecify.annotations.Nullable;
import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.Identifier; import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmObjectLike; import org.pkl.core.runtime.VmObjectLike;
import org.pkl.core.runtime.VmUtils;
public abstract sealed class AbstractInvokeMethodLexicalNode extends ExpressionNode /**
permits InvokeObjectMethodNode, InvokeClassMethodNode { * A non-virtual (statically dispatched) method call.
*
* <p>Subclasses differ only in how they obtain the {@code owner}/{@code receiver} that the method
* is invoked on: either by walking the frame chain ({@link InvokeLexicalClassMethodNode}, {@link
* InvokeLexicalObjectMethodNode}), or off of an explicit receiver expression ({@link
* InvokeQualifiedClassMethodNode}, {@link InvokeQualifiedObjectMethodNode}).
*/
public abstract sealed class AbstractInvokeMethodNode extends ExpressionNode
permits AbstractInvokeQualifiedMethodNode, AbstractInvokeLexicalMethodNode {
protected final Identifier methodName; protected final Identifier methodName;
protected final int levelsUp;
private final boolean needsConst; private final boolean needsConst;
@Children private ExpressionNode[] argumentNodes; @Children private ExpressionNode[] argumentNodes;
@Child private @Nullable DirectCallNode callNode; @Child private @Nullable DirectCallNode callNode;
@CompilationFinal protected boolean isConstChecked; @CompilationFinal protected boolean isConstChecked;
protected AbstractInvokeMethodLexicalNode( protected AbstractInvokeMethodNode(
SourceSection sourceSection, SourceSection sourceSection,
Identifier methodName, Identifier methodName,
int levelsUp,
ExpressionNode[] argumentNodes, ExpressionNode[] argumentNodes,
boolean needsConst) { boolean needsConst) {
super(sourceSection); super(sourceSection);
this.methodName = methodName; this.methodName = methodName;
this.levelsUp = levelsUp;
this.argumentNodes = argumentNodes; this.argumentNodes = argumentNodes;
this.needsConst = needsConst; this.needsConst = needsConst;
this.isConstChecked = false; this.isConstChecked = false;
} }
@Override
@ExplodeLoop @ExplodeLoop
public final Object executeGeneric(VirtualFrame frame) { protected final Object invoke(VirtualFrame frame, VmObjectLike owner, Object receiver) {
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); checkConst(owner);
var args = new Object[2 + argumentNodes.length];
args[0] = receiver; args[0] = receiver;
args[1] = owner; args[1] = owner;
for (var i = 0; i < argumentNodes.length; i++) { for (var i = 0; i < argumentNodes.length; i++) {
@@ -81,10 +81,6 @@ public abstract sealed class AbstractInvokeMethodLexicalNode extends ExpressionN
protected abstract void doCheckConst(VmObjectLike owner); protected abstract void doCheckConst(VmObjectLike owner);
protected final VmObjectLike getOwner(VirtualFrame frame) {
return VmUtils.getOwner(frame, levelsUp);
}
protected DirectCallNode getCallNode(VmObjectLike owner) { protected DirectCallNode getCallNode(VmObjectLike owner) {
if (callNode == null) { if (callNode == null) {
CompilerDirectives.transferToInterpreterAndInvalidate(); CompilerDirectives.transferToInterpreterAndInvalidate();
@@ -0,0 +1,43 @@
/*
* 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.frame.VirtualFrame;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmTyped;
public abstract sealed class AbstractInvokeQualifiedMethodNode extends AbstractInvokeMethodNode
permits InvokeQualifiedClassMethodNode, InvokeQualifiedObjectMethodNode {
@Child private ExpressionNode getReceiverNode;
protected AbstractInvokeQualifiedMethodNode(
SourceSection sourceSection,
Identifier methodName,
ExpressionNode[] argumentNodes,
boolean needsConst,
ExpressionNode getReceiverNode) {
super(sourceSection, methodName, argumentNodes, needsConst);
this.getReceiverNode = getReceiverNode;
}
@Override
public final Object executeGeneric(VirtualFrame frame) {
var receiver = (VmTyped) getReceiverNode.executeGeneric(frame);
return invoke(frame, receiver, receiver);
}
}
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -15,69 +15,38 @@
*/ */
package org.pkl.core.ast.expression.member; package org.pkl.core.ast.expression.member;
import com.oracle.truffle.api.CompilerAsserts;
import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal; 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.DirectCallNode;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.source.SourceSection; import com.oracle.truffle.api.source.SourceSection;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.pkl.core.PklBugException;
import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.runtime.Identifier; import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmObjectLike; 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 abstract class AbstractReadLocalPropertyNode extends ExpressionNode {
public final class ReadLocalPropertyNode extends ExpressionNode {
private final Identifier name; private final Identifier name;
private final int levelsUp;
private final boolean needsConst; private final boolean needsConst;
@Child private @Nullable DirectCallNode callNode; @Child private @Nullable DirectCallNode callNode;
@CompilationFinal @Nullable private ObjectMember property; @CompilationFinal @Nullable private ObjectMember property;
public ReadLocalPropertyNode( public AbstractReadLocalPropertyNode(
SourceSection sourceSection, Identifier name, int levelsUp, boolean needsConst) { SourceSection sourceSection, Identifier name, boolean needsConst) {
super(sourceSection); super(sourceSection);
CompilerAsserts.neverPartOfCompilation();
this.name = name; this.name = name;
this.levelsUp = levelsUp;
this.needsConst = needsConst; this.needsConst = needsConst;
} }
@Override protected ObjectMember getProperty(VmObjectLike owner) {
@ExplodeLoop
public Object executeGeneric(VirtualFrame frame) {
var owner = VmUtils.getOwner(frame, levelsUp);
var property = getProperty(owner);
var constantValue = property.getConstantValue();
if (constantValue != null) {
return constantValue;
}
var receiver = (VmObjectLike) VmUtils.getReceiver(frame, levelsUp);
var result = receiver.getCachedValue(property);
if (result == null) {
result = getCallNode(property).call(receiver, owner, property.getName());
receiver.setCachedValue(property, result);
}
return result;
}
private ObjectMember getProperty(VmObjectLike owner) {
if (property == null) { if (property == null) {
CompilerDirectives.transferToInterpreterAndInvalidate(); CompilerDirectives.transferToInterpreterAndInvalidate();
property = owner.getMember(name); property = owner.getMember(name);
if (property == null) { if (property == null) {
// should never happen // should never happen
CompilerDirectives.transferToInterpreter(); CompilerDirectives.transferToInterpreter();
throw new PklBugException("Couldn't find local variable `" + name + "`."); throw exceptionBuilder().bug("Couldn't find local variable `" + name + "`.").build();
} }
if (needsConst && !property.isConst()) { if (needsConst && !property.isConst()) {
throw exceptionBuilder().evalError("propertyMustBeConst", name.toString()).build(); throw exceptionBuilder().evalError("propertyMustBeConst", name.toString()).build();
@@ -86,7 +55,7 @@ public final class ReadLocalPropertyNode extends ExpressionNode {
return property; return property;
} }
public DirectCallNode getCallNode(ObjectMember property) { protected DirectCallNode getCallNode(ObjectMember property) {
if (callNode == null) { if (callNode == null) {
CompilerDirectives.transferToInterpreterAndInvalidate(); CompilerDirectives.transferToInterpreterAndInvalidate();
callNode = DirectCallNode.create(property.getCallTarget()); callNode = DirectCallNode.create(property.getCallTarget());
@@ -24,11 +24,9 @@ import org.pkl.core.runtime.VmObjectLike;
/** /**
* A non-virtual call of closed methods (methods whose enclosing class/module is not open nor * A non-virtual call of closed methods (methods whose enclosing class/module is not open nor
* abstract, and is lexically scoped). * abstract, and is lexically scoped).
*
* <p>For local methods, use {@link InvokeObjectMethodNode}.
*/ */
public final class InvokeClassMethodNode extends AbstractInvokeMethodLexicalNode { public final class InvokeLexicalClassMethodNode extends AbstractInvokeLexicalMethodNode {
public InvokeClassMethodNode( public InvokeLexicalClassMethodNode(
SourceSection sourceSection, SourceSection sourceSection,
Identifier methodName, Identifier methodName,
int levelsUp, int levelsUp,
@@ -22,9 +22,9 @@ import org.pkl.core.ast.VmModifier;
import org.pkl.core.runtime.Identifier; import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmObjectLike; import org.pkl.core.runtime.VmObjectLike;
/** A non-virtual call of a local method. */ /** A non-virtual call of an object method that is lexically scoped. */
public final class InvokeObjectMethodNode extends AbstractInvokeMethodLexicalNode { public final class InvokeLexicalObjectMethodNode extends AbstractInvokeLexicalMethodNode {
public InvokeObjectMethodNode( public InvokeLexicalObjectMethodNode(
SourceSection sourceSection, SourceSection sourceSection,
Identifier methodName, Identifier methodName,
int levelsUp, int levelsUp,
@@ -33,6 +33,7 @@ public final class InvokeObjectMethodNode extends AbstractInvokeMethodLexicalNod
super(sourceSection, methodName, levelsUp, argumentNodes, needsConst); super(sourceSection, methodName, levelsUp, argumentNodes, needsConst);
} }
@Override
protected void doCheckConst(VmObjectLike owner) { protected void doCheckConst(VmObjectLike owner) {
var member = owner.getMember(methodName); var member = owner.getMember(methodName);
assert member != null; assert member != null;
@@ -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.runtime.Identifier;
import org.pkl.core.runtime.VmObjectLike;
/** A non-virtual call of a closed class method, invoked off of an explicit receiver. */
public final class InvokeQualifiedClassMethodNode extends AbstractInvokeQualifiedMethodNode {
public InvokeQualifiedClassMethodNode(
SourceSection sourceSection,
Identifier methodName,
ExpressionNode[] argumentNodes,
boolean needsConst,
ExpressionNode getReceiverNode) {
super(sourceSection, methodName, argumentNodes, needsConst, getReceiverNode);
}
@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());
}
}
@@ -0,0 +1,51 @@
/*
* 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 an object method, invoked off of an explicit receiver. */
public final class InvokeQualifiedObjectMethodNode extends AbstractInvokeQualifiedMethodNode {
public InvokeQualifiedObjectMethodNode(
SourceSection sourceSection,
Identifier methodName,
ExpressionNode[] argumentNodes,
boolean needsConst,
ExpressionNode getReceiverNode) {
super(sourceSection, methodName, argumentNodes, needsConst, getReceiverNode);
}
@Override
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);
}
}
@@ -67,7 +67,7 @@ public final class ReadAmbiguousLocalityPropertyNode extends ExpressionNode {
CompilerDirectives.transferToInterpreterAndInvalidate(); CompilerDirectives.transferToInterpreterAndInvalidate();
readLocalPropertyNode = readLocalPropertyNode =
insert( insert(
new ReadLocalPropertyNode( new ReadLexicalLocalPropertyNode(
sourceSection, name.toLocalProperty(), levelsUp, needsConst)); sourceSection, name.toLocalProperty(), levelsUp, needsConst));
} }
return readLocalPropertyNode; return readLocalPropertyNode;
@@ -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.CompilerAsserts;
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.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 ReadLexicalLocalPropertyNode extends AbstractReadLocalPropertyNode {
private final int levelsUp;
public ReadLexicalLocalPropertyNode(
SourceSection sourceSection, Identifier name, int levelsUp, boolean needsConst) {
super(sourceSection, name, needsConst);
CompilerAsserts.neverPartOfCompilation();
this.levelsUp = levelsUp;
}
@Override
@ExplodeLoop
public Object executeGeneric(VirtualFrame frame) {
var owner = VmUtils.getOwner(frame, levelsUp);
var property = getProperty(owner);
var constantValue = property.getConstantValue();
if (constantValue != null) {
return constantValue;
}
var receiver = (VmObjectLike) VmUtils.getReceiver(frame, levelsUp);
var result = receiver.getCachedValue(property);
if (result == null) {
result = getCallNode(property).call(receiver, owner, property.getName());
receiver.setCachedValue(property, result);
}
return result;
}
}
@@ -0,0 +1,53 @@
/*
* 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.frame.VirtualFrame;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmTyped;
/** Reads a local property off of the receiver node. */
public final class ReadQualifiedLocalPropertyNode extends AbstractReadLocalPropertyNode {
@Child private ExpressionNode getReceiverNode;
public ReadQualifiedLocalPropertyNode(
SourceSection sourceSection,
Identifier identifier,
boolean needsConst,
ExpressionNode getReceiverNode) {
super(sourceSection, identifier, needsConst);
this.getReceiverNode = getReceiverNode;
}
@Override
public Object executeGeneric(VirtualFrame frame) {
var receiver = (VmTyped) getReceiverNode.executeGeneric(frame);
var property = getProperty(receiver);
var constantValue = property.getConstantValue();
if (constantValue != null) {
return constantValue;
}
var result = receiver.getCachedValue(property);
if (result == null) {
result = getCallNode(property).call(receiver, receiver, property.getName());
receiver.setCachedValue(property, result);
}
return result;
}
}
@@ -0,0 +1,52 @@
/*
* 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.primary;
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.source.SourceSection;
import org.jspecify.annotations.Nullable;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.VmTyped;
public final class GetTypeAliasModuleNode extends ExpressionNode {
@CompilationFinal private @Nullable VmTyped module;
public GetTypeAliasModuleNode(SourceSection sourceSection) {
super(sourceSection);
}
@Override
public boolean isInstrumentable() {
return false;
}
public void lateInitModule(VmTyped module) {
// must only set this once; the first typealias initialization wins.
// guards against nested typealiases; e.g. `typealias A = module; typealias B = A`
if (this.module != null) return;
CompilerDirectives.transferToInterpreterAndInvalidate();
this.module = module;
}
@Override
public Object executeGeneric(VirtualFrame frame) {
assert module != null;
return module;
}
}
@@ -24,6 +24,7 @@ import java.util.List;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.pkl.core.TypeParameter; import org.pkl.core.TypeParameter;
import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.expression.primary.GetTypeAliasModuleNode;
import org.pkl.core.ast.type.UnresolvedTypeNode; import org.pkl.core.ast.type.UnresolvedTypeNode;
import org.pkl.core.runtime.VmTypeAlias; import org.pkl.core.runtime.VmTypeAlias;
import org.pkl.core.runtime.VmTyped; import org.pkl.core.runtime.VmTyped;
@@ -86,7 +87,15 @@ public final class TypeAliasNode extends ExpressionNode {
frame.materialize()); frame.materialize());
VmUtils.evaluateAnnotations(frame, annotationNodes, annotations); VmUtils.evaluateAnnotations(frame, annotationNodes, annotations);
cachedTypeAlias.initTypeCheckNode(typeAnnotationNode.execute(frame)); var bodyTypeNode = typeAnnotationNode.execute(frame);
bodyTypeNode.accept(
node -> {
if (node instanceof GetTypeAliasModuleNode getTypeAliasModuleNode) {
getTypeAliasModuleNode.lateInitModule(module);
}
return true;
});
cachedTypeAlias.initTypeCheckNode(bodyTypeNode);
return cachedTypeAlias; return cachedTypeAlias;
} }
@@ -21,7 +21,6 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Fallback; import com.oracle.truffle.api.dsl.Fallback;
import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.Frame;
import com.oracle.truffle.api.frame.FrameDescriptor; import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.FrameSlotKind; import com.oracle.truffle.api.frame.FrameSlotKind;
import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.frame.VirtualFrame;
@@ -2735,56 +2734,18 @@ public abstract class TypeNode extends PklNode {
return getMirrors(typeArgumentNodes); return getMirrors(typeArgumentNodes);
} }
/**
* A typealias body is effectively inlined into the type node, and not executed in its own
* frame.
*
* <p>Before executing the typealias body, use the owner and receiver of the original frame
* where the typealias was declared, so that we preserve its original scope.
*/
protected Object executeLazily(VirtualFrame frame, Object value) { protected Object executeLazily(VirtualFrame frame, Object value) {
var prevOwner = VmUtils.getOwner(frame); return aliasedTypeNode.executeLazily(frame, value);
var prevReceiver = VmUtils.getReceiver(frame);
setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
try {
return aliasedTypeNode.executeLazily(frame, value);
} finally {
setOwner(frame, prevOwner);
setReceiver(frame, prevReceiver);
}
} }
@Override @Override
public Object executeEagerly(VirtualFrame frame, Object value) { public Object executeEagerly(VirtualFrame frame, Object value) {
var prevOwner = VmUtils.getOwner(frame); return aliasedTypeNode.executeEagerly(frame, value);
var prevReceiver = VmUtils.getReceiver(frame);
setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
try {
return aliasedTypeNode.executeEagerly(frame, value);
} finally {
setOwner(frame, prevOwner);
setReceiver(frame, prevReceiver);
}
} }
/** See docstring on {@link TypeAliasTypeNode#executeLazily}. */
@Override @Override
public Object executeAndSet(VirtualFrame frame, Object value) { public Object executeAndSet(VirtualFrame frame, Object value) {
var prevOwner = VmUtils.getOwner(frame); return aliasedTypeNode.executeAndSet(frame, value);
var prevReceiver = VmUtils.getReceiver(frame);
setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
try {
return aliasedTypeNode.executeAndSet(frame, value);
} finally {
setOwner(frame, prevOwner);
setReceiver(frame, prevReceiver);
}
} }
@TruffleBoundary @TruffleBoundary
@@ -2859,22 +2820,6 @@ public abstract class TypeNode extends PklNode {
protected boolean isParametric() { protected boolean isParametric() {
return typeArgumentNodes.length > 0; return typeArgumentNodes.length > 0;
} }
// Note that mutating a frame's receiver and owner argument is very risky
// because any VmObject instantiated within the same root node execution
// holds a reference to (not immutable snapshot of) the frame
// via VmObjectLike.enclosingFrame.
// *Maybe* this works out for TypeAliasTypeNode because an object instantiated
// within a type constraint doesn't escape the constraint expression.
// If mutating receiver and owner can't be avoided, it would be safer
// to have VmObjectLike store them directly instead of storing enclosingFrame.
private static void setReceiver(Frame frame, Object receiver) {
frame.getArguments()[0] = receiver;
}
private static void setOwner(Frame frame, VmObjectLike owner) {
frame.getArguments()[1] = owner;
}
} }
public static final class ConstrainedTypeNode extends TypeNode { public static final class ConstrainedTypeNode extends TypeNode {
@@ -0,0 +1 @@
typealias Id<T> = T
@@ -0,0 +1 @@
name: String = "base"
@@ -0,0 +1,9 @@
amends "typeAliasAmendBase.pkl"
local const function isValid(s: String): Boolean = s.startsWith("Bob")
local typealias ValidString4 = String(isValid(this))
local myProp: ValidString4 = "Bob Marley"
name = myProp
@@ -0,0 +1,3 @@
const function isValid(s: String): Boolean = s.startsWith("Bob")
typealias ValidString2 = String(isValid(this))
@@ -0,0 +1,3 @@
local const function isValid(s: String): Boolean = s.startsWith("Bob")
typealias ValidString3 = String(isValid(this))
@@ -0,0 +1 @@
typealias MyModule = module
@@ -0,0 +1,5 @@
open module typeAliasOpenModule
const function isValid(s: String): Boolean = s.startsWith("Bob")
typealias ValidString = String(isValid(this))
@@ -0,0 +1,3 @@
const name = "Bob"
typealias Inner = String(startsWith(name))
@@ -0,0 +1,5 @@
import "typealias1.pkl"
const name = "Alice"
typealias Outer = typealias1.Inner
@@ -0,0 +1,8 @@
import "../../input-helper/types/idType.pkl"
hidden modules: Listing<idType.Id<module>> = new {
new module {}
new module {}
}
res = modules.length
@@ -0,0 +1,3 @@
import "../../input-helper/types/typeAliasModule.pkl"
res: typeAliasModule = module
@@ -0,0 +1,3 @@
import "../../input-helper/types/typealias2.pkl"
myProp: typealias2.Outer = "Alice Jenkins"
@@ -0,0 +1,5 @@
import "../../input-helper/types/typeAliasOpenModule.pkl"
const function isValid(s: String): Boolean = s.startsWith("Alice")
myProp: typeAliasOpenModule.ValidString = "Alice Jenkins"
@@ -0,0 +1,5 @@
import "../../input-helper/types/typeAliasClosedModule.pkl"
const function isValid(s: String): Boolean = s.startsWith("Alice")
myProp: typeAliasClosedModule.ValidString2 = "Alice Jenkins"
@@ -0,0 +1,5 @@
import "../../input-helper/types/typeAliasLocalMethod.pkl"
local const function isValid(s: String): Boolean = s.startsWith("Alice")
myProp: typeAliasLocalMethod.ValidString3 = "Alice Jenkins"
@@ -0,0 +1,5 @@
import "../../input-helper/types/typeAliasAmendModule.pkl"
local const function isValid(s: String): Boolean = s.startsWith("Alice")
res = typeAliasAmendModule.name
@@ -0,0 +1 @@
res = 2
@@ -0,0 +1,15 @@
–– Pkl Error ––
Expected value of type `typeAliasModule`, but got type `typeAlias6`.
Value: new ModuleClass { res = ? }
x | res: typeAliasModule = module
^^^^^^^^^^^^^^^
at typeAlias6#res (file:///$snippetsDir/input/types/typeAlias6.pkl)
xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -0,0 +1,23 @@
–– Pkl Error ––
Type constraint `startsWith(name)` violated.
Value: "Alice Jenkins"
startsWith(name)
│ │
false "Bob"
x | typealias Inner = String(startsWith(name))
^^^^^^^^^^^^^^^^
at typeAliasConstraint4#myProp (file:///$snippetsDir/input-helper/types/typealias1.pkl)
x | myProp: typealias2.Outer = "Alice Jenkins"
^^^^^^^^^^^^^^^
at typeAliasConstraint4#myProp (file:///$snippetsDir/input/types/typeAliasConstraint4.pkl)
xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -0,0 +1,23 @@
–– Pkl Error ––
Type constraint `isValid(this)` violated.
Value: "Alice Jenkins"
isValid(this)
│ │
false "Alice Jenkins"
x | typealias ValidString = String(isValid(this))
^^^^^^^^^^^^^
at typeAliasConstraint5#myProp (file:///$snippetsDir/input-helper/types/typeAliasOpenModule.pkl)
x | myProp: typeAliasOpenModule.ValidString = "Alice Jenkins"
^^^^^^^^^^^^^^^
at typeAliasConstraint5#myProp (file:///$snippetsDir/input/types/typeAliasConstraint5.pkl)
xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -0,0 +1,23 @@
–– Pkl Error ––
Type constraint `isValid(this)` violated.
Value: "Alice Jenkins"
isValid(this)
│ │
false "Alice Jenkins"
x | typealias ValidString2 = String(isValid(this))
^^^^^^^^^^^^^
at typeAliasConstraint6#myProp (file:///$snippetsDir/input-helper/types/typeAliasClosedModule.pkl)
x | myProp: typeAliasClosedModule.ValidString2 = "Alice Jenkins"
^^^^^^^^^^^^^^^
at typeAliasConstraint6#myProp (file:///$snippetsDir/input/types/typeAliasConstraint6.pkl)
xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -0,0 +1,23 @@
–– Pkl Error ––
Type constraint `isValid(this)` violated.
Value: "Alice Jenkins"
isValid(this)
│ │
false "Alice Jenkins"
x | typealias ValidString3 = String(isValid(this))
^^^^^^^^^^^^^
at typeAliasConstraint7#myProp (file:///$snippetsDir/input-helper/types/typeAliasLocalMethod.pkl)
x | myProp: typeAliasLocalMethod.ValidString3 = "Alice Jenkins"
^^^^^^^^^^^^^^^
at typeAliasConstraint7#myProp (file:///$snippetsDir/input/types/typeAliasConstraint7.pkl)
xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -0,0 +1 @@
res = "Bob Marley"