Add support for either in wast
diff --git a/scripts/test/shared.py b/scripts/test/shared.py index 18ca85d..0f14ad6 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py
@@ -396,7 +396,7 @@ # Test invalid 'elem.wast', - # Requires wast `either` support + # Requires scoping of `register` statements within `thread` blocks 'threads/thread.wast', # Requires better support for multi-threaded tests @@ -453,12 +453,9 @@ 'type-subtyping.wast', # ShellExternalInterface::callTable does not handle subtyping 'memory64.wast', # Requires validations on the max memory size 'imports3.wast', # Requires better checking of exports from the special "spectest" module - 'i16x8_relaxed_q15mulr_s.wast', # Requires wast `either` support - 'i8x16_relaxed_swizzle.wast', # Requires wast `either` support - 'relaxed_dot_product.wast', # Requires wast `either` support - 'relaxed_laneselect.wast', # Requires wast `either` support - 'relaxed_madd_nmadd.wast', # Requires wast `either` support - 'relaxed_min_max.wast', # Requires wast `either` support + 'relaxed_dot_product.wast', # i16x8.relaxed_dot_i8x16_i7x16_s instruction not supported + 'relaxed_laneselect.wast', # i8x16.relaxed_laneselect instruction not supported + 'relaxed_min_max.wast', # Non-canonical NaN from f32x4.relaxed_min 'simd_const.wast', # Hex float constant not recognized as out of range 'simd_conversions.wast', # Promoted NaN should be canonical 'simd_f32x4.wast', # Min of 0 and NaN should give a canonical NaN
diff --git a/src/parser/wast-parser.cpp b/src/parser/wast-parser.cpp index c5f5f91..b98bef0 100644 --- a/src/parser/wast-parser.cpp +++ b/src/parser/wast-parser.cpp
@@ -324,12 +324,30 @@ return in.err("unrecognized result"); } +Result<ResultAlternatives> eitherResult(Lexer& in) { + if (in.takeSExprStart("either"sv)) { + ResultAlternatives alternatives; + do { + auto r = result(in); + CHECK_ERR(r); + + alternatives.push_back(*std::move(r)); + } while (!in.takeRParen()); + + return alternatives; + } + + auto r = result(in); + CHECK_ERR(r); + return ResultAlternatives{*std::move(r)}; +} + Result<ExpectedResults> results(Lexer& in) { ExpectedResults res; while (!in.peekRParen()) { - auto r = result(in); + auto r = eitherResult(in); CHECK_ERR(r); - res.emplace_back(std::move(*r)); + res.emplace_back(*std::move(r)); } return res; } @@ -648,7 +666,7 @@ return cmds; } CHECK_ERR(cmd); - cmds.push_back(ScriptEntry{std::move(*cmd), line}); + cmds.push_back(ScriptEntry{*std::move(cmd), line}); } return cmds; }
diff --git a/src/parser/wat-parser.h b/src/parser/wat-parser.h index 4eac32b..f833fc8 100644 --- a/src/parser/wat-parser.h +++ b/src/parser/wat-parser.h
@@ -87,7 +87,13 @@ using ExpectedResult = std::variant<Literal, NullRefResult, RefResult, NaNResult, LaneResults>; -using ExpectedResults = std::vector<ExpectedResult>; +using ResultAlternatives = std::vector<ExpectedResult>; + +// The WAST spec states that `either`s maybe be nested arbitrarily e.g. +// (either (either "a" "b") (either "a" "c")) +// but we store this flattened since there's no way to tell the difference +// anyway. +using ExpectedResults = std::vector<ResultAlternatives>; struct AssertReturn { Action action;
diff --git a/src/support/result.h b/src/support/result.h index 7cd360d..f34f630 100644 --- a/src/support/result.h +++ b/src/support/result.h
@@ -36,21 +36,22 @@ // Check a Result or MaybeResult for error and return the error if it exists. #define CHECK_ERR(val) \ if (auto _val = (val); auto err = _val.getErr()) { \ - return Err{*err}; \ + return (typename decltype(_val)::ErrorType)(*err); \ } // Represent a result of type T or an error message. -template<typename T = Ok> struct [[nodiscard]] Result { - std::variant<T, Err> val; +template<typename T = Ok, typename E = Err> struct [[nodiscard]] Result { + using ErrorType = E; + std::variant<T, E> val; - Result(Result<T>& other) = default; - Result(Result<T>&& other) = default; - Result(const Err& e) : val(std::in_place_type<Err>, e) {} - Result(Err&& e) : val(std::in_place_type<Err>, std::move(e)) {} + Result(Result<T, E>& other) = default; + Result(Result<T, E>&& other) = default; + Result(const E& e) : val(std::in_place_type<E>, e) {} + Result(E&& e) : val(std::in_place_type<E>, std::move(e)) {} template<typename U = T> Result(U&& u) : val(std::in_place_type<T>, std::forward<U>(u)) {} - Err* getErr() { return std::get_if<Err>(&val); } + E* getErr() { return std::get_if<E>(&val); } T& operator*() { return *std::get_if<T>(&val); } T* operator->() { return std::get_if<T>(&val); } }; @@ -58,6 +59,7 @@ // Represent an optional result of type T or an error message. template<typename T = Ok> struct [[nodiscard]] MaybeResult { std::variant<T, None, Err> val; + using ErrorType = Err; MaybeResult() : val(None{}) {} MaybeResult(MaybeResult<T>& other) = default;
diff --git a/src/tools/wasm-shell.cpp b/src/tools/wasm-shell.cpp index 4eb6f52..59d1e29 100644 --- a/src/tools/wasm-shell.cpp +++ b/src/tools/wasm-shell.cpp
@@ -319,13 +319,13 @@ switch (nan.kind) { case NaNKind::Canonical: if (val.type != nan.type || !val.isCanonicalNaN()) { - err << "expected canonical " << nan.type << " NaN, got " << val; + err << "canonical " << nan.type; return Err{err.str()}; } break; case NaNKind::Arithmetic: if (val.type != nan.type || !val.isArithmeticNaN()) { - err << "expected arithmetic " << nan.type << " NaN, got " << val; + err << "arithmetic " << nan.type; return Err{err.str()}; } break; @@ -333,17 +333,17 @@ return Ok{}; } - Result<> checkLane(Literal val, LaneResult expected, Index index) { + Result<> checkLane(Literal val, LaneResult expected) { std::stringstream err; if (auto* e = std::get_if<Literal>(&expected)) { if (*e != val) { - err << "expected " << *e << ", got " << val << " at lane " << index; + err << *e; 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; + err << e->msg; return Err{err.str()}; } } else { @@ -352,6 +352,99 @@ return Ok{}; } + struct AlternativeErr { + std::string expected; + int lane = -1; + }; + + Result<Ok, AlternativeErr> matchAlternative(const Literal& val, + const ExpectedResult& expected, + bool isAlternative) { + std::stringstream err; + + if (auto* v = std::get_if<Literal>(&expected)) { + if (val != *v) { + if (val.type.isVector() && v->type.isVector() && isAlternative) { + auto valLanes = val.getLanesI32x4(); + auto expLanes = v->getLanesI32x4(); + for (int i = 0; i < 4; ++i) { + if (valLanes[i] != expLanes[i]) { + err << "0x" << std::setfill('0') << std::setw(8) << std::hex + << expLanes[i] << std::dec; + return AlternativeErr{err.str(), i}; + } + } + } + err << *v; + return AlternativeErr{err.str()}; + } + } else if (auto* ref = std::get_if<RefResult>(&expected)) { + if (!val.type.isRef() || + !HeapType::isSubType(val.type.getHeapType(), ref->type)) { + err << ref->type; + return AlternativeErr{err.str()}; + } + } else if ([[maybe_unused]] auto* nullRef = + std::get_if<NullRefResult>(&expected)) { + if (!val.isNull()) { + err << "ref.null"; + return AlternativeErr{err.str()}; + } + } else if (auto* nan = std::get_if<NaNResult>(&expected)) { + auto check = checkNaN(val, *nan); + if (auto* e = check.getErr()) { + err << e->msg; + return AlternativeErr{err.str()}; + } + } else if (auto* l = std::get_if<LaneResults>(&expected)) { + auto* lanes = &l->lanes; + + auto check = [&](int size, const auto& vals) -> Result<> { + for (int i = 0; i < size; ++i) { + auto check = checkLane(vals[i], (*lanes)[i], i); + if (auto* e = check.getErr()) { + err << e->msg << atIndex(); + return Err{err.str()}; + } + } + return Ok{}; + }; + + bool isFloat = l->type == WATParser::LaneResults::LaneType::Float; + switch (lanes->size()) { + // Use unsigned values for the smaller types here to avoid sign + // extension when storing 8/16-bit values in 32-bit ints. This isn't + // needed for i32 and i64. + case 16: { + // There is no f8. + CHECK_ERR(check(16, val.getLanesUI8x16())); + break; + } + case 8: { + CHECK_ERR( + check(8, isFloat ? val.getLanesF16x8() : val.getLanesUI16x8())); + break; + } + case 4: { + CHECK_ERR( + check(4, isFloat ? val.getLanesF32x4() : val.getLanesI32x4())); + break; + } + case 2: { + CHECK_ERR( + check(2, isFloat ? val.getLanesF64x2() : val.getLanesI64x2())); + break; + } + default: + WASM_UNREACHABLE("unexpected number of lanes"); + } + + } else { + WASM_UNREACHABLE("unexpected expectation"); + } + return Ok{}; + } + Result<> assertReturn(AssertReturn& assn) { std::stringstream err; auto result = doAction(assn.action); @@ -374,78 +467,55 @@ 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 ([[maybe_unused]] auto* nullRef = - std::get_if<NullRefResult>(&expected)) { - if (!val.isNull()) { - err << "expected ref.null, 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* l = std::get_if<LaneResults>(&expected)) { - auto* lanes = &l->lanes; - - auto check = [&](int size, const auto& vals) -> Result<> { - for (int i = 0; i < size; ++i) { - auto check = checkLane(vals[i], (*lanes)[i], i); - if (auto* e = check.getErr()) { - err << e->msg << atIndex(); - return Err{err.str()}; - } + // non-either case + if (assn.expected[i].size() == 1) { + auto result = matchAlternative( + (*values)[i], assn.expected[i][0], /*isAlternative=*/false); + if (auto* e = result.getErr()) { + std::stringstream ss; + ss << "expected " << e->expected << ", got " << (*values)[i]; + if (e->lane != -1) { + ss << " at lane " << e->lane; } - return Ok{}; - }; - - bool isFloat = l->type == WATParser::LaneResults::LaneType::Float; - switch (lanes->size()) { - // Use unsigned values for the smaller types here to avoid sign - // extension when storing 8/16-bit values in 32-bit ints. This isn't - // needed for i32 and i64. - case 16: { - // There is no f8. - CHECK_ERR(check(16, val.getLanesUI8x16())); - break; - } - case 8: { - CHECK_ERR( - check(8, isFloat ? val.getLanesF16x8() : val.getLanesUI16x8())); - break; - } - case 4: { - CHECK_ERR( - check(4, isFloat ? val.getLanesF32x4() : val.getLanesI32x4())); - break; - } - case 2: { - CHECK_ERR( - check(2, isFloat ? val.getLanesF64x2() : val.getLanesI64x2())); - break; - } - default: - WASM_UNREACHABLE("unexpected number of lanes"); + ss << atIndex(); + return Err{ss.str()}; } - } else { - WASM_UNREACHABLE("unexpected expectation"); + continue; } + + // either case + bool success = false; + std::vector<std::string> expecteds; + int failedLane = -1; + for (const auto& alternative : assn.expected[i]) { + auto result = + matchAlternative((*values)[i], alternative, /*isAlternative=*/true); + if (!result.getErr()) { + success = true; + break; + } + + auto* e = result.getErr(); + expecteds.push_back(e->expected); + if (failedLane == -1 && e->lane != -1) { + failedLane = e->lane; + } + } + if (success) { + continue; + } + std::stringstream ss; + ss << "Expected one of (" << String::join(expecteds, " | ") << ")"; + if (failedLane != -1) { + ss << " at lane " << failedLane; + } + ss << " but got " << (*values)[i]; + + ss << atIndex(); + + return Err{ss.str()}; } + return Ok{}; }
diff --git a/src/tools/wasm2js.cpp b/src/tools/wasm2js.cpp index 4821070..03be0f7 100644 --- a/src/tools/wasm2js.cpp +++ b/src/tools/wasm2js.cpp
@@ -605,7 +605,15 @@ Name asmModule) { if (assn.expected.size() > 1) { Fatal() << "multivalue assert_return not supported"; + return {}; } + for (const auto& alternatives : assn.expected) { + if (alternatives.size() > 1) { + Fatal() << "(either ...) not supported"; + return {}; + } + } + auto* invoke = std::get_if<InvokeAction>(&assn.action); if (!invoke) { Fatal() << "only invoke actions are supported in assert_return"; @@ -619,7 +627,7 @@ } else { body = actual; } - } else if (auto* expectedVal = std::get_if<Literal>(&assn.expected[0])) { + } else if (auto* expectedVal = std::get_if<Literal>(&assn.expected[0][0])) { if (!expectedVal->type.isBasic()) { Fatal() << "unsupported type in assert_return: " << expectedVal->type; } @@ -648,7 +656,7 @@ Fatal() << "Unhandled type in assert: " << expected->type; } } - } else if (std::get_if<NaNResult>(&assn.expected[0])) { + } else if (std::get_if<NaNResult>(&assn.expected[0][0])) { body = builder.makeCall("isNaN", {actual}, Type::i32); } std::unique_ptr<Function> testFunc(