// Copyright 2018 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Flags: --experimental-wasm-reftypes --expose-gc

load("test/mjsunit/wasm/wasm-module-builder.js");

(function TestDefaultValue() {
  print(arguments.callee.name);
  let builder = new WasmModuleBuilder();
  const g_null = builder.addGlobal(kWasmExternRef, true).index;
  const g_nullfunc = builder.addGlobal(kWasmAnyFunc, true).index;
  builder.addFunction("get_externref_global", kSig_r_v)
    .addBody([kExprGlobalGet, g_null])
    .exportAs("get_externref_global");
  builder.addFunction("get_anyfunc_global", kSig_a_v)
    .addBody([kExprGlobalGet, g_nullfunc])
    .exportAs("get_anyfunc_global");

  const instance = builder.instantiate();
  assertEquals(null, instance.exports.get_externref_global());
  assertEquals(null, instance.exports.get_anyfunc_global());
})();

(function TestDefaultValueSecondGlobal() {
  print(arguments.callee.name);
  let builder = new WasmModuleBuilder();
  const g_setref = builder.addGlobal(kWasmExternRef, true);
  const g_setfunc = builder.addGlobal(kWasmAnyFunc, true);
  const g_null = builder.addGlobal(kWasmExternRef, true);
  const g_nullfunc = builder.addGlobal(kWasmAnyFunc, true);
  builder.addFunction("get_externref_global", kSig_r_r)
    .addBody([
      kExprLocalGet, 0,
      kExprGlobalSet, g_setref.index,
      kExprGlobalGet, g_null.index
    ])
    .exportAs("get_externref_global");
  builder.addFunction("get_anyfunc_global", kSig_a_a)
    .addBody([
      kExprLocalGet, 0,
      kExprGlobalSet, g_setfunc.index,
      kExprGlobalGet, g_nullfunc.index
    ])
    .exportAs("get_anyfunc_global");

  const instance = builder.instantiate();
  assertEquals(null, instance.exports.get_externref_global({}));
  assertEquals(null, instance.exports.get_anyfunc_global(
    instance.exports.get_externref_global));
})();

(function TestExternRefGlobalChangeValue() {
  print(arguments.callee.name);
  let builder = new WasmModuleBuilder();
  // Dummy global for offset.
  builder.addGlobal(kWasmExternRef, true);
  const g = builder.addGlobal(kWasmExternRef, true);
  builder.addFunction("main", kSig_r_r)
    .addBody([
      kExprLocalGet, 0,
      kExprGlobalSet, g.index,
      kExprGlobalGet, g.index
    ])
    .exportAs("main");

  const instance = builder.instantiate();

  const test_value = { hello: 'world' };
  assertSame(test_value, instance.exports.main(test_value));
})();

(function TestAnyFuncGlobalChangeValue() {
  print(arguments.callee.name);
  let builder = new WasmModuleBuilder();
  // Dummy global for offset.
  builder.addGlobal(kWasmAnyFunc, true);
  const g = builder.addGlobal(kWasmAnyFunc, true);
  builder.addFunction("main", kSig_a_a)
    .addBody([
      kExprLocalGet, 0,
      kExprGlobalSet, g.index,
      kExprGlobalGet, g.index
    ])
    .exportAs("main");

  const instance = builder.instantiate();

  const test_value = instance.exports.main;
  assertSame(test_value, instance.exports.main(test_value));
})();

(function TestGlobalChangeValueWithGC() {
  print(arguments.callee.name);
  let builder = new WasmModuleBuilder();
  const gc_index = builder.addImport("q", "gc", kSig_v_v);
  // Dummy global for offset.
  builder.addGlobal(kWasmExternRef, true);
  const g = builder.addGlobal(kWasmExternRef, true);
  builder.addFunction("main", kSig_r_r)
    .addBody([
      kExprLocalGet, 0,
      kExprGlobalSet, g.index,
      kExprCallFunction, gc_index,  // call gc
      kExprGlobalGet, g.index
    ])
    .exportAs("main");

  const instance = builder.instantiate({ q: { gc: gc } });

  const test_value = { hello: 'world' };
  assertSame(test_value, instance.exports.main(test_value));
  assertSame(5, instance.exports.main(5));
  assertSame("Hello", instance.exports.main("Hello"));
})();

