// Tests for throwing exceptions to JS from Wasm.

function assertWasmThrowsExn(thunk) {
  let thrown = false;

  try {
    thunk();
  } catch (exn) {
    thrown = true;
    assertEq(exn instanceof WebAssembly.Exception, true);
  }

  assertEq(thrown, true, "missing exception");
}

// Test that handler-less trys don't catch anything.
assertWasmThrowsExn(() =>
  wasmEvalText(
    `(module
       (type (func (param)))
       (tag $exn (type 0))
       (func (export "f")
         try (throw $exn) end))`
  ).exports.f()
);

assertWasmThrowsExn(() =>
  wasmEvalText(
    `(module
       (type (func (param)))
       (tag $exn (type 0))
       (func $g (throw $exn))
       (func (export "f")
         try (call $g) end)
)`
  ).exports.f()
);

assertWasmThrowsExn(() =>
  wasmEvalText(
    `(module
       (type (func (param)))
       (tag $exn (type 0))
       (func (export "f")
         try try (throw $exn) end end))`
  ).exports.f()
);

assertWasmThrowsExn(() =>
  wasmEvalText(
    `(module
       (tag $exn (param))
       (func (export "f")
         try
           try
             throw $exn
           delegate 0
         end))`
  ).exports.f()
);

assertWasmThrowsExn(() =>
  wasmEvalText(
    `(module
       (tag $exn (param))
       (func (export "f")
         try
           try
             throw $exn
           delegate 1
         end))`
  ).exports.f()
);

assertWasmThrowsExn(() =>
  wasmEvalText(
    `(module
       (tag $exn (param))
       (func (export "f")
         block
           try
             throw $exn
           delegate 0
         end))`
  ).exports.f()
);

assertWasmThrowsExn(() =>
  wasmEvalText(
    `(module
       (tag $exn (param))
       (func (export "f")
         loop
           try
             throw $exn
           delegate 0
         end))`
  ).exports.f()
);

assertWasmThrowsExn(() =>
  wasmEvalText(
    `(module
       (tag $exn (param))
       (func (export "f")
         (i32.const 1)
         if
           try
             throw $exn
           delegate 0
         end))`
  ).exports.f()
);

// Test throwing simple empty exceptions to JS.
assertWasmThrowsExn(() =>
  wasmEvalText(
    `(module
       (type (func (param)))
       (tag $exn (type 0))
       (func (export "f")
         (throw $exn)))`
  ).exports.f()
);

// Test that wasm preserves the values of non-object exceptions that pass
// through it back to JS.
assertThrowsValue(
  () =>
    wasmEvalText(
      `(module
         (tag $exn)
         (import "m" "import" (func $import))
         (func (export "f")
           try
             (call $import)
           catch $exn
             ;; this block shouldn't be reached
           end))`,
      {
        m: {
          import: () => {
            throw 42;
          },
        },
      }
    ).exports.f(),
  42
);

// Like previous test, but using a rethrow instruction instead.
assertThrowsValue(
  () =>
    wasmEvalText(
      `(module
         (import "m" "import" (func $import))
         (func (export "f")
           try
             (call $import)
           catch_all
             (rethrow 0)
           end))`,
      {
        m: {
          import: () => {
            throw 42;
          },
        },
      }
    ).exports.f(),
  42
);

// Test for throwing to JS and then back to Wasm.
{
  var wasmThrower;
  let exports = wasmEvalText(
    `(module
       (type (func (param i32)))
       (tag $exn (type 0))
       (import "m" "import" (func $import (result i32)))
       (func (export "thrower")
         (i32.const 42)
         (throw $exn))
       (func (export "catcher") (result i32)
         try (result i32)
           (call $import)
         catch $exn
         end))`,
    {
      m: {
        import: () => {
          return wasmThrower();
        },
      },
    }
  ).exports;

  wasmThrower = exports.thrower;
  assertEq(exports.catcher(), 42);
}

// Tests for checking the tags of exceptions.
{
  let exports = wasmEvalText(
    `(module
       (type (func (param i32)))
       (tag $exn (export "exn") (type 0))
       (func (export "thrower")
         (i32.const 42)
         (throw $exn)))`
  ).exports;

  let imports = {
    store: {
      throws: () => {
        return exports.thrower();
      },
      exn: exports.exn,
    },
  };

  // This passes the exception tag check and the exception is caught.
  assertEq(
    wasmEvalText(
      `(module
         (type (func (param i32)))
         (import "store" "throws" (func $thrower (result i32)))
         (import "store" "exn" (tag $exn (type 0)))
         (func (export "catches") (result i32)
           try (result i32)
             (call $thrower)
           catch $exn
             (i32.const 15)
             (i32.sub)
           end))`,
      imports
    ).exports.catches(),
    27
  );

  // This fails the exception tag check, despite the local exception having
  // a matching signature.
  assertWasmThrowsExn(() =>
    wasmEvalText(
      `(module
         (type (func (param i32)))
         (import "store" "throws" (func $thrower (result i32)))
         (tag $exn (type 0))
         (func (export "catchesFail") (result i32)
           try (result i32)
             (call $thrower)
           catch $exn ;; This should not recognise $exn, thus not unpack 42.
           end))`,
      imports
    ).exports.catchesFail()
  );
}

// Test that JS finally block executes after a Wasm throw.
assertEq(
  (() => {
    try {
      wasmEvalText(
        `(module
           (type (func (param)))
           (tag $exn (type 0))
           (func (export "f")
             (throw $exn)))`
      ).exports.f();
    } finally {
      return true;
    }
    return false;
  })(),
  true
);

// Test that a wasm trap that passes through JS cannot be caught in Wasm.
{
  let throwTrap = wasmEvalText(`(module (func (export "f") unreachable))`)
    .exports.f;
  let catcher = wasmEvalText(
    `(module
       (type (func))
       (tag $exn (type 0))
       (import "m" "f" (func $foreign (param) (result)))
       (func (export "f")
         try
           call $foreign
         catch $exn
         catch_all
         end))`,
    {
      m: {
        // JS frame that goes between the two wasm frames and just rethrows.
        f: () => {
          try {
            throwTrap();
          } catch (e) {
            throw e;
          }
        },
      },
    }
  ).exports.f;

  assertErrorMessage(
    () => catcher(),
    WebAssembly.RuntimeError,
    "unreachable executed"
  );
}

// Test delegate throwing out of function.
assertWasmThrowsExn(() =>
  wasmEvalText(
    `(module
       (tag $exn (param))
       (func (export "f") (result i32)
         try (result i32)
           throw $exn
         delegate 0))`
  ).exports.f()
);

assertWasmThrowsExn(() =>
  wasmEvalText(
    `(module
       (tag $exn (param))
       (func (export "f") (result i32)
         try (result i32)
           i32.const 0
           if
             i32.const 1
             return
           else
             throw $exn
           end
           i32.const 0
         delegate 0))`
  ).exports.f()
);
