diff options
Diffstat (limited to 'src/asm2wasm.h')
-rw-r--r-- | src/asm2wasm.h | 797 |
1 files changed, 572 insertions, 225 deletions
diff --git a/src/asm2wasm.h b/src/asm2wasm.h index 9fa8d11dd..ca88fa883 100644 --- a/src/asm2wasm.h +++ b/src/asm2wasm.h @@ -7,34 +7,17 @@ #include "wasm.h" #include "emscripten-optimizer/optimizer.h" #include "mixed_arena.h" +#include "shared-constants.h" +#include "asm_v_wasm.h" namespace wasm { using namespace cashew; -int debug = 0; // wasm::debug is set in main(), typically from an env var +extern int debug; // wasm::debug is set in main(), typically from an env var // Utilities -IString GLOBAL("global"), NAN_("NaN"), INFINITY_("Infinity"), - TOPMOST("topmost"), - INT8ARRAY("Int8Array"), - INT16ARRAY("Int16Array"), - INT32ARRAY("Int32Array"), - UINT8ARRAY("Uint8Array"), - UINT16ARRAY("Uint16Array"), - UINT32ARRAY("Uint32Array"), - FLOAT32ARRAY("Float32Array"), - FLOAT64ARRAY("Float64Array"), - IMPOSSIBLE_CONTINUE("impossible-continue"), - MATH("Math"), - IMUL("imul"), - CLZ32("clz32"), - FROUND("fround"), - ASM2WASM("asm2wasm"), - F64_REM("f64-rem"); - - static void abort_on(std::string why) { std::cerr << why << '\n'; abort(); @@ -75,13 +58,69 @@ struct AstStackHelper { std::vector<Ref> AstStackHelper::astStack; // +// Asm2WasmPreProcessor - does some initial parsing/processing +// of asm.js code. +// + +struct Asm2WasmPreProcessor { + bool memoryGrowth = false; + + char* process(char* input) { + // emcc --separate-asm modules can look like + // + // Module["asm"] = (function(global, env, buffer) { + // .. + // }); + // + // we need to clean that up. + if (*input == 'M') { + size_t num = strlen(input); + while (*input != 'f') { + input++; + num--; + } + char *end = input + num - 1; + while (*end != '}') { + *end = 0; + end--; + } + } + + // asm.js memory growth uses a quite elaborate pattern. Instead of parsing and + // matching it, we do a simpler detection on emscripten's asm.js output format + const char* START_FUNCS = "// EMSCRIPTEN_START_FUNCS"; + char *marker = strstr(input, START_FUNCS); + if (marker) { + *marker = 0; // look for memory growth code just up to here + char *growthSign = strstr(input, "return true;"); // this can only show up in growth code, as normal asm.js lacks "true" + if (growthSign) { + memoryGrowth = true; + // clean out this function, we don't need it + char *growthFuncStart = strstr(input, "function "); + assert(strstr(growthFuncStart + 1, "function ") == 0); // should be only this one function in this area, so no confusion for us + char *growthFuncEnd = strchr(growthSign, '}'); + assert(growthFuncEnd > growthFuncStart + 5); + growthFuncStart[0] = '/'; + growthFuncStart[1] = '*'; + growthFuncEnd--; + growthFuncEnd[0] = '*'; + growthFuncEnd[1] = '/'; + } + *marker = START_FUNCS[0]; + } + + return input; + } +}; + +// // Asm2WasmBuilder - converts an asm.js module into WebAssembly // class Asm2WasmBuilder { - Module& wasm; + AllocatingModule& wasm; - MixedArena allocator; + MixedArena &allocator; // globals @@ -96,9 +135,34 @@ class Asm2WasmBuilder { MappedGlobal(unsigned address, WasmType type, bool import, IString module, IString base) : address(address), type(type), import(import), module(module), base(base) {} }; + // function table + std::map<IString, int> functionTableStarts; // each asm function table gets a range in the one wasm table, starting at a location + std::map<CallIndirect*, IString> callIndirects; // track these, as we need to fix them after we know the functionTableStarts. this maps call => its function table + + bool memoryGrowth; + public: std::map<IString, MappedGlobal> mappedGlobals; + // the global mapping info is not present in the output wasm. We need to save it on the side + // if we intend to load and run this module's wasm. + void serializeMappedGlobals(const char *filename) { + FILE *f = fopen(filename, "w"); + assert(f); + fprintf(f, "{\n"); + bool first = true; + for (auto& pair : mappedGlobals) { + auto name = pair.first; + auto& global = pair.second; + if (first) first = false; + else fprintf(f, ","); + fprintf(f, "\"%s\": { \"address\": %d, \"type\": %d, \"import\": %d, \"module\": \"%s\", \"base\": \"%s\" }\n", + name.str, global.address, global.type, global.import, global.module.str, global.base.str); + } + fprintf(f, "}"); + fclose(f); + } + private: void allocateGlobal(IString name, WasmType type, bool import, IString module = IString(), IString base = IString()) { assert(mappedGlobals.find(name) == mappedGlobals.end()); @@ -116,9 +180,14 @@ private: }; std::map<IString, View> views; // name (e.g. HEAP8) => view info - IString Math_imul; // imported name of Math.imul - IString Math_clz32; // imported name of Math.imul - IString Math_fround; // imported name of Math.fround + + // Imported names of Math.* + IString Math_imul; + IString Math_clz32; + IString Math_fround; + IString Math_abs; + IString Math_floor; + IString Math_sqrt; // function types. we fill in this information as we see // uses, in the first pass @@ -150,9 +219,9 @@ private: if (previous.params.size() > i) { if (previous.params[i] == none) { previous.params[i] = type.params[i]; // use a more concrete type - } else { - previous.params.push_back(type.params[i]); // add a new param } + } else { + previous.params.push_back(type.params[i]); // add a new param } } if (previous.result == none) { @@ -164,22 +233,13 @@ private: } } - char getSigFromType(WasmType type) { - switch (type) { - case i32: return 'i'; - case f64: return 'd'; - case none: return 'v'; - default: abort(); - } - } - FunctionType *getFunctionType(Ref parent, ExpressionList& operands) { // generate signature WasmType result = detectWasmType(parent, nullptr); std::string str = "FUNCSIG$"; - str += getSigFromType(result); + str += getSig(result); for (auto operand : operands) { - str += getSigFromType(operand->type); + str += getSig(operand->type); } IString sig(str.c_str(), false); if (wasm.functionTypesMap.find(sig) == wasm.functionTypesMap.end()) { @@ -196,35 +256,12 @@ private: } public: - Asm2WasmBuilder(Module& wasm) : wasm(wasm), nextGlobal(8), maxGlobal(1000) {} // XXX sync with emcc + Asm2WasmBuilder(AllocatingModule& wasm, bool memoryGrowth) : wasm(wasm), allocator(wasm.allocator), nextGlobal(8), maxGlobal(1000), memoryGrowth(memoryGrowth) {} void processAsm(Ref ast); void optimize(); private: - WasmType asmToWasmType(AsmType asmType) { - switch (asmType) { - case ASM_INT: return WasmType::i32; - case ASM_DOUBLE: return WasmType::f64; - case ASM_FLOAT: return WasmType::f32; - case ASM_NONE: return WasmType::none; - default: {} - } - abort_on("confused asmType", asmType); - return (WasmType)-1; // avoid warning - } - AsmType wasmToAsmType(WasmType type) { - switch (type) { - case WasmType::i32: return ASM_INT; - case WasmType::f32: return ASM_FLOAT; - case WasmType::f64: return ASM_DOUBLE; - case WasmType::none: return ASM_NONE; - default: {} - } - abort_on("confused wasmType", type); - return (AsmType)-1; // avoid warning - } - AsmType detectAsmType(Ref ast, AsmData *data) { if (ast[0] == NAME) { IString name = ast[1]->getIString(); @@ -240,31 +277,29 @@ private: return view->second.type; } } - return detectType(ast, data); + return detectType(ast, data, false, Math_fround); } WasmType detectWasmType(Ref ast, AsmData *data) { return asmToWasmType(detectAsmType(ast, data)); } - bool isUnsignedCoercion(Ref ast) { // TODO: use detectSign? - if (ast[0] == BINARY && ast[1] == TRSHIFT) return true; - return false; + bool isUnsignedCoercion(Ref ast) { + return detectSign(ast, Math_fround) == ASM_UNSIGNED; } - // an asm.js binary op can either be a binary or a relational in wasm - bool parseAsmBinaryOp(IString op, Ref left, Ref right, BinaryOp &binary, RelationalOp &relational, AsmData *asmData) { - if (op == PLUS) { binary = BinaryOp::Add; return true; } - if (op == MINUS) { binary = BinaryOp::Sub; return true; } - if (op == MUL) { binary = BinaryOp::Mul; return true; } - if (op == AND) { binary = BinaryOp::And; return true; } - if (op == OR) { binary = BinaryOp::Or; return true; } - if (op == XOR) { binary = BinaryOp::Xor; return true; } - if (op == LSHIFT) { binary = BinaryOp::Shl; return true; } - if (op == RSHIFT) { binary = BinaryOp::ShrS; return true; } - if (op == TRSHIFT) { binary = BinaryOp::ShrU; return true; } - if (op == EQ) { relational = RelationalOp::Eq; return false; } - if (op == NE) { relational = RelationalOp::Ne; return false; } + BinaryOp parseAsmBinaryOp(IString op, Ref left, Ref right, AsmData *asmData) { + if (op == PLUS) return BinaryOp::Add; + if (op == MINUS) return BinaryOp::Sub; + if (op == MUL) return BinaryOp::Mul; + if (op == AND) return BinaryOp::And; + if (op == OR) return BinaryOp::Or; + if (op == XOR) return BinaryOp::Xor; + if (op == LSHIFT) return BinaryOp::Shl; + if (op == RSHIFT) return BinaryOp::ShrS; + if (op == TRSHIFT) return BinaryOp::ShrU; + if (op == EQ) return BinaryOp::Eq; + if (op == NE) return BinaryOp::Ne; WasmType leftType = detectWasmType(left, asmData); #if 0 std::cout << "CHECK\n"; @@ -278,42 +313,42 @@ private: bool isUnsigned = isUnsignedCoercion(left) || isUnsignedCoercion(right); if (op == DIV) { if (isInteger) { - { binary = isUnsigned ? BinaryOp::DivU : BinaryOp::DivS; return true; } + return isUnsigned ? BinaryOp::DivU : BinaryOp::DivS; } - { binary = BinaryOp::Div; return true; } + return BinaryOp::Div; } if (op == MOD) { if (isInteger) { - { binary = isUnsigned ? BinaryOp::RemU : BinaryOp::RemS; return true; } + return isUnsigned ? BinaryOp::RemU : BinaryOp::RemS; } - { binary = BinaryOp::RemS; return true; } // XXX no floating-point remainder op, this must be handled by the caller + return BinaryOp::RemS; // XXX no floating-point remainder op, this must be handled by the caller } if (op == GE) { if (isInteger) { - { relational = isUnsigned ? RelationalOp::GeU : RelationalOp::GeS; return false; } + return isUnsigned ? BinaryOp::GeU : BinaryOp::GeS; } - { relational = RelationalOp::Ge; return false; } + return BinaryOp::Ge; } if (op == GT) { if (isInteger) { - { relational = isUnsigned ? RelationalOp::GtU : RelationalOp::GtS; return false; } + return isUnsigned ? BinaryOp::GtU : BinaryOp::GtS; } - { relational = RelationalOp::Gt; return false; } + return BinaryOp::Gt; } if (op == LE) { if (isInteger) { - { relational = isUnsigned ? RelationalOp::LeU : RelationalOp::LeS; return false; } + return isUnsigned ? BinaryOp::LeU : BinaryOp::LeS; } - { relational = RelationalOp::Le; return false; } + return BinaryOp::Le; } if (op == LT) { if (isInteger) { - { relational = isUnsigned ? RelationalOp::LtU : RelationalOp::LtS; return false; } + return isUnsigned ? BinaryOp::LtU : BinaryOp::LtS; } - { relational = RelationalOp::Lt; return false; } + return BinaryOp::Lt; } abort_on("bad wasm binary op", op); - return false; // avoid warning + abort(); // avoid warning } unsigned bytesToShift(unsigned bytes) { @@ -330,20 +365,83 @@ private: std::map<unsigned, Ref> tempNums; - Literal getLiteral(Ref ast) { + Literal checkLiteral(Ref ast) { if (ast[0] == NUM) { return Literal((int32_t)ast[1]->getInteger()); } else if (ast[0] == UNARY_PREFIX) { + if (ast[1] == PLUS && ast[2][0] == NUM) { + return Literal((double)ast[2][1]->getNumber()); + } if (ast[1] == MINUS && ast[2][0] == NUM) { double num = -ast[2][1]->getNumber(); assert(isInteger32(num)); return Literal((int32_t)num); } + if (ast[1] == PLUS && ast[2][0] == UNARY_PREFIX && ast[2][1] == MINUS && ast[2][2][0] == NUM) { + return Literal((double)-ast[2][2][1]->getNumber()); + } if (ast[1] == MINUS && ast[2][0] == UNARY_PREFIX && ast[2][1] == PLUS && ast[2][2][0] == NUM) { return Literal((double)-ast[2][2][1]->getNumber()); } } - abort(); + return Literal(); + } + + Literal getLiteral(Ref ast) { + Literal ret = checkLiteral(ast); + if (ret.type == none) abort(); + return ret; + } + + void fixCallType(Expression* call, WasmType type) { + if (call->is<Call>()) call->type = type; + if (call->is<CallImport>()) call->type = type; + else if (call->is<CallIndirect>()) call->type = type; + } + + FunctionType* getBuiltinFunctionType(Name module, Name base, ExpressionList* operands = nullptr) { + if (module == GLOBAL_MATH) { + if (base == ABS) { + assert(operands && operands->size() == 1); + WasmType type = (*operands)[0]->type; + if (type == i32) { + static FunctionType* builtin = nullptr; + if (!builtin) { + builtin = new FunctionType(); + builtin->params.push_back(i32); + builtin->result = i32; + } + return builtin; + } + if (type == f32) { + static FunctionType* builtin = nullptr; + if (!builtin) { + builtin = new FunctionType(); + builtin->params.push_back(f32); + builtin->result = f32; + } + return builtin; + } + if (type == f64) { + static FunctionType* builtin = nullptr; + if (!builtin) { + builtin = new FunctionType(); + builtin->params.push_back(f64); + builtin->result = f64; + } + return builtin; + } + + } + } + return nullptr; + } + + Block* blockify(Expression* expression) { + if (expression->is<Block>()) return expression->dyn_cast<Block>(); + auto ret = allocator.alloc<Block>(); + ret->list.push_back(expression); + return ret; } Function* processFunction(Ref ast); @@ -376,6 +474,18 @@ void Asm2WasmBuilder::processAsm(Ref ast) { assert(Math_fround.isNull()); Math_fround = name; return; + } else if (imported[2] == ABS) { + assert(Math_abs.isNull()); + Math_abs = name; + return; + } else if (imported[2] == FLOOR) { + assert(Math_floor.isNull()); + Math_floor = name; + return; + } else if (imported[2] == SQRT) { + assert(Math_sqrt.isNull()); + Math_sqrt = name; + return; } } std::string fullName = module[1][1]->getCString(); @@ -402,7 +512,9 @@ void Asm2WasmBuilder::processAsm(Ref ast) { } }; - // first pass - do almost everything, but function imports + IString Int8Array, Int16Array, Int32Array, UInt8Array, UInt16Array, UInt32Array, Float32Array, Float64Array; + + // first pass - do almost everything, but function imports and indirect calls for (unsigned i = 1; i < body->size(); i++) { Ref curr = body[i]; @@ -437,56 +549,96 @@ void Asm2WasmBuilder::processAsm(Ref ast) { assert(value[1][0] == NAME && value[1][1] == Math_fround && value[2][0][0] == NUM && value[2][0][1]->getNumber() == 0); allocateGlobal(name, WasmType::f32, false); } else if (value[0] == DOT) { + // simple module.base import. can be a view, or a function. + if (value[1][0] == NAME) { + IString module = value[1][1]->getIString(); + IString base = value[2]->getIString(); + if (module == GLOBAL) { + if (base == INT8ARRAY) { + Int8Array = name; + } else if (base == INT16ARRAY) { + Int16Array = name; + } else if (base == INT32ARRAY) { + Int32Array = name; + } else if (base == UINT8ARRAY) { + UInt8Array = name; + } else if (base == UINT16ARRAY) { + UInt16Array = name; + } else if (base == UINT32ARRAY) { + UInt32Array = name; + } else if (base == FLOAT32ARRAY) { + Float32Array = name; + } else if (base == FLOAT64ARRAY) { + Float64Array = name; + } + } + } // function import addImport(name, value, WasmType::none); } else if (value[0] == NEW) { // ignore imports of typed arrays, but note the names of the arrays value = value[1]; assert(value[0] == CALL); - Ref constructor = value[1]; - assert(constructor[0] == DOT); // global.*Array - IString heap = constructor[2]->getIString(); unsigned bytes; bool integer, signed_; AsmType asmType; - if (heap == INT8ARRAY) { - bytes = 1; integer = true; signed_ = true; asmType = ASM_INT; - } else if (heap == INT16ARRAY) { - bytes = 2; integer = true; signed_ = true; asmType = ASM_INT; - } else if (heap == INT32ARRAY) { - bytes = 4; integer = true; signed_ = true; asmType = ASM_INT; - } else if (heap == UINT8ARRAY) { - bytes = 1; integer = true; signed_ = false; asmType = ASM_INT; - } else if (heap == UINT16ARRAY) { - bytes = 2; integer = true; signed_ = false; asmType = ASM_INT; - } else if (heap == UINT32ARRAY) { - bytes = 4; integer = true; signed_ = false; asmType = ASM_INT; - } else if (heap == FLOAT32ARRAY) { - bytes = 4; integer = false; signed_ = true; asmType = ASM_DOUBLE; - } else if (heap == FLOAT64ARRAY) { - bytes = 8; integer = false; signed_ = true; asmType = ASM_DOUBLE; + Ref constructor = value[1]; + if (constructor[0] == DOT) { // global.*Array + IString heap = constructor[2]->getIString(); + if (heap == INT8ARRAY) { + bytes = 1; integer = true; signed_ = true; asmType = ASM_INT; + } else if (heap == INT16ARRAY) { + bytes = 2; integer = true; signed_ = true; asmType = ASM_INT; + } else if (heap == INT32ARRAY) { + bytes = 4; integer = true; signed_ = true; asmType = ASM_INT; + } else if (heap == UINT8ARRAY) { + bytes = 1; integer = true; signed_ = false; asmType = ASM_INT; + } else if (heap == UINT16ARRAY) { + bytes = 2; integer = true; signed_ = false; asmType = ASM_INT; + } else if (heap == UINT32ARRAY) { + bytes = 4; integer = true; signed_ = false; asmType = ASM_INT; + } else if (heap == FLOAT32ARRAY) { + bytes = 4; integer = false; signed_ = true; asmType = ASM_FLOAT; + } else if (heap == FLOAT64ARRAY) { + bytes = 8; integer = false; signed_ = true; asmType = ASM_DOUBLE; + } else { + abort_on("invalid view import", heap); + } + } else { // *ArrayView that was previously imported + assert(constructor[0] == NAME); + IString viewName = constructor[1]->getIString(); + if (viewName == Int8Array) { + bytes = 1; integer = true; signed_ = true; asmType = ASM_INT; + } else if (viewName == Int16Array) { + bytes = 2; integer = true; signed_ = true; asmType = ASM_INT; + } else if (viewName == Int32Array) { + bytes = 4; integer = true; signed_ = true; asmType = ASM_INT; + } else if (viewName == UInt8Array) { + bytes = 1; integer = true; signed_ = false; asmType = ASM_INT; + } else if (viewName == UInt16Array) { + bytes = 2; integer = true; signed_ = false; asmType = ASM_INT; + } else if (viewName == UInt32Array) { + bytes = 4; integer = true; signed_ = false; asmType = ASM_INT; + } else if (viewName == Float32Array) { + bytes = 4; integer = false; signed_ = true; asmType = ASM_FLOAT; + } else if (viewName == Float64Array) { + bytes = 8; integer = false; signed_ = true; asmType = ASM_DOUBLE; + } else { + abort_on("invalid short view import", viewName); + } } assert(views.find(name) == views.end()); views.emplace(name, View(bytes, integer, signed_, asmType)); } else if (value[0] == ARRAY) { - // function table. we "merge" them, so e.g. [foo, b1] , [b2, bar] => [foo, bar] , assuming b* are the aborting thunks - // when minified, we can't tell from the name b\d+, but null thunks appear multiple times in a table; others never do - // TODO: we can drop some b*s at the end of the table + // function table. we merge them into one big table, so e.g. [foo, b1] , [b2, bar] => [foo, b1, b2, bar] + // TODO: when not using aliasing function pointers, we could merge them by noticing that + // index 0 in each table is the null func, and each other index should only have one + // non-null func. However, that breaks down when function pointer casts are emulated. + functionTableStarts[name] = wasm.table.names.size(); // this table starts here Ref contents = value[1]; - std::map<IString, unsigned> counts; // name -> how many times seen for (unsigned k = 0; k < contents->size(); k++) { IString curr = contents[k][1]->getIString(); - counts[curr]++; - } - for (unsigned k = 0; k < contents->size(); k++) { - IString curr = contents[k][1]->getIString(); - if (wasm.table.names.size() <= k) { - wasm.table.names.push_back(curr); - } else { - if (counts[curr] == 1) { // if just one appearance, not a null thunk - wasm.table.names[k] = curr; - } - } + wasm.table.names.push_back(curr); } } else { abort_on("invalid var element", pair); @@ -512,7 +664,7 @@ void Asm2WasmBuilder::processAsm(Ref ast) { } } - // second pass - function imports + // second pass. first, function imports std::vector<IString> toErase; @@ -520,6 +672,12 @@ void Asm2WasmBuilder::processAsm(Ref ast) { IString name = pair.first; Import& import = *pair.second; if (importedFunctionTypes.find(name) != importedFunctionTypes.end()) { + // special math builtins + FunctionType* builtin = getBuiltinFunctionType(import.module, import.base); + if (builtin) { + import.type = *builtin; + continue; + } import.type = importedFunctionTypes[name]; } else if (import.module != ASM2WASM) { // special-case the special module // never actually used @@ -530,6 +688,40 @@ void Asm2WasmBuilder::processAsm(Ref ast) { for (auto curr : toErase) { wasm.removeImport(curr); } + + // finalize indirect calls + + for (auto& pair : callIndirects) { + CallIndirect* call = pair.first; + IString tableName = pair.second; + assert(functionTableStarts.find(tableName) != functionTableStarts.end()); + auto sub = allocator.alloc<Binary>(); + // note that the target is already masked, so we just offset it, we don't need to guard against overflow (which would be an error anyhow) + sub->op = Add; + sub->left = call->target; + sub->right = allocator.alloc<Const>()->set(Literal((int32_t)functionTableStarts[tableName])); + sub->type = WasmType::i32; + call->target = sub; + } + + // apply memory growth, if relevant + if (memoryGrowth) { + // create and export a function that just calls memory growth + auto growWasmMemory = allocator.alloc<Function>(); + growWasmMemory->name = GROW_WASM_MEMORY; + growWasmMemory->params.emplace_back(NEW_SIZE, i32); // the new size + auto get = allocator.alloc<GetLocal>(); + get->name = NEW_SIZE; + auto grow = allocator.alloc<Host>(); + grow->op = GrowMemory; + grow->operands.push_back(get); + growWasmMemory->body = grow; + wasm.addFunction(growWasmMemory); + auto export_ = allocator.alloc<Export>(); + export_->name = export_->value = GROW_WASM_MEMORY; + wasm.addExport(export_); + } + } Function* Asm2WasmBuilder::processFunction(Ref ast) { @@ -563,7 +755,7 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { IStringSet functionVariables; // params or locals - IString parentLabel; // set in LABEL, then read in WHILE/DO + IString parentLabel; // set in LABEL, then read in WHILE/DO/SWITCH std::vector<IString> breakStack; // where a break will go std::vector<IString> continueStack; // where a continue will go @@ -575,7 +767,7 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { curr = curr[1]; assert(curr[0] == ASSIGN && curr[2][0] == NAME); IString name = curr[2][1]->getIString(); - AsmType asmType = detectType(curr[3]); + AsmType asmType = detectType(curr[3], nullptr, false, Math_fround); function->params.emplace_back(name, asmToWasmType(asmType)); functionVariables.insert(name); asmData.addParam(name, asmType); @@ -586,7 +778,7 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { for (unsigned j = 0; j < curr[1]->size(); j++) { Ref pair = curr[1][j]; IString name = pair[0]->getIString(); - AsmType asmType = detectType(pair[1], nullptr, true); + AsmType asmType = detectType(pair[1], nullptr, true, Math_fround); function->locals.emplace_back(name, asmToWasmType(asmType)); functionVariables.insert(name); asmData.addVar(name, asmType); @@ -594,6 +786,15 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { start++; } + bool addedI32Temp = false; + auto ensureI32Temp = [&]() { + if (addedI32Temp) return; + addedI32Temp = true; + function->locals.emplace_back(I32_TEMP, i32); + functionVariables.insert(I32_TEMP); + asmData.addVar(I32_TEMP, ASM_INT); + }; + bool seenReturn = false; // function->result is updated if we see a return bool needTopmost = false; // we label the topmost b lock if we need one for a return // processors @@ -646,63 +847,63 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { ret->align = view.bytes; ret->ptr = processUnshifted(target[2], view.bytes); ret->value = process(ast[3]); - ret->type = ret->value->type; + ret->type = asmToWasmType(view.type); + if (ret->type != ret->value->type) { + // in asm.js we have some implicit coercions that we must do explicitly here + if (ret->type == f32 && ret->value->type == f64) { + auto conv = allocator.alloc<Unary>(); + conv->op = DemoteFloat64; + conv->value = ret->value; + conv->type = WasmType::f32; + ret->value = conv; + } else { + abort(); + } + } return ret; } abort_on("confusing assign", ast); } else if (what == BINARY) { - if (ast[1] == OR && ast[3][0] == NUM && ast[3][1]->getNumber() == 0) { - auto ret = process(ast[2]); // just look through the ()|0 coercion - ret->type = WasmType::i32; // we add it here for e.g. call coercions + if ((ast[1] == OR || ast[1] == TRSHIFT) && ast[3][0] == NUM && ast[3][1]->getNumber() == 0) { + auto ret = process(ast[2]); // just look through the ()|0 or ()>>>0 coercion + fixCallType(ret, i32); return ret; } - BinaryOp binary; - RelationalOp relational; - bool isBinary = parseAsmBinaryOp(ast[1]->getIString(), ast[2], ast[3], binary, relational, &asmData); - if (isBinary) { - auto ret = allocator.alloc<Binary>(); - ret->op = binary; - ret->left = process(ast[2]); - ret->right = process(ast[3]); - ret->type = ret->left->type; - if (binary == BinaryOp::RemS && isWasmTypeFloat(ret->type)) { - // WebAssembly does not have floating-point remainder, we have to emit a call to a special import of ours - CallImport *call = allocator.alloc<CallImport>(); - call->target = F64_REM; - call->operands.push_back(ret->left); - call->operands.push_back(ret->right); - call->type = f64; - static bool addedImport = false; - if (!addedImport) { - addedImport = true; - auto import = allocator.alloc<Import>(); // f64-rem = asm2wasm.f64-rem; - import->name = F64_REM; - import->module = ASM2WASM; - import->base = F64_REM; - import->type.name = F64_REM; - import->type.result = f64; - import->type.params.push_back(f64); - import->type.params.push_back(f64); - wasm.addImport(import); - } - return call; + BinaryOp binary = parseAsmBinaryOp(ast[1]->getIString(), ast[2], ast[3], &asmData); + auto ret = allocator.alloc<Binary>(); + ret->op = binary; + ret->left = process(ast[2]); + ret->right = process(ast[3]); + ret->finalize(); + if (binary == BinaryOp::RemS && isWasmTypeFloat(ret->type)) { + // WebAssembly does not have floating-point remainder, we have to emit a call to a special import of ours + CallImport *call = allocator.alloc<CallImport>(); + call->target = F64_REM; + call->operands.push_back(ret->left); + call->operands.push_back(ret->right); + call->type = f64; + static bool addedImport = false; + if (!addedImport) { + addedImport = true; + auto import = allocator.alloc<Import>(); // f64-rem = asm2wasm.f64-rem; + import->name = F64_REM; + import->module = ASM2WASM; + import->base = F64_REM; + import->type.name = F64_REM; + import->type.result = f64; + import->type.params.push_back(f64); + import->type.params.push_back(f64); + wasm.addImport(import); } - return ret; - } else { - auto ret = allocator.alloc<Compare>(); - ret->op = relational; - ret->left = process(ast[2]); - ret->right = process(ast[3]); - assert(ret->left->type == ret->right->type); - ret->inputType = ret->left->type; - return ret; + return call; } + return ret; } else if (what == NUM) { auto ret = allocator.alloc<Const>(); double num = ast[1]->getNumber(); if (isInteger32(num)) { ret->value.type = WasmType::i32; - ret->value.i32 = num; + ret->value.i32 = toInteger32(num); } else { ret->value.type = WasmType::f64; ret->value.f64 = num; @@ -718,6 +919,23 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { ret->type = asmToWasmType(asmData.getType(name)); return ret; } + if (name == DEBUGGER) { + CallImport *call = allocator.alloc<CallImport>(); + call->target = DEBUGGER; + call->type = none; + static bool addedImport = false; + if (!addedImport) { + addedImport = true; + auto import = allocator.alloc<Import>(); // debugger = asm2wasm.debugger; + import->name = DEBUGGER; + import->module = ASM2WASM; + import->base = DEBUGGER; + import->type.name = DEBUGGER; + import->type.result = none; + wasm.addImport(import); + } + return call; + } // global var, do a load from memory assert(mappedGlobals.find(name) != mappedGlobals.end()); MappedGlobal global = mappedGlobals[name]; @@ -748,24 +966,26 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { return ret; } else if (what == UNARY_PREFIX) { if (ast[1] == PLUS) { - if (ast[2][0] == NUM) { - auto ret = allocator.alloc<Const>(); - ret->value.type = WasmType::f64; - ret->value.f64 = ast[2][1]->getNumber(); - ret->type = ret->value.type; - return ret; + Literal literal = checkLiteral(ast); + if (literal.type != none) { + return allocator.alloc<Const>()->set(literal); } - AsmType childType = detectAsmType(ast[2], &asmData); - if (childType == ASM_INT) { - auto ret = allocator.alloc<Convert>(); - ret->op = isUnsignedCoercion(ast[2]) ? ConvertUInt32 : ConvertSInt32; - ret->value = process(ast[2]); - ret->type = WasmType::f64; - return ret; + auto ret = process(ast[2]); // we are a +() coercion + if (ret->type == i32) { + auto conv = allocator.alloc<Unary>(); + conv->op = isUnsignedCoercion(ast[2]) ? ConvertUInt32 : ConvertSInt32; + conv->value = ret; + conv->type = WasmType::f64; + return conv; + } + if (ret->type == f32) { + auto conv = allocator.alloc<Unary>(); + conv->op = PromoteFloat32; + conv->value = ret; + conv->type = WasmType::f64; + return conv; } - assert(childType == ASM_NONE || childType == ASM_DOUBLE); // e.g. a coercion on a call or for a return - auto ret = process(ast[2]); // just look through the +() coercion - ret->type = WasmType::f64; // we add it here for e.g. call coercions + fixCallType(ret, f64); return ret; } else if (ast[1] == MINUS) { if (ast[2][0] == NUM || (ast[2][0] == UNARY_PREFIX && ast[2][1] == PLUS && ast[2][2][0] == NUM)) { @@ -784,20 +1004,45 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { ret->type = WasmType::i32; return ret; } - assert(asmType == ASM_DOUBLE); auto ret = allocator.alloc<Unary>(); ret->op = Neg; ret->value = process(ast[2]); - ret->type = WasmType::f64; + if (asmType == ASM_DOUBLE) { + ret->type = WasmType::f64; + } else if (asmType == ASM_FLOAT) { + ret->type = WasmType::f32; + } else { + abort(); + } return ret; } else if (ast[1] == B_NOT) { // ~, might be ~~ as a coercion or just a not if (ast[2][0] == UNARY_PREFIX && ast[2][1] == B_NOT) { - auto ret = allocator.alloc<Convert>(); +#if 0 + auto ret = allocator.alloc<Unary>(); ret->op = TruncSFloat64; // equivalent to U, except for error handling, which asm.js doesn't have anyhow ret->value = process(ast[2][2]); ret->type = WasmType::i32; return ret; +#endif + // WebAssembly traps on float-to-int overflows, but asm.js wouldn't, so we must emulate that + CallImport *ret = allocator.alloc<CallImport>(); + ret->target = F64_TO_INT; + ret->operands.push_back(process(ast[2][2])); + ret->type = i32; + static bool addedImport = false; + if (!addedImport) { + addedImport = true; + auto import = allocator.alloc<Import>(); // f64-to-int = asm2wasm.f64-to-int; + import->name = F64_TO_INT; + import->module = ASM2WASM; + import->base = F64_TO_INT; + import->type.name = F64_TO_INT; + import->type.result = i32; + import->type.params.push_back(f64); + wasm.addImport(import); + } + return ret; } // no bitwise unary not, so do xor with -1 auto ret = allocator.alloc<Binary>(); @@ -808,12 +1053,12 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { return ret; } else if (ast[1] == L_NOT) { // no logical unary not, so do == 0 - auto ret = allocator.alloc<Compare>(); + auto ret = allocator.alloc<Binary>(); ret->op = Eq; ret->left = process(ast[2]); ret->right = allocator.alloc<Const>()->set(Literal(0)); assert(ret->left->type == ret->right->type); - ret->inputType = ret->left->type; + ret->finalize(); return ret; } abort_on("bad unary", ast); @@ -843,20 +1088,96 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { ret->type = WasmType::i32; return ret; } + if (name == Math_fround) { + assert(ast[2]->size() == 1); + Literal lit = checkLiteral(ast[2][0]); + if (lit.type == i32) { + return allocator.alloc<Const>()->set(Literal((float)lit.geti32())); + } else if (lit.type == f64) { + return allocator.alloc<Const>()->set(Literal((float)lit.getf64())); + } + auto ret = allocator.alloc<Unary>(); + ret->value = process(ast[2][0]); + if (ret->value->type == f64) { + ret->op = DemoteFloat64; + } else if (ret->value->type == i32) { + ret->op = ConvertSInt32; + } else if (ret->value->type == f32) { + return ret->value; + } else if (ret->value->type == none) { // call, etc. + ret->value->type = f32; + return ret->value; + } else { + abort_on("confusing fround target", ast[2][0]); + } + ret->type = f32; + return ret; + } + if (name == Math_abs) { + // overloaded on type: i32, f32 or f64 + Expression* value = process(ast[2][0]); + if (value->type == i32) { + // No wasm support, so use a temp local + ensureI32Temp(); + auto set = allocator.alloc<SetLocal>(); + set->name = I32_TEMP; + set->value = value; + set->type = i32; + auto get = [&]() { + auto ret = allocator.alloc<GetLocal>(); + ret->name = I32_TEMP; + ret->type = i32; + return ret; + }; + auto isNegative = allocator.alloc<Binary>(); + isNegative->op = LtS; + isNegative->left = get(); + isNegative->right = allocator.alloc<Const>()->set(0); + isNegative->finalize(); + auto block = allocator.alloc<Block>(); + block->list.push_back(set); + auto flip = allocator.alloc<Binary>(); + flip->op = Sub; + flip->left = allocator.alloc<Const>()->set(0); + flip->right = get(); + flip->type = i32; + auto select = allocator.alloc<Select>(); + select->condition = isNegative; + select->ifTrue = flip; + select->ifFalse = get(); + select->type = i32; + block->list.push_back(select); + block->type = i32; + return block; + } else if (value->type == f32 || value->type == f64) { + auto ret = allocator.alloc<Unary>(); + ret->op = Abs; + ret->value = value; + ret->type = value->type; + return ret; + } else { + abort(); + } + } + if (name == Math_floor || name == Math_sqrt) { + // overloaded on type: f32 or f64 + Expression* value = process(ast[2][0]); + if (value->type == f32 || value->type == f64) { + auto ret = allocator.alloc<Unary>(); + ret->op = name == Math_floor ? Floor : Sqrt; + ret->value = value; + ret->type = value->type; + return ret; + } else { + abort(); + } + } Call* ret; if (wasm.importsMap.find(name) != wasm.importsMap.end()) { Ref parent = astStackHelper.getParent(); WasmType type = !!parent ? detectWasmType(parent, &asmData) : none; -#ifndef __EMSCRIPTEN__ - // no imports yet in reference interpreter, fake it - if (type == none) return allocator.alloc<Nop>(); - if (type == i32) return allocator.alloc<Const>()->set(Literal((int32_t)0)); - if (type == f64) return allocator.alloc<Const>()->set(Literal((double)0.0)); - abort(); -#else ret = allocator.alloc<CallImport>(); noteImportedFunctionCall(ast, type, &asmData); -#endif } else { ret = allocator.alloc<Call>(); } @@ -871,12 +1192,14 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { auto ret = allocator.alloc<CallIndirect>(); Ref target = ast[1]; assert(target[0] == SUB && target[1][0] == NAME && target[2][0] == BINARY && target[2][1] == AND && target[2][3][0] == NUM); // FUNCTION_TABLE[(expr) & mask] - ret->target = process(target[2][2]); + ret->target = process(target[2]); // TODO: as an optimization, we could look through the mask Ref args = ast[2]; for (unsigned i = 0; i < args->size(); i++) { ret->operands.push_back(process(args[i])); } - ret->type = getFunctionType(astStackHelper.getParent(), ret->operands); + ret->fullType = getFunctionType(astStackHelper.getParent(), ret->operands); + ret->type = ret->fullType->result; + callIndirects[ret] = target[1][1]->getIString(); // we need to fix this up later, when we know how asm function tables are layed out inside the wasm table. return ret; } else if (what == RETURN) { WasmType type = !!ast[1] ? detectWasmType(ast[1], &asmData) : none; @@ -892,7 +1215,26 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { ret->value = !!ast[1] ? process(ast[1]) : nullptr; return ret; } else if (what == BLOCK) { - return processStatements(ast[1], 0); + Name name; + if (parentLabel.is()) { + name = getBreakLabelName(parentLabel); + parentLabel = IString(); + breakStack.push_back(name); + } + auto ret = processStatements(ast[1], 0); + if (name.is()) { + breakStack.pop_back(); + Block* block = ret->dyn_cast<Block>(); + if (block && block->name.isNull()) { + block->name = name; + } else { + block = allocator.alloc<Block>(); + block->name = name; + block->list.push_back(ret); + ret = block; + } + } + return ret; } else if (what == BREAK) { auto ret = allocator.alloc<Break>(); assert(breakStack.size() > 0); @@ -933,6 +1275,12 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { body->list.push_back(process(ast[2])); ret->body = body; } + // loops do not automatically loop, add a branch back + Block* block = blockify(ret->body); + auto continuer = allocator.alloc<Break>(); + continuer->name = ret->in; + block->list.push_back(continuer); + ret->body = block; continueStack.pop_back(); breakStack.pop_back(); return ret; @@ -973,22 +1321,15 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { ret->body = process(ast[2]); continueStack.pop_back(); breakStack.pop_back(); - Break *breakOut = allocator.alloc<Break>(); - breakOut->name = out; - If *condition = allocator.alloc<If>(); - condition->condition = process(ast[1]); - condition->ifTrue = allocator.alloc<Nop>(); - condition->ifFalse = breakOut; - if (Block *block = ret->body->dyn_cast<Block>()) { - block->list.push_back(condition); - } else { - auto newBody = allocator.alloc<Block>(); - newBody->list.push_back(ret->body); - newBody->list.push_back(condition); - ret->body = newBody; - } + Break *continuer = allocator.alloc<Break>(); + continuer->name = in; + continuer->condition = process(ast[1]); + Block *block = blockify(ret->body); + block->list.push_back(continuer); + ret->body = block; return ret; } else if (what == LABEL) { + assert(parentLabel.isNull()); parentLabel = ast[1]->getIString(); return process(ast[2]); } else if (what == CONDITIONAL) { @@ -1005,8 +1346,13 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { ret->type = ret->list[1]->type; return ret; } else if (what == SWITCH) { - // XXX switch is still in flux in the spec repo, just emit a placeholder - IString name = getNextId("switch"); + IString name; + if (!parentLabel.isNull()) { + name = getBreakLabelName(parentLabel); + parentLabel = IString(); + } else { + name = getNextId("switch"); + } breakStack.push_back(name); auto ret = allocator.alloc<Switch>(); ret->name = name; @@ -1118,6 +1464,7 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { } // cleanups/checks assert(breakStack.size() == 0 && continueStack.size() == 0); + assert(parentLabel.isNull()); return function; } |