diff options
Diffstat (limited to 'js/src/jit-test/tests/wasm/gc')
36 files changed, 5345 insertions, 0 deletions
diff --git a/js/src/jit-test/tests/wasm/gc/TypedObject.js b/js/src/jit-test/tests/wasm/gc/TypedObject.js new file mode 100644 index 0000000000..93eae428c4 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/TypedObject.js @@ -0,0 +1,121 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// We can read the object fields from JS + +{ + let ins = wasmEvalText(`(module + (type $p (struct (field f64) (field (mut i32)))) + + (func (export "mkp") (result eqref) + (struct.new $p (f64.const 1.5) (i32.const 33))))`).exports; + + let p = ins.mkp(); + assertEq(p[0], 1.5); + assertEq(p[1], 33); + assertEq(p[2], undefined); +} + +// Writing an immutable field from JS throws. + +{ + let ins = wasmEvalText(`(module + (type $p (struct (field f64))) + + (func (export "mkp") (result eqref) + (struct.new $p (f64.const 1.5))))`).exports; + + let p = ins.mkp(); + assertErrorMessage(() => p[0] = 5.7, + Error, + /setting immutable field/); +} + +// MVA v1 restriction: structs have no prototype + +{ + let ins = wasmEvalText(`(module + (type $q (struct (field (mut f64)))) + (type $p (struct (field (mut (ref null $q))))) + + (type $r (struct (field (mut eqref)))) + + (func (export "mkp") (result eqref) + (struct.new $p (ref.null $q))) + + (func (export "mkr") (result eqref) + (struct.new $r (ref.null eq))))`).exports; + + assertEq(Object.getPrototypeOf(ins.mkp()), null); + assertEq(Object.getPrototypeOf(ins.mkr()), null); +} + +// MVA v1 restriction: all fields are immutable + +{ + let ins = wasmEvalText(`(module + (type $q (struct (field (mut f64)))) + (type $p (struct (field (mut (ref null $q))) (field (mut eqref)))) + + (func (export "mkq") (result eqref) + (struct.new $q (f64.const 1.5))) + + (func (export "mkp") (result eqref) + (struct.new $p (ref.null $q) (ref.null eq))))`).exports; + let q = ins.mkq(); + assertEq(typeof q, "object"); + assertEq(q[0], 1.5); + + let p = ins.mkp(); + assertEq(typeof p, "object"); + assertEq(p[0], null); + + assertErrorMessage(() => { p[0] = q }, + Error, + /setting immutable field/); + + assertErrorMessage(() => { p[1] = q }, + Error, + /setting immutable field/); +} + +// MVA v1 restriction: structs that expose i64 fields make those fields +// immutable from JS, and the structs are not constructible from JS. + +{ + let ins = wasmEvalText(`(module + (type $p (struct (field (mut i64)))) + (func (export "mkp") (result eqref) + (struct.new $p (i64.const 0x1234567887654321))))`).exports; + + let p = ins.mkp(); + assertEq(typeof p, "object"); + assertEq(p[0], 0x1234567887654321n) + + assertErrorMessage(() => { p[0] = 0 }, + Error, + /setting immutable field/); +} + +// A consequence of the current mapping of i64 as two i32 fields is that we run +// a risk of struct.narrow not recognizing the difference. So check this. + +{ + let ins = wasmEvalText( + `(module + (type $p (struct (field i64))) + (type $q (struct (field i32) (field i32))) + (func $f (param eqref) (result i32) + local.get 0 + ref.test (ref $q) + ) + (func $g (param eqref) (result i32) + local.get 0 + ref.test (ref $p) + ) + (func (export "t1") (result i32) + (call $f (struct.new $p (i64.const 0)))) + (func (export "t2") (result i32) + (call $g (struct.new $q (i32.const 0) (i32.const 0)))))`).exports; + assertEq(ins.t1(), 0); + assertEq(ins.t2(), 0); +} diff --git a/js/src/jit-test/tests/wasm/gc/arrays.js b/js/src/jit-test/tests/wasm/gc/arrays.js new file mode 100644 index 0000000000..44770b85ea --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/arrays.js @@ -0,0 +1,1104 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Test array instructions on different valtypes + +const GENERAL_TESTS = [ + [ + 'i32', + 0, + 0xce, + ], [ + 'i64', + 0n, + 0xabcdefn, + ], [ + 'f32', + 0, + 13.5, + ], [ + 'f64', + 0, + 13.5, + ], [ + 'externref', + null, + 'hello', + ], +]; + +for (let [valtype, def, nondef] of GENERAL_TESTS) { + let {create, createDefault, get, set, len} = wasmEvalText(`(module + (type $a (array (mut ${valtype}))) + + (; new T[0] = { 1... } ;) + (func (export "create") (param i32 ${valtype}) (result eqref) + local.get 1 + local.get 0 + array.new $a + ) + + (; new T[0] ;) + (func (export "createDefault") (param i32) (result eqref) + local.get 0 + array.new_default $a + ) + + (; 0[1] ;) + (func (export "get") (param eqref i32) (result ${valtype}) + local.get 0 + ref.cast (ref null $a) + local.get 1 + array.get $a + ) + + (; 0[1] = 2 ;) + (func (export "set") (param eqref i32 ${valtype}) + local.get 0 + ref.cast (ref null $a) + local.get 1 + local.get 2 + array.set $a + ) + + (; len(a) ;) + (func (export "len") (param eqref) (result i32) + local.get 0 + ref.cast (ref null $a) + array.len + ) + )`).exports; + + function checkArray(array, length, init, setval) { + // Check length + assertEq(len(array), length); + assertEq(array.length, length); + + // Check init value + for (let i = 0; i < length; i++) { + assertEq(array[i], init); + assertEq(get(array, i), init); + } + + // Set every element to setval + for (let i = 0; i < length; i++) { + set(array, i, setval); + + // Check there is no overwrite + for (let j = i + 1; j < length; j++) { + assertEq(array[j], init); + assertEq(get(array, j), init); + } + } + + // Check out of bounds conditions + for (let loc of [-1, length, length + 1]) { + assertErrorMessage(() => { + get(array, loc); + }, WebAssembly.RuntimeError, /out of bounds/); + assertErrorMessage(() => { + set(array, loc, setval); + }, WebAssembly.RuntimeError, /out of bounds/); + } + } + + for (let arrayLength = 0; arrayLength < 5; arrayLength++) { + checkArray(createDefault(arrayLength), arrayLength, def, nondef); + checkArray(create(arrayLength, nondef), arrayLength, nondef, def); + } +} + +// Check packed array reads and writes + +for (let [fieldtype, max] of [ + [ + 'i8', + 0xff, + ], [ + 'i16', + 0xffff, + ] +]) { + let {create, getS, getU, set} = wasmEvalText(`(module + (type $a (array (mut ${fieldtype}))) + + (; new T[0] = { 1... } ;) + (func (export "create") (param i32 i32) (result eqref) + local.get 1 + local.get 0 + array.new $a + ) + + (; 0[1] ;) + (func (export "getS") (param eqref i32) (result i32) + local.get 0 + ref.cast (ref null $a) + local.get 1 + array.get_s $a + ) + + (; 0[1] ;) + (func (export "getU") (param eqref i32) (result i32) + local.get 0 + ref.cast (ref null $a) + local.get 1 + array.get_u $a + ) + + (; 0[1] = 2 ;) + (func (export "set") (param eqref i32 i32) + local.get 0 + ref.cast (ref null $a) + local.get 1 + local.get 2 + array.set $a + ) + )`).exports; + + // Check zero and sign extension + let a = create(1, 0); + set(a, 0, max); + assertEq(getS(a, 0), -1); + assertEq(getU(a, 0), max); + + // JS-API defaults to sign extension + assertEq(a[0], getS(a, 0)); + + // Check array.new truncates init value + assertEq(create(1, max + 1)[0], 0); + + // Check array.set truncates + let b = create(1, 0); + set(b, 0, max + 1); + assertEq(getU(b, 0), 0); + + // Check no overwrite on set + let c = create(2, 0); + set(c, 0, max << 4); + assertEq(getU(c, 0), (max << 4) & max); + assertEq(getU(c, 1), 0); +} + +// Check arrays must be mutable to mutate + +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array i32)) + (func + (array.set $a + (array.new $a + i32.const 0xff + i32.const 10) + i32.const 0 + i32.const 0 + ) + ) +) +`), WebAssembly.CompileError, /array is not mutable/); + +// Check operations trap on null + +assertErrorMessage(() => { + wasmEvalText(`(module + (type $a (array (mut i32))) + (func + ref.null $a + i32.const 0 + array.get $a + drop + ) + (start 0) + )`); +}, WebAssembly.RuntimeError, /null/); + +assertErrorMessage(() => { + wasmEvalText(`(module + (type $a (array (mut i32))) + (func + ref.null $a + i32.const 0 + i32.const 0 + array.set $a + ) + (start 0) + )`); +}, WebAssembly.RuntimeError, /null/); + +assertErrorMessage(() => { + wasmEvalText(`(module + (type $a (array (mut i32))) + (func + ref.null $a + array.len + drop + ) + (start 0) + )`); +}, WebAssembly.RuntimeError, /null/); + +// Check an extension postfix is present iff the element is packed + +for ([fieldtype, packed] of [ + ['i8', true], + ['i16', true], + ['i32', false], + ['i64', false], + ['f32', false], + ['f64', false], +]) { + let extensionModule = `(module + (type $a (array ${fieldtype})) + (func + ref.null $a + i32.const 0 + array.get_s $a + drop + ) + (func + ref.null $a + i32.const 0 + array.get_u $a + drop + ) + )`; + let noExtensionModule = `(module + (type $a (array ${fieldtype})) + (func + ref.null $a + i32.const 0 + array.get $a + drop + ) + )`; + + if (packed) { + wasmValidateText(extensionModule); + wasmFailValidateText(noExtensionModule, /must specify signedness/); + } else { + wasmFailValidateText(extensionModule, /must not specify signedness/); + wasmValidateText(noExtensionModule); + } +} + +////////////////////////////////////////////////////////////////////////////// +// +// array.new_fixed +/* + validation: + array-type imm-operand needs to be "in range" + array-type imm-operand must refer to an array type + operands (on stack) must all match ("be compatible with") the array elem + type + number of operands (on stack) must not be less than the num-of-elems + imm-operand + zero elements doesn't fail compilation + reftypes elements doesn't fail compilation + trying to create a 1-billion-element array fails gracefully + run: + resulting 4-element array is as expected + resulting zero-element array is as expected + resulting 30-element array is as expected +*/ + +// validation: array-type imm-operand needs to be "in range" +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array i8)) + (func (result eqref) + i32.const 66 + i32.const 77 + array.new_fixed 2 2 ;; type index 2 is the first invalid one + ) +) +`), WebAssembly.CompileError, /type index out of range/); + +// validation: array-type imm-operand must refer to an array type +assertErrorMessage(() => wasmEvalText(`(module + (type $a (func (param f64) (result f64))) + (func (result eqref) + i32.const 66 + i32.const 77 + array.new_fixed $a 2 + ) +) +`), WebAssembly.CompileError, /not an array type/); + +// validation: operands (on stack) must all match ("be compatible with") +// the array elem type +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array i32)) + (func (result eqref) + f32.const 66.6 + f64.const 77.7 + array.new_fixed $a 2 + ) +) +`), WebAssembly.CompileError, /expression has type f64 but expected i32/); + +// validation: number of operands (on stack) must not be less than the +// num-of-elems imm-operand +assertNoWarning(() => wasmEvalText(`(module + (type $a (array f32)) + (func + f64.const 66.6 ;; we won't put this in the array + f32.const 77.7 + f32.const 88.8 + array.new_fixed $a 2 ;; use up 88.8 and 77.7 and replace with array + drop ;; dump the array + f64.const 99.9 + f64.mul ;; check the 66.6 value is still on the stack + drop ;; now should be empty + ) +) +`)); +// (more) +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array i64)) + (func (param i64) (result eqref) + local.get 0 + array.new_fixed $a 2 + ) +) +`), WebAssembly.CompileError, /popping value from empty stack/); + +// validation: zero elements doesn't fail compilation +assertNoWarning(() => wasmEvalText(`(module + (type $a (array i32)) + (func (result eqref) + array.new_fixed $a 0 + ) +) +`)); + +// validation: reftyped elements doesn't fail compilation +assertNoWarning(() => wasmEvalText(`(module + (type $a (array eqref)) + (func (param eqref) (result eqref) + local.get 0 + array.new_fixed $a 1 + ) +) +`)); + +// validation: trying to create a 1-billion-element array fails gracefully +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array f32)) + (func (export "newFixed") (result eqref) + f32.const 1337.0 + f32.const 4771.0 + array.new_fixed $a 1000000000 + ) +) +`), WebAssembly.CompileError, /popping value from empty stack/); + +// run: resulting 4-element array is as expected +{ + let { newFixed } = wasmEvalText(`(module + (type $a (array i8)) + (func (export "newFixed") (result eqref) + i32.const 66 + i32.const 77 + i32.const 88 + i32.const 99 + array.new_fixed $a 4 + ) + )`).exports; + let a = newFixed(); + assertEq(a.length, 4); + assertEq(a[0], 66); + assertEq(a[1], 77); + assertEq(a[2], 88); + assertEq(a[3], 99); +} + +// run: resulting zero-element array is as expected +{ + let { newFixed } = wasmEvalText(`(module + (type $a (array i16)) + (func (export "newFixed") (result eqref) + array.new_fixed $a 0 + ) + )`).exports; + let a = newFixed(); + assertEq(a.length, 0); +} + +// run: resulting 30-element array is as expected +{ + let { newFixed } = wasmEvalText(`(module + (type $a (array i16)) + (func (export "newFixed") (result eqref) + i32.const 1 + i32.const 2 + i32.const 3 + i32.const 4 + i32.const 5 + i32.const 6 + i32.const 7 + i32.const 8 + i32.const 9 + i32.const 10 + i32.const 11 + i32.const 12 + i32.const 13 + i32.const 14 + i32.const 15 + i32.const 16 + i32.const 17 + i32.const 18 + i32.const 19 + i32.const 20 + i32.const 21 + i32.const 22 + i32.const 23 + i32.const 24 + i32.const 25 + i32.const 26 + i32.const 27 + i32.const 28 + i32.const 29 + i32.const 30 + array.new_fixed $a 30 + ) + )`).exports; + let a = newFixed(); + assertEq(a.length, 30); + for (i = 0; i < 30; i++) { + assertEq(a[i], i + 1); + } +} + +////////////////////////////////////////////////////////////////////////////// +// +// array.new_data +/* + validation: + array-type imm-operand needs to be "in range" + array-type imm-operand must refer to an array type + array-type imm-operand must refer to an array of numeric elements + segment index must be "in range" + run: + if segment is "already used" (active, or passive that has subsequently + been dropped), then only a zero length array can be created + range to copy would require OOB read on data segment + resulting array is as expected +*/ + +// validation: array-type imm-operand needs to be "in range" +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array i8)) + (data $d "1337") + (func (export "newData") (result eqref) + (; offset=0 into data ;) i32.const 0 + (; size=4 into data ;) i32.const 4 + array.new_data 2 $d + ) +)`), WebAssembly.CompileError, /type index out of range/); + +// validation: array-type imm-operand must refer to an array type +assertErrorMessage(() => wasmEvalText(`(module + (type $a (func (param f32) (result f32))) + (data $d "1337") + (func (export "newData") (result eqref) + (; offset=0 into data ;) i32.const 0 + (; size=4 into data ;) i32.const 4 + array.new_data $a $d + ) +)`), WebAssembly.CompileError, /not an array type/); + +// validation: array-type imm-operand must refer to an array of numeric elements +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array eqref)) + (data $d "1337") + (func (export "newData") (result eqref) + (; offset=0 into data ;) i32.const 0 + (; size=4 into data ;) i32.const 4 + array.new_data $a $d + ) +)`), WebAssembly.CompileError, + /element type must be i8\/i16\/i32\/i64\/f32\/f64\/v128/); + +// validation: segment index must be "in range" +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array i8)) + (data $d "1337") + (func (export "newData") (result eqref) + (; offset=0 into data ;) i32.const 0 + (; size=4 into data ;) i32.const 4 + array.new_data $a 1 ;; 1 is the lowest invalid dseg index + ) +)`), WebAssembly.CompileError, /segment index is out of range/); + +// run: if segment is "already used" (active, or passive that has subsequently +// been dropped), then only a zero length array can be created #1 +{ + let { newData } = wasmEvalText(`(module + (memory 1) + (type $a (array i8)) + (data $d (offset (i32.const 0)) "1337") + (func (export "newData") (result eqref) + (; offset=0 into data ;) i32.const 0 + (; size=4 into data ;) i32.const 4 + array.new_data $a $d + ) + )`).exports; + assertErrorMessage(() => { + newData(); + },WebAssembly.RuntimeError, /index out of bounds/); +} + +// run: if segment is "already used" (active, or passive that has subsequently +// been dropped), then only a zero length array can be created #2 +{ + let { newData } = wasmEvalText(`(module + (memory 1) + (type $a (array i8)) + (data $d (offset (i32.const 0)) "1337") + (func (export "newData") (result eqref) + (; offset=4 into data ;) i32.const 4 + (; size=0 into data ;) i32.const 0 + array.new_data $a $d + ) + )`).exports; + assertErrorMessage(() => { + newData(); + },WebAssembly.RuntimeError, /index out of bounds/); +} + +// run: if segment is "already used" (active, or passive that has subsequently +// been dropped), then only a zero length array can be created #3 +{ + let { newData } = wasmEvalText(`(module + (memory 1) + (type $a (array i8)) + (data $d (offset (i32.const 0)) "1337") + (func (export "newData") (result eqref) + (; offset=0 into data ;) i32.const 0 + (; size=0 into data ;) i32.const 0 + array.new_data $a $d + ) + )`).exports; + let arr = newData(); + assertEq(arr.length, 0); +} + +// run: range to copy would require OOB read on data segment +{ + let { newData } = wasmEvalText(`(module + (type $a (array i8)) + (data $d "1337") + (func (export "newData") (result eqref) + (; offset=0 into data ;) i32.const 1 + (; size=4 into data ;) i32.const 4 + array.new_data $a $d + ) + )`).exports; + assertErrorMessage(() => { + newData(); + },WebAssembly.RuntimeError, /index out of bounds/); +} + +// run: resulting array is as expected +{ + let { newData } = wasmEvalText(`(module + (type $a (array i8)) + (data $other "\\\\9") + (data $d "1337") + (func (export "newData") (result eqref) + (; offset=0 into data ;) i32.const 0 + (; size=4 into data ;) i32.const 4 + array.new_data $a $d + ) + )`).exports; + let arr = newData(); + assertEq(arr.length, 4); + assertEq(arr[0], 48+1); + assertEq(arr[1], 48+3); + assertEq(arr[2], 48+3); + assertEq(arr[3], 48+7); +} + +////////////////////////////////////////////////////////////////////////////// +// +// array.new_elem +/* + validation: + array-type imm-operand needs to be "in range" + array-type imm-operand must refer to an array type + array-type imm-operand must refer to an array of ref typed elements + destination elem type must be a supertype of src elem type + segment index must be "in range" + run: + if segment is "already used" (active, or passive that has subsequently + been dropped), then only a zero length array can be created + range to copy would require OOB read on elem segment + resulting array is as expected +*/ + +// validation: array-type imm-operand needs to be "in range" +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array funcref)) + (elem $e funcref $f1 $f2 $f3 $f4) + (func $f1 (export "f1")) + (func $f2 (export "f2")) + (func $f3 (export "f3")) + (func $f4 (export "f4")) + (func (export "newElem") (result eqref) + (; offset=0 into elem ;) i32.const 0 + (; size=4 into elem ;) i32.const 4 + array.new_elem 3 $e + ) +)`), WebAssembly.CompileError, /type index out of range/); + +// validation: array-type imm-operand must refer to an array type +assertErrorMessage(() => wasmEvalText(`(module + (type $a (func (param i64) (result f64))) + (elem $e funcref $f1 $f2 $f3 $f4) + (func $f1 (export "f1")) + (func $f2 (export "f2")) + (func $f3 (export "f3")) + (func $f4 (export "f4")) + (func (export "newElem") (result eqref) + (; offset=0 into elem ;) i32.const 0 + (; size=4 into elem ;) i32.const 4 + array.new_elem $a $e + ) +)`), WebAssembly.CompileError, /not an array type/); + +// validation: array-type imm-operand must refer to an array of ref typed +// elements +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array f32)) + (elem $e funcref $f1 $f2 $f3 $f4) + (func $f1 (export "f1")) + (func $f2 (export "f2")) + (func $f3 (export "f3")) + (func $f4 (export "f4")) + (func (export "newElem") (result eqref) + (; offset=0 into elem ;) i32.const 0 + (; size=4 into elem ;) i32.const 4 + array.new_elem $a $e + ) +)`), WebAssembly.CompileError, /element type is not a reftype/); + +// validation: destination elem type must be a supertype of src elem type +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array eqref)) + (elem $e funcref $f1) + (func $f1 (export "f1")) + ;; The implied copy here is from elem-seg-of-funcrefs to + ;; array-of-eqrefs, which must fail, because funcref isn't + ;; a subtype of eqref. + (func (export "newElem") (result eqref) + (; offset=0 into elem ;) i32.const 0 + (; size=0 into elem ;) i32.const 0 + array.new_elem $a $e + ) +)`), WebAssembly.CompileError, /incompatible element types/); + +// validation: segment index must be "in range" +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array funcref)) + (elem $e funcref $f1 $f2 $f3 $f4) + (func $f1 (export "f1")) + (func $f2 (export "f2")) + (func $f3 (export "f3")) + (func $f4 (export "f4")) + (func (export "newElem") (result eqref) + (; offset=0 into elem ;) i32.const 0 + (; size=4 into elem ;) i32.const 4 + array.new_elem $a 1 ;; 1 is the lowest invalid eseg index + ) +)`), WebAssembly.CompileError, /segment index is out of range/); + +// run: if segment is "already used" (active, or passive that has subsequently +// been dropped), then only a zero length array can be created #1 +{ + let { newElem } = wasmEvalText(`(module + (table 4 funcref) + (type $a (array funcref)) + (elem $e (offset (i32.const 0)) funcref $f1 $f2 $f3 $f4) + (func $f1 (export "f1")) + (func $f2 (export "f2")) + (func $f3 (export "f3")) + (func $f4 (export "f4")) + (func (export "newElem") (result eqref) + (; offset=0 into elem ;) i32.const 0 + (; size=4 into elem ;) i32.const 4 + array.new_elem $a $e + ) + )`).exports; + assertErrorMessage(() => { + newElem(); + }, WebAssembly.RuntimeError, /index out of bounds/); +} + +// run: if segment is "already used" (active, or passive that has subsequently +// been dropped), then only a zero length array can be created #2 +{ + let { newElem } = wasmEvalText(`(module + (table 4 funcref) + (type $a (array funcref)) + (elem $e (offset (i32.const 0)) funcref $f1 $f2 $f3 $f4) + (func $f1 (export "f1")) + (func $f2 (export "f2")) + (func $f3 (export "f3")) + (func $f4 (export "f4")) + (func (export "newElem") (result eqref) + (; offset=4 into elem ;) i32.const 4 + (; size=0 into elem ;) i32.const 0 + array.new_elem $a $e + ) + )`).exports; + assertErrorMessage(() => { + newElem(); + }, WebAssembly.RuntimeError, /index out of bounds/); +} + +// run: if segment is "already used" (active, or passive that has subsequently +// been dropped), then only a zero length array can be created #3 +{ + let { newElem } = wasmEvalText(`(module + (table 4 funcref) + (type $a (array funcref)) + (elem $e (offset (i32.const 0)) funcref $f1 $f2 $f3 $f4) + (func $f1 (export "f1")) + (func $f2 (export "f2")) + (func $f3 (export "f3")) + (func $f4 (export "f4")) + (func (export "newElem") (result eqref) + (; offset=0 into elem ;) i32.const 0 + (; size=0 into elem ;) i32.const 0 + array.new_elem $a $e + ) + )`).exports; + let arr = newElem(); + assertEq(arr.length, 0); +} + +// run: range to copy would require OOB read on elem segment +{ + let { newElem } = wasmEvalText(`(module + (type $a (array funcref)) + (elem $e funcref $f1 $f2 $f3 $f4) + (func $f1 (export "f1")) + (func $f2 (export "f2")) + (func $f3 (export "f3")) + (func $f4 (export "f4")) + (func (export "newElem") (result eqref) + (; offset=0 into elem ;) i32.const 1 + (; size=4 into elem ;) i32.const 4 + array.new_elem $a $e + ) + )`).exports; + assertErrorMessage(() => { + newElem(); + },WebAssembly.RuntimeError, /index out of bounds/); +} + +// run: resulting array is as expected +{ + let { newElem, f1, f2, f3, f4 } = wasmEvalText(`(module + (type $a (array funcref)) + (elem $e funcref $f1 $f2 $f3 $f4) + (func $f1 (export "f1")) + (func $f2 (export "f2")) + (func $f3 (export "f3")) + (func $f4 (export "f4")) + (func (export "newElem") (result eqref) + (; offset=0 into elem ;) i32.const 0 + (; size=4 into elem ;) i32.const 4 + array.new_elem $a $e + ) + )`).exports; + let arr = newElem(); + assertEq(arr.length, 4); + assertEq(arr[0], f1); + assertEq(arr[1], f2); + assertEq(arr[2], f3); + assertEq(arr[3], f4); +} + +////////////////////////////////////////////////////////////////////////////// +// +// array.copy +/* + validation: + dest array must be mutable + validation: src and dest arrays must have compatible element types + run: + check for OOB conditions on src/dest arrays for non-zero length copies + check for OOB conditions on src/dest arrays for zero length copies + check resulting arrays are as expected + check that null src or dest array causes a trap +*/ + +// validation: dest array must be mutable +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array i32)) + (func (param eqref) + (array.copy $a $a (local.get 0) (i32.const 1) + (local.get 0) (i32.const 2) (i32.const 3)) + ) +) +`), WebAssembly.CompileError, /array is not mutable/); + +// validation: src and dest arrays must have compatible element types #1 +assertErrorMessage(() => wasmEvalText(`(module + (type $a32 (array (mut i32))) + (type $a8 (array i8)) + (func (param eqref) + (array.copy $a32 $a8 (local.get 0) (i32.const 1) + (local.get 0) (i32.const 2) (i32.const 3)) + ) +) +`), WebAssembly.CompileError, /incompatible element types/); + +// validation: src and dest arrays must have compatible element types #2 +assertErrorMessage(() => wasmEvalText(`(module + (type $a64 (array (mut i64))) + (type $aER (array eqref)) + (func (param eqref) + (array.copy $a64 $aER (local.get 0) (i32.const 1) + (local.get 0) (i32.const 2) (i32.const 3)) + ) +) +`), WebAssembly.CompileError, /incompatible element types/); + +// validation: src and dest arrays must have compatible element types #3 +// +// We can only copy from a child (sub) reftype to a parent (super) reftype [or +// to the same reftype.] Here, we have an array of eqref and an array of +// arrays. An array is a child type of eqref, so it's invalid to try and copy +// eqrefs into the array of arrays. +assertErrorMessage(() => wasmEvalText(`(module + (type $ty (array i32)) + (type $child (array (mut (ref $ty)))) + (type $parent (array (mut eqref))) + (func (param (ref null $child) (ref null $parent)) + ;; implied copy from parent to child -> not allowed + (array.copy $child $parent (local.get 1) (i32.const 1) + (local.get 0) (i32.const 2) (i32.const 3)) + ) +) +`), WebAssembly.CompileError, /incompatible element types/); + +// run: check for OOB conditions on src/dest arrays for non-zero length copies +// run: check for OOB conditions on src/dest arrays for zero length copies +// run: check resulting arrays are as expected +const ARRAY_COPY_TESTS = [ + // Format is: + // array element type + // corresponding value type + // source array + // first expected result + // second expected result + // + // Value-type cases + [ 'i32', 'i32', + [22, 33, 44, 55, 66, 77], + [22, 55, 66, 77, 66, 77], + [22, 33, 22, 33, 44, 77] + ], + [ 'i64', 'i64', + [0x2022002220002n, 0x3033003330003n, 0x4044004440004n, + 0x5055005550005n, 0x6066006660006n, 0x7077007770007n], + [0x2022002220002n, 0x5055005550005n, 0x6066006660006n, + 0x7077007770007n, 0x6066006660006n, 0x7077007770007n], + [0x2022002220002n, 0x3033003330003n, 0x2022002220002n, + 0x3033003330003n, 0x4044004440004n, 0x7077007770007n] + ], + [ 'f32', 'f32', + [22.0, 33.0, 44.0, 55.0, 66.0, 77.0], + [22.0, 55.0, 66.0, 77.0, 66.0, 77.0], + [22.0, 33.0, 22.0, 33.0, 44.0, 77.0] + ], + [ 'f64', 'f64', + [22.0, 33.0, 44.0, 55.0, 66.0, 77.0], + [22.0, 55.0, 66.0, 77.0, 66.0, 77.0], + [22.0, 33.0, 22.0, 33.0, 44.0, 77.0] + ], + [ 'externref', 'externref', + ['two', 'three', 'four', 'five', 'six', 'seven'], + ['two', 'five', 'six', 'seven', 'six', 'seven'], + ['two', 'three', 'two', 'three', 'four', 'seven'] + ], + // non-Value-type cases + [ 'i8', 'i32', + [22, 33, 44, 55, 66, 77], + [22, 55, 66, 77, 66, 77], + [22, 33, 22, 33, 44, 77] + ], + [ 'i16', 'i32', + [22, 33, 44, 55, 66, 77], + [22, 55, 66, 77, 66, 77], + [22, 33, 22, 33, 44, 77] + ] +]; + +for (let [elemTy, valueTy, src, exp1, exp2] of ARRAY_COPY_TESTS) { + let { arrayNew, arrayCopy } = wasmEvalText( + `(module + (type $arrTy (array (mut ${elemTy}))) + (func (export "arrayNew") + (param ${valueTy} ${valueTy} ${valueTy} + ${valueTy} ${valueTy} ${valueTy}) + (result eqref) + local.get 0 + local.get 1 + local.get 2 + local.get 3 + local.get 4 + local.get 5 + array.new_fixed $arrTy 6 + ) + (func (export "arrayCopy") + (param eqref i32 eqref i32 i32) + (array.copy $arrTy $arrTy + (ref.cast (ref null $arrTy) local.get 0) (local.get 1) + (ref.cast (ref null $arrTy) local.get 2) (local.get 3) (local.get 4) + ) + ) + )` + ).exports; + + assertEq(src.length, 6); + assertEq(exp1.length, 6); + assertEq(exp2.length, 6); + + function eqArrays(a1, a2) { + assertEq(a1.length, 6); + assertEq(a2.length, 6); + for (i = 0; i < 6; i++) { + if (a1[i] !== a2[i]) + return false; + } + return true; + } + function show(who, arr) { + print(who + ": " + arr[0] + " " + arr[1] + " " + arr[2] + " " + + arr[3] + " " + arr[4] + " " + arr[5] + " "); + } + + // Check that "normal" copying gives expected results. + let srcTO; + srcTO = arrayNew(src[0], src[1], src[2], src[3], src[4], src[5]); + arrayCopy(srcTO, 1, srcTO, 3, 3); + assertEq(eqArrays(srcTO, exp1), true); + + srcTO = arrayNew(src[0], src[1], src[2], src[3], src[4], src[5]); + arrayCopy(srcTO, 2, srcTO, 0, 3); + assertEq(eqArrays(srcTO, exp2), true); + + // Check out-of-bounds conditions + let exp1TO = arrayNew(exp1[0], exp1[1], exp1[2], exp1[3], exp1[4], exp1[5]); + let exp2TO = arrayNew(exp2[0], exp2[1], exp2[2], exp2[3], exp2[4], exp2[5]); + + // dst overrun, wants to write [5, 6] + assertErrorMessage(() => { + arrayCopy(exp1TO, 5, exp2TO, 1, 2); + },WebAssembly.RuntimeError, /index out of bounds/); + + // dst overrun, wants to write [7, 8] + assertErrorMessage(() => { + arrayCopy(exp1TO, 7, exp2TO, 1, 2); + },WebAssembly.RuntimeError, /index out of bounds/); + + // dst zero-len overrun, wants to write no elements, but starting at 9 + assertErrorMessage(() => { + arrayCopy(exp1TO, 9, exp2TO, 1, 0); + },WebAssembly.RuntimeError, /index out of bounds/); + + // src overrun, wants to read [5, 6] + assertErrorMessage(() => { + arrayCopy(exp1TO, 1, exp2TO, 5, 2); + },WebAssembly.RuntimeError, /index out of bounds/); + + // src overrun, wants to read [7, 8] + assertErrorMessage(() => { + arrayCopy(exp1TO, 1, exp2TO, 7, 2); + },WebAssembly.RuntimeError, /index out of bounds/); + + // src zero-len overrun, wants to read no elements, but starting at 9 + assertErrorMessage(() => { + arrayCopy(exp1TO, 1, exp2TO, 9, 0); + },WebAssembly.RuntimeError, /index out of bounds/); +} + +// run: check that null src or dest array causes a trap #1 +{ + let { shouldTrap } = wasmEvalText(`(module + (type $a (array (mut f32))) + (func (export "shouldTrap") + ref.null $a + i32.const 1 + (array.new_fixed $a 3 (f32.const 1.23) (f32.const 4.56) (f32.const 7.89)) + i32.const 1 + i32.const 1 + array.copy $a $a + ) + )`).exports; + assertErrorMessage(() => { + shouldTrap(); + }, WebAssembly.RuntimeError, /null/); +} + +// run: check that null src or dest array causes a trap #2 +{ + let { shouldTrap } = wasmEvalText(`(module + (type $a (array (mut f32))) + (func (export "shouldTrap") + (array.new_fixed $a 3 (f32.const 1.23) (f32.const 4.56) (f32.const 7.89)) + i32.const 1 + ref.null $a + i32.const 1 + i32.const 1 + array.copy $a $a + ) + )`).exports; + assertErrorMessage(() => { + shouldTrap(); + }, WebAssembly.RuntimeError, /null/); +} + +////////////////////////////////////////////////////////////////////////////// +// +// Checks for requests for oversize arrays (more than MaxArrayPayloadBytes), +// where MaxArrayPayloadBytes == 1,987,654,321. + +// array.new +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array i32)) + (func + ;; request exactly 2,000,000,000 bytes + (array.new $a (i32.const 0xABCD1234) (i32.const 500000000)) + drop + ) + (start 0) +) +`), WebAssembly.RuntimeError, /too many array elements/); + +// array.new_default +assertErrorMessage(() => wasmEvalText(`(module + (type $a (array f64)) + (func + ;; request exactly 2,000,000,000 bytes + (array.new_default $a (i32.const 250000000)) + drop + ) + (start 0) +) +`), WebAssembly.RuntimeError, /too many array elements/); + +// array.new_fixed +// This is impossible to test because it would require to create, at a +// minimum, 1,987,654,321 (MaxArrayPayloadBytes) / 16 = 124.3 million +// values, if each value is a v128. However, the max number of bytes per +// function is 7,654,321 (MaxFunctionBytes). Even if it were possible to +// create a value using just one insn byte, there wouldn't be enough. + +// array.new_data +// Similarly, impossible to test because the max data segment length is 1GB +// (1,073,741,824 bytes) (MaxDataSegmentLengthPages * PageSize), which is less +// than MaxArrayPayloadBytes. + +// array.new_element +// Similarly, impossible to test because an element segment can contain at +// most 10,000,000 (MaxElemSegmentLength) entries. diff --git a/js/src/jit-test/tests/wasm/gc/binary.js b/js/src/jit-test/tests/wasm/gc/binary.js new file mode 100644 index 0000000000..1ef4a586a8 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/binary.js @@ -0,0 +1,29 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +load(libdir + "wasm-binary.js"); + +const v2vSig = {args:[], ret:VoidCode}; +const v2vSigSection = sigSection([v2vSig]); + +function checkInvalid(body, errorMessage) { + assertErrorMessage(() => new WebAssembly.Module( + moduleWithSections([v2vSigSection, declSection([0]), bodySection([body])])), + WebAssembly.CompileError, + errorMessage); +} + +const invalidRefBlockType = funcBody({locals:[], body:[ + BlockCode, + OptRefCode, + 0x42, + EndCode, +]}); +checkInvalid(invalidRefBlockType, /heap type/); + +const invalidTooBigRefType = funcBody({locals:[], body:[ + BlockCode, + OptRefCode, + varU32(1000000), + EndCode, +]}); +checkInvalid(invalidTooBigRefType, /heap type/); diff --git a/js/src/jit-test/tests/wasm/gc/br-on-cast-fail.js b/js/src/jit-test/tests/wasm/gc/br-on-cast-fail.js new file mode 100644 index 0000000000..3482f7815a --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/br-on-cast-fail.js @@ -0,0 +1,199 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +function typingModule(types, from, to, brParams, branchResults, fallthroughResults) { + return `(module + ${types} + (func + (param ${brParams.join(' ')}) + (result ${branchResults.join(' ')}) + + (block (result ${fallthroughResults.join(' ')}) + (; push params onto the stack in the same order as they appear, leaving + the last param at the top of the stack. ;) + ${brParams.map((_, i) => `local.get ${i}`).join('\n')} + br_on_cast_fail 1 ${from} ${to} + ) + unreachable + ) + )`; +} + +function validTyping(types, from, to, brParams, branchResults, fallthroughResults) { + wasmValidateText(typingModule(types, from, to, brParams, branchResults, fallthroughResults)); +} + +function invalidTyping(types, from, to, brParams, branchResults, fallthroughResults, error) { + wasmFailValidateText(typingModule(types, from, to, brParams, branchResults, fallthroughResults), error); +} + +// valid: eqref -> struct +validTyping('(type $a (struct))', 'eqref', '(ref $a)', ['eqref'], ['eqref'], ['(ref $a)']); +// valid: eqref -> struct (and looser types on results) +validTyping('(type $a (struct))', 'eqref', '(ref $a)', ['eqref'], ['anyref'], ['(ref null $a)']); +// valid: eqref -> nullable struct (note that branch type becomes non-nullable) +validTyping('(type $a (struct))', 'eqref', '(ref null $a)', ['eqref'], ['(ref eq)'], ['(ref null $a)']); +// valid: struct -> struct (from anyref) +validTyping('(type $a (struct))', 'anyref', '(ref $a)', ['(ref $a)'], ['anyref'], ['(ref $a)']); +// valid: struct -> struct (canonicalized) +validTyping('(type $a (struct)) (type $b (struct))', '(ref $a)', '(ref $b)', ['(ref $a)'], ['(ref $b)'], ['(ref $a)']); +// valid: nullable struct -> non-nullable struct (canonicalized) +validTyping('(type $a (struct)) (type $b (struct))', '(ref null $a)', '(ref $b)', ['(ref null $a)'], ['(ref null $a)'], ['(ref $b)']); +// valid: nullable struct -> nullable struct (canonicalized) +validTyping('(type $a (struct)) (type $b (struct))', '(ref null $a)', '(ref null $b)', ['(ref null $a)'], ['(ref $a)'], ['(ref null $a)']); +// valid: eqref -> struct with extra arg +validTyping('(type $a (struct))', 'eqref', '(ref $a)', ['i32', 'eqref'], ['i32', 'eqref'], ['i32', '(ref $a)']); +// valid: eqref -> struct with two extra args +validTyping('(type $a (struct))', 'eqref', '(ref $a)', ['i32', 'f32', 'eqref'], ['i32', 'f32', 'eqref'], ['i32', 'f32', '(ref $a)']); + +// invalid: block result type must have slot for casted-to type +invalidTyping('(type $a (struct))', 'eqref', '(ref $a)', ['eqref'], [], ['(ref $a)'], /type mismatch/); +// invalid: block result type must be supertype of casted-to type +invalidTyping('(type $a (struct)) (type $b (struct (field i32)))', 'eqref', '(ref $a)', ['eqref'], ['(ref $b)'], ['(ref $a)'], /type mismatch/); +// invalid: input is missing extra i32 from the branch target type +invalidTyping('(type $a (struct))', 'eqref', '(ref $a)', ['f32', 'eqref'], ['i32', 'f32', 'eqref'], ['i32', 'f32', '(ref $a)'], /popping value/); +// invalid: input has extra [i32, f32] swapped from the branch target type +invalidTyping('(type $a (struct))', 'eqref', '(ref $a)', ['i32', 'f32', 'eqref'], ['f32', 'i32', 'eqref'], ['i32', 'f32', '(ref $a)'], /type mismatch/); +// invalid: input has extra [i32, f32] swapped from the branch fallthrough type +invalidTyping('(type $a (struct))', 'eqref', '(ref $a)', ['i32', 'f32', 'eqref'], ['i32', 'f32', 'eqref'], ['f32', 'i32', '(ref $a)'], /type mismatch/); +// invalid: casting to non-nullable but fallthrough not nullable +invalidTyping('(type $a (struct))', 'eqref', '(ref $a)', ['eqref'], ['(ref $a)'], ['(ref eq)'], /type mismatch/); +// invalid: struct -> struct (same recursion group) +invalidTyping('(rec (type $a (struct)) (type $b (struct)))', '(ref $a)', '(ref $b)', ['(ref $a)'], ['(ref $a)'], ['(ref $b)'], /type mismatch/); + +// Simple runtime test of cast-fail-ing +{ + let { makeA, makeB, isA, isB } = wasmEvalText(`(module + (type $a (struct)) + (type $b (struct (field i32))) + + (func (export "makeA") (result eqref) + struct.new_default $a + ) + + (func (export "makeB") (result eqref) + struct.new_default $b + ) + + (func (export "isA") (param eqref) (result i32) + (block (result eqref) + local.get 0 + br_on_cast_fail 0 eqref (ref $a) + + i32.const 1 + br 1 + ) + drop + i32.const 0 + ) + + (func (export "isB") (param eqref) (result i32) + (block (result eqref) + local.get 0 + br_on_cast_fail 0 eqref (ref $b) + + i32.const 1 + br 1 + ) + drop + i32.const 0 + ) + )`).exports; + + let a = makeA(); + let b = makeB(); + + assertEq(isA(a), 1); + assertEq(isA(b), 0); + assertEq(isB(a), 0); + assertEq(isB(b), 1); +} + +// Runtime test of cast-fail-ing with extra values +{ + function assertEqResults(a, b) { + if (!(a instanceof Array)) { + a = [a]; + } + if (!(b instanceof Array)) { + b = [b]; + } + if (a.length !== b.length) { + assertEq(a.length, b.length); + } + for (let i = 0; i < a.length; i++) { + let x = a[i]; + let y = b[i]; + // intentionally use loose equality to allow bigint to compare equally + // to number, as can happen with how we use the JS-API here. + assertEq(x == y, true); + } + } + + function testExtra(values) { + let { makeT, makeF, select } = wasmEvalText(`(module + (type $t (struct)) + (type $f (struct (field i32))) + + (func (export "makeT") (result eqref) + struct.new_default $t + ) + (func (export "makeF") (result eqref) + struct.new_default $f + ) + + (func (export "select") + (param eqref) (result ${values.map((type) => type).join(" ")}) + (block (result eqref) + local.get 0 + br_on_cast_fail 0 eqref (ref $t) + + ${values.map((type, i) => `${type}.const ${values.length + i}`) + .join("\n")} + br 1 + ) + drop + ${values.map((type, i) => `${type}.const ${i}`).join("\n")} + ) + )`).exports; + + let t = makeT(); + let f = makeF(); + + let trueValues = values.map((type, i) => i); + let falseValues = values.map((type, i) => values.length + i); + + assertEqResults(select(t), falseValues); + assertEqResults(select(f), trueValues); + } + + // multiples of primitive valtypes + for (let valtype of ['i32', 'i64', 'f32', 'f64']) { + testExtra([valtype]); + testExtra([valtype, valtype]); + testExtra([valtype, valtype, valtype]); + testExtra([valtype, valtype, valtype, valtype, valtype, valtype, valtype, valtype]); + } + + // random sundry of valtypes + testExtra(['i32', 'f32', 'i64', 'f64']); + testExtra(['i32', 'f32', 'i64', 'f64', 'i32', 'f32', 'i64', 'f64']); +} + +// This test causes the `values` vector returned by +// `OpIter<Policy>::readBrOnCastFail` to contain three entries, the last of +// which is the argument, hence is reftyped. This is used to verify an +// assertion to that effect in FunctionCompiler::brOnCastCommon. +{ + let tOnCastFail = + `(module + (type $a (struct)) + (func (export "onCastFail") (param f32 i32 eqref) (result f32 i32 eqref) + local.get 0 + local.get 1 + local.get 2 + br_on_cast_fail 0 eqref (ref $a) + unreachable + ) + )`; + let { onCastFail } = wasmEvalText(tOnCastFail).exports; +} diff --git a/js/src/jit-test/tests/wasm/gc/br-on-cast.js b/js/src/jit-test/tests/wasm/gc/br-on-cast.js new file mode 100644 index 0000000000..cf83f1ba00 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/br-on-cast.js @@ -0,0 +1,197 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +function typingModule(types, from, to, brParams, branchResults, fallthroughResults) { + return `(module + ${types} + (func + (param ${brParams.join(' ')}) + (result ${branchResults.join(' ')}) + + (block (result ${fallthroughResults.join(' ')}) + (; push params onto the stack in the same order as they appear, leaving + the last param at the top of the stack. ;) + ${brParams.map((_, i) => `local.get ${i}`).join('\n')} + br_on_cast 1 ${from} ${to} + ) + unreachable + ) + )`; +} + +function validTyping(types, from, to, brParams, branchResults, fallthroughResults) { + wasmValidateText(typingModule(types, from, to, brParams, branchResults, fallthroughResults)); +} + +function invalidTyping(types, from, to, brParams, branchResults, fallthroughResults, error) { + wasmFailValidateText(typingModule(types, from, to, brParams, branchResults, fallthroughResults), error); +} + +// valid: eqref -> struct +validTyping('(type $a (struct))', 'eqref', '(ref $a)', ['eqref'], ['(ref $a)'], ['eqref']); +// valid: eqref -> struct (and looser types on results) +validTyping('(type $a (struct))', 'eqref', '(ref $a)', ['eqref'], ['(ref null $a)'], ['anyref']); +// valid: eqref -> nullable struct (note that fallthrough becomes non-nullable) +validTyping('(type $a (struct))', 'eqref', '(ref null $a)', ['eqref'], ['(ref null $a)'], ['(ref eq)']); +// valid: struct -> struct (from anyref) +validTyping('(type $a (struct))', 'anyref', '(ref $a)', ['(ref $a)'], ['(ref $a)'], ['anyref']); +// valid: struct -> struct (canonicalized) +validTyping('(type $a (struct)) (type $b (struct))', '(ref $a)', '(ref $b)', ['(ref $a)'], ['(ref $b)'], ['(ref $b)']); +// valid: nullable struct -> non-nullable struct (canonicalized) +validTyping('(type $a (struct)) (type $b (struct))', '(ref null $a)', '(ref $b)', ['(ref null $a)'], ['(ref $b)'], ['(ref null $a)']); +// valid: nullable struct -> nullable struct (canonicalized) +validTyping('(type $a (struct)) (type $b (struct))', '(ref null $a)', '(ref null $b)', ['(ref null $a)'], ['(ref null $a)'], ['(ref $a)']); +// valid: eqref -> struct with extra arg +validTyping('(type $a (struct))', 'eqref', '(ref $a)', ['i32', 'eqref'], ['i32', '(ref $a)'], ['i32', 'eqref']); +// valid: eqref -> struct with two extra args +validTyping('(type $a (struct))', 'eqref', '(ref $a)', ['i32', 'f32', 'eqref'], ['i32', 'f32', '(ref $a)'], ['i32', 'f32', 'eqref']); + +// invalid: block result type must have slot for casted-to type +invalidTyping('(type $a (struct))', 'eqref', '(ref $a)', ['eqref'], [], ['eqref'], /type mismatch/); +// invalid: block result type must be supertype of casted-to type +invalidTyping('(type $a (struct)) (type $b (struct (field i32)))', 'eqref', '(ref $a)', ['eqref'], ['(ref $b)'], ['(ref $a)'], /type mismatch/); +// invalid: input is missing extra i32 from the branch target type +invalidTyping('(type $a (struct))', 'eqref', '(ref $a)', ['f32', 'eqref'], ['i32', 'f32', '(ref $a)'], ['i32', 'f32', 'eqref'], /popping value/); +// invalid: input has extra [i32, f32] swapped from the branch target type +invalidTyping('(type $a (struct))', 'eqref', '(ref $a)', ['i32', 'f32', 'eqref'], ['f32', 'i32', '(ref $a)'], ['i32', 'f32', 'eqref'], /type mismatch/); +// invalid: input has extra [i32, f32] swapped from the branch fallthrough type +invalidTyping('(type $a (struct))', 'eqref', '(ref $a)', ['i32', 'f32', 'eqref'], ['i32', 'f32', '(ref $a)'], ['f32', 'i32', 'eqref'], /type mismatch/); +// invalid: casting to non-nullable but fallthrough not nullable +invalidTyping('(type $a (struct))', 'eqref', '(ref $a)', ['eqref'], ['(ref $a)'], ['(ref eq)'], /type mismatch/); +// invalid: struct -> struct (same recursion group) +invalidTyping('(rec (type $a (struct)) (type $b (struct)))', '(ref $a)', '(ref $b)', ['(ref $a)'], ['(ref $b)'], ['(ref $a)'], /type mismatch/); + +// Simple runtime test of casting +{ + let { makeA, makeB, isA, isB } = wasmEvalText(`(module + (type $a (struct)) + (type $b (sub $a (struct (field i32)))) + + (func (export "makeA") (result eqref) + struct.new_default $a + ) + + (func (export "makeB") (result eqref) + struct.new_default $b + ) + + (func (export "isA") (param eqref) (result i32) + (block (result (ref $a)) + local.get 0 + br_on_cast 0 anyref (ref $a) + + i32.const 0 + br 1 + ) + drop + i32.const 1 + ) + + (func (export "isB") (param eqref) (result i32) + (block (result (ref $a)) + local.get 0 + br_on_cast 0 anyref (ref $b) + + i32.const 0 + br 1 + ) + drop + i32.const 1 + ) + )`).exports; + + let a = makeA(); + let b = makeB(); + + assertEq(isA(a), 1); + assertEq(isA(b), 1); + assertEq(isB(a), 0); + assertEq(isB(b), 1); +} + +// Runtime test of casting with extra values +{ + function assertEqResults(a, b) { + if (!(a instanceof Array)) { + a = [a]; + } + if (!(b instanceof Array)) { + b = [b]; + } + if (a.length !== b.length) { + assertEq(a.length, b.length); + } + for (let i = 0; i < a.length; i++) { + let x = a[i]; + let y = b[i]; + // intentionally use loose equality to allow bigint to compare equally + // to number, as can happen with how we use the JS-API here. + assertEq(x == y, true); + } + } + + function testExtra(values) { + let { makeT, makeF, select } = wasmEvalText(`(module + (type $t (struct)) + (type $f (struct (field i32))) + + (func (export "makeT") (result eqref) + struct.new_default $t + ) + (func (export "makeF") (result eqref) + struct.new_default $f + ) + + (func (export "select") (param eqref) (result ${values.map((type) => type).join(" ")}) + (block (result (ref $t)) + local.get 0 + br_on_cast 0 anyref (ref $t) + + ${values.map((type, i) => `${type}.const ${values.length + i}`).join("\n")} + br 1 + ) + drop + ${values.map((type, i) => `${type}.const ${i}`).join("\n")} + ) + )`).exports; + + let t = makeT(); + let f = makeF(); + + let trueValues = values.map((type, i) => i); + let falseValues = values.map((type, i) => values.length + i); + + assertEqResults(select(t), trueValues); + assertEqResults(select(f), falseValues); + } + + // multiples of primitive valtypes + for (let valtype of ['i32', 'i64', 'f32', 'f64']) { + testExtra([valtype]); + testExtra([valtype, valtype]); + testExtra([valtype, valtype, valtype]); + testExtra([valtype, valtype, valtype, valtype, valtype, valtype, valtype, valtype]); + } + + // random sundry of valtypes + testExtra(['i32', 'f32', 'i64', 'f64']); + testExtra(['i32', 'f32', 'i64', 'f64', 'i32', 'f32', 'i64', 'f64']); +} + +// This test causes the `values` vector returned by +// `OpIter<Policy>::readBrOnCast` to contain three entries, the last of which +// is the argument, hence is reftyped. This is used to verify an assertion to +// that effect in FunctionCompiler::brOnCastCommon. +{ + let tOnCast = + `(module + (type $a (struct)) + (func (export "onCast") (param f32 i32 eqref) (result f32 i32 (ref $a)) + local.get 0 + local.get 1 + local.get 2 + br_on_cast 0 anyref (ref $a) + unreachable + ) + )`; + let { onCast } = wasmEvalText(tOnCast).exports; +} diff --git a/js/src/jit-test/tests/wasm/gc/cast-abstract.js b/js/src/jit-test/tests/wasm/gc/cast-abstract.js new file mode 100644 index 0000000000..ec2a44014c --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/cast-abstract.js @@ -0,0 +1,264 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +const preamble = ` + (type $s1 (struct)) + (type $s2 (sub $s1 (struct (field i32)))) + (type $a1 (array (ref null $s1))) + (type $a2 (sub $a1 (array (ref null $s2)))) + (type $f1 (func)) + + (func $f (type $f1)) + (elem declare func $f) ;; allow $f to be ref.func'd +`; + +// All the concrete subtype relationships present in the preamble. +// [x, y] means x <: y. +const subtypes = [ + ['$s2', '$s1'], + ['$a2', '$a1'], +]; + +const typeSets = [ + [ + { name: 'any' }, + { name: 'eq' }, + { name: 'struct' }, + { name: '$s1', make: 'struct.new_default $s1' }, + { name: '$s2', make: 'struct.new_default $s2' }, + { name: 'none', none: true }, + ], + [ + { name: 'any' }, + { name: 'eq' }, + { name: 'array' }, + { name: '$a1', make: '(array.new_default $a1 (i32.const 10))' }, + { name: '$a2', make: '(array.new_default $a2 (i32.const 10))' }, + { name: 'none', none: true }, + ], + [ + { name: 'any' }, + { name: 'eq' }, + { name: '$s1', make: 'struct.new_default $s1' }, + { name: '$s2', make: 'struct.new_default $s2' }, + { name: '$a1', make: '(array.new_default $a1 (i32.const 10))' }, + { name: '$a2', make: '(array.new_default $a2 (i32.const 10))' }, + { name: 'none', none: true }, + ], + // i31 eventually + + // Apparently we don't support casting functions yet? That should be remedied... + // [ + // { name: 'func' }, + // { name: '$f1', make: 'ref.func $f' }, + // { name: 'nofunc', none: true }, + // ], + + // TODO: extern +]; + +const nullables = [ // for example: + [true, true, true], // null $s1 -> null any -> null eq + [true, true, false], // null $s1 -> null any -> eq + [true, false, true], // null $s1 -> any -> null eq + [true, false, false], // null $s1 -> any -> eq + [false, true, true], // $s1 -> null any -> null eq + [false, true, false], // $s1 -> null any -> eq + [false, false, true], // $s1 -> any -> null eq + [false, false, false], // $s1 -> any -> eq +] + +function isSubtype(src, dest) { + if (src.name === dest.name) { + return true; + } + for (const [src2, dest2] of subtypes) { + if (src.name === src2 && dest.name === dest2) { + return true; + } + } + return false; +} + +let numCases = 0; + +// Replace this with a string like 'non-null (ref $s1) -> (ref any) -> (ref any)' to test exactly one specific case. Makes debugging easier. +const specificTest = ''; + +// This will generate an enormous pile of test cases. All of these should be valid, +// as in passing WebAssembly.validate, but some may not be good casts at runtime. +for (const typeSet of typeSets) { + for (const start of typeSet) { + for (const middle of typeSet) { + for (const end of typeSet) { + for (const [nullable0, nullable1, nullable2] of nullables) { + for (const makeNull of [true, false]) { + const concrete0 = !!start.make; + const concrete1 = !!middle.make; + const concrete2 = !!end.make; + + if (!concrete0 && !makeNull) { + // We can only start with null values for abstract types + continue; + } + + if (!nullable0 && makeNull) { + // Can't use null as a valid value for a non-nullable type + continue; + } + + numCases += 1; + + let good1 = true; + let good2 = true; + + // Null values will fail casts to non-nullable types + if (makeNull) { + if (!nullable1) { + good1 = false; + } + if (!nullable2) { + good2 = false; + } + } + + // Bottom types can't represent non-null, so this will always fail + if (!makeNull) { + if (middle.none) { + good1 = false; + } + if (end.none) { + good2 = false; + } + } + + // Concrete values are subject to subtyping relationships + if (!makeNull) { + if (concrete1 && !isSubtype(start, middle)) { + good1 = false; + } + if (concrete2 && !isSubtype(start, end)) { + good2 = false; + } + } + + let emoji1 = good1 ? '✅' : '❌'; + let emoji2 = good2 ? '✅' : '❌'; + + if (!good1) { + good2 = false; + emoji2 = '❓'; + } + + const name = `${makeNull ? 'null' : 'non-null'} (ref ${nullable0 ? 'null ' : ''}${start.name}) -> (ref ${nullable1 ? 'null ' : ''}${middle.name}) -> (ref ${nullable2 ? 'null ' : ''}${end.name})`; + if (specificTest && name != specificTest) { + continue; + } + + print(`${emoji1}${emoji2} ${name}`); + const make = makeNull ? `ref.null ${start.name}` : start.make; + const type1 = `(ref ${nullable1 ? 'null ' : ''}${middle.name})`; + const type2 = `(ref ${nullable2 ? 'null ' : ''}${end.name})`; + const moduleText = `(module + ${preamble} + (func (export "cast1") (result ${type1}) + ${make} + ref.cast ${type1} + ) + (func (export "cast2") (param ${type1}) (result ${type2}) + local.get 0 + ref.cast ${type2} + ) + + (func (export "test1") (result i32) + ${make} + ref.test ${type1} + ) + (func (export "test2") (param ${type1}) (result i32) + local.get 0 + ref.test ${type2} + ) + + ;; these are basically ref.test but with branches + (func (export "branch1") (result i32) + (block (result ${type1}) + ${make} + br_on_cast 0 anyref ${type1} + drop + (return (i32.const 0)) + ) + drop + (return (i32.const 1)) + ) + (func (export "branch2") (param ${type1}) (result i32) + (block (result ${type2}) + local.get 0 + br_on_cast 0 anyref ${type2} + drop + (return (i32.const 0)) + ) + drop + (return (i32.const 1)) + ) + (func (export "branchfail1") (result i32) + (block (result anyref) + ${make} + br_on_cast_fail 0 anyref ${type1} + drop + (return (i32.const 1)) + ) + drop + (return (i32.const 0)) + ) + (func (export "branchfail2") (param ${type1}) (result i32) + (block (result anyref) + local.get 0 + br_on_cast_fail 0 anyref ${type2} + drop + (return (i32.const 1)) + ) + drop + (return (i32.const 0)) + ) + )`; + + try { + // The casts are split up so the stack trace will show you which cast is failing. + const { + cast1, cast2, + test1, test2, + branch1, branch2, + branchfail1, branchfail2, + } = wasmEvalText(moduleText).exports; + + function assertCast(func, good) { + if (good) { + return [func(), true]; + } else { + assertErrorMessage(func, WebAssembly.RuntimeError, /bad cast/); + return [null, false]; + } + } + + const [res1, ok1] = assertCast(cast1, good1); + assertEq(test1(), good1 ? 1 : 0); + assertEq(branch1(), good1 ? 1 : 0); + assertEq(branchfail1(), good1 ? 1 : 0); + if (ok1) { + assertCast(() => cast2(res1), good2); + assertEq(test2(res1), good2 ? 1 : 0); + assertEq(branch2(res1), good2 ? 1 : 0); + assertEq(branchfail2(res1), good2 ? 1 : 0); + } + } catch (e) { + print("Failed! Module text was:"); + print(moduleText); + throw e; + } + } + } + } + } + } +} + +print(`we hope you have enjoyed these ${numCases} test cases 😁`); diff --git a/js/src/jit-test/tests/wasm/gc/cast-extern.js b/js/src/jit-test/tests/wasm/gc/cast-extern.js new file mode 100644 index 0000000000..9fd9ddf2c9 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/cast-extern.js @@ -0,0 +1,54 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Casting an external value is an expected failure +let { refCast, refTest, brOnCast, brOnCastFail } = wasmEvalText(` + (module + (; give this struct a unique identity to avoid conflict with + the struct type defined in wasm.js ;) + (type (struct i64 i32 i64 i32)) + + (func (export "refTest") (param externref) (result i32) + local.get 0 + extern.internalize + ref.test (ref 0) + ) + (func (export "refCast") (param externref) (result i32) + local.get 0 + extern.internalize + ref.cast (ref null 0) + drop + i32.const 0 + ) + (func (export "brOnCast") (param externref) (result i32) + (block (result (ref 0)) + local.get 0 + extern.internalize + br_on_cast 0 anyref (ref 0) + drop + i32.const 0 + br 1 + ) + drop + i32.const 1 + ) + (func (export "brOnCastFail") (param externref) (result i32) + (block (result anyref) + local.get 0 + extern.internalize + br_on_cast_fail 0 anyref (ref 0) + drop + i32.const 1 + br 1 + ) + drop + i32.const 0 + ) + ) +`).exports; + +for (let v of WasmNonNullExternrefValues) { + assertErrorMessage(() => refCast(v), WebAssembly.RuntimeError, /bad cast/); + assertEq(refTest(v), 0); + assertEq(brOnCast(v), 0); + assertEq(brOnCastFail(v), 0); +} diff --git a/js/src/jit-test/tests/wasm/gc/casting.js b/js/src/jit-test/tests/wasm/gc/casting.js new file mode 100644 index 0000000000..7c07bfcf69 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/casting.js @@ -0,0 +1,128 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Test the maximum depth limit +{ + const MaxDepth = 31; + let types = `(type (struct))\n`; + for (let depth = 1; depth <= MaxDepth + 1; depth++) { + types += `(type (sub ${depth - 1} (struct)))\n`; + } + wasmFailValidateText(`(module + ${types} + )`, /too deep/); +} + +// Test all possible casting combinations of the following graph: +// +// A1 A2 +// | | +// B1 B2 +// | \ | +// C1 C2 C3 +// | \ | +// D1 D2 D3 +// | \ | +// E1 E2 E3 +// | \ | +// F1 F2 F3 +// | \ | +// G1 G2 G3 +// | \ | +// H1 H2 H3 +// | \ | +// I1 I2 I3 +// | \ | +// J1 J2 J3 +// +// NOTE: this object will be mutated and needs to be ordered such that parent +// definitions come before children. Note also, to be properly effective, +// these trees need to have a depth of at least MinSuperTypeVectorLength as +// defined in wasm/WasmCodegenConstants.h; keep it in sync with that. +const TYPES = { + 'A1': { super: null }, + 'A2': { super: null }, + 'B1': { super: 'A1' }, + 'B2': { super: 'A2' }, + 'C1': { super: 'B1' }, + 'C2': { super: 'B1' }, + 'C3': { super: 'B2' }, + 'D1': { super: 'C1' }, + 'D2': { super: 'C1' }, + 'D3': { super: 'C3' }, + 'E1': { super: 'D1' }, + 'E2': { super: 'D1' }, + 'E3': { super: 'D3' }, + 'F1': { super: 'E1' }, + 'F2': { super: 'E1' }, + 'F3': { super: 'E3' }, + 'G1': { super: 'F1' }, + 'G2': { super: 'F1' }, + 'G3': { super: 'F3' }, + 'H1': { super: 'G1' }, + 'H2': { super: 'G1' }, + 'H3': { super: 'G3' }, + 'I1': { super: 'H1' }, + 'I2': { super: 'H1' }, + 'I3': { super: 'H3' }, + 'J1': { super: 'I1' }, + 'J2': { super: 'I1' }, + 'J3': { super: 'I3' }, +}; + +// The oracle method for testing the declared subtype relationship. +function manualIsSubtype(types, subType, superType) { + while (subType !== superType && subType.super !== null) { + subType = types[subType.super]; + } + return subType === superType; +} + +function testAllCasts(types) { + let typeSection = ``; + let funcSection = ``; + for (let name in types) { + let type = types[name]; + if (type.super === null) { + typeSection += `(type \$${name} (struct))\n`; + } else { + typeSection += `(type \$${name} (sub \$${type.super} (struct)))\n`; + } + funcSection += ` + (func (export "new${name}") (result externref) + struct.new_default \$${name} + extern.externalize + ) + (func (export "is${name}") (param externref) (result i32) + local.get 0 + extern.internalize + ref.test (ref \$${name}) + )`; + } + // NOTE: we place all types in a single recursion group to prevent + // canonicalization from + let moduleText = `(module + (rec ${typeSection}) + ${funcSection} + )`; + + // Instantiate the module and acquire the testing methods + let exports = wasmEvalText(moduleText).exports; + for (let name in types) { + let type = types[name]; + type['new'] = exports[`new${name}`]; + type['is'] = exports[`is${name}`]; + } + + // Test every combination of types, comparing the oracle method against the + // JIT'ed method. + for (let subTypeName in types) { + let subType = types[subTypeName]; + for (let superTypeName in types) { + let superType = types[subTypeName]; + assertEq( + manualIsSubtype(types, subType, superType) ? 1 : 0, + superType['is'](subType['new']())); + } + } +} +testAllCasts(TYPES); diff --git a/js/src/jit-test/tests/wasm/gc/debugger.js b/js/src/jit-test/tests/wasm/gc/debugger.js new file mode 100644 index 0000000000..dea6581eb8 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/debugger.js @@ -0,0 +1,38 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() + +(function() { + let g = newGlobal({newCompartment: true}); + let dbg = new Debugger(g); + g.eval(`o = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary('(module (func (param externref) (result externref) local.get 0) (export "" (func 0)))')));`); +})(); + +(function() { + var g = newGlobal({newCompartment: true}); + g.parent = this; + + let src = ` + (module + (func (export "func") (param $ref externref) (result externref) + local.get $ref + ) + ) + `; + + g.eval(` + var obj = { somekey: 'somevalue' }; + Debugger(parent).onEnterFrame = function(frame) { + let v = frame.environment.getVariable('var0'); + assertEq(typeof v, 'object'); + + let prop = v.unwrap().getOwnPropertyDescriptor('somekey'); + assertEq(typeof prop, 'object'); + assertEq(typeof prop.value, 'string'); + assertEq(prop.value, 'somevalue'); + + // Disable onEnterFrame hook. + Debugger(parent).onEnterFrame = undefined; + }; + `); + + new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary(`${src}`))).exports.func(g.obj); +})(); diff --git a/js/src/jit-test/tests/wasm/gc/directives.txt b/js/src/jit-test/tests/wasm/gc/directives.txt new file mode 100644 index 0000000000..49594ced70 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/directives.txt @@ -0,0 +1 @@ +|jit-test| test-also=--wasm-compiler=optimizing; test-also=--wasm-function-references --wasm-gc; test-also=--wasm-compiler=baseline --wasm-function-references --wasm-gc; include:wasm.js diff --git a/js/src/jit-test/tests/wasm/gc/disabled.js b/js/src/jit-test/tests/wasm/gc/disabled.js new file mode 100644 index 0000000000..791c6ff25e --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/disabled.js @@ -0,0 +1,25 @@ +// |jit-test| skip-if: wasmGcEnabled() + +const { CompileError, validate } = WebAssembly; + +const UNRECOGNIZED_OPCODE_OR_BAD_TYPE = /unrecognized opcode|(Structure|reference|gc) types not enabled|invalid heap type|invalid inline block type|bad type|\(ref T\) types not enabled|Invalid type|invalid function type/; + +let simpleTests = [ + "(module (func (drop (ref.null eq))))", + "(module (func $test (local eqref)))", + "(module (func $test (param eqref)))", + "(module (func $test (result eqref) (ref.null eq)))", + "(module (func $test (block (result eqref) (unreachable)) unreachable))", + "(module (func $test (result i32) (local eqref) (ref.is_null (local.get 0))))", + `(module (import "a" "b" (func (param eqref))))`, + `(module (import "a" "b" (func (result eqref))))`, + `(module (type $s (struct)))`, +]; + +// Test that use of gc-types fails when gc is disabled. + +for (let src of simpleTests) { + let bin = wasmTextToBinary(src); + assertEq(validate(bin), false); + wasmCompilationShouldFail(bin, UNRECOGNIZED_OPCODE_OR_BAD_TYPE); +} diff --git a/js/src/jit-test/tests/wasm/gc/externref-boxing-struct.js b/js/src/jit-test/tests/wasm/gc/externref-boxing-struct.js new file mode 100644 index 0000000000..b9cfed080f --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/externref-boxing-struct.js @@ -0,0 +1,74 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Moving a JS value through a wasm externref is a pair of boxing/unboxing +// conversions that leaves the value unchanged. There are many cases, +// along these axes: +// +// - global variables, see externref-boxing.js +// - tables, see externref-boxing.js +// - function parameters and returns, see externref-boxing.js +// - struct fields [for the gc feature], this file +// - TypedObject fields when we construct with the externref type; this file + +// Struct fields of externref type can receive their value in these ways: +// +// - the struct.new and struct.set instructions +// - storing into mutable fields from JS +// - from a constructor called from JS +// +// Struct fields can be read in these ways: +// +// - the struct.get instruction +// - reading the field from JS +// +// We're especially interested in two cases: where JS stores a non-object value +// into a field, in this case there should be boxing; and where JS reads a +// non-pointer value from a field, in this case there should be unboxing. + +// Write with struct.new, read with the JS getter + +for (let v of WasmExternrefValues) +{ + let ins = wasmEvalText( + `(module + (type $S (struct (field $S.x (mut externref)))) + (func (export "make") (param $v externref) (result eqref) + (struct.new $S (local.get $v))))`); + let x = ins.exports.make(v); + assertEq(x[0], v); +} + +// Try to make sure externrefs are properly traced + +{ + let fields = iota(10).map(() => `(field externref)`).join(' '); + let params = iota(10).map((i) => `(param $${i} externref)`).join(' '); + let args = iota(10).map((i) => `(local.get $${i})`).join(' '); + let txt = `(module + (type $S (struct ${fields})) + (func (export "make") ${params} (result eqref) + (struct.new $S ${args})))`; + let ins = wasmEvalText(txt); + let x = ins.exports.make({x:0}, {x:1}, {x:2}, {x:3}, {x:4}, {x:5}, {x:6}, {x:7}, {x:8}, {x:9}) + gc('shrinking'); + assertEq(typeof x[0], "object"); + assertEq(x[0].x, 0); + assertEq(typeof x[1], "object"); + assertEq(x[1].x, 1); + assertEq(typeof x[2], "object"); + assertEq(x[2].x, 2); + assertEq(typeof x[3], "object"); + assertEq(x[3].x, 3); + assertEq(typeof x[4], "object"); + assertEq(x[4].x, 4); + assertEq(typeof x[5], "object"); + assertEq(x[5].x, 5); + assertEq(typeof x[6], "object"); + assertEq(x[6].x, 6); + assertEq(typeof x[7], "object"); + assertEq(x[7].x, 7); + assertEq(typeof x[8], "object"); + assertEq(x[8].x, 8); + assertEq(typeof x[9], "object"); + assertEq(x[9].x, 9); +} diff --git a/js/src/jit-test/tests/wasm/gc/externref-conversions.js b/js/src/jit-test/tests/wasm/gc/externref-conversions.js new file mode 100644 index 0000000000..12c5a2bbcc --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/externref-conversions.js @@ -0,0 +1,94 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Output type is non-nullable if the input type is non-nullable +wasmValidateText(`(module + (func (param externref) (result anyref) + local.get 0 + extern.internalize + ) + (func (param anyref) (result externref) + local.get 0 + extern.externalize + ) + (func (param (ref extern)) (result (ref any)) + local.get 0 + extern.internalize + ) + (func (param (ref any)) (result (ref extern)) + local.get 0 + extern.externalize + ) + (func (result (ref any)) + unreachable + extern.internalize + ) + (func (result (ref extern)) + unreachable + extern.externalize + ) +)`); + +// Output type is nullable if the input type is nullable +wasmFailValidateText(`(module + (func (param externref) (result (ref any)) + local.get 0 + extern.internalize + ) +)`, /expected/); +wasmFailValidateText(`(module + (func (param anyref) (result (ref extern)) + local.get 0 + extern.externalize + ) +)`, /expected/); + +// Can round trip an externref through anyref and get the same thing back +let {roundtripThroughAny} = wasmEvalText(`(module + (func (export "roundtripThroughAny") (param externref) (result externref) + local.get 0 + extern.internalize + extern.externalize + ) +)`).exports; +for (let value of WasmExternrefValues) { + assertEq(value, roundtripThroughAny(value)); +} + +// Can round trip GC objects through externref and get the same thing back +let {testStruct, testArray} = wasmEvalText(`(module + (type $struct (struct)) + (type $array (array i32)) + + (func (export "testStruct") (result i32) + (local (ref $struct)) + (local.set 0 struct.new $struct) + local.get 0 + (ref.eq + (ref.cast (ref null $struct) + (extern.internalize + (extern.externalize + local.get 0 + ) + ) + ) + ) + ) + + (func (export "testArray") (result i32) + (local (ref $array)) + (local.set 0 (array.new $array i32.const 0 i32.const 0)) + local.get 0 + (ref.eq + (ref.cast (ref null $array) + (extern.internalize + (extern.externalize + local.get 0 + ) + ) + ) + ) + ) +)`).exports; + +assertEq(testStruct(), 1); +assertEq(testArray(), 1); diff --git a/js/src/jit-test/tests/wasm/gc/function_subtyping.js b/js/src/jit-test/tests/wasm/gc/function_subtyping.js new file mode 100644 index 0000000000..6b4e719a74 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/function_subtyping.js @@ -0,0 +1,79 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Validate rules for function subtyping: +// - Same number of parameters and results +// - Function return types are covariant +// - Function parameter types are contravariant +wasmValidateText( +`(module + (type $A (struct (field i32))) + (type $B (sub $A (struct (field i32) (field i32)))) + (type $C (sub $B (struct (field i32) (field i32) (field i64)))) + (type $D (sub $B (struct (field i32) (field i32) (field f32)))) + + ;; Same types (invariant) + (type $func1 (func (param (ref $A) (ref $A)) (result (ref $C)))) + (type $func2 (sub $func1 (func (param (ref $A) (ref $A)) (result (ref $C))))) + + ;; Covariant return types are valid + (type $func3 (func (param (ref $A) (ref $A)) (result (ref $B)))) + (type $func4 (sub $func3 (func (param (ref $A) (ref $A)) (result (ref $C))))) + (type $func5 (func (param (ref $A) (ref $A)) (result (ref $A) (ref $B)))) + (type $func6 (sub $func5 (func (param (ref $A) (ref $A)) (result (ref $D) (ref $C))))) + + ;; Contravariant parameter types are valid + (type $func7 (func (param (ref $A) (ref $C)) (result (ref $C)))) + (type $func8 (sub $func7 (func (param (ref $A) (ref $B)) (result (ref $C))))) + (type $func9 (func (param (ref $D) (ref $C)) (result (ref $C)))) + (type $func10 (sub $func9 (func (param (ref $A) (ref $B)) (result (ref $C))))) + + ;; Mixing covariance and contravariance + (type $func11 (func (param (ref $D) (ref $C)) (result (ref $A)))) + (type $func12 (sub $func11 (func (param (ref $A) (ref $B)) (result (ref $C))))) +) +`); + +// Validate that incorrect subtyping examples are failing as expected +const typeError = /incompatible super type/; + +var code =` +(module + (type $A (struct (field i32))) + (type $B (sub $A (struct (field i32) (field i32)))) + (type $C (sub $B (struct (field i32) (field i32) (field i64)))) + (type $D (sub $B (struct (field i32) (field i32) (field f32)))) + + ;; Not the same number of arguments/results + (type $func1 (func (param (ref $A) (ref $A)) (result (ref $C)))) + (type $func2 (sub $func1 (func (param (ref $A) (ref $A)) (result (ref $C) (ref $A))))) +)`; + +wasmFailValidateText(code, typeError); + +code =` +(module + (type $A (struct (field i32))) + (type $B (sub $A (struct (field i32) (field i32)))) + (type $C (sub $B (struct (field i32) (field i32) (field i64)))) + (type $D (sub $B (struct (field i32) (field i32) (field f32)))) + + ;; Contravariant result types are invalid + (type $func3 (func (param (ref $A) (ref $A)) (result (ref $C)))) + (type $func4 (sub $func3 (func (param (ref $A) (ref $A)) (result (ref $A))))) +)`; + +wasmFailValidateText(code, typeError); + +code =` +(module + (type $A (struct (field i32))) + (type $B (sub $A (struct (field i32) (field i32)))) + (type $C (sub $B (struct (field i32) (field i32) (field i64)))) + (type $D (sub $B (struct (field i32) (field i32) (field f32)))) + + ;; Covariant parameters are invalid + (type $func5 (func (param (ref $A) (ref $A)) (result (ref $C)))) + (type $func6 (sub $func5 (func (param (ref $B) (ref $A)) (result (ref $C))))) +)`; + +wasmFailValidateText(code, typeError); diff --git a/js/src/jit-test/tests/wasm/gc/global-get.js b/js/src/jit-test/tests/wasm/gc/global-get.js new file mode 100644 index 0000000000..1409fe39cd --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/global-get.js @@ -0,0 +1,30 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// global.get cannot refer to self or after globals after self + +assertErrorMessage(() => wasmEvalText(`(module + (global i32 global.get 0) +)`), WebAssembly.CompileError, /global/); + +assertErrorMessage(() => wasmEvalText(`(module + (global i32 global.get 1) + (global i32 i32.const 0) +)`), WebAssembly.CompileError, /global/); + +// global.get works on previous globals + +{ + let {func, b, c, e} = wasmEvalText(`(module + (func $func (export "func")) + + (global $a i32 i32.const 1) + (global $b (export "b") i32 global.get $a) + (global $c (export "c") i32 global.get $b) + + (global $d funcref ref.func $func) + (global $e (export "e") funcref global.get $d) + )`).exports; + assertEq(b.value, 1); + assertEq(c.value, 1); + assertEq(e.value, func); +} diff --git a/js/src/jit-test/tests/wasm/gc/init-expr.js b/js/src/jit-test/tests/wasm/gc/init-expr.js new file mode 100644 index 0000000000..b4d0b0950b --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/init-expr.js @@ -0,0 +1,75 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Test wasm::InitExpr GC specific constant expressions + +// struct.new and struct.new_default + +const { structNew, structNewDefault, structLarge } = wasmEvalText(`(module + (type $r (struct (field i32) (field f32))) + (type $xxl (struct + (field i64) (field f64) (field i64) (field f64) + (field i64) (field f64) (field i64) (field f64) + (field i64) (field f64) (field i64) (field f64) + (field i64) (field f64) (field i64) (field f64) + (field i64) (field f64) (field i64) (field f64))) + + (global $g1 (ref null $r) (struct.new_default $r)) + + (global $g2 (ref null $r) (struct.new $r + (i32.const 2) + (f32.const 3.14))) + + (global $gx (ref null $xxl) (struct.new $xxl + (i64.const 1) (f64.const 2.) (i64.const 3) (f64.const 4.) + (i64.const 5) (f64.const 2.) (i64.const 3) (f64.const 4.) + (i64.const 1) (f64.const 8.) (i64.const 9) (f64.const 4.) + (i64.const 1) (f64.const 2.) (i64.const 12) (f64.const 3.14) + (i64.const 16) (f64.const 17.) (i64.const 18) (f64.const 19.))) + + (func (export "structNewDefault") (result eqref) global.get $g1) + (func (export "structNew") (result eqref) global.get $g2) + (func (export "structLarge") (result eqref) global.get $gx) + )`).exports; + +let result; +result = structNew(); +assertEq(result[0], 2); +assertEq(result[1], new Float32Array([3.140000104904175])[0]); +result = structNewDefault(); +assertEq(result[0], 0); +assertEq(result[1], 0); +result = structLarge(); +assertEq(result[2], 3n); +assertEq(result[19], 19); + +// array.new, array.new_default, and array.new_fixed + +const { arrayNew, arrayNewDefault, arrayNewFixed } = wasmEvalText(`(module + (type $r (struct (field i32) (field f32))) + (type $a1 (array f64)) + (type $a2 (array i32)) + (type $a3 (array (ref null $r))) + + (global $g1 (ref null $a1) (array.new $a1 (f64.const 3.14) (i32.const 3))) + (global $g2 (ref null $a2) (array.new_default $a2 (i32.const 2))) + (global $g3 (ref null $a3) (array.new_fixed $a3 2 + (struct.new $r (i32.const 10) (f32.const 16.0)) + (ref.null $r))) + + (func (export "arrayNew") (result eqref) global.get $g1) + (func (export "arrayNewDefault") (result eqref) global.get $g2) + (func (export "arrayNewFixed") (result eqref) global.get $g3) + )`).exports; + +result = arrayNew(); +assertEq(result.length, 3); +assertEq(result[0], 3.14); +assertEq(result[2], 3.14); +result = arrayNewDefault(); +assertEq(result.length, 2); +assertEq(result[1], 0); +result = arrayNewFixed(); +assertEq(result.length, 2); +assertEq(result[0][0], 10); +assertEq(result[0][1], 16); +assertEq(result[1], null); diff --git a/js/src/jit-test/tests/wasm/gc/ion-and-baseline.js b/js/src/jit-test/tests/wasm/gc/ion-and-baseline.js new file mode 100644 index 0000000000..5a4951c585 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/ion-and-baseline.js @@ -0,0 +1,88 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Attempt to test intercalls from ion to baseline and back. +// +// We get into this situation when the modules are compiled with different +// tiering policies, or one tiers up before the other, or (for now) one opts +// into gc and is baseline-compiled and the other does not and is ion-compiled. +// There are lots of variables here. Generally, a small module will be +// ion-compiled unless there's reason to baseline-compile it, so we're likely +// actually testing something here. +// +// Some logging with printf confirms that refmod is baseline-compiled and +// nonrefmod is ion-compiled at present, with --wasm-gc enabled. + +var refmod = new WebAssembly.Module(wasmTextToBinary( + `(module + (import "" "tbl" (table $tbl 4 funcref)) + (import "" "print" (func $print (param i32))) + + ;; Just a dummy + (type $s (struct (field i32))) + + (type $htype (func (param externref))) + (type $itype (func (result externref))) + + (elem (i32.const 0) $f $g) + + (func $f (param externref) + (call $print (i32.const 1))) + + (func $g (result externref) + (call $print (i32.const 2)) + (ref.null extern)) + + (func (export "test_h") + (call_indirect (type $htype) (ref.null extern) (i32.const 2))) + + (func (export "test_i") + (drop (call_indirect (type $itype) (i32.const 3)))) + + )`)); + +var nonrefmod = new WebAssembly.Module(wasmTextToBinary( + `(module + (import "" "tbl" (table $tbl 4 funcref)) + (import "" "print" (func $print (param i32))) + + (type $ftype (func (param i32))) + (type $gtype (func (result i32))) + + (elem (i32.const 2) $h $i) + + ;; Should fail because of the signature mismatch: parameter + (func (export "test_f") + (call_indirect (type $ftype) (i32.const 37) (i32.const 0))) + + ;; Should fail because of the signature mismatch: return value + (func (export "test_g") + (drop (call_indirect (type $gtype) (i32.const 1)))) + + (func $h (param i32) + (call $print (i32.const 2))) + + (func $i (result i32) + (call $print (i32.const 3)) + (i32.const 37)) + )`)); + +var tbl = new WebAssembly.Table({initial:4, element:"anyfunc"}); +var refins = new WebAssembly.Instance(refmod, {"":{print, tbl}}).exports; +var nonrefins = new WebAssembly.Instance(nonrefmod, {"":{print, tbl}}).exports; + +assertErrorMessage(() => nonrefins.test_f(), + WebAssembly.RuntimeError, + /indirect call signature mismatch/); + +assertErrorMessage(() => nonrefins.test_g(), + WebAssembly.RuntimeError, + /indirect call signature mismatch/); + +assertErrorMessage(() => refins.test_h(), + WebAssembly.RuntimeError, + /indirect call signature mismatch/); + +assertErrorMessage(() => refins.test_i(), + WebAssembly.RuntimeError, + /indirect call signature mismatch/); + diff --git a/js/src/jit-test/tests/wasm/gc/js-boundary.js b/js/src/jit-test/tests/wasm/gc/js-boundary.js new file mode 100644 index 0000000000..ebf5387704 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/js-boundary.js @@ -0,0 +1,107 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Tests of dynamic type checks +test('anyref', WasmAnyrefValues, WasmNonAnyrefValues); +test('eqref', WasmEqrefValues, WasmNonAnyrefValues); +test('structref', WasmStructrefValues, WasmNonAnyrefValues); +test('arrayref', WasmArrayrefValues, WasmNonAnyrefValues); +let { newStruct } = wasmEvalText(` + (module + (type $s (struct)) + (func (export "newStruct") (result anyref) + struct.new $s) + )`).exports; +test('(ref null 0)', [newStruct()], WasmNonAnyrefValues, '(type (struct))'); +test('nullref', [null], WasmNonAnyrefValues); + +function test(type, validValues, invalidValues, typeSection) { + const CheckError = /can only pass|bad type/; + + if (!typeSection) { + typeSection = ""; + } + + // 1. Exported function params + let {a} = wasmEvalText(`(module + ${typeSection} + (func (export "a") (param ${type})) + )`).exports; + for (let val of invalidValues) { + assertErrorMessage(() => a(val), TypeError, CheckError); + } + for (let val of validValues) { + a(val); + } + + // 2. Imported function results + for (let val of invalidValues) { + function returnVal() { + return val; + } + let {test} = wasmEvalText(`(module + ${typeSection} + (func $returnVal (import "" "returnVal") (result ${type})) + (func (export "test") + call $returnVal + drop + ) + )`, {"": {returnVal}}).exports; + assertErrorMessage(() => test(), TypeError, CheckError); + } + for (let val of validValues) { + function returnVal() { + return val; + } + let {test} = wasmEvalText(`(module + ${typeSection} + (func $returnVal (import "" "returnVal") (result ${type})) + (func (export "test") + call $returnVal + drop + ) + )`, {"": {returnVal}}).exports; + test(val); + } + + // TODO: the rest of the tests cannot handle type sections yet. + if (typeSection !== "") { + return; + } + + // 3. Global value setter + for (let val of validValues) { + // Construct global from JS-API with initial value + let a = new WebAssembly.Global({value: type}, val); + assertEq(a.value, val, 'roundtrip matches'); + + // Construct global from JS-API with null value, then set + let b = new WebAssembly.Global({value: type, mutable: true}, null); + b.value = val; + assertEq(b.value, val, 'roundtrip matches'); + } + for (let val of invalidValues) { + // Construct global from JS-API with initial value + assertErrorMessage(() => new WebAssembly.Global({value: type}, val), + TypeError, + CheckError); + + // Construct global from JS-API with null value, then set + let a = new WebAssembly.Global({value: type, mutable: true}, null); + assertErrorMessage(() => a.value = val, + TypeError, + CheckError); + } + + // 4. Table set method + for (let val of validValues) { + let table = new WebAssembly.Table({element: type, initial: 1, maximum: 1}); + table.set(0, val); + assertEq(table.get(0), val, 'roundtrip matches'); + } + for (let val of invalidValues) { + let table = new WebAssembly.Table({element: type, initial: 1, maximum: 1}); + assertErrorMessage(() => table.set(0, val), + TypeError, + CheckError); + } +} diff --git a/js/src/jit-test/tests/wasm/gc/linking.js b/js/src/jit-test/tests/wasm/gc/linking.js new file mode 100644 index 0000000000..586011219a --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/linking.js @@ -0,0 +1,72 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +function linkGlobals(typeSection, exportInit, linkType) { + let {global} = wasmEvalText(`(module + ${typeSection} + (global (export "global") ${linkType} ${exportInit}) + )`).exports; + + wasmEvalText(`(module + ${typeSection} + (import "" "global" + (global ${linkType}) + ) + )`, {"": {global}}); +} + +function linkTables(typeSection, exportInit, linkType) { + let {table} = wasmEvalText(`(module + ${typeSection} + (table (export "table") ${linkType} (elem (${exportInit}))) + )`).exports; + + wasmEvalText(`(module + ${typeSection} + (import "" "table" + (table 0 1 ${linkType}) + ) + )`, {"": {table}}); +} + +function linkFuncs(typeSection, exportInit, linkType) { + let {func} = wasmEvalText(`(module + ${typeSection} + (func + (export "func") + (param ${linkType}) + (result ${linkType}) + unreachable + ) + )`).exports; + + wasmEvalText(`(module + ${typeSection} + (import "" "func" + (func (param ${linkType}) (result ${linkType})) + ) + )`, {"": {func}}); +} + +const TESTS = [ + [ + "(type (struct (field i32)))", + "ref.null 0", + "(ref null 0)", + ], + [ + "(type (array i32))", + "ref.null 0", + "(ref null 0)", + ], + [ + "(type (struct (field i32))) (type (struct (field (ref 0))))", + "ref.null 1", + "(ref null 1)", + ] +]; + +for (let test of TESTS) { + linkGlobals(...test); + linkTables(...test); + linkFuncs(...test); +} diff --git a/js/src/jit-test/tests/wasm/gc/ref-eq.js b/js/src/jit-test/tests/wasm/gc/ref-eq.js new file mode 100644 index 0000000000..e266916320 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/ref-eq.js @@ -0,0 +1,27 @@ +// |jit-test| skip-if: !wasmGcEnabled() +// +// ref.eq is part of the gc feature, not the reftypes feature. + +let { exports: { make, ref_eq, ref_eq_for_control } } = wasmEvalText(`(module + (type $s (struct)) + + (func (export "make") (result eqref) struct.new $s) + + (func (export "ref_eq") (param $a eqref) (param $b eqref) (result i32) + (ref.eq (local.get $a) (local.get $b))) + + (func (export "ref_eq_for_control") (param $a eqref) (param $b eqref) (result f64) + (if (result f64) (ref.eq (local.get $a) (local.get $b)) + (f64.const 5.0) + (f64.const 3.0))))`); + +let a = make(); +let b = make(); + +assertEq(ref_eq(null, null), 1); +assertEq(ref_eq(null, a), 0); +assertEq(ref_eq(b, b), 1); +assertEq(ref_eq_for_control(null, null), 5); +assertEq(ref_eq_for_control(null, a), 3); +assertEq(ref_eq_for_control(b, b), 5); + diff --git a/js/src/jit-test/tests/wasm/gc/ref-global.js b/js/src/jit-test/tests/wasm/gc/ref-global.js new file mode 100644 index 0000000000..1e2a87edc6 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/ref-global.js @@ -0,0 +1,96 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Basic private-to-module functionality. At the moment all we have is null +// pointers, not very exciting. + +{ + let bin = wasmTextToBinary( + `(module + (type $point (struct + (field $x f64) + (field $y f64))) + + (global $g1 (mut (ref null $point)) (ref.null $point)) + (global $g2 (mut (ref null $point)) (ref.null $point)) + (global $g3 (ref null $point) (ref.null $point)) + + ;; Restriction: cannot expose Refs outside the module, not even + ;; as a return value. See ref-restrict.js. + + (func (export "get") (result eqref) + (global.get $g1)) + + (func (export "copy") + (global.set $g2 (global.get $g1))) + + (func (export "clear") + (global.set $g1 (global.get $g3)) + (global.set $g2 (ref.null $point))))`); + + let mod = new WebAssembly.Module(bin); + let ins = new WebAssembly.Instance(mod).exports; + + assertEq(ins.get(), null); + ins.copy(); // Should not crash + ins.clear(); // Should not crash +} + +// Global with struct type + +{ + let bin = wasmTextToBinary( + `(module + (type $point (struct + (field $x f64) + (field $y f64))) + + (global $glob (mut (ref null $point)) (ref.null $point)) + + (func (export "init") + (global.set $glob (struct.new $point (f64.const 0.5) (f64.const 2.75)))) + + (func (export "change") + (global.set $glob (struct.new $point (f64.const 3.5) (f64.const 37.25)))) + + (func (export "clear") + (global.set $glob (ref.null $point))) + + (func (export "x") (result f64) + (struct.get $point 0 (global.get $glob))) + + (func (export "y") (result f64) + (struct.get $point 1 (global.get $glob))))`); + + let mod = new WebAssembly.Module(bin); + let ins = new WebAssembly.Instance(mod).exports; + + assertErrorMessage(() => ins.x(), WebAssembly.RuntimeError, /dereferencing null pointer/); + + ins.init(); + assertEq(ins.x(), 0.5); + assertEq(ins.y(), 2.75); + + ins.change(); + assertEq(ins.x(), 3.5); + assertEq(ins.y(), 37.25); + + ins.clear(); + assertErrorMessage(() => ins.x(), WebAssembly.RuntimeError, /dereferencing null pointer/); +} + +// Global value of type externref for initializer from a WebAssembly.Global, +// just check that it works. +{ + let bin = wasmTextToBinary( + `(module + (import "" "g" (global $g externref)) + (global $glob externref (global.get $g)) + (func (export "get") (result externref) + (global.get $glob)))`); + + let mod = new WebAssembly.Module(bin); + let obj = {zappa:37}; + let g = new WebAssembly.Global({value: "externref"}, obj); + let ins = new WebAssembly.Instance(mod, {"":{g}}).exports; + assertEq(ins.get(), obj); +} diff --git a/js/src/jit-test/tests/wasm/gc/ref-struct.js b/js/src/jit-test/tests/wasm/gc/ref-struct.js new file mode 100644 index 0000000000..59376ac1e7 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/ref-struct.js @@ -0,0 +1,328 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// We'll be running some binary-format tests shortly. + +load(libdir + "wasm-binary.js"); + +const v2vSigSection = sigSection([{args:[], ret:VoidCode}]); + +function checkInvalid(body, errorMessage) { + assertErrorMessage(() => new WebAssembly.Module( + moduleWithSections([v2vSigSection, + declSection([0]), + bodySection([body])])), + WebAssembly.CompileError, + errorMessage); +} + +// General test case for struct.new, struct.get, and struct.set: binary tree +// manipulation. + +{ + let bin = wasmTextToBinary( + `(module + (import "" "print_lp" (func $print_lp)) + (import "" "print_rp" (func $print_rp)) + (import "" "print_int" (func $print_int (param i32))) + + (type $wabbit (struct + (field $x (mut i32)) + (field $left (mut (ref null $wabbit))) + (field $right (mut (ref null $wabbit))))) + + (global $g (mut (ref null $wabbit)) (ref.null $wabbit)) + + (global $k (mut i32) (i32.const 0)) + + (func (export "init") (param $n i32) + (global.set $g (call $make (local.get $n)))) + + (func $make (param $n i32) (result (ref null $wabbit)) + (local $tmp i32) + (local.set $tmp (global.get $k)) + (global.set $k (i32.add (local.get $tmp) (i32.const 1))) + (if (result (ref null $wabbit)) (i32.le_s (local.get $n) (i32.const 2)) + (struct.new $wabbit (local.get $tmp) (ref.null $wabbit) (ref.null $wabbit)) + (block (result (ref null $wabbit)) + (struct.new $wabbit + (local.get $tmp) + (call $make (i32.sub (local.get $n) (i32.const 1))) + (call $make (i32.sub (local.get $n) (i32.const 2))))))) + + (func (export "accumulate") (result i32) + (call $accum (global.get $g))) + + (func $accum (param $w (ref null $wabbit)) (result i32) + (if (result i32) (ref.is_null (local.get $w)) + (i32.const 0) + (i32.add (struct.get $wabbit 0 (local.get $w)) + (i32.sub (call $accum (struct.get $wabbit 1 (local.get $w))) + (call $accum (struct.get $wabbit 2 (local.get $w))))))) + + (func (export "reverse") + (call $reverse (global.get $g))) + + (func $reverse (param $w (ref null $wabbit)) + (local $tmp (ref null $wabbit)) + (if (i32.eqz (ref.is_null (local.get $w))) + (block + (struct.set $wabbit 0 (local.get $w) (i32.mul (i32.const 2) (struct.get $wabbit 0 (local.get $w)))) + (local.set $tmp (struct.get $wabbit 1 (local.get $w))) + (struct.set $wabbit 1 (local.get $w) (struct.get $wabbit 2 (local.get $w))) + (struct.set $wabbit 2 (local.get $w) (local.get $tmp)) + (call $reverse (struct.get $wabbit 1 (local.get $w))) + (call $reverse (struct.get $wabbit 2 (local.get $w)))))) + + (func (export "print") + (call $pr (global.get $g))) + + (func $pr (param $w (ref null $wabbit)) + (if (i32.eqz (ref.is_null (local.get $w))) + (block + (call $print_lp) + (call $print_int (struct.get $wabbit 0 (local.get $w))) + (call $pr (struct.get $wabbit 1 (local.get $w))) + (call $pr (struct.get $wabbit 2 (local.get $w))) + (call $print_rp)))) + )`); + + let s = ""; + function pr_int(k) { s += k + " "; } + function pr_lp() { s += "(" }; + function pr_rp() { s += ")" } + + let mod = new WebAssembly.Module(bin); + let ins = new WebAssembly.Instance(mod, {"":{print_int:pr_int,print_lp:pr_lp,print_rp:pr_rp}}).exports; + + ins.init(6); + s = ""; ins.print(); assertEq(s, "(0 (1 (2 (3 (4 )(5 ))(6 ))(7 (8 )(9 )))(10 (11 (12 )(13 ))(14 )))"); + assertEq(ins.accumulate(), -13); + + ins.reverse(); + s = ""; ins.print(); assertEq(s, "(0 (20 (28 )(22 (26 )(24 )))(2 (14 (18 )(16 ))(4 (12 )(6 (10 )(8 )))))"); + assertEq(ins.accumulate(), 14); + + for (let i=10; i < 22; i++ ) { + ins.init(i); + ins.reverse(); + gc(); + ins.reverse(); + } +} + +// Sanity check for struct.set: we /can/ store a (ref null T) into a (ref null U) field +// with struct.set if T <: U; this should fall out of normal coercion but good +// to test. + +wasmEvalText( + `(module + (type $node (struct (field (mut (ref null $node))))) + (type $nix (sub $node (struct (field (mut (ref null $node))) (field i32)))) + (func $f (param $p (ref null $node)) (param $q (ref null $nix)) + (struct.set $node 0 (local.get $p) (local.get $q))))`); + +// ref.cast: if the pointer is null we trap + +assertErrorMessage(() => wasmEvalText( + `(module + (type $node (struct (field i32))) + (type $node2 (struct (field i32) (field f32))) + (func $f (param $p (ref null $node)) (result (ref null $node2)) + (ref.cast (ref $node2) (local.get $p))) + (func (export "test") (result eqref) + (call $f (ref.null $node))))`).exports.test(), + WebAssembly.RuntimeError, + /bad cast/, +); + +// ref.cast null: if the pointer is null we do not trap + +wasmEvalText( + `(module + (type $node (struct (field i32))) + (type $node2 (struct (field i32) (field f32))) + (func $f (param $p (ref null $node)) (result (ref null $node2)) + (ref.cast (ref null $node2) (local.get $p))) + (func (export "test") (result eqref) + (call $f (ref.null $node))))`).exports.test(); + +// ref.cast: if the downcast succeeds we get the original pointer + +assertEq(wasmEvalText( + `(module + (type $node (struct (field i32))) + (type $node2 (sub $node (struct (field i32) (field f32)))) + (func $f (param $p (ref null $node)) (result (ref null $node2)) + (ref.cast (ref null $node2) (local.get $p))) + (func (export "test") (result i32) + (local $n (ref null $node)) + (local.set $n (struct.new $node2 (i32.const 0) (f32.const 12))) + (ref.eq (call $f (local.get $n)) (local.get $n))))`).exports.test(), + 1); + +// And once more with mutable fields + +assertEq(wasmEvalText( + `(module + (type $node (struct (field (mut i32)))) + (type $node2 (sub $node (struct (field (mut i32)) (field f32)))) + (func $f (param $p (ref null $node)) (result (ref null $node2)) + (ref.cast (ref null $node2) (local.get $p))) + (func (export "test") (result i32) + (local $n (ref null $node)) + (local.set $n (struct.new $node2 (i32.const 0) (f32.const 12))) + (ref.eq (call $f (local.get $n)) (local.get $n))))`).exports.test(), + 1); + +// ref.cast: eqref -> struct when the eqref is the right struct; +// special case since eqref requires unboxing + +assertEq(wasmEvalText( + `(module + (type $node (struct (field i32))) + (func $f (param $p eqref) (result (ref null $node)) + (ref.cast (ref null $node) (local.get $p))) + (func (export "test") (result i32) + (local $n (ref null $node)) + (local.set $n (struct.new $node (i32.const 0))) + (ref.eq (call $f (local.get $n)) (local.get $n))))`).exports.test(), + 1); + +// Can default initialize a struct which zero initializes + +{ + let {makeA, makeB, makeC} = wasmEvalText(` + (module + (type $a (struct)) + (type $b (struct (field i32) (field f32))) + (type $c (struct (field eqref))) + + (func (export "makeA") (result eqref) + struct.new_default $a + ) + (func (export "makeB") (result eqref) + struct.new_default $b + ) + (func (export "makeC") (result eqref) + struct.new_default $c + ) + )`).exports; + let a = makeA(); + + let b = makeB(); + assertEq(b[0], 0); + assertEq(b[1], 0); + + let c = makeC(); + assertEq(c[0], null); +} + +// struct.new_default: valid if all struct fields are defaultable + +wasmFailValidateText(`(module + (type $a (struct (field (ref $a)))) + (func + struct.new_default $a + ) +)`, /defaultable/); + +wasmFailValidateText(`(module + (type $a (struct (field i32) (field i32) (field (ref $a)))) + (func + struct.new_default $a + ) +)`, /defaultable/); + +// Negative tests + +// Attempting to mutate immutable field with struct.set + +assertErrorMessage(() => wasmEvalText( + `(module + (type $node (struct (field i32))) + (func $f (param $p (ref null $node)) + (struct.set $node 0 (local.get $p) (i32.const 37))))`), + WebAssembly.CompileError, + /field is not mutable/); + +// Attempting to store incompatible value in mutable field with struct.set + +assertErrorMessage(() => wasmEvalText( + `(module + (type $node (struct (field (mut i32)))) + (func $f (param $p (ref null $node)) + (struct.set $node 0 (local.get $p) (f32.const 37))))`), + WebAssembly.CompileError, + /expression has type f32 but expected i32/); + +// Out-of-bounds reference for struct.get + +assertErrorMessage(() => wasmEvalText( + `(module + (type $node (struct (field i32))) + (func $f (param $p (ref null $node)) (result i32) + (struct.get $node 1 (local.get $p))))`), + WebAssembly.CompileError, + /field index out of range/); + +// Out-of-bounds reference for struct.set + +assertErrorMessage(() => wasmEvalText( + `(module + (type $node (struct (field (mut i32)))) + (func $f (param $p (ref null $node)) + (struct.set $node 1 (local.get $p) (i32.const 37))))`), + WebAssembly.CompileError, + /field index out of range/); + +// Base pointer is of unrelated type to stated type in struct.get + +assertErrorMessage(() => wasmEvalText( + `(module + (type $node (struct (field i32))) + (type $snort (struct (field f64))) + (func $f (param $p (ref null $snort)) (result i32) + (struct.get $node 0 (local.get $p))))`), + WebAssembly.CompileError, + /expression has type.*but expected.*/); + +// Base pointer is of unrelated type to stated type in struct.set + +assertErrorMessage(() => wasmEvalText( + `(module + (type $node (struct (field (mut i32)))) + (type $snort (struct (field f64))) + (func $f (param $p (ref null $snort)) (result i32) + (struct.set $node 0 (local.get $p) (i32.const 0))))`), + WebAssembly.CompileError, + /expression has type.*but expected.*/); + +// Null pointer dereference in struct.get + +assertErrorMessage(function() { + let ins = wasmEvalText( + `(module + (type $node (struct (field i32))) + (func (export "test") + (drop (call $f (ref.null $node)))) + (func $f (param $p (ref null $node)) (result i32) + (struct.get $node 0 (local.get $p))))`); + ins.exports.test(); +}, + WebAssembly.RuntimeError, + /dereferencing null pointer/); + +// Null pointer dereference in struct.set + +assertErrorMessage(function() { + let ins = wasmEvalText( + `(module + (type $node (struct (field (mut i32)))) + (func (export "test") + (call $f (ref.null $node))) + (func $f (param $p (ref null $node)) + (struct.set $node 0 (local.get $p) (i32.const 0))))`); + ins.exports.test(); +}, + WebAssembly.RuntimeError, + /dereferencing null pointer/); diff --git a/js/src/jit-test/tests/wasm/gc/ref.js b/js/src/jit-test/tests/wasm/gc/ref.js new file mode 100644 index 0000000000..30f5413029 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/ref.js @@ -0,0 +1,202 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Parsing and resolving. + +var text = `(module + (rec + (type $cons (struct + (field $car i32) + (field $cdr (ref null $cons)))) + + (type $odd (struct + (field $odd.x i32) + (field $to_even (ref null $even)))) + + (type $even (struct + (field $even.x i32) + (field $to_odd (ref null $odd)))) + ) + + ;; Use eqref on the API since struct types cannot be exposed outside the module yet. + + (import "m" "f" (func $imp (param eqref) (result eqref))) + + ;; The bodies do nothing since we have no operations on structs yet. + ;; Note none of these functions are exported, as they use Ref types in their signatures. + + (func (param (ref null $cons)) (result i32) + (i32.const 0)) + + (func $cdr (param $p (ref null $cons)) (result (ref null $cons)) + (local $l (ref null $cons)) + ;; store null value of correct type + (local.set $l (ref.null $cons)) + ;; store local of correct type + (local.set $l (local.get $p)) + ;; store call result of correct type + (local.set $l (call $cdr (local.get $p))) + ;; TODO: eventually also a test with global.get + ;; blocks and if with result type + (block (result (ref null $cons)) + (if (result (ref null $cons)) (i32.eqz (i32.const 0)) + (unreachable) + (ref.null $cons)))) + + (func (param (ref null $even)) (result (ref null $odd)) + (ref.null $odd)) + + (func (param (ref null $odd)) (result (ref null $even)) + (ref.null $even)) + + (func (param (ref null $cons)) + (call $cdr (local.get 0)) + drop + (call $imp (local.get 0)) + drop) + + (func (param (ref null $cons)) + (drop (ref.eq (local.get 0) (ref.null $cons))) + (drop (ref.eq (ref.null $cons) (local.get 0))) + (drop (ref.eq (local.get 0) (ref.null $cons))) + (drop (ref.eq (ref.null $cons) (local.get 0)))) + )`; + +// Validation + +wasmValidateText(text); + +// ref.is_null should work on any reference type + +new WebAssembly.Module(wasmTextToBinary(` +(module + (type $s (struct)) + (func $null (param (ref null $s)) (result i32) + (ref.is_null (local.get 0)))) +`)) + +// Automatic upcast to eqref + +new WebAssembly.Module(wasmTextToBinary(` +(module + (type $s (struct (field i32))) + (func $f (param (ref null $s)) (call $g (local.get 0))) + (func $g (param eqref) (unreachable))) +`)); + +// Misc failure modes + +assertErrorMessage(() => wasmEvalText(` +(module + (func (param (ref null $odd)) (unreachable))) +`), +SyntaxError, /failed to find name/); + +// Ref type mismatch in parameter is allowed through the prefix rule +// but not if the structs are incompatible. + +wasmEvalText(` +(module + (type $s (struct (field i32))) + (type $t (struct (field i32))) + (func $f (param (ref null $s)) (unreachable)) + (func $g (param (ref null $t)) (call $f (local.get 0))) +)`); + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (struct (field i32))) + (type $t (struct (field f32))) ;; Incompatible type + (func $f (param (ref null $s)) (unreachable)) + (func $g (param (ref null $t)) (call $f (local.get 0))) +)`), +WebAssembly.CompileError, /expression has type \(ref null.*\) but expected \(ref null.*\)/); + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (struct (field i32))) + (type $t (struct (field (mut i32)))) ;; Incompatible mutability + (func $f (param (ref null $s)) (unreachable)) + (func $g (param (ref null $t)) (call $f (local.get 0))) +)`), +WebAssembly.CompileError, /expression has type \(ref null.*\) but expected \(ref null.*\)/); + +// Ref type mismatch in assignment to local but the prefix rule allows +// the assignment to succeed if the structs are the same. + +wasmEvalText(` +(module + (type $s (struct (field i32))) + (type $t (struct (field i32))) + (func $f (param (ref null $s)) (local (ref null $t)) (local.set 1 (local.get 0)))) +`) + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (struct (field i32))) + (type $t (struct (field f32))) + (func $f (param (ref null $s)) (local (ref null $t)) (local.set 1 (local.get 0)))) +`), +WebAssembly.CompileError, /expression has type \(ref null.*\) but expected \(ref null.*\)/); + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (struct (field i32))) + (type $t (struct (field (mut i32)))) + (func $f (param (ref null $s)) (unreachable)) + (func $g (param (ref null $t)) (call $f (local.get 0))) +)`), +WebAssembly.CompileError, /expression has type \(ref null.*\) but expected \(ref null.*\)/); + +// Ref type mismatch in return but the prefix rule allows the return +// to succeed if the structs are the same. + +wasmEvalText(` +(module + (type $s (struct (field i32))) + (type $t (struct (field i32))) + (func $f (param (ref null $s)) (result (ref null $t)) (local.get 0))) +`); + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (struct (field i32))) + (type $t (struct (field f32))) + (func $f (param (ref null $s)) (result (ref null $t)) (local.get 0))) +`), +WebAssembly.CompileError, /expression has type \(ref null.*\) but expected \(ref null.*\)/); + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (struct (field i32))) + (type $t (struct (field (mut i32)))) + (func $f (param (ref null $s)) (result (ref null $t)) (local.get 0))) +`), +WebAssembly.CompileError, /expression has type \(ref null.*\) but expected \(ref null.*\)/); + +if (!wasmFunctionReferencesEnabled()) { + // Ref type can't reference a function type + + assertErrorMessage(() => wasmEvalText(` +(module + (type $x (func (param i32))) + (func $f (param (ref null $x)) (unreachable))) +`), + WebAssembly.CompileError, /does not reference a gc type/); + + assertErrorMessage(() => wasmEvalText(` +(module + (type (func (param i32))) + (func $f (param (ref null 0)) (unreachable))) +`), + WebAssembly.CompileError, /does not reference a gc type/); +} + +// No automatic downcast from eqref + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (struct (field i32))) + (func $f (param eqref) (call $g (local.get 0))) + (func $g (param (ref null $s)) (unreachable))) +`), +WebAssembly.CompileError, /expression has type eqref but expected \(ref null.*\)/); diff --git a/js/src/jit-test/tests/wasm/gc/regress-1633355.js b/js/src/jit-test/tests/wasm/gc/regress-1633355.js new file mode 100644 index 0000000000..97c611efd8 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/regress-1633355.js @@ -0,0 +1,30 @@ +// |jit-test| skip-if: !wasmGcEnabled() || wasmCompileMode().includes("ion") + +load(libdir + "asserts.js"); + +var g23 = newGlobal({newCompartment: true}); +g23.parent = this; +g23.eval(` + var dbg = new Debugger(parent); + dbg.onEnterFrame = function(frame) {} +`); +let bin = wasmTextToBinary(` + (type $wabbit (struct + (field $x (mut i32)) + (field $left (mut (ref null $wabbit))) + (field $right (mut (ref null $wabbit))) + )) + (global $g (mut (ref null $wabbit)) (ref.null $wabbit)) + (func (export "init") (param $n i32) + (global.set $g (call $make (local.get $n))) + ) + (func $make (param $n i32) (result (ref null $wabbit)) + (local $tmp i32) + (struct.new $wabbit (local.get $tmp) (ref.null $wabbit) (ref.null $wabbit)) + ) +`); +let mod = new WebAssembly.Module(bin); +let ins = new WebAssembly.Instance(mod).exports; + +// Debugger can handle non-exposable fields, like (ref T). +ins.init(6) diff --git a/js/src/jit-test/tests/wasm/gc/regress-1739330.js b/js/src/jit-test/tests/wasm/gc/regress-1739330.js new file mode 100644 index 0000000000..99e519d2d9 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/regress-1739330.js @@ -0,0 +1,23 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +try { + gczeal(4); + function a(b) { + binary = wasmTextToBinary(b) + c = new WebAssembly.Module(binary) + return new WebAssembly.Instance(c) + } + d = []; + let { newStruct } = a(` + (type $e (struct)) + (func (export "newStruct") + (result eqref) + struct.new $e + ) + `).exports + d.push(newStruct()); + gczeal(14, 7); + throw d; +} catch (d) { + assertEq(d instanceof Array, true); +} diff --git a/js/src/jit-test/tests/wasm/gc/regress-1745391.js b/js/src/jit-test/tests/wasm/gc/regress-1745391.js new file mode 100644 index 0000000000..5ea8989d3e --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/regress-1745391.js @@ -0,0 +1,13 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +let { createDefault } = wasmEvalText(` + (module (type $a (array (mut i32))) + (func (export "createDefault") (param i32) (result eqref) + local.get 0 + array.new_default $a + ) + ) +`).exports; +for (len = -1; len > -100; len--) { + assertErrorMessage(() => createDefault(len), WebAssembly.RuntimeError, /too many array elements/); +} diff --git a/js/src/jit-test/tests/wasm/gc/regress-1754701.js b/js/src/jit-test/tests/wasm/gc/regress-1754701.js new file mode 100644 index 0000000000..656aa5d625 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/regress-1754701.js @@ -0,0 +1,30 @@ +// |jit-test| skip-if: !wasmGcEnabled() || !('oomTest' in this) + +let { testArray, testStructInline, testStructOutline } = wasmEvalText(` + (module + (type $array (array i32)) + (type $structInline (struct)) + (type $structOutline + (struct + ${`(field i32)`.repeat(100)} + ) + ) + (func (export "testArray") + (param i32) + (result eqref) + local.get 0 + array.new_default $array + ) + (func (export "testStructInline") + (result eqref) + struct.new_default $structInline + ) + (func (export "testStructOutline") + (result eqref) + struct.new_default $structOutline + ) + ) +`).exports +oomTest(() => testArray(1)); +oomTest(() => testStructInline()); +oomTest(() => testStructOutline()); diff --git a/js/src/jit-test/tests/wasm/gc/regress-1830975.js b/js/src/jit-test/tests/wasm/gc/regress-1830975.js new file mode 100644 index 0000000000..ae6cba6bdd --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/regress-1830975.js @@ -0,0 +1,17 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Hello, future wasm dev. This test will start failing when you add casting +// for func and extern types. When that happens, remove the assertErrorMessage. + +assertErrorMessage(() => { + const { test } = wasmEvalText(` + (module + (type $f (func (result i32))) + (func (export "test") (type $f) + ref.null $f + ref.test (ref null $f) + ) + ) + `).exports; + assertEq(test(), 1); +}, WebAssembly.CompileError, /ref.test only supports the any hierarchy/); diff --git a/js/src/jit-test/tests/wasm/gc/regress-outline-repr.js b/js/src/jit-test/tests/wasm/gc/regress-outline-repr.js new file mode 100644 index 0000000000..f49b6480df --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/regress-outline-repr.js @@ -0,0 +1,151 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// White-box test for bug 1617908. The significance of this test is that the +// type $S is too large to fit in an inline TypedObject, and the write barrier +// logic must take this into account when storing the (ref $S2) into the last +// field of the object. + +const wat = ` +(module + (type $S2 (struct)) + (type $S (sub $S2 + (struct + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut eqref))))) + + (func $main + (struct.set $S 18 + (struct.new $S + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (ref.null eq)) + (struct.new $S2))) + (start $main)) +` +wasmEvalText(wat); + +// Test subtyping across outline/inline representations works + +wasmEvalText(` +(module + (type $inline + (struct + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + )) + (type $outline (sub $inline + (struct + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64)) + (field (mut i64))))) + + (func $main + (local $outline (ref null $outline)) + (local $inline (ref null $inline)) + + (; create an outline object and acquire multiple views to it ;) + (struct.new $outline + (i64.const 0xFF) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0) + (i64.const 0)) + local.tee $outline + local.set $inline + + (; clobber the object header ;) + (struct.set $inline 0 + local.get $inline + i64.const 0 + ) + (struct.set $inline 1 + local.get $inline + i64.const 0 + ) + (struct.set $inline 2 + local.get $inline + i64.const 0 + ) + (struct.set $inline 3 + local.get $inline + i64.const 0 + ) + (struct.set $inline 4 + local.get $inline + i64.const 0 + ) + + (; try to read a field ;) + (struct.get $outline 0 + local.get $outline + ) + drop + ) + (start $main)) +`); diff --git a/js/src/jit-test/tests/wasm/gc/signal-null-check.js b/js/src/jit-test/tests/wasm/gc/signal-null-check.js new file mode 100644 index 0000000000..a56526031d --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/signal-null-check.js @@ -0,0 +1,148 @@ +// |jit-test| skip-if: !wasmGcEnabled() +// +// Checks if null dereference works. + +for (let [fieldType, signedness, defaultValue] of [ + ['i8', '_u', 'i32.const 42'], + ['i8', '_s', 'i32.const -42'], + ['i16', '_u', 'i32.const 1'], + ['i16', '_s', 'i32.const -1'], + ['i32', '', 'i32.const 3'], + ['i64', '', 'i64.const -77777777777'], + ['f32', '', 'f32.const 1.4'], + ['f64', '', 'f64.const 3.14'], + ['externref', '', 'ref.null extern'], +].concat( + wasmSimdEnabled() ? [['v128', '', 'v128.const i32x4 1 2 -3 4']] : [] +)) { + // Check struct.get from null struct of a field of fieldType works + // Check struct.set similarly + testStructGetSet(fieldType, signedness, defaultValue, 4); + // - Also when the field is in the outline_ data area? + testStructGetSet(fieldType, signedness, defaultValue, 1000); + // Check array.get similarly + // Check array.set from null array with element of type fieldType + testArrayGetSet(fieldType, signedness, defaultValue, 100); +} + +function testStructGetSet(fieldType, signedness, defaultValue, numFields) { + const ins = wasmEvalText(` +(module + (type $t (struct ${ + Array(numFields).fill("(field (mut " + fieldType + "))").join(' ') + })) + (global $g (mut (ref null $t)) (ref.null $t)) + (func (export "init-null") + ref.null $t + global.set $g + ) + (func (export "init-non-null") + ${ Array(numFields).fill(defaultValue).join('\n ') } + struct.new $t + global.set $g + ) + (func (export "test_get_first") + global.get $g + struct.get${signedness} $t 0 + drop + ) + (func (export "test_set_first") + global.get $g + ${defaultValue} + struct.set $t 0 + ) + (func (export "test_get_mid") + global.get $g + struct.get${signedness} $t ${numFields >> 1} + drop + ) + (func (export "test_set_mid") + global.get $g + ${defaultValue} + struct.set $t ${numFields >> 1} + ) + (func (export "test_get_last") + global.get $g + struct.get${signedness} $t ${numFields - 1} + drop + ) + (func (export "test_set_last") + global.get $g + ${defaultValue} + struct.set $t ${numFields - 1} + ) +)`); + ins.exports["init-non-null"](); + ins.exports["test_get_first"](); + ins.exports["test_get_mid"](); + ins.exports["test_get_last"](); + ins.exports["test_set_first"](); + ins.exports["test_set_mid"](); + ins.exports["test_set_last"](); + + ins.exports["init-null"](); + assertDerefenceNull(() => ins.exports["test_get_first"]()); + assertDerefenceNull(() => ins.exports["test_get_mid"]()); + assertDerefenceNull(() => ins.exports["test_get_last"]()); + assertDerefenceNull(() => ins.exports["test_set_first"]()); + assertDerefenceNull(() => ins.exports["test_set_mid"]()); + assertDerefenceNull(() => ins.exports["test_set_last"]()); + + ins.exports["init-non-null"](); + ins.exports["test_set_last"](); + ins.exports["test_get_first"](); +} + +function testArrayGetSet(fieldType, signedness, defaultValue, numItems) { + const ins = wasmEvalText(` +(module + (type $t (array (mut ${fieldType}))) + (global $g (mut (ref null $t)) (ref.null $t)) + (func (export "init-null") + ref.null $t + global.set $g + ) + (func (export "init-non-null") + ${defaultValue} + i32.const ${numItems} + array.new $t + global.set $g + ) + (func (export "test_get") (param i32) + global.get $g + local.get 0 + array.get${signedness} $t + drop + ) + (func (export "test_set") (param i32) + global.get $g + local.get 0 + ${defaultValue} + array.set $t + ) +)`); + ins.exports["init-non-null"](); + ins.exports["test_get"](0); + ins.exports["test_get"](numItems >> 1); + ins.exports["test_get"](numItems - 1); + ins.exports["test_set"](0); + ins.exports["test_set"](numItems >> 1); + ins.exports["test_set"](numItems - 1); + + ins.exports["init-null"](); + assertDerefenceNull(() => ins.exports["test_get"](0)); + assertDerefenceNull(() => ins.exports["test_get"](numItems >> 1)); + assertDerefenceNull(() => ins.exports["test_get"](numItems - 1)); + assertDerefenceNull(() => ins.exports["test_set"](0)); + assertDerefenceNull(() => ins.exports["test_set"](numItems >> 1)); + assertDerefenceNull(() => ins.exports["test_set"](numItems - 1)); + + ins.exports["init-non-null"](); + ins.exports["test_set"](3); + ins.exports["test_get"](0); +} + + +function assertDerefenceNull(fun) { + assertErrorMessage(fun, WebAssembly.RuntimeError, /dereferencing null pointer/); +} diff --git a/js/src/jit-test/tests/wasm/gc/structs.js b/js/src/jit-test/tests/wasm/gc/structs.js new file mode 100644 index 0000000000..388541c4ee --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/structs.js @@ -0,0 +1,724 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// This tests a bunch of wasm struct stuff, but not i8 or i16 fields. +// See structs2.js for i8/i16 field tests. + +var conf = getBuildConfiguration(); + +var bin = wasmTextToBinary( + `(module + (func $x1 (import "m" "x1") (type $f1)) + (func $x2 (import "m" "x2") (type $f2)) + + (table 2 funcref) + (elem (i32.const 0) $doit $doitagain) + + ;; Type array has a mix of types + + (type $f1 (func (param i32) (result i32))) + + (type $point (struct + (field $point_x i32) + (field $point_y i32))) + + (type $f2 (func (param f64) (result f64))) + + (type $int_node (struct + (field $intbox_val (mut i32)) + (field $intbox_next (mut externref)))) + + ;; Test all the types. + + (type $omni (struct + (field $omni_i32 i32) + (field $omni_i32m (mut i32)) + (field $omni_i64 i64) + (field $omni_i64m (mut i64)) + (field $omni_f32 f32) + (field $omni_f32m (mut f32)) + (field $omni_f64 f64) + (field $omni_f64m (mut f64)) + (field $omni_externref externref) + (field $omni_externrefm (mut externref)))) + + ;; Various ways to reference a type in the middle of the + ;; type array, make sure we get the right one + + (func (export "hello") (param f64) (param i32) (result f64) + (call_indirect (type $f2) (local.get 0) (local.get 1))) + + (func $doit (param f64) (result f64) + (f64.sqrt (local.get 0))) + + (func $doitagain (param f64) (result f64) + (f64.mul (local.get 0) (local.get 0))) + + (func (export "x1") (param i32) (result i32) + (call $x1 (local.get 0))) + + (func (export "x2") (param f64) (result f64) + (call $x2 (local.get 0))) + + ;; Useful for testing to ensure that the type is not type #0 here. + + (func (export "mk_point") (result eqref) + (struct.new $point (i32.const 37) (i32.const 42))) + + (func (export "mk_int_node") (param i32) (param externref) (result eqref) + (struct.new $int_node (local.get 0) (local.get 1))) + + ;; Too big to fit in an InlineTypedObject. + + (type $bigger (struct + (field $a i32) + (field $b i32) + (field $c i32) + (field $d i32) + (field $e i32) + (field $f i32) + (field $g i32) + (field $h i32) + (field $i i32) + (field $j i32) + (field $k i32) + (field $l i32) + (field $m i32) + (field $n i32) + (field $o i32) + (field $p i32) + (field $q i32) + (field $r i32) + (field $s i32) + (field $t i32) + (field $u i32) + (field $v i32) + (field $w i32) + (field $x i32) + (field $y i32) + (field $z i32) + (field $aa i32) + (field $ab i32) + (field $ac i32) + (field $ad i32) + (field $ae i32) + (field $af i32) + (field $ag i32) + (field $ah i32) + (field $ai i32) + (field $aj i32) + (field $ak i32) + (field $al i32) + (field $am i32) + (field $an i32) + (field $ao i32) + (field $ap i32) + (field $aq i32) + (field $ar i32) + (field $as i32) + (field $at i32) + (field $au i32) + (field $av i32) + (field $aw i32) + (field $ax i32) + (field $ay i32) + (field $az i32))) + + (func (export "mk_bigger") (result eqref) + (struct.new $bigger + (i32.const 0) + (i32.const 1) + (i32.const 2) + (i32.const 3) + (i32.const 4) + (i32.const 5) + (i32.const 6) + (i32.const 7) + (i32.const 8) + (i32.const 9) + (i32.const 10) + (i32.const 11) + (i32.const 12) + (i32.const 13) + (i32.const 14) + (i32.const 15) + (i32.const 16) + (i32.const 17) + (i32.const 18) + (i32.const 19) + (i32.const 20) + (i32.const 21) + (i32.const 22) + (i32.const 23) + (i32.const 24) + (i32.const 25) + (i32.const 26) + (i32.const 27) + (i32.const 28) + (i32.const 29) + (i32.const 30) + (i32.const 31) + (i32.const 32) + (i32.const 33) + (i32.const 34) + (i32.const 35) + (i32.const 36) + (i32.const 37) + (i32.const 38) + (i32.const 39) + (i32.const 40) + (i32.const 41) + (i32.const 42) + (i32.const 43) + (i32.const 44) + (i32.const 45) + (i32.const 46) + (i32.const 47) + (i32.const 48) + (i32.const 49) + (i32.const 50) + (i32.const 51))) + + (type $withfloats (struct + (field $f1 f32) + (field $f2 f64) + (field $f3 externref) + (field $f4 f32) + (field $f5 i32))) + + (func (export "mk_withfloats") + (param f32) (param f64) (param externref) (param f32) (param i32) + (result eqref) + (struct.new $withfloats (local.get 0) (local.get 1) (local.get 2) (local.get 3) (local.get 4))) + + )`) + +var mod = new WebAssembly.Module(bin); +var ins = new WebAssembly.Instance(mod, {m:{x1(x){ return x*3 }, x2(x){ return Math.PI }}}).exports; + +assertEq(ins.hello(4.0, 0), 2.0) +assertEq(ins.hello(4.0, 1), 16.0) + +assertEq(ins.x1(12), 36) +assertEq(ins.x2(8), Math.PI) + +var point = ins.mk_point(); +assertEq(0 in point, true); +assertEq(1 in point, true); +assertEq(2 in point, false); +assertEq(point[0], 37); +assertEq(point[1], 42); + +var int_node = ins.mk_int_node(78, point); +assertEq(int_node[0], 78); +assertEq(int_node[1], point); + +var bigger = ins.mk_bigger(); +for ( let i=0; i < 52; i++ ) + assertEq(bigger[i], i); + +assertEq(bigger[-1], undefined); +assertEq(bigger[52], undefined); + +var withfloats = ins.mk_withfloats(1/3, Math.PI, bigger, 5/6, 0x1337); +assertEq(withfloats[0], Math.fround(1/3)); +assertEq(withfloats[1], Math.PI); +assertEq(withfloats[2], bigger); +assertEq(withfloats[3], Math.fround(5/6)); +assertEq(withfloats[4], 0x1337); + +// A simple stress test + +var stress = wasmTextToBinary( + `(module + (type $node (struct (field i32) (field (ref null $node)))) + (func (export "iota1") (param $n i32) (result eqref) + (local $list (ref null $node)) + (block $exit + (loop $loop + (br_if $exit (i32.eqz (local.get $n))) + (local.set $list (struct.new $node (local.get $n) (local.get $list))) + (local.set $n (i32.sub (local.get $n) (i32.const 1))) + (br $loop))) + (local.get $list)))`); +var stressIns = new WebAssembly.Instance(new WebAssembly.Module(stress)).exports; +var stressLevel = conf.x64 && !conf.tsan && !conf.asan && !conf.valgrind ? 100000 : 1000; +var the_list = stressIns.iota1(stressLevel); +for (let i=1; i <= stressLevel; i++) { + assertEq(the_list[0], i); + the_list = the_list[1]; +} +assertEq(the_list, null); + +// Fields and their exposure in JS. We can't export types yet so hide them +// inside the module with globals. + +// i64 fields. + +{ + let txt = + `(module + (type $big (struct + (field (mut i32)) + (field (mut i64)) + (field (mut i32)))) + + (func (export "set") (param eqref) + (local (ref null $big)) + (local.set 1 (ref.cast (ref null $big) (local.get 0))) + (struct.set $big 1 (local.get 1) (i64.const 0x3333333376544567))) + + (func (export "set2") (param $p eqref) + (struct.set $big 1 + (ref.cast (ref null $big) (local.get $p)) + (i64.const 0x3141592653589793))) + + (func (export "low") (param $p eqref) (result i32) + (i32.wrap/i64 (struct.get $big 1 (ref.cast (ref null $big) (local.get $p))))) + + (func (export "high") (param $p eqref) (result i32) + (i32.wrap/i64 (i64.shr_u + (struct.get $big 1 (ref.cast (ref null $big) (local.get $p))) + (i64.const 32)))) + + (func (export "mk") (result eqref) + (struct.new $big (i32.const 0x7aaaaaaa) (i64.const 0x4201020337) (i32.const 0x6bbbbbbb))) + + )`; + + let ins = wasmEvalText(txt).exports; + + let v = ins.mk(); + assertEq(typeof v, "object"); + assertEq(v[0], 0x7aaaaaaa); + assertEq(v[1], 0x4201020337n); + assertEq(ins.low(v), 0x01020337); + assertEq(ins.high(v), 0x42); + assertEq(v[2], 0x6bbbbbbb); + + ins.set(v); + assertEq(v[0], 0x7aaaaaaa); + assertEq(v[1], 0x3333333376544567n); + assertEq(v[2], 0x6bbbbbbb); + + ins.set2(v); + assertEq(v[1], 0x3141592653589793n); + assertEq(ins.low(v), 0x53589793); + assertEq(ins.high(v), 0x31415926) +} + +{ + let txt = + `(module + (type $big (struct + (field (mut i32)) + (field (mut i64)) + (field (mut i32)))) + + (global $g (mut (ref null $big)) (ref.null $big)) + + (func (export "make") (result eqref) + (global.set $g + (struct.new $big (i32.const 0x7aaaaaaa) (i64.const 0x4201020337) (i32.const 0x6bbbbbbb))) + (global.get $g)) + + (func (export "update0") (param $x i32) + (struct.set $big 0 (global.get $g) (local.get $x))) + + (func (export "get0") (result i32) + (struct.get $big 0 (global.get $g))) + + (func (export "update1") (param $hi i32) (param $lo i32) + (struct.set $big 1 (global.get $g) + (i64.or + (i64.shl (i64.extend_u/i32 (local.get $hi)) (i64.const 32)) + (i64.extend_u/i32 (local.get $lo))))) + + (func (export "get1_low") (result i32) + (i32.wrap/i64 (struct.get $big 1 (global.get $g)))) + + (func (export "get1_high") (result i32) + (i32.wrap/i64 + (i64.shr_u (struct.get $big 1 (global.get $g)) (i64.const 32)))) + + (func (export "update2") (param $x i32) + (struct.set $big 2 (global.get $g) (local.get $x))) + + (func (export "get2") (result i32) + (struct.get $big 2 (global.get $g))) + + )`; + + let ins = wasmEvalText(txt).exports; + + let v = ins.make(); + assertEq(v[0], 0x7aaaaaaa); + assertEq(v[1], 0x4201020337n); + assertEq(v[2], 0x6bbbbbbb); + + ins.update0(0x45367101); + assertEq(v[0], 0x45367101); + assertEq(ins.get0(), 0x45367101); + assertEq(v[1], 0x4201020337n); + assertEq(v[2], 0x6bbbbbbb); + + ins.update2(0x62345123); + assertEq(v[0], 0x45367101); + assertEq(v[1], 0x4201020337n); + assertEq(ins.get2(), 0x62345123); + assertEq(v[2], 0x62345123); + + ins.update1(0x77777777, 0x22222222); + assertEq(v[0], 0x45367101); + assertEq(ins.get1_low(), 0x22222222); + assertEq(ins.get1_high(), 0x77777777); + assertEq(v[1], 0x7777777722222222n); + assertEq(v[2], 0x62345123); +} + + +var bin = wasmTextToBinary( + `(module + (type $cons (struct (field i32) (field (ref null $cons)))) + + (global $g (mut (ref null $cons)) (ref.null $cons)) + + (func (export "push") (param i32) + (global.set $g (struct.new $cons (local.get 0) (global.get $g)))) + + (func (export "top") (result i32) + (struct.get $cons 0 (global.get $g))) + + (func (export "pop") + (global.set $g (struct.get $cons 1 (global.get $g)))) + + (func (export "is_empty") (result i32) + (ref.is_null (global.get $g))) + + )`); + +var mod = new WebAssembly.Module(bin); +var ins = new WebAssembly.Instance(mod).exports; +ins.push(37); +ins.push(42); +ins.push(86); +assertEq(ins.top(), 86); +ins.pop(); +assertEq(ins.top(), 42); +ins.pop(); +assertEq(ins.top(), 37); +ins.pop(); +assertEq(ins.is_empty(), 1); +assertErrorMessage(() => ins.pop(), + WebAssembly.RuntimeError, + /dereferencing null pointer/); + +// Check that a wrapped object cannot be passed as an eqref even if the wrapper +// points to the right type. This is a temporary restriction, until we're able +// to avoid dealing with wrappers inside the engine. + +{ + var ins = wasmEvalText( + `(module + (type $Node (struct (field i32))) + (func (export "mk") (result eqref) + (struct.new $Node (i32.const 37))) + (func (export "f") (param $n eqref) (result eqref) + (ref.cast (ref null $Node) (local.get $n))))`).exports; + var n = ins.mk(); + assertEq(ins.f(n), n); + assertErrorMessage(() => ins.f(wrapWithProto(n, {})), TypeError, /can only pass a WebAssembly GC object/); +} + +// Field names. + +// Test that names map to the right fields. + +{ + let ins = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary( + `(module + (type $s (struct + (field $x i32) + (field $y i32))) + + (func $f (param $p (ref null $s)) (result i32) + (struct.get $s $x (local.get $p))) + + (func $g (param $p (ref null $s)) (result i32) + (struct.get $s $y (local.get $p))) + + (func (export "testf") (param $n i32) (result i32) + (call $f (struct.new $s (local.get $n) (i32.mul (local.get $n) (i32.const 2))))) + + (func (export "testg") (param $n i32) (result i32) + (call $g (struct.new $s (local.get $n) (i32.mul (local.get $n) (i32.const 2))))) + + )`))).exports; + + assertEq(ins.testf(10), 10); + assertEq(ins.testg(10), 20); +} + +// Test that field names must be unique in the module. + +assertErrorMessage(() => wasmTextToBinary( + `(module + (type $s (struct (field $x i32))) + (type $t (struct (field $x i32))) + )`), + SyntaxError, + /duplicate identifier for field/); + +// negative tests + +// Wrong type passed as initializer + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary(` +(module + (type $r (struct (field i32))) + (func $f (param f64) (result eqref) + (struct.new $r (local.get 0))) +)`)), +WebAssembly.CompileError, /type mismatch/); + +// Too few values passed for initializer + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary(` +(module + (type $r (struct (field i32) (field i32))) + (func $f (result eqref) + (struct.new $r (i32.const 0))) +)`)), +WebAssembly.CompileError, /popping value from empty stack/); + +// Too many values passed for initializer, sort of + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary(` +(module + (type $r (struct (field i32) (field i32))) + (func $f (result eqref) + (i32.const 0) + (i32.const 1) + (i32.const 2) + struct.new $r) +)`)), +WebAssembly.CompileError, /unused values/); + +// Not referencing a structure type + +assertErrorMessage(() => new WebAssembly.Module(wasmTextToBinary(` +(module + (type (func (param i32) (result i32))) + (func $f (result eqref) + (struct.new 0)) +)`)), +WebAssembly.CompileError, /not a struct type/); + +// Nominal type equivalence for structs, but the prefix rule allows this +// conversion to succeed. + +wasmEvalText(` + (module + (type $p (struct (field i32))) + (type $q (struct (field i32))) + (func $f (result (ref null $p)) + (struct.new $q (i32.const 0)))) +`); + +// The field name is optional, so this should work. + +wasmEvalText(` +(module + (type $s (struct (field i32)))) +`) + +// Empty structs are OK. + +wasmEvalText(` +(module + (type $s (struct))) +`) + +// Multiply defined structures. + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (struct (field $x i32))) + (type $s (struct (field $y i32)))) +`), +SyntaxError, /duplicate type identifier/); + +// Bogus type definition syntax. + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s)) +`), +SyntaxError, /wasm text error/); + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (field $x i32))) +`), +SyntaxError, /expected one of: `func`, `struct`, `array`/); + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (struct (field $x i31)))) +`), +SyntaxError, /wasm text error/); + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (struct (fjeld $x i32)))) +`), +SyntaxError, /wasm text error/); + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (struct abracadabra))) +`), +SyntaxError, /wasm text error/); + +// Function should not reference struct type: syntactic test + +assertErrorMessage(() => wasmEvalText(` +(module + (type $s (struct)) + (type $f (func (param i32) (result i32))) + (func (type 0) (param i32) (result i32) (unreachable))) +`), +WebAssembly.CompileError, /signature index references non-signature/); + +// Can't set immutable fields from JS + +{ + let ins = wasmEvalText( + `(module + (type $s (struct + (field i32) + (field (mut i64)))) + (func (export "make") (result eqref) + (struct.new $s (i32.const 37) (i64.const 42))))`).exports; + let v = ins.make(); + assertErrorMessage(() => v[0] = 12, + Error, + /setting immutable field/); + assertErrorMessage(() => v[1] = 12, + Error, + /setting immutable field/); +} + +// Function should not reference struct type: binary test + +var bad = new Uint8Array([0x00, 0x61, 0x73, 0x6d, + 0x01, 0x00, 0x00, 0x00, + + 0x01, // Type section + 0x03, // Section size + 0x01, // One type + 0x5f, // Struct + 0x00, // Zero fields + + 0x03, // Function section + 0x02, // Section size + 0x01, // One function + 0x00, // Type of function + + 0x0a, // Code section + 0x05, // Section size + 0x01, // One body + 0x03, // Body size + 0x00, // Zero locals + 0x00, // UNREACHABLE + 0x0b]); // END + +assertErrorMessage(() => new WebAssembly.Module(bad), + WebAssembly.CompileError, /signature index references non-signature/); + +// Exercise alias-analysis code for struct access +{ + let txt = + `(module + (type $meh (struct)) + (type $hasOOL (struct + ;; In-line storage + (field i64) (field i64) + (field $ILnonref (mut i64)) (field $ILref (mut eqref)) + (field i64) (field i64) (field i64) (field i64) + (field i64) (field i64) (field i64) (field i64) + (field i64) (field i64) (field i64) (field i64) + ;; Out-of-line storage (or maybe it starts earlier, but + ;; definitely not after this point). + (field $OOLnonref (mut i64)) (field $OOLref (mut eqref))) + ) + (func (export "create") (result eqref) + (struct.new $hasOOL + (i64.const 1) (i64.const 2) + (i64.const 9876) (ref.null $meh) + (i64.const 3) (i64.const 4) (i64.const 5) (i64.const 6) + (i64.const 7) (i64.const 8) (i64.const 9) (i64.const 10) + (i64.const 11) (i64.const 12) (i64.const 13) (i64.const 14) + (i64.const 4321) (ref.null $meh)) + ) + ;; Write to an OOL field, then an IL field, then to an OOL field, so + ;; that we can at least check (from inspection of the optimised MIR) + ;; that the GVN+alias analysis causes the OOL block pointer not to be + ;; reloaded for the second OOL write. First for non-ref fields .. + (func (export "threeSetsNonReffy") (param eqref) + (local (ref $hasOOL)) + (local.set 1 (ref.as_non_null (ref.cast (ref null $hasOOL) (local.get 0)))) + (struct.set $hasOOL 16 (local.get 1) (i64.const 1337)) ;; set $OOLnonref + (struct.set $hasOOL 2 (local.get 1) (i64.const 7331)) ;; set $ILnonref + (struct.set $hasOOL 16 (local.get 1) (i64.const 9009)) ;; set $OOLnonref + ) + ;; and the same for ref fields. + (func (export "threeSetsReffy") (param eqref) + (local (ref $hasOOL)) + (local.set 1 (ref.as_non_null (ref.cast (ref null $hasOOL) (local.get 0)))) + (struct.set $hasOOL 17 (local.get 1) (ref.null $meh)) ;; set $OOLref + (struct.set $hasOOL 3 (local.get 1) (ref.null $meh)) ;; set $ILref + (struct.set $hasOOL 17 (local.get 1) (ref.null $meh)) ;; set $OOLref + ) + )`; + let exports = wasmEvalText(txt).exports; +} + +////////////////////////////////////////////////////////////////////////////// +// +// Checks for requests to create structs with more than MaxStructFields, where +// MaxStructFields == 1000. + +function structNewOfManyFields(numFields) { + let defString = "(type $s (struct "; + for (i = 0; i < numFields; i++) { + defString += "(field i32) "; + } + defString += "))"; + + let insnString = "(struct.new $s "; + for (i = 0; i < numFields; i++) { + insnString += "(i32.const 1337) "; + } + insnString += ")"; + + return "(module " + + defString + + " (func (export \"create\") (result eqref) " + + insnString + + "))"; +} + +{ + // 2000 fields is allowable + let exports = wasmEvalText(structNewOfManyFields(2000)).exports; + let s = exports.create(); + assertEq(s, s); +} +{ + // but 2001 is not + assertErrorMessage(() => wasmEvalText(structNewOfManyFields(2001)), + WebAssembly.CompileError, + /too many fields in struct/); +} + +// FIXME: also check struct.new_default, once it is available in both compilers. diff --git a/js/src/jit-test/tests/wasm/gc/structs2.js b/js/src/jit-test/tests/wasm/gc/structs2.js new file mode 100644 index 0000000000..8c5b94db11 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/structs2.js @@ -0,0 +1,243 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// This tests 8- and 16-bit field accesses for structs. + +// Check that struct.new writes for 8-bit fields do not overwrite subsequent +// data. Because the writes happen forwards in the address space, the only +// way I could think to do this is to force an 8-bit field to occupy the last +// byte of the OOL malloc'd block, and then hope that ASan runs in automation +// will pick up any overrun. I think it's impossible to test this from inside +// the JS+wasm universe. Hence the test is pretty pointless from a purely +// JS+wasm interpretation. +{ + let txt = + `(module + (type $hasOOL (struct + ;; In-line storage; 16 fields that preserve 16-alignment + (field i64) (field i64) (field i64) (field i64) ;; 32 + (field i64) (field i64) (field i64) (field i64) ;; 64 + (field i64) (field i64) (field i64) (field i64) ;; 96 + (field i64) (field i64) (field i64) (field i64) ;; 128 + ;; Out-of-line storage (or maybe it starts earlier, but + ;; definitely not after this point). 16 bytes on the + ;; basis that StructLayout::close will round the requested + ;; block size up to at max the next 16 byte boundary. + ;; The goal is that the last (field i8) is right at the + ;; end of the resulting malloc'd block, so that, if the + ;; struct.new initialisation code mistakenly initialises + ;; that field with a write larger than 8 bits, then we'll + ;; have a write off the end of the malloc'd block, which + ;; ASan automation runs should detect. + (field i8) (field i8) (field i8) (field i8) + (field i8) (field i8) (field i8) (field i8) + (field i8) (field i8) (field i8) (field i8) + (field i8) (field i8) (field i8) (field i8)) + ) + (func (export "build8") + (param $filler i64) (param $interesting i32) (result eqref) + (struct.new $hasOOL + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $interesting) (local.get $interesting) + (local.get $interesting) (local.get $interesting) + (local.get $interesting) (local.get $interesting) + (local.get $interesting) (local.get $interesting) + (local.get $interesting) (local.get $interesting) + (local.get $interesting) (local.get $interesting) + (local.get $interesting) (local.get $interesting) + (local.get $interesting) (local.get $interesting) + ) + ) + )`; + let exports = wasmEvalText(txt).exports; + let obj8 = exports.build8(0x1234n, 0x5678); + // The above call should trigger OOB writes if the struct.new field + // writes are too large, but those will only be visible if we're running + // on ASan or Valgrind. In any case, add a fake data dependency below, so + // that the construction of the object can't (so easily) be optimised away. + assertEq(obj8[0] + BigInt(obj8[31]), 0x12ACn); // == 0x1234 + 0x78 +} + +// And exactly the same, except for 16 bit fields. +{ + let txt = + `(module + (type $hasOOL (struct + ;; in-line storage + (field i64) (field i64) (field i64) (field i64) ;; 32 + (field i64) (field i64) (field i64) (field i64) ;; 64 + (field i64) (field i64) (field i64) (field i64) ;; 96 + (field i64) (field i64) (field i64) (field i64) ;; 128 + (field i16) (field i16) (field i16) (field i16) + (field i16) (field i16) (field i16) (field i16)) + ) + (func (export "build16") + (param $filler i64) (param $interesting i32) (result eqref) + (struct.new $hasOOL + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $filler) (local.get $filler) + (local.get $interesting) (local.get $interesting) + (local.get $interesting) (local.get $interesting) + (local.get $interesting) (local.get $interesting) + (local.get $interesting) (local.get $interesting) + ) + ) + )`; + let exports = wasmEvalText(txt).exports; + let obj16 = exports.build16(0x4321n, 0x7865); + assertEq(obj16[0] + BigInt(obj16[23]), 0xBB86n); // == 0x4321 + 0x7865 +} + +// Test that 8-bit field writes do not overwrite adjacent fields. +{ + let txt = + `(module + (type $struct8x8 + (struct (field i8) (field i8) (field i8) (field (mut i8)) + (field i8) (field i8) (field i8) (field i8) + )) + (func (export "create") (result eqref) + (struct.new $struct8x8 (i32.const 0x55) (i32.const 0x55) + (i32.const 0x55) (i32.const 0x55) + (i32.const 0x55) (i32.const 0x55) + (i32.const 0x55) (i32.const 0x55) + )) + (func (export "writeField8x8_3") (param $p eqref) (param $v i32) + (struct.set $struct8x8 3 (ref.cast (ref null $struct8x8) (local.get $p)) + (local.get $v)) + ) + )`; + let exports = wasmEvalText(txt).exports; + let theObject = exports.create(); + exports.writeField8x8_3(theObject, 0x77); + assertEq(theObject[0], 0x55); + assertEq(theObject[1], 0x55); + assertEq(theObject[2], 0x55); + assertEq(theObject[3], 0x77); + assertEq(theObject[4], 0x55); + assertEq(theObject[5], 0x55); + assertEq(theObject[6], 0x55); + assertEq(theObject[7], 0x55); +} + +// Test that 16-bit field writes do not overwrite adjacent fields. +{ + let txt = + `(module + (type $struct16x8 + (struct (field i16) (field i16) (field i16) (field (mut i16)) + (field i16) (field i16) (field i16) (field i16) + )) + (func (export "create") (result eqref) + (struct.new $struct16x8 (i32.const 0x5555) (i32.const 0x5555) + (i32.const 0x5555) (i32.const 0x5555) + (i32.const 0x5555) (i32.const 0x5555) + (i32.const 0x5555) (i32.const 0x5555) + )) + (func (export "writeField16x8_3") (param $p eqref) (param $v i32) + (struct.set $struct16x8 3 (ref.cast (ref null $struct16x8) (local.get $p)) + (local.get $v)) + ) + )`; + let exports = wasmEvalText(txt).exports; + let theObject = exports.create(); + exports.writeField16x8_3(theObject, 0x7766); + assertEq(theObject[0], 0x5555); + assertEq(theObject[1], 0x5555); + assertEq(theObject[2], 0x5555); + assertEq(theObject[3], 0x7766); + assertEq(theObject[4], 0x5555); + assertEq(theObject[5], 0x5555); + assertEq(theObject[6], 0x5555); + assertEq(theObject[7], 0x5555); +} + +// Test that 8-bit field reads sign/zero extend correctly. +{ + let txt = + `(module + (type $struct8x8 + (struct (field i8) (field i8) (field i8) (field i8) + (field i8) (field i8) (field i8) (field i8) + )) + (func (export "create") (result eqref) + (struct.new $struct8x8 (i32.const 0x11) (i32.const 0x82) + (i32.const 0x23) (i32.const 0x94) + (i32.const 0x35) (i32.const 0xA6) + (i32.const 0x47) (i32.const 0xB8) + )) + ;; read i8 from a field, unsigned extend, read value has top bit 0 + (func (export "readU8hi0") (param $p eqref) (result i32) + (struct.get_u $struct8x8 2 (ref.cast (ref null $struct8x8) (local.get $p))) + ) + ;; read i8 from a field, unsigned extend, read value has top bit 1 + (func (export "readU8hi1") (param $p eqref) (result i32) + (struct.get_u $struct8x8 3 (ref.cast (ref null $struct8x8) (local.get $p))) + ) + ;; read i8 from a field, signed extend, read value has top bit 0 + (func (export "readS8hi0") (param $p eqref) (result i32) + (struct.get_s $struct8x8 4 (ref.cast (ref null $struct8x8) (local.get $p))) + ) + ;; read i8 from a field, signed extend, read value has top bit 1 + (func (export "readS8hi1") (param $p eqref) (result i32) + (struct.get_s $struct8x8 5 (ref.cast (ref null $struct8x8) (local.get $p))) + ) + )`; + let exports = wasmEvalText(txt).exports; + let theObject = exports.create(); + assertEq(exports.readU8hi0(theObject), 0x23); // zx of 0x23 + assertEq(exports.readU8hi1(theObject), 0x94); // zx of 0x94 + assertEq(exports.readS8hi0(theObject), 0x35); // sx of 0x35 + assertEq(exports.readS8hi1(theObject), -0x5A); // sx of 0xA6 +} + +// Test that 16-bit field reads sign/zero extend correctly. +{ + let txt = + `(module + (type $struct16x8 + (struct (field i16) (field i16) (field i16) (field i16) + (field i16) (field i16) (field i16) (field i16) + )) + (func (export "create") (result eqref) + (struct.new $struct16x8 (i32.const 0x11FF) (i32.const 0x82FE) + (i32.const 0x23FD) (i32.const 0x94FC) + (i32.const 0x35FB) (i32.const 0xA6FA) + (i32.const 0x47F9) (i32.const 0xB8F8) + )) + ;; read i16 from a field, unsigned extend, read value has top bit 0 + (func (export "readU16hi0") (param $p eqref) (result i32) + (struct.get_u $struct16x8 2 (ref.cast (ref null $struct16x8) (local.get $p))) + ) + ;; read i16 from a field, unsigned extend, read value has top bit 1 + (func (export "readU16hi1") (param $p eqref) (result i32) + (struct.get_u $struct16x8 3 (ref.cast (ref null $struct16x8) (local.get $p))) + ) + ;; read i16 from a field, signed extend, read value has top bit 0 + (func (export "readS16hi0") (param $p eqref) (result i32) + (struct.get_s $struct16x8 4 (ref.cast (ref null $struct16x8) (local.get $p))) + ) + ;; read i16 from a field, signed extend, read value has top bit 1 + (func (export "readS16hi1") (param $p eqref) (result i32) + (struct.get_s $struct16x8 5 (ref.cast (ref null $struct16x8) (local.get $p))) + ) + )`; + let exports = wasmEvalText(txt).exports; + let theObject = exports.create(); + assertEq(exports.readU16hi0(theObject), 0x23FD); // zx of 0x23FD + assertEq(exports.readU16hi1(theObject), 0x94FC); // zx of 0x94FC + assertEq(exports.readS16hi0(theObject), 0x35FB); // sx of 0x35FB + assertEq(exports.readS16hi1(theObject), -0x5906); // sx of 0xA6FC +} diff --git a/js/src/jit-test/tests/wasm/gc/supertype_later_in_group.js b/js/src/jit-test/tests/wasm/gc/supertype_later_in_group.js new file mode 100644 index 0000000000..6f5ce48282 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/supertype_later_in_group.js @@ -0,0 +1,15 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// Checking that we are correctly validating all subtyping rules. +// In this example, $b should be a subtype of $a, even if their field types +// will be loaded later. +wasmValidateText(` +(module + (rec + (type $a (struct (field (ref $notParsedYet)))) + (type $b (sub $a (struct (field (ref $notParsedYet2))))) + + (type $notParsedYet (struct)) + (type $notParsedYet2 (sub $notParsedYet (struct (field i32)))) + ) +)`); diff --git a/js/src/jit-test/tests/wasm/gc/tables-generalized-struct.js b/js/src/jit-test/tests/wasm/gc/tables-generalized-struct.js new file mode 100644 index 0000000000..b0bd964027 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/tables-generalized-struct.js @@ -0,0 +1,48 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +// table.set in bounds with i32 x eqref - works, no value generated +// table.set with (ref null T) - works +// table.set with null - works +// table.set out of bounds - fails + +{ + let ins = wasmEvalText( + `(module + (table (export "t") 10 eqref) + (type $dummy (struct (field i32))) + (func (export "set_eqref") (param i32) (param eqref) + (table.set (local.get 0) (local.get 1))) + (func (export "set_null") (param i32) + (table.set (local.get 0) (ref.null eq))) + (func (export "set_ref") (param i32) (param eqref) + (table.set (local.get 0) (ref.cast (ref null $dummy) (local.get 1)))) + (func (export "make_struct") (result eqref) + (struct.new $dummy (i32.const 37))))`); + let a = ins.exports.make_struct(); + ins.exports.set_eqref(3, a); + assertEq(ins.exports.t.get(3), a); + ins.exports.set_null(3); + assertEq(ins.exports.t.get(3), null); + let b = ins.exports.make_struct(); + ins.exports.set_ref(5, b); + assertEq(ins.exports.t.get(5), b); + + assertErrorMessage(() => ins.exports.set_eqref(10, a), WebAssembly.RuntimeError, /index out of bounds/); + assertErrorMessage(() => ins.exports.set_eqref(-1, a), WebAssembly.RuntimeError, /index out of bounds/); +} + +// table.grow on table of eqref with non-null ref value + +{ + let ins = wasmEvalText( + `(module + (type $S (struct (field i32) (field f64))) + (table (export "t") 2 eqref) + (func (export "f") (result i32) + (table.grow (struct.new $S (i32.const 0) (f64.const 3.14)) (i32.const 1))))`); + assertEq(ins.exports.t.length, 2); + assertEq(ins.exports.f(), 2); + assertEq(ins.exports.t.length, 3); + assertEq(typeof ins.exports.t.get(2), "object"); +} + diff --git a/js/src/jit-test/tests/wasm/gc/trailers-gc-stress.js b/js/src/jit-test/tests/wasm/gc/trailers-gc-stress.js new file mode 100644 index 0000000000..d1975a9541 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/trailers-gc-stress.js @@ -0,0 +1,166 @@ +// |jit-test| skip-if: !wasmGcEnabled() || getBuildConfiguration().simulator + +// This test is intended to test what was committed in +// +// Bug 1817385 - wasm-gc: reduce cost of allocation and GC paths +// and +// Bug 1820120 - Manage Wasm{Array,Struct}Object OOL-storage-blocks +// using a thread-private cache +// +// and in particular the latter. The patches in these bugs reduce the cost of +// wasm-gc struct/array allocation and collection, in part by better +// integrating those objects with our generational GC facility. +// +// Existing tests do not cover all of those paths. In particular they do not +// exercise both set-subtraction algorithms in Nursery::freeTrailerBlocks. +// This test does, though. +// +// The test first creates an "primary" array of 1500 elements. Each element +// is a reference to a secondary array of between 1 and 50 int32s. These +// secondary arrays have size chosen randomly, and the elements are also +// random. +// +// Then, elements of the primary array are replaced. An index in the range 0 +// .. N - 1 is randomly chosen, and the element there is replaced by a +// randomly-created secondary array. This is repeated 500,000 times with +// N = 800. +// +// Finally, all of the above is repeated, but with N = 1200. +// +// As a result just over a million arrays and their trailer blocks, of various +// sizes, are allocated and deallocated. With N = 800, in +// js::Nursery::freeTrailerBlocks, we end up with trailersRemovedUsed_ of +// around 800, so one of the set-subtraction algorithms is exercised. +// With N = 1200, the other is exercised. It's not entirely clear why changing +// N causes trailersRemovedUsed_ to have more or less the same value during +// nursery collection, but the correlation does seem fairly robust. +// +// The test is skipped on the simulator because it takes too long to run, and +// triggers timeouts. + +let t = +`(module + + ;; A simple pseudo-random number generator. + ;; Produces numbers in the range 0 .. 2^16-1. + (global $rngState + (mut i32) (i32.const 1) + ) + (func $rand (export "rand") (result i32) + (local $t i32) + ;; update $rngState + (local.set $t (global.get $rngState)) + (local.set $t (i32.mul (local.get $t) (i32.const 1103515245))) + (local.set $t (i32.add (local.get $t) (i32.const 12345))) + (global.set $rngState (local.get $t)) + ;; pull 16 random bits out of it + (local.set $t (i32.shr_u (local.get $t) (i32.const 15))) + (local.set $t (i32.and (local.get $t) (i32.const 0xFFFF))) + (local.get $t) + ) + + ;; Array types + (type $tArrayI32 (array (mut i32))) ;; "secondary array" above + (type $tArrayArrayI32 (array (mut (ref null $tArrayI32)))) ;; "primary array" + + ;; Create an array ("secondary array") containing random numbers, with a + ;; size between 1 and 50, also randomly chosen. + (func $createSecondaryArray (export "createSecondaryArray") + (result (ref $tArrayI32)) + (local $i i32) + (local $nElems i32) + (local $arr (ref $tArrayI32)) + (local.set $nElems (call $rand)) + (local.set $nElems (i32.rem_u (local.get $nElems) (i32.const 50))) + (local.set $nElems (i32.add (local.get $nElems) (i32.const 1))) + (local.set $arr (array.new $tArrayI32 (i32.const 0) (local.get $nElems))) + (loop $cont + (array.set $tArrayI32 (local.get $arr) (local.get $i) (call $rand)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $cont (i32.lt_u (local.get $i) (local.get $nElems))) + ) + (local.get $arr) + ) + + ;; Create an array (the "primary array") of 1500 elements of + ;; type ref-of-tArrayI32. + (func $createPrimaryArray (export "createPrimaryArray") + (result (ref $tArrayArrayI32)) + (local $i i32) + (local $arrarr (ref $tArrayArrayI32)) + (local.set $arrarr (array.new $tArrayArrayI32 (ref.null $tArrayI32) + (i32.const 1500))) + (loop $cont + (array.set $tArrayArrayI32 (local.get $arrarr) + (local.get $i) (call $createSecondaryArray)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $cont (i32.lt_u (local.get $i) (i32.const 1500))) + ) + (local.get $arrarr) + ) + + ;; Use $createPrimaryArray to create an initial array. Then randomly replace + ;; elements for a while. + (func $churn (export "churn") (param $thresh i32) (result i32) + (local $i i32) + (local $j i32) + (local $finalSum i32) + (local $arrarr (ref $tArrayArrayI32)) + (local $arr (ref null $tArrayI32)) + (local $arrLen i32) + (local.set $arrarr (call $createPrimaryArray)) + ;; This loop iterates 500,000 times. Each iteration, it chooses + ;; a randomly element in $arrarr and replaces it with a new + ;; random array of 32-bit ints. + (loop $cont + ;; make $j be a random number in 0 .. $thresh-1. + ;; Then replace that index in $arrarr with a new random arrayI32. + (local.set $j (i32.rem_u (call $rand) (local.get $thresh))) + (array.set $tArrayArrayI32 (local.get $arrarr) + (local.get $j) (call $createSecondaryArray)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $cont (i32.lt_u (local.get $i) (i32.const 500000))) + ) + + ;; Finally, compute a checksum by summing all the numbers + ;; in all secondary arrays. This simply assumes that all of the refs to + ;; secondary arrays are non-null, which isn't per-se guaranteed by the + ;; previous loop, but it works in this case because the RNG + ;; produces each index value to overwrite at least once. + (local.set $finalSum (i32.const 0)) + (local.set $i (i32.const 0)) ;; loop var for the outer loop + (loop $outer + ;; body of outer loop + ;; $arr = $arrarr[i] + (local.set $arr (array.get $tArrayArrayI32 (local.get $arrarr) + (local.get $i))) + ;; iterate over $arr + (local.set $arrLen (array.len (local.get $arr))) + (local.set $j (i32.const 0)) ;; loop var for the inner loop + (loop $inner + ;; body of inner loop + (local.set $finalSum + (i32.rotl (local.get $finalSum) (i32.const 1))) + (local.set $finalSum + (i32.xor (local.get $finalSum) + (array.get $tArrayI32 (local.get $arr) + (local.get $j)))) + ;; loop control for the inner loop + (local.set $j (i32.add (local.get $j) (i32.const 1))) + (br_if $inner (i32.lt_u (local.get $j) (local.get $arrLen))) + ) + ;; loop control for the outer loop + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $outer (i32.lt_u (local.get $i) (i32.const 1500))) + ) + + ;; finally, roll in the final value of the RNG state + (i32.xor (local.get $finalSum) (global.get $rngState)) + ) +)`; + +let i = wasmEvalText(t); +let fns = i.exports; + +assertEq(fns.churn(800), -575895114); +assertEq(fns.churn(1200), -1164697516); diff --git a/js/src/jit-test/tests/wasm/gc/value_subtyping.js b/js/src/jit-test/tests/wasm/gc/value_subtyping.js new file mode 100644 index 0000000000..bc0a3df678 --- /dev/null +++ b/js/src/jit-test/tests/wasm/gc/value_subtyping.js @@ -0,0 +1,305 @@ +// |jit-test| skip-if: !wasmGcEnabled() + +function simpleTypeSection(types) { + return types.map((x, i) => `(type \$${i} ${x})`).join('\n'); +} + +function assertSubtype(superType, subType, types) { + types = types || []; + wasmEvalText(`(module + ${types} + (func + unreachable + (block (param ${subType}) + (block (param ${superType}) + drop + ) + ) + ) + )`); +} + +function assertNotSubtype(superType, subType, types) { + assertErrorMessage(() => { + assertSubtype(superType, subType, types); + }, WebAssembly.CompileError, /type mismatch/); +} + +// Primitive trivial subtyping +assertSubtype('i32', 'i32'); +assertSubtype('i64', 'i64'); +assertSubtype('f32', 'f32'); +assertSubtype('f64', 'f64'); +assertSubtype('eqref', 'eqref'); +assertSubtype('funcref', 'funcref'); + +// No subtyping relation between funcref, anyref, externref. These are our top +// types. +assertNotSubtype('funcref', 'anyref'); +assertNotSubtype('anyref', 'funcref'); +assertNotSubtype('funcref', 'externref'); +assertNotSubtype('externref', 'funcref'); +assertNotSubtype('externref', 'anyref'); +assertNotSubtype('anyref', 'externref'); + +// eqref is a subtype of anyref +assertSubtype('anyref', 'eqref'); + +// structref is a subtype of eqref and anyref +assertSubtype('anyref', 'structref'); +assertSubtype('eqref', 'structref'); + +// arrayref is a subtype of eqref and anyref +assertSubtype('anyref', 'arrayref'); +assertSubtype('eqref', 'arrayref'); + +// Structs are subtypes of anyref, eqref, and structref +assertSubtype( + 'anyref', + '(ref 0)', + simpleTypeSection(['(struct)'])); +assertSubtype( + 'eqref', + '(ref 0)', + simpleTypeSection(['(struct)'])); +assertSubtype( + 'structref', + '(ref 0)', + simpleTypeSection(['(struct)'])); + +// Struct identity +assertSubtype( + '(ref 0)', + '(ref 1)', + simpleTypeSection(['(struct)', '(struct)'])); +assertSubtype( + '(ref 1)', + '(ref 0)', + simpleTypeSection(['(struct)', '(struct)'])); + +// Self referential struct +assertSubtype( + '(ref 1)', + '(ref 0)', + simpleTypeSection(['(struct (ref 0))', '(struct (ref 1))'])); + +// Mutually referential structs +assertSubtype( + '(ref 2)', + '(ref 0)', + `(rec + (type (struct (ref 1))) + (type (struct (ref 0))) + ) + (rec + (type (struct (ref 3))) + (type (struct (ref 2))) + )`); + +// Struct subtypes can have extra fields +assertSubtype( + '(ref 0)', + '(ref 1)', + `(type (struct)) + (type (sub 0 (struct (field i32))))`); +assertSubtype( + '(ref 0)', + '(ref 1)', + `(type (struct)) + (type (sub 0 (struct (field i32) (field i32))))`); + +// Struct supertypes cannot have extra fields +assertNotSubtype( + '(ref 0)', + '(ref 1)', + simpleTypeSection([ + '(struct (field i32))', + '(struct)'])); + +// Struct field mutability must match +assertSubtype( + '(ref 0)', + '(ref 1)', + simpleTypeSection([ + '(struct (field (mut i32)))', + '(struct (field (mut i32)))'])); +assertSubtype( + '(ref 0)', + '(ref 1)', + simpleTypeSection([ + '(struct (field i32))', + '(struct (field i32))'])); +assertNotSubtype( + '(ref 0)', + '(ref 1)', + simpleTypeSection([ + '(struct (field (mut i32)))', + '(struct (field i32))'])); +assertNotSubtype( + '(ref 0)', + '(ref 1)', + simpleTypeSection([ + '(struct (field i32))', + '(struct (field (mut i32)))'])); + +// Struct fields are invariant when mutable +assertSubtype( + '(ref 2)', + '(ref 3)', + simpleTypeSection([ + '(struct)', + '(struct)', + '(struct (field (mut (ref 0))))', + '(struct (field (mut (ref 1))))'])); +assertNotSubtype( + '(ref 2)', + '(ref 3)', + simpleTypeSection([ + '(struct)', + '(struct (field i32))', + '(struct (field (mut (ref 0))))', + '(struct (field (mut (ref 1))))'])); + +// Struct fields are covariant when immutable +assertSubtype( + '(ref 2)', + '(ref 3)', + `(type (struct)) + (type (sub 0 (struct (field i32)))) + (type (struct (field (ref 0)))) + (type (sub 2 (struct (field (ref 1)))))`); + +// Arrays are subtypes of anyref, eqref, and arrayref +assertSubtype( + 'anyref', + '(ref 0)', + simpleTypeSection(['(array i32)'])); +assertSubtype( + 'eqref', + '(ref 0)', + simpleTypeSection(['(array i32)'])); +assertSubtype( + 'arrayref', + '(ref 0)', + simpleTypeSection(['(array i32)'])); + +// Array identity +assertSubtype( + '(ref 0)', + '(ref 1)', + simpleTypeSection(['(array i32)', '(array i32)'])); +assertSubtype( + '(ref 1)', + '(ref 0)', + simpleTypeSection(['(array i32)', '(array i32)'])); + +// Self referential array +assertSubtype( + '(ref 1)', + '(ref 0)', + simpleTypeSection(['(array (ref 0))', '(array (ref 1))'])); + +// Mutually referential arrays +assertSubtype( + '(ref 2)', + '(ref 0)', + `(rec + (type (array (ref 1))) + (type (array (ref 0))) + ) + (rec + (type (array (ref 3))) + (type (array (ref 2))) + )`); + +// Array mutability must match +assertSubtype( + '(ref 0)', + '(ref 1)', + simpleTypeSection([ + '(array (mut i32))', + '(array (mut i32))'])); +assertSubtype( + '(ref 0)', + '(ref 1)', + simpleTypeSection([ + '(array i32)', + '(array i32)'])); +assertNotSubtype( + '(ref 0)', + '(ref 1)', + simpleTypeSection([ + '(array (mut i32))', + '(array i32)'])); +assertNotSubtype( + '(ref 0)', + '(ref 1)', + simpleTypeSection([ + '(array i32)', + '(array (mut i32))'])); + +// Array elements are invariant when mutable +assertSubtype( + '(ref 2)', + '(ref 3)', + simpleTypeSection([ + '(struct)', + '(struct)', + '(array (mut (ref 0)))', + '(array (mut (ref 1)))'])); +assertNotSubtype( + '(ref 2)', + '(ref 3)', + simpleTypeSection([ + '(struct)', + '(struct (field i32))', + '(array (mut (ref 0)))', + '(array (mut (ref 1)))'])); + +// Array elements are covariant when immutable +assertSubtype( + '(ref 2)', + '(ref 3)', + `(type (struct)) + (type (sub 0 (struct (field i32)))) + (type (array (ref 0))) + (type (sub 2 (array (ref 1))))`); + +// nullref is a subtype of everything in anyref hierarchy +assertSubtype('anyref', 'nullref'); +assertSubtype('eqref', 'nullref'); +assertSubtype('structref', 'nullref'); +assertSubtype('arrayref', 'nullref'); +assertSubtype('(ref null 0)', 'nullref', simpleTypeSection(['(struct)'])); +assertSubtype('(ref null 0)', 'nullref', simpleTypeSection(['(array i32)'])); + +// nullref is not a subtype of any other hierarchy +assertNotSubtype('funcref', 'nullref'); +assertNotSubtype('(ref null 0)', 'nullref', simpleTypeSection(['(func)'])); +assertNotSubtype('externref', 'nullref'); + +// nullfuncref is a subtype of everything in funcref hierarchy +assertSubtype('funcref', 'nullfuncref'); +assertSubtype('(ref null 0)', 'nullfuncref', simpleTypeSection(['(func)'])); + +// nullfuncref is not a subtype of any other hierarchy +assertNotSubtype('anyref', 'nullfuncref'); +assertNotSubtype('eqref', 'nullfuncref'); +assertNotSubtype('structref', 'nullfuncref'); +assertNotSubtype('arrayref', 'nullfuncref'); +assertNotSubtype('externref', 'nullfuncref'); +assertNotSubtype('(ref null 0)', 'nullfuncref', simpleTypeSection(['(struct)'])); +assertNotSubtype('(ref null 0)', 'nullfuncref', simpleTypeSection(['(array i32)'])); + +// nullexternref is a subtype of everything in externref hierarchy +assertSubtype('externref', 'nullexternref'); + +// nullexternref is not a subtype of any other hierarchy +assertNotSubtype('anyref', 'nullexternref'); +assertNotSubtype('eqref', 'nullexternref'); +assertNotSubtype('structref', 'nullexternref'); +assertNotSubtype('arrayref', 'nullexternref'); +assertNotSubtype('funcref', 'nullexternref'); +assertNotSubtype('(ref null 0)', 'nullexternref', simpleTypeSection(['(struct)'])); +assertNotSubtype('(ref null 0)', 'nullexternref', simpleTypeSection(['(array i32)'])); +assertNotSubtype('(ref null 0)', 'nullexternref', simpleTypeSection(['(func)'])); |