(function TestGlobalAsRoot() {
  print(arguments.callee.name);
  let builder = new WasmModuleBuilder();
  const g = builder.addGlobal(kWasmExternRef, true);
  builder.addFunction("get_global", kSig_r_v)
    .addBody([
      kExprGlobalGet, g.index
    ])
    .exportAs("get_global");

  builder.addFunction("set_global", kSig_v_r)
    .addBody([
      kExprLocalGet, 0,
      kExprGlobalSet, g.index
    ])
    .exportAs("set_global");

  const instance = builder.instantiate();

  let test_value = { hello: 'world' };
  instance.exports.set_global(test_value);
  test_value = null;
  gc();

  const result = instance.exports.get_global();

  assertEquals('world', result.hello);
})();

(function TestImportedExternRef() {
  print(arguments.callee.name);
  function Test(obj) {
    let builder = new WasmModuleBuilder();
    const g = builder.addImportedGlobal('m', 'val', kWasmExternRef);
    builder.addFunction('main', kSig_r_v)
      .addBody([kExprGlobalGet, g])
      .exportAs('main');

    const instance = builder.instantiate({ m: { val: obj } });
    assertSame(obj, instance.exports.main());
  }
  Test(null);
  Test(undefined);
  Test(1653);
  Test("mystring");
  Test({ q: 14 });
  Test(print);
})();

function dummy_func() {
  let builder = new WasmModuleBuilder();
  builder.addFunction("dummy", kSig_i_v)
    .addBody([kExprI32Const, 12])
    .exportAs("dummy");
  return builder.instantiate().exports.dummy;
}

(function TestImportedAnyFunc() {
  print(arguments.callee.name);

  let builder = new WasmModuleBuilder();
  const g = builder.addImportedGlobal('m', 'val', kWasmAnyFunc);
  builder.addFunction('main', kSig_a_v)
    .addBody([kExprGlobalGet, g])
    .exportAs('main');

  const module = builder.toModule();
  const instance = new WebAssembly.Instance(module, { m: { val: null } });
  assertSame(null, instance.exports.main());

  const instance2 = new WebAssembly.Instance(
    module, { m: { val: instance.exports.main } });
  assertSame(instance.exports.main, instance2.exports.main());

  assertThrows(() => new WebAssembly.Instance(module, { m: { val: {} } }),
    WebAssembly.LinkError);
})();

(function TestExternRefGlobalObjectDefaultValue() {
  print(arguments.callee.name);
  let default_init = new WebAssembly.Global({ value: 'externref', mutable: true });
  assertSame(null, default_init.value);
  assertSame(null, default_init.valueOf());
})();

(function TestAnyFuncGlobalObjectDefaultValue() {
  print(arguments.callee.name);
  let default_init = new WebAssembly.Global({ value: 'anyfunc', mutable: true });
  assertSame(null, default_init.value);
  assertSame(null, default_init.valueOf());
})();

(function TestExternRefGlobalObject() {
  print(arguments.callee.name);
  function TestGlobal(obj) {
    const global = new WebAssembly.Global({ value: 'externref' }, obj);
    assertSame(obj, global.value);
    assertSame(obj, global.valueOf());
  }

  TestGlobal(null);
  TestGlobal(undefined);
  TestGlobal(1663);
  TestGlobal("testmyglobal");
  TestGlobal({ a: 11 });
  TestGlobal(print);
})();

(function TestAnyFuncGlobalObject() {
  print(arguments.callee.name);

  const dummy = dummy_func();
  const global = new WebAssembly.Global({ value: 'anyfunc' }, dummy);
  assertSame(dummy, global.value);
  assertSame(dummy, global.valueOf());

  const global_null = new WebAssembly.Global({ value: 'anyfunc' }, null);
  assertSame(null, global_null.value);
  assertSame(null, global_null.valueOf());

  assertThrows(() => new WebAssembly.Global({ value: 'anyfunc' }, {}), TypeError);
})();

(function TestExternRefGlobalObjectSetValue() {
  print(arguments.callee.name);
  let global = new WebAssembly.Global({ value: 'externref', mutable: true });

  function TestGlobal(obj) {
    global.value = obj;
    assertSame(obj, global.value);
    assertSame(obj, global.valueOf());
  }

  TestGlobal(null);
  assertThrows(() => TestGlobal(undefined), TypeError);
  TestGlobal(1663);
  TestGlobal("testmyglobal");
  TestGlobal({ a: 11 });
  TestGlobal(print);
})();


(function TestAnyFuncGlobalObjectSetValue() {
  print(arguments.callee.name);
  let global = new WebAssembly.Global({ value: 'anyfunc', mutable: true });

  const dummy = dummy_func();
  global.value = dummy;
  assertSame(dummy, global.value);
  assertSame(dummy, global.valueOf());

  global.value = null;
  assertSame(null, global.value);
  assertSame(null, global.valueOf());

  assertThrows(() => global.value = {}, TypeError);
})();

