// Copyright 2021 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: --harmony-rab-gsab --allow-natives-syntax

"use strict";

d8.file.execute('test/mjsunit/typedarray-helpers.js');

function resizeHelper(ab, value) {
  const return_value = ab.resize(value);
  assertEquals(undefined, return_value);
  assertEquals(value, ab.byteLength);
}

function growHelper(ab, value) {
  const return_value = ab.grow(value);
  assertEquals(undefined, return_value);
  assertEquals(value, ab.byteLength);
}

(function TestRABBasics() {
  const rab = CreateResizableArrayBuffer(10, 20);
  assertTrue(rab instanceof ArrayBuffer);
  assertFalse(rab instanceof SharedArrayBuffer);
  assertEquals(10, rab.byteLength);
  assertEquals(20, rab.maxByteLength);
})();

(function TestRABCtorByteLengthEqualsMax() {
  const rab = CreateResizableArrayBuffer(10, 10);
  assertEquals(10, rab.byteLength);
  assertEquals(10, rab.maxByteLength);
})();

(function TestRABCtorByteLengthZero() {
  const rab = CreateResizableArrayBuffer(0, 10);
  assertEquals(0, rab.byteLength);
  assertEquals(10, rab.maxByteLength);
})();

(function TestRABCtorByteLengthAndMaxZero() {
  const rab = CreateResizableArrayBuffer(0, 0);
  assertEquals(0, rab.byteLength);
  assertEquals(0, rab.maxByteLength);
})();

const arrayBufferCtors = [[ArrayBuffer, (b) => b.resizable],
                          [SharedArrayBuffer, (b) => b.growable]];

(function TestOptionsBagNotObject() {
  for (let [ctor, resizable] of arrayBufferCtors) {
    const buffer = new ctor(10, 'this is not an options bag');
    assertFalse(resizable(buffer));
  }
})();

(function TestOptionsBagMaxByteLengthGetterThrows() {
  let evil = {};
  Object.defineProperty(evil, 'maxByteLength',
                        {get: () => { throw new Error('thrown'); }});
  for (let [ctor, resizable] of arrayBufferCtors) {
    let caught = false;
    try {
      new ctor(10, evil);
    } catch(e) {
      assertEquals('thrown', e.message);
      caught = true;
    }
    assertTrue(caught);
  }
})();

(function TestMaxByteLengthNonExisting() {
  for (let [ctor, resizable] of arrayBufferCtors) {
    const buffer = new ctor(10, {});
    assertFalse(resizable(buffer));
  }
})();

(function TestMaxByteLengthUndefinedOrNan() {
  for (let [ctor, resizable] of arrayBufferCtors) {
    const buffer1 = new ctor(10, {maxByteLength: undefined});
    assertFalse(resizable(buffer1));
    const buffer2 = new ctor(0, {maxByteLength: NaN});
    assertTrue(resizable(buffer2));
    assertEquals(0, buffer2.byteLength);
    assertEquals(0, buffer2.maxByteLength);
  }
})();

(function TestMaxByteLengthBooleanNullOrString() {
  for (let [ctor, resizable] of arrayBufferCtors) {
    const buffer1 = new ctor(0, {maxByteLength: true});
    assertTrue(resizable(buffer1));
    assertEquals(0, buffer1.byteLength);
    assertEquals(1, buffer1.maxByteLength);
    const buffer2 = new ctor(0, {maxByteLength: false});
    assertTrue(resizable(buffer2));
    assertEquals(0, buffer2.byteLength);
    assertEquals(0, buffer2.maxByteLength);
    const buffer3 = new ctor(0, {maxByteLength: null});
    assertTrue(resizable(buffer3));
    assertEquals(0, buffer3.byteLength);
    assertEquals(0, buffer3.maxByteLength);
    const buffer4 = new ctor(0, {maxByteLength: '100'});
    assertTrue(resizable(buffer4));
    assertEquals(0, buffer4.byteLength);
    assertEquals(100, buffer4.maxByteLength);
  }
})();

(function TestMaxByteLengthDouble() {
  for (let [ctor, resizable] of arrayBufferCtors) {
    const buffer1 = new ctor(0, {maxByteLength: -0.0});
    assertTrue(resizable(buffer1));
    assertEquals(0, buffer1.byteLength);
    assertEquals(0, buffer1.maxByteLength);
    const buffer2 = new ctor(0, {maxByteLength: -0.1});
    assertTrue(resizable(buffer2));
    assertEquals(0, buffer2.byteLength);
    assertEquals(0, buffer2.maxByteLength);
    const buffer3 = new ctor(0, {maxByteLength: 1.2});
    assertTrue(resizable(buffer3));
    assertEquals(0, buffer3.byteLength);
    assertEquals(1, buffer3.maxByteLength);
    assertThrows(() => { new ctor(0, {maxByteLength: -1.5}) });
    assertThrows(() => { new ctor(0, {maxByteLength: -1}) });
  }
})();

