Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions JSTests/stress/get-by-val-buffered-identifier-lifetime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//@ requireOptions("--useZombieMode=1", "--repatchBufferingCountdown=10")

// A by-val inline cache buffers (structure, property name) pairs while it waits for enough cases to
// generate a stub, and every later miss on the site compares against all of them. The subscripts
// here are fresh strings that nothing else refers to once the call returns, so the buffered pairs
// have to keep working across collections that sweep those strings.

function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error("bad value: expected " + expected + " but got " + actual);
}

var prefix = "ta";
var suffix = "rgetx";

// A rope, which toPropertyKey resolves in place, so each call caches a different string cell.
function freshKey() {
return prefix + suffix.substring(0, 4);
}

function makeSite() {
return new Function("o", "k", "return o[k];");
}

function makeBase(shape, value) {
var o = {};
for (var i = 0; i <= shape; ++i)
o["pad" + shape + "_" + i] = i;
o.target = value;
o[0] = value + 1000;
return o;
}

var siteCount = 16;
var shapeCount = 6;
var sites = [];
var bases = [];
for (var s = 0; s < siteCount; ++s) {
sites.push(makeSite());
var group = [];
for (var shape = 0; shape < shapeCount; ++shape)
group.push(makeBase(shape + s * shapeCount, shape));
bases.push(group);
}

function churn() {
var last = "";
for (var i = 0; i < 20000; ++i)
last = ("a" + i) + ("b" + i);
return last.length;
}

for (var round = 0; round < 40; ++round) {
for (var s = 0; s < siteCount; ++s) {
var site = sites[s];
var group = bases[s];
for (var i = 0; i < 24; ++i) {
var shape = i % shapeCount;
shouldBe(site(group[shape], freshKey()), shape);
}
}

churn();
gc();
churn();
edenGC();

// Integer subscripts compare against every buffered pair on the site.
for (var s = 0; s < siteCount; ++s) {
var group = bases[s];
for (var i = 0; i < shapeCount; ++i)
shouldBe(sites[s](group[i], 0), i + 1000);
}

// String subscripts compare against them too, with keys that are fresh cells again.
for (var s = 0; s < siteCount; ++s) {
var group = bases[s];
for (var i = 0; i < shapeCount; ++i)
shouldBe(sites[s](group[i], freshKey()), i);
}
}
17 changes: 17 additions & 0 deletions JSTests/stress/inline-cache-proxy-and-getter-spill-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// A stub records one spill state shared by all of its access cases, so every case that calls out
// to JS has to spill the same registers. This inline cache sees both a Proxy and a JS getter.
const target = { p: 1 };
const proxy = new Proxy(target, { get(t, k) { return t[k]; } });
const withGetter = {};
Object.defineProperty(withGetter, "p", { get: function () { return 2; } });

function f(o) { return o.p; }
noInline(f);

globalThis.testLoopCount ??= 1e4;
for (let i = 0; i < testLoopCount; ++i) {
if (f(proxy) !== 1)
throw new Error("proxy load returned the wrong value");
if (f(withGetter) !== 2)
throw new Error("getter load returned the wrong value");
}
36 changes: 36 additions & 0 deletions JSTests/stress/instanceof-osr-exit-from-inlined-proxy-get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//@ runDefault("--forceOSRExitToLLInt=true", "--thresholdForJITAfterWarmUp=10", "--thresholdForOptimizeAfterWarmUp=20")

// The DFG inlines a proxy's get trap for the Symbol.hasInstance and prototype reads that instanceof
// performs. An OSR exit out of that inlined call returns into the LLInt at op_instanceof's return
// location, which has to finish the operation rather than fall through to the next opcode with the
// destination register still holding whatever was there before.

function assert(b) {
if (!b)
throw new Error("Bad assertion");
}

function test(f) {
for (let i = 0; i < 1000; i++)
f();
}

// A pass-through proxy, so this instanceof is genuinely true and the site caches a hit first.
test(function() {
let proxy = new Proxy(function () { }, { });
assert(new proxy instanceof proxy);
});

// The trap hands back a fresh object every time "prototype" is read, so the instance is never an
// instanceof the proxy.
test(function() {
let handler = {
get: function(target, prop) {
if (prop === "prototype")
return { };
return target[prop];
}
};
let proxy = new Proxy(function () { }, handler);
assert(!(new proxy instanceof proxy));
});
19 changes: 19 additions & 0 deletions JSTests/stress/instanceof-osr-exit-hasInstance-getter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function F1() {
Object instanceof Proxy;
}
noInline(F1);