(function TestExportMutableRefGlobal() {
  print(arguments.callee.name);
  let builder = new WasmModuleBuilder();
  const g1 = builder.addGlobal(kWasmExternRef, true).exportAs("global1");
  const g2 = builder.addGlobal(kWasmAnyFunc, true).exportAs("global2");
  builder.addGlobal(kWasmI32, true); // Dummy.
  builder.addGlobal(kWasmExternRef, true); // Dummy.
  const g3 = builder.addGlobal(kWasmExternRef, true).exportAs("global3");
  const g4 = builder.addGlobal(kWasmAnyFunc, true).exportAs("global4");
  builder.addFunction("main",
    makeSig([kWasmExternRef, kWasmAnyFunc, kWasmExternRef, kWasmAnyFunc], []))
    .addBody([
      kExprLocalGet, 0,
      kExprGlobalSet, g1.index,
      kExprLocalGet, 1,
      kExprGlobalSet, g2.index,
      kExprLocalGet, 2,
      kExprGlobalSet, g3.index,
      kExprLocalGet, 3,
      kExprGlobalSet, g4.index
    ])
    .exportAs("main");

  const instance = builder.instantiate();
  const obj1 = { x: 221 };
  const func2 = instance.exports.main;
  const obj3 = print;
  const func4 = dummy_func();
  instance.exports.main(obj1, func2, obj3, func4);
  assertSame(obj1, instance.exports.global1.value);
  assertSame(func2, instance.exports.global2.value);
  assertSame(obj3, instance.exports.global3.value);
  assertSame(func4, instance.exports.global4.value);
})();

(function TestImportMutableExternRefGlobal() {
  print(arguments.callee.name);
  function Test(obj) {
    let builder = new WasmModuleBuilder();
    const g = builder.addImportedGlobal('m', 'val', kWasmExternRef, true);
    builder.addFunction('main', kSig_r_v)
      .addBody([kExprGlobalGet, g])
      .exportAs('main');

    const global = new WebAssembly.Global({ value: 'externref', mutable: 'true' }, obj);
    const instance = builder.instantiate({ m: { val: global } });
    assertSame(obj, instance.exports.main());
  }
  Test(null);
  Test(undefined);
  Test(1653);
  Test("mystring");
  Test({ q: 14 });
  Test(print);
})();

(function TestImportMutableAnyFuncGlobal() {
  print(arguments.callee.name);
  function Test(obj) {
    let builder = new WasmModuleBuilder();
    const g = builder.addImportedGlobal('m', 'val', kWasmAnyFunc, true);
    builder.addFunction('main', kSig_a_v)
      .addBody([kExprGlobalGet, g])
      .exportAs('main');

    const global = new WebAssembly.Global({ value: 'anyfunc', mutable: 'true' }, obj);
    const instance = builder.instantiate({ m: { val: global } });
    assertSame(obj, instance.exports.main());
  }
  Test(dummy_func());
  Test(null);
})();

