/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 *
 * Copyright 2016 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef wasm_pi_h
#define wasm_pi_h

#include "mozilla/DoublyLinkedList.h"  // for DoublyLinkedListElement

#include "js/TypeDecls.h"
#include "wasm/WasmAnyRef.h"
#include "wasm/WasmTypeDef.h"

// [SMDOC] JS Promise Integration
//
// The API provides relatively efficient and relatively ergonomic interop
// between JavaScript promises and WebAssembly but works under the constraint
// that the only changes are to the JS API and not to the core wasm.
//
// Secondary (suspendable) stacks are introduced at the entrance into the wasm
// code -- a promising function. A suspendable stack can contain/store only
// wasm frames and be part of one activation. If there is a need to execute
// a JS script, the stack must be switched back (to the main stack).
//
// There is a special exit from the suspendable stack where it is expected to
// receive a promise from a JS script -- a suspending function/import. If wasm
// code calls such import, the suspendable stack will be unlinked from
// the current activation allowing the main stack to continue returning to
// the event loop.
//
// Here is a small example that uses JS Promise Integration API:
//
//     const suspending = new WebAssembly.Suspending(async () => 42)
//     const ins = wasmTextEval(`(module
//         (import "" "suspending" (func $imp (result i32)))
//         (func (export "entry") (result i32) (call $imp))
//     )`, {"": { suspending, }})
//     const promising = WebAssembly.promising(ins.exports.entry)
//     assertEq(await promising(), 42)
//
// The states transitions can be described by the following diagram:
//
//               Invoke
//               Promising                Promise
//     +-------+ Export     +----------+  Resolved  +---------+
//     |Initial+----------->|Wasm Logic|<-----------+Suspended|
//     +-------+            ++-+------++            +---------+
//                           | | ^    |Invoke            ^ Suspending Function
//                Return from| | |    |Suspending        | Returns a Promise
//     +--------+ Wasm Call  | | |    |Import       +----+---+
//     |Finished|<-----------+ | |    +------------>|JS Logic|
//     +--------+              | |                  +----+---+
//                +------------+ |                       | Re-entry
//                |Invoke Other  |Return                 +------>
//                |Import     +--+-----+
//                +---------->|JS Logic|
//                            +--------+
//
// The Invoke Promising Export transition creates a suspendable stack,
// switches to it, and continues execution of wasm code there. When the callee
// frame is popped, the promise is returned to the JS caller.
//
// The Invoke Suspending Import switches stack to the main one and sets
// the suspended stack aside. It is expected that the suspending promise
// is returned by JS. The callee returns to the moment the promising call was
// instantiated.
//
// The Return from Wasm Call transition destroys the suspendable stack,
// continues execution on the main stack, and resolves the promising promise
// with the results of the call.
//
// The Promise Resolve transition is invoked when the suspending promise is
// resolved, which wakes the suspendable stack and extends the main one.
// The execution will continue on the suspendable stack that returns the
// resolution values to the wasm code as return values.
//
// The Invoke Other Import transition temporary switches to the main stack and
// invokes the JS code. The suspendable stack will not be removed from the
// chain of frames.
//
// Notice that calling wasm and then invoking a suspendable import from
// the main stack is not allowed. For example, re-exporting $imp, from
// the small example above, and calling it directly from the JS main thread
// will fail.
//
// The `new WebAssembly.Suspending(fn)` logic is implemented in a Wasm module
// generated by `SuspendingFunctionModuleFactory` utility (see WasmPI.cpp).
// The `WebAssembly.promising(wasmfn)` logic is implemented in a Wasm module
// generated by `PromisingFunctionModuleFactory` utility.

