Add inference for let bindings (#1824)

This commit is contained in:
Jen Basch
2026-09-15 05:20:42 +00:00
committed by GitHub
parent d0de24b85b
commit c71b107e32
6 changed files with 192 additions and 41 deletions
@@ -109,6 +109,7 @@ import org.pkl.core.ast.expression.literal.MapLiteralNode;
import org.pkl.core.ast.expression.literal.PropertiesLiteralNodeGen; import org.pkl.core.ast.expression.literal.PropertiesLiteralNodeGen;
import org.pkl.core.ast.expression.literal.SetLiteralNode; import org.pkl.core.ast.expression.literal.SetLiteralNode;
import org.pkl.core.ast.expression.literal.TrueLiteralNode; import org.pkl.core.ast.expression.literal.TrueLiteralNode;
import org.pkl.core.ast.expression.member.InferParentWithinLetBindingNodeGen;
import org.pkl.core.ast.expression.member.InferParentWithinMethodArgumentNodeGen; import org.pkl.core.ast.expression.member.InferParentWithinMethodArgumentNodeGen;
import org.pkl.core.ast.expression.member.InferParentWithinMethodNodeGen; import org.pkl.core.ast.expression.member.InferParentWithinMethodNodeGen;
import org.pkl.core.ast.expression.member.InferParentWithinObjectMethodNodeGen; import org.pkl.core.ast.expression.member.InferParentWithinObjectMethodNodeGen;
@@ -1073,6 +1074,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
} }
private ExpressionNode doVisitNewExprWithInferredParent(NewExpr expr) { private ExpressionNode doVisitNewExprWithInferredParent(NewExpr expr) {
var sourceSection = createSourceSection(expr.newSpan());
ExpressionNode inferredParentNode; ExpressionNode inferredParentNode;
Node child = expr; Node child = expr;
@@ -1083,8 +1085,8 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
while (parent instanceof IfExpr ifExpr while (parent instanceof IfExpr ifExpr
&& (ifExpr.getThen() == child || ifExpr.getEls() == child) && (ifExpr.getThen() == child || ifExpr.getEls() == child)
|| parent instanceof TraceExpr || parent instanceof TraceExpr
|| parent instanceof LetExpr letExpr && letExpr.getExpr() == child) { || parent instanceof LetExpr letExpr && letExpr.getExpr() == child
|| parent instanceof ParenthesizedExpr) {
child = parent; child = parent;
parent = parent.parent(); parent = parent.parent();
} }
@@ -1092,15 +1094,13 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
if (parent instanceof ClassProperty || parent instanceof ObjectProperty) { if (parent instanceof ClassProperty || parent instanceof ObjectProperty) {
inferredParentNode = inferredParentNode =
InferParentWithinPropertyNodeGen.create( InferParentWithinPropertyNodeGen.create(
createSourceSection(expr.newSpan()), scope.getName(), new GetOwnerNode()); sourceSection, scope.getName(), new GetOwnerNode());
} else if (parent instanceof ObjectElement } else if (parent instanceof ObjectElement
|| parent instanceof ObjectEntry objectEntry && objectEntry.getValue() == child) { || parent instanceof ObjectEntry objectEntry && objectEntry.getValue() == child) {
inferredParentNode = inferredParentNode =
ApplyVmFunction1NodeGen.create( ApplyVmFunction1NodeGen.create(
ReadPropertyNodeGen.create( ReadPropertyNodeGen.create(
createSourceSection(expr.newSpan()), sourceSection, org.pkl.core.runtime.Identifier.DEFAULT, new GetReceiverNode()),
org.pkl.core.runtime.Identifier.DEFAULT,
new GetReceiverNode()),
new GetMemberKeyNode()); new GetMemberKeyNode());
} else if (parent instanceof ClassMethod || parent instanceof ObjectMethod) { } else if (parent instanceof ClassMethod || parent instanceof ObjectMethod) {
var isObjectMethod = var isObjectMethod =
@@ -1110,29 +1110,24 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
inferredParentNode = inferredParentNode =
isObjectMethod isObjectMethod
? InferParentWithinObjectMethodNodeGen.create( ? InferParentWithinObjectMethodNodeGen.create(
createSourceSection(expr.newSpan()), language, scopeName, new GetOwnerNode()) sourceSection, language, scopeName, new GetOwnerNode())
: InferParentWithinMethodNodeGen.create( : InferParentWithinMethodNodeGen.create(
createSourceSection(expr.newSpan()), language, scopeName, new GetOwnerNode()); sourceSection, language, scopeName, new GetOwnerNode());
} else if (parent instanceof LetExpr letExpr && letExpr.getBindingExpr() == child) { } else if (parent instanceof LetExpr letExpr && letExpr.getBindingExpr() == child) {
// TODO correctly infer parent, e.g. `let (x: Person = new {}) ...` inferredParentNode = InferParentWithinLetBindingNodeGen.create(sourceSection, language);
throw exceptionBuilder()
.evalError("cannotInferParent")
.withSourceSection(createSourceSection(expr.newSpan()))
.build();
} else if (parent instanceof ArgumentList argumentList) { } else if (parent instanceof ArgumentList argumentList) {
// cases we can't cover currently // cases we can't cover currently
// - FunctionN.apply: the parameter type nodes are not stored // - FunctionN.apply: the parameter type nodes are not stored
// - pkl.base intrinsic constructors: List(), Set(), Map(), Bytes() // - pkl.base intrinsic constructors: List(), Set(), Map(), Bytes()
// - generic methods: pkl.base#Pair(), etc. // - generic methods: pkl.base#Pair(), etc.
// these will throw cannotInferParent at runtime // these will throw cannotInferParent at runtime
var sourceSection = createSourceSection(expr.newSpan());
var argIndex = argumentList.getArguments().indexOf(child); var argIndex = argumentList.getArguments().indexOf(child);
inferredParentNode = inferredParentNode =
InferParentWithinMethodArgumentNodeGen.create(sourceSection, language, argIndex); InferParentWithinMethodArgumentNodeGen.create(sourceSection, language, argIndex);
} else { } else {
throw exceptionBuilder() throw exceptionBuilder()
.evalError("cannotInferParent") .evalError("cannotInferParent")
.withSourceSection(createSourceSection(expr.newSpan())) .withSourceSection(sourceSection)
.build(); .build();
} }
@@ -49,22 +49,29 @@ public abstract class LetExprNode extends ExpressionNode {
this.slot = slot; this.slot = slot;
} }
private TypeNode getTypeNode(VirtualFrame frame) { public TypeNode getTypeNode(VirtualFrame frame) {
if (typeNode == null) { if (typeNode != null) return typeNode;
CompilerDirectives.transferToInterpreterAndInvalidate(); CompilerDirectives.transferToInterpreterAndInvalidate();
if (unresolvedTypeNode != null) { if (unresolvedTypeNode != null && slot >= 0) {
typeNode = unresolvedTypeNode.execute(frame); typeNode = unresolvedTypeNode.execute(frame);
} else { } else {
typeNode = new TypeNode.UnknownTypeNode(VmUtils.unavailableSourceSection()); typeNode = new TypeNode.UnknownTypeNode(VmUtils.unavailableSourceSection());
} }
if (slot >= 0) {
typeNode.initWriteSlotNode(slot); typeNode.initWriteSlotNode(slot);
frame.getFrameDescriptor().setSlotKind(slot, typeNode.getFrameSlotKind()); frame.getFrameDescriptor().setSlotKind(slot, typeNode.getFrameSlotKind());
insert(typeNode);
} }
assert typeNode != null; assert typeNode != null;
return typeNode; return insert(typeNode);
} }
public String getQualifiedName() {
return qualifiedName;
}
public abstract ExpressionNode getBindingNode();
@Specialization @Specialization
protected Object eval(VirtualFrame frame, Object value) { protected Object eval(VirtualFrame frame, Object value) {
if (slot != -1) { if (slot != -1) {
@@ -0,0 +1,77 @@
/*
* 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.dsl.Cached;
import com.oracle.truffle.api.dsl.Cached.Shared;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.expression.binary.LetExprNode;
import org.pkl.core.ast.type.TypeNode;
import org.pkl.core.runtime.VmLanguage;
public abstract class InferParentWithinLetBindingNode extends AbstractInferParentNode {
public InferParentWithinLetBindingNode(SourceSection sourceSection, VmLanguage language) {
super(sourceSection, language);
}
protected LetExprNode getLetNode() {
Node child = this;
LetExprNode letNode = null;
for (var node = getParent(); node != null; node = node.getParent()) {
if (node instanceof LetExprNode let && let.getBindingNode() == child) {
letNode = let;
break;
}
child = node;
}
assert letNode != null
: "AstBuilder created an InferParentWithinLetBindingNode outside of a let binding";
return letNode;
}
// keep specializations in sync with other AbstractInferParentNode subclasses
@Specialization(guards = {"typeNode.isFinalType()"})
protected final Object evalCached(
@SuppressWarnings("unused") VirtualFrame frame,
@Cached(value = "getLetNode()", neverDefault = true, adopt = false)
@Shared
@SuppressWarnings("unused")
LetExprNode letNode,
@Cached(value = "letNode.getTypeNode(frame)", neverDefault = true, adopt = false)
@SuppressWarnings("unused")
TypeNode typeNode,
@Cached(
value =
"getDefaultValue(frame, typeNode, letNode.getSourceSection(), letNode.getQualifiedName())",
neverDefault = true)
Object defaultValue) {
return defaultValue;
}
@Specialization
protected final Object eval(
VirtualFrame frame,
@Cached(value = "getLetNode()", neverDefault = true, adopt = false) @Shared
LetExprNode letNode,
@Cached(value = "letNode.getTypeNode(frame)", neverDefault = true, adopt = false)
TypeNode typeNode) {
return getDefaultValue(frame, typeNode, letNode.getSourceSection(), letNode.getQualifiedName());
}
}
@@ -20,6 +20,7 @@ import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; 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.Idempotent;
import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.dsl.Specialization;
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;
@@ -140,6 +141,7 @@ public abstract class TypeNode extends PklNode {
return null; return null;
} }
@Idempotent
public final boolean isFinalType() { public final boolean isFinalType() {
var ret = new MutableBoolean(true); var ret = new MutableBoolean(true);
acceptTypeNode( acceptTypeNode(
@@ -1,5 +1,9 @@
open module letTyped
import "pkl:test" import "pkl:test"
y: Int = 0
res1 = res1 =
let (x: Int = 42) let (x: Int = 42)
x + 1 x + 1
@@ -17,19 +21,74 @@ res4 =
let (xs: List<Null | Boolean | "Pigeon"> = List("Pigeon", true, null)) let (xs: List<Null | Boolean | "Pigeon"> = List("Pigeon", true, null))
xs xs
res5 = test.catch(() -> res5 =
test.catch(() ->
let (x: String = 42) let (x: String = 42)
x + 1) x + 1
)
res6 = test.catch(() -> res6 =
test.catch(() ->
let (str: String(endsWith("A")) = "Pigeon".reverse()) let (str: String(endsWith("A")) = "Pigeon".reverse())
str.reverse()) str.reverse()
)
res7 = test.catch(() -> res7 =
test.catch(() ->
let (x: Duration(unit == "min") = 3.min + 2.min) let (x: Duration(unit == "min") = 3.min + 2.min)
let (y: Float(!isPositive) = 1.2 + 1.3) let (y: Float(!isPositive) = 1.2 + 1.3)
x * y) x * y
)
res8 = test.catch(() -> res8 =
test.catch(() ->
let (xs: List<Null | Boolean | "Pigeon"> = List("Barn Owl", true, null)) let (xs: List<Null | Boolean | "Pigeon"> = List("Barn Owl", true, null))
xs) xs
)
// test basic `new {}` inference
class Foo {
y: Int
}
res9 =
let (x: Foo = new { y = 1 })
x.y
// test inference with let in binding expression
res10 =
let (x: Foo = let (z = 0) new { y = z })
x.y
res10a =
let (x: Foo = (new { y = 0 }))
x.y
res10b =
let (x = new { y = 0 })
x.getClass().toString()
// test inference against underscore binding
res11 = let (_ = new { y = 1 }) 0 // binding is a new Dynamic
// test inference of non-instantiable types
abstract class Bar {
y: Int
}
res12a = test.catch(() -> let (x: Int = new { y = 1 }) x)
res12b = test.catch(() -> let (x: Bar = new { y = 1 }) x)
res12c = test.catch(() -> let (x: nothing = new { y = 1 }) x)
// test inference of a self type
function make(_y: Int): module =
let (z: module = new { y = _y })
z
class Child extends module
res13a = make(2).getClass().toString()
res13b = new Child {}.make(3).getClass().toString()
@@ -1,3 +1,4 @@
y = 0
res1 = 43 res1 = 43
res2 = "Pigeon" res2 = "Pigeon"
res3 = 12.5.min res3 = 12.5.min
@@ -6,3 +7,13 @@ res5 = "Expected value of type `String`, but got type `Int`. Value: 42"
res6 = "Type constraint `endsWith(\"A\")` violated. Value: \"noegiP\"" res6 = "Type constraint `endsWith(\"A\")` violated. Value: \"noegiP\""
res7 = "Type constraint `!isPositive` violated. Value: 2.5" res7 = "Type constraint `!isPositive` violated. Value: 2.5"
res8 = "Expected value of type `Null | Boolean | \"Pigeon\"`, but got type `String`. Value: \"Barn Owl\"" res8 = "Expected value of type `Null | Boolean | \"Pigeon\"`, but got type `String`. Value: \"Barn Owl\""
res9 = 1
res10 = 0
res10a = 0
res10b = "Dynamic"
res11 = 0
res12a = "Cannot instantiate, or amend an instance of, external class `Int`."
res12b = "Cannot instantiate abstract class `letTyped#Bar`."
res12c = "Cannot instantiate type `nothing`."
res13a = "letTyped"
res13b = "letTyped#Child"