blob: a96716a021912453c95de199e8d1d93e97c22eb8 [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.
*/
//
// A WebAssembly shell, loads a .wast file (WebAssembly in S-Expression format)
// and executes it. This provides similar functionality as the reference
// interpreter, like assert_* calls, so it can run the spec test suite.
//
#include <setjmp.h>
#include <memory>
#include "pass.h"
#include "support/command-line.h"
#include "support/file.h"
#include "wasm-interpreter.h"
#include "wasm-s-parser.h"
#include "wasm-validator.h"
using namespace cashew;
using namespace wasm;
// Globals
MixedArena globalAllocator;
IString ASSERT_RETURN("assert_return"),
ASSERT_TRAP("assert_trap"),
ASSERT_INVALID("assert_invalid"),
SPECTEST("spectest"),
PRINT("print"),
INVOKE("invoke"),
EXIT("exit");
struct ExitException {
};
//
// Implementation of the shell interpreter execution environment
//
struct ShellExternalInterface : ModuleInstance::ExternalInterface {
// The underlying memory can be accessed through unaligned pointers which
// isn't well-behaved in C++. WebAssembly nonetheless expects it to behave
// properly. Avoid emitting unaligned load/store by checking for alignment
// explicitly, and performing memcpy if unaligned.
//
// The allocated memory tries to have the same alignment as the memory being
// simulated.
class Memory {
// Use char because it doesn't run afoul of aliasing rules.
std::vector<char> memory;
template <typename T>
static bool aligned(const char* address) {
static_assert(!(alignof(T) & (alignof(T) - 1)), "must be a power of 2");
return 0 == (reinterpret_cast<uintptr_t>(address) & (alignof(T) - 1));
}
Memory(Memory&) = delete;
Memory& operator=(const Memory&) = delete;
public:
Memory() {}
void resize(size_t newSize) {
// Allocate at least this size to get proper alignment: the allocator will
// usually start allocating page-sized chunks which are properly aligned.
//
// The code is optimistic this will work until WG21's p0035r0 happens.
const size_t minSize = 1 << 12;
size_t oldSize = memory.size();
memory.resize(std::max(minSize, newSize));
if (newSize < oldSize && newSize < minSize) {
std::memset(&memory[newSize], 0, minSize - newSize);
}
}
template <typename T>
void set(size_t address, T value) {
if (aligned<T>(&memory[address])) {
*reinterpret_cast<T*>(&memory[address]) = value;
} else {
std::memcpy(&memory[address], &value, sizeof(T));
}
}
template <typename T>
T get(size_t address) {
if (aligned<T>(&memory[address])) {
return *reinterpret_cast<T*>(&memory[address]);
} else {
T loaded;
std::memcpy(&loaded, &memory[address], sizeof(T));
return loaded;
}
}
} memory;
ShellExternalInterface() : memory() {}
void init(Module& wasm) override {
memory.resize(wasm.memory.initial);
// apply memory segments
for (auto segment : wasm.memory.segments) {
assert(segment.offset + segment.size <= wasm.memory.initial);
for (size_t i = 0; i != segment.size; ++i) {
memory.set(segment.offset + i, segment.data[i]);
}
}
}
Literal callImport(Import *import, ModuleInstance::LiteralList& arguments) override {
if (import->module == SPECTEST && import->base == PRINT) {
for (auto argument : arguments) {
std::cout << argument << '\n';
}
return Literal();
} else if (import->module == ENV && import->base == EXIT) {
std::cout << "exit()\n";
throw ExitException();
}
std::cout << "callImport " << import->name.str << "\n";
abort();
}
Literal load(Load* load, size_t addr) override {
// ignore align - assume we are on x86 etc. which does that
switch (load->type) {
case i32: {
switch (load->bytes) {
case 1: return load->signed_ ? Literal((int32_t)memory.get<int8_t>(addr)) : Literal((int32_t)memory.get<uint8_t>(addr));
case 2: return load->signed_ ? Literal((int32_t)memory.get<int16_t>(addr)) : Literal((int32_t)memory.get<uint16_t>(addr));
case 4: return load->signed_ ? Literal((int32_t)memory.get<int32_t>(addr)) : Literal((int32_t)memory.get<uint32_t>(addr));
default: abort();
}
break;
}
case i64: {
switch (load->bytes) {
case 1: return load->signed_ ? Literal((int64_t)memory.get<int8_t>(addr)) : Literal((int64_t)memory.get<uint8_t>(addr));
case 2: return load->signed_ ? Literal((int64_t)memory.get<int16_t>(addr)) : Literal((int64_t)memory.get<uint16_t>(addr));
case 4: return load->signed_ ? Literal((int64_t)memory.get<int32_t>(addr)) : Literal((int64_t)memory.get<uint32_t>(addr));
case 8: return load->signed_ ? Literal((int64_t)memory.get<int64_t>(addr)) : Literal((int64_t)memory.get<uint64_t>(addr));
default: abort();
}
break;
}
case f32: return Literal(memory.get<float>(addr));
case f64: return Literal(memory.get<double>(addr));
default: abort();
}
}
void store(Store* store, size_t addr, Literal value) override {
// ignore align - assume we are on x86 etc. which does that
switch (store->type) {
case i32: {
switch (store->bytes) {
case 1: memory.set<int8_t>(addr, value.geti32()); break;
case 2: memory.set<int16_t>(addr, value.geti32()); break;
case 4: memory.set<int32_t>(addr, value.geti32()); break;
default: abort();
}
break;
}
case i64: {
switch (store->bytes) {
case 1: memory.set<int8_t>(addr, value.geti64()); break;
case 2: memory.set<int16_t>(addr, value.geti64()); break;
case 4: memory.set<int32_t>(addr, value.geti64()); break;
case 8: memory.set<int64_t>(addr, value.geti64()); break;
default: abort();
}
break;
}
// write floats carefully, ensuring all bits reach memory
case f32: memory.set<int32_t>(addr, value.reinterpreti32()); break;
case f64: memory.set<int64_t>(addr, value.reinterpreti64()); break;
default: abort();
}
}
void growMemory(size_t /*oldSize*/, size_t newSize) override {
memory.resize(newSize);
}
jmp_buf trapState;
void trap(const char* why) override {
std::cerr << "[trap " << why << "]\n";
longjmp(trapState, 1);
}
};
//
// An invocation into a module
//
struct Invocation {
ModuleInstance* instance;
IString name;
ModuleInstance::LiteralList arguments;
Invocation(Element& invoke, ModuleInstance* instance, SExpressionWasmBuilder& builder) : instance(instance) {
assert(invoke[0]->str() == INVOKE);
name = invoke[1]->str();
for (size_t j = 2; j < invoke.size(); j++) {
Expression* argument = builder.parseExpression(*invoke[j]);
arguments.push_back(argument->dyn_cast<Const>()->value);
}
}
Literal invoke() {
return instance->callExport(name, arguments);
}
};
static void run_asserts(size_t* i, bool* checked, AllocatingModule* wasm,
Element* root,
std::unique_ptr<SExpressionWasmBuilder>* builder,
bool print_before, bool print_after,
Name entry) {
auto interface = new ShellExternalInterface();
auto instance = new ModuleInstance(*wasm, interface);
if (entry.is() > 0) {
Function* function = wasm->functionsMap[entry];
if (!function) {
std::cerr << "Unknown entry " << entry << std::endl;
} else {
ModuleInstance::LiteralList arguments;
for (NameType param : function->params) {
arguments.push_back(Literal(param.type));
}
try {
instance->callExport(entry, arguments);
} catch (ExitException& x) {
}
}
}
while (*i < root->size()) {
Element& curr = *(*root)[*i];
IString id = curr[0]->str();
if (id == MODULE) break;
*checked = true;
Colors::red(std::cerr);
std::cerr << *i << '/' << (root->size()-1);
Colors::green(std::cerr);
std::cerr << " CHECKING: ";
Colors::normal(std::cerr);
std::cerr << curr << '\n';
if (id == ASSERT_INVALID) {
// a module invalidity test
AllocatingModule wasm;
bool invalid = false;
jmp_buf trapState;
if (setjmp(trapState) == 0) {
*builder = std::unique_ptr<SExpressionWasmBuilder>(new SExpressionWasmBuilder(wasm, *curr[1], [&]() {
invalid = true;
longjmp(trapState, 1);
}));
}
if (print_before || print_after) {
Colors::bold(std::cout);
std::cerr << "printing in module invalidity test:\n";
Colors::normal(std::cout);
std::cout << wasm;
}
if (!invalid) {
// maybe parsed ok, but otherwise incorrect
invalid = !WasmValidator().validate(wasm);
}
assert(invalid);
} else if (id == INVOKE) {
Invocation invocation(curr, instance, *builder->get());
invocation.invoke();
} else {
// an invoke test
Invocation invocation(*curr[1], instance, *builder->get());
bool trapped = false;
Literal result;
if (setjmp(interface->trapState) == 0) {
result = invocation.invoke();
} else {
trapped = true;
}
if (id == ASSERT_RETURN) {
assert(!trapped);
if (curr.size() >= 3) {
Literal expected = builder->get()
->parseExpression(*curr[2])
->dyn_cast<Const>()
->value;
std::cerr << "seen " << result << ", expected " << expected << '\n';
assert(expected == result);
} else {
Literal expected;
std::cerr << "seen " << result << ", expected " << expected << '\n';
assert(expected == result);
}
}
if (id == ASSERT_TRAP) assert(trapped);
}
*i += 1;
}
}
//
// main
//
int main(int argc, const char* argv[]) {
bool print_before = false;
bool print_after = false;
Name entry;
std::vector<std::string> passes;
static const char* default_passes[] = {"remove-unused-brs",
"remove-unused-names", "merge-blocks",
"simplify-locals"};
Options options("binaryen-shell", "Execute .wast files");
options
.add("--output", "-o", "Output file (stdout if not specified)",
Options::Arguments::One,
[](Options* o, const std::string& argument) {
o->extra["output"] = argument;
Colors::disable();
})
.add(
"--entry", "-e", "call the entry point after parsing the module",
Options::Arguments::One,
[&entry](Options*, const std::string& argument) { entry = argument; })
.add("", "-O", "execute default optimization passes",
Options::Arguments::Zero,
[&passes](Options*, const std::string&) {
for (const auto* p : default_passes) passes.push_back(p);
})
.add("--print-before", "", "Print modules before processing them",
Options::Arguments::Zero,
[&print_before](Options*, const std::string&) {
print_before = true;
})
.add("--print-after", "", "Print modules after processing them",
Options::Arguments::Zero,
[&print_after](Options*, const std::string&) { print_after = true; })
.add_positional("INFILE", Options::Arguments::One,
[](Options* o, const std::string& argument) {
o->extra["infile"] = argument;
});
for (const auto& p : PassRegistry::get()->getRegisteredNames()) {
options.add(
std::string("--") + p, "", PassRegistry::get()->getPassDescription(p),
Options::Arguments::Zero,
[&passes, p](Options*, const std::string&) { passes.push_back(p); });
}
options.parse(argc, argv);
auto input(read_file<std::vector<char>>(options.extra["infile"], options.debug));
if (options.debug) std::cerr << "parsing text to s-expressions...\n";
SExpressionParser parser(input.data());
Element& root = *parser.root;
if (options.debug) std::cout << root << '\n';
// A .wast may have multiple modules, with some asserts after them
bool checked = false;
size_t i = 0;
while (i < root.size()) {
if (options.debug) std::cerr << "parsing s-expressions to wasm...\n";
AllocatingModule wasm;
std::unique_ptr<SExpressionWasmBuilder> builder(
new SExpressionWasmBuilder(wasm, *root[i], [&]() { abort(); }, options.debug));
i++;
if (print_before) {
Colors::bold(std::cout);
std::cerr << "printing before:\n";
Colors::normal(std::cout);
std::cout << wasm;
}
MixedArena moreModuleAllocations;
if (passes.size() > 0) {
if (options.debug) std::cerr << "running passes...\n";
PassRunner passRunner(&moreModuleAllocations);
for (auto& passName : passes) {
passRunner.add(passName);
}
passRunner.run(&wasm);
}
if (print_after) {
Colors::bold(std::cout);
std::cerr << "printing after:\n";
Colors::normal(std::cout);
std::cout << wasm;
}
run_asserts(&i, &checked, &wasm, &root, &builder, print_before,
print_after, entry);
}
if (checked) {
Colors::green(std::cerr);
Colors::bold(std::cerr);
std::cerr << "all checks passed.\n";
Colors::normal(std::cerr);
}
}