blob: 2c48e5be0644e6b3c848ab18d003e5be5b29f112 [file] [log] [blame] [edit]
/*
* 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.
*/
//
// wasm2js console tool
//
#include "wasm2js.h"
#include "optimization-options.h"
#include "parser/wat-parser.h"
#include "pass.h"
#include "support/colors.h"
#include "support/command-line.h"
#include "support/file.h"
using namespace cashew;
using namespace wasm;
using namespace wasm::WATParser;
// helpers
namespace {
static void optimizeWasm(Module& wasm, PassOptions options) {
// Perform various optimizations that will be good for JS, but would not be
// great for wasm in general
struct OptimizeForJS : public WalkerPass<PostWalker<OptimizeForJS>> {
bool isFunctionParallel() override { return true; }
std::unique_ptr<Pass> create() override {
return std::make_unique<OptimizeForJS>();
}
void visitBinary(Binary* curr) {
// x - -c (where c is a constant) is larger than x + c, in js (but not
// necessarily in wasm, where LEBs prefer negatives).
if (curr->op == SubInt32) {
if (auto* c = curr->right->dynCast<Const>()) {
if (c->value.geti32() < 0) {
curr->op = AddInt32;
c->value = c->value.neg();
}
}
}
}
};
PassRunner runner(&wasm, options);
OptimizeForJS().run(&runner, &wasm);
}
template<typename T> static void printJS(Ref ast, T& output) {
JSPrinter jser(true, true, ast);
jser.printAst();
output << jser.buffer << '\n';
}
// Traversals
struct TraverseInfo {
TraverseInfo() = default;
TraverseInfo(Ref node) : node(node) {
assert(node.get());
if (node->isArray()) {
for (size_t i = 0; i < node->size(); i++) {
maybeAdd(node[i]);
}
} else if (node->isAssign()) {
auto assign = node->asAssign();
maybeAdd(assign->target());
maybeAdd(assign->value());
} else if (node->isAssignName()) {
auto assign = node->asAssignName();
maybeAdd(assign->value());
} else {
// no children
}
}
Ref node;
bool scanned = false;
std::vector<Ref> children;
private:
void maybeAdd(Ref child) {
if (child.get()) {
children.push_back(child);
}
}
};
// Traverse, calling visit after the children
static void traversePrePost(Ref node,
std::function<void(Ref)> visitPre,
std::function<void(Ref)> visitPost) {
std::vector<TraverseInfo> stack;
stack.push_back(TraverseInfo(node));
while (!stack.empty()) {
TraverseInfo& back = stack.back();
if (!back.scanned) {
back.scanned = true;
// This is the first time we see this.
visitPre(back.node);
for (auto child : back.children) {
stack.emplace_back(child);
}
continue;
}
// Time to post-visit the node itself
auto node = back.node;
stack.pop_back();
visitPost(node);
}
}
static void traversePost(Ref node, std::function<void(Ref)> visit) {
traversePrePost(
node, [](Ref node) {}, visit);
}
static void replaceInPlace(Ref target, Ref value) {
assert(target->isArray() && value->isArray());
target->setSize(value->size());
for (size_t i = 0; i < value->size(); i++) {
target[i] = value[i];
}
}
static void replaceInPlaceIfPossible(Ref target, Ref value) {
if (target->isArray() && value->isArray()) {
replaceInPlace(target, value);
}
}
static void optimizeJS(Ref ast, Wasm2JSBuilder::Flags flags) {
// Helpers
auto isBinary = [](Ref node, IString op) {
return node->isArray() && !node->empty() && node[0] == BINARY &&
node[1] == op;
};
auto isConstantBinary = [&](Ref node, IString op, int num) {
return isBinary(node, op) && node[3]->isNumber() &&
node[3]->getNumber() == num;
};
auto isOrZero = [&](Ref node) { return isConstantBinary(node, OR, 0); };
auto isTrshiftZero = [&](Ref node) {
return isConstantBinary(node, TRSHIFT, 0);
};
auto isPlus = [](Ref node) {
return node->isArray() && !node->empty() && node[0] == UNARY_PREFIX &&
node[1] == PLUS;
};
auto isFround = [](Ref node) {
return node->isArray() && !node->empty() && node[0] == cashew::CALL &&
node[1] == MATH_FROUND;
};
auto isBitwise = [](Ref node) {
if (node->isArray() && !node->empty() && node[0] == BINARY) {
auto op = node[1];
return op == OR || op == AND || op == XOR || op == RSHIFT ||
op == TRSHIFT || op == LSHIFT;
}
return false;
};
auto isSignedBitwise = [](Ref node) {
if (node->isArray() && !node->empty() && node[0] == BINARY) {
auto op = node[1];
return op == OR || op == AND || op == XOR || op == RSHIFT || op == LSHIFT;
}
return false;
};
auto isUnary = [](Ref node, IString op) {
return node->isArray() && !node->empty() && node[0] == UNARY_PREFIX &&
node[1] == op;
};
auto isWhile = [](Ref node) {
return node->isArray() && !node->empty() && node[0] == WHILE;
};
auto isDo = [](Ref node) {
return node->isArray() && !node->empty() && node[0] == DO;
};
auto isIf = [](Ref node) {
return node->isArray() && !node->empty() && node[0] == IF;
};
auto removeOrZero = [&](Ref node) {
while (isOrZero(node)) {
node = node[2];
}
return node;
};
auto removePlus = [&](Ref node) {
while (isPlus(node)) {
node = node[2];
}
return node;
};
auto removePlusAndFround = [&](Ref node) {
while (1) {
if (isFround(node)) {
node = node[2][0];
} else if (isPlus(node)) {
node = node[2];
} else {
break;
}
}
return node;
};
auto getHeapFromAccess = [](Ref node) { return node[1]->getIString(); };
auto setHeapOnAccess = [](Ref node, IString heap) {
node[1] = ValueBuilder::makeName(heap);
};
auto isIntegerHeap = [](IString heap) {
return heap == HEAP8 || heap == HEAPU8 || heap == HEAP16 ||
heap == HEAPU16 || heap == HEAP32 || heap == HEAPU32;
};
auto isFloatHeap = [](IString heap) {
return heap == HEAPF32 || heap == HEAPF64;
};
auto isHeapAccess = [&](Ref node) {
if (node->isArray() && !node->empty() && node[0] == SUB &&
node[1]->isString()) {
auto heap = getHeapFromAccess(node);
return isIntegerHeap(heap) || isFloatHeap(heap);
}
return false;
};
// Optimize given that the expression is flowing into a boolean context
auto optimizeBoolean = [&](Ref node) {
// TODO: in some cases it may be possible to turn
//
// if (x | 0)
//
// into
//
// if (x)
//
// In general this is unsafe if e.g. x is -2147483648 + -2147483648 (which
// the | 0 turns into 0, but without it is a truthy value).
//
// Another issue is that in deterministic mode we care about corner cases
// that would trap in wasm, like an integer divide by zero:
//
// if ((1 / 0) | 0) => condition is Infinity | 0 = 0 which is falsey
//
// while
//
// if (1 / 0) => condition is Infinity which is truthy
//
// Thankfully this is not common, and does not occur on % (1 % 0 is a NaN
// which has the right truthiness), so we could perhaps do
//
// if (!(flags.deterministic && isBinary(node[2], DIV))) return node[2];
//
// (but there is still the first issue).
return node;
};
// Optimizations
// Pre-simplification
traversePost(ast, [&](Ref node) {
// x >> 0 => x | 0
if (isConstantBinary(node, RSHIFT, 0)) {
node[1]->setString(OR);
}
});
traversePost(ast, [&](Ref node) {
if (isBitwise(node)) {
// x | 0 going into a bitwise op => skip the | 0
node[2] = removeOrZero(node[2]);
node[3] = removeOrZero(node[3]);
// (x | 0 or similar) | 0 => (x | 0 or similar)
if (isOrZero(node)) {
if (isSignedBitwise(node[2])) {
replaceInPlace(node, node[2]);
}
}
if (isHeapAccess(node[2])) {
auto heap = getHeapFromAccess(node[2]);
IString replacementHeap;
// We can avoid a cast of a load by using the load to do it instead.
if (isOrZero(node)) {
if (isIntegerHeap(heap)) {
replacementHeap = heap;
}
} else if (isTrshiftZero(node)) {
// For signed or unsigned loads smaller than 32 bits, doing an | 0
// was safe either way - they aren't in the range an | 0 can affect.
// For >>> 0 however, a negative value would change, so we still
// need the cast.
if (heap == HEAP32 || heap == HEAPU32) {
replacementHeap = HEAPU32;
} else if (heap == HEAPU16) {
replacementHeap = HEAPU16;
} else if (heap == HEAPU8) {
replacementHeap = HEAPU8;
}
}
if (!replacementHeap.isNull()) {
setHeapOnAccess(node[2], replacementHeap);
replaceInPlace(node, node[2]);
return;
}
// A load into an & may allow using a simpler heap, e.g. HEAPU8[..] & 1
// (a load of a boolean) may be HEAP8[..] & 1. The signed heaps are more
// commonly used, so it compresses better, and also they seem to have
// better performance (perhaps since HEAPU32 is at risk of not being a
// smallint).
if (node[1] == AND) {
if (isConstantBinary(node, AND, 1)) {
if (heap == HEAPU8) {
setHeapOnAccess(node[2], HEAP8);
} else if (heap == HEAPU16) {
setHeapOnAccess(node[2], HEAP16);
}
}
}
}
// Pre-compute constant [op] constant, which the lowering can generate
// in loads etc.
if (node[2]->isNumber() && node[3]->isNumber()) {
int32_t left = node[2]->getNumber();
int32_t right = node[3]->getNumber();
if (node[1] == OR) {
node->setNumber(left | right);
} else if (node[1] == AND) {
node->setNumber(left & right);
} else if (node[1] == XOR) {
node->setNumber(left ^ right);
} else if (node[1] == LSHIFT) {
node->setNumber(left << (right & 31));
} else if (node[1] == RSHIFT) {
node->setNumber(int32_t(left) >> int32_t(right & 31));
} else if (node[1] == TRSHIFT) {
node->setNumber(uint32_t(left) >> uint32_t(right & 31));
}
return;
}
}
// +(+x) => +x
else if (isPlus(node)) {
node[2] = removePlus(node[2]);
}
// +(+x) => +x
else if (isFround(node)) {
node[2] = removePlusAndFround(node[2]);
} else if (isUnary(node, L_NOT)) {
node[2] = optimizeBoolean(node[2]);
}
// Add/subtract can merge coercions up, except when a child is a division,
// which needs to be eagerly truncated to remove fractional results.
else if (isBinary(node, PLUS) || isBinary(node, MINUS)) {
auto left = node[2];
auto right = node[3];
if (isOrZero(left) && isOrZero(right) && !isBinary(left[2], DIV) &&
!isBinary(right[2], DIV)) {
auto op = node[1]->getIString();
// Add a coercion on top.
node[1]->setString(OR);
node[2] = left;
node[3] = ValueBuilder::makeNum(0);
// Add/subtract the inner uncoerced values.
left[1]->setString(op);
left[3] = right[2];
}
}
// Assignment into a heap coerces.
else if (node->isAssign()) {
auto assign = node->asAssign();
auto target = assign->target();
if (isHeapAccess(target)) {
auto heap = getHeapFromAccess(target);
if (isIntegerHeap(heap)) {
if (heap == HEAP8 || heap == HEAPU8) {
while (isOrZero(assign->value()) ||
isConstantBinary(assign->value(), AND, 255)) {
assign->value() = assign->value()[2];
}
} else if (heap == HEAP16 || heap == HEAPU16) {
while (isOrZero(assign->value()) ||
isConstantBinary(assign->value(), AND, 65535)) {
assign->value() = assign->value()[2];
}
} else {
assert(heap == HEAP32 || heap == HEAPU32);
assign->value() = removeOrZero(assign->value());
}
} else {
assert(isFloatHeap(heap));
if (heap == HEAPF32) {
assign->value() = removePlusAndFround(assign->value());
} else {
assign->value() = removePlus(assign->value());
}
}
}
} else if (isWhile(node) || isDo(node) || isIf(node)) {
node[1] = optimizeBoolean(node[1]);
}
});
// Remove unnecessary break/continue labels, when the name is that of the
// highest target anyhow, which we would reach without the name.
std::vector<Ref> breakCapturers;
std::vector<Ref> continueCapturers;
std::unordered_map<IString, Ref>
labelToValue; // maps the label to the loop/etc.
std::unordered_set<Value*> labelled; // all things with a label on them.
Value INVALID;
traversePrePost(
ast,
[&](Ref node) {
if (node->isArray() && !node->empty()) {
if (node[0] == LABEL) {
auto label = node[1]->getIString();
labelToValue[label] = node[2];
labelled.insert(node[2].get());
} else if (node[0] == WHILE || node[0] == DO || node[0] == FOR) {
breakCapturers.push_back(node);
continueCapturers.push_back(node);
} else if (node[0] == cashew::BLOCK) {
if (labelled.count(node.get())) {
// Cannot break to a block without the label.
breakCapturers.push_back(Ref(&INVALID));
}
} else if (node[0] == SWITCH) {
breakCapturers.push_back(node);
}
}
},
[&](Ref node) {
if (node->isArray() && !node->empty()) {
if (node[0] == LABEL) {
auto label = node[1]->getIString();
labelToValue.erase(label);
labelled.erase(node[2].get());
} else if (node[0] == WHILE || node[0] == DO || node[0] == FOR) {
breakCapturers.pop_back();
continueCapturers.pop_back();
} else if (node[0] == cashew::BLOCK) {
if (labelled.count(node.get())) {
breakCapturers.pop_back();
}
} else if (node[0] == SWITCH) {
breakCapturers.pop_back();
} else if (node[0] == BREAK || node[0] == CONTINUE) {
if (!node[1]->isNull()) {
auto label = node[1]->getIString();
assert(labelToValue.count(label));
auto& capturers =
node[0] == BREAK ? breakCapturers : continueCapturers;
assert(!capturers.empty());
if (capturers.back() == labelToValue[label]) {
// Success, the break/continue goes exactly where we would if we
// didn't have the label!
node[1]->setNull();
}
}
}
}
});
// Remove unnecessary block/loop labels.
std::set<IString> usedLabelNames;
traversePost(ast, [&](Ref node) {
if (node->isArray() && !node->empty()) {
if (node[0] == BREAK || node[0] == CONTINUE) {
if (!node[1]->isNull()) {
auto label = node[1]->getIString();
usedLabelNames.insert(label);
}
} else if (node[0] == LABEL) {
auto label = node[1]->getIString();
if (usedLabelNames.count(label)) {
// It's used; just erase it from the data structure.
usedLabelNames.erase(label);
} else {
// It's not used - get rid of it.
replaceInPlaceIfPossible(node, node[2]);
}
}
}
});
}
static void emitWasm(Module& wasm,
Output& output,
Wasm2JSBuilder::Flags flags,
PassOptions options,
Name name) {
if (options.optimizeLevel > 0) {
optimizeWasm(wasm, options);
}
Wasm2JSBuilder wasm2js(flags, options);
auto js = wasm2js.processWasm(&wasm, name);
if (options.optimizeLevel >= 2) {
optimizeJS(js, flags);
}
Wasm2JSGlue glue(wasm, output, flags, name);
glue.emitPre();
printJS(js, output);
glue.emitPost();
}
class AssertionEmitter {
public:
AssertionEmitter(WASTScript& script,
Output& out,
Wasm2JSBuilder::Flags flags,
const ToolOptions& options)
: script(script), out(out), flags(flags), options(options) {}
void emit();
private:
WASTScript& script;
Output& out;
Wasm2JSBuilder::Flags flags;
ToolOptions options;
Module tempAllocationModule;
Expression* translateInvoke(InvokeAction& invoke, Module& wasm);
Ref emitAssertReturnFunc(AssertReturn& assn,
Module& wasm,
Name testFuncName,
Name asmModule);
Ref emitAssertTrapFunc(InvokeAction& invoke,
Module& wasm,
Name testFuncName,
Name asmModule);
Ref emitInvokeFunc(InvokeAction& invoke,
Module& wasm,
Name testFuncName,
Name asmModule);
void fixCalls(Ref asmjs, Name asmModule);
Ref processFunction(Function* func) {
Wasm2JSBuilder sub(flags, options.passOptions);
return sub.processStandaloneFunction(&tempAllocationModule, func);
}
void emitFunction(Ref func) {
JSPrinter jser(true, true, func);
jser.printAst();
out << jser.buffer << std::endl;
}
};
Expression* AssertionEmitter::translateInvoke(InvokeAction& invoke,
Module& wasm) {
std::vector<Expression*> args;
Builder builder(wasm);
for (auto& arg : invoke.args) {
args.push_back(builder.makeConstantExpression(arg));
}
Type type =
wasm.getFunction(wasm.getExport(invoke.name)->value)->getResults();
return builder.makeCall(invoke.name, args, type);
}
Ref AssertionEmitter::emitAssertReturnFunc(AssertReturn& assn,
Module& wasm,
Name testFuncName,
Name asmModule) {
if (assn.expected.size() > 1) {
Fatal() << "multivalue assert_return not supported";
}
auto* invoke = std::get_if<InvokeAction>(&assn.action);
if (!invoke) {
Fatal() << "only invoke actions are supported in assert_return";
}
Expression* actual = translateInvoke(*invoke, wasm);
Expression* body = nullptr;
Builder builder(wasm);
if (assn.expected.empty()) {
if (actual->type == Type::none) {
body = builder.blockify(actual, builder.makeConst(uint32_t(1)));
} else {
body = actual;
}
} else if (auto* expectedVal = std::get_if<Literal>(&assn.expected[0])) {
if (!expectedVal->type.isBasic()) {
Fatal() << "unsupported type in assert_return: " << expectedVal->type;
}
Expression* expected = builder.makeConstantExpression(*expectedVal);
switch (expected->type.getBasic()) {
case Type::i32:
body = builder.makeBinary(EqInt32, actual, expected);
break;
case Type::i64:
body = builder.makeCall(
"i64Equal",
{actual,
builder.makeCall(WASM_FETCH_HIGH_BITS, {}, Type::i32),
expected},
Type::i32);
break;
case Type::f32: {
body = builder.makeCall("f32Equal", {actual, expected}, Type::i32);
break;
}
case Type::f64: {
body = builder.makeCall("f64Equal", {actual, expected}, Type::i32);
break;
}
default: {
Fatal() << "Unhandled type in assert: " << expected->type;
}
}
} else if (std::get_if<NaNResult>(&assn.expected[0])) {
body = builder.makeCall("isNaN", {actual}, Type::i32);
}
std::unique_ptr<Function> testFunc(
builder.makeFunction(testFuncName,
std::vector<NameType>{},
Signature(Type::none, body->type),
std::vector<NameType>{},
body));
Ref jsFunc = processFunction(testFunc.get());
fixCalls(jsFunc, asmModule);
emitFunction(jsFunc);
return jsFunc;
}
Ref AssertionEmitter::emitAssertTrapFunc(InvokeAction& invoke,
Module& wasm,
Name testFuncName,
Name asmModule) {
Name innerFuncName("f");
Expression* expr = translateInvoke(invoke, wasm);
std::unique_ptr<Function> exprFunc(
Builder(wasm).makeFunction(innerFuncName,
std::vector<NameType>{},
Signature(Type::none, expr->type),
std::vector<NameType>{},
expr));
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::makeInt(1)));
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;
}
Ref AssertionEmitter::emitInvokeFunc(InvokeAction& invoke,
Module& wasm,
Name testFuncName,
Name asmModule) {
Expression* body = translateInvoke(invoke, wasm);
std::unique_ptr<Function> testFunc(
Builder(wasm).makeFunction(testFuncName,
std::vector<NameType>{},
Signature(Type::none, body->type),
std::vector<NameType>{},
body));
Ref jsFunc = processFunction(testFunc.get());
fixCalls(jsFunc, asmModule);
emitFunction(jsFunc);
return jsFunc;
}
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() {
// 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 | 0) == (expected_lo | 0) && (actual_hi | 0) == (expected_hi | 0);
}
)";
Name asmModule = std::string("ret") + ASM_FUNC.toString();
// Track the last built module.
std::shared_ptr<Module> wasm;
for (Index i = 0; i < script.size(); ++i) {
Name testFuncName("check" + std::to_string(i));
auto& cmd = script[i].cmd;
if (auto* mod = std::get_if<WASTModule>(&cmd)) {
if (auto* w = std::get_if<std::shared_ptr<Module>>(mod)) {
wasm = *w;
// We have already done the parse, but we still do this to apply the
// features from the command line.
options.applyOptionsBeforeParse(*wasm);
std::stringstream funcNameS;
funcNameS << ASM_FUNC << i;
std::stringstream moduleNameS;
moduleNameS << "ret" << ASM_FUNC << i;
Name funcName(funcNameS.str());
asmModule = Name(moduleNameS.str());
emitWasm(*wasm, out, flags, options.passOptions, funcName);
} else {
Fatal() << "unsupported quoted module on line " << script[i].line;
}
} else if (auto* assn = std::get_if<Assertion>(&cmd)) {
if (auto* assnRet = std::get_if<AssertReturn>(assn)) {
emitAssertReturnFunc(*assnRet, *wasm, testFuncName, asmModule);
} else if (auto* assnAct = std::get_if<AssertAction>(assn)) {
if (auto* invoke = std::get_if<InvokeAction>(&assnAct->action);
invoke && assnAct->type == ActionAssertionType::Trap &&
flags.pedantic) {
emitAssertTrapFunc(*invoke, *wasm, testFuncName, asmModule);
} else {
// Skip other action assertions.
continue;
}
} else if (std::get_if<AssertModule>(assn)) {
// Skip module assertions
continue;
} else {
Fatal() << "unsupported assertion on line " << script[i].line;
}
out << "if (!" << testFuncName << "()) throw 'assertion failed on line "
<< script[i].line << "';\n";
} else if (auto* act = std::get_if<Action>(&cmd)) {
if (auto* invoke = std::get_if<InvokeAction>(act)) {
emitInvokeFunc(*invoke, *wasm, testFuncName, asmModule);
out << testFuncName << "();\n";
} else {
Fatal() << "unsupported action on line " << script[i].line;
}
} else {
Fatal() << "unsupported command on line " << script[i].line;
}
}
}
} // anonymous namespace
// Main
int main(int argc, const char* argv[]) {
Wasm2JSBuilder::Flags flags;
const std::string Wasm2JSOption = "wasm2js options";
OptimizationOptions options("wasm2js",
"Transform .wasm/.wat files to asm.js");
options
.add("--output",
"-o",
"Output file (stdout if not specified)",
Wasm2JSOption,
Options::Arguments::One,
[](Options* o, const std::string& argument) {
o->extra["output"] = argument;
Colors::setEnabled(false);
})
.add("--allow-asserts",
"",
"Allow compilation of .wast testing asserts",
Wasm2JSOption,
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) {
flags.allowAsserts = true;
o->extra["asserts"] = "1";
})
.add(
"--pedantic",
"",
"Emulate WebAssembly trapping behavior",
Wasm2JSOption,
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) { flags.pedantic = true; })
.add(
"--emscripten",
"",
"Emulate the glue in emscripten-compatible form (and not ES6 module "
"form)",
Wasm2JSOption,
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) { flags.emscripten = true; })
.add(
"--deterministic",
"",
"Replace WebAssembly trapping behavior deterministically "
"(the default is to not care about what would trap in wasm, like a load "
"out of bounds or integer divide by zero; with this flag, we try to be "
"deterministic at least in what happens, which might or might not be "
"to trap like wasm, but at least should not vary)",
Wasm2JSOption,
Options::Arguments::Zero,
[&](Options* o, const std::string& argument) {
flags.deterministic = true;
})
.add(
"--symbols-file",
"",
"Emit a symbols file that maps function indexes to their original names",
Wasm2JSOption,
Options::Arguments::One,
[&](Options* o, const std::string& argument) {
flags.symbolsFile = argument;
})
.add_positional("INFILE",
Options::Arguments::One,
[](Options* o, const std::string& argument) {
o->extra["infile"] = argument;
});
options.parse(argc, argv);
if (options.debug) {
flags.debug = true;
}
std::optional<WASTScript> script;
std::shared_ptr<Module> wasm;
Ref js;
auto& input = options.extra["infile"];
std::string suffix(".wasm");
bool binaryInput =
input.size() >= suffix.size() &&
input.compare(input.size() - suffix.size(), suffix.size(), suffix) == 0;
try {
// If the input filename ends in `.wasm`, then parse it in binary form,
// otherwise assume it's a `*.wat` file and go from there.
//
// Note that we're not using the built-in `ModuleReader` which will also do
// similar logic here because when testing JS files we use the
// `--allow-asserts` flag which means we need to parse the extra
// s-expressions that come at the end of the `*.wast` file after the module
// is defined.
if (binaryInput) {
wasm = std::make_shared<Module>();
options.applyOptionsBeforeParse(*wasm);
ModuleReader reader;
reader.read(input, *wasm, "");
} else {
auto input(read_file<std::string>(options.extra["infile"], Flags::Text));
auto parsed = parseScript(input);
if (auto* err = parsed.getErr()) {
Fatal() << err->msg;
}
script = std::move(*parsed);
// Find the first module in the script.
if (script->empty()) {
Fatal() << "expected module";
}
if (auto* mod = std::get_if<WASTModule>(&(*script)[0].cmd)) {
if (auto* w = std::get_if<std::shared_ptr<Module>>(mod)) {
wasm = *w;
// This isn't actually before the parse, but we can't apply the
// feature options any earlier. FIXME.
options.applyOptionsBeforeParse(*wasm);
}
}
if (!wasm) {
Fatal() << "expected module as first command in script";
}
}
} catch (ParseException& p) {
p.dump(std::cerr);
Fatal() << "error in parsing input";
} catch (std::bad_alloc&) {
Fatal() << "error in building module, std::bad_alloc (possibly invalid "
"request for silly amounts of memory)";
}
// TODO: Remove this restriction when wasm2js can handle multiple tables
if (wasm->tables.size() > 1) {
Fatal() << "error: modules with multiple tables are not supported yet.";
}
options.applyOptionsAfterParse(*wasm);
if (options.passOptions.validate) {
if (!WasmValidator().validate(*wasm)) {
std::cout << *wasm << '\n';
Fatal() << "error in validating input";
}
}
if (options.debug) {
std::cerr << "j-printing..." << std::endl;
}
Output output(options.extra["output"], Flags::Text);
if (script && options.extra["asserts"] == "1") {
AssertionEmitter(*script, output, flags, options).emit();
} else {
emitWasm(*wasm, output, flags, options.passOptions, "asmFunc");
}
if (options.debug) {
std::cerr << "done." << std::endl;
}
}