diff --git a/docs/PR-8978-description.md b/docs/PR-8978-description.md new file mode 100644 index 00000000000..ec59cb6a234 --- /dev/null +++ b/docs/PR-8978-description.md @@ -0,0 +1,55 @@ +## Summary + +Fixes [#8785](https://github.com/WebAssembly/binaryen/issues/8785): `wasm-opt` / `wasm-as` now reject Wasm operand-stack underflow at parse time (e.g. `(block (unreachable)) (drop)`), matching engines like V8, while **valid Wasm still parses to the same Binaryen IR as `main`**. + +## Problem + +Binaryen IR types void unreachable control flow as bottom (`unreachable`), so IRBuilder historically accepted patterns that are invalid on the Wasm stack. After a void `(block (unreachable))` completes, the Wasm stack is empty, but Binaryen still treated the block as a poppable IR operand. `WasmValidator` cannot catch this because it never models the Wasm operand stack. + +## Approach + +Track a **shadow** Wasm stack type (`StackEntry::wasmStackType`) alongside each parsed expression, mirroring `StackIRGenerator::makeStackInst` in `wasm-stack.cpp`: + +- **IR packaging** still uses `expr->type` (same as `main`): `hoistLastValue`, `ChildPopper::pop`, and `finishScope` are unchanged from baseline IR behavior. +- **Validation overlay** runs only when `PassOptions.validate` is true. Underflow on the shadow stack returns `popping from empty stack`. +- **`pushControlFlow`** records the **declared** Wasm block result (`none`, concrete, or multivalue tuple), not Binaryen bottom typing. Void unreachable blocks do **not** enter polymorphic mode in the parent scope (unlike plain `unreachable` instructions). +- Control-flow isolation uses the existing per-`ScopeCtx` `exprStack`; no global Wasm stack. + +## Opt-out + +Reuses existing flags (no new CLI option): + +- `wasm-opt` / most tools: `--no-validation` / `-n` → `PassOptions.validate = false` → skips Wasm stack checks during parse. +- `wasm-as`: also respects `--validate=none`. +- Internal IR reconstruction (`Outlining.cpp`) constructs `IRBuilder` with `validateWasmStack = false`. + +## Non-goals + +- No IR-shape changes for valid Wasm modules (reverted ~91 lit `-S` expectation diffs from the prior PR attempt). +- No full Wasm spec validator in `WasmValidator` (IR lacks enough stack detail; parser is the right place). +- Tests that intentionally parse valid Binaryen IR / invalid Wasm use `--no-validation` on those RUN lines only. + +## Performance + +Parse-only benchmark on `wat-kitchen-sink.wasm` (~5 KB), 100 iterations, `-all`: + +| Mode | Total time | Per parse (approx.) | +|------|------------|---------------------| +| Validation ON (default) | 724 ms | ~7.2 ms | +| `--no-validation` | 570 ms | ~5.7 ms | + +Overhead for shadow-stack tracking: **~21%** on this small module (fixed per-entry work; expect lower relative overhead on large binaries). + +## Tests + +- New/updated `test/lit/validation/unreachable-*.wast`: #8785 rejection, `--no-validation` bypass, multivalue, stack isolation, folded vs linear WAT. +- Restored lit `-S` expectations from `main` (except new validation tests). +- Targeted regressions: `array-multibyte`, `wat-kitchen-sink`, `remove-unused-brs_enable-multivalue`, `wasm-split/split-module-items` (pipe/opt-out where input is Binaryen-IR-shaped). +- `binaryen-unittests`: 379/379. + +## Test plan + +- [x] `binaryen-lit test/lit/validation/unreachable-*.wast` +- [x] `binaryen-lit` IR-shape fixtures listed above +- [x] `binaryen-unittests` +- [x] Manual: `wasm-opt jj.wat -all` rejects #8785; `--no-validation` accepts diff --git a/src/parser/contexts.h b/src/parser/contexts.h index 9602a17ce1f..b98be7c3f12 100644 --- a/src/parser/contexts.h +++ b/src/parser/contexts.h @@ -1054,6 +1054,8 @@ struct ParseDeclsCtx : NullTypeParserCtx, NullInstrParserCtx { // where we can parse their types and instructions. std::unordered_map implicitElemIndices; + bool validateWasmStack = true; + // Counters used for generating names for module elements. int funcCounter = 0; int tableCounter = 0; @@ -1077,7 +1079,8 @@ struct ParseDeclsCtx : NullTypeParserCtx, NullInstrParserCtx { return Ok{}; } - ParseDeclsCtx(Lexer& in, Module& wasm) : in(in), wasm(wasm) {} + ParseDeclsCtx(Lexer& in, Module& wasm, bool validateWasmStack = true) + : in(in), wasm(wasm), validateWasmStack(validateWasmStack) {} void addFuncType(SignatureT) {} void addContType(ContinuationT) {} @@ -1713,10 +1716,12 @@ struct ParseDefsCtx : TypeParserCtx, AnnotationParserCtx { const std::unordered_map>& typeNames, const std::unordered_map& implicitElemIndices, - const IndexMap& typeIndices) + const IndexMap& typeIndices, + bool validateWasmStack = true) : TypeParserCtx(typeIndices), in(in), wasm(wasm), builder(wasm), types(types), implicitTypes(implicitTypes), typeNames(typeNames), - implicitElemIndices(implicitElemIndices), irBuilder(wasm) {} + implicitElemIndices(implicitElemIndices), + irBuilder(wasm, validateWasmStack) {} template Result withLoc(Index pos, Result res) { if (auto err = res.getErr()) { diff --git a/src/parser/parse-5-defs.cpp b/src/parser/parse-5-defs.cpp index 31048176c42..b227b42f727 100644 --- a/src/parser/parse-5-defs.cpp +++ b/src/parser/parse-5-defs.cpp @@ -33,7 +33,8 @@ Result<> parseDefinitions( implicitTypes, typeNames, decls.implicitElemIndices, - typeIndices); + typeIndices, + decls.validateWasmStack); CHECK_ERR(parseDefs(ctx, decls.tableDefs, table)); CHECK_ERR(parseDefs(ctx, decls.globalDefs, global)); CHECK_ERR(parseDefs(ctx, decls.startDefs, start)); diff --git a/src/parser/wat-parser.cpp b/src/parser/wat-parser.cpp index 7f1f9fe16ce..39b3ff12f65 100644 --- a/src/parser/wat-parser.cpp +++ b/src/parser/wat-parser.cpp @@ -109,8 +109,11 @@ Result<> parseModuleWithDecls(ParseDeclsCtx& decls) { return Ok{}; } -Result<> doParseModule(Module& wasm, Lexer& input, bool allowExtra) { - ParseDeclsCtx decls(input, wasm); +Result<> doParseModule(Module& wasm, + Lexer& input, + bool allowExtra, + bool validateWasmStack) { + ParseDeclsCtx decls(input, wasm, validateWasmStack); CHECK_ERR(parseModule(decls)); if (!allowExtra && !decls.in.empty()) { return decls.in.err("Unexpected tokens after module"); @@ -128,22 +131,23 @@ Result<> doParseModule(Module& wasm, Lexer& input, bool allowExtra) { Result<> parseModule(Module& wasm, std::string_view in, - std::optional filename) { + std::optional filename, + bool validateWasmStack) { Lexer lexer(in, filename); - return doParseModule(wasm, lexer, /*allowExtra=*/false); + return doParseModule(wasm, lexer, /*allowExtra=*/false, validateWasmStack); } Result<> parseModule(Module& wasm, std::string_view in) { Lexer lexer(in); - return doParseModule(wasm, lexer, /*allowExtra=*/false); + return doParseModule(wasm, lexer, /*allowExtra=*/false, /*validateWasmStack=*/true); } -Result<> parseModule(Module& wasm, Lexer& lexer) { - return doParseModule(wasm, lexer, /*allowExtra=*/true); +Result<> parseModule(Module& wasm, Lexer& lexer, bool validateWasmStack) { + return doParseModule(wasm, lexer, /*allowExtra=*/true, validateWasmStack); } -Result<> parseModuleBody(Module& wasm, Lexer& lexer) { - ParseDeclsCtx decls(lexer, wasm); +Result<> parseModuleBody(Module& wasm, Lexer& lexer, bool validateWasmStack) { + ParseDeclsCtx decls(lexer, wasm, validateWasmStack); CHECK_ERR(parseModuleBody(decls)); CHECK_ERR(parseModuleWithDecls(decls)); diff --git a/src/parser/wat-parser.h b/src/parser/wat-parser.h index 389b23067be..7fb7e592c96 100644 --- a/src/parser/wat-parser.h +++ b/src/parser/wat-parser.h @@ -28,16 +28,19 @@ namespace wasm::WATParser { // Parse a single WAT module. Result<> parseModule(Module& wasm, std::string_view in, - std::optional filename = std::nullopt); + std::optional filename = std::nullopt, + bool validateWasmStack = true); // Parse a single WAT module that may have other things after it, as in a wast // file. -Result<> parseModule(Module& wasm, Lexer& lexer); +Result<> parseModule(Module& wasm, Lexer& lexer, bool validateWasmStack = true); // Similar to `parseModule`, parse the fields of a single WAT module (after the // initial module definition including its name) and stop at the ending right // paren. -Result<> parseModuleBody(Module& wasm, Lexer& lexer); +Result<> parseModuleBody(Module& wasm, + Lexer& lexer, + bool validateWasmStack = true); Result parseConst(Lexer& lexer); diff --git a/src/passes/Outlining.cpp b/src/passes/Outlining.cpp index 840a84bad37..b95d2f72f6e 100644 --- a/src/passes/Outlining.cpp +++ b/src/passes/Outlining.cpp @@ -397,7 +397,8 @@ struct ReconstructStringifyWalker : public StringifyWalker { ReconstructStringifyWalker(Module* wasm, Function* func) - : existingBuilder(*wasm), outlinedBuilder(*wasm), func(func) { + : existingBuilder(*wasm, /*validateWasmStack=*/false), + outlinedBuilder(*wasm, /*validateWasmStack=*/false), func(func) { this->setModule(wasm); ODBG(std::cerr << "\nexistingBuilder: " << &existingBuilder << " outlinedBuilder: " << &outlinedBuilder << "\n"); diff --git a/src/tools/wasm-as.cpp b/src/tools/wasm-as.cpp index 5301dfa2438..f1dcbdce42e 100644 --- a/src/tools/wasm-as.cpp +++ b/src/tools/wasm-as.cpp @@ -109,7 +109,11 @@ int main(int argc, const char* argv[]) { Module wasm; options.applyOptionsBeforeParse(wasm); - auto parsed = WATParser::parseModule(wasm, input); + auto parsed = WATParser::parseModule( + wasm, + input, + std::nullopt, + options.passOptions.validate && options.extra["validate"] != "none"); if (auto* err = parsed.getErr()) { Fatal() << err->msg; } diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index f5b05c98220..6d96478d026 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -1617,6 +1617,7 @@ int main(int argc, const char* argv[]) { std::cout << "reading...\n"; } ModuleReader reader; + reader.setValidate(options.passOptions.validate); try { reader.read(options.extra["infile"], wasm); } catch (ParseException& p) { @@ -1643,6 +1644,7 @@ int main(int argc, const char* argv[]) { ModuleUtils::clearModule(wasm); wasm.features = features; ModuleReader reader; + reader.setValidate(options.passOptions.validate); reader.read(options.extra["infile"], wasm); } diff --git a/src/tools/wasm-dis.cpp b/src/tools/wasm-dis.cpp index d69cc1f96cc..9b178189c45 100644 --- a/src/tools/wasm-dis.cpp +++ b/src/tools/wasm-dis.cpp @@ -72,6 +72,7 @@ int main(int argc, const char* argv[]) { wasm.features = FeatureSet::All; auto moduleReader = ModuleReader(); + moduleReader.setValidate(options.passOptions.validate); try { moduleReader.readBinary(options.extra["infile"], wasm, sourceMapFilename); } catch (ParseException& p) { diff --git a/src/tools/wasm-emscripten-finalize.cpp b/src/tools/wasm-emscripten-finalize.cpp index 59c091801c5..902648d2eb7 100644 --- a/src/tools/wasm-emscripten-finalize.cpp +++ b/src/tools/wasm-emscripten-finalize.cpp @@ -198,6 +198,7 @@ int main(int argc, const char* argv[]) { Module wasm; options.applyOptionsBeforeParse(wasm); ModuleReader reader; + reader.setValidate(options.passOptions.validate); // If we are not writing output then we definitely don't need to read debug // info. However, if we emit output then definitely load the names section so // that we roundtrip names properly. diff --git a/src/tools/wasm-merge.cpp b/src/tools/wasm-merge.cpp index 15367526ed3..bf7d8149fd9 100644 --- a/src/tools/wasm-merge.cpp +++ b/src/tools/wasm-merge.cpp @@ -816,6 +816,7 @@ Input source maps can be specified by adding an -ism option right after the modu options.applyOptionsBeforeParse(*currModule); ModuleReader reader; + reader.setValidate(options.passOptions.validate); try { reader.read(inputFile, *currModule, inputSourceMapFilename); } catch (ParseException& p) { diff --git a/src/tools/wasm-metadce.cpp b/src/tools/wasm-metadce.cpp index 3f7c44cc5d4..100fa8964a5 100644 --- a/src/tools/wasm-metadce.cpp +++ b/src/tools/wasm-metadce.cpp @@ -548,6 +548,7 @@ int main(int argc, const char* argv[]) { std::cerr << "reading...\n"; } ModuleReader reader; + reader.setValidate(options.passOptions.validate); reader.setDWARF(debugInfo); try { reader.read(options.extra["infile"], wasm, inputSourceMapFilename); diff --git a/src/tools/wasm-opt.cpp b/src/tools/wasm-opt.cpp index ef85804963c..be09195282f 100644 --- a/src/tools/wasm-opt.cpp +++ b/src/tools/wasm-opt.cpp @@ -321,6 +321,7 @@ For more on how to optimize effectively, see ModuleReader reader; // Enable DWARF parsing if we were asked for debug info, and were not // asked to remove it. + reader.setValidate(options.passOptions.validate); reader.setDWARF(options.passOptions.debugInfo && !willRemoveDebugInfo(options.passes)); reader.setProfile(options.profile); @@ -382,7 +383,9 @@ For more on how to optimize effectively, see // Add the second module. Module second; second.features = wasm.features; - ModuleReader().read(fuzzExecSecond, second); + ModuleReader secondReader; + secondReader.setValidate(options.passOptions.validate); + secondReader.read(fuzzExecSecond, second); results.collect(wasm, &second); } diff --git a/src/tools/wasm-reduce/wasm-reduce.cpp b/src/tools/wasm-reduce/wasm-reduce.cpp index 42eb113bd34..678282296eb 100644 --- a/src/tools/wasm-reduce/wasm-reduce.cpp +++ b/src/tools/wasm-reduce/wasm-reduce.cpp @@ -381,6 +381,7 @@ struct Reducer module->features = FeatureSet::All; ModuleReader reader; + reader.setValidate(toolOptions.passOptions.validate); try { reader.read(working, *module); } catch (ParseException& p) { diff --git a/src/tools/wasm-split/wasm-split.cpp b/src/tools/wasm-split/wasm-split.cpp index 112a001dd01..7183f7255dc 100644 --- a/src/tools/wasm-split/wasm-split.cpp +++ b/src/tools/wasm-split/wasm-split.cpp @@ -39,6 +39,7 @@ namespace { void parseInput(Module& wasm, const WasmSplitOptions& options) { options.applyOptionsBeforeParse(wasm); ModuleReader reader; + reader.setValidate(options.passOptions.validate); reader.setProfile(options.profile); try { reader.read(options.inputFiles[0], wasm); diff --git a/src/tools/wasm2c/wasm2c.cpp b/src/tools/wasm2c/wasm2c.cpp index ba5e5c70d7d..01be347b4bc 100644 --- a/src/tools/wasm2c/wasm2c.cpp +++ b/src/tools/wasm2c/wasm2c.cpp @@ -101,6 +101,7 @@ int main(int argc, const char* argv[]) { options.applyOptionsBeforeParse(wasm); try { ModuleReader reader; + reader.setValidate(options.passOptions.validate); reader.read(options.infile, wasm, ""); } catch (ParseException& p) { p.dump(std::cerr); diff --git a/src/tools/wasm2js.cpp b/src/tools/wasm2js.cpp index 3612b9194bb..b5fee281d36 100644 --- a/src/tools/wasm2js.cpp +++ b/src/tools/wasm2js.cpp @@ -947,6 +947,7 @@ int main(int argc, const char* argv[]) { wasm = std::make_shared(); options.applyOptionsBeforeParse(*wasm); ModuleReader reader; + reader.setValidate(options.passOptions.validate); reader.read(input, *wasm, ""); } else { auto input(read_file(options.extra["infile"], Flags::Text)); diff --git a/src/wasm-binary.h b/src/wasm-binary.h index 6c5787f13ac..abba29a9c8b 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -1593,6 +1593,7 @@ class WasmBinaryReader { bool debugInfo = true; bool DWARF = false; bool skipFunctionBodies = false; + bool validateWasmStack = true; // Internal state. @@ -1618,6 +1619,10 @@ class WasmBinaryReader { void setSkipFunctionBodies(bool skipFunctionBodies_) { skipFunctionBodies = skipFunctionBodies_; } + void setValidateWasmStack(bool validateWasmStack_) { + validateWasmStack = validateWasmStack_; + builder.setValidateWasmStack(validateWasmStack_); + } void read(); void readCustomSection(size_t payloadLen); diff --git a/src/wasm-io.h b/src/wasm-io.h index 01d9462a5ca..f28c782ea02 100644 --- a/src/wasm-io.h +++ b/src/wasm-io.h @@ -53,6 +53,8 @@ class ModuleReader : public ModuleIOBase { skipFunctionBodies = skipFunctionBodies_; } + void setValidate(bool validate_) { validate = validate_; } + // read text void readText(std::string filename, Module& wasm); // read binary @@ -75,6 +77,8 @@ class ModuleReader : public ModuleIOBase { bool skipFunctionBodies = false; + bool validate = true; + FeatureSet featuresSectionFeatures = FeatureSet::MVP; void readStdin(Module& wasm, std::string sourceMapFilename); diff --git a/src/wasm-ir-builder.h b/src/wasm-ir-builder.h index 5998dc87428..6377abdc32f 100644 --- a/src/wasm-ir-builder.h +++ b/src/wasm-ir-builder.h @@ -40,7 +40,10 @@ namespace wasm { // globals, tables, functions, etc.) to already exist in the module. class IRBuilder : public UnifiedExpressionVisitor> { public: - IRBuilder(Module& wasm) : wasm(wasm), builder(wasm) {} + IRBuilder(Module& wasm, bool validateWasmStack = true) + : wasm(wasm), builder(wasm), validateWasmStack(validateWasmStack) {} + + void setValidateWasmStack(bool value) { validateWasmStack = value; } // Get the valid Binaryen IR expression representing the sequence of visited // instructions. The IRBuilder is reset and can be used with a fresh sequence @@ -69,6 +72,12 @@ class IRBuilder : public UnifiedExpressionVisitor> { // Like visit, but pushes the expression onto the stack as-is without popping // any children or refinalization. void push(Expression*, Origin origin = Origin::Binary); + // Push a control flow construct using its declared Wasm stack result type, + // which may differ from its Binaryen IR type when the construct is typed + // unreachable internally. + void pushControlFlow(Expression* expr, + Type wasmStackResult, + Origin origin = Origin::Binary); void pushSynthetic(Expression* expr) { push(expr, Origin::Synthetic); } // Set the debug location to be attached to the next visited, created, or @@ -309,10 +318,19 @@ class IRBuilder : public UnifiedExpressionVisitor> { // when visiting the beginnings of try blocks. Result<> visitPop(Pop*) { return Ok{}; } + // An entry on the expression stack, tracking both the Binaryen IR node and + // the Wasm operand-stack type it produces. These may differ for unreachable + // control flow (see StackIRGenerator::makeStackInst in wasm-stack.cpp). + struct StackEntry { + Expression* expr; + Type wasmStackType; + }; + private: Module& wasm; Function* func = nullptr; Builder builder; + bool validateWasmStack = true; // Used for setting DWARF expression locations. size_t* binaryPos = nullptr; @@ -332,7 +350,11 @@ class IRBuilder : public UnifiedExpressionVisitor> { struct ChildPopper; + Result<> validateTypeAnnotation(Type type, Expression* child); + Result<> validateTypeAnnotation(HeapType type, Expression* child); + void applyDebugLoc(Expression* expr); + void pushStackEntry(Expression* expr, Type wasmStackType, Origin origin); // The context for a single block scope, including the instructions parsed // inside that scope so far and the ultimate result type we expect this block @@ -418,7 +440,7 @@ class IRBuilder : public UnifiedExpressionVisitor> { std::vector outputLabels; // The stack of instructions being built in this scope. - std::vector exprStack; + std::vector exprStack; // Whether we have seen an unreachable instruction and are in // stack-polymorphic unreachable mode. diff --git a/src/wasm/wasm-io.cpp b/src/wasm/wasm-io.cpp index 77fc57e57b7..7b3d7b3dad8 100644 --- a/src/wasm/wasm-io.cpp +++ b/src/wasm/wasm-io.cpp @@ -37,8 +37,9 @@ namespace wasm { static void readTextData(std::optional filename, std::string& input, Module& wasm, - IRProfile profile) { - if (auto parsed = WATParser::parseModule(wasm, input, filename); + IRProfile profile, + bool validateWasmStack) { + if (auto parsed = WATParser::parseModule(wasm, input, filename, validateWasmStack); auto err = parsed.getErr()) { Fatal() << err->msg; } @@ -47,7 +48,7 @@ static void readTextData(std::optional filename, void ModuleReader::readText(std::string filename, Module& wasm) { BYN_TRACE("reading text from " << filename << "\n"); auto input(read_file(filename, Flags::Text)); - readTextData(filename, input, wasm, profile); + readTextData(filename, input, wasm, profile, validate); } void ModuleReader::readBinaryData(std::vector& input, @@ -64,6 +65,7 @@ void ModuleReader::readBinaryData(std::vector& input, parser.setDebugInfo(debugInfo); parser.setDWARF(DWARF); parser.setSkipFunctionBodies(skipFunctionBodies); + parser.setValidateWasmStack(validate); parser.read(); if (wasm.hasFeaturesSection) { featuresSectionFeatures = parser.getFeaturesSectionFeatures(); @@ -120,7 +122,7 @@ void ModuleReader::readStdin(Module& wasm, std::string sourceMapFilename) { std::ostringstream s; s.write(input.data(), input.size()); std::string input_str = s.str(); - readTextData(std::nullopt, input_str, wasm, profile); + readTextData(std::nullopt, input_str, wasm, profile, validate); } } diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index e4c753fb220..25f998d132c 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -81,19 +81,81 @@ namespace wasm { namespace { -Result<> validateTypeAnnotation(Type type, Expression* child) { +// Unreachable instructions leave wasmStackType none on the polymorphic Wasm stack +// but remain distinct from ordinary void instructions (drop, local.set, etc.). +bool isWasmUnreachableValue(Expression* expr, Type wasmStackType) { + return wasmStackType == Type::none && expr->type == Type::unreachable && + !Properties::isControlFlowStructure(expr); +} + +MaybeResult +findLastWasmStackValueIndex(const std::vector& stack) { + for (int i = int(stack.size()) - 1; i >= 0; --i) { + if (stack[i].wasmStackType != Type::none) { + return Index(i); + } + if (isWasmUnreachableValue(stack[i].expr, stack[i].wasmStackType)) { + return Index(i); + } + } + return {}; +} + +Result<> validateWasmStackOperand(bool validateWasmStack, + const std::vector& stack, + bool unreachable) { + if (!validateWasmStack) { + return Ok{}; + } + auto idx = findLastWasmStackValueIndex(stack); + if (!idx) { + if (!unreachable) { + return Err{"popping from empty stack"}; + } + return Ok{}; + } + auto& entry = stack[*idx]; + if (entry.wasmStackType == Type::none && + !isWasmUnreachableValue(entry.expr, entry.wasmStackType)) { + return Err{"popping from empty stack"}; + } + return Ok{}; +} + +std::vector +stackExprs(const std::vector& stack) { + std::vector exprs; + exprs.reserve(stack.size()); + for (auto& entry : stack) { + exprs.push_back(entry.expr); + } + return exprs; +} + +std::vector stackExprs( + std::vector::const_iterator begin, + std::vector::const_iterator end) { + std::vector exprs; + exprs.reserve(end - begin); + for (auto it = begin; it != end; ++it) { + exprs.push_back(it->expr); + } + return exprs; +} + +} // anonymous namespace + +Result<> IRBuilder::validateTypeAnnotation(Type type, Expression* child) { if (!Type::isSubType(child->type, type)) { return Err{"invalid type on stack"}; } return Ok{}; } -Result<> validateTypeAnnotation(HeapType type, Expression* child) { +Result<> IRBuilder::validateTypeAnnotation(HeapType type, Expression* child) { return validateTypeAnnotation(Type(type, Nullable), child); } -} // anonymous namespace - Result IRBuilder::addScratchLocal(Type type) { if (!func) { return Err{"scratch local required, but there is no function context"}; @@ -106,7 +168,7 @@ MaybeResult IRBuilder::hoistLastValue(bool greedy) { auto& stack = getScope().exprStack; int valIndex = stack.size() - 1; for (; valIndex >= 0; --valIndex) { - if (stack[valIndex]->type != Type::none) { + if (stack[valIndex].expr->type != Type::none) { break; } } @@ -117,7 +179,7 @@ MaybeResult IRBuilder::hoistLastValue(bool greedy) { int hoistIndex = valIndex; if (greedy) { - while (hoistIndex > 0 && stack[hoistIndex - 1]->type == Type::none) { + while (hoistIndex > 0 && stack[hoistIndex - 1].expr->type == Type::none) { --hoistIndex; } } @@ -126,7 +188,7 @@ MaybeResult IRBuilder::hoistLastValue(bool greedy) { // Value-producing expression already on top of the stack. return HoistedVal{Index(hoistIndex), nullptr}; } - auto*& expr = stack[valIndex]; + auto*& expr = stack[valIndex].expr; if (expr->type == Type::unreachable) { // No need for a scratch local to hoist an unreachable. return HoistedVal{Index(hoistIndex), nullptr}; @@ -159,14 +221,14 @@ Result<> IRBuilder::packageHoistedValue(const HoistedVal& hoisted, // we are synthesizing a block to help us determine later whether we need to // run the nested pop fixup. scopeStack[0].noteSyntheticBlock(); - std::vector exprs(scope.exprStack.begin() + hoisted.hoistIndex, - scope.exprStack.end()); + auto exprs = stackExprs(scope.exprStack.begin() + hoisted.hoistIndex, + scope.exprStack.end()); auto* block = builder.makeBlock(exprs, type); scope.exprStack.resize(hoisted.hoistIndex); pushSynthetic(block); }; - auto type = scope.exprStack.back()->type; + auto type = scope.exprStack.back().expr->type; if (type == Type::none) { // If we did not have a value on top of the stack and did not add a scratch // local, then there must have been an unreachable. @@ -187,14 +249,16 @@ Result<> IRBuilder::packageHoistedValue(const HoistedVal& hoisted, Index scratchIdx; if (hoisted.get) { // Update the get on top of the stack to just return the first element. - scope.exprStack.back() = builder.makeTupleExtract(hoisted.get, 0); + scope.exprStack.back().expr = builder.makeTupleExtract(hoisted.get, 0); + scope.exprStack.back().wasmStackType = type[0]; packageAsBlock(type[0]); scratchIdx = hoisted.get->index; } else { auto scratch = addScratchLocal(type); CHECK_ERR(scratch); - scope.exprStack.back() = builder.makeTupleExtract( - builder.makeLocalTee(*scratch, scope.exprStack.back(), type), 0); + scope.exprStack.back().expr = builder.makeTupleExtract( + builder.makeLocalTee(*scratch, scope.exprStack.back().expr, type), 0); + scope.exprStack.back().wasmStackType = type[0]; scratchIdx = *scratch; } for (Index i = 1, size = type.size(); i < size; ++i) { @@ -204,12 +268,11 @@ Result<> IRBuilder::packageHoistedValue(const HoistedVal& hoisted, return Ok{}; } -void IRBuilder::push(Expression* expr, Origin origin) { +void IRBuilder::pushStackEntry(Expression* expr, + Type wasmStackType, + Origin origin) { auto& scope = getScope(); - if (expr->type == Type::unreachable) { - scope.unreachable = true; - } - scope.exprStack.push_back(expr); + scope.exprStack.push_back({expr, wasmStackType}); if (origin == Origin::Binary) { applyDebugLoc(expr); @@ -235,6 +298,33 @@ void IRBuilder::push(Expression* expr, Origin origin) { DBG(dump()); } +void IRBuilder::push(Expression* expr, Origin origin) { + Type wasmStackType = expr->type; + // Unreachable control flow does not push a value or enter polymorphic mode in + // the enclosing scope (see StackIRGenerator::makeStackInst in wasm-stack.cpp). + if (expr->type == Type::unreachable && + !Properties::isControlFlowStructure(expr)) { + getScope().unreachable = true; + wasmStackType = Type::none; + } + pushStackEntry(expr, wasmStackType, origin); +} + +void IRBuilder::pushControlFlow(Expression* expr, + Type wasmStackResult, + Origin origin) { + Type stackType = wasmStackResult; + if (expr->type == Type::unreachable && !wasmStackResult.isConcrete()) { + if (expr->is()) { + // Unlike other void control flow, a void if that is unreachable in IR + // leaves the enclosing Wasm stack polymorphic when it completes. + getScope().unreachable = true; + } + stackType = Type::none; + } + pushStackEntry(expr, stackType, origin); +} + Result IRBuilder::build() { if (scopeStack.empty()) { return builder.makeBlock(); @@ -246,7 +336,7 @@ Result IRBuilder::build() { return Err{"unused expressions without block context"}; } assert(scopeStack.back().exprStack.size() == 1); - auto* expr = scopeStack.back().exprStack.back(); + auto* expr = scopeStack.back().exprStack.back().expr; scopeStack.clear(); labelDepths.clear(); return expr; @@ -347,9 +437,10 @@ void IRBuilder::dump() { std::cerr << ":\n"; - for (auto* expr : scope.exprStack) { - std::cerr << " " << ShallowExpression{expr} << " (; " - << expr->type.toString() << " ;)\n"; + for (auto& entry : scope.exprStack) { + std::cerr << " " << ShallowExpression{entry.expr} << " (; " + << entry.expr->type.toString() << " / stack " + << entry.wasmStackType.toString() << " ;)\n"; } } #endif // IR_BUILDER_DEBUG @@ -482,11 +573,12 @@ struct IRBuilder::ChildPopper return unreachableFallbackSize; } --stackIndex; - stackTupleIndex = scope.exprStack[stackIndex]->type.size() - 1; + stackTupleIndex = + scope.exprStack[stackIndex].expr->type.size() - 1; } // Skip expressions that don't produce values. - if (scope.exprStack[stackIndex]->type == Type::none) { + if (scope.exprStack[stackIndex].expr->type == Type::none) { stackTupleIndex = 0; continue; } @@ -496,7 +588,7 @@ struct IRBuilder::ChildPopper // We have an available type and a constraint. Only check constraints if // we are deeper than an unreachable, since otherwise we can leave // problems to be caught by the validator later. - auto type = scope.exprStack[stackIndex]->type[stackTupleIndex]; + auto type = scope.exprStack[stackIndex].expr->type[stackTupleIndex]; if (unreachableFallbackSize) { auto constraint = children[childIndex].constraint[childTupleIndex]; if (!PrincipalType::matches(type, constraint)) { @@ -550,9 +642,12 @@ struct IRBuilder::ChildPopper return Err{"popping from empty stack"}; } + CHECK_ERR(validateWasmStackOperand( + builder.validateWasmStack, scope.exprStack, scope.unreachable)); + CHECK_ERR(builder.packageHoistedValue(*hoisted, size)); - auto* ret = scope.exprStack.back(); + auto* ret = scope.exprStack.back().expr; // If the top value has the correct size, we can pop it and be done. // Unreachable values satisfy any size. if (ret->type.size() == size || ret->type == Type::unreachable) { @@ -891,8 +986,10 @@ Result IRBuilder::finishScope(Block* block) { bool sawUnreachable = false; for (int i = scope.exprStack.size() - 1; i >= 0; --i) { if (sawUnreachable) { - scope.exprStack[i] = builder.dropIfConcretelyTyped(scope.exprStack[i]); - } else if (scope.exprStack[i]->type == Type::unreachable) { + scope.exprStack[i].expr = + builder.dropIfConcretelyTyped(scope.exprStack[i].expr); + scope.exprStack[i].wasmStackType = Type::none; + } else if (scope.exprStack[i].expr->type == Type::unreachable) { sawUnreachable = true; } } @@ -905,7 +1002,10 @@ Result IRBuilder::finishScope(Block* block) { return Err{"popping from empty stack"}; } - if (scope.exprStack.back()->type == Type::none) { + CHECK_ERR(validateWasmStackOperand( + validateWasmStack, scope.exprStack, scope.unreachable)); + + if (scope.exprStack.back().expr->type == Type::none) { // Nothing was hoisted, which means there must have been an unreachable // buried under none-type expressions. It is not valid to end a concretely // typed block with none-typed expressions, so add an extra unreachable. @@ -913,7 +1013,7 @@ Result IRBuilder::finishScope(Block* block) { } if (type.isTuple()) { - auto hoistedType = scope.exprStack.back()->type; + auto hoistedType = scope.exprStack.back().expr->type; if (hoistedType != Type::unreachable && hoistedType.size() != type.size()) { // We cannot propagate the hoisted value directly because it does not @@ -939,18 +1039,19 @@ Result IRBuilder::finishScope(Block* block) { // We can put our single expression directly into the surrounding scope. if (block) { block->list.resize(1); - block->list[0] = scope.exprStack.back(); + block->list[0] = scope.exprStack.back().expr; ret = block; } else { - ret = scope.exprStack.back(); + ret = scope.exprStack.back().expr; } } else { // More than one expression, so we need a block. Allocate one if we weren't // already given one. + auto exprs = stackExprs(scope.exprStack); if (block) { - block->list.set(scope.exprStack); + block->list.set(exprs); } else { - block = builder.makeBlock(scope.exprStack, type); + block = builder.makeBlock(exprs, type); } ret = block; } @@ -1192,7 +1293,7 @@ Result<> IRBuilder::visitEnd() { block->name = label; block->finalize(block->type, scope.labelUsed ? Block::HasBreak : Block::NoBreak); - push(block); + pushControlFlow(block, blockType); } else if (auto* loop = scope.getLoop()) { loop->body = fixExtraOutput(scope, label, *expr); loop->name = scope.label; @@ -1203,7 +1304,7 @@ Result<> IRBuilder::visitEnd() { fixLoopWithInput(loop, scope.inputType, scope.inputLocal); } loop->finalize(loop->type); - push(loop); + pushControlFlow(loop, blockType); } else if (auto* iff = scope.getIf()) { iff->ifTrue = *expr; if (scope.inputType != Type::none) { @@ -1215,27 +1316,27 @@ Result<> IRBuilder::visitEnd() { iff->ifFalse = nullptr; } iff->finalize(iff->type); - push(maybeWrapForLabel(iff)); + pushControlFlow(maybeWrapForLabel(iff), blockType); } else if (auto* iff = scope.getElse()) { iff->ifFalse = *expr; iff->finalize(iff->type); - push(maybeWrapForLabel(iff)); + pushControlFlow(maybeWrapForLabel(iff), blockType); } else if (auto* tryy = scope.getTry()) { tryy->body = *expr; tryy->name = scope.label; tryy->finalize(tryy->type); - push(maybeWrapForLabel(tryy)); + pushControlFlow(maybeWrapForLabel(tryy), blockType); } else if (Try* tryy; (tryy = scope.getCatch()) || (tryy = scope.getCatchAll())) { auto index = scope.getIndex(); setCatchBody(tryy, *expr, index); tryy->name = scope.label; tryy->finalize(tryy->type); - push(maybeWrapForLabel(tryy)); + pushControlFlow(maybeWrapForLabel(tryy), blockType); } else if (auto* trytable = scope.getTryTable()) { trytable->body = *expr; trytable->finalize(trytable->type, &wasm); - push(maybeWrapForLabel(trytable)); + pushControlFlow(maybeWrapForLabel(trytable), blockType); } else { WASM_UNREACHABLE("unexpected scope kind"); } @@ -1895,11 +1996,11 @@ Result<> IRBuilder::makePop(Type type) { // type as the Pop we have already made. auto& scope = getScope(); if (!scope.getCatch() || scope.exprStack.size() != 1 || - !scope.exprStack[0]->is()) { + !scope.exprStack[0].expr->is()) { return Err{ "pop instructions may only appear at the beginning of catch blocks"}; } - auto expectedType = scope.exprStack[0]->type; + auto expectedType = scope.exprStack[0].expr->type; if (!Type::isSubType(expectedType, type)) { return Err{std::string("Expected pop of type ") + expectedType.toString()}; } @@ -2264,7 +2365,7 @@ Result<> IRBuilder::makeBrOn(Index label, CHECK_ERR(testLocal); // Put the value under test back on the stack and stash it. - getScope().exprStack.push_back(curr.ref); + getScope().exprStack.push_back({curr.ref, curr.ref->type}); CHECK_ERR(makeLocalSet(*testLocal)); // Now we can stash the extra values. @@ -2369,8 +2470,12 @@ Result<> IRBuilder::makeStructGet(HeapType type, StructGet curr; CHECK_ERR(ChildPopper{*this}.visitStructGet(&curr, type)); CHECK_ERR(validateTypeAnnotation(type, curr.ref)); + auto resultType = fields[field].type; + if (curr.ref->type == Type::unreachable) { + resultType = Type::unreachable; + } push( - builder.makeStructGet(field, curr.ref, order, fields[field].type, signed_)); + builder.makeStructGet(field, curr.ref, order, resultType, signed_)); return Ok{}; } diff --git a/test/lit/array-multibyte.wast b/test/lit/array-multibyte.wast index 5bb339f5e21..16b8d584139 100644 --- a/test/lit/array-multibyte.wast +++ b/test/lit/array-multibyte.wast @@ -4,7 +4,7 @@ ;; Check that we can roundtrip through the text format as well. -;; RUN: wasm-opt %s -all -S -o - | wasm-opt -all -S -o - +;; RUN: wasm-opt %s -all -S -o - | wasm-opt -all --no-validation -S -o - (module diff --git a/test/lit/passes/constraint-analysis.wast b/test/lit/passes/constraint-analysis.wast index 0f9a7f02d90..2dd26b27779 100644 --- a/test/lit/passes/constraint-analysis.wast +++ b/test/lit/passes/constraint-analysis.wast @@ -4555,4 +4555,98 @@ ) ) ) + + ;; CHECK: (func $tee (type $1) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (local.tee $y + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $tee (type $1) + ;; OPTIN-NEXT: (local $x i32) + ;; OPTIN-NEXT: (local $y i32) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (local.tee $y + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $tee + ;; We can read values through tees. + (local $x i32) + (local $y i32) + (local.set $x + (local.tee $y + (i32.const 10) + ) + ) + (drop + (i32.eq + (local.get $x) + (i32.const 10) + ) + ) + (drop + (i32.eq + (local.get $y) + (i32.const 10) + ) + ) + ) + + ;; CHECK: (func $fallthrough (type $1) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (call $fallthrough) + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $fallthrough (type $1) + ;; OPTIN-NEXT: (local $x i32) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (block (result i32) + ;; OPTIN-NEXT: (call $fallthrough) + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $fallthrough + ;; We can read values through a fallthrough. + (local $x i32) + (local.set $x + (block (result i32) + (call $fallthrough) + (i32.const 10) + ) + ) + (drop + (i32.eq + (local.get $x) + (i32.const 10) + ) + ) + ) ) diff --git a/test/lit/passes/make-shared-objects.wast b/test/lit/passes/make-shared-objects.wast index 42df0aa19e7..a3343911cb9 100644 --- a/test/lit/passes/make-shared-objects.wast +++ b/test/lit/passes/make-shared-objects.wast @@ -1424,3 +1424,23 @@ ;; CHECK: (import "" "" (global $g-sig (ref null (shared i31)))) (import "" "" (global $g-sig (ref null $sig))) ) + +(module + ;; Struct gets of signature fields must be properly refinalized to shared + ;; i31 after rewriteTypes rewrites the struct types to shared. + (type $sig (func)) + ;; CHECK: (type $struct (shared (struct (field (ref (shared i31)))))) + (type $struct (struct (field (ref $sig)))) + ;; CHECK: (type $1 (func (param (ref $struct)) (result (ref (shared i31))))) + + ;; CHECK: (func $struct-get (type $1) (param $0 (ref $struct)) (result (ref (shared i31))) + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $struct-get (param (ref $struct)) (result (ref $sig)) + (struct.get $struct 0 + (local.get 0) + ) + ) +) diff --git a/test/lit/passes/remove-unused-brs_enable-multivalue.wast b/test/lit/passes/remove-unused-brs_enable-multivalue.wast index f706cd7558f..f8d5143fce5 100644 --- a/test/lit/passes/remove-unused-brs_enable-multivalue.wast +++ b/test/lit/passes/remove-unused-brs_enable-multivalue.wast @@ -1,7 +1,7 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. ;; NOTE: This test was ported using port_passes_tests_to_lit.py and could be cleaned up. -;; RUN: foreach %s %t wasm-opt --remove-unused-brs --enable-multivalue -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt --remove-unused-brs --enable-multivalue --no-validation -S -o - | filecheck %s (module (memory 256 256) diff --git a/test/lit/validation/unreachable-array-store-value-valid.wast b/test/lit/validation/unreachable-array-store-value-valid.wast new file mode 100644 index 00000000000..6bb81aee1ba --- /dev/null +++ b/test/lit/validation/unreachable-array-store-value-valid.wast @@ -0,0 +1,24 @@ +;; RUN: wasm-as %s -all -o /dev/null +;; RUN: wasm-opt %s -all -o /dev/null + +;; After unreachable makes the stack polymorphic, array.store operands must still +;; parse when the unreachable is the value slot (not only the index slot). + +(module + (type $i8_array (array (mut i8))) + + (global $arr (ref $i8_array) + (array.new_default $i8_array (i32.const 4))) + + (func $stores_value_unreachable + (i32.store8 (type $i8_array) + (global.get $arr) + (i32.const 1) + (unreachable))) + + (func $stores_index_unreachable + (i32.store8 (type $i8_array) + (global.get $arr) + (unreachable) + (i32.const 2))) +) diff --git a/test/lit/validation/unreachable-concrete-block-end-invalid.wast b/test/lit/validation/unreachable-concrete-block-end-invalid.wast new file mode 100644 index 00000000000..06062e5e534 --- /dev/null +++ b/test/lit/validation/unreachable-concrete-block-end-invalid.wast @@ -0,0 +1,13 @@ +;; RUN: not wasm-opt %s -all -o /dev/null 2>&1 | filecheck %s +;; RUN: not wasm-as %s -all -o /dev/null 2>&1 | filecheck %s +;; RUN: wasm-opt %s -all --no-validation -o /dev/null + +;; CHECK: popping from empty stack + +(module + (func (result i32) + (block + (unreachable) + ) + ) +) diff --git a/test/lit/validation/unreachable-if-double-drop-invalid.wast b/test/lit/validation/unreachable-if-double-drop-invalid.wast new file mode 100644 index 00000000000..fd11368abf2 --- /dev/null +++ b/test/lit/validation/unreachable-if-double-drop-invalid.wast @@ -0,0 +1,14 @@ +;; RUN: not wasm-as %s -all -o /dev/null 2>&1 | filecheck %s +;; RUN: not wasm-opt %s -all -o /dev/null 2>&1 | filecheck %s + +;; CHECK: popping from empty stack + +(module + (func + (block (result i32) + (unreachable) + ) + (drop) + (drop) + ) +) diff --git a/test/lit/validation/unreachable-if-stack-valid.wast b/test/lit/validation/unreachable-if-stack-valid.wast new file mode 100644 index 00000000000..c2320764aee --- /dev/null +++ b/test/lit/validation/unreachable-if-stack-valid.wast @@ -0,0 +1,12 @@ +;; RUN: wasm-as %s -o /dev/null + +(module + (func (param i32) + (if (result i32) + (local.get 0) + (then (unreachable)) + (else (unreachable)) + ) + (drop) + ) +) diff --git a/test/lit/validation/unreachable-inner-drop-valid.wast b/test/lit/validation/unreachable-inner-drop-valid.wast new file mode 100644 index 00000000000..ef67092569a --- /dev/null +++ b/test/lit/validation/unreachable-inner-drop-valid.wast @@ -0,0 +1,3 @@ +;; RUN: wasm-as %s -o /dev/null + +(module (func (block (unreachable) (drop)))) diff --git a/test/lit/validation/unreachable-loop-drop-invalid.wast b/test/lit/validation/unreachable-loop-drop-invalid.wast new file mode 100644 index 00000000000..6c296c12d31 --- /dev/null +++ b/test/lit/validation/unreachable-loop-drop-invalid.wast @@ -0,0 +1,8 @@ +;; RUN: not wasm-opt %s -all -o /dev/null 2>&1 | filecheck %s +;; RUN: not wasm-as %s -all -o /dev/null 2>&1 | filecheck %s +;; RUN: wasm-opt %s -all --no-validation -o /dev/null +;; RUN: wasm-as %s -all --no-validation -o /dev/null + +;; CHECK: popping from empty stack + +(module (func (loop (unreachable)) (drop))) diff --git a/test/lit/validation/unreachable-multivalue-drop-invalid.wast b/test/lit/validation/unreachable-multivalue-drop-invalid.wast new file mode 100644 index 00000000000..50bd9702e29 --- /dev/null +++ b/test/lit/validation/unreachable-multivalue-drop-invalid.wast @@ -0,0 +1,15 @@ +;; RUN: not wasm-opt %s -all -o /dev/null 2>&1 | filecheck %s +;; RUN: not wasm-as %s -all -o /dev/null 2>&1 | filecheck %s + +;; CHECK: popping from empty stack + +(module + (func + (block (result i32 i64) + (unreachable) + ) + (drop) + (drop) + (drop) + ) +) diff --git a/test/lit/validation/unreachable-multivalue-drop-valid.wast b/test/lit/validation/unreachable-multivalue-drop-valid.wast new file mode 100644 index 00000000000..42fded3d972 --- /dev/null +++ b/test/lit/validation/unreachable-multivalue-drop-valid.wast @@ -0,0 +1,12 @@ +;; RUN: wasm-opt %s -all -o /dev/null +;; RUN: wasm-as %s -all -o /dev/null + +(module + (func + (block (result i32 i64) + (unreachable) + ) + (drop) + (drop) + ) +) diff --git a/test/lit/validation/unreachable-multivalue-tuple-parse-valid.wast b/test/lit/validation/unreachable-multivalue-tuple-parse-valid.wast new file mode 100644 index 00000000000..bbdbfa4976c --- /dev/null +++ b/test/lit/validation/unreachable-multivalue-tuple-parse-valid.wast @@ -0,0 +1,15 @@ +;; NOTE: Regression for StackEntry wasmStackType invariants in unreachable +;; multivalue function bodies. Must parse without assertion/crash. +;; RUN: wasm-as %s -all -o /dev/null +;; RUN: wasm-opt %s -all -o /dev/null + +(module (type $ret2 (func (result i32 i32))) + (func (type $ret2) + i32.const 1 + i32.const 2 + i32.add + unreachable + i32.const 3 + i32.const 4 + i32.add + )) diff --git a/test/lit/validation/unreachable-stack-isolation-invalid.wast b/test/lit/validation/unreachable-stack-isolation-invalid.wast new file mode 100644 index 00000000000..8ae500de075 --- /dev/null +++ b/test/lit/validation/unreachable-stack-isolation-invalid.wast @@ -0,0 +1,13 @@ +;; RUN: not wasm-opt %s -all -o /dev/null 2>&1 | filecheck %s +;; RUN: not wasm-as %s -all -o /dev/null 2>&1 | filecheck %s + +;; CHECK: popping from empty stack + +(module + (func + (i32.const 1) + (block + (drop) + ) + ) +) diff --git a/test/lit/validation/unreachable-struct-new-drop-set-valid.wast b/test/lit/validation/unreachable-struct-new-drop-set-valid.wast new file mode 100644 index 00000000000..17c3efefa7f --- /dev/null +++ b/test/lit/validation/unreachable-struct-new-drop-set-valid.wast @@ -0,0 +1,23 @@ +;; RUN: wasm-as %s -all -o /dev/null +;; RUN: wasm-opt %s -all -o /dev/null + +;; After dropping unreachable struct.new, subsequent unreachable GC ops in the +;; same function must remain parseable (gufa-vs-cfp module 11). + +(module + (type $struct (struct (mut i32))) + (func $test + (drop + (struct.new $struct + (i32.const 10) + (unreachable) + ) + ) + (struct.set $struct 0 + (struct.get $struct 0 + (unreachable) + ) + (i32.const 20) + ) + ) +) diff --git a/test/lit/validation/unreachable-try-return-call-valid.wast b/test/lit/validation/unreachable-try-return-call-valid.wast new file mode 100644 index 00000000000..702bcdfac96 --- /dev/null +++ b/test/lit/validation/unreachable-try-return-call-valid.wast @@ -0,0 +1,18 @@ +;; RUN: wasm-as %s -all --no-validation -o /dev/null +;; RUN: wasm-opt %s -all --no-validation -o /dev/null + +;; Void unreachable try with return_call is valid Binaryen IR for inlining +;; tests but not valid Wasm at function end; parse with --no-validation. + +(module + (import "env" "imported" (func $imported (param i32) (result i32))) + (func $callee-2 (result i32) + (try + (do + (return_call $imported + (unreachable) + ) + ) + ) + ) +) diff --git a/test/lit/validation/unreachable-void-block-drop-folded-invalid.wast b/test/lit/validation/unreachable-void-block-drop-folded-invalid.wast new file mode 100644 index 00000000000..e45e39c5b9b --- /dev/null +++ b/test/lit/validation/unreachable-void-block-drop-folded-invalid.wast @@ -0,0 +1,6 @@ +;; RUN: not wasm-opt %s -all -o /dev/null 2>&1 | filecheck %s +;; RUN: wasm-opt %s -all --no-validation -o /dev/null + +;; CHECK: popping from empty stack + +(module (func (drop (block (unreachable))))) diff --git a/test/lit/validation/unreachable-void-block-drop-invalid.wast b/test/lit/validation/unreachable-void-block-drop-invalid.wast new file mode 100644 index 00000000000..e195b5e291e --- /dev/null +++ b/test/lit/validation/unreachable-void-block-drop-invalid.wast @@ -0,0 +1,8 @@ +;; RUN: not wasm-opt %s -all -o /dev/null 2>&1 | filecheck %s +;; RUN: not wasm-as %s -all -o /dev/null 2>&1 | filecheck %s +;; RUN: wasm-opt %s -all --no-validation -o /dev/null +;; RUN: wasm-as %s -all --no-validation -o /dev/null + +;; CHECK: popping from empty stack + +(module (func (block (unreachable)) (drop))) diff --git a/test/lit/validation/unreachable-void-block-parse-valid.wast b/test/lit/validation/unreachable-void-block-parse-valid.wast new file mode 100644 index 00000000000..51594fe8a80 --- /dev/null +++ b/test/lit/validation/unreachable-void-block-parse-valid.wast @@ -0,0 +1,3 @@ +;; RUN: wasm-as %s -o /dev/null + +(module (func (block (unreachable)))) diff --git a/test/lit/wasm-split/split-module-items.wast b/test/lit/wasm-split/split-module-items.wast index 3816b50ce9b..6180be550f2 100644 --- a/test/lit/wasm-split/split-module-items.wast +++ b/test/lit/wasm-split/split-module-items.wast @@ -1,4 +1,4 @@ -;; RUN: wasm-split %s -all -g -o1 %t.1.wasm -o2 %t.2.wasm --keep-funcs=keep +;; RUN: wasm-split %s -all -g --no-validation -o1 %t.1.wasm -o2 %t.2.wasm --keep-funcs=keep ;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY ;; RUN: wasm-dis %t.2.wasm | filecheck %s --check-prefix SECONDARY