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 (sub (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, `expected ${x} == ${y}, at ${i}`); } } 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']); } // Runtime test of casting with extra values passed through the branch { 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, `expected ${x} == ${y}, at ${i}`); } } function testExtra(values) { let types = values.map((type) => type).join(" "); // Construct a `select` function which takes `values` twice and a value to // cast. It will return the first values if the 'true' value is passed, // otherwise it will return the 'false' 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 ${types} ${types} eqref) (result ${types}) (block (result ${types} anyref) (block (result ${types} (ref $t)) ;; branch to 0, passing along 'true' values and the 'casted' value ${values.map((_, i) => `local.get ${i}`).join(" ")} local.get ${values.length * 2} br_on_cast 0 anyref (ref $t) ;; branch to 1, passing along 'false' values and the 'uncasted' value ${values.map((_, i) => `local.get ${values.length + i}`).join(" ")} local.get ${values.length * 2} br_on_cast_fail 1 anyref (ref $t) ;; cast and cast_fail of the same type cannot both fallthrough unreachable ) ;; fallthrough, passing our values along ) ;; drop the casted/uncasted value at the top, we don't need it drop ) )`).exports; let t = makeT(); let f = makeF(); let trueValues = values.map((type, i) => type == 'i64' ? BigInt(i) : i); let falseValues = values.map((type, i) => { let value = values.length + i; return type == 'i64' ? BigInt(value) : value; }); assertEqResults(select(...trueValues, ...falseValues, t), trueValues); assertEqResults(select(...trueValues, ...falseValues, 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::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; }