blob: e061d95156b4b2d7d19c01404ea49a31cefe4276 [file] [edit]
// Exercises the recognition of loops that copy or fill linear memory one byte per iteration,
// which OMG compiles as a bulk memory operation instead. The bulk form is only faithful to the
// loop for some inputs, so every case here is checked against a JavaScript model of the loop
// itself: copies that overlap the wrong way replicate bytes rather than move them, a copy that
// runs off the end of memory traps, and the loop leaves its pointer and counter locals at
// specific values.
import { instantiate } from "../wabt-wrapper.js";
import * as assert from "../assert.js";
const PAGE = 65536;
// Every case in the matrix below touches memory inside this window, which leaves room on both sides
// for a bulk operation that wrote outside the loop's range to show up as a difference.
const WINDOW_BEGIN = 512;
const WINDOW_END = 3072;
const golden = new Uint8Array(PAGE);
for (let i = 0; i < PAGE; ++i)
golden[i] = (i * 31 + 7) & 0xff;
let wat = `
(module
(memory (export "mem") 1)
(global $d (mut i32) (i32.const 0))
(global $s (mut i32) (i32.const 0))
(global $n (mut i32) (i32.const 0))
;; for (; n; --n) mem[d++] = mem[s++];
(func (export "copyForward") (param i32 i32 i32)
(local i32)
local.get 2
if
local.get 0
local.set 3
loop
local.get 3
local.get 1
i32.load8_u
i32.store8
local.get 3
i32.const 1
i32.add
local.set 3
local.get 1
i32.const 1
i32.add
local.set 1
local.get 2
i32.const 1
i32.sub
local.tee 2
br_if 0
end
end
local.get 3
global.set $d
local.get 1
global.set $s
local.get 2
global.set $n)
;; for (; n; --n) mem[--d] = mem[--s]; -- the caller passes the ends of the regions.
(func (export "copyBackward") (param i32 i32 i32)
(local i32)
local.get 2
if
local.get 0
local.set 3
loop
local.get 3
i32.const 1
i32.sub
local.tee 3
local.get 1
i32.const 1
i32.sub
local.tee 1
i32.load8_u
i32.store8
local.get 2
i32.const 1
i32.sub
local.tee 2
br_if 0
end
end
local.get 3
global.set $d
local.get 1
global.set $s
local.get 2
global.set $n)
;; for (; n; --n) mem[d++] = v;
(func (export "fill") (param i32 i32 i32)
(local i32)
local.get 2
if
local.get 0
local.set 3
loop
local.get 3
local.get 1
i32.store8
local.get 3
i32.const 1
i32.add
local.set 3
local.get 2
i32.const 1
i32.sub
local.tee 2
br_if 0
end
end
local.get 3
global.set $d
local.get 2
global.set $n)
(func (export "readD") (result i32) global.get $d)
(func (export "readS") (result i32) global.get $s)
(func (export "readN") (result i32) global.get $n)
)
`;
// The models throw where the wasm loop would trap, so the same function serves both the in-bounds
// cases and the out-of-bounds ones.
function access(i)
{
if (i < 0 || i >= PAGE)
throw new RangeError("out of bounds");
return i;
}
function modelCopyForward(memory, d, s, n)
{
if (n) {
do {
memory[access(d)] = memory[access(s)];
d = (d + 1) | 0;
s = (s + 1) | 0;
} while (--n);
}
return [d, s, n];
}
function modelCopyBackward(memory, d, s, n)
{
if (n) {
do {
d = (d - 1) | 0;
s = (s - 1) | 0;
memory[access(d)] = memory[access(s)];
} while (--n);
}
return [d, s, n];
}
function modelFill(memory, d, v, n)
{
if (n) {
do {
memory[access(d)] = v & 0xff;
d = (d + 1) | 0;
} while (--n);
}
return [d, n];
}
// Lengths bracket the sizes at which a bulk copy switches between vector, 64-bit, 32-bit and
// byte moves, and the addresses cover overlap in both directions at several distances.
const lengths = [1, 2, 3, 7, 8, 15, 16, 17, 31, 63, 127, 128, 129, 300];
const addressPairs = [[1000, 2000], [2000, 1000], [1000, 1001], [1001, 1000], [1000, 1003], [1003, 1000], [1000, 1000]];
async function test()
{
const instance = await instantiate(wat, {}, {});
const { copyForward, copyBackward, fill, readD, readS, readN, mem } = instance.exports;
const memory = new Uint8Array(mem.buffer);
const expected = new Uint8Array(PAGE);
const goldenWindow = golden.subarray(WINDOW_BEGIN, WINDOW_END);
function seedWindow()
{
memory.set(goldenWindow, WINDOW_BEGIN);
expected.set(goldenWindow, WINDOW_BEGIN);
}
function seedEverything()
{
memory.set(golden);
expected.set(golden);
}
function assertMemoryMatches(begin, end)
{
let i = begin;
while (i < end && memory[i] === expected[i])
++i;
assert.eq(i, end, "memory differs at index " + i);
}
function checkEveryCase()
{
for (const n of lengths) {
for (const [d, s] of addressPairs.concat([[1000, 1000 + n], [1000 + n, 1000]])) {
seedWindow();
copyForward(d, s, n);
assert.eq(modelCopyForward(expected, d, s, n), [readD(), readS(), readN()]);
assertMemoryMatches(WINDOW_BEGIN, WINDOW_END);
seedWindow();
copyBackward(d, s, n);
assert.eq(modelCopyBackward(expected, d, s, n), [readD(), readS(), readN()]);
assertMemoryMatches(WINDOW_BEGIN, WINDOW_END);
seedWindow();
fill(d, s, n);
assert.eq(modelFill(expected, d, s, n), [readD(), readN()]);
assertMemoryMatches(WINDOW_BEGIN, WINDOW_END);
}
}
}
// Check every case below OMG, then warm the functions up until OMG compiles them and check every
// case again there, since only OMG replaces the loops with a bulk operation.
for (let repeat = 0; repeat < 2; ++repeat) {
checkEveryCase();
for (let i = 0; i < wasmTestLoopCount; ++i) {
copyForward(1000, 2000, 64);
copyBackward(1500, 2500, 64);
fill(1000, 0x5a, 64);
}
}
checkEveryCase();
// A copy that runs off the end of memory must trap. How far it got before trapping is left
// unchecked, since the bulk operation and the loop need not agree on that.
function assertTraps(copy, model, d, s, n)
{
seedEverything();
assert.throws(() => model(expected, d, s, n), RangeError, "out of bounds");
assert.throws(() => copy(d, s, n), WebAssembly.RuntimeError, "Out of bounds memory access");
}
assertTraps(copyForward, modelCopyForward, PAGE - 10, 0, 40);
assertTraps(copyForward, modelCopyForward, 0, PAGE - 10, 40);
assertTraps(copyBackward, modelCopyBackward, 10, 4000, 40);
assertTraps(copyBackward, modelCopyBackward, 4000, 10, 40);
// Right up against the end of memory is in bounds and must not trap.
seedEverything();
copyForward(PAGE - 40, 0, 40);
modelCopyForward(expected, PAGE - 40, 0, 40);
assertMemoryMatches(0, PAGE);
}
await assert.asyncTest(test());