| /* | |
| * 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 <memory> | |
| #include "execution-results.h" | |
| #include "ir/element-utils.h" | |
| #include "parser/lexer.h" | |
| #include "parser/wat-parser.h" | |
| #include "pass.h" | |
| #include "shell-interface.h" | |
| #include "support/command-line.h" | |
| #include "support/file.h" | |
| #include "support/result.h" | |
| #include "wasm-binary.h" | |
| #include "wasm-interpreter.h" | |
| #include "wasm-validator.h" | |
| using namespace wasm; | |
| using namespace wasm::WATParser; | |
| struct Shell { | |
| std::map<Name, std::shared_ptr<Module>> modules; | |
| std::map<Name, std::shared_ptr<ShellExternalInterface>> interfaces; | |
| std::map<Name, std::shared_ptr<ModuleRunner>> instances; | |
| // used for imports | |
| std::map<Name, std::shared_ptr<ModuleRunner>> linkedInstances; | |
| Name lastModule; | |
| Options& options; | |
| Shell(Options& options) : options(options) { buildSpectestModule(); } | |
| Result<> run(WASTScript& script) { | |
| size_t i = 0; | |
| for (auto& entry : script) { | |
| Colors::red(std::cerr); | |
| std::cerr << i++ << ' '; | |
| Colors::normal(std::cerr); | |
| if (std::get_if<WASTModule>(&entry.cmd)) { | |
| Colors::green(std::cerr); | |
| std::cerr << "BUILDING MODULE [line: " << entry.line << "]\n"; | |
| Colors::normal(std::cerr); | |
| } else if (auto* reg = std::get_if<Register>(&entry.cmd)) { | |
| Colors::green(std::cerr); | |
| std::cerr << "REGISTER MODULE INSTANCE AS \"" << reg->name | |
| << "\" [line: " << entry.line << "]\n"; | |
| Colors::normal(std::cerr); | |
| } else { | |
| Colors::green(std::cerr); | |
| std::cerr << "CHECKING [line: " << entry.line << "]\n"; | |
| Colors::normal(std::cerr); | |
| } | |
| CHECK_ERR(runCommand(entry.cmd)); | |
| } | |
| return Ok{}; | |
| } | |
| Result<> runCommand(WASTCommand& cmd) { | |
| if (auto* mod = std::get_if<WASTModule>(&cmd)) { | |
| return addModule(*mod); | |
| } else if (auto* reg = std::get_if<Register>(&cmd)) { | |
| return addRegistration(*reg); | |
| } else if (auto* act = std::get_if<Action>(&cmd)) { | |
| doAction(*act); | |
| return Ok{}; | |
| } else if (auto* assn = std::get_if<Assertion>(&cmd)) { | |
| return doAssertion(*assn); | |
| } else { | |
| WASM_UNREACHABLE("unexpected command"); | |
| } | |
| } | |
| Result<std::shared_ptr<Module>> makeModule(WASTModule& mod) { | |
| std::shared_ptr<Module> wasm; | |
| if (auto* quoted = std::get_if<QuotedModule>(&mod)) { | |
| wasm = std::make_shared<Module>(); | |
| switch (quoted->type) { | |
| case QuotedModuleType::Text: { | |
| CHECK_ERR(parseModule(*wasm, quoted->module)); | |
| break; | |
| } | |
| case QuotedModuleType::Binary: { | |
| std::vector<char> buffer(quoted->module.begin(), | |
| quoted->module.end()); | |
| WasmBinaryReader reader(*wasm, FeatureSet::All, buffer); | |
| try { | |
| reader.read(); | |
| } catch (ParseException& p) { | |
| std::stringstream ss; | |
| p.dump(ss); | |
| return Err{ss.str()}; | |
| } | |
| break; | |
| } | |
| } | |
| } else if (auto* ptr = std::get_if<std::shared_ptr<Module>>(&mod)) { | |
| wasm = *ptr; | |
| } else { | |
| WASM_UNREACHABLE("unexpected module kind"); | |
| } | |
| wasm->features = FeatureSet::All; | |
| return wasm; | |
| } | |
| Result<> validateModule(Module& wasm) { | |
| if (!WasmValidator().validate(wasm)) { | |
| return Err{"failed validation"}; | |
| } | |
| return Ok{}; | |
| } | |
| using InstanceInfo = std::pair<std::shared_ptr<ShellExternalInterface>, | |
| std::shared_ptr<ModuleRunner>>; | |
| Result<InstanceInfo> instantiate(Module& wasm) { | |
| try { | |
| auto interface = | |
| std::make_shared<ShellExternalInterface>(linkedInstances); | |
| auto instance = | |
| std::make_shared<ModuleRunner>(wasm, interface.get(), linkedInstances); | |
| instance->instantiate(); | |
| return {{std::move(interface), std::move(instance)}}; | |
| } catch (...) { | |
| return Err{"failed to instantiate module"}; | |
| } | |
| } | |
| Result<> addModule(WASTModule& mod) { | |
| auto module = makeModule(mod); | |
| CHECK_ERR(module); | |
| auto wasm = *module; | |
| CHECK_ERR(validateModule(*wasm)); | |
| auto instanceInfo = instantiate(*wasm); | |
| CHECK_ERR(instanceInfo); | |
| auto& [interface, instance] = *instanceInfo; | |
| lastModule = wasm->name; | |
| modules[lastModule] = std::move(wasm); | |
| interfaces[lastModule] = std::move(interface); | |
| instances[lastModule] = std::move(instance); | |
| return Ok{}; | |
| } | |
| Result<> addRegistration(Register& reg) { | |
| auto instance = instances[lastModule]; | |
| if (!instance) { | |
| return Err{"register called without a module"}; | |
| } | |
| linkedInstances[reg.name] = instance; | |
| // We copy pointers as a registered module's name might still be used | |
| // in an assertion or invoke command. | |
| modules[reg.name] = modules[lastModule]; | |
| interfaces[reg.name] = interfaces[lastModule]; | |
| instances[reg.name] = instances[lastModule]; | |
| return Ok{}; | |
| } | |
| struct TrapResult {}; | |
| struct HostLimitResult {}; | |
| struct ExceptionResult {}; | |
| using ActionResult = | |
| std::variant<Literals, TrapResult, HostLimitResult, ExceptionResult>; | |
| std::string resultToString(ActionResult& result) { | |
| if (std::get_if<TrapResult>(&result)) { | |
| return "trap"; | |
| } else if (std::get_if<HostLimitResult>(&result)) { | |
| return "exceeded host limit"; | |
| } else if (std::get_if<ExceptionResult>(&result)) { | |
| return "exception"; | |
| } else if (auto* vals = std::get_if<Literals>(&result)) { | |
| std::stringstream ss; | |
| ss << *vals; | |
| return ss.str(); | |
| } else { | |
| WASM_UNREACHABLE("unexpected result"); | |
| } | |
| } | |
| ActionResult doAction(Action& act) { | |
| assert(instances[lastModule].get()); | |
| if (auto* invoke = std::get_if<InvokeAction>(&act)) { | |
| auto it = instances.find(invoke->base ? *invoke->base : lastModule); | |
| if (it == instances.end()) { | |
| return TrapResult{}; | |
| } | |
| auto& instance = it->second; | |
| try { | |
| return instance->callExport(invoke->name, invoke->args); | |
| } catch (TrapException&) { | |
| return TrapResult{}; | |
| } catch (HostLimitException&) { | |
| return HostLimitResult{}; | |
| } catch (WasmException&) { | |
| return ExceptionResult{}; | |
| } catch (...) { | |
| WASM_UNREACHABLE("unexpected error"); | |
| } | |
| } else if (auto* get = std::get_if<GetAction>(&act)) { | |
| auto it = instances.find(get->base ? *get->base : lastModule); | |
| if (it == instances.end()) { | |
| return TrapResult{}; | |
| } | |
| auto& instance = it->second; | |
| try { | |
| return instance->getExport(get->name); | |
| } catch (TrapException&) { | |
| return TrapResult{}; | |
| } catch (...) { | |
| WASM_UNREACHABLE("unexpected error"); | |
| } | |
| } else { | |
| WASM_UNREACHABLE("unexpected action"); | |
| } | |
| } | |
| Result<> doAssertion(Assertion& assn) { | |
| if (auto* ret = std::get_if<AssertReturn>(&assn)) { | |
| return assertReturn(*ret); | |
| } else if (auto* act = std::get_if<AssertAction>(&assn)) { | |
| return assertAction(*act); | |
| } else if (auto* mod = std::get_if<AssertModule>(&assn)) { | |
| return assertModule(*mod); | |
| } else { | |
| WASM_UNREACHABLE("unexpected assertion"); | |
| } | |
| } | |
| Result<> checkNaN(Literal val, NaNResult nan) { | |
| std::stringstream err; | |
| switch (nan.kind) { | |
| case NaNKind::Canonical: | |
| if (val.type != nan.type || !val.isCanonicalNaN()) { | |
| err << "expected canonical " << nan.type << " NaN, got " << val; | |
| return Err{err.str()}; | |
| } | |
| break; | |
| case NaNKind::Arithmetic: | |
| if (val.type != nan.type || !val.isArithmeticNaN()) { | |
| err << "expected arithmetic " << nan.type << " NaN, got " << val; | |
| return Err{err.str()}; | |
| } | |
| break; | |
| } | |
| return Ok{}; | |
| } | |
| Result<> checkLane(Literal val, LaneResult expected, Index index) { | |
| std::stringstream err; | |
| if (auto* e = std::get_if<Literal>(&expected)) { | |
| if (*e != val) { | |
| err << "expected " << *e << ", got " << val << " at lane " << index; | |
| return Err{err.str()}; | |
| } | |
| } else if (auto* nan = std::get_if<NaNResult>(&expected)) { | |
| auto check = checkNaN(val, *nan); | |
| if (auto* e = check.getErr()) { | |
| err << e->msg << " at lane " << index; | |
| return Err{err.str()}; | |
| } | |
| } else { | |
| WASM_UNREACHABLE("unexpected lane expectation"); | |
| } | |
| return Ok{}; | |
| } | |
| Result<> assertReturn(AssertReturn& assn) { | |
| std::stringstream err; | |
| auto result = doAction(assn.action); | |
| auto* values = std::get_if<Literals>(&result); | |
| if (!values) { | |
| return Err{std::string("expected return, got ") + resultToString(result)}; | |
| } | |
| if (values->size() != assn.expected.size()) { | |
| err << "expected " << assn.expected.size() << " values, got " | |
| << resultToString(result); | |
| return Err{err.str()}; | |
| } | |
| for (Index i = 0; i < values->size(); ++i) { | |
| auto atIndex = [&]() { | |
| if (values->size() <= 1) { | |
| return std::string{}; | |
| } | |
| std::stringstream ss; | |
| ss << " at index " << i; | |
| return ss.str(); | |
| }; | |
| Literal val = (*values)[i]; | |
| auto& expected = assn.expected[i]; | |
| if (auto* v = std::get_if<Literal>(&expected)) { | |
| if (val != *v) { | |
| err << "expected " << *v << ", got " << val << atIndex(); | |
| return Err{err.str()}; | |
| } | |
| } else if (auto* ref = std::get_if<RefResult>(&expected)) { | |
| if (!val.type.isRef() || | |
| !HeapType::isSubType(val.type.getHeapType(), ref->type)) { | |
| err << "expected " << ref->type << " reference, got " << val | |
| << atIndex(); | |
| return Err{err.str()}; | |
| } | |
| } else if (auto* nan = std::get_if<NaNResult>(&expected)) { | |
| auto check = checkNaN(val, *nan); | |
| if (auto* e = check.getErr()) { | |
| err << e->msg << atIndex(); | |
| return Err{err.str()}; | |
| } | |
| } else if (auto* lanes = std::get_if<LaneResults>(&expected)) { | |
| switch (lanes->size()) { | |
| case 4: { | |
| auto vals = val.getLanesF32x4(); | |
| for (Index i = 0; i < 4; ++i) { | |
| auto check = checkLane(vals[i], (*lanes)[i], i); | |
| if (auto* e = check.getErr()) { | |
| err << e->msg << atIndex(); | |
| return Err{err.str()}; | |
| } | |
| } | |
| break; | |
| } | |
| case 2: { | |
| auto vals = val.getLanesF64x2(); | |
| for (Index i = 0; i < 2; ++i) { | |
| auto check = checkLane(vals[i], (*lanes)[i], i); | |
| if (auto* e = check.getErr()) { | |
| err << e->msg << atIndex(); | |
| return Err{err.str()}; | |
| } | |
| } | |
| break; | |
| } | |
| default: | |
| WASM_UNREACHABLE("unexpected number of lanes"); | |
| } | |
| } else { | |
| WASM_UNREACHABLE("unexpected expectation"); | |
| } | |
| } | |
| return Ok{}; | |
| } | |
| Result<> assertAction(AssertAction& assn) { | |
| std::stringstream err; | |
| auto result = doAction(assn.action); | |
| switch (assn.type) { | |
| case ActionAssertionType::Trap: | |
| if (std::get_if<TrapResult>(&result)) { | |
| return Ok{}; | |
| } | |
| err << "expected trap"; | |
| break; | |
| case ActionAssertionType::Exhaustion: | |
| if (std::get_if<HostLimitResult>(&result)) { | |
| return Ok{}; | |
| } | |
| err << "expected exhaustion"; | |
| break; | |
| case ActionAssertionType::Exception: | |
| if (std::get_if<ExceptionResult>(&result)) { | |
| return Ok{}; | |
| } | |
| err << "expected exception"; | |
| break; | |
| } | |
| err << ", got " << resultToString(result); | |
| return Err{err.str()}; | |
| } | |
| Result<> assertModule(AssertModule& assn) { | |
| auto wasm = makeModule(assn.wasm); | |
| if (const auto* err = wasm.getErr()) { | |
| if (assn.type == ModuleAssertionType::Malformed || | |
| assn.type == ModuleAssertionType::Invalid) { | |
| return Ok{}; | |
| } | |
| return Err{err->msg}; | |
| } | |
| if (assn.type == ModuleAssertionType::Malformed) { | |
| return Err{"expected malformed module"}; | |
| } | |
| auto valid = validateModule(**wasm); | |
| if (auto* err = valid.getErr()) { | |
| if (assn.type == ModuleAssertionType::Invalid) { | |
| return Ok{}; | |
| } | |
| return Err{err->msg}; | |
| } | |
| if (assn.type == ModuleAssertionType::Invalid) { | |
| return Err{"expected invalid module"}; | |
| } | |
| auto instance = instantiate(**wasm); | |
| if (auto* err = instance.getErr()) { | |
| if (assn.type == ModuleAssertionType::Unlinkable || | |
| assn.type == ModuleAssertionType::Trap) { | |
| return Ok{}; | |
| } | |
| return Err{err->msg}; | |
| } | |
| if (assn.type == ModuleAssertionType::Unlinkable) { | |
| return Err{"expected unlinkable module"}; | |
| } | |
| if (assn.type == ModuleAssertionType::Trap) { | |
| return Err{"expected instantiation to trap"}; | |
| } | |
| WASM_UNREACHABLE("unexpected module assertion"); | |
| } | |
| // spectest module is a default host-provided module defined by the spec's | |
| // reference interpreter. It's been replaced by the `(register ...)` | |
| // mechanism in the recent spec tests, and is kept for legacy tests only. | |
| // | |
| // TODO: spectest module is considered deprecated by the spec. Remove when | |
| // is actually removed from the spec test. | |
| void buildSpectestModule() { | |
| auto spectest = std::make_shared<Module>(); | |
| spectest->features = FeatureSet::All; | |
| Builder builder(*spectest); | |
| spectest->addGlobal(builder.makeGlobal(Name::fromInt(0), | |
| Type::i32, | |
| builder.makeConst<uint32_t>(666), | |
| Builder::Immutable)); | |
| spectest->addGlobal(builder.makeGlobal(Name::fromInt(1), | |
| Type::i64, | |
| builder.makeConst<uint64_t>(666), | |
| Builder::Immutable)); | |
| spectest->addGlobal(builder.makeGlobal(Name::fromInt(2), | |
| Type::f32, | |
| builder.makeConst<float>(666.6f), | |
| Builder::Immutable)); | |
| spectest->addGlobal(builder.makeGlobal(Name::fromInt(3), | |
| Type::f64, | |
| builder.makeConst<double>(666.6), | |
| Builder::Immutable)); | |
| spectest->addExport( | |
| builder.makeExport("global_i32", Name::fromInt(0), ExternalKind::Global)); | |
| spectest->addExport( | |
| builder.makeExport("global_i64", Name::fromInt(1), ExternalKind::Global)); | |
| spectest->addExport( | |
| builder.makeExport("global_f32", Name::fromInt(2), ExternalKind::Global)); | |
| spectest->addExport( | |
| builder.makeExport("global_f64", Name::fromInt(3), ExternalKind::Global)); | |
| spectest->addTable(builder.makeTable( | |
| Name::fromInt(0), Type(HeapType::func, Nullable), 10, 20)); | |
| spectest->addExport( | |
| builder.makeExport("table", Name::fromInt(0), ExternalKind::Table)); | |
| spectest->addTable(builder.makeTable( | |
| Name::fromInt(1), Type(HeapType::func, Nullable), 10, 20, Type::i64)); | |
| spectest->addExport( | |
| builder.makeExport("table64", Name::fromInt(1), ExternalKind::Table)); | |
| Memory* memory = | |
| spectest->addMemory(builder.makeMemory(Name::fromInt(0), 1, 2)); | |
| spectest->addExport( | |
| builder.makeExport("memory", memory->name, ExternalKind::Memory)); | |
| // print_* functions are handled separately, no need to define here. | |
| WASTModule mod = std::move(spectest); | |
| auto added = addModule(mod); | |
| if (added.getErr()) { | |
| WASM_UNREACHABLE("error building spectest module"); | |
| } | |
| Register registration{"spectest"}; | |
| auto registered = addRegistration(registration); | |
| if (registered.getErr()) { | |
| WASM_UNREACHABLE("error registering spectest module"); | |
| } | |
| } | |
| }; | |
| int main(int argc, const char* argv[]) { | |
| Name entry; | |
| std::set<size_t> skipped; | |
| // Read stdin by default. | |
| std::string infile = "-"; | |
| Options options("wasm-shell", "Execute .wast files"); | |
| options.add_positional( | |
| "INFILE", | |
| Options::Arguments::One, | |
| [&](Options* o, const std::string& argument) { infile = argument; }); | |
| options.parse(argc, argv); | |
| auto input = read_file<std::string>(infile, Flags::Text); | |
| // Check that we can parse the script correctly with the new parser. | |
| auto script = WATParser::parseScript(input); | |
| if (auto* err = script.getErr()) { | |
| std::cerr << err->msg << '\n'; | |
| exit(1); | |
| } | |
| Lexer lexer(input); | |
| auto result = Shell(options).run(*script); | |
| if (auto* err = result.getErr()) { | |
| std::cerr << err->msg << '\n'; | |
| exit(1); | |
| } | |
| Colors::green(std::cerr); | |
| Colors::bold(std::cerr); | |
| std::cerr << "all checks passed.\n"; | |
| Colors::normal(std::cerr); | |
| } |