diff options
Diffstat (limited to 'src/asm2wasm.h')
-rw-r--r-- | src/asm2wasm.h | 1065 |
1 files changed, 1065 insertions, 0 deletions
diff --git a/src/asm2wasm.h b/src/asm2wasm.h new file mode 100644 index 000000000..6a49559a0 --- /dev/null +++ b/src/asm2wasm.h @@ -0,0 +1,1065 @@ + +#include "wasm.h" +#include "optimizer.h" + +namespace wasm { + +using namespace cashew; + +int debug = 0; + +// 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"); + + +static void abort_on(std::string why) { + std::cerr << why << '\n'; + abort(); +} +static void abort_on(std::string why, int x) { + std::cerr << why << ' ' << x << '\n'; + abort(); +} +static void abort_on(std::string why, Ref element) { + std::cerr << why << ' '; + element->stringify(std::cerr); + std::cerr << '\n'; + abort(); +} +static void abort_on(std::string why, IString element) { + std::cerr << why << ' ' << element.str << '\n'; + abort(); +} + +// useful when we need to see our parent, in an asm.js expression stack +struct AstStackHelper { + static std::vector<Ref> astStack; + AstStackHelper(Ref curr) { + astStack.push_back(curr); + } + ~AstStackHelper() { + astStack.pop_back(); + } + Ref getParent() { + assert(astStack.size() >= 2); + return astStack[astStack.size()-2]; + } +}; + +std::vector<Ref> AstStackHelper::astStack; + +// +// Asm2WasmBuilder - converts an asm.js module into WebAssembly +// + +class Asm2WasmBuilder { + Module& wasm; + + wasm::Arena allocator; + + // globals + + unsigned nextGlobal; // next place to put a global + unsigned maxGlobal; // highest address we can put a global + struct MappedGlobal { + unsigned address; + WasmType type; + bool import; // if true, this is an import - we should read the value, not just set a zero + MappedGlobal() : address(0), type(none), import(false) {} + MappedGlobal(unsigned address, WasmType type, bool import) : address(address), type(type), import(import) {} + }; + std::map<IString, MappedGlobal> mappedGlobals; + + void allocateGlobal(IString name, WasmType type, bool import) { + assert(mappedGlobals.find(name) == mappedGlobals.end()); + mappedGlobals.emplace(name, MappedGlobal(nextGlobal, type, import)); + nextGlobal += 8; + assert(nextGlobal < maxGlobal); + } + + struct View { + unsigned bytes; + bool integer, signed_; + View() : bytes(0) {} + View(unsigned bytes, bool integer, bool signed_) : bytes(bytes), integer(integer), signed_(signed_) {} + }; + + 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 + + // function types. we fill in this information as we see + // uses, in the first pass + + std::map<IString, FunctionType> importedFunctionTypes; + + void noteImportedFunctionCall(Ref ast, Ref parent, AsmData *asmData) { + assert(ast[0] == CALL && ast[1][0] == NAME); + IString importName = ast[1][1]->getIString(); + FunctionType type; + type.name = IString((std::string("type$") + importName.str).c_str(), false); // TODO: make a list of such types + type.result = detectWasmType(parent, asmData); + Ref args = ast[2]; + for (unsigned i = 0; i < args->size(); i++) { + type.params.push_back(detectWasmType(args[i], asmData)); + } + // if we already saw this signature, verify it's the same (or else handle that) + if (importedFunctionTypes.find(importName) != importedFunctionTypes.end()) { + FunctionType& previous = importedFunctionTypes[importName]; +#if 0 + std::cout << "compare " << importName.str << "\nfirst: "; + type.print(std::cout, 0); + std::cout << "\nsecond: "; + previous.print(std::cout, 0) << ".\n"; +#endif + if (type != previous) { + // merge it in. we'll add on extra 0 parameters for ones not actually used, etc. + for (size_t i = 0; i < type.params.size(); i++) { + 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 + } + } + } + if (previous.result == none) { + previous.result = type.result; // use a more concrete type + } + } + } else { + importedFunctionTypes[importName] = type; + } + } + + 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); + for (auto operand : operands) { + str += getSigFromType(operand->type); + } + IString sig(str.c_str(), false); + if (wasm.functionTypes.find(sig) == wasm.functionTypes.end()) { + // add new type + auto type = allocator.alloc<FunctionType>(); + type->name = sig; + type->result = result; + for (auto operand : operands) { + type->params.push_back(operand->type); + } + wasm.functionTypes[sig] = type; + assert(wasm.functionTypes.find(sig) != wasm.functionTypes.end()); + } + return wasm.functionTypes[sig]; + } + +public: + Asm2WasmBuilder(Module& wasm) : wasm(wasm), nextGlobal(8), maxGlobal(1000) {} + + 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_NONE: return WasmType::none; + default: abort_on("confused asmType", asmType); + } + } + AsmType wasmToAsmType(WasmType type) { + switch (type) { + case WasmType::i32: return ASM_INT; + case WasmType::f64: return ASM_DOUBLE; + case WasmType::none: return ASM_NONE; + default: abort_on("confused wasmType", type); + } + } + + AsmType detectAsmType(Ref ast, AsmData *data) { + if (ast[0] == NAME) { + IString name = ast[1]->getIString(); + if (!data->isLocal(name)) { + // must be global + assert(mappedGlobals.find(name) != mappedGlobals.end()); + return wasmToAsmType(mappedGlobals[name].type); + } + } + return detectType(ast, data); + } + + 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; + } + + // 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; } + WasmType leftType = detectWasmType(left, asmData); +#if 0 + std::cout << "CHECK\n"; + left->stringify(std::cout); + std::cout << " => " << printWasmType(leftType); + std::cout << '\n'; + right->stringify(std::cout); + std::cout << " => " << printWasmType(detectWasmType(right, asmData)) << "\n"; +#endif + bool isInteger = leftType == WasmType::i32; + bool isUnsigned = isUnsignedCoercion(left) || isUnsignedCoercion(right); + if (op == DIV) { + if (isInteger) { + { binary = isUnsigned ? BinaryOp::DivU : BinaryOp::DivS; return true; } + } + { binary = BinaryOp::Div; return true; } + } + if (op == MOD) { + if (isInteger) { + { binary = isUnsigned ? BinaryOp::RemU : BinaryOp::RemS; return true; } + } + abort_on("non-integer rem"); + } + if (op == GE) { + if (isInteger) { + { relational = isUnsigned ? RelationalOp::GeU : RelationalOp::GeS; return false; } + } + { relational = RelationalOp::Ge; return false; } + } + if (op == GT) { + if (isInteger) { + { relational = isUnsigned ? RelationalOp::GtU : RelationalOp::GtS; return false; } + } + { relational = RelationalOp::Gt; return false; } + } + if (op == LE) { + if (isInteger) { + { relational = isUnsigned ? RelationalOp::LeU : RelationalOp::LeS; return false; } + } + { relational = RelationalOp::Le; return false; } + } + if (op == LT) { + if (isInteger) { + { relational = isUnsigned ? RelationalOp::LtU : RelationalOp::LtS; return false; } + } + { relational = RelationalOp::Lt; return false; } + } + abort_on("bad wasm binary op", op); + } + + unsigned bytesToShift(unsigned bytes) { + switch (bytes) { + case 1: return 0; + case 2: return 1; + case 4: return 2; + case 8: return 3; + default: abort(); + } + } + + wasm::Arena tempAllocator; + + std::map<unsigned, Ref> tempNums; + + Literal getLiteral(Ref ast) { + if (ast[0] == NUM) { + return Literal((int32_t)ast[1]->getInteger()); + } else if (ast[0] == UNARY_PREFIX) { + 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] == MINUS && ast[2][0] == UNARY_PREFIX && ast[2][1] == PLUS && ast[2][2][0] == NUM) { + return Literal((double)-ast[2][2][1]->getNumber()); + } + } + abort(); + } + + Function* processFunction(Ref ast); +}; + +void Asm2WasmBuilder::processAsm(Ref ast) { + assert(ast[0] == TOPLEVEL); + Ref asmFunction = ast[1][0]; + assert(asmFunction[0] == DEFUN); + Ref body = asmFunction[3]; + assert(body[0][0] == STAT && body[0][1][0] == STRING && (body[0][1][1]->getIString() == IString("use asm") || body[0][1][1]->getIString() == IString("almost asm"))); + + auto addImport = [&](IString name, Ref imported, WasmType type) { + assert(imported[0] == DOT); + Ref module = imported[1]; + if (module[0] == DOT) { + // we can have (global.Math).floor; skip the 'Math' + assert(module[1][0] == NAME); + if (module[2] == MATH) { + if (imported[2] == IMUL) { + assert(Math_imul.isNull()); + Math_imul = name; + return; + } else if (imported[2] == CLZ32) { + assert(Math_clz32.isNull()); + Math_clz32 = name; + return; + } + } + module = module[1]; + } + assert(module[0] == NAME); + Import import; + import.name = name; + import.module = module[1]->getIString(); + import.base = imported[2]->getIString(); + // special-case some asm builtins + if (import.module == GLOBAL && (import.base == NAN_ || import.base == INFINITY_)) { + type = WasmType::f64; + } + if (type != WasmType::none) { + // wasm has no imported constants, so allocate a global, and we need to write the value into that + allocateGlobal(name, type, true); + } else { + wasm.imports.emplace(name, import); + } + }; + + // first pass - do almost everything, but function imports + + for (unsigned i = 1; i < body->size(); i++) { + Ref curr = body[i]; + if (curr[0] == VAR) { + // import, global, or table + for (unsigned j = 0; j < curr[1]->size(); j++) { + Ref pair = curr[1][j]; + IString name = pair[0]->getIString(); + Ref value = pair[1]; + if (value[0] == NUM) { + // global int + assert(value[1]->getNumber() == 0); + allocateGlobal(name, WasmType::i32, false); + } else if (value[0] == BINARY) { + // int import + assert(value[1] == OR && value[3][0] == NUM && value[3][1]->getNumber() == 0); + Ref import = value[2]; // env.what + addImport(name, import, WasmType::i32); + } else if (value[0] == UNARY_PREFIX) { + // double import or global + assert(value[1] == PLUS); + Ref import = value[2]; + if (import[0] == NUM) { + // global + assert(import[1]->getNumber() == 0); + allocateGlobal(name, WasmType::f64, false); + } else { + // import + addImport(name, import, WasmType::f64); + } + } else if (value[0] == DOT) { + // 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_; + if (heap == INT8ARRAY) { + bytes = 1; integer = true; signed_ = true; + } else if (heap == INT16ARRAY) { + bytes = 2; integer = true; signed_ = true; + } else if (heap == INT32ARRAY) { + bytes = 4; integer = true; signed_ = true; + } else if (heap == UINT8ARRAY) { + bytes = 1; integer = true; signed_ = false; + } else if (heap == UINT16ARRAY) { + bytes = 2; integer = true; signed_ = false; + } else if (heap == UINT32ARRAY) { + bytes = 4; integer = true; signed_ = false; + } else if (heap == FLOAT32ARRAY) { + bytes = 4; integer = false; signed_ = true; + } else if (heap == FLOAT64ARRAY) { + bytes = 8; integer = false; signed_ = true; + } + assert(views.find(name) == views.end()); + views.emplace(name, View(bytes, integer, signed_)); + } 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 + 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; + } + } + } + } else { + abort_on("invalid var element", pair); + } + } + } else if (curr[0] == DEFUN) { + // function + wasm.functions.push_back(processFunction(curr)); + } else if (curr[0] == RETURN) { + // exports + Ref object = curr[1]; + Ref contents = curr[1][1]; + for (unsigned k = 0; k < contents->size(); k++) { + Ref pair = contents[k]; + IString key = pair[0]->getIString(); + Ref value = pair[1]; + assert(value[0] == NAME); + Export export_; + export_.name = key; + export_.value = value[1]->getIString(); + wasm.exports.push_back(export_); + } + } + } + + // second pass - function imports + + std::vector<IString> toErase; + + for (auto& pair : wasm.imports) { + IString name = pair.first; + Import& import = pair.second; + if (importedFunctionTypes.find(name) != importedFunctionTypes.end()) { + import.type = importedFunctionTypes[name]; + } else { + // never actually used + toErase.push_back(name); + } + } + + for (auto curr : toErase) { + wasm.imports.erase(curr); + } + + // cleanups + + tempAllocator.clear(); +} + +Function* Asm2WasmBuilder::processFunction(Ref ast) { + //if (ast[1] != IString("qta")) return nullptr; + + if (debug) { + std::cout << "\nfunc: " << ast[1]->getIString().str << '\n'; + if (debug >= 2) { + ast->stringify(std::cout); + std::cout << '\n'; + } + } + + auto function = allocator.alloc<Function>(); + function->name = ast[1]->getIString(); + Ref params = ast[2]; + Ref body = ast[3]; + + unsigned nextId = 0; + auto getNextId = [&nextId](std::string prefix) { + return IString((prefix + '$' + std::to_string(nextId++)).c_str(), false); + }; + + // given an asm.js label, returns the wasm label for breaks or continues + auto getBreakLabelName = [](IString label) { + return IString((std::string("label$break$") + label.str).c_str(), false); + }; + auto getContinueLabelName = [](IString label) { + return IString((std::string("label$continue$") + label.str).c_str(), false); + }; + + IStringSet functionVariables; // params or locals + + IString parentLabel; // set in LABEL, then read in WHILE/DO + std::vector<IString> breakStack; // where a break will go + std::vector<IString> continueStack; // where a continue will go + + AsmData asmData; // need to know var and param types, for asm type detection + + for (unsigned i = 0; i < params->size(); i++) { + Ref curr = body[i]; + assert(curr[0] == STAT); + curr = curr[1]; + assert(curr[0] == ASSIGN && curr[2][0] == NAME); + IString name = curr[2][1]->getIString(); + AsmType asmType = detectType(curr[3]); + function->params.emplace_back(name, asmToWasmType(asmType)); + functionVariables.insert(name); + asmData.addParam(name, asmType); + } + unsigned start = params->size(); + while (start < body->size() && body[start][0] == VAR) { + Ref curr = body[start]; + 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); + function->locals.emplace_back(name, asmToWasmType(asmType)); + functionVariables.insert(name); + asmData.addVar(name, asmType); + } + start++; + } + + 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 + std::function<Expression* (Ref, unsigned)> processStatements; + std::function<Expression* (Ref, unsigned)> processUnshifted; + + std::function<Expression* (Ref)> process = [&](Ref ast) -> Expression* { + AstStackHelper astStackHelper(ast); // TODO: only create one when we need it? + if (debug >= 2) { + std::cout << "at: "; + ast->stringify(std::cout); + std::cout << '\n'; + } + IString what = ast[0]->getIString(); + if (what == STAT) { + return process(ast[1]); // and drop return value, if any + } else if (what == ASSIGN) { + if (ast[2][0] == NAME) { + IString name = ast[2][1]->getIString(); + if (functionVariables.has(name)) { + auto ret = allocator.alloc<SetLocal>(); + ret->name = ast[2][1]->getIString(); + ret->value = process(ast[3]); + ret->type = ret->value->type; + return ret; + } + // global var, do a store to memory + assert(mappedGlobals.find(name) != mappedGlobals.end()); + MappedGlobal global = mappedGlobals[name]; + auto ret = allocator.alloc<Store>(); + ret->bytes = getWasmTypeSize(global.type); + ret->float_ = isFloat(global.type); + ret->offset = 0; + ret->align = ret->bytes; + auto ptr = allocator.alloc<Const>(); + ptr->value.type = WasmType::i32; // XXX for wasm64, need 64 + ptr->value.i32 = global.address; + ret->ptr = ptr; + ret->value = process(ast[3]); + ret->type = global.type; + return ret; + } else if (ast[2][0] == SUB) { + Ref target = ast[2]; + assert(target[1][0] == NAME); + IString heap = target[1][1]->getIString(); + assert(views.find(heap) != views.end()); + View& view = views[heap]; + auto ret = allocator.alloc<Store>(); + ret->bytes = view.bytes; + ret->float_ = !view.integer; + ret->offset = 0; + ret->align = view.bytes; + ret->ptr = processUnshifted(target[2], view.bytes); + ret->value = process(ast[3]); + ret->type = ret->value->type; + 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 + 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; + 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; + } + } 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; + } else { + ret->value.type = WasmType::f64; + ret->value.f64 = num; + } + ret->type = ret->value.type; + return ret; + } else if (what == NAME) { + IString name = ast[1]->getIString(); + if (functionVariables.has(name)) { + // var in scope + auto ret = allocator.alloc<GetLocal>(); + ret->name = name; + ret->type = asmToWasmType(asmData.getType(name)); + return ret; + } + // global var, do a load from memory + assert(mappedGlobals.find(name) != mappedGlobals.end()); + MappedGlobal global = mappedGlobals[name]; + auto ret = allocator.alloc<Load>(); + ret->bytes = getWasmTypeSize(global.type); + ret->signed_ = true; // but doesn't matter + ret->float_ = isFloat(global.type); + ret->offset = 0; + ret->align = ret->bytes; + auto ptr = allocator.alloc<Const>(); + ptr->value.type = WasmType::i32; // XXX for wasm64, need 64 + ptr->value.i32 = global.address; + ret->ptr = ptr; + ret->type = global.type; + return ret; + } else if (what == SUB) { + Ref target = ast[1]; + assert(target[0] == NAME); + IString heap = target[1]->getIString(); + assert(views.find(heap) != views.end()); + View& view = views[heap]; + auto ret = allocator.alloc<Load>(); + ret->bytes = view.bytes; + ret->signed_ = view.signed_; + ret->float_ = !view.integer; + ret->offset = 0; + ret->align = view.bytes; + ret->ptr = processUnshifted(ast[2], view.bytes); + ret->type = getWasmType(view.bytes, !view.integer); + 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; + } + 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; + } + 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 + 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)) { + auto ret = allocator.alloc<Const>(); + ret->value = getLiteral(ast); + ret->type = ret->value.type; + return ret; + } + AsmType asmType = detectAsmType(ast[2], &asmData); + if (asmType == ASM_INT) { + // wasm has no unary negation for int, so do 0- + auto ret = allocator.alloc<Binary>(); + ret->op = Sub; + ret->left = allocator.alloc<Const>()->set(Literal((int32_t)0)); + ret->right = process(ast[2]); + 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; + 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>(); + 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; + } + // no bitwise unary not, so do xor with -1 + auto ret = allocator.alloc<Binary>(); + ret->op = Xor; + ret->left = process(ast[2]); + ret->right = allocator.alloc<Const>()->set(Literal(int32_t(-1))); + ret->type = WasmType::i32; + return ret; + } else if (ast[1] == L_NOT) { + // no logical unary not, so do == 0 + auto ret = allocator.alloc<Compare>(); + 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; + return ret; + } + abort_on("bad unary", ast); + } else if (what == IF) { + auto ret = allocator.alloc<If>(); + ret->condition = process(ast[1]); + ret->ifTrue = process(ast[2]); + ret->ifFalse = !!ast[3] ? process(ast[3]) : nullptr; + return ret; + } else if (what == CALL) { + if (ast[1][0] == NAME) { + IString name = ast[1][1]->getIString(); + if (name == Math_imul) { + assert(ast[2]->size() == 2); + auto ret = allocator.alloc<Binary>(); + ret->op = Mul; + ret->left = process(ast[2][0]); + ret->right = process(ast[2][1]); + ret->type = WasmType::i32; + return ret; + } + if (name == Math_clz32) { + assert(ast[2]->size() == 1); + auto ret = allocator.alloc<Unary>(); + ret->op = Clz; + ret->value = process(ast[2][0]); + ret->type = WasmType::i32; + return ret; + } + Call* ret; + if (wasm.imports.find(name) != wasm.imports.end()) { + // no imports yet in reference interpreter, fake it + AsmType asmType = detectAsmType(astStackHelper.getParent(), &asmData); + if (asmType == ASM_NONE) return allocator.alloc<Nop>(); + if (asmType == ASM_INT) return allocator.alloc<Const>()->set(Literal((int32_t)0)); + if (asmType == ASM_DOUBLE) return allocator.alloc<Const>()->set(Literal((double)0.0)); + abort(); +#if 0 + ret = allocator.alloc<CallImport>(); + noteImportedFunctionCall(ast, astStackHelper.getParent(), &asmData); +#endif + } else { + ret = allocator.alloc<Call>(); + } + ret->target = name; + Ref args = ast[2]; + for (unsigned i = 0; i < args->size(); i++) { + ret->operands.push_back(process(args[i])); + } + return ret; + } + // function pointers + 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]); + 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); + return ret; + } else if (what == RETURN) { + WasmType type = !!ast[1] ? detectWasmType(ast[1], &asmData) : none; + if (seenReturn) { + assert(function->result == type); + } else { + function->result = type; + } + // wasm has no return, so we just break on the topmost block + needTopmost = true; + auto ret = allocator.alloc<Break>(); + ret->name = TOPMOST; + ret->condition = nullptr; + ret->value = !!ast[1] ? process(ast[1]) : nullptr; + return ret; + } else if (what == BLOCK) { + return processStatements(ast[1], 0); + } else if (what == BREAK) { + auto ret = allocator.alloc<Break>(); + assert(breakStack.size() > 0); + ret->name = !!ast[1] ? getBreakLabelName(ast[1]->getIString()) : breakStack.back(); + ret->condition = nullptr; + ret->value = nullptr; + return ret; + } else if (what == CONTINUE) { + auto ret = allocator.alloc<Break>(); + assert(continueStack.size() > 0); + ret->name = !!ast[1] ? getContinueLabelName(ast[1]->getIString()) : continueStack.back(); + ret->condition = nullptr; + ret->value = nullptr; + return ret; + } else if (what == WHILE) { + bool forever = ast[1][0] == NUM && ast[1][1]->getInteger() == 1; + auto ret = allocator.alloc<Loop>(); + IString out, in; + if (!parentLabel.isNull()) { + out = getBreakLabelName(parentLabel); + in = getContinueLabelName(parentLabel); + parentLabel = IString(); + } else { + out = getNextId("while-out"); + in = getNextId("while-in"); + } + ret->out = out; + ret->in = in; + breakStack.push_back(out); + continueStack.push_back(in); + if (forever) { + ret->body = process(ast[2]); + } else { + Break *continueWhile = allocator.alloc<Break>(); + continueWhile->name = in; + continueWhile->condition = process(ast[1]); + continueWhile->value = nullptr; + auto body = allocator.alloc<Block>(); + body->list.push_back(continueWhile); + body->list.push_back(process(ast[2])); + ret->body = body; + } + continueStack.pop_back(); + breakStack.pop_back(); + return ret; + } else if (what == DO) { + if (ast[1][0] == NUM && ast[1][1]->getNumber() == 0) { + // one-time loop + auto block = allocator.alloc<Block>(); + IString stop; + if (!parentLabel.isNull()) { + stop = getBreakLabelName(parentLabel); + parentLabel = IString(); + } else { + stop = getNextId("do-once"); + } + block->name = stop; + breakStack.push_back(stop); + continueStack.push_back(IMPOSSIBLE_CONTINUE); + block->list.push_back(process(ast[2])); + continueStack.pop_back(); + breakStack.pop_back(); + return block; + } + // general do-while loop + auto ret = allocator.alloc<Loop>(); + IString out, in; + if (!parentLabel.isNull()) { + out = getBreakLabelName(parentLabel); + in = getContinueLabelName(parentLabel); + parentLabel = IString(); + } else { + out = getNextId("do-out"); + in = getNextId("do-in"); + } + ret->out = out; + ret->in = in; + breakStack.push_back(out); + continueStack.push_back(in); + ret->body = process(ast[2]); + continueStack.pop_back(); + breakStack.pop_back(); + Break *continueIf = allocator.alloc<Break>(); + continueIf->name = in; + continueIf->condition = process(ast[1]); + continueIf->value = nullptr; + if (Block *block = dynamic_cast<Block*>(ret->body)) { + block->list.push_back(continueIf); + } else { + auto newBody = allocator.alloc<Block>(); + newBody->list.push_back(ret->body); + newBody->list.push_back(continueIf); + ret->body = newBody; + } + return ret; + } else if (what == LABEL) { + parentLabel = ast[1]->getIString(); + return process(ast[2]); + } else if (what == CONDITIONAL) { + auto ret = allocator.alloc<If>(); + ret->condition = process(ast[1]); + ret->ifTrue = process(ast[2]); + ret->ifFalse = process(ast[3]); + ret->type = ret->ifTrue->type; + return ret; + } else if (what == SEQ) { + auto ret = allocator.alloc<Block>(); + ret->list.push_back(process(ast[1])); + ret->list.push_back(process(ast[2])); + return ret; + } else if (what == SWITCH) { + // XXX switch is still in flux in the spec repo, just emit a placeholder + return allocator.alloc<Nop>(); +#if 0 + IString name = getNextId("switch"); + breakStack.push_back(name); + auto ret = allocator.alloc<Switch>(); + ret->name = name; + ret->value = process(ast[1]); + Ref cases = ast[2]; + for (unsigned i = 0; i < cases->size(); i++) { + Ref curr = cases[i]; + Ref condition = curr[0]; + Ref body = curr[1]; + if (condition->isNull()) { + ret->default_ = processStatements(body, 0); + } else { + assert(condition[0] == NUM || condition[0] == UNARY_PREFIX); + Switch::Case case_; + case_.value = getLiteral(condition); + case_.body = processStatements(body, 0); + case_.fallthru = false; // XXX we assume no fallthru, ever + ret->cases.push_back(case_); + } + } + breakStack.pop_back(); + return ret; +#endif + } + abort_on("confusing expression", ast); + }; + + // given HEAP32[addr >> 2], we need an absolute address, and would like to remove that shift. + // if there is a shift, we can just look through it, etc. + processUnshifted = [&](Ref ptr, unsigned bytes) { + unsigned shifts = bytesToShift(bytes); + if (ptr[0] == BINARY && ptr[1] == RSHIFT && ptr[3][0] == NUM && ptr[3][1]->getInteger() == shifts) { + return process(ptr[2]); // look through it + } else if (ptr[0] == NUM) { + // constant, apply a shift (e.g. HEAP32[1] is address 4) + unsigned addr = ptr[1]->getInteger(); + unsigned shifted = addr << shifts; + auto ret = allocator.alloc<Const>(); + ret->value.type = WasmType::i32; + ret->value.i32 = shifted; + return (Expression*)ret; + } + abort_on("bad processUnshifted", ptr); + }; + + processStatements = [&](Ref ast, unsigned from) -> Expression* { + unsigned size = ast->size() - from; + if (size == 0) return allocator.alloc<Nop>(); + if (size == 1) return process(ast[from]); + auto block = allocator.alloc<Block>(); + for (unsigned i = from; i < ast->size(); i++) { + block->list.push_back(process(ast[i])); + } + return block; + }; + // body + function->body = processStatements(body, start); + if (needTopmost) { + Block* topmost = dynamic_cast<Block*>(function->body); + // if there's no block there, or there is a block but it already has a name, we need a new block. + if (!topmost || topmost->name.is()) { + topmost = allocator.alloc<Block>(); + topmost->list.push_back(function->body); + function->body = topmost; + } + topmost->name = TOPMOST; + } + // cleanups/checks + assert(breakStack.size() == 0 && continueStack.size() == 0); + + return function; +} + +void Asm2WasmBuilder::optimize() { + struct BlockRemover : public WasmWalker { + BlockRemover() : WasmWalker(nullptr) {} + + Expression* visitBlock(Block *curr) override { + if (curr->list.size() != 1) return curr; + // just one element; maybe we can return just the element + if (curr->name.isNull()) return curr->list[0]; + // we might be broken to, but if it's a trivial singleton child break, we can optimize here as well + Break *child = dynamic_cast<Break*>(curr->list[0]); + if (!child || child->name != curr->name || !child->value) return curr; + + struct BreakSeeker : public WasmWalker { + IString target; // look for this one + size_t found; + + BreakSeeker(IString target) : target(target), found(false) {} + + Expression* visitBreak(Break *curr) override { + if (curr->name == target) found++; + } + }; + + // look in the child's children to see if there are more uses of this name + BreakSeeker breakSeeker(curr->name); + breakSeeker.walk(child->condition); + breakSeeker.walk(child->value); + if (breakSeeker.found == 0) return child->value; + + return curr; // failed to optimize + } + }; + + BlockRemover blockRemover; + for (auto function : wasm.functions) { + blockRemover.startWalk(function); + } +} + +} // namespace wasm + |