Typecheck Mapping/Listing members lazily (#628)

This changes how the language performs typechecks for mappings and
listings.

Currently, Pkl will shallow-force any Mapping and Listing to check it
the type parameter (e.g. Listing<Person> means each element is checked
to be an instance of Person).

This changes the language to check each member's type when the member
is accessed.

This also adjust test runner to handle thrown errors from within tests.

With the change to make mapping/listing typechecks lazy, we can now
correctly handle thrown errors from within a single test case.

This adjusts the test runner to consider any thrown errors as a failure
for that specific test case.
This commit is contained in:
Daniel Chao
2024-09-06 15:05:23 -07:00
committed by GitHub
parent 7001a42149
commit 7868d9d9c8
86 changed files with 3342 additions and 385 deletions
@@ -22,7 +22,6 @@ import java.util.function.Function;
import org.pkl.core.ast.member.DefaultPropertyBodyNode;
import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.runtime.VmLanguage;
import org.pkl.core.runtime.VmUtils;
import org.pkl.core.util.Nullable;
public abstract class MemberNode extends PklRootNode {
@@ -57,19 +56,6 @@ public abstract class MemberNode extends PklRootNode {
return new VmExceptionBuilder().withSourceSection(getHeaderSection());
}
/**
* If true, the property value computed by this node is not the final value exposed to user code
* but will still be amended.
*
* <p>Used to disable type check for to-be-amended properties. See {@link
* org.pkl.core.runtime.VmUtils#SKIP_TYPECHECK_MARKER}. IDEA: might be more appropriate to only
* skip constraints check
*/
protected final boolean shouldRunTypeCheck(VirtualFrame frame) {
return frame.getArguments().length != 4
|| frame.getArguments()[3] != VmUtils.SKIP_TYPECHECK_MARKER;
}
public boolean isUndefined() {
return bodyNode instanceof DefaultPropertyBodyNode propBodyNode && propBodyNode.isUndefined();
}
@@ -442,13 +442,21 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
: doVisitNewExprWithInferredParent(ctx);
}
// `new Listing<Person> {}` is sugar for: `new Listing<Person> {} as Listing<Person>`
private Object doVisitNewExprWithExplicitParent(NewExprContext ctx, TypeContext typeCtx) {
return doVisitObjectBody(
ctx.objectBody(),
new GetParentForTypeNode(
createSourceSection(ctx),
visitType(typeCtx),
symbolTable.getCurrentScope().getQualifiedName()));
var parentType = visitType(typeCtx);
var expr =
doVisitObjectBody(
ctx.objectBody(),
new GetParentForTypeNode(
createSourceSection(ctx),
parentType,
symbolTable.getCurrentScope().getQualifiedName()));
if (typeCtx instanceof DeclaredTypeContext declaredTypeContext
&& declaredTypeContext.typeArgumentList() != null) {
return new TypeCastNode(parentType.getSourceSection(), expr, parentType);
}
return expr;
}
private Object doVisitNewExprWithInferredParent(NewExprContext ctx) {
@@ -103,7 +103,7 @@ public abstract class GeneratorPredicateMemberNode extends GeneratorMemberNode {
var callTarget = member.getCallTarget();
value = callTarget.call(parent, owner, key);
}
owner.setCachedValue(key, value);
owner.setCachedValue(key, value, member);
}
frame.setAuxiliarySlot(customThisSlot, value);
@@ -150,8 +150,8 @@ public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode {
@Fallback
@TruffleBoundary
protected void fallback(Object parent) {
elementsEntriesFallback(parent, values[0], false);
protected Object fallback(Object parent) {
return elementsEntriesFallback(parent, values[0], false);
}
@ExplodeLoop
@@ -285,7 +285,7 @@ public abstract class SpecializedObjectLiteralNode extends ObjectLiteralNode {
}
@TruffleBoundary
protected void elementsEntriesFallback(
protected Object elementsEntriesFallback(
Object parent, @Nullable ObjectMember firstElemOrEntry, boolean isElementsOnly) {
var parentIsClass = parent instanceof VmClass;
var parentClass = parentIsClass ? (VmClass) parent : VmUtils.getClass(parent);
@@ -71,7 +71,7 @@ public final class ReadLocalPropertyNode extends ExpressionNode {
if (result == null) {
result = callNode.call(objReceiver, owner, property.getName());
objReceiver.setCachedValue(property, result);
objReceiver.setCachedValue(property, result, property);
}
return result;
@@ -184,12 +184,12 @@ public final class ResolveVariableNode extends ExpressionNode {
if (member != null) {
var constantValue = member.getConstantValue();
if (constantValue != null) {
baseModule.setCachedValue(variableName, constantValue);
baseModule.setCachedValue(variableName, constantValue, member);
return new ConstantValueNode(sourceSection, constantValue);
}
var computedValue = member.getCallTarget().call(baseModule, baseModule);
baseModule.setCachedValue(variableName, computedValue);
baseModule.setCachedValue(variableName, computedValue, member);
return new ConstantValueNode(sourceSection, computedValue);
}
}
@@ -118,7 +118,7 @@ public final class FunctionNode extends RegularMemberNode {
var result = bodyNode.executeGeneric(frame);
if (checkedReturnTypeNode != null) {
checkedReturnTypeNode.execute(frame, result);
return checkedReturnTypeNode.execute(frame, result);
}
return result;
@@ -0,0 +1,64 @@
/**
* Copyright © 2024 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.member;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.PklRootNode;
import org.pkl.core.ast.type.TypeNode;
import org.pkl.core.ast.type.VmTypeMismatchException;
import org.pkl.core.runtime.VmLanguage;
import org.pkl.core.util.Nullable;
/** Performs a typecast on a Mapping entry value, or a Listing element. */
public class ListingOrMappingTypeCastNode extends PklRootNode {
@Child private TypeNode typeNode;
private final String qualifiedName;
public ListingOrMappingTypeCastNode(
VmLanguage language, FrameDescriptor descriptor, TypeNode typeNode, String qualifiedName) {
super(language, descriptor);
this.typeNode = typeNode;
this.qualifiedName = qualifiedName;
}
public TypeNode getTypeNode() {
return typeNode;
}
@Override
public SourceSection getSourceSection() {
return typeNode.getSourceSection();
}
@Override
public @Nullable String getName() {
return qualifiedName;
}
@Override
public Object execute(VirtualFrame frame) {
try {
return typeNode.execute(frame, frame.getArguments()[2]);
} catch (VmTypeMismatchException e) {
CompilerDirectives.transferToInterpreter();
throw e.toVmException();
}
}
}
@@ -66,8 +66,7 @@ public final class LocalTypedPropertyNode extends RegularMemberNode {
unresolvedTypeNode = null;
}
var result = bodyNode.executeGeneric(frame);
typeNode.execute(frame, result);
return result;
return typeNode.execute(frame, result);
} catch (VmTypeMismatchException e) {
CompilerDirectives.transferToInterpreter();
throw e.toVmException();
@@ -134,7 +134,8 @@ public final class ObjectMember extends Member {
var skip = 0;
var text = candidate.getCharacters();
var ch = text.charAt(skip);
while (ch == '=' || Character.isWhitespace(ch)) {
// body section of entries needs to chomp the ending delimiter too.
while ((ch == ']' && isEntry()) || ch == '=' || Character.isWhitespace(ch)) {
ch = text.charAt(++skip);
}
return source.createSection(candidate.getCharIndex() + skip, candidate.getCharLength() - skip);
@@ -62,10 +62,9 @@ public final class PropertyTypeNode extends PklRootNode {
}
@Override
public @Nullable Object execute(VirtualFrame frame) {
public Object execute(VirtualFrame frame) {
try {
typeNode.execute(frame, frame.getArguments()[2]);
return null;
return typeNode.execute(frame, frame.getArguments()[2]);
} catch (VmTypeMismatchException e) {
CompilerDirectives.transferToInterpreter();
throw e.toVmException();
@@ -54,8 +54,8 @@ public abstract class TypeCheckedPropertyNode extends RegularMemberNode {
var result = executeBody(frame);
// TODO: propagate SUPER_CALL_MARKER to disable constraint (but not type) check
if (callNode != null && shouldRunTypeCheck(frame)) {
callNode.call(VmUtils.getReceiverOrNull(frame), property.getOwner(), result);
if (callNode != null && VmUtils.shouldRunTypeCheck(frame)) {
return callNode.call(VmUtils.getReceiverOrNull(frame), property.getOwner(), result);
}
return result;
@@ -67,11 +67,11 @@ public abstract class TypeCheckedPropertyNode extends RegularMemberNode {
var result = executeBody(frame);
if (shouldRunTypeCheck(frame)) {
if (VmUtils.shouldRunTypeCheck(frame)) {
var property = getProperty(owner.getVmClass());
var typeAnnNode = property.getTypeNode();
if (typeAnnNode != null) {
callNode.call(
return callNode.call(
typeAnnNode.getCallTarget(),
VmUtils.getReceiverOrNull(frame),
property.getOwner(),
@@ -45,8 +45,9 @@ public final class TypedPropertyNode extends RegularMemberNode {
@Override
public Object execute(VirtualFrame frame) {
var propertyValue = executeBody(frame);
if (shouldRunTypeCheck(frame)) {
typeCheckCallNode.call(VmUtils.getReceiver(frame), VmUtils.getOwner(frame), propertyValue);
if (VmUtils.shouldRunTypeCheck(frame)) {
return typeCheckCallNode.call(
VmUtils.getReceiver(frame), VmUtils.getOwner(frame), propertyValue);
}
return propertyValue;
}
@@ -66,7 +66,7 @@ public final class IdentityMixinNode extends PklRootNode {
try {
var argument = arguments[2];
if (argumentTypeNode != null) {
argumentTypeNode.execute(frame, argument);
return argumentTypeNode.execute(frame, argument);
}
return argument;
} catch (VmTypeMismatchException e) {
@@ -73,7 +73,7 @@ public abstract class ResolveDeclaredTypeNode extends ExpressionNode {
var result = module.getCachedValue(importName);
if (result == null) {
result = callNode.call(member.getCallTarget(), module, module, importName);
module.setCachedValue(importName, result);
module.setCachedValue(importName, result, member);
}
return (VmTyped) result;
}
@@ -94,7 +94,7 @@ public abstract class ResolveDeclaredTypeNode extends ExpressionNode {
var result = module.getCachedValue(typeName);
if (result == null) {
result = callNode.call(member.getCallTarget(), module, module, typeName);
module.setCachedValue(typeName, result);
module.setCachedValue(typeName, result, member);
}
return result;
}
@@ -17,10 +17,12 @@ package org.pkl.core.ast.type;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.NodeInfo;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.util.LateInit;
@NodeInfo(shortName = "as")
public final class TypeCastNode extends ExpressionNode {
@Child private ExpressionNode valueNode;
@Child private UnresolvedTypeNode unresolvedTypeNode;
@@ -47,8 +49,7 @@ public final class TypeCastNode extends ExpressionNode {
var value = valueNode.executeGeneric(frame);
try {
typeNode.execute(frame, value);
return value;
return typeNode.execute(frame, value);
} catch (VmTypeMismatchException e) {
CompilerDirectives.transferToInterpreter();
throw e.toVmException();
File diff suppressed because it is too large Load Diff
@@ -17,10 +17,12 @@ package org.pkl.core.ast.type;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.NodeInfo;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.util.Nullable;
@NodeInfo(shortName = "is")
public final class TypeTestNode extends ExpressionNode {
@Child private ExpressionNode valueNode;
@Child private UnresolvedTypeNode unresolvedTypeNode;
@@ -50,9 +52,11 @@ public final class TypeTestNode extends ExpressionNode {
unresolvedTypeNode = null;
}
// TODO: throw if typeNode is FunctionTypeNode (it's impossible to check)
// https://github.com/apple/pkl/issues/639
Object value = valueNode.executeGeneric(frame);
try {
typeNode.execute(frame, value);
typeNode.executeEagerly(frame, value);
return true;
} catch (VmTypeMismatchException e) {
return false;
@@ -219,11 +219,11 @@ public abstract class UnresolvedTypeNode extends PklNode {
checkNumberOfTypeArguments(clazz);
if (clazz.isCollectionClass()) {
return CollectionTypeNodeGen.create(sourceSection, typeArgumentNodes[0].execute(frame));
return new CollectionTypeNode(sourceSection, typeArgumentNodes[0].execute(frame));
}
if (clazz.isListClass()) {
return ListTypeNodeGen.create(sourceSection, typeArgumentNodes[0].execute(frame));
return new ListTypeNode(sourceSection, typeArgumentNodes[0].execute(frame));
}
if (clazz.isSetClass()) {
@@ -231,25 +231,25 @@ public abstract class UnresolvedTypeNode extends PklNode {
}
if (clazz.isMapClass()) {
return MapTypeNodeGen.create(
return new MapTypeNode(
sourceSection,
typeArgumentNodes[0].execute(frame),
typeArgumentNodes[1].execute(frame));
}
if (clazz.isListingClass()) {
return ListingTypeNodeGen.create(sourceSection, typeArgumentNodes[0].execute(frame));
return new ListingTypeNode(sourceSection, typeArgumentNodes[0].execute(frame));
}
if (clazz.isMappingClass()) {
return MappingTypeNodeGen.create(
return new MappingTypeNode(
sourceSection,
typeArgumentNodes[0].execute(frame),
typeArgumentNodes[1].execute(frame));
}
if (clazz.isPairClass()) {
return PairTypeNodeGen.create(
return new PairTypeNode(
sourceSection,
typeArgumentNodes[0].execute(frame),
typeArgumentNodes[1].execute(frame));
@@ -324,7 +324,7 @@ public abstract class UnresolvedTypeNode extends PklNode {
public TypeNode execute(VirtualFrame frame) {
CompilerDirectives.transferToInterpreter();
return NullableTypeNodeGen.create(sourceSection, elementTypeNode.execute(frame));
return new NullableTypeNode(sourceSection, elementTypeNode.execute(frame));
}
}
@@ -15,16 +15,19 @@
*/
package org.pkl.core.ast.type;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.nodes.ControlFlowException;
import com.oracle.truffle.api.source.SourceSection;
import java.util.*;
import java.util.stream.Collectors;
import org.pkl.core.StackFrame;
import org.pkl.core.ValueFormatter;
import org.pkl.core.ast.type.TypeNode.UnionTypeNode;
import org.pkl.core.runtime.*;
import org.pkl.core.runtime.VmException.ProgramValue;
import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.Nullable;
/**
* Indicates that a type check failed. [TypeNode]s use this exception instead of [VmException] to
@@ -35,18 +38,35 @@ import org.pkl.core.util.ErrorMessages;
public abstract class VmTypeMismatchException extends ControlFlowException {
protected final SourceSection sourceSection;
protected final Object actualValue;
protected @Nullable Map<CallTarget, StackFrame> insertedStackFrames = null;
protected VmTypeMismatchException(SourceSection sourceSection, Object actualValue) {
this.sourceSection = sourceSection;
this.actualValue = actualValue;
}
@TruffleBoundary
public void putInsertedStackFrame(CallTarget callTarget, StackFrame stackFrame) {
if (this.insertedStackFrames == null) {
this.insertedStackFrames = new HashMap<>();
}
this.insertedStackFrames.put(callTarget, stackFrame);
}
@TruffleBoundary
public abstract void describe(StringBuilder builder, String indent);
@TruffleBoundary
public abstract VmException toVmException();
protected VmExceptionBuilder exceptionBuilder() {
var builder = new VmExceptionBuilder();
if (insertedStackFrames != null) {
builder.withInsertedStackFrames(insertedStackFrames);
}
return builder;
}
public static final class Simple extends VmTypeMismatchException {
private final Object expectedType;
@@ -128,11 +148,12 @@ public abstract class VmTypeMismatchException extends ControlFlowException {
return exceptionBuilder().build();
}
private VmExceptionBuilder exceptionBuilder() {
@Override
protected VmExceptionBuilder exceptionBuilder() {
var builder = new StringBuilder();
describe(builder, "");
return new VmExceptionBuilder()
return super.exceptionBuilder()
.adhocEvalError(builder.toString())
.withSourceSection(sourceSection);
}
@@ -162,11 +183,12 @@ public abstract class VmTypeMismatchException extends ControlFlowException {
return exceptionBuilder().build();
}
private VmExceptionBuilder exceptionBuilder() {
@Override
protected VmExceptionBuilder exceptionBuilder() {
var builder = new StringBuilder();
describe(builder, "");
return new VmExceptionBuilder()
return super.exceptionBuilder()
.adhocEvalError(builder.toString())
.withSourceSection(sourceSection);
}
@@ -199,14 +221,15 @@ public abstract class VmTypeMismatchException extends ControlFlowException {
return exceptionBuilder().build();
}
private VmExceptionBuilder exceptionBuilder() {
@Override
protected VmExceptionBuilder exceptionBuilder() {
var summary = new StringBuilder();
describeSummary(summary, "");
var details = new StringBuilder();
describeDetails(details, "");
return new VmExceptionBuilder()
return super.exceptionBuilder()
.adhocEvalError(summary.toString())
.withSourceSection(sourceSection)
.withHint(details.toString());
@@ -304,11 +327,12 @@ public abstract class VmTypeMismatchException extends ControlFlowException {
return exceptionBuilder().build();
}
private VmExceptionBuilder exceptionBuilder() {
@Override
protected VmExceptionBuilder exceptionBuilder() {
var builder = new StringBuilder();
describe(builder, "");
return new VmExceptionBuilder()
return super.exceptionBuilder()
.adhocEvalError(builder.toString())
.withSourceSection(sourceSection);
}
@@ -79,19 +79,25 @@ public final class TestRunner {
var factsMapping = (VmMapping) facts;
factsMapping.forceAndIterateMemberValues(
(groupKey, groupMember, groupValue) -> {
var listing = (VmListing) groupValue;
var result = results.newResult(String.valueOf(groupKey));
var groupListing = (VmListing) groupValue;
groupListing.forceAndIterateMemberValues(
((factIndex, factMember, factValue) -> {
assert factValue instanceof Boolean;
if (factValue == Boolean.FALSE) {
result.addFailure(
Failure.buildFactFailure(
factMember.getSourceSection(), getDisplayUri(factMember)));
return listing.iterateMembers(
(idx, member) -> {
if (member.isLocalOrExternalOrHidden()) {
return true;
}
try {
var factValue = VmUtils.readMember(listing, idx);
if (factValue == Boolean.FALSE) {
result.addFailure(
Failure.buildFactFailure(member.getSourceSection(), getDisplayUri(member)));
}
} catch (VmException err) {
result.addError(
new Error(err.getMessage(), err.toPklException(stackFrameTransformer)));
}
return true;
}));
return true;
});
});
}
@@ -142,12 +148,14 @@ public final class TestRunner {
var expectedExampleOutputs = loadExampleOutputs(expectedOutputFile);
var actualExampleOutputs = new MutableReference<VmDynamic>(null);
var allGroupsSucceeded = new MutableBoolean(true);
var errored = new MutableBoolean(false);
examples.forceAndIterateMemberValues(
(groupKey, groupMember, groupValue) -> {
var testName = String.valueOf(groupKey);
var group = (VmListing) groupValue;
var expectedGroup =
(VmDynamic) VmUtils.readMemberOrNull(expectedExampleOutputs, groupKey);
var result = results.newResult(testName);
if (expectedGroup == null) {
results.newResult(
@@ -158,8 +166,7 @@ public final class TestRunner {
}
if (group.getLength() != expectedGroup.getLength()) {
results.newResult(
testName,
result.addFailure(
Failure.buildExampleLengthMismatchFailure(
getDisplayUri(groupMember),
String.valueOf(groupKey),
@@ -169,8 +176,21 @@ public final class TestRunner {
}
var groupSucceeded = new MutableBoolean(true);
group.forceAndIterateMemberValues(
((exampleIndex, exampleMember, exampleValue) -> {
group.iterateMembers(
((exampleIndex, exampleMember) -> {
if (exampleMember.isLocalOrExternalOrHidden()) {
return true;
}
Object exampleValue;
try {
exampleValue = VmUtils.readMember(group, exampleIndex);
} catch (VmException err) {
errored.set(true);
result.addError(
new Error(err.getMessage(), err.toPklException(stackFrameTransformer)));
groupSucceeded.set(false);
return true;
}
var expectedValue = VmUtils.readMember(expectedGroup, exampleIndex);
var exampleValuePcf = renderAsPcf(exampleValue);
@@ -202,8 +222,7 @@ public final class TestRunner {
.build();
}
results.newResult(
testName,
result.addFailure(
Failure.buildExampleFailure(
getDisplayUri(exampleMember),
getDisplayUri(expectedMember),
@@ -215,9 +234,7 @@ public final class TestRunner {
return true;
}));
if (groupSucceeded.get()) {
results.newResult(testName);
} else {
if (!groupSucceeded.get()) {
allGroupsSucceeded.set(false);
}
@@ -231,26 +248,52 @@ public final class TestRunner {
}
if (examples.getCachedValue(groupKey) == null) {
allGroupsSucceeded.set(false);
results.newResult(
String.valueOf(groupKey),
Failure.buildExamplePropertyMismatchFailure(
getDisplayUri(groupMember), String.valueOf(groupKey), false));
results
.newResult(String.valueOf(groupKey))
.addFailure(
Failure.buildExamplePropertyMismatchFailure(
getDisplayUri(groupMember), String.valueOf(groupKey), false));
}
return true;
});
if (!allGroupsSucceeded.get() && actualExampleOutputs.isNull()) {
if (!allGroupsSucceeded.get() && actualExampleOutputs.isNull() && !errored.get()) {
writeExampleOutputs(actualOutputFile, examples);
}
}
private void doRunAndWriteExamples(VmMapping examples, Path outputFile, TestResults results) {
examples.forceAndIterateMemberValues(
(groupKey, groupMember, groupValue) -> {
results.newResult(String.valueOf(groupKey)).setExampleWritten(true);
return true;
});
writeExampleOutputs(outputFile, examples);
var allSucceeded =
examples.forceAndIterateMemberValues(
(groupKey, groupMember, groupValue) -> {
var listing = (VmListing) groupValue;
var success =
listing.iterateMembers(
(idx, member) -> {
if (member.isLocalOrExternalOrHidden()) {
return true;
}
try {
VmUtils.readMember(listing, idx);
return true;
} catch (VmException err) {
results
.newResult(String.valueOf(groupKey))
.addError(
new Error(
err.getMessage(), err.toPklException(stackFrameTransformer)));
return false;
}
});
if (!success) {
return false;
}
results.newResult(String.valueOf(groupKey)).setExampleWritten(true);
return true;
});
if (allSucceeded) {
writeExampleOutputs(outputFile, examples);
}
}
private void writeExampleOutputs(Path outputFile, VmMapping examples) {
@@ -15,10 +15,12 @@
*/
package org.pkl.core.runtime;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.SourceSection;
import java.util.List;
import java.util.Map;
import org.pkl.core.*;
import org.pkl.core.util.Nullable;
@@ -32,7 +34,8 @@ public final class VmBugException extends VmException {
@Nullable Node location,
@Nullable SourceSection sourceSection,
@Nullable String memberName,
@Nullable String hint) {
@Nullable String hint,
Map<CallTarget, StackFrame> insertedStackFrames) {
super(
message,
@@ -43,7 +46,8 @@ public final class VmBugException extends VmException {
location,
sourceSection,
memberName,
hint);
hint,
insertedStackFrames);
}
@Override
@@ -15,9 +15,12 @@
*/
package org.pkl.core.runtime;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.SourceSection;
import java.util.List;
import java.util.Map;
import org.pkl.core.StackFrame;
import org.pkl.core.util.Nullable;
public class VmEvalException extends VmException {
@@ -30,7 +33,8 @@ public class VmEvalException extends VmException {
@Nullable Node location,
@Nullable SourceSection sourceSection,
@Nullable String memberName,
@Nullable String hint) {
@Nullable String hint,
Map<CallTarget, StackFrame> insertedStackFrames) {
super(
message,
@@ -41,6 +45,7 @@ public class VmEvalException extends VmException {
location,
sourceSection,
memberName,
hint);
hint,
insertedStackFrames);
}
}
@@ -20,7 +20,6 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.exception.AbstractTruffleException;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.SourceSection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.pkl.core.*;
@@ -33,8 +32,7 @@ public abstract class VmException extends AbstractTruffleException {
private final @Nullable SourceSection sourceSection;
private final @Nullable String memberName;
protected @Nullable String hint;
private final Map<CallTarget, StackFrame> insertedStackFrames = new HashMap<>();
private final Map<CallTarget, StackFrame> insertedStackFrames;
public VmException(
String message,
@@ -45,7 +43,8 @@ public abstract class VmException extends AbstractTruffleException {
@Nullable Node location,
@Nullable SourceSection sourceSection,
@Nullable String memberName,
@Nullable String hint) {
@Nullable String hint,
Map<CallTarget, StackFrame> insertedStackFrames) {
super(message, cause, UNLIMITED_STACK_TRACE, location);
this.isExternalMessage = isExternalMessage;
this.messageArguments = messageArguments;
@@ -53,6 +52,7 @@ public abstract class VmException extends AbstractTruffleException {
this.sourceSection = sourceSection;
this.memberName = memberName;
this.hint = hint;
this.insertedStackFrames = insertedStackFrames;
}
public final boolean isExternalMessage() {
@@ -15,11 +15,13 @@
*/
package org.pkl.core.runtime;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.SourceSection;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.pkl.core.StackFrame;
import org.pkl.core.runtime.MemberLookupSuggestions.Candidate.Kind;
import org.pkl.core.runtime.VmException.ProgramValue;
import org.pkl.core.util.Nullable;
@@ -50,6 +52,7 @@ import org.pkl.core.util.Nullable;
public final class VmExceptionBuilder {
private @Nullable Object receiver;
private @Nullable Map<CallTarget, StackFrame> insertedStackFrames;
public static class MultilineValue {
private final Iterable<?> lines;
@@ -329,11 +332,19 @@ public final class VmExceptionBuilder {
return this;
}
public VmExceptionBuilder withInsertedStackFrames(
Map<CallTarget, StackFrame> insertedStackFrames) {
this.insertedStackFrames = insertedStackFrames;
return this;
}
public VmException build() {
if (message == null) {
throw new IllegalStateException("No message set.");
}
var effectiveInsertedStackFrames =
insertedStackFrames == null ? new HashMap<CallTarget, StackFrame>() : insertedStackFrames;
return switch (kind) {
case EVAL_ERROR ->
new VmEvalException(
@@ -345,7 +356,8 @@ public final class VmExceptionBuilder {
location,
sourceSection,
memberName,
hint);
hint,
effectiveInsertedStackFrames);
case UNDEFINED_VALUE ->
new VmUndefinedValueException(
message,
@@ -357,7 +369,8 @@ public final class VmExceptionBuilder {
sourceSection,
memberName,
hint,
receiver);
receiver,
effectiveInsertedStackFrames);
case BUG ->
new VmBugException(
message,
@@ -368,7 +381,8 @@ public final class VmExceptionBuilder {
location,
sourceSection,
memberName,
hint);
hint,
effectiveInsertedStackFrames);
};
}
@@ -113,7 +113,7 @@ public final class VmFunction extends VmObjectLike {
@Override
@TruffleBoundary
public void setCachedValue(Object key, Object value) {
public void setCachedValue(Object key, Object value, ObjectMember objectMember) {
throw new VmExceptionBuilder()
.bug("Class `VmFunction` does not support method `setCachedValue()`.")
.build();
@@ -21,13 +21,12 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.ast.member.ListingOrMappingTypeCastNode;
import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.Nullable;
// TODO: make sure that "default" isn't forced
// when a listing is rendered ("default" should be allowed to be partial)
public final class VmListing extends VmObject {
public final class VmListing extends VmListingOrMapping<VmListing> {
private static final class EmptyHolder {
private static final VmListing EMPTY =
new VmListing(
@@ -48,7 +47,25 @@ public final class VmListing extends VmObject {
VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members,
int length) {
super(enclosingFrame, Objects.requireNonNull(parent), members);
super(enclosingFrame, Objects.requireNonNull(parent), members, null, null, null);
this.length = length;
}
public VmListing(
MaterializedFrame enclosingFrame,
VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members,
int length,
@Nullable VmListing delegate,
ListingOrMappingTypeCastNode typeCheckNode,
MaterializedFrame typeNodeFrame) {
super(
enclosingFrame,
Objects.requireNonNull(parent),
members,
delegate,
typeCheckNode,
typeNodeFrame);
this.length = length;
}
@@ -100,6 +117,20 @@ public final class VmListing extends VmObject {
return converter.convertListing(this, path);
}
@Override
public VmListing withCheckedMembers(
ListingOrMappingTypeCastNode typeCheckNode, MaterializedFrame typeNodeFrame) {
return new VmListing(
getEnclosingFrame(),
Objects.requireNonNull(parent),
members,
length,
this,
typeCheckNode,
typeNodeFrame);
}
@Override
@TruffleBoundary
public boolean equals(@Nullable Object obj) {
@@ -0,0 +1,169 @@
/**
* Copyright © 2024 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.runtime;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.frame.MaterializedFrame;
import com.oracle.truffle.api.nodes.IndirectCallNode;
import org.graalvm.collections.EconomicMap;
import org.graalvm.collections.EconomicSet;
import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.PklBugException;
import org.pkl.core.ast.member.ListingOrMappingTypeCastNode;
import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.ast.type.TypeNode;
import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.EconomicSets;
import org.pkl.core.util.Nullable;
public abstract class VmListingOrMapping<SELF extends VmListingOrMapping<SELF>> extends VmObject {
/**
* A Listing or Mapping typecast creates a new object that contains a new typecheck node, and
* delegates member lookups to this delegate.
*/
private final @Nullable SELF delegate;
private final @Nullable ListingOrMappingTypeCastNode typeCastNode;
private final MaterializedFrame typeNodeFrame;
private final EconomicMap<Object, ObjectMember> cachedMembers = EconomicMaps.create();
private final EconomicSet<Object> checkedMembers = EconomicSets.create();
public VmListingOrMapping(
MaterializedFrame enclosingFrame,
@Nullable VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members,
@Nullable SELF delegate,
@Nullable ListingOrMappingTypeCastNode typeCastNode,
@Nullable MaterializedFrame typeNodeFrame) {
super(enclosingFrame, parent, members);
this.delegate = delegate;
this.typeCastNode = typeCastNode;
this.typeNodeFrame = typeNodeFrame;
}
ObjectMember findMember(Object key) {
var member = EconomicMaps.get(cachedMembers, key);
if (member != null) {
return member;
}
if (delegate != null) {
return delegate.findMember(key);
}
// member is guaranteed to exist; this is only called if `getCachedValue()` returns non-null
// and `setCachedValue` will record the object member in `cachedMembers`.
throw PklBugException.unreachableCode();
}
public @Nullable ListingOrMappingTypeCastNode getTypeCastNode() {
return typeCastNode;
}
@Override
public void setCachedValue(Object key, Object value, ObjectMember objectMember) {
super.setCachedValue(key, value, objectMember);
EconomicMaps.put(cachedMembers, key, objectMember);
}
@Override
public @Nullable Object getCachedValue(Object key) {
var myCachedValue = super.getCachedValue(key);
if (myCachedValue != null || delegate == null) {
return myCachedValue;
}
var memberValue = delegate.getCachedValue(key);
// if this object member appears inside `checkedMembers`, we have already checked its type
// and can safely return it.
if (EconomicSets.contains(checkedMembers, key)) {
return memberValue;
}
if (memberValue == null) {
return null;
}
// If a cached value already exists on the delegate, run a typecast on it.
// optimization: don't use `VmUtils.findMember` to avoid iterating over all members
var objectMember = findMember(key);
var ret = typecastObjectMember(objectMember, memberValue, IndirectCallNode.getUncached());
if (ret != memberValue) {
EconomicMaps.put(cachedValues, key, ret);
} else {
// optimization: don't add to own cached values if typecast results in the same value
EconomicSets.add(checkedMembers, key);
}
return ret;
}
@Override
public Object getExtraStorage() {
if (delegate != null) {
return delegate.getExtraStorage();
}
assert extraStorage != null;
return extraStorage;
}
/** Perform a typecast on this member, */
public Object typecastObjectMember(
ObjectMember member, Object memberValue, IndirectCallNode callNode) {
if (!(member.isEntry() || member.isElement()) || typeCastNode == null) {
return memberValue;
}
assert typeNodeFrame != null;
var ret = memberValue;
if (delegate != null) {
ret = delegate.typecastObjectMember(member, ret, callNode);
}
var callTarget = typeCastNode.getCallTarget();
try {
return callNode.call(
callTarget, VmUtils.getReceiver(typeNodeFrame), VmUtils.getOwner(typeNodeFrame), ret);
} catch (VmException vmException) {
CompilerDirectives.transferToInterpreter();
// treat typecheck as part of the call stack to read the original member if there is a
// source section for it.
var sourceSection = member.getBodySection();
if (!sourceSection.isAvailable()) {
sourceSection = member.getSourceSection();
}
if (sourceSection.isAvailable()) {
vmException
.getInsertedStackFrames()
.put(callTarget, VmUtils.createStackFrame(sourceSection, member.getQualifiedName()));
}
throw vmException;
}
}
public abstract SELF withCheckedMembers(
ListingOrMappingTypeCastNode typeCastNode, MaterializedFrame typeNodeFrame);
/** Tells if this mapping/listing runs the same typechecks as {@code typeNode}. */
public boolean hasSameChecksAs(TypeNode typeNode) {
if (typeCastNode == null) {
return false;
}
if (typeCastNode.getTypeNode().isEquivalentTo(typeNode)) {
return true;
}
// we can say the check is the same if the delegate has this check.
// when `Listing<Any>` delegates to `Listing<UInt>`, it has the same checks as a `UInt`
// typenode.
if (delegate != null) {
return delegate.hasSameChecksAs(typeNode);
}
return false;
}
}
@@ -21,14 +21,14 @@ import java.util.Map;
import java.util.Objects;
import javax.annotation.concurrent.GuardedBy;
import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.ast.member.ListingOrMappingTypeCastNode;
import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.util.CollectionUtils;
import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.LateInit;
// TODO: make sure that "default" isn't forced
// when a mapping is rendered ("default" should be allowed to be partial)
public final class VmMapping extends VmObject {
public final class VmMapping extends VmListingOrMapping<VmMapping> {
private int cachedEntryCount = -1;
@GuardedBy("this")
@@ -51,7 +51,23 @@ public final class VmMapping extends VmObject {
VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members) {
super(enclosingFrame, Objects.requireNonNull(parent), members);
super(enclosingFrame, Objects.requireNonNull(parent), members, null, null, null);
}
public VmMapping(
MaterializedFrame enclosingFrame,
VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members,
VmMapping delegate,
ListingOrMappingTypeCastNode typeCheckNode,
MaterializedFrame typeNodeFrame) {
super(
enclosingFrame,
Objects.requireNonNull(parent),
members,
delegate,
typeCheckNode,
typeNodeFrame);
}
public static boolean isDefaultProperty(Object propertyKey) {
@@ -181,4 +197,17 @@ public final class VmMapping extends VmObject {
cachedEntryCount = result;
return result;
}
@Override
@TruffleBoundary
public VmMapping withCheckedMembers(
ListingOrMappingTypeCastNode typeCheckNode, MaterializedFrame typeNodeFrame) {
return new VmMapping(
getEnclosingFrame(),
Objects.requireNonNull(getParent()),
getMembers(),
this,
typeCheckNode,
typeNodeFrame);
}
}
@@ -82,12 +82,12 @@ public abstract class VmObject extends VmObjectLike {
}
@Override
public final @Nullable Object getCachedValue(Object key) {
public @Nullable Object getCachedValue(Object key) {
return EconomicMaps.get(cachedValues, key);
}
@Override
public final void setCachedValue(Object key, Object value) {
public void setCachedValue(Object key, Object value, ObjectMember objectMember) {
EconomicMaps.put(cachedValues, key, value);
}
@@ -52,7 +52,7 @@ public abstract class VmObjectLike extends VmValue {
return extraStorage != null;
}
public final Object getExtraStorage() {
public Object getExtraStorage() {
assert extraStorage != null;
return extraStorage;
}
@@ -96,7 +96,7 @@ public abstract class VmObjectLike extends VmValue {
* receiver.
*/
@TruffleBoundary
public abstract void setCachedValue(Object key, Object value);
public abstract void setCachedValue(Object key, Object value, ObjectMember objectMember);
/**
* Prefer this method over {@link #getCachedValue} if the value is not required. (There is no
@@ -15,11 +15,22 @@
*/
package org.pkl.core.runtime;
import java.util.HashMap;
import java.util.List;
public final class VmStackOverflowException extends VmException {
public VmStackOverflowException(StackOverflowError e) {
super("stackOverflow", e, true, new Object[0], List.of(), null, null, null, null);
super(
"stackOverflow",
e,
true,
new Object[0],
List.of(),
null,
null,
null,
null,
new HashMap<>());
}
}
@@ -15,10 +15,13 @@
*/
package org.pkl.core.runtime;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.SourceSection;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import org.pkl.core.StackFrame;
import org.pkl.core.parser.Lexer;
import org.pkl.core.util.Nullable;
@@ -35,7 +38,8 @@ public final class VmUndefinedValueException extends VmEvalException {
@Nullable SourceSection sourceSection,
@Nullable String memberName,
@Nullable String hint,
@Nullable Object receiver) {
@Nullable Object receiver,
Map<CallTarget, StackFrame> insertedStackFrames) {
super(
message,
@@ -46,7 +50,8 @@ public final class VmUndefinedValueException extends VmEvalException {
location,
sourceSection,
memberName,
hint);
hint,
insertedStackFrames);
this.receiver = receiver;
}
@@ -263,6 +263,7 @@ public final class VmUtils {
final var constantValue = member.getConstantValue();
if (constantValue != null) {
var ret = constantValue;
// for a property, do a type check
if (member.isProp()) {
var property = receiver.getVmClass().getProperty(member.getName());
@@ -270,10 +271,15 @@ public final class VmUtils {
var callTarget = property.getTypeNode().getCallTarget();
try {
if (checkType) {
callNode.call(callTarget, receiver, property.getOwner(), constantValue);
ret = callNode.call(callTarget, receiver, property.getOwner(), constantValue);
} else {
callNode.call(
callTarget, receiver, property.getOwner(), constantValue, SKIP_TYPECHECK_MARKER);
ret =
callNode.call(
callTarget,
receiver,
property.getOwner(),
constantValue,
VmUtils.SKIP_TYPECHECK_MARKER);
}
} catch (VmException e) {
CompilerDirectives.transferToInterpreter();
@@ -293,21 +299,25 @@ public final class VmUtils {
throw e;
}
}
} else if (receiver instanceof VmListingOrMapping<?> vmListingOrMapping) {
ret = vmListingOrMapping.typecastObjectMember(member, ret, callNode);
}
receiver.setCachedValue(memberKey, constantValue);
return constantValue;
receiver.setCachedValue(memberKey, ret, member);
return ret;
}
var callTarget = member.getCallTarget();
Object computedValue;
Object ret;
if (checkType) {
computedValue = callNode.call(callTarget, receiver, owner, memberKey);
ret = callNode.call(callTarget, receiver, owner, memberKey);
} else {
computedValue = callNode.call(callTarget, receiver, owner, memberKey, SKIP_TYPECHECK_MARKER);
ret = callNode.call(callTarget, receiver, owner, memberKey, VmUtils.SKIP_TYPECHECK_MARKER);
}
receiver.setCachedValue(memberKey, computedValue);
return computedValue;
if (receiver instanceof VmListingOrMapping<?> vmListingOrMapping) {
ret = vmListingOrMapping.typecastObjectMember(member, ret, callNode);
}
receiver.setCachedValue(memberKey, ret, member);
return ret;
}
public static @Nullable ObjectMember findMember(VmObjectLike receiver, Object memberKey) {
@@ -849,4 +859,17 @@ public final class VmUtils {
public static <K, V> V getMapValue(Map<K, V> map, K key) {
return map.get(key);
}
/**
* If true, the value computed by this node is not the final value exposed to user code but will
* still be amended.
*
* <p>Used to disable type check for to-be-amended properties. See {@link
* org.pkl.core.runtime.VmUtils#SKIP_TYPECHECK_MARKER}. IDEA: might be more appropriate to only
* skip constraints check
*/
public static boolean shouldRunTypeCheck(VirtualFrame frame) {
return frame.getArguments().length != 4
|| frame.getArguments()[3] != VmUtils.SKIP_TYPECHECK_MARKER;
}
}
@@ -18,6 +18,7 @@ package org.pkl.core.stdlib.base;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.IndirectCallNode;
import java.util.HashSet;
import org.pkl.core.ast.lambda.ApplyVmFunction3Node;
import org.pkl.core.ast.lambda.ApplyVmFunction3NodeGen;
import org.pkl.core.runtime.*;
@@ -50,9 +51,14 @@ public final class MappingNodes {
@Specialization
@TruffleBoundary
protected long eval(VmMapping self) {
MutableLong count = new MutableLong(0);
self.iterateMemberValues(
(key, member, value) -> {
var count = new MutableLong(0);
var visited = new HashSet<>();
self.iterateMembers(
(key, member) -> {
var alreadyVisited = !visited.add(key);
// important to record hidden member as visited before skipping it
// because any overriding member won't carry a `hidden` identifier
if (alreadyVisited || member.isLocalOrExternalOrHidden()) return true;
count.getAndIncrement();
return true;
});
@@ -42,8 +42,6 @@ import org.pkl.core.ast.type.TypeNode.StringTypeNode;
import org.pkl.core.ast.type.TypeNode.TypeAliasTypeNode;
import org.pkl.core.ast.type.TypeNode.UnionOfStringLiteralsTypeNode;
import org.pkl.core.ast.type.TypeNode.UnionTypeNode;
import org.pkl.core.ast.type.TypeNodeFactory.ListingTypeNodeGen;
import org.pkl.core.ast.type.TypeNodeFactory.MappingTypeNodeGen;
import org.pkl.core.ast.type.VmTypeMismatchException;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmClass;
@@ -575,7 +573,7 @@ public final class RendererNodes {
type =
requiresWrapper()
? null
: ListingTypeNodeGen.create(VmUtils.unavailableSourceSection(), valueType);
: new ListingTypeNode(VmUtils.unavailableSourceSection(), valueType);
return type;
} else if (type instanceof MappingTypeNode mappingType) {
var keyType = resolveType(mappingType.getKeyTypeNode());
@@ -589,8 +587,7 @@ public final class RendererNodes {
}
var valueType = resolveType(mappingType.getValueTypeNode());
assert valueType != null : "Incomplete or malformed Mapping type";
mappingType =
MappingTypeNodeGen.create(VmUtils.unavailableSourceSection(), keyType, valueType);
mappingType = new MappingTypeNode(VmUtils.unavailableSourceSection(), keyType, valueType);
type = requiresWrapper() ? null : mappingType;
return type;
@@ -30,4 +30,9 @@ public final class EconomicSets {
public static <E> boolean add(EconomicSet<E> self, E element) {
return self.add(element);
}
@TruffleBoundary
public static <E> boolean contains(EconomicSet<E> self, E element) {
return self.contains(element);
}
}