// |jit-test| allow-unhandlable-oom

// weakmap marking tests that use the testing mark queue to force an ordering
// of marking.

// We are carefully controlling the sequence of GC events.
gczeal(0);

// If a command-line parameter is given, use it as a substring restriction on
// the tests to run.
var testRestriction = scriptArgs[0];
printErr(`testRestriction is ${testRestriction || '(run all tests)'}`);

function runtest(func) {
  if (testRestriction && ! func.name.includes(testRestriction)) {
    print("\Skipping " + func.name);
  } else {
    print("\nRunning " + func.name);
    func();
  }
}

function reportMarks(prefix = "") {
  const marks = getMarks();
  const current = currentgc();
  const markstr = marks.join("/");
  print(`${prefix}[${current.incrementalState}/${current.sweepGroup}@${current.queuePos}] ${markstr}`);
  return markstr;
}

function startGCMarking() {
  startgc(100000);
  while (gcstate() === "Prepare") {
    gcslice(100000);
  }
}

function purgeKey() {
  const m = new WeakMap();
  const vals = {};
  vals.key = Object.create(null);
  vals.val = Object.create(null);
  m.set(vals.key, vals.val);

  minorgc();

  addMarkObservers([m, vals.key, vals.val]);

  enqueueMark(m);
  enqueueMark("yield");

  enqueueMark(vals.key);
  enqueueMark("yield");

  vals.key = vals.val = null;

  startGCMarking();
  // getMarks() returns map/key/value
  assertEq(getMarks().join("/"), "black/unmarked/unmarked",
           "marked the map black");

  gcslice(100000);
  assertEq(getMarks().join("/"), "black/black/unmarked",
           "key is now marked");

  // Trigger purgeWeakKey: the key is in weakkeys (because it was unmarked when
  // the map was marked), and now we're removing it.
  m.delete(nondeterministicGetWeakMapKeys(m)[0]);

  finishgc(); // Finish the GC
  assertEq(getMarks().join("/"), "black/black/black",
           "at end, value is marked too");

  clearMarkQueue();
  clearMarkObservers();
}

if (this.enqueueMark)
  runtest(purgeKey);

function removeKey() {
  reportMarks("removeKey start: ");

  const m = new WeakMap();
  const vals = {};
  vals.key = Object.create(null);
  vals.val = Object.create(null);
  m.set(vals.key, vals.val);

  minorgc();

  addMarkObservers([m, vals.key, vals.val]);

  enqueueMark(m);
  enqueueMark("yield");

  startGCMarking();
  reportMarks("first: ");
  var marks = getMarks();
  assertEq(marks[0], "black", "map is black");
  assertEq(marks[1], "unmarked", "key not marked yet");
  assertEq(marks[2], "unmarked", "value not marked yet");
  m.delete(vals.key);

  finishgc(); // Finish the GC
  reportMarks("done: ");
  marks = getMarks();
  assertEq(marks[0], "black", "map is black");
  assertEq(marks[1], "black", "key is black");
  assertEq(marks[2], "black", "value is black");

  // Do it again, but this time, remove all other roots.
  m.set(vals.key, vals.val);
  vals.key = vals.val = null;
  startgc(10000);
  while (gcstate() !== "Mark") {
    gcslice(100000);
  }
  marks = getMarks();
  assertEq(marks[0], "black", "map is black");
  assertEq(marks[1], "unmarked", "key not marked yet");
  assertEq(marks[2], "unmarked", "value not marked yet");

  // This was meant to test the weakmap deletion barrier, which would remove
  // the key from weakkeys. Unfortunately, JS-exposed WeakMaps now have a read
  // barrier on lookup that marks the key, and deletion requires a lookup.
  m.delete(nondeterministicGetWeakMapKeys(m)[0]);

  finishgc();
  marks = getMarks();
  assertEq(marks[0], "black", "map is black");
  assertEq(marks[1], "black", "key was blackened by lookup read barrier during deletion");
  assertEq(marks[2], "black", "value is black because map and key are black");

  clearMarkQueue();
  clearMarkObservers();
}

if (this.enqueueMark)
  runtest(removeKey);