(function TestMaxByteLengthThrows() {
  const evil = {valueOf: () => { throw new Error('thrown');}};
  for (let [ctor, resizable] of arrayBufferCtors) {
    let caught = false;
    try {
      new ctor(0, {maxByteLength: evil});
    } catch (e) {
      assertEquals('thrown', e.message);
      caught = true;
    }
    assertTrue(caught);
  }
})();

(function TestByteLengthThrows() {
  const evil1 = {valueOf: () => { throw new Error('byteLength throws');}};
  const evil2 = {valueOf: () => { throw new Error('maxByteLength throws');}};
  for (let [ctor, resizable] of arrayBufferCtors) {
    let caught = false;
    try {
      new ctor(evil1, {maxByteLength: evil2});
    } catch (e) {
      assertEquals('byteLength throws', e.message);
      caught = true;
    }
    assertTrue(caught);
  }
})();

(function TestAllocatingOutrageouslyMuchThrows() {
  assertThrows(() => { CreateResizableArrayBuffer(0, 2 ** 100);}, RangeError);
})();

(function TestRABCtorOperationOrder() {
  let log = '';
  const mock_length = {valueOf: function() {
      log += 'valueof length, '; return 10; }};
  const mock_max_length = {valueOf: function() {
      log += 'valueof max_length, '; return 10; }};
  CreateResizableArrayBuffer(mock_length, mock_max_length);

  assertEquals('valueof length, valueof max_length, ', log);
})();

(function TestGSABCtorOperationOrder() {
  let log = '';
  const mock_length = {valueOf: function() {
      log += 'valueof length, '; return 10; }};
  const mock_max_length = {valueOf: function() {
      log += 'valueof max_length, '; return 10; }};
  CreateResizableArrayBuffer(mock_length, mock_max_length);

  assertEquals('valueof length, valueof max_length, ', log);
})();

(function TestByteLengthGetterReceiverChecks() {
  const name = 'byteLength';
  const ab_getter = Object.getOwnPropertyDescriptor(
      ArrayBuffer.prototype, name).get;
  const sab_getter = Object.getOwnPropertyDescriptor(
      SharedArrayBuffer.prototype, name).get;

  const ab = new ArrayBuffer(40);
  const sab = new SharedArrayBuffer(40);
  const rab = CreateResizableArrayBuffer(40, 40);
  const gsab = CreateGrowableSharedArrayBuffer(40, 40);

  assertEquals(40, ab_getter.call(ab));
  assertEquals(40, ab_getter.call(rab));
  assertEquals(40, sab_getter.call(sab));
  assertEquals(40, sab_getter.call(gsab));

  assertThrows(() => { ab_getter.call(sab);});
  assertThrows(() => { ab_getter.call(gsab);});

  assertThrows(() => { sab_getter.call(ab);});
  assertThrows(() => { sab_getter.call(rab);});
})();

(function TestMaxByteLengthGetterReceiverChecks() {
  const name = 'maxByteLength';
  const ab_getter = Object.getOwnPropertyDescriptor(
      ArrayBuffer.prototype, name).get;
  const sab_getter = Object.getOwnPropertyDescriptor(
      SharedArrayBuffer.prototype, name).get;

  const ab = new ArrayBuffer(40);
  const sab = new SharedArrayBuffer(40);
  const rab = CreateResizableArrayBuffer(20, 40);
  const gsab = CreateGrowableSharedArrayBuffer(20, 40);

  assertEquals(40, ab_getter.call(ab));
  assertEquals(40, ab_getter.call(rab));
  assertEquals(40, sab_getter.call(sab));
  assertEquals(40, sab_getter.call(gsab));

  assertThrows(() => { ab_getter.call(sab);});
  assertThrows(() => { ab_getter.call(gsab);});

  assertThrows(() => { sab_getter.call(ab);});
  assertThrows(() => { sab_getter.call(rab);});
})();

