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:
odenix
2024-12-06 04:41:33 -08:00
committed by Islon Scherer
parent 7b850dd6d9
commit aeace8bb3c
34 changed files with 489 additions and 295 deletions

View File

@@ -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));
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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));
}

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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();

View File

@@ -0,0 +1,3 @@
function isValid(str): Boolean = str.startsWith("a")
foo: Listing<String(isValid(this))>(isDistinct)

View 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

View 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

View 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

View 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

View 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"
}

View File

@@ -0,0 +1,5 @@
const local lastName = "Birdo"
typealias Birds = Listing<String(endsWith(lastName))>
typealias Birds2 = Pair<Listing<String(endsWith(lastName))>, Int>

View File

@@ -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)

View File

@@ -0,0 +1,3 @@
import "helpers/originalTypealias.pkl"
res: originalTypealias.Birds = new { "Jimmy Bird" }

View 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)

View 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)

View 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)

View File

@@ -0,0 +1,8 @@
foo1 {
"hello"
}
foo2 {
"hello"
}
res1 = true
res2 = true

View File

@@ -0,0 +1,4 @@
foo {
"abcdx"
"ax"
}

View File

@@ -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)

View 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)