// Test:
//   1. mark the map
//     - that inserts the delegate into weakKeys
//   2. nuke the CCW key
//     - removes the delegate from weakKeys
//   3. mark the key
//   4. enter weak marking mode
//
// The problem it's attempting to recreate is that entering weak marking mode
// will no longer mark the value, because there's no delegate to trigger it,
// and the key was not added to weakKeys (because at the time the map was
// scanned, the key had a delegate, so it was added to weakKeys instead.)
function nukeMarking() {
  const g1 = newGlobal({newCompartment: true});

  const vals = {};
  vals.map = new WeakMap();
  vals.key = g1.eval("Object.create(null)");
  vals.val = Object.create(null);
  vals.map.set(vals.key, vals.val);
  vals.val = null;
  gc();

  // Set up the sequence of marking events.
  enqueueMark(vals.map);
  enqueueMark("yield");
  // We will nuke the key's delegate here.
  enqueueMark(vals.key);
  enqueueMark("enter-weak-marking-mode");

  // Okay, run through the GC now.
  startgc(1000000);
  while (gcstate() !== "Mark") {
    gcslice(100000);
  }
  assertEq(gcstate(), "Mark", "expected to yield after marking map");
  // We should have marked the map and then yielded back here.
  nukeCCW(vals.key);
  // Finish up the GC.
  gcslice();

  clearMarkQueue();
}

if (this.enqueueMark)
  runtest(nukeMarking);

// Similar to the above, but trying to get a different failure:
//  - start marking
//  - find a map, add its key to ephemeronEdges
//  - nuke the key (and all other CCWs between the key -> delegate zones)
//  - when sweeping, we will no longer have any edges between the key
//    and delegate zones. So they will be placed in separate sweep groups.
//  - for this test, the delegate zone must be swept after the key zone
//  - make sure we don't try to mark back in the key zone (due to an
//    ephemeron edge) while sweeping the delegate zone. In a DEBUG build,
//    this would assert.
function nukeMarkingSweepGroups() {
  // Create g1 before host, because that will result in the right zone
  // ordering to trigger the bug.
  const g1 = newGlobal({newCompartment: true});
  const host = newGlobal({newCompartment: true});
  host.g1 = g1;
  host.eval(`
  const vals = {};
  vals.map = new WeakMap();
  vals.key = g1.eval("Object.create(null)");
  vals.val = Object.create(null);
  vals.map.set(vals.key, vals.val);
  vals.val = null;
  gc();

  // Set up the sequence of marking events.
  enqueueMark(vals.map);
  enqueueMark("yield");
  // We will nuke the key's delegate here.
  enqueueMark(vals.key);
  enqueueMark("enter-weak-marking-mode");

  // Okay, run through the GC now.
  startgc(1);
  while (gcstate() === "Prepare") {
    gcslice(100000);
  }
  assertEq(gcstate(), "Mark", "expected to yield after marking map");
  // We should have marked the map and then yielded back here.
  nukeAllCCWs();
  // Finish up the GC.
  while (gcstate() === "Mark") {
    gcslice(1000);
  }
  gcslice();

  clearMarkQueue();
  `);
}

if (this.enqueueMark)
  runtest(nukeMarkingSweepGroups);

function transplantMarking() {
  const g1 = newGlobal({newCompartment: true});

  const vals = {};
  vals.map = new WeakMap();
  let {object, transplant} = transplantableObject();
  vals.key = object;
  object = null;
  vals.val = Object.create(null);
  vals.map.set(vals.key, vals.val);
  vals.val = null;
  gc();

  // Set up the sequence of marking events.
  enqueueMark(vals.map);
  enqueueMark("yield");
  // We will transplant the key here.
  enqueueMark(vals.key);
  enqueueMark("enter-weak-marking-mode");

  // Okay, run through the GC now.
  startgc(1000000);
  while (gcstate() !== "Mark") {
    gcslice(100000);
  }
  assertEq(gcstate(), "Mark", "expected to yield after marking map");
  // We should have marked the map and then yielded back here.
  transplant(g1);
  // Finish up the GC.
  gcslice();

  clearMarkQueue();
}

if (this.enqueueMark)
  runtest(transplantMarking);