(function TestResizableGetterReceiverChecks() {
  const ab_getter = Object.getOwnPropertyDescriptor(
      ArrayBuffer.prototype, 'resizable').get;
  const sab_getter = Object.getOwnPropertyDescriptor(
      SharedArrayBuffer.prototype, 'growable').get;

  const ab = new ArrayBuffer(40);
  const sab = new SharedArrayBuffer(40);
  const rab = CreateResizableArrayBuffer(40, 40);
  const gsab = CreateGrowableSharedArrayBuffer(40, 40);

  assertEquals(false, ab_getter.call(ab));
  assertEquals(true, ab_getter.call(rab));
  assertEquals(false, sab_getter.call(sab));
  assertEquals(true, sab_getter.call(gsab));

  assertThrows(() => { ab_getter.call(sab);});
  assertThrows(() => { ab_getter.call(gsab);});

  assertThrows(() => { sab_getter.call(ab);});
  assertThrows(() => { sab_getter.call(rab);});
})();

(function TestByteLengthAndMaxByteLengthOfDetached() {
  const rab = CreateResizableArrayBuffer(10, 20);
  %ArrayBufferDetach(rab);
  assertEquals(0, rab.byteLength);
  assertEquals(0, rab.maxByteLength);
})();

(function TestResizeAndGrowReceiverChecks() {
  const rab_resize = ArrayBuffer.prototype.resize;
  const gsab_grow = SharedArrayBuffer.prototype.grow;

  const ab = new ArrayBuffer(40);
  const sab = new SharedArrayBuffer(40);
  const rab = CreateResizableArrayBuffer(10, 40);
  const gsab = CreateGrowableSharedArrayBuffer(10, 40);

  rab_resize.call(rab, 20);
  gsab_grow.call(gsab, 20);
  assertThrows(() => { rab_resize.call(ab, 30);});
  assertThrows(() => { rab_resize.call(sab, 30);});
  assertThrows(() => { rab_resize.call(gsab, 30);});

  assertThrows(() => { gsab_grow.call(ab, 30);});
  assertThrows(() => { gsab_grow.call(sab, 30);});
  assertThrows(() => { gsab_grow.call(rab, 30);});
})();

(function TestRABResizeToMax() {
  const rab = CreateResizableArrayBuffer(10, 20);
  resizeHelper(rab, 20);
})();

(function TestRABResizeToSameSize() {
  const rab = CreateResizableArrayBuffer(10, 20);
  resizeHelper(rab, 10);
})();

(function TestRABResizeToSmaller() {
  const rab = CreateResizableArrayBuffer(10, 20);
  resizeHelper(rab, 5);
})();

(function TestRABResizeToZero() {
  const rab = CreateResizableArrayBuffer(10, 20);
  resizeHelper(rab, 0);
})();

(function TestRABResizeZeroToZero() {
  const rab = CreateResizableArrayBuffer(0, 20);
  resizeHelper(rab, 0);
})();

(function TestRABGrowBeyondMaxThrows() {
  const rab = CreateResizableArrayBuffer(10, 20);
  assertEquals(10, rab.byteLength);
  assertThrows(() => {rab.grow(21)});
  assertEquals(10, rab.byteLength);
})();

(function TestRABResizeMultipleTimes() {
  const rab = CreateResizableArrayBuffer(10, 20);
  const sizes = [15, 7, 7, 0, 8, 20, 20, 10];
  for (let s of sizes) {
    resizeHelper(rab, s);
  }
})();

(function TestRABResizeParameters() {
  const rab = CreateResizableArrayBuffer(10, 20);
  rab.resize('15');
  assertEquals(15, rab.byteLength);
  rab.resize({valueOf: function() { return 16; }});
  assertEquals(16, rab.byteLength);
  rab.resize(17.9);
  assertEquals(17, rab.byteLength);
})();

(function TestRABResizeInvalidParameters() {
  const rab = CreateResizableArrayBuffer(10, 20);
  assertThrows(() => { rab.resize(-1) }, RangeError);
  assertThrows(() => { rab.resize({valueOf: function() {
      throw new Error('length param'); }})});
  assertEquals(10, rab.byteLength);

  // Various non-numbers are converted to NaN which is treated as 0.
  rab.resize('string');
  assertEquals(0, rab.byteLength);
  rab.resize();
  assertEquals(0, rab.byteLength);
})();

(function TestRABResizeDetached() {
  const rab = CreateResizableArrayBuffer(10, 20);
  %ArrayBufferDetach(rab);
  assertThrows(() => { rab.resize(15) }, TypeError);
})();

(function DetachInsideResizeParameterConversion() {
  const rab = CreateResizableArrayBuffer(40, 80);

  const evil = {
    valueOf: () => { %ArrayBufferDetach(rab); return 20; }
  };

  assertThrows(() => { rab.resize(evil); });
})();

