/* * 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-JS code translator. Converts wasm functions into // valid JavaScript (with a somewhat asm.js-ish flavor). // #ifndef wasm_wasm2js_h #define wasm_wasm2js_h #include #include #include "abi/js.h" #include "asm_v_wasm.h" #include "asmjs/asmangle.h" #include "asmjs/shared-constants.h" #include "emscripten-optimizer/optimizer.h" #include "ir/branch-utils.h" #include "ir/effects.h" #include "ir/find_all.h" #include "ir/import-utils.h" #include "ir/load-utils.h" #include "ir/module-utils.h" #include "ir/names.h" #include "ir/table-utils.h" #include "ir/utils.h" #include "mixed_arena.h" #include "passes/passes.h" #include "support/base64.h" #include "support/file.h" #include "wasm-builder.h" #include "wasm-io.h" #include "wasm-validator.h" #include "wasm.h" namespace wasm { using namespace cashew; // 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); } } // Appends extra to a chain of sequence elements void sequenceAppend(Ref& ast, Ref extra) { if (!ast.get()) { ast = extra; return; } ast = ValueBuilder::makeSeq(ast, extra); } bool isTableExported(Module& wasm) { if (!wasm.table.exists || wasm.table.imported()) { return false; } for (auto& ex : wasm.exports) { if (ex->kind == ExternalKind::Table && ex->value == wasm.table.name) { return true; } } return false; } bool hasActiveSegments(Module& wasm) { for (Index i = 0; i < wasm.memory.segments.size(); i++) { if (!wasm.memory.segments[i].isPassive) { return true; } } return false; } bool needsBufferView(Module& wasm) { if (!wasm.memory.exists) { return false; } // If there are any active segments, initActiveSegments needs access // to bufferView. if (hasActiveSegments(wasm)) { return true; } // The special support functions are emitted as part of the JS glue, if we // need them. bool need = false; ModuleUtils::iterImportedFunctions(wasm, [&](Function* import) { if (ABI::wasm2js::isHelper(import->base)) { need = true; } }); return need; } IString stringToIString(std::string str) { return IString(str.c_str(), false); } // 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. // // Export: Export names // Top: The main scope which contains functions and globals // Local: Local variables in a function. // Label: Label identifiers in a function enum class NameScope { Export, Top, Local, Label, Max, }; // // Wasm2JSBuilder - converts a WebAssembly module's functions into JS // // 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 => JS // is tricky because wasm has statements == expressions, or in // other words, things like `break` and `if` can show up // in places where JS can't handle them, like inside an // a loop's condition check. For that reason we use flat IR here. // We do optimize it later, to allow some nesting, but we avoid // non-JS-compatible nesting like block return values control // flow in an if condition, etc. // class Wasm2JSBuilder { public: struct Flags { // see wasm2js.cpp for details bool debug = false; bool pedantic = false; bool allowAsserts = false; bool emscripten = false; bool deterministic = false; std::string symbolsFile; }; Wasm2JSBuilder(Flags f, PassOptions options_) : flags(f), options(options_) { // We don't try to model wasm's trapping precisely - if we did, each load // and store would need to do a check. Given that, we can just ignore // implicit traps like those when optimizing. (When not optimizing, it's // nice to see codegen that matches wasm more precisely.) if (options.optimizeLevel > 0) { options.ignoreImplicitTraps = true; } } Ref processWasm(Module* wasm, Name funcName = ASM_FUNC); Ref processFunction(Module* wasm, Function* func, bool standalone = false); Ref processStandaloneFunction(Module* wasm, Function* func) { return processFunction(wasm, func, true); } // The second pass on an expression: process it fully, generating // JS Ref processFunctionBody(Module* m, Function* func, bool standalone); // Get a temp var. IString getTemp(Type type, Function* func) { IString ret; TODO_SINGLE_COMPOUND(type); if (frees[type.getBasic()].size() > 0) { ret = frees[type.getBasic()].back(); frees[type.getBasic()].pop_back(); } else { size_t index = temps[type.getBasic()]++; ret = IString((std::string("wasm2js_") + type.toString() + "$" + 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) { TODO_SINGLE_COMPOUND(type); frees[type.getBasic()].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& map = wasmNameToMangledName[(int)scope]; auto it = map.find(name.c_str()); if (it != map.end()) { return it->second; } // The mangled names in our scope. auto& scopeMangledNames = mangledNames[(int)scope]; // In some cases (see below) we need to also check the Top scope. auto& topMangledNames = mangledNames[int(NameScope::Top)]; // 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 = stringToIString(mangled); if (scopeMangledNames.count(ret)) { // When export names collide things may be confusing, as this is // observable externally by the person using the JS. Report a warning. if (scope == NameScope::Export) { std::cerr << "wasm2js: warning: export names colliding: " << mangled << '\n'; } continue; } // The Local scope is special: a Local name must not collide with a Top // name, as they are in a single namespace in JS and can conflict: // // function foo(bar) { // var bar = 0; // } // function bar() { .. if (scope == NameScope::Local && topMangledNames.count(ret)) { continue; } // We found a good name, use it. scopeMangledNames.insert(ret); map[name.c_str()] = ret; return ret; } } private: Flags flags; PassOptions options; // How many temp vars we need std::vector temps; // type => num temps // Which are currently free to use std::vector> frees; // type => list of free names // Mangled names cache by interned names. // Utilizes the usually reused underlying cstring's pointer as the key. std::unordered_map wasmNameToMangledName[(int)NameScope::Max]; // Set of all mangled names in each scope. std::unordered_set mangledNames[(int)NameScope::Max]; // If a function is callable from outside, we'll need to cast the inputs // and our return value. Otherwise, internally, casts are only needed // on operations. std::unordered_set functionsCallableFromOutside; void addBasics(Ref ast, Module* wasm); void addFunctionImport(Ref ast, Function* import); void addGlobalImport(Ref ast, Global* import); void addTable(Ref ast, Module* wasm); void addStart(Ref ast, Module* wasm); void addExports(Ref ast, Module* wasm); void addGlobal(Ref ast, Global* global); void addMemoryFuncs(Ref ast, Module* wasm); void addMemoryGrowFunc(Ref ast, Module* wasm); Wasm2JSBuilder() = delete; Wasm2JSBuilder(const Wasm2JSBuilder&) = delete; Wasm2JSBuilder& operator=(const Wasm2JSBuilder&) = delete; }; Ref Wasm2JSBuilder::processWasm(Module* wasm, Name funcName) { // Scan the wasm for important things. for (auto& exp : wasm->exports) { if (exp->kind == ExternalKind::Function) { functionsCallableFromOutside.insert(exp->value); } } for (auto& segment : wasm->table.segments) { for (auto name : segment.data) { functionsCallableFromOutside.insert(name); } } // Ensure the scratch memory helpers. // If later on they aren't needed, we'll clean them up. ABI::wasm2js::ensureHelpers(wasm); // Process the code, and optimize if relevant. // First, do the lowering to a JS-friendly subset. { PassRunner runner(wasm, options); runner.add(make_unique()); // TODO: only legalize if necessary - emscripten would already do so, and // likely other toolchains. but spec test suite needs that. runner.add("legalize-js-interface"); // 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 to produce correct code. For some more details about this see // #1480 runner.add("flatten"); runner.add("i64-to-i32-lowering"); runner.add("alignment-lowering"); // Next, optimize that as best we can. This should not generate // non-JS-friendly things. if (options.optimizeLevel > 0) { // It is especially import to propagate constants after the lowering. // However, this can be a slow operation, especially after flattening; // some local simplification helps. if (options.optimizeLevel >= 3 || options.shrinkLevel >= 1) { runner.add("simplify-locals-nonesting"); runner.add("precompute-propagate"); // Avoiding reinterpretation is helped by propagation. We also run // it later down as default optimizations help as well. runner.add("avoid-reinterprets"); } runner.addDefaultOptimizationPasses(); runner.add("avoid-reinterprets"); } // Finally, get the code into the flat form we need for wasm2js itself, and // optimize that a little in a way that keeps that property. runner.add("flatten"); // Regardless of optimization level, run some simple optimizations to undo // some of the effects of flattening. runner.add("simplify-locals-notee-nostructure"); // Some operations can be very slow if we didn't run full optimizations // earlier, so don't run them automatically. if (options.optimizeLevel > 0) { runner.add("remove-unused-names"); runner.add("merge-blocks"); runner.add("reorder-locals"); runner.add("coalesce-locals"); } runner.add("reorder-locals"); runner.add("vacuum"); runner.add("remove-unused-module-elements"); // DCE at the end to make sure all IR nodes have valid types for conversion // to JS, and not unreachable. runner.add("dce"); runner.setDebug(flags.debug); runner.run(); } if (flags.symbolsFile.size() > 0) { Output out(flags.symbolsFile, wasm::Flags::Text); Index i = 0; for (auto& func : wasm->functions) { out.getStream() << i++ << ':' << func->name.str << '\n'; } } #ifndef NDEBUG if (!WasmValidator().validate(*wasm)) { std::cout << *wasm << '\n'; Fatal() << "error in validating wasm2js output"; } #endif Ref ret = ValueBuilder::makeToplevel(); Ref asmFunc = ValueBuilder::makeFunction(funcName); ret[1]->push_back(asmFunc); ValueBuilder::appendArgumentToFunction(asmFunc, ENV); // add memory import if (wasm->memory.exists) { if (wasm->memory.imported()) { // find memory and buffer in imports Ref theVar = ValueBuilder::makeVar(); asmFunc[3]->push_back(theVar); ValueBuilder::appendToVar( theVar, "memory", ValueBuilder::makeDot(ValueBuilder::makeName(ENV), ValueBuilder::makeName(wasm->memory.base))); // Assign `buffer = memory.buffer` Ref buf = ValueBuilder::makeVar(); asmFunc[3]->push_back(buf); ValueBuilder::appendToVar( buf, BUFFER, ValueBuilder::makeDot(ValueBuilder::makeName("memory"), ValueBuilder::makeName("buffer"))); // If memory is growable, override the imported memory's grow method to // ensure so that when grow is called from the output it works as expected if (wasm->memory.max > wasm->memory.initial) { asmFunc[3]->push_back( ValueBuilder::makeStatement(ValueBuilder::makeBinary( ValueBuilder::makeDot(ValueBuilder::makeName("memory"), ValueBuilder::makeName("grow")), SET, ValueBuilder::makeName(WASM_MEMORY_GROW)))); } } else { Ref theVar = ValueBuilder::makeVar(); asmFunc[3]->push_back(theVar); ValueBuilder::appendToVar( theVar, BUFFER, ValueBuilder::makeNew(ValueBuilder::makeCall( ValueBuilder::makeName("ArrayBuffer"), ValueBuilder::makeInt(Address::address32_t(wasm->memory.initial.addr * Memory::kPageSize))))); } } // add table import if (wasm->table.exists && wasm->table.imported()) { Ref theVar = ValueBuilder::makeVar(); asmFunc[3]->push_back(theVar); ValueBuilder::appendToVar( theVar, FUNCTION_TABLE, ValueBuilder::makeDot(ValueBuilder::makeName(ENV), wasm->table.base)); } // create heaps, etc addBasics(asmFunc[3], wasm); ModuleUtils::iterImportedFunctions( *wasm, [&](Function* import) { addFunctionImport(asmFunc[3], import); }); ModuleUtils::iterImportedGlobals( *wasm, [&](Global* import) { addGlobalImport(asmFunc[3], import); }); // Note the names of functions. We need to do this here as when generating // mangled local names we need them not to conflict with these (see fromName) // so we can't wait until we parse each function to note its name. for (auto& f : wasm->functions) { fromName(f->name, NameScope::Top); } // globals bool generateFetchHighBits = false; ModuleUtils::iterDefinedGlobals(*wasm, [&](Global* global) { addGlobal(asmFunc[3], global); if (flags.allowAsserts && global->name == INT64_TO_32_HIGH_BITS) { generateFetchHighBits = true; } }); if (flags.emscripten) { asmFunc[3]->push_back( ValueBuilder::makeName("// EMSCRIPTEN_START_FUNCS\n")); } // functions ModuleUtils::iterDefinedFunctions(*wasm, [&](Function* func) { asmFunc[3]->push_back(processFunction(wasm, func)); }); if (generateFetchHighBits) { Builder builder(*wasm); asmFunc[3]->push_back( processFunction(wasm, wasm->addFunction(builder.makeFunction( WASM_FETCH_HIGH_BITS, Signature(Type::none, Type::i32), {}, builder.makeReturn(builder.makeGlobalGet( INT64_TO_32_HIGH_BITS, Type::i32)))))); auto e = new Export(); e->name = WASM_FETCH_HIGH_BITS; e->value = WASM_FETCH_HIGH_BITS; e->kind = ExternalKind::Function; wasm->addExport(e); } if (flags.emscripten) { asmFunc[3]->push_back(ValueBuilder::makeName("// EMSCRIPTEN_END_FUNCS\n")); } if (needsBufferView(*wasm)) { asmFunc[3]->push_back( ValueBuilder::makeBinary(ValueBuilder::makeName("bufferView"), SET, ValueBuilder::makeName(HEAPU8))); } if (hasActiveSegments(*wasm)) { asmFunc[3]->push_back( ValueBuilder::makeCall(ValueBuilder::makeName("initActiveSegments"), ValueBuilder::makeName(ENV))); } addTable(asmFunc[3], wasm); addStart(asmFunc[3], wasm); addExports(asmFunc[3], wasm); return ret; } void Wasm2JSBuilder::addBasics(Ref ast, Module* wasm) { if (wasm->memory.exists) { // 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( 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::makeName(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_TRUNC, TRUNC); 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::makeName("NaN")); Ref infinityVar = ValueBuilder::makeVar(); ast->push_back(infinityVar); ValueBuilder::appendToVar( infinityVar, "infinity", ValueBuilder::makeName("Infinity")); } void Wasm2JSBuilder::addFunctionImport(Ref ast, Function* import) { // The scratch memory helpers are emitted in the glue, see code and comments // below. if (ABI::wasm2js::isHelper(import->base)) { return; } Ref theVar = ValueBuilder::makeVar(); ast->push_back(theVar); // TODO: handle nested module imports Ref module = ValueBuilder::makeName(ENV); ValueBuilder::appendToVar( theVar, fromName(import->name, NameScope::Top), ValueBuilder::makeDot(module, fromName(import->base, NameScope::Top))); } void Wasm2JSBuilder::addGlobalImport(Ref ast, Global* import) { Ref theVar = ValueBuilder::makeVar(); ast->push_back(theVar); // TODO: handle nested module imports Ref module = ValueBuilder::makeName(ENV); Ref value = ValueBuilder::makeDot(module, fromName(import->base, NameScope::Top)); if (import->type == Type::i32) { value = makeAsmCoercion(value, ASM_INT); } ValueBuilder::appendToVar( theVar, fromName(import->name, NameScope::Top), value); } void Wasm2JSBuilder::addTable(Ref ast, Module* wasm) { if (!wasm->table.exists) { return; } bool perElementInit = false; // Emit a simple flat table as a JS array literal. Otherwise, // emit assignments separately for each index. Ref theArray = ValueBuilder::makeArray(); if (!wasm->table.imported()) { TableUtils::FlatTable flat(wasm->table); if (flat.valid) { Name null("null"); for (auto& name : flat.names) { if (name.is()) { name = fromName(name, NameScope::Top); } else { name = null; } ValueBuilder::appendToArray(theArray, ValueBuilder::makeName(name)); } } else { perElementInit = true; Ref initial = ValueBuilder::makeInt(Address::address32_t(wasm->table.initial.addr)); theArray = ValueBuilder::makeNew( ValueBuilder::makeCall(IString("Array"), initial)); } } else { perElementInit = true; } if (isTableExported(*wasm)) { // If the table is exported use a fake WebAssembly.Table object // We don't handle the case where a table is both imported and exported. if (wasm->table.imported()) { Fatal() << "wasm2js doesn't support a table that is both imported and " "exported\n"; } Ref theVar = ValueBuilder::makeVar(); ast->push_back(theVar); Ref table = ValueBuilder::makeCall(IString("Table"), theArray); ValueBuilder::appendToVar(theVar, FUNCTION_TABLE, table); } else if (!wasm->table.imported()) { // Otherwise if the table is internal (neither imported not exported). // Just use a plain array in this case, avoiding the Table. Ref theVar = ValueBuilder::makeVar(); ast->push_back(theVar); ValueBuilder::appendToVar(theVar, FUNCTION_TABLE, theArray); } if (perElementInit) { // TODO: optimize for size for (auto& segment : wasm->table.segments) { auto offset = segment.offset; for (Index i = 0; i < segment.data.size(); i++) { Ref index; if (auto* c = offset->dynCast()) { index = ValueBuilder::makeInt(c->value.geti32() + i); } else if (auto* get = offset->dynCast()) { index = ValueBuilder::makeBinary( ValueBuilder::makeName(stringToIString(asmangle(get->name.str))), PLUS, ValueBuilder::makeNum(i)); } else { WASM_UNREACHABLE("unexpected expr type"); } ast->push_back(ValueBuilder::makeStatement(ValueBuilder::makeBinary( ValueBuilder::makeSub(ValueBuilder::makeName(FUNCTION_TABLE), index), SET, ValueBuilder::makeName(fromName(segment.data[i], NameScope::Top))))); } } } } void Wasm2JSBuilder::addStart(Ref ast, Module* wasm) { if (wasm->start.is()) { ast->push_back( ValueBuilder::makeCall(fromName(wasm->start, NameScope::Top))); } } void Wasm2JSBuilder::addExports(Ref ast, Module* wasm) { Ref exports = ValueBuilder::makeObject(); for (auto& export_ : wasm->exports) { switch (export_->kind) { case ExternalKind::Function: { ValueBuilder::appendToObjectWithQuotes( exports, fromName(export_->name, NameScope::Export), ValueBuilder::makeName(fromName(export_->value, NameScope::Top))); break; } case ExternalKind::Memory: { Ref descs = ValueBuilder::makeObject(); Ref growDesc = ValueBuilder::makeObject(); ValueBuilder::appendToObjectWithQuotes( descs, IString("grow"), growDesc); if (wasm->memory.max > wasm->memory.initial) { ValueBuilder::appendToObjectWithQuotes( growDesc, IString("value"), ValueBuilder::makeName(WASM_MEMORY_GROW)); } Ref bufferDesc = ValueBuilder::makeObject(); Ref bufferGetter = ValueBuilder::makeFunction(IString("")); bufferGetter[3]->push_back( ValueBuilder::makeReturn(ValueBuilder::makeName(BUFFER))); ValueBuilder::appendToObjectWithQuotes( bufferDesc, IString("get"), bufferGetter); ValueBuilder::appendToObjectWithQuotes( 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::appendToObjectWithQuotes( exports, fromName(export_->name, NameScope::Export), memory); break; } case ExternalKind::Table: { ValueBuilder::appendToObjectWithQuotes( exports, fromName(export_->name, NameScope::Export), ValueBuilder::makeName(FUNCTION_TABLE)); break; } case ExternalKind::Global: { ValueBuilder::appendToObjectWithQuotes( exports, fromName(export_->name, NameScope::Export), ValueBuilder::makeName(fromName(export_->value, NameScope::Top))); break; } case ExternalKind::Event: case ExternalKind::Invalid: Fatal() << "unsupported export type: " << export_->name << "\n"; } } if (wasm->memory.exists) { addMemoryFuncs(ast, wasm); } ast->push_back( ValueBuilder::makeStatement(ValueBuilder::makeReturn(exports))); } void Wasm2JSBuilder::addGlobal(Ref ast, Global* global) { if (auto* const_ = global->init->dynCast()) { Ref theValue; TODO_SINGLE_COMPOUND(const_->type); switch (const_->type.getBasic()) { 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 if (auto* get = global->init->dynCast()) { Ref theVar = ValueBuilder::makeVar(); ast->push_back(theVar); ValueBuilder::appendToVar( theVar, fromName(global->name, NameScope::Top), ValueBuilder::makeName(fromName(get->name, NameScope::Top))); } else { assert(false && "Top init type not supported"); } } Ref Wasm2JSBuilder::processFunction(Module* m, Function* func, bool standaloneFunction) { if (standaloneFunction) { // We are only printing a function, not a whole module. Prepare it for // translation now (if there were a module, we'd have done this for all // functions in parallel, earlier). PassRunner runner(m); // We only run a subset of all passes here. TODO: create a full valid module // for each assertion body. runner.add("flatten"); runner.add("simplify-locals-notee-nostructure"); runner.add("reorder-locals"); runner.add("remove-unused-names"); runner.add("vacuum"); runner.runOnFunction(func); } // 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(Type::i32, std::max(Type::f32, Type::f64)) + 1); temps.clear(); temps.resize(std::max(Type::i32, std::max(Type::f32, Type::f64)) + 1); temps[Type::i32] = temps[Type::f32] = temps[Type::f64] = 0; // arguments bool needCoercions = options.optimizeLevel == 0 || standaloneFunction || functionsCallableFromOutside.count(func->name); for (Index i = 0; i < func->getNumParams(); i++) { IString name = fromName(func->getLocalNameOrGeneric(i), NameScope::Local); ValueBuilder::appendArgumentToFunction(ret, name); if (needCoercions) { 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 flattenAppend(ret, processFunctionBody(m, func, standaloneFunction)); // 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: all temp vars should be free at the end assert(frees[Type::i32].size() == temps[Type::i32]); assert(frees[Type::f32].size() == temps[Type::f32]); assert(frees[Type::f64].size() == temps[Type::f64]); return ret; } Ref Wasm2JSBuilder::processFunctionBody(Module* m, Function* func, bool standaloneFunction) { // Switches are tricky to handle - in wasm they often come with // massively-nested "towers" of blocks, which if naively translated // to JS may exceed parse recursion limits of VMs. Therefore even when // not optimizing we work hard to emit minimal and minimally-nested // switches. // We do so by pre-scanning for br_tables and noting which of their // targets can be hoisted up into them, e.g. // // (block $a // (block $b // (block $c // (block $d // (block $e // (br_table $a $b $c $d $e (..)) // ) // ;; code X (for block $e) // ;; implicit fallthrough - can be done in the switch too // ) // ;; code Y // (br $c) ;; branch which is identical to a fallthrough // ) // ;; code Z // (br $a) ;; skip some blocks - can't do this in a switch! // ) // ;; code W // ) // // Every branch we see is a potential hazard - all targets must not // be optimized into the switch, since they must be reached normally, // unless they happen to be right after us, in which case it's just // a fallthrough anyhow. struct SwitchProcessor : public ExpressionStackWalker { // A list of expressions we don't need to emit, as we are handling them // in another way. std::set unneededExpressions; struct SwitchCase { Name target; std::vector code; SwitchCase(Name target) : target(target) {} }; // The switch cases we found that we can hoist up. std::map> hoistedSwitchCases; void visitSwitch(Switch* brTable) { Index i = expressionStack.size() - 1; assert(expressionStack[i] == brTable); // A set of names we must stop at, since we've seen branches to them. std::set namesBranchedTo; while (1) { // Stop if we are at the top level. if (i == 0) { break; } i--; auto* child = expressionStack[i + 1]; auto* curr = expressionStack[i]; // Stop if the current node is not a block with the child in the // first position, i.e., the classic switch pattern. auto* block = curr->dynCast(); if (!block || block->list[0] != child) { break; } // Ignore the case of a name-less block for simplicity (merge-blocks // would have removed it). if (!block->name.is()) { break; } // If we have already seen this block, stop here. if (unneededExpressions.count(block)) { // XXX FIXME we should probably abort the entire optimization break; } auto& list = block->list; if (child == brTable) { // Nothing more to do here (we can in fact skip any code til // the parent block). continue; } // Ok, we are a block and our child in the first position is a // block, and the neither is branched to - unless maybe the child // branches to the parent, check that. Note how we treat the // final element which may be a break that is a fallthrough. Expression* unneededBr = nullptr; for (Index j = 1; j < list.size(); j++) { auto* item = list[j]; auto newBranches = BranchUtils::getExitingBranches(item); if (auto* br = item->dynCast()) { if (j == list.size() - 1) { if (!br->condition && br->name == block->name) { // This is a natural, unnecessary-to-emit fallthrough. unneededBr = br; break; } } } namesBranchedTo.insert(newBranches.begin(), newBranches.end()); } if (namesBranchedTo.count(block->name)) { break; } // We can move code after the child (reached by branching on the // child) into the switch. auto* childBlock = child->cast(); hoistedSwitchCases[brTable].emplace_back(childBlock->name); SwitchCase& case_ = hoistedSwitchCases[brTable].back(); for (Index j = 1; j < list.size(); j++) { auto* item = list[j]; if (item != unneededBr) { case_.code.push_back(item); } } list.resize(1); // Finally, mark the block as unneeded outside the switch. unneededExpressions.insert(childBlock); } } }; struct ExpressionProcessor : public OverriddenVisitor { Wasm2JSBuilder* parent; IString result; // TODO: remove Function* func; Module* module; bool standaloneFunction; SwitchProcessor switchProcessor; ExpressionProcessor(Wasm2JSBuilder* parent, Module* m, Function* func, bool standaloneFunction) : parent(parent), func(func), module(m), standaloneFunction(standaloneFunction) {} Ref process() { switchProcessor.walk(func->body); return visit(func->body, NO_RESULT); } // A scoped temporary variable. struct ScopedTemp { Wasm2JSBuilder* parent; Type type; IString temp; // TODO: switch to indexes; avoid names 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 = OverriddenVisitor::visit(curr); // keep it consistent for the rest of this frame, which may call visit on // multiple children result = old; return ret; } Ref visit(Expression* curr, ScopedTemp& temp) { return visit(curr, temp.temp); } Ref visitAndAssign(Expression* curr, IString result) { assert(result != NO_RESULT); Ref ret = visit(curr, result); return ValueBuilder::makeStatement( ValueBuilder::makeBinary(ValueBuilder::makeName(result), SET, ret)); } Ref visitAndAssign(Expression* curr, ScopedTemp& temp) { return visitAndAssign(curr, temp.getName()); } // 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; } // Breaks to the top of a loop should be emitted as continues, to that // loop's main label std::unordered_set continueLabels; IString fromName(Name name, NameScope scope) { return parent->fromName(name, scope); } // Visitors Ref visitBlock(Block* curr) { if (switchProcessor.unneededExpressions.count(curr)) { // We have had our tail hoisted into a switch that is nested in our // first position, so we don't need to emit that code again, or // ourselves in fact. return visit(curr->list[0], NO_RESULT); } Ref ret = ValueBuilder::makeBlock(); size_t size = curr->list.size(); for (size_t i = 0; i < size; i++) { flattenAppend( ret, ValueBuilder::makeStatement(visit(curr->list[i], NO_RESULT))); } if (curr->name.is()) { ret = ValueBuilder::makeLabel(fromName(curr->name, NameScope::Label), ret); } return ret; } Ref visitIf(If* curr) { Ref condition = visit(curr->condition, EXPRESSION_RESULT); Ref ifTrue = visit(curr->ifTrue, NO_RESULT); Ref ifFalse; if (curr->ifFalse) { ifFalse = visit(curr->ifFalse, NO_RESULT); } return ValueBuilder::makeIf(condition, ifTrue, ifFalse); // simple if } Ref visitLoop(Loop* curr) { Name asmLabel = curr->name; continueLabels.insert(asmLabel); Ref body = visit(curr->body, result); // if we can reach the end of the block, we must leave the while (1) loop if (curr->body->type != Type::unreachable) { assert(curr->body->type == Type::none); // flat IR body = blockify(body); flattenAppend( body, ValueBuilder::makeBreak(fromName(asmLabel, NameScope::Label))); } Ref ret = ValueBuilder::makeWhile(ValueBuilder::makeInt(1), body); return ValueBuilder::makeLabel(fromName(asmLabel, NameScope::Label), ret); } Ref makeBreakOrContinue(Name name) { if (continueLabels.count(name)) { return ValueBuilder::makeContinue(fromName(name, NameScope::Label)); } else { return ValueBuilder::makeBreak(fromName(name, NameScope::Label)); } } 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; fakeIf.condition = curr->condition; fakeIf.ifTrue = &fakeBreak; return visit(&fakeIf, result); } return makeBreakOrContinue(curr->name); } Expression* defaultBody = nullptr; // default must be last in asm.js Ref visitSwitch(Switch* curr) { // Even without optimizations, we work hard here to emit minimal and // especially minimally-nested code, since otherwise we may get block // nesting of a size that JS engines can't handle. Ref condition = visit(curr->condition, EXPRESSION_RESULT); Ref theSwitch = ValueBuilder::makeSwitch(makeAsmCoercion(condition, ASM_INT)); // First, group the switch targets. std::map> targetIndexes; for (size_t i = 0; i < curr->targets.size(); i++) { targetIndexes[curr->targets[i]].push_back(i); } // Emit first any hoisted groups. auto& hoistedCases = switchProcessor.hoistedSwitchCases[curr]; std::set emittedTargets; bool hoistedEndsWithUnreachable = false; for (auto& case_ : hoistedCases) { auto target = case_.target; auto& code = case_.code; emittedTargets.insert(target); if (target != curr->default_) { auto& indexes = targetIndexes[target]; for (auto i : indexes) { ValueBuilder::appendCaseToSwitch(theSwitch, ValueBuilder::makeNum(i)); } } else { ValueBuilder::appendDefaultToSwitch(theSwitch); } for (auto* c : code) { ValueBuilder::appendCodeToSwitch( theSwitch, blockify(visit(c, NO_RESULT)), false); hoistedEndsWithUnreachable = c->type == Type::unreachable; } } // After the hoisted cases, if any remain we must make sure not to // fall through into them. If no code was hoisted, this is unnecessary, // and if the hoisted code ended with an unreachable it also is not // necessary. bool stoppedFurtherFallthrough = false; auto stopFurtherFallthrough = [&]() { if (!stoppedFurtherFallthrough && !hoistedCases.empty() && !hoistedEndsWithUnreachable) { stoppedFurtherFallthrough = true; ValueBuilder::appendCodeToSwitch( theSwitch, blockify(ValueBuilder::makeBreak(IString())), false); } }; // Emit any remaining groups by just emitting branches to their code, // which will appear outside the switch. for (auto& pair : targetIndexes) { auto target = pair.first; auto& indexes = pair.second; if (emittedTargets.count(target)) { continue; } stopFurtherFallthrough(); if (target != curr->default_) { for (auto i : indexes) { ValueBuilder::appendCaseToSwitch(theSwitch, ValueBuilder::makeNum(i)); } ValueBuilder::appendCodeToSwitch( theSwitch, blockify(makeBreakOrContinue(target)), false); } else { // For the group going to the same place as the default, we can just // emit the default itself, which we do at the end. } } // TODO: if the group the default is in is not the largest, we can turn // the largest into // the default by using a local and a check on the range if (!emittedTargets.count(curr->default_)) { stopFurtherFallthrough(); ValueBuilder::appendDefaultToSwitch(theSwitch); ValueBuilder::appendCodeToSwitch( theSwitch, blockify(makeBreakOrContinue(curr->default_)), false); } return theSwitch; } Ref visitCall(Call* curr) { if (curr->isReturn) { Fatal() << "tail calls not yet supported in wasm2js"; } Ref theCall = ValueBuilder::makeCall(fromName(curr->target, NameScope::Top)); // For wasm => wasm calls, we don't need coercions. TODO: even imports // might be safe? bool needCoercions = parent->options.optimizeLevel == 0 || standaloneFunction || module->getFunction(curr->target)->imported(); for (auto operand : curr->operands) { auto value = visit(operand, EXPRESSION_RESULT); if (needCoercions) { value = makeAsmCoercion(value, wasmToAsmType(operand->type)); } theCall[2]->push_back(value); } if (needCoercions) { theCall = makeAsmCoercion(theCall, wasmToAsmType(curr->type)); } return theCall; } Ref visitCallIndirect(CallIndirect* curr) { if (curr->isReturn) { Fatal() << "tail calls not yet supported in wasm2js"; } // If the target has effects that interact with the operands, we must // reorder it to the start. bool mustReorder = false; EffectAnalyzer targetEffects( parent->options, module->features, curr->target); if (targetEffects.hasAnything()) { for (auto* operand : curr->operands) { if (targetEffects.invalidates( EffectAnalyzer(parent->options, module->features, operand))) { mustReorder = true; break; } } } // Ensure the function pointer is a number. In general in wasm2js we are // ok with true/false being present, as they are immediately cast to a // number anyhow on their use. However, FUNCTION_TABLE[true] is *not* the // same as FUNCTION_TABLE[1], so we must cast. This is a rare exception // because FUNCTION_TABLE is just a normal JS object, not a typed array // or a mathematical operation (all of which coerce to a number for us). auto target = visit(curr->target, EXPRESSION_RESULT); target = makeAsmCoercion(target, ASM_INT); if (mustReorder) { Ref ret; ScopedTemp idx(Type::i32, parent, func); std::vector temps; // TODO: utility class, with destructor? for (auto* operand : curr->operands) { temps.push_back(new ScopedTemp(operand->type, parent, func)); IString temp = temps.back()->temp; sequenceAppend(ret, visitAndAssign(operand, temp)); } sequenceAppend(ret, ValueBuilder::makeBinary( ValueBuilder::makeName(idx.getName()), SET, target)); Ref theCall = ValueBuilder::makeCall(ValueBuilder::makeSub( ValueBuilder::makeName(FUNCTION_TABLE), idx.getAstName())); for (size_t i = 0; i < temps.size(); i++) { IString temp = temps[i]->temp; auto& operand = curr->operands[i]; theCall[2]->push_back(makeAsmCoercion(ValueBuilder::makeName(temp), wasmToAsmType(operand->type))); } theCall = makeAsmCoercion(theCall, wasmToAsmType(curr->type)); sequenceAppend(ret, theCall); for (auto temp : temps) { delete temp; } return ret; } else { // Target has no side effects, emit simple code Ref theCall = ValueBuilder::makeCall(ValueBuilder::makeSub( ValueBuilder::makeName(FUNCTION_TABLE), target)); for (auto* operand : curr->operands) { theCall[2]->push_back(visit(operand, EXPRESSION_RESULT)); } theCall = makeAsmCoercion(theCall, wasmToAsmType(curr->type)); return theCall; } } // TODO: remove Ref makeSetVar(Expression* curr, Expression* value, Name name, NameScope scope) { return ValueBuilder::makeBinary( ValueBuilder::makeName(fromName(name, scope)), SET, visit(value, EXPRESSION_RESULT)); } Ref visitLocalGet(LocalGet* curr) { return ValueBuilder::makeName( fromName(func->getLocalNameOrGeneric(curr->index), NameScope::Local)); } Ref visitLocalSet(LocalSet* curr) { return makeSetVar(curr, curr->value, func->getLocalNameOrGeneric(curr->index), NameScope::Local); } Ref visitGlobalGet(GlobalGet* curr) { return ValueBuilder::makeName(fromName(curr->name, NameScope::Top)); } Ref visitGlobalSet(GlobalSet* curr) { return makeSetVar(curr, curr->value, curr->name, NameScope::Top); } Ref visitLoad(Load* curr) { // Unaligned loads and stores must have been fixed up already. assert(curr->align == 0 || curr->align == curr->bytes); // normal load Ref ptr = makePointer(curr->ptr, curr->offset); Ref ret; switch (curr->type.getBasic()) { case Type::i32: { switch (curr->bytes) { case 1: ret = ValueBuilder::makeSub( ValueBuilder::makeName( LoadUtils::isSignRelevant(curr) && curr->signed_ ? HEAP8 : HEAPU8), ValueBuilder::makePtrShift(ptr, 0)); break; case 2: ret = ValueBuilder::makeSub( ValueBuilder::makeName( LoadUtils::isSignRelevant(curr) && curr->signed_ ? HEAP16 : HEAPU16), ValueBuilder::makePtrShift(ptr, 1)); break; case 4: ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAP32), ValueBuilder::makePtrShift(ptr, 2)); break; default: { Fatal() << "Unhandled number of bytes in i32 load: " << curr->bytes; } } break; } case Type::f32: ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAPF32), ValueBuilder::makePtrShift(ptr, 2)); break; case Type::f64: ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAPF64), ValueBuilder::makePtrShift(ptr, 3)); break; default: { Fatal() << "Unhandled type in load: " << curr->type; } } if (curr->isAtomic) { Ref call = ValueBuilder::makeCall( ValueBuilder::makeDot(ValueBuilder::makeName(ATOMICS), LOAD)); ValueBuilder::appendToCall(call, ret[1]); ValueBuilder::appendToCall(call, ret[2]); ret = call; } // Coercions are not actually needed, as if the user reads beyond valid // memory, it's undefined behavior anyhow, and so we don't care much about // slowness of undefined values etc. bool needCoercions = parent->options.optimizeLevel == 0 || standaloneFunction; if (needCoercions) { ret = makeAsmCoercion(ret, wasmToAsmType(curr->type)); } return ret; } Ref visitStore(Store* curr) { if (module->memory.initial < module->memory.max && curr->type != Type::unreachable) { // In JS, if memory grows then it is dangerous to write // HEAP[f()] = .. // or // HEAP[..] = f() // since if the call swaps HEAP (in a growth operation) then // we will not actually write to the new version (since the // semantics of JS mean we already looked at HEAP and have // decided where to assign to). if (!FindAll(curr->ptr).list.empty() || !FindAll(curr->value).list.empty() || !FindAll(curr->ptr).list.empty() || !FindAll(curr->value).list.empty() || !FindAll(curr->ptr).list.empty() || !FindAll(curr->value).list.empty()) { Ref ret; ScopedTemp ptr(Type::i32, parent, func); sequenceAppend(ret, visitAndAssign(curr->ptr, ptr)); ScopedTemp value(curr->value->type, parent, func); sequenceAppend(ret, visitAndAssign(curr->value, value)); LocalGet getPtr; getPtr.index = func->getLocalIndex(ptr.getName()); getPtr.type = Type::i32; LocalGet getValue; getValue.index = func->getLocalIndex(value.getName()); getValue.type = curr->value->type; Store fakeStore = *curr; fakeStore.ptr = &getPtr; fakeStore.value = &getValue; sequenceAppend(ret, visitStore(&fakeStore)); return ret; } } // FIXME if memory growth, store ptr cannot contain a function call // also other stores to memory, check them, all makeSub's // Unaligned loads and stores must have been fixed up already. assert(curr->align == 0 || curr->align == curr->bytes); // normal store Ref ptr = makePointer(curr->ptr, curr->offset); Ref value = visit(curr->value, EXPRESSION_RESULT); Ref ret; switch (curr->valueType.getBasic()) { case Type::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 Type::f32: ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAPF32), ValueBuilder::makePtrShift(ptr, 2)); break; case Type::f64: ret = ValueBuilder::makeSub(ValueBuilder::makeName(HEAPF64), ValueBuilder::makePtrShift(ptr, 3)); break; default: { Fatal() << "Unhandled type in store: " << curr->valueType; } } if (curr->isAtomic) { Ref call = ValueBuilder::makeCall( ValueBuilder::makeDot(ValueBuilder::makeName(ATOMICS), STORE)); ValueBuilder::appendToCall(call, ret[1]); ValueBuilder::appendToCall(call, ret[2]); ValueBuilder::appendToCall(call, value); return call; } return ValueBuilder::makeBinary(ret, SET, value); } Ref visitDrop(Drop* curr) { return visit(curr->value, NO_RESULT); } Ref visitConst(Const* curr) { switch (curr->type.getBasic()) { case Type::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 Type::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 Type::f32: { Ref ret = ValueBuilder::makeCall(MATH_FROUND); Const fake; fake.value = Literal(double(curr->value.getf32())); fake.type = Type::f64; ret[2]->push_back(visitConst(&fake)); return ret; } case Type::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: Fatal() << "unknown const type"; } } Ref visitUnary(Unary* curr) { // normal unary switch (curr->type.getBasic()) { case Type::i32: { switch (curr->op) { case ClzInt32: { return ValueBuilder::makeCall( MATH_CLZ32, visit(curr->value, EXPRESSION_RESULT)); } case CtzInt32: case PopcntInt32: { WASM_UNREACHABLE("i32 unary should have been removed"); } case EqZInt32: { // XXX !x does change the type to bool, which is correct, but may // be slower? return ValueBuilder::makeUnary( L_NOT, visit(curr->value, EXPRESSION_RESULT)); } case ReinterpretFloat32: { ABI::wasm2js::ensureHelpers(module, ABI::wasm2js::SCRATCH_STORE_F32); ABI::wasm2js::ensureHelpers(module, ABI::wasm2js::SCRATCH_LOAD_I32); Ref store = ValueBuilder::makeCall(ABI::wasm2js::SCRATCH_STORE_F32, visit(curr->value, EXPRESSION_RESULT)); // 32-bit scratch memory uses index 2, so that it does not // conflict with indexes 0, 1 which are used for 64-bit, see // comment where |scratchBuffer| is defined. Ref load = ValueBuilder::makeCall(ABI::wasm2js::SCRATCH_LOAD_I32, ValueBuilder::makeInt(2)); return ValueBuilder::makeSeq(store, load); } // generate (~~expr), what Emscripten does case TruncSFloat32ToInt32: case TruncSFloat64ToInt32: case TruncSatSFloat32ToInt32: case TruncSatSFloat64ToInt32: { return ValueBuilder::makeUnary( B_NOT, ValueBuilder::makeUnary(B_NOT, visit(curr->value, EXPRESSION_RESULT))); } // generate (~~expr >>> 0), what Emscripten does case TruncUFloat32ToInt32: case TruncUFloat64ToInt32: case TruncSatUFloat32ToInt32: case TruncSatUFloat64ToInt32: { return ValueBuilder::makeBinary( ValueBuilder::makeUnary( B_NOT, ValueBuilder::makeUnary( B_NOT, visit(curr->value, EXPRESSION_RESULT))), TRSHIFT, ValueBuilder::makeNum(0)); } case ExtendS8Int32: { return ValueBuilder::makeBinary( ValueBuilder::makeBinary(visit(curr->value, EXPRESSION_RESULT), LSHIFT, ValueBuilder::makeNum(24)), RSHIFT, ValueBuilder::makeNum(24)); } case ExtendS16Int32: { return ValueBuilder::makeBinary( ValueBuilder::makeBinary(visit(curr->value, EXPRESSION_RESULT), LSHIFT, ValueBuilder::makeNum(16)), RSHIFT, ValueBuilder::makeNum(16)); } default: { Fatal() << "Unhandled unary i32 operator: " << curr; } } } case Type::f32: case Type::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 TruncFloat32: case TruncFloat64: ret = ValueBuilder::makeCall( MATH_TRUNC, 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: { ABI::wasm2js::ensureHelpers(module, ABI::wasm2js::SCRATCH_STORE_I32); ABI::wasm2js::ensureHelpers(module, ABI::wasm2js::SCRATCH_LOAD_F32); // 32-bit scratch memory uses index 2, so that it does not // conflict with indexes 0, 1 which are used for 64-bit, see // comment where |scratchBuffer| is defined. Ref store = ValueBuilder::makeCall(ABI::wasm2js::SCRATCH_STORE_I32, ValueBuilder::makeNum(2), visit(curr->value, EXPRESSION_RESULT)); Ref load = ValueBuilder::makeCall(ABI::wasm2js::SCRATCH_LOAD_F32); return ValueBuilder::makeSeq(store, load); } // 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: WASM_UNREACHABLE( "operation should have been removed in previous passes"); default: WASM_UNREACHABLE("unhandled unary float operator"); } if (curr->type == Type::f32) { // doubles need much less coercing return makeAsmCoercion(ret, ASM_FLOAT); } return ret; } default: { Fatal() << "Unhandled type in unary: " << curr; } } } Ref visitBinary(Binary* curr) { // normal binary Ref left = visit(curr->left, EXPRESSION_RESULT); Ref right = visit(curr->right, EXPRESSION_RESULT); Ref ret; switch (curr->type.getBasic()) { case Type::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 == 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: { return ValueBuilder::makeBinary(makeSigning(left, ASM_SIGNED), EQ, makeSigning(right, ASM_SIGNED)); } case NeInt32: { return ValueBuilder::makeBinary(makeSigning(left, ASM_SIGNED), NE, makeSigning(right, ASM_SIGNED)); } 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: WASM_UNREACHABLE("should be removed already"); default: WASM_UNREACHABLE("unhandled i32 binary operator"); } break; } case Type::f32: case Type::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: Fatal() << "Unhandled binary float operator: "; } if (curr->type == Type::f32) { return makeAsmCoercion(ret, ASM_FLOAT); } return ret; default: Fatal() << "Unhandled type in binary: " << curr; } return makeAsmCoercion(ret, wasmToAsmType(curr->type)); } Ref visitSelect(Select* curr) { // If the condition has effects that interact with the operands, we must // reorder it to the start. We must also use locals if the values have // side effects, as a JS conditional does not visit both sides. bool useLocals = false; EffectAnalyzer conditionEffects( parent->options, module->features, curr->condition); EffectAnalyzer ifTrueEffects( parent->options, module->features, curr->ifTrue); EffectAnalyzer ifFalseEffects( parent->options, module->features, curr->ifFalse); if (conditionEffects.invalidates(ifTrueEffects) || conditionEffects.invalidates(ifFalseEffects) || ifTrueEffects.hasSideEffects() || ifFalseEffects.hasSideEffects()) { useLocals = true; } if (useLocals) { ScopedTemp tempIfTrue(curr->type, parent, func), tempIfFalse(curr->type, parent, func), tempCondition(Type::i32, parent, func); Ref ifTrue = visit(curr->ifTrue, EXPRESSION_RESULT); Ref ifFalse = visit(curr->ifFalse, EXPRESSION_RESULT); Ref condition = visit(curr->condition, EXPRESSION_RESULT); 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())))); } else { // Simple case without reordering. return ValueBuilder::makeConditional( visit(curr->condition, EXPRESSION_RESULT), visit(curr->ifTrue, EXPRESSION_RESULT), visit(curr->ifFalse, EXPRESSION_RESULT)); } } Ref visitReturn(Return* curr) { if (!curr->value) { return ValueBuilder::makeReturn(Ref()); } Ref val = visit(curr->value, EXPRESSION_RESULT); bool needCoercion = parent->options.optimizeLevel == 0 || standaloneFunction || parent->functionsCallableFromOutside.count(func->name); if (needCoercion) { val = makeAsmCoercion(val, wasmToAsmType(curr->value->type)); } return ValueBuilder::makeReturn(val); } Ref visitMemorySize(MemorySize* curr) { return ValueBuilder::makeCall(WASM_MEMORY_SIZE); } Ref visitMemoryGrow(MemoryGrow* curr) { if (module->memory.exists && module->memory.max > module->memory.initial) { return ValueBuilder::makeCall( WASM_MEMORY_GROW, makeAsmCoercion(visit(curr->delta, EXPRESSION_RESULT), wasmToAsmType(curr->delta->type))); } else { return ValueBuilder::makeCall(ABORT_FUNC); } } Ref visitNop(Nop* curr) { return ValueBuilder::makeToplevel(); } Ref visitPrefetch(Prefetch* curr) { return ValueBuilder::makeToplevel(); } Ref visitUnreachable(Unreachable* curr) { return ValueBuilder::makeCall(ABORT_FUNC); } // Atomics struct HeapAndPointer { Ref heap; Ref ptr; }; HeapAndPointer getHeapAndAdjustedPointer(Index bytes, Expression* ptr, Index offset) { IString heap; Ref adjustedPtr = makePointer(ptr, offset); switch (bytes) { case 1: heap = HEAP8; break; case 2: heap = HEAP16; adjustedPtr = ValueBuilder::makePtrShift(adjustedPtr, 1); break; case 4: heap = HEAP32; adjustedPtr = ValueBuilder::makePtrShift(adjustedPtr, 2); break; default: { WASM_UNREACHABLE("unimp"); } } return {ValueBuilder::makeName(heap), adjustedPtr}; } Ref visitAtomicRMW(AtomicRMW* curr) { auto hap = getHeapAndAdjustedPointer(curr->bytes, curr->ptr, curr->offset); IString target; switch (curr->op) { case RMWAdd: target = IString("add"); break; case RMWSub: target = IString("sub"); break; case RMWAnd: target = IString("and"); break; case RMWOr: target = IString("or"); break; case RMWXor: target = IString("xor"); break; case RMWXchg: target = IString("exchange"); break; default: WASM_UNREACHABLE("unimp"); } Ref call = ValueBuilder::makeCall( ValueBuilder::makeDot(ValueBuilder::makeName(ATOMICS), target)); ValueBuilder::appendToCall(call, hap.heap); ValueBuilder::appendToCall(call, hap.ptr); ValueBuilder::appendToCall(call, visit(curr->value, EXPRESSION_RESULT)); return call; } Ref visitAtomicCmpxchg(AtomicCmpxchg* curr) { auto hap = getHeapAndAdjustedPointer(curr->bytes, curr->ptr, curr->offset); Ref expected = visit(curr->expected, EXPRESSION_RESULT); Ref replacement = visit(curr->replacement, EXPRESSION_RESULT); Ref call = ValueBuilder::makeCall(ValueBuilder::makeDot( ValueBuilder::makeName(ATOMICS), COMPARE_EXCHANGE)); ValueBuilder::appendToCall(call, hap.heap); ValueBuilder::appendToCall(call, hap.ptr); ValueBuilder::appendToCall(call, expected); ValueBuilder::appendToCall(call, replacement); return makeAsmCoercion(call, wasmToAsmType(curr->type)); } Ref visitAtomicWait(AtomicWait* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitAtomicNotify(AtomicNotify* curr) { Ref call = ValueBuilder::makeCall(ValueBuilder::makeDot( ValueBuilder::makeName(ATOMICS), IString("notify"))); ValueBuilder::appendToCall(call, ValueBuilder::makeName(HEAP32)); ValueBuilder::appendToCall( call, ValueBuilder::makePtrShift(makePointer(curr->ptr, curr->offset), 2)); ValueBuilder::appendToCall(call, visit(curr->notifyCount, EXPRESSION_RESULT)); return call; } Ref visitAtomicFence(AtomicFence* curr) { // Sequentially consistent fences can be lowered to no operation return ValueBuilder::makeToplevel(); } // TODOs Ref visitSIMDExtract(SIMDExtract* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitSIMDReplace(SIMDReplace* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitSIMDShuffle(SIMDShuffle* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitSIMDTernary(SIMDTernary* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitSIMDShift(SIMDShift* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitSIMDLoad(SIMDLoad* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitSIMDLoadStoreLane(SIMDLoadStoreLane* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitMemoryInit(MemoryInit* curr) { ABI::wasm2js::ensureHelpers(module, ABI::wasm2js::MEMORY_INIT); return ValueBuilder::makeCall(ABI::wasm2js::MEMORY_INIT, ValueBuilder::makeNum(curr->segment), visit(curr->dest, EXPRESSION_RESULT), visit(curr->offset, EXPRESSION_RESULT), visit(curr->size, EXPRESSION_RESULT)); } Ref visitDataDrop(DataDrop* curr) { ABI::wasm2js::ensureHelpers(module, ABI::wasm2js::DATA_DROP); return ValueBuilder::makeCall(ABI::wasm2js::DATA_DROP, ValueBuilder::makeNum(curr->segment)); } Ref visitMemoryCopy(MemoryCopy* curr) { ABI::wasm2js::ensureHelpers(module, ABI::wasm2js::MEMORY_COPY); return ValueBuilder::makeCall(ABI::wasm2js::MEMORY_COPY, visit(curr->dest, EXPRESSION_RESULT), visit(curr->source, EXPRESSION_RESULT), visit(curr->size, EXPRESSION_RESULT)); } Ref visitMemoryFill(MemoryFill* curr) { ABI::wasm2js::ensureHelpers(module, ABI::wasm2js::MEMORY_FILL); return ValueBuilder::makeCall(ABI::wasm2js::MEMORY_FILL, visit(curr->dest, EXPRESSION_RESULT), visit(curr->value, EXPRESSION_RESULT), visit(curr->size, EXPRESSION_RESULT)); } Ref visitRefNull(RefNull* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitRefIsNull(RefIsNull* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitRefFunc(RefFunc* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitRefEq(RefEq* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitTry(Try* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitThrow(Throw* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitRethrow(Rethrow* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitBrOnExn(BrOnExn* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitPop(Pop* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitTupleMake(TupleMake* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitTupleExtract(TupleExtract* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitI31New(I31New* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitI31Get(I31Get* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitCallRef(CallRef* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitRefTest(RefTest* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitRefCast(RefCast* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitBrOnCast(BrOnCast* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitRttCanon(RttCanon* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitRttSub(RttSub* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitStructNew(StructNew* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitStructGet(StructGet* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitStructSet(StructSet* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitArrayNew(ArrayNew* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitArrayGet(ArrayGet* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitArraySet(ArraySet* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } Ref visitArrayLen(ArrayLen* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); } private: Ref makePointer(Expression* ptr, Address offset) { auto ret = visit(ptr, EXPRESSION_RESULT); if (offset) { ret = makeAsmCoercion( ValueBuilder::makeBinary(ret, PLUS, ValueBuilder::makeNum(offset)), ASM_INT); } return ret; } void unimplemented(Expression* curr) { Fatal() << "wasm2js cannot convert " << getExpressionName(curr); } }; return ExpressionProcessor(this, m, func, standaloneFunction).process(); } void Wasm2JSBuilder::addMemoryFuncs(Ref ast, Module* wasm) { Ref memorySizeFunc = ValueBuilder::makeFunction(WASM_MEMORY_SIZE); memorySizeFunc[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(memorySizeFunc); if (wasm->memory.max > wasm->memory.initial) { addMemoryGrowFunc(ast, wasm); } } void Wasm2JSBuilder::addMemoryGrowFunc(Ref ast, Module* wasm) { Ref memoryGrowFunc = ValueBuilder::makeFunction(WASM_MEMORY_GROW); ValueBuilder::appendArgumentToFunction(memoryGrowFunc, IString("pagesToAdd")); memoryGrowFunc[3]->push_back( ValueBuilder::makeStatement(ValueBuilder::makeBinary( ValueBuilder::makeName(IString("pagesToAdd")), SET, makeAsmCoercion(ValueBuilder::makeName(IString("pagesToAdd")), AsmType::ASM_INT)))); Ref oldPages = ValueBuilder::makeVar(); memoryGrowFunc[3]->push_back(oldPages); ValueBuilder::appendToVar( oldPages, IString("oldPages"), makeAsmCoercion(ValueBuilder::makeCall(WASM_MEMORY_SIZE), AsmType::ASM_INT)); Ref newPages = ValueBuilder::makeVar(); memoryGrowFunc[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(); memoryGrowFunc[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::kMaxSize32))), 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::makeName(INT8ARRAY), ValueBuilder::makeName(IString("newBuffer"))))); ValueBuilder::appendToBlock( block, ValueBuilder::makeCall( ValueBuilder::makeDot(ValueBuilder::makeName(IString("newHEAP8")), IString("set")), ValueBuilder::makeName(HEAP8))); auto setHeap = [&](IString name, IString view) { ValueBuilder::appendToBlock( block, ValueBuilder::makeBinary( ValueBuilder::makeName(name), SET, ValueBuilder::makeNew(ValueBuilder::makeCall( ValueBuilder::makeName(view), ValueBuilder::makeName(IString("newBuffer")))))); }; setHeap(HEAP8, INT8ARRAY); 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")))); // apply the changes to the memory import if (wasm->memory.imported()) { ValueBuilder::appendToBlock( block, ValueBuilder::makeBinary( ValueBuilder::makeDot(ValueBuilder::makeName("memory"), ValueBuilder::makeName(BUFFER)), SET, ValueBuilder::makeName(BUFFER))); } if (needsBufferView(*wasm)) { ValueBuilder::appendToBlock( block, ValueBuilder::makeBinary(ValueBuilder::makeName("bufferView"), SET, ValueBuilder::makeName(HEAPU8))); } memoryGrowFunc[3]->push_back( ValueBuilder::makeReturn(ValueBuilder::makeName(IString("oldPages")))); ast->push_back(memoryGrowFunc); } // 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 emitPreEmscripten(); void emitPreES6(); void emitPostEmscripten(); void emitPostES6(); void emitMemory(); void emitSpecialSupport(); }; void Wasm2JSGlue::emitPre() { if (flags.emscripten) { emitPreEmscripten(); } else { emitPreES6(); } if (isTableExported(wasm)) { out << "function Table(ret) {\n"; if (wasm.table.initial == wasm.table.max) { out << " // grow method not included; table is not growable\n"; } else { out << " ret.grow = function(by) {\n" << " var old = this.length;\n" << " this.length = this.length + by;\n" << " return old;\n" << " };\n"; } out << " ret.set = function(i, func) {\n" << " this[i] = func;\n" << " };\n" << " ret.get = function(i) {\n" << " return this[i];\n" << " };\n" << " return ret;\n" << "}\n\n"; } emitMemory(); emitSpecialSupport(); } void Wasm2JSGlue::emitPreEmscripten() { out << "function instantiate(asmLibraryArg) {\n"; } void Wasm2JSGlue::emitPreES6() { std::unordered_map 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"; } baseModuleMap[base] = module; out << "import { " << asmangle(base.str) << " } from '" << module.str << "';\n"; }; ImportInfo imports(wasm); ModuleUtils::iterImportedGlobals( wasm, [&](Global* import) { noteImport(import->module, import->base); }); ModuleUtils::iterImportedTables( wasm, [&](Table* import) { noteImport(import->module, import->base); }); ModuleUtils::iterImportedFunctions(wasm, [&](Function* import) { // The special helpers are emitted in the glue, see code and comments // below. if (ABI::wasm2js::isHelper(import->base)) { return; } noteImport(import->module, import->base); }); out << '\n'; } void Wasm2JSGlue::emitPost() { if (flags.emscripten) { emitPostEmscripten(); } else { emitPostES6(); } } void Wasm2JSGlue::emitPostEmscripten() { out << " return asmFunc(asmLibraryArg);\n}\n"; } void Wasm2JSGlue::emitPostES6() { // 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. if (wasm.memory.exists && wasm.memory.imported()) { out << "var mem" << moduleName.str << " = new ArrayBuffer(" << wasm.memory.initial.addr * Memory::kPageSize << ");\n"; } // Actually invoke the `asmFunc` generated function, passing in all global // values followed by all imports out << "var ret" << moduleName.str << " = " << moduleName.str << "("; out << " { abort: function() { throw new Error('abort'); }"; ModuleUtils::iterImportedFunctions(wasm, [&](Function* import) { // The special helpers are emitted in the glue, see code and comments // below. if (ABI::wasm2js::isHelper(import->base)) { return; } out << ",\n " << asmangle(import->base.str); }); ModuleUtils::iterImportedMemories(wasm, [&](Memory* import) { // The special helpers are emitted in the glue, see code and comments // below. if (ABI::wasm2js::isHelper(import->base)) { return; } out << ",\n " << asmangle(import->base.str) << ": { buffer : mem" << moduleName.str << " }"; }); ModuleUtils::iterImportedTables(wasm, [&](Table* import) { // The special helpers are emitted in the glue, see code and comments // below. if (ABI::wasm2js::isHelper(import->base)) { return; } out << ",\n " << asmangle(import->base.str); }); out << "\n });\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 var " << asmangle(exp->name.str) << " = ret" << moduleName.str << "." << asmangle(exp->name.str) << ";\n"; } } void Wasm2JSGlue::emitMemory() { if (needsBufferView(wasm)) { // Create a helper bufferView to access the buffer if we need one. We use it // for creating memory segments if we have any (we may not if the segments // are shipped in a side .mem file, for example), and also in bulk memory // operations. // This will get assigned during `asmFunc` (and potentially re-assigned // during __wasm_memory_grow). // TODO: We should probably just share a single HEAPU8 var. out << " var bufferView;\n"; } // If there are no memory segments, we don't need to emit any support code for // segment creation. if ((!wasm.memory.exists) || wasm.memory.segments.empty()) { return; } // If we have passive memory segments, we need to store those. for (auto& seg : wasm.memory.segments) { if (seg.isPassive) { out << " var memorySegments = {};\n"; break; } } out << R"( var base64ReverseLookup = new Uint8Array(123/*'z'+1*/); for (var i = 25; i >= 0; --i) { base64ReverseLookup[48+i] = 52+i; // '0-9' base64ReverseLookup[65+i] = i; // 'A-Z' base64ReverseLookup[97+i] = 26+i; // 'a-z' } base64ReverseLookup[43] = 62; // '+' base64ReverseLookup[47] = 63; // '/' /** @noinline Inlining this function would mean expanding the base64 string 4x times in the source code, which Closure seems to be happy to do. */ function base64DecodeToExistingUint8Array(uint8Array, offset, b64) { var b1, b2, i = 0, j = offset, bLength = b64.length, end = offset + (bLength*3>>2) - (b64[bLength-2] == '=') - (b64[bLength-1] == '='); for (; i < bLength; i += 4) { b1 = base64ReverseLookup[b64.charCodeAt(i+1)]; b2 = base64ReverseLookup[b64.charCodeAt(i+2)]; uint8Array[j++] = base64ReverseLookup[b64.charCodeAt(i)] << 2 | b1 >> 4; if (j < end) uint8Array[j++] = b1 << 4 | b2 >> 2; if (j < end) uint8Array[j++] = b2 << 6 | base64ReverseLookup[b64.charCodeAt(i+3)]; })"; if (wasm.features.hasBulkMemory()) { // Passive segments in bulk memory are initialized into new arrays that are // passed into here, and we need to return them. out << R"( return uint8Array;)"; } out << R"( } )"; for (Index i = 0; i < wasm.memory.segments.size(); i++) { auto& seg = wasm.memory.segments[i]; if (seg.isPassive) { // Fancy passive segments are decoded into typed arrays on the side, for // later copying. out << "memorySegments[" << i << "] = base64DecodeToExistingUint8Array(new Uint8Array(" << seg.data.size() << ")" << ", 0, \"" << base64Encode(seg.data) << "\");\n"; } } if (hasActiveSegments(wasm)) { auto globalOffset = [&](const Memory::Segment& segment) { if (auto* c = segment.offset->dynCast()) { return std::to_string(c->value.getInteger()); } if (auto* get = segment.offset->dynCast()) { auto internalName = get->name; auto importedName = wasm.getGlobal(internalName)->base; return std::string("imports[") + asmangle(importedName.str) + "]"; } Fatal() << "non-constant offsets aren't supported yet\n"; }; out << "function initActiveSegments(imports) {\n"; for (Index i = 0; i < wasm.memory.segments.size(); i++) { auto& seg = wasm.memory.segments[i]; if (!seg.isPassive) { // Plain active segments are decoded directly into the main memory. out << " base64DecodeToExistingUint8Array(bufferView, " << globalOffset(seg) << ", \"" << base64Encode(seg.data) << "\");\n"; } } out << "}\n"; } } void Wasm2JSGlue::emitSpecialSupport() { // The special support functions are emitted as part of the JS glue, if we // need them. bool need = false; ModuleUtils::iterImportedFunctions(wasm, [&](Function* import) { if (ABI::wasm2js::isHelper(import->base)) { need = true; } }); if (!need) { return; } // Scratch memory uses 3 indexes, each referring to 4 bytes. Indexes 0, 1 are // used for 64-bit operations, while 2 is for 32-bit. These operations need // separate indexes because we need to handle the case where the optimizer // reorders a 32-bit reinterpret in between a 64-bit's split-out parts. // That situation can occur because the 64-bit reinterpret was split up into // pieces early, in the 64-bit lowering pass, while the 32-bit reinterprets // are lowered only at the very end, and until then the optimizer sees wasm // reinterprets which have no side effects (but they will have the side effect // of altering scratch memory). That is, conceptual code like this: // // a = reinterpret_64(b) // x = reinterpret_32(y) // // turns into // // scratch_write(b) // a_low = scratch_read(0) // a_high = scratch_read(1) // x = reinterpret_32(y) // // (Note how the single wasm instruction for a 64-bit reinterpret turns into // multiple operations. We have to do such splitting, because in JS we will // have to have separate operations to receive each 32-bit chunk anyhow. A // *32*-bit reinterpret *could* be a single function, but given we have the // separate functions anyhow for the 64-bit case, it's more compact to reuse // those.) // At this point, the scratch_* functions look like they have side effects to // the optimizer (which is true, as they modify scratch memory), but the // reinterpret_32 is still a normal wasm instruction without side effects, so // the optimizer might do this: // // scratch_write(b) // a_low = scratch_read(0) // x = reinterpret_32(y) ;; this moved one line up // a_high = scratch_read(1) // // When we do lower the reinterpret_32 into JS, we get: // // scratch_write(b) // a_low = scratch_read(0) // scratch_write(y) // x = scratch_read() // a_high = scratch_read(1) // // The second write occurs before the first's values have been read, so they // interfere. // // There isn't a problem with reordering 32-bit reinterprets with each other // as each is lowered into a pair of write+read in JS (after the wasm // optimizer runs), so they are guaranteed to be adjacent (and a JS optimizer // that runs later will handle that ok since they are calls, which can always // have side effects). out << R"( var scratchBuffer = new ArrayBuffer(16); var i32ScratchView = new Int32Array(scratchBuffer); var f32ScratchView = new Float32Array(scratchBuffer); var f64ScratchView = new Float64Array(scratchBuffer); )"; ModuleUtils::iterImportedFunctions(wasm, [&](Function* import) { if (import->base == ABI::wasm2js::SCRATCH_STORE_I32) { out << R"( function wasm2js_scratch_store_i32(index, value) { i32ScratchView[index] = value; } )"; } else if (import->base == ABI::wasm2js::SCRATCH_LOAD_I32) { out << R"( function wasm2js_scratch_load_i32(index) { return i32ScratchView[index]; } )"; } else if (import->base == ABI::wasm2js::SCRATCH_STORE_F32) { out << R"( function wasm2js_scratch_store_f32(value) { f32ScratchView[2] = value; } )"; } else if (import->base == ABI::wasm2js::SCRATCH_LOAD_F32) { out << R"( function wasm2js_scratch_load_f32() { return f32ScratchView[2]; } )"; } else if (import->base == ABI::wasm2js::SCRATCH_STORE_F64) { out << R"( function wasm2js_scratch_store_f64(value) { f64ScratchView[0] = value; } )"; } else if (import->base == ABI::wasm2js::SCRATCH_LOAD_F64) { out << R"( function wasm2js_scratch_load_f64() { return f64ScratchView[0]; } )"; } else if (import->base == ABI::wasm2js::MEMORY_INIT) { out << R"( function wasm2js_memory_init(segment, dest, offset, size) { // TODO: traps on invalid things bufferView.set(memorySegments[segment].subarray(offset, offset + size), dest); } )"; } else if (import->base == ABI::wasm2js::MEMORY_FILL) { out << R"( function wasm2js_memory_fill(dest, value, size) { dest = dest >>> 0; size = size >>> 0; if (dest + size > bufferView.length) throw "trap: invalid memory.fill"; bufferView.fill(value, dest, dest + size); } )"; } else if (import->base == ABI::wasm2js::MEMORY_COPY) { out << R"( function wasm2js_memory_copy(dest, source, size) { // TODO: traps on invalid things bufferView.copyWithin(dest, source, source + size); } )"; } else if (import->base == ABI::wasm2js::DATA_DROP) { out << R"( function wasm2js_data_drop(segment) { // TODO: traps on invalid things memorySegments[segment] = new Uint8Array(0); } )"; } else if (import->base == ABI::wasm2js::ATOMIC_WAIT_I32) { out << R"( function wasm2js_atomic_wait_i32(ptr, expected, timeoutLow, timeoutHigh) { if (timeoutLow != -1 || timeoutHigh != -1) throw 'unsupported timeout'; var view = new Int32Array(bufferView.buffer); // TODO cache var result = Atomics.wait(view, ptr, expected); if (result == 'ok') return 0; if (result == 'not-equal') return 1; if (result == 'timed-out') return 2; throw 'bad result ' + result; } )"; } else if (import->base == ABI::wasm2js::ATOMIC_RMW_I64) { out << R"( function wasm2js_atomic_rmw_i64(op, bytes, offset, ptr, valueLow, valueHigh) { assert(bytes == 8); // TODO: support 1, 2, 4 as well var view = new BigInt64Array(bufferView.buffer); // TODO cache ptr = (ptr + offset) >> 3; var value = BigInt(valueLow >>> 0) | (BigInt(valueHigh >>> 0) << BigInt(32)); var result; switch (op) { case 0: { // Add result = Atomics.add(view, ptr, value); break; } case 1: { // Sub result = Atomics.sub(view, ptr, value); break; } case 2: { // And result = Atomics.and(view, ptr, value); break; } case 3: { // Or result = Atomics.or(view, ptr, value); break; } case 4: { // Xor result = Atomics.xor(view, ptr, value); break; } case 5: { // Xchg result = Atomics.exchange(view, ptr, value); break; } default: throw 'bad op'; } var low = Number(result & BigInt(0xffffffff)) | 0; var high = Number((result >> BigInt(32)) & BigInt(0xffffffff)) | 0; stashedBits = high; return low; } )"; } else if (import->base == ABI::wasm2js::GET_STASHED_BITS) { out << R"( var stashedBits = 0; function wasm2js_get_stashed_bits() { return stashedBits; } )"; } }); out << '\n'; } } // namespace wasm #endif // wasm_wasm2js_h