mirror of
https://github.com/apple/pkl.git
synced 2026-03-12 13:21:40 +01:00
Improve lazy type checking of listings and mappings (#789)
Motivation:
- simplify implementation of lazy type checking
- fix correctness issues of lazy type checking (#785)
Changes:
- implement listing/mapping type cast via amendment (`parent`) instead of delegation (`delegate`)
- handle type checking of *computed* elements/entries in the same way as type checking of computed properties
- ElementOrEntryNode is the equivalent of TypeCheckedPropertyNode
- remove fields VmListingOrMapping.delegate/typeNodeFrame/cachedMembers/checkedMembers
- fix #785 by executing all type casts between a member's owner and receiver
- fix #823 by storing owner and receiver directly
instead of storing the mutable frame containing them (typeNodeFrame)
- remove overrides of VmObject methods that are no longer required
- good for Truffle partial evaluation and JVM inlining
- revert a85a173faa except for added tests
- move `VmUtils.setOwner` and `VmUtils.setReceiver` and make them private
- these methods aren't generally safe to use
Result:
- simpler code with greater optimization potential
- VmListingOrMapping can now have both a type node and new members
- fewer changes to surrounding code
- smaller memory footprint
- better performance in some cases
- fixes https://github.com/apple/pkl/issues/785
- fixes https://github.com/apple/pkl/issues/823
Potential future optimizations:
- avoid lazy type checking overhead for untyped listings/mappings
- improve efficiency of forcing a typed listing/mapping
- currently, lazy type checking will traverse the parent chain once per member,
reducing the performance benefit of shallow-forcing
a listing/mapping over evaluating each member individually
- avoid creating an intermediate untyped listing/mapping in the following cases:
- `new Listing<X> {...}`
- amendment of `property: Listing<X>`
This commit is contained in:
@@ -1221,7 +1221,7 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
member.initConstantValue(constantNode);
|
||||
} else {
|
||||
member.initMemberNode(
|
||||
new UntypedObjectMemberNode(
|
||||
ElementOrEntryNodeGen.create(
|
||||
language, scope.buildFrameDescriptor(), member, elementNode));
|
||||
}
|
||||
|
||||
@@ -1278,7 +1278,7 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
member.initConstantValue(constantNode);
|
||||
} else {
|
||||
member.initMemberNode(
|
||||
new UntypedObjectMemberNode(
|
||||
ElementOrEntryNodeGen.create(
|
||||
language, scope.buildFrameDescriptor(), member, valueNode));
|
||||
}
|
||||
} else { // ["key"] { ... }
|
||||
@@ -1287,7 +1287,7 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
objectBodyCtxs,
|
||||
new ReadSuperEntryNode(unavailableSourceSection(), new GetMemberKeyNode()));
|
||||
member.initMemberNode(
|
||||
new UntypedObjectMemberNode(
|
||||
ElementOrEntryNodeGen.create(
|
||||
language, scope.buildFrameDescriptor(), member, objectBody));
|
||||
}
|
||||
|
||||
@@ -2446,6 +2446,7 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
|
||||
return new UnresolvedTypeNode.Parameterized(
|
||||
createSourceSection(ctx),
|
||||
language,
|
||||
doVisitTypeName(idCtx),
|
||||
argCtx.ts.stream().map(this::visitType).toArray(UnresolvedTypeNode[]::new));
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public abstract class GeneratorPredicateMemberNode extends GeneratorMemberNode {
|
||||
var callTarget = member.getCallTarget();
|
||||
value = callTarget.call(parent, owner, key);
|
||||
}
|
||||
owner.setCachedValue(key, value, member);
|
||||
owner.setCachedValue(key, value);
|
||||
}
|
||||
|
||||
frame.setAuxiliarySlot(customThisSlot, value);
|
||||
|
||||
@@ -71,7 +71,7 @@ public final class ReadLocalPropertyNode extends ExpressionNode {
|
||||
|
||||
if (result == null) {
|
||||
result = callNode.call(objReceiver, owner, property.getName());
|
||||
objReceiver.setCachedValue(property, result, property);
|
||||
objReceiver.setCachedValue(property, result);
|
||||
}
|
||||
|
||||
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, member);
|
||||
baseModule.setCachedValue(variableName, constantValue);
|
||||
return new ConstantValueNode(sourceSection, constantValue);
|
||||
}
|
||||
|
||||
var computedValue = member.getCallTarget().call(baseModule, baseModule);
|
||||
baseModule.setCachedValue(variableName, computedValue, member);
|
||||
baseModule.setCachedValue(variableName, computedValue);
|
||||
return new ConstantValueNode(sourceSection, computedValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.dsl.Cached;
|
||||
import com.oracle.truffle.api.dsl.Cached.Shared;
|
||||
import com.oracle.truffle.api.dsl.Executed;
|
||||
import com.oracle.truffle.api.dsl.Specialization;
|
||||
import com.oracle.truffle.api.frame.FrameDescriptor;
|
||||
import com.oracle.truffle.api.frame.VirtualFrame;
|
||||
import com.oracle.truffle.api.nodes.IndirectCallNode;
|
||||
import org.pkl.core.ast.ExpressionNode;
|
||||
import org.pkl.core.ast.expression.primary.GetReceiverNode;
|
||||
import org.pkl.core.runtime.VmDynamic;
|
||||
import org.pkl.core.runtime.VmLanguage;
|
||||
import org.pkl.core.runtime.VmListing;
|
||||
import org.pkl.core.runtime.VmMapping;
|
||||
import org.pkl.core.runtime.VmUtils;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
/** Equivalent of {@link TypedPropertyNode} for elements/entries. */
|
||||
public abstract class ElementOrEntryNode extends RegularMemberNode {
|
||||
@Child @Executed protected ExpressionNode receiverNode = new GetReceiverNode();
|
||||
|
||||
protected ElementOrEntryNode(
|
||||
@Nullable VmLanguage language,
|
||||
FrameDescriptor descriptor,
|
||||
ObjectMember member,
|
||||
ExpressionNode bodyNode) {
|
||||
|
||||
super(language, descriptor, member, bodyNode);
|
||||
}
|
||||
|
||||
@Specialization
|
||||
protected Object evalListing(
|
||||
VirtualFrame frame,
|
||||
VmListing receiver,
|
||||
@Cached("create()") @Shared("callNode") IndirectCallNode callNode) {
|
||||
var result = executeBody(frame);
|
||||
return VmUtils.shouldRunTypeCheck(frame)
|
||||
? receiver.executeTypeCasts(result, VmUtils.getOwner(frame), callNode, null, null)
|
||||
: result;
|
||||
}
|
||||
|
||||
@Specialization
|
||||
protected Object evalMapping(
|
||||
VirtualFrame frame,
|
||||
VmMapping receiver,
|
||||
@Cached("create()") @Shared("callNode") IndirectCallNode callNode) {
|
||||
var result = executeBody(frame);
|
||||
return VmUtils.shouldRunTypeCheck(frame)
|
||||
? receiver.executeTypeCasts(result, VmUtils.getOwner(frame), callNode, null, null)
|
||||
: result;
|
||||
}
|
||||
|
||||
@Specialization
|
||||
protected Object evalDynamic(VirtualFrame frame, VmDynamic ignored) {
|
||||
return executeBody(frame);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ 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 {
|
||||
public final class ListingOrMappingTypeCastNode extends PklRootNode {
|
||||
|
||||
@Child private TypeNode typeNode;
|
||||
private final String qualifiedName;
|
||||
|
||||
@@ -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, member);
|
||||
module.setCachedValue(importName, result);
|
||||
}
|
||||
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, member);
|
||||
module.setCachedValue(typeName, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ 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.Specialization;
|
||||
import com.oracle.truffle.api.frame.Frame;
|
||||
import com.oracle.truffle.api.frame.FrameDescriptor;
|
||||
import com.oracle.truffle.api.frame.FrameSlotKind;
|
||||
import com.oracle.truffle.api.frame.VirtualFrame;
|
||||
@@ -1418,8 +1419,9 @@ public abstract class TypeNode extends PklNode {
|
||||
}
|
||||
|
||||
public static final class ListingTypeNode extends ListingOrMappingTypeNode {
|
||||
public ListingTypeNode(SourceSection sourceSection, TypeNode valueTypeNode) {
|
||||
super(sourceSection, null, valueTypeNode);
|
||||
public ListingTypeNode(
|
||||
SourceSection sourceSection, VmLanguage language, TypeNode valueTypeNode) {
|
||||
super(sourceSection, language, null, valueTypeNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1427,7 +1429,17 @@ public abstract class TypeNode extends PklNode {
|
||||
if (!(value instanceof VmListing vmListing)) {
|
||||
throw typeMismatch(value, BaseModule.getListingClass());
|
||||
}
|
||||
return doTypeCast(frame, vmListing);
|
||||
if (vmListing.isValueTypeKnownSubtypeOf(valueTypeNode)) {
|
||||
return vmListing;
|
||||
}
|
||||
return new VmListing(
|
||||
vmListing.getEnclosingFrame(),
|
||||
vmListing,
|
||||
EconomicMaps.emptyMap(),
|
||||
vmListing.getLength(),
|
||||
getValueTypeCastNode(),
|
||||
VmUtils.getReceiver(frame),
|
||||
VmUtils.getOwner(frame));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1470,9 +1482,12 @@ public abstract class TypeNode extends PklNode {
|
||||
|
||||
public static final class MappingTypeNode extends ListingOrMappingTypeNode {
|
||||
public MappingTypeNode(
|
||||
SourceSection sourceSection, TypeNode keyTypeNode, TypeNode valueTypeNode) {
|
||||
SourceSection sourceSection,
|
||||
VmLanguage language,
|
||||
TypeNode keyTypeNode,
|
||||
TypeNode valueTypeNode) {
|
||||
|
||||
super(sourceSection, keyTypeNode, valueTypeNode);
|
||||
super(sourceSection, language, keyTypeNode, valueTypeNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1482,7 +1497,16 @@ public abstract class TypeNode extends PklNode {
|
||||
}
|
||||
// execute type checks on mapping keys
|
||||
doEagerCheck(frame, vmMapping, false, true);
|
||||
return doTypeCast(frame, vmMapping);
|
||||
if (vmMapping.isValueTypeKnownSubtypeOf(valueTypeNode)) {
|
||||
return vmMapping;
|
||||
}
|
||||
return new VmMapping(
|
||||
vmMapping.getEnclosingFrame(),
|
||||
vmMapping,
|
||||
EconomicMaps.emptyMap(),
|
||||
getValueTypeCastNode(),
|
||||
VmUtils.getReceiver(frame),
|
||||
VmUtils.getOwner(frame));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1530,17 +1554,22 @@ public abstract class TypeNode extends PklNode {
|
||||
}
|
||||
|
||||
public abstract static class ListingOrMappingTypeNode extends ObjectSlotTypeNode {
|
||||
private final VmLanguage language;
|
||||
@Child protected @Nullable TypeNode keyTypeNode;
|
||||
@Child protected TypeNode valueTypeNode;
|
||||
@Child @Nullable protected ListingOrMappingTypeCastNode listingOrMappingTypeCastNode;
|
||||
@Child @Nullable protected ListingOrMappingTypeCastNode valueTypeCastNode;
|
||||
|
||||
private final boolean skipKeyTypeChecks;
|
||||
private final boolean skipValueTypeChecks;
|
||||
|
||||
protected ListingOrMappingTypeNode(
|
||||
SourceSection sourceSection, @Nullable TypeNode keyTypeNode, TypeNode valueTypeNode) {
|
||||
SourceSection sourceSection,
|
||||
VmLanguage language,
|
||||
@Nullable TypeNode keyTypeNode,
|
||||
TypeNode valueTypeNode) {
|
||||
|
||||
super(sourceSection);
|
||||
this.language = language;
|
||||
this.keyTypeNode = keyTypeNode;
|
||||
this.valueTypeNode = valueTypeNode;
|
||||
|
||||
@@ -1560,17 +1589,14 @@ public abstract class TypeNode extends PklNode {
|
||||
return valueTypeNode;
|
||||
}
|
||||
|
||||
protected ListingOrMappingTypeCastNode getListingOrMappingTypeCastNode() {
|
||||
if (listingOrMappingTypeCastNode == null) {
|
||||
protected ListingOrMappingTypeCastNode getValueTypeCastNode() {
|
||||
if (valueTypeCastNode == null) {
|
||||
CompilerDirectives.transferToInterpreterAndInvalidate();
|
||||
listingOrMappingTypeCastNode =
|
||||
valueTypeCastNode =
|
||||
new ListingOrMappingTypeCastNode(
|
||||
VmLanguage.get(this),
|
||||
getRootNode().getFrameDescriptor(),
|
||||
valueTypeNode,
|
||||
getRootNode().getName());
|
||||
language, new FrameDescriptor(), valueTypeNode, getRootNode().getName());
|
||||
}
|
||||
return listingOrMappingTypeCastNode;
|
||||
return valueTypeCastNode;
|
||||
}
|
||||
|
||||
// either (if defaultMemberValue != null):
|
||||
@@ -1651,15 +1677,6 @@ public abstract class TypeNode extends PklNode {
|
||||
EconomicMaps.of(Identifier.DEFAULT, defaultMember));
|
||||
}
|
||||
|
||||
protected <T extends VmListingOrMapping<T>> T doTypeCast(VirtualFrame frame, T original) {
|
||||
// optimization: don't create new object if the original already has the same typecheck, or if
|
||||
// this typecheck is a no-op.
|
||||
if (isNoopTypeCheck() || original.hasSameChecksAs(valueTypeNode)) {
|
||||
return original;
|
||||
}
|
||||
return original.withCheckedMembers(getListingOrMappingTypeCastNode(), frame.materialize());
|
||||
}
|
||||
|
||||
protected void doEagerCheck(VirtualFrame frame, VmObject object) {
|
||||
doEagerCheck(frame, object, skipKeyTypeChecks, skipValueTypeChecks);
|
||||
}
|
||||
@@ -1704,7 +1721,7 @@ public abstract class TypeNode extends PklNode {
|
||||
var callTarget = member.getCallTarget();
|
||||
memberValue = callTarget.call(object, owner, memberKey);
|
||||
}
|
||||
object.setCachedValue(memberKey, memberValue, member);
|
||||
object.setCachedValue(memberKey, memberValue);
|
||||
}
|
||||
valueTypeNode.executeEagerly(frame, memberValue);
|
||||
}
|
||||
@@ -2391,14 +2408,14 @@ public abstract class TypeNode extends PklNode {
|
||||
public Object execute(VirtualFrame frame, Object value) {
|
||||
var prevOwner = VmUtils.getOwner(frame);
|
||||
var prevReceiver = VmUtils.getReceiver(frame);
|
||||
VmUtils.setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
|
||||
VmUtils.setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
|
||||
setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
|
||||
setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
|
||||
|
||||
try {
|
||||
return aliasedTypeNode.execute(frame, value);
|
||||
} finally {
|
||||
VmUtils.setOwner(frame, prevOwner);
|
||||
VmUtils.setReceiver(frame, prevReceiver);
|
||||
setOwner(frame, prevOwner);
|
||||
setReceiver(frame, prevReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2407,14 +2424,14 @@ public abstract class TypeNode extends PklNode {
|
||||
public Object executeAndSet(VirtualFrame frame, Object value) {
|
||||
var prevOwner = VmUtils.getOwner(frame);
|
||||
var prevReceiver = VmUtils.getReceiver(frame);
|
||||
VmUtils.setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
|
||||
VmUtils.setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
|
||||
setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
|
||||
setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
|
||||
|
||||
try {
|
||||
return aliasedTypeNode.executeAndSet(frame, value);
|
||||
} finally {
|
||||
VmUtils.setOwner(frame, prevOwner);
|
||||
VmUtils.setReceiver(frame, prevReceiver);
|
||||
setOwner(frame, prevOwner);
|
||||
setReceiver(frame, prevReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2423,14 +2440,14 @@ public abstract class TypeNode extends PklNode {
|
||||
public Object executeEagerlyAndSet(VirtualFrame frame, Object value) {
|
||||
var prevOwner = VmUtils.getOwner(frame);
|
||||
var prevReceiver = VmUtils.getReceiver(frame);
|
||||
VmUtils.setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
|
||||
VmUtils.setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
|
||||
setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
|
||||
setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
|
||||
|
||||
try {
|
||||
return aliasedTypeNode.executeEagerlyAndSet(frame, value);
|
||||
} finally {
|
||||
VmUtils.setOwner(frame, prevOwner);
|
||||
VmUtils.setReceiver(frame, prevReceiver);
|
||||
setOwner(frame, prevOwner);
|
||||
setReceiver(frame, prevReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2502,6 +2519,22 @@ public abstract class TypeNode extends PklNode {
|
||||
protected boolean isParametric() {
|
||||
return typeArgumentNodes.length > 0;
|
||||
}
|
||||
|
||||
// Note that mutating a frame's receiver and owner argument is very risky
|
||||
// because any VmObject instantiated within the same root node execution
|
||||
// holds a reference to (not immutable snapshot of) the frame
|
||||
// via VmObjectLike.enclosingFrame.
|
||||
// *Maybe* this works out for TypeAliasTypeNode because an object instantiated
|
||||
// within a type constraint doesn't escape the constraint expression.
|
||||
// If mutating receiver and owner can't be avoided, it would be safer
|
||||
// to have VmObjectLike store them directly instead of storing enclosingFrame.
|
||||
private static void setReceiver(Frame frame, Object receiver) {
|
||||
frame.getArguments()[0] = receiver;
|
||||
}
|
||||
|
||||
private static void setOwner(Frame frame, VmObjectLike owner) {
|
||||
frame.getArguments()[1] = owner;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ConstrainedTypeNode extends TypeNode {
|
||||
|
||||
@@ -197,14 +197,17 @@ public abstract class UnresolvedTypeNode extends PklNode {
|
||||
}
|
||||
|
||||
public static final class Parameterized extends UnresolvedTypeNode {
|
||||
private final VmLanguage language;
|
||||
@Child private ExpressionNode resolveTypeNode;
|
||||
@Children private final UnresolvedTypeNode[] typeArgumentNodes;
|
||||
|
||||
public Parameterized(
|
||||
SourceSection sourceSection,
|
||||
VmLanguage language,
|
||||
ExpressionNode resolveTypeNode,
|
||||
UnresolvedTypeNode[] typeArgumentNodes) {
|
||||
super(sourceSection);
|
||||
this.language = language;
|
||||
this.resolveTypeNode = resolveTypeNode;
|
||||
this.typeArgumentNodes = typeArgumentNodes;
|
||||
}
|
||||
@@ -238,12 +241,13 @@ public abstract class UnresolvedTypeNode extends PklNode {
|
||||
}
|
||||
|
||||
if (clazz.isListingClass()) {
|
||||
return new ListingTypeNode(sourceSection, typeArgumentNodes[0].execute(frame));
|
||||
return new ListingTypeNode(sourceSection, language, typeArgumentNodes[0].execute(frame));
|
||||
}
|
||||
|
||||
if (clazz.isMappingClass()) {
|
||||
return new MappingTypeNode(
|
||||
sourceSection,
|
||||
language,
|
||||
typeArgumentNodes[0].execute(frame),
|
||||
typeArgumentNodes[1].execute(frame));
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ public final class VmFunction extends VmObjectLike {
|
||||
|
||||
@Override
|
||||
@TruffleBoundary
|
||||
public void setCachedValue(Object key, Object value, ObjectMember objectMember) {
|
||||
public void setCachedValue(Object key, Object value) {
|
||||
throw new VmExceptionBuilder()
|
||||
.bug("Class `VmFunction` does not support method `setCachedValue()`.")
|
||||
.build();
|
||||
|
||||
@@ -19,14 +19,13 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import com.oracle.truffle.api.frame.MaterializedFrame;
|
||||
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;
|
||||
|
||||
public final class VmListing extends VmListingOrMapping<VmListing> {
|
||||
public final class VmListing extends VmListingOrMapping {
|
||||
private static final class EmptyHolder {
|
||||
private static final VmListing EMPTY =
|
||||
new VmListing(
|
||||
@@ -47,7 +46,7 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
|
||||
VmObject parent,
|
||||
UnmodifiableEconomicMap<Object, ObjectMember> members,
|
||||
int length) {
|
||||
super(enclosingFrame, Objects.requireNonNull(parent), members, null, null, null);
|
||||
super(enclosingFrame, parent, members);
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
@@ -56,16 +55,10 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
|
||||
VmObject parent,
|
||||
UnmodifiableEconomicMap<Object, ObjectMember> members,
|
||||
int length,
|
||||
@Nullable VmListing delegate,
|
||||
ListingOrMappingTypeCastNode typeCheckNode,
|
||||
MaterializedFrame typeNodeFrame) {
|
||||
super(
|
||||
enclosingFrame,
|
||||
Objects.requireNonNull(parent),
|
||||
members,
|
||||
delegate,
|
||||
typeCheckNode,
|
||||
typeNodeFrame);
|
||||
ListingOrMappingTypeCastNode typeCastNode,
|
||||
Object typeCheckReceiver,
|
||||
VmObjectLike typeCheckOwner) {
|
||||
super(enclosingFrame, parent, members, typeCastNode, typeCheckReceiver, typeCheckOwner);
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
@@ -117,20 +110,6 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
|
||||
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) {
|
||||
@@ -142,10 +121,14 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
|
||||
force(false);
|
||||
other.force(false);
|
||||
|
||||
for (var i = 0L; i < length; i++) {
|
||||
var value = getCachedValue(i);
|
||||
var cursor = cachedValues.getEntries();
|
||||
while (cursor.advance()) {
|
||||
var key = cursor.getKey();
|
||||
if (key instanceof Identifier) continue;
|
||||
|
||||
var value = cursor.getValue();
|
||||
assert value != null;
|
||||
var otherValue = other.getCachedValue(i);
|
||||
var otherValue = other.getCachedValue(key);
|
||||
if (!value.equals(otherValue)) return false;
|
||||
}
|
||||
|
||||
@@ -156,14 +139,16 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
|
||||
@TruffleBoundary
|
||||
public int hashCode() {
|
||||
if (cachedHash != 0) return cachedHash;
|
||||
// It's possible that the delegate has already computed its hash code.
|
||||
// If so, we can go ahead and use it.
|
||||
if (delegate != null && delegate.cachedHash != 0) return delegate.cachedHash;
|
||||
|
||||
force(false);
|
||||
var result = 0;
|
||||
for (var i = 0L; i < length; i++) {
|
||||
var value = getCachedValue(i);
|
||||
var cursor = cachedValues.getEntries();
|
||||
|
||||
while (cursor.advance()) {
|
||||
var key = cursor.getKey();
|
||||
if (key instanceof Identifier) continue;
|
||||
|
||||
var value = cursor.getValue();
|
||||
assert value != null;
|
||||
result = 31 * result + value.hashCode();
|
||||
}
|
||||
|
||||
@@ -16,159 +16,122 @@
|
||||
package org.pkl.core.runtime;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives;
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
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.
|
||||
*/
|
||||
protected final @Nullable SELF delegate;
|
||||
|
||||
public abstract class VmListingOrMapping extends VmObject {
|
||||
// reified type of listing elements and mapping values
|
||||
private final @Nullable ListingOrMappingTypeCastNode typeCastNode;
|
||||
private final MaterializedFrame typeNodeFrame;
|
||||
private final EconomicMap<Object, ObjectMember> cachedMembers = EconomicMaps.create();
|
||||
private final EconomicSet<Object> checkedMembers = EconomicSets.create();
|
||||
private final @Nullable Object typeCheckReceiver;
|
||||
private final @Nullable VmObjectLike typeCheckOwner;
|
||||
|
||||
public VmListingOrMapping(
|
||||
MaterializedFrame enclosingFrame,
|
||||
@Nullable VmObject parent,
|
||||
UnmodifiableEconomicMap<Object, ObjectMember> members) {
|
||||
super(enclosingFrame, parent, members);
|
||||
typeCastNode = null;
|
||||
typeCheckReceiver = null;
|
||||
typeCheckOwner = null;
|
||||
}
|
||||
|
||||
public VmListingOrMapping(
|
||||
MaterializedFrame enclosingFrame,
|
||||
@Nullable VmObject parent,
|
||||
UnmodifiableEconomicMap<Object, ObjectMember> members,
|
||||
@Nullable SELF delegate,
|
||||
@Nullable ListingOrMappingTypeCastNode typeCastNode,
|
||||
@Nullable MaterializedFrame typeNodeFrame) {
|
||||
ListingOrMappingTypeCastNode typeCastNode,
|
||||
Object typeCheckReceiver,
|
||||
VmObjectLike typeCheckOwner) {
|
||||
super(enclosingFrame, parent, members);
|
||||
this.delegate = delegate;
|
||||
this.typeCastNode = typeCastNode;
|
||||
this.typeNodeFrame = typeNodeFrame;
|
||||
this.typeCheckReceiver = typeCheckReceiver;
|
||||
this.typeCheckOwner = typeCheckOwner;
|
||||
}
|
||||
|
||||
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 boolean hasCachedValue(Object key) {
|
||||
return super.hasCachedValue(key) || delegate != null && delegate.hasCachedValue(key);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
// Recursively executes type casts between `owner` and `this` and returns the resulting value.
|
||||
public final Object executeTypeCasts(
|
||||
Object value,
|
||||
VmObjectLike owner,
|
||||
IndirectCallNode callNode,
|
||||
// if non-null, a stack frame for this member is inserted if a type cast fails
|
||||
@Nullable ObjectMember member,
|
||||
// Next type cast to be performed by the caller.
|
||||
// Avoids repeating the same type cast in some cases.
|
||||
@Nullable ListingOrMappingTypeCastNode nextTypeCastNode) {
|
||||
var newNextTypeCastNode = typeCastNode != null ? typeCastNode : nextTypeCastNode;
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
var result =
|
||||
this == owner
|
||||
? value
|
||||
: ((VmListingOrMapping) parent)
|
||||
.executeTypeCasts(value, owner, callNode, member, newNextTypeCastNode);
|
||||
if (typeCastNode == null || typeCastNode == nextTypeCastNode) return result;
|
||||
var callTarget = typeCastNode.getCallTarget();
|
||||
try {
|
||||
return callNode.call(
|
||||
callTarget, VmUtils.getReceiver(typeNodeFrame), VmUtils.getOwner(typeNodeFrame), ret);
|
||||
} catch (VmException vmException) {
|
||||
return callNode.call(callTarget, typeCheckReceiver, typeCheckOwner, result);
|
||||
} catch (VmException e) {
|
||||
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 (member != null) {
|
||||
VmUtils.insertStackFrame(member, callTarget, e);
|
||||
}
|
||||
if (sourceSection.isAvailable()) {
|
||||
vmException
|
||||
.getInsertedStackFrames()
|
||||
.put(callTarget, VmUtils.createStackFrame(sourceSection, member.getQualifiedName()));
|
||||
}
|
||||
throw vmException;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract SELF withCheckedMembers(
|
||||
ListingOrMappingTypeCastNode typeCastNode, MaterializedFrame typeNodeFrame);
|
||||
@Override
|
||||
@TruffleBoundary
|
||||
public final @Nullable Object getCachedValue(Object key) {
|
||||
var result = EconomicMaps.get(cachedValues, key);
|
||||
// if this object has members, `this[key]` may differ from `parent[key]`, so stop the search
|
||||
if (result != null || !members.isEmpty()) return result;
|
||||
|
||||
/** Tells if this mapping/listing runs the same typechecks as {@code typeNode}. */
|
||||
public boolean hasSameChecksAs(TypeNode typeNode) {
|
||||
// Optimization: Recursively steal value from parent cache to avoid computing it multiple times.
|
||||
// The current implementation has the following limitations and drawbacks:
|
||||
// * It only works if a parent has, coincidentally, already cached `key`.
|
||||
// * It turns getCachedValue() into an operation that isn't guaranteed to be fast and fail-safe.
|
||||
// * It requires making VmObject.getCachedValue() non-final,
|
||||
// which is unfavorable for Truffle partial evaluation and JVM inlining.
|
||||
// * It may not be worth its cost for constant members and members that are cheap to compute.
|
||||
|
||||
assert parent != null; // VmListingOrMapping always has a parent
|
||||
result = parent.getCachedValue(key);
|
||||
if (result == null) return null;
|
||||
|
||||
if (typeCastNode != null && !(key instanceof Identifier)) {
|
||||
var callNode = IndirectCallNode.getUncached();
|
||||
var callTarget = typeCastNode.getCallTarget();
|
||||
try {
|
||||
result = callNode.call(callTarget, typeCheckReceiver, typeCheckOwner, result);
|
||||
} catch (VmException e) {
|
||||
var member = VmUtils.findMember(parent, key);
|
||||
assert member != null; // already found the member's cached value
|
||||
VmUtils.insertStackFrame(member, callTarget, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
setCachedValue(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the value type of this listing/mapping is known to be a subtype of {@code
|
||||
* typeNode}. If {@code true}, type checks of individual values can be elided because
|
||||
* listings/mappings are covariant in their value type.
|
||||
*/
|
||||
public final boolean isValueTypeKnownSubtypeOf(TypeNode typeNode) {
|
||||
if (typeNode.isNoopTypeCheck()) {
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
return typeCastNode.getTypeNode().isEquivalentTo(typeNode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.pkl.core.runtime;
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import com.oracle.truffle.api.frame.MaterializedFrame;
|
||||
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;
|
||||
@@ -27,7 +26,7 @@ import org.pkl.core.util.CollectionUtils;
|
||||
import org.pkl.core.util.EconomicMaps;
|
||||
import org.pkl.core.util.LateInit;
|
||||
|
||||
public final class VmMapping extends VmListingOrMapping<VmMapping> {
|
||||
public final class VmMapping extends VmListingOrMapping {
|
||||
|
||||
private int cachedEntryCount = -1;
|
||||
|
||||
@@ -50,24 +49,17 @@ public final class VmMapping extends VmListingOrMapping<VmMapping> {
|
||||
MaterializedFrame enclosingFrame,
|
||||
VmObject parent,
|
||||
UnmodifiableEconomicMap<Object, ObjectMember> members) {
|
||||
|
||||
super(enclosingFrame, Objects.requireNonNull(parent), members, null, null, null);
|
||||
super(enclosingFrame, parent, members);
|
||||
}
|
||||
|
||||
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);
|
||||
ListingOrMappingTypeCastNode typeCastNode,
|
||||
Object typeCheckReceiver,
|
||||
VmObjectLike typeCheckOwner) {
|
||||
super(enclosingFrame, parent, members, typeCastNode, typeCheckReceiver, typeCheckOwner);
|
||||
}
|
||||
|
||||
public static boolean isDefaultProperty(Object propertyKey) {
|
||||
@@ -81,16 +73,12 @@ public final class VmMapping extends VmListingOrMapping<VmMapping> {
|
||||
|
||||
@TruffleBoundary
|
||||
public VmSet getAllKeys() {
|
||||
if (delegate != null) {
|
||||
return delegate.getAllKeys();
|
||||
}
|
||||
synchronized (this) {
|
||||
if (__allKeys == null) {
|
||||
// building upon parent's `getAllKeys()` should improve at least worst case efficiency
|
||||
var parentKeys =
|
||||
getParent() instanceof VmMapping mapping ? mapping.getAllKeys() : VmSet.EMPTY;
|
||||
var parentKeys = parent instanceof VmMapping mapping ? mapping.getAllKeys() : VmSet.EMPTY;
|
||||
var builder = VmSet.builder(parentKeys);
|
||||
for (var cursor = getMembers().getEntries(); cursor.advance(); ) {
|
||||
for (var cursor = members.getEntries(); cursor.advance(); ) {
|
||||
var member = cursor.getValue();
|
||||
if (!member.isEntry()) continue;
|
||||
builder.add(cursor.getKey());
|
||||
@@ -133,12 +121,17 @@ public final class VmMapping extends VmListingOrMapping<VmMapping> {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof VmMapping other)) return false;
|
||||
|
||||
if (getEntryCount() != other.getEntryCount()) return false;
|
||||
// could use shallow force, but deep force is cached
|
||||
force(false);
|
||||
other.force(false);
|
||||
for (var key : getAllKeys()) {
|
||||
var value = getCachedValue(key);
|
||||
if (getEntryCount() != other.getEntryCount()) return false;
|
||||
|
||||
var cursor = cachedValues.getEntries();
|
||||
while (cursor.advance()) {
|
||||
Object key = cursor.getKey();
|
||||
if (key instanceof Identifier) continue;
|
||||
|
||||
var value = cursor.getValue();
|
||||
assert value != null;
|
||||
var otherValue = other.getCachedValue(key);
|
||||
if (!value.equals(otherValue)) return false;
|
||||
@@ -151,38 +144,34 @@ public final class VmMapping extends VmListingOrMapping<VmMapping> {
|
||||
@TruffleBoundary
|
||||
public int hashCode() {
|
||||
if (cachedHash != 0) return cachedHash;
|
||||
// It's possible that the delegate has already computed its hash code.
|
||||
// If so, we can go ahead and use it.
|
||||
if (delegate != null && delegate.cachedHash != 0) return delegate.cachedHash;
|
||||
|
||||
force(false);
|
||||
var result = 0;
|
||||
for (var key : getAllKeys()) {
|
||||
var cursor = cachedValues.getEntries();
|
||||
|
||||
while (cursor.advance()) {
|
||||
var key = cursor.getKey();
|
||||
if (key instanceof Identifier) continue;
|
||||
var value = getCachedValue(key);
|
||||
|
||||
var value = cursor.getValue();
|
||||
assert value != null;
|
||||
result += key.hashCode() ^ value.hashCode();
|
||||
}
|
||||
|
||||
cachedHash = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
// assumes mapping has been forced
|
||||
public int getEntryCount() {
|
||||
if (cachedEntryCount != -1) return cachedEntryCount;
|
||||
cachedEntryCount = getAllKeys().getLength();
|
||||
return cachedEntryCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
@TruffleBoundary
|
||||
public VmMapping withCheckedMembers(
|
||||
ListingOrMappingTypeCastNode typeCheckNode, MaterializedFrame typeNodeFrame) {
|
||||
return new VmMapping(
|
||||
getEnclosingFrame(),
|
||||
Objects.requireNonNull(getParent()),
|
||||
getMembers(),
|
||||
this,
|
||||
typeCheckNode,
|
||||
typeNodeFrame);
|
||||
var result = 0;
|
||||
for (var key : cachedValues.getKeys()) {
|
||||
if (key instanceof Identifier) continue;
|
||||
result += 1;
|
||||
}
|
||||
cachedEntryCount = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,12 +87,12 @@ public abstract class VmObject extends VmObjectLike {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCachedValue(Object key, Object value, ObjectMember objectMember) {
|
||||
public final void setCachedValue(Object key, Object value) {
|
||||
EconomicMaps.put(cachedValues, key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasCachedValue(Object key) {
|
||||
public final boolean hasCachedValue(Object key) {
|
||||
return EconomicMaps.containsKey(cachedValues, key);
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ public abstract class VmObjectLike extends VmValue {
|
||||
* receiver.
|
||||
*/
|
||||
@TruffleBoundary
|
||||
public abstract void setCachedValue(Object key, Object value, ObjectMember objectMember);
|
||||
public abstract void setCachedValue(Object key, Object value);
|
||||
|
||||
/**
|
||||
* Prefer this method over {@link #getCachedValue} if the value is not required. (There is no
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.pkl.core.runtime;
|
||||
|
||||
import com.oracle.truffle.api.CallTarget;
|
||||
import com.oracle.truffle.api.CompilerDirectives;
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import com.oracle.truffle.api.Truffle;
|
||||
@@ -134,10 +135,6 @@ public final class VmUtils {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void setReceiver(Frame frame, Object receiver) {
|
||||
frame.getArguments()[0] = receiver;
|
||||
}
|
||||
|
||||
public static VmObjectLike getObjectReceiver(Frame frame) {
|
||||
return (VmObjectLike) getReceiver(frame);
|
||||
}
|
||||
@@ -158,10 +155,6 @@ public final class VmUtils {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void setOwner(Frame frame, VmObjectLike owner) {
|
||||
frame.getArguments()[1] = owner;
|
||||
}
|
||||
|
||||
/** Returns a `ObjectMember`'s key while executing the corresponding `MemberNode`. */
|
||||
public static Object getMemberKey(Frame frame) {
|
||||
return frame.getArguments()[2];
|
||||
@@ -261,17 +254,17 @@ public final class VmUtils {
|
||||
|
||||
final var constantValue = member.getConstantValue();
|
||||
if (constantValue != null) {
|
||||
var ret = constantValue;
|
||||
// for a property, do a type check
|
||||
var result = constantValue;
|
||||
// for a property, Listing element, or Mapping value, do a type check
|
||||
if (member.isProp()) {
|
||||
var property = receiver.getVmClass().getProperty(member.getName());
|
||||
if (property != null && property.getTypeNode() != null) {
|
||||
var callTarget = property.getTypeNode().getCallTarget();
|
||||
try {
|
||||
if (checkType) {
|
||||
ret = callNode.call(callTarget, receiver, property.getOwner(), constantValue);
|
||||
result = callNode.call(callTarget, receiver, property.getOwner(), constantValue);
|
||||
} else {
|
||||
ret =
|
||||
result =
|
||||
callNode.call(
|
||||
callTarget,
|
||||
receiver,
|
||||
@@ -281,44 +274,52 @@ public final class VmUtils {
|
||||
}
|
||||
} catch (VmException e) {
|
||||
CompilerDirectives.transferToInterpreter();
|
||||
// A failed property type check looks as follows in the stack trace:
|
||||
// x: Int(isPositive)
|
||||
// at ...
|
||||
// x = -10
|
||||
// at ...
|
||||
// However, if the value of `x` is a parse-time constant (as in `x = -10`),
|
||||
// there's no root node for it and hence no stack trace element.
|
||||
// In this case, insert a stack trace element to make the stack trace look the same
|
||||
// as in the non-constant case. (Parse-time constants are an internal optimization.)
|
||||
e.getInsertedStackFrames()
|
||||
.put(
|
||||
callTarget,
|
||||
createStackFrame(member.getBodySection(), member.getQualifiedName()));
|
||||
insertStackFrame(member, callTarget, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} else if (receiver instanceof VmListingOrMapping<?> vmListingOrMapping) {
|
||||
if (owner != receiver && owner instanceof VmListingOrMapping<?> vmListingOrMappingOwner) {
|
||||
ret = vmListingOrMappingOwner.typecastObjectMember(member, ret, callNode);
|
||||
}
|
||||
ret = vmListingOrMapping.typecastObjectMember(member, ret, callNode);
|
||||
} else if (receiver instanceof VmListingOrMapping listingOrMapping
|
||||
&& owner instanceof VmListingOrMapping) {
|
||||
// `owner instanceof VmListingOrMapping` guards against
|
||||
// PropertiesRenderer amending VmDynamic with VmListing (hack?)
|
||||
result = listingOrMapping.executeTypeCasts(constantValue, owner, callNode, member, null);
|
||||
}
|
||||
receiver.setCachedValue(memberKey, ret, member);
|
||||
return ret;
|
||||
|
||||
receiver.setCachedValue(memberKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
var callTarget = member.getCallTarget();
|
||||
Object ret;
|
||||
Object result;
|
||||
if (checkType) {
|
||||
ret = callNode.call(callTarget, receiver, owner, memberKey);
|
||||
result = callNode.call(callTarget, receiver, owner, memberKey);
|
||||
} else {
|
||||
ret = callNode.call(callTarget, receiver, owner, memberKey, VmUtils.SKIP_TYPECHECK_MARKER);
|
||||
result = callNode.call(callTarget, receiver, owner, memberKey, VmUtils.SKIP_TYPECHECK_MARKER);
|
||||
}
|
||||
if (receiver instanceof VmListingOrMapping<?> vmListingOrMapping) {
|
||||
ret = vmListingOrMapping.typecastObjectMember(member, ret, callNode);
|
||||
receiver.setCachedValue(memberKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// A failed property type check looks as follows in the stack trace:
|
||||
// x: Int(isPositive)
|
||||
// at ...
|
||||
// x = -10
|
||||
// at ...
|
||||
// However, if the value of `x` is a parse-time constant (as in `x = -10`),
|
||||
// there's no root node for it and hence no stack trace element.
|
||||
// In this case, insert a stack trace element to make the stack trace look the same
|
||||
// as in the non-constant case. (Parse-time constants are an internal optimization.)
|
||||
public static void insertStackFrame(
|
||||
ObjectMember member, CallTarget location, VmException exception) {
|
||||
var sourceSection = member.getBodySection();
|
||||
if (!sourceSection.isAvailable()) {
|
||||
sourceSection = member.getSourceSection();
|
||||
}
|
||||
if (sourceSection.isAvailable()) {
|
||||
exception
|
||||
.getInsertedStackFrames()
|
||||
.put(location, createStackFrame(sourceSection, member.getQualifiedName()));
|
||||
}
|
||||
receiver.setCachedValue(memberKey, ret, member);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static @Nullable ObjectMember findMember(VmObjectLike receiver, Object memberKey) {
|
||||
|
||||
@@ -50,6 +50,7 @@ import org.pkl.core.runtime.VmDuration;
|
||||
import org.pkl.core.runtime.VmDynamic;
|
||||
import org.pkl.core.runtime.VmExceptionBuilder;
|
||||
import org.pkl.core.runtime.VmIntSeq;
|
||||
import org.pkl.core.runtime.VmLanguage;
|
||||
import org.pkl.core.runtime.VmList;
|
||||
import org.pkl.core.runtime.VmListing;
|
||||
import org.pkl.core.runtime.VmMap;
|
||||
@@ -573,7 +574,8 @@ public final class RendererNodes {
|
||||
type =
|
||||
requiresWrapper()
|
||||
? null
|
||||
: new ListingTypeNode(VmUtils.unavailableSourceSection(), valueType);
|
||||
: new ListingTypeNode(
|
||||
VmUtils.unavailableSourceSection(), VmLanguage.get(null), valueType);
|
||||
return type;
|
||||
} else if (type instanceof MappingTypeNode mappingType) {
|
||||
var keyType = resolveType(mappingType.getKeyTypeNode());
|
||||
@@ -587,7 +589,9 @@ public final class RendererNodes {
|
||||
}
|
||||
var valueType = resolveType(mappingType.getValueTypeNode());
|
||||
assert valueType != null : "Incomplete or malformed Mapping type";
|
||||
mappingType = new MappingTypeNode(VmUtils.unavailableSourceSection(), keyType, valueType);
|
||||
mappingType =
|
||||
new MappingTypeNode(
|
||||
VmUtils.unavailableSourceSection(), VmLanguage.get(null), keyType, valueType);
|
||||
|
||||
type = requiresWrapper() ? null : mappingType;
|
||||
return type;
|
||||
|
||||
@@ -30,6 +30,10 @@ import org.graalvm.collections.UnmodifiableMapCursor;
|
||||
public final class EconomicMaps {
|
||||
private EconomicMaps() {}
|
||||
|
||||
public static <K, V> UnmodifiableEconomicMap<K, V> emptyMap() {
|
||||
return EconomicMap.emptyMap();
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
public static <K, V> EconomicMap<K, V> create() {
|
||||
return EconomicMap.create();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
function isValid(str): Boolean = str.startsWith("a")
|
||||
|
||||
foo: Listing<String(isValid(this))>(isDistinct)
|
||||
6
pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError8.pkl
vendored
Normal file
6
pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError8.pkl
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
local a = new Listing { new Listing { 0 } }
|
||||
local b = a as Listing<Listing<String>>
|
||||
local c = (b) { new Listing { 1 } }
|
||||
local d = c as Listing<Listing<Int>>
|
||||
|
||||
result = d
|
||||
10
pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError9.pkl
vendored
Normal file
10
pkl-core/src/test/files/LanguageSnippetTests/input/errors/listingTypeCheckError9.pkl
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
local a = new Listing { new Foo {} }
|
||||
local b = (a) { new Bar {} }
|
||||
local c = b as Listing<Bar>
|
||||
local d = (c) { new Foo {} }
|
||||
local e = d as Listing<Foo>
|
||||
|
||||
result = e
|
||||
|
||||
open class Foo
|
||||
class Bar extends Foo
|
||||
10
pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError11.pkl
vendored
Normal file
10
pkl-core/src/test/files/LanguageSnippetTests/input/errors/mappingTypeCheckError11.pkl
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
local a = new Mapping { [0] = new Foo {} }
|
||||
local b = (a) { [1] = new Bar {} }
|
||||
local c = b as Mapping<Int, Bar>
|
||||
local d = (c) { [2] = new Foo {} }
|
||||
local e = d as Mapping<Int, Foo>
|
||||
|
||||
result = e
|
||||
|
||||
open class Foo
|
||||
class Bar extends Foo
|
||||
7
pkl-core/src/test/files/LanguageSnippetTests/input/listings/cacheStealing.pkl
vendored
Normal file
7
pkl-core/src/test/files/LanguageSnippetTests/input/listings/cacheStealing.pkl
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
foo1: Listing<String> = new { "hello" }
|
||||
foo2: Listing<String|Int> = foo1
|
||||
|
||||
res1 = foo1.isDistinct
|
||||
// steals isDistinct from foo1's VmListing.cachedValues but must not
|
||||
// perform a String|Int type check because isDistinct is not an element
|
||||
res2 = foo2.isDistinct
|
||||
8
pkl-core/src/test/files/LanguageSnippetTests/input/listings/cacheStealingTypeCheck.pkl
vendored
Normal file
8
pkl-core/src/test/files/LanguageSnippetTests/input/listings/cacheStealingTypeCheck.pkl
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
amends "../../input-helper/listings/cacheStealingTypeCheck.pkl"
|
||||
|
||||
// this test covers a regression where the wrong receiver
|
||||
// and owner was used to typecheck a stolen value
|
||||
foo {
|
||||
"abcdx"
|
||||
"ax"
|
||||
}
|
||||
5
pkl-core/src/test/files/LanguageSnippetTests/input/types/helpers/originalTypealias.pkl
vendored
Normal file
5
pkl-core/src/test/files/LanguageSnippetTests/input/types/helpers/originalTypealias.pkl
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
const local lastName = "Birdo"
|
||||
|
||||
typealias Birds = Listing<String(endsWith(lastName))>
|
||||
|
||||
typealias Birds2 = Pair<Listing<String(endsWith(lastName))>, Int>
|
||||
@@ -1,5 +1,7 @@
|
||||
import "pkl:test"
|
||||
|
||||
import "helpers/originalTypealias.pkl"
|
||||
|
||||
typealias Simple = String
|
||||
const function simple(arg: Simple): Simple = arg
|
||||
|
||||
@@ -105,3 +107,8 @@ res19: LocalTypeAlias = "abc"
|
||||
typealias VeryComposite = Pair<Composite, Composite>
|
||||
|
||||
res20: VeryComposite = Pair(Map("abc", List("def")), Map("abc", List("def")))
|
||||
|
||||
// typealiases should be executed in their original context
|
||||
res21: originalTypealias.Birds = new { "John Birdo" }
|
||||
|
||||
res22: originalTypealias.Birds2 = Pair(new Listing { "John Birdo" }, 0)
|
||||
|
||||
3
pkl-core/src/test/files/LanguageSnippetTests/input/types/typeAliasContext.pkl
vendored
Normal file
3
pkl-core/src/test/files/LanguageSnippetTests/input/types/typeAliasContext.pkl
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import "helpers/originalTypealias.pkl"
|
||||
|
||||
res: originalTypealias.Birds = new { "Jimmy Bird" }
|
||||
15
pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError8.err
vendored
Normal file
15
pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError8.err
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
–– Pkl Error ––
|
||||
Expected value of type `String`, but got type `Int`.
|
||||
Value: 0
|
||||
|
||||
x | local b = a as Listing<Listing<String>>
|
||||
^^^^^^
|
||||
at listingTypeCheckError8#b (file:///$snippetsDir/input/errors/listingTypeCheckError8.pkl)
|
||||
|
||||
x | local a = new Listing { new Listing { 0 } }
|
||||
^
|
||||
at listingTypeCheckError8#a[#1][#1] (file:///$snippetsDir/input/errors/listingTypeCheckError8.pkl)
|
||||
|
||||
xxx | text = renderer.renderDocument(value)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
at pkl.base#Module.output.text (pkl:base)
|
||||
15
pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError9.err
vendored
Normal file
15
pkl-core/src/test/files/LanguageSnippetTests/output/errors/listingTypeCheckError9.err
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
–– Pkl Error ––
|
||||
Expected value of type `listingTypeCheckError9#Bar`, but got type `listingTypeCheckError9#Foo`.
|
||||
Value: new Foo {}
|
||||
|
||||
x | local c = b as Listing<Bar>
|
||||
^^^
|
||||
at listingTypeCheckError9#c (file:///$snippetsDir/input/errors/listingTypeCheckError9.pkl)
|
||||
|
||||
x | local a = new Listing { new Foo {} }
|
||||
^^^^^^^^^^
|
||||
at listingTypeCheckError9#a[#1] (file:///$snippetsDir/input/errors/listingTypeCheckError9.pkl)
|
||||
|
||||
xxx | text = renderer.renderDocument(value)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
at pkl.base#Module.output.text (pkl:base)
|
||||
15
pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError11.err
vendored
Normal file
15
pkl-core/src/test/files/LanguageSnippetTests/output/errors/mappingTypeCheckError11.err
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
–– Pkl Error ––
|
||||
Expected value of type `mappingTypeCheckError11#Bar`, but got type `mappingTypeCheckError11#Foo`.
|
||||
Value: new Foo {}
|
||||
|
||||
x | local c = b as Mapping<Int, Bar>
|
||||
^^^
|
||||
at mappingTypeCheckError11#c (file:///$snippetsDir/input/errors/mappingTypeCheckError11.pkl)
|
||||
|
||||
x | local a = new Mapping { [0] = new Foo {} }
|
||||
^^^^^^^^^^
|
||||
at mappingTypeCheckError11#a[0] (file:///$snippetsDir/input/errors/mappingTypeCheckError11.pkl)
|
||||
|
||||
xxx | text = renderer.renderDocument(value)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
at pkl.base#Module.output.text (pkl:base)
|
||||
8
pkl-core/src/test/files/LanguageSnippetTests/output/listings/cacheStealing.pcf
vendored
Normal file
8
pkl-core/src/test/files/LanguageSnippetTests/output/listings/cacheStealing.pcf
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
foo1 {
|
||||
"hello"
|
||||
}
|
||||
foo2 {
|
||||
"hello"
|
||||
}
|
||||
res1 = true
|
||||
res2 = true
|
||||
4
pkl-core/src/test/files/LanguageSnippetTests/output/listings/cacheStealingTypeCheck.pcf
vendored
Normal file
4
pkl-core/src/test/files/LanguageSnippetTests/output/listings/cacheStealingTypeCheck.pcf
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
foo {
|
||||
"abcdx"
|
||||
"ax"
|
||||
}
|
||||
@@ -38,3 +38,9 @@ res18 = "Expected value of type `Duration`, but got type `DataSize`. Value: 5.mb
|
||||
res18b = "Expected value of type `Duration`, but got type `DataSize`. Value: 5.mb"
|
||||
res19 = "abc"
|
||||
res20 = Pair(Map("abc", List("def")), Map("abc", List("def")))
|
||||
res21 {
|
||||
"John Birdo"
|
||||
}
|
||||
res22 = Pair(new {
|
||||
"John Birdo"
|
||||
}, 0)
|
||||
|
||||
15
pkl-core/src/test/files/LanguageSnippetTests/output/types/typeAliasContext.err
vendored
Normal file
15
pkl-core/src/test/files/LanguageSnippetTests/output/types/typeAliasContext.err
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
–– Pkl Error ––
|
||||
Type constraint `endsWith(lastName)` violated.
|
||||
Value: "Jimmy Bird"
|
||||
|
||||
x | typealias Birds = Listing<String(endsWith(lastName))>
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
at typeAliasContext#res (file:///$snippetsDir/input/types/helpers/originalTypealias.pkl)
|
||||
|
||||
x | res: originalTypealias.Birds = new { "Jimmy Bird" }
|
||||
^^^^^^^^^^^^
|
||||
at typeAliasContext#res[#1] (file:///$snippetsDir/input/types/typeAliasContext.pkl)
|
||||
|
||||
xxx | text = renderer.renderDocument(value)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
at pkl.base#Module.output.text (pkl:base)
|
||||
Reference in New Issue
Block a user