// 1. Mark the map
//   => add delegate to weakKeys
// 2. Mark the delegate black
//   => do nothing because we are not in weak marking mode
// 3. Mark the key gray
//   => mark value gray, not that we really care
// 4. Enter weak marking mode
//   => black delegate darkens the key from gray to black
function grayMarkingMapFirst() {
  const g = newGlobal({newCompartment: true});
  const vals = {};
  vals.map = new WeakMap();
  vals.key = g.eval("Object.create(null)");
  vals.val = Object.create(null);
  vals.map.set(vals.key, vals.val);

  g.delegate = vals.key;
  g.eval("dummy = Object.create(null)");
  g.eval("grayRoot().push(delegate, dummy)");
  addMarkObservers([vals.map, vals.key]);
  g.addMarkObservers([vals.key, g.dummy]);
  addMarkObservers([vals.val]);

  gc();

  enqueueMark(vals.map);
  enqueueMark("yield"); // checkpoint 1

  g.enqueueMark(vals.key);
  enqueueMark("yield"); // checkpoint 2

  vals.val = null;
  vals.key = null;
  g.delegate = null;
  g.dummy = null;

  const showmarks = () => {
    print("[map,key,delegate,graydummy,value] marked " + JSON.stringify(getMarks()));
  };

  print("Starting incremental GC");
  startGCMarking();
  // Checkpoint 1, after marking map
  showmarks();
  var marks = getMarks();
  assertEq(marks[0], "black", "map is black");
  assertEq(marks[1], "unmarked", "key is not marked yet");
  assertEq(marks[2], "unmarked", "delegate is not marked yet");

  gcslice(100000);
  // Checkpoint 2, after marking delegate
  showmarks();
  marks = getMarks();
  assertEq(marks[0], "black", "map is black");
  assertEq(marks[1], "unmarked", "key is not marked yet");
  assertEq(marks[2], "black", "delegate is black");

  gcslice();
  // GC complete. Key was marked black (b/c of delegate), then gray marking saw
  // it was already black and skipped it.
  showmarks();
  marks = getMarks();
  assertEq(marks[0], "black", "map is black");
  assertEq(marks[1], "black", "delegate marked key black because of weakmap");
  assertEq(marks[2], "black", "delegate is still black");
  assertEq(marks[3], "gray", "basic gray marking is working");
  assertEq(marks[4], "black", "black map + black delegate => black value");

  clearMarkQueue();
  clearMarkObservers();
  grayRoot().length = 0;
  g.eval("grayRoot().length = 0");
}

if (this.enqueueMark)
  runtest(grayMarkingMapFirst);

function grayMarkingMapLast() {
  const g = newGlobal({newCompartment: true});
  const vals = {};
  vals.map = new WeakMap();
  vals.key = g.eval("Object.create(null)");
  vals.val = Object.create(null);
  vals.map.set(vals.key, vals.val);

  vals.map2 = new WeakMap();
  vals.key2 = g.eval("Object.create(null)");
  vals.val2 = Object.create(null);
  vals.map2.set(vals.key2, vals.val2);

  g.delegate = vals.key;
  g.eval("grayRoot().push(delegate)");
  addMarkObservers([vals.map, vals.key]);
  g.addMarkObservers([vals.key]);
  addMarkObservers([vals.val]);

  grayRoot().push(vals.key2);
  addMarkObservers([vals.map2, vals.key2]);
  g.addMarkObservers([vals.key2]);
  addMarkObservers([vals.val2]);

  const labels = ["map", "key", "delegate", "value", "map2", "key2", "delegate2", "value2"];

  gc();

  g.enqueueMark(vals.key);
  g.enqueueMark(vals.key2);
  enqueueMark("yield"); // checkpoint 1

  vals.val = null;
  vals.key = null;
  g.delegate = null;

  vals.map2 = null; // Important! Second map is never marked, keeps nothing alive.
  vals.key2 = null;
  vals.val2 = null;
  g.delegate2 = null;

  const labeledMarks = () => {
    const info = {};
    const marks = getMarks();
    for (let i = 0; i < labels.length; i++)
      info[labels[i]] = marks[i];
    return info;
  };

  const showmarks = () => {
    print("Marks:");
    for (const [label, mark] of Object.entries(labeledMarks()))
      print(`  ${label}: ${mark}`);
  };

  print("Starting incremental GC");
  startGCMarking();
  // Checkpoint 1, after marking key
  showmarks();
  var marks = labeledMarks();
  assertEq(marks.map, "unmarked", "map is unmarked");
  assertEq(marks.key, "unmarked", "key is not marked yet");
  assertEq(marks.delegate, "black", "delegate is black");
  assertEq(marks.map2, "unmarked", "map2 is unmarked");
  assertEq(marks.key2, "unmarked", "key2 is not marked yet");
  assertEq(marks.delegate2, "black", "delegate2 is black");

  gcslice();
  // GC complete. When entering weak marking mode, black delegate propagated to
  // key.
  showmarks();
  marks = labeledMarks();
  assertEq(marks.map, "black", "map is black");
  assertEq(marks.key, "black", "delegate marked key black because of weakmap");
  assertEq(marks.delegate, "black", "delegate is still black");
  assertEq(marks.value, "black", "black map + black delegate => black value");
  assertEq(marks.map2, "dead", "map2 is dead");
  assertEq(marks.key2, "gray", "key2 marked gray, map2 had no effect");
  assertEq(marks.delegate2, "black", "delegate artificially marked black via mark queue");
  assertEq(marks.value2, "dead", "dead map + black delegate => dead value");

  clearMarkQueue();
  clearMarkObservers();
  grayRoot().length = 0;
  g.eval("grayRoot().length = 0");

  return vals; // To prevent the JIT from optimizing out vals.
}