(function TestImportMutableExternRefGlobalFromOtherInstance() {
  print(arguments.callee.name);

  // Create an instance which exports globals.
  let builder1 = new WasmModuleBuilder();
  const g3 = builder1.addGlobal(kWasmExternRef, true).exportAs("e3");
  builder1.addGlobal(kWasmI32, true).exportAs("e1"); // Dummy.
  builder1.addGlobal(kWasmExternRef, true).exportAs("e4"); // Dummy.
  const g2 = builder1.addGlobal(kWasmExternRef, true).exportAs("e2");

  builder1.addFunction("set_globals", kSig_v_rr)
    .addBody([
      kExprLocalGet, 0,
      kExprGlobalSet, g2.index,
      kExprLocalGet, 1,
      kExprGlobalSet, g3.index,
    ])
    .exportAs("set_globals");

  builder1.addFunction('get_global2', kSig_r_v)
    .addBody([kExprGlobalGet, g2.index])
    .exportAs('get_global2');

  builder1.addFunction('get_global3', kSig_r_v)
    .addBody([kExprGlobalGet, g3.index])
    .exportAs('get_global3');

  const instance1 = builder1.instantiate();
  const obj2 = { x: 221 };
  const obj3 = print;
  instance1.exports.set_globals(obj2, obj3);

  // Create an instance which imports the globals of the other instance.
  let builder2 = new WasmModuleBuilder();
  const i1 = builder2.addImportedGlobal('exports', 'e1', kWasmI32, true);
  const i2 = builder2.addImportedGlobal('exports', 'e2', kWasmExternRef, true);
  const i3 = builder2.addImportedGlobal('exports', 'e3', kWasmExternRef, true);
  const i4 = builder2.addImportedGlobal('exports', 'e4', kWasmExternRef, true);

  builder2.addExportOfKind("reexport1", kExternalGlobal, i1);
  builder2.addExportOfKind("reexport2", kExternalGlobal, i2);
  builder2.addExportOfKind("reexport3", kExternalGlobal, i3);
  builder2.addExportOfKind("reexport4", kExternalGlobal, i4);

  builder2.addFunction("set_globals", kSig_v_rr)
    .addBody([
      kExprLocalGet, 0,
      kExprGlobalSet, i2,
      kExprLocalGet, 1,
      kExprGlobalSet, i3,
    ])
    .exportAs("set_globals");

  builder2.addFunction('get_global2', kSig_r_v)
    .addBody([kExprGlobalGet, i2])
    .exportAs('get_global2');

  builder2.addFunction('get_global3', kSig_r_v)
    .addBody([kExprGlobalGet, i3])
    .exportAs('get_global3');

  const instance2 = builder2.instantiate(instance1);
  // Check if the globals were imported correctly.
  assertSame(obj2, instance2.exports.get_global2());
  assertSame(obj3, instance2.exports.get_global3());

  assertSame(obj2, instance2.exports.reexport2.value);
  assertSame(obj3, instance2.exports.reexport3.value);

  // Check if instance2 can make changes visible for instance1.
  instance2.exports.set_globals(null, undefined);
  assertEquals(null, instance1.exports.get_global2());
  assertEquals(undefined, instance1.exports.get_global3());

  assertEquals(null, instance2.exports.reexport2.value);
  assertEquals(undefined, instance2.exports.reexport3.value);

  // Check if instance1 can make changes visible for instance2.
  instance1.exports.set_globals("foo", 66343);
  assertEquals("foo", instance2.exports.get_global2());
  assertEquals(66343, instance2.exports.get_global3());

  assertEquals("foo", instance2.exports.reexport2.value);
  assertEquals(66343, instance2.exports.reexport3.value);

  const bar2 = { f: "oo" };
  const bar3 = { b: "ar" };
  instance2.exports.reexport2.value = bar2;
  instance2.exports.reexport3.value = bar3;

  assertSame(bar2, instance1.exports.get_global2());
  assertSame(bar3, instance1.exports.get_global3());
  assertSame(bar2, instance2.exports.get_global2());
  assertSame(bar3, instance2.exports.get_global3());
})();

(function TestImportMutableAnyFuncGlobalFromOtherInstance() {
  print(arguments.callee.name);

  // Create an instance which exports globals.
  let builder1 = new WasmModuleBuilder();
  const g3 = builder1.addGlobal(kWasmAnyFunc, true).exportAs("e3");
  builder1.addGlobal(kWasmI32, true).exportAs("e1"); // Dummy.
  builder1.addGlobal(kWasmAnyFunc, true).exportAs("e4"); // Dummy.
  const g2 = builder1.addGlobal(kWasmAnyFunc, true).exportAs("e2");

  builder1.addFunction("set_globals", kSig_v_aa)
    .addBody([
      kExprLocalGet, 0,
      kExprGlobalSet, g2.index,
      kExprLocalGet, 1,
      kExprGlobalSet, g3.index,
    ])
    .exportAs("set_globals");

  builder1.addFunction('get_global2', kSig_a_v)
    .addBody([kExprGlobalGet, g2.index])
    .exportAs('get_global2');

  builder1.addFunction('get_global3', kSig_a_v)
    .addBody([kExprGlobalGet, g3.index])
    .exportAs('get_global3');

  const instance1 = builder1.instantiate();
  const obj2 = dummy_func();
  const obj3 = instance1.exports.set_globals;
  const obj4 = instance1.exports.get_global3;
  instance1.exports.set_globals(obj2, obj3);

  // Create an instance which imports the globals of the other instance.
  let builder2 = new WasmModuleBuilder();
  const i1 = builder2.addImportedGlobal('exports', 'e1', kWasmI32, true);
  const i2 = builder2.addImportedGlobal('exports', 'e2', kWasmAnyFunc, true);
  const i3 = builder2.addImportedGlobal('exports', 'e3', kWasmAnyFunc, true);
  const i4 = builder2.addImportedGlobal('exports', 'e4', kWasmAnyFunc, true);

  builder2.addExportOfKind("reexport1", kExternalGlobal, i1);
  builder2.addExportOfKind("reexport2", kExternalGlobal, i2);
  builder2.addExportOfKind("reexport3", kExternalGlobal, i3);
  builder2.addExportOfKind("reexport4", kExternalGlobal, i4);

  builder2.addFunction("set_globals", kSig_v_aa)
    .addBody([
      kExprLocalGet, 0,
      kExprGlobalSet, i2,
      kExprLocalGet, 1,
      kExprGlobalSet, i3,
    ])
    .exportAs("set_globals");

  builder2.addFunction('get_global2', kSig_a_v)
    .addBody([kExprGlobalGet, i2])
    .exportAs('get_global2');

  builder2.addFunction('get_global3', kSig_a_v)
    .addBody([kExprGlobalGet, i3])
    .exportAs('get_global3');

  const instance2 = builder2.instantiate(instance1);
  // Check if the globals were imported correctly.
  assertSame(obj2, instance2.exports.get_global2());
  assertSame(obj3, instance2.exports.get_global3());

  assertSame(obj2, instance2.exports.reexport2.value);
  assertSame(obj3, instance2.exports.reexport3.value);

  // Check if instance2 can make changes visible for instance1.
  instance2.exports.set_globals(null, obj4);
  assertEquals(null, instance1.exports.get_global2());
  assertEquals(obj4, instance1.exports.get_global3());

  assertEquals(null, instance2.exports.reexport2.value);
  assertEquals(obj4, instance2.exports.reexport3.value);

  // Check if instance1 can make changes visible for instance2.
  instance1.exports.set_globals(obj2, obj3);
  assertEquals(obj2, instance2.exports.get_global2());
  assertEquals(obj3, instance2.exports.get_global3());

  assertEquals(obj2, instance2.exports.reexport2.value);
  assertEquals(obj3, instance2.exports.reexport3.value);
})();

