mirror of
https://github.com/apple/pkl.git
synced 2026-09-17 15:21:35 +02:00
Add inference for let bindings (#1824)
This commit is contained in:
@@ -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) {
|
||||
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) {
|
||||
|
||||
+77
@@ -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
|
||||
@@ -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<Null|Boolean|"Pigeon"> = List("Pigeon", true, null))
|
||||
let (xs: List<Null | Boolean | "Pigeon"> = 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<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"
|
||||
|
||||
Reference in New Issue
Block a user