diff options
author | Alex Crichton <alex@alexcrichton.com> | 2018-08-30 16:10:26 -0700 |
---|---|---|
committer | Alon Zakai <alonzakai@gmail.com> | 2018-08-30 16:10:26 -0700 |
commit | f109f3cae1cd81db22ba490a4da17a7a4c495047 (patch) | |
tree | fd7307a567505a28f879ccce00a30d2d0d27b848 /src/wasm2js.h | |
parent | 3976440ccb2c3ab9d67af7239f87ae04ebdeda1e (diff) | |
download | binaryen-f109f3cae1cd81db22ba490a4da17a7a4c495047.tar.gz binaryen-f109f3cae1cd81db22ba490a4da17a7a4c495047.tar.bz2 binaryen-f109f3cae1cd81db22ba490a4da17a7a4c495047.zip |
Rename `wasm2asm` to `wasm2js`, emit ESM by default (#1642)
* Rename the `wasm2asm` tool to `wasm2js`
This commit performs a relatively simple rename of the `wasm2asm` tool to
`wasm2js`. The functionality of the tool doesn't change just yet but it's
intended that we'll start generating an ES module instead of just an `asm.js`
function soon.
* wasm2js: Support `*.wasm` input files
Previously `wasm2js` only supported `*.wast` files but to make it a bit easier
to use in tooling pipelines this commit adds support for reading in a `*.wasm`
file directly. Determining which parser to use depends on the input filename,
where the binary parser is used with `*.wasm` files and the wast parser is used
for all other files.
* wasm2js: Emit ESM imports/exports by default
This commit alters the default behavior of `wasm2js` to emit an ESM by default,
either importing items from the environment or exporting. Items like
initialization of memory are also handled here.
Diffstat (limited to 'src/wasm2js.h')
-rw-r--r-- | src/wasm2js.h | 2488 |
1 files changed, 2488 insertions, 0 deletions
diff --git a/src/wasm2js.h b/src/wasm2js.h new file mode 100644 index 000000000..40f8e3057 --- /dev/null +++ b/src/wasm2js.h @@ -0,0 +1,2488 @@ +/* + * Copyright 2015 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// WebAssembly-to-asm.js translator. Uses the Emscripten optimizer +// infrastructure. +// + +#ifndef wasm_wasm2js_h +#define wasm_wasm2js_h + +#include <cmath> +#include <numeric> + +#include "asmjs/shared-constants.h" +#include "asmjs/asmangle.h" +#include "wasm.h" +#include "wasm-builder.h" +#include "wasm-io.h" +#include "wasm-validator.h" +#include "emscripten-optimizer/optimizer.h" +#include "mixed_arena.h" +#include "asm_v_wasm.h" +#include "ir/names.h" +#include "ir/utils.h" +#include "passes/passes.h" + +namespace wasm { + +using namespace cashew; + +IString ASM_FUNC("asmFunc"), + ABORT_FUNC("abort"), + FUNCTION_TABLE("FUNCTION_TABLE"), + NO_RESULT("wasm2js$noresult"), // no result at all + EXPRESSION_RESULT("wasm2js$expresult"); // result in an expression, no temp var + +// Appends extra to block, flattening out if extra is a block as well +void flattenAppend(Ref ast, Ref extra) { + int index; + if (ast[0] == BLOCK || ast[0] == TOPLEVEL) index = 1; + else if (ast[0] == DEFUN) index = 3; + else abort(); + if (extra->isArray() && extra[0] == BLOCK) { + for (size_t i = 0; i < extra[1]->size(); i++) { + ast[index]->push_back(extra[1][i]); + } + } else { + ast[index]->push_back(extra); + } +} + +// Used when taking a wasm name and generating a JS identifier. Each scope here +// is used to ensure that all names have a unique name but the same wasm name +// within a scope always resolves to the same symbol. +enum class NameScope { + Top, + Local, + Label, + Max, +}; + +template<typename T> +static uint64_t constOffset(const T& segment) { + auto* c = segment.offset->template dynCast<Const>(); + if (!c) { + Fatal() << "non-constant offsets aren't supported yet\n"; + abort(); + } + return c->value.getInteger(); +} + +// +// Wasm2JSBuilder - converts a WebAssembly module into asm.js +// +// In general, asm.js => wasm is very straightforward, as can +// be seen in asm2wasm.h. Just a single pass, plus a little +// state bookkeeping (breakStack, etc.), and a few after-the +// fact corrections for imports, etc. However, wasm => asm.js +// is tricky because wasm has statements == expressions, or in +// other words, things like `break` and `if` can show up +// in places where asm.js can't handle them, like inside an +// a loop's condition check. +// +// We therefore need the ability to lower an expression into +// a block of statements, and we keep statementizing until we +// reach a context in which we can emit those statments. This +// requires that we create temp variables to store values +// that would otherwise flow directly into their targets if +// we were an expression (e.g. if a loop's condition check +// is a bunch of statements, we execute those statements, +// then use the computed value in the loop's condition; +// we might also be able to avoid an assign to a temp var +// at the end of those statements, and put just that +// value in the loop's condition). +// +// It is possible to do this in a single pass, if we just +// allocate temp vars freely. However, pathological cases +// can easily show bad behavior here, with many unnecessary +// temp vars. We could rely on optimization passes like +// Emscripten's eliminate/registerize pair, but we want +// wasm2js to be fairly fast to run, as it might run on +// the client. +// +// The approach taken here therefore performs 2 passes on +// each function. First, it finds which expression will need to +// be statementized. It also sees which labels can receive a break +// with a value. Given that information, in the second pass we can +// allocate // temp vars in an efficient manner, as we know when we +// need them and when their use is finished. They are allocated +// using an RAII class, so that they are automatically freed +// when the scope ends. This means that a node cannot allocate +// its own temp var; instead, the parent - which knows the +// child will return a value in a temp var - allocates it, +// and tells the child what temp var to emit to. The child +// can then pass forward that temp var to its children, +// optimizing away unnecessary forwarding. + + +class Wasm2JSBuilder { + MixedArena allocator; + +public: + struct Flags { + bool debug = false; + bool pedantic = false; + bool allowAsserts = false; + }; + + Wasm2JSBuilder(Flags f) : flags(f) {} + + Ref processWasm(Module* wasm, Name funcName = ASM_FUNC); + Ref processFunction(Module* wasm, Function* func); + + // The first pass on an expression: scan it to see whether it will + // need to be statementized, and note spooky returns of values at + // a distance (aka break with a value). + void scanFunctionBody(Expression* curr); + + // The second pass on an expression: process it fully, generating + // asm.js + // @param result Whether the context we are in receives a value, + // and its type, or if not, then we can drop our return, + // if we have one. + Ref processFunctionBody(Module* m, Function* func, IString result); + + Ref processAsserts(Module* wasm, Element& e, SExpressionWasmBuilder& sexpBuilder); + + // Get a temp var. + IString getTemp(Type type, Function* func) { + IString ret; + if (frees[type].size() > 0) { + ret = frees[type].back(); + frees[type].pop_back(); + } else { + size_t index = temps[type]++; + ret = IString((std::string("wasm2js_") + printType(type) + "$" + + std::to_string(index)).c_str(), false); + } + if (func->localIndices.find(ret) == func->localIndices.end()) { + Builder::addVar(func, ret, type); + } + return ret; + } + + // Free a temp var. + void freeTemp(Type type, IString temp) { + frees[type].push_back(temp); + } + + // Generates a mangled name from `name` within the specified scope. + // + // The goal of this function is to ensure that all identifiers in JS ar + // unique. Otherwise there can be clashes with locals and functions and cause + // unwanted name shadowing. + // + // The returned string from this function is constant for a particular `name` + // within a `scope`. Or in other words, the same `name` and `scope` pair will + // always return the same result. If `scope` changes, however, the return + // value may differ even if the same `name` is passed in. + IString fromName(Name name, NameScope scope) { + // TODO: checking names do not collide after mangling + + // First up check our cached of mangled names to avoid doing extra work + // below + auto &mangledScope = mangledNames[(int) scope]; + auto it = mangledScope.find(name.c_str()); + if (it != mangledScope.end()) { + return it->second; + } + + // This is the first time we've seen the `name` and `scope` pair. Generate a + // globally unique name based on `name` and then register that in our cache + // and return it. + // + // Identifiers here generated are of the form `${name}_${n}` where `_${n}` + // is omitted if `n==0` and otherwise `n` is just looped over to find the + // next unused identifier. + IString ret; + for (int i = 0;; i++) { + std::ostringstream out; + out << name.c_str(); + if (i > 0) { + out << "_" << i; + } + auto mangled = asmangle(out.str()); + ret = IString(mangled.c_str(), false); + if (!allMangledNames.count(ret)) { + break; + } + + // In the global scope that's how you refer to actual function exports, so + // it's a bug currently if they're not globally unique. This should + // probably be fixed via a different namespace for exports or something + // like that. + if (scope == NameScope::Top) { + Fatal() << "global scope is colliding with other scope: " << mangled << '\n'; + abort(); + } + } + allMangledNames.insert(ret); + mangledScope[name.c_str()] = ret; + return ret; + } + + void setStatement(Expression* curr) { + willBeStatement.insert(curr); + } + + bool isStatement(Expression* curr) { + return curr && willBeStatement.find(curr) != willBeStatement.end(); + } + + size_t getTableSize() { + return tableSize; + } + +private: + Flags flags; + + // How many temp vars we need + std::vector<size_t> temps; // type => num temps + // Which are currently free to use + std::vector<std::vector<IString>> frees; // type => list of free names + + // Expressions that will be a statement. + std::set<Expression*> willBeStatement; + + // Mangled names cache by interned names. + // Utilizes the usually reused underlying cstring's pointer as the key. + std::unordered_map<const char*, IString> mangledNames[(int) NameScope::Max]; + std::unordered_set<IString> allMangledNames; + + // All our function tables have the same size TODO: optimize? + size_t tableSize; + + bool almostASM = false; + + void addEsmImports(Ref ast, Module* wasm); + void addEsmExportsAndInstantiate(Ref ast, Module* wasm, Name funcName); + void addBasics(Ref ast); + void addImport(Ref ast, Import* import); + void addTables(Ref ast, Module* wasm); + void addExports(Ref ast, Module* wasm); + void addGlobal(Ref ast, Global* global); + void setNeedsAlmostASM(const char *reason); + void addMemoryGrowthFuncs(Ref ast); + bool isAssertHandled(Element& e); + Ref makeAssertReturnFunc(SExpressionWasmBuilder& sexpBuilder, + Module* wasm, + Builder& wasmBuilder, + Element& e, + Name testFuncName, + Name asmModule); + Ref makeAssertReturnNanFunc(SExpressionWasmBuilder& sexpBuilder, + Module* wasm, + Builder& wasmBuilder, + Element& e, + Name testFuncName, + Name asmModule); + Ref makeAssertTrapFunc(SExpressionWasmBuilder& sexpBuilder, + Module* wasm, + Builder& wasmBuilder, + Element& e, + Name testFuncName, + Name asmModule); + Wasm2JSBuilder() = delete; + Wasm2JSBuilder(const Wasm2JSBuilder &) = delete; + Wasm2JSBuilder &operator=(const Wasm2JSBuilder&) = delete; +}; + +Ref Wasm2JSBuilder::processWasm(Module* wasm, Name funcName) { + PassRunner runner(wasm); + runner.add<AutoDrop>(); + // First up remove as many non-JS operations we can, including things like + // 64-bit integer multiplication/division, `f32.nearest` instructions, etc. + // This may inject intrinsics which use i64 so it needs to be run before the + // i64-to-i32 lowering pass. + runner.add("remove-non-js-ops"); + // Currently the i64-to-32 lowering pass requires that `flatten` be run before + // it produce correct code. For some more details about this see #1480 + runner.add("flatten"); + runner.add("i64-to-i32-lowering"); + runner.add("flatten"); + runner.add("simplify-locals-notee-nostructure"); + runner.add("reorder-locals"); + runner.add("vacuum"); + runner.setDebug(flags.debug); + runner.run(); + + // Make sure we didn't corrupt anything if we're in --allow-asserts mode (aka + // tests) +#ifndef NDEBUG + if (!WasmValidator().validate(*wasm)) { + WasmPrinter::printModule(wasm); + Fatal() << "error in validating input"; + } +#endif + + Ref ret = ValueBuilder::makeToplevel(); + addEsmImports(ret, wasm); + Ref asmFunc = ValueBuilder::makeFunction(funcName); + ret[1]->push_back(asmFunc); + addEsmExportsAndInstantiate(ret, wasm, funcName); + ValueBuilder::appendArgumentToFunction(asmFunc, GLOBAL); + ValueBuilder::appendArgumentToFunction(asmFunc, ENV); + ValueBuilder::appendArgumentToFunction(asmFunc, BUFFER); + asmFunc[3]->push_back(ValueBuilder::makeStatement(ValueBuilder::makeString(USE_ASM))); + // create heaps, etc + addBasics(asmFunc[3]); + for (auto& import : wasm->imports) { + addImport(asmFunc[3], import.get()); + } + // figure out the table size + tableSize = std::accumulate(wasm->table.segments.begin(), + wasm->table.segments.end(), + 0, [&](size_t size, Table::Segment seg) -> size_t { + return size + seg.data.size() + constOffset(seg); + }); + size_t pow2ed = 1; + while (pow2ed < tableSize) { + pow2ed <<= 1; + } + tableSize = pow2ed; + + // make sure exports get their expected names + for (auto& e : wasm->exports) { + if (e->kind == ExternalKind::Function) { + fromName(e->name, NameScope::Top); + } + } + for (auto& f : wasm->functions) { + fromName(f->name, NameScope::Top); + } + fromName(WASM_FETCH_HIGH_BITS, NameScope::Top); + // 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(wasm, func.get())); + } + if (generateFetchHighBits) { + Builder builder(allocator); + std::vector<Type> params; + std::vector<Type> vars; + asmFunc[3]->push_back(processFunction(wasm, 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); + return ret; +} + +void Wasm2JSBuilder::addEsmImports(Ref ast, Module *wasm) { + std::unordered_map<Name, Name> nameMap; + + for (auto& import : wasm->imports) { + // Only function imports are supported for now, but eventually imported + // memories can probably be supported at least. + switch (import->kind) { + case ExternalKind::Function: break; + default: + Fatal() << "non-function imports aren't supported yet\n"; + abort(); + } + + // Right now codegen requires a flat namespace going into the module, + // meaning we don't importing the same name from multiple namespaces yet. + if (nameMap.count(import->base) && nameMap[import->base] != import->module) { + Fatal() << "the name " << import->base << " cannot be imported from " + << "two different modules yet\n"; + abort(); + } + + nameMap[import->base] = import->module; + + std::ostringstream out; + out << "import { " + << import->base.str + << " } from '" + << import->module.str + << "'"; + std::string os = out.str(); + IString name(os.c_str(), false); + flattenAppend(ast, ValueBuilder::makeName(name)); + } +} + +static std::string base64Encode(std::vector<char> &data) { + std::string ret; + size_t i = 0; + + const char* alphabet = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + while (i + 3 <= data.size()) { + int bits = + (((int) data[i + 0]) << 16) | + (((int) data[i + 1]) << 8) | + (((int) data[i + 2]) << 0); + ret += alphabet[(bits >> 18) & 0x3f]; + ret += alphabet[(bits >> 12) & 0x3f]; + ret += alphabet[(bits >> 6) & 0x3f]; + ret += alphabet[(bits >> 0) & 0x3f]; + i += 3; + } + + if (i + 2 == data.size()) { + int bits = + (((int) data[i + 0]) << 8) | + (((int) data[i + 1]) << 0); + ret += alphabet[(bits >> 10) & 0x3f]; + ret += alphabet[(bits >> 4) & 0x3f]; + ret += alphabet[(bits << 2) & 0x3f]; + ret += '='; + } else if (i + 1 == data.size()) { + int bits =(int) data[i + 0]; + ret += alphabet[(bits >> 2) & 0x3f]; + ret += alphabet[(bits << 4) & 0x3f]; + ret += '='; + ret += '='; + } else { + assert(i == data.size()); + } + + return ret; +} + +void Wasm2JSBuilder::addEsmExportsAndInstantiate(Ref ast, Module *wasm, Name funcName) { + // Create an initial `ArrayBuffer` and populate it with static data. + // Currently we use base64 encoding to encode static data and we decode it at + // instantiation time. + // + // Note that the translation here expects that the lower values of this memory + // can be used for conversions, so make sure there's at least one page. + { + auto pages = wasm->memory.initial == 0 ? 1 : wasm->memory.initial.addr; + std::ostringstream out; + out << "const mem" << funcName.str << " = new ArrayBuffer(" + << pages * Memory::kPageSize + << ")"; + std::string os = out.str(); + IString name(os.c_str(), false); + flattenAppend(ast, ValueBuilder::makeName(name)); + } + + if (wasm->memory.segments.size() > 0) { + auto expr = R"( + function(mem) { + const _mem = new Uint8Array(mem); + return function(offset, s) { + if (typeof Buffer === 'undefined') { + const bytes = atob(s); + for (let i = 0; i < bytes.length; i++) + _mem[offset + i] = bytes.charCodeAt(i); + } else { + const bytes = Buffer.from(s, 'base64'); + for (let i = 0; i < bytes.length; i++) + _mem[offset + i] = bytes[i]; + } + } + } + )"; + + // const assign$name = ($expr)(mem$name); + std::ostringstream out; + out << "const assign" << funcName.str + << " = (" << expr << ")(mem" << funcName.str << ")"; + std::string os = out.str(); + IString name(os.c_str(), false); + flattenAppend(ast, ValueBuilder::makeName(name)); + } + for (auto& seg : wasm->memory.segments) { + std::ostringstream out; + out << "assign" << funcName.str << "(" + << constOffset(seg) + << ", \"" + << base64Encode(seg.data) + << "\")"; + std::string os = out.str(); + IString name(os.c_str(), false); + flattenAppend(ast, ValueBuilder::makeName(name)); + } + + // Actually invoke the `asmFunc` generated function, passing in all global + // values followed by all imports (imported via addEsmImports above) + std::ostringstream construct; + construct << "const ret" << funcName.str << " = " << funcName.str << "({" + << "Math," + << "Int8Array," + << "Uint8Array," + << "Int16Array," + << "Uint16Array," + << "Int32Array," + << "Uint32Array," + << "Float32Array," + << "Float64Array," + << "NaN," + << "Infinity" + << "}, {"; + + construct << "abort:function() { throw new Error('abort'); }"; + for (auto& import : wasm->imports) { + switch (import->kind) { + case ExternalKind::Function: break; + default: continue; + } + construct << "," << import->base.str; + } + construct << "},mem" << funcName.str << ")"; + std::string sconstruct = construct.str(); + IString name(sconstruct.c_str(), false); + flattenAppend(ast, ValueBuilder::makeName(name)); + + if (flags.allowAsserts) { + return; + } + + // And now that we have our returned instance, export all our functions + // that are hanging off it. + for (auto& exp : wasm->exports) { + switch (exp->kind) { + case ExternalKind::Function: + case ExternalKind::Memory: + break; + + // Exported globals and function tables aren't supported yet + default: + continue; + } + std::ostringstream export_name; + for (auto *ptr = exp->name.str; *ptr; ptr++) { + if (*ptr == '-') { + export_name << '_'; + } else { + export_name << *ptr; + } + } + std::ostringstream out; + out << "export const " + << fromName(exp->name, NameScope::Top).str + << " = ret" + << funcName.str + << "." + << fromName(exp->name, NameScope::Top).str; + std::string os = out.str(); + IString name(os.c_str(), false); + flattenAppend(ast, ValueBuilder::makeName(name)); + } +} + +void Wasm2JSBuilder::addBasics(Ref ast) { + // heaps, var HEAP8 = new global.Int8Array(buffer); etc + auto addHeap = [&](IString name, IString view) { + Ref theVar = ValueBuilder::makeVar(); + ast->push_back(theVar); + ValueBuilder::appendToVar(theVar, + name, + ValueBuilder::makeNew( + ValueBuilder::makeCall( + ValueBuilder::makeDot( + ValueBuilder::makeName(GLOBAL), + view + ), + ValueBuilder::makeName(BUFFER) + ) + ) + ); + }; + addHeap(HEAP8, INT8ARRAY); + addHeap(HEAP16, INT16ARRAY); + addHeap(HEAP32, INT32ARRAY); + addHeap(HEAPU8, UINT8ARRAY); + addHeap(HEAPU16, UINT16ARRAY); + addHeap(HEAPU32, UINT32ARRAY); + addHeap(HEAPF32, FLOAT32ARRAY); + addHeap(HEAPF64, FLOAT64ARRAY); + // core asm.js imports + auto addMath = [&](IString name, IString base) { + Ref theVar = ValueBuilder::makeVar(); + ast->push_back(theVar); + ValueBuilder::appendToVar(theVar, + name, + ValueBuilder::makeDot( + ValueBuilder::makeDot( + ValueBuilder::makeName(GLOBAL), + MATH + ), + base + ) + ); + }; + addMath(MATH_IMUL, IMUL); + addMath(MATH_FROUND, FROUND); + addMath(MATH_ABS, ABS); + addMath(MATH_CLZ32, CLZ32); + addMath(MATH_MIN, MIN); + addMath(MATH_MAX, MAX); + addMath(MATH_FLOOR, FLOOR); + addMath(MATH_CEIL, CEIL); + addMath(MATH_SQRT, SQRT); + // abort function + Ref abortVar = ValueBuilder::makeVar(); + ast->push_back(abortVar); + ValueBuilder::appendToVar(abortVar, + "abort", + ValueBuilder::makeDot( + ValueBuilder::makeName(ENV), + ABORT_FUNC + ) + ); + // TODO: this shouldn't be needed once we stop generating literal asm.js code + // NaN and Infinity variables + Ref nanVar = ValueBuilder::makeVar(); + ast->push_back(nanVar); + ValueBuilder::appendToVar(nanVar, + "nan", + ValueBuilder::makeDot(ValueBuilder::makeName(GLOBAL), "NaN") + ); + Ref infinityVar = ValueBuilder::makeVar(); + ast->push_back(infinityVar); + ValueBuilder::appendToVar(infinityVar, + "infinity", + ValueBuilder::makeDot(ValueBuilder::makeName(GLOBAL), "Infinity") + ); +} + +void Wasm2JSBuilder::addImport(Ref ast, Import* import) { + Ref theVar = ValueBuilder::makeVar(); + ast->push_back(theVar); + Ref module = ValueBuilder::makeName(ENV); // TODO: handle nested module imports + ValueBuilder::appendToVar(theVar, + fromName(import->name, NameScope::Top), + ValueBuilder::makeDot( + module, + fromName(import->base, NameScope::Top) + ) + ); +} + +void Wasm2JSBuilder::addTables(Ref ast, Module* wasm) { + std::map<std::string, std::vector<IString>> tables; // asm.js tables, sig => contents of table + for (Table::Segment& seg : wasm->table.segments) { + for (size_t i = 0; i < seg.data.size(); i++) { + Name name = seg.data[i]; + auto func = wasm->getFunction(name); + std::string sig = getSig(func); + auto& table = tables[sig]; + if (table.size() == 0) { + // fill it with the first of its type seen. we have to fill with something; and for asm2wasm output, the first is the null anyhow + table.resize(tableSize); + for (size_t j = 0; j < tableSize; j++) { + table[j] = fromName(name, NameScope::Top); + } + } else { + table[i + constOffset(seg)] = fromName(name, NameScope::Top); + } + } + } + for (auto& pair : tables) { + auto& sig = pair.first; + auto& table = pair.second; + std::string stable = std::string("FUNCTION_TABLE_") + sig; + IString asmName = IString(stable.c_str(), false); + // add to asm module + Ref theVar = ValueBuilder::makeVar(); + ast->push_back(theVar); + Ref theArray = ValueBuilder::makeArray(); + ValueBuilder::appendToVar(theVar, asmName, theArray); + for (auto& name : table) { + ValueBuilder::appendToArray(theArray, ValueBuilder::makeName(name)); + } + } +} + +void Wasm2JSBuilder::addExports(Ref ast, Module* wasm) { + Ref exports = ValueBuilder::makeObject(); + for (auto& export_ : wasm->exports) { + if (export_->kind == ExternalKind::Function) { + ValueBuilder::appendToObject( + exports, + fromName(export_->name, NameScope::Top), + ValueBuilder::makeName(fromName(export_->value, NameScope::Top)) + ); + } + if (export_->kind == ExternalKind::Memory) { + setNeedsAlmostASM("memory export"); + Ref descs = ValueBuilder::makeObject(); + Ref growDesc = ValueBuilder::makeObject(); + ValueBuilder::appendToObject( + descs, + IString("grow"), + growDesc); + ValueBuilder::appendToObject( + growDesc, + IString("value"), + ValueBuilder::makeName(WASM_GROW_MEMORY)); + Ref bufferDesc = ValueBuilder::makeObject(); + Ref bufferGetter = ValueBuilder::makeFunction(IString("")); + bufferGetter[3]->push_back(ValueBuilder::makeReturn( + ValueBuilder::makeName(BUFFER) + )); + ValueBuilder::appendToObject( + bufferDesc, + IString("get"), + bufferGetter); + ValueBuilder::appendToObject( + descs, + IString("buffer"), + bufferDesc); + Ref memory = ValueBuilder::makeCall( + ValueBuilder::makeDot(ValueBuilder::makeName(IString("Object")), IString("create")), + ValueBuilder::makeDot(ValueBuilder::makeName(IString("Object")), IString("prototype"))); + ValueBuilder::appendToCall( + memory, + descs); + ValueBuilder::appendToObject( + exports, + fromName(export_->name, NameScope::Top), + memory); + } + } + if (almostASM) { + // replace "use asm" + ast[0] = ValueBuilder::makeStatement(ValueBuilder::makeString(ALMOST_ASM)); + addMemoryGrowthFuncs(ast); + } + ast->push_back(ValueBuilder::makeStatement(ValueBuilder::makeReturn(exports))); +} + +void Wasm2JSBuilder::addGlobal(Ref ast, Global* global) { + if (auto* const_ = global->init->dynCast<Const>()) { + Ref theValue; + switch (const_->type) { + case Type::i32: { + theValue = ValueBuilder::makeInt(const_->value.geti32()); + break; + } + case Type::f32: { + theValue = ValueBuilder::makeCall(MATH_FROUND, + makeAsmCoercion(ValueBuilder::makeDouble(const_->value.getf32()), ASM_DOUBLE) + ); + break; + } + case Type::f64: { + theValue = makeAsmCoercion(ValueBuilder::makeDouble(const_->value.getf64()), ASM_DOUBLE); + break; + } + default: { + assert(false && "Top const type not supported"); + } + } + Ref theVar = ValueBuilder::makeVar(); + ast->push_back(theVar); + ValueBuilder::appendToVar(theVar, + fromName(global->name, NameScope::Top), + theValue + ); + } else { + assert(false && "Top init type not supported"); + } +} + +static bool expressionEndsInReturn(Expression *e) { + if (e->is<Return>()) { + return true; + } + if (!e->is<Block>()) { + return false; + } + ExpressionList* stats = &static_cast<Block*>(e)->list; + return expressionEndsInReturn((*stats)[stats->size()-1]); +} + +Ref Wasm2JSBuilder::processFunction(Module* m, Function* func) { + if (flags.debug) { + static int fns = 0; + std::cerr << "processFunction " << (fns++) << " " << func->name + << std::endl; + } + // We will be symbolically referring to all variables in the function, so make + // sure that everything has a name and it's unique. + Names::ensureNames(func); + Ref ret = ValueBuilder::makeFunction(fromName(func->name, NameScope::Top)); + frees.clear(); + frees.resize(std::max(i32, std::max(f32, f64)) + 1); + temps.clear(); + temps.resize(std::max(i32, std::max(f32, f64)) + 1); + temps[i32] = temps[f32] = temps[f64] = 0; + // arguments + for (Index i = 0; i < func->getNumParams(); i++) { + IString name = fromName(func->getLocalNameOrGeneric(i), NameScope::Local); + ValueBuilder::appendArgumentToFunction(ret, name); + ret[3]->push_back( + ValueBuilder::makeStatement( + ValueBuilder::makeBinary( + ValueBuilder::makeName(name), SET, + makeAsmCoercion( + ValueBuilder::makeName(name), + wasmToAsmType(func->getLocalType(i)) + ) + ) + ) + ); + } + Ref theVar = ValueBuilder::makeVar(); + size_t theVarIndex = ret[3]->size(); + ret[3]->push_back(theVar); + // body + auto appendFinalReturn = [&] (Ref retVal) { + flattenAppend( + ret, + ValueBuilder::makeReturn( + makeAsmCoercion(retVal, wasmToAsmType(func->result)) + ) + ); + }; + scanFunctionBody(func->body); + bool endsInReturn = expressionEndsInReturn(func->body); + if (endsInReturn) { + // return already taken care of + flattenAppend(ret, processFunctionBody(m, func, NO_RESULT)); + } else if (isStatement(func->body)) { + // store result in variable then return it + IString result = + func->result != none ? getTemp(func->result, func) : NO_RESULT; + flattenAppend(ret, processFunctionBody(m, func, result)); + if (func->result != none) { + appendFinalReturn(ValueBuilder::makeName(result)); + freeTemp(func->result, result); + } + } else if (func->result != none) { + // whole thing is an expression, just return body + appendFinalReturn(processFunctionBody(m, func, EXPRESSION_RESULT)); + } else { + // func has no return + flattenAppend(ret, processFunctionBody(m, func, NO_RESULT)); + } + // vars, including new temp vars + for (Index i = func->getVarIndexBase(); i < func->getNumLocals(); i++) { + ValueBuilder::appendToVar( + theVar, + fromName(func->getLocalNameOrGeneric(i), NameScope::Local), + makeAsmCoercedZero(wasmToAsmType(func->getLocalType(i))) + ); + } + if (theVar[1]->size() == 0) { + ret[3]->splice(theVarIndex, 1); + } + // checks + assert(frees[i32].size() == temps[i32]); // all temp vars should be free at the end + assert(frees[f32].size() == temps[f32]); // all temp vars should be free at the end + assert(frees[f64].size() == temps[f64]); // all temp vars should be free at the end + // cleanups + willBeStatement.clear(); + return ret; +} + +void Wasm2JSBuilder::scanFunctionBody(Expression* curr) { + struct ExpressionScanner : public PostWalker<ExpressionScanner> { + Wasm2JSBuilder* parent; + + ExpressionScanner(Wasm2JSBuilder* parent) : parent(parent) {} + + // Visitors + + void visitBlock(Block* curr) { + parent->setStatement(curr); + } + void visitIf(If* curr) { + parent->setStatement(curr); + } + void visitLoop(Loop* curr) { + parent->setStatement(curr); + } + void visitBreak(Break* curr) { + parent->setStatement(curr); + } + void visitSwitch(Switch* curr) { + parent->setStatement(curr); + } + void visitCall(Call* curr) { + for (auto item : curr->operands) { + if (parent->isStatement(item)) { + parent->setStatement(curr); + break; + } + } + } + void visitCallImport(CallImport* curr) { + for (auto item : curr->operands) { + if (parent->isStatement(item)) { + parent->setStatement(curr); + break; + } + } + } + void visitCallIndirect(CallIndirect* curr) { + // TODO: this is a pessimization that probably wants to get tweaked in + // the future. If none of the arguments have any side effects then we + // should be able to safely have tighter codegen. + parent->setStatement(curr); + } + void visitSetLocal(SetLocal* curr) { + if (parent->isStatement(curr->value)) { + parent->setStatement(curr); + } + } + void visitLoad(Load* curr) { + if (parent->isStatement(curr->ptr)) { + parent->setStatement(curr); + } + } + void visitStore(Store* curr) { + parent->setStatement(curr); + } + void visitUnary(Unary* curr) { + if (parent->isStatement(curr->value)) { + parent->setStatement(curr); + } + } + void visitBinary(Binary* curr) { + if (parent->isStatement(curr->left) || parent->isStatement(curr->right)) { + parent->setStatement(curr); + } + } + void visitSelect(Select* curr) { + if (parent->isStatement(curr->ifTrue) || parent->isStatement(curr->ifFalse) || parent->isStatement(curr->condition)) { + parent->setStatement(curr); + } + } + void visitReturn(Return* curr) { + parent->setStatement(curr); + } + void visitHost(Host* curr) { + for (auto item : curr->operands) { + if (parent->isStatement(item)) { + parent->setStatement(curr); + break; + } + } + } + }; + ExpressionScanner(this).walk(curr); +} + +Ref Wasm2JSBuilder::processFunctionBody(Module* m, Function* func, IString result) { + struct ExpressionProcessor : public Visitor<ExpressionProcessor, Ref> { + Wasm2JSBuilder* parent; + IString result; + Function* func; + Module* module; + MixedArena allocator; + ExpressionProcessor(Wasm2JSBuilder* parent, Module* m, Function* func) + : parent(parent), func(func), module(m) {} + + // A scoped temporary variable. + struct ScopedTemp { + Wasm2JSBuilder* parent; + Type type; + IString temp; + bool needFree; + // @param possible if provided, this is a variable we can use as our temp. it has already been + // allocated in a higher scope, and we can just assign to it as our result is + // going there anyhow. + ScopedTemp(Type type, Wasm2JSBuilder* parent, Function* func, + IString possible = NO_RESULT) : parent(parent), type(type) { + assert(possible != EXPRESSION_RESULT); + if (possible == NO_RESULT) { + temp = parent->getTemp(type, func); + needFree = true; + } else { + temp = possible; + needFree = false; + } + } + ~ScopedTemp() { + if (needFree) { + parent->freeTemp(type, temp); + } + } + + IString getName() { + return temp; + } + Ref getAstName() { + return ValueBuilder::makeName(temp); + } + }; + + Ref visit(Expression* curr, IString nextResult) { + IString old = result; + result = nextResult; + Ref ret = Visitor::visit(curr); + result = old; // keep it consistent for the rest of this frame, which may call visit on multiple children + return ret; + } + + Ref visit(Expression* curr, ScopedTemp& temp) { + return visit(curr, temp.temp); + } + + // this result is for an asm expression slot, but it might be a statement + Ref visitForExpression(Expression* curr, Type type, IString& tempName) { + if (isStatement(curr)) { + ScopedTemp temp(type, parent, func); + tempName = temp.temp; + return visit(curr, temp); + } else { + return visit(curr, EXPRESSION_RESULT); + } + } + + Ref visitAndAssign(Expression* curr, IString result) { + Ref ret = visit(curr, result); + // if it's not already a statement, then it's an expression, and we need to assign it + // (if it is a statement, it already assigns to the result var) + if (!isStatement(curr) && result != NO_RESULT) { + ret = ValueBuilder::makeStatement( + ValueBuilder::makeBinary(ValueBuilder::makeName(result), SET, ret)); + } + return ret; + } + + Ref visitAndAssign(Expression* curr, ScopedTemp& temp) { + return visitAndAssign(curr, temp.getName()); + } + + bool isStatement(Expression* curr) { + return parent->isStatement(curr); + } + + // Expressions with control flow turn into a block, which we must + // then handle, even if we are an expression. + bool isBlock(Ref ast) { + return !!ast && ast->isArray() && ast[0] == BLOCK; + } + + Ref blockify(Ref ast) { + if (isBlock(ast)) return ast; + Ref ret = ValueBuilder::makeBlock(); + ret[1]->push_back(ValueBuilder::makeStatement(ast)); + return ret; + } + + // For spooky return-at-a-distance/break-with-result, this tells us + // what the result var is for a specific label. + std::map<Name, IString> breakResults; + + // Breaks to the top of a loop should be emitted as continues, to that loop's main label + std::unordered_set<Name> continueLabels; + + IString fromName(Name name, NameScope scope) { + return parent->fromName(name, scope); + } + + // Visitors + + Ref visitBlock(Block* curr) { + breakResults[curr->name] = result; + Ref ret = ValueBuilder::makeBlock(); + size_t size = curr->list.size(); + auto noResults = result == NO_RESULT ? size : size-1; + for (size_t i = 0; i < noResults; i++) { + flattenAppend(ret, ValueBuilder::makeStatement(visit(curr->list[i], NO_RESULT))); + } + if (result != NO_RESULT) { + flattenAppend(ret, visitAndAssign(curr->list[size-1], result)); + } + if (curr->name.is()) { + ret = ValueBuilder::makeLabel(fromName(curr->name, NameScope::Label), ret); + } + return ret; + } + + Ref visitIf(If* curr) { + IString temp; + Ref condition = visitForExpression(curr->condition, i32, temp); + Ref ifTrue = ValueBuilder::makeStatement(visitAndAssign(curr->ifTrue, result)); + Ref ifFalse; + if (curr->ifFalse) { + ifFalse = ValueBuilder::makeStatement(visitAndAssign(curr->ifFalse, result)); + } + if (temp.isNull()) { + return ValueBuilder::makeIf(condition, ifTrue, ifFalse); // simple if + } + condition = blockify(condition); + // just add an if to the block + condition[1]->push_back(ValueBuilder::makeIf(ValueBuilder::makeName(temp), ifTrue, ifFalse)); + return condition; + } + + Ref visitLoop(Loop* curr) { + Name asmLabel = curr->name; + continueLabels.insert(asmLabel); + Ref body = blockify(visit(curr->body, result)); + flattenAppend(body, ValueBuilder::makeBreak(fromName(asmLabel, NameScope::Label))); + Ref ret = ValueBuilder::makeDo(body, ValueBuilder::makeInt(1)); + return ValueBuilder::makeLabel(fromName(asmLabel, NameScope::Label), ret); + } + + Ref visitBreak(Break* curr) { + if (curr->condition) { + // we need an equivalent to an if here, so use that code + Break fakeBreak = *curr; + fakeBreak.condition = nullptr; + If fakeIf(allocator); + fakeIf.condition = curr->condition; + fakeIf.ifTrue = &fakeBreak; + return visit(&fakeIf, result); + } + Ref theBreak; + auto iter = continueLabels.find(curr->name); + if (iter == continueLabels.end()) { + theBreak = ValueBuilder::makeBreak(fromName(curr->name, NameScope::Label)); + } else { + theBreak = ValueBuilder::makeContinue(fromName(curr->name, NameScope::Label)); + } + if (!curr->value) return theBreak; + // generate the value, including assigning to the result, and then do the break + Ref ret = visitAndAssign(curr->value, breakResults[curr->name]); + ret = blockify(ret); + ret[1]->push_back(theBreak); + return ret; + } + + Expression* defaultBody = nullptr; // default must be last in asm.js + + Ref visitSwitch(Switch* curr) { + assert(!curr->value); + Ref ret = ValueBuilder::makeBlock(); + Ref condition; + if (isStatement(curr->condition)) { + ScopedTemp temp(i32, parent, func); + flattenAppend(ret[2], visit(curr->condition, temp)); + condition = temp.getAstName(); + } else { + condition = visit(curr->condition, EXPRESSION_RESULT); + } + Ref theSwitch = + ValueBuilder::makeSwitch(makeAsmCoercion(condition, ASM_INT)); + ret[1]->push_back(theSwitch); + for (size_t i = 0; i < curr->targets.size(); i++) { + ValueBuilder::appendCaseToSwitch(theSwitch, ValueBuilder::makeNum(i)); + ValueBuilder::appendCodeToSwitch(theSwitch, blockify(ValueBuilder::makeBreak(fromName(curr->targets[i], NameScope::Label))), false); + } + ValueBuilder::appendDefaultToSwitch(theSwitch); + ValueBuilder::appendCodeToSwitch(theSwitch, blockify(ValueBuilder::makeBreak(fromName(curr->default_, NameScope::Label))), false); + return ret; + } + + Ref makeStatementizedCall(ExpressionList& operands, + Ref ret, + std::function<Ref()> genTheCall, + IString result, + Type type) { + std::vector<ScopedTemp*> temps; // TODO: utility class, with destructor? + for (auto& operand : operands) { + temps.push_back(new ScopedTemp(operand->type, parent, func)); + IString temp = temps.back()->temp; + flattenAppend(ret, visitAndAssign(operand, temp)); + } + Ref theCall = genTheCall(); + for (size_t i = 0; i < temps.size(); i++) { + IString temp = temps[i]->temp; + auto &operand = operands[i]; + theCall[2]->push_back(makeAsmCoercion(ValueBuilder::makeName(temp), wasmToAsmType(operand->type))); + } + theCall = makeAsmCoercion(theCall, wasmToAsmType(type)); + if (result != NO_RESULT) { + theCall = ValueBuilder::makeStatement( + ValueBuilder::makeBinary( + ValueBuilder::makeName(result), SET, theCall)); + } + flattenAppend(ret, theCall); + for (auto temp : temps) { + delete temp; + } + return ret; + } + + Ref visitGenericCall(Expression* curr, Name target, + ExpressionList& operands) { + Ref theCall = ValueBuilder::makeCall(fromName(target, NameScope::Top)); + if (!isStatement(curr)) { + // none of our operands is a statement; go right ahead and create a + // simple expression + for (auto operand : operands) { + theCall[2]->push_back( + makeAsmCoercion(visit(operand, EXPRESSION_RESULT), + wasmToAsmType(operand->type))); + } + return makeAsmCoercion(theCall, wasmToAsmType(curr->type)); + } + // we must statementize them all + return makeStatementizedCall(operands, ValueBuilder::makeBlock(), + [&]() { return theCall; }, + result, curr->type); + } + + Ref visitCall(Call* curr) { + return visitGenericCall(curr, curr->target, curr->operands); + } + + Ref visitCallImport(CallImport* curr) { + return visitGenericCall(curr, curr->target, curr->operands); + } + + Ref visitCallIndirect(CallIndirect* curr) { + // TODO: the codegen here is a pessimization of what the ideal codegen + // looks like. Eventually if necessary this should be tightened up in the + // case that the argument expression don't have any side effects. + assert(isStatement(curr)); + std::string stable = std::string("FUNCTION_TABLE_") + + getSig(module->getFunctionType(curr->fullType)); + IString table = IString(stable.c_str(), false); + Ref ret = ValueBuilder::makeBlock(); + ScopedTemp idx(i32, parent, func); + return makeStatementizedCall( + curr->operands, + ret, + [&]() { + flattenAppend(ret, visitAndAssign(curr->target, idx)); + return ValueBuilder::makeCall(ValueBuilder::makeSub( + ValueBuilder::makeName(table), + ValueBuilder::makeBinary(idx.getAstName(), AND, ValueBuilder::makeInt(parent->getTableSize()-1)) + )); + }, + result, + curr->type + ); + } + + Ref makeSetVar(Expression* curr, Expression* value, Name name, NameScope scope) { + if (!isStatement(curr)) { + return ValueBuilder::makeBinary( + ValueBuilder::makeName(fromName(name, scope)), SET, + visit(value, EXPRESSION_RESULT) + ); + } + // if result was provided, our child can just assign there. + // Otherwise, allocate a temp for it to assign to. + ScopedTemp temp(value->type, parent, func, result); + Ref ret = blockify(visit(value, temp)); + // the output was assigned to result, so we can just assign it to our target + ret[1]->push_back( + ValueBuilder::makeStatement( + ValueBuilder::makeBinary( + ValueBuilder::makeName(fromName(name, scope)), SET, + temp.getAstName() + ) + ) + ); + return ret; + } + + Ref visitGetLocal(GetLocal* curr) { + return ValueBuilder::makeName( + fromName(func->getLocalNameOrGeneric(curr->index), NameScope::Local) + ); + } + + Ref visitSetLocal(SetLocal* curr) { + return makeSetVar( + curr, + curr->value, + func->getLocalNameOrGeneric(curr->index), + NameScope::Local + ); + } + + Ref visitGetGlobal(GetGlobal* curr) { + return ValueBuilder::makeName(fromName(curr->name, NameScope::Top)); + } + + Ref visitSetGlobal(SetGlobal* curr) { + return makeSetVar(curr, curr->value, curr->name, NameScope::Top); + } + + Ref visitLoad(Load* curr) { + if (isStatement(curr)) { + ScopedTemp temp(i32, parent, func); + GetLocal fakeLocal(allocator); + fakeLocal.index = func->getLocalIndex(temp.getName()); + Load fakeLoad = *curr; + fakeLoad.ptr = &fakeLocal; + Ref ret = blockify(visitAndAssign(curr->ptr, temp)); + flattenAppend(ret, visitAndAssign(&fakeLoad, result)); + return ret; + } + if (curr->align != 0 && curr->align < curr->bytes) { + // set the pointer to a local + ScopedTemp temp(i32, parent, func); + SetLocal set(allocator); + set.index = func->getLocalIndex(temp.getName()); + set.value = curr->ptr; + Ref ptrSet = visit(&set, NO_RESULT); + GetLocal get(allocator); + get.index = func->getLocalIndex(temp.getName()); + // fake loads + Load load = *curr; + load.ptr = &get; + load.bytes = 1; // do the worst + Ref rest; + switch (curr->type) { + case i32: { + rest = makeAsmCoercion(visit(&load, EXPRESSION_RESULT), ASM_INT); + for (size_t i = 1; i < curr->bytes; i++) { + ++load.offset; + Ref add = makeAsmCoercion(visit(&load, EXPRESSION_RESULT), ASM_INT); + add = ValueBuilder::makeBinary(add, LSHIFT, ValueBuilder::makeNum(8*i)); + rest = ValueBuilder::makeBinary(rest, OR, add); + } + break; + } + default: { + std::cerr << "Unhandled type in load: " << curr->type << std::endl; + abort(); + } + } + return ValueBuilder::makeSeq(ptrSet, rest); + } + // normal load + Ref ptr = visit(curr->ptr, EXPRESSION_RESULT); + if (curr->offset) { + ptr = makeAsmCoercion( + ValueBuilder::makeBinary(ptr, PLUS, ValueBuilder::makeNum(curr->offset)), + ASM_INT); + } + Ref ret; + switch (curr->type) { + case i32: { + switch (curr->bytes) { + case 1: + ret = ValueBuilder::makeSub( + ValueBuilder::makeName(curr->signed_ ? HEAP8 : HEAPU8 ), + ValueBuilder::makePtrShift(ptr, 0)); + break; + case 2: + ret = ValueBuilder::makeSub( + ValueBuilder::makeName(curr->signed_ ? HEAP16 : HEAPU16), + ValueBuilder::makePtrShift(ptr, 1)); + break; + case 4: + ret = ValueBuilder::makeSub( + ValueBuilder::makeName(curr->signed_ ? HEAP32 : HEAPU32), + ValueBuilder::makePtrShift(ptr, 2)); + break; + default: { + std::cerr << "Unhandled number of bytes in i32 load: " + << curr->bytes << std::endl; + abort(); + } + } + break; + } + case f32: + ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAPF32), + ValueBuilder::makePtrShift(ptr, 2)); + break; + case f64: + ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAPF64), + ValueBuilder::makePtrShift(ptr, 3)); + break; + default: { + std::cerr << "Unhandled type in load: " << curr->type << std::endl; + abort(); + } + } + return makeAsmCoercion(ret, wasmToAsmType(curr->type)); + } + + Ref visitStore(Store* curr) { + if (isStatement(curr)) { + ScopedTemp tempPtr(i32, parent, func); + ScopedTemp tempValue(curr->valueType, parent, func); + GetLocal fakeLocalPtr(allocator); + fakeLocalPtr.index = func->getLocalIndex(tempPtr.getName()); + fakeLocalPtr.type = curr->ptr->type; + GetLocal fakeLocalValue(allocator); + fakeLocalValue.index = func->getLocalIndex(tempValue.getName()); + fakeLocalValue.type = curr->value->type; + Store fakeStore = *curr; + fakeStore.ptr = &fakeLocalPtr; + fakeStore.value = &fakeLocalValue; + Ref ret = blockify(visitAndAssign(curr->ptr, tempPtr)); + flattenAppend(ret, visitAndAssign(curr->value, tempValue)); + flattenAppend(ret, visitAndAssign(&fakeStore, result)); + return ret; + } + if (curr->align != 0 && curr->align < curr->bytes) { + // set the pointer to a local + ScopedTemp temp(i32, parent, func); + SetLocal set(allocator); + set.index = func->getLocalIndex(temp.getName()); + set.value = curr->ptr; + Ref ptrSet = visit(&set, NO_RESULT); + GetLocal get(allocator); + get.index = func->getLocalIndex(temp.getName()); + // set the value to a local + ScopedTemp tempValue(curr->value->type, parent, func); + SetLocal setValue(allocator); + setValue.index = func->getLocalIndex(tempValue.getName()); + setValue.value = curr->value; + Ref valueSet = visit(&setValue, NO_RESULT); + GetLocal getValue(allocator); + getValue.index = func->getLocalIndex(tempValue.getName()); + // fake stores + Store store = *curr; + store.ptr = &get; + store.bytes = 1; // do the worst + Ref rest; + switch (curr->valueType) { + case i32: { + Const _255(allocator); + _255.value = Literal(int32_t(255)); + _255.type = i32; + for (size_t i = 0; i < curr->bytes; i++) { + Const shift(allocator); + shift.value = Literal(int32_t(8*i)); + shift.type = i32; + Binary shifted(allocator); + shifted.op = ShrUInt32; + shifted.left = &getValue; + shifted.right = &shift; + shifted.type = i32; + Binary anded(allocator); + anded.op = AndInt32; + anded.left = i > 0 ? static_cast<Expression*>(&shifted) : static_cast<Expression*>(&getValue); + anded.right = &_255; + anded.type = i32; + store.value = &anded; + Ref part = visit(&store, NO_RESULT); + if (i == 0) { + rest = part; + } else { + rest = ValueBuilder::makeSeq(rest, part); + } + ++store.offset; + } + break; + } + default: { + std::cerr << "Unhandled type in store: " << curr->valueType + << std::endl; + abort(); + } + } + return ValueBuilder::makeSeq(ValueBuilder::makeSeq(ptrSet, valueSet), rest); + } + // normal store + Ref ptr = visit(curr->ptr, EXPRESSION_RESULT); + if (curr->offset) { + ptr = makeAsmCoercion(ValueBuilder::makeBinary(ptr, PLUS, ValueBuilder::makeNum(curr->offset)), ASM_INT); + } + Ref value = visit(curr->value, EXPRESSION_RESULT); + Ref ret; + switch (curr->valueType) { + case i32: { + switch (curr->bytes) { + case 1: ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAP8), ValueBuilder::makePtrShift(ptr, 0)); break; + case 2: ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAP16), ValueBuilder::makePtrShift(ptr, 1)); break; + case 4: ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAP32), ValueBuilder::makePtrShift(ptr, 2)); break; + default: abort(); + } + break; + } + case f32: ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAPF32), ValueBuilder::makePtrShift(ptr, 2)); break; + case f64: ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAPF64), ValueBuilder::makePtrShift(ptr, 3)); break; + default: { + std::cerr << "Unhandled type in store: " << curr->valueType + << std::endl; + abort(); + } + } + return ValueBuilder::makeBinary(ret, SET, value); + } + + Ref visitDrop(Drop* curr) { + assert(!isStatement(curr)); + return visitAndAssign(curr->value, 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: { + auto lo = (unsigned) curr->value.geti64(); + auto 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); + fake.value = Literal(double(curr->value.getf32())); + fake.type = f64; + ret[2]->push_back(visitConst(&fake)); + return ret; + } + case f64: { + double d = curr->value.getf64(); + if (d == 0 && std::signbit(d)) { // negative zero + return ValueBuilder::makeUnary(PLUS, ValueBuilder::makeUnary(MINUS, ValueBuilder::makeDouble(0))); + } + return ValueBuilder::makeUnary(PLUS, ValueBuilder::makeDouble(curr->value.getf64())); + } + default: abort(); + } + } + + Ref visitUnary(Unary* curr) { + if (isStatement(curr)) { + ScopedTemp temp(curr->value->type, parent, func); + GetLocal fakeLocal(allocator); + fakeLocal.index = func->getLocalIndex(temp.getName()); + Unary fakeUnary = *curr; + fakeUnary.value = &fakeLocal; + Ref ret = blockify(visitAndAssign(curr->value, temp)); + flattenAppend(ret, visitAndAssign(&fakeUnary, result)); + return ret; + } + // normal unary + switch (curr->type) { + case i32: { + switch (curr->op) { + case ClzInt32: + return ValueBuilder::makeCall( + MATH_CLZ32, + visit(curr->value, EXPRESSION_RESULT) + ); + case CtzInt32: + case PopcntInt32: + std::cerr << "i32 unary should have been removed: " << curr + << std::endl; + WASM_UNREACHABLE(); + case EqZInt32: + return ValueBuilder::makeBinary( + makeAsmCoercion(visit(curr->value, + EXPRESSION_RESULT), ASM_INT), EQ, + makeAsmCoercion(ValueBuilder::makeInt(0), ASM_INT)); + case ReinterpretFloat32: { + // Naively assume that the address 0 and the next 4 bytes are + // permanently unused by the source program, which is definitely + // true for languages like C/C++/Rust + Ref zero = ValueBuilder::makeInt(0); + Ref ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAPF32), zero); + Ref value = visit(curr->value, EXPRESSION_RESULT); + Ref store = ValueBuilder::makeBinary(ret, SET, value); + return ValueBuilder::makeSeq( + store, + makeAsmCoercion( + ValueBuilder::makeSub(ValueBuilder::makeName(HEAP32), zero), + ASM_INT + ) + ); + } + // 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; + abort(); + } + } + } + case f32: + case f64: { + Ref ret; + switch (curr->op) { + case NegFloat32: + case NegFloat64: + ret = ValueBuilder::makeUnary( + MINUS, + visit(curr->value, EXPRESSION_RESULT) + ); + break; + case AbsFloat32: + case AbsFloat64: + ret = ValueBuilder::makeCall( + MATH_ABS, + visit(curr->value, EXPRESSION_RESULT) + ); + break; + case CeilFloat32: + case CeilFloat64: + ret = ValueBuilder::makeCall( + MATH_CEIL, + visit(curr->value, EXPRESSION_RESULT) + ); + break; + case FloorFloat32: + case FloorFloat64: + ret = ValueBuilder::makeCall( + MATH_FLOOR, + visit(curr->value, EXPRESSION_RESULT) + ); + break; + case SqrtFloat32: + case SqrtFloat64: + ret = ValueBuilder::makeCall( + MATH_SQRT, + visit(curr->value, EXPRESSION_RESULT) + ); + break; + case PromoteFloat32: + return makeAsmCoercion(visit(curr->value, EXPRESSION_RESULT), + ASM_DOUBLE); + case DemoteFloat64: + return makeAsmCoercion(visit(curr->value, EXPRESSION_RESULT), + ASM_FLOAT); + case ReinterpretInt32: { + // Like above, assume address 0 is unused. + Ref zero = ValueBuilder::makeInt(0); + Ref ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAP32), zero); + Ref value = visit(curr->value, EXPRESSION_RESULT); + Ref store = ValueBuilder::makeBinary(ret, SET, value); + return ValueBuilder::makeSeq( + store, + 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 + case NearestFloat32: + case NearestFloat64: + case TruncFloat32: + case TruncFloat64: + std::cerr << "operation should have been removed in previous passes" + << std::endl; + WASM_UNREACHABLE(); + + default: + std::cerr << "Unhandled unary float operator: " << curr + << std::endl; + abort(); + } + if (curr->type == f32) { // doubles need much less coercing + return makeAsmCoercion(ret, ASM_FLOAT); + } + return ret; + } + default: { + std::cerr << "Unhandled type in unary: " << curr << std::endl; + abort(); + } + } + } + + Ref visitBinary(Binary* curr) { + if (isStatement(curr)) { + ScopedTemp tempLeft(curr->left->type, parent, func); + GetLocal fakeLocalLeft(allocator); + fakeLocalLeft.index = func->getLocalIndex(tempLeft.getName()); + ScopedTemp tempRight(curr->right->type, parent, func); + GetLocal fakeLocalRight(allocator); + fakeLocalRight.index = func->getLocalIndex(tempRight.getName()); + Binary fakeBinary = *curr; + fakeBinary.left = &fakeLocalLeft; + fakeBinary.right = &fakeLocalRight; + Ref ret = blockify(visitAndAssign(curr->left, tempLeft)); + flattenAppend(ret, visitAndAssign(curr->right, tempRight)); + flattenAppend(ret, visitAndAssign(&fakeBinary, result)); + return ret; + } + // normal binary + Ref left = visit(curr->left, EXPRESSION_RESULT); + Ref right = visit(curr->right, EXPRESSION_RESULT); + Ref ret; + switch (curr->type) { + case i32: { + switch (curr->op) { + case AddInt32: + ret = ValueBuilder::makeBinary(left, PLUS, right); + break; + case SubInt32: + ret = ValueBuilder::makeBinary(left, MINUS, right); + break; + case MulInt32: { + if (curr->type == i32) { + // TODO: when one operand is a small int, emit a multiply + return ValueBuilder::makeCall(MATH_IMUL, left, right); + } else { + return ValueBuilder::makeBinary(left, MUL, right); + } + } + case DivSInt32: + ret = ValueBuilder::makeBinary(makeSigning(left, ASM_SIGNED), DIV, + makeSigning(right, ASM_SIGNED)); + break; + case DivUInt32: + ret = ValueBuilder::makeBinary(makeSigning(left, ASM_UNSIGNED), DIV, + makeSigning(right, ASM_UNSIGNED)); + break; + case RemSInt32: + ret = ValueBuilder::makeBinary(makeSigning(left, ASM_SIGNED), MOD, + makeSigning(right, ASM_SIGNED)); + break; + case RemUInt32: + ret = ValueBuilder::makeBinary(makeSigning(left, ASM_UNSIGNED), MOD, + makeSigning(right, ASM_UNSIGNED)); + break; + case AndInt32: + ret = ValueBuilder::makeBinary(left, AND, right); + break; + case OrInt32: + ret = ValueBuilder::makeBinary(left, OR, right); + break; + case XorInt32: + ret = ValueBuilder::makeBinary(left, XOR, right); + break; + case ShlInt32: + ret = ValueBuilder::makeBinary(left, LSHIFT, right); + break; + case ShrUInt32: + ret = ValueBuilder::makeBinary(left, TRSHIFT, right); + break; + case ShrSInt32: + ret = ValueBuilder::makeBinary(left, RSHIFT, right); + break; + case EqInt32: { + // TODO: check if this condition is still valid/necessary + if (curr->left->type == i32) { + return ValueBuilder::makeBinary(makeSigning(left, ASM_SIGNED), EQ, + makeSigning(right, ASM_SIGNED)); + } else { + return ValueBuilder::makeBinary(left, EQ, right); + } + } + case NeInt32: { + if (curr->left->type == i32) { + return ValueBuilder::makeBinary(makeSigning(left, ASM_SIGNED), NE, + makeSigning(right, ASM_SIGNED)); + } else { + return ValueBuilder::makeBinary(left, NE, right); + } + } + case LtSInt32: + return ValueBuilder::makeBinary(makeSigning(left, ASM_SIGNED), LT, + makeSigning(right, ASM_SIGNED)); + case LtUInt32: + return ValueBuilder::makeBinary(makeSigning(left, ASM_UNSIGNED), LT, + makeSigning(right, ASM_UNSIGNED)); + case LeSInt32: + return ValueBuilder::makeBinary(makeSigning(left, ASM_SIGNED), LE, + makeSigning(right, ASM_SIGNED)); + case LeUInt32: + return ValueBuilder::makeBinary(makeSigning(left, ASM_UNSIGNED), LE, + makeSigning(right, ASM_UNSIGNED)); + case GtSInt32: + return ValueBuilder::makeBinary(makeSigning(left, ASM_SIGNED), GT, + makeSigning(right, ASM_SIGNED)); + case GtUInt32: + return ValueBuilder::makeBinary(makeSigning(left, ASM_UNSIGNED), GT, + makeSigning(right, ASM_UNSIGNED)); + case GeSInt32: + return ValueBuilder::makeBinary(makeSigning(left, ASM_SIGNED), GE, + makeSigning(right, ASM_SIGNED)); + case GeUInt32: + return ValueBuilder::makeBinary(makeSigning(left, ASM_UNSIGNED), GE, + makeSigning(right, ASM_UNSIGNED)); + case EqFloat32: + case EqFloat64: + return ValueBuilder::makeBinary(left, EQ, right); + case NeFloat32: + case NeFloat64: + return ValueBuilder::makeBinary(left, NE, right); + case GeFloat32: + case GeFloat64: + return ValueBuilder::makeBinary(left, GE, right); + case GtFloat32: + case GtFloat64: + return ValueBuilder::makeBinary(left, GT, right); + case LeFloat32: + case LeFloat64: + return ValueBuilder::makeBinary(left, LE, right); + case LtFloat32: + case LtFloat64: + return ValueBuilder::makeBinary(left, LT, right); + case RotLInt32: + case RotRInt32: + std::cerr << "should be removed already" << std::endl; + WASM_UNREACHABLE(); + default: { + std::cerr << "Unhandled i32 binary operator: " << curr << std::endl; + abort(); + } + } + break; + } + case f32: + case f64: + switch (curr->op) { + case AddFloat32: + case AddFloat64: + ret = ValueBuilder::makeBinary(left, PLUS, right); + break; + case SubFloat32: + case SubFloat64: + ret = ValueBuilder::makeBinary(left, MINUS, right); + break; + case MulFloat32: + case MulFloat64: + ret = ValueBuilder::makeBinary(left, MUL, right); + break; + case DivFloat32: + case DivFloat64: + ret = ValueBuilder::makeBinary(left, DIV, right); + break; + case MinFloat32: + case MinFloat64: + ret = ValueBuilder::makeCall(MATH_MIN, left, right); + break; + case MaxFloat32: + case MaxFloat64: + ret = ValueBuilder::makeCall(MATH_MAX, left, right); + break; + case CopySignFloat32: + case CopySignFloat64: + default: + std::cerr << "Unhandled binary float operator: " << curr << std::endl; + abort(); + } + if (curr->type == f32) { + return makeAsmCoercion(ret, ASM_FLOAT); + } + return ret; + default: + std::cerr << "Unhandled type in binary: " << curr << std::endl; + abort(); + } + return makeAsmCoercion(ret, wasmToAsmType(curr->type)); + } + + Ref visitSelect(Select* curr) { + if (isStatement(curr)) { + ScopedTemp tempIfTrue(curr->ifTrue->type, parent, func); + GetLocal fakeLocalIfTrue(allocator); + fakeLocalIfTrue.index = func->getLocalIndex(tempIfTrue.getName()); + ScopedTemp tempIfFalse(curr->ifFalse->type, parent, func); + GetLocal fakeLocalIfFalse(allocator); + fakeLocalIfFalse.index = func->getLocalIndex(tempIfFalse.getName()); + ScopedTemp tempCondition(i32, parent, func); + GetLocal fakeCondition(allocator); + fakeCondition.index = func->getLocalIndex(tempCondition.getName()); + Select fakeSelect = *curr; + fakeSelect.ifTrue = &fakeLocalIfTrue; + fakeSelect.ifFalse = &fakeLocalIfFalse; + fakeSelect.condition = &fakeCondition; + Ref ret = blockify(visitAndAssign(curr->ifTrue, tempIfTrue)); + flattenAppend(ret, visitAndAssign(curr->ifFalse, tempIfFalse)); + flattenAppend(ret, visitAndAssign(curr->condition, tempCondition)); + flattenAppend(ret, visitAndAssign(&fakeSelect, result)); + return ret; + } + // normal select + Ref ifTrue = visit(curr->ifTrue, EXPRESSION_RESULT); + Ref ifFalse = visit(curr->ifFalse, EXPRESSION_RESULT); + Ref condition = visit(curr->condition, EXPRESSION_RESULT); + ScopedTemp tempIfTrue(curr->type, parent, func), + tempIfFalse(curr->type, parent, func), + tempCondition(i32, parent, func); + return + ValueBuilder::makeSeq( + ValueBuilder::makeBinary(tempIfTrue.getAstName(), SET, ifTrue), + ValueBuilder::makeSeq( + ValueBuilder::makeBinary(tempIfFalse.getAstName(), SET, ifFalse), + ValueBuilder::makeSeq( + ValueBuilder::makeBinary(tempCondition.getAstName(), SET, condition), + ValueBuilder::makeConditional( + tempCondition.getAstName(), + tempIfTrue.getAstName(), + tempIfFalse.getAstName() + ) + ) + ) + ); + } + + Ref visitReturn(Return* curr) { + if (curr->value == nullptr) { + return ValueBuilder::makeReturn(Ref()); + } + if (isStatement(curr->value)) { + ScopedTemp temp(curr->value->type, parent, func); + Ref ret = ValueBuilder::makeBlock(); + flattenAppend(ret, visitAndAssign(curr->value, temp)); + Ref coerced = makeAsmCoercion( + temp.getAstName(), + wasmToAsmType(curr->value->type) + ); + flattenAppend(ret, ValueBuilder::makeReturn(coerced)); + return ret; + } else { + Ref val = makeAsmCoercion( + visit(curr->value, NO_RESULT), + wasmToAsmType(curr->value->type) + ); + return ValueBuilder::makeReturn(val); + } + } + + Ref visitHost(Host* curr) { + Ref call; + if (curr->op == HostOp::GrowMemory) { + parent->setNeedsAlmostASM("grow_memory op"); + call = ValueBuilder::makeCall(WASM_GROW_MEMORY, + makeAsmCoercion( + visit(curr->operands[0], EXPRESSION_RESULT), + wasmToAsmType(curr->operands[0]->type))); + } else if (curr->op == HostOp::CurrentMemory) { + parent->setNeedsAlmostASM("current_memory op"); + call = ValueBuilder::makeCall(WASM_CURRENT_MEMORY); + } else { + return ValueBuilder::makeCall(ABORT_FUNC); + } + if (isStatement(curr)) { + return ValueBuilder::makeBinary(ValueBuilder::makeName(result), SET, call); + } else { + return call; + } + } + + Ref visitNop(Nop* curr) { + return ValueBuilder::makeToplevel(); + } + + Ref visitUnreachable(Unreachable* curr) { + return ValueBuilder::makeCall(ABORT_FUNC); + } + }; + return ExpressionProcessor(this, m, func).visit(func->body, result); +} + +static void makeHelpers(Ref ret, Name funcName, Name moduleName, bool first) { + if (first) { + // TODO: nan and infinity shouldn't be needed once literal asm.js code isn't + // generated + flattenAppend(ret, ValueBuilder::makeName(R"( + var nan = NaN; + var infinity = Infinity; + )")); + + // 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; + } + + function f64Equal(a, b) { + var i = new Int32Array(2); + var f = new Float64Array(i.buffer); + f[0] = a; + var ai1 = i[0]; + var ai2 = i[1]; + f[0] = b; + var bi1 = i[0]; + var bi2 = i[1]; + + return (isNaN(a) && isNaN(b)) || (ai1 == bi1 && ai2 == bi2); + } + + function i64Equal(actual_lo, actual_hi, expected_lo, expected_hi) { + return actual_lo == (expected_lo | 0) && actual_hi == (expected_hi | 0); + } + )")); + } +} + +static void prefixCalls(Ref asmjs, Name asmModule) { + if (asmjs->isArray()) { + ArrayStorage& arr = asmjs->getArray(); + for (Ref& r : arr) { + prefixCalls(r, asmModule); + } + if (arr.size() > 0 && arr[0]->isString() && arr[0]->getIString() == CALL) { + assert(arr.size() >= 2); + if (arr[1]->getIString() == "f32Equal" || + arr[1]->getIString() == "f64Equal" || + arr[1]->getIString() == "i64Equal" || + arr[1]->getIString() == "isNaN") { + // ... + } else if (arr[1]->getIString() == "Math_fround") { + arr[1]->setString("Math.fround"); + } else { + Ref prefixed = ValueBuilder::makeDot(ValueBuilder::makeName(asmModule), + arr[1]->getIString()); + arr[1]->setArray(prefixed->getArray()); + } + } + } + + if (asmjs->isAssign()) { + prefixCalls(asmjs->asAssign()->target(), asmModule); + prefixCalls(asmjs->asAssign()->value(), asmModule); + } + if (asmjs->isAssignName()) { + prefixCalls(asmjs->asAssignName()->value(), asmModule); + } +} + +Ref Wasm2JSBuilder::makeAssertReturnFunc(SExpressionWasmBuilder& sexpBuilder, + Module* wasm, + Builder& wasmBuilder, + Element& e, + Name testFuncName, + Name asmModule) { + Expression* actual = sexpBuilder.parseExpression(e[1]); + Expression* body = nullptr; + if (e.size() == 2) { + if (actual->type == none) { + body = wasmBuilder.blockify( + actual, + wasmBuilder.makeConst(Literal(uint32_t(1))) + ); + } else { + body = actual; + } + } else if (e.size() == 3) { + Expression* expected = sexpBuilder.parseExpression(e[2]); + Type resType = expected->type; + actual->type = resType; + switch (resType) { + case i32: + body = wasmBuilder.makeBinary(EqInt32, actual, expected); + break; + + case i64: + body = wasmBuilder.makeCall( + "i64Equal", + {actual, wasmBuilder.makeCall(WASM_FETCH_HIGH_BITS, {}, i32), 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(); + } + } + } else { + assert(false && "Unexpected number of parameters in assert_return"); + } + std::unique_ptr<Function> testFunc( + wasmBuilder.makeFunction( + testFuncName, + std::vector<NameType>{}, + body->type, + std::vector<NameType>{}, + body + ) + ); + Ref jsFunc = processFunction(wasm, testFunc.get()); + prefixCalls(jsFunc, asmModule); + return jsFunc; +} + +Ref Wasm2JSBuilder::makeAssertReturnNanFunc(SExpressionWasmBuilder& sexpBuilder, + Module* wasm, + Builder& wasmBuilder, + Element& e, + Name testFuncName, + Name asmModule) { + 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(wasm, testFunc.get()); + prefixCalls(jsFunc, asmModule); + return jsFunc; +} + +Ref Wasm2JSBuilder::makeAssertTrapFunc(SExpressionWasmBuilder& sexpBuilder, + Module* wasm, + Builder& wasmBuilder, + Element& e, + Name testFuncName, + Name asmModule) { + Name innerFuncName("f"); + Expression* expr = sexpBuilder.parseExpression(e[1]); + std::unique_ptr<Function> exprFunc( + wasmBuilder.makeFunction(innerFuncName, + std::vector<NameType>{}, + expr->type, + std::vector<NameType>{}, + expr) + ); + IString expectedErr = e[2]->str(); + Ref innerFunc = processFunction(wasm, exprFunc.get()); + prefixCalls(innerFunc, asmModule); + Ref outerFunc = ValueBuilder::makeFunction(testFuncName); + outerFunc[3]->push_back(innerFunc); + Ref tryBlock = ValueBuilder::makeBlock(); + ValueBuilder::appendToBlock(tryBlock, ValueBuilder::makeCall(innerFuncName)); + Ref catchBlock = ValueBuilder::makeBlock(); + ValueBuilder::appendToBlock( + catchBlock, + ValueBuilder::makeReturn( + ValueBuilder::makeCall( + ValueBuilder::makeDot( + ValueBuilder::makeName(IString("e")), + ValueBuilder::makeName(IString("message")), + ValueBuilder::makeName(IString("includes")) + ), + ValueBuilder::makeString(expectedErr) + ) + ) + ); + outerFunc[3]->push_back(ValueBuilder::makeTry( + tryBlock, + ValueBuilder::makeName((IString("e"))), + catchBlock)); + outerFunc[3]->push_back(ValueBuilder::makeReturn(ValueBuilder::makeInt(0))); + return outerFunc; +} + +void Wasm2JSBuilder::setNeedsAlmostASM(const char *reason) { + if (!almostASM) { + almostASM = true; + std::cerr << "Switching to \"almost asm\" mode, reason: " << reason << std::endl; + } +} + +void Wasm2JSBuilder::addMemoryGrowthFuncs(Ref ast) { + Ref growMemoryFunc = ValueBuilder::makeFunction(WASM_GROW_MEMORY); + ValueBuilder::appendArgumentToFunction(growMemoryFunc, IString("pagesToAdd")); + + growMemoryFunc[3]->push_back( + ValueBuilder::makeStatement( + ValueBuilder::makeBinary( + ValueBuilder::makeName(IString("pagesToAdd")), SET, + makeAsmCoercion( + ValueBuilder::makeName(IString("pagesToAdd")), + AsmType::ASM_INT + ) + ) + ) + ); + + Ref oldPages = ValueBuilder::makeVar(); + growMemoryFunc[3]->push_back(oldPages); + ValueBuilder::appendToVar( + oldPages, + IString("oldPages"), + makeAsmCoercion(ValueBuilder::makeCall(WASM_CURRENT_MEMORY), AsmType::ASM_INT)); + + Ref newPages = ValueBuilder::makeVar(); + growMemoryFunc[3]->push_back(newPages); + ValueBuilder::appendToVar( + newPages, + IString("newPages"), + makeAsmCoercion(ValueBuilder::makeBinary( + ValueBuilder::makeName(IString("oldPages")), + PLUS, + ValueBuilder::makeName(IString("pagesToAdd")) + ), AsmType::ASM_INT)); + + Ref block = ValueBuilder::makeBlock(); + growMemoryFunc[3]->push_back(ValueBuilder::makeIf( + ValueBuilder::makeBinary( + ValueBuilder::makeBinary( + ValueBuilder::makeName(IString("oldPages")), + LT, + ValueBuilder::makeName(IString("newPages")) + ), + IString("&&"), + ValueBuilder::makeBinary( + ValueBuilder::makeName(IString("newPages")), + LT, + ValueBuilder::makeInt(Memory::kMaxSize) + ) + ), block, NULL)); + + Ref newBuffer = ValueBuilder::makeVar(); + ValueBuilder::appendToBlock(block, newBuffer); + ValueBuilder::appendToVar( + newBuffer, + IString("newBuffer"), + ValueBuilder::makeNew(ValueBuilder::makeCall( + ARRAY_BUFFER, + ValueBuilder::makeCall( + MATH_IMUL, + ValueBuilder::makeName(IString("newPages")), + ValueBuilder::makeInt(Memory::kPageSize))))); + + Ref newHEAP8 = ValueBuilder::makeVar(); + ValueBuilder::appendToBlock(block, newHEAP8); + ValueBuilder::appendToVar( + newHEAP8, + IString("newHEAP8"), + ValueBuilder::makeNew( + ValueBuilder::makeCall( + ValueBuilder::makeDot( + ValueBuilder::makeName(GLOBAL), + INT8ARRAY + ), + ValueBuilder::makeName(IString("newBuffer")) + ) + )); + + ValueBuilder::appendToBlock(block, + ValueBuilder::makeCall( + ValueBuilder::makeDot( + ValueBuilder::makeName(IString("newHEAP8")), + IString("set") + ), + ValueBuilder::makeName(HEAP8) + ) + ); + + ValueBuilder::appendToBlock(block, + ValueBuilder::makeBinary( + ValueBuilder::makeName(HEAP8), + SET, + ValueBuilder::makeName(IString("newHEAP8")) + ) + ); + + auto setHeap = [&](IString name, IString view) { + ValueBuilder::appendToBlock(block, + ValueBuilder::makeBinary( + ValueBuilder::makeName(name), + SET, + ValueBuilder::makeNew( + ValueBuilder::makeCall( + ValueBuilder::makeDot( + ValueBuilder::makeName(GLOBAL), + view + ), + ValueBuilder::makeName(IString("newBuffer")) + ) + ) + ) + ); + }; + + setHeap(HEAP16, INT16ARRAY); + setHeap(HEAP32, INT32ARRAY); + setHeap(HEAPU8, UINT8ARRAY); + setHeap(HEAPU16, UINT16ARRAY); + setHeap(HEAPU32, UINT32ARRAY); + setHeap(HEAPF32, FLOAT32ARRAY); + setHeap(HEAPF64, FLOAT64ARRAY); + + ValueBuilder::appendToBlock(block, + ValueBuilder::makeBinary( + ValueBuilder::makeName(BUFFER), + SET, + ValueBuilder::makeName(IString("newBuffer")) + ) + ); + + growMemoryFunc[3]->push_back( + ValueBuilder::makeReturn( + ValueBuilder::makeName(IString("oldPages")))); + + Ref currentMemoryFunc = ValueBuilder::makeFunction(WASM_CURRENT_MEMORY); + currentMemoryFunc[3]->push_back(ValueBuilder::makeReturn( + makeAsmCoercion( + ValueBuilder::makeBinary( + ValueBuilder::makeDot( + ValueBuilder::makeName(BUFFER), + IString("byteLength") + ), + DIV, + ValueBuilder::makeInt(Memory::kPageSize) + ), + AsmType::ASM_INT + ) + )); + ast->push_back(growMemoryFunc); + ast->push_back(currentMemoryFunc); +} + +bool Wasm2JSBuilder::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"); +} + +Ref Wasm2JSBuilder::processAsserts(Module* wasm, + Element& root, + SExpressionWasmBuilder& sexpBuilder) { + Builder wasmBuilder(sexpBuilder.getAllocator()); + Ref ret = ValueBuilder::makeBlock(); + std::stringstream asmModuleS; + asmModuleS << "ret" << ASM_FUNC.c_str(); + Name asmModule(asmModuleS.str().c_str()); + makeHelpers(ret, ASM_FUNC, asmModule, true); + for (size_t i = 1; i < root.size(); ++i) { + Element& e = *root[i]; + if (e.isList() && e.size() >= 1 && e[0]->isStr() && e[0]->str() == Name("module")) { + std::stringstream funcNameS; + funcNameS << ASM_FUNC.c_str() << i; + std::stringstream moduleNameS; + moduleNameS << "ret" << ASM_FUNC.c_str() << i; + Name funcName(funcNameS.str().c_str()); + asmModule = Name(moduleNameS.str().c_str()); + Module wasm; + SExpressionWasmBuilder builder(wasm, e); + flattenAppend(ret, processWasm(&wasm, funcName)); + makeHelpers(ret, funcName, asmModule, false); + continue; + } + if (!isAssertHandled(e)) { + std::cerr << "skipping " << e << std::endl; + continue; + } + 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); + // Need to claim dollared to get string as function target + testOp[1]->setString(testOp[1]->str(), /*dollared=*/true, false); + + Ref testFunc = isReturn ? + makeAssertReturnFunc(sexpBuilder, wasm, wasmBuilder, e, testFuncName, asmModule) : + (isReturnNan ? + makeAssertReturnNanFunc(sexpBuilder, wasm, wasmBuilder, e, testFuncName, asmModule) : + makeAssertTrapFunc(sexpBuilder, wasm, wasmBuilder, e, testFuncName, asmModule)); + + flattenAppend(ret, testFunc); + std::stringstream failFuncName; + failFuncName << "fail" << std::to_string(i); + IString testName = fromName(testFuncName, NameScope::Top); + flattenAppend( + ret, + ValueBuilder::makeIf( + ValueBuilder::makeUnary(L_NOT, ValueBuilder::makeCall(testName)), + ValueBuilder::makeCall(IString(failFuncName.str().c_str(), false)), + Ref() + ) + ); + } + return ret; +} + + +} // namespace wasm + +#endif // wasm_wasm2js_h |