let count = 0;
function f20() {
count++;
OSRExit();
return () => { };
}
Object.defineProperty(Proxy, Symbol.hasInstance, { get: f20 });

globalThis.testLoopCount ??= 1e4;
for (let i = 0; i < testLoopCount; i++) {
F1();
}
if (count != testLoopCount)
throw new Error("bad!");
19 changes: 19 additions & 0 deletions JSTests/stress/instanceof-osr-exit-prototype-getter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function F1() {
Object instanceof Proxy;
}
noInline(F1);

let count = 0;
function f20() {
count++;
OSRExit();
return f20;
}
Object.defineProperty(Proxy, "prototype", { get: f20 });

globalThis.testLoopCount ??= 1e4;
for (let i = 0; i < testLoopCount; i++) {
F1();
}
if (count != testLoopCount)
throw new Error("bad!");
22 changes: 22 additions & 0 deletions JSTests/stress/op-catch-restores-metadata-table-register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// A strict-mode indexed store whose Proxy set trap returns false throws out of the by-val inline
// cache. Nothing between the throw and the handler copies the handler frame's callee saves into the
// entry frame buffer, so op_catch restores a metadata table belonging to another CodeBlock and has to
// rematerialize metadataTableRegister. A named store (proxy.x) takes a different path and does not
// reach this, so keep the subscript numeric.

globalThis.testLoopCount ??= 1e4;

(function() {
"use strict";
const proxy = new Proxy({}, { set: function() { return false; } });
for (let i = 0; i < testLoopCount; ++i) {
let threw = false;
try {
proxy[42] = 40;
} catch (e) {
threw = e instanceof TypeError;
}
if (!threw)
throw new Error("strict indexed store through a rejecting proxy set trap should throw a TypeError");
}
})();
88 changes: 88 additions & 0 deletions JSTests/stress/proxy-ic-does-not-clobber-callee-saves.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// A proxy inline cache emits a JS call, and values the enclosing JIT holds in callee save registers
// have to survive it. Each function below keeps more live integers than ARMv7 has allocatable GPRs,
// so some of them land in the registers that hold metadataTable and jitData there.

function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error(`bad value: ${actual}, expected ${expected}`);
}

const handler = {
get(target, property) {
return target[property];
},
set(target, property, value) {
target[property] = value;
return true;
},
};

const loadProxy = new Proxy({ field: 42 }, handler);
const indexedProxy = new Proxy({ 0: 7 }, handler);
const storeProxy = new Proxy({ field: 0 }, handler);

function load(proxy, x) {
const v0 = (x + 1) | 0;
const v1 = (x + 2) | 0;
const v2 = (x + 3) | 0;
const v3 = (x + 4) | 0;
const v4 = (x + 5) | 0;
const v5 = (x + 6) | 0;
const v6 = (x + 7) | 0;
const v7 = (x + 8) | 0;
const v8 = (x + 9) | 0;
const v9 = (x + 10) | 0;
const v10 = (x + 11) | 0;
const v11 = (x + 12) | 0;
const got = proxy.field;
return (v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + v9 + v10 + v11 + got) | 0;
}
noInline(load);

function loadByVal(proxy, x, index) {
const v0 = (x + 1) | 0;
const v1 = (x + 2) | 0;
const v2 = (x + 3) | 0;
const v3 = (x + 4) | 0;
const v4 = (x + 5) | 0;
const v5 = (x + 6) | 0;
const v6 = (x + 7) | 0;
const v7 = (x + 8) | 0;
const v8 = (x + 9) | 0;
const v9 = (x + 10) | 0;
const v10 = (x + 11) | 0;
const v11 = (x + 12) | 0;
const got = proxy[index];
return (v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + v9 + v10 + v11 + got) | 0;
}
noInline(loadByVal);

