diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/binaryen-c.cpp | 11 | ||||
-rw-r--r-- | src/support/base64.h | 66 | ||||
-rw-r--r-- | src/tools/wasm2js.cpp | 356 | ||||
-rw-r--r-- | src/wasm2js.h | 664 |
4 files changed, 561 insertions, 536 deletions
diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index 927cae574..2ae3da650 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -2468,13 +2468,16 @@ void BinaryenModulePrintAsmjs(BinaryenModuleRef module) { } Module* wasm = (Module*)module; - Wasm2JSBuilder::Flags builderFlags; - Wasm2JSBuilder wasm2js(builderFlags); + Wasm2JSBuilder::Flags flags; + Wasm2JSBuilder wasm2js(flags); Ref asmjs = wasm2js.processWasm(wasm); JSPrinter jser(true, true, asmjs); + Output out("", Flags::Text, Flags::Release); // stdout + Wasm2JSGlue glue(*wasm, out, flags, "asmFunc"); + glue.emitPre(); jser.printAst(); - - std::cout << jser.buffer; + std::cout << jser.buffer << std::endl; + glue.emitPost(); } int BinaryenModuleValidate(BinaryenModuleRef module) { diff --git a/src/support/base64.h b/src/support/base64.h new file mode 100644 index 000000000..0c87c37c1 --- /dev/null +++ b/src/support/base64.h @@ -0,0 +1,66 @@ +/* + * Copyright 2019 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. + */ + +#ifndef wasm_support_base64_h +#define wasm_support_base64_h + +#include <cassert> +#include <string> +#include <vector> + +inline 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()) { + uint32_t bits = + (((uint32_t)(uint8_t) data[i + 0]) << 16) | + (((uint32_t)(uint8_t) data[i + 1]) << 8) | + (((uint32_t)(uint8_t) 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()) { + uint32_t bits = + (((uint32_t)(uint8_t) data[i + 0]) << 8) | + (((uint32_t)(uint8_t) 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()) { + uint32_t bits = (uint32_t)(uint8_t) data[i + 0]; + ret += alphabet[(bits >> 2) & 0x3f]; + ret += alphabet[(bits << 4) & 0x3f]; + ret += '='; + ret += '='; + } else { + assert(i == data.size()); + } + + return ret; +} + +#endif // wasm_support_base64_h diff --git a/src/tools/wasm2js.cpp b/src/tools/wasm2js.cpp index 6a55c4483..76a724f86 100644 --- a/src/tools/wasm2js.cpp +++ b/src/tools/wasm2js.cpp @@ -28,8 +28,329 @@ using namespace cashew; using namespace wasm; +// helpers + +namespace { + +static void emitWasm(Module& wasm, Output& output, Wasm2JSBuilder::Flags flags, Name name) { + Wasm2JSBuilder wasm2js(flags); + auto js = wasm2js.processWasm(&wasm, name); + Wasm2JSGlue glue(wasm, output, flags, name); + glue.emitPre(); + JSPrinter jser(true, true, js); + jser.printAst(); + output << jser.buffer << std::endl; + glue.emitPost(); +} + +class AssertionEmitter { +public: + AssertionEmitter(Element& root, + SExpressionWasmBuilder& sexpBuilder, + Output& out, + Wasm2JSBuilder::Flags flags) : root(root), sexpBuilder(sexpBuilder), out(out), flags(flags) {} + + void emit(); + +private: + Element& root; + SExpressionWasmBuilder& sexpBuilder; + Output& out; + Wasm2JSBuilder::Flags flags; + + Ref emitAssertReturnFunc(Builder& wasmBuilder, + Element& e, + Name testFuncName, + Name asmModule); + Ref emitAssertReturnNanFunc(Builder& wasmBuilder, + Element& e, + Name testFuncName, + Name asmModule); + Ref emitAssertTrapFunc(Builder& wasmBuilder, + Element& e, + Name testFuncName, + Name asmModule); + bool isAssertHandled(Element& e); + void fixCalls(Ref asmjs, Name asmModule); + + Ref processFunction(Function* func) { + Wasm2JSBuilder sub(flags); + return sub.processFunction(nullptr, func); + } + + void emitFunction(Ref func) { + JSPrinter jser(true, true, func); + jser.printAst(); + out << jser.buffer << std::endl; + } +}; + +Ref AssertionEmitter::emitAssertReturnFunc(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(testFunc.get()); + fixCalls(jsFunc, asmModule); + emitFunction(jsFunc); + return jsFunc; +} + +Ref AssertionEmitter::emitAssertReturnNanFunc(Builder& wasmBuilder, + Element& e, + Name testFuncName, + Name asmModule) { + Expression* actual = sexpBuilder.parseExpression(e[1]); + Expression* body = wasmBuilder.makeCall("isNaN", {actual}, i32); + std::unique_ptr<Function> testFunc( + wasmBuilder.makeFunction( + testFuncName, + std::vector<NameType>{}, + body->type, + std::vector<NameType>{}, + body + ) + ); + Ref jsFunc = processFunction(testFunc.get()); + fixCalls(jsFunc, asmModule); + emitFunction(jsFunc); + return jsFunc; +} + +Ref AssertionEmitter::emitAssertTrapFunc(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(exprFunc.get()); + fixCalls(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))); + emitFunction(outerFunc); + return outerFunc; +} + +bool AssertionEmitter::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"); +} + +void AssertionEmitter::fixCalls(Ref asmjs, Name asmModule) { + if (asmjs->isArray()) { + ArrayStorage& arr = asmjs->getArray(); + for (Ref& r : arr) { + fixCalls(r, asmModule); + } + if (arr.size() > 0 && arr[0]->isString() && arr[0]->getIString() == cashew::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 fixed = ValueBuilder::makeDot(ValueBuilder::makeName(asmModule), + arr[1]->getIString()); + arr[1]->setArray(fixed->getArray()); + } + } + } + + if (asmjs->isAssign()) { + fixCalls(asmjs->asAssign()->target(), asmModule); + fixCalls(asmjs->asAssign()->value(), asmModule); + } + if (asmjs->isAssignName()) { + fixCalls(asmjs->asAssignName()->value(), asmModule); + } +} + +void AssertionEmitter::emit() { + // TODO: nan and infinity shouldn't be needed once literal asm.js code isn't + // generated + out << 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. + out << 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); + } + )"; + + Builder wasmBuilder(sexpBuilder.getAllocator()); + Name asmModule = std::string("ret") + ASM_FUNC.str; + for (size_t i = 0; 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); + emitWasm(wasm, out, flags, funcName); + 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); + + if (isReturn) { + emitAssertReturnFunc(wasmBuilder, e, testFuncName, asmModule); + } else if (isReturnNan) { + emitAssertReturnNanFunc(wasmBuilder, e, testFuncName, asmModule); + } else { + emitAssertTrapFunc(wasmBuilder, e, testFuncName, asmModule); + } + + out << "if (!" + << testFuncName.str + << "()) throw 'assertion failed: " + << e + << "';\n"; + } +} + +} // anonymous namespace + +// Main + int main(int argc, const char *argv[]) { - Wasm2JSBuilder::Flags builderFlags; + Wasm2JSBuilder::Flags flags; ToolOptions options("wasm2js", "Transform .wasm/.wast files to asm.js"); options .add("--output", "-o", "Output file (stdout if not specified)", @@ -41,24 +362,24 @@ int main(int argc, const char *argv[]) { .add("--allow-asserts", "", "Allow compilation of .wast testing asserts", Options::Arguments::Zero, [&](Options* o, const std::string& argument) { - builderFlags.allowAsserts = true; + flags.allowAsserts = true; o->extra["asserts"] = "1"; }) .add("--pedantic", "", "Emulate WebAssembly trapping behavior", Options::Arguments::Zero, [&](Options* o, const std::string& argument) { - builderFlags.pedantic = true; + flags.pedantic = true; }) .add_positional("INFILE", Options::Arguments::One, [](Options *o, const std::string& argument) { o->extra["infile"] = argument; }); options.parse(argc, argv); - if (options.debug) builderFlags.debug = true; + if (options.debug) flags.debug = true; Element* root = nullptr; Module wasm; - Ref asmjs; + Ref js; std::unique_ptr<SExpressionParser> sexprParser; std::unique_ptr<SExpressionWasmBuilder> sexprBuilder; @@ -106,28 +427,13 @@ int main(int argc, const char *argv[]) { } } - if (options.debug) std::cerr << "asming..." << std::endl; - Wasm2JSBuilder wasm2js(builderFlags); - asmjs = wasm2js.processWasm(&wasm); - - if (!binaryInput) { - if (options.extra["asserts"] == "1") { - if (options.debug) std::cerr << "asserting..." << std::endl; - flattenAppend(asmjs, wasm2js.processAsserts(&wasm, *root, *sexprBuilder)); - } - } - - if (options.debug) { - std::cerr << "a-printing..." << std::endl; - asmjs->stringify(std::cout, true); - std::cout << '\n'; - } - if (options.debug) std::cerr << "j-printing..." << std::endl; - JSPrinter jser(true, true, asmjs); - jser.printAst(); Output output(options.extra["output"], Flags::Text, options.debug ? Flags::Debug : Flags::Release); - output << jser.buffer << std::endl; + if (!binaryInput && options.extra["asserts"] == "1") { + AssertionEmitter(*root, *sexprBuilder, output, flags).emit(); + } else { + emitWasm(wasm, output, flags, "asmFunc"); + } if (options.debug) std::cerr << "done." << std::endl; } diff --git a/src/wasm2js.h b/src/wasm2js.h index fde436722..c6970e3e8 100644 --- a/src/wasm2js.h +++ b/src/wasm2js.h @@ -15,8 +15,8 @@ */ // -// WebAssembly-to-asm.js translator. Uses the Emscripten optimizer -// infrastructure. +// WebAssembly-to-JS code translator. Converts wasm functions into +// valid JavaScript (with a somewhat asm.js-ish flavor). // #ifndef wasm_wasm2js_h @@ -40,6 +40,7 @@ #include "ir/names.h" #include "ir/utils.h" #include "passes/passes.h" +#include "support/base64.h" namespace wasm { @@ -87,15 +88,15 @@ static uint64_t constOffset(const T& segment) { } // -// Wasm2JSBuilder - converts a WebAssembly module into asm.js +// Wasm2JSBuilder - converts a WebAssembly module's functions into JS // -// In general, asm.js => wasm is very straightforward, as can +// In general, JS (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 +// fact corrections for imports, etc. However, wasm => 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 +// in places where JS can't handle them, like inside an // a loop's condition check. // // We therefore need the ability to lower an expression into @@ -154,14 +155,12 @@ public: void scanFunctionBody(Expression* curr); // The second pass on an expression: process it fully, generating - // asm.js + // 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; @@ -272,8 +271,6 @@ private: bool almostASM = false; - void addEsmImports(Ref ast, Module* wasm); - void addEsmExportsAndInstantiate(Ref ast, Module* wasm, Name funcName); void addBasics(Ref ast); void addFunctionImport(Ref ast, Function* import); void addGlobalImport(Ref ast, Global* import); @@ -282,25 +279,7 @@ private: 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; @@ -315,7 +294,7 @@ Ref Wasm2JSBuilder::processWasm(Module* wasm, Name funcName) { // 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 + // it to produce correct code. For some more details about this see #1480 runner.add("flatten"); runner.add("i64-to-i32-lowering"); runner.add("flatten"); @@ -335,10 +314,8 @@ Ref Wasm2JSBuilder::processWasm(Module* wasm, Name funcName) { #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); @@ -409,204 +386,6 @@ Ref Wasm2JSBuilder::processWasm(Module* wasm, Name funcName) { return ret; } -void Wasm2JSBuilder::addEsmImports(Ref ast, Module* wasm) { - std::unordered_map<Name, Name> baseModuleMap; - - auto noteImport = [&](Name module, Name base) { - // Right now codegen requires a flat namespace going into the module, - // meaning we don't support importing the same name from multiple namespaces yet. - if (baseModuleMap.count(base) && baseModuleMap[base] != module) { - Fatal() << "the name " << base << " cannot be imported from " - << "two different modules yet\n"; - abort(); - } - baseModuleMap[base] = module; - - std::ostringstream out; - out << "import { " - << base.str - << " } from '" - << module.str - << "'"; - std::string os = out.str(); - Name name(os.c_str()); - flattenAppend(ast, ValueBuilder::makeName(name)); - }; - - ImportInfo imports(*wasm); - - ModuleUtils::iterImportedGlobals(*wasm, [&](Global* import) { - Fatal() << "non-function imports aren't supported yet\n"; - noteImport(import->module, import->base); - }); - ModuleUtils::iterImportedFunctions(*wasm, [&](Function* import) { - noteImport(import->module, import->base); - }); -} - -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()) { - uint32_t bits = - (((uint32_t)(uint8_t) data[i + 0]) << 16) | - (((uint32_t)(uint8_t) data[i + 1]) << 8) | - (((uint32_t)(uint8_t) 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()) { - uint32_t bits = - (((uint32_t)(uint8_t) data[i + 0]) << 8) | - (((uint32_t)(uint8_t) 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()) { - uint32_t bits = (uint32_t)(uint8_t) 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) { - assert(!seg.isPassive && "passive segments not implemented yet"); - 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'); }"; - - ModuleUtils::iterImportedFunctions(*wasm, [&](Function* import) { - 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) { @@ -2050,218 +1829,6 @@ Ref Wasm2JSBuilder::processFunctionBody(Module* m, Function* func, IString resul 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.makeCall("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; @@ -2420,74 +1987,157 @@ void Wasm2JSBuilder::addMemoryGrowthFuncs(Ref ast) { 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"); +// Wasm2JSGlue emits the core of the module - the functions etc. that would +// be the asm.js function in an asm.js world. This class emits the rest of the +// "glue" around that. +class Wasm2JSGlue { +public: + Wasm2JSGlue(Module& wasm, Output& out, Wasm2JSBuilder::Flags flags, Name moduleName) : wasm(wasm), out(out), flags(flags), moduleName(moduleName) {} + + void emitPre(); + void emitPost(); + +private: + Module& wasm; + Output& out; + Wasm2JSBuilder::Flags flags; + Name moduleName; +}; + +void Wasm2JSGlue::emitPre() { + std::unordered_map<Name, Name> baseModuleMap; + + auto noteImport = [&](Name module, Name base) { + // Right now codegen requires a flat namespace going into the module, + // meaning we don't support importing the same name from multiple namespaces yet. + if (baseModuleMap.count(base) && baseModuleMap[base] != module) { + Fatal() << "the name " << base << " cannot be imported from " + << "two different modules yet\n"; + abort(); + } + baseModuleMap[base] = module; + + out << "import { " + << base.str + << " } from '" + << module.str + << "';\n"; + }; + + ImportInfo imports(wasm); + + ModuleUtils::iterImportedGlobals(wasm, [&](Global* import) { + Fatal() << "non-function imports aren't supported yet\n"; + noteImport(import->module, import->base); + }); + ModuleUtils::iterImportedFunctions(wasm, [&](Function* import) { + noteImport(import->module, import->base); + }); + + out << '\n'; } -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() - ) - ); +void Wasm2JSGlue::emitPost() { + // 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; + out << "const mem" << moduleName.str << " = new ArrayBuffer(" + << pages * Memory::kPageSize + << ");\n"; } - return ret; -} + 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); + out << "const assign" << moduleName.str + << " = (" << expr << ")(mem" << moduleName.str << ");\n"; + } + for (auto& seg : wasm.memory.segments) { + assert(!seg.isPassive && "passive segments not implemented yet"); + out << "assign" << moduleName.str << "(" + << constOffset(seg) + << ", \"" + << base64Encode(seg.data) + << "\");\n"; + } + + // Actually invoke the `asmFunc` generated function, passing in all global + // values followed by all imports + out << "const ret" << moduleName.str << " = " << moduleName.str << "({" + << "Math," + << "Int8Array," + << "Uint8Array," + << "Int16Array," + << "Uint16Array," + << "Int32Array," + << "Uint32Array," + << "Float32Array," + << "Float64Array," + << "NaN," + << "Infinity" + << "}, {"; + + out << "abort:function() { throw new Error('abort'); }"; + + ModuleUtils::iterImportedFunctions(wasm, [&](Function* import) { + out << "," << import->base.str; + }); + out << "},mem" << moduleName.str << ");\n"; + + 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; + } + } + out << "export const " + << asmangle(exp->name.str) + << " = ret" + << moduleName.str + << "." + << asmangle(exp->name.str) + << ";\n"; + } +} } // namespace wasm |