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.SetLiteralNode;
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.InferParentWithinMethodNodeGen;
import org.pkl.core.ast.expression.member.InferParentWithinObjectMethodNodeGen;
@@ -1073,6 +1074,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
}
private ExpressionNode doVisitNewExprWithInferredParent(NewExpr expr) {
var sourceSection = createSourceSection(expr.newSpan());
ExpressionNode inferredParentNode;
Node child = expr;
@@ -1083,8 +1085,8 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
while (parent instanceof IfExpr ifExpr
&& (ifExpr.getThen() == child || ifExpr.getEls() == child)
|| parent instanceof TraceExpr
|| parent instanceof LetExpr letExpr && letExpr.getExpr() == child) {
|| parent instanceof LetExpr letExpr && letExpr.getExpr() == child
|| parent instanceof ParenthesizedExpr) {
child = parent;
parent = parent.parent();
}
@@ -1092,15 +1094,13 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
if (parent instanceof ClassProperty || parent instanceof ObjectProperty) {
inferredParentNode =
InferParentWithinPropertyNodeGen.create(
createSourceSection(expr.newSpan()), scope.getName(), new GetOwnerNode());
sourceSection, scope.getName(), new GetOwnerNode());
} else if (parent instanceof ObjectElement
|| parent instanceof ObjectEntry objectEntry && objectEntry.getValue() == child) {
inferredParentNode =
ApplyVmFunction1NodeGen.create(
ReadPropertyNodeGen.create(
createSourceSection(expr.newSpan()),
org.pkl.core.runtime.Identifier.DEFAULT,
new GetReceiverNode()),
sourceSection, org.pkl.core.runtime.Identifier.DEFAULT, new GetReceiverNode()),
new GetMemberKeyNode());
} else if (parent instanceof ClassMethod || parent instanceof ObjectMethod) {
var isObjectMethod =
@@ -1110,29 +1110,24 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
inferredParentNode =
isObjectMethod
? InferParentWithinObjectMethodNodeGen.create(
createSourceSection(expr.newSpan()), language, scopeName, new GetOwnerNode())
sourceSection, language, scopeName, new GetOwnerNode())
: InferParentWithinMethodNodeGen.create(
createSourceSection(expr.newSpan()), language, scopeName, new GetOwnerNode());
sourceSection, language, scopeName, new GetOwnerNode());
} else if (parent instanceof LetExpr letExpr && letExpr.getBindingExpr() == child) {
// TODO correctly infer parent, e.g. `let (x: Person = new {}) ...`
throw exceptionBuilder()
.evalError("cannotInferParent")
.withSourceSection(createSourceSection(expr.newSpan()))
.build();
inferredParentNode = InferParentWithinLetBindingNodeGen.create(sourceSection, language);
} else if (parent instanceof ArgumentList argumentList) {
// cases we can't cover currently
// - FunctionN.apply: the parameter type nodes are not stored
// - pkl.base intrinsic constructors: List(), Set(), Map(), Bytes()
// - generic methods: pkl.base#Pair(), etc.
// these will throw cannotInferParent at runtime
var sourceSection = createSourceSection(expr.newSpan());
var argIndex = argumentList.getArguments().indexOf(child);
inferredParentNode =
InferParentWithinMethodArgumentNodeGen.create(sourceSection, language, argIndex);
} else {
throw exceptionBuilder()
.evalError("cannotInferParent")
.withSourceSection(createSourceSection(expr.newSpan()))
.withSourceSection(sourceSection)
.build();
}
@@ -49,22 +49,29 @@ public abstract class LetExprNode extends ExpressionNode {
this.slot = slot;
}
private TypeNode getTypeNode(VirtualFrame frame) {
if (typeNode == null) {
public TypeNode getTypeNode(VirtualFrame frame) {
if (typeNode != null) return typeNode;
CompilerDirectives.transferToInterpreterAndInvalidate();
if (unresolvedTypeNode != null) {
if (unresolvedTypeNode != null && slot >= 0) {
typeNode = unresolvedTypeNode.execute(frame);
} else {
typeNode = new TypeNode.UnknownTypeNode(VmUtils.unavailableSourceSection());
}
if (slot >= 0) {
typeNode.initWriteSlotNode(slot);
frame.getFrameDescriptor().setSlotKind(slot, typeNode.getFrameSlotKind());
insert(typeNode);
}
assert typeNode != null;
return typeNode;
return insert(typeNode);
}
public String getQualifiedName() {
return qualifiedName;
}
public abstract ExpressionNode getBindingNode();
@Specialization
protected Object eval(VirtualFrame frame, Object value) {
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.dsl.Cached;
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.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.FrameSlotKind;
@@ -140,6 +141,7 @@ public abstract class TypeNode extends PklNode {
return null;
}
@Idempotent
public final boolean isFinalType() {
var ret = new MutableBoolean(true);
acceptTypeNode(
@@ -1,5 +1,9 @@
open module letTyped
import "pkl:test"
y: Int = 0
res1 =
let (x: Int = 42)
x + 1
@@ -14,22 +18,77 @@ res3 =
x * y
res4 =
let (xs: List<Null|Boolean|"Pigeon"> = List("Pigeon", true, null))
let (xs: List<Null | Boolean | "Pigeon"> = List("Pigeon", true, null))
xs
res5 = test.catch(() ->
res5 =
test.catch(() ->
let (x: String = 42)
x + 1)
x + 1
)
res6 = test.catch(() ->
res6 =
test.catch(() ->
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 (y: Float(!isPositive) = 1.2 + 1.3)
x * y)
x * y
)
res8 = test.catch(() ->
let (xs: List<Null|Boolean|"Pigeon"> = List("Barn Owl", true, null))
xs)
res8 =
test.catch(() ->
let (xs: List<Null | Boolean | "Pigeon"> = List("Barn Owl", true, null))
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
res2 = "Pigeon"
res3 = 12.5.min
@@ -5,4 +6,14 @@ res4 = List("Pigeon", true, null)
res5 = "Expected value of type `String`, but got type `Int`. Value: 42"
res6 = "Type constraint `endsWith(\"A\")` violated. Value: \"noegiP\""
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"