From c71b107e328aa99819c09aca8c0799d0d8099f5d Mon Sep 17 00:00:00 2001 From: Jen Basch Date: Mon, 14 Sep 2026 22:20:42 -0700 Subject: [PATCH] Add inference for let bindings (#1824) --- .../org/pkl/core/ast/builder/AstBuilder.java | 25 +++--- .../ast/expression/binary/LetExprNode.java | 27 +++--- .../InferParentWithinLetBindingNode.java | 77 ++++++++++++++++ .../java/org/pkl/core/ast/type/TypeNode.java | 2 + .../input/basic/letTyped.pkl | 89 +++++++++++++++---- .../output/basic/letTyped.pcf | 13 ++- 6 files changed, 192 insertions(+), 41 deletions(-) create mode 100644 pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinLetBindingNode.java 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 446c58e1c..1b6a4d219 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 @@ -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 { } private ExpressionNode doVisitNewExprWithInferredParent(NewExpr expr) { + var sourceSection = createSourceSection(expr.newSpan()); ExpressionNode inferredParentNode; Node child = expr; @@ -1083,8 +1085,8 @@ public class AstBuilder extends AbstractAstBuilder { 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 { 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 { 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(); } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LetExprNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LetExprNode.java index 0555dbee2..e5c888031 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LetExprNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/binary/LetExprNode.java @@ -49,22 +49,29 @@ public abstract class LetExprNode extends ExpressionNode { this.slot = slot; } - private TypeNode getTypeNode(VirtualFrame frame) { - if (typeNode == null) { - CompilerDirectives.transferToInterpreterAndInvalidate(); - if (unresolvedTypeNode != null) { - typeNode = unresolvedTypeNode.execute(frame); - } else { - typeNode = new TypeNode.UnknownTypeNode(VmUtils.unavailableSourceSection()); - } + public TypeNode getTypeNode(VirtualFrame frame) { + if (typeNode != null) return typeNode; + + CompilerDirectives.transferToInterpreterAndInvalidate(); + 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) { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinLetBindingNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinLetBindingNode.java new file mode 100644 index 000000000..4b45d8991 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/InferParentWithinLetBindingNode.java @@ -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()); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java index 951cc343c..7612c3656 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java @@ -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( diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/letTyped.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/letTyped.pkl index d20e0bde3..c6ec8657e 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/letTyped.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/letTyped.pkl @@ -1,5 +1,9 @@ +open module letTyped + import "pkl:test" +y: Int = 0 + res1 = let (x: Int = 42) x + 1 @@ -10,26 +14,81 @@ res2 = res3 = let (x: Duration(unit == "min") = 3.min + 2.min) - let (y: Float(isPositive) = 1.2 + 1.3) - x * y + let (y: Float(isPositive) = 1.2 + 1.3) + x * y res4 = - let (xs: List = List("Pigeon", true, null)) + let (xs: List = List("Pigeon", true, null)) xs -res5 = test.catch(() -> - let (x: String = 42) - x + 1) +res5 = + test.catch(() -> + let (x: String = 42) + x + 1 + ) -res6 = test.catch(() -> - let (str: String(endsWith("A")) = "Pigeon".reverse()) - str.reverse()) +res6 = + test.catch(() -> + let (str: String(endsWith("A")) = "Pigeon".reverse()) + str.reverse() + ) -res7 = test.catch(() -> - let (x: Duration(unit == "min") = 3.min + 2.min) +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 = List("Barn Owl", true, null)) - xs) +res8 = + test.catch(() -> + let (xs: List = 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() diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/basic/letTyped.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/letTyped.pcf index 87480153a..b25070f0c 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/basic/letTyped.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/letTyped.pcf @@ -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"