namespace js {

class PromiseObject;
class WasmStructObject;

namespace wasm {

class SuspenderContext;
class SuspenderObject;

static const uint32_t SuspenderObjectDataSlot = 0;

enum SuspenderState {
  Initial,
  Moribund,
  Active,
  Suspended,
};

class SuspenderObjectData
    : public mozilla::DoublyLinkedListElement<SuspenderObjectData> {
  void* stackMemory_;

  // Stored main stack FP register.
  void* mainFP_;

  // Stored main stack SP register.
  void* mainSP_;

  // Stored suspendable stack FP register.
  void* suspendableFP_;

  // Stored suspendable stack SP register.
  void* suspendableSP_;

  // Stored suspendable stack exit/bottom frame pointer.
  void* suspendableExitFP_;

  // Stored return address for return to suspendable stack.
  void* suspendedReturnAddress_;

  // Stored main stack exit/top frame pointer.
  void* mainExitFP_;

  SuspenderState state_;

  // Identify context that is holding suspended stack, otherwise nullptr.
  SuspenderContext* suspendedBy_;

#if defined(_WIN32)
  // The storage of main stack limits during stack switching.
  // See updateTibFields and restoreTibFields below.
  void* savedStackBase_;
  void* savedStackLimit_;
#endif

 public:
  explicit SuspenderObjectData(void* stackMemory);

  inline SuspenderState state() const { return state_; }
  void setState(SuspenderState state) { state_ = state; }

  inline bool traceable() const { return suspendedBy_ != nullptr; }
  inline SuspenderContext* suspendedBy() const { return suspendedBy_; }
  void setSuspendedBy(SuspenderContext* suspendedBy) {
    suspendedBy_ = suspendedBy;
  }

  inline void* stackMemory() const { return stackMemory_; }
  inline void* mainFP() const { return mainFP_; }
  inline void* mainSP() const { return mainSP_; }
  inline void* mainExitFP() const { return mainExitFP_; }
  inline void* suspendableFP() const { return suspendableFP_; }
  inline void* suspendableSP() const { return suspendableSP_; }
  inline void* suspendableExitFP() const { return suspendableExitFP_; }
  inline void* suspendedReturnAddress() const {
    return suspendedReturnAddress_;
  }

  void releaseStackMemory();

#if defined(_WIN32)
  void updateTIBStackFields();
  void restoreTIBStackFields();
#endif

#if defined(JS_SIMULATOR_ARM64) || defined(JS_SIMULATOR_ARM)
  void switchSimulatorToMain();
  void switchSimulatorToSuspendable();
#endif

  static constexpr size_t offsetOfMainFP() {
    return offsetof(SuspenderObjectData, mainFP_);
  }

  static constexpr size_t offsetOfMainSP() {
    return offsetof(SuspenderObjectData, mainSP_);
  }

  static constexpr size_t offsetOfSuspendableFP() {
    return offsetof(SuspenderObjectData, suspendableFP_);
  }

  static constexpr size_t offsetOfSuspendableSP() {
    return offsetof(SuspenderObjectData, suspendableSP_);
  }

  static constexpr size_t offsetOfSuspendableExitFP() {
    return offsetof(SuspenderObjectData, suspendableExitFP_);
  }

  static constexpr size_t offsetOfMainExitFP() {
    return offsetof(SuspenderObjectData, mainExitFP_);
  }

  static constexpr size_t offsetOfSuspendedReturnAddress() {
    return offsetof(SuspenderObjectData, suspendedReturnAddress_);
  }
};

#ifdef ENABLE_WASM_JSPI

using CallOnMainStackFn = bool (*)(void* data);
bool CallOnMainStack(JSContext* cx, CallOnMainStackFn fn, void* data);

JSFunction* WasmSuspendingFunctionCreate(JSContext* cx, HandleObject func,
                                         wasm::ValTypeVector&& params,
                                         wasm::ValTypeVector&& results);

JSFunction* WasmSuspendingFunctionCreate(JSContext* cx, HandleObject func,
                                         const FuncType& type);

JSFunction* WasmPromisingFunctionCreate(JSContext* cx, HandleObject func,
                                        wasm::ValTypeVector&& params,
                                        wasm::ValTypeVector&& results);

SuspenderObject* CurrentSuspender(Instance* instance, int reserved);

SuspenderObject* CreateSuspender(Instance* instance, int reserved);

PromiseObject* CreatePromisingPromise(Instance* instance,
                                      SuspenderObject* suspender);

JSObject* GetSuspendingPromiseResult(Instance* instance, void* result,
                                     SuspenderObject* suspender);

void* AddPromiseReactions(Instance* instance, SuspenderObject* suspender,
                          void* result, JSFunction* continueOnSuspendable);

void* ForwardExceptionToSuspended(Instance* instance,
                                  SuspenderObject* suspender, void* exception);

int32_t SetPromisingPromiseResults(Instance* instance,
                                   SuspenderObject* suspender,
                                   WasmStructObject* results);

void UpdateSuspenderState(Instance* instance, SuspenderObject* suspender,
                          UpdateSuspenderStateAction action);

#endif  // ENABLE_WASM_JSPI

}  // namespace wasm
}  // namespace js

#endif  // wasm_pi_h
