diff options
author | Alex Crichton <alex@alexcrichton.com> | 2018-05-16 18:40:45 -0500 |
---|---|---|
committer | Alon Zakai <alonzakai@gmail.com> | 2018-05-16 16:40:45 -0700 |
commit | 3c5a2de669542caebd7ef001db6678b70375f0b5 (patch) | |
tree | bbe25d21142627cc04041d7ecbea9d4d15f3a680 /src/wasm2asm.h | |
parent | d4aa0d30234ac9e553d743bd881a767d96554a4a (diff) | |
download | binaryen-3c5a2de669542caebd7ef001db6678b70375f0b5.tar.gz binaryen-3c5a2de669542caebd7ef001db6678b70375f0b5.tar.bz2 binaryen-3c5a2de669542caebd7ef001db6678b70375f0b5.zip |
wasm2asm: Implement float<->int conversions (#1550)
This commit lifts the same conversion strategy that `emcc` takes to convert
between floats point numbers and integers, and it should implement all the
various matrices of i32/u32/i64/u64 to f32/f64
Some refactoring was performed in the i64->i32 pass to allow for temporary
variables to get allocated which have types other than i32, but otherwise this
contains a pretty direct translation of `emcc`'s operations to `wasm2asm`.
Diffstat (limited to 'src/wasm2asm.h')
-rw-r--r-- | src/wasm2asm.h | 252 |
1 files changed, 234 insertions, 18 deletions
diff --git a/src/wasm2asm.h b/src/wasm2asm.h index 9e3abaa25..8dd569d82 100644 --- a/src/wasm2asm.h +++ b/src/wasm2asm.h @@ -114,6 +114,7 @@ public: struct Flags { bool debug = false; bool pedantic = false; + bool allowAsserts = false; }; Wasm2AsmBuilder(Flags f) : flags(f) {} @@ -213,6 +214,9 @@ private: Ref makeAssertReturnFunc(SExpressionWasmBuilder& sexpBuilder, Builder& wasmBuilder, Element& e, Name testFuncName); + Ref makeAssertReturnNanFunc(SExpressionWasmBuilder& sexpBuilder, + Builder& wasmBuilder, + Element& e, Name testFuncName); Ref makeAssertTrapFunc(SExpressionWasmBuilder& sexpBuilder, Builder& wasmBuilder, Element& e, Name testFuncName); @@ -416,13 +420,35 @@ Ref Wasm2AsmBuilder::processWasm(Module* wasm) { } tableSize = pow2ed; // globals + bool generateFetchHighBits = false; for (auto& global : wasm->globals) { addGlobal(asmFunc[3], global.get()); + if (flags.allowAsserts && global->name == INT64_TO_32_HIGH_BITS) { + generateFetchHighBits = true; + } } // functions for (auto& func : wasm->functions) { asmFunc[3]->push_back(processFunction(func.get())); } + if (generateFetchHighBits) { + Builder builder(allocator); + std::vector<Type> params; + std::vector<Type> vars; + asmFunc[3]->push_back(processFunction(builder.makeFunction( + WASM_FETCH_HIGH_BITS, + std::move(params), + i32, + std::move(vars), + builder.makeGetGlobal(INT64_TO_32_HIGH_BITS, i32) + ))); + auto e = new Export(); + e->name = WASM_FETCH_HIGH_BITS; + e->value = WASM_FETCH_HIGH_BITS; + e->kind = ExternalKind::Function; + wasm->addExport(e); + } + addTables(asmFunc[3], wasm); // memory XXX addExports(asmFunc[3], wasm); @@ -1320,6 +1346,18 @@ Ref Wasm2AsmBuilder::processFunctionBody(Function* func, IString result) { Ref visitConst(Const* curr) { switch (curr->type) { case i32: return ValueBuilder::makeInt(curr->value.geti32()); + // An i64 argument translates to two actual arguments to asm.js + // functions, so we do a bit of a hack here to get our one `Ref` to look + // like two function arguments. + case i64: { + unsigned lo = (unsigned) curr->value.geti64(); + unsigned hi = (unsigned) (curr->value.geti64() >> 32); + std::ostringstream out; + out << lo << "," << hi; + std::string os = out.str(); + IString name(os.c_str(), false); + return ValueBuilder::makeName(name); + } case f32: { Ref ret = ValueBuilder::makeCall(MATH_FROUND); Const fake(allocator); @@ -1393,6 +1431,31 @@ Ref Wasm2AsmBuilder::processFunctionBody(Function* func, IString result) { ValueBuilder::makeSub(ValueBuilder::makeName(HEAP32), zero) ); } + // generate (~~expr), what Emscripten does + case TruncSFloat32ToInt32: + case TruncSFloat64ToInt32: + return ValueBuilder::makeUnary( + B_NOT, + ValueBuilder::makeUnary( + B_NOT, + visit(curr->value, EXPRESSION_RESULT) + )); + + // generate (~~expr >>> 0), what Emscripten does + case TruncUFloat32ToInt32: + case TruncUFloat64ToInt32: + return ValueBuilder::makeBinary( + ValueBuilder::makeUnary( + B_NOT, + ValueBuilder::makeUnary( + B_NOT, + visit(curr->value, EXPRESSION_RESULT) + ) + ), + TRSHIFT, + ValueBuilder::makeNum(0) + ); + default: { std::cerr << "Unhandled unary i32 operator: " << curr << std::endl; @@ -1470,6 +1533,37 @@ Ref Wasm2AsmBuilder::processFunctionBody(Function* func, IString result) { ValueBuilder::makeSub(ValueBuilder::makeName(HEAPF32), zero) ); } + // Coerce the integer to a float as emscripten does + case ConvertSInt32ToFloat32: + return makeAsmCoercion( + makeAsmCoercion(visit(curr->value, EXPRESSION_RESULT), ASM_INT), + ASM_FLOAT + ); + case ConvertSInt32ToFloat64: + return makeAsmCoercion( + makeAsmCoercion(visit(curr->value, EXPRESSION_RESULT), ASM_INT), + ASM_DOUBLE + ); + + // Generate (expr >>> 0), followed by a coercion + case ConvertUInt32ToFloat32: + return makeAsmCoercion( + ValueBuilder::makeBinary( + visit(curr->value, EXPRESSION_RESULT), + TRSHIFT, + ValueBuilder::makeInt(0) + ), + ASM_FLOAT + ); + case ConvertUInt32ToFloat64: + return makeAsmCoercion( + ValueBuilder::makeBinary( + visit(curr->value, EXPRESSION_RESULT), + TRSHIFT, + ValueBuilder::makeInt(0) + ), + ASM_DOUBLE + ); // TODO: more complex unary conversions default: std::cerr << "Unhandled unary float operator: " << curr @@ -1756,7 +1850,36 @@ Ref Wasm2AsmBuilder::processFunctionBody(Function* func, IString result) { return ExpressionProcessor(this, func).visit(func->body, result); } -static Ref makeInstantiation() { +static void makeInstantiation(Ref ret) { + // var __array_buffer = new ArrayBuffer(..) + Ref mem = ValueBuilder::makeNew( + ValueBuilder::makeCall(ARRAY_BUFFER, ValueBuilder::makeInt(0x10000))); + Ref arrayBuffer = ValueBuilder::makeVar(); + Name buffer("__array_buffer"); + ValueBuilder::appendToVar(arrayBuffer, buffer, mem); + flattenAppend(ret, arrayBuffer); + + // var HEAP32 = new Int32Array(__array_buffer); + Ref heap32Array = ValueBuilder::makeNew( + ValueBuilder::makeCall(INT32ARRAY, ValueBuilder::makeName(buffer))); + Ref heap32 = ValueBuilder::makeVar(); + ValueBuilder::appendToVar(heap32, HEAP32, heap32Array); + flattenAppend(ret, heap32); + + // var HEAPF32 = new Float32Array(__array_buffer); + Ref heapf32Array = ValueBuilder::makeNew( + ValueBuilder::makeCall(FLOAT32ARRAY, ValueBuilder::makeName(buffer))); + Ref heapf32 = ValueBuilder::makeVar(); + ValueBuilder::appendToVar(heapf32, HEAPF32, heapf32Array); + flattenAppend(ret, heapf32); + + // var HEAPF64 = new Float64Array(__array_buffer); + Ref heapf64Array = ValueBuilder::makeNew( + ValueBuilder::makeCall(FLOAT64ARRAY, ValueBuilder::makeName(buffer))); + Ref heapf64 = ValueBuilder::makeVar(); + ValueBuilder::appendToVar(heapf64, HEAPF64, heapf64Array); + flattenAppend(ret, heapf64); + Ref lib = ValueBuilder::makeObject(); auto insertItem = [&](IString item) { ValueBuilder::appendToObject(lib, item, ValueBuilder::makeName(item)); @@ -1771,12 +1894,56 @@ static Ref makeInstantiation() { insertItem(FLOAT32ARRAY); insertItem(FLOAT64ARRAY); Ref env = ValueBuilder::makeObject(); - Ref mem = ValueBuilder::makeNew( - ValueBuilder::makeCall(ARRAY_BUFFER, ValueBuilder::makeInt(0x10000))); - Ref call = ValueBuilder::makeCall(IString(ASM_FUNC), lib, env, mem); - Ref ret = ValueBuilder::makeVar(); - ValueBuilder::appendToVar(ret, ASM_MODULE, call); - return ret; + Ref call = ValueBuilder::makeCall(IString(ASM_FUNC), lib, env, + ValueBuilder::makeName(buffer)); + Ref module = ValueBuilder::makeVar(); + ValueBuilder::appendToVar(module, ASM_MODULE, call); + flattenAppend(ret, module); + + // When equating floating point values in spec tests we want to use bitwise + // equality like wasm does. Unfortunately though NaN makes this tricky. JS + // implementations like Spidermonkey and JSC will canonicalize NaN loads from + // `Float32Array`, but V8 will not. This means that NaN representations are + // kind of all over the place and difficult to bitwise equate. + // + // To work around this problem we just use a small shim which considers all + // NaN representations equivalent and otherwise tests for bitwise equality. + flattenAppend(ret, ValueBuilder::makeName(R"( + function f32Equal(a, b) { + var i = new Int32Array(1); + var f = new Float32Array(i.buffer); + f[0] = a; + var ai = f[0]; + f[0] = b; + var bi = f[0]; + + return (isNaN(a) && isNaN(b)) || a == b; + } + )")); + flattenAppend(ret, ValueBuilder::makeName(R"( + function f64Equal(a, b) { + var i = new Int32Array(2); + var f = new Float64Array(i.buffer); + f[0] = a; + var ai1 = f[0]; + var ai2 = f[1]; + f[0] = b; + var bi1 = f[0]; + var bi2 = f[1]; + + return (isNaN(a) && isNaN(b)) || (ai1 == bi1 && ai2 == bi2); + } + )")); + + // 64-bit numbers get a different ABI w/ wasm2asm, and in general you can't + // actually export them from wasm at the boundary. We hack around this though + // to get the spec tests working. + flattenAppend(ret, ValueBuilder::makeName(R"( + function i64Equal(actual_lo, expected_lo, expected_hi) { + return actual_lo == (expected_lo | 0) && + asmModule.__wasm_fetch_high_bits() == (expected_hi | 0); + } + )")); } static void prefixCalls(Ref asmjs) { @@ -1787,11 +1954,25 @@ static void prefixCalls(Ref asmjs) { } if (arr.size() > 0 && arr[0]->isString() && arr[0]->getIString() == CALL) { assert(arr.size() >= 2); - Ref prefixed = ValueBuilder::makeDot(ValueBuilder::makeName(ASM_MODULE), - arr[1]->getIString()); - arr[1]->setArray(prefixed->getArray()); + if (arr[1]->getIString() == "f32Equal" || + arr[1]->getIString() == "f64Equal" || + arr[1]->getIString() == "i64Equal") { + // ... + } else if (arr[1]->getIString() == "Math_fround") { + arr[1]->setString("Math.fround"); + } else { + Name name = arr[1]->getIString() == "isNaN" ? "Math" : ASM_MODULE; + Ref prefixed = ValueBuilder::makeDot(ValueBuilder::makeName(name), + arr[1]->getIString()); + arr[1]->setArray(prefixed->getArray()); + } } } + + if (asmjs->isAssign()) { + prefixCalls(asmjs->asAssign()->target()); + prefixCalls(asmjs->asAssign()->value()); + } } Ref Wasm2AsmBuilder::makeAssertReturnFunc(SExpressionWasmBuilder& sexpBuilder, @@ -1812,18 +1993,29 @@ Ref Wasm2AsmBuilder::makeAssertReturnFunc(SExpressionWasmBuilder& sexpBuilder, Expression* expected = sexpBuilder.parseExpression(e[2]); Type resType = expected->type; actual->type = resType; - BinaryOp eqOp; switch (resType) { - case i32: eqOp = EqInt32; break; - case i64: eqOp = EqInt64; break; - case f32: eqOp = EqFloat32; break; - case f64: eqOp = EqFloat64; break; + case i32: + body = wasmBuilder.makeBinary(EqInt32, actual, expected); + break; + + case i64: + body = wasmBuilder.makeCall("i64Equal", {actual, expected}, i32); + break; + + case f32: { + body = wasmBuilder.makeCall("f32Equal", {actual, expected}, i32); + break; + } + case f64: { + body = wasmBuilder.makeCall("f64Equal", {actual, expected}, i32); + break; + } + default: { std::cerr << "Unhandled type in assert: " << resType << std::endl; abort(); } } - body = wasmBuilder.makeBinary(eqOp, actual, expected); } else { assert(false && "Unexpected number of parameters in assert_return"); } @@ -1841,6 +2033,25 @@ Ref Wasm2AsmBuilder::makeAssertReturnFunc(SExpressionWasmBuilder& sexpBuilder, return jsFunc; } +Ref Wasm2AsmBuilder::makeAssertReturnNanFunc(SExpressionWasmBuilder& sexpBuilder, + Builder& wasmBuilder, + Element& e, Name testFuncName) { + Expression* actual = sexpBuilder.parseExpression(e[1]); + Expression* body = wasmBuilder.makeCallImport("isNaN", {actual}, i32); + std::unique_ptr<Function> testFunc( + wasmBuilder.makeFunction( + testFuncName, + std::vector<NameType>{}, + body->type, + std::vector<NameType>{}, + body + ) + ); + Ref jsFunc = processFunction(testFunc.get()); + prefixCalls(jsFunc); + return jsFunc; +} + Ref Wasm2AsmBuilder::makeAssertTrapFunc(SExpressionWasmBuilder& sexpBuilder, Builder& wasmBuilder, Element& e, Name testFuncName) { @@ -1855,6 +2066,7 @@ Ref Wasm2AsmBuilder::makeAssertTrapFunc(SExpressionWasmBuilder& sexpBuilder, ); IString expectedErr = e[2]->str(); Ref innerFunc = processFunction(exprFunc.get()); + prefixCalls(innerFunc); Ref outerFunc = ValueBuilder::makeFunction(testFuncName); outerFunc[3]->push_back(innerFunc); Ref tryBlock = ValueBuilder::makeBlock(); @@ -2042,6 +2254,7 @@ void Wasm2AsmBuilder::addMemoryGrowthFuncs(Ref ast) { bool Wasm2AsmBuilder::isAssertHandled(Element& e) { return e.isList() && e.size() >= 2 && e[0]->isStr() && (e[0]->str() == Name("assert_return") || + e[0]->str() == Name("assert_return_nan") || (flags.pedantic && e[0]->str() == Name("assert_trap"))) && e[1]->isList() && e[1]->size() >= 2 && (*e[1])[0]->isStr() && (*e[1])[0]->str() == Name("invoke"); @@ -2051,7 +2264,7 @@ Ref Wasm2AsmBuilder::processAsserts(Element& root, SExpressionWasmBuilder& sexpBuilder) { Builder wasmBuilder(sexpBuilder.getAllocator()); Ref ret = ValueBuilder::makeBlock(); - flattenAppend(ret, makeInstantiation()); + makeInstantiation(ret); for (size_t i = 1; i < root.size(); ++i) { Element& e = *root[i]; if (!isAssertHandled(e)) { @@ -2060,6 +2273,7 @@ Ref Wasm2AsmBuilder::processAsserts(Element& root, } Name testFuncName(IString(("check" + std::to_string(i)).c_str(), false)); bool isReturn = (e[0]->str() == Name("assert_return")); + bool isReturnNan = (e[0]->str() == Name("assert_return_nan")); Element& testOp = *e[1]; // Replace "invoke" with "call" testOp[0]->setString(IString("call"), false, false); @@ -2068,7 +2282,9 @@ Ref Wasm2AsmBuilder::processAsserts(Element& root, Ref testFunc = isReturn ? makeAssertReturnFunc(sexpBuilder, wasmBuilder, e, testFuncName) : - makeAssertTrapFunc(sexpBuilder, wasmBuilder, e, testFuncName); + (isReturnNan ? + makeAssertReturnNanFunc(sexpBuilder, wasmBuilder, e, testFuncName) : + makeAssertTrapFunc(sexpBuilder, wasmBuilder, e, testFuncName)); flattenAppend(ret, testFunc); std::stringstream failFuncName; |