Make typechecking more PE friendly (#1871)
This makes various improvements to help truffle do partial evaluation of code related to typechecking. * Split `UnionTypeNode` into `UnionTypeNodeExploded` and `UnionTypeNodeLooped` to avoid gigantic graphs. This especially helps in the case of typealiases, because they are inlined into their usage sites. * Move constraint-failure exception construction in `TypeConstraintNode` behind `@TruffleBoundary` helper methods instead of `transferToInterpreterAndInvalidate`, since building the exception is a rare, off-hot-path operation. * Move `ClassNode`'s one-time class-initialization logic behind a `@TruffleBoundary` method (`initClass`), store `VmLanguage` as a field instead of recomputing it via `VmLanguage.get(this)`, and use `transferToInterpreterAndInvalidate` since class init never needs to re-run on the compiled path. * Pass `VmLanguage` explicitly into `TypeTestNode` and `ClassNode` (via `AstBuilder`) rather than resolving it with `VmLanguage.get(this)` on a hot path. * Fix various issues where the partial evaluator determines that a frame should have been materialized but was not. * Pre-initialize call targets in `ObjectMember.setMemberNode()` and `FunctionLiteralNode` so the first invocation doesn't trigger a compilation invalidation. * Fix truffle node tree issues (don't mark `@RootNode` as child, deepCopy instead of passing an already adopted node). * Remove unnecessary interpreter transfers when throwing type mismatch exceptions
This commit is contained in:
1 parent
8331570aaf
commit
b8c7a42318
16 files changed
+345
-151
No files matched your search
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.pkl.core;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
@@ -115,14 +116,17 @@ public abstract class PType implements Serializable {
|
||||
this.typeArguments = typeArguments;
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
public Class(PClass pClass) {
|
||||
this(pClass, List.of());
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
public Class(PClass pClass, PType typeArgument1) {
|
||||
this(pClass, List.of(typeArgument1));
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
public Class(PClass pClass, PType typeArgument1, PType typeArgument2) {
|
||||
this(pClass, List.of(typeArgument1, typeArgument2));
|
||||
}
|
||||
|
||||
@@ -1268,7 +1268,7 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
@Override
|
||||
public ExpressionNode visitTypeCheckExpr(TypeCheckExpr expr) {
|
||||
return new TypeTestNode(
|
||||
createSourceSection(expr), visitExpr(expr.getExpr()), visitType(expr.getType()));
|
||||
createSourceSection(expr), language, visitExpr(expr.getExpr()), visitType(expr.getType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1697,7 +1697,8 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
supertypeNode,
|
||||
moduleProperties,
|
||||
unresolvedPropertyNodes,
|
||||
doVisitMethodDefs(mod.getMethods()));
|
||||
doVisitMethodDefs(mod.getMethods()),
|
||||
language);
|
||||
|
||||
return new ModuleNode(
|
||||
language, moduleInfo.getSourceSection(), moduleInfo.getModuleName(), classNode);
|
||||
@@ -1930,7 +1931,8 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
supertypeNode,
|
||||
EconomicMaps.create(),
|
||||
doVisitClassProperties(properties, propertyNames),
|
||||
doVisitMethodDefs(methods));
|
||||
doVisitMethodDefs(methods),
|
||||
language);
|
||||
|
||||
var result =
|
||||
new ObjectMember(
|
||||
|
||||
@@ -46,6 +46,9 @@ public final class FunctionLiteralNode extends ExpressionNode {
|
||||
if (functionNode == null) {
|
||||
CompilerDirectives.transferToInterpreterAndInvalidate();
|
||||
functionNode = unresolvedFunctionNode.execute(frame);
|
||||
// pre-initialize the function node's call target so that the first call to this function
|
||||
// won't invalidate again.
|
||||
functionNode.getCallTarget();
|
||||
if (isCustomThisScope) {
|
||||
customThisSlot = VmUtils.findCustomThisSlot(frame);
|
||||
}
|
||||
|
||||
+4
-1
@@ -71,6 +71,9 @@ public final class ExecuteCustomThisWithRootNode extends ExpressionNode {
|
||||
@Override
|
||||
public Object executeGeneric(VirtualFrame frame) {
|
||||
var customThis = customThisNode.executeGeneric(frame);
|
||||
return callNode.call(VmUtils.getReceiver(frame), VmUtils.getOwner(frame), customThis, frame);
|
||||
// can't pass a non-materialized frame as an argument into a call node (truffle compiler will
|
||||
// error)
|
||||
return callNode.call(
|
||||
VmUtils.getReceiver(frame), VmUtils.getOwner(frame), customThis, frame.materialize());
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,8 @@ import org.pkl.core.util.GlobResolver.InvalidGlobPatternException;
|
||||
@NodeInfo(shortName = "import*")
|
||||
public class ImportGlobNode extends AbstractImportNode {
|
||||
private final String globPattern;
|
||||
@Child private @Nullable SharedMemberNode memberNode;
|
||||
// Not a @Child: a RootNode must never be adopted as a child (it always has a null parent).
|
||||
private @Nullable SharedMemberNode memberNode;
|
||||
@CompilationFinal private @Nullable VmMapping cachedResult;
|
||||
|
||||
public ImportGlobNode(
|
||||
|
||||
@@ -41,7 +41,7 @@ import org.pkl.core.util.IoUtils;
|
||||
@NodeInfo(shortName = "read*")
|
||||
public abstract class ReadGlobNode extends AbstractReadNode {
|
||||
private final EconomicMap<String, VmMapping> cachedResults = EconomicMap.create();
|
||||
@Child private @Nullable SharedMemberNode memberNode;
|
||||
private @Nullable SharedMemberNode memberNode;
|
||||
|
||||
protected ReadGlobNode(SourceSection sourceSection, ModuleKey currentModule) {
|
||||
super(sourceSection, currentModule);
|
||||
|
||||
@@ -48,6 +48,7 @@ public final class ClassNode extends ExpressionNode {
|
||||
@Children private final UnresolvedMethodNode[] unresolvedMethodNodes;
|
||||
|
||||
@CompilationFinal @LateInit private VmClass cachedClass;
|
||||
private final VmLanguage language;
|
||||
|
||||
public ClassNode(
|
||||
SourceSection section,
|
||||
@@ -61,7 +62,8 @@ public final class ClassNode extends ExpressionNode {
|
||||
@Nullable UnresolvedTypeNode unresolvedSupertypeNode,
|
||||
EconomicMap<Object, ObjectMember> prototypeMembers,
|
||||
UnresolvedPropertyNode[] unresolvedPropertyNodes,
|
||||
UnresolvedMethodNode[] unresolvedMethodNodes) {
|
||||
UnresolvedMethodNode[] unresolvedMethodNodes,
|
||||
VmLanguage language) {
|
||||
|
||||
super(section);
|
||||
this.headerSection = headerSection;
|
||||
@@ -75,6 +77,7 @@ public final class ClassNode extends ExpressionNode {
|
||||
this.prototypeMembers = prototypeMembers;
|
||||
this.unresolvedPropertyNodes = unresolvedPropertyNodes;
|
||||
this.unresolvedMethodNodes = unresolvedMethodNodes;
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,7 +91,6 @@ public final class ClassNode extends ExpressionNode {
|
||||
if (cachedClass != null) return cachedClass;
|
||||
|
||||
CompilerDirectives.transferToInterpreter();
|
||||
|
||||
var module = VmUtils.getTypedObjectReceiver(frame);
|
||||
|
||||
VmTyped prototype;
|
||||
@@ -123,12 +125,12 @@ public final class ClassNode extends ExpressionNode {
|
||||
typeParameters,
|
||||
prototype);
|
||||
|
||||
var localContext = VmLanguage.get(this).localContext.get();
|
||||
var localContext = language.localContext.get();
|
||||
localContext.beginClassInit(cachedClass);
|
||||
|
||||
try {
|
||||
if (unresolvedSupertypeNode != null) {
|
||||
var supertypeNode = unresolvedSupertypeNode.execute(frame);
|
||||
var supertypeNode = insert(unresolvedSupertypeNode.execute(frame));
|
||||
var superclass = supertypeNode.getType().getVmClass();
|
||||
|
||||
checkSupertype(supertypeNode, superclass);
|
||||
|
||||
@@ -47,9 +47,12 @@ public final class LocalTypedPropertyNode extends RegularMemberNode {
|
||||
public @Nullable Object getDefaultValue(VirtualFrame frame) {
|
||||
if (!defaultValueInitialized) {
|
||||
assert typeNode != null;
|
||||
// typeNode.createDefaultValue() is a regular polymorphic call rather than a child node.
|
||||
// passing a non-materialized frame will cause GraalVM to emit diagnostics that it failed
|
||||
// to compile this call.
|
||||
defaultValue =
|
||||
typeNode.createDefaultValue(
|
||||
frame, language, member.getHeaderSection(), member.getQualifiedName());
|
||||
frame.materialize(), language, member.getHeaderSection(), member.getQualifiedName());
|
||||
defaultValueInitialized = true;
|
||||
}
|
||||
return defaultValue;
|
||||
|
||||
@@ -58,6 +58,9 @@ public final class ObjectMember extends Member {
|
||||
assert memberNode == null;
|
||||
|
||||
memberNode = node;
|
||||
// pre-initialize the call target so that later calls to `getCallTarget()` does not invalidate
|
||||
// the caller's compiled code
|
||||
node.getCallTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.pkl.core.runtime.*;
|
||||
|
||||
public final class PropertyTypeNode extends PklRootNode {
|
||||
private final String qualifiedPropertyName;
|
||||
private final VmLanguage language;
|
||||
@Child private TypeNode typeNode;
|
||||
|
||||
private @Nullable Object defaultValue;
|
||||
@@ -43,6 +44,7 @@ public final class PropertyTypeNode extends PklRootNode {
|
||||
super(language, descriptor);
|
||||
this.qualifiedPropertyName = qualifiedPropertyName;
|
||||
this.typeNode = childNode;
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
public TypeNode getTypeNode() {
|
||||
@@ -65,15 +67,18 @@ public final class PropertyTypeNode extends PklRootNode {
|
||||
}
|
||||
|
||||
public @Nullable Object getDefaultValue(VirtualFrame frame) {
|
||||
if (defaultValueInitialized) return defaultValue;
|
||||
|
||||
defaultValue =
|
||||
typeNode.createDefaultValue(
|
||||
frame, VmLanguage.get(this), getSourceSection(), qualifiedPropertyName);
|
||||
// can't cache default value for `module` type in a non-final module because it's a self-type
|
||||
// (the default value changes when inherited).
|
||||
if (typeNode.isFinalType()) {
|
||||
defaultValueInitialized = true;
|
||||
if (!defaultValueInitialized) {
|
||||
// typeNode.createDefaultValue() is a regular polymorphic call rather than a child node.
|
||||
// passing a non-materialized frame will cause GraalVM to emit diagnostics that it failed
|
||||
// to compile this call.
|
||||
defaultValue =
|
||||
typeNode.createDefaultValue(
|
||||
frame.materialize(), language, getSourceSection(), qualifiedPropertyName);
|
||||
// can't cache default value for `module` type in a non-final module because it's a self-type
|
||||
// (the default value changes when inherited).
|
||||
if (typeNode.isFinalType()) {
|
||||
defaultValueInitialized = true;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@@ -17,10 +17,12 @@ package org.pkl.core.ast.type;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives;
|
||||
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import com.oracle.truffle.api.dsl.Cached;
|
||||
import com.oracle.truffle.api.dsl.Fallback;
|
||||
import com.oracle.truffle.api.dsl.NodeChild;
|
||||
import com.oracle.truffle.api.dsl.Specialization;
|
||||
import com.oracle.truffle.api.frame.MaterializedFrame;
|
||||
import com.oracle.truffle.api.frame.VirtualFrame;
|
||||
import com.oracle.truffle.api.source.SourceSection;
|
||||
import org.pkl.core.ast.ExpressionNode;
|
||||
@@ -54,28 +56,32 @@ public abstract class TypeConstraintNode extends PklNode {
|
||||
initConstraintSlot(frame);
|
||||
|
||||
if (!result) {
|
||||
CompilerDirectives.transferToInterpreterAndInvalidate();
|
||||
var vmContext = VmContext.get(this);
|
||||
var localContext = VmLanguage.get(this).localContext.get();
|
||||
// Use power assertions if enabled and not in type test or already instrumenting.
|
||||
// This prevents `is` checks from triggering instrumentation, but allows them to
|
||||
// participate if instrumentation is already active.
|
||||
var usePowerAssertions =
|
||||
vmContext.getPowerAssertionsEnabled()
|
||||
&& (!localContext.isInTypeTest() || localContext.hasActiveTracker());
|
||||
if (usePowerAssertions) {
|
||||
try (var valueTracker = vmContext.getValueTrackerFactory().create()) {
|
||||
getBodyNode().executeGeneric(frame);
|
||||
throw new VmTypeMismatchException.Constraint(
|
||||
sourceSection,
|
||||
frame.getAuxiliarySlot(customThisSlot),
|
||||
sourceSection,
|
||||
valueTracker.values());
|
||||
}
|
||||
} else {
|
||||
throw new VmTypeMismatchException.Constraint(
|
||||
sourceSection, frame.getAuxiliarySlot(customThisSlot), sourceSection, null);
|
||||
throw typeMismatchException(frame.materialize());
|
||||
}
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
private VmTypeMismatchException typeMismatchException(MaterializedFrame frame) {
|
||||
var vmContext = VmContext.get(this);
|
||||
var localContext = VmLanguage.get(this).localContext.get();
|
||||
// Use power assertions if enabled and not in type test or already instrumenting.
|
||||
// This prevents `is` checks from triggering instrumentation, but allows them to
|
||||
// participate if instrumentation is already active.
|
||||
var usePowerAssertions =
|
||||
vmContext.getPowerAssertionsEnabled()
|
||||
&& (!localContext.isInTypeTest() || localContext.hasActiveTracker());
|
||||
if (usePowerAssertions) {
|
||||
try (var valueTracker = vmContext.getValueTrackerFactory().create()) {
|
||||
getBodyNode().executeGeneric(frame);
|
||||
return new VmTypeMismatchException.Constraint(
|
||||
sourceSection,
|
||||
frame.getAuxiliarySlot(customThisSlot),
|
||||
sourceSection,
|
||||
valueTracker.values());
|
||||
}
|
||||
} else {
|
||||
return new VmTypeMismatchException.Constraint(
|
||||
sourceSection, frame.getAuxiliarySlot(customThisSlot), sourceSection, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,28 +95,30 @@ public abstract class TypeConstraintNode extends PklNode {
|
||||
var value = frame.getAuxiliarySlot(customThisSlot);
|
||||
var result = applyNode.executeBoolean(function, value);
|
||||
if (!result) {
|
||||
CompilerDirectives.transferToInterpreterAndInvalidate();
|
||||
var vmContext = VmContext.get(this);
|
||||
var localContext = VmLanguage.get(this).localContext.get();
|
||||
// Use power assertions if enabled and not in type test or already instrumenting.
|
||||
// This prevents `is` checks from triggering instrumentation, but allows them to
|
||||
// participate if instrumentation is already active.
|
||||
var usePowerAssertions =
|
||||
vmContext.getPowerAssertionsEnabled()
|
||||
&& (!localContext.isInTypeTest() || localContext.hasActiveTracker());
|
||||
if (usePowerAssertions) {
|
||||
try (var valueTracker = vmContext.getValueTrackerFactory().create()) {
|
||||
applyNode.executeBoolean(function, value);
|
||||
throw new VmTypeMismatchException.Constraint(
|
||||
sourceSection,
|
||||
value,
|
||||
function.getRootNode().getSourceSection(),
|
||||
valueTracker.values());
|
||||
}
|
||||
} else {
|
||||
throw new VmTypeMismatchException.Constraint(
|
||||
sourceSection, value, function.getRootNode().getSourceSection(), null);
|
||||
throw typeMismatchException(function, value, applyNode);
|
||||
}
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
private VmTypeMismatchException typeMismatchException(
|
||||
VmFunction function, Object value, ApplyVmFunction1Node applyNode) {
|
||||
var vmContext = VmContext.get(this);
|
||||
var localContext = VmLanguage.get(this).localContext.get();
|
||||
// Use power assertions if enabled and not in type test or already instrumenting.
|
||||
// This prevents `is` checks from triggering instrumentation, but allows them to
|
||||
// participate if instrumentation is already active.
|
||||
var usePowerAssertions =
|
||||
vmContext.getPowerAssertionsEnabled()
|
||||
&& (!localContext.isInTypeTest() || localContext.hasActiveTracker());
|
||||
if (usePowerAssertions) {
|
||||
try (var valueTracker = vmContext.getValueTrackerFactory().create()) {
|
||||
applyNode.executeBoolean(function, value);
|
||||
return new VmTypeMismatchException.Constraint(
|
||||
sourceSection, value, function.getRootNode().getSourceSection(), valueTracker.values());
|
||||
}
|
||||
} else {
|
||||
return new VmTypeMismatchException.Constraint(
|
||||
sourceSection, value, function.getRootNode().getSourceSection(), null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,11 +24,13 @@ import com.oracle.truffle.api.dsl.Idempotent;
|
||||
import com.oracle.truffle.api.dsl.Specialization;
|
||||
import com.oracle.truffle.api.frame.FrameDescriptor;
|
||||
import com.oracle.truffle.api.frame.FrameSlotKind;
|
||||
import com.oracle.truffle.api.frame.MaterializedFrame;
|
||||
import com.oracle.truffle.api.frame.VirtualFrame;
|
||||
import com.oracle.truffle.api.nodes.ExplodeLoop;
|
||||
import com.oracle.truffle.api.nodes.LoopNode;
|
||||
import com.oracle.truffle.api.nodes.Node;
|
||||
import com.oracle.truffle.api.nodes.NodeUtil;
|
||||
import com.oracle.truffle.api.profiles.LoopConditionProfile;
|
||||
import com.oracle.truffle.api.source.SourceSection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -200,6 +202,7 @@ public abstract class TypeNode extends PklNode {
|
||||
/** Visit child type nodes of this type. */
|
||||
protected abstract boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer);
|
||||
|
||||
@TruffleBoundary
|
||||
protected VmTypeMismatchException constraintException(Object value, SourceSection sourceSection) {
|
||||
throw new VmTypeMismatchException.Constraint(
|
||||
sourceSection,
|
||||
@@ -376,7 +379,6 @@ public abstract class TypeNode extends PklNode {
|
||||
|
||||
@Override
|
||||
protected Object executeLazily(VirtualFrame frame, Object value) {
|
||||
CompilerDirectives.transferToInterpreter();
|
||||
throw new VmTypeMismatchException.Nothing(sourceSection, value);
|
||||
}
|
||||
|
||||
@@ -570,6 +572,10 @@ public abstract class TypeNode extends PklNode {
|
||||
String qualifiedName) {
|
||||
return literal;
|
||||
}
|
||||
|
||||
public String getLiteral() {
|
||||
return literal;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class TypedTypeNode extends ObjectSlotTypeNode {
|
||||
@@ -798,7 +804,7 @@ public abstract class TypeNode extends PklNode {
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnionTypeNode extends WriteFrameSlotTypeNode {
|
||||
public abstract static class UnionTypeNode extends WriteFrameSlotTypeNode {
|
||||
@Children final TypeNode[] elementTypeNodes;
|
||||
private final int defaultIndex;
|
||||
|
||||
@@ -820,16 +826,16 @@ public abstract class TypeNode extends PklNode {
|
||||
return MirrorFactories.unionTypeFactory.create(this);
|
||||
}
|
||||
|
||||
public VmList getElementTypeMirrors() {
|
||||
public final VmList getElementTypeMirrors() {
|
||||
return getMirrors(elementTypeNodes);
|
||||
}
|
||||
|
||||
public TypeNode[] getElementTypeNodes() {
|
||||
public final TypeNode[] getElementTypeNodes() {
|
||||
return elementTypeNodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNoopTypeCheck() {
|
||||
public final boolean isNoopTypeCheck() {
|
||||
for (var element : elementTypeNodes) {
|
||||
if (!element.isNoopTypeCheck()) return false;
|
||||
}
|
||||
@@ -837,7 +843,7 @@ public abstract class TypeNode extends PklNode {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object createDefaultValue(
|
||||
public final @Nullable Object createDefaultValue(
|
||||
VirtualFrame frame,
|
||||
VmLanguage language,
|
||||
SourceSection headerSection,
|
||||
@@ -849,27 +855,6 @@ public abstract class TypeNode extends PklNode {
|
||||
frame, language, headerSection, qualifiedName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ExplodeLoop
|
||||
protected boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) {
|
||||
if (!consumer.accept(this)) {
|
||||
return false;
|
||||
}
|
||||
var ret = true;
|
||||
// don't break early to ensure constant number of iterations
|
||||
//noinspection ForLoopReplaceableByForEach
|
||||
for (var i = 0; i < elementTypeNodes.length; i++) {
|
||||
if (!ret) {
|
||||
continue;
|
||||
}
|
||||
if (!elementTypeNodes[i].acceptTypeNode(visitTypeArguments, consumer)) {
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
LoopNode.reportLoopCount(this, elementTypeNodes.length);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the union type should be eagerly checked or not (shallow-force members of
|
||||
* Listing/Mapping).
|
||||
@@ -878,33 +863,120 @@ public abstract class TypeNode extends PklNode {
|
||||
* type; e.g. {@code Listing<Person>|Listing<Animal>}
|
||||
*/
|
||||
@TruffleBoundary
|
||||
private boolean shouldEagerCheck() {
|
||||
protected final boolean shouldEagerCheck() {
|
||||
var seenParameterizedClasses = EconomicSets.<VmClass>create();
|
||||
var ret = new MutableBoolean(false);
|
||||
acceptTypeNode(
|
||||
this.acceptTypeNode(
|
||||
false,
|
||||
(typeNode) -> {
|
||||
if (!typeNode.getType().isParametric()) {
|
||||
return true;
|
||||
}
|
||||
var typeClass = typeNode.getType().getVmClass();
|
||||
if (typeClass == null) {
|
||||
var typeNodeClass = typeNode.getType().getVmClass();
|
||||
if (typeNodeClass == null) {
|
||||
return true;
|
||||
}
|
||||
if (seenParameterizedClasses.contains(typeClass)) {
|
||||
if (seenParameterizedClasses.contains(typeNodeClass)) {
|
||||
ret.set(true);
|
||||
return false;
|
||||
} else {
|
||||
EconomicSets.add(seenParameterizedClasses, typeClass);
|
||||
EconomicSets.add(seenParameterizedClasses, typeNodeClass);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return ret.get();
|
||||
}
|
||||
|
||||
@Fallback
|
||||
// All members failed to type check; if enabled, re-execute type checks to generate power
|
||||
// assertions.
|
||||
//
|
||||
// Intentionally no `@ExplodeLoop` because this is the error path (prevent error path code from
|
||||
// exploding the compiled IR node count).
|
||||
//
|
||||
// Needs a `MaterializedFrame` (rather than VirtualFrame): this method is called from within an
|
||||
// `@ExplodeLoop` caller, and passing a VirtualFrame into a separately-compiled method called
|
||||
// from a loop makes Graal's escape analysis unable to prove the frame can stay virtual across
|
||||
// the call, causing a permanent "must not let virtual object escape" bailout.
|
||||
//
|
||||
// Materializing here is fine; this only runs once all union members have already failed to type
|
||||
// check.
|
||||
protected final VmTypeMismatchException.Union computeLazyUnionMismatch(
|
||||
MaterializedFrame frame,
|
||||
Object value,
|
||||
VmTypeMismatchException[] typeMismatches,
|
||||
boolean shouldEagerCheck,
|
||||
VmLocalContext localContext,
|
||||
boolean wasInTypeTest) {
|
||||
localContext.setInTypeTest(wasInTypeTest);
|
||||
if (VmContext.get(this).getPowerAssertionsEnabled()
|
||||
&& (!wasInTypeTest || localContext.hasActiveTracker())) {
|
||||
for (var i = 0; i < elementTypeNodes.length; i++) {
|
||||
var elementTypeNode = elementTypeNodes[i];
|
||||
try {
|
||||
if (shouldEagerCheck) {
|
||||
elementTypeNode.executeEagerly(frame, value);
|
||||
} else {
|
||||
elementTypeNode.executeLazily(frame, value);
|
||||
}
|
||||
} catch (VmTypeMismatchException e) {
|
||||
typeMismatches[i] = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new VmTypeMismatchException.Union(sourceSection, value, this, typeMismatches);
|
||||
}
|
||||
|
||||
// All members failed to type check; if enabled, re-execute type checks to generate power
|
||||
// assertions.
|
||||
//
|
||||
// Intentionally no `@ExplodeLoop` because this is the error path (prevent error path code from
|
||||
// exploding the compiled IR node count).
|
||||
//
|
||||
// Needs a `MaterializedFrame`, see comment on `computeLazyUnionMismatch` for details.
|
||||
protected final VmTypeMismatchException.Union computeEagerUnionMismatch(
|
||||
MaterializedFrame frame,
|
||||
Object value,
|
||||
VmTypeMismatchException[] typeMismatches,
|
||||
VmLocalContext localContext,
|
||||
boolean wasInTypeTest) {
|
||||
localContext.setInTypeTest(wasInTypeTest);
|
||||
if (VmContext.get(this).getPowerAssertionsEnabled()
|
||||
&& (!wasInTypeTest || localContext.hasActiveTracker())) {
|
||||
for (var i = 0; i < elementTypeNodes.length; i++) {
|
||||
try {
|
||||
elementTypeNodes[i].executeEagerly(frame, value);
|
||||
} catch (VmTypeMismatchException e) {
|
||||
typeMismatches[i] = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new VmTypeMismatchException.Union(sourceSection, value, this, typeMismatches);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnionTypeNodeExploded extends UnionTypeNode {
|
||||
public UnionTypeNodeExploded(
|
||||
SourceSection sourceSection, int defaultIndex, TypeNode[] elementTypeNodes) {
|
||||
super(sourceSection, defaultIndex, elementTypeNodes);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ExplodeLoop
|
||||
protected Object executeLazily(VirtualFrame frame, Object value) {
|
||||
protected boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) {
|
||||
if (!consumer.accept(this)) {
|
||||
return false;
|
||||
}
|
||||
for (var elementTypeNode : elementTypeNodes) {
|
||||
if (!elementTypeNode.acceptTypeNode(visitTypeArguments, consumer)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
@ExplodeLoop
|
||||
protected final Object executeLazily(VirtualFrame frame, Object value) {
|
||||
// escape analysis should remove this allocation in compiled code
|
||||
var typeMismatches = new VmTypeMismatchException[elementTypeNodes.length];
|
||||
|
||||
@@ -931,30 +1003,17 @@ public abstract class TypeNode extends PklNode {
|
||||
}
|
||||
}
|
||||
|
||||
// all members failed to type check
|
||||
// if enabled, re-execute type checks to generate power assertions
|
||||
localContext.setInTypeTest(wasInTypeTest);
|
||||
if (VmContext.get(this).getPowerAssertionsEnabled()
|
||||
&& (!wasInTypeTest || localContext.hasActiveTracker())) {
|
||||
for (var i = 0; i < elementTypeNodes.length; i++) {
|
||||
var elementTypeNode = elementTypeNodes[i];
|
||||
try {
|
||||
if (shouldEagerCheck) {
|
||||
elementTypeNode.executeEagerly(frame, value);
|
||||
} else {
|
||||
elementTypeNode.executeLazily(frame, value);
|
||||
}
|
||||
} catch (VmTypeMismatchException e) {
|
||||
typeMismatches[i] = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new VmTypeMismatchException.Union(sourceSection, value, this, typeMismatches);
|
||||
throw computeLazyUnionMismatch(
|
||||
frame.materialize(),
|
||||
value,
|
||||
typeMismatches,
|
||||
shouldEagerCheck,
|
||||
localContext,
|
||||
wasInTypeTest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object executeEagerly(VirtualFrame frame, Object value) {
|
||||
@ExplodeLoop
|
||||
public final Object executeEagerly(VirtualFrame frame, Object value) {
|
||||
// escape analysis should remove this allocation in compiled code
|
||||
var typeMismatches = new VmTypeMismatchException[elementTypeNodes.length];
|
||||
|
||||
@@ -974,21 +1033,91 @@ public abstract class TypeNode extends PklNode {
|
||||
}
|
||||
}
|
||||
|
||||
// all members failed to type check
|
||||
// if enabled, re-execute type checks to generate power assertions
|
||||
localContext.setInTypeTest(wasInTypeTest);
|
||||
if (VmContext.get(this).getPowerAssertionsEnabled()
|
||||
&& (!wasInTypeTest || localContext.hasActiveTracker())) {
|
||||
for (var i = 0; i < elementTypeNodes.length; i++) {
|
||||
try {
|
||||
elementTypeNodes[i].executeEagerly(frame, value);
|
||||
} catch (VmTypeMismatchException e) {
|
||||
typeMismatches[i] = e;
|
||||
}
|
||||
throw computeEagerUnionMismatch(
|
||||
frame.materialize(), value, typeMismatches, localContext, wasInTypeTest);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnionTypeNodeLooped extends UnionTypeNode {
|
||||
private final LoopConditionProfile loopConditionProfile = LoopConditionProfile.create();
|
||||
|
||||
public UnionTypeNodeLooped(
|
||||
SourceSection sourceSection, int defaultIndex, TypeNode[] elementTypeNodes) {
|
||||
super(sourceSection, defaultIndex, elementTypeNodes);
|
||||
}
|
||||
|
||||
// keep in sync with UnionTypeNodeExploded.acceptTypeNode
|
||||
@Override
|
||||
protected boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) {
|
||||
if (!consumer.accept(this)) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; loopConditionProfile.inject(i < elementTypeNodes.length); i++) {
|
||||
if (!elementTypeNodes[i].acceptTypeNode(visitTypeArguments, consumer)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
@Override
|
||||
protected Object executeLazily(VirtualFrame frame, Object value) {
|
||||
// escape analysis should remove this allocation in compiled code
|
||||
var typeMismatches = new VmTypeMismatchException[elementTypeNodes.length];
|
||||
var materializedFrame = frame.materialize();
|
||||
|
||||
// disallow power assertions from triggering in case one union member checks successfully
|
||||
var localContext = VmLanguage.get(this).localContext.get();
|
||||
var wasInTypeTest = localContext.isInTypeTest();
|
||||
localContext.setInTypeTest(true);
|
||||
|
||||
// Do eager checks (shallow-force) if there are two listings or two mappings represented.
|
||||
// (we can't know that `new Listing { 0; "hi" }[0]` fails for `Listing<Int>|Listing<String>`
|
||||
// without checking both index 0 and index 1).
|
||||
var shouldEagerCheck = shouldEagerCheck();
|
||||
for (var i = 0; loopConditionProfile.inject(i < elementTypeNodes.length); i++) {
|
||||
var elementTypeNode = elementTypeNodes[i];
|
||||
try {
|
||||
var result =
|
||||
shouldEagerCheck
|
||||
? elementTypeNode.executeEagerly(materializedFrame, value)
|
||||
: elementTypeNode.executeLazily(materializedFrame, value);
|
||||
localContext.setInTypeTest(wasInTypeTest);
|
||||
return result;
|
||||
} catch (VmTypeMismatchException e) {
|
||||
typeMismatches[i] = e;
|
||||
}
|
||||
}
|
||||
|
||||
throw new VmTypeMismatchException.Union(sourceSection, value, this, typeMismatches);
|
||||
throw computeLazyUnionMismatch(
|
||||
materializedFrame, value, typeMismatches, shouldEagerCheck, localContext, wasInTypeTest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object executeEagerly(VirtualFrame frame, Object value) {
|
||||
// escape analysis should remove this allocation in compiled code
|
||||
var typeMismatches = new VmTypeMismatchException[elementTypeNodes.length];
|
||||
var materializedFrame = frame.materialize();
|
||||
|
||||
// disallow power assertions from triggering in case one union member checks successfully
|
||||
var localContext = VmLanguage.get(this).localContext.get();
|
||||
var wasInTypeTest = localContext.isInTypeTest();
|
||||
localContext.setInTypeTest(true);
|
||||
|
||||
for (var i = 0; loopConditionProfile.inject(i < elementTypeNodes.length); i++) {
|
||||
// eager checks
|
||||
try {
|
||||
var result = elementTypeNodes[i].executeEagerly(materializedFrame, value);
|
||||
localContext.setInTypeTest(wasInTypeTest);
|
||||
return result;
|
||||
} catch (VmTypeMismatchException e) {
|
||||
typeMismatches[i] = e;
|
||||
}
|
||||
}
|
||||
|
||||
throw computeEagerUnionMismatch(
|
||||
materializedFrame, value, typeMismatches, localContext, wasInTypeTest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1001,8 +1130,8 @@ public abstract class TypeNode extends PklNode {
|
||||
SourceSection sourceSection, int defaultIndex, Set<String> stringLiterals) {
|
||||
super(sourceSection);
|
||||
assert !stringLiterals.isEmpty();
|
||||
this.stringLiterals = stringLiterals;
|
||||
this.defaultIndex = defaultIndex;
|
||||
this.stringLiterals = stringLiterals;
|
||||
if (defaultIndex == -1) {
|
||||
unionDefault = null;
|
||||
} else {
|
||||
@@ -1054,6 +1183,14 @@ public abstract class TypeNode extends PklNode {
|
||||
String qualifiedName) {
|
||||
return unionDefault;
|
||||
}
|
||||
|
||||
public int getDefaultIndex() {
|
||||
return defaultIndex;
|
||||
}
|
||||
|
||||
public Set<String> getStringLiterals() {
|
||||
return stringLiterals;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class CollectionTypeNode extends ObjectSlotTypeNode {
|
||||
@@ -1207,7 +1344,6 @@ public abstract class TypeNode extends PklNode {
|
||||
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
@Override
|
||||
@ExplodeLoop
|
||||
protected Object executeLazily(VirtualFrame frame, Object value) {
|
||||
if (!(value instanceof VmList vmList)) {
|
||||
throw typeMismatch(value, BaseModule.getListClass());
|
||||
@@ -1493,7 +1629,10 @@ public abstract class TypeNode extends PklNode {
|
||||
private final VmLanguage language;
|
||||
@Child protected @Nullable TypeNode keyTypeNode;
|
||||
@Child protected TypeNode valueTypeNode;
|
||||
@Child @Nullable protected ListingOrMappingTypeCastNode valueTypeCastNode;
|
||||
// Not a @Child: a RootNode must never be adopted as a child (it always has a null parent).
|
||||
// It is only ever invoked indirectly, via an IndirectCallNode held by the VmListing/VmMapping
|
||||
// values this type node produces (see ElementOrEntryNode).
|
||||
@Nullable protected ListingOrMappingTypeCastNode valueTypeCastNode;
|
||||
|
||||
protected ListingOrMappingTypeNode(
|
||||
SourceSection sourceSection,
|
||||
@@ -1521,9 +1660,14 @@ public abstract class TypeNode extends PklNode {
|
||||
protected ListingOrMappingTypeCastNode getValueTypeCastNode(FrameDescriptor descriptor) {
|
||||
if (valueTypeCastNode == null) {
|
||||
CompilerDirectives.transferToInterpreterAndInvalidate();
|
||||
// `valueTypeNode` is already an adopted @Child of `this`, and is also executed directly
|
||||
// by `this` (see executeEagerly()/executeLazily() below). It must not be reused as a
|
||||
// @Child of a second, unrelated root node -- a node can only ever have one parent, and
|
||||
// doing so corrupts `valueTypeNode.getRootNode()`, which breaks the compiler's ability to
|
||||
// constant-fold context/language lookups reached through it.
|
||||
valueTypeCastNode =
|
||||
new ListingOrMappingTypeCastNode(
|
||||
language, descriptor, valueTypeNode, getRootNode().getName());
|
||||
language, descriptor, (TypeNode) valueTypeNode.deepCopy(), getRootNode().getName());
|
||||
}
|
||||
return valueTypeCastNode;
|
||||
}
|
||||
@@ -1656,10 +1800,7 @@ public abstract class TypeNode extends PklNode {
|
||||
try {
|
||||
keyTypeNode.executeEagerly(frame, memberKey);
|
||||
} catch (VmTypeMismatchException e) {
|
||||
CompilerDirectives.transferToInterpreter();
|
||||
e.putInsertedStackFrame(
|
||||
getRootNode().getCallTarget(),
|
||||
VmUtils.createStackFrame(member.getHeaderSection(), member.getQualifiedName()));
|
||||
putStackFrame(member, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -1681,6 +1822,13 @@ public abstract class TypeNode extends PklNode {
|
||||
|
||||
LoopNode.reportLoopCount(this, loopCount);
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
private void putStackFrame(ObjectMember member, VmTypeMismatchException e) {
|
||||
e.putInsertedStackFrame(
|
||||
getRootNode().getCallTarget(),
|
||||
VmUtils.createStackFrame(member.getHeaderSection(), member.getQualifiedName()));
|
||||
}
|
||||
}
|
||||
|
||||
// A type such as `(Int, String) -> Duration`.
|
||||
@@ -2057,7 +2205,6 @@ public abstract class TypeNode extends PklNode {
|
||||
@Override
|
||||
protected Object executeLazily(VirtualFrame frame, Object value) {
|
||||
if (value instanceof VmNull) {
|
||||
CompilerDirectives.transferToInterpreterAndInvalidate();
|
||||
throw constraintException(value, BaseModule.getNonNullTypeAlias().getConstraintSection());
|
||||
}
|
||||
return value;
|
||||
@@ -2095,7 +2242,6 @@ public abstract class TypeNode extends PklNode {
|
||||
if (value instanceof Long l) {
|
||||
if ((l & mask) == l) return value;
|
||||
|
||||
CompilerDirectives.transferToInterpreterAndInvalidate();
|
||||
var sourceSection = typeAlias.getConstraintSection();
|
||||
throw constraintException(value, sourceSection);
|
||||
}
|
||||
@@ -2136,7 +2282,6 @@ public abstract class TypeNode extends PklNode {
|
||||
if (value instanceof Long l) {
|
||||
if (l == l.byteValue()) return value;
|
||||
|
||||
CompilerDirectives.transferToInterpreterAndInvalidate();
|
||||
var sourceSection = BaseModule.getInt8TypeAlias().getConstraintSection();
|
||||
throw constraintException(value, sourceSection);
|
||||
}
|
||||
@@ -2171,7 +2316,6 @@ public abstract class TypeNode extends PklNode {
|
||||
if (value instanceof Long l) {
|
||||
if (l == l.shortValue()) return value;
|
||||
|
||||
CompilerDirectives.transferToInterpreterAndInvalidate();
|
||||
var sourceSection = BaseModule.getInt16TypeAlias().getConstraintSection();
|
||||
throw constraintException(value, sourceSection);
|
||||
}
|
||||
@@ -2206,11 +2350,9 @@ public abstract class TypeNode extends PklNode {
|
||||
if (value instanceof Long l) {
|
||||
if (l == l.intValue()) return value;
|
||||
|
||||
CompilerDirectives.transferToInterpreterAndInvalidate();
|
||||
throw constraintException(value, BaseModule.getInt32TypeAlias().getConstraintSection());
|
||||
}
|
||||
|
||||
CompilerDirectives.transferToInterpreterAndInvalidate();
|
||||
throw new VmTypeMismatchException.Simple(
|
||||
BaseModule.getInt32TypeAlias().getBaseTypeSection(), value, BaseModule.getIntClass());
|
||||
}
|
||||
@@ -2348,6 +2490,10 @@ public abstract class TypeNode extends PklNode {
|
||||
protected boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) {
|
||||
return consumer.accept(this) && aliasedTypeNode.acceptTypeNode(visitTypeArguments, consumer);
|
||||
}
|
||||
|
||||
public TypeNode getAliasedTypeNode() {
|
||||
return aliasedTypeNode;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ConstrainedTypeNode extends TypeNode {
|
||||
@@ -2696,12 +2842,10 @@ public abstract class TypeNode extends PklNode {
|
||||
|
||||
// clazz will be null iff the type arg is a not a valid class type
|
||||
if (clazz == null) {
|
||||
CompilerDirectives.transferToInterpreter();
|
||||
throw new VmTypeMismatchException.ClassType(sourceSection, value, getType());
|
||||
}
|
||||
|
||||
if (!value.isSubclassOf(clazz)) {
|
||||
CompilerDirectives.transferToInterpreter();
|
||||
throw new VmTypeMismatchException.ClassType(sourceSection, value, clazz);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,15 @@ public final class TypeTestNode extends ExpressionNode {
|
||||
@Child private ExpressionNode valueNode;
|
||||
@Child private @Nullable UnresolvedTypeNode unresolvedTypeNode;
|
||||
@Child private @Nullable TypeNode typeNode;
|
||||
private final VmLanguage language;
|
||||
|
||||
public TypeTestNode(
|
||||
SourceSection sourceSection,
|
||||
VmLanguage language,
|
||||
ExpressionNode valueNode,
|
||||
UnresolvedTypeNode unresolvedTypeNode) {
|
||||
super(sourceSection);
|
||||
this.language = language;
|
||||
this.valueNode = valueNode;
|
||||
this.unresolvedTypeNode = unresolvedTypeNode;
|
||||
}
|
||||
@@ -57,8 +60,8 @@ public final class TypeTestNode extends ExpressionNode {
|
||||
// TODO: throw if typeNode is FunctionTypeNode (it's impossible to check)
|
||||
// https://github.com/apple/pkl/issues/639
|
||||
Object value = valueNode.executeGeneric(frame);
|
||||
var localContext = VmLanguage.get(this).localContext.get();
|
||||
boolean wasInTypeTest = localContext.isInTypeTest();
|
||||
var localContext = language.localContext.get();
|
||||
var wasInTypeTest = localContext.isInTypeTest();
|
||||
localContext.setInTypeTest(true);
|
||||
try {
|
||||
typeNode.executeEagerly(frame, value);
|
||||
|
||||
@@ -364,6 +364,8 @@ public abstract class UnresolvedTypeNode extends PklNode {
|
||||
}
|
||||
|
||||
public static final class Union extends UnresolvedTypeNode {
|
||||
private static final int MAX_EXPLODE_LOOP = 20;
|
||||
|
||||
@Children private final UnresolvedTypeNode[] unresolvedElementTypeNodes;
|
||||
private final int defaultIndex;
|
||||
|
||||
@@ -386,7 +388,15 @@ public abstract class UnresolvedTypeNode extends PklNode {
|
||||
elementTypeNodes[i] = elementTypeNode;
|
||||
}
|
||||
|
||||
return new UnionTypeNode(sourceSection, defaultIndex, elementTypeNodes);
|
||||
// Don't use `@ExplodeLoop` variant of union type of union is too large;
|
||||
// this can easily turn into too many IR nodes; the compiler will spend too much time
|
||||
// compiling these code paths.
|
||||
//
|
||||
// This is especially true for typealiases of unions, because they get inlined into their
|
||||
// usage sites.
|
||||
return elementTypeNodes.length > MAX_EXPLODE_LOOP
|
||||
? new UnionTypeNodeLooped(sourceSection, defaultIndex, elementTypeNodes)
|
||||
: new UnionTypeNodeExploded(sourceSection, defaultIndex, elementTypeNodes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -175,12 +175,14 @@ public abstract class VmTypeMismatchException extends ControlFlowException {
|
||||
private final String renderedExpected;
|
||||
private final @Nullable VmClass expectedClass;
|
||||
|
||||
@TruffleBoundary
|
||||
public ClassType(SourceSection sourceSection, VmClass actualClass, VmClass expectedClass) {
|
||||
super(sourceSection, actualClass);
|
||||
this.expectedClass = expectedClass;
|
||||
renderedExpected = "Class<" + expectedClass.toPklString() + ">";
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
public ClassType(SourceSection sourceSection, VmClass actualClass, VmType expectedType) {
|
||||
super(sourceSection, actualClass);
|
||||
this.expectedClass = null;
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.pkl.core.ast.type.TypeNode.StringTypeNode;
|
||||
import org.pkl.core.ast.type.TypeNode.TypeAliasTypeNode;
|
||||
import org.pkl.core.ast.type.TypeNode.UnionOfStringLiteralsTypeNode;
|
||||
import org.pkl.core.ast.type.TypeNode.UnionTypeNode;
|
||||
import org.pkl.core.ast.type.TypeNode.UnionTypeNodeExploded;
|
||||
import org.pkl.core.ast.type.VmTypeMismatchException;
|
||||
import org.pkl.core.runtime.Identifier;
|
||||
import org.pkl.core.runtime.VmBytes;
|
||||
@@ -648,7 +649,7 @@ public final class RendererNodes {
|
||||
type =
|
||||
hasString && elements.size() == 1
|
||||
? elements.get(0)
|
||||
: new UnionTypeNode(
|
||||
: new UnionTypeNodeExploded(
|
||||
VmUtils.unavailableSourceSection(), -1, elements.toArray(new TypeNode[0]));
|
||||
return type;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user