(function TestImportMutableAnyFuncGlobalAsExternRefFails() {
  print(arguments.callee.name);
  let builder1 = new WasmModuleBuilder();
  const g3 = builder1.addGlobal(kWasmAnyFunc, true).exportAs("e3");
  builder1.addGlobal(kWasmExternRef, true).exportAs("e1"); // Dummy.
  builder1.addGlobal(kWasmAnyFunc, true).exportAs("e2"); // Dummy.
  const instance1 = builder1.instantiate();

  let builder2 = new WasmModuleBuilder();
  const i1 = builder2.addImportedGlobal('exports', 'e1', kWasmExternRef, true);
  const i2 = builder2.addImportedGlobal('exports', 'e2', kWasmExternRef, true);
  assertThrows(() => builder2.instantiate(instance1), WebAssembly.LinkError);
})();

(function TestRefFuncGlobalInit() {
  print(arguments.callee.name);
  let builder = new WasmModuleBuilder();
  const g_func = builder.addGlobal(kWasmAnyFunc, true);
  const f_func = builder.addFunction('get_anyfunc_global', kSig_a_v)
                     .addBody([kExprGlobalGet, g_func.index])
                     .exportAs('get_anyfunc_global');
  builder.addDeclarativeElementSegment([f_func.index]);
  g_func.function_index = f_func.index;

  const instance = builder.instantiate();
  assertEquals(
      instance.exports.get_anyfunc_global,
      instance.exports.get_anyfunc_global());
})();

(function TestRefFuncGlobalInitWithImport() {
  print(arguments.callee.name);
  let builder = new WasmModuleBuilder();
  const sig_index = builder.addType(kSig_i_v);
  const import_wasm = builder.addImport('m', 'wasm', sig_index);
  const import_js = builder.addImport('m', 'js', sig_index);
  const g_wasm = builder.addGlobal(kWasmAnyFunc, true);
  const g_js = builder.addGlobal(kWasmAnyFunc, true);
  builder.addDeclarativeElementSegment([import_wasm, import_js]);
  g_wasm.function_index = import_wasm;
  g_js.function_index = import_js;
  builder.addFunction('get_global_wasm', kSig_a_v)
      .addBody([kExprGlobalGet, g_wasm.index])
      .exportFunc();
  builder.addFunction('get_global_js', kSig_a_v)
      .addBody([kExprGlobalGet, g_js.index])
      .exportFunc();

  const expected_wasm = dummy_func();
  const expected_val = 27;
  // I want to test here that imported JS functions get wrapped by wasm-to-js
  // and js-to-wasm wrappers. That's why {expected_js} does not return an
  // integer directly but an object with a {valueOf} function.
  function expected_js() {
    const result = {};
    result.valueOf = () => expected_val;
    return result;
  };

  const instance =
      builder.instantiate({m: {wasm: expected_wasm, js: expected_js}});

  assertSame(expected_wasm, instance.exports.get_global_wasm());
  assertSame(expected_val, instance.exports.get_global_js()());
})();