if (this.enqueueMark)
  runtest(grayMarkingMapLast);

function grayMapKey() {
  const vals = {};
  vals.m = new WeakMap();
  vals.key = Object.create(null);
  vals.val = Object.create(null);
  vals.m.set(vals.key, vals.val);

  // Maps are allocated black, so we won't be able to mark it gray during the
  // first GC.
  gc();

  addMarkObservers([vals.m, vals.key, vals.val]);

  // Wait until we can mark gray (ie, sweeping). Mark the map gray and yield.
  // This should happen all in one slice.
  enqueueMark("set-color-gray");
  enqueueMark(vals.m);
  enqueueMark("unset-color");
  enqueueMark("yield");

  // Make the weakmap no longer reachable from the roots, so we can mark it
  // gray.
  vals.m = null;

  enqueueMark(vals.key);
  enqueueMark("yield");

  vals.key = vals.val = null;

  startGCMarking();
  assertEq(getMarks().join("/"), "gray/unmarked/unmarked",
           "marked the map gray");

  gcslice(100000);
  assertEq(getMarks().join("/"), "gray/black/unmarked",
           "key is now marked black");

  finishgc(); // Finish the GC

  assertEq(getMarks().join("/"), "gray/black/gray",
           "at end: black/gray => gray");

  clearMarkQueue();
  clearMarkObservers();
}

if (this.enqueueMark)
  runtest(grayMapKey);

function grayKeyMap() {
  const vals = {};
  vals.m = new WeakMap();
  vals.key = Object.create(null);
  vals.val = Object.create(null);
  vals.m.set(vals.key, vals.val);

  addMarkObservers([vals.m, vals.key, vals.val]);

  enqueueMark(vals.key);
  enqueueMark("yield");

  // Wait until we are gray marking.
  enqueueMark("set-color-gray");
  enqueueMark(vals.m);
  enqueueMark("unset-color");
  enqueueMark("yield");

  enqueueMark("set-color-black");
  enqueueMark(vals.m);
  enqueueMark("unset-color");

  // Make the weakmap no longer reachable from the roots, so we can mark it
  // gray.
  vals.m = null;

  vals.key = vals.val = null;

  // Only mark this zone, to avoid interference from other tests that may have
  // created additional zones.
  schedulezone(vals);

  startGCMarking();
  // getMarks() returns map/key/value
  reportMarks("1: ");
  assertEq(getMarks().join("/"), "unmarked/black/unmarked",
           "marked key black");

  // We always yield before sweeping (in the absence of zeal), so we will see
  // the unmarked state another time.
  gcslice(100000);
  reportMarks("2: ");
  assertEq(getMarks().join("/"), "unmarked/black/unmarked",
           "marked key black, yield before sweeping");

  gcslice(100000);
  reportMarks("3: ");
  assertEq(getMarks().join("/"), "gray/black/gray",
           "marked the map gray, which marked the value when map scanned");

  finishgc(); // Finish the GC
  reportMarks("4: ");
  assertEq(getMarks().join("/"), "black/black/black",
           "further marked the map black, so value should also be blackened");

  clearMarkQueue();
  clearMarkObservers();
}

if (this.enqueueMark)
  runtest(grayKeyMap);