(function ResizeInsideResizeParameterConversion() {
  const rab = CreateResizableArrayBuffer(40, 80);

  const evil = {
    valueOf: () => { rab.resize(10); return 20; }
  };

  rab.resize(evil);
  assertEquals(20, rab.byteLength);
})();


(function TestRABNewMemoryAfterResizeInitializedToZero() {
  const maybe_page_size = 4096;
  const rab = CreateResizableArrayBuffer(maybe_page_size, 2 * maybe_page_size);
  const i8a = new Int8Array(rab);
  rab.resize(2 * maybe_page_size);
  for (let i = 0; i < 2 * maybe_page_size; ++i) {
    assertEquals(0, i8a[i]);
  }
})();

(function TestRABMemoryInitializedToZeroAfterShrinkAndGrow() {
  const maybe_page_size = 4096;
  const rab = CreateResizableArrayBuffer(maybe_page_size, 2 * maybe_page_size);
  const i8a = new Int8Array(rab);
  for (let i = 0; i < maybe_page_size; ++i) {
    i8a[i] = 1;
  }
  rab.resize(maybe_page_size / 2);
  rab.resize(maybe_page_size);
  for (let i = maybe_page_size / 2; i < maybe_page_size; ++i) {
    assertEquals(0, i8a[i]);
  }
})();

(function TestGSABBasics() {
  const gsab = CreateGrowableSharedArrayBuffer(10, 20);
  assertFalse(gsab instanceof ArrayBuffer);
  assertTrue(gsab instanceof SharedArrayBuffer);
  assertEquals(10, gsab.byteLength);
  assertEquals(20, gsab.maxByteLength);
})();

(function TestGSABCtorByteLengthEqualsMax() {
  const gsab = CreateGrowableSharedArrayBuffer(10, 10);
  assertEquals(10, gsab.byteLength);
  assertEquals(10, gsab.maxByteLength);
})();

(function TestGSABCtorByteLengthZero() {
  const gsab = CreateGrowableSharedArrayBuffer(0, 10);
  assertEquals(0, gsab.byteLength);
  assertEquals(10, gsab.maxByteLength);
})();

(function TestGSABCtorByteLengthAndMaxZero() {
  const gsab = CreateGrowableSharedArrayBuffer(0, 0);
  assertEquals(0, gsab.byteLength);
  assertEquals(0, gsab.maxByteLength);
})();

(function TestAllocatingOutrageouslyMuchThrows() {
  assertThrows(() => { CreateGrowableSharedArrayBuffer(0, 2 ** 100);},
               RangeError);
})();

(function TestGSABGrowToMax() {
  const gsab = CreateGrowableSharedArrayBuffer(10, 20);
  assertEquals(10, gsab.byteLength);
  growHelper(gsab, 20);
})();

(function TestGSABGrowToSameSize() {
  const gsab = CreateGrowableSharedArrayBuffer(10, 20);
  assertEquals(10, gsab.byteLength);
  growHelper(gsab, 10);
})();

(function TestGSABGrowToSmallerThrows() {
  const gsab = CreateGrowableSharedArrayBuffer(10, 20);
  assertEquals(10, gsab.byteLength);
  assertThrows(() => {gsab.grow(5)});
  assertEquals(10, gsab.byteLength);
})();

(function TestGSABGrowToZeroThrows() {
  const gsab = CreateGrowableSharedArrayBuffer(10, 20);
  assertEquals(10, gsab.byteLength);
  assertThrows(() => {gsab.grow(0)});
  assertEquals(10, gsab.byteLength);
})();

(function TestGSABGrowBeyondMaxThrows() {
  const gsab = CreateGrowableSharedArrayBuffer(10, 20);
  assertEquals(10, gsab.byteLength);
  assertThrows(() => {gsab.grow(21)});
  assertEquals(10, gsab.byteLength);
})();

(function TestGSABGrowMultipleTimes() {
  const gsab = CreateGrowableSharedArrayBuffer(10, 20);
  assertEquals(10, gsab.byteLength);
  const sizes = [15, 7, 7, 0, 8, 20, 20, 10];
  for (let s of sizes) {
    const current_size = gsab.byteLength;
    if (s < gsab.byteLength) {
      assertThrows(() => {gsab.grow(s)});
      assertEquals(current_size, gsab.byteLength);
    } else {
      growHelper(gsab, s);
    }
  }
})();

(function TestGSABGrowParameters() {
  const gsab = CreateGrowableSharedArrayBuffer(10, 20);
  gsab.grow('15');
  assertEquals(15, gsab.byteLength);
  gsab.grow({valueOf: function() { return 16; }});
  assertEquals(16, gsab.byteLength);
  gsab.grow(17.9);
  assertEquals(17, gsab.byteLength);
})();

