diff options
author | Alon Zakai <alonzakai@gmail.com> | 2019-04-11 14:01:51 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-04-11 14:01:51 -0700 |
commit | b769b4ede65eb014376b67f78ba5e6cb04e0cef8 (patch) | |
tree | f0e4925b2d2e3328750c3e7d0190b0801f211faf /src | |
parent | 4905c3d3b4be20a89920ea2a56c1868294e77b65 (diff) | |
download | binaryen-b769b4ede65eb014376b67f78ba5e6cb04e0cef8.tar.gz binaryen-b769b4ede65eb014376b67f78ba5e6cb04e0cef8.tar.bz2 binaryen-b769b4ede65eb014376b67f78ba5e6cb04e0cef8.zip |
Wasm2js refactoring (#1997)
Early work for #1929
* Leave core wasm module - the "asm.js function" - to Wasm2JSBuilder, and add Wasm2JSGlue which emits the code before and after that. Currently that's some ES6 code, but we may want to change that later.
* Add add AssertionEmitter class for the sole purpose of emitting modules + assertions for testing. This avoids some hacks from before like starting from index 1 (assuming the module at first position was already parsed and printed) and printing of the f32Equal etc. functions not at the very top (which was due to technical limitations before).
Logic-wise, there should be no visible change, except some whitespace and reodering, and that I made the exceptions print out the source of the assertion that failed from the wast:
-if (!check2()) fail2();
+if (!check2()) throw 'assertion failed: ( assert_return ( call add ( i32.const 1 ) ( i32.const 1 ) ) ( i32.const 2 ) )';
(fail2 etc. did not exist, and seems to just have given a unique number for each assertion?)
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 |