package com.oracle.truffle.js.builtins;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.dsl.ImportStatic;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.object.DynamicObject;
import com.oracle.truffle.api.object.HiddenKey;
import com.oracle.truffle.js.builtins.ArrayPrototypeBuiltins.ArraySpeciesConstructorNode;
import com.oracle.truffle.js.builtins.PromisePrototypeBuiltinsFactory.CatchNodeGen;
import com.oracle.truffle.js.builtins.PromisePrototypeBuiltinsFactory.FinallyNodeGen;
import com.oracle.truffle.js.builtins.PromisePrototypeBuiltinsFactory.ThenNodeGen;
import com.oracle.truffle.js.nodes.JavaScriptNode;
import com.oracle.truffle.js.nodes.access.PropertyGetNode;
import com.oracle.truffle.js.nodes.access.PropertyNode;
import com.oracle.truffle.js.nodes.access.PropertySetNode;
import com.oracle.truffle.js.nodes.arguments.AccessFunctionNode;
import com.oracle.truffle.js.nodes.arguments.AccessIndexedArgumentNode;
import com.oracle.truffle.js.nodes.control.ThrowNode;
import com.oracle.truffle.js.nodes.function.JSBuiltin;
import com.oracle.truffle.js.nodes.function.JSBuiltinNode;
import com.oracle.truffle.js.nodes.function.JSFunctionCallNode;
import com.oracle.truffle.js.nodes.promise.NewPromiseCapabilityNode;
import com.oracle.truffle.js.nodes.promise.PerformPromiseThenNode;
import com.oracle.truffle.js.nodes.promise.PromiseResolveNode;
import com.oracle.truffle.js.nodes.unary.IsCallableNode;
import com.oracle.truffle.js.runtime.Errors;
import com.oracle.truffle.js.runtime.JSArguments;
import com.oracle.truffle.js.runtime.JSContext;
import com.oracle.truffle.js.runtime.JSFrameUtil;
import com.oracle.truffle.js.runtime.JSRuntime;
import com.oracle.truffle.js.runtime.JavaScriptRootNode;
import com.oracle.truffle.js.runtime.builtins.BuiltinEnum;
import com.oracle.truffle.js.runtime.builtins.JSFunction;
import com.oracle.truffle.js.runtime.builtins.JSFunctionData;
import com.oracle.truffle.js.runtime.builtins.JSPromise;
import com.oracle.truffle.js.runtime.objects.PromiseCapabilityRecord;
import com.oracle.truffle.js.runtime.objects.Undefined;
public final class PromisePrototypeBuiltins extends JSBuiltinsContainer.SwitchEnum<PromisePrototypeBuiltins.PromisePrototype> {
public static final JSBuiltinsContainer BUILTINS = new PromisePrototypeBuiltins();
protected PromisePrototypeBuiltins() {
super(JSPromise.PROTOTYPE_NAME, PromisePrototype.class);
}
public enum PromisePrototype implements BuiltinEnum<PromisePrototype> {
then(2),
catch_(1),
finally_(1);
private final int length;
PromisePrototype(int length) {
this.length = length;
}
@Override
public int getLength() {
return length;
}
}
@Override
protected Object createNode(JSContext context, JSBuiltin builtin, boolean construct, boolean newTarget, PromisePrototype builtinEnum) {
switch (builtinEnum) {
case then:
return ThenNodeGen.create(context, builtin, args().withThis().fixedArgs(2).createArgumentNodes(context));
case catch_:
return CatchNodeGen.create(context, builtin, args().withThis().fixedArgs(1).createArgumentNodes(context));
case finally_:
return FinallyNodeGen.create(context, builtin, args().withThis().fixedArgs(1).createArgumentNodes(context));
}
return null;
}
@ImportStatic({JSPromise.class})
public abstract static class PromiseMethodNode extends JSBuiltinNode {
@Child private ArraySpeciesConstructorNode speciesConstructorNode;
protected PromiseMethodNode(JSContext context, JSBuiltin builtin) {
super(context, builtin);
this.speciesConstructorNode = ArraySpeciesConstructorNode.create(context, false);
}
protected final DynamicObject speciesConstructor(DynamicObject promise) {
return speciesConstructorNode.speciesConstructor(promise, getContext().getRealm().getPromiseConstructor());
}
}
public abstract static class ThenNode extends PromiseMethodNode {
@Child private NewPromiseCapabilityNode newPromiseCapability;
@Child private PerformPromiseThenNode performPromiseThen;
protected ThenNode(JSContext context, JSBuiltin builtin) {
super(context, builtin);
this.newPromiseCapability = NewPromiseCapabilityNode.create(context);
this.performPromiseThen = PerformPromiseThenNode.create(context);
}
@Specialization(guards = "isJSPromise(promise)")
protected DynamicObject doPromise(DynamicObject promise, Object onFulfilled, Object onRejected) {
DynamicObject constructor = speciesConstructor(promise);
getContext().notifyPromiseHook(-1 , promise);
PromiseCapabilityRecord resultCapability = newPromiseCapability.execute(constructor);
return performPromiseThen.execute(promise, onFulfilled, onRejected, resultCapability);
}
@SuppressWarnings("unused")
@Specialization(guards = "!isJSPromise(thisObj)")
protected DynamicObject doNotPromise(Object thisObj, Object onFulfilled, Object onRejected) {
throw Errors.createTypeErrorIncompatibleReceiver(thisObj);
}
}
public abstract static class CatchNode extends JSBuiltinNode {
@Child private PropertyGetNode getThen;
@Child private JSFunctionCallNode callThen;
protected CatchNode(JSContext context, JSBuiltin builtin) {
super(context, builtin);
this.getThen = PropertyGetNode.create(JSPromise.THEN, false, context);
this.callThen = JSFunctionCallNode.createCall();
}
@Specialization
protected Object doObject(Object promise, Object onRejected) {
return callThen.executeCall(JSArguments.create(promise, getThen.getValue(promise), Undefined.instance, onRejected));
}
}
public abstract static class FinallyNode extends PromiseMethodNode {
@Child private IsCallableNode isCallable = IsCallableNode.create();
@Child private PropertyGetNode getThen;
@Child private JSFunctionCallNode callThen;
@Child private PropertySetNode setConstructor;
@Child private PropertySetNode setOnFinally;
static final HiddenKey VALUE_KEY = new HiddenKey("Value");
protected FinallyNode(JSContext context, JSBuiltin builtin) {
super(context, builtin);
this.getThen = PropertyGetNode.create(JSPromise.THEN, false, context);
this.callThen = JSFunctionCallNode.createCall();
}
@Specialization(guards = "isJSObject(promise)")
protected Object doObject(DynamicObject promise, Object onFinally) {
DynamicObject constructor = speciesConstructor(promise);
assert JSRuntime.isConstructor(constructor);
Object thenFinally;
Object catchFinally;
if (!isCallable.executeBoolean(onFinally)) {
thenFinally = onFinally;
catchFinally = onFinally;
} else {
thenFinally = createFinallyFunction(constructor, onFinally, true);
catchFinally = createFinallyFunction(constructor, onFinally, false);
}
return callThen.executeCall(JSArguments.create(promise, getThen.getValue(promise), thenFinally, catchFinally));
}
@SuppressWarnings("unused")
@Specialization(guards = "!isJSObject(thisObj)")
protected DynamicObject doNotObject(Object thisObj, Object onFinally) {
throw Errors.createTypeErrorIncompatibleReceiver(thisObj);
}
private DynamicObject createFinallyFunction(DynamicObject constructor, Object onFinally, boolean thenFinally) {
if (setConstructor == null || setOnFinally == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
this.setConstructor = insert(PropertySetNode.createSetHidden(JSPromise.PROMISE_FINALLY_CONSTRUCTOR, getContext()));
this.setOnFinally = insert(PropertySetNode.createSetHidden(JSPromise.PROMISE_ON_FINALLY, getContext()));
}
JSFunctionData functionData;
if (thenFinally) {
functionData = getContext().getOrCreateBuiltinFunctionData(JSContext.BuiltinFunctionKey.PromiseThenFinally, (c) -> createPromiseFinallyFunction(c, true));
} else {
functionData = getContext().getOrCreateBuiltinFunctionData(JSContext.BuiltinFunctionKey.PromiseCatchFinally, (c) -> createPromiseFinallyFunction(c, false));
}
DynamicObject function = JSFunction.create(getContext().getRealm(), functionData);
setConstructor.setValue(function, constructor);
setOnFinally.setValue(function, onFinally);
return function;
}
private static JSFunctionData createPromiseFinallyFunction(JSContext context, boolean thenFinally) {
class PromiseFinallyRootNode extends JavaScriptRootNode {
@Child private JavaScriptNode valueNode = AccessIndexedArgumentNode.create(0);
@Child private PropertyGetNode getConstructor = PropertyGetNode.createGetHidden(JSPromise.PROMISE_FINALLY_CONSTRUCTOR, context);
@Child private PropertyGetNode getOnFinally = PropertyGetNode.createGetHidden(JSPromise.PROMISE_ON_FINALLY, context);
@Child private PromiseResolveNode promiseResolve = PromiseResolveNode.create(context);
@Child private JSFunctionCallNode callFinally = JSFunctionCallNode.createCall();
@Child private PropertyGetNode getThen = PropertyGetNode.create(JSPromise.THEN, false, context);
@Child private JSFunctionCallNode callThen = JSFunctionCallNode.createCall();
@Child private PropertySetNode setValue = PropertySetNode.createSetHidden(VALUE_KEY, context);
@Override
public Object execute(VirtualFrame frame) {
DynamicObject functionObject = JSFrameUtil.getFunctionObject(frame);
DynamicObject onFinally = (DynamicObject) getOnFinally.getValue(functionObject);
assert JSRuntime.isCallable(onFinally);
Object result = callFinally.executeCall(JSArguments.createZeroArg(Undefined.instance, onFinally));
DynamicObject constructor = (DynamicObject) getConstructor.getValue(functionObject);
assert JSRuntime.isConstructor(constructor);
DynamicObject promise = promiseResolve.execute(constructor, result);
Object value = valueNode.execute(frame);
Object thunk = createHandlerFunction(value);
return callThen.executeCall(JSArguments.create(promise, getThen.getValue(promise), thunk));
}
private Object createHandlerFunction(Object value) {
JSFunctionData functionData;
if (thenFinally) {
functionData = context.getOrCreateBuiltinFunctionData(JSContext.BuiltinFunctionKey.PromiseValueThunk, (c) -> createValueThunk(c));
} else {
functionData = context.getOrCreateBuiltinFunctionData(JSContext.BuiltinFunctionKey.PromiseThrower, (c) -> createThrower(c));
}
DynamicObject function = JSFunction.create(context.getRealm(), functionData);
setValue.setValue(function, value);
return function;
}
}
CallTarget callTarget = Truffle.getRuntime().createCallTarget(new PromiseFinallyRootNode());
return JSFunctionData.createCallOnly(context, callTarget, 1, "");
}
static JSFunctionData createThrower(JSContext context) {
return createThunkImpl(context, ThrowNode.create(PropertyNode.createGetHidden(context, AccessFunctionNode.create(), VALUE_KEY), context));
}
static JSFunctionData createValueThunk(JSContext context) {
return createThunkImpl(context, PropertyNode.createGetHidden(context, AccessFunctionNode.create(), VALUE_KEY));
}
private static JSFunctionData createThunkImpl(JSContext context, JavaScriptNode expression) {
CallTarget callTarget = Truffle.getRuntime().createCallTarget(new JavaScriptRootNode() {
@Child private JavaScriptNode body = expression;
@Override
public Object execute(VirtualFrame frame) {
return body.execute(frame);
}
});
return JSFunctionData.createCallOnly(context, callTarget, 0, "");
}
}
}