(function TestGSABGrowInvalidParameters() {
  const gsab = CreateGrowableSharedArrayBuffer(0, 20);
  assertThrows(() => { gsab.grow(-1) }, RangeError);
  assertThrows(() => { gsab.grow({valueOf: function() {
      throw new Error('length param'); }})});
  assertEquals(0, gsab.byteLength);

  // Various non-numbers are converted to NaN which is treated as 0.
  gsab.grow('string');
  assertEquals(0, gsab.byteLength);
  gsab.grow();
  assertEquals(0, gsab.byteLength);
})();

(function TestGSABMemoryInitializedToZeroAfterGrow() {
  const maybe_page_size = 4096;
  const gsab = CreateGrowableSharedArrayBuffer(maybe_page_size,
                                             2 * maybe_page_size);
  const i8a = new Int8Array(gsab);
  gsab.grow(2 * maybe_page_size);
  assertEquals(2 * maybe_page_size, i8a.length);
  for (let i = 0; i < 2 * maybe_page_size; ++i) {
    assertEquals(0, i8a[i]);
  }
})();

(function GrowGSABOnADifferentThread() {
  const gsab = CreateGrowableSharedArrayBuffer(10, 20);
  assertEquals(10, gsab.byteLength);
  function workerCode() {
    function assert(thing) {
      if (!thing) {
        postMessage('error');
      }
    }
    onmessage = function(params) {
      const gsab = params.gsab;
      assert(!(gsab instanceof ArrayBuffer));
      assert(gsab instanceof SharedArrayBuffer);
      assert(10 == gsab.byteLength);
      assert(20 == gsab.maxByteLength);
      gsab.grow(15);
      postMessage('ok');
    }
  }
  const w = new Worker(workerCode, {type: 'function'});
  w.postMessage({gsab: gsab});
  assertEquals('ok', w.getMessage());
  assertEquals(15, gsab.byteLength);
})();

(function Slice() {
  const rab = CreateResizableArrayBuffer(10, 20);
  const sliced1 = rab.slice();
  assertEquals(10, sliced1.byteLength);
  assertTrue(sliced1 instanceof ArrayBuffer);
  assertFalse(sliced1 instanceof SharedArrayBuffer);
  assertFalse(sliced1.resizable);

  const gsab = CreateGrowableSharedArrayBuffer(10, 20);
  const sliced2 = gsab.slice();
  assertEquals(10, sliced2.byteLength);
  assertFalse(sliced2 instanceof ArrayBuffer);
  assertTrue(sliced2 instanceof SharedArrayBuffer);
  assertFalse(sliced2.growable);
})();

(function SliceSpeciesConstructorReturnsResizable() {
  class MyArrayBuffer extends ArrayBuffer {
    static get [Symbol.species]() { return MyResizableArrayBuffer; }
  }

  class MyResizableArrayBuffer extends ArrayBuffer {
    constructor(byteLength) {
      super(byteLength, {maxByteLength: byteLength * 2});
    }
  }

  const ab = new MyArrayBuffer(20);
  const sliced1 = ab.slice();
  assertTrue(sliced1.resizable);

  class MySharedArrayBuffer extends SharedArrayBuffer {
    static get [Symbol.species]() { return MyGrowableSharedArrayBuffer; }
  }

  class MyGrowableSharedArrayBuffer extends SharedArrayBuffer {
    constructor(byteLength) {
      super(byteLength, {maxByteLength: byteLength * 2});
    }
  }

  const sab = new MySharedArrayBuffer(20);
  const sliced2 = sab.slice();
  assertTrue(sliced2.growable);
})();

(function SliceSpeciesConstructorResizes() {
  let rab;
  let resizeWhenConstructorCalled = false;
  class MyArrayBuffer extends ArrayBuffer {
    constructor(...params) {
      super(...params);
      if (resizeWhenConstructorCalled) {
        rab.resize(2);
      }
    }
  }
  rab = new MyArrayBuffer(4, {maxByteLength: 8});
  const taWrite = new Uint8Array(rab);
  for (let i = 0; i < 4; ++i) {
    taWrite[i] = 1;
  }
  assertEquals([1, 1, 1, 1], ToNumbers(taWrite));
  resizeWhenConstructorCalled = true;
  const sliced = rab.slice();
  assertEquals(2, rab.byteLength);
  assertEquals(4, sliced.byteLength);
  assertEquals([1, 1, 0, 0], ToNumbers(new Uint8Array(sliced)));
})();