// Cause a key to be marked black *during gray marking*, by first marking a
// delegate black, then marking the map and key gray. When the key is scanned,
// it should be seen to be a CCW of a black delegate and so should itself be
// marked black.
//
// The bad behavior being prevented is:
//
//  1. You wrap an object in a CCW and use it as a weakmap key to some
//     information.
//  2. You keep a strong reference to the object (in its compartment).
//  3. The only references to the CCW are gray, and are in fact part of a cycle.
//  4. The CC runs and discards the CCW.
//  5. You look up the object in the weakmap again. This creates a new wrapper
//     to use as a key. It is not in the weakmap, so the information you stored
//     before is not found. (It may have even been collected, if you had no
//     other references to it.)
//
function blackDuringGray() {
  const g = newGlobal({newCompartment: true});
  const vals = {};
  vals.map = new WeakMap();
  vals.key = g.eval("Object.create(null)");
  vals.val = Object.create(null);
  vals.map.set(vals.key, vals.val);

  g.delegate = vals.key;
  addMarkObservers([vals.map, vals.key]);
  g.addMarkObservers([vals.key]);
  addMarkObservers([vals.val]);
  // Mark observers: map, key, delegate, value

  gc();

  g.enqueueMark(vals.key); // Mark the delegate black
  enqueueMark("yield"); // checkpoint 1

  // Mark the map gray. This will scan through all entries, find our key, and
  // mark it black because its delegate is black.
  enqueueMark("set-color-gray");
  enqueueMark(vals.map); // Mark the map gray

  vals.map = null;
  vals.val = null;
  vals.key = null;
  g.delegate = null;

  const showmarks = () => {
    print("[map,key,delegate,value] marked " + JSON.stringify(getMarks()));
  };

  print("Starting incremental GC");
  startGCMarking();
  // Checkpoint 1, after marking delegate black
  showmarks();
  var marks = getMarks();
  assertEq(marks[0], "unmarked", "map is not marked yet");
  assertEq(marks[1], "unmarked", "key is not marked yet");
  assertEq(marks[2], "black", "delegate is black");
  assertEq(marks[3], "unmarked", "values is not marked yet");

  finishgc();
  showmarks();
  marks = getMarks();
  assertEq(marks[0], "gray", "map is gray");
  assertEq(marks[1], "gray", "gray map + black delegate should mark key gray");
  assertEq(marks[2], "black", "delegate is still black");
  assertEq(marks[3], "gray", "gray map + gray key => gray value");

  clearMarkQueue();
  clearMarkObservers();
  grayRoot().length = 0;
  g.eval("grayRoot().length = 0");
}

if (this.enqueueMark)
  runtest(blackDuringGray);

// Same as above, except relying on the implicit edge from delegate -> key.
function blackDuringGrayImplicit() {
  const g = newGlobal({newCompartment: true});
  const vals = {};
  vals.map = new WeakMap();
  vals.key = g.eval("Object.create(null)");
  vals.val = Object.create(null);
  vals.map.set(vals.key, vals.val);

  g.delegate = vals.key;
  addMarkObservers([vals.map, vals.key]);
  g.addMarkObservers([vals.key]);
  addMarkObservers([vals.val]);
  // Mark observers: map, key, delegate, value

  gc();

  // Mark the map gray. This will scan through all entries, find our key, and
  // add implicit edges from delegate -> key and delegate -> value.
  enqueueMark("set-color-gray");
  enqueueMark(vals.map); // Mark the map gray
  enqueueMark("yield"); // checkpoint 1

  enqueueMark("set-color-black");
  g.enqueueMark(vals.key); // Mark the delegate black, propagating to key.

  vals.map = null;
  vals.val = null;
  vals.key = null;
  g.delegate = null;

  const showmarks = () => {
    print("[map,key,delegate,value] marked " + JSON.stringify(getMarks()));
  };

  print("Starting incremental GC");
  startGCMarking();
  // Checkpoint 1, after marking map gray
  showmarks();
  var marks = getMarks();
  assertEq(marks[0], "gray", "map is gray");
  assertEq(marks[1], "unmarked", "key is not marked yet");
  assertEq(marks[2], "unmarked", "delegate is not marked yet");
  assertEq(marks[3], "unmarked", "value is not marked yet");

  finishgc();
  showmarks();
  marks = getMarks();
  assertEq(marks[0], "gray", "map is gray");
  assertEq(marks[1], "gray", "gray map + black delegate should mark key gray");
  assertEq(marks[2], "black", "delegate is black");
  assertEq(marks[3], "gray", "gray map + gray key => gray value via delegate -> value");

  clearMarkQueue();
  clearMarkObservers();
  grayRoot().length = 0;
  g.eval("grayRoot().length = 0");
}

if (this.enqueueMark)
  runtest(blackDuringGrayImplicit);
