mirror of
https://github.com/apple/pkl.git
synced 2026-09-06 01:47:31 +02:00
Improve partial-evaluation when amending null parents (#1836)
GraalVM's partial evaulator can't handle recursive calls into the same node. The current implementation around evaluating `VmNull` as a parent causes the partial evaluator to bail out, leaving Pkl stuck in interpreter mode. This rewrites the various object literals to add individual specializations for each type of default value we can see from a VmNull parent. Also: * Fix `isTypeObjectClass` impl * Fix bug when amending with generator object literal node with object params
This commit is contained in:
+59
-9
@@ -17,6 +17,7 @@ package org.pkl.core.ast.expression.generator;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives;
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import com.oracle.truffle.api.dsl.Bind;
|
||||
import com.oracle.truffle.api.dsl.Cached;
|
||||
import com.oracle.truffle.api.dsl.Fallback;
|
||||
import com.oracle.truffle.api.dsl.Idempotent;
|
||||
@@ -34,7 +35,7 @@ import org.pkl.core.ast.type.UnresolvedTypeNode;
|
||||
import org.pkl.core.runtime.*;
|
||||
|
||||
/** An object literal node that contains at least one for- or when-expression. */
|
||||
@ImportStatic(BaseModule.class)
|
||||
@ImportStatic({BaseModule.class, VmUtils.class})
|
||||
public abstract class GeneratorObjectLiteralNode extends ObjectLiteralNode {
|
||||
@Children private final GeneratorMemberNode[] memberNodes;
|
||||
|
||||
@@ -80,6 +81,20 @@ public abstract class GeneratorObjectLiteralNode extends ObjectLiteralNode {
|
||||
return data.storeGeneratorFrames(result);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"getClass(defaultValue).isDynamicClass()", "checkObjectCannotHaveParameters()"})
|
||||
protected Object evalNullWithDynamicDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
var parentDynamic = (VmDynamic) defaultValue;
|
||||
var data = executeChildren(frame, parentDynamic, parentDynamic.getLength());
|
||||
if (data.hasNoMembers()) {
|
||||
return parentDynamic;
|
||||
}
|
||||
var result = new VmDynamic(frame.materialize(), parentDynamic, data.members(), data.length());
|
||||
return data.storeGeneratorFrames(result);
|
||||
}
|
||||
|
||||
@Specialization(guards = "checkObjectCannotHaveParameters()")
|
||||
protected VmTyped evalTyped(VirtualFrame frame, VmTyped parent) {
|
||||
VmUtils.checkIsInstantiable(parent.getVmClass(), getParentNode());
|
||||
@@ -91,6 +106,14 @@ public abstract class GeneratorObjectLiteralNode extends ObjectLiteralNode {
|
||||
return new VmTyped(frame.materialize(), parent, parent.getVmClass(), data.members());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"isTypedObjectClass(getClass(defaultValue))", "checkObjectCannotHaveParameters()"})
|
||||
protected Object evalNullWithTypedDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalTyped(frame, (VmTyped) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization(guards = "checkListingCannotHaveParameters()")
|
||||
protected VmListing evalListing(VirtualFrame frame, VmListing parent) {
|
||||
var data = executeChildren(frame, parent, parent.getLength());
|
||||
@@ -101,6 +124,14 @@ public abstract class GeneratorObjectLiteralNode extends ObjectLiteralNode {
|
||||
return data.storeGeneratorFrames(result);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"getClass(defaultValue).isListingClass()", "checkListingCannotHaveParameters()"})
|
||||
protected Object evalNullWithListingDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalListing(frame, (VmListing) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization(guards = "checkMappingCannotHaveParameters()")
|
||||
protected VmMapping evalMapping(VirtualFrame frame, VmMapping parent) {
|
||||
var data = executeChildren(frame, parent, 0);
|
||||
@@ -111,10 +142,12 @@ public abstract class GeneratorObjectLiteralNode extends ObjectLiteralNode {
|
||||
return data.storeGeneratorFrames(result);
|
||||
}
|
||||
|
||||
@Specialization(guards = "checkObjectCannotHaveParameters()")
|
||||
protected Object evalNull(VirtualFrame frame, VmNull parent) {
|
||||
// assumes that Graal PE can handle recursive call to same node
|
||||
return executeWithParent(frame, parent.getDefaultValue());
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"getClass(defaultValue).isMappingClass()", "checkMappingCannotHaveParameters()"})
|
||||
protected Object evalNullWithMappingDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalMapping(frame, (VmMapping) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization(guards = "checkIsValidFunctionAmendment(parent)")
|
||||
@@ -127,6 +160,18 @@ public abstract class GeneratorObjectLiteralNode extends ObjectLiteralNode {
|
||||
return amendFunctionNode.execute(frame, parent);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"isFunction(defaultValue)", "checkIsValidFunctionAmendment(defaultValue)"})
|
||||
protected Object evalNullWithFunctionDefault(
|
||||
VirtualFrame frame,
|
||||
VmNull parent,
|
||||
@Bind("getNullDefaultValue(parent)") Object defaultValue,
|
||||
@Cached(value = "createAmendFunctionNode(frame)", neverDefault = true)
|
||||
AmendFunctionNode amendFunctionNode) {
|
||||
return evalFunction(frame, (VmFunction) defaultValue, amendFunctionNode);
|
||||
}
|
||||
|
||||
@Specialization(guards = {"parent == getDynamicClass()", "checkObjectCannotHaveParameters()"})
|
||||
protected VmDynamic evalDynamicClass(VirtualFrame frame, VmClass parent) {
|
||||
var data = executeChildren(frame, parent, 0);
|
||||
@@ -173,8 +218,13 @@ public abstract class GeneratorObjectLiteralNode extends ObjectLiteralNode {
|
||||
@Fallback
|
||||
@TruffleBoundary
|
||||
protected void fallback(Object parent) {
|
||||
var value = parent;
|
||||
// blame the non-null type (e.g. blame `Duration` instead of `Null` in the case of `Duration?`)
|
||||
if (value instanceof VmNull vmNull) {
|
||||
value = getNullDefaultValue(vmNull);
|
||||
}
|
||||
VmUtils.checkIsInstantiable(
|
||||
parent instanceof VmClass vmClass ? vmClass : VmUtils.getClass(parent), getParentNode());
|
||||
value instanceof VmClass vmClass ? vmClass : VmUtils.getClass(value), getParentNode());
|
||||
|
||||
throw exceptionBuilder().unreachableCode().build();
|
||||
}
|
||||
@@ -186,7 +236,7 @@ public abstract class GeneratorObjectLiteralNode extends ObjectLiteralNode {
|
||||
CompilerDirectives.transferToInterpreter();
|
||||
throw exceptionBuilder()
|
||||
.evalError("objectAmendmentCannotHaveParameters")
|
||||
.withLocation(parameterTypes[0])
|
||||
.withLocation(getParentNode())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -197,7 +247,7 @@ public abstract class GeneratorObjectLiteralNode extends ObjectLiteralNode {
|
||||
CompilerDirectives.transferToInterpreter();
|
||||
throw exceptionBuilder()
|
||||
.evalError("listingAmendmentCannotHaveParameters")
|
||||
.withLocation(parameterTypes[0])
|
||||
.withLocation(getParentNode())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -208,7 +258,7 @@ public abstract class GeneratorObjectLiteralNode extends ObjectLiteralNode {
|
||||
CompilerDirectives.transferToInterpreter();
|
||||
throw exceptionBuilder()
|
||||
.evalError("mappingAmendmentCannotHaveParameters")
|
||||
.withLocation(parameterTypes[0])
|
||||
.withLocation(getParentNode())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
+40
-6
@@ -33,7 +33,7 @@ import org.pkl.core.runtime.*;
|
||||
* all entry keys are constants. Example: `new foo { ["one"] = 1 }`
|
||||
*/
|
||||
// IDEA: don't materialize frames if all members have constant values
|
||||
@ImportStatic(BaseModule.class)
|
||||
@ImportStatic({BaseModule.class, VmUtils.class})
|
||||
public abstract class ConstantEntriesLiteralNode extends SpecializedObjectLiteralNode {
|
||||
public ConstantEntriesLiteralNode(
|
||||
SourceSection sourceSection,
|
||||
@@ -73,21 +73,38 @@ public abstract class ConstantEntriesLiteralNode extends SpecializedObjectLitera
|
||||
return new VmMapping(frame.materialize(), parent, members);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"getClass(defaultValue).isMappingClass()", "checkIsValidMappingAmendment()"})
|
||||
protected Object evalNullWithMappingDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalMapping(frame, (VmMapping) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization
|
||||
protected VmDynamic evalDynamic(VirtualFrame frame, VmDynamic parent) {
|
||||
return new VmDynamic(frame.materialize(), parent, members, parent.getLength());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(guards = "getClass(defaultValue).isDynamicClass()")
|
||||
protected Object evalNullWithDynamicDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalDynamic(frame, (VmDynamic) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization(guards = "checkIsValidListingAmendment()")
|
||||
protected VmListing evalListing(VirtualFrame frame, VmListing parent) {
|
||||
checkMaxListingMemberIndex(parent.getLength());
|
||||
return new VmListing(frame.materialize(), parent, members, parent.getLength());
|
||||
}
|
||||
|
||||
@Specialization
|
||||
protected Object evalNull(VirtualFrame frame, VmNull parent) {
|
||||
// assumes that Graal PE can handle recursive call to same node
|
||||
return executeWithParent(frame, parent.getDefaultValue());
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"getClass(defaultValue).isListingClass()", "checkIsValidListingAmendment()"})
|
||||
protected Object evalNullWithListingDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalListing(frame, (VmListing) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization(guards = "checkIsValidFunctionAmendment(parent)")
|
||||
@@ -100,6 +117,18 @@ public abstract class ConstantEntriesLiteralNode extends SpecializedObjectLitera
|
||||
return amendFunctionNode.execute(frame, parent);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"isFunction(defaultValue)", "checkIsValidFunctionAmendment(defaultValue)"})
|
||||
protected Object evalNullWithFunctionDefault(
|
||||
VirtualFrame frame,
|
||||
VmNull parent,
|
||||
@Bind("getNullDefaultValue(parent)") Object defaultValue,
|
||||
@Cached(value = "createAmendFunctionNode(frame)", neverDefault = true)
|
||||
AmendFunctionNode amendFunctionNode) {
|
||||
return evalFunction(frame, (VmFunction) defaultValue, amendFunctionNode);
|
||||
}
|
||||
|
||||
@Specialization(guards = {"parent == getMappingClass()", "checkIsValidMappingAmendment()"})
|
||||
protected VmMapping evalMappingClass(
|
||||
VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) {
|
||||
@@ -127,6 +156,11 @@ public abstract class ConstantEntriesLiteralNode extends SpecializedObjectLitera
|
||||
@Fallback
|
||||
@TruffleBoundary
|
||||
protected void fallback(Object parent) {
|
||||
elementsEntriesFallback(parent, findFirstNonProperty(members), false);
|
||||
var value = parent;
|
||||
// blame the non-null type (e.g. blame `Duration` instead of `Null` in the case of `Duration?`)
|
||||
if (value instanceof VmNull vmNull) {
|
||||
value = getNullDefaultValue(vmNull);
|
||||
}
|
||||
elementsEntriesFallback(value, findFirstNonProperty(members), false);
|
||||
}
|
||||
}
|
||||
|
||||
+33
-6
@@ -16,6 +16,7 @@
|
||||
package org.pkl.core.ast.expression.literal;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import com.oracle.truffle.api.dsl.Bind;
|
||||
import com.oracle.truffle.api.dsl.Cached;
|
||||
import com.oracle.truffle.api.dsl.Fallback;
|
||||
import com.oracle.truffle.api.dsl.ImportStatic;
|
||||
@@ -36,7 +37,7 @@ import org.pkl.core.util.EconomicMaps;
|
||||
* Object literal that contains both elements and entries (and possibly properties). Example: `new
|
||||
* foo { "pigeon", [3] = "barn owl" }`
|
||||
*/
|
||||
@ImportStatic(BaseModule.class)
|
||||
@ImportStatic({BaseModule.class, VmUtils.class})
|
||||
public abstract class ElementsEntriesLiteralNode extends SpecializedObjectLiteralNode {
|
||||
private final ObjectMember[] elements;
|
||||
@Children private final ExpressionNode[] keyNodes;
|
||||
@@ -95,6 +96,14 @@ public abstract class ElementsEntriesLiteralNode extends SpecializedObjectLitera
|
||||
parent.getLength() + elements.length);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"getClass(defaultValue).isListingClass()", "checkIsValidListingAmendment()"})
|
||||
protected Object evalNullWithListingDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalListing(frame, (VmListing) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization
|
||||
protected VmDynamic evalDynamic(VirtualFrame frame, VmDynamic parent) {
|
||||
return new VmDynamic(
|
||||
@@ -104,10 +113,11 @@ public abstract class ElementsEntriesLiteralNode extends SpecializedObjectLitera
|
||||
parent.getLength() + elements.length);
|
||||
}
|
||||
|
||||
@Specialization
|
||||
protected Object evalNull(VirtualFrame frame, VmNull parent) {
|
||||
// assumes that Graal PE can handle recursive call to same node
|
||||
return executeWithParent(frame, parent.getDefaultValue());
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(guards = "getClass(defaultValue).isDynamicClass()")
|
||||
protected Object evalNullWithDynamicDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalDynamic(frame, (VmDynamic) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization(guards = "checkIsValidFunctionAmendment(parent)")
|
||||
@@ -120,6 +130,18 @@ public abstract class ElementsEntriesLiteralNode extends SpecializedObjectLitera
|
||||
return amendFunctionNode.execute(frame, parent);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"isFunction(defaultValue)", "checkIsValidFunctionAmendment(defaultValue)"})
|
||||
protected Object evalNullWithFunctionDefault(
|
||||
VirtualFrame frame,
|
||||
VmNull parent,
|
||||
@Bind("getNullDefaultValue(parent)") Object defaultValue,
|
||||
@Cached(value = "createAmendFunctionNode(frame)", neverDefault = true)
|
||||
AmendFunctionNode amendFunctionNode) {
|
||||
return evalFunction(frame, (VmFunction) defaultValue, amendFunctionNode);
|
||||
}
|
||||
|
||||
@Specialization(guards = {"parent == getListingClass()", "checkIsValidListingAmendment()"})
|
||||
protected VmListing evalListingClass(
|
||||
VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) {
|
||||
@@ -145,7 +167,12 @@ public abstract class ElementsEntriesLiteralNode extends SpecializedObjectLitera
|
||||
@Fallback
|
||||
@TruffleBoundary
|
||||
protected void fallback(Object parent) {
|
||||
elementsEntriesFallback(parent, elements[0], true);
|
||||
var value = parent;
|
||||
// blame the non-null type (e.g. blame `Duration` instead of `Null` in the case of `Duration?`)
|
||||
if (value instanceof VmNull vmNull) {
|
||||
value = getNullDefaultValue(vmNull);
|
||||
}
|
||||
elementsEntriesFallback(value, elements[0], true);
|
||||
}
|
||||
|
||||
@ExplodeLoop
|
||||
|
||||
+32
-6
@@ -32,7 +32,7 @@ import org.pkl.core.util.EconomicMaps;
|
||||
* Object literal that contains elements (and possibly properties) but not entries. Example: `new
|
||||
* foo { "pigeon" }`
|
||||
*/
|
||||
@ImportStatic(BaseModule.class)
|
||||
@ImportStatic({BaseModule.class, VmUtils.class})
|
||||
public abstract class ElementsLiteralNode extends SpecializedObjectLiteralNode {
|
||||
private final ObjectMember[] elements;
|
||||
|
||||
@@ -94,10 +94,11 @@ public abstract class ElementsLiteralNode extends SpecializedObjectLiteralNode {
|
||||
parent.getLength() + elements.length);
|
||||
}
|
||||
|
||||
@Specialization
|
||||
protected Object evalNull(VirtualFrame frame, VmNull parent) {
|
||||
// assumes that Graal PE can handle recursive call to same node
|
||||
return executeWithParent(frame, parent.getDefaultValue());
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(guards = "getClass(defaultValue).isDynamicClass()")
|
||||
protected Object evalNullWithDynamicDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalDynamicUncached(frame, (VmDynamic) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization(guards = "checkIsValidFunctionAmendment(parent)")
|
||||
@@ -110,6 +111,18 @@ public abstract class ElementsLiteralNode extends SpecializedObjectLiteralNode {
|
||||
return amendFunctionNode.execute(frame, parent);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"isFunction(defaultValue)", "checkIsValidFunctionAmendment(defaultValue)"})
|
||||
protected Object evalNullWithFunctionDefault(
|
||||
VirtualFrame frame,
|
||||
VmNull parent,
|
||||
@Bind("getNullDefaultValue(parent)") Object defaultValue,
|
||||
@Cached(value = "createAmendFunctionNode(frame)", neverDefault = true)
|
||||
AmendFunctionNode amendFunctionNode) {
|
||||
return evalFunction(frame, (VmFunction) defaultValue, amendFunctionNode);
|
||||
}
|
||||
|
||||
@Specialization(
|
||||
guards = {
|
||||
"parent == getListingClass()",
|
||||
@@ -162,10 +175,23 @@ public abstract class ElementsLiteralNode extends SpecializedObjectLiteralNode {
|
||||
parent.getLength() + elements.length);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"getClass(defaultValue).isListingClass()", "checkIsValidListingAmendment()"})
|
||||
protected Object evalNullWithListingDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalListingUncached(frame, (VmListing) defaultValue);
|
||||
}
|
||||
|
||||
@Fallback
|
||||
@TruffleBoundary
|
||||
protected void fallback(Object parent) {
|
||||
elementsEntriesFallback(parent, elements[0], true);
|
||||
var value = parent;
|
||||
// blame the non-null type (e.g. blame `Duration` instead of `Null` in the case of `Duration?`)
|
||||
if (value instanceof VmNull vmNull) {
|
||||
value = getNullDefaultValue(vmNull);
|
||||
}
|
||||
elementsEntriesFallback(value, elements[0], true);
|
||||
}
|
||||
|
||||
// offset element keys according to parentLength
|
||||
|
||||
@@ -38,7 +38,7 @@ import org.pkl.core.util.EconomicMaps;
|
||||
* used.) Example: `foo { ["on" + "e"] = 1 }`
|
||||
*/
|
||||
// IDEA: don't materialize frames if all members have constant values
|
||||
@ImportStatic(BaseModule.class)
|
||||
@ImportStatic({BaseModule.class, VmUtils.class})
|
||||
public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode {
|
||||
@Children private final ExpressionNode[] keyNodes;
|
||||
private final ObjectMember[] values;
|
||||
@@ -92,11 +92,26 @@ public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode {
|
||||
return new VmMapping(frame.materialize(), parent, createMapMembers(frame));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"getClass(defaultValue).isMappingClass()", "checkIsValidMappingAmendment()"})
|
||||
protected Object evalNullWithMappingDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalMapping(frame, (VmMapping) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization
|
||||
protected VmDynamic evalDynamic(VirtualFrame frame, VmDynamic parent) {
|
||||
return new VmDynamic(frame.materialize(), parent, createMapMembers(frame), parent.getLength());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(guards = {"getClass(defaultValue).isDynamicClass()"})
|
||||
protected Object evalNullWithDynamicDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalDynamic(frame, (VmDynamic) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization(guards = "checkIsValidListingAmendment()")
|
||||
protected VmListing evalListing(VirtualFrame frame, VmListing parent) {
|
||||
return new VmListing(
|
||||
@@ -107,10 +122,12 @@ public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode {
|
||||
parent.getLength());
|
||||
}
|
||||
|
||||
@Specialization
|
||||
protected Object evalNull(VirtualFrame frame, VmNull parent) {
|
||||
// assumes that Graal PE can handle recursive call to same node
|
||||
return executeWithParent(frame, parent.getDefaultValue());
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"getClass(defaultValue).isListingClass()", "checkIsValidListingAmendment()"})
|
||||
protected Object evalNullWithListingDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalListing(frame, (VmListing) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization(guards = "checkIsValidFunctionAmendment(parent)")
|
||||
@@ -123,6 +140,18 @@ public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode {
|
||||
return amendFunctionNode.execute(frame, parent);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"isFunction(defaultValue)", "checkIsValidFunctionAmendment(defaultValue)"})
|
||||
protected Object evalNullWithFunctionDefault(
|
||||
VirtualFrame frame,
|
||||
VmNull parent,
|
||||
@Bind("getNullDefaultValue(parent)") Object defaultValue,
|
||||
@Cached(value = "createAmendFunctionNode(frame)", neverDefault = true)
|
||||
AmendFunctionNode amendFunctionNode) {
|
||||
return evalFunction(frame, (VmFunction) defaultValue, amendFunctionNode);
|
||||
}
|
||||
|
||||
@Specialization(guards = {"parent == getMappingClass()", "checkIsValidMappingAmendment()"})
|
||||
protected VmMapping evalMappingClass(
|
||||
VirtualFrame frame, @SuppressWarnings("unused") VmClass parent) {
|
||||
@@ -152,7 +181,12 @@ public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode {
|
||||
@Fallback
|
||||
@TruffleBoundary
|
||||
protected Object fallback(Object parent) {
|
||||
return elementsEntriesFallback(parent, values[0], false);
|
||||
var value = parent;
|
||||
// blame the non-null type (e.g. blame `Duration` instead of `Null` in the case of `Duration?`)
|
||||
if (value instanceof VmNull vmNull) {
|
||||
value = getNullDefaultValue(vmNull);
|
||||
}
|
||||
return elementsEntriesFallback(value, values[0], false);
|
||||
}
|
||||
|
||||
@ExplodeLoop
|
||||
|
||||
@@ -20,14 +20,17 @@ import com.oracle.truffle.api.dsl.Idempotent;
|
||||
import com.oracle.truffle.api.dsl.NodeChild;
|
||||
import com.oracle.truffle.api.frame.FrameDescriptor;
|
||||
import com.oracle.truffle.api.frame.VirtualFrame;
|
||||
import com.oracle.truffle.api.nodes.LoopNode;
|
||||
import com.oracle.truffle.api.source.SourceSection;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.pkl.core.ast.ExpressionNode;
|
||||
import org.pkl.core.ast.type.TypeNode;
|
||||
import org.pkl.core.ast.type.UnresolvedTypeNode;
|
||||
import org.pkl.core.runtime.BaseModule;
|
||||
import org.pkl.core.runtime.VmClass;
|
||||
import org.pkl.core.runtime.VmFunction;
|
||||
import org.pkl.core.runtime.VmLanguage;
|
||||
import org.pkl.core.runtime.VmNull;
|
||||
import org.pkl.core.runtime.VmUtils;
|
||||
|
||||
// IDEA: don't materialize frames when all members are constants
|
||||
@@ -57,8 +60,6 @@ public abstract class ObjectLiteralNode extends ExpressionNode {
|
||||
|
||||
protected abstract ExpressionNode getParentNode();
|
||||
|
||||
protected abstract Object executeWithParent(VirtualFrame frame, Object parent);
|
||||
|
||||
protected abstract ObjectLiteralNode copy(ExpressionNode newParentNode);
|
||||
|
||||
protected final AmendFunctionNode createAmendFunctionNode(VirtualFrame frame) {
|
||||
@@ -71,7 +72,14 @@ public abstract class ObjectLiteralNode extends ExpressionNode {
|
||||
|
||||
@Idempotent
|
||||
protected static boolean isTypedObjectClass(VmClass clazz) {
|
||||
return !(clazz.isListingClass() || clazz.isMappingClass() || clazz.isDynamicClass());
|
||||
if (clazz.isListingClass()
|
||||
|| clazz.isMappingClass()
|
||||
|| clazz.isDynamicClass()
|
||||
|| clazz.isFunctionClass()
|
||||
|| clazz.isFunctionNClass()) {
|
||||
return false;
|
||||
}
|
||||
return BaseModule.getTypedClass().isSuperclassOf(clazz);
|
||||
}
|
||||
|
||||
protected final boolean checkIsValidFunctionAmendment(VmFunction parent) {
|
||||
@@ -85,4 +93,25 @@ public abstract class ObjectLiteralNode extends ExpressionNode {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Idempotent
|
||||
protected final boolean checkIsValidFunctionAmendment(Object parent) {
|
||||
return checkIsValidFunctionAmendment((VmFunction) parent);
|
||||
}
|
||||
|
||||
@Idempotent
|
||||
protected final boolean isFunction(Object value) {
|
||||
return value instanceof VmFunction;
|
||||
}
|
||||
|
||||
protected final Object getNullDefaultValue(VmNull parent) {
|
||||
var value = parent.getDefaultValue();
|
||||
var count = 0;
|
||||
while (value instanceof VmNull n) {
|
||||
value = n.getDefaultValue();
|
||||
count++;
|
||||
}
|
||||
LoopNode.reportLoopCount(this, count);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
+76
-5
@@ -16,7 +16,9 @@
|
||||
package org.pkl.core.ast.expression.literal;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import com.oracle.truffle.api.dsl.Bind;
|
||||
import com.oracle.truffle.api.dsl.Cached;
|
||||
import com.oracle.truffle.api.dsl.ImportStatic;
|
||||
import com.oracle.truffle.api.dsl.Specialization;
|
||||
import com.oracle.truffle.api.frame.FrameDescriptor;
|
||||
import com.oracle.truffle.api.frame.VirtualFrame;
|
||||
@@ -31,6 +33,7 @@ import org.pkl.core.runtime.*;
|
||||
|
||||
/** Object literal that contains properties but not elements or entries. */
|
||||
// IDEA: don't materialize frame when all members are constants
|
||||
@ImportStatic({BaseModule.class, VmUtils.class})
|
||||
public abstract class PropertiesLiteralNode extends SpecializedObjectLiteralNode {
|
||||
public PropertiesLiteralNode(
|
||||
SourceSection sourceSection,
|
||||
@@ -82,26 +85,77 @@ public abstract class PropertiesLiteralNode extends SpecializedObjectLiteralNode
|
||||
return new VmTyped(frame.materialize(), parent, parent.getVmClass(), members);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {
|
||||
"parentClass == getClass(defaultValue)",
|
||||
"isTypedObjectClass(parentClass)",
|
||||
"checkIsValidTypedAmendment(parentClass)"
|
||||
})
|
||||
protected Object evalNullWithTypedObjectDefaultCached(
|
||||
VirtualFrame frame,
|
||||
VmNull parent,
|
||||
@Bind("getNullDefaultValue(parent)") Object defaultValue,
|
||||
@Cached("getClass(defaultValue)") VmClass parentClass) {
|
||||
var parentTyped = (VmTyped) defaultValue;
|
||||
return new VmTyped(frame.materialize(), parentTyped, parentTyped.getVmClass(), members);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {
|
||||
"isTypedObjectClass(getClass(defaultValue))",
|
||||
"checkIsValidTypedAmendment(defaultValue)"
|
||||
})
|
||||
protected Object evalNullWithTypedObjectDefaultUncached(
|
||||
VirtualFrame frame,
|
||||
VmNull parent,
|
||||
@Bind(value = "getNullDefaultValue(parent)") Object defaultValue) {
|
||||
var parentTyped = (VmTyped) defaultValue;
|
||||
return new VmTyped(frame.materialize(), parentTyped, parentTyped.getVmClass(), members);
|
||||
}
|
||||
|
||||
@Specialization
|
||||
protected Object evalDynamic(VirtualFrame frame, VmDynamic parent) {
|
||||
return new VmDynamic(frame.materialize(), parent, members, parent.getLength());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(guards = "getClass(defaultValue).isDynamicClass()")
|
||||
protected Object evalNullWithDynamicDefault(
|
||||
VirtualFrame frame, VmNull parent, @Bind("getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalDynamic(frame, (VmDynamic) defaultValue);
|
||||
}
|
||||
|
||||
@Specialization(guards = "checkIsValidListingAmendment()")
|
||||
protected Object evalListing(VirtualFrame frame, VmListing parent) {
|
||||
return new VmListing(frame.materialize(), parent, members, parent.getLength());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"getClass(defaultValue).isListingClass()", "checkIsValidListingAmendment()"})
|
||||
protected Object evalNullWithListingDefault(
|
||||
VirtualFrame frame,
|
||||
VmNull parent,
|
||||
@Bind(value = "getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalListing(frame, (VmListing) defaultValue);
|
||||
}
|
||||
|
||||
@ExplodeLoop
|
||||
@Specialization(guards = "checkIsValidMappingAmendment()")
|
||||
protected Object evalMapping(VirtualFrame frame, VmMapping parent) {
|
||||
return new VmMapping(frame.materialize(), parent, members);
|
||||
}
|
||||
|
||||
@Specialization
|
||||
protected Object evalNull(VirtualFrame frame, VmNull parent) {
|
||||
// assumes that Graal PE can handle recursive call to same node
|
||||
return executeWithParent(frame, parent.getDefaultValue());
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"getClass(defaultValue).isMappingClass()", "checkIsValidMappingAmendment()"})
|
||||
protected Object evalNullWithMappingDefault(
|
||||
VirtualFrame frame,
|
||||
VmNull parent,
|
||||
@Bind(value = "getNullDefaultValue(parent)") Object defaultValue) {
|
||||
return evalMapping(frame, (VmMapping) defaultValue);
|
||||
}
|
||||
|
||||
// Ultimately, this lambda or a lambda returned from it will call one of the other
|
||||
@@ -121,6 +175,18 @@ public abstract class PropertiesLiteralNode extends SpecializedObjectLiteralNode
|
||||
return amendFunctionNode.execute(frame, parent);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Specialization(
|
||||
guards = {"isFunction(defaultValue)", "checkIsValidFunctionAmendment(defaultValue)"})
|
||||
protected Object evalNullWithFunctionDefault(
|
||||
VirtualFrame frame,
|
||||
VmNull parent,
|
||||
@Bind("getNullDefaultValue(parent)") Object defaultValue,
|
||||
@Cached(value = "createAmendFunctionNode(frame)", neverDefault = true)
|
||||
AmendFunctionNode amendFunctionNode) {
|
||||
return evalFunction(frame, (VmFunction) defaultValue, amendFunctionNode);
|
||||
}
|
||||
|
||||
@Specialization(
|
||||
guards = {
|
||||
"parent == cachedParent",
|
||||
@@ -199,8 +265,13 @@ public abstract class PropertiesLiteralNode extends SpecializedObjectLiteralNode
|
||||
@Specialization
|
||||
@TruffleBoundary
|
||||
protected void fallback(Object parent) {
|
||||
var value = parent;
|
||||
// blame the non-null type (e.g. blame `Duration` instead of `Null` in the case of `Duration?`)
|
||||
if (value instanceof VmNull vmNull) {
|
||||
value = getNullDefaultValue(vmNull);
|
||||
}
|
||||
// should always throw
|
||||
checkIsValidTypedAmendment(parent);
|
||||
checkIsValidTypedAmendment(value);
|
||||
|
||||
throw exceptionBuilder().unreachableCode().build();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import static org.pkl.core.PClassInfo.pklBaseUri;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives;
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import com.oracle.truffle.api.dsl.Idempotent;
|
||||
|
||||
public final class BaseModule extends StdLibModule {
|
||||
static final VmTyped instance = VmUtils.createEmptyModule();
|
||||
@@ -91,6 +92,7 @@ public final class BaseModule extends StdLibModule {
|
||||
return SetClass.instance;
|
||||
}
|
||||
|
||||
@Idempotent
|
||||
public static VmClass getListingClass() {
|
||||
return ListingClass.instance;
|
||||
}
|
||||
@@ -99,10 +101,12 @@ public final class BaseModule extends StdLibModule {
|
||||
return MapClass.instance;
|
||||
}
|
||||
|
||||
@Idempotent
|
||||
public static VmClass getMappingClass() {
|
||||
return MappingClass.instance;
|
||||
}
|
||||
|
||||
@Idempotent
|
||||
public static VmClass getDynamicClass() {
|
||||
return DynamicClass.instance;
|
||||
}
|
||||
@@ -135,6 +139,7 @@ public final class BaseModule extends StdLibModule {
|
||||
return RegexMatchClass.instance;
|
||||
}
|
||||
|
||||
@Idempotent
|
||||
public static VmClass getFunctionClass() {
|
||||
return FunctionClass.instance;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import com.oracle.truffle.api.Truffle;
|
||||
import com.oracle.truffle.api.TruffleLanguage;
|
||||
import com.oracle.truffle.api.TruffleStackTrace;
|
||||
import com.oracle.truffle.api.dsl.Idempotent;
|
||||
import com.oracle.truffle.api.frame.*;
|
||||
import com.oracle.truffle.api.nodes.*;
|
||||
import com.oracle.truffle.api.source.Source;
|
||||
@@ -528,6 +529,7 @@ public final class VmUtils {
|
||||
}
|
||||
|
||||
// implements same behavior as AnyNodes#getClass
|
||||
@Idempotent
|
||||
public static VmClass getClass(Object value) {
|
||||
if (value instanceof VmValue vmValue) {
|
||||
return vmValue.getVmClass();
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
class Person { name: String }
|
||||
|
||||
hidden x: Mixin<Person>?
|
||||
|
||||
local y = (x) {
|
||||
name = "Fred"
|
||||
}
|
||||
|
||||
res = y.apply(new Person {})
|
||||
@@ -26,6 +26,25 @@ res2 = new Listing {
|
||||
new Mapping { ["one"] = 1; ["two"] = 2 }
|
||||
}
|
||||
|
||||
res2n = (Null(new Listing {})) {
|
||||
null
|
||||
true
|
||||
42
|
||||
1.23
|
||||
"foo"
|
||||
Regex("bar")
|
||||
5.gb
|
||||
3.min
|
||||
Pair(1, 2)
|
||||
List(1, 2, 3)
|
||||
Set(1, 2, 3)
|
||||
Map("one", 1, "two", 2)
|
||||
new Dynamic { name = "Pigeon"; age = 42 }
|
||||
new Person { name = "Pigeon"; age = 42 }
|
||||
new Listing { 1; 2; 3 }
|
||||
new Mapping { ["one"] = 1; ["two"] = 2 }
|
||||
}
|
||||
|
||||
res3 = new Listing {
|
||||
id(null)
|
||||
id(true)
|
||||
@@ -44,3 +63,22 @@ res3 = new Listing {
|
||||
id(new Listing { 1; 2; 3 })
|
||||
id(new Mapping { ["one"] = 1; ["two"] = 2 })
|
||||
}
|
||||
|
||||
res3n = (Null(new Listing {})) {
|
||||
id(null)
|
||||
id(true)
|
||||
id(42)
|
||||
id(1.23)
|
||||
id("foo")
|
||||
id(Regex("bar"))
|
||||
id(5.gb)
|
||||
id(3.min)
|
||||
id(Pair(1, 2))
|
||||
id(List(1, 2, 3))
|
||||
id(Set(1, 2, 3))
|
||||
id(Map("one", 1, "two", 2))
|
||||
id(new Dynamic { name = "Pigeon"; age = 42 })
|
||||
id(new Person { name = "Pigeon"; age = 42 })
|
||||
id(new Listing { 1; 2; 3 })
|
||||
id(new Mapping { ["one"] = 1; ["two"] = 2 })
|
||||
}
|
||||
|
||||
@@ -73,11 +73,20 @@ res8 = test.catch(() -> (x) { ["wrong type"] = "value" })
|
||||
res8a = test.catch(() -> (x) { [id("wrong type")] = id("value") })
|
||||
|
||||
res9 = test.catch(() -> new Listing { foo = "bar" })
|
||||
res9n = test.catch(() -> (Null(new Listing {})) { foo = "bar" })
|
||||
res10 = test.catch(() -> (x) { foo = "foo" })
|
||||
res10n = test.catch(() -> (Null(x)) { foo = "foo" })
|
||||
res11 = test.catch(() -> new Listing { "one"; foo = "foo" })
|
||||
res11n = test.catch(() -> (Null(new Listing { "one" })) { foo = "foo" })
|
||||
res12 = test.catch(() -> (x) { "four"; foo = "foo" })
|
||||
res12n = test.catch(() -> (Null(x)) { "four"; foo = "foo" })
|
||||
res12a = test.catch(() -> (x) { id("four"); foo = "foo" })
|
||||
res12an = test.catch(() -> (Null(x)) { id("four"); foo = "foo" })
|
||||
res13 = test.catch(() -> (x) { [1] = "updated two"; foo = "foo" })
|
||||
res13n = test.catch(() -> (Null(x)) { [1] = "updated two"; foo = "foo" })
|
||||
res13a = test.catch(() -> (x) { [id(1)] = id("updated two"); foo = "foo" })
|
||||
res13an = test.catch(() -> (Null(x)) { [id(1)] = id("updated two"); foo = "foo" })
|
||||
res14 = test.catch(() -> (x) { "four"; [1] = "updated two"; foo = "foo" })
|
||||
res14n = test.catch(() -> (Null(x)) { "four"; [1] = "updated two"; foo = "foo" })
|
||||
res14a = test.catch(() -> (x) { id("four"); [id(1)] = id("updated two"); foo = "foo" })
|
||||
res14an = test.catch(() -> (Null(x)) { id("four"); [id(1)] = id("updated two"); foo = "foo" })
|
||||
|
||||
@@ -26,6 +26,25 @@ res2 = new Mapping {
|
||||
[new Mapping { ["one"] = 1; ["two"] = 2 }] = new Mapping { ["one"] = 1; ["two"] = 2 }
|
||||
}
|
||||
|
||||
res2n = (Null(new Mapping {})) {
|
||||
[null] = null
|
||||
[true] = true
|
||||
[42] = 42
|
||||
[1.23] = 1.23
|
||||
["foo"] = "foo"
|
||||
[Regex("bar")] = Regex("bar")
|
||||
[5.gb] = 5.gb
|
||||
[3.min] = 3.min
|
||||
[Pair(1, 2)] = Pair(1, 2)
|
||||
[List(1, 2, 3)] = List(1, 2, 3)
|
||||
[Set(1, 2, 3)] = Set(1, 2, 3)
|
||||
[Map("one", 1, "two", 2)] = Map("one", 1, "two", 2)
|
||||
[new Dynamic { name = "Pigeon"; age = 42 }] = new Dynamic { name = "Pigeon"; age = 42 }
|
||||
[new Person { name = "Pigeon"; age = 42 }] = new Person { name = "Pigeon"; age = 42 }
|
||||
[new Listing { 1; 2; 3 }] = new Listing { 1; 2; 3 }
|
||||
[new Mapping { ["one"] = 1; ["two"] = 2 }] = new Mapping { ["one"] = 1; ["two"] = 2 }
|
||||
}
|
||||
|
||||
res3 = new Mapping {
|
||||
[id(null)] = id(null)
|
||||
[id(true)] = id(true)
|
||||
@@ -44,3 +63,34 @@ res3 = new Mapping {
|
||||
[id(new Listing { 1; 2; 3 })] = id(new Listing { 1; 2; 3 })
|
||||
[id(new Mapping { ["one"] = 1; ["two"] = 2 })] = id(new Mapping { ["one"] = 1; ["two"] = 2 })
|
||||
}
|
||||
|
||||
res3n = (Null(new Mapping {})) {
|
||||
[id(null)] = id(null)
|
||||
[id(true)] = id(true)
|
||||
[id(42)] = id(42)
|
||||
[id(1.23)] = id(1.23)
|
||||
[id("foo")] = id("foo")
|
||||
[id(Regex("bar"))] = id(Regex("bar"))
|
||||
[id(5.gb)] = id(5.gb)
|
||||
[id(3.min)] = id(3.min)
|
||||
[id(Pair(1, 2))] = id(Pair(1, 2))
|
||||
[id(List(1, 2, 3))] = id(List(1, 2, 3))
|
||||
[id(Set(1, 2, 3))] = id(Set(1, 2, 3))
|
||||
[id(Map("one", 1, "two", 2))] = id(Map("one", 1, "two", 2))
|
||||
[id(new Dynamic { name = "Pigeon"; age = 42 })] = id(new Dynamic { name = "Pigeon"; age = 42 })
|
||||
[id(new Person { name = "Pigeon"; age = 42 })] = id(new Person { name = "Pigeon"; age = 42 })
|
||||
[id(new Listing { 1; 2; 3 })] = id(new Listing { 1; 2; 3 })
|
||||
[id(new Mapping { ["one"] = 1; ["two"] = 2 })] = id(new Mapping { ["one"] = 1; ["two"] = 2 })
|
||||
}
|
||||
|
||||
// ConstantEntriesLiteralNode
|
||||
res4 = new Mapping {
|
||||
["foo"] = "1"
|
||||
["bar"] = "2"
|
||||
}
|
||||
|
||||
// ConstantEntriesLiteralNode
|
||||
res4n = (Null(new Mapping {})) {
|
||||
["foo"] = "1"
|
||||
["bar"] = "2"
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// Covers ConstantEntriesLiteralNode's specializations for amending a `VmNull` whose default value
|
||||
// is each of the underlying parent types it supports. All entry keys below are constants, so
|
||||
// these bodies compile to ConstantEntriesLiteralNode rather than EntriesLiteralNode.
|
||||
import "pkl:test"
|
||||
|
||||
// evalNullWithMappingDefault
|
||||
local mapping = Null(new Mapping { ["a"] = 1 })
|
||||
res1 = (mapping) {
|
||||
["b"] = 2
|
||||
}
|
||||
|
||||
// evalNullWithDynamicDefault
|
||||
local dynamic = Null(new Dynamic { a = 1 })
|
||||
res2 = (dynamic) {
|
||||
["b"] = 2
|
||||
}
|
||||
|
||||
// evalNullWithListingDefault
|
||||
local listing = Null(new Listing { "a"; "b" })
|
||||
res3 = (listing) {
|
||||
[0] = "updated a"
|
||||
}
|
||||
|
||||
// evalNullWithFunctionDefault
|
||||
local fun = Null(() -> new Mapping { ["a"] = 1 })
|
||||
local amendedFun = (fun) {
|
||||
["b"] = 2
|
||||
}
|
||||
res4 = amendedFun.apply()
|
||||
|
||||
// error: listing entry index out of range
|
||||
res5 = test.catch(() -> (Null(new Listing { "a" })) { [5] = "value" })
|
||||
|
||||
// error: listing cannot have a property (other than `default`)
|
||||
res6 = test.catch(() -> (Null(new Listing { "a" })) { [0] = "updated a"; foo = "bar" })
|
||||
|
||||
// error: mapping cannot have a property (other than `default`)
|
||||
res7 = test.catch(() -> (Null(new Mapping { ["a"] = 1 })) { ["b"] = 2; foo = "bar" })
|
||||
|
||||
// error: function amendment with wrong parameter count
|
||||
local twoArgFun = (x, y) -> new Mapping {}
|
||||
res8 = test.catch(() -> (Null(twoArgFun)) { x -> ["a"] = 1 })
|
||||
|
||||
hidden foo: "foo"?
|
||||
|
||||
// error: default value's type has no matching specialization; falls back to the generic error
|
||||
res9 = test.catch(() -> (foo) { ["a"] = 1 })
|
||||
@@ -0,0 +1,37 @@
|
||||
// Covers ElementsLiteralNode's specializations for amending a `VmNull` whose default value is
|
||||
// each of the underlying parent types it supports.
|
||||
import "pkl:test"
|
||||
|
||||
// evalNullWithDynamicDefault
|
||||
local dynamic = Null(new Dynamic { a = 1 })
|
||||
res1 = (dynamic) {
|
||||
"b"
|
||||
"c"
|
||||
}
|
||||
|
||||
// evalNullWithListingDefault
|
||||
local listing = Null(new Listing { "a" })
|
||||
res2 = (listing) {
|
||||
"b"
|
||||
"c"
|
||||
}
|
||||
|
||||
// evalNullWithFunctionDefault
|
||||
local fun = Null(() -> new Listing { "a" })
|
||||
local amendedFun = (fun) {
|
||||
"b"
|
||||
}
|
||||
res3 = amendedFun.apply()
|
||||
|
||||
// error: mapping cannot have an element
|
||||
res4 = test.catch(() -> (Null(new Mapping { ["a"] = 1 })) { "b" })
|
||||
|
||||
// error: listing cannot have a property (other than `default`)
|
||||
res5 = test.catch(() -> (Null(new Listing { "a" })) { "b"; foo = "bar" })
|
||||
|
||||
// error: function amendment with wrong parameter count
|
||||
local twoArgFun = (x, y) -> new Listing {}
|
||||
res6 = test.catch(() -> (Null(twoArgFun)) { x -> "b" })
|
||||
|
||||
// error: default value's type has no matching specialization; falls back to the generic error
|
||||
res7 = test.catch(() -> (Null(42)) { "b" })
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// Covers ElementsEntriesLiteralNode's specializations for amending a `VmNull` whose default value
|
||||
// is each of the underlying parent types it supports. Element and entry keys are mixed, and at
|
||||
// least one key is non-constant (via `id()`), matching this node's shape.
|
||||
import "pkl:test"
|
||||
|
||||
function id(x) = x
|
||||
|
||||
// evalNullWithDynamicDefault
|
||||
local dynamic = Null(new Dynamic { "a" })
|
||||
|
||||
res1 = (dynamic) {
|
||||
[id(0)] = "updated a"
|
||||
"b"
|
||||
}
|
||||
|
||||
// evalNullWithListingDefault
|
||||
local listing = Null(new Listing { "a"; "b" })
|
||||
res2 = (listing) {
|
||||
"c"
|
||||
[id(0)] = "updated a"
|
||||
}
|
||||
|
||||
// evalNullWithFunctionDefault
|
||||
local fun = Null(() -> new Listing { "a" })
|
||||
local amendedFun = (fun) {
|
||||
"b"
|
||||
[id(0)] = "updated a"
|
||||
}
|
||||
res3 = amendedFun.apply()
|
||||
|
||||
// error: mapping cannot have an element
|
||||
res4 = test.catch(() -> (Null(new Mapping { ["a"] = 1 })) { "b"; [id("c")] = 2 })
|
||||
|
||||
// error: listing entry index out of range
|
||||
res5 = test.catch(() -> (Null(new Listing { "a" })) { "b"; [id(5)] = "value" })
|
||||
|
||||
// error: listing cannot have a property (other than `default`)
|
||||
res6 = test.catch(() -> (Null(new Listing { "a" })) { "b"; [id(0)] = "updated a"; foo = "bar" })
|
||||
|
||||
// error: function amendment with wrong parameter count
|
||||
local twoArgFun = (x, y) -> new Listing {}
|
||||
res7 = test.catch(() -> (Null(twoArgFun)) { x -> "b"; [id(0)] = "updated a" })
|
||||
|
||||
// error: default value's type has no matching specialization; falls back to the generic error
|
||||
res8 = test.catch(() -> (Null(42)) { "b"; [id(0)] = "updated a" })
|
||||
@@ -0,0 +1,53 @@
|
||||
// Covers EntriesLiteralNode's specializations for amending a `VmNull` whose default value is
|
||||
// each of the underlying parent types it supports. At least one entry key below is non-constant
|
||||
// (via `id()`) so that these bodies compile to EntriesLiteralNode rather than
|
||||
// ConstantEntriesLiteralNode.
|
||||
import "pkl:test"
|
||||
|
||||
function id(x) = x
|
||||
|
||||
// evalNullableMapping
|
||||
local mapping = Null(new Mapping { ["a"] = 1 })
|
||||
res1 = (mapping) {
|
||||
[id("b")] = 2
|
||||
}
|
||||
|
||||
// evalNullWithDynamicDefault
|
||||
local dynamic = Null(new Dynamic { a = 1 })
|
||||
res2 = (dynamic) {
|
||||
[id("b")] = 2
|
||||
}
|
||||
|
||||
// evalNullWithListingDefault
|
||||
local listing = Null(new Listing { "a"; "b" })
|
||||
res3 = (listing) {
|
||||
[id(0)] = "updated a"
|
||||
}
|
||||
|
||||
// evalNullWithFunctionDefault
|
||||
local fun = Null(() -> new Mapping { ["a"] = 1 })
|
||||
local amendedFun = (fun) {
|
||||
[id("b")] = 2
|
||||
}
|
||||
res4 = amendedFun.apply()
|
||||
|
||||
// error: listing entry with wrong key type
|
||||
res5 = test.catch(() -> (Null(new Listing { "a" })) { [id("wrong type")] = "value" })
|
||||
|
||||
// error: listing entry index out of range
|
||||
res6 = test.catch(() -> (Null(new Listing { "a" })) { [id(5)] = "value" })
|
||||
|
||||
// error: mapping cannot have a property (other than `default`)
|
||||
res7 = test.catch(() -> (Null(new Mapping { ["a"] = 1 })) { foo = "bar"; [id("b")] = 2 })
|
||||
|
||||
// error: duplicate key
|
||||
res8 = test.catch(() -> (Null(new Mapping {})) { [id("a")] = 1; [id("a")] = 2 })
|
||||
|
||||
// error: function amendment with wrong parameter count
|
||||
local twoArgFun = (x, y) -> new Mapping {}
|
||||
res9 = test.catch(() -> (Null(twoArgFun)) { x -> [id("a")] = 1 })
|
||||
|
||||
hidden foo: "foo"?
|
||||
|
||||
// error: default value's type has no matching specialization; falls back to the generic error
|
||||
res10 = test.catch(() -> (foo) { [id("a")] = 1 })
|
||||
@@ -0,0 +1,68 @@
|
||||
// Covers GeneratorObjectLiteralNode's specializations for amending a `VmNull` whose default value
|
||||
// is each of the underlying parent types it supports. Each body contains a `for`- or
|
||||
// when-generator, which is what forces this node type instead of ElementsLiteralNode/
|
||||
// EntriesLiteralNode/PropertiesLiteralNode.
|
||||
import "pkl:test"
|
||||
|
||||
local class Person {
|
||||
name: String
|
||||
age: Int = 0
|
||||
}
|
||||
|
||||
// evalNullWithDynamicDefault
|
||||
local dynamic = Null(new Dynamic { a = 1 })
|
||||
res1 = (dynamic) {
|
||||
for (i in List(2, 3)) {
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
// evalNullWithTypedDefault
|
||||
local person = Null(new Person { name = "Pigeon" })
|
||||
res2 = (person) {
|
||||
when (true) {
|
||||
age = 10
|
||||
}
|
||||
}
|
||||
|
||||
// evalNullWithListingDefault
|
||||
local listing = Null(new Listing { "a" })
|
||||
res3 = (listing) {
|
||||
for (i in List(2, 3)) {
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
// evalNullableMapping
|
||||
local mapping = Null(new Mapping { ["a"] = 1 })
|
||||
res4 = (mapping) {
|
||||
for (i in List(2, 3)) {
|
||||
[i] = i
|
||||
}
|
||||
}
|
||||
|
||||
// evalNullWithFunctionDefault
|
||||
local fun = Null(() -> new Dynamic { a = 1 })
|
||||
local amendedFun = (fun) {
|
||||
for (i in List(2, 3)) {
|
||||
i
|
||||
}
|
||||
}
|
||||
res5 = amendedFun.apply()
|
||||
|
||||
// error: listing amendment cannot have parameters
|
||||
res6 = test.catch(() -> (Null(new Listing { "a" })) { x -> for (i in List(2)) { i } })
|
||||
|
||||
// error: mapping amendment cannot have parameters
|
||||
res7 = test.catch(() -> (Null(new Mapping { ["a"] = 1 })) { x -> for (i in List(2)) { [i] = i } })
|
||||
|
||||
// error: object amendment cannot have parameters
|
||||
res8 = test.catch(() -> (Null(new Dynamic { a = 1 })) { x -> for (i in List(2)) { i } })
|
||||
|
||||
hidden foo: "foo"?
|
||||
|
||||
// error: default value's type has no matching specialization; falls back to the generic error
|
||||
res9 = test.catch(() -> (foo) { for (i in List(2)) { i } })
|
||||
|
||||
hidden x: List<Int>?
|
||||
res10 = test.catch(() -> ((x) { when (true) { foo = 1 } }))
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
// Covers PropertiesLiteralNode's specializations for amending a `VmNull` whose default value is
|
||||
// each of the underlying parent types it supports.
|
||||
import "pkl:test"
|
||||
|
||||
local class Person {
|
||||
name: String
|
||||
age: Int = 0
|
||||
}
|
||||
|
||||
// evalNullWithTypedDefault
|
||||
local person = Null(new Person { name = "Pigeon" })
|
||||
res1 = (person) {
|
||||
age = 3
|
||||
}
|
||||
|
||||
// evalNullWithDynamicDefault
|
||||
local dynamic = Null(new Dynamic { name = "Pigeon" })
|
||||
res2 = (dynamic) {
|
||||
age = 3
|
||||
}
|
||||
|
||||
// evalNullWithListingDefault
|
||||
local listing = Null(new Listing { "one"; "two" })
|
||||
res3 = (listing) {
|
||||
default = (_) -> "three"
|
||||
}.getOrDefault(5)
|
||||
|
||||
// evalNullableMapping
|
||||
local mapping = Null(new Mapping { ["one"] = 1 })
|
||||
res4 = (mapping) {
|
||||
default = (_) -> 0
|
||||
}.getOrDefault("missing")
|
||||
|
||||
// evalNullWithFunctionDefault
|
||||
local fun = Null(() -> new Dynamic { zero = 0 })
|
||||
local amendedFun = (fun) {
|
||||
amended = "amended"
|
||||
}
|
||||
res5 = amendedFun.apply()
|
||||
|
||||
// error: typed object amendment with an unknown property
|
||||
res6 = test.catch(() -> (Null(new Person { name = "Pigeon" })) { nickname = "Pidge" })
|
||||
|
||||
// error: listing cannot have a property (other than `default`)
|
||||
res7 = test.catch(() -> (Null(new Listing { "one" })) { foo = "bar" })
|
||||
|
||||
// error: mapping cannot have a property (other than `default`)
|
||||
res8 = test.catch(() -> (Null(new Mapping { ["one"] = 1 })) { foo = "bar" })
|
||||
|
||||
// error: function amendment with wrong parameter count
|
||||
local twoArgFun = (x, y) -> new Dynamic {}
|
||||
res9 = test.catch(() -> (Null(twoArgFun)) { x -> prop = x })
|
||||
|
||||
hidden foo: "foo"?
|
||||
|
||||
// error: default value's type has no matching specialization; falls back to the generic error
|
||||
res10 = test.catch(() -> (foo) { foo = "bar" })
|
||||
|
||||
hidden clazz: Class?
|
||||
|
||||
res11 = (clazz) {
|
||||
x = 1
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
res {
|
||||
name = "Fred"
|
||||
}
|
||||
@@ -30,6 +30,37 @@ res2 {
|
||||
["two"] = 2
|
||||
}
|
||||
}
|
||||
res2n {
|
||||
null
|
||||
true
|
||||
42
|
||||
1.23
|
||||
"foo"
|
||||
Regex("bar")
|
||||
5.gb
|
||||
3.min
|
||||
Pair(1, 2)
|
||||
List(1, 2, 3)
|
||||
Set(1, 2, 3)
|
||||
Map("one", 1, "two", 2)
|
||||
new {
|
||||
name = "Pigeon"
|
||||
age = 42
|
||||
}
|
||||
new {
|
||||
name = "Pigeon"
|
||||
age = 42
|
||||
}
|
||||
new {
|
||||
1
|
||||
2
|
||||
3
|
||||
}
|
||||
new {
|
||||
["one"] = 1
|
||||
["two"] = 2
|
||||
}
|
||||
}
|
||||
res3 {
|
||||
null
|
||||
true
|
||||
@@ -61,3 +92,34 @@ res3 {
|
||||
["two"] = 2
|
||||
}
|
||||
}
|
||||
res3n {
|
||||
null
|
||||
true
|
||||
42
|
||||
1.23
|
||||
"foo"
|
||||
Regex("bar")
|
||||
5.gb
|
||||
3.min
|
||||
Pair(1, 2)
|
||||
List(1, 2, 3)
|
||||
Set(1, 2, 3)
|
||||
Map("one", 1, "two", 2)
|
||||
new {
|
||||
name = "Pigeon"
|
||||
age = 42
|
||||
}
|
||||
new {
|
||||
name = "Pigeon"
|
||||
age = 42
|
||||
}
|
||||
new {
|
||||
1
|
||||
2
|
||||
3
|
||||
}
|
||||
new {
|
||||
["one"] = 1
|
||||
["two"] = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +55,20 @@ res7a = "Element index `-1` is out of range `0`..`2`."
|
||||
res8 = "Expected key of type `Int`, but got type `String`."
|
||||
res8a = "Expected key of type `Int`, but got type `String`."
|
||||
res9 = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res9n = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res10 = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res10n = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res11 = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res11n = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res12 = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res12n = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res12a = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res12an = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res13 = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res13n = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res13a = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res13an = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res14 = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res14n = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res14a = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res14an = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
|
||||
@@ -43,6 +43,50 @@ res2 {
|
||||
["two"] = 2
|
||||
}
|
||||
}
|
||||
res2n {
|
||||
[null] = null
|
||||
[true] = true
|
||||
[42] = 42
|
||||
[1.23] = 1.23
|
||||
["foo"] = "foo"
|
||||
[Regex("bar")] = Regex("bar")
|
||||
[5.gb] = 5.gb
|
||||
[3.min] = 3.min
|
||||
[Pair(1, 2)] = Pair(1, 2)
|
||||
[List(1, 2, 3)] = List(1, 2, 3)
|
||||
[Set(1, 2, 3)] = Set(1, 2, 3)
|
||||
[Map("one", 1, "two", 2)] = Map("one", 1, "two", 2)
|
||||
[new {
|
||||
name = "Pigeon"
|
||||
age = 42
|
||||
}] {
|
||||
name = "Pigeon"
|
||||
age = 42
|
||||
}
|
||||
[new {
|
||||
name = "Pigeon"
|
||||
age = 42
|
||||
}] {
|
||||
name = "Pigeon"
|
||||
age = 42
|
||||
}
|
||||
[new {
|
||||
1
|
||||
2
|
||||
3
|
||||
}] {
|
||||
1
|
||||
2
|
||||
3
|
||||
}
|
||||
[new {
|
||||
["one"] = 1
|
||||
["two"] = 2
|
||||
}] {
|
||||
["one"] = 1
|
||||
["two"] = 2
|
||||
}
|
||||
}
|
||||
res3 {
|
||||
[null] = null
|
||||
[true] = true
|
||||
@@ -87,3 +131,55 @@ res3 {
|
||||
["two"] = 2
|
||||
}
|
||||
}
|
||||
res3n {
|
||||
[null] = null
|
||||
[true] = true
|
||||
[42] = 42
|
||||
[1.23] = 1.23
|
||||
["foo"] = "foo"
|
||||
[Regex("bar")] = Regex("bar")
|
||||
[5.gb] = 5.gb
|
||||
[3.min] = 3.min
|
||||
[Pair(1, 2)] = Pair(1, 2)
|
||||
[List(1, 2, 3)] = List(1, 2, 3)
|
||||
[Set(1, 2, 3)] = Set(1, 2, 3)
|
||||
[Map("one", 1, "two", 2)] = Map("one", 1, "two", 2)
|
||||
[new {
|
||||
name = "Pigeon"
|
||||
age = 42
|
||||
}] {
|
||||
name = "Pigeon"
|
||||
age = 42
|
||||
}
|
||||
[new {
|
||||
name = "Pigeon"
|
||||
age = 42
|
||||
}] {
|
||||
name = "Pigeon"
|
||||
age = 42
|
||||
}
|
||||
[new {
|
||||
1
|
||||
2
|
||||
3
|
||||
}] {
|
||||
1
|
||||
2
|
||||
3
|
||||
}
|
||||
[new {
|
||||
["one"] = 1
|
||||
["two"] = 2
|
||||
}] {
|
||||
["one"] = 1
|
||||
["two"] = 2
|
||||
}
|
||||
}
|
||||
res4 {
|
||||
["foo"] = "1"
|
||||
["bar"] = "2"
|
||||
}
|
||||
res4n {
|
||||
["foo"] = "1"
|
||||
["bar"] = "2"
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
res1 {
|
||||
["a"] = 1
|
||||
["b"] = 2
|
||||
}
|
||||
res2 {
|
||||
a = 1
|
||||
["b"] = 2
|
||||
}
|
||||
res3 {
|
||||
"updated a"
|
||||
"b"
|
||||
}
|
||||
res4 {
|
||||
["a"] = 1
|
||||
["b"] = 2
|
||||
}
|
||||
res5 = "Element index `5` is out of range `0`..`0`."
|
||||
res6 = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res7 = "Object of type `Mapping` cannot have a property (other than `default`)."
|
||||
res8 = "Expected a function with 1 parameters, but got 2."
|
||||
res9 = "Cannot instantiate, or amend an instance of, external class `String`."
|
||||
@@ -0,0 +1,18 @@
|
||||
res1 {
|
||||
a = 1
|
||||
"b"
|
||||
"c"
|
||||
}
|
||||
res2 {
|
||||
"a"
|
||||
"b"
|
||||
"c"
|
||||
}
|
||||
res3 {
|
||||
"a"
|
||||
"b"
|
||||
}
|
||||
res4 = "Object of type `Mapping` cannot have an element."
|
||||
res5 = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res6 = "Expected a function with 1 parameters, but got 2."
|
||||
res7 = "Expected value of type `Object | Function<Object>`, but got type `Int`. Value: 42"
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
res1 {
|
||||
"updated a"
|
||||
"b"
|
||||
}
|
||||
res2 {
|
||||
"updated a"
|
||||
"b"
|
||||
"c"
|
||||
}
|
||||
res3 {
|
||||
"updated a"
|
||||
"b"
|
||||
}
|
||||
res4 = "Object of type `Mapping` cannot have an element."
|
||||
res5 = "Element index `5` is out of range `0`..`0`."
|
||||
res6 = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res7 = "Expected a function with 1 parameters, but got 2."
|
||||
res8 = "Expected value of type `Object | Function<Object>`, but got type `Int`. Value: 42"
|
||||
@@ -0,0 +1,22 @@
|
||||
res1 {
|
||||
["a"] = 1
|
||||
["b"] = 2
|
||||
}
|
||||
res2 {
|
||||
a = 1
|
||||
["b"] = 2
|
||||
}
|
||||
res3 {
|
||||
"updated a"
|
||||
"b"
|
||||
}
|
||||
res4 {
|
||||
["a"] = 1
|
||||
["b"] = 2
|
||||
}
|
||||
res5 = "Expected key of type `Int`, but got type `String`."
|
||||
res6 = "Element index `5` is out of range `0`..`0`."
|
||||
res7 = "Object of type `Mapping` cannot have a property (other than `default`)."
|
||||
res8 = "Duplicate definition of member `\"a\"`."
|
||||
res9 = "Expected a function with 1 parameters, but got 2."
|
||||
res10 = "Cannot instantiate, or amend an instance of, external class `String`."
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
res1 {
|
||||
a = 1
|
||||
2
|
||||
3
|
||||
}
|
||||
res2 {
|
||||
name = "Pigeon"
|
||||
age = 10
|
||||
}
|
||||
res3 {
|
||||
"a"
|
||||
2
|
||||
3
|
||||
}
|
||||
res4 {
|
||||
["a"] = 1
|
||||
[2] = 2
|
||||
[3] = 3
|
||||
}
|
||||
res5 {
|
||||
a = 1
|
||||
2
|
||||
3
|
||||
}
|
||||
res6 = "Expected a function that returns a `Listing`, but got a `Listing`."
|
||||
res7 = "Expected a function that returns a `Mapping`, but got a `Mapping`."
|
||||
res8 = "Expected a function that returns an object, but got an object."
|
||||
res9 = "Cannot instantiate, or amend an instance of, external class `String`."
|
||||
res10 = "Cannot instantiate, or amend an instance of, external class `List`."
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
res1 {
|
||||
name = "Pigeon"
|
||||
age = 3
|
||||
}
|
||||
res2 {
|
||||
name = "Pigeon"
|
||||
age = 3
|
||||
}
|
||||
res3 = "three"
|
||||
res4 = 0
|
||||
res5 {
|
||||
zero = 0
|
||||
amended = "amended"
|
||||
}
|
||||
res6 = "Cannot find property `nickname` in object of type `properties#Person`."
|
||||
res7 = "Object of type `Listing` cannot have a property (other than `default`)."
|
||||
res8 = "Object of type `Mapping` cannot have a property (other than `default`)."
|
||||
res9 = "Expected a function with 1 parameters, but got 2."
|
||||
res10 = "Cannot instantiate, or amend an instance of, external class `String`."
|
||||
res11 {
|
||||
x = 1
|
||||
}
|
||||
Reference in New Issue
Block a user