function store(proxy, x) {
const v0 = (x + 1) | 0;
const v1 = (x + 2) | 0;
const v2 = (x + 3) | 0;
const v3 = (x + 4) | 0;
const v4 = (x + 5) | 0;
const v5 = (x + 6) | 0;
const v6 = (x + 7) | 0;
const v7 = (x + 8) | 0;
const v8 = (x + 9) | 0;
const v9 = (x + 10) | 0;
const v10 = (x + 11) | 0;
const v11 = (x + 12) | 0;
proxy.field = x;
return (v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7 + v8 + v9 + v10 + v11) | 0;
}
noInline(store);

// The first call runs before any inline cache exists, so its result is the oracle for every tier.
const expectedLoad = load(loadProxy, 1);
const expectedLoadByVal = loadByVal(indexedProxy, 1, 0);
const expectedStore = store(storeProxy, 1);

for (let i = 0; i < 5e4; ++i) {
shouldBe(load(loadProxy, 1), expectedLoad);
shouldBe(loadByVal(indexedProxy, 1, 0), expectedLoadByVal);
shouldBe(store(storeProxy, 1), expectedStore);
shouldBe(storeProxy.field, 1);
}
74 changes: 74 additions & 0 deletions JSTests/wasm/stress/armv7-fused-branch-compare-unsigned-ge-zero.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//@ requireOptions("--useBBQJIT=1", "--useWasmLLInt=0", "--useOMGJIT=0")

import { instantiate } from "../wabt-wrapper.js"
import * as assert from "../assert.js"

// BBQ fuses a comparison into a following br_if/if and emits the inverted condition, so
// the jump it hands back is the "branch not taken" edge. i64.ge_u inverts to Below, and
// every unsigned value is >= 0, so these reach the never-taken case of
// MacroAssemblerARMv7::branch64(Below, hi, lo, TrustedImm64(0)).
let wat = `
(module
(func (export "geUnsignedZeroBrIf") (param $x i64) (result i32)
(block $done
(br_if $done (i64.ge_u (local.get $x) (i64.const 0)))
(return (i32.const 0))
)
(i32.const 1)
)

;; Constant on the left: i64.le_u inverts to Above, which emitBranchI64 commutes to Below.
(func (export "zeroLeUnsignedBrIf") (param $x i64) (result i32)
(block $done
(br_if $done (i64.le_u (i64.const 0) (local.get $x)))
(return (i32.const 0))
)
(i32.const 1)
)

;; The if-fusion path, which stores the jump in ControlData::m_ifBranch.
(func (export "geUnsignedZeroIf") (param $x i64) (result i32)
(if (result i32) (i64.ge_u (local.get $x) (i64.const 0))
(then (i32.const 1))
(else (i32.const 0))
)
)

;; i64.lt_u inverts to AboveOrEqual, the always-taken sibling of the case above.
(func (export "ltUnsignedZeroBrIf") (param $x i64) (result i32)
(block $done
(br_if $done (i64.lt_u (local.get $x) (i64.const 0)))
(return (i32.const 0))
)
(i32.const 1)
)

;; A non-zero constant is genuinely conditional and misses the compare-with-zero paths.
(func (export "geUnsignedTenBrIf") (param $x i64) (result i32)
(block $done
(br_if $done (i64.ge_u (local.get $x) (i64.const 10)))
(return (i32.const 0))
)
(i32.const 1)
)
)
`

async function test() {
const instance = await instantiate(wat, {}, {})
const { geUnsignedZeroBrIf, zeroLeUnsignedBrIf, geUnsignedZeroIf, ltUnsignedZeroBrIf, geUnsignedTenBrIf } = instance.exports

// Cover values whose high word, low word, or neither is zero.
for (const x of [0n, 1n, 0xffffffffn, 0x100000000n, 0xffffffff00000000n, 0xffffffffffffffffn]) {
assert.eq(geUnsignedZeroBrIf(x), 1)
assert.eq(zeroLeUnsignedBrIf(x), 1)
assert.eq(geUnsignedZeroIf(x), 1)
assert.eq(ltUnsignedZeroBrIf(x), 0)
}

assert.eq(geUnsignedTenBrIf(9n), 0)
assert.eq(geUnsignedTenBrIf(10n), 1)
assert.eq(geUnsignedTenBrIf(0xffffffffffffffffn), 1)
}

await assert.asyncTest(test())
1 change: 1 addition & 0 deletions JSTests/wasm/stress/array-element-creation.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//@ skip if $memoryLimited
//@ runDefault("--useConcurrentJIT=0")

function main() {
Expand Down
Loading