From ef181383a23a488f26457e93a261106471a5ca9f Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Wed, 1 Apr 2026 10:35:40 -0700 Subject: [PATCH 001/168] Canonicalize NaNs from nearbyint (#8561) Followup to #8558 and #8472. Part of #8261. --- scripts/test/shared.py | 9 --------- src/wasm/literal.cpp | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/scripts/test/shared.py b/scripts/test/shared.py index 72b64c762ad..3d36fb35fa1 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -443,15 +443,6 @@ def get_tests(test_dir, extensions=[], recursive=False): 'token.wast', # Lexer should require spaces between strings and non-paren tokens ] -if get_platform() == 'linux': - SPEC_TESTSUITE_TESTS_TO_SKIP += [ - # Errors on Linux x86_64 with musl, https://github.com/WebAssembly/binaryen/pull/8557 - 'f32.wast', - 'f64.wast', - 'simd_f32x4_rounding.wast', - 'simd_f64x2_rounding.wast', - ] - def _can_run_spec_test(test): test = Path(test) diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index 62e16e6b504..5c2a114af75 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -1142,9 +1142,9 @@ Literal Literal::trunc() const { Literal Literal::nearbyint() const { switch (type.getBasic()) { case Type::f32: - return Literal(std::nearbyint(getf32())); + return standardizeNaN(Literal(std::nearbyint(getf32()))); case Type::f64: - return Literal(std::nearbyint(getf64())); + return standardizeNaN(Literal(std::nearbyint(getf64()))); default: WASM_UNREACHABLE("unexpected type"); } From c32215e106d47d7132be74bd6f1af865285707d1 Mon Sep 17 00:00:00 2001 From: Spotandjake <40705786+spotandjake@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:17:26 -0400 Subject: [PATCH 002/168] [Js_of_ocaml] Update jsoo build process for libbinaryen (#8565) When #7995 was merged it broke our upgrade process in libbinaryen. This pr fixes our build process. JS_OF_OCAML still doesn't support esm modules however it supports es6 features, so we can safely remove `target_link_libraries(binaryen_js PRIVATE optimized "--closure-args=\"--language_out=ECMASCRIPT5\"")`. Because we are not using esm modules and jsoo doesn't support top level awaits we need to dissable async compilation. --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4cf32bc6481..97863ffb52b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -550,8 +550,8 @@ if(EMSCRIPTEN) if(JS_OF_OCAML) # js_of_ocaml needs a specified variable with special comment to provide the library to consumer target_link_libraries(binaryen_js PRIVATE "--extern-pre-js=${CMAKE_CURRENT_SOURCE_DIR}/src/js/binaryen.jsoo-extern-pre.js") - # Currently, js_of_ocaml can only process ES5 code - target_link_libraries(binaryen_js PRIVATE optimized "--closure-args=\"--language_out=ECMASCRIPT5\"") + # js_of_ocaml does not support top level await + target_link_libraries(binaryen_js PRIVATE "-sWASM_ASYNC_COMPILATION=0") else() target_link_libraries(binaryen_js PRIVATE "-sEXPORT_ES6") endif() From 88a07e028cfb4aa68e7a94743646a0867b31c15b Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 1 Apr 2026 14:34:55 -0700 Subject: [PATCH 003/168] Handle ref before expected in Heap2Local for StructCmpxchg (#8566) When the same optimized allocation flows into both the `ref` and `expected` fields of a StructCmpxchg, we previously (arbitrarily) prioritized optimizing based on the flow through `expected`. But this optimization stores the `ref` in a scratch local and then creates a new struct.get of its value. Since the same optimized allocation is also flowing through the `ref` field, that means we end up trying to do a struct.get on a null, which traps. To fix the problem, prioritize doing the optimization based on the flow through `ref` instead. This drops the other expressions and does not introduce any new accesses of the optimized value. Fixes #8563. --- src/passes/Heap2Local.cpp | 137 ++++++++++++++-------------- test/lit/passes/heap2local-rmw.wast | 67 ++++++++++++++ 2 files changed, 136 insertions(+), 68 deletions(-) diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index adba4f0ae36..7515d1999e9 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -1137,83 +1137,84 @@ struct Struct2Local : PostWalker { // The allocation might flow into `ref` or `expected`, but not // `replacement`, because then it would be considered to have escaped. - if (analyzer.getInteraction(curr->expected) == - ParentChildInteraction::Flows) { - // Since the allocation does not escape, it cannot possibly match the - // value already in the struct. The cmpxchg will just do a read. Drop the - // other arguments and do the atomic read at the end, when the cmpxchg - // would have happened. Use a nullable scratch local in case we also - // optimize `ref` later and need to replace it with a null. - auto refType = curr->ref->type.with(Nullable); - auto refScratch = builder.addVar(func, refType); - auto* setRefScratch = builder.makeLocalSet(refScratch, curr->ref); - auto* getRefScratch = builder.makeLocalGet(refScratch, refType); - auto* structGet = builder.makeStructGet( - curr->index, getRefScratch, curr->order, curr->type); - auto* block = builder.makeBlock({setRefScratch, - builder.makeDrop(curr->expected), - builder.makeDrop(curr->replacement), - structGet}); + if (analyzer.getInteraction(curr->ref) == ParentChildInteraction::Flows) { + [[maybe_unused]] auto& field = fields[curr->index]; + auto type = curr->type; + assert(type == field.type); + assert(!field.isPacked()); + + // Hold everything in scratch locals, just like for other RMW ops and + // struct.new. Use a nullable (shared) eqref local for `expected` to + // accommodate any allowed optimized or unoptimized value there. + auto expectedType = type; + if (type.isRef()) { + expectedType = Type( + HeapTypes::eq.getBasic(type.getHeapType().getShared()), Nullable); + } + auto oldScratch = builder.addVar(func, type); + auto expectedScratch = builder.addVar(func, expectedType); + auto replacementScratch = builder.addVar(func, type); + auto local = localIndexes[curr->index]; + + auto* block = builder.makeBlock( + {builder.makeDrop(curr->ref), + builder.makeLocalSet(expectedScratch, curr->expected), + builder.makeLocalSet(replacementScratch, curr->replacement), + builder.makeLocalSet(oldScratch, builder.makeLocalGet(local, type))}); + + // Create the check for whether we should do the exchange. + auto* lhs = builder.makeLocalGet(local, type); + auto* rhs = builder.makeLocalGet(expectedScratch, expectedType); + Expression* pred; + if (type.isRef()) { + pred = builder.makeRefEq(lhs, rhs); + } else { + pred = + builder.makeBinary(Abstract::getBinary(type, Abstract::Eq), lhs, rhs); + } + + // The conditional exchange. + block->list.push_back(builder.makeIf( + pred, + builder.makeLocalSet(local, + builder.makeLocalGet(replacementScratch, type)))); + + // Unstash the old value. + block->list.push_back(builder.makeLocalGet(oldScratch, type)); + block->type = type; replaceCurrent(block); - // Record the new data flow into and out of the new scratch local. This is - // necessary in case `ref` gets processed later so we can detect that it - // flows to the new struct.atomic.get, which may need to be replaced. - analyzer.parents.setParent(curr->ref, setRefScratch); - analyzer.scratchInfo.insert({setRefScratch, getRefScratch}); - analyzer.parents.setParent(getRefScratch, structGet); return; } - if (analyzer.getInteraction(curr->ref) != ParentChildInteraction::Flows) { + if (analyzer.getInteraction(curr->expected) != + ParentChildInteraction::Flows) { // Since the allocation does not flow from `ref`, it must not flow through // this cmpxchg at all. return; } - [[maybe_unused]] auto& field = fields[curr->index]; - auto type = curr->type; - assert(type == field.type); - assert(!field.isPacked()); - - // Hold everything in scratch locals, just like for other RMW ops and - // struct.new. Use a nullable (shared) eqref local for `expected` to - // accommodate any allowed optimized or unoptimized value there. - auto expectedType = type; - if (type.isRef()) { - expectedType = - Type(HeapTypes::eq.getBasic(type.getHeapType().getShared()), Nullable); - } - auto oldScratch = builder.addVar(func, type); - auto expectedScratch = builder.addVar(func, expectedType); - auto replacementScratch = builder.addVar(func, type); - auto local = localIndexes[curr->index]; - - auto* block = builder.makeBlock( - {builder.makeDrop(curr->ref), - builder.makeLocalSet(expectedScratch, curr->expected), - builder.makeLocalSet(replacementScratch, curr->replacement), - builder.makeLocalSet(oldScratch, builder.makeLocalGet(local, type))}); - - // Create the check for whether we should do the exchange. - auto* lhs = builder.makeLocalGet(local, type); - auto* rhs = builder.makeLocalGet(expectedScratch, expectedType); - Expression* pred; - if (type.isRef()) { - pred = builder.makeRefEq(lhs, rhs); - } else { - pred = - builder.makeBinary(Abstract::getBinary(type, Abstract::Eq), lhs, rhs); - } - - // The conditional exchange. - block->list.push_back( - builder.makeIf(pred, - builder.makeLocalSet( - local, builder.makeLocalGet(replacementScratch, type)))); - - // Unstash the old value. - block->list.push_back(builder.makeLocalGet(oldScratch, type)); - block->type = type; + // Since the allocation does not escape, it cannot possibly match the value + // already in the struct. The cmpxchg will just do a read. Drop the other + // arguments and do the atomic read at the end, when the cmpxchg would have + // happened. Use a nullable scratch local in case we also optimize `ref` + // later and need to replace it with a null. + auto refType = curr->ref->type.with(Nullable); + auto refScratch = builder.addVar(func, refType); + auto* setRefScratch = builder.makeLocalSet(refScratch, curr->ref); + auto* getRefScratch = builder.makeLocalGet(refScratch, refType); + auto* structGet = builder.makeStructGet( + curr->index, getRefScratch, curr->order, curr->type); + auto* block = builder.makeBlock({setRefScratch, + builder.makeDrop(curr->expected), + builder.makeDrop(curr->replacement), + structGet}); replaceCurrent(block); + // Record the new data flow into and out of the new scratch local. This is + // necessary in case `ref` gets processed later so we can detect that it + // flows to the new struct.atomic.get, which may need to be replaced. + analyzer.parents.setParent(curr->ref, setRefScratch); + analyzer.scratchInfo.insert({setRefScratch, getRefScratch}); + analyzer.parents.setParent(getRefScratch, structGet); + return; } void visitArrayCmpxchg(ArrayCmpxchg* curr) { diff --git a/test/lit/passes/heap2local-rmw.wast b/test/lit/passes/heap2local-rmw.wast index 5ded32d4378..48fb88df62b 100644 --- a/test/lit/passes/heap2local-rmw.wast +++ b/test/lit/passes/heap2local-rmw.wast @@ -1354,6 +1354,73 @@ ) ) +(module + ;; CHECK: (type $struct (struct (field (mut (ref null $struct))))) + (type $struct (struct (field (mut (ref null $struct))))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (export "test" (func $cmpxchg-ref-and-expected)) + + ;; CHECK: (func $cmpxchg-ref-and-expected (type $1) + ;; CHECK-NEXT: (local $local (ref $struct)) + ;; CHECK-NEXT: (local $1 (ref null $struct)) + ;; CHECK-NEXT: (local $2 (ref null $struct)) + ;; CHECK-NEXT: (local $3 eqref) + ;; CHECK-NEXT: (local $4 (ref null $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref null $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cmpxchg-ref-and-expected (export "test") + (local $local (ref $struct)) + (drop + ;; The allocation flows to both `ref` and `expected` fields. We must + ;; prioritize the optimization for flows through `ref`. Otherwise, if we + ;; did the optimization for `expected` first, we would end up with a + ;; struct.get of the ref value, but the ref value would have been changed + ;; to a null and we would introduce a trap. + (struct.atomic.rmw.cmpxchg $struct 0 + (local.tee $local + (struct.new_default $struct) + ) + (local.get $local) + (ref.null none) + ) + ) + ) +) + (module (type $array (shared (array i8))) ;; CHECK: (type $struct (shared (struct (field (mut (ref null (shared array))))))) From c64410ae131c891fd70fc21e3721225bb603bd5e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 2 Apr 2026 08:11:20 -0700 Subject: [PATCH 004/168] GUFA: Ignore unreachable packed reads (#8564) Without this early return, we'd try to find the packed size in bits of the unreachable type, which asserts. --- src/ir/possible-contents.cpp | 10 ++++++---- test/lit/passes/gufa-refs.wast | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/ir/possible-contents.cpp b/src/ir/possible-contents.cpp index 905349f1c68..10b01f986dc 100644 --- a/src/ir/possible-contents.cpp +++ b/src/ir/possible-contents.cpp @@ -3136,24 +3136,20 @@ void Flower::filterPackedDataReads(PossibleContents& contents, Expression* ref; Index index; unsigned bytes = 0; - Type resultType = Type::none; if (auto* get = expr->dynCast()) { signed_ = get->signed_; ref = get->ref; index = get->index; - resultType = get->type; } else if (auto* get = expr->dynCast()) { signed_ = get->signed_; ref = get->ref; // Arrays are treated as having a single field. index = 0; - resultType = get->type; } else if (auto* load = expr->dynCast()) { signed_ = load->signed_; ref = load->ref; index = 0; bytes = load->bytes; - resultType = load->type; } else { WASM_UNREACHABLE("bad packed read"); } @@ -3161,6 +3157,12 @@ void Flower::filterPackedDataReads(PossibleContents& contents, return; } + Type resultType = expr->type; + if (resultType == Type::unreachable) { + // This read never executes. + return; + } + // If there is no struct or array to read, no value will ever be returned. if (ref->type.isNull()) { contents = PossibleContents::none(); diff --git a/test/lit/passes/gufa-refs.wast b/test/lit/passes/gufa-refs.wast index 9907fd90c23..14f362f5e3d 100644 --- a/test/lit/passes/gufa-refs.wast +++ b/test/lit/passes/gufa-refs.wast @@ -6141,6 +6141,25 @@ ) ) ) + + ;; CHECK: (func $unreachable (type $1) + ;; CHECK-NEXT: (array.get_s $array + ;; CHECK-NEXT: (array.new_default $array + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unreachable + ;; This array.get is unreachable, and when handling packing we should not + ;; error. + (array.get_s $array + (array.new_default $array + (i32.const 0) + ) + (unreachable) + ) + ) ) ;; Atomic accesses require special handling From 13cb9187a4c8cb49350e0f24fd18186e35ce8c16 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 2 Apr 2026 09:54:52 -0700 Subject: [PATCH 005/168] Group locals by written type in binary writer (#8568) We have logic to group locals by type in the binary writer to take advantage of the run-length encoding of locals. But that logic previously grouped locals by their IR types, rather than the types that would actually be written to the binary. These can differ in when the IR uses more precise types than are allowed to be written given the enabled feature set. For example, the IR might use exact types but have to write inexact types because custom descriptors are not enabled. In such cases, it is possible that different groups of locals would be written with the same type, which is suboptimal. Fix the problem by grouping locals by their written types given the enabled features. Fixes #7934. --- src/wasm-stack.h | 1 + src/wasm/wasm-stack.cpp | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/wasm-stack.h b/src/wasm-stack.h index 2c9fdd616fa..05898623187 100644 --- a/src/wasm-stack.h +++ b/src/wasm-stack.h @@ -150,6 +150,7 @@ class BinaryInstWriter : public OverriddenVisitor { std::unordered_map numLocalsByType; void noteLocalType(Type type, Index count = 1); + Index getNumLocalsForType(Type type); // Keeps track of the binary index of the scratch locals used to lower // tuple.extract. If there are multiple scratch locals of the same type, they diff --git a/src/wasm/wasm-stack.cpp b/src/wasm/wasm-stack.cpp index 496d15ca878..ea932dc175c 100644 --- a/src/wasm/wasm-stack.cpp +++ b/src/wasm/wasm-stack.cpp @@ -3245,7 +3245,7 @@ void BinaryInstWriter::mapLocalsAndEmitHeader() { Index baseIndex = func->getVarIndexBase(); for (auto& type : localTypes) { nextFreeIndex[type] = baseIndex; - baseIndex += numLocalsByType[type]; + baseIndex += getNumLocalsForType(type); } // Map the IR index pairs to indices. @@ -3261,14 +3261,20 @@ void BinaryInstWriter::mapLocalsAndEmitHeader() { scratchLocals[type] = nextFreeIndex[type]; } - o << U32LEB(numLocalsByType.size()); + o << U32LEB(localTypes.size()); for (auto& localType : localTypes) { - o << U32LEB(numLocalsByType.at(localType)); + o << U32LEB(getNumLocalsForType(localType)); parent.writeType(localType); } } void BinaryInstWriter::noteLocalType(Type type, Index count) { + // Group locals by the type they will eventually be written out as. For + // example, we do not need to differentiate exact and inexact versions of the + // same reference type if custom descriptors is not enabled and the type will + // be written as inexact either way. + auto feats = parent.getModule()->features; + type = type.asWrittenGivenFeatures(feats); auto& num = numLocalsByType[type]; if (num == 0) { localTypes.push_back(type); @@ -3276,6 +3282,15 @@ void BinaryInstWriter::noteLocalType(Type type, Index count) { num += count; } +Index BinaryInstWriter::getNumLocalsForType(Type type) { + auto feats = parent.getModule()->features; + type = type.asWrittenGivenFeatures(feats); + if (auto it = numLocalsByType.find(type); it != numLocalsByType.end()) { + return it->second; + } + return 0; +} + InsertOrderedMap BinaryInstWriter::countScratchLocals() { struct ScratchLocalFinder : PostWalker { BinaryInstWriter& parent; From 0bb686f8367d880a1d530e7f8f8169f94b86b3eb Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 2 Apr 2026 10:59:15 -0700 Subject: [PATCH 006/168] More ArrayCmpxchg expected opts in Heap2Local (#8567) When non-escaping allocations flow into the `expected` field of an ArrayCmpxchg, optimize even if the ArrayCmpxchg has a non-constant index. We typically only optimize array instructions with constant fields, but that's because we need to know what field of the array is accessed. Arrays flowing into the `expected` field are not accessed, though, so there is no need for the accessed index to be constant. Make sure that effects in the potentially non-constant index field are preserved in the correct order by using a new scratch local to propagate the index value past the `expected` and `replacement` expressions to the newly generated `struct.atomic.get`. --- src/passes/Heap2Local.cpp | 63 ++++++++-------- test/lit/passes/heap2local-rmw.wast | 107 +++++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 34 deletions(-) diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index 7515d1999e9..ca5a4a3413c 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -461,40 +461,31 @@ struct EscapeAnalyzer { } } void visitArraySet(ArraySet* curr) { - if (!curr->index->is()) { - // Array operations on nonconstant indexes do not escape in the normal - // sense, but they do escape from our being able to analyze them, so - // stop as soon as we see one. - return; - } - - // As StructGet. - if (curr->ref == child) { + // Arrays flowing into array operations on nonconstant indexes do not + // escape in the normal sense, but they do escape from our being able to + // analyze them, so stop as soon as we see one. + if (child == curr->ref && curr->index->is()) { escapes = false; fullyConsumes = true; } } void visitArrayGet(ArrayGet* curr) { - if (!curr->index->is()) { - return; + if (child == curr->ref && curr->index->is()) { + escapes = false; + fullyConsumes = true; } - escapes = false; - fullyConsumes = true; } void visitArrayRMW(ArrayRMW* curr) { - if (!curr->index->is()) { - return; - } - if (curr->ref == child) { + if (child == curr->ref && curr->index->is()) { escapes = false; fullyConsumes = true; } } void visitArrayCmpxchg(ArrayCmpxchg* curr) { - if (!curr->index->is()) { - return; - } - if (curr->ref == child || curr->expected == child) { + // Allocations flowing into `expected` are fully consumed and + // optimizable even if the index is not constant. + if (child == curr->expected || + (child == curr->ref && curr->index->is())) { escapes = false; fullyConsumes = true; } @@ -1233,9 +1224,15 @@ struct Struct2Local : PostWalker { auto refScratch = builder.addVar(func, refType); auto* setRefScratch = builder.makeLocalSet(refScratch, curr->ref); auto* getRefScratch = builder.makeLocalGet(refScratch, refType); + + auto indexScratch = builder.addVar(func, Type::i32); + auto* setIndexScratch = builder.makeLocalSet(indexScratch, curr->index); + auto* getIndexScratch = builder.makeLocalGet(indexScratch, Type::i32); + auto* arrayGet = builder.makeArrayGet( - getRefScratch, curr->index, curr->order, curr->type); + getRefScratch, getIndexScratch, curr->order, curr->type); auto* block = builder.makeBlock({setRefScratch, + setIndexScratch, builder.makeDrop(curr->expected), builder.makeDrop(curr->replacement), arrayGet}); @@ -1467,20 +1464,20 @@ struct Array2Struct : PostWalker { return; } - auto index = getIndex(curr->index); - if (index >= numFields) { - replaceCurrent(builder.makeBlock({builder.makeDrop(curr->ref), - builder.makeDrop(curr->expected), - builder.makeDrop(curr->replacement), - builder.makeUnreachable()})); - refinalize = true; - return; - } - // The allocation might flow into `ref` or `expected`, but not // `replacement`, because then it would be considered to have escaped. if (analyzer.getInteraction(curr->ref) == ParentChildInteraction::Flows) { - // The accessed array is being optimzied. Convert the ArrayCmpxchg into a + auto index = getIndex(curr->index); + if (index >= numFields) { + replaceCurrent(builder.makeBlock({builder.makeDrop(curr->ref), + builder.makeDrop(curr->expected), + builder.makeDrop(curr->replacement), + builder.makeUnreachable()})); + refinalize = true; + return; + } + + // The accessed array is being optimized. Convert the ArrayCmpxchg into a // StructCmpxchg. replaceCurrent(builder.makeStructCmpxchg( index, curr->ref, curr->expected, curr->replacement, curr->order)); diff --git a/test/lit/passes/heap2local-rmw.wast b/test/lit/passes/heap2local-rmw.wast index 48fb88df62b..7d329d2b53e 100644 --- a/test/lit/passes/heap2local-rmw.wast +++ b/test/lit/passes/heap2local-rmw.wast @@ -1465,11 +1465,15 @@ ;; CHECK: (func $array-cmpxchg-expected (type $1) (param $array (ref $array)) ;; CHECK-NEXT: (local $1 eqref) ;; CHECK-NEXT: (local $2 (ref null $array)) + ;; CHECK-NEXT: (local $3 i32) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result eqref) ;; CHECK-NEXT: (local.set $2 ;; CHECK-NEXT: (local.get $array) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result nullref) ;; CHECK-NEXT: (local.set $1 @@ -1483,7 +1487,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (array.atomic.get $array ;; CHECK-NEXT: (local.get $2) - ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (local.get $3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -1504,3 +1508,104 @@ ) ) ) + +(module + ;; CHECK: (type $array (array (mut eqref))) + (type $array (array (mut eqref))) + ;; CHECK: (type $1 (func (param (ref $array)))) + + ;; CHECK: (type $2 (func (result i32))) + + ;; CHECK: (type $3 (func (result eqref))) + + ;; CHECK: (import "" "" (func $effect-i32 (type $2) (result i32))) + (import "" "" (func $effect-i32 (result i32))) + ;; CHECK: (import "" "" (func $effect-eq (type $3) (result eqref))) + (import "" "" (func $effect-eq (result eqref))) + + ;; CHECK: (func $array-cmpxchg-expected-index-effect (type $1) (param $array (ref $array)) + ;; CHECK-NEXT: (local $1 eqref) + ;; CHECK-NEXT: (local $2 (ref null $array)) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result eqref) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $array) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (call $effect-i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect-eq) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (array.atomic.get $array + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $array-cmpxchg-expected-index-effect (param $array (ref $array)) + (drop + ;; The index is non-constant, but we can still optimize the expected + ;; field. We must preserve the index and the order of its effects. + (array.atomic.rmw.cmpxchg $array + (local.get $array) + (call $effect-i32) + (array.new_default $array (i32.const 1)) + (call $effect-eq) + ) + ) + ) + + ;; CHECK: (func $array-cmpxchg-expected-index-oob (type $1) (param $array (ref $array)) + ;; CHECK-NEXT: (local $1 eqref) + ;; CHECK-NEXT: (local $2 (ref null $array)) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result eqref) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $array) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (i32.const -1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $effect-eq) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (array.atomic.get $array + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $array-cmpxchg-expected-index-oob (param $array (ref $array)) + (drop + ;; Now the index is constant but surely out-of-bounds. We still optimize + ;; the same, way leaving the array.atomic.get to trap. + (array.atomic.rmw.cmpxchg $array + (local.get $array) + (i32.const -1) + (array.new_default $array (i32.const 1)) + (call $effect-eq) + ) + ) + ) +) From 6c70e2caa4b559f9fc9714d535b2986a05815fb0 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 2 Apr 2026 17:07:15 -0700 Subject: [PATCH 007/168] Fix typos (#8570) Co-authored-by: Alon Zakai --- scripts/clusterfuzz/run.py | 2 +- scripts/test/generate_atomic_spec_test.py | 2 +- src/analysis/lattice.h | 2 +- src/analysis/monotone-analyzer.h | 2 +- .../reaching-definitions-transfer-function.h | 6 +++--- src/binaryen-c.h | 4 ++-- src/cfg/Relooper.h | 2 +- src/cfg/cfg-traversal.h | 2 +- src/ir/ExpressionManipulator.cpp | 2 +- src/ir/child-typer.h | 2 +- src/ir/eh-utils.cpp | 2 +- src/ir/manipulation.h | 4 ++-- src/ir/match.h | 2 +- src/ir/module-splitting.cpp | 2 +- src/ir/principal-type.cpp | 2 +- src/ir/table-utils.h | 2 +- src/parser/parsers.h | 2 +- src/passes/CodeFolding.cpp | 6 +++--- src/passes/CodePushing.cpp | 2 +- src/passes/DataFlowOpts.cpp | 2 +- src/passes/DeNaN.cpp | 2 +- src/passes/EncloseWorld.cpp | 2 +- src/passes/Flatten.cpp | 2 +- src/passes/GenerateDynCalls.cpp | 2 +- src/passes/GlobalEffects.cpp | 2 +- src/passes/GlobalStructInference.cpp | 6 +++--- src/passes/GlobalTypeOptimization.cpp | 2 +- src/passes/I64ToI32Lowering.cpp | 2 +- src/passes/Inlining.cpp | 4 ++-- src/passes/LLVMNontrappingFPToIntLowering.cpp | 2 +- src/passes/MemoryPacking.cpp | 4 ++-- src/passes/Monomorphize.cpp | 2 +- src/passes/OptimizeCasts.cpp | 2 +- src/passes/OptimizeInstructions.cpp | 21 ++++++++++--------- src/passes/PostEmscripten.cpp | 2 +- src/passes/Precompute.cpp | 2 +- src/passes/Print.cpp | 2 +- src/passes/RedundantSetElimination.cpp | 4 ++-- src/passes/RemoveUnusedBrs.cpp | 2 +- src/passes/SafeHeap.cpp | 2 +- src/passes/SignaturePruning.cpp | 2 +- src/passes/SimplifyGlobals.cpp | 4 ++-- src/passes/TypeGeneralizing.cpp | 4 ++-- src/passes/TypeMerging.cpp | 2 +- src/passes/TypeSSA.cpp | 6 +++--- src/passes/Unsubtyping.cpp | 2 +- src/passes/Vacuum.cpp | 2 +- src/support/mixed_arena.h | 4 ++-- src/support/path.h | 2 +- src/support/topological_sort.h | 2 +- src/tools/optimization-options.h | 2 +- src/tools/wasm-ctor-eval.cpp | 4 ++-- src/tools/wasm-reduce/wasm-reduce.cpp | 2 +- src/wasm-interpreter.h | 4 ++-- src/wasm-ir-builder.h | 2 +- src/wasm-traversal.h | 2 +- src/wasm-type.h | 2 +- src/wasm.h | 6 +++--- src/wasm/wasm-emscripten.cpp | 2 +- src/wasm/wasm-stack-opts.cpp | 2 +- src/wasm/wasm-type.cpp | 2 +- test/lit/exec/delegate-vacuum.wast | 2 +- test/lit/passes/cfp.wast | 2 +- test/lit/passes/heap2local.wast | 2 +- test/lit/passes/optimize-instructions-gc.wast | 2 +- .../lit/passes/optimize-instructions-mvp.wast | 6 +++--- test/lit/passes/vacuum_all-features.wast | 2 +- test/passes/remove-unused-names_vacuum.wast | 8 +++---- test/spec/br_on_cast_desc_eq.wast | 2 +- test/spec/relaxed-atomics.wast | 2 +- third_party/llvm-project/DWARFContext.cpp | 2 +- 71 files changed, 106 insertions(+), 105 deletions(-) diff --git a/scripts/clusterfuzz/run.py b/scripts/clusterfuzz/run.py index abd1d5ad5ba..811c4be85e2 100755 --- a/scripts/clusterfuzz/run.py +++ b/scripts/clusterfuzz/run.py @@ -87,7 +87,7 @@ # Enable all features but disable ones not yet ready for fuzzing. This may # be a smaller set than fuzz_opt.py, as that enables a few experimental # flags, while here we just fuzz with d8's --wasm-staging. This should be - # synchonized with bundle_clusterfuzz. + # synchronized with bundle_clusterfuzz. '-all', '--disable-shared-everything', '--disable-fp16', diff --git a/scripts/test/generate_atomic_spec_test.py b/scripts/test/generate_atomic_spec_test.py index 0b98b98579c..51b06767028 100644 --- a/scripts/test/generate_atomic_spec_test.py +++ b/scripts/test/generate_atomic_spec_test.py @@ -147,7 +147,7 @@ def func(): ) """ return f''';; Memory index must come before memory ordering if present. -;; Both immediates are optional; an ommitted memory ordering will be treated as seqcst. +;; Both immediates are optional; an omitted memory ordering will be treated as seqcst. (func $test-all-ops {indent(newline.join(statement(template, mem_idx, mem_ptr_type, ordering) for template, (mem_idx, mem_ptr_type), ordering in all_combinations()))} )''' diff --git a/src/analysis/lattice.h b/src/analysis/lattice.h index 977bea0d5a9..f0de5e07e25 100644 --- a/src/analysis/lattice.h +++ b/src/analysis/lattice.h @@ -61,7 +61,7 @@ concept Lattice = requires(const L& lattice, // The analysis framework only uses bottom elements and least upper bounds (i.e. // joins) directly, so lattices do not necessarily need to implement top -// elements and greatest lower bounds (i.e. meets) to be useable, even though +// elements and greatest lower bounds (i.e. meets) to be usable, even though // they are required for mathematical lattices. Implementing top elements and // meets does have the benefit of making a lattice generically invertable, // though. See lattices/inverted.h. diff --git a/src/analysis/monotone-analyzer.h b/src/analysis/monotone-analyzer.h index 91251103248..a24c38baef1 100644 --- a/src/analysis/monotone-analyzer.h +++ b/src/analysis/monotone-analyzer.h @@ -22,7 +22,7 @@ template class MonotoneCFGAnalyzer { std::vector states; public: - // Will constuct BlockState objects corresponding to BasicBlocks from the + // Will construct BlockState objects corresponding to BasicBlocks from the // given CFG. MonotoneCFGAnalyzer(L& lattice, TxFn& txfn, CFG& cfg); diff --git a/src/analysis/reaching-definitions-transfer-function.h b/src/analysis/reaching-definitions-transfer-function.h index 7a4fe1afcbe..3cca04839d4 100644 --- a/src/analysis/reaching-definitions-transfer-function.h +++ b/src/analysis/reaching-definitions-transfer-function.h @@ -26,7 +26,7 @@ namespace wasm::analysis { // When collecting results, the transfer function takes the states and converts // it into a map of LocalGets to LocalSets which affect it. The fictitious -// inital value LocalSetes will be converted to nullptrs. +// initial value LocalSetes will be converted to nullptrs. class ReachingDefinitionsTransferFunction : public VisitorTransferFunc fakeInitialValueSets; @@ -54,7 +54,7 @@ class ReachingDefinitionsTransferFunction // Helper function which creates fictitious LocalSets for a function, // inserts them into fakeInitialValueSets and fakeSetPtrs. It returns a // vector of actual LocalSets in the function and fictitious LocalSets for - // use when instatitating the lattice. + // use when instantiating the lattice. static std::vector listLocalSets(Function* func, std::vector& fakeInitialValueSets, diff --git a/src/binaryen-c.h b/src/binaryen-c.h index 10225ce027a..63a8020e095 100644 --- a/src/binaryen-c.h +++ b/src/binaryen-c.h @@ -3062,7 +3062,7 @@ BINARYEN_API void BinaryenModulePrintStackIR(BinaryenModuleRef module); BINARYEN_API void BinaryenModulePrintAsmjs(BinaryenModuleRef module); // Validate a module, showing errors on problems. -// @return 0 if an error occurred, 1 if validated succesfully +// @return 0 if an error occurred, 1 if validated successfully BINARYEN_API bool BinaryenModuleValidate(BinaryenModuleRef module); // Runs the standard optimization passes on the module. Uses the currently set @@ -3283,7 +3283,7 @@ BINARYEN_API BinaryenModuleAllocateAndWriteResult BinaryenModuleAllocateAndWrite(BinaryenModuleRef module, const char* sourceMapUrl); -// Serialize a module in s-expression form. Implicity allocates the returned +// Serialize a module in s-expression form. Implicitly allocates the returned // char* with malloc(), and expects the user to free() them manually // once not needed anymore. BINARYEN_API char* BinaryenModuleAllocateAndWriteText(BinaryenModuleRef module); diff --git a/src/cfg/Relooper.h b/src/cfg/Relooper.h index 28a19033196..b89c8a39ffd 100644 --- a/src/cfg/Relooper.h +++ b/src/cfg/Relooper.h @@ -15,7 +15,7 @@ */ /* -This is an optimized C++ implemention of the Relooper algorithm originally +This is an optimized C++ implementation of the Relooper algorithm originally developed as part of Emscripten. This implementation includes optimizations added since the original academic paper [1] was published about it. diff --git a/src/cfg/cfg-traversal.h b/src/cfg/cfg-traversal.h index 2afc76a6835..ca5021e9696 100644 --- a/src/cfg/cfg-traversal.h +++ b/src/cfg/cfg-traversal.h @@ -304,7 +304,7 @@ struct CFGWalker : public PostWalker { } } - // Exception thrown. Note outselves so that we will create a link to each + // Exception thrown. Note ourselves so that we will create a link to each // catch within the try / each destination block within the try_table when // we get there. self->throwingInstsStack[i].push_back(self->currBasicBlock); diff --git a/src/ir/ExpressionManipulator.cpp b/src/ir/ExpressionManipulator.cpp index 51ed7552d48..23a36ebdcc7 100644 --- a/src/ir/ExpressionManipulator.cpp +++ b/src/ir/ExpressionManipulator.cpp @@ -21,7 +21,7 @@ namespace wasm::ExpressionManipulator { Expression* flexibleCopy(Expression* original, Module& wasm, CustomCopier custom) { - // Perform the copy using a stack of tasks (avoiding recusion). + // Perform the copy using a stack of tasks (avoiding recursion). struct CopyTask { // The thing to copy. Expression* original; diff --git a/src/ir/child-typer.h b/src/ir/child-typer.h index 07b7004b8c8..132a0168855 100644 --- a/src/ir/child-typer.h +++ b/src/ir/child-typer.h @@ -22,7 +22,7 @@ namespace wasm { -// CRTP visitor for determining constaints on the types of expression children. +// CRTP visitor for determining constraints on the types of expression children. // For each child of the visited expression, calls a callback with the VarTypes // giving the constraint on the child: // diff --git a/src/ir/eh-utils.cpp b/src/ir/eh-utils.cpp index 43fd1627d31..19f48477a70 100644 --- a/src/ir/eh-utils.cpp +++ b/src/ir/eh-utils.cpp @@ -42,7 +42,7 @@ getFirstPop(Expression* catchBody, bool& isPopNested, Expression**& popPtr) { auto* implicitBlock = catchBody->dynCast(); // Go down the line for the first child until we reach a leaf. A pop should be - // in that first-decendant line. + // in that first-descendant line. Expression** firstChildPtr = nullptr; while (true) { if (firstChild->is()) { diff --git a/src/ir/manipulation.h b/src/ir/manipulation.h index 1ad2b1161bb..dbb34edbe66 100644 --- a/src/ir/manipulation.h +++ b/src/ir/manipulation.h @@ -26,7 +26,7 @@ template inline OutputType* convert(InputType* input) { static_assert(sizeof(OutputType) <= sizeof(InputType), "Can only convert to a smaller size Expression node"); - input->~InputType(); // arena-allocaed, so no destructor, but avoid UB. + input->~InputType(); // arena-allocated, so no destructor, but avoid UB. OutputType* output = (OutputType*)(input); new (output) OutputType; return output; @@ -57,7 +57,7 @@ inline Unreachable* unreachable(InputType* target) { template inline OutputType* convert(InputType* input, MixedArena& allocator) { assert(sizeof(OutputType) <= sizeof(InputType)); - input->~InputType(); // arena-allocaed, so no destructor, but avoid UB. + input->~InputType(); // arena-allocated, so no destructor, but avoid UB. OutputType* output = (OutputType*)(input); new (output) OutputType(allocator); return output; diff --git a/src/ir/match.h b/src/ir/match.h index 3d42eee359a..383ff8d057a 100644 --- a/src/ir/match.h +++ b/src/ir/match.h @@ -67,7 +67,7 @@ namespace wasm::Match { // // Matches Binary expressions. Takes an optional pointer to Binary* at which // to store the matched Binary*, followed by either a BinaryOp or an -// Abstract::Op describing which binary expresions to match, followed by +// Abstract::Op describing which binary expressions to match, followed by // matchers to apply to the binary expression's left and right operands. // // select diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index d88f21ee24a..f5825a93bf0 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -1141,7 +1141,7 @@ void ModuleSplitter::shareImportableItems() { // It's not used anywhere, so delete it. Unlike other unused module items // (memories, tables, and tags) that can just sit in the primary module // and later be DCE'ed by another pass, we should remove it here, because - // an unused global can contain an initialier that refers to another + // an unused global can contain an initializer that refers to another // global that will be moved to a secondary module, like // (global $unused i32 (global.get $a)) // $a is moved to a secondary globalsToRemove.push_back(global->name); diff --git a/src/ir/principal-type.cpp b/src/ir/principal-type.cpp index c0c502b6e3a..66e87e4f02c 100644 --- a/src/ir/principal-type.cpp +++ b/src/ir/principal-type.cpp @@ -1031,7 +1031,7 @@ bool PrincipalType::compose(const PrincipalType& next) { apply(assignments, *this); // If a type variable was instantiated with bottom type (i.e. unreachable) due - // to popping from an unreachabile stack, the result type may end with some + // to popping from an unreachable stack, the result type may end with some // number of unreachables. This is nonsensical, since `unreachable` is not a // concrete type. We could have alternatively left the variables // uninstantiated, but it would have no corresponding introduction on the left diff --git a/src/ir/table-utils.h b/src/ir/table-utils.h index cee88fcdbc7..4e788a2685e 100644 --- a/src/ir/table-utils.h +++ b/src/ir/table-utils.h @@ -122,7 +122,7 @@ bool usesExpressions(ElementSegment* curr, Module* module); // Information about a table's optimizability. struct TableInfo { - // Whether the table may be modifed at runtime, either because it is imported + // Whether the table may be modified at runtime, either because it is imported // or exported, or table.set operations exist for it in the code. bool mayBeModified = false; diff --git a/src/parser/parsers.h b/src/parser/parsers.h index c6919d0b3ed..db41a2534cc 100644 --- a/src/parser/parsers.h +++ b/src/parser/parsers.h @@ -1340,7 +1340,7 @@ loop(Ctx& ctx, const std::vector& annotations, bool folded) { // | '(' 'try' label blocktype '(' 'do' instr* ')' // ('(' 'catch' tagidx instr* ')')* // ('(' 'catch_all' instr* ')')? ')' -// | 'try' label blocktype instr* 'deledate' label +// | 'try' label blocktype instr* 'delegate' label // | '(' 'try' label blocktype '(' 'do' instr* ')' // '(' 'delegate' label ')' ')' template diff --git a/src/passes/CodeFolding.cpp b/src/passes/CodeFolding.cpp index 65e3e3471ed..c4a7804695c 100644 --- a/src/passes/CodeFolding.cpp +++ b/src/passes/CodeFolding.cpp @@ -246,7 +246,7 @@ struct CodeFolding auto* right = curr->ifFalse->dynCast(); // If one is a block and the other isn't, and the non-block is a tail of the // other, we can fold that - for our convenience, we just add a block and - // run the rest of the optimization mormally. + // run the rest of the optimization normally. auto maybeAddBlock = [this](Block* block, Expression*& other) -> Block* { // If other is a suffix of the block, wrap it in a block. // @@ -594,10 +594,10 @@ struct CodeFolding for (auto* item : items) { saved += Measurer::measure(item) * (tails.size() - 1); } - // compure the cost: in non-fallthroughs, we are replacing the final + // compute the cost: in non-fallthroughs, we are replacing the final // element with a br; for a fallthrough, if there is one, we must // add a return element (for the function body, so it doesn't reach us) - // TODO: handle fallthroughts for return + // TODO: handle fallthroughs for return Index cost = tails.size(); // we also need to add two blocks: for us to break to, and to contain // that block and the merged code. very possibly one of the blocks diff --git a/src/passes/CodePushing.cpp b/src/passes/CodePushing.cpp index 67bef95c468..57dd9993417 100644 --- a/src/passes/CodePushing.cpp +++ b/src/passes/CodePushing.cpp @@ -424,7 +424,7 @@ class Pusher { // TODO: After pushing we could recurse and run both this function and // optimizeSegment in that location. For now, leave that to later - // cycles of the optimizer, as this case seems rairly rare. + // cycles of the optimizer, as this case seems fairly rare. return true; }; diff --git a/src/passes/DataFlowOpts.cpp b/src/passes/DataFlowOpts.cpp index 79878f4c962..a3d169ec8ce 100644 --- a/src/passes/DataFlowOpts.cpp +++ b/src/passes/DataFlowOpts.cpp @@ -135,7 +135,7 @@ struct DataFlowOpts : public WalkerPass> { } // Now we know that all our DataFlow inputs are constant, and all // our Binaryen IR representations of them are constant too. RUn - // precompute, which will transform the expression into a constanat. + // precompute, which will transform the expression into a constant. Module temp; // XXX we should copy expr here, in principle, and definitely will need to // when we do arbitrarily regenerated expressions diff --git a/src/passes/DeNaN.cpp b/src/passes/DeNaN.cpp index 0251a0c589f..01845f4a6b0 100644 --- a/src/passes/DeNaN.cpp +++ b/src/passes/DeNaN.cpp @@ -201,7 +201,7 @@ struct DeNaN : public WalkerPass< module->addFunction(std::move(func)); }; - // Check if a contant v128 may contain f32 or f64 NaNs. + // Check if a constant v128 may contain f32 or f64 NaNs. bool hasNaNLane(Const* c) { assert(c->type == Type::v128); auto value = c->value; diff --git a/src/passes/EncloseWorld.cpp b/src/passes/EncloseWorld.cpp index 34ceb51bfc2..2061937d8b6 100644 --- a/src/passes/EncloseWorld.cpp +++ b/src/passes/EncloseWorld.cpp @@ -17,7 +17,7 @@ // // "Closes" the world, in the sense of making it more compatible with the // --closed-world flag, in a potentially destructive manner. This is mainly -// useful for fuzzing (in that a random module is usually very incomptable with +// useful for fuzzing (in that a random module is usually very incompatible with // closed world, with most types being public and hence unoptimizable, but // running this pass makes as many as we can fully private). // diff --git a/src/passes/Flatten.cpp b/src/passes/Flatten.cpp index 1c2cfbcd536..10c791f609e 100644 --- a/src/passes/Flatten.cpp +++ b/src/passes/Flatten.cpp @@ -265,7 +265,7 @@ struct Flatten // br_if leaves a value on the stack if not taken, which later can // be the last element of the enclosing innermost block and flow // out. The local we created using 'getTempForBreakTarget' returns - // the return type of the block this branch is targetting, which may + // the return type of the block this branch is targeting, which may // not be the same with the innermost block's return type. For // example, // (block $any (result anyref) diff --git a/src/passes/GenerateDynCalls.cpp b/src/passes/GenerateDynCalls.cpp index a49ff1408d9..2645994461a 100644 --- a/src/passes/GenerateDynCalls.cpp +++ b/src/passes/GenerateDynCalls.cpp @@ -18,7 +18,7 @@ // Create `dynCall` helper functions used by emscripten. These allow JavaScript // to call back into WebAssembly given a function pointer (table index). These // are used primarily to implement the `invoke` functions which in turn are used -// to implment exceptions handling and setjmp/longjmp. Creates one for each +// to implement exceptions handling and setjmp/longjmp. Creates one for each // signature in the indirect function table. // diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 06e25edf090..ef0977d12fa 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -102,7 +102,7 @@ struct GenerateGlobalEffects : public Pass { // Compute the transitive closure of effects. To do so, first construct for // each function a list of the functions that it is called by (so we need to - // propogate its effects to them), and then we'll construct the closure of + // propagate its effects to them), and then we'll construct the closure of // that. // // callers[foo] = [func that calls foo, another func that calls foo, ..] diff --git a/src/passes/GlobalStructInference.cpp b/src/passes/GlobalStructInference.cpp index ae9d14488be..bb5a077648a 100644 --- a/src/passes/GlobalStructInference.cpp +++ b/src/passes/GlobalStructInference.cpp @@ -473,8 +473,8 @@ struct GlobalStructInference : public Pass { if (values.size() == 1) { // The case of 1 value is simple: trap if the ref is null, and // otherwise return the value. Since the field is immutable, there - // cannot have been any writes to it we must synchonize with, so we do - // not need a fence. + // cannot have been any writes to it we must synchronize with, so we + // do not need a fence. replaceCurrent(builder.makeSequence( builder.makeDrop(builder.makeRefAs(RefAsNonNull, ref)), getReadValue(values[0], fieldIndex, field, curr))); @@ -655,7 +655,7 @@ struct GlobalStructInference : public Pass { } }; - // Find the optimization opportunitites in parallel. + // Find the optimization opportunities in parallel. ModuleUtils::ParallelFunctionAnalysis optimization( *module, [&](Function* func, GlobalsToUnnest& globalsToUnnest) { if (func->imported()) { diff --git a/src/passes/GlobalTypeOptimization.cpp b/src/passes/GlobalTypeOptimization.cpp index dcb28817ea0..8171ce1c501 100644 --- a/src/passes/GlobalTypeOptimization.cpp +++ b/src/passes/GlobalTypeOptimization.cpp @@ -424,7 +424,7 @@ struct GlobalTypeOptimization : public Pass { std::unordered_set subtypesExposed; // Mark the relevant prototype field as read and return true iff we newly - // know we have to propate the exposure to subtypes. + // know we have to propagate the exposure to subtypes. auto noteExposed = [&](HeapType type, Exactness exact = Inexact) -> bool { if (auto desc = type.getDescriptorType(); desc && JSUtils::hasPossibleJSPrototypeField(*desc)) { diff --git a/src/passes/I64ToI32Lowering.cpp b/src/passes/I64ToI32Lowering.cpp index 86377067f6f..42feafa3861 100644 --- a/src/passes/I64ToI32Lowering.cpp +++ b/src/passes/I64ToI32Lowering.cpp @@ -489,7 +489,7 @@ struct I64ToI32Lowering : public WalkerPass> { return; } // We cannot break this up into smaller operations as it must be atomic. - // Lower to an instrinsic function that wasm2js will implement. + // Lower to an intrinsic function that wasm2js will implement. TempVar lowBits = getTemp(); TempVar highBits = getTemp(); auto* getLow = builder->makeCall( diff --git a/src/passes/Inlining.cpp b/src/passes/Inlining.cpp index 13d0b7630f1..cd328a7e1be 100644 --- a/src/passes/Inlining.cpp +++ b/src/passes/Inlining.cpp @@ -63,7 +63,7 @@ enum class InliningMode { // We do not know yet if this function can be inlined, as that has // not been computed yet. Unknown, - // This function cannot be inlinined in any way. + // This function cannot be inlined in any way. Uninlineable, // This function can be inlined fully, that is, normally: the entire function // can be inlined. This is in contrast to split/partial inlining, see below. @@ -1260,7 +1260,7 @@ struct Inlining : public Pass { // whether to optimize where we inline bool optimize = false; - // the information for each function. recomputed in each iteraction + // the information for each function. recomputed in each interaction NameInfoMap infos; std::unique_ptr functionSplitter; diff --git a/src/passes/LLVMNontrappingFPToIntLowering.cpp b/src/passes/LLVMNontrappingFPToIntLowering.cpp index d14e58af806..382d4155b36 100644 --- a/src/passes/LLVMNontrappingFPToIntLowering.cpp +++ b/src/passes/LLVMNontrappingFPToIntLowering.cpp @@ -74,7 +74,7 @@ struct LLVMNonTrappingFPToIntLoweringImpl Builder builder(*getModule()); Index v = Builder::addVar(getFunction(), curr->value->type); // if fabs(operand) < INT_MAX then use the trapping operation, else return - // INT_MIN. The altnernate value is correct for the case where the input is + // INT_MIN. The alternate value is correct for the case where the input is // INT_MIN itself; otherwise it's UB so any value will do. replaceCurrent(builder.makeIf( builder.makeBinary( diff --git a/src/passes/MemoryPacking.cpp b/src/passes/MemoryPacking.cpp index 9d94492ecb2..83fb1b6e494 100644 --- a/src/passes/MemoryPacking.cpp +++ b/src/passes/MemoryPacking.cpp @@ -44,7 +44,7 @@ namespace wasm { namespace { -// A subsection of an orginal memory segment. If `isZero` is true, memory.fill +// A subsection of an original memory segment. If `isZero` is true, memory.fill // will be used instead of memory.init for this range. struct Range { bool isZero; @@ -651,7 +651,7 @@ void MemoryPacking::createSplitSegments( if (segment->name.is()) { // Name the first range after the original segment and all following // ranges get numbered accordingly. This means that for segments that - // canot be split (segments that contains a single range) the input and + // cannot be split (segments that contains a single range) the input and // output segment have the same name. if (!segmentCount) { name = segment->name; diff --git a/src/passes/Monomorphize.cpp b/src/passes/Monomorphize.cpp index 2798eaec4ba..f4536692402 100644 --- a/src/passes/Monomorphize.cpp +++ b/src/passes/Monomorphize.cpp @@ -636,7 +636,7 @@ struct Monomorphize : public Pass { return; } - // TODO: ignore calls with unreachable operands for simplicty + // TODO: ignore calls with unreachable operands for simplicity // Compute the call context, and the new operands that the call would send // if we use that context. diff --git a/src/passes/OptimizeCasts.cpp b/src/passes/OptimizeCasts.cpp index 55f9a72899e..e2e7c8c43ab 100644 --- a/src/passes/OptimizeCasts.cpp +++ b/src/passes/OptimizeCasts.cpp @@ -387,7 +387,7 @@ struct EarlyCastApplier : public PostWalker { } }; -// Find the best casted verisons of local.gets: other local.gets with the same +// Find the best casted versions of local.gets: other local.gets with the same // value, but cast to a more refined type. struct BestCastFinder : public LinearExecutionWalker { diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index e388775caab..9ea8a7aa982 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -783,7 +783,7 @@ struct OptimizeInstructions curr->op == DivUInt32) { // u32(x) / C ==> u32(x) >= C iff C > 2^31 // We avoid applying this for C == 2^31 due to conflict - // with other rule which transform to more prefereble + // with other rule which transform to more preferable // right shift operation. curr->op = c == -1 ? EqInt32 : GeUInt32; return replaceCurrent(curr); @@ -813,7 +813,7 @@ struct OptimizeInstructions c > std::numeric_limits::min() && curr->op == DivUInt64) { // u64(x) / C ==> u64(u64(x) >= C) iff C > 2^63 // We avoid applying this for C == 2^31 due to conflict - // with other rule which transform to more prefereble + // with other rule which transform to more preferable // right shift operation. // And apply this only for shrinkLevel == 0 due to it // increasing size by one byte. @@ -1478,7 +1478,7 @@ struct OptimizeInstructions // To avoid such risks we should keep in mind the following: // // * Before removing a cast we should use its type information in the best - // way we can. Only after doing so should a cast be removed. In the exmaple + // way we can. Only after doing so should a cast be removed. In the example // above, that means first seeing that the ref.test must return 1, and only // then possibly removing the ref.cast. // * Do not remove a cast if removing it might remove useful information for @@ -1673,7 +1673,7 @@ struct OptimizeInstructions // // TODO We could recurse here. // TODO We could do similar things for casts (rule out an impossible arm). - // TODO Worth thinking about an 'assume' instrinsic of some form that + // TODO Worth thinking about an 'assume' intrinsic of some form that // annotates knowledge about a value, or another mechanism to allow // that information to be passed around. @@ -2783,7 +2783,7 @@ struct OptimizeInstructions } // Check if two consecutive inputs to an instruction are equal. As they are - // consecutive, no code can execeute in between them, which simplies the + // consecutive, no code can execute in between them, which simplifies the // problem here (and which is the case we care about in this pass, which does // simple peephole optimizations - all we care about is a single instruction // at a time, and its inputs). @@ -3463,7 +3463,8 @@ struct OptimizeInstructions } // remove added/subbed zeros struct ZeroRemover : public PostWalker { - // TODO: we could save the binarys and costs we drop, and reuse them later + // TODO: we could save the Binary and Const nodes we drop, and reuse them + // later PassOptions& passOptions; @@ -4586,7 +4587,7 @@ struct OptimizeInstructions c1->value = Literal::makeFromInt32(total, c1->type); return inner; } else { - // overflow. Handle different scenarious + // overflow. Handle different scenarios if (hasAnyRotateShift(op)) { // overflow always accepted in rotation shifts c1->value = Literal::makeFromInt32(effectiveTotal, c1->type); @@ -5025,7 +5026,7 @@ struct OptimizeInstructions switch (curr->op) { case TruncSFloat64ToInt32: case TruncSatSFloat64ToInt32: { - // i32 -> f64 -> i32 rountripping optimization: + // i32 -> f64 -> i32 roundtripping optimization: // i32.trunc(_sat)_f64_s(f64.convert_i32_s(x)) ==> x Expression* x; if (matches(curr->value, unary(ConvertSInt32ToFloat64, any(&x)))) { @@ -5035,7 +5036,7 @@ struct OptimizeInstructions } case TruncUFloat64ToInt32: case TruncSatUFloat64ToInt32: { - // u32 -> f64 -> u32 rountripping optimization: + // u32 -> f64 -> u32 roundtripping optimization: // i32.trunc(_sat)_f64_u(f64.convert_i32_u(x)) ==> x Expression* x; if (matches(curr->value, unary(ConvertUInt32ToFloat64, any(&x)))) { @@ -5850,7 +5851,7 @@ struct OptimizeInstructions if (validTypes && validEffects && validChildren) { // Replace ifTrue with its child. curr->ifTrue = ifTrueChild; - // Relace ifFalse with its child, and reuse that node outside. + // Replace ifFalse with its child, and reuse that node outside. auto* reuse = curr->ifFalse; curr->ifFalse = ifFalseChild; // curr's type may have changed, if the instructions we moved out diff --git a/src/passes/PostEmscripten.cpp b/src/passes/PostEmscripten.cpp index 72630663591..12ea129173d 100644 --- a/src/passes/PostEmscripten.cpp +++ b/src/passes/PostEmscripten.cpp @@ -82,7 +82,7 @@ static void calcSegmentOffsets(Module& wasm, OffsetSearcher(std::unordered_map& offsets) : offsets(offsets) {} void visitMemoryInit(MemoryInit* curr) { - // The desitination of the memory.init is either a constant + // The destination of the memory.init is either a constant // or the result of an addition with __memory_base in the // case of PIC code. auto* dest = curr->dest->dynCast(); diff --git a/src/passes/Precompute.cpp b/src/passes/Precompute.cpp index a5f8da4b42c..fbaf09232b9 100644 --- a/src/passes/Precompute.cpp +++ b/src/passes/Precompute.cpp @@ -104,7 +104,7 @@ class PrecomputingExpressionRunner // Limit evaluation depth for 2 reasons: first, it is highly unlikely // that we can do anything useful to precompute a hugely nested expression - // (we should succed at smaller parts of it first). Second, a low limit is + // (we should succeed at smaller parts of it first). Second, a low limit is // helpful to avoid platform differences in native stack sizes. static const Index MAX_DEPTH = 50; diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 5432a2f471b..1e910868ef8 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -3766,7 +3766,7 @@ class MinifiedPrinter : public Printer { Pass* createMinifiedPrinterPass() { return new MinifiedPrinter(); } -// Prints out a module withough elision, i.e., the full ast +// Prints out a module without elision, i.e., the full ast class FullPrinter : public Printer { public: diff --git a/src/passes/RedundantSetElimination.cpp b/src/passes/RedundantSetElimination.cpp index 7ede2ba35e9..c5017848b1d 100644 --- a/src/passes/RedundantSetElimination.cpp +++ b/src/passes/RedundantSetElimination.cpp @@ -23,7 +23,7 @@ // A risk here is that we extend live ranges, e.g. we may use the default // value at the very end of a function, keeping that local alive throughout. // For that reason it is probably better to run this near the end of -// optimization, and especially after coalesce-locals. A final vaccum +// optimization, and especially after coalesce-locals. A final vacuum // should be done after it, as this pass can leave around drop()s of // values no longer necessary. // @@ -123,7 +123,7 @@ struct RedundantSetElimination // Use a value numbering for the values of expressions. ValueNumbering valueNumbering; - // In additon to valueNumbering, each block has values for each merge. + // In addition to valueNumbering, each block has values for each merge. std::unordered_map> blockMergeValues; diff --git a/src/passes/RemoveUnusedBrs.cpp b/src/passes/RemoveUnusedBrs.cpp index bb539a5c823..77fd6840818 100644 --- a/src/passes/RemoveUnusedBrs.cpp +++ b/src/passes/RemoveUnusedBrs.cpp @@ -1192,7 +1192,7 @@ struct RemoveUnusedBrs : public WalkerPass> { : public PostWalker> { // Map of all labels (branch targets) to the branches going to them. (We - // only care about blocks here, and not loops, but for simplicitly we + // only care about blocks here, and not loops, but for simplicity we // store all branch targets since blocks are 99% of that set anyhow. Any // loops are ignored later.) std::unordered_map> labelToBranches; diff --git a/src/passes/SafeHeap.cpp b/src/passes/SafeHeap.cpp index fe5b21507fb..d5a4f8843de 100644 --- a/src/passes/SafeHeap.cpp +++ b/src/passes/SafeHeap.cpp @@ -17,7 +17,7 @@ // // Instruments code to check for incorrect heap access. This checks // for dereferencing 0 (null pointer access), reading past the valid -// top of sbrk()-addressible memory, and incorrect alignment notation. +// top of sbrk()-addressable memory, and incorrect alignment notation. // #include "asmjs/shared-constants.h" diff --git a/src/passes/SignaturePruning.cpp b/src/passes/SignaturePruning.cpp index ae920839fd4..4670d1c0015 100644 --- a/src/passes/SignaturePruning.cpp +++ b/src/passes/SignaturePruning.cpp @@ -315,7 +315,7 @@ struct SignaturePruning : public Pass { // Create a new signature. When the TypeRewriter operates below it will // modify the existing heap type in place to change its signature to this // one. TypeRewriter will also ensure that distinct types remain - // disctinct, even if they have the same signature after optimization. + // distinct, even if they have the same signature after optimization. newSignatures[type] = Signature(Type(newParams), sig.results); // removeParameters() updates the type as it goes, but in this pass we diff --git a/src/passes/SimplifyGlobals.cpp b/src/passes/SimplifyGlobals.cpp index 14cd6f6de9e..83eeb84e84f 100644 --- a/src/passes/SimplifyGlobals.cpp +++ b/src/passes/SimplifyGlobals.cpp @@ -148,7 +148,7 @@ struct GlobalUseScanner : public WalkerPass> { // if (global % 17 < 4) { global = 1 } // // What we want to disallow is using the global to actually do something that - // is noticeeable *aside* from writing the global, like this: + // is noticeable *aside* from writing the global, like this: // // if (global ? foo() : bar()) { .. } // @@ -582,7 +582,7 @@ struct SimplifyGlobals : public Pass { } if (info.imported || info.exported) { - // If the global is observable from the outside, we can't do anythng + // If the global is observable from the outside, we can't do anything // here. // // TODO: optimize the case of an imported but immutable global, etc. diff --git a/src/passes/TypeGeneralizing.cpp b/src/passes/TypeGeneralizing.cpp index 139ba8efbbd..03a0a0f11a7 100644 --- a/src/passes/TypeGeneralizing.cpp +++ b/src/passes/TypeGeneralizing.cpp @@ -560,7 +560,7 @@ struct TransferFn : OverriddenVisitor { for (size_t i = 0; i < numParams; ++i) { if (candidateSig.params[i] != sig.params[i]) { // Generalizing further would restrict how much we could generalize - // this argument, so we choose not to generalize futher. + // this argument, so we choose not to generalize further. // TODO: Experiment with making the opposite choice. goto done; } @@ -815,7 +815,7 @@ struct TransferFn : OverriddenVisitor { auto srcType = curr->srcRef->type.getHeapType(); if (destType.isBottom() || srcType.isBottom()) { // This will be emitted as unreachable. Do not require anything of the - // input, exept that the bottom refs remain bottom. + // input, except that the bottom refs remain bottom. clearStack(); auto nullref = Type(HeapType::none, Nullable); push(destType.isBottom() ? nullref : Type::none); diff --git a/src/passes/TypeMerging.cpp b/src/passes/TypeMerging.cpp index 5853ead9ad6..8ca33699716 100644 --- a/src/passes/TypeMerging.cpp +++ b/src/passes/TypeMerging.cpp @@ -467,7 +467,7 @@ bool TypeMerging::merge(MergeKind kind) { // Normally splitting partitions like this would require re-running DFA // minimization afterward, but in this case it is not possible that the // manual splits cause types in any other partition to become - // differentiatable. A type and its subtype cannot differ by referring to + // differentiable. A type and its subtype cannot differ by referring to // different, unrelated types in the same position because then they would // not be in a valid subtype relationship. std::vector> newPartitions; diff --git a/src/passes/TypeSSA.cpp b/src/passes/TypeSSA.cpp index 2c99f91ed76..ee63e5ee4b8 100644 --- a/src/passes/TypeSSA.cpp +++ b/src/passes/TypeSSA.cpp @@ -167,7 +167,7 @@ struct Analyzer void note(Expression**, Constraints type) { // Check closed type constraints for exactness. Other kinds of type - // constaints do not concern us. + // constraints do not concern us. // TODO: Handle tuples? for (auto varType : type) { if (auto* t = std::get_if(&varType)) { @@ -198,8 +198,8 @@ struct Analyzer void visitGlobal(Global* global) { // This could be more precise by checking that the init expression is not - // null before inhibiting optimization, or by just inhibiting optmization of - // the allocations used in the initialization, but this is simpler. + // null before inhibiting optimization, or by just inhibiting optimization + // of the allocations used in the initialization, but this is simpler. for (auto type : global->type) { if (type.isExact()) { disallowedTypes.insert(type.getHeapType()); diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index b3eac815170..5f01610adeb 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -1066,7 +1066,7 @@ struct Unsubtyping : Pass, Noter { } // TODO: Consider running the fixup only if we are actually removing any // descriptors. This would require a better way of detecting this than - // collecing and iterating over all the types, though. + // collecting and iterating over all the types, though. struct Rewriter : WalkerPass> { const TypeTree& types; diff --git a/src/passes/Vacuum.cpp b/src/passes/Vacuum.cpp index 2b5ec3f191b..a450b133b32 100644 --- a/src/passes/Vacuum.cpp +++ b/src/passes/Vacuum.cpp @@ -417,7 +417,7 @@ struct Vacuum : public WalkerPass> { } // sink a drop into an arm of an if-else if the other arm ends in an // unreachable, as it if is a branch, this can make that branch optimizable - // and more vaccuming possible + // and more vacuuming possible auto* iff = curr->value->dynCast(); if (iff && iff->ifFalse && iff->type.isConcrete()) { // reuse the drop in both cases diff --git a/src/support/mixed_arena.h b/src/support/mixed_arena.h index bb06d1aa062..a5f00281e3f 100644 --- a/src/support/mixed_arena.h +++ b/src/support/mixed_arena.h @@ -32,7 +32,7 @@ // // Arena-style bump allocation is important for two reasons: First, so that // allocation is quick, and second, so that allocated items are close together, -// which is cache-friendy. Arena allocation is also useful for a minor third +// which is cache-friendly. Arena allocation is also useful for a minor third // reason which is to make freeing all the items in an arena very quick. // // Each WebAssembly Module has an arena allocator, which should be used @@ -100,7 +100,7 @@ struct MixedArena { // allocator for us there. but carefully, as others may do so as // well. we may waste a few allocations here, but it doesn't matter // as this can only happen as the chain is built up, i.e., - // O(# of cores) per allocator, and our allocatrs are long-lived. + // O(# of cores) per allocator, and our allocators are long-lived. if (!allocated) { allocated = new MixedArena(); // has our thread id } diff --git a/src/support/path.h b/src/support/path.h index 8383bc53ea8..82c9f751845 100644 --- a/src/support/path.h +++ b/src/support/path.h @@ -44,7 +44,7 @@ char getPathSeparator(); std::string getDirName(const std::string& path); std::string getBaseName(const std::string& path); -// Get the binaryen root dor. +// Get the binaryen root dir. std::string getBinaryenRoot(); // Get the binaryen bin dir. diff --git a/src/support/topological_sort.h b/src/support/topological_sort.h index c0e1a93712e..c96047e07b1 100644 --- a/src/support/topological_sort.h +++ b/src/support/topological_sort.h @@ -38,7 +38,7 @@ struct CycleException {}; // An adjacency list containing edges from vertices to their successors. Uses // `Index` because we are primarily sorting elements of Wasm modules. If we ever -// need to sort signficantly larger objects, we might need to switch to +// need to sort significantly larger objects, we might need to switch to // `size_t` or make this a template parameter. using Graph = std::vector>; diff --git a/src/tools/optimization-options.h b/src/tools/optimization-options.h index bc1ac4a1478..1e089f0b8c8 100644 --- a/src/tools/optimization-options.h +++ b/src/tools/optimization-options.h @@ -357,7 +357,7 @@ struct OptimizationOptions : public ToolOptions { // Pass arguments with the same name as the pass are stored per-instance on // PassInfo, while all other arguments are stored globally on - // passOptions.arguments (which is what the overriden method on ToolOptions + // passOptions.arguments (which is what the overridden method on ToolOptions // does). void addPassArg(const std::string& key, const std::string& value) override { // Scan the current pass list for the last defined instance of a pass named diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index adc5a4fe988..8cccf8c7ea2 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -1107,7 +1107,7 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, // the locals here. That is, we need to save the local state in the function, // which we do by setting up at the entry. We update this list of expressions // at the same time as applyToModule() - we must only do it after an entire - // atomic "chunk" has been processed succesfully, we do not want partial + // atomic "chunk" has been processed successfully, we do not want partial // updates from an item in the block that we only partially evalled. When we // construct the (partially) evalled function, we will create local.sets of // these expressions at the beginning. @@ -1213,7 +1213,7 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, // module. Note that we must serialize the locals now as doing so may // cause changes that must be applied to the module (e.g. GC data may // cause globals to be added). And we must apply to the module now, and - // not later, as we must do so right after a successfull partial eval + // not later, as we must do so right after a successful partial eval // (after any failure to eval, the global state is no long valid to be // applied to the module, as incomplete changes may have occurred). // diff --git a/src/tools/wasm-reduce/wasm-reduce.cpp b/src/tools/wasm-reduce/wasm-reduce.cpp index bc5dabfcab0..e002a499545 100644 --- a/src/tools/wasm-reduce/wasm-reduce.cpp +++ b/src/tools/wasm-reduce/wasm-reduce.cpp @@ -1559,7 +1559,7 @@ More documentation can be found at } } - // no point in a factor lorger than the size + // no point in a factor larger than the size assert(newSize > 4); // wasm modules are >4 bytes anyhow factor = std::min(factor, int(newSize) / 4); diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 5b6990ea57b..af9567ae762 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -188,7 +188,7 @@ struct ExnData { // // The key idea in this approach to suspending and resuming is that to suspend // you want to unwind the stack - you "jump" back to some outer scope - and to -// reume, we want to rewind the stack - to get everything back exactly the way +// resume, we want to rewind the stack - to get everything back exactly the way // it was, so we can pick things back up. And, to achieve that, we really just // need two things: // * To rewind the call stack. If we called foo() and then bar(), we want to @@ -1958,7 +1958,7 @@ class ExpressionRunner : public OverriddenVisitor { Flow visitTryTable(TryTable* curr) { WASM_UNREACHABLE("unimp"); } Flow visitThrow(Throw* curr) { // Single-module implementation. This is used from Precompute, for example. - // It is overriden in ModuleRunner to add logic for finding the proper + // It is overridden in ModuleRunner to add logic for finding the proper // imported tag (which single-module cases don't care about). Literals arguments; VISIT_ARGUMENTS(flow, curr->operands, arguments); diff --git a/src/wasm-ir-builder.h b/src/wasm-ir-builder.h index d6aed8a1c32..44b4ba0b270 100644 --- a/src/wasm-ir-builder.h +++ b/src/wasm-ir-builder.h @@ -466,7 +466,7 @@ class IRBuilder : public UnifiedExpressionVisitor> { // When transitioning to a new scope for a delimiter like `else` or catch, // most of the scope context is preserved, but some parts need to be reset. // `keepInput` means that control flow parameters are available at the - // begninning of the scope after the delimiter. + // beginning of the scope after the delimiter. void resetForDelimiter(bool keepInput) { exprStack.clear(); unreachable = false; diff --git a/src/wasm-traversal.h b/src/wasm-traversal.h index a3bdc5a3905..e8f6a62b45b 100644 --- a/src/wasm-traversal.h +++ b/src/wasm-traversal.h @@ -115,7 +115,7 @@ struct UnifiedExpressionVisitor : public Visitor { // template struct Walker : public VisitorType { - // Useful methods for visitor implementions + // Useful methods for visitor implementations // Replace the current node. You can call this in your visit*() methods. // Note that the visit*() for the result node is not called for you (i.e., diff --git a/src/wasm-type.h b/src/wasm-type.h index 3667f67888e..a0340c2b083 100644 --- a/src/wasm-type.h +++ b/src/wasm-type.h @@ -764,7 +764,7 @@ struct Array { // TypeBuilder - allows for the construction of recursive types. Contains a // table of `n` mutable HeapTypes and can construct temporary types that are -// backed by those HeapTypes, refering to them by reference. Those temporary +// backed by those HeapTypes, referring to them by reference. Those temporary // types are owned by the TypeBuilder and should only be used in the // construction of HeapTypes to insert into the TypeBuilder. Temporary types // should never be used in the construction of normal Types, only other diff --git a/src/wasm.h b/src/wasm.h index 9f61b4432ae..941d759ce6b 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -122,7 +122,7 @@ enum UnaryOp { TruncSFloat64ToInt64, TruncUFloat64ToInt32, TruncUFloat64ToInt64, - // reintepret bits to int + // reinterpret bits to int ReinterpretFloat32, ReinterpretFloat64, // int to float @@ -143,7 +143,7 @@ enum UnaryOp { ReinterpretInt64, // Extend signed subword-sized integer. This differs from e.g. ExtendSInt32 - // because the input integer is in an i64 value insetad of an i32 value. + // because the input integer is in an i64 value instead of an i32 value. ExtendS8Int32, ExtendS16Int32, ExtendS8Int64, @@ -1095,7 +1095,7 @@ class AtomicFence : public SpecificExpression { AtomicFence() = default; AtomicFence(MixedArena& allocator) : AtomicFence() {} - // Current wasm threads only supports sequentialy consistent atomics, but + // Current wasm threads only supports sequentially consistent atomics, but // other orderings may be added in the future. This field is reserved for // that, and currently set to 0. uint8_t order = 0; diff --git a/src/wasm/wasm-emscripten.cpp b/src/wasm/wasm-emscripten.cpp index 4c450bdb2d1..592020e1fef 100644 --- a/src/wasm/wasm-emscripten.cpp +++ b/src/wasm/wasm-emscripten.cpp @@ -121,7 +121,7 @@ class StringConstantTracker { OffsetSearcher(std::unordered_map& offsets) : offsets(offsets) {} void visitMemoryInit(MemoryInit* curr) { - // The desitination of the memory.init is either a constant + // The destination of the memory.init is either a constant // or the result of an addition with __memory_base in the // case of PIC code. auto* dest = curr->dest->dynCast(); diff --git a/src/wasm/wasm-stack-opts.cpp b/src/wasm/wasm-stack-opts.cpp index b3a4d15e26d..eae9b6c681f 100644 --- a/src/wasm/wasm-stack-opts.cpp +++ b/src/wasm/wasm-stack-opts.cpp @@ -209,7 +209,7 @@ void StackIROptimizer::local2Stack() { bool optimized = false; // Do not optimize multivalue locals, since those will be better // optimized when they are visited in the binary writer and this - // optimization would intefere with that one. + // optimization would interfere with that one. if (auto* get = inst->origin->dynCast(); get && inst->type.isSingle() && !deferredGets.contains(get)) { // Use another local to clarify what instIndex means in this scope. diff --git a/src/wasm/wasm-type.cpp b/src/wasm/wasm-type.cpp index b7c2c8d2bd0..8ff0e9c362a 100644 --- a/src/wasm/wasm-type.cpp +++ b/src/wasm/wasm-type.cpp @@ -2761,7 +2761,7 @@ TypeBuilder::BuildResult TypeBuilder::build() { auto group = (*built)[0].getRecGroup(); auto uniqueGroup = impl->unique.insertOrGet(group); if (group != uniqueGroup) { - // There is a conflict. Find the set of missing featuers that would + // There is a conflict. Find the set of missing features that would // resolve the conflict if enabled. FeatureSet missingFeatures = FeatureSet::None; FeatureSet potential = FeatureSet::GC | FeatureSet::CustomDescriptors; diff --git a/test/lit/exec/delegate-vacuum.wast b/test/lit/exec/delegate-vacuum.wast index 90436fe309e..171fca8e85f 100644 --- a/test/lit/exec/delegate-vacuum.wast +++ b/test/lit/exec/delegate-vacuum.wast @@ -1,7 +1,7 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py --output=fuzz-exec and should not be edited. ;; RUN: wasm-opt %s --vacuum --fuzz-exec -all -q -o /dev/null 2>&1 | filecheck %s -;; Test the effect of vaccum on delegation. The delegate target must not +;; Test the effect of vacuum on delegation. The delegate target must not ;; "escape" the current function scope and affect anything external, that is, ;; it must be cleared on function exit. diff --git a/test/lit/passes/cfp.wast b/test/lit/passes/cfp.wast index 81946889b05..fa7fb9277d6 100644 --- a/test/lit/passes/cfp.wast +++ b/test/lit/passes/cfp.wast @@ -3262,7 +3262,7 @@ ) ) (drop - ;; This can be optimzied in principle, but our analysis cannot yet prove + ;; This can be optimized in principle, but our analysis cannot yet prove ;; there is no synchronization. TODO. (struct.atomic.get acqrel $shared 0 (local.get 0) diff --git a/test/lit/passes/heap2local.wast b/test/lit/passes/heap2local.wast index ae42ea45c1b..1f72d5f947c 100644 --- a/test/lit/passes/heap2local.wast +++ b/test/lit/passes/heap2local.wast @@ -1004,7 +1004,7 @@ ;; After the outer one is optimized, the inner one can be optimized in ;; principle, as it can be seen to no longer escape. However, we depend ;; on other optimizations to actually remove the outer allocation (like - ;; vacuum), and so it cannot be optimized. If we ran vaccum, and then + ;; vacuum), and so it cannot be optimized. If we ran vacuum, and then ;; additional iterations, this might be handled. (struct.new_default $struct.recursive) ) diff --git a/test/lit/passes/optimize-instructions-gc.wast b/test/lit/passes/optimize-instructions-gc.wast index 4d0703d5f91..9dc52715724 100644 --- a/test/lit/passes/optimize-instructions-gc.wast +++ b/test/lit/passes/optimize-instructions-gc.wast @@ -679,7 +679,7 @@ (func $flip-tee-of-as-non-null-non-nullable (param $x (ref any)) (param $y (ref null any)) (drop (local.tee $x - ;; this *cannnot* be moved through the tee outward, as the param is in + ;; this *cannot* be moved through the tee outward, as the param is in ;; fact non-nullable, and we depend on the ref.as_non_null in order to ;; get a valid type to assign to it (ref.as_non_null diff --git a/test/lit/passes/optimize-instructions-mvp.wast b/test/lit/passes/optimize-instructions-mvp.wast index c744c0eb3ad..a301cc6858c 100644 --- a/test/lit/passes/optimize-instructions-mvp.wast +++ b/test/lit/passes/optimize-instructions-mvp.wast @@ -18144,7 +18144,7 @@ ;; CHECK-NEXT: ) (func $skip-added-constants-negative (result i32) ;; Reasonable negative constants can be optimized. But the add is - ;; canoncalized into a sub, and atm we do not optimize such added constants. + ;; canonicalized into a sub, and atm we do not optimize such added constants. (i32.ge_s (i32.add (i32.shr_u @@ -18174,7 +18174,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $skip-added-constants-negative-flip (result i32) - ;; As above, but flipped. The add is canoncalized into a sub, and atm we do + ;; As above, but flipped. The add is canonicalized into a sub, and atm we do ;; not optimize such added constants. (i32.ge_s (i32.add @@ -18233,7 +18233,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $skip-added-constants-mix-flip (result i32) - ;; As above, but with sign flipped. The add is canoncalized into a sub, and + ;; As above, but with sign flipped. The add is canonicalized into a sub, and ;; atm we do not optimize such added constants. (i32.ge_s (i32.add diff --git a/test/lit/passes/vacuum_all-features.wast b/test/lit/passes/vacuum_all-features.wast index e689b853668..bdc313e7b1b 100644 --- a/test/lit/passes/vacuum_all-features.wast +++ b/test/lit/passes/vacuum_all-features.wast @@ -1394,7 +1394,7 @@ (func $1 (tuple.drop 2 (block $block (result funcref i32) - ;; we can vaccum out all parts of this block: the br_if is not taken, there + ;; we can vacuum out all parts of this block: the br_if is not taken, there ;; is a nop, and the tuple at the end goes to a dropped block anyhow. this ;; test specifically verifies handling of tuples containing non-nullable ;; types, for which we try to create a zero in an intermediate step along diff --git a/test/passes/remove-unused-names_vacuum.wast b/test/passes/remove-unused-names_vacuum.wast index 66412353cac..128e1af08f3 100644 --- a/test/passes/remove-unused-names_vacuum.wast +++ b/test/passes/remove-unused-names_vacuum.wast @@ -1,7 +1,7 @@ (module (func $return-i32-but-body-is-unreachable3 (result i32) (local $label i32) - (block ;; without a name here, vaccum had a too-eager bug + (block ;; without a name here, vacuum had a too-eager bug (loop $while-in$1 (br $while-in$1) ) @@ -9,7 +9,7 @@ ) (func $return-i32-but-body-is-unreachable4 (result i32) (local $label i32) - (block ;; without a name here, vaccum had a too-eager bug + (block ;; without a name here, vacuum had a too-eager bug (loop $while-in$1 (br $while-in$1) ) @@ -25,13 +25,13 @@ ) (func $return-i32-but-body-is-unreachable5 (result i32) (local $label i32) - (block ;; without a name here, vaccum had a too-eager bug + (block ;; without a name here, vacuum had a too-eager bug (unreachable) ) ) (func $return-i32-but-body-is-unreachable6 (result i32) (local $label i32) - (block ;; without a name here, vaccum had a too-eager bug + (block ;; without a name here, vacuum had a too-eager bug (unreachable) ) (i32.const 0) diff --git a/test/spec/br_on_cast_desc_eq.wast b/test/spec/br_on_cast_desc_eq.wast index c40653b753a..a85eac015aa 100644 --- a/test/spec/br_on_cast_desc_eq.wast +++ b/test/spec/br_on_cast_desc_eq.wast @@ -356,7 +356,7 @@ (type $desc (describes $struct) (struct)) ) (func (param $any anyref) (param $desc (ref null $desc)) (result (ref null (exact $struct))) - ;; The sent type cannnot be exact because the descriptor is not exact. + ;; The sent type cannot be exact because the descriptor is not exact. (br_on_cast_desc_eq 0 anyref (ref null $struct) (local.get $any) (local.get $desc) diff --git a/test/spec/relaxed-atomics.wast b/test/spec/relaxed-atomics.wast index c23d3c3da96..3707daf3148 100644 --- a/test/spec/relaxed-atomics.wast +++ b/test/spec/relaxed-atomics.wast @@ -5,7 +5,7 @@ (memory i64 1 1) ;; Memory index must come before memory ordering if present. - ;; Both immediates are optional; an ommitted memory ordering will be treated as seqcst. + ;; Both immediates are optional; an omitted memory ordering will be treated as seqcst. (func $test-all-ops (drop (i32.atomic.load (i32.const 0))) (drop (i32.atomic.load acqrel (i32.const 0))) diff --git a/third_party/llvm-project/DWARFContext.cpp b/third_party/llvm-project/DWARFContext.cpp index 64c153bfa6e..f11aa1fde18 100644 --- a/third_party/llvm-project/DWARFContext.cpp +++ b/third_party/llvm-project/DWARFContext.cpp @@ -1296,7 +1296,7 @@ struct SymInfo { }; /// Returns the address of symbol relocation used against and a section index. -/// Used for futher relocations computation. Symbol's section load address is +/// Used for further relocations computation. Symbol's section load address is static Expected getSymbolInfo(const object::ObjectFile &Obj, const RelocationRef &Reloc, const LoadedObjectInfo *L, From 6780d4b160648df0a28d9ba62385ce7354b31710 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 3 Apr 2026 12:39:22 -0700 Subject: [PATCH 008/168] Make everything 4% faster by skipping empty tasks [NFC] (#8571) When a visitor is the original ```cpp void visitFoo(Foo* curr) {}` ``` (that is, empty), and the doVisit is also unchanged, ```cpp static void doVisitFoo(Self* self, Foo* curr) { self->visitFoo(curr); } ``` (that is, it just calls the visitor), then we do not need to queue such tasks for execution at all. Measurements show a 2.5%-5% speedup, average 4%. --- src/wasm-traversal.h | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/wasm-traversal.h b/src/wasm-traversal.h index e8f6a62b45b..9a5c5d83226 100644 --- a/src/wasm-traversal.h +++ b/src/wasm-traversal.h @@ -36,7 +36,10 @@ namespace wasm { // A generic visitor, defaulting to doing nothing on each visit -template struct Visitor { +template struct Visitor { + // Capture the parameter in something we can access later. + using ReturnType = ReturnType_; + // Expression visitors #define DELEGATE(CLASS_TO_VISIT) \ ReturnType visit##CLASS_TO_VISIT(CLASS_TO_VISIT* curr) { \ @@ -351,9 +354,40 @@ struct PostWalker : public Walker { #define DELEGATE_ID curr->_id + // Don't push empty tasks, that is, functions that we just push to the + // stack, pop, and then nothing happens when we call the empty function. The + // default visitFoo() in Visitor is empty, and the static doVisitFoo() in + // Walker just calls it, so if neither have been changed, we know that + // nothing will run. + // + // Note that we check Visitor<..> and not VisitorType. Only Visitor is the + // actual top type we know has empty visitors, while VisitorType could be + // anything. + // + // Unfortunately we must avoid this in gcc 11 and earlier, as they error on + // these function pointers not being constexpr. Remove the constexpr there. + // Note that even if this ends up being a runtime check, it should be faster + // than pushing empty tasks, as the check is much faster than the push/pop/ + // call, and a large number of our calls (most, perhaps) are not overridden. +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 11 +#define DELEGATE_START(id) \ + if (&SubType::visit##id != \ + &Visitor::visit##id || \ + &SubType::doVisit##id != &Walker::doVisit##id) { \ + self->pushTask(SubType::doVisit##id, currp); \ + } \ + [[maybe_unused]] auto* cast = curr->cast(); +#else #define DELEGATE_START(id) \ - self->pushTask(SubType::doVisit##id, currp); \ + if constexpr (&SubType::visit##id != \ + &Visitor::visit##id || \ + &SubType::doVisit##id != \ + &Walker::doVisit##id) { \ + self->pushTask(SubType::doVisit##id, currp); \ + } \ [[maybe_unused]] auto* cast = curr->cast(); +#endif #define DELEGATE_GET_FIELD(id, field) cast->field From a3ac1d959b7bbe9f33ec1d05cd3ccbf41886dcb6 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 6 Apr 2026 12:52:16 -0700 Subject: [PATCH 009/168] [NFC] Allow SubTypes::iterSubTypes to stop early (#8573) In ConstantFieldPropagation this is important: we scan subtypes to check if they all have 2 possible values we can `ref.test` between. In the common case there are many values and we can stop early. This makes the pass 4.2x faster on a large Dart testcase, and `-O3` overall 3.5% faster. --- src/ir/possible-contents.cpp | 3 +++ src/ir/subtypes.h | 20 +++++++++++----- src/passes/ConstantFieldPropagation.cpp | 28 +++++++++-------------- src/passes/RemoveUnusedModuleElements.cpp | 1 + src/tools/fuzzing/heap-types.cpp | 6 +++-- test/gtest/type-builder.cpp | 19 +++++++++++++++ 6 files changed, 52 insertions(+), 25 deletions(-) diff --git a/src/ir/possible-contents.cpp b/src/ir/possible-contents.cpp index 10b01f986dc..4dc6da9c9d9 100644 --- a/src/ir/possible-contents.cpp +++ b/src/ir/possible-contents.cpp @@ -881,6 +881,7 @@ struct InfoCollector info.links.push_back({SignatureResultLocation{subType, i}, ResultLocation{getFunction(), i}}); } + return true; }); } } @@ -3282,6 +3283,7 @@ void Flower::readFromData(Type declaredType, [&](HeapType type, Index depth) { connectDuringFlow(DataLocation{type, fieldIndex}, coneReadLocation); + return true; }); // TODO: we can end up with redundant links here if we see one cone first @@ -3351,6 +3353,7 @@ void Flower::writeToData(Expression* ref, cone.type.getHeapType(), normalizedDepth, [&](HeapType type, Index depth) { auto heapLoc = DataLocation{type, fieldIndex}; updateContents(heapLoc, valueContents); + return true; }); } diff --git a/src/ir/subtypes.h b/src/ir/subtypes.h index ae3a27878f5..912d61f878d 100644 --- a/src/ir/subtypes.h +++ b/src/ir/subtypes.h @@ -168,15 +168,19 @@ struct SubTypes { // Efficiently iterate on subtypes of a type, up to a particular depth (depth // 0 means not to traverse subtypes, etc.). The callback function receives - // (type, depth). + // (type, depth) and returns whether to continue the scan, i.e. if it returns + // false, we stop. Returns the last value returned to it, that is, returns + // true if we did not stop early, and false if we did. template - void iterSubTypes(HeapType type, Index depth, F func) const { + bool iterSubTypes(HeapType type, Index depth, F func) const { // Start by traversing the type itself. - func(type, 0); + if (!func(type, 0)) { + return false; + } if (depth == 0) { // Nothing else to scan. - return; + return true; } // getImmediateSubTypes() returns vectors of subtypes, so for efficiency @@ -201,17 +205,21 @@ struct SubTypes { auto& currVec = *item.vec; assert(currDepth <= depth); for (auto type : currVec) { - func(type, currDepth); + if (!func(type, currDepth)) { + return false; + } auto* subVec = &getImmediateSubTypes(type); if (currDepth + 1 <= depth && !subVec->empty()) { work.push_back({subVec, currDepth + 1}); } } } + + return true; } // As above, but iterate to the maximum depth. - template void iterSubTypes(HeapType type, F func) const { + template bool iterSubTypes(HeapType type, F func) const { return iterSubTypes(type, std::numeric_limits::max(), func); } diff --git a/src/passes/ConstantFieldPropagation.cpp b/src/passes/ConstantFieldPropagation.cpp index e2b75e88de8..0063a8d3a69 100644 --- a/src/passes/ConstantFieldPropagation.cpp +++ b/src/passes/ConstantFieldPropagation.cpp @@ -328,31 +328,23 @@ struct FunctionOptimizer : public WalkerPass> { } values[2]; // Handle one of the subtypes of the relevant type. We check what value it - // has for the field, and update |values|. If we hit a problem, we mark us - // as having failed. - auto fail = false; + // has for the field, and update |values|. If we hit a problem, we stop + // early. auto handleType = [&](HeapType type, Index depth) { - if (fail) { - // TODO: Add a mechanism to halt |iterSubTypes| in the middle, as once - // we fail there is no point to further iterating. - return; - } - auto iter = refTestInfos.find({type, Exact}); if (iter == refTestInfos.end()) { // This type has no allocations, so we can ignore it: it is abstract. - return; + return true; } auto value = iter->second[index]; if (!value.hasNoted()) { // Also abstract and ignorable. - return; + return true; } if (!value.isConstant()) { // The value here is not constant, so give up entirely. - fail = true; - return; + return false; } // Consider the constant value compared to previous ones. @@ -375,14 +367,15 @@ struct FunctionOptimizer : public WalkerPass> { // least, we can do that if there is another iteration: If it's already // the last, we've failed to find only two values. if (i == 1) { - fail = true; - return; + return false; } } + + return true; }; - subTypes.iterSubTypes(refHeapType, handleType); - if (fail) { + // If we stopped early, we hit a problem and failed. + if (!subTypes.iterSubTypes(refHeapType, handleType)) { return; } @@ -677,6 +670,7 @@ struct ConstantFieldPropagation : public Pass { if (readable[{sub, Exact}][dst.index].combine(val)) { applyCopiesFrom(sub, Exact, dst.index, val); } + return true; }); } else { // The copy destination is exact, so there are no subtypes to diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 34418009cf4..8bba990af8d 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -496,6 +496,7 @@ struct Analyzer { } } unreadStructFieldExprMap.erase(subStructField); + return true; }); } } diff --git a/src/tools/fuzzing/heap-types.cpp b/src/tools/fuzzing/heap-types.cpp index 426a9b76a9c..e2b6b552623 100644 --- a/src/tools/fuzzing/heap-types.cpp +++ b/src/tools/fuzzing/heap-types.cpp @@ -1000,8 +1000,10 @@ void Inhabitator::markNullable(FieldPos field) { // this extra `index` variable once we have C++20. It's a workaround for // lambdas being unable to capture structured bindings. const size_t index = idx; - subtypes.iterSubTypes( - curr, [&](HeapType type, Index) { nullables.insert({type, index}); }); + subtypes.iterSubTypes(curr, [&](HeapType type, Index) { + nullables.insert({type, index}); + return true; + }); break; } } diff --git a/test/gtest/type-builder.cpp b/test/gtest/type-builder.cpp index a92f10d3ee0..e6ce3bce61b 100644 --- a/test/gtest/type-builder.cpp +++ b/test/gtest/type-builder.cpp @@ -1695,6 +1695,7 @@ TEST_F(TypeTest, TestIterSubTypes) { HeapType A, B, C, D; { TypeBuilder builder(4); + builder.createRecGroup(0, 4); builder[0].setOpen() = Struct(); builder[1].setOpen().subTypeOf(builder[0]) = Struct(); builder[2].setOpen().subTypeOf(builder[0]) = Struct(); @@ -1717,6 +1718,7 @@ TEST_F(TypeTest, TestIterSubTypes) { TypeDepths ret; subTypes.iterSubTypes(type, depth, [&](HeapType subType, Index depth) { ret.insert({subType, depth}); + return true; }); return ret; }; @@ -1729,6 +1731,23 @@ TEST_F(TypeTest, TestIterSubTypes) { EXPECT_EQ(getSubTypes(C, 0), TypeDepths({{C, 0}})); EXPECT_EQ(getSubTypes(C, 1), TypeDepths({{C, 0}, {D, 1}})); EXPECT_EQ(getSubTypes(C, 2), TypeDepths({{C, 0}, {D, 1}})); + + // When the iteration function returns |false|, we stop. + int count = 0; + subTypes.iterSubTypes(A, 3, [&](HeapType subType, Index depth) { + count++; + // Stop after the second increment. + return count != 2; + }); + EXPECT_EQ(count, 2); + + // If we return true, we iterate through all four. + count = 0; + subTypes.iterSubTypes(A, 3, [&](HeapType subType, Index depth) { + count++; + return true; + }); + EXPECT_EQ(count, 4); } // Test supertypes From b09fad05ca73491a59de1a0b353019aecb0feafa Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 6 Apr 2026 14:01:44 -0700 Subject: [PATCH 010/168] Temporarily disable Split in fuzz_opt (#8575) Until a fix for #8510 has landed. --- scripts/fuzz_opt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 89ae3ff0007..784476d89c2 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2301,7 +2301,7 @@ def handle(self, wasm): TrapsNeverHappen(), CtorEval(), Merge(), - Split(), + # Split(), # https://github.com/WebAssembly/binaryen/issues/8510 RoundtripText(), ClusterFuzz(), Two(), From cfa8abd25943c595b8f99ae7c7cc70dd3938ef70 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 7 Apr 2026 08:33:33 -0700 Subject: [PATCH 011/168] [NFC] Optimize ModuleUtils type-scanning code (#8572) Before, we always did a loop on `type.getHeapTypeChildren()` which means setting up a scanner object and going through a generic path. Instead, handle the common cases directly. This avoids any generic path in the common case. This makes us 1% faster on `-O3`, as measured by instruction count, number of branches, and walltime. The noise in the first two is incredibly small, so this looks reliably faster. --- src/ir/module-utils.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/ir/module-utils.cpp b/src/ir/module-utils.cpp index 01f4e1cbc65..6ee94d35d65 100644 --- a/src/ir/module-utils.cpp +++ b/src/ir/module-utils.cpp @@ -363,8 +363,15 @@ struct TypeInfos { } } void note(Type type) { - for (HeapType ht : type.getHeapTypeChildren()) { - note(ht); + // Handle the common case of a ref directly, to avoid a scan of children. + if (type.isRef()) { + note(type.getHeapType()); + return; + } + if (type.isTuple()) { + for (HeapType ht : type.getHeapTypeChildren()) { + note(ht); + } } } // Ensure a type is included without increasing its count. @@ -374,8 +381,14 @@ struct TypeInfos { } } void include(Type type) { - for (HeapType ht : type.getHeapTypeChildren()) { - include(ht); + if (type.isRef()) { + include(type.getHeapType()); + return; + } + if (type.isTuple()) { + for (HeapType ht : type.getHeapTypeChildren()) { + include(ht); + } } } void noteControlFlow(Signature sig) { From 27dbce1c0b5d6cb39f6a1fab93bf79cf271db94b Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 7 Apr 2026 15:25:56 -0700 Subject: [PATCH 012/168] [FP16] Implement f32x4.promote_low_f16x8. (#8578) Specified at https://github.com/WebAssembly/half-precision/blob/main/proposals/half-precision/Overview.md Note: The instruction name `promote_low_f16x8` is different than the overview. I intend to update the spec overview to fix the name to match the other promote instruction. --- scripts/gen-s-parser.py | 1 + src/binaryen-c.cpp | 3 +++ src/binaryen-c.h | 1 + src/gen-s-parser.inc | 29 ++++++++++++++++++++--------- src/ir/child-typer.h | 1 + src/ir/cost.h | 1 + src/literal.h | 1 + src/passes/Print.cpp | 3 +++ src/tools/fuzzing/fuzzing.cpp | 3 ++- src/wasm-binary.h | 1 + src/wasm-interpreter.h | 2 ++ src/wasm.h | 1 + src/wasm/literal.cpp | 8 ++++++++ src/wasm/wasm-binary.cpp | 2 ++ src/wasm/wasm-stack.cpp | 4 ++++ src/wasm/wasm-validator.cpp | 1 + src/wasm/wasm.cpp | 1 + test/lit/basic/f16.wast | 22 ++++++++++++++++++++++ test/spec/f16.wast | 21 +++++++++++++++++++++ 19 files changed, 96 insertions(+), 10 deletions(-) diff --git a/scripts/gen-s-parser.py b/scripts/gen-s-parser.py index 4b73fee9f89..d0f08d0546b 100755 --- a/scripts/gen-s-parser.py +++ b/scripts/gen-s-parser.py @@ -549,6 +549,7 @@ ("i16x8.trunc_sat_f16x8_u", "makeUnary(UnaryOp::TruncSatUVecF16x8ToVecI16x8)"), ("f16x8.convert_i16x8_s", "makeUnary(UnaryOp::ConvertSVecI16x8ToVecF16x8)"), ("f16x8.convert_i16x8_u", "makeUnary(UnaryOp::ConvertUVecI16x8ToVecF16x8)"), + ("f32x4.promote_low_f16x8", "makeUnary(UnaryOp::PromoteLowVecF16x8ToVecF32x4)"), ("f16x8.madd", "makeSIMDTernary(SIMDTernaryOp::MaddVecF16x8)"), ("f16x8.nmadd", "makeSIMDTernary(SIMDTernaryOp::NmaddVecF16x8)"), diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index d7dcb8224f0..8a87bdb917a 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -1021,6 +1021,9 @@ BinaryenOp BinaryenDemoteZeroVecF64x2ToVecF32x4(void) { BinaryenOp BinaryenPromoteLowVecF32x4ToVecF64x2(void) { return PromoteLowVecF32x4ToVecF64x2; } +BinaryenOp BinaryenPromoteLowVecF16x8ToVecF32x4(void) { + return PromoteLowVecF16x8ToVecF32x4; +} BinaryenOp BinaryenRelaxedTruncSVecF32x4ToVecI32x4(void) { return RelaxedTruncSVecF32x4ToVecI32x4; } diff --git a/src/binaryen-c.h b/src/binaryen-c.h index 63a8020e095..a0c66ff9b7e 100644 --- a/src/binaryen-c.h +++ b/src/binaryen-c.h @@ -684,6 +684,7 @@ BINARYEN_API BinaryenOp BinaryenTruncSatZeroSVecF64x2ToVecI32x4(void); BINARYEN_API BinaryenOp BinaryenTruncSatZeroUVecF64x2ToVecI32x4(void); BINARYEN_API BinaryenOp BinaryenDemoteZeroVecF64x2ToVecF32x4(void); BINARYEN_API BinaryenOp BinaryenPromoteLowVecF32x4ToVecF64x2(void); +BINARYEN_API BinaryenOp BinaryenPromoteLowVecF16x8ToVecF32x4(void); BINARYEN_API BinaryenOp BinaryenRelaxedTruncSVecF32x4ToVecI32x4(void); BINARYEN_API BinaryenOp BinaryenRelaxedTruncUVecF32x4ToVecI32x4(void); BINARYEN_API BinaryenOp BinaryenRelaxedTruncZeroSVecF64x2ToVecI32x4(void); diff --git a/src/gen-s-parser.inc b/src/gen-s-parser.inc index 132806efcf1..eca86c6ed77 100644 --- a/src/gen-s-parser.inc +++ b/src/gen-s-parser.inc @@ -1127,16 +1127,27 @@ switch (buf[0]) { } } case 'p': { - switch (buf[8]) { - case 'a': - if (op == "f32x4.pmax"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::PMaxVecF32x4)); - return Ok{}; + switch (buf[7]) { + case 'm': { + switch (buf[8]) { + case 'a': + if (op == "f32x4.pmax"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::PMaxVecF32x4)); + return Ok{}; + } + goto parse_error; + case 'i': + if (op == "f32x4.pmin"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::PMinVecF32x4)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; - case 'i': - if (op == "f32x4.pmin"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::PMinVecF32x4)); + } + case 'r': + if (op == "f32x4.promote_low_f16x8"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::PromoteLowVecF16x8ToVecF32x4)); return Ok{}; } goto parse_error; diff --git a/src/ir/child-typer.h b/src/ir/child-typer.h index 132a0168855..385e0fa8290 100644 --- a/src/ir/child-typer.h +++ b/src/ir/child-typer.h @@ -447,6 +447,7 @@ template struct ChildTyper : OverriddenVisitor { case TruncSatUVecF16x8ToVecI16x8: case ConvertSVecI16x8ToVecF16x8: case ConvertUVecI16x8ToVecF16x8: + case PromoteLowVecF16x8ToVecF32x4: case AnyTrueVec128: case AllTrueVecI8x16: case AllTrueVecI16x8: diff --git a/src/ir/cost.h b/src/ir/cost.h index 1cf67abe9e6..0042d27bcb2 100644 --- a/src/ir/cost.h +++ b/src/ir/cost.h @@ -284,6 +284,7 @@ struct CostAnalyzer : public OverriddenVisitor { case TruncSatUVecF16x8ToVecI16x8: case ConvertSVecI16x8ToVecF16x8: case ConvertUVecI16x8ToVecF16x8: + case PromoteLowVecF16x8ToVecF32x4: ret = 1; break; case InvalidUnary: diff --git a/src/literal.h b/src/literal.h index 80dda773410..4fcb2ee8a2e 100644 --- a/src/literal.h +++ b/src/literal.h @@ -723,6 +723,7 @@ class Literal { Literal truncSatZeroUToI32x4() const; Literal demoteZeroToF32x4() const; Literal promoteLowToF64x2() const; + Literal promoteLowF16x8ToF32x4() const; Literal truncSatToSI16x8() const; Literal truncSatToUI16x8() const; Literal convertSToF16x8() const; diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 1e910868ef8..d043735f315 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -1401,6 +1401,9 @@ struct PrintExpressionContents case ConvertUVecI16x8ToVecF16x8: o << "f16x8.convert_i16x8_u"; break; + case PromoteLowVecF16x8ToVecF32x4: + o << "f32x4.promote_low_f16x8"; + break; case InvalidUnary: WASM_UNREACHABLE("unvalid unary operator"); } diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index ba786b927a3..222d68698ae 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -4526,7 +4526,8 @@ Expression* TranslateToFuzzReader::makeUnary(Type type) { TruncSatSVecF16x8ToVecI16x8, TruncSatUVecF16x8ToVecI16x8, ConvertSVecI16x8ToVecF16x8, - ConvertUVecI16x8ToVecF16x8)), + ConvertUVecI16x8ToVecF16x8, + PromoteLowVecF16x8ToVecF32x4)), make(Type::v128)}); } WASM_UNREACHABLE("invalid value"); diff --git a/src/wasm-binary.h b/src/wasm-binary.h index c6fa761a2bd..386f495a905 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -1126,6 +1126,7 @@ enum ASTNodes { I16x8TruncSatF16x8U = 0x146, F16x8ConvertI16x8S = 0x147, F16x8ConvertI16x8U = 0x148, + F32x4PromoteLowF16x8 = 0x14b, // bulk memory opcodes diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index af9567ae762..e47559b597d 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -1164,6 +1164,8 @@ class ExpressionRunner : public OverriddenVisitor { return value.convertSToF16x8(); case ConvertUVecI16x8ToVecF16x8: return value.convertUToF16x8(); + case PromoteLowVecF16x8ToVecF32x4: + return value.promoteLowF16x8ToF32x4(); case InvalidUnary: WASM_UNREACHABLE("invalid unary op"); } diff --git a/src/wasm.h b/src/wasm.h index 941d759ce6b..5935cd47c66 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -251,6 +251,7 @@ enum UnaryOp { TruncSatUVecF16x8ToVecI16x8, ConvertSVecI16x8ToVecF16x8, ConvertUVecI16x8ToVecF16x8, + PromoteLowVecF16x8ToVecF32x4, InvalidUnary }; diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index 5c2a114af75..b3156fab0b3 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -2915,6 +2915,14 @@ Literal Literal::demoteZeroToF32x4() const { Literal Literal::promoteLowToF64x2() const { return extendF32(*this); } +Literal Literal::promoteLowF16x8ToF32x4() const { + auto lanes = getLanesF16x8(); + LaneArray<4> result; + for (size_t i = 0; i < 4; ++i) { + result[i] = lanes[i]; + } + return Literal(result); +} Literal Literal::swizzleI8x16(const Literal& other) const { auto lanes = getLanesUI8x16(); diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 8f3d7600457..da49533f55d 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -4474,6 +4474,8 @@ Result<> WasmBinaryReader::readInst() { return builder.makeUnary(ConvertSVecI16x8ToVecF16x8); case BinaryConsts::F16x8ConvertI16x8U: return builder.makeUnary(ConvertUVecI16x8ToVecF16x8); + case BinaryConsts::F32x4PromoteLowF16x8: + return builder.makeUnary(PromoteLowVecF16x8ToVecF32x4); case BinaryConsts::I8x16ExtractLaneS: return builder.makeSIMDExtract(ExtractLaneSVecI8x16, getLaneIndex(16)); diff --git a/src/wasm/wasm-stack.cpp b/src/wasm/wasm-stack.cpp index ea932dc175c..cb308271bc7 100644 --- a/src/wasm/wasm-stack.cpp +++ b/src/wasm/wasm-stack.cpp @@ -1459,6 +1459,10 @@ void BinaryInstWriter::visitUnary(Unary* curr) { o << static_cast(BinaryConsts::SIMDPrefix) << U32LEB(BinaryConsts::F16x8ConvertI16x8U); break; + case PromoteLowVecF16x8ToVecF32x4: + o << static_cast(BinaryConsts::SIMDPrefix) + << U32LEB(BinaryConsts::F32x4PromoteLowF16x8); + break; case InvalidUnary: WASM_UNREACHABLE("invalid unary op"); } diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index a6a7b292958..f8c394072dd 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -2380,6 +2380,7 @@ void FunctionValidator::visitUnary(Unary* curr) { case TruncSatZeroUVecF64x2ToVecI32x4: case DemoteZeroVecF64x2ToVecF32x4: case PromoteLowVecF32x4ToVecF64x2: + case PromoteLowVecF16x8ToVecF32x4: case RelaxedTruncSVecF32x4ToVecI32x4: case RelaxedTruncUVecF32x4ToVecI32x4: case RelaxedTruncZeroSVecF64x2ToVecI32x4: diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index 536f33ae59f..a77a25ce874 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -714,6 +714,7 @@ void Unary::finalize() { case TruncSatUVecF16x8ToVecI16x8: case ConvertSVecI16x8ToVecF16x8: case ConvertUVecI16x8ToVecF16x8: + case PromoteLowVecF16x8ToVecF32x4: type = Type::v128; break; case AnyTrueVec128: diff --git a/test/lit/basic/f16.wast b/test/lit/basic/f16.wast index c7240b25aab..d5e204d87f7 100644 --- a/test/lit/basic/f16.wast +++ b/test/lit/basic/f16.wast @@ -597,6 +597,22 @@ (local.get $0) ) ) + + ;; CHECK-TEXT: (func $f32x4.promote_low_f16x8 (type $1) (param $0 v128) (result v128) + ;; CHECK-TEXT-NEXT: (f32x4.promote_low_f16x8 + ;; CHECK-TEXT-NEXT: (local.get $0) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-BIN: (func $f32x4.promote_low_f16x8 (type $1) (param $0 v128) (result v128) + ;; CHECK-BIN-NEXT: (f32x4.promote_low_f16x8 + ;; CHECK-BIN-NEXT: (local.get $0) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + (func $f32x4.promote_low_f16x8 (param $0 v128) (result v128) + (f32x4.promote_low_f16x8 + (local.get $0) + ) + ) ) ;; CHECK-BIN-NODEBUG: (type $0 (func (param v128 v128) (result v128))) @@ -827,3 +843,9 @@ ;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) ;; CHECK-BIN-NODEBUG-NEXT: ) ;; CHECK-BIN-NODEBUG-NEXT: ) + +;; CHECK-BIN-NODEBUG: (func $32 (type $1) (param $0 v128) (result v128) +;; CHECK-BIN-NODEBUG-NEXT: (f32x4.promote_low_f16x8 +;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) diff --git a/test/spec/f16.wast b/test/spec/f16.wast index 4664e92f5cf..a36d5032d4f 100644 --- a/test/spec/f16.wast +++ b/test/spec/f16.wast @@ -38,6 +38,7 @@ (func (export "i16x8.trunc_sat_f16x8_u") (param $0 v128) (result v128) (i16x8.trunc_sat_f16x8_u (local.get $0))) (func (export "f16x8.convert_i16x8_s") (param $0 v128) (result v128) (f16x8.convert_i16x8_s (local.get $0))) (func (export "f16x8.convert_i16x8_u") (param $0 v128) (result v128) (f16x8.convert_i16x8_u (local.get $0))) + (func (export "f32x4.promote_low_f16x8") (param $0 v128) (result v128) (f32x4.promote_low_f16x8 (local.get $0))) ;; Multiple operation tests: (func (export "splat_replace") (result v128) (f16x8.replace_lane 0 (f16x8.splat (f32.const 1)) (f32.const 99)) ) @@ -247,3 +248,23 @@ (v128.const i16x8 0 1 -1 -32 0 0 0 0)) ;; 1 inf 65504 (v128.const i16x8 0 0x3c00 0x7c00 0x7bff 0 0 0 0)) + +(assert_return (invoke "f32x4.promote_low_f16x8" + ;; 1.0 -1.0 2.0 -2.0 0 0 0 0 + (v128.const i16x8 0x3c00 0xbc00 0x4000 0xc000 0 0 0 0)) + ;; 1.0 -1.0 2.0 -2.0 + (v128.const i32x4 0x3f800000 0xbf800000 0x40000000 0xc0000000)) + +;; Edge cases: Infinities, NaNs, Zeros +(assert_return (invoke "f32x4.promote_low_f16x8" + ;; inf -inf nan -0.0 0 0 0 0 + (v128.const i16x8 0x7c00 0xfc00 0x7e00 0x8000 0 0 0 0)) + ;; inf -inf nan -0.0 + (v128.const i32x4 0x7f800000 0xff800000 0x7fc00000 0x80000000)) + +;; Edge cases: Denormal +(assert_return (invoke "f32x4.promote_low_f16x8" + ;; denormal + (v128.const i16x8 0x0001 0 0 0 0 0 0 0)) + ;; 2^-24 + (v128.const i32x4 0x33800000 0 0 0)) From a6f85e5785fc51ab0eda7e9f7e465af9aafc7c54 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 7 Apr 2026 16:24:20 -0700 Subject: [PATCH 013/168] Fix optimized grouping of locals (#8577) In #8568 we optimized the grouping of locals in the binary writer to account for how types will be written given the enabled features. However, that change did not properly update the handling of scratch locals accordingly, leading to inconsistencies in the indices assigned to local types in different locations. Fix the problem by reverting the changes from #8568 and handling the mapping from IR types to written types at a lower level; specifically, create a new `TypeIndexMap` type that extends `InsertOrderedMap` but always applies `asWrittenGivenFeatures` to its keys. Use this new map type both for the `numLocalsByType` map and the `scratchLocals` map. --- src/wasm-stack.h | 37 ++++++++++++++++++++++++++++++++----- src/wasm/wasm-stack.cpp | 30 +++++++----------------------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/wasm-stack.h b/src/wasm-stack.h index 05898623187..6a6649d0aa4 100644 --- a/src/wasm-stack.h +++ b/src/wasm-stack.h @@ -94,7 +94,9 @@ class BinaryInstWriter : public OverriddenVisitor { BufferWithRandomAccess& o, Function* func, bool DWARF) - : parent(parent), o(o), func(func), DWARF(DWARF) {} + : parent(parent), o(o), func(func), DWARF(DWARF), + numLocalsByType(parent.getModule()->features), + scratchLocals(parent.getModule()->features) {} void visit(Expression* curr) { if (func) { @@ -144,20 +146,45 @@ class BinaryInstWriter : public OverriddenVisitor { std::vector breakStack; + // Map types to indices or counts, but transparently convert the key types to + // their written versions given the enabled features. + struct TypeIndexMap : private InsertOrderedMap { + FeatureSet feats; + + public: + using InsertOrderedMap::iterator; + using InsertOrderedMap::const_iterator; + using InsertOrderedMap::begin; + using InsertOrderedMap::end; + using InsertOrderedMap::size; + + TypeIndexMap(FeatureSet feats) + : InsertOrderedMap(), feats(feats) {} + + Index& operator[](Type type) { + return InsertOrderedMap::operator[](type.asWrittenGivenFeatures(feats)); + } + iterator find(Type type) { + return InsertOrderedMap::find(type.asWrittenGivenFeatures(feats)); + } + const_iterator find(Type type) const { + return InsertOrderedMap::find(type.asWrittenGivenFeatures(feats)); + } + }; + // The types of locals in the compact form, in order. std::vector localTypes; // type => number of locals of that type in the compact form - std::unordered_map numLocalsByType; + TypeIndexMap numLocalsByType; void noteLocalType(Type type, Index count = 1); - Index getNumLocalsForType(Type type); // Keeps track of the binary index of the scratch locals used to lower // tuple.extract. If there are multiple scratch locals of the same type, they // are contiguous and this map holds the index of the first. - InsertOrderedMap scratchLocals; + TypeIndexMap scratchLocals; // Return the type and number of required scratch locals. - InsertOrderedMap countScratchLocals(); + TypeIndexMap countScratchLocals(); // local.get, local.tee, and global.get expressions that will be followed by // tuple.extracts. We can optimize these by getting only the local for the diff --git a/src/wasm/wasm-stack.cpp b/src/wasm/wasm-stack.cpp index cb308271bc7..82708ca7f51 100644 --- a/src/wasm/wasm-stack.cpp +++ b/src/wasm/wasm-stack.cpp @@ -15,10 +15,8 @@ */ #include "wasm-stack.h" -#include "ir/find_all.h" #include "ir/properties.h" #include "wasm-binary.h" -#include "wasm-debug.h" namespace wasm { @@ -3245,11 +3243,11 @@ void BinaryInstWriter::mapLocalsAndEmitHeader() { // Map IR (local index, tuple index) pairs to binary local indices. Since // locals are grouped by type, start by calculating the base indices for each // type. - std::unordered_map nextFreeIndex; + TypeIndexMap nextFreeIndex(parent.getModule()->features); Index baseIndex = func->getVarIndexBase(); for (auto& type : localTypes) { nextFreeIndex[type] = baseIndex; - baseIndex += getNumLocalsForType(type); + baseIndex += numLocalsByType[type]; } // Map the IR index pairs to indices. @@ -3267,18 +3265,12 @@ void BinaryInstWriter::mapLocalsAndEmitHeader() { o << U32LEB(localTypes.size()); for (auto& localType : localTypes) { - o << U32LEB(getNumLocalsForType(localType)); + o << U32LEB(numLocalsByType[localType]); parent.writeType(localType); } } void BinaryInstWriter::noteLocalType(Type type, Index count) { - // Group locals by the type they will eventually be written out as. For - // example, we do not need to differentiate exact and inexact versions of the - // same reference type if custom descriptors is not enabled and the type will - // be written as inexact either way. - auto feats = parent.getModule()->features; - type = type.asWrittenGivenFeatures(feats); auto& num = numLocalsByType[type]; if (num == 0) { localTypes.push_back(type); @@ -3286,21 +3278,13 @@ void BinaryInstWriter::noteLocalType(Type type, Index count) { num += count; } -Index BinaryInstWriter::getNumLocalsForType(Type type) { - auto feats = parent.getModule()->features; - type = type.asWrittenGivenFeatures(feats); - if (auto it = numLocalsByType.find(type); it != numLocalsByType.end()) { - return it->second; - } - return 0; -} - -InsertOrderedMap BinaryInstWriter::countScratchLocals() { +BinaryInstWriter::TypeIndexMap BinaryInstWriter::countScratchLocals() { struct ScratchLocalFinder : PostWalker { BinaryInstWriter& parent; - InsertOrderedMap scratches; + TypeIndexMap scratches; - ScratchLocalFinder(BinaryInstWriter& parent) : parent(parent) {} + ScratchLocalFinder(BinaryInstWriter& parent) + : parent(parent), scratches(parent.parent.getModule()->features) {} void visitTupleExtract(TupleExtract* curr) { if (curr->type == Type::unreachable) { From 98ad697ba231abd5c63aef3ed9e18139c6b20ccc Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Tue, 7 Apr 2026 18:18:31 -0700 Subject: [PATCH 014/168] Make getters const in Module class (#8579) Split off from my [WIP for improving effects analysis for indirect calls](https://github.com/WebAssembly/binaryen/compare/indirect-effects-3?expand=1). These methods don't mutate the Module so they can be const. Also move getModuleElement into the anonymous namespace to prevent the name from leaking. Since these getters are now const, I also change some usages of Module&/Module* to const e.g. EffectsAnalyzer, since these usages also only need read-only access to the Module. --- src/ir/effects.h | 6 ++-- src/ir/intrinsics.h | 4 +-- src/ir/js-utils.h | 2 +- src/passes/Unsubtyping.cpp | 2 +- src/wasm.h | 38 ++++++++++++------------ src/wasm/wasm.cpp | 61 +++++++++++++++++++++----------------- 6 files changed, 59 insertions(+), 54 deletions(-) diff --git a/src/ir/effects.h b/src/ir/effects.h index 1e05ab0fb7c..e8ab4c8ef69 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -32,7 +32,7 @@ namespace wasm { class EffectAnalyzer { public: - EffectAnalyzer(const PassOptions& passOptions, Module& module) + EffectAnalyzer(const PassOptions& passOptions, const Module& module) : ignoreImplicitTraps(passOptions.ignoreImplicitTraps), trapsNeverHappen(passOptions.trapsNeverHappen), branchesOut(false), calls(false), readsMemory(false), writesMemory(false), @@ -46,7 +46,7 @@ class EffectAnalyzer { features(module.features) {} EffectAnalyzer(const PassOptions& passOptions, - Module& module, + const Module& module, Expression* ast) : EffectAnalyzer(passOptions, module) { walk(ast); @@ -136,7 +136,7 @@ class EffectAnalyzer { // more here.) bool hasReturnCallThrow : 1; - Module& module; + const Module& module; FeatureSet features; std::set localsRead; diff --git a/src/ir/intrinsics.h b/src/ir/intrinsics.h index a9b32f31f1b..2f6809c826b 100644 --- a/src/ir/intrinsics.h +++ b/src/ir/intrinsics.h @@ -33,10 +33,10 @@ namespace wasm { class Intrinsics { - Module& module; + const Module& module; public: - Intrinsics(Module& module) : module(module) {} + Intrinsics(const Module& module) : module(module) {} // Check if an instruction is the Binaryen call.without.effects intrinsic. // diff --git a/src/ir/js-utils.h b/src/ir/js-utils.h index 7591b4fe7e0..105dea499cf 100644 --- a/src/ir/js-utils.h +++ b/src/ir/js-utils.h @@ -45,7 +45,7 @@ inline bool hasPossibleJSPrototypeField(HeapType type) { // Calls flowIn and flowOut on all types that may flow in from or out to JS. template -void iterJSInterface(Module& wasm, In flowIn, Out flowOut) { +void iterJSInterface(const Module& wasm, In flowIn, Out flowOut) { // @binaryen.js.called functions are called from JS. Their parameters flow // in from JS and their results flow back out. for (auto f : Intrinsics(wasm).getJSCalledFunctions()) { diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index 5f01610adeb..f3165b8147c 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -645,7 +645,7 @@ struct Unsubtyping : Pass, Noter { } } - void analyzeJSInterface(Module& wasm) { + void analyzeJSInterface(const Module& wasm) { if (!wasm.features.hasCustomDescriptors()) { return; } diff --git a/src/wasm.h b/src/wasm.h index 5935cd47c66..a6a32bbeb98 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2700,30 +2700,30 @@ class Module { public: Module() = default; - Export* getExport(Name name); - Function* getFunction(Name name); - Table* getTable(Name name); - ElementSegment* getElementSegment(Name name); - Memory* getMemory(Name name); - DataSegment* getDataSegment(Name name); - Global* getGlobal(Name name); - Tag* getTag(Name name); - - Export* getExportOrNull(Name name); - Table* getTableOrNull(Name name); - Memory* getMemoryOrNull(Name name); - ElementSegment* getElementSegmentOrNull(Name name); - DataSegment* getDataSegmentOrNull(Name name); - Function* getFunctionOrNull(Name name); - Global* getGlobalOrNull(Name name); - Tag* getTagOrNull(Name name); + Export* getExport(Name name) const; + Function* getFunction(Name name) const; + Table* getTable(Name name) const; + ElementSegment* getElementSegment(Name name) const; + Memory* getMemory(Name name) const; + DataSegment* getDataSegment(Name name) const; + Global* getGlobal(Name name) const; + Tag* getTag(Name name) const; + + Export* getExportOrNull(Name name) const; + Table* getTableOrNull(Name name) const; + Memory* getMemoryOrNull(Name name) const; + ElementSegment* getElementSegmentOrNull(Name name) const; + DataSegment* getDataSegmentOrNull(Name name) const; + Function* getFunctionOrNull(Name name) const; + Global* getGlobalOrNull(Name name) const; + Tag* getTagOrNull(Name name) const; // get* methods that are generic over the kind, that is, items are identified // by their kind and their name. Otherwise, they are similar to the above // get* methods. These return items that can be imports. // TODO: Add methods for things that cannot be imports (segments). - Importable* getImport(ModuleItemKind kind, Name name); - Importable* getImportOrNull(ModuleItemKind kind, Name name); + Importable* getImport(ModuleItemKind kind, Name name) const; + Importable* getImportOrNull(ModuleItemKind kind, Name name) const; Export* addExport(Export* curr); Function* addFunction(Function* curr); diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index a77a25ce874..31de2de0362 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -22,6 +22,21 @@ namespace wasm { +namespace { + +template +const Value& getModuleElement(const std::unordered_map& m, + Name name, + std::string_view funcName) { + auto iter = m.find(name); + if (iter == m.end()) { + Fatal() << "Module::" << funcName << ": " << name << " does not exist"; + } + return iter->second; +} + +} // namespace + // shared constants Name RETURN_FLOW("*return:)*"); @@ -1706,45 +1721,35 @@ void Function::clearDebugInfo() { epilogLocation.reset(); } -template -typename Map::mapped_type& -getModuleElement(Map& m, Name name, std::string_view funcName) { - auto iter = m.find(name); - if (iter == m.end()) { - Fatal() << "Module::" << funcName << ": " << name << " does not exist"; - } - return iter->second; -} - -Export* Module::getExport(Name name) { +Export* Module::getExport(Name name) const { return getModuleElement(exportsMap, name, "getExport"); } -Function* Module::getFunction(Name name) { +Function* Module::getFunction(Name name) const { return getModuleElement(functionsMap, name, "getFunction"); } -Table* Module::getTable(Name name) { +Table* Module::getTable(Name name) const { return getModuleElement(tablesMap, name, "getTable"); } -ElementSegment* Module::getElementSegment(Name name) { +ElementSegment* Module::getElementSegment(Name name) const { return getModuleElement(elementSegmentsMap, name, "getElementSegment"); } -Memory* Module::getMemory(Name name) { +Memory* Module::getMemory(Name name) const { return getModuleElement(memoriesMap, name, "getMemory"); } -DataSegment* Module::getDataSegment(Name name) { +DataSegment* Module::getDataSegment(Name name) const { return getModuleElement(dataSegmentsMap, name, "getDataSegment"); } -Global* Module::getGlobal(Name name) { +Global* Module::getGlobal(Name name) const { return getModuleElement(globalsMap, name, "getGlobal"); } -Tag* Module::getTag(Name name) { +Tag* Module::getTag(Name name) const { return getModuleElement(tagsMap, name, "getTag"); } @@ -1757,39 +1762,39 @@ typename Map::mapped_type getModuleElementOrNull(Map& m, Name name) { return iter->second; } -Export* Module::getExportOrNull(Name name) { +Export* Module::getExportOrNull(Name name) const { return getModuleElementOrNull(exportsMap, name); } -Function* Module::getFunctionOrNull(Name name) { +Function* Module::getFunctionOrNull(Name name) const { return getModuleElementOrNull(functionsMap, name); } -Table* Module::getTableOrNull(Name name) { +Table* Module::getTableOrNull(Name name) const { return getModuleElementOrNull(tablesMap, name); } -ElementSegment* Module::getElementSegmentOrNull(Name name) { +ElementSegment* Module::getElementSegmentOrNull(Name name) const { return getModuleElementOrNull(elementSegmentsMap, name); } -Memory* Module::getMemoryOrNull(Name name) { +Memory* Module::getMemoryOrNull(Name name) const { return getModuleElementOrNull(memoriesMap, name); } -DataSegment* Module::getDataSegmentOrNull(Name name) { +DataSegment* Module::getDataSegmentOrNull(Name name) const { return getModuleElementOrNull(dataSegmentsMap, name); } -Global* Module::getGlobalOrNull(Name name) { +Global* Module::getGlobalOrNull(Name name) const { return getModuleElementOrNull(globalsMap, name); } -Tag* Module::getTagOrNull(Name name) { +Tag* Module::getTagOrNull(Name name) const { return getModuleElementOrNull(tagsMap, name); } -Importable* Module::getImport(ModuleItemKind kind, Name name) { +Importable* Module::getImport(ModuleItemKind kind, Name name) const { switch (kind) { case ModuleItemKind::Function: return getFunction(name); @@ -1810,7 +1815,7 @@ Importable* Module::getImport(ModuleItemKind kind, Name name) { WASM_UNREACHABLE("unexpected kind"); } -Importable* Module::getImportOrNull(ModuleItemKind kind, Name name) { +Importable* Module::getImportOrNull(ModuleItemKind kind, Name name) const { auto doReturn = [](Importable* importable) { return importable ? importable->imported() ? importable : nullptr : nullptr; }; From 15ad2b27c0d7c4e3c233b8f63b8d0685904bc03d Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 8 Apr 2026 07:26:49 -0700 Subject: [PATCH 015/168] Make everything 1.5% faster by calling leaf visitors immediately [NFC] (#8581) Continuing #8571, use a constexpr check to see when we are about to visit something that has no children. In that case we don't need to push a task for it and pop it later, we can just do the visit inline. --- src/wasm-traversal.h | 34 ++++++++++++++++-- test/gtest/CMakeLists.txt | 1 + test/gtest/leaves.cpp | 73 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 test/gtest/leaves.cpp diff --git a/src/wasm-traversal.h b/src/wasm-traversal.h index 9a5c5d83226..9717c053d8c 100644 --- a/src/wasm-traversal.h +++ b/src/wasm-traversal.h @@ -343,6 +343,27 @@ struct Walker : public VisitorType { Module* currModule = nullptr; // current module being processed }; +// Define which expression classes are leaves. We can handle them more +// optimally below. The accuracy of this list is tested in leaves.cpp. +template struct IsLeaf : std::false_type {}; + +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; +template<> struct IsLeaf : std::true_type {}; + // Walks in post-order, i.e., children first. When there isn't an obvious // order to operands, we follow them in order of execution. @@ -369,6 +390,10 @@ struct PostWalker : public Walker { // Note that even if this ends up being a runtime check, it should be faster // than pushing empty tasks, as the check is much faster than the push/pop/ // call, and a large number of our calls (most, perhaps) are not overridden. + // + // If we do *not* have an empty visitor, we can still optimize in the case + // of a leaf: leaves have no children, so we can just call doVisit* rather + // than push that task, pop it later, and call that. #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 11 #define DELEGATE_START(id) \ if (&SubType::visit##id != \ @@ -377,17 +402,22 @@ struct PostWalker : public Walker { self->pushTask(SubType::doVisit##id, currp); \ } \ [[maybe_unused]] auto* cast = curr->cast(); -#else +#else // constexpr #define DELEGATE_START(id) \ if constexpr (&SubType::visit##id != \ &Visitor::visit##id || \ &SubType::doVisit##id != \ &Walker::doVisit##id) { \ + if constexpr (IsLeaf::value && \ + &SubType::scan == &PostWalker::scan) { \ + SubType::doVisit##id(self, currp); \ + return; \ + } \ self->pushTask(SubType::doVisit##id, currp); \ } \ [[maybe_unused]] auto* cast = curr->cast(); -#endif +#endif // constexpr #define DELEGATE_GET_FIELD(id, field) cast->field diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt index 058766f58e4..41d16f28e92 100644 --- a/test/gtest/CMakeLists.txt +++ b/test/gtest/CMakeLists.txt @@ -12,6 +12,7 @@ set(unittest_SOURCES dataflow.cpp dfa_minimization.cpp disjoint_sets.cpp + leaves.cpp glbs.cpp interpreter.cpp intervals.cpp diff --git a/test/gtest/leaves.cpp b/test/gtest/leaves.cpp new file mode 100644 index 00000000000..2457f0d35f7 --- /dev/null +++ b/test/gtest/leaves.cpp @@ -0,0 +1,73 @@ +#include "wasm-traversal.h" +#include "wasm.h" + +#include "gtest/gtest.h" + +using LeavesTest = ::testing::Test; + +using namespace wasm; + +TEST_F(LeavesTest, Manual) { + // Verify some interesting cases manually. + + // LocalGet is a leaf. + EXPECT_TRUE(IsLeaf::value); + // GlobalSet is not a leaf due to a child. + EXPECT_FALSE(IsLeaf::value); + // Return is not a leaf due to an optional child. + EXPECT_FALSE(IsLeaf::value); + // Call is not a leaf due to a vector of children. + EXPECT_FALSE(IsLeaf::value); +} + +TEST_F(LeavesTest, Automatic) { + // Verify them all automatically. + + // Count total expression classes and total with children. + size_t total = 0, totalWithChildren = 0; + +#define DELEGATE_FIELD_CASE_START(id) \ + { \ + bool hasChildren = false; + +#define DELEGATE_FIELD_CHILD(id, field) hasChildren = true; + +#define DELEGATE_FIELD_OPTIONAL_CHILD(id, field) hasChildren = true; + +#define DELEGATE_FIELD_CHILD_VECTOR(id, field) hasChildren = true; + + // Verify that IsLeaf has the right value. +#define DELEGATE_FIELD_CASE_END(id) \ + EXPECT_EQ(IsLeaf::value, !hasChildren); \ + total++; \ + if (hasChildren) { \ + totalWithChildren++; \ + } \ + } + +#define DELEGATE_FIELD_INT(id, field) +#define DELEGATE_FIELD_LITERAL(id, field) +#define DELEGATE_FIELD_NAME(id, field) +#define DELEGATE_FIELD_SCOPE_NAME_DEF(id, field) +#define DELEGATE_FIELD_SCOPE_NAME_USE(id, field) +#define DELEGATE_FIELD_TYPE(id, field) +#define DELEGATE_FIELD_HEAPTYPE(id, field) +#define DELEGATE_FIELD_ADDRESS(id, field) +#define DELEGATE_FIELD_INT_ARRAY(id, field) +#define DELEGATE_FIELD_INT_VECTOR(id, field) +#define DELEGATE_FIELD_NAME_VECTOR(id, field) +#define DELEGATE_FIELD_NAME_USE_VECTOR(id, field) +#define DELEGATE_FIELD_TYPE_VECTOR(id, field) +#define DELEGATE_FIELD_SCOPE_NAME_USE_VECTOR(id, field) + +#define DELEGATE_FIELD_MAIN_START +#define DELEGATE_FIELD_MAIN_END + +#include "wasm-delegations-fields.def" + + // Not all have children (this just verifies the macros are actually doing + // something). + EXPECT_LT(totalWithChildren, total); + EXPECT_GT(totalWithChildren, 0); + EXPECT_GT(total, 0); +} From b9a9afb22dabcc6d001cdf67a484843e4601dd7c Mon Sep 17 00:00:00 2001 From: Spotandjake <40705786+spotandjake@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:17:02 -0400 Subject: [PATCH 016/168] [JS & C API] Rename MemorySegment functions to DataSegment (#8576) fixes #8537 --- CHANGELOG.md | 9 +++++++++ src/binaryen-c.cpp | 13 ++++++------- src/binaryen-c.h | 15 +++++++-------- src/js/binaryen.js-post.js | 18 +++++++++--------- test/binaryen.js/kitchen-sink.js | 4 ++-- test/example/c-api-kitchen-sink.c | 10 +++++----- 6 files changed, 38 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f39a79773e4..458fefd10bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,15 @@ full changeset diff at the end of each section. Current Trunk ------------- + - Rename `MemorySegment` functions to `DataSegment` in the c and js apis + - Rename `BinaryenGetNumMemorySegments` to `BinaryenGetNumDataSegments` in c api. + - Rename `BinaryenGetMemorySegmentByteOffset` to `BinaryenGetDataSegmentByteOffset` in c api. + - Rename `BinaryenGetMemorySegmentByteLength` to `BinaryenGetDataSegmentByteLength` in c api. + - Rename `BinaryenGetMemorySegmentPassive` to `BinaryenGetDataSegmentPassive` in c api. + - Rename `BinaryenCopyMemorySegmentData` to `BinaryenCopyDataSegmentData` in c api. + - Rename `module.getNumMemorySegments` to `module.getNumDataSegments` in js api. + - Rename `module.getMemorySegmentInfo` to `module.getDataSegmentInfo` in js api. + v129 ---- diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index 8a87bdb917a..133d17e92b4 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -5492,7 +5492,7 @@ void BinaryenSetMemory(BinaryenModuleRef module, // Memory segments -uint32_t BinaryenGetNumMemorySegments(BinaryenModuleRef module) { +uint32_t BinaryenGetNumDataSegments(BinaryenModuleRef module) { return ((Module*)module)->dataSegments.size(); } BinaryenDataSegmentRef BinaryenGetDataSegment(BinaryenModuleRef module, @@ -5510,8 +5510,8 @@ BinaryenDataSegmentRef BinaryenGetDataSegmentByIndex(BinaryenModuleRef module, const char* BinaryenDataSegmentGetName(BinaryenDataSegmentRef segment) { return ((DataSegment*)segment)->name.str.data(); } -uint32_t BinaryenGetMemorySegmentByteOffset(BinaryenModuleRef module, - BinaryenDataSegmentRef segment) { +uint32_t BinaryenGetDataSegmentByteOffset(BinaryenModuleRef module, + BinaryenDataSegmentRef segment) { auto* wasm = (Module*)module; auto globalOffset = [&](const Expression* const& expr, @@ -5628,14 +5628,13 @@ bool BinaryenMemoryIs64(BinaryenModuleRef module, const char* name) { } return memory->is64(); } -size_t BinaryenGetMemorySegmentByteLength(BinaryenDataSegmentRef segment) { +size_t BinaryenGetDataSegmentByteLength(BinaryenDataSegmentRef segment) { return ((DataSegment*)segment)->data.size(); } -bool BinaryenGetMemorySegmentPassive(BinaryenDataSegmentRef segment) { +bool BinaryenGetDataSegmentPassive(BinaryenDataSegmentRef segment) { return ((DataSegment*)segment)->isPassive; } -void BinaryenCopyMemorySegmentData(BinaryenDataSegmentRef segment, - char* buffer) { +void BinaryenCopyDataSegmentData(BinaryenDataSegmentRef segment, char* buffer) { std::copy(((DataSegment*)segment)->data.cbegin(), ((DataSegment*)segment)->data.cend(), buffer); diff --git a/src/binaryen-c.h b/src/binaryen-c.h index a0c66ff9b7e..24e0fe3071a 100644 --- a/src/binaryen-c.h +++ b/src/binaryen-c.h @@ -3004,25 +3004,24 @@ BINARYEN_API bool BinaryenMemoryIsShared(BinaryenModuleRef module, BINARYEN_API bool BinaryenMemoryIs64(BinaryenModuleRef module, const char* name); -// Memory segments. Query utilities. +// Data segments. Query utilities. BINARYEN_REF(DataSegment); -BINARYEN_API uint32_t BinaryenGetNumMemorySegments(BinaryenModuleRef module); +BINARYEN_API uint32_t BinaryenGetNumDataSegments(BinaryenModuleRef module); BINARYEN_API BinaryenDataSegmentRef BinaryenGetDataSegment(BinaryenModuleRef module, const char* segmentName); BINARYEN_API BinaryenDataSegmentRef BinaryenGetDataSegmentByIndex(BinaryenModuleRef module, BinaryenIndex index); BINARYEN_API const char* BinaryenDataSegmentGetName(BinaryenDataSegmentRef segment); -BINARYEN_API uint32_t BinaryenGetMemorySegmentByteOffset( +BINARYEN_API uint32_t BinaryenGetDataSegmentByteOffset( BinaryenModuleRef module, BinaryenDataSegmentRef segment); BINARYEN_API size_t -BinaryenGetMemorySegmentByteLength(BinaryenDataSegmentRef segment); -BINARYEN_API bool -BinaryenGetMemorySegmentPassive(BinaryenDataSegmentRef segment); -BINARYEN_API void BinaryenCopyMemorySegmentData(BinaryenDataSegmentRef segment, - char* buffer); +BinaryenGetDataSegmentByteLength(BinaryenDataSegmentRef segment); +BINARYEN_API bool BinaryenGetDataSegmentPassive(BinaryenDataSegmentRef segment); +BINARYEN_API void BinaryenCopyDataSegmentData(BinaryenDataSegmentRef segment, + char* buffer); BINARYEN_API void BinaryenAddDataSegment(BinaryenModuleRef module, const char* segmentName, const char* memoryName, diff --git a/src/js/binaryen.js-post.js b/src/js/binaryen.js-post.js index d3ac4a2eafa..0044b5e7c36 100644 --- a/src/js/binaryen.js-post.js +++ b/src/js/binaryen.js-post.js @@ -2769,8 +2769,8 @@ function wrapModule(module, self = {}) { return memoryInfo; }); }; - self['getNumMemorySegments'] = function() { - return Module['_BinaryenGetNumMemorySegments'](module); + self['getNumDataSegments'] = function() { + return Module['_BinaryenGetNumDataSegments'](module); }; /** * Gets the data segment with the given name. @@ -2795,9 +2795,9 @@ function wrapModule(module, self = {}) { return Module['_BinaryenGetDataSegmentByIndex'](module, index); }; /** - * Queries information about a memory segment. + * Queries information about a data segment. * - * @param {number} segment - A MemorySegmentRef referring to the memory segment to get information about. + * @param {number} segment - A DataSegmentRef referring to the data segment to get information about. * @returns {Object} An object containing the following fields: * - `name`: The name of the segment. * - `offset`: If the segment is active, the offset expression of the segment. Otherwise, `null`. @@ -2806,19 +2806,19 @@ function wrapModule(module, self = {}) { * * @throws If the given segment reference is invalid. */ - self['getMemorySegmentInfo'] = function(segment) { - const passive = Boolean(Module['_BinaryenGetMemorySegmentPassive'](segment)); + self['getDataSegmentInfo'] = function(segment) { + const passive = Boolean(Module['_BinaryenGetDataSegmentPassive'](segment)); let offset = null; if (!passive) { - offset = Module['_BinaryenGetMemorySegmentByteOffset'](module, segment); + offset = Module['_BinaryenGetDataSegmentByteOffset'](module, segment); } return { 'name': UTF8ToString(Module['_BinaryenDataSegmentGetName'](segment)), 'offset': offset, 'data': (function(){ - const size = Module['_BinaryenGetMemorySegmentByteLength'](segment); + const size = Module['_BinaryenGetDataSegmentByteLength'](segment); const ptr = _malloc(size); - Module['_BinaryenCopyMemorySegmentData'](segment, ptr); + Module['_BinaryenCopyDataSegmentData'](segment, ptr); const res = new Uint8Array(size); res.set(HEAP8.subarray(ptr, ptr + size)); _free(ptr); diff --git a/test/binaryen.js/kitchen-sink.js b/test/binaryen.js/kitchen-sink.js index 7e2b9897dbb..89753393591 100644 --- a/test/binaryen.js/kitchen-sink.js +++ b/test/binaryen.js/kitchen-sink.js @@ -1195,9 +1195,9 @@ function test_for_each() { ], false); assert(module.getDataSegment(expected_names[0]) !== 0); assert(module.getDataSegment("NonExistantSegment") === 0); - for (i = 0; i < module.getNumMemorySegments(); i++) { + for (i = 0; i < module.getNumDataSegments(); i++) { var segment = module.getDataSegmentByIndex(i); - var info = module.getMemorySegmentInfo(segment); + var info = module.getDataSegmentInfo(segment); assert(expected_names[i] === info.name); assert(expected_offsets[i] === info.offset); var data8 = new Uint8Array(info.data); diff --git a/test/example/c-api-kitchen-sink.c b/test/example/c-api-kitchen-sink.c index 726ff00bf55..225e5feef58 100644 --- a/test/example/c-api-kitchen-sink.c +++ b/test/example/c-api-kitchen-sink.c @@ -2021,16 +2021,16 @@ void test_for_each() { makeInt32(module, expected_offsets[1])); assert(BinaryenGetDataSegment(module, segmentNames[0]) != NULL); assert(BinaryenGetDataSegment(module, "NonExistentSegment") == NULL); - for (i = 0; i < BinaryenGetNumMemorySegments(module); i++) { + for (i = 0; i < BinaryenGetNumDataSegments(module); i++) { char out[15] = {}; BinaryenDataSegmentRef segment = BinaryenGetDataSegmentByIndex(module, i); assert(segment != NULL); assert(BinaryenDataSegmentGetName(segment) != NULL); - assert(BinaryenGetMemorySegmentByteOffset(module, segment) == + assert(BinaryenGetDataSegmentByteOffset(module, segment) == expected_offsets[i]); - assert(BinaryenGetMemorySegmentByteLength(segment) == segmentSizes[i]); - assert(BinaryenGetMemorySegmentPassive(segment) == segmentPassives[i]); - BinaryenCopyMemorySegmentData(segment, out); + assert(BinaryenGetDataSegmentByteLength(segment) == segmentSizes[i]); + assert(BinaryenGetDataSegmentPassive(segment) == segmentPassives[i]); + BinaryenCopyDataSegmentData(segment, out); assert(0 == strcmp(segmentDatas[i], out)); } } From 68ea908068659dafc2dd2932cb59ab8ce6b061fb Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Wed, 8 Apr 2026 15:11:58 -0700 Subject: [PATCH 017/168] [ci] Use emsdk-setup github action (#8584) See https://github.com/emscripten-core/setup-emsdk --- .github/workflows/ci.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05e9b5be3c4..600768b9cdb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -357,23 +357,20 @@ jobs: - name: install ninja run: sudo apt-get install ninja-build - name: emsdk install - run: | - mkdir $HOME/emsdk - git clone --depth 1 https://github.com/emscripten-core/emsdk.git $HOME/emsdk - $HOME/emsdk/emsdk update-tags - $HOME/emsdk/emsdk install tot - $HOME/emsdk/emsdk activate tot + uses: emscripten-core/setup-emsdk@v15 + with: + version: tot - name: override emscripten repository if: ${{ env.EMSCRIPTEN_REPO != '' }} run: | - $HOME/emsdk/emsdk install emscripten-main-64bit \ + $EMSDK/emsdk install emscripten-main-64bit \ --override-repository emscripten-main-64bit@$EMSCRIPTEN_REPO - $HOME/emsdk/emsdk activate emscripten-main-64bit + $EMSDK/emsdk activate emscripten-main-64bit - name: update path - run: echo "PATH=$PATH:$HOME/emsdk" >> $GITHUB_ENV + run: echo "PATH=$PATH:$EMSDK" >> $GITHUB_ENV - name: emcc-tests run: | - source $HOME/emsdk/emsdk_env.sh + source $EMSDK/emsdk_env.sh ./scripts/emcc-tests.sh # Windows + gcc needs work before the tests will run, so just test the compile From 1527ce09fbade0f13a98ac9dccc5364886a37929 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 07:46:43 -0700 Subject: [PATCH 018/168] Move a v8 fuzzer flag to a more prominent place (#8582) This is basically NFC but in the new place more code paths end up using the flag, so this may increase our coverage slightly. --- scripts/fuzz_opt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 784476d89c2..2f32eac59fb 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -688,6 +688,10 @@ def get_v8_extra_flags(): if random.random() < 0.5: flags += ['--wasm-assert-types'] + # Some other options make sense to use sometimes. + if random.random() < 0.5: + flags += ['--no-wasm-generic-wrapper'] + return flags @@ -862,8 +866,6 @@ class D8Turboshaft(D8): def run(self, wasm): flags = ['--no-liftoff'] - if random.random() < 0.5: - flags += ['--no-wasm-generic-wrapper'] return super().run(wasm, extra_d8_flags=flags) class Wasm2C: From d918f9c2743c24e764f56d79c3ea2c00572b95dc Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 09:03:36 -0700 Subject: [PATCH 019/168] [Stack Switching] wasm-ctor-eval: stop on serializing continuations to globals (#8585) Continuations cannot be serialized. --- src/tools/wasm-ctor-eval.cpp | 7 +++ test/lit/ctor-eval/cont-noserial.wast | 73 +++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index 8cccf8c7ea2..93e8d4ae51f 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -213,6 +213,13 @@ class EvallingModuleRunner : public ModuleRunnerBase { return ModuleRunnerBase::visitGlobalGet(curr); } + Flow visitGlobalSet(GlobalSet* curr) { + if (curr->value->type.isContinuation()) { + throw FailToEvalException("cannot serialize continuations to globals"); + } + return ModuleRunnerBase::visitGlobalSet(curr); + } + Flow visitTableGet(TableGet* curr) { // We support tableLoad, below, so that call_indirect works (it calls it // internally), but we want to disable table.get for now. diff --git a/test/lit/ctor-eval/cont-noserial.wast b/test/lit/ctor-eval/cont-noserial.wast index e8070dbb56f..a5e2541053d 100644 --- a/test/lit/ctor-eval/cont-noserial.wast +++ b/test/lit/ctor-eval/cont-noserial.wast @@ -351,3 +351,76 @@ (func $export (export "export") ) ) + +;; Now the problem happens when we write a continuation to a global, which we +;; cannot do. Nothing can be optimized here. +(module + ;; CHECK: (type $func (func)) + ;; NOKEEP: (type $func (func)) + (type $func (func)) + ;; CHECK: (type $cont (cont $func)) + ;; NOKEEP: (type $cont (cont $func)) + (type $cont (cont $func)) + + ;; CHECK: (type $2 (func (result (ref null $cont)))) + + ;; CHECK: (global $global (mut (ref null $cont)) (ref.null nocont)) + ;; NOKEEP: (type $2 (func (result (ref null $cont)))) + + ;; NOKEEP: (global $global (mut (ref null $cont)) (ref.null nocont)) + (global $global (mut (ref null $cont)) (ref.null $cont)) + + ;; CHECK: (elem declare func $func) + + ;; CHECK: (export "read" (func $read)) + + ;; CHECK: (export "test" (func $test)) + ;; NOKEEP: (elem declare func $func) + + ;; NOKEEP: (export "read" (func $read)) + + ;; NOKEEP: (export "test" (func $test)) + (export "test" (func $test)) + + ;; CHECK: (func $func (type $func) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; NOKEEP: (func $func (type $func) + ;; NOKEEP-NEXT: (nop) + ;; NOKEEP-NEXT: ) + (func $func + ) + + ;; CHECK: (func $test (type $func) + ;; CHECK-NEXT: (global.set $global + ;; CHECK-NEXT: (cont.new $cont + ;; CHECK-NEXT: (ref.func $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; NOKEEP: (func $test (type $func) + ;; NOKEEP-NEXT: (global.set $global + ;; NOKEEP-NEXT: (cont.new $cont + ;; NOKEEP-NEXT: (ref.func $func) + ;; NOKEEP-NEXT: ) + ;; NOKEEP-NEXT: ) + ;; NOKEEP-NEXT: ) + (func $test + (global.set $global + (cont.new $cont + (ref.func $func) + ) + ) + ) + + ;; CHECK: (func $read (type $2) (result (ref null $cont)) + ;; CHECK-NEXT: (global.get $global) + ;; CHECK-NEXT: ) + ;; NOKEEP: (func $read (type $2) (result (ref null $cont)) + ;; NOKEEP-NEXT: (global.get $global) + ;; NOKEEP-NEXT: ) + (func $read (export "read") (result (ref null $cont)) + (global.get $global) + ) +) + From f7b08ed50ad5aec56584d25478be34b475a488cb Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 11:16:00 -0700 Subject: [PATCH 020/168] [NFC] Move fuzzer VMs out of CompareVMs (#8587) Diff without whitespace is trivial. --- scripts/fuzz_opt.py | 340 +++++++++++++++++++++++--------------------- 1 file changed, 174 insertions(+), 166 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 2f32eac59fb..83d351d67f1 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -786,6 +786,180 @@ class FuzzExec(TestCaseHandler): def handle_pair(self, input, before_wasm, after_wasm, opts): run([in_bin('wasm-opt'), before_wasm] + opts + ['--fuzz-exec']) +# VMs + + +class BinaryenInterpreter: + name = 'binaryen interpreter' + + def run(self, wasm): + output = run_bynterp(wasm, ['--fuzz-exec-before']) + if output != IGNORE: + calls = output.count(FUZZ_EXEC_EXPORT_PREFIX) + errors = output.count(TRAP_PREFIX) + output.count(HOST_LIMIT_PREFIX) + if errors > calls / 2: + # A significant amount of execution on this testcase + # simply trapped, and was not very useful, so mark it + # as ignored. Ideally the fuzzer testcases would be + # improved to reduce this number. + # + # Note that we don't change output=IGNORE as there may + # still be useful testing here (up to 50%), so we only + # note that this is a mostly-ignored run, but we do not + # ignore the parts that are useful. + # + # Note that we set amount to 0.5 because we are run both + # on the before wasm and the after wasm. Those will be + # in sync (because the optimizer does not remove traps) + # and so by setting 0.5 we only increment by 1 for the + # entire iteration. + note_ignored_vm_run('too many errors vs calls', + extra_text=f' ({calls} calls, {errors} errors)', + amount=0.5) + return output + + def can_run(self, wasm): + return True + + def can_compare_to_self(self): + return True + + def can_compare_to_other(self, other): + return True + + +class D8: + name = 'd8' + + def run(self, wasm, extra_d8_flags=[]): + return run_vm([shared.V8, get_fuzz_shell_js()] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm]) + + def can_run(self, wasm): + return all_disallowed(DISALLOWED_FEATURES_IN_V8) + + def can_compare_to_self(self): + # With nans, VM differences can confuse us, so only very simple VMs + # can compare to themselves after opts in that case. + return not NANS + + def can_compare_to_other(self, other): + # Relaxed SIMD allows different behavior between VMs, so only + # allow comparisons to other d8 variants if it is enabled. + if not all_disallowed(['relaxed-simd']) and not other.name.startswith('d8'): + return False + + # If not legalized, the JS will fail immediately, so no point to + # compare to others. + return self.can_compare_to_self() and LEGALIZE + + +class D8Liftoff(D8): + name = 'd8_liftoff' + + def run(self, wasm): + return super().run(wasm, extra_d8_flags=V8_LIFTOFF_ARGS) + + +class D8Turboshaft(D8): + name = 'd8_turboshaft' + + def run(self, wasm): + flags = ['--no-liftoff'] + return super().run(wasm, extra_d8_flags=flags) + + +class Wasm2C: + name = 'wasm2c' + + def __init__(self): + # look for wabt in the path. if it's not here, don't run wasm2c + try: + wabt_bin = shared.which('wasm2c') + wabt_root = os.path.dirname(os.path.dirname(wabt_bin)) + self.wasm2c_dir = os.path.join(wabt_root, 'wasm2c') + if not os.path.isdir(self.wasm2c_dir): + print('wabt found, but not wasm2c support dir') + self.wasm2c_dir = None + except Exception as e: + print('warning: no wabt found:', e) + self.wasm2c_dir = None + + def can_run(self, wasm): + if self.wasm2c_dir is None: + return False + # if we legalize for JS, the ABI is not what C wants + if LEGALIZE: + return False + # relatively slow, so run it less frequently + if random.random() < 0.5: + return False + # wasm2c doesn't support most features + return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'custom-descriptors', 'relaxed-atomics']) + + def run(self, wasm): + run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) + run(['wasm2c', wasm, '-o', 'wasm.c']) + compile_cmd = ['clang', 'main.c', 'wasm.c', os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), '-I' + self.wasm2c_dir, '-lm', '-Werror'] + run(compile_cmd) + return run_vm(['./a.out']) + + def can_compare_to_self(self): + # The binaryen optimizer changes NaNs in the ways that wasm + # expects, but that's not quite what C has + return not NANS + + def can_compare_to_other(self, other): + # C won't trap on OOB, and NaNs can differ from wasm VMs + return not OOB and not NANS + + +class Wasm2C2Wasm(Wasm2C): + name = 'wasm2c2wasm' + + def __init__(self): + super().__init__() + + self.has_emcc = shared.which('emcc') is not None + + def run(self, wasm): + run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) + run(['wasm2c', wasm, '-o', 'wasm.c']) + compile_cmd = ['emcc', 'main.c', 'wasm.c', + os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), + '-I' + self.wasm2c_dir, + '-lm', + '-s', 'ENVIRONMENT=shell', + '-s', 'ALLOW_MEMORY_GROWTH'] + # disable the signal handler: emcc looks like unix, but wasm has + # no signals + compile_cmd += ['-DWASM_RT_MEMCHECK_SIGNAL_HANDLER=0'] + if random.random() < 0.5: + compile_cmd += ['-O' + str(random.randint(1, 3))] + elif random.random() < 0.5: + if random.random() < 0.5: + compile_cmd += ['-Os'] + else: + compile_cmd += ['-Oz'] + # avoid pass-debug on the emcc invocation itself (which runs + # binaryen to optimize the wasm), as the wasm here can be very + # large and it isn't what we are focused on testing here + with no_pass_debug(): + run(compile_cmd) + return run_d8_js(abspath('a.out.js')) + + def can_run(self, wasm): + # quite slow (more steps), so run it less frequently + if random.random() < 0.8: + return False + # prefer not to run if the wasm is very large, as it can OOM + # the JS engine. + return super().can_run(wasm) and self.has_emcc and \ + os.path.getsize(wasm) <= INPUT_SIZE_MEAN + + def can_compare_to_other(self, other): + # NaNs can differ from wasm VMs + return not NANS + class CompareVMs(TestCaseHandler): frequency = 1 @@ -793,172 +967,6 @@ class CompareVMs(TestCaseHandler): def __init__(self): super().__init__() - class BinaryenInterpreter: - name = 'binaryen interpreter' - - def run(self, wasm): - output = run_bynterp(wasm, ['--fuzz-exec-before']) - if output != IGNORE: - calls = output.count(FUZZ_EXEC_EXPORT_PREFIX) - errors = output.count(TRAP_PREFIX) + output.count(HOST_LIMIT_PREFIX) - if errors > calls / 2: - # A significant amount of execution on this testcase - # simply trapped, and was not very useful, so mark it - # as ignored. Ideally the fuzzer testcases would be - # improved to reduce this number. - # - # Note that we don't change output=IGNORE as there may - # still be useful testing here (up to 50%), so we only - # note that this is a mostly-ignored run, but we do not - # ignore the parts that are useful. - # - # Note that we set amount to 0.5 because we are run both - # on the before wasm and the after wasm. Those will be - # in sync (because the optimizer does not remove traps) - # and so by setting 0.5 we only increment by 1 for the - # entire iteration. - note_ignored_vm_run('too many errors vs calls', - extra_text=f' ({calls} calls, {errors} errors)', - amount=0.5) - return output - - def can_run(self, wasm): - return True - - def can_compare_to_self(self): - return True - - def can_compare_to_other(self, other): - return True - - class D8: - name = 'd8' - - def run(self, wasm, extra_d8_flags=[]): - return run_vm([shared.V8, get_fuzz_shell_js()] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm]) - - def can_run(self, wasm): - return all_disallowed(DISALLOWED_FEATURES_IN_V8) - - def can_compare_to_self(self): - # With nans, VM differences can confuse us, so only very simple VMs - # can compare to themselves after opts in that case. - return not NANS - - def can_compare_to_other(self, other): - # Relaxed SIMD allows different behavior between VMs, so only - # allow comparisons to other d8 variants if it is enabled. - if not all_disallowed(['relaxed-simd']) and not other.name.startswith('d8'): - return False - - # If not legalized, the JS will fail immediately, so no point to - # compare to others. - return self.can_compare_to_self() and LEGALIZE - - class D8Liftoff(D8): - name = 'd8_liftoff' - - def run(self, wasm): - return super().run(wasm, extra_d8_flags=V8_LIFTOFF_ARGS) - - class D8Turboshaft(D8): - name = 'd8_turboshaft' - - def run(self, wasm): - flags = ['--no-liftoff'] - return super().run(wasm, extra_d8_flags=flags) - - class Wasm2C: - name = 'wasm2c' - - def __init__(self): - # look for wabt in the path. if it's not here, don't run wasm2c - try: - wabt_bin = shared.which('wasm2c') - wabt_root = os.path.dirname(os.path.dirname(wabt_bin)) - self.wasm2c_dir = os.path.join(wabt_root, 'wasm2c') - if not os.path.isdir(self.wasm2c_dir): - print('wabt found, but not wasm2c support dir') - self.wasm2c_dir = None - except Exception as e: - print('warning: no wabt found:', e) - self.wasm2c_dir = None - - def can_run(self, wasm): - if self.wasm2c_dir is None: - return False - # if we legalize for JS, the ABI is not what C wants - if LEGALIZE: - return False - # relatively slow, so run it less frequently - if random.random() < 0.5: - return False - # wasm2c doesn't support most features - return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'custom-descriptors', 'relaxed-atomics']) - - def run(self, wasm): - run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) - run(['wasm2c', wasm, '-o', 'wasm.c']) - compile_cmd = ['clang', 'main.c', 'wasm.c', os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), '-I' + self.wasm2c_dir, '-lm', '-Werror'] - run(compile_cmd) - return run_vm(['./a.out']) - - def can_compare_to_self(self): - # The binaryen optimizer changes NaNs in the ways that wasm - # expects, but that's not quite what C has - return not NANS - - def can_compare_to_other(self, other): - # C won't trap on OOB, and NaNs can differ from wasm VMs - return not OOB and not NANS - - class Wasm2C2Wasm(Wasm2C): - name = 'wasm2c2wasm' - - def __init__(self): - super().__init__() - - self.has_emcc = shared.which('emcc') is not None - - def run(self, wasm): - run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) - run(['wasm2c', wasm, '-o', 'wasm.c']) - compile_cmd = ['emcc', 'main.c', 'wasm.c', - os.path.join(self.wasm2c_dir, 'wasm-rt-impl.c'), - '-I' + self.wasm2c_dir, - '-lm', - '-s', 'ENVIRONMENT=shell', - '-s', 'ALLOW_MEMORY_GROWTH'] - # disable the signal handler: emcc looks like unix, but wasm has - # no signals - compile_cmd += ['-DWASM_RT_MEMCHECK_SIGNAL_HANDLER=0'] - if random.random() < 0.5: - compile_cmd += ['-O' + str(random.randint(1, 3))] - elif random.random() < 0.5: - if random.random() < 0.5: - compile_cmd += ['-Os'] - else: - compile_cmd += ['-Oz'] - # avoid pass-debug on the emcc invocation itself (which runs - # binaryen to optimize the wasm), as the wasm here can be very - # large and it isn't what we are focused on testing here - with no_pass_debug(): - run(compile_cmd) - return run_d8_js(abspath('a.out.js')) - - def can_run(self, wasm): - # quite slow (more steps), so run it less frequently - if random.random() < 0.8: - return False - # prefer not to run if the wasm is very large, as it can OOM - # the JS engine. - return super().can_run(wasm) and self.has_emcc and \ - os.path.getsize(wasm) <= INPUT_SIZE_MEAN - - def can_compare_to_other(self, other): - # NaNs can differ from wasm VMs - return not NANS - # the binaryen interpreter is specifically useful for various things self.bynterpreter = BinaryenInterpreter() From eb6c79d29ec1a5133f52fed122541c1bdb23388d Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 12:05:34 -0700 Subject: [PATCH 021/168] [NFC] Fuzzer: Add a run_js() method (#8588) This refactors the code a bit to allow the VM classes in the fuzzer to run JS. The function also allows running it in a checked (Python exception on a non-0 return code) or unchecked way. A future fuzzer will use this `run_js` method. --- scripts/fuzz_opt.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 83d351d67f1..177b0f9dd66 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -606,7 +606,7 @@ def note_ignored_vm_run(reason, extra_text='', amount=1): # Run a VM command, and filter out known issues. -def run_vm(cmd): +def run_vm(cmd, checked=True): def filter_known_issues(output): known_issues = [ # can be caused by flatten, ssa, etc. passes @@ -649,7 +649,11 @@ def filter_known_issues(output): try: # some known issues do not cause the entire process to fail - return filter_known_issues(run(cmd)) + if checked: + ret = run(cmd) + else: + ret = run_unchecked(cmd) + return filter_known_issues(ret) except subprocess.CalledProcessError: # other known issues do make it fail, so re-run without checking for # success and see if we should ignore it @@ -696,6 +700,7 @@ def get_v8_extra_flags(): V8_LIFTOFF_ARGS = ['--liftoff'] +V8_NO_LIFTOFF_ARGS = ['--no-liftoff'] # Default to running with liftoff enabled, because we need to pick either @@ -831,8 +836,13 @@ def can_compare_to_other(self, other): class D8: name = 'd8' - def run(self, wasm, extra_d8_flags=[]): - return run_vm([shared.V8, get_fuzz_shell_js()] + shared.V8_OPTS + get_v8_extra_flags() + extra_d8_flags + ['--', wasm]) + extra_d8_flags = [] + + def run_js(self, js, wasm, checked=True): + return run_vm([shared.V8, js] + shared.V8_OPTS + get_v8_extra_flags() + self.extra_d8_flags + ['--', wasm], checked=checked) + + def run(self, wasm): + return self.run_js(js=get_fuzz_shell_js(), wasm=wasm) def can_run(self, wasm): return all_disallowed(DISALLOWED_FEATURES_IN_V8) @@ -856,16 +866,13 @@ def can_compare_to_other(self, other): class D8Liftoff(D8): name = 'd8_liftoff' - def run(self, wasm): - return super().run(wasm, extra_d8_flags=V8_LIFTOFF_ARGS) + extra_d8_flags = V8_LIFTOFF_ARGS class D8Turboshaft(D8): name = 'd8_turboshaft' - def run(self, wasm): - flags = ['--no-liftoff'] - return super().run(wasm, extra_d8_flags=flags) + extra_d8_flags = V8_NO_LIFTOFF_ARGS class Wasm2C: From a827aa524c0aa8b77943c98cc2d57a26ba891a63 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 9 Apr 2026 16:45:41 -0700 Subject: [PATCH 022/168] Fuzzer: Make --fuzz-preserve-imports-and-exports also preserve the start function (#8589) The start may be needed for the ABI between the wasm and the outside. The point of preserve-imports-and-exports is to not break such ABIs (or at least have a chance of not doing so), so it doesn't seem like we need a new option here. --- src/tools/fuzzing/fuzzing.cpp | 8 ++++++-- src/tools/wasm-opt.cpp | 2 +- test/lit/fuzz-preserve-imports-exports.wast | 10 ++++++++++ test/lit/help/wasm-opt.test | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index 222d68698ae..af206ab2488 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -2381,8 +2381,12 @@ void TranslateToFuzzReader::modifyInitialFunctions() { } // Remove a start function - the fuzzing harness expects code to run only - // from exports. - wasm.start = Name(); + // from exports. When preserving imports and exports, however, we need to + // keep any start method, as it may be important to keep the contract between + // the wasm and the outside. + if (!preserveImportsAndExports) { + wasm.start = Name(); + } } void TranslateToFuzzReader::dropToLog(Function* func) { diff --git a/src/tools/wasm-opt.cpp b/src/tools/wasm-opt.cpp index 4dd58db122d..5c2807c25e4 100644 --- a/src/tools/wasm-opt.cpp +++ b/src/tools/wasm-opt.cpp @@ -206,7 +206,7 @@ For more on how to optimize effectively, see [&](Options* o, const std::string& arguments) { fuzzOOB = false; }) .add("--fuzz-preserve-imports-exports", "", - "don't add imports and exports in -ttf mode", + "don't add imports and exports in -ttf mode, and keep the start", WasmOptOption, Options::Arguments::Zero, [&](Options* o, const std::string& arguments) { diff --git a/test/lit/fuzz-preserve-imports-exports.wast b/test/lit/fuzz-preserve-imports-exports.wast index 3883c6aed3f..e8cde8dadbe 100644 --- a/test/lit/fuzz-preserve-imports-exports.wast +++ b/test/lit/fuzz-preserve-imports-exports.wast @@ -18,8 +18,12 @@ ;; PRESERVE: (import "a" "f" (func $ifunc ;; PRESERVE: (import "a" "c" (tag $itag +;; The export is preserved. ;; PRESERVE: (export "foo" (func $foo)) +;; The start function is preserved. +;; PRESERVE: (start $on_load) + ;; And, without the flag, we do generate both imports and exports. ;; RUN: wasm-opt %s.dat --initial-fuzz=%s -all -ttf \ @@ -42,8 +46,14 @@ (import "a" "e" (table $itable 10 20 funcref)) (import "a" "f" (func $ifunc)) + (start $on_load) + ;; One existing export. (func $foo (export "foo") ) + + (func $on_load + (call $ifunc) + ) ) diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 8aed53a603a..ee2d5b86798 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -70,7 +70,7 @@ ;; CHECK-NEXT: fuzzing ;; CHECK-NEXT: ;; CHECK-NEXT: --fuzz-preserve-imports-exports don't add imports and exports in -;; CHECK-NEXT: -ttf mode +;; CHECK-NEXT: -ttf mode, and keep the start ;; CHECK-NEXT: ;; CHECK-NEXT: --fuzz-import a module to use as an import in ;; CHECK-NEXT: -ttf mode From baa15640556aff4043c57a7ca8a05535c3107e02 Mon Sep 17 00:00:00 2001 From: daichifukui Date: Sat, 11 Apr 2026 01:43:20 +0900 Subject: [PATCH 023/168] [NFC] Fix spelling typos (#8591) Fix various spelling typos in source and test files, as reported by Debian Lintian. --- src/passes/Print.cpp | 4 ++-- src/passes/Souperify.cpp | 2 +- src/passes/pass.cpp | 4 ++-- src/tools/fuzzing/fuzzing.cpp | 4 ++-- src/tools/wasm-fuzz-lattices.cpp | 2 +- test/lit/help/wasm-metadce.test | 4 ++-- test/lit/help/wasm-opt.test | 4 ++-- test/lit/help/wasm2js.test | 4 ++-- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index d043735f315..11a73315b74 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -3868,7 +3868,7 @@ printStackInst(StackInst* inst, std::ostream& o, Function* func) { break; } default: - WASM_UNREACHABLE("unexpeted op"); + WASM_UNREACHABLE("unexpected op"); } return o; } @@ -3961,7 +3961,7 @@ static std::ostream& printStackIR(StackIR* ir, PrintSExpression& printer) { break; } default: - WASM_UNREACHABLE("unexpeted op"); + WASM_UNREACHABLE("unexpected op"); } o << '\n'; } diff --git a/src/passes/Souperify.cpp b/src/passes/Souperify.cpp index 9ed11e550a4..041ea286946 100644 --- a/src/passes/Souperify.cpp +++ b/src/passes/Souperify.cpp @@ -662,7 +662,7 @@ struct Printer { std::cout << ", "; printInternal(node->getValue(2)); } else { - WASM_UNREACHABLE("unexecpted node type"); + WASM_UNREACHABLE("unexpected node type"); } } diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index bcf3242e78c..e5de76176ba 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -178,7 +178,7 @@ void PassRegistry::registerPasses() { registerPass( "func-metrics", "reports function metrics", createFunctionMetricsPass); registerPass("generate-dyncalls", - "generate dynCall fuctions used by emscripten ABI", + "generate dynCall functions used by emscripten ABI", createGenerateDynCallsPass); registerPass( "generate-i64-dyncalls", @@ -367,7 +367,7 @@ void PassRegistry::registerPasses() { "pick load signs based on their uses", createPickLoadSignsPass); registerPass( - "poppify", "Tranform Binaryen IR into Poppy IR", createPoppifyPass); + "poppify", "Transform Binaryen IR into Poppy IR", createPoppifyPass); registerPass("post-emscripten", "miscellaneous optimizations for Emscripten-generated code", createPostEmscriptenPass); diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index af206ab2488..e7696532472 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -4921,7 +4921,7 @@ Expression* TranslateToFuzzReader::makeAtomic(Type type) { bytes = pick(1, 2, 4); break; default: - WASM_UNREACHABLE("invalide value"); + WASM_UNREACHABLE("invalid value"); } break; } @@ -4940,7 +4940,7 @@ Expression* TranslateToFuzzReader::makeAtomic(Type type) { bytes = pick(1, 2, 4, 8); break; default: - WASM_UNREACHABLE("invalide value"); + WASM_UNREACHABLE("invalid value"); } break; } diff --git a/src/tools/wasm-fuzz-lattices.cpp b/src/tools/wasm-fuzz-lattices.cpp index 4d36761c69e..efd7a61a24f 100644 --- a/src/tools/wasm-fuzz-lattices.cpp +++ b/src/tools/wasm-fuzz-lattices.cpp @@ -1067,7 +1067,7 @@ int main(int argc, const char* argv[]) { Options options("wasm-fuzz-lattices", "Fuzz lattices for reflexivity, transitivity, and " - "anti-symmetry, and tranfer functions for monotonicity."); + "anti-symmetry, and transfer functions for monotonicity."); std::optional seed; options.add("--seed", diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index f1b9f25e167..ea5806e0199 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -163,7 +163,7 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --func-metrics reports function metrics ;; CHECK-NEXT: -;; CHECK-NEXT: --generate-dyncalls generate dynCall fuctions used +;; CHECK-NEXT: --generate-dyncalls generate dynCall functions used ;; CHECK-NEXT: by emscripten ABI ;; CHECK-NEXT: ;; CHECK-NEXT: --generate-global-effects generate global effect info @@ -347,7 +347,7 @@ ;; CHECK-NEXT: --pick-load-signs pick load signs based on their ;; CHECK-NEXT: uses ;; CHECK-NEXT: -;; CHECK-NEXT: --poppify Tranform Binaryen IR into Poppy +;; CHECK-NEXT: --poppify Transform Binaryen IR into Poppy ;; CHECK-NEXT: IR ;; CHECK-NEXT: ;; CHECK-NEXT: --post-emscripten miscellaneous optimizations for diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index ee2d5b86798..1c0a53801e4 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -195,7 +195,7 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --func-metrics reports function metrics ;; CHECK-NEXT: -;; CHECK-NEXT: --generate-dyncalls generate dynCall fuctions used +;; CHECK-NEXT: --generate-dyncalls generate dynCall functions used ;; CHECK-NEXT: by emscripten ABI ;; CHECK-NEXT: ;; CHECK-NEXT: --generate-global-effects generate global effect info @@ -379,7 +379,7 @@ ;; CHECK-NEXT: --pick-load-signs pick load signs based on their ;; CHECK-NEXT: uses ;; CHECK-NEXT: -;; CHECK-NEXT: --poppify Tranform Binaryen IR into Poppy +;; CHECK-NEXT: --poppify Transform Binaryen IR into Poppy ;; CHECK-NEXT: IR ;; CHECK-NEXT: ;; CHECK-NEXT: --post-emscripten miscellaneous optimizations for diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index 06248ab0b55..39bc23554a9 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -127,7 +127,7 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --func-metrics reports function metrics ;; CHECK-NEXT: -;; CHECK-NEXT: --generate-dyncalls generate dynCall fuctions used +;; CHECK-NEXT: --generate-dyncalls generate dynCall functions used ;; CHECK-NEXT: by emscripten ABI ;; CHECK-NEXT: ;; CHECK-NEXT: --generate-global-effects generate global effect info @@ -311,7 +311,7 @@ ;; CHECK-NEXT: --pick-load-signs pick load signs based on their ;; CHECK-NEXT: uses ;; CHECK-NEXT: -;; CHECK-NEXT: --poppify Tranform Binaryen IR into Poppy +;; CHECK-NEXT: --poppify Transform Binaryen IR into Poppy ;; CHECK-NEXT: IR ;; CHECK-NEXT: ;; CHECK-NEXT: --post-emscripten miscellaneous optimizations for From 39906157445c9dfa1db5c355f25e40d17f700c21 Mon Sep 17 00:00:00 2001 From: Changqing Jing Date: Tue, 14 Apr 2026 03:48:57 +0800 Subject: [PATCH 024/168] [NFC] Use unordered_set in effects.h and CodePushing (#8586) This avoids large slowdowns in cases with very long string names, etc. --- .gitignore | 2 ++ src/ir/effects.h | 9 +++++---- src/passes/CodeFolding.cpp | 26 ++++++++++++++++---------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 95af1a39719..b88109b38e0 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,5 @@ CMakeUserPresets.json # files related to clangd cache .cache/* + +.venv/ diff --git a/src/ir/effects.h b/src/ir/effects.h index e8ab4c8ef69..af866b9e536 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -18,6 +18,7 @@ #define wasm_ir_effects_h #include +#include #include "ir/intrinsics.h" #include "pass.h" @@ -141,8 +142,8 @@ class EffectAnalyzer { std::set localsRead; std::set localsWritten; - std::set mutableGlobalsRead; - std::set globalsWritten; + std::unordered_set mutableGlobalsRead; + std::unordered_set globalsWritten; // The nested depth of try-catch_all. If an instruction that may throw is // inside an inner try-catch_all, we don't mark it as 'throws_', because it @@ -513,8 +514,8 @@ class EffectAnalyzer { return hasAnything(); } - std::set breakTargets; - std::set delegateTargets; + std::unordered_set breakTargets; + std::unordered_set delegateTargets; private: struct InternalAnalyzer diff --git a/src/passes/CodeFolding.cpp b/src/passes/CodeFolding.cpp index c4a7804695c..e53cd4f880d 100644 --- a/src/passes/CodeFolding.cpp +++ b/src/passes/CodeFolding.cpp @@ -56,6 +56,8 @@ // #include +#include +#include #include "ir/branch-utils.h" #include "ir/effects.h" @@ -74,9 +76,9 @@ static const Index WORTH_ADDING_BLOCK_TO_REMOVE_THIS_MUCH = 3; struct ExpressionMarker : public PostWalker> { - std::set& marked; + std::unordered_set& marked; - ExpressionMarker(std::set& marked, Expression* expr) + ExpressionMarker(std::unordered_set& marked, Expression* expr) : marked(marked) { walk(expr); } @@ -122,13 +124,16 @@ struct CodeFolding // pass state - std::map> breakTails; // break target name => tails - // that reach it + std::unordered_map> + breakTails; // break target name => tails + // that reach it std::vector unreachableTails; // tails leading to (unreachable) std::vector returnTails; // tails leading to (return) - std::set unoptimizables; // break target names that we can't handle - std::set modifieds; // modified code should not be processed - // again, wait for next pass + std::unordered_set + unoptimizables; // break target names that we can't handle + std::unordered_set + modifieds; // modified code should not be processed + // again, wait for next pass // walking @@ -644,9 +649,10 @@ struct CodeFolding if (next.size() >= 2) { // now we want to find a mergeable item - any item that is equal among a // subset - std::map hashes; // expression => hash value + std::unordered_map + hashes; // expression => hash value // hash value => expressions with that hash - std::map> hashed; + std::unordered_map> hashed; for (auto& tail : next) { auto* item = getItem(tail, num); auto hash = hashes[item] = ExpressionAnalyzer::hash(item); @@ -654,7 +660,7 @@ struct CodeFolding } // look at each hash value exactly once. we do this in a deterministic // order by iterating over a vector retaining insertion order. - std::set seen; + std::unordered_set seen; for (auto& tail : next) { auto* item = getItem(tail, num); auto digest = hashes[item]; From 031e1639e6b15b90b4224105ed62484e52d806a6 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 13 Apr 2026 16:05:25 -0700 Subject: [PATCH 025/168] Increase Alpine stack size to 8MB (#8595) The default in musl is apparently tiny, and MergeSimilarFunctions has recursion which can hit it. We can perhaps improve that pass to avoid recursion, but this change seems generally good for robustness. It just makes us use the usual 8 MB stack size on Linux that all other Linuxes use. Fixes #8594 --- .github/workflows/ci.yml | 7 +- .github/workflows/create_release.yml | 6 +- .../merge-similar-functions_recursion.wast | 1893 +++++++++++++++++ 3 files changed, 1903 insertions(+), 3 deletions(-) create mode 100644 test/lit/passes/merge-similar-functions_recursion.wast diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 600768b9cdb..75ddc691741 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -221,7 +221,7 @@ jobs: # Run tests on Alpine Linux, which we use to make our release builds. # Note: Alpine uses musl libc. - # Keep in sync with build_release.yml. The only difference is that here we + # Keep in sync with create_release.yml. The only difference is that here we # do not have the "archive" and "upload tarball" jobs. build-alpine: name: alpine @@ -259,8 +259,11 @@ jobs: run: ./alpine.sh pip3 install --break-system-packages -r requirements-dev.txt - name: cmake + # Build with an 8MB stack size, as otherwise Alpine/musl's default stack + # size for pthreads is tiny, + # https://github.com/WebAssembly/binaryen/issues/8594 run: | - ./alpine.sh cmake . -G Ninja -DCMAKE_CXX_FLAGS="-static" -DCMAKE_C_FLAGS="-static" -DCMAKE_BUILD_TYPE=Release -DBUILD_STATIC_LIB=ON -DBUILD_MIMALLOC=ON -DCMAKE_INSTALL_PREFIX=install + ./alpine.sh cmake . -G Ninja -DCMAKE_CXX_FLAGS="-static" -DCMAKE_C_FLAGS="-static" -DCMAKE_EXE_LINKER_FLAGS="-Wl,-z,stack-size=8388608" -DCMAKE_BUILD_TYPE=Release -DBUILD_STATIC_LIB=ON -DBUILD_MIMALLOC=ON -DCMAKE_INSTALL_PREFIX=install - name: build run: | diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index ed8bb2fb6c4..07cbdc8fef5 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -108,6 +108,7 @@ jobs: ${{ steps.archive-arm64.outputs.SHASUM }} # Build with gcc 6.3 and run tests on Alpine Linux (inside chroot). + # Keep in sync with ci.yml. # Note: Alpine uses musl libc. build-alpine: name: alpine @@ -145,8 +146,11 @@ jobs: run: ./alpine.sh pip3 install --break-system-packages -r requirements-dev.txt - name: cmake + # Build with an 8MB stack size, as otherwise Alpine/musl's default stack + # size for pthreads is tiny, + # https://github.com/WebAssembly/binaryen/issues/8594 run: | - ./alpine.sh cmake . -G Ninja -DCMAKE_CXX_FLAGS="-static" -DCMAKE_C_FLAGS="-static" -DCMAKE_BUILD_TYPE=Release -DBUILD_STATIC_LIB=ON -DBUILD_MIMALLOC=ON -DCMAKE_INSTALL_PREFIX=install + ./alpine.sh cmake . -G Ninja -DCMAKE_CXX_FLAGS="-static" -DCMAKE_C_FLAGS="-static" -DCMAKE_EXE_LINKER_FLAGS="-Wl,-z,stack-size=8388608" -DCMAKE_BUILD_TYPE=Release -DBUILD_STATIC_LIB=ON -DBUILD_MIMALLOC=ON -DCMAKE_INSTALL_PREFIX=install - name: build run: | diff --git a/test/lit/passes/merge-similar-functions_recursion.wast b/test/lit/passes/merge-similar-functions_recursion.wast new file mode 100644 index 00000000000..459ae7f3a89 --- /dev/null +++ b/test/lit/passes/merge-similar-functions_recursion.wast @@ -0,0 +1,1893 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --merge-similar-functions -S -o - | filecheck %s + +;; This has very deep recursion of calls, which end up processed recursively in +;; the pass. This should still work even on Alpine/musl which has very small +;; default stack sizes. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (sub (struct))) + (type $0 (sub (struct))) + ;; CHECK: (type $2 (sub $0 (struct))) + (type $2 (sub $0 (struct))) + ;; CHECK: (type $1 (sub $0 (struct))) + (type $1 (sub $0 (struct))) + ;; CHECK: (type $4 (sub final $0 (struct))) + (type $4 (sub final $0 (struct))) + ;; CHECK: (type $5 (sub final $2 (struct))) + (type $5 (sub final $2 (struct))) + ;; CHECK: (type $6 (sub final $0 (struct))) + (type $6 (sub final $0 (struct))) + ;; CHECK: (type $7 (sub final $1 (struct))) + (type $7 (sub final $1 (struct))) + ;; CHECK: (type $8 (sub final $1 (struct))) + (type $8 (sub final $1 (struct))) + ;; CHECK: (type $11 (sub final $0 (struct (field (ref null $0)) (field (ref null $8)) (field (ref null $7)) (field (ref null $6)) (field (ref null $6)) (field (ref null $12)) (field (ref null $6)) (field (ref null $0)) (field (ref null $6)) (field (ref null $7)) (field (ref null $5)) (field (ref null $12)) (field (ref null $6)) (field (ref null $4)) (field (ref null $7)) (field (ref null $12)) (field (ref null $12)) (field (ref null $12)) (field (ref null $12))))) + (type $11 (sub final $0 (struct (field (ref null $0)) (field (ref null $8)) (field (ref null $7)) (field (ref null $6)) (field (ref null $6)) (field (ref null $12)) (field (ref null $6)) (field (ref null $0)) (field (ref null $6)) (field (ref null $7)) (field (ref null $5)) (field (ref null $12)) (field (ref null $6)) (field (ref null $4)) (field (ref null $7)) (field (ref null $12)) (field (ref null $12)) (field (ref null $12)) (field (ref null $12))))) + ;; CHECK: (type $12 (sub final $0 (struct))) + (type $12 (sub final $0 (struct))) + ;; CHECK: (type $13 (func (param (ref null $0)) (result (ref null $12)))) + (type $13 (func (param (ref null $0)) (result (ref null $12)))) + ;; CHECK: (type $17 (func (param (ref null $12) (ref null $0)) (result (ref (exact $12))))) + (type $17 (func (param (ref null $12) (ref null $0)) (result (ref (exact $12))))) + ) + ;; CHECK: (func $0 (type $17) (param $0 (ref null $12)) (param $1 (ref null $0)) (result (ref (exact $12))) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $0 (type $17) (param $0 (ref null $12)) (param $1 (ref null $0)) (result (ref (exact $12))) + (unreachable) + ) + ;; CHECK: (func $3 (type $13) (param $0 (ref null $0)) (result (ref null $12)) + ;; CHECK-NEXT: (local $1 (ref (exact $12))) + ;; CHECK-NEXT: (local $2 (ref null (exact $12))) + ;; CHECK-NEXT: (local $3 (ref null $11)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (call $0 + ;; CHECK-NEXT: (local.tee $1 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 0 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 1 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 2 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 3 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 4 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 5 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 6 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 7 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 8 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 9 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 10 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 11 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 12 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 13 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 14 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 15 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 16 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 17 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $11 18 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + (func $3 (type $13) (param $0 (ref null $0)) (result (ref null $12)) + (local $1 (ref (exact $12))) + (local $2 (ref null (exact $12))) + (local $3 (ref null $11)) + (drop + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (call $0 + (local.tee $1 + (ref.as_non_null + (local.get $2) + ) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (struct.get $11 0 + (local.get $3) + ) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (struct.get $11 1 + (local.get $3) + ) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (struct.get $11 2 + (local.get $3) + ) + ) + (local.get $1) + ) + (struct.get $11 3 + (local.get $3) + ) + ) + (local.get $1) + ) + (local.get $1) + ) + (struct.get $11 4 + (local.get $3) + ) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (local.get $1) + ) + (struct.get $11 5 + (local.get $3) + ) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (struct.get $11 6 + (local.get $3) + ) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (struct.get $11 7 + (local.get $3) + ) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (struct.get $11 8 + (local.get $3) + ) + ) + (local.get $1) + ) + (struct.get $11 9 + (local.get $3) + ) + ) + (local.get $1) + ) + (struct.get $11 10 + (local.get $3) + ) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (struct.get $11 11 + (local.get $3) + ) + ) + (local.get $1) + ) + (struct.get $11 12 + (local.get $3) + ) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (struct.get $11 13 + (local.get $3) + ) + ) + (local.get $1) + ) + (struct.get $11 14 + (local.get $3) + ) + ) + (local.get $1) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (struct.get $11 15 + (local.get $3) + ) + ) + (local.get $1) + ) + (struct.get $11 16 + (local.get $3) + ) + ) + (local.get $1) + ) + (struct.get $11 17 + (local.get $3) + ) + ) + (local.get $1) + ) + (struct.get $11 18 + (local.get $3) + ) + ) + (local.get $1) + ) + (ref.as_non_null + (local.get $2) + ) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (local.get $1) + ) + (ref.null none) + ) + (local.get $1) + ) + (ref.null none) + ) + ) + (local.get $1) + ) +) From ce7f86949d5f951292f1a276808ebd75696b691d Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Mon, 13 Apr 2026 16:07:58 -0700 Subject: [PATCH 026/168] Refactor graph traversal in GlobalEffects (#8593) Refactor GlobalEffects to not compute the transitive call graph explicitly but instead aggregate effects as we go. This improves the runtime of the pass by 4.3% on calcworker (1.21792 s -> 1.16586 s averaged over 20 compilations). It also helps prepare the code for future changes to support effects for indirect calls. Another potential future improvement here is to use SCC, which would let us stop processing children early in cases where there are no effects to update. Currently we can't do this because we add trap effects to potentially-recursive call loops, so even if no effects were updated, we need to keep going to find potential cycles. --- src/passes/GlobalEffects.cpp | 285 ++++++++++++++++++----------------- 1 file changed, 143 insertions(+), 142 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index ef0977d12fa..ac17037902b 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -27,163 +27,162 @@ namespace wasm { -struct GenerateGlobalEffects : public Pass { - void run(Module* module) override { - // First, we do a scan of each function to see what effects they have, - // including which functions they call directly (so that we can compute - // transitive effects later). - - struct FuncInfo { - // Effects in this function. - std::optional effects; - - // Directly-called functions from this function. - std::unordered_set calledFunctions; - }; - - ModuleUtils::ParallelFunctionAnalysis analysis( - *module, [&](Function* func, FuncInfo& funcInfo) { - if (func->imported()) { - // Imports can do anything, so we need to assume the worst anyhow, - // which is the same as not specifying any effects for them in the - // map (which we do by not setting funcInfo.effects). - return; - } - - // Gather the effects. - funcInfo.effects.emplace(getPassOptions(), *module, func); - - if (funcInfo.effects->calls) { - // There are calls in this function, which we will analyze in detail. - // Clear the |calls| field first, and we'll handle calls of all sorts - // below. - funcInfo.effects->calls = false; - - // Clear throws as well, as we are "forgetting" calls right now, and - // want to forget their throwing effect as well. If we see something - // else that throws, below, then we'll note that there. - funcInfo.effects->throws_ = false; - - struct CallScanner - : public PostWalker> { - Module& wasm; - PassOptions& options; - FuncInfo& funcInfo; - - CallScanner(Module& wasm, PassOptions& options, FuncInfo& funcInfo) - : wasm(wasm), options(options), funcInfo(funcInfo) {} - - void visitExpression(Expression* curr) { - ShallowEffectAnalyzer effects(options, wasm, curr); - if (auto* call = curr->dynCast()) { - // Note the direct call. - funcInfo.calledFunctions.insert(call->target); - } else if (effects.calls) { - // This is an indirect call of some sort, so we must assume the - // worst. To do so, clear the effects, which indicates nothing - // is known (so anything is possible). - // TODO: We could group effects by function type etc. - funcInfo.effects.reset(); - } else { - // No call here, but update throwing if we see it. (Only do so, - // however, if we have effects; if we cleared it - see before - - // then we assume the worst anyhow, and have nothing to update.) - if (effects.throws_ && funcInfo.effects) { - funcInfo.effects->throws_ = true; - } +namespace { + +constexpr auto UnknownEffects = std::nullopt; + +struct FuncInfo { + // Effects in this function. nullopt / UnknownEffects means that we don't know + // what effects this function has, so we conservatively assume all effects. + // Nullopt cases won't be copied to Function::effects. + std::optional effects; + + // Directly-called functions from this function. + std::unordered_set calledFunctions; +}; + +std::map analyzeFuncs(Module& module, + const PassOptions& passOptions) { + ModuleUtils::ParallelFunctionAnalysis analysis( + module, [&](Function* func, FuncInfo& funcInfo) { + if (func->imported()) { + // Imports can do anything, so we need to assume the worst anyhow, + // which is the same as not specifying any effects for them in the + // map (which we do by not setting funcInfo.effects). + return; + } + + // Gather the effects. + funcInfo.effects.emplace(passOptions, module, func); + + if (funcInfo.effects->calls) { + // There are calls in this function, which we will analyze in detail. + // Clear the |calls| field first, and we'll handle calls of all sorts + // below. + funcInfo.effects->calls = false; + + // Clear throws as well, as we are "forgetting" calls right now, and + // want to forget their throwing effect as well. If we see something + // else that throws, below, then we'll note that there. + funcInfo.effects->throws_ = false; + + struct CallScanner + : public PostWalker> { + Module& wasm; + const PassOptions& options; + FuncInfo& funcInfo; + + CallScanner(Module& wasm, + const PassOptions& options, + FuncInfo& funcInfo) + : wasm(wasm), options(options), funcInfo(funcInfo) {} + + void visitExpression(Expression* curr) { + ShallowEffectAnalyzer effects(options, wasm, curr); + if (auto* call = curr->dynCast()) { + // Note the direct call. + funcInfo.calledFunctions.insert(call->target); + } else if (effects.calls) { + // This is an indirect call of some sort, so we must assume the + // worst. To do so, clear the effects, which indicates nothing + // is known (so anything is possible). + // TODO: We could group effects by function type etc. + funcInfo.effects = UnknownEffects; + } else { + // No call here, but update throwing if we see it. (Only do so, + // however, if we have effects; if we cleared it - see before - + // then we assume the worst anyhow, and have nothing to update.) + if (effects.throws_ && funcInfo.effects) { + funcInfo.effects->throws_ = true; } } - }; - CallScanner scanner(*module, getPassOptions(), funcInfo); - scanner.walkFunction(func); - } - }); - - // Compute the transitive closure of effects. To do so, first construct for - // each function a list of the functions that it is called by (so we need to - // propagate its effects to them), and then we'll construct the closure of - // that. - // - // callers[foo] = [func that calls foo, another func that calls foo, ..] - // - std::unordered_map> callers; - - // Our work queue contains info about a new call pair: a call from a caller - // to a called function, that is information we then apply and propagate. - using CallPair = std::pair; // { caller, called } - UniqueDeferredQueue work; - for (auto& [func, info] : analysis.map) { - for (auto& called : info.calledFunctions) { - work.push({func->name, called}); + } + }; + CallScanner scanner(module, passOptions, funcInfo); + scanner.walkFunction(func); } + }); + + return std::move(analysis.map); +} + +// Propagate effects from callees to callers transitively +// e.g. if A -> B -> C (A calls B which calls C) +// Then B inherits effects from C and A inherits effects from both B and C. +void propagateEffects( + const Module& module, + const std::unordered_map>& reverseCallGraph, + std::map& funcInfos) { + + UniqueNonrepeatingDeferredQueue> work; + + for (const auto& [callee, callers] : reverseCallGraph) { + for (const auto& caller : callers) { + work.push(std::pair(callee, caller)); } + } - // Compute the transitive closure of the call graph, that is, fill out - // |callers| so that it contains the list of all callers - even through a - // chain - of each function. - while (!work.empty()) { - auto [caller, called] = work.pop(); - - // We must not already have an entry for this call (that would imply we - // are doing wasted work). - assert(!callers[called].contains(caller)); - - // Apply the new call information. - callers[called].insert(caller); - - // We just learned that |caller| calls |called|. It also calls - // transitively, which we need to propagate to all places unaware of that - // information yet. - // - // caller => called => called by called - // - auto& calledInfo = analysis.map[module->getFunction(called)]; - for (auto calledByCalled : calledInfo.calledFunctions) { - if (!callers[calledByCalled].contains(caller)) { - work.push({caller, calledByCalled}); - } - } + auto propagate = [&](Name callee, Name caller) { + auto& callerEffects = funcInfos.at(module.getFunction(caller)).effects; + const auto& calleeEffects = + funcInfos.at(module.getFunction(callee)).effects; + if (!callerEffects) { + return; } - // Now that we have transitively propagated all static calls, apply that - // information. First, apply infinite recursion: if a function can call - // itself then it might recurse infinitely, which we consider an effect (a - // trap). - for (auto& [func, info] : analysis.map) { - if (callers[func->name].contains(func->name)) { - if (info.effects) { - info.effects->trap = true; - } + if (!calleeEffects) { + callerEffects = UnknownEffects; + return; + } + + callerEffects->mergeIn(*calleeEffects); + }; + + while (!work.empty()) { + auto [callee, caller] = work.pop(); + + if (callee == caller) { + auto& callerEffects = funcInfos.at(module.getFunction(caller)).effects; + if (callerEffects) { + callerEffects->trap = true; } } - // Next, apply function effects to their callers. - for (auto& [func, info] : analysis.map) { - auto& funcEffects = info.effects; - - for (auto& caller : callers[func->name]) { - auto& callerEffects = analysis.map[module->getFunction(caller)].effects; - if (!callerEffects) { - // Nothing is known for the caller, which is already the worst case. - continue; - } - - if (!funcEffects) { - // Nothing is known for the called function, which means nothing is - // known for the caller either. - callerEffects.reset(); - continue; - } - - // Add func's effects to the caller. - callerEffects->mergeIn(*funcEffects); + // Even if nothing changed, we still need to keep traversing the callers + // to look for a potential cycle which adds a trap affect on the above + // lines. + propagate(callee, caller); + + const auto& callerCallers = reverseCallGraph.find(caller); + if (callerCallers == reverseCallGraph.end()) { + continue; + } + + for (const Name& callerCaller : callerCallers->second) { + work.push(std::pair(callee, callerCaller)); + } + } +} + +struct GenerateGlobalEffects : public Pass { + void run(Module* module) override { + std::map funcInfos = + analyzeFuncs(*module, getPassOptions()); + + // callee : caller + std::unordered_map> callers; + for (const auto& [func, info] : funcInfos) { + for (const auto& callee : info.calledFunctions) { + callers[callee].insert(func->name); } } + propagateEffects(*module, callers, funcInfos); + // Generate the final data, starting from a blank slate where nothing is // known. - for (auto& [func, info] : analysis.map) { + for (auto& [func, info] : funcInfos) { func->effects.reset(); if (!info.effects) { continue; @@ -202,6 +201,8 @@ struct DiscardGlobalEffects : public Pass { } }; +} // namespace + Pass* createGenerateGlobalEffectsPass() { return new GenerateGlobalEffects(); } Pass* createDiscardGlobalEffectsPass() { return new DiscardGlobalEffects(); } From 1f65c573ac6a644f3f99a18d5f0c08aabadea3a1 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 14 Apr 2026 08:20:13 -0700 Subject: [PATCH 027/168] New fuzzer: PreserveImportsExportsJS (#8592) This starts from wasm+js testcases and then modifies the wasm in a way that preserves imports and exports, so the wasm+js can still be run. This is very different from our usual approach of starting with only wasm, then bashing it into the shape that our general js code can handle. The main benefit here is testing of more interesting wasm+js interactions, specifically for the JS Interop proposal. Three wasm+js combinations are added in this PR that test features from that proposal. --- scripts/fuzz_opt.py | 150 ++++++++++++++++++++++++- scripts/test/fuzzing.py | 4 + test/js_wasm/js_interop_cases.mjs | 56 ++++++++++ test/js_wasm/js_interop_cases.wat | 161 ++++++++++++++++++++++++++ test/js_wasm/js_interop_corners.mjs | 92 +++++++++++++++ test/js_wasm/js_interop_corners.wat | 168 ++++++++++++++++++++++++++++ test/js_wasm/js_interop_counter.mjs | 32 ++++++ test/js_wasm/js_interop_counter.wat | 105 +++++++++++++++++ 8 files changed, 765 insertions(+), 3 deletions(-) create mode 100644 test/js_wasm/js_interop_cases.mjs create mode 100644 test/js_wasm/js_interop_cases.wat create mode 100644 test/js_wasm/js_interop_corners.mjs create mode 100644 test/js_wasm/js_interop_corners.wat create mode 100644 test/js_wasm/js_interop_counter.mjs create mode 100644 test/js_wasm/js_interop_counter.wat diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 177b0f9dd66..bf0892be681 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -31,6 +31,7 @@ import json import math import os +import pathlib import random import re import shutil @@ -2049,8 +2050,9 @@ def compare_to_merged_output(self, output, merged_output): compare(output, merged_output, 'Two-Merged') -# Test --fuzz-preserve-imports-exports, which never modifies imports or exports. -class PreserveImportsExports(TestCaseHandler): +# Test --fuzz-preserve-imports-exports on random inputs. This should never +# modify imports or exports. +class PreserveImportsExportsRandom(TestCaseHandler): frequency = 0.1 def handle(self, wasm): @@ -2095,6 +2097,147 @@ def get_relevant_lines(wat): compare(get_relevant_lines(original), get_relevant_lines(processed), 'Preserve') +# Test --fuzz-preserve-imports-exports on a realistic js+wasm input. Unlike +# PreserveImportsExportsRandom which starts with a random file and modifies it, +# this starts with a fixed js+wasm testcase, known to work and to have +# interesting operations on the js/wasm boundary, and then randomly modifies +# the wasm. This simulates how an external fuzzer could use binaryen to modify +# its known-working testcases (parallel to how we test ClusterFuzz here). +# +# This reads wasm+js combinations from the test/js_wasm directory, so as new +# testcases are added there, this will fuzz them. +# +# Note that bugs found by this fuzzer require BINARYEN_TRUST_GIVEN_WASM=1 in the +# env for reduction. TODO: simplify this +class PreserveImportsExportsJS(TestCaseHandler): + frequency = 1 + + def handle_pair(self, input, before_wasm, after_wasm, opts): + try: + self.do_handle_pair(input, before_wasm, after_wasm, opts) + except Exception as e: + if not os.environ.get('BINARYEN_TRUST_GIVEN_WASM'): + # We errored, and we were not given a wasm file to trust as we + # reduce, so this is the first time we hit an error. Save the + # pre wasm file, the one we began with, as `before_wasm`, so + # that the reducer will make us proceed exactly from there. + shutil.copyfile(self.pre_wasm, before_wasm) + raise e + + def do_handle_pair(self, input, before_wasm, after_wasm, opts): + # Some of the time use a custom input. The normal inputs the fuzzer + # generates are in range INPUT_SIZE_MIN-INPUT_SIZE_MAX, which is good + # for new testcases, but the more changes we make to js+wasm testcases, + # the more chance we have to break things entirely (the js/wasm boundary + # is fragile). It is useful to also fuzz smaller sizes. + if random.random() < 0.25: + size = random.randint(0, INPUT_SIZE_MIN * 2) + make_random_input(size, input) + + # Pick a js+wasm pair. + js_files = list(pathlib.Path(in_binaryen('test', 'js_wasm')).glob('*.mjs')) + js_file = str(random.choice(js_files)) + print(f'js file: {js_file}') + wat_file = str(pathlib.Path(js_file).with_suffix('.wat')) + + # Verify the wat works with our features + try: + run([in_bin('wasm-opt'), wat_file] + FEATURE_OPTS, + stderr=subprocess.PIPE, + silent=True) + except Exception: + note_ignored_vm_run('PreserveImportsExportsJS: features not compatible with js+wasm') + return + + # Make sure the testcase runs by itself - there should be no invalid + # testcases. + original_wasm = 'orig.wasm' + run([in_bin('wasm-opt'), wat_file, '-o', original_wasm] + FEATURE_OPTS) + D8().run_js(js_file, original_wasm) + + # Modify the initial wat to get the pre-optimizations wasm. + pre_wasm = abspath('pre.wasm') + run([in_bin('wasm-opt'), input] + FEATURE_OPTS + [ + '-ttf', + '--fuzz-preserve-imports-exports', + '--initial-fuzz=' + wat_file, + '-o', pre_wasm, + '-g', + ]) + + # We successfully generated pre_wasm; stash it for possible reduction + # purposes later. + self.pre_wasm = pre_wasm + + # If we were given a wasm file, use that instead of all the above. We + # do this now, after creating pre_wasm, because we still need to consume + # all the randomness normally. + if os.environ.get('BINARYEN_TRUST_GIVEN_WASM'): + print('using given wasm', before_wasm) + pre_wasm = before_wasm + + # Pick a vm and run before we optimize the wasm. + vms = [ + D8(), + D8Liftoff(), + D8Turboshaft(), + ] + pre_vm = random.choice(vms) + pre = self.do_run(pre_vm, js_file, pre_wasm) + + # Optimize. + post_wasm = abspath('post.wasm') + cmd = [in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + opts + FEATURE_OPTS + print(' '.join(cmd)) + proc = subprocess.run(cmd, capture_output=True, text=True) + if proc.returncode: + if 'Invalid configureAll' in proc.stderr: + # We have a hard error on unfamiliar configureAll patterns atm. + # Mutation of configureAll will easily break that pattern, so we + # must ignore such cases. + note_ignored_vm_run('PreserveImportsExportsJS: bad configureAll') + return + + # Anything else is a problem. + print(proc.stderr) + raise Exception('opts failed') + + # Run after opts, in a random vm. + post_vm = random.choice(vms) + post = self.do_run(post_vm, js_file, post_wasm) + + # Compare + compare(pre, post, 'PreserveImportsExportsJS') + + def do_run(self, vm, js, wasm): + out = vm.run_js(js, wasm, checked=False) + + cleaned = [] + for line in out.splitlines(): + if 'RuntimeError:' in line or 'TypeError:' in line: + # This is part of an error like + # + # wasm-function[2]:0x273: RuntimeError: unreachable + # + # We must ignore the binary location, which opts can change. We + # must also remove the specific trap, as Binaryen can change + # that. + line = 'TRAP' + elif 'wasm://' in line or '()' in line: + # This is part of a stack trace like + # + # at wasm://wasm/12345678:wasm-function[42]:0x123 + # at () + # + # Ignore it, as traces differ based on optimizations. + continue + cleaned.append(line) + return '\n'.join(cleaned) + + def can_run_on_wasm(self, wasm): + return all_disallowed(DISALLOWED_FEATURES_IN_V8) + + # Test that we preserve branch hints properly. The invariant that we test here # is that, given correct branch hints (that is, the input wasm's branch hints # are always correct: a branch is taken iff the hint is that it is taken), then @@ -2322,7 +2465,8 @@ def handle(self, wasm): RoundtripText(), ClusterFuzz(), Two(), - PreserveImportsExports(), + PreserveImportsExportsRandom(), + PreserveImportsExportsJS(), BranchHintPreservation(), ] diff --git a/scripts/test/fuzzing.py b/scripts/test/fuzzing.py index 29e4cf0e4ac..8a23262280d 100644 --- a/scripts/test/fuzzing.py +++ b/scripts/test/fuzzing.py @@ -117,6 +117,10 @@ 'waitqueue.wast', # TODO: fix handling of the non-utf8 names here 'name-high-bytes.wast', + # JS interop testcases have complex js-wasm interactions + 'js_interop_counter.wat', + 'js_interop_cases.wat', + 'js_interop_corners.wat', ] diff --git a/test/js_wasm/js_interop_cases.mjs b/test/js_wasm/js_interop_cases.mjs new file mode 100644 index 00000000000..c54758b2b98 --- /dev/null +++ b/test/js_wasm/js_interop_cases.mjs @@ -0,0 +1,56 @@ +let protoFactory = new Proxy({}, { + get(target, prop, receiver) { + // Always return a fresh, empty object. + return {}; + } +}); + +let constructors = {}; + +let imports = { + "protos": protoFactory, + "env": { constructors }, +}; + +let compileOptions = { builtins: ["js-prototypes"] }; + +let buffer = readbuffer(arguments[0]); + +let { module, instance } = + await WebAssembly.instantiate(buffer, imports, compileOptions); + +let Base = constructors.Base; +let Derived = constructors.Derived; + +// Test Base +console.log("Testing Base..."); +let b = new Base(10); +console.log("b.getValue():", b.getValue()); // 10 +console.log("b.value getter:", b.value); // 10 +b.value = 20; +console.log("b.value after setter:", b.getValue()); // 20 +console.log("b instanceof Base:", b instanceof Base); // true +console.log("b instanceof Derived:", b instanceof Derived); // false + +// Test Derived +console.log("\nTesting Derived..."); +let d = new Derived(100, 500); +console.log("d.getValue() (inherited):", d.getValue()); // 100 +console.log("d.getExtra():", d.getExtra()); // 500 +console.log("d.value getter (inherited):", d.value); // 100 +d.value = 150; +console.log("d.value after setter (inherited):", d.getValue()); // 150 +console.log("d instanceof Derived:", d instanceof Derived); // true +console.log("d instanceof Base (inheritance):", d instanceof Base); // true +console.log("Derived.staticMethod():", Derived.staticMethod()); // 42 + +// Test Wasm-side descriptor checks +console.log("\nTesting Wasm-side descriptor checks..."); +console.log("checkDesc(b):", instance.exports.checkDesc(b)); // 1 +console.log("checkDesc(d):", instance.exports.checkDesc(d)); // 2 +console.log("isDerived(b):", instance.exports.isDerived(b)); // 0 +console.log("isDerived(d):", instance.exports.isDerived(d)); // 1 + +// Test cross-checks +console.log("\nTesting cross-checks..."); +console.log("get_base_val(d):", instance.exports.get_base_val(d)); // 150 diff --git a/test/js_wasm/js_interop_cases.wat b/test/js_wasm/js_interop_cases.wat new file mode 100644 index 00000000000..b5685793c98 --- /dev/null +++ b/test/js_wasm/js_interop_cases.wat @@ -0,0 +1,161 @@ +(module + (rec + (type $Base (sub (descriptor $Base.vtable) (struct (field $val (mut i32))))) + (type $Base.vtable (sub (describes $Base) (struct + (field $proto (ref extern)) + (field $getValue (ref $getValue_t)) + (field $setValue (ref $setValue_t)) + ))) + (type $getValue_t (func (param (ref null $Base)) (result i32))) + (type $setValue_t (func (param (ref null $Base)) (param i32))) + + (type $Derived (sub $Base (descriptor $Derived.vtable) (struct (field $val (mut i32)) (field $extra i32)))) + (type $Derived.vtable (sub $Base.vtable (describes $Derived) (struct + (field $proto (ref extern)) + (field $getValue (ref $getValue_t)) + (field $setValue (ref $setValue_t)) + (field $getExtra (ref $getExtra_t)) + ))) + (type $getExtra_t (func (param (ref null $Derived)) (result i32))) + (type $staticMethod_t (func (result i32))) + ) + + (type $newBase_t (func (param i32) (result (ref $Base)))) + (type $newDerived_t (func (param i32 i32) (result (ref $Derived)))) + + ;; Types for prototype configuration + (type $prototypes (array (mut externref))) + (type $functions (array (mut funcref))) + (type $data (array (mut i8))) + (type $configureAll (func (param (ref null $prototypes)) + (param (ref null $functions)) + (param (ref null $data)) + (param externref))) + + (import "protos" "Base.proto" (global $Base.proto (ref extern))) + (import "protos" "Derived.proto" (global $Derived.proto (ref extern))) + + (import "env" "constructors" (global $constructors externref)) + + (import "wasm:js-prototypes" "configureAll" + (func $configureAll (type $configureAll))) + + (elem $prototypes externref + (global.get $Base.proto) + (global.get $Derived.proto) + ) + + (elem $functions funcref + (ref.func $Base.new) + (ref.func $Base.getValue) + (ref.func $Base.getValue) + (ref.func $Base.setValue) + (ref.func $Derived.new) + (ref.func $Derived.staticMethod) + (ref.func $Derived.getExtra) + ) + + ;; \02 - 2 protoconfigs + ;; Base: + ;; \01 - 1 constructorconfig + ;; \04Base - "Base" + ;; \00 - 0 static methods + ;; \03 - 3 methodconfigs + ;; \00\08getValue - method "getValue" + ;; \01\05value - getter "value" + ;; \02\05value - setter "value" + ;; \7f - parentidx -1 + ;; Derived: + ;; \01 - 1 constructorconfig + ;; \07Derived - "Derived" + ;; \01 - 1 static method + ;; \00\0cstaticMethod + ;; \01 - 1 methodconfig + ;; \00\08getExtra + ;; \00 - parentidx 0 (Base) + (data $data "\02\01\04Base\00\03\00\08getValue\01\05value\02\05value\7f\01\07Derived\01\00\0cstaticMethod\01\00\08getExtra\00") + + (global $Base.vtable (export "Base.vtable") (ref (exact $Base.vtable)) + (struct.new $Base.vtable + (global.get $Base.proto) + (ref.func $Base.getValue) + (ref.func $Base.setValue) + ) + ) + + (global $Derived.vtable (export "Derived.vtable") (ref (exact $Derived.vtable)) + (struct.new $Derived.vtable + (global.get $Derived.proto) + (ref.func $Base.getValue) + (ref.func $Base.setValue) + (ref.func $Derived.getExtra) + ) + ) + + (func $Base.new (type $newBase_t) (param $val i32) (result (ref $Base)) + (struct.new_desc $Base + (local.get $val) + (global.get $Base.vtable) + ) + ) + + (func $Base.getValue (type $getValue_t) (param $this (ref null $Base)) (result i32) + (struct.get $Base $val (local.get $this)) + ) + + (func $Base.setValue (type $setValue_t) (param $this (ref null $Base)) (param $val i32) + (struct.set $Base $val (local.get $this) (local.get $val)) + ) + + (func $Derived.new (type $newDerived_t) (param $val i32) (param $extra i32) (result (ref $Derived)) + (struct.new_desc $Derived + (local.get $val) + (local.get $extra) + (global.get $Derived.vtable) + ) + ) + + (func $Derived.getExtra (type $getExtra_t) (param $this (ref null $Derived)) (result i32) + (struct.get $Derived $extra (local.get $this)) + ) + + (func $Derived.staticMethod (type $staticMethod_t) (result i32) + (i32.const 42) + ) + + (func $start + (call $configureAll + (array.new_elem $prototypes $prototypes (i32.const 0) (i32.const 2)) + (array.new_elem $functions $functions (i32.const 0) (i32.const 7)) + (array.new_data $data $data (i32.const 0) (i32.const 70)) + (global.get $constructors) + ) + ) + + (start $start) + + ;; Additional tests for descriptor instructions + (func (export "get_base_val") (param $b (ref $Base)) (result i32) + (call $Base.getValue (local.get $b)) + ) + + (func (export "checkDesc") (param $b (ref $Base)) (result i32) + (block $derived (result (ref $Derived)) + (block $base (result (ref $Base)) + (br_on_cast_desc_eq $base (ref $Base) (ref $Base) (local.get $b) (global.get $Base.vtable)) + (br_on_cast_desc_eq $derived (ref $Base) (ref $Derived) (local.get $b) (global.get $Derived.vtable)) + (return (i32.const 0)) + ) + (return (i32.const 1)) + ) + (return (i32.const 2)) + ) + + (func (export "isDerived") (param $b (ref $Base)) (result i32) + (if (result i32) + (ref.test (ref $Derived) (local.get $b)) + (then (i32.const 1)) + (else (i32.const 0)) + ) + ) +) diff --git a/test/js_wasm/js_interop_corners.mjs b/test/js_wasm/js_interop_corners.mjs new file mode 100644 index 00000000000..75ed2feeb27 --- /dev/null +++ b/test/js_wasm/js_interop_corners.mjs @@ -0,0 +1,92 @@ +let protoFactory = new Proxy({}, { + get(target, prop, receiver) { + // Always return a fresh, empty object. + return {}; + } +}); + +let constructors = {}; + +let imports = { + "protos": protoFactory, + "env": { + constructors, + exact_func: (x) => x + 100, + }, +}; + +let compileOptions = { builtins: ["js-prototypes"] }; + +let buffer = readbuffer(arguments[0]); + +let { module, instance } = + await WebAssembly.instantiate(buffer, imports, compileOptions); + +// Test exact function import +console.log("call_exact(5):", instance.exports.call_exact(5)); + +// Test A (no constructor, just methods on prototype) +let a = instance.exports.newA(10); +console.log("a.getA():", a.getA()); +console.log("Object.getPrototypeOf(a) exists:", !!Object.getPrototypeOf(a)); + +// Test B (inherits from A) +let B = constructors.B; +let b = new B(20, 30); +console.log("b.getA():", b.getA()); +console.log("b.getB():", b.getB()); +console.log("b instanceof B:", b instanceof B); +console.log("b instanceof constructors.B:", b instanceof constructors.B); + +// Test C (inherits from B) +let C = constructors.C; +let c = new C(40, 50, 60); +console.log("c.getA():", c.getA()); +console.log("c.getB():", c.getB()); +console.log("c.getC():", c.getC()); +console.log("c instanceof C:", c instanceof C); +console.log("c instanceof B:", c instanceof B); +console.log("C.s1():", C.s1()); +console.log("C.s2():", C.s2()); + +// Test Meta-descriptor +let Meta = constructors.Meta; +let m = new Meta(70); +console.log("m.getM():", m.getM()); + +let mDesc = instance.exports.get_meta_desc(m); +console.log("mDesc.getVal():", mDesc.getVal()); +console.log("mDesc instanceof Object:", mDesc instanceof Object); +// The descriptor itself has a prototype configured! +console.log("mDesc.getVal inherited:", !!mDesc.getVal); + +// Test NoProto (invalid prototype source in descriptor) +let noProto = instance.exports.newNoProto(80); +try { + console.log("Object.getPrototypeOf(noProto):", Object.getPrototypeOf(noProto)); +} catch (e) { + console.log("Object.getPrototypeOf(noProto) threw:", e.name); +} + +// Test cast instructions +let bVtable = instance.exports.get_B_vtable(); +try { + let castedB = instance.exports.test_cast_desc_eq(b, bVtable); + console.log("test_cast_desc_eq(b, bVtable) succeeded:", !!castedB); +} catch (e) { + console.log("test_cast_desc_eq(b, bVtable) failed:", e.name); +} + +try { + instance.exports.test_cast_desc_eq(a, bVtable); + console.log("test_cast_desc_eq(a, bVtable) succeeded unexpectedly"); +} catch (e) { + console.log("test_cast_desc_eq(a, bVtable) failed as expected:", e.name); +} + +console.log("test_br_on_cast_desc_eq_fail(b, bVtable):", instance.exports.test_br_on_cast_desc_eq_fail(b, bVtable)); +console.log("test_br_on_cast_desc_eq_fail(a, bVtable):", instance.exports.test_br_on_cast_desc_eq_fail(a, bVtable)); + +// Test newDefault +let def = instance.exports.newDefault(); +console.log("newDefault exists:", !!def); diff --git a/test/js_wasm/js_interop_corners.wat b/test/js_wasm/js_interop_corners.wat new file mode 100644 index 00000000000..0fd013b58cf --- /dev/null +++ b/test/js_wasm/js_interop_corners.wat @@ -0,0 +1,168 @@ +(module + (rec + ;; A -> B -> C inheritance chain + (type $A (sub (descriptor $A.desc) (struct (field $a (mut i32))))) + (type $A.desc (sub (describes $A) (struct (field $proto (ref extern)) (field $getA (ref $getA_t))))) + (type $getA_t (func (param (ref null $A)) (result i32))) + + (type $B (sub $A (descriptor $B.desc) (struct (field $a (mut i32)) (field $b (mut i32))))) + (type $B.desc (sub $A.desc (describes $B) (struct (field $proto (ref extern)) (field $getA (ref $getA_t)) (field $getB (ref $getB_t))))) + (type $getB_t (func (param (ref null $B)) (result i32))) + + (type $C (sub $B (descriptor $C.desc) (struct (field $a (mut i32)) (field $b (mut i32)) (field $c (mut i32))))) + (type $C.desc (sub $B.desc (describes $C) (struct (field $proto (ref extern)) (field $getA (ref $getA_t)) (field $getB (ref $getB_t)) (field $getC (ref $getC_t))))) + (type $getC_t (func (param (ref null $C)) (result i32))) + + ;; Type with meta-descriptor (descriptor for the descriptor) + (type $Meta (sub (descriptor $Meta.desc) (struct (field $m (mut i32))))) + (type $Meta.desc (sub (describes $Meta) (descriptor $Meta.meta-desc) (struct (field $proto (ref extern)) (field $val i32) (field $getM (ref $getM_t))))) + (type $getM_t (func (param (ref null $Meta)) (result i32))) + (type $Meta.meta-desc (sub (describes $Meta.desc) (struct (field $proto (ref extern)) (field $metaVal i32) (field $getVal (ref $getVal_t))))) + (type $getVal_t (func (param (ref null $Meta.desc)) (result i32))) + + ;; Type with invalid prototype source (first field is i32, not externref) + (type $NoProto (sub (descriptor $NoProto.desc) (struct (field $x i32)))) + (type $NoProto.desc (sub (describes $NoProto) (struct (field $val i32)))) + + ;; Test struct.new_default_desc + (type $Default (sub (descriptor $Default.desc) (struct (field i32)))) + (type $Default.desc (sub (describes $Default) (struct (field (ref extern))))) + ) + + (type $exact_f_t (func (param i32) (result i32))) + (type $newB_t (func (param i32 i32) (result (ref $B)))) + (type $newC_t (func (param i32 i32 i32) (result (ref $C)))) + (type $newMeta_t (func (param i32) (result (ref $Meta)))) + + ;; Types for configureAll + (type $prototypes (array (mut externref))) + (type $functions (array (mut funcref))) + (type $data (array (mut i8))) + (type $configureAll (func (param (ref null $prototypes)) + (param (ref null $functions)) + (param (ref null $data)) + (param externref))) + + (import "wasm:js-prototypes" "configureAll" (func $configureAll (type $configureAll))) + (import "env" "constructors" (global $constructors externref)) + (import "protos" "A.proto" (global $A.proto (ref extern))) + (import "protos" "B.proto" (global $B.proto (ref extern))) + (import "protos" "C.proto" (global $C.proto (ref extern))) + (import "protos" "Meta.proto" (global $Meta.proto (ref extern))) + (import "protos" "MetaDesc.proto" (global $MetaDesc.proto (ref extern))) + + ;; Exact function import test + (import "env" "exact_func" (func $exact_func (exact (type $exact_f_t)))) + + (elem $prototypes externref + (global.get $A.proto) + (global.get $B.proto) + (global.get $C.proto) + (global.get $Meta.proto) + (global.get $MetaDesc.proto) + ) + + (elem $functions funcref + (ref.func $A.getA) ;; 0: method for A + (ref.func $B.new) ;; 1: constructor for B + (ref.func $B.getB) ;; 2: method for B + (ref.func $C.new) ;; 3: constructor for C + (ref.func $static1) ;; 4: static 1 for C + (ref.func $static2) ;; 5: static 2 for C + (ref.func $C.getC) ;; 6: method for C + (ref.func $Meta.new) ;; 7: constructor for Meta + (ref.func $Meta.getM) ;; 8: method for Meta + (ref.func $MetaDesc.getVal) ;; 9: method for MetaDesc + ) + + ;; \05 (5 protoconfigs) + ;; 1. A: \00 (0 constructors) \01 (1 method) \00\04getA \7f (parent -1) + ;; 2. B: \01 (1 constructor) \01B \00 (0 static) \01 (1 method) \00\04getB \00 (parent 0) + ;; 3. C: \01 (1 constructor) \01C \02 (2 static) \00\02s1 \00\02s2 \01 (1 method) \00\04getC \01 (parent 1) + ;; 4. Meta: \01 (1 constructor) \04Meta \00 (0 static) \01 (1 method) \00\04getM \7f (parent -1) + ;; 5. MetaDesc: \00 (0 constructors) \01 (1 method) \00\06getVal \7f (parent -1) + (data $data "\05\00\01\00\04getA\7f\01\01B\00\01\00\04getB\00\01\01C\02\00\02s1\00\02s2\01\00\04getC\01\01\04Meta\00\01\00\04getM\7f\00\01\00\06getVal\7f") + + (global $A.vtable (ref (exact $A.desc)) + (struct.new $A.desc (global.get $A.proto) (ref.func $A.getA)) + ) + (global $B.vtable (ref (exact $B.desc)) + (struct.new $B.desc (global.get $B.proto) (ref.func $A.getA) (ref.func $B.getB)) + ) + (global $C.vtable (ref (exact $C.desc)) + (struct.new $C.desc (global.get $C.proto) (ref.func $A.getA) (ref.func $B.getB) (ref.func $C.getC)) + ) + + (global $Meta.meta-vtable (ref (exact $Meta.meta-desc)) + (struct.new $Meta.meta-desc (global.get $MetaDesc.proto) (i32.const 42) (ref.func $MetaDesc.getVal)) + ) + (global $Meta.vtable (ref (exact $Meta.desc)) + (struct.new_desc $Meta.desc (global.get $Meta.proto) (i32.const 100) (ref.func $Meta.getM) (global.get $Meta.meta-vtable)) + ) + + (global $NoProto.vtable (ref (exact $NoProto.desc)) + (struct.new $NoProto.desc (i32.const 123)) + ) + + (func $A.getA (type $getA_t) (param $this (ref null $A)) (result i32) (struct.get $A $a (local.get $this))) + (func $B.getB (type $getB_t) (param $this (ref null $B)) (result i32) (struct.get $B $b (local.get $this))) + (func $C.getC (type $getC_t) (param $this (ref null $C)) (result i32) (struct.get $C $c (local.get $this))) + (func $Meta.getM (type $getM_t) (param $this (ref null $Meta)) (result i32) (struct.get $Meta $m (local.get $this))) + (func $MetaDesc.getVal (type $getVal_t) (param $this (ref null $Meta.desc)) (result i32) (struct.get $Meta.desc $val (local.get $this))) + (func $static1 (result i32) (i32.const 1)) + (func $static2 (result i32) (i32.const 2)) + + (func $A.new (export "newA") (param $a i32) (result (ref $A)) + (struct.new_desc $A (local.get $a) (global.get $A.vtable)) + ) + (func $B.new (type $newB_t) (param $a i32) (param $b i32) (result (ref $B)) + (struct.new_desc $B (local.get $a) (local.get $b) (global.get $B.vtable)) + ) + (func $C.new (type $newC_t) (param $a i32) (param $b i32) (param $c i32) (result (ref $C)) + (struct.new_desc $C (local.get $a) (local.get $b) (local.get $c) (global.get $C.vtable)) + ) + (func $Meta.new (type $newMeta_t) (param $m i32) (result (ref $Meta)) + (struct.new_desc $Meta (local.get $m) (global.get $Meta.vtable)) + ) + (func $NoProto.new (export "newNoProto") (param $x i32) (result (ref $NoProto)) + (struct.new_desc $NoProto (local.get $x) (global.get $NoProto.vtable)) + ) + + (func $start + (call $configureAll + (array.new_elem $prototypes $prototypes (i32.const 0) (i32.const 5)) + (array.new_elem $functions $functions (i32.const 0) (i32.const 10)) + (array.new_data $data $data (i32.const 0) (i32.const 68)) + (global.get $constructors) + ) + ) + (start $start) + + (func (export "get_meta_desc") (param $m (ref $Meta)) (result (ref $Meta.desc)) + (ref.get_desc $Meta (local.get $m)) + ) + + (func (export "test_cast_desc_eq") (param $a (ref $A)) (param $desc (ref (exact $B.desc))) (result (ref null $B)) + (ref.cast_desc_eq (ref null $B) (local.get $a) (local.get $desc)) + ) + + (func (export "test_br_on_cast_desc_eq_fail") (param $a (ref $A)) (param $desc (ref (exact $B.desc))) (result i32) + (block $fail (result (ref $A)) + (br_on_cast_desc_eq_fail $fail (ref $A) (ref $B) (local.get $a) (local.get $desc)) + (return (i32.const 1)) + ) + (return (i32.const 0)) + ) + + (func (export "get_B_vtable") (result (ref (exact $B.desc))) (global.get $B.vtable)) + + (global $Default.vtable (ref (exact $Default.desc)) (struct.new $Default.desc (global.get $A.proto))) + + (func (export "newDefault") (result (ref $Default)) + (struct.new_default_desc $Default (global.get $Default.vtable)) + ) + + (func (export "call_exact") (param i32) (result i32) + (call $exact_func (local.get 0)) + ) +) diff --git a/test/js_wasm/js_interop_counter.mjs b/test/js_wasm/js_interop_counter.mjs new file mode 100644 index 00000000000..3a646aa8f0a --- /dev/null +++ b/test/js_wasm/js_interop_counter.mjs @@ -0,0 +1,32 @@ +// https://github.com/WebAssembly/custom-descriptors/blob/main/proposals/custom-descriptors/Overview.md + +let protoFactory = new Proxy({}, { + get(target, prop, receiver) { + // Always return a fresh, empty object. + return {}; + } +}); + +let constructors = {}; + +let imports = { + "protos": protoFactory, + "env": { constructors }, +}; + +let compileOptions = { builtins: ["js-prototypes"] }; + +let buffer = readbuffer(arguments[0]); // XXX modified to read the wasm filename + +let { module, instance } = + await WebAssembly.instantiate(buffer, imports, compileOptions); + +let Counter = constructors.Counter; + +let count = new Counter(0); + +console.log(count.get()); +count.inc(); +console.log(count.get()); + +console.log(count instanceof Counter); diff --git a/test/js_wasm/js_interop_counter.wat b/test/js_wasm/js_interop_counter.wat new file mode 100644 index 00000000000..781816829ef --- /dev/null +++ b/test/js_wasm/js_interop_counter.wat @@ -0,0 +1,105 @@ +;; https://github.com/WebAssembly/custom-descriptors/blob/main/proposals/custom-descriptors/Overview.md + +(module + (rec + (type $counter (descriptor $counter.vtable) (struct (field $val (mut i32)))) + (type $counter.vtable (describes $counter) (struct + (field $proto (ref extern)) + (field $get (ref $get_t)) + (field $inc (ref $inc_t)) + )) + (type $get_t (func (param (ref null $counter)) (result i32))) + (type $inc_t (func (param (ref null $counter)))) + ) + (type $new_t (func (param i32) (result (ref $counter)))) + + ;; Types for prototype configuration + (type $prototypes (array (mut externref))) + (type $functions (array (mut funcref))) + (type $data (array (mut i8))) + (type $configureAll (func (param (ref null $prototypes)) + (param (ref null $functions)) + (param (ref null $data)) + (param externref))) + + (import "protos" "counter.proto" (global $counter.proto (ref extern))) + + ;; The object where configured constructors will be installed. + (import "env" "constructors" (global $constructors externref)) + + (import "wasm:js-prototypes" "configureAll" + (func $configureAll (type $configureAll))) + + ;; Segments used to create arrays passed to $configureAll + (elem $prototypes externref + (global.get $counter.proto) + ) + (elem $functions funcref + (ref.func $counter.new) + (ref.func $counter.get) + (ref.func $counter.inc) + ) + ;; \01 one protoconfig + ;; \01 one constructorconfig + ;; \07 length of name "Counter" + ;; Counter constructor name + ;; \00 no static methods + ;; \02 two methodconfigs + ;; \00 method (not getter or setter) + ;; \03 length of name "get" + ;; get method name + ;; \00 method (not getter or setter) + ;; \03 length of name "inc" + ;; inc method name + ;; \7f no parent prototype (-1 s32) + (data $data "\01\01\07Counter\00\02\00\03get\00\03inc\7f") + + (global $counter.vtable (ref (exact $counter.vtable)) + (struct.new $counter.vtable + (global.get $counter.proto) + (ref.func $counter.get) + (ref.func $counter.inc) + ) + ) + + (func $counter.get (type $get_t) (param (ref null $counter)) (result i32) + (struct.get $counter $val (local.get 0)) + ) + + (func $counter.inc (type $inc_t) (param (ref null $counter)) + (struct.set $counter $val + (local.get 0) + (i32.add + (struct.get $counter $val (local.get 0)) + (i32.const 1) + ) + ) + ) + + (func $counter.new (type $new_t) (param i32) (result (ref $counter)) + (struct.new_desc $counter + (local.get 0) + (global.get $counter.vtable) + ) + ) + + (func $start + (call $configureAll + (array.new_elem $prototypes $prototypes + (i32.const 0) + (i32.const 1) + ) + (array.new_elem $functions $functions + (i32.const 0) + (i32.const 3) + ) + (array.new_data $data $data + (i32.const 0) + (i32.const 23) + ) + (global.get $constructors) + ) + ) + + (start $start) +) From 54f9f7afa703ade4a34aa3291abbe237bb0cd8a4 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 14 Apr 2026 08:29:51 -0700 Subject: [PATCH 028/168] [NFC] Simplify lexer and move to header (#8597) The lexer previously used its own internal `LexerCtx` abstraction that allowed it to consume the characters that made up a token without changing the lexer state, then update the state at once when committing to consuming the characters. However, manually resetting the lexer to the original position when giving up on parsing a token is simple enough that this abstraction was not holding its weight. Simplify the lexer by removing internal contexts, and move the simplified method bodies to lexer.h. Generally we try to avoid putting lots of code in headers, but in this case making the code available to the inliner, along with removing the extra layer of abstraction, makes the parser about 20% faster. --- src/parser/CMakeLists.txt | 1 - src/parser/contexts.h | 2 +- src/parser/lexer.cpp | 1188 ------------------------------------- src/parser/lexer.h | 1095 +++++++++++++++++++++++++++++++--- test/gtest/wat-lexer.cpp | 11 + 5 files changed, 1038 insertions(+), 1259 deletions(-) delete mode 100644 src/parser/lexer.cpp diff --git a/src/parser/CMakeLists.txt b/src/parser/CMakeLists.txt index 8b7846ca9e9..7d4704dba24 100644 --- a/src/parser/CMakeLists.txt +++ b/src/parser/CMakeLists.txt @@ -2,7 +2,6 @@ FILE(GLOB parser_HEADERS *.h) set(parser_SOURCES context-decls.cpp context-defs.cpp - lexer.cpp parse-1-decls.cpp parse-2-typedefs.cpp parse-3-implicit-types.cpp diff --git a/src/parser/contexts.h b/src/parser/contexts.h index fbdd8d0505a..eb09a0bb3b0 100644 --- a/src/parser/contexts.h +++ b/src/parser/contexts.h @@ -1985,7 +1985,7 @@ struct ParseDefsCtx : TypeParserCtx, AnnotationParserCtx { void setSrcLoc(const std::vector& annotations) { const Annotation* annotation = nullptr; for (auto& a : annotations) { - if (a.kind == srcAnnotationKind) { + if (a.kind.str == std::string_view("src")) { annotation = &a; } } diff --git a/src/parser/lexer.cpp b/src/parser/lexer.cpp deleted file mode 100644 index 5d2aedabe66..00000000000 --- a/src/parser/lexer.cpp +++ /dev/null @@ -1,1188 +0,0 @@ -/* - * Copyright 2023 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. - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "lexer.h" -#include "support/bits.h" -#include "support/string.h" - -using namespace std::string_view_literals; - -namespace wasm::WATParser { - -Name srcAnnotationKind("src"); - -namespace { - -// ================ -// Lexical Analysis -// ================ - -// The result of lexing a token fragment. -struct LexResult { - std::string_view span; -}; - -// Lexing context that accumulates lexed input to produce a token fragment. -struct LexCtx { -private: - // The input we are lexing. - std::string_view input; - - // How much of the input we have already lexed. - size_t lexedSize = 0; - -public: - explicit LexCtx(std::string_view in) : input(in) {} - - // Return the fragment that has been lexed so far. - std::optional lexed() const { - if (lexedSize > 0) { - return {LexResult{input.substr(0, lexedSize)}}; - } - return {}; - } - - // The next input that has not already been lexed. - std::string_view next() const { return input.substr(lexedSize); } - - // Get the next character without consuming it. - uint8_t peek() const { return next()[0]; } - - // The size of the unlexed input. - size_t size() const { return input.size() - lexedSize; } - - // Whether there is no more input. - bool empty() const { return size() == 0; } - - // Tokens must be separated by spaces or parentheses. - bool canFinish() const; - - // Whether the unlexed input starts with prefix `sv`. - size_t startsWith(std::string_view sv) const { - return next().substr(0, sv.size()) == sv; - } - - // Consume the next `n` characters. - void take(size_t n) { lexedSize += n; } - - // Consume an additional lexed fragment. - void take(const LexResult& res) { lexedSize += res.span.size(); } - - // Consume the prefix and return true if possible. - bool takePrefix(std::string_view sv) { - if (startsWith(sv)) { - take(sv.size()); - return true; - } - return false; - } - - // Consume the rest of the input. - void takeAll() { lexedSize = input.size(); } -}; - -enum OverflowBehavior { DisallowOverflow, IgnoreOverflow }; - -std::optional getDigit(char c) { - if ('0' <= c && c <= '9') { - return c - '0'; - } - return {}; -} - -std::optional getHexDigit(char c) { - if ('0' <= c && c <= '9') { - return c - '0'; - } - if ('A' <= c && c <= 'F') { - return 10 + c - 'A'; - } - if ('a' <= c && c <= 'f') { - return 10 + c - 'a'; - } - return {}; -} - -enum Sign { NoSign, Pos, Neg }; - -// The result of lexing an integer token fragment. -struct LexIntResult : LexResult { - uint64_t n; - Sign sign; - - template bool isUnsigned() { - static_assert(std::is_integral_v && std::is_unsigned_v); - return sign == NoSign && n <= std::numeric_limits::max(); - } - - template bool isSigned() { - static_assert(std::is_integral_v && std::is_signed_v); - if (sign == Neg) { - return uint64_t(std::numeric_limits::min()) <= n || n == 0; - } - return n <= uint64_t(std::numeric_limits::max()); - } -}; - -// Lexing context that accumulates lexed input to produce an integer token -// fragment. -struct LexIntCtx : LexCtx { - using LexCtx::take; - -private: - uint64_t n = 0; - Sign sign = NoSign; - bool overflow = false; - -public: - explicit LexIntCtx(std::string_view in) : LexCtx(in) {} - - // Lex only the underlying span, ignoring the overflow and value. - std::optional lexedRaw() { - if (auto basic = LexCtx::lexed()) { - return LexIntResult{*basic, 0, NoSign}; - } - return {}; - } - - std::optional lexed() { - if (overflow) { - return {}; - } - if (auto basic = LexCtx::lexed()) { - return LexIntResult{*basic, sign == Neg ? -n : n, sign}; - } - return {}; - } - - void takeSign() { - if (takePrefix("+"sv)) { - sign = Pos; - } else if (takePrefix("-"sv)) { - sign = Neg; - } else { - sign = NoSign; - } - } - - bool takeDigit() { - if (!empty()) { - if (auto d = getDigit(peek())) { - take(1); - uint64_t newN = n * 10 + *d; - if (newN < n) { - overflow = true; - } - n = newN; - return true; - } - } - return false; - } - - bool takeHexdigit() { - if (!empty()) { - if (auto h = getHexDigit(peek())) { - take(1); - uint64_t newN = n * 16 + *h; - if (newN < n) { - overflow = true; - } - n = newN; - return true; - } - } - return false; - } - - void take(const LexIntResult& res) { - LexCtx::take(res); - n = res.n; - } -}; - -struct LexFloatResult : LexResult { - // The payload if we lexed a nan with payload. We cannot store the payload - // directly in `d` because we do not know at this point whether we are parsing - // an f32 or f64 and therefore we do not know what the allowable payloads are. - // No payload with NaN means to use the default payload for the expected float - // width. - std::optional nanPayload; - double d; -}; - -struct LexFloatCtx : LexCtx { - std::optional nanPayload; - - LexFloatCtx(std::string_view in) : LexCtx(in) {} - - std::optional lexed() { - const double posNan = std::copysign(NAN, 1.0); - const double negNan = std::copysign(NAN, -1.0); - assert(!std::signbit(posNan) && "expected positive NaN to be positive"); - assert(std::signbit(negNan) && "expected negative NaN to be negative"); - auto basic = LexCtx::lexed(); - if (!basic) { - return {}; - } - // strtod does not return NaNs with the expected signs on all platforms. - // TODO: use starts_with once we have C++20. - if (basic->span.substr(0, 3) == "nan"sv || - basic->span.substr(0, 4) == "+nan"sv) { - return LexFloatResult{*basic, nanPayload, posNan}; - } - if (basic->span.substr(0, 4) == "-nan"sv) { - return LexFloatResult{*basic, nanPayload, negNan}; - } - // Do not try to implement fully general and precise float parsing - // ourselves. Instead, call out to std::strtod to do our parsing. This means - // we need to strip any underscores since `std::strtod` does not understand - // them. - std::stringstream ss; - for (const char *curr = basic->span.data(), - *end = curr + basic->span.size(); - curr != end; - ++curr) { - if (*curr != '_') { - ss << *curr; - } - } - std::string str = ss.str(); - char* last; - double d = std::strtod(str.data(), &last); - assert(last == str.data() + str.size() && "could not parse float"); - return LexFloatResult{*basic, {}, d}; - } -}; - -struct LexStrResult : LexResult { - // Allocate a string only if there are escape sequences, otherwise just use - // the original string_view. - std::optional str; - - std::string_view getStr() { - if (str) { - return *str; - } - return span; - } -}; - -struct LexStrCtx : LexCtx { -private: - // Used to build a string with resolved escape sequences. Only used when the - // parsed string contains escape sequences, otherwise we can just use the - // parsed string directly. - std::optional escapeBuilder; - -public: - LexStrCtx(std::string_view in) : LexCtx(in) {} - - std::optional lexed() { - if (auto basic = LexCtx::lexed()) { - if (escapeBuilder) { - return LexStrResult{*basic, {escapeBuilder->str()}}; - } else { - return LexStrResult{*basic, {}}; - } - } - return {}; - } - - void takeChar() { - if (escapeBuilder) { - *escapeBuilder << peek(); - } - LexCtx::take(1); - } - - void ensureBuildingEscaped() { - if (escapeBuilder) { - return; - } - // Drop the opening '"'. - escapeBuilder = std::stringstream{}; - *escapeBuilder << LexCtx::lexed()->span.substr(1); - } - - void appendEscaped(char c) { *escapeBuilder << c; } - - bool appendUnicode(uint64_t u) { - if ((0xd800 <= u && u < 0xe000) || 0x110000 <= u) { - return false; - } - String::writeWTF8CodePoint(*escapeBuilder, u); - return true; - } -}; - -struct LexIdResult : LexResult { - bool isStr = false; - std::optional str; -}; - -struct LexIdCtx : LexCtx { - bool isStr = false; - std::optional str; - - LexIdCtx(std::string_view in) : LexCtx(in) {} - - std::optional lexed() { - if (auto basic = LexCtx::lexed()) { - return LexIdResult{*basic, isStr, str}; - } - return {}; - } -}; - -struct LexAnnotationResult : LexResult { - Annotation annotation; -}; - -struct LexAnnotationCtx : LexCtx { - std::string_view kind; - size_t kindSize = 0; - std::string_view contents; - size_t contentsSize = 0; - - explicit LexAnnotationCtx(std::string_view in) : LexCtx(in) {} - - void startKind() { kind = next(); } - - void takeKind(size_t size) { - kindSize += size; - take(size); - } - - void setKind(std::string_view kind) { - this->kind = kind; - kindSize = kind.size(); - } - - void startContents() { contents = next(); } - - void takeContents(size_t size) { - contentsSize += size; - take(size); - } - - std::optional lexed() { - if (auto basic = LexCtx::lexed()) { - return LexAnnotationResult{ - *basic, - {Name(kind.substr(0, kindSize)), contents.substr(0, contentsSize)}}; - } - return std::nullopt; - } -}; - -std::optional idchar(std::string_view); -std::optional space(std::string_view); -std::optional keyword(std::string_view); -std::optional integer(std::string_view); -std::optional float_(std::string_view); -std::optional str(std::string_view); -std::optional ident(std::string_view); - -// annotation ::= ';;@' [^\n]* | '(@'idchar+ annotelem* ')' -// annotelem ::= keyword | reserved | uN | sN | fN | string | id -// | '(' annotelem* ')' | '(@'idchar+ annotelem* ')' -std::optional annotation(std::string_view in) { - LexAnnotationCtx ctx(in); - if (ctx.takePrefix(";;@"sv)) { - ctx.setKind(srcAnnotationKind.str); - ctx.startContents(); - if (auto size = ctx.next().find('\n'); size != ""sv.npos) { - ctx.takeContents(size); - } else { - ctx.takeContents(ctx.next().size()); - } - } else if (ctx.takePrefix("(@"sv)) { - ctx.startKind(); - bool hasIdchar = false; - while (auto lexed = idchar(ctx.next())) { - ctx.takeKind(1); - hasIdchar = true; - } - if (!hasIdchar) { - return std::nullopt; - } - ctx.startContents(); - size_t depth = 1; - while (true) { - if (ctx.empty()) { - return std::nullopt; - } - if (auto lexed = space(ctx.next())) { - ctx.takeContents(lexed->span.size()); - continue; - } - if (auto lexed = keyword(ctx.next())) { - ctx.takeContents(lexed->span.size()); - continue; - } - if (auto lexed = integer(ctx.next())) { - ctx.takeContents(lexed->span.size()); - continue; - } - if (auto lexed = float_(ctx.next())) { - ctx.takeContents(lexed->span.size()); - continue; - } - if (auto lexed = str(ctx.next())) { - ctx.takeContents(lexed->span.size()); - continue; - } - if (auto lexed = ident(ctx.next())) { - ctx.takeContents(lexed->span.size()); - continue; - } - if (ctx.startsWith("(@"sv)) { - ctx.takeContents(2); - bool hasIdchar = false; - while (auto lexed = idchar(ctx.next())) { - ctx.takeContents(1); - hasIdchar = true; - } - if (!hasIdchar) { - return std::nullopt; - } - ++depth; - continue; - } - if (ctx.startsWith("("sv)) { - ctx.takeContents(1); - ++depth; - continue; - } - if (ctx.startsWith(")"sv)) { - --depth; - if (depth == 0) { - ctx.take(1); - break; - } - ctx.takeContents(1); - continue; - } - // Unrecognized token. - return std::nullopt; - } - } - return ctx.lexed(); -} - -// comment ::= linecomment | blockcomment -// linecomment ::= ';;' linechar* ('\n' | eof) -// linechar ::= c:char (if c != '\n') -// blockcomment ::= '(;' blockchar* ';)' -// blockchar ::= c:char (if c != ';' and c != '(') -// | ';' (if the next char is not ')') -// | '(' (if the next char is not ';') -// | blockcomment -std::optional comment(std::string_view in) { - LexCtx ctx(in); - if (ctx.size() < 2) { - return {}; - } - - // Line comment - if (!ctx.startsWith(";;@"sv) && ctx.takePrefix(";;"sv)) { - if (auto size = ctx.next().find('\n'); size != ""sv.npos) { - ctx.take(size); - } else { - ctx.takeAll(); - } - return ctx.lexed(); - } - - // Block comment (possibly nested!) - if (ctx.takePrefix("(;"sv)) { - size_t depth = 1; - while (depth > 0 && ctx.size() >= 2) { - if (ctx.takePrefix("(;"sv)) { - ++depth; - } else if (ctx.takePrefix(";)"sv)) { - --depth; - } else { - ctx.take(1); - } - } - if (depth > 0) { - // TODO: Add error production for non-terminated block comment. - return {}; - } - return ctx.lexed(); - } - - return {}; -} - -std::optional spacechar(std::string_view in) { - LexCtx ctx(in); - ctx.takePrefix(" "sv) || ctx.takePrefix("\n"sv) || ctx.takePrefix("\r"sv) || - ctx.takePrefix("\t"sv); - return ctx.lexed(); -} - -// space ::= (' ' | format | comment)* -// format ::= '\t' | '\n' | '\r' -std::optional space(std::string_view in) { - LexCtx ctx(in); - while (ctx.size()) { - if (auto lexed = spacechar(ctx.next())) { - ctx.take(*lexed); - } else if (auto lexed = comment(ctx.next())) { - ctx.take(*lexed); - } else { - break; - } - } - return ctx.lexed(); -} - -bool LexCtx::canFinish() const { - // Logically we want to check for eof, parens, and space. But we don't - // actually want to parse more than a couple characters of space, so check for - // individual space chars or comment starts instead. - return empty() || startsWith("("sv) || startsWith(")"sv) || - spacechar(next()) || startsWith(";;"sv); -} - -// num ::= d:digit => d -// | n:num '_'? d:digit => 10*n + d -// digit ::= '0' => 0 | ... | '9' => 9 -std::optional num(std::string_view in, - OverflowBehavior overflow = DisallowOverflow) { - LexIntCtx ctx(in); - if (ctx.empty()) { - return {}; - } - if (!ctx.takeDigit()) { - return {}; - } - while (true) { - bool under = ctx.takePrefix("_"sv); - if (!ctx.takeDigit()) { - if (!under) { - return overflow == DisallowOverflow ? ctx.lexed() : ctx.lexedRaw(); - } - // TODO: Add error production for trailing underscore. - return {}; - } - } -} - -// hexnum ::= h:hexdigit => h -// | n:hexnum '_'? h:hexdigit => 16*n + h -// hexdigit ::= d:digit => d -// | 'A' => 10 | ... | 'F' => 15 -// | 'a' => 10 | ... | 'f' => 15 -std::optional -hexnum(std::string_view in, OverflowBehavior overflow = DisallowOverflow) { - LexIntCtx ctx(in); - if (!ctx.takeHexdigit()) { - return {}; - } - while (true) { - bool under = ctx.takePrefix("_"sv); - if (!ctx.takeHexdigit()) { - if (!under) { - return overflow == DisallowOverflow ? ctx.lexed() : ctx.lexedRaw(); - } - // TODO: Add error production for trailing underscore. - return {}; - } - } -} - -// uN ::= n:num => n (if n < 2^N) -// | '0x' n:hexnum => n (if n < 2^N) -// sN ::= s:sign n:num => [s]n (if -2^(N-1) <= [s]n < 2^(N-1)) -// | s:sign '0x' n:hexnum => [s]n (if -2^(N-1) <= [s]n < 2^(N-1)) -// sign ::= {} => + | '+' => + | '-' => - -// -// Note: Defer bounds and sign checking until we know what kind of integer we -// expect. -std::optional integer(std::string_view in) { - LexIntCtx ctx(in); - ctx.takeSign(); - if (ctx.takePrefix("0x"sv)) { - if (auto lexed = hexnum(ctx.next())) { - ctx.take(*lexed); - if (ctx.canFinish()) { - return ctx.lexed(); - } - } - // TODO: Add error production for unrecognized hexnum. - return {}; - } - if (auto lexed = num(ctx.next())) { - ctx.take(*lexed); - if (ctx.canFinish()) { - return ctx.lexed(); - } - } - return {}; -} - -// float ::= p:num '.'? => p -// | p:num '.' q:frac => p + q -// | p:num '.'? ('E'|'e') s:sign e:num => p * 10^([s]e) -// | p:num '.' q:frac ('E'|'e') s:sign e:num => (p + q) * 10^([s]e) -// frac ::= d:digit => d/10 -// | d:digit '_'? p:frac => (d + p/10) / 10 -std::optional decfloat(std::string_view in) { - LexCtx ctx(in); - if (auto lexed = num(ctx.next(), IgnoreOverflow)) { - ctx.take(*lexed); - } else { - return {}; - } - // Optional '.' followed by optional frac - if (ctx.takePrefix("."sv)) { - if (auto lexed = num(ctx.next(), IgnoreOverflow)) { - ctx.take(*lexed); - } - } - if (ctx.takePrefix("E"sv) || ctx.takePrefix("e"sv)) { - // Optional sign - ctx.takePrefix("+"sv) || ctx.takePrefix("-"sv); - if (auto lexed = num(ctx.next(), IgnoreOverflow)) { - ctx.take(*lexed); - } else { - // TODO: Add error production for missing exponent. - return {}; - } - } - return ctx.lexed(); -} - -// hexfloat ::= '0x' p:hexnum '.'? => p -// | '0x' p:hexnum '.' q:hexfrac => p + q -// | '0x' p:hexnum '.'? ('P'|'p') s:sign e:num => p * 2^([s]e) -// | '0x' p:hexnum '.' q:hexfrac ('P'|'p') s:sign e:num -// => (p + q) * 2^([s]e) -// hexfrac ::= h:hexdigit => h/16 -// | h:hexdigit '_'? p:hexfrac => (h + p/16) / 16 -std::optional hexfloat(std::string_view in) { - LexCtx ctx(in); - if (!ctx.takePrefix("0x"sv)) { - return {}; - } - if (auto lexed = hexnum(ctx.next(), IgnoreOverflow)) { - ctx.take(*lexed); - } else { - return {}; - } - // Optional '.' followed by optional hexfrac - if (ctx.takePrefix("."sv)) { - if (auto lexed = hexnum(ctx.next(), IgnoreOverflow)) { - ctx.take(*lexed); - } - } - if (ctx.takePrefix("P"sv) || ctx.takePrefix("p"sv)) { - // Optional sign - ctx.takePrefix("+"sv) || ctx.takePrefix("-"sv); - if (auto lexed = num(ctx.next(), IgnoreOverflow)) { - ctx.take(*lexed); - } else { - // TODO: Add error production for missing exponent. - return {}; - } - } - return ctx.lexed(); -} - -// fN ::= s:sign z:fNmag => [s]z -// fNmag ::= z:float => float_N(z) (if float_N(z) != +/-infinity) -// | z:hexfloat => float_N(z) (if float_N(z) != +/-infinity) -// | 'inf' => infinity -// | 'nan' => nan(2^(signif(N)-1)) -// | 'nan:0x' n:hexnum => nan(n) (if 1 <= n < 2^signif(N)) -std::optional float_(std::string_view in) { - LexFloatCtx ctx(in); - // Optional sign - ctx.takePrefix("+"sv) || ctx.takePrefix("-"sv); - if (auto lexed = hexfloat(ctx.next())) { - ctx.take(*lexed); - } else if (auto lexed = decfloat(ctx.next())) { - ctx.take(*lexed); - } else if (ctx.takePrefix("inf"sv)) { - // nop - } else if (ctx.takePrefix("nan"sv)) { - if (ctx.takePrefix(":0x"sv)) { - if (auto lexed = hexnum(ctx.next())) { - ctx.take(*lexed); - ctx.nanPayload = lexed->n; - } else { - // TODO: Add error production for malformed NaN payload. - return {}; - } - } else { - // No explicit payload necessary; we will inject the default payload - // later. - } - } else { - return {}; - } - if (ctx.canFinish()) { - return ctx.lexed(); - } - return {}; -} - -// idchar ::= '0' | ... | '9' -// | 'A' | ... | 'Z' -// | 'a' | ... | 'z' -// | '!' | '#' | '$' | '%' | '&' | ''' | '*' | '+' -// | '-' | '.' | '/' | ':' | '<' | '=' | '>' | '?' -// | '@' | '\' | '^' | '_' | '`' | '|' | '~' -std::optional idchar(std::string_view in) { - LexCtx ctx(in); - if (ctx.empty()) { - return {}; - } - uint8_t c = ctx.peek(); - // All the allowed characters lie in the range '!' to '~', and within that - // range the vast majority of characters are allowed, so it is significantly - // faster to check for the disallowed characters instead. - if (c < '!' || c > '~') { - return ctx.lexed(); - } - switch (c) { - case '"': - case '(': - case ')': - case ',': - case ';': - case '[': - case ']': - case '{': - case '}': - return ctx.lexed(); - } - ctx.take(1); - return ctx.lexed(); -} - -// string ::= '"' (b*:stringelem)* '"' => concat((b*)*) -// (if |concat((b*)*)| < 2^32) -// stringelem ::= c:stringchar => utf8(c) -// | '\' n:hexdigit m:hexdigit => 16*n + m -// stringchar ::= c:char => c -// (if c >= U+20 && c != U+7f && c != '"' && c != '\') -// | '\t' => \t | '\n' => \n | '\r' => \r -// | '\\' => \ | '\"' => " | '\'' => ' -// | '\u{' n:hexnum '}' => U+(n) -// (if n < 0xD800 and 0xE000 <= n <= 0x110000) -std::optional str(std::string_view in) { - LexStrCtx ctx(in); - if (!ctx.takePrefix("\""sv)) { - return {}; - } - while (!ctx.takePrefix("\""sv)) { - if (ctx.empty()) { - // TODO: Add error production for unterminated string. - return {}; - } - if (ctx.startsWith("\\"sv)) { - // Escape sequences - ctx.ensureBuildingEscaped(); - ctx.take(1); - if (ctx.takePrefix("t"sv)) { - ctx.appendEscaped('\t'); - } else if (ctx.takePrefix("n"sv)) { - ctx.appendEscaped('\n'); - } else if (ctx.takePrefix("r"sv)) { - ctx.appendEscaped('\r'); - } else if (ctx.takePrefix("\\"sv)) { - ctx.appendEscaped('\\'); - } else if (ctx.takePrefix("\""sv)) { - ctx.appendEscaped('"'); - } else if (ctx.takePrefix("'"sv)) { - ctx.appendEscaped('\''); - } else if (ctx.takePrefix("u{"sv)) { - auto lexed = hexnum(ctx.next()); - if (!lexed) { - // TODO: Add error production for malformed unicode escapes. - return {}; - } - ctx.take(*lexed); - if (!ctx.takePrefix("}"sv)) { - // TODO: Add error production for malformed unicode escapes. - return {}; - } - if (!ctx.appendUnicode(lexed->n)) { - // TODO: Add error production for invalid unicode values. - return {}; - } - } else { - LexIntCtx ictx(ctx.next()); - if (!ictx.takeHexdigit() || !ictx.takeHexdigit()) { - // TODO: Add error production for unrecognized escape sequence. - return {}; - } - auto lexed = *ictx.lexed(); - ctx.take(lexed); - ctx.appendEscaped(char(lexed.n)); - } - } else { - // Normal characters - if (uint8_t c = ctx.peek(); c >= 0x20 && c != 0x7F) { - ctx.takeChar(); - } else { - // TODO: Add error production for unescaped control characters. - return {}; - } - } - } - return ctx.lexed(); -} - -// id ::= '$' idchar+ | '$' str -std::optional ident(std::string_view in) { - LexIdCtx ctx(in); - if (!ctx.takePrefix("$"sv)) { - return {}; - } - // Quoted identifier e.g. $"foo" - if (auto s = str(ctx.next())) { - if (!String::isUTF8(s->getStr())) { - return {}; - } - - // empty names, including $"" are not allowed. - if (s->span == "\"\"") { - return {}; - } - - ctx.isStr = true; - ctx.str = s->str; - ctx.take(*s); - } else if (auto lexed = idchar(ctx.next())) { - ctx.take(*lexed); - while (auto lexed = idchar(ctx.next())) { - ctx.take(*lexed); - } - } else { - return {}; - } - if (ctx.canFinish()) { - return ctx.lexed(); - } - return {}; -} - -// keyword ::= ( 'a' | ... | 'z' ) idchar* (if literal terminal in grammar) -// reserved ::= idchar+ -// -// The "keyword" token we lex here covers both keywords as well as any reserved -// tokens that match the keyword format. This saves us from having to enumerate -// all the valid keywords here. These invalid keywords will still produce -// errors, just at a higher level of the parser. -std::optional keyword(std::string_view in) { - LexCtx ctx(in); - if (ctx.empty()) { - return {}; - } - uint8_t start = ctx.peek(); - if ('a' <= start && start <= 'z') { - ctx.take(1); - } else { - return {}; - } - while (auto lexed = idchar(ctx.next())) { - ctx.take(*lexed); - } - return ctx.lexed(); -} - -} // anonymous namespace - -void Lexer::skipSpace() { - while (true) { - if (auto ctx = annotation(next())) { - pos += ctx->span.size(); - annotations.push_back(ctx->annotation); - continue; - } - if (auto ctx = space(next())) { - pos += ctx->span.size(); - continue; - } - break; - } -} - -std::optional Lexer::peekChar() const { - auto n = next(); - if (n.empty()) { - return std::nullopt; - } - - return n[0]; -} - -bool Lexer::takeLParen() { - if (LexCtx(next()).startsWith("("sv)) { - ++pos; - advance(); - return true; - } - return false; -} - -bool Lexer::takeRParen() { - if (LexCtx(next()).startsWith(")"sv)) { - ++pos; - advance(); - return true; - } - return false; -} - -std::optional Lexer::takeString() { - if (auto result = str(next())) { - pos += result->span.size(); - advance(); - if (result->str) { - return result->str; - } - // Remove quotes. - return std::string(result->span.substr(1, result->span.size() - 2)); - } - return std::nullopt; -} - -std::optional Lexer::takeID() { - if (auto result = ident(next())) { - pos += result->span.size(); - advance(); - if (result->str) { - return Name(*result->str); - } - if (result->isStr) { - // Remove '$' and quotes. - return Name(result->span.substr(2, result->span.size() - 3)); - } - // Remove '$'. - return Name(result->span.substr(1)); - } - return std::nullopt; -} - -std::optional Lexer::takeKeyword() { - if (auto result = keyword(next())) { - pos += result->span.size(); - advance(); - return result->span; - } - return std::nullopt; -} - -bool Lexer::takeKeyword(std::string_view expected) { - if (auto result = keyword(next()); result && result->span == expected) { - pos += expected.size(); - advance(); - return true; - } - return false; -} - -std::optional Lexer::takeOffset() { - if (auto result = keyword(next())) { - if (result->span.substr(0, 7) != "offset="sv) { - return std::nullopt; - } - Lexer subLexer(result->span.substr(7)); - if (auto o = subLexer.takeU64()) { - pos += result->span.size(); - advance(); - return o; - } - } - return std::nullopt; -} - -std::optional Lexer::takeAlign() { - if (auto result = keyword(next())) { - if (result->span.substr(0, 6) != "align="sv) { - return std::nullopt; - } - Lexer subLexer(result->span.substr(6)); - if (auto o = subLexer.takeU32()) { - if (Bits::popCount(*o) != 1) { - return std::nullopt; - } - pos += result->span.size(); - advance(); - return o; - } - } - return std::nullopt; -} - -template std::optional Lexer::takeU() { - static_assert(std::is_integral_v && std::is_unsigned_v); - if (auto result = integer(next()); result && result->isUnsigned()) { - pos += result->span.size(); - advance(); - return T(result->n); - } - // TODO: Add error production for unsigned overflow. - return std::nullopt; -} - -template std::optional Lexer::takeS() { - static_assert(std::is_integral_v && std::is_signed_v); - if (auto result = integer(next()); result && result->isSigned()) { - pos += result->span.size(); - advance(); - return T(result->n); - } - return std::nullopt; -} - -template std::optional Lexer::takeI() { - static_assert(std::is_integral_v && std::is_unsigned_v); - if (auto result = integer(next())) { - if (result->isUnsigned() || result->isSigned>()) { - pos += result->span.size(); - advance(); - return T(result->n); - } - } - return std::nullopt; -} - -template std::optional Lexer::takeU(); -template std::optional Lexer::takeS(); -template std::optional Lexer::takeI(); -template std::optional Lexer::takeU(); -template std::optional Lexer::takeS(); -template std::optional Lexer::takeI(); -template std::optional Lexer::takeU(); -template std::optional Lexer::takeS(); -template std::optional Lexer::takeI(); -template std::optional Lexer::takeU(); -template std::optional Lexer::takeS(); -template std::optional Lexer::takeI(); - -std::optional Lexer::takeF64() { - constexpr int signif = 52; - constexpr uint64_t payloadMask = (1ull << signif) - 1; - constexpr uint64_t nanDefault = 1ull << (signif - 1); - if (auto result = float_(next())) { - double d = result->d; - if (std::isnan(d)) { - // Inject payload. - uint64_t payload = result->nanPayload ? *result->nanPayload : nanDefault; - if (payload == 0 || payload > payloadMask) { - // TODO: Add error production for out-of-bounds payload. - return std::nullopt; - } - uint64_t bits; - static_assert(sizeof(bits) == sizeof(d)); - memcpy(&bits, &d, sizeof(bits)); - bits = (bits & ~payloadMask) | payload; - memcpy(&d, &bits, sizeof(bits)); - } - pos += result->span.size(); - advance(); - return d; - } - if (auto result = integer(next())) { - pos += result->span.size(); - advance(); - if (result->sign == Neg) { - if (result->n == 0) { - return -0.0; - } - return double(int64_t(result->n)); - } - return double(result->n); - } - return std::nullopt; -} - -std::optional Lexer::takeF32() { - constexpr int signif = 23; - constexpr uint32_t payloadMask = (1u << signif) - 1; - constexpr uint64_t nanDefault = 1ull << (signif - 1); - if (auto result = float_(next())) { - float f = result->d; - if (std::isnan(f)) { - // Validate and inject payload. - uint64_t payload = result->nanPayload ? *result->nanPayload : nanDefault; - if (payload == 0 || payload > payloadMask) { - // TODO: Add error production for out-of-bounds payload. - return std::nullopt; - } - uint32_t bits; - static_assert(sizeof(bits) == sizeof(f)); - memcpy(&bits, &f, sizeof(bits)); - bits = (bits & ~payloadMask) | payload; - memcpy(&f, &bits, sizeof(bits)); - } - pos += result->span.size(); - advance(); - return f; - } - if (auto result = integer(next())) { - pos += result->span.size(); - advance(); - if (result->sign == Neg) { - if (result->n == 0) { - return -0.0f; - } - return float(int64_t(result->n)); - } - return float(result->n); - } - return std::nullopt; -} - -TextPos Lexer::position(const char* c) const { - assert(size_t(c - buffer.data()) <= buffer.size()); - TextPos pos{1, 0}; - for (const char* p = buffer.data(); p != c; ++p) { - if (*p == '\n') { - pos.line++; - pos.col = 0; - } else { - pos.col++; - } - } - return pos; -} - -bool TextPos::operator==(const TextPos& other) const { - return line == other.line && col == other.col; -} - -std::ostream& operator<<(std::ostream& os, const TextPos& pos) { - return os << pos.line << ":" << pos.col; -} - -} // namespace wasm::WATParser diff --git a/src/parser/lexer.h b/src/parser/lexer.h index ac6549f0de8..3a9fd74e27a 100644 --- a/src/parser/lexer.h +++ b/src/parser/lexer.h @@ -14,34 +14,42 @@ * limitations under the License. */ +#ifndef parser_lexer_h +#define parser_lexer_h + +#include +#include #include #include #include -#include #include #include +#include #include #include +#include +#include "support/bits.h" #include "support/name.h" #include "support/result.h" #include "support/string.h" -#ifndef parser_lexer_h -#define parser_lexer_h - namespace wasm::WATParser { struct TextPos { size_t line; size_t col; - bool operator==(const TextPos& other) const; + bool operator==(const TextPos& other) const { + return line == other.line && col == other.col; + } bool operator!=(const TextPos& other) const { return !(*this == other); } - - friend std::ostream& operator<<(std::ostream& os, const TextPos& pos); }; +inline std::ostream& operator<<(std::ostream& os, const TextPos& pos) { + return os << pos.line << ":" << pos.col; +} + // =========== // Annotations // =========== @@ -51,8 +59,6 @@ struct Annotation { std::string_view contents; }; -extern Name srcAnnotationKind; - // ===== // Lexer // ===== @@ -66,10 +72,8 @@ struct Lexer { public: std::string_view buffer; - Lexer(std::string_view buffer, std::optional file = std::nullopt) - : file(file), buffer(buffer) { - setPos(0); - } + Lexer(std::string_view buffer, + std::optional file = std::nullopt); size_t getPos() const { return pos; } @@ -80,40 +84,23 @@ struct Lexer { std::optional peekChar() const; + bool peekLParen() { return !empty() && peek() == '('; } + bool takeLParen(); - bool peekLParen() { return Lexer(*this).takeLParen(); } + bool peekRParen() { return !empty() && peek() == ')'; } bool takeRParen(); - bool peekRParen() { return Lexer(*this).takeRParen(); } - - bool takeUntilParen() { - while (true) { - if (empty()) { - return false; - } - if (peekLParen() || peekRParen()) { - return true; - } - // Do not count the parentheses in strings. - if (takeString()) { - continue; - } - ++pos; - advance(); - } - } + bool takeUntilParen(); std::optional takeID(); + std::optional peekKeyword(); + std::optional takeKeyword(); bool takeKeyword(std::string_view expected); - std::optional peekKeyword() { - return Lexer(*this).takeKeyword(); - } - std::optional takeOffset(); std::optional takeAlign(); @@ -125,62 +112,38 @@ struct Lexer { std::optional takeU8() { return takeU(); } std::optional takeI8() { return takeI(); } - std::optional takeF64(); std::optional takeF32(); + std::optional takeF64(); std::optional takeString(); - std::optional takeName() { - auto str = takeString(); - if (!str || !String::isUTF8(*str)) { - return std::nullopt; - } - return Name(*str); - } + std::optional takeName(); - bool takeSExprStart(std::string_view expected) { - auto original = *this; - if (takeLParen() && takeKeyword(expected)) { - return true; - } - *this = original; - return false; - } + bool takeSExprStart(std::string_view expected); - bool peekSExprStart(std::string_view expected) { - auto original = *this; - if (!takeLParen()) { - return false; - } - bool ret = takeKeyword(expected); - *this = original; - return ret; - } + bool peekSExprStart(std::string_view expected); std::string_view next() const { return buffer.substr(pos); } + uint8_t peek() const { return buffer[pos]; } + void advance() { annotations.clear(); skipSpace(); } bool empty() const { return pos == buffer.size(); } + size_t remaining() const { return buffer.size() - pos; } TextPos position(const char* c) const; + TextPos position(size_t i) const { return position(buffer.data() + i); } TextPos position(std::string_view span) const { return position(span.data()); } TextPos position() const { return position(getPos()); } - [[nodiscard]] Err err(size_t pos, std::string reason) { - std::stringstream msg; - if (file) { - msg << *file << ":"; - } - msg << position(pos) << ": error: " << reason; - return Err{msg.str()}; - } + [[nodiscard]] Err err(size_t pos, std::string reason); [[nodiscard]] Err err(std::string reason) { return err(getPos(), reason); } @@ -192,13 +155,1007 @@ struct Lexer { } private: + // Whether the unlexed input starts with prefix `sv`. + size_t startsWith(std::string_view sv) const { + return next().starts_with(sv); + } + + // Consume the next `n` characters. + void take(size_t n) { pos += n; } + void takeAll() { pos = buffer.size(); } + + std::optional getDigit(char c); + + std::optional getHexDigit(char c); + + // Consume the prefix and return true if possible. + bool takePrefix(std::string_view sv); + + std::optional takeDigit(); + + std::optional takeHexdigit(); + + enum OverflowBehavior { DisallowOverflow, IgnoreOverflow }; + + std::optional takeNum(OverflowBehavior behavior = DisallowOverflow); + + std::optional + takeHexnum(OverflowBehavior behavior = DisallowOverflow); + + enum Sign { NoSign, Pos, Neg }; + + Sign takeSign(); + + struct LexedInteger { + uint64_t n; + Sign sign; + + template bool isUnsigned(); + template bool isSigned(); + }; + + std::optional takeInteger(); + template std::optional takeU(); + template std::optional takeS(); + template std::optional takeI(); + std::optional takeDecfloat(); + + std::optional takeHexfloat(); + + struct LexedFloat { + std::optional nanPayload; + double d; + }; + + std::optional takeFloat(); + + struct StringOrView : std::variant { + using std::variant::variant; + std::string_view str() const { + return std::visit([](auto& s) -> std::string_view { return s; }, *this); + } + }; + + std::optional takeStr(); + + bool idchar(); + + std::optional takeIdent(); + + bool spacechar(); + + bool takeSpacechar(); + + bool takeComment(); + + bool takeSpace(); + + std::optional takeAnnotation(); + void skipSpace(); + + bool canFinish(); }; +inline Lexer::Lexer(std::string_view buffer, std::optional file) + : file(file), buffer(buffer) { + setPos(0); +} + +inline std::optional Lexer::peekChar() const { + if (!empty()) { + return peek(); + } + return std::nullopt; +} + +inline bool Lexer::takeLParen() { + if (peekLParen()) { + take(1); + advance(); + return true; + } + return false; +} + +inline bool Lexer::takeRParen() { + if (peekRParen()) { + take(1); + advance(); + return true; + } + return false; +} + +inline bool Lexer::takeUntilParen() { + while (true) { + if (empty()) { + return false; + } + if (peekLParen() || peekRParen()) { + return true; + } + // Do not count the parentheses in strings. + if (takeString()) { + continue; + } + ++pos; + advance(); + } +} + +inline std::optional Lexer::takeID() { + if (auto result = takeIdent()) { + auto name = Name(result->str()); + advance(); + return name; + } + return std::nullopt; +} + +inline std::optional Lexer::peekKeyword() { + if (empty()) { + return std::nullopt; + } + auto startPos = pos; + uint8_t start = peek(); + if ('a' <= start && start <= 'z') { + take(1); + } else { + return std::nullopt; + } + while (idchar()) { + take(1); + } + auto ret = buffer.substr(startPos, pos - startPos); + pos = startPos; + return ret; +} + +inline std::optional Lexer::takeKeyword() { + auto keyword = peekKeyword(); + if (keyword) { + take(keyword->size()); + advance(); + } + return keyword; +} + +inline bool Lexer::takeKeyword(std::string_view expected) { + if (!startsWith(expected)) { + return false; + } + auto startPos = pos; + take(expected.size()); + if (canFinish()) { + advance(); + return true; + } + pos = startPos; + return false; +} + +inline std::optional Lexer::takeOffset() { + using namespace std::string_view_literals; + auto startPos = pos; + if (auto offset = takeKeyword()) { + if (!offset->starts_with("offset="sv)) { + pos = startPos; + return std::nullopt; + } + Lexer subLexer(offset->substr(7)); + if (auto o = subLexer.takeU64()) { + advance(); + return o; + } + } + pos = startPos; + return std::nullopt; +} + +inline std::optional Lexer::takeAlign() { + using namespace std::string_view_literals; + auto startPos = pos; + if (auto result = takeKeyword()) { + if (!result->starts_with("align="sv)) { + pos = startPos; + return std::nullopt; + } + Lexer subLexer(result->substr(6)); + if (auto o = subLexer.takeU32()) { + if (Bits::popCount(*o) != 1) { + pos = startPos; + return std::nullopt; + } + advance(); + return o; + } + } + pos = startPos; + return std::nullopt; +} + +inline std::optional Lexer::takeF32() { + constexpr int signif = 23; + constexpr uint32_t payloadMask = (1u << signif) - 1; + constexpr uint64_t nanDefault = 1ull << (signif - 1); + auto startPos = pos; + if (auto result = takeFloat()) { + float f = result->d; + if (std::isnan(f)) { + // Validate and inject payload. + uint64_t payload = result->nanPayload ? *result->nanPayload : nanDefault; + if (payload == 0 || payload > payloadMask) { + // TODO: Add error production for out-of-bounds payload. + pos = startPos; + return std::nullopt; + } + uint32_t bits; + static_assert(sizeof(bits) == sizeof(f)); + memcpy(&bits, &f, sizeof(bits)); + bits = (bits & ~payloadMask) | payload; + memcpy(&f, &bits, sizeof(bits)); + } + advance(); + return f; + } + if (auto result = takeInteger()) { + advance(); + if (result->sign == Neg) { + if (result->n == 0) { + return -0.0f; + } + return -static_cast(result->n); + } + return static_cast(result->n); + } + return std::nullopt; +} + +inline std::optional Lexer::takeF64() { + constexpr int signif = 52; + constexpr uint64_t payloadMask = (1ull << signif) - 1; + constexpr uint64_t nanDefault = 1ull << (signif - 1); + auto startPos = pos; + if (auto result = takeFloat()) { + double d = result->d; + if (std::isnan(d)) { + // Inject payload. + uint64_t payload = result->nanPayload ? *result->nanPayload : nanDefault; + if (payload == 0 || payload > payloadMask) { + // TODO: Add error production for out-of-bounds payload. + pos = startPos; + return std::nullopt; + } + uint64_t bits; + static_assert(sizeof(bits) == sizeof(d)); + memcpy(&bits, &d, sizeof(bits)); + bits = (bits & ~payloadMask) | payload; + memcpy(&d, &bits, sizeof(bits)); + } + advance(); + return d; + } + if (auto result = takeInteger()) { + advance(); + if (result->sign == Neg) { + if (result->n == 0) { + return -0.0; + } + return -static_cast(result->n); + } + return static_cast(result->n); + } + return std::nullopt; +} + +inline std::optional Lexer::takeString() { + if (auto str = takeStr()) { + advance(); + if (auto* s = std::get_if(&*str)) { + return std::move(*s); + } + auto view = std::get(*str); + return std::string(view); + } + return std::nullopt; +} + +inline std::optional Lexer::takeName() { + auto str = takeString(); + if (!str || !String::isUTF8(*str)) { + return std::nullopt; + } + return Name(*str); +} + +inline bool Lexer::takeSExprStart(std::string_view expected) { + auto original = *this; + if (takeLParen() && takeKeyword(expected)) { + return true; + } + *this = original; + return false; +} + +inline bool Lexer::peekSExprStart(std::string_view expected) { + auto original = *this; + if (!takeLParen()) { + return false; + } + bool ret = takeKeyword(expected); + *this = original; + return ret; +} + +inline TextPos Lexer::position(const char* c) const { + assert(size_t(c - buffer.data()) <= buffer.size()); + TextPos pos{1, 0}; + for (const char* p = buffer.data(); p != c; ++p) { + if (*p == '\n') { + pos.line++; + pos.col = 0; + } else { + pos.col++; + } + } + return pos; +} + +inline Err Lexer::err(size_t pos, std::string reason) { + std::stringstream msg; + if (file) { + msg << *file << ":"; + } + msg << position(pos) << ": error: " << reason; + return Err{msg.str()}; +} + +inline std::optional Lexer::getDigit(char c) { + if ('0' <= c && c <= '9') { + return c - '0'; + } + return std::nullopt; +} + +inline std::optional Lexer::getHexDigit(char c) { + if (auto d = getDigit(c)) { + return d; + } + if ('A' <= c && c <= 'F') { + return 10 + c - 'A'; + } + if ('a' <= c && c <= 'f') { + return 10 + c - 'a'; + } + return std::nullopt; +} + +inline bool Lexer::takePrefix(std::string_view sv) { + if (startsWith(sv)) { + take(sv.size()); + return true; + } + return false; +} + +inline std::optional Lexer::takeDigit() { + if (empty()) { + return std::nullopt; + } + if (auto d = getDigit(peek())) { + take(1); + return d; + } + return std::nullopt; +} + +inline std::optional Lexer::takeHexdigit() { + if (empty()) { + return std::nullopt; + } + if (auto h = getHexDigit(peek())) { + take(1); + return h; + } + return std::nullopt; +} + +inline std::optional Lexer::takeNum(OverflowBehavior behavior) { + using namespace std::string_view_literals; + auto startPos = pos; + bool overflow = false; + uint64_t n = 0; + if (auto d = takeDigit()) { + n = *d; + } else { + return std::nullopt; + } + while (true) { + bool under = takePrefix("_"sv); + if (auto d = takeDigit()) { + uint64_t newN = n * 10 + *d; + if (newN < n) { + overflow = true; + } + n = newN; + continue; + } + if (!under && (!overflow || behavior == IgnoreOverflow)) { + return n; + } + // TODO: Add error productions for trailing underscore and overflow. + pos = startPos; + return std::nullopt; + } +} + +inline std::optional Lexer::takeHexnum(OverflowBehavior behavior) { + using namespace std::string_view_literals; + auto startPos = pos; + bool overflow = false; + uint64_t n = 0; + if (auto d = takeHexdigit()) { + n = *d; + } else { + return std::nullopt; + } + while (true) { + bool under = takePrefix("_"sv); + if (auto d = takeHexdigit()) { + uint64_t newN = n * 16 + *d; + if (newN < n) { + overflow = true; + } + n = newN; + continue; + } + if (!under && (!overflow || behavior == IgnoreOverflow)) { + return n; + } + // TODO: Add error productions for trailing underscore and overflow. + pos = startPos; + return std::nullopt; + } +} + +inline Lexer::Sign Lexer::takeSign() { + auto c = peek(); + if (c == '+') { + take(1); + return Pos; + } + if (c == '-') { + take(1); + return Neg; + } + return NoSign; +} + +template bool Lexer::LexedInteger::isUnsigned() { + static_assert(std::is_integral_v && std::is_unsigned_v); + return sign == NoSign && n <= std::numeric_limits::max(); +} + +template bool Lexer::LexedInteger::isSigned() { + static_assert(std::is_integral_v && std::is_signed_v); + if (sign == Neg) { + // Absolute value of min() for two's complement integers is max() + 1. + uint64_t absMin = uint64_t(std::numeric_limits::max()) + 1; + return n <= absMin; + } + return n <= uint64_t(std::numeric_limits::max()); +} + +inline std::optional Lexer::takeInteger() { + using namespace std::string_view_literals; + auto startPos = pos; + auto sign = takeSign(); + if (takePrefix("0x"sv)) { + if (auto n = takeHexnum()) { + if (canFinish()) { + return LexedInteger{*n, sign}; + } + } + // TODO: Add error production for unrecognized hexnum. + pos = startPos; + return std::nullopt; + } + if (auto n = takeNum()) { + if (canFinish()) { + return LexedInteger{*n, sign}; + } + } + pos = startPos; + return std::nullopt; +} + +template std::optional Lexer::takeU() { + static_assert(std::is_integral_v && std::is_unsigned_v); + auto startPos = pos; + if (auto result = takeInteger(); result && result->isUnsigned()) { + advance(); + return static_cast(result->n); + } + // TODO: Add error production for unsigned overflow. + pos = startPos; + return std::nullopt; +} + +template std::optional Lexer::takeS() { + static_assert(std::is_integral_v && std::is_signed_v); + auto startPos = pos; + if (auto result = takeInteger(); result && result->isSigned()) { + advance(); + if (result->sign == Neg) { + return static_cast(-result->n); + } + return static_cast(result->n); + } + pos = startPos; + return std::nullopt; +} + +template std::optional Lexer::takeI() { + static_assert(std::is_integral_v && std::is_unsigned_v); + auto startPos = pos; + if (auto result = takeInteger()) { + if (result->isUnsigned() || result->isSigned>()) { + advance(); + if (result->sign == Neg) { + return static_cast(-result->n); + } + return static_cast(result->n); + } + } + pos = startPos; + return std::nullopt; +} + +inline std::optional Lexer::takeDecfloat() { + using namespace std::string_view_literals; + auto startPos = pos; + if (!takeNum(IgnoreOverflow)) { + return std::nullopt; + } + // Optional '.' followed by optional frac + if (takePrefix("."sv)) { + takeNum(IgnoreOverflow); + } + if (takePrefix("E"sv) || takePrefix("e"sv)) { + // Optional sign + takeSign(); + if (!takeNum(IgnoreOverflow)) { + // TODO: Add error production for missing exponent. + pos = startPos; + return std::nullopt; + } + } + return buffer.substr(startPos, pos - startPos); +} + +inline std::optional Lexer::takeHexfloat() { + using namespace std::string_view_literals; + auto startPos = pos; + if (!takePrefix("0x"sv)) { + return std::nullopt; + } + if (!takeHexnum(IgnoreOverflow)) { + pos = startPos; + return std::nullopt; + } + // Optional '.' followed by optional hexfrac + if (takePrefix("."sv)) { + takeHexnum(IgnoreOverflow); + } + if (takePrefix("P"sv) || takePrefix("p"sv)) { + // Optional sign + takeSign(); + if (!takeNum(IgnoreOverflow)) { + // TODO: Add error production for missing exponent. + pos = startPos; + return std::nullopt; + } + } + return buffer.substr(startPos, pos - startPos); +} + +inline std::optional Lexer::takeFloat() { + using namespace std::string_view_literals; + auto startPos = pos; + std::optional nanPayload; + bool isNan = false; + // Optional sign + auto sign = takeSign(); + if (takeHexfloat() || takeDecfloat() || takePrefix("inf"sv)) { + // nop. + } else if (takePrefix("nan"sv)) { + isNan = true; + if (takePrefix(":0x"sv)) { + if (auto n = takeHexnum()) { + nanPayload = n; + } else { + // TODO: Add error production for malformed NaN payload. + pos = startPos; + return std::nullopt; + } + } else { + // No explicit payload necessary; we will inject the default payload + // later. + } + } else { + pos = startPos; + return std::nullopt; + } + if (!canFinish()) { + pos = startPos; + return std::nullopt; + } + // strtod does not return NaNs with the expected signs on all platforms. + if (isNan) { + if (sign == Neg) { + const double negNan = std::copysign(NAN, -1.0); + assert(std::signbit(negNan) && "expected negative NaN to be negative"); + return LexedFloat{nanPayload, negNan}; + } else { + const double posNan = std::copysign(NAN, 1.0); + assert(!std::signbit(posNan) && "expected positive NaN to be positive"); + return LexedFloat{nanPayload, posNan}; + } + } + // Do not try to implement fully general and precise float parsing + // ourselves. Instead, call out to std::strtod to do our parsing. This means + // we need to strip any underscores since `std::strtod` does not understand + // them. + std::stringstream ss; + for (const char *curr = &buffer[startPos], *end = &buffer[pos]; curr != end; + ++curr) { + if (*curr != '_') { + ss << *curr; + } + } + std::string str = ss.str(); + char* last; + double d = std::strtod(str.data(), &last); + assert(last == str.data() + str.size() && "could not parse float"); + return LexedFloat{std::nullopt, d}; +} + +inline std::optional Lexer::takeStr() { + using namespace std::string_view_literals; + auto startPos = pos; + if (!takePrefix("\""sv)) { + return std::nullopt; + } + // Used to build a string with resolved escape sequences. Only used when the + // parsed string contains escape sequences, otherwise we can just use the + // parsed string directly. + std::optional escapeBuilder; + auto ensureBuildingEscaped = [&]() { + if (escapeBuilder) { + return; + } + // Drop the opening '"'. + escapeBuilder = std::stringstream{}; + *escapeBuilder << buffer.substr(startPos + 1, pos - startPos - 1); + }; + while (!takePrefix("\""sv)) { + if (empty()) { + // TODO: Add error production for unterminated string. + pos = startPos; + return std::nullopt; + } + if (startsWith("\\"sv)) { + // Escape sequences + ensureBuildingEscaped(); + take(1); + auto c = peek(); + take(1); + switch (c) { + case 't': + *escapeBuilder << '\t'; + break; + case 'n': + *escapeBuilder << '\n'; + break; + case 'r': + *escapeBuilder << '\r'; + break; + case '\\': + *escapeBuilder << '\\'; + break; + case '"': + *escapeBuilder << '"'; + break; + case '\'': + *escapeBuilder << '\''; + break; + case 'u': { + if (!takePrefix("{"sv)) { + pos = startPos; + return std::nullopt; + } + auto code = takeHexnum(); + if (!code) { + // TODO: Add error production for malformed unicode escapes. + pos = startPos; + return std::nullopt; + } + if (!takePrefix("}"sv)) { + // TODO: Add error production for malformed unicode escapes. + pos = startPos; + return std::nullopt; + } + if ((0xd800 <= *code && *code < 0xe000) || 0x110000 <= *code) { + // TODO: Add error production for invalid unicode values. + pos = startPos; + return std::nullopt; + } + String::writeWTF8CodePoint(*escapeBuilder, *code); + break; + } + default: { + // Byte escape: \hh + // We already took the first h as c. + auto first = getHexDigit(c); + auto second = takeHexdigit(); + if (!first || !second) { + // TODO: Add error production for unrecognized escape sequence. + pos = startPos; + return std::nullopt; + } + *escapeBuilder << char(*first * 16 + *second); + } + } + } else { + // Normal characters + if (uint8_t c = peek(); c >= 0x20 && c != 0x7F) { + if (escapeBuilder) { + *escapeBuilder << c; + } + take(1); + } else { + // TODO: Add error production for unescaped control characters. + pos = startPos; + return std::nullopt; + } + } + } + if (escapeBuilder) { + return escapeBuilder->str(); + } + // Drop the quotes. + return buffer.substr(startPos + 1, pos - startPos - 2); +} + +inline bool Lexer::idchar() { + if (empty()) { + return false; + } + uint8_t c = peek(); + // All the allowed characters lie in the range '!' to '~', and within that + // range the vast majority of characters are allowed, so it is significantly + // faster to check for the disallowed characters instead. + if (c < '!' || c > '~') { + return false; + } + switch (c) { + case '"': + case '(': + case ')': + case ',': + case ';': + case '[': + case ']': + case '{': + case '}': + return false; + } + return true; +} + +inline std::optional Lexer::takeIdent() { + using namespace std::string_view_literals; + auto startPos = pos; + if (!takePrefix("$"sv)) { + return {}; + } + // Quoted identifier e.g. $"foo" + std::optional str; + if ((str = takeStr())) { + if (str->str().empty() || !String::isUTF8(str->str())) { + pos = startPos; + return std::nullopt; + } + } else if (idchar()) { + take(1); + while (idchar()) { + take(1); + } + } else { + pos = startPos; + return std::nullopt; + } + if (canFinish()) { + if (str) { + return str; + } + // Drop the "$". + return buffer.substr(startPos + 1, pos - startPos - 1); + } + pos = startPos; + return std::nullopt; +} + +inline bool Lexer::spacechar() { + if (empty()) { + return false; + } + switch (peek()) { + case ' ': + case '\n': + case '\r': + case '\t': + return true; + default: + return false; + } +} + +inline bool Lexer::takeSpacechar() { + if (spacechar()) { + take(1); + return true; + } + return false; +} + +inline bool Lexer::takeComment() { + using namespace std::string_view_literals; + + if (remaining() < 2) { + return false; + } + + // Line comment + if (!startsWith(";;@"sv) && takePrefix(";;"sv)) { + if (auto size = next().find('\n'); size != ""sv.npos) { + take(size); + } else { + takeAll(); + } + return true; + } + + // Block comment (possibly nested!) + if (takePrefix("(;"sv)) { + size_t depth = 1; + while (depth > 0 && remaining() >= 2) { + if (takePrefix("(;"sv)) { + ++depth; + } else if (takePrefix(";)"sv)) { + --depth; + } else { + take(1); + } + } + if (depth > 0) { + // TODO: Add error production for non-terminated block comment. + return false; + } + return true; + } + + return false; +} + +inline bool Lexer::takeSpace() { + bool taken = false; + while (remaining() && (takeSpacechar() || takeComment())) { + taken = true; + continue; + } + return taken; +} + +inline std::optional Lexer::takeAnnotation() { + using namespace std::string_view_literals; + auto startPos = pos; + std::string_view kind; + std::string_view contents; + if (takePrefix(";;@"sv)) { + kind = "src"sv; + auto contentPos = pos; + if (auto size = next().find('\n'); size != ""sv.npos) { + take(size); + } else { + takeAll(); + } + contents = buffer.substr(contentPos, pos - contentPos); + } else if (takePrefix("(@"sv)) { + auto kindPos = pos; + bool hasIdchar = false; + while (idchar()) { + take(1); + hasIdchar = true; + } + if (!hasIdchar) { + pos = startPos; + return std::nullopt; + } + kind = buffer.substr(kindPos, pos - kindPos); + auto contentPos = pos; + size_t depth = 1; + while (true) { + if (empty()) { + pos = startPos; + return std::nullopt; + } + if (takeSpace() || takeKeyword() || takeInteger() || takeFloat() || + takeStr() || takeIdent()) { + continue; + } + if (takePrefix("(@"sv)) { + bool hasIdchar = false; + while (idchar()) { + take(1); + hasIdchar = true; + } + if (!hasIdchar) { + pos = startPos; + return std::nullopt; + } + ++depth; + continue; + } + if (takeLParen()) { + ++depth; + continue; + } + if (takePrefix(")"sv)) { + --depth; + if (depth == 0) { + break; + } + continue; + } + // Unrecognized token. + pos = startPos; + return std::nullopt; + } + contents = buffer.substr(contentPos, pos - contentPos - 1); + } else { + return std::nullopt; + } + return Annotation{Name(kind), contents}; +} + +inline void Lexer::skipSpace() { + while (true) { + if (auto annotation = takeAnnotation()) { + annotations.emplace_back(*std::move(annotation)); + continue; + } + if (takeSpace()) { + continue; + } + break; + } +} + +inline bool Lexer::canFinish() { + // Logically we want to check for eof, parens, and space. But we don't + // actually want to parse more than a couple characters of space, so check + // for individual space chars or comment starts instead. + using namespace std::string_view_literals; + return empty() || spacechar() || peek() == '(' || peek() == ')' || + startsWith(";;"sv); +} + } // namespace wasm::WATParser #endif // parser_lexer_h diff --git a/test/gtest/wat-lexer.cpp b/test/gtest/wat-lexer.cpp index a6f4f6d6cfe..3a4cd49e246 100644 --- a/test/gtest/wat-lexer.cpp +++ b/test/gtest/wat-lexer.cpp @@ -931,6 +931,17 @@ TEST(LexerTest, LexString) { EXPECT_FALSE(Lexer("\"too big \\u{110000}\""sv).takeString()); } +TEST(LexerTest, Annotations) { + Lexer lexer( + " (@metadata.code.branch_hint \"\\01\")\n (@metadata.code.branch_hint \"\\00\")\n (br_if $out"sv); + // Trigger advance/skipSpace which parses annotations. + lexer.takeID(); + auto annotations = lexer.takeAnnotations(); + ASSERT_EQ(annotations.size(), 2u); + EXPECT_EQ(annotations[0].contents, " \"\\01\""sv); + EXPECT_EQ(annotations[1].contents, " \"\\00\""sv); +} + TEST(LexerTest, LexKeywords) { Lexer lexer("module type func import rEsErVeD"); ASSERT_EQ(lexer.takeKeyword(), "module"sv); From fc43f0d6c882941df0688d2b912a3d689229ab58 Mon Sep 17 00:00:00 2001 From: Changqing Jing Date: Wed, 15 Apr 2026 02:14:00 +0800 Subject: [PATCH 029/168] [NFC] Use unordered containers for Name sets in SimplifyLocals and DuplicateFunctionElimination (#8600) This PR is proposed by https://github.com/WebAssembly/binaryen/pull/8586#issuecomment-4240228894 The passes get 0.7% faster. --- src/passes/DuplicateFunctionElimination.cpp | 2 +- src/passes/SimplifyLocals.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/passes/DuplicateFunctionElimination.cpp b/src/passes/DuplicateFunctionElimination.cpp index b86d657dc51..88253e00583 100644 --- a/src/passes/DuplicateFunctionElimination.cpp +++ b/src/passes/DuplicateFunctionElimination.cpp @@ -64,7 +64,7 @@ struct DuplicateFunctionElimination : public Pass { }); // Find actually equal functions and prepare to replace them std::map replacements; - std::set duplicates; + std::unordered_set duplicates; for (auto& [_, group] : hashGroups) { Index size = group.size(); if (size == 1) { diff --git a/src/passes/SimplifyLocals.cpp b/src/passes/SimplifyLocals.cpp index bf7902443bc..08791380a5e 100644 --- a/src/passes/SimplifyLocals.cpp +++ b/src/passes/SimplifyLocals.cpp @@ -102,11 +102,11 @@ struct SimplifyLocals // a list of all sinkable traces that exit a block. the last // is falling off the end, others are branches. this is used for // block returns - std::map> blockBreaks; + std::unordered_map> blockBreaks; // blocks that we can't produce a block return value for them. // (switch target, or some other reason) - std::set unoptimizableBlocks; + std::unordered_set unoptimizableBlocks; // A stack of sinkables from the current traversal state. When // execution reaches an if-else, it splits, and can then From 2fa35d65d6a2bf05266e396fe7e8de4376c40601 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 14 Apr 2026 16:11:23 -0700 Subject: [PATCH 030/168] [NFC] Simplify a bit of logic in RemoveUnusedBrs (#8603) The checks in the old code were not needed: we have a block that has a single child, another block. Any branch to the child sends a value that flows out to the parent immediately. No circumstances exist in which we can error. (block $outer (block $inner ..code and a br to $inner, which can branch to $outer instead.. ) ) Also, this code only handles blocks *without* a value, so even subtyping is not an issue here. (Merging blocks with different types is handled elsewhere, RemoveUnusedNames, so no need to add new logic here.) --- src/passes/RemoveUnusedBrs.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/passes/RemoveUnusedBrs.cpp b/src/passes/RemoveUnusedBrs.cpp index 77fd6840818..bc7e1e5b7c3 100644 --- a/src/passes/RemoveUnusedBrs.cpp +++ b/src/passes/RemoveUnusedBrs.cpp @@ -1224,11 +1224,7 @@ struct RemoveUnusedBrs : public WalkerPass> { // if this block has just one child, a sub-block, then jumps to the // former are jumps to us, really if (auto* child = list[0]->dynCast()) { - // the two blocks must have the same type for us to update the - // branch, as otherwise one block may be unreachable and the other - // concrete, so one might lack a value - if (child->name.is() && child->name != curr->name && - child->type == curr->type) { + if (child->name.is()) { redirectBranches(child, curr->name); } } From 95d88767a7543abbb42071457970433a9a0d7732 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 15 Apr 2026 10:05:02 -0700 Subject: [PATCH 031/168] [NFC] Skip parsing instructions in first parser pass (#8601) The first parser pass is responsible for two things: finding the locations of definitions of top-level module items like globals and functions and finding the locations of implicit function type definitions. It previously accomplished the latter by fully parsing every instruction in each function. But the IR is not constructed in this phase of parsing, so fully parsing every instruction was largely wasted work. Optimize the parser by parsing only the instructions that might have implicit type definitions and otherwise just blindly match parentheses to skip the function body. Combined with #8597, this speeds up parsing by 30-40%. --- src/parser/context-decls.cpp | 33 +++++++++++++++++++++++++++++++++ src/parser/contexts.h | 2 ++ src/parser/lexer.h | 18 +++++++++--------- src/parser/parsers.h | 6 ++++-- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/parser/context-decls.cpp b/src/parser/context-decls.cpp index 252185d6634..73327bd1640 100644 --- a/src/parser/context-decls.cpp +++ b/src/parser/context-decls.cpp @@ -15,6 +15,7 @@ */ #include "contexts.h" +#include "parsers.h" namespace wasm::WATParser { @@ -302,4 +303,36 @@ Result<> ParseDeclsCtx::addTag(Name name, return Ok{}; } +bool ParseDeclsCtx::skipFunctionBody() { + using namespace std::string_view_literals; + size_t depth = 1; + while (depth > 0 && !in.empty()) { + if (in.takeLParen()) { + ++depth; + continue; + } + if (in.takeRParen()) { + --depth; + continue; + } + if (auto kw = in.takeKeyword()) { + if (*kw == "block"sv || *kw == "loop"sv || *kw == "if"sv || + *kw == "try"sv || *kw == "try_table"sv) { + in.takeID(); + (void)typeuse(*this); + continue; + } + if (*kw == "call_indirect"sv || *kw == "return_call_indirect"sv) { + (void)maybeTableidx(*this); + (void)typeuse(*this, false); + continue; + } + continue; + } + in.take(1); + in.advance(); + } + return true; +} + } // namespace wasm::WATParser diff --git a/src/parser/contexts.h b/src/parser/contexts.h index eb09a0bb3b0..88b7d8a941c 100644 --- a/src/parser/contexts.h +++ b/src/parser/contexts.h @@ -1074,6 +1074,8 @@ struct ParseDeclsCtx : NullTypeParserCtx, NullInstrParserCtx { recTypeDefs.push_back({{}, pos, Index(recTypeDefs.size()), {}}); } + bool skipFunctionBody(); + Limits makeLimits(uint64_t n, std::optional m) { return Limits{n, m}; } diff --git a/src/parser/lexer.h b/src/parser/lexer.h index 3a9fd74e27a..2f5cb7a0291 100644 --- a/src/parser/lexer.h +++ b/src/parser/lexer.h @@ -82,6 +82,15 @@ struct Lexer { advance(); } + // Consume the next `n` characters. + void take(size_t n) { pos += n; } + void takeAll() { pos = buffer.size(); } + + // Whether the unlexed input starts with prefix `sv`. + size_t startsWith(std::string_view sv) const { + return next().starts_with(sv); + } + std::optional peekChar() const; bool peekLParen() { return !empty() && peek() == '('; } @@ -155,15 +164,6 @@ struct Lexer { } private: - // Whether the unlexed input starts with prefix `sv`. - size_t startsWith(std::string_view sv) const { - return next().starts_with(sv); - } - - // Consume the next `n` characters. - void take(size_t n) { pos += n; } - void takeAll() { pos = buffer.size(); } - std::optional getDigit(char c); std::optional getHexDigit(char c); diff --git a/src/parser/parsers.h b/src/parser/parsers.h index db41a2534cc..4a5fb71b771 100644 --- a/src/parser/parsers.h +++ b/src/parser/parsers.h @@ -3491,6 +3491,7 @@ template MaybeResult<> func(Ctx& ctx) { typename Ctx::TypeUseT type; Exactness exact = Exact; std::optional localVars; + bool skipped = false; if (import) { auto use = exacttypeuse(ctx); @@ -3505,13 +3506,14 @@ template MaybeResult<> func(Ctx& ctx) { CHECK_ERR(l); localVars = *l; } - if (!ctx.skipFunctionBody()) { + skipped = ctx.skipFunctionBody(); + if (!skipped) { CHECK_ERR(instrs(ctx)); ctx.setSrcLoc(ctx.in.takeAnnotations()); } } - if (!ctx.skipFunctionBody() && !ctx.in.takeRParen()) { + if ((import || !skipped) && !ctx.in.takeRParen()) { return ctx.in.err("expected end of function"); } From ea9820051110c37310bddb88bde3af186e4dcc85 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 15 Apr 2026 16:39:47 -0700 Subject: [PATCH 032/168] Heap2Local: Handle unreachable ref.test replacement (#8605) As with `local.get` in other cases, here we replace with something concrete (a `const`), and we can't do that if we became unreachable. --- src/passes/Heap2Local.cpp | 7 +++ test/lit/passes/heap2local-desc.wast | 68 ++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index ca5a4a3413c..59c1e7e3fc5 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -862,6 +862,13 @@ struct Struct2Local : PostWalker { return; } + if (curr->type == Type::unreachable) { + // We must not modify unreachable code here, as we will replace it with a + // const, which has a concrete type (similar to the situation with + // local.get in other cases in this pass). + return; + } + // This test operates on the allocation, which means we can compute whether // it will succeed statically. We do not even need // GCTypeUtils::evaluateCastCheck because we know the allocation's type diff --git a/test/lit/passes/heap2local-desc.wast b/test/lit/passes/heap2local-desc.wast index a6673463504..b6467beb544 100644 --- a/test/lit/passes/heap2local-desc.wast +++ b/test/lit/passes/heap2local-desc.wast @@ -1360,3 +1360,71 @@ ) ) ) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (descriptor $desc) (struct)) + (type $struct (descriptor $desc) (struct)) + ;; CHECK: (type $desc (sub (describes $struct) (struct))) + (type $desc (sub (describes $struct) (struct))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $test (type $2) + ;; CHECK-NEXT: (local $temp (ref $desc)) + ;; CHECK-NEXT: (local $1 (ref none)) + ;; CHECK-NEXT: (local $2 (ref none)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.test (ref none) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + (local $temp (ref $desc)) + (local.set $temp + (struct.new_default $desc) + ) + ;; The ref.test's input will become unreachable after we optimize. We should + ;; not emit a const for the test result, even though we know it, as this is + ;; unreachable code which would not validate. + (drop + (ref.test (ref none) + (ref.cast_desc_eq (ref $struct) + (struct.new_default_desc $struct + (ref.as_non_null + (ref.null none) + ) + ) + (local.get $temp) + ) + ) + ) + ) +) + From 4301eae42107ec446a49dd2849691010a0d3ba3a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 15 Apr 2026 17:10:34 -0700 Subject: [PATCH 033/168] Use fewer scratch locals in IRBuilder (#8608) When popping children for an expression, greedily pop none-typed expressions below the last value-producing expression in the stack, packaging all the popped instructions into a new block. This avoids leaving the none-typed expressions on top of the stack, which is good because having them on top of the stack would force the use of a scratch local when the next operand is popped. Besides producing better IR with fewer scratch locals, this also has the benefit of better round-tripping IR through binaries. The binary writer has an optimization where it will elide unnamed blocks because they cannot possibly be branch targets, but this could create "stacky" code that would previously introduce a scratch local when it was parsed back into IR. Now IRBuilder just recreates exactly the unnamed block that had been present in the IR in first place. While we don't generally guarantee that we can perfectly round-trip IR through binaries, this reduces the number of cases where round-trips lead to increased code size. Fixes #8413. --- src/wasm-ir-builder.h | 13 ++- src/wasm/wasm-ir-builder.cpp | 56 ++++++---- test/lit/basic/extra-branch-values.wast | 44 ++++---- test/lit/passes/roundtrip-gc.wast | 44 -------- test/lit/string.as_wtf16.wast | 132 ++++++++++-------------- test/lit/wat-kitchen-sink.wast | 40 +++---- test/stacky.wasm.fromBinary | 8 +- 7 files changed, 136 insertions(+), 201 deletions(-) delete mode 100644 test/lit/passes/roundtrip-gc.wast diff --git a/src/wasm-ir-builder.h b/src/wasm-ir-builder.h index 44b4ba0b270..92b8a8acfb1 100644 --- a/src/wasm-ir-builder.h +++ b/src/wasm-ir-builder.h @@ -713,15 +713,20 @@ class IRBuilder : public UnifiedExpressionVisitor> { Result addScratchLocal(Type); struct HoistedVal { - // The index in the stack of the original value-producing expression. - Index valIndex; + // The index in the stack of the deepest expression to be popped. This can + // be the original value-producing expression, or if we are popping + // greedily, it might be the deepest none-typed expression under the + // value-producing expression. + Index hoistIndex; // The local.get placed on the stack, if any. LocalGet* get; }; // Find the last value-producing expression, if any, and hoist its value to - // the top of the stack using a scratch local if necessary. - MaybeResult hoistLastValue(); + // the top of the stack using a scratch local if necessary. If `greedy`, then + // also include none-typed expressions and the value-producing expression in + // the hoisted range of expressions. + MaybeResult hoistLastValue(bool greedy = false); // Transform the stack as necessary such that the original producer of the // hoisted value will be popped along with the final expression that produces // the value, if they are different. May only be called directly after diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index 61aff65f24d..55ef512b103 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -61,29 +61,37 @@ Result IRBuilder::addScratchLocal(Type type) { return Builder::addVar(func, name, type); } -MaybeResult IRBuilder::hoistLastValue() { +MaybeResult IRBuilder::hoistLastValue(bool greedy) { auto& stack = getScope().exprStack; - int index = stack.size() - 1; - for (; index >= 0; --index) { - if (stack[index]->type != Type::none) { + int valIndex = stack.size() - 1; + for (; valIndex >= 0; --valIndex) { + if (stack[valIndex]->type != Type::none) { break; } } - if (index < 0) { + if (valIndex < 0) { // There is no value-producing or unreachable expression. return {}; } - if (unsigned(index) == stack.size() - 1) { + + int hoistIndex = valIndex; + if (greedy) { + while (hoistIndex > 0 && stack[hoistIndex - 1]->type == Type::none) { + --hoistIndex; + } + } + + if (unsigned(valIndex) == stack.size() - 1) { // Value-producing expression already on top of the stack. - return HoistedVal{Index(index), nullptr}; + return HoistedVal{Index(hoistIndex), nullptr}; } - auto*& expr = stack[index]; + auto*& expr = stack[valIndex]; if (expr->type == Type::unreachable) { // Make sure the top of the stack also has an unreachable expression. if (stack.back()->type != Type::unreachable) { pushSynthetic(builder.makeUnreachable()); } - return HoistedVal{Index(index), nullptr}; + return HoistedVal{Index(hoistIndex), nullptr}; } // Hoist with a scratch local. Normally the scratch local is the same type as // the hoisted expression, but we may need to adjust it given the enabled @@ -99,7 +107,7 @@ MaybeResult IRBuilder::hoistLastValue() { expr = builder.makeLocalSet(*scratchIdx, expr); auto* get = builder.makeLocalGet(*scratchIdx, type); pushSynthetic(get); - return HoistedVal{Index(index), get}; + return HoistedVal{Index(hoistIndex), get}; } Result<> IRBuilder::packageHoistedValue(const HoistedVal& hoisted, @@ -113,17 +121,17 @@ 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.valIndex, + std::vector exprs(scope.exprStack.begin() + hoisted.hoistIndex, scope.exprStack.end()); auto* block = builder.makeBlock(exprs, type); - scope.exprStack.resize(hoisted.valIndex); + scope.exprStack.resize(hoisted.hoistIndex); pushSynthetic(block); }; auto type = scope.exprStack.back()->type; if (type.size() == sizeHint || type.size() <= 1) { - if (hoisted.get) { + if (hoisted.hoistIndex < scope.exprStack.size() - 1) { packageAsBlock(type); } return Ok{}; @@ -379,8 +387,10 @@ struct IRBuilder::ChildPopper continue; } - // Pop a child normally. - auto val = pop(children[i].constraint.size()); + // Pop a child normally. Pop greedily for children other than the first + // (i.e. the last to be popped). + bool greedy = i > 0; + auto val = pop(children[i].constraint.size(), greedy); CHECK_ERR(val); *children[i].childp = *val; } @@ -458,12 +468,22 @@ struct IRBuilder::ChildPopper return false; } - Result pop(size_t size) { + // If `greedy`, then we will pop additional none-typed expressions that come + // before the value-producing expression. The additional expressions will be + // packaged into a block with the value-producing expression. This is better + // than leaving them on top of the stack, where they will force the use of a + // scratch local when the next operand is popped. `greedy` should be used when + // popping all children of an expression except the first (i.e. the + // last child to be popped). Not being greedy for the last popped child defers + // the creation of a block to hold its none-typed predecessors. It may turn + // out that such a block is not necessary, for example when the none-typed + // expressions can be included directly into a parent block scope. + Result pop(size_t size, bool greedy = false) { assert(size >= 1); auto& scope = builder.getScope(); // Find the suffix of expressions that do not produce values. - auto hoisted = builder.hoistLastValue(); + auto hoisted = builder.hoistLastValue(greedy); CHECK_ERR(hoisted); if (!hoisted) { // There are no expressions that produce values. @@ -489,7 +509,7 @@ struct IRBuilder::ChildPopper std::vector elems; elems.resize(size); for (int i = size - 1; i >= 0; --i) { - auto elem = pop(1); + auto elem = pop(1, greedy || i > 0); CHECK_ERR(elem); elems[i] = *elem; } diff --git a/test/lit/basic/extra-branch-values.wast b/test/lit/basic/extra-branch-values.wast index ca840036818..953ca7d634f 100644 --- a/test/lit/basic/extra-branch-values.wast +++ b/test/lit/basic/extra-branch-values.wast @@ -2383,17 +2383,15 @@ ;; CHECK-NEXT: (local $scratch_6 i32) ;; CHECK-NEXT: (local $scratch_7 i32) ;; CHECK-NEXT: (local $scratch_8 (ref any)) - ;; CHECK-NEXT: (local $scratch_9 i32) - ;; CHECK-NEXT: (local $scratch_10 anyref) - ;; CHECK-NEXT: (local $scratch_11 i32) - ;; CHECK-NEXT: (local $scratch_12 (ref any)) - ;; CHECK-NEXT: (local $scratch_13 i32) - ;; CHECK-NEXT: (local $scratch_14 eqref) + ;; CHECK-NEXT: (local $scratch_9 anyref) + ;; CHECK-NEXT: (local $scratch_10 i32) + ;; CHECK-NEXT: (local $scratch_11 (ref any)) + ;; CHECK-NEXT: (local $scratch_12 eqref) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (local.get $0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (block $label (type $0) (result i32 eqref) - ;; CHECK-NEXT: (local.set $scratch_14 + ;; CHECK-NEXT: (local.set $scratch_12 ;; CHECK-NEXT: (block $label0 (result eqref) ;; CHECK-NEXT: (br $label ;; CHECK-NEXT: (if (type $0) (result i32 eqref) @@ -2416,46 +2414,40 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (tuple.make 2 - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $scratch_9 - ;; CHECK-NEXT: (local.get $scratch_6) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_6) + ;; CHECK-NEXT: (block (result (ref eq)) ;; CHECK-NEXT: (global.set $any ;; CHECK-NEXT: (local.get $scratch_8) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $scratch_9) + ;; CHECK-NEXT: (global.get $eq) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (global.get $eq) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (else ;; CHECK-NEXT: (local.set $scratch_6 ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $scratch_11 + ;; CHECK-NEXT: (local.set $scratch_10 ;; CHECK-NEXT: (local.get $scratch) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $scratch_10 + ;; CHECK-NEXT: (local.set $scratch_9 ;; CHECK-NEXT: (local.get $3) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $scratch_11) + ;; CHECK-NEXT: (local.get $scratch_10) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $scratch_12 + ;; CHECK-NEXT: (local.set $scratch_11 ;; CHECK-NEXT: (br_on_cast $label0 anyref eqref - ;; CHECK-NEXT: (local.get $scratch_10) + ;; CHECK-NEXT: (local.get $scratch_9) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (tuple.make 2 - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $scratch_13 - ;; CHECK-NEXT: (local.get $scratch_6) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_6) + ;; CHECK-NEXT: (block (result eqref) ;; CHECK-NEXT: (global.set $any - ;; CHECK-NEXT: (local.get $scratch_12) + ;; CHECK-NEXT: (local.get $scratch_11) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $scratch_13) + ;; CHECK-NEXT: (global.get $eqref) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (global.get $eqref) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -2464,7 +2456,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (tuple.make 2 ;; CHECK-NEXT: (local.get $scratch_6) - ;; CHECK-NEXT: (local.get $scratch_14) + ;; CHECK-NEXT: (local.get $scratch_12) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) diff --git a/test/lit/passes/roundtrip-gc.wast b/test/lit/passes/roundtrip-gc.wast deleted file mode 100644 index 805d477b8a2..00000000000 --- a/test/lit/passes/roundtrip-gc.wast +++ /dev/null @@ -1,44 +0,0 @@ -;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. -;; RUN: wasm-opt %s -all --generate-stack-ir --optimize-stack-ir --roundtrip -S -o - | filecheck %s - -(module - (type $"{i32}" (struct (field i32))) - ;; CHECK: (export "export" (func $test)) - (export "export" (func $test)) - ;; CHECK: (func $test (type $1) - ;; CHECK-NEXT: (local $scratch (ref (exact $\7bi32\7d))) - ;; CHECK-NEXT: (call $help - ;; CHECK-NEXT: (block (result (ref (exact $\7bi32\7d))) - ;; CHECK-NEXT: (local.set $scratch - ;; CHECK-NEXT: (struct.new_default $\7bi32\7d) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $other) - ;; CHECK-NEXT: (local.get $scratch) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (call $help - (struct.new_default $"{i32}") - ;; Stack IR optimizations can remove this block, leaving a call in an odd - ;; "stacky" location. On load, we will use a local to work around that. It - ;; is fine for the local to be non-nullable since the get is later in that - ;; same block. - (block $block (result i32) - (call $other) - (i32.const 1) - ) - ) - ) - ;; CHECK: (func $help (type $2) (param $3 (ref $\7bi32\7d)) (param $4 i32) - ;; CHECK-NEXT: ) - (func $help (param $3 (ref $"{i32}")) (param $4 i32) - (nop) - ) - - ;; CHECK: (func $other (type $1) - ;; CHECK-NEXT: ) - (func $other - ) -) diff --git a/test/lit/string.as_wtf16.wast b/test/lit/string.as_wtf16.wast index 74bbc97eaff..db5cf6c746a 100644 --- a/test/lit/string.as_wtf16.wast +++ b/test/lit/string.as_wtf16.wast @@ -26,35 +26,27 @@ ;; CHECK-NEXT: ) ;; RTRIP: (func $codeunit (type $1) (result i32) ;; RTRIP-NEXT: (local $0 i32) - ;; RTRIP-NEXT: (local $scratch (ref string)) ;; RTRIP-NEXT: (stringview_wtf16.get_codeunit - ;; RTRIP-NEXT: (block (result (ref string)) - ;; RTRIP-NEXT: (local.set $scratch - ;; RTRIP-NEXT: (string.const "abc") - ;; RTRIP-NEXT: ) + ;; RTRIP-NEXT: (string.const "abc") + ;; RTRIP-NEXT: (block (result i32) ;; RTRIP-NEXT: (local.set $0 ;; RTRIP-NEXT: (i32.const 0) ;; RTRIP-NEXT: ) - ;; RTRIP-NEXT: (local.get $scratch) + ;; RTRIP-NEXT: (local.get $0) ;; RTRIP-NEXT: ) - ;; RTRIP-NEXT: (local.get $0) ;; RTRIP-NEXT: ) ;; RTRIP-NEXT: ) ;; RRTRP: (func $codeunit (type $1) (result i32) ;; RRTRP-NEXT: (local $0 i32) - ;; RRTRP-NEXT: (local $scratch (ref string)) - ;; RRTRP-NEXT: (local $scratch_2 (ref string)) + ;; RRTRP-NEXT: (local $1 i32) ;; RRTRP-NEXT: (stringview_wtf16.get_codeunit - ;; RRTRP-NEXT: (block (result (ref string)) - ;; RRTRP-NEXT: (local.set $scratch_2 - ;; RRTRP-NEXT: (string.const "abc") - ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.set $0 + ;; RRTRP-NEXT: (string.const "abc") + ;; RRTRP-NEXT: (block (result i32) + ;; RRTRP-NEXT: (local.set $1 ;; RRTRP-NEXT: (i32.const 0) ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.get $scratch_2) + ;; RRTRP-NEXT: (local.get $1) ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.get $0) ;; RRTRP-NEXT: ) ;; RRTRP-NEXT: ) (func $codeunit (result i32) @@ -109,12 +101,9 @@ ;; RTRIP-NEXT: (local $0 i32) ;; RTRIP-NEXT: (local $1 i32) ;; RTRIP-NEXT: (local $scratch i32) - ;; RTRIP-NEXT: (local $scratch_3 (ref string)) ;; RTRIP-NEXT: (stringview_wtf16.slice - ;; RTRIP-NEXT: (block (result (ref string)) - ;; RTRIP-NEXT: (local.set $scratch_3 - ;; RTRIP-NEXT: (string.const "abc") - ;; RTRIP-NEXT: ) + ;; RTRIP-NEXT: (string.const "abc") + ;; RTRIP-NEXT: (block (result i32) ;; RTRIP-NEXT: (local.set $0 ;; RTRIP-NEXT: (block (result i32) ;; RTRIP-NEXT: (local.set $scratch @@ -126,9 +115,8 @@ ;; RTRIP-NEXT: (local.get $scratch) ;; RTRIP-NEXT: ) ;; RTRIP-NEXT: ) - ;; RTRIP-NEXT: (local.get $scratch_3) + ;; RTRIP-NEXT: (local.get $0) ;; RTRIP-NEXT: ) - ;; RTRIP-NEXT: (local.get $0) ;; RTRIP-NEXT: (local.get $1) ;; RTRIP-NEXT: ) ;; RTRIP-NEXT: ) @@ -136,29 +124,29 @@ ;; RRTRP-NEXT: (local $0 i32) ;; RRTRP-NEXT: (local $1 i32) ;; RRTRP-NEXT: (local $scratch i32) - ;; RRTRP-NEXT: (local $scratch_3 (ref string)) - ;; RRTRP-NEXT: (local $scratch_4 i32) - ;; RRTRP-NEXT: (local $scratch_5 (ref string)) + ;; RRTRP-NEXT: (local $3 i32) + ;; RRTRP-NEXT: (local $4 i32) + ;; RRTRP-NEXT: (local $scratch_5 i32) ;; RRTRP-NEXT: (stringview_wtf16.slice - ;; RRTRP-NEXT: (block (result (ref string)) - ;; RRTRP-NEXT: (local.set $scratch_5 - ;; RRTRP-NEXT: (string.const "abc") - ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.set $0 + ;; RRTRP-NEXT: (string.const "abc") + ;; RRTRP-NEXT: (block (result i32) + ;; RRTRP-NEXT: (local.set $3 ;; RRTRP-NEXT: (block (result i32) - ;; RRTRP-NEXT: (local.set $scratch_4 + ;; RRTRP-NEXT: (local.set $scratch_5 ;; RRTRP-NEXT: (i32.const 1) ;; RRTRP-NEXT: ) ;; RRTRP-NEXT: (local.set $1 ;; RRTRP-NEXT: (i32.const 2) ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.get $scratch_4) + ;; RRTRP-NEXT: (local.set $4 + ;; RRTRP-NEXT: (local.get $1) + ;; RRTRP-NEXT: ) + ;; RRTRP-NEXT: (local.get $scratch_5) ;; RRTRP-NEXT: ) ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.get $scratch_5) + ;; RRTRP-NEXT: (local.get $3) ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.get $0) - ;; RRTRP-NEXT: (local.get $1) + ;; RRTRP-NEXT: (local.get $4) ;; RRTRP-NEXT: ) ;; RRTRP-NEXT: ) (func $slice (result stringref) @@ -186,12 +174,9 @@ ;; RTRIP-NEXT: (local $1 i32) ;; RTRIP-NEXT: (local $2 i32) ;; RTRIP-NEXT: (local $scratch i32) - ;; RTRIP-NEXT: (local $scratch_4 (ref string)) ;; RTRIP-NEXT: (stringview_wtf16.slice - ;; RTRIP-NEXT: (block (result (ref string)) - ;; RTRIP-NEXT: (local.set $scratch_4 - ;; RTRIP-NEXT: (string.const "abc") - ;; RTRIP-NEXT: ) + ;; RTRIP-NEXT: (string.const "abc") + ;; RTRIP-NEXT: (block (result i32) ;; RTRIP-NEXT: (local.set $1 ;; RTRIP-NEXT: (block (result i32) ;; RTRIP-NEXT: (local.set $scratch @@ -203,9 +188,8 @@ ;; RTRIP-NEXT: (local.get $scratch) ;; RTRIP-NEXT: ) ;; RTRIP-NEXT: ) - ;; RTRIP-NEXT: (local.get $scratch_4) + ;; RTRIP-NEXT: (local.get $1) ;; RTRIP-NEXT: ) - ;; RTRIP-NEXT: (local.get $1) ;; RTRIP-NEXT: (local.get $2) ;; RTRIP-NEXT: ) ;; RTRIP-NEXT: ) @@ -214,29 +198,29 @@ ;; RRTRP-NEXT: (local $1 i32) ;; RRTRP-NEXT: (local $2 i32) ;; RRTRP-NEXT: (local $scratch i32) - ;; RRTRP-NEXT: (local $scratch_4 (ref string)) - ;; RRTRP-NEXT: (local $scratch_5 i32) - ;; RRTRP-NEXT: (local $scratch_6 (ref string)) + ;; RRTRP-NEXT: (local $4 i32) + ;; RRTRP-NEXT: (local $5 i32) + ;; RRTRP-NEXT: (local $scratch_6 i32) ;; RRTRP-NEXT: (stringview_wtf16.slice - ;; RRTRP-NEXT: (block (result (ref string)) - ;; RRTRP-NEXT: (local.set $scratch_6 - ;; RRTRP-NEXT: (string.const "abc") - ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.set $1 + ;; RRTRP-NEXT: (string.const "abc") + ;; RRTRP-NEXT: (block (result i32) + ;; RRTRP-NEXT: (local.set $4 ;; RRTRP-NEXT: (block (result i32) - ;; RRTRP-NEXT: (local.set $scratch_5 + ;; RRTRP-NEXT: (local.set $scratch_6 ;; RRTRP-NEXT: (local.get $start) ;; RRTRP-NEXT: ) ;; RRTRP-NEXT: (local.set $2 ;; RRTRP-NEXT: (i32.const 2) ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.get $scratch_5) + ;; RRTRP-NEXT: (local.set $5 + ;; RRTRP-NEXT: (local.get $2) + ;; RRTRP-NEXT: ) + ;; RRTRP-NEXT: (local.get $scratch_6) ;; RRTRP-NEXT: ) ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.get $scratch_6) + ;; RRTRP-NEXT: (local.get $4) ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.get $1) - ;; RRTRP-NEXT: (local.get $2) + ;; RRTRP-NEXT: (local.get $5) ;; RRTRP-NEXT: ) ;; RRTRP-NEXT: ) (func $slice-start-get (result stringref) @@ -262,12 +246,9 @@ ;; RTRIP-NEXT: (local $1 i32) ;; RTRIP-NEXT: (local $2 i32) ;; RTRIP-NEXT: (local $scratch i32) - ;; RTRIP-NEXT: (local $scratch_4 (ref string)) ;; RTRIP-NEXT: (stringview_wtf16.slice - ;; RTRIP-NEXT: (block (result (ref string)) - ;; RTRIP-NEXT: (local.set $scratch_4 - ;; RTRIP-NEXT: (string.const "abc") - ;; RTRIP-NEXT: ) + ;; RTRIP-NEXT: (string.const "abc") + ;; RTRIP-NEXT: (block (result i32) ;; RTRIP-NEXT: (local.set $1 ;; RTRIP-NEXT: (block (result i32) ;; RTRIP-NEXT: (local.set $scratch @@ -279,9 +260,8 @@ ;; RTRIP-NEXT: (local.get $scratch) ;; RTRIP-NEXT: ) ;; RTRIP-NEXT: ) - ;; RTRIP-NEXT: (local.get $scratch_4) + ;; RTRIP-NEXT: (local.get $1) ;; RTRIP-NEXT: ) - ;; RTRIP-NEXT: (local.get $1) ;; RTRIP-NEXT: (local.get $2) ;; RTRIP-NEXT: ) ;; RTRIP-NEXT: ) @@ -290,29 +270,29 @@ ;; RRTRP-NEXT: (local $1 i32) ;; RRTRP-NEXT: (local $2 i32) ;; RRTRP-NEXT: (local $scratch i32) - ;; RRTRP-NEXT: (local $scratch_4 (ref string)) - ;; RRTRP-NEXT: (local $scratch_5 i32) - ;; RRTRP-NEXT: (local $scratch_6 (ref string)) + ;; RRTRP-NEXT: (local $4 i32) + ;; RRTRP-NEXT: (local $5 i32) + ;; RRTRP-NEXT: (local $scratch_6 i32) ;; RRTRP-NEXT: (stringview_wtf16.slice - ;; RRTRP-NEXT: (block (result (ref string)) - ;; RRTRP-NEXT: (local.set $scratch_6 - ;; RRTRP-NEXT: (string.const "abc") - ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.set $1 + ;; RRTRP-NEXT: (string.const "abc") + ;; RRTRP-NEXT: (block (result i32) + ;; RRTRP-NEXT: (local.set $4 ;; RRTRP-NEXT: (block (result i32) - ;; RRTRP-NEXT: (local.set $scratch_5 + ;; RRTRP-NEXT: (local.set $scratch_6 ;; RRTRP-NEXT: (i32.const 1) ;; RRTRP-NEXT: ) ;; RRTRP-NEXT: (local.set $2 ;; RRTRP-NEXT: (local.get $end) ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.get $scratch_5) + ;; RRTRP-NEXT: (local.set $5 + ;; RRTRP-NEXT: (local.get $2) + ;; RRTRP-NEXT: ) + ;; RRTRP-NEXT: (local.get $scratch_6) ;; RRTRP-NEXT: ) ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.get $scratch_6) + ;; RRTRP-NEXT: (local.get $4) ;; RRTRP-NEXT: ) - ;; RRTRP-NEXT: (local.get $1) - ;; RRTRP-NEXT: (local.get $2) + ;; RRTRP-NEXT: (local.get $5) ;; RRTRP-NEXT: ) ;; RRTRP-NEXT: ) (func $slice-end-get (result stringref) diff --git a/test/lit/wat-kitchen-sink.wast b/test/lit/wat-kitchen-sink.wast index 9e3c6171f4b..4b355c464f1 100644 --- a/test/lit/wat-kitchen-sink.wast +++ b/test/lit/wat-kitchen-sink.wast @@ -586,16 +586,12 @@ ) ;; CHECK: (func $add-stacky (type $1) (result i32) - ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $scratch - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (i32.const 2) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (i32.const 2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $add-stacky (result i32) @@ -646,17 +642,11 @@ ;; CHECK: (func $add-stacky-4 (type $1) (result i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local $scratch_1 i32) - ;; CHECK-NEXT: (local $scratch_2 i32) - ;; CHECK-NEXT: (local.set $scratch_2 + ;; CHECK-NEXT: (local.set $scratch_1 ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $scratch_1 - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (local.get $scratch_1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (block (result i32) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (i32.const 2) ;; CHECK-NEXT: ) @@ -666,7 +656,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (local.get $scratch_2) + ;; CHECK-NEXT: (local.get $scratch_1) ;; CHECK-NEXT: ) (func $add-stacky-4 (result i32) i32.const 1 @@ -738,21 +728,17 @@ ) ;; CHECK: (func $add-twice-stacky (type $ret2) (result i32 i32) - ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $scratch - ;; CHECK-NEXT: (i32.add - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (i32.const 2) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (local.get $scratch) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (i32.add - ;; CHECK-NEXT: (i32.const 3) - ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) diff --git a/test/stacky.wasm.fromBinary b/test/stacky.wasm.fromBinary index d7c85091e1d..8543fca0a4c 100644 --- a/test/stacky.wasm.fromBinary +++ b/test/stacky.wasm.fromBinary @@ -3,18 +3,14 @@ (memory $0 256 256) (export "add" (func $0)) (func $0 (param $0 i32) (param $1 i32) (result i32) - (local $scratch i32) (i32.add + (local.get $0) (block (result i32) - (local.set $scratch - (local.get $0) - ) (local.set $0 (i32.const 100) ) - (local.get $scratch) + (local.get $1) ) - (local.get $1) ) ) ) From a7d3f18c224b23db38468bbe48fa2c61c8d13918 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 16 Apr 2026 13:27:50 -0700 Subject: [PATCH 034/168] Handle strings when skipping function bodies (#8614) Explicitly parse strings in the logic for mostly skipping function bodies in the first parser phase. This avoids parentheses inside strings causing the parser to skip too much or too little of the input. --- src/parser/context-decls.cpp | 5 +++++ test/lit/wat-kitchen-sink.wast | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/src/parser/context-decls.cpp b/src/parser/context-decls.cpp index 73327bd1640..3afa960ee5a 100644 --- a/src/parser/context-decls.cpp +++ b/src/parser/context-decls.cpp @@ -329,6 +329,11 @@ bool ParseDeclsCtx::skipFunctionBody() { } continue; } + // Avoid confusion due to parens inside strings by skipping strings as a + // unit. + if (in.takeString()) { + continue; + } in.take(1); in.advance(); } diff --git a/test/lit/wat-kitchen-sink.wast b/test/lit/wat-kitchen-sink.wast index 4b355c464f1..7a92d0dcfdf 100644 --- a/test/lit/wat-kitchen-sink.wast +++ b/test/lit/wat-kitchen-sink.wast @@ -5259,4 +5259,11 @@ ) ) ) + + (func $paren-in-string + ;; We should not be tripped up by an extra close parenthesis inside a string. + (drop + (string.const ")") + ) + ) ) From ef4cbbe7dfa234b8146d5575f917af1bbaf1d041 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 16 Apr 2026 14:31:26 -0700 Subject: [PATCH 035/168] Validate descriptor clause forward references (#8611) When the descriptor clause on a type definition was a forward reference to a future rec group, we previously failed an assertion that it was not a temporary type. Replace this assertion with a validation check. Fixes #8606. --- src/wasm-type.h | 2 ++ src/wasm/wasm-type.cpp | 11 ++++++++++- test/lit/validation/descriptor-forward-reference.wast | 8 ++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 test/lit/validation/descriptor-forward-reference.wast diff --git a/src/wasm-type.h b/src/wasm-type.h index a0340c2b083..4033c6a36b6 100644 --- a/src/wasm-type.h +++ b/src/wasm-type.h @@ -917,6 +917,8 @@ struct TypeBuilder { NonStructDescribes, // The described type is an invalid forward reference. ForwardDescribesReference, + // The descriptor type is an invalid forward reference. + ForwardDescriptorReference, // The described type does not have this type as a descriptor. MismatchedDescribes, // A descriptor clause on a non-struct type. diff --git a/src/wasm/wasm-type.cpp b/src/wasm/wasm-type.cpp index 8ff0e9c362a..8826985ca19 100644 --- a/src/wasm/wasm-type.cpp +++ b/src/wasm/wasm-type.cpp @@ -1528,6 +1528,8 @@ std::ostream& operator<<(std::ostream& os, return os << "Describes clause on a non-struct type"; case TypeBuilder::ErrorReasonKind::ForwardDescribesReference: return os << "Describes clause is a forward reference"; + case TypeBuilder::ErrorReasonKind::ForwardDescriptorReference: + return os << "Descriptor clause is a forward reference"; case TypeBuilder::ErrorReasonKind::MismatchedDescribes: return os << "Described type is not a matching descriptor"; case TypeBuilder::ErrorReasonKind::NonStructDescriptor: @@ -2509,7 +2511,6 @@ validateTypeInfo(HeapTypeInfo& info, if (info.kind != HeapTypeKind::Struct) { return TypeBuilder::ErrorReasonKind::NonStructDescribes; } - assert(desc->isTemp && "unexpected canonical described type"); if (!seenTypes.contains(HeapType(uintptr_t(desc)))) { return TypeBuilder::ErrorReasonKind::ForwardDescribesReference; } @@ -2654,6 +2655,14 @@ buildRecGroup(std::unique_ptr&& groupInfo, i, TypeBuilder::ErrorReasonKind::ForwardChildReference}}; } } + if (auto desc = type.getDescriptorType()) { + if (isTemp(*desc) && !seenTypes.contains(*desc)) { + return {TypeBuilder::Error{ + i, TypeBuilder::ErrorReasonKind::ForwardDescriptorReference}}; + } + } + // Describes clauses were already checked as we validated each type in the + // group. } // The rec group is valid, so we can try to move the group into the global rec diff --git a/test/lit/validation/descriptor-forward-reference.wast b/test/lit/validation/descriptor-forward-reference.wast new file mode 100644 index 00000000000..a5f74219cd4 --- /dev/null +++ b/test/lit/validation/descriptor-forward-reference.wast @@ -0,0 +1,8 @@ +;; RUN: not wasm-opt -all %s 2>&1 | filecheck %s + +(module + ;; These types should be in the same rec group! + ;; CHECK: invalid type: Descriptor clause is a forward reference + (type $Default (descriptor $Default.desc) (struct (field i32))) + (type $Default.desc (describes $Default) (struct (field (ref extern)))) +) From 069c945176879513a47623b26e02d3e8030d78ba Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 16 Apr 2026 15:34:45 -0700 Subject: [PATCH 036/168] PreserveImportsExportsJS fuzzer: Handle JS differences in stack traces (#8610) Generalize our stack-trace ignoring code, as it turns out the JS parts of traces can differ based on optimizations. --- scripts/fuzz_opt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index bf0892be681..3e41356477e 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2223,13 +2223,14 @@ def do_run(self, vm, js, wasm): # must also remove the specific trap, as Binaryen can change # that. line = 'TRAP' - elif 'wasm://' in line or '()' in line: + elif line.startswith(' at '): # This is part of a stack trace like # # at wasm://wasm/12345678:wasm-function[42]:0x123 # at () + # at file.js # - # Ignore it, as traces differ based on optimizations. + # Ignore it, as details of traces differ based on optimizations. continue cleaned.append(line) return '\n'.join(cleaned) From 2d093c2d2d4e3892b34ef048fa4fd0ae2a969d6b Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Thu, 16 Apr 2026 15:36:50 -0700 Subject: [PATCH 037/168] NFC: Propagate global effects via strongly-connected components (#8607) Helps prevent huge visited sets and work queues for edges when traversing large call graphs. After adding support for indirect call effects, the previous algorithm would OOM or timeout when computing global effects for large binaries like calcworker. Yields a small runtime improvement when tested on calcworker: 0.13451015 -> 0.1328012 (1.3%, averaged from 20 tries with a release build). Part of #8615. --- src/passes/GlobalEffects.cpp | 190 +++++++++++++++++++++++++---------- 1 file changed, 136 insertions(+), 54 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index ac17037902b..7f47cf108eb 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -22,7 +22,7 @@ #include "ir/effects.h" #include "ir/module-utils.h" #include "pass.h" -#include "support/unique_deferring_queue.h" +#include "support/strongly_connected_components.h" #include "wasm.h" namespace wasm { @@ -107,61 +107,158 @@ std::map analyzeFuncs(Module& module, return std::move(analysis.map); } +using CallGraph = std::unordered_map>; + +CallGraph buildCallGraph(const Module& module, + const std::map& funcInfos) { + CallGraph callGraph; + for (const auto& [func, info] : funcInfos) { + if (info.calledFunctions.empty()) { + continue; + } + + auto& callees = callGraph[func]; + for (Name callee : info.calledFunctions) { + callees.insert(module.getFunction(callee)); + } + } + + return callGraph; +} + +void mergeMaybeEffects(std::optional& dest, + const std::optional& src) { + if (dest == UnknownEffects) { + return; + } + if (src == UnknownEffects) { + dest = UnknownEffects; + return; + } + + dest->mergeIn(*src); +} + // Propagate effects from callees to callers transitively // e.g. if A -> B -> C (A calls B which calls C) // Then B inherits effects from C and A inherits effects from both B and C. -void propagateEffects( - const Module& module, - const std::unordered_map>& reverseCallGraph, - std::map& funcInfos) { - - UniqueNonrepeatingDeferredQueue> work; +// +// Generate SCC for the call graph, then traverse it in reverse topological +// order processing each callee before its callers. When traversing: +// - Merge all of the effects of functions within the CC +// - Also merge the (already computed) effects of each callee CC +// - Add trap effects for potentially recursive call chains +void propagateEffects(const Module& module, + const PassOptions& passOptions, + std::map& funcInfos, + const CallGraph& callGraph) { + struct CallGraphSCCs + : SCCs::const_iterator, CallGraphSCCs> { + const std::map& funcInfos; + const std::unordered_map>& + callGraph; + const Module& module; + + CallGraphSCCs( + const std::vector& funcs, + const std::map& funcInfos, + const std::unordered_map>& + callGraph, + const Module& module) + : SCCs::const_iterator, CallGraphSCCs>( + funcs.begin(), funcs.end()), + funcInfos(funcInfos), callGraph(callGraph), module(module) {} + + void pushChildren(Function* f) { + auto callees = callGraph.find(f); + if (callees == callGraph.end()) { + return; + } - for (const auto& [callee, callers] : reverseCallGraph) { - for (const auto& caller : callers) { - work.push(std::pair(callee, caller)); + for (auto* callee : callees->second) { + push(callee); + } } + }; + + std::vector allFuncs; + for (auto& [func, info] : funcInfos) { + allFuncs.push_back(func); } + CallGraphSCCs sccs(allFuncs, funcInfos, callGraph, module); + + std::vector> componentEffects; + // Points to an index in componentEffects + std::unordered_map funcComponents; - auto propagate = [&](Name callee, Name caller) { - auto& callerEffects = funcInfos.at(module.getFunction(caller)).effects; - const auto& calleeEffects = - funcInfos.at(module.getFunction(callee)).effects; - if (!callerEffects) { - return; + for (auto ccIterator : sccs) { + std::optional& ccEffects = + componentEffects.emplace_back(std::in_place, passOptions, module); + + std::vector ccFuncs(ccIterator.begin(), ccIterator.end()); + + for (Function* f : ccFuncs) { + funcComponents.emplace(f, componentEffects.size() - 1); } - if (!calleeEffects) { - callerEffects = UnknownEffects; - return; + std::unordered_set calleeSccs; + for (Function* caller : ccFuncs) { + auto callees = callGraph.find(caller); + if (callees == callGraph.end()) { + continue; + } + for (auto* callee : callees->second) { + calleeSccs.insert(funcComponents.at(callee)); + } } - callerEffects->mergeIn(*calleeEffects); - }; + // Merge in effects from callees + for (int calleeScc : calleeSccs) { + const auto& calleeComponentEffects = componentEffects.at(calleeScc); + mergeMaybeEffects(ccEffects, calleeComponentEffects); + } - while (!work.empty()) { - auto [callee, caller] = work.pop(); + // Add trap effects for potential cycles. + if (ccFuncs.size() > 1) { + if (ccEffects != UnknownEffects) { + ccEffects->trap = true; + } + } else { + auto* func = ccFuncs[0]; + if (funcInfos.at(func).calledFunctions.contains(func->name)) { + if (ccEffects != UnknownEffects) { + ccEffects->trap = true; + } + } + } - if (callee == caller) { - auto& callerEffects = funcInfos.at(module.getFunction(caller)).effects; - if (callerEffects) { - callerEffects->trap = true; + // Aggregate effects within this CC + if (ccEffects) { + for (Function* f : ccFuncs) { + const auto& effects = funcInfos.at(f).effects; + mergeMaybeEffects(ccEffects, effects); } } - // Even if nothing changed, we still need to keep traversing the callers - // to look for a potential cycle which adds a trap affect on the above - // lines. - propagate(callee, caller); + // Assign each function's effects to its CC effects. + for (Function* f : ccFuncs) { + if (!ccEffects) { + funcInfos.at(f).effects = UnknownEffects; + } else { + funcInfos.at(f).effects.emplace(*ccEffects); + } + } + } +} - const auto& callerCallers = reverseCallGraph.find(caller); - if (callerCallers == reverseCallGraph.end()) { +void copyEffectsToFunctions(const std::map& funcInfos) { + for (auto& [func, info] : funcInfos) { + func->effects.reset(); + if (!info.effects) { continue; } - for (const Name& callerCaller : callerCallers->second) { - work.push(std::pair(callee, callerCaller)); - } + func->effects = std::make_shared(*info.effects); } } @@ -170,26 +267,11 @@ struct GenerateGlobalEffects : public Pass { std::map funcInfos = analyzeFuncs(*module, getPassOptions()); - // callee : caller - std::unordered_map> callers; - for (const auto& [func, info] : funcInfos) { - for (const auto& callee : info.calledFunctions) { - callers[callee].insert(func->name); - } - } + auto callGraph = buildCallGraph(*module, funcInfos); - propagateEffects(*module, callers, funcInfos); + propagateEffects(*module, getPassOptions(), funcInfos, callGraph); - // Generate the final data, starting from a blank slate where nothing is - // known. - for (auto& [func, info] : funcInfos) { - func->effects.reset(); - if (!info.effects) { - continue; - } - - func->effects = std::make_shared(*info.effects); - } + copyEffectsToFunctions(funcInfos); } }; From f0519450ad58168d591db15a9d1e2d171adb6e3c Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 17 Apr 2026 12:38:33 -0700 Subject: [PATCH 038/168] BranchHinting fuzzing: Do not remove effects when deinstrumenting (#8613) The BranchHinting fuzzer broke after #8608, but it wasn't that PR's fault - just that we now generate a different pattern of blocks that BranchHinting's deinstrumentation did not handle yet. The problem was that the block with the condition that we replace now has effects, so we can't always remove it wholesale. --- src/passes/InstrumentBranchHints.cpp | 42 +++++++++++++- .../lit/passes/deinstrument-branch-hints.wast | 55 +++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/passes/InstrumentBranchHints.cpp b/src/passes/InstrumentBranchHints.cpp index 656397a5ad1..0fa6f3009b6 100644 --- a/src/passes/InstrumentBranchHints.cpp +++ b/src/passes/InstrumentBranchHints.cpp @@ -96,6 +96,7 @@ // #include "ir/drop.h" +#include "ir/effects.h" #include "ir/eh-utils.h" #include "ir/find_all.h" #include "ir/local-graph.h" @@ -272,6 +273,8 @@ struct InstrumentationProcessor : public WalkerPass> { // The condition before the instrumentation (a pointer to it, so we can // replace it). Expression** originalCondition; + // The local that the original condition is stored in temporarily. + Index tempLocal; // The call to the logging that the instrumentation added. Call* call; }; @@ -330,7 +333,7 @@ struct InstrumentationProcessor : public WalkerPass> { return {}; } // Great, this is indeed a prior instrumentation. - return Instrumentation{&set->value, call}; + return Instrumentation{&set->value, set->index, call}; } }; @@ -374,7 +377,27 @@ struct DeInstrumentBranchHints // IR, and the original condition is still used in another place, until // we remove the logging calls; since we will remove the calls anyhow, we // just need some valid IR there). - std::swap(curr->condition, *info->originalCondition); + // + // Check for dangerous effects in the condition we are about to replace, + // to avoid a situation where the condition looks like this: + // + // (set $temp (original condition)) + // ..effects.. + // (local.get $temp) + // + // We cannot replace all this with the original condition, as it would + // remove the effects. (Even in that case we will remove the actual call + // to log the branch hint, below, so this just prevents some cleanup that + // is normally safe - the cleanup is mainly useful to allow inspection of + // testcases for debugging.) + EffectAnalyzer effects(getPassOptions(), *getModule(), curr->condition); + // The only condition we allow is a write to the temp local from the + // instrumentation, which getInstrumentation() verified has no other uses + // than us. + effects.localsWritten.erase(info->tempLocal); + if (!effects.hasUnremovableSideEffects()) { + std::swap(curr->condition, *info->originalCondition); + } } } @@ -403,6 +426,21 @@ struct DeInstrumentBranchHints } } } + + void doWalkModule(Module* module) { + auto logBranchImport = getLogBranchImport(module); + if (!logBranchImport) { + Fatal() + << "No branch hint logging import found. Was this code instrumented?"; + } + + // Mark the log-branch import as having no side effects - we are removing it + // entirely here, and its effect should not stop us when we compute effects. + module->getFunction(logBranchImport)->effects = + std::make_shared(getPassOptions(), *module); + + InstrumentationProcessor::doWalkModule(module); + } }; } // anonymous namespace diff --git a/test/lit/passes/deinstrument-branch-hints.wast b/test/lit/passes/deinstrument-branch-hints.wast index 6d619c4b28b..3f9019028e7 100644 --- a/test/lit/passes/deinstrument-branch-hints.wast +++ b/test/lit/passes/deinstrument-branch-hints.wast @@ -7,6 +7,8 @@ ;; CHECK: (type $1 (func (param i32 i32 i32))) + ;; CHECK: (type $2 (func (result i32))) + ;; CHECK: (import "fuzzing-support" "log-branch" (func $log (type $1) (param i32 i32 i32))) (import "fuzzing-support" "log-branch" (func $log (param i32 i32 i32))) @@ -164,4 +166,57 @@ ) ) ) + + ;; CHECK: (func $if-unreachable (type $2) (result i32) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (block $block (result i32) + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $if-unreachable (result i32) + (local $0 i32) + ;; The unreachable here must be executed. Normally we replace the br_if's + ;; entire condition, but here we only remove the call to $log. + (block $block (result i32) + (br_if $block + (i32.const 0) + (block (result i32) + (block + (local.set $0 + (i32.const 42) + ) + (if + (i32.const 1) + (then + (unreachable) + ) + ) + ) + (call $log + (i32.const 0) + (i32.const 0) + (local.get $0) + ) + (local.get $0) + ) + ) + ) + ) ) From 9a1516f87c5fb4b75d74bfd93fdcb50807dac6e9 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 20 Apr 2026 07:28:48 -0700 Subject: [PATCH 039/168] [NFC] Simplify printing of unreachable replacements (#8616) When an instruction has a type immediate that we cannot print because the expression it is supposed to come from has unreachable or null type, we instead print an unreachable block with a comment saying what instruction we failed to print. We previously handled this via different code paths for type immediates that come from child expressions that type immediates that come from the printed expression's own return type. Unify these code paths in the printer by improving `Properties::hasUnwritableTypeImmediate` to handle both cases. Also fix the printing of `ShallowExpression` to check for unwritable type immediates first to avoid assertion failures. --- src/ir/properties.h | 22 +++++++++- src/passes/Print.cpp | 74 ++------------------------------- src/wasm-delegations-fields.def | 17 ++++++++ 3 files changed, 41 insertions(+), 72 deletions(-) diff --git a/src/ir/properties.h b/src/ir/properties.h index e52902597a0..4b4126d750d 100644 --- a/src/ir/properties.h +++ b/src/ir/properties.h @@ -521,8 +521,8 @@ inline MemoryOrder getMemoryOrder(Expression* curr) { } // Whether this instruction will be unwritable in the text and binary formats -// because it requires a type index immediate giving the type of a child that -// has unreachable or null type, and therefore does not have a type index. +// because it requires a type index immediate computed from an expression with +// unreachable or null type, and therefore no type index. inline bool hasUnwritableTypeImmediate(Expression* curr) { #define DELEGATE_ID curr->_id @@ -534,6 +534,24 @@ inline bool hasUnwritableTypeImmediate(Expression* curr) { } \ } +#define DELEGATE_IMMEDIATE_TYPED_RESULT(id) \ + if (curr->type == Type::unreachable) { \ + if constexpr (id::SpecificId == Expression::Id::RefCastId) { \ + auto* cast = curr->cast(); \ + if (!cast->desc) { \ + return true; \ + } \ + if (!cast->desc->type.isRef()) { \ + return true; \ + } \ + if (!cast->desc->type.getHeapType().getDescribedType()) { \ + return true; \ + } \ + return false; \ + } \ + return true; \ + } + #define DELEGATE_FIELD_CHILD(id, field) #define DELEGATE_FIELD_CHILD_VECTOR(id, field) #define DELEGATE_FIELD_INT(id, field) diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 11a73315b74..68981953211 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -320,63 +320,6 @@ struct PrintSExpression : public UnifiedExpressionVisitor { void visitTryTable(TryTable* curr); void printUnreachableReplacement(Expression* curr); - bool maybePrintUnreachableReplacement(Expression* curr, Type type); - void visitRefCast(RefCast* curr) { - if ((curr->desc && curr->desc->type != Type::unreachable) || - !maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitStructNew(StructNew* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitArrayNew(ArrayNew* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitArrayNewData(ArrayNewData* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitArrayNewElem(ArrayNewElem* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitArrayNewFixed(ArrayNewFixed* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitContNew(ContNew* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitContBind(ContBind* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitResume(Resume* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitResumeThrow(ResumeThrow* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } - void visitStackSwitch(StackSwitch* curr) { - if (!maybePrintUnreachableReplacement(curr, curr->type)) { - visitExpression(curr); - } - } // Module-level visitors void handleSignature(Function* curr, bool printImplicitNames = false); @@ -3143,19 +3086,6 @@ void PrintSExpression::printUnreachableReplacement(Expression* curr) { decIndent(); } -bool PrintSExpression::maybePrintUnreachableReplacement(Expression* curr, - Type type) { - // When we cannot print an instruction because the child from which it's - // supposed to get a type immediate is unreachable, then we print a - // semantically-equivalent block that drops each of the children and ends in - // an unreachable. - if (type == Type::unreachable) { - printUnreachableReplacement(curr); - return true; - } - return false; -} - static bool requiresExplicitFuncType(HeapType type) { // When the `(type $f)` in a function's typeuse is omitted, the typeuse // matches or declares an MVP function type. When the intended type is not an @@ -4009,6 +3939,10 @@ std::ostream& operator<<(std::ostream& o, wasm::ModuleExpression pair) { } std::ostream& operator<<(std::ostream& o, wasm::ShallowExpression expression) { + if (Properties::hasUnwritableTypeImmediate(expression.expr)) { + o << "(; unreachable " << getExpressionName(expression.expr) << " ;)"; + return o; + } wasm::PrintSExpression printer(o); printer.setModule(expression.module); wasm::PrintExpressionContents(printer).visit(expression.expr); diff --git a/src/wasm-delegations-fields.def b/src/wasm-delegations-fields.def index 142d1e7be70..432f4d832c9 100644 --- a/src/wasm-delegations-fields.def +++ b/src/wasm-delegations-fields.def @@ -29,6 +29,10 @@ // // DELEGATE_END(id) - called at the end of a case. // +// DELEGATE_IMMEDIATE_TYPED_RESULT(id) - does not actually represent a field, +// but is called for expressions whose result types are used to compute type +// immediates. Defining this is optional. +// // DELEGATE_GET_FIELD(id, field) - called to get a field by its name. This must // know the object on which to get it, so it is just useful for the case // where you operate on a single such object, but in that case it is nice @@ -117,6 +121,10 @@ #define DELEGATE_END(id) #endif +#ifndef DELEGATE_IMMEDIATE_TYPED_RESULT +#define DELEGATE_IMMEDIATE_TYPED_RESULT(id) +#endif + #ifndef DELEGATE_FIELD_CHILD #error please define DELEGATE_FIELD_CHILD(id, field) #endif @@ -659,6 +667,7 @@ DELEGATE_FIELD_CHILD(RefTest, ref) DELEGATE_FIELD_CASE_END(RefTest) DELEGATE_FIELD_CASE_START(RefCast) +DELEGATE_IMMEDIATE_TYPED_RESULT(RefCast) DELEGATE_FIELD_OPTIONAL_IMMEDIATE_TYPED_CHILD(RefCast, desc) DELEGATE_FIELD_CHILD(RefCast, ref) DELEGATE_FIELD_CASE_END(RefCast) @@ -676,6 +685,7 @@ DELEGATE_FIELD_CHILD(BrOn, ref) DELEGATE_FIELD_CASE_END(BrOn) DELEGATE_FIELD_CASE_START(StructNew) +DELEGATE_IMMEDIATE_TYPED_RESULT(StructNew) DELEGATE_FIELD_OPTIONAL_CHILD(StructNew, desc) DELEGATE_FIELD_CHILD_VECTOR(StructNew, operands) DELEGATE_FIELD_CASE_END(StructNew) @@ -711,23 +721,27 @@ DELEGATE_FIELD_INT(StructCmpxchg, order) DELEGATE_FIELD_CASE_END(StructCmpxchg) DELEGATE_FIELD_CASE_START(ArrayNew) +DELEGATE_IMMEDIATE_TYPED_RESULT(ArrayNew) DELEGATE_FIELD_CHILD(ArrayNew, size) DELEGATE_FIELD_OPTIONAL_CHILD(ArrayNew, init) DELEGATE_FIELD_CASE_END(ArrayNew) DELEGATE_FIELD_CASE_START(ArrayNewData) +DELEGATE_IMMEDIATE_TYPED_RESULT(ArrayNewData) DELEGATE_FIELD_NAME_KIND(ArrayNewData, segment, ModuleItemKind::DataSegment) DELEGATE_FIELD_CHILD(ArrayNewData, size) DELEGATE_FIELD_CHILD(ArrayNewData, offset) DELEGATE_FIELD_CASE_END(ArrayNewData) DELEGATE_FIELD_CASE_START(ArrayNewElem) +DELEGATE_IMMEDIATE_TYPED_RESULT(ArrayNewElem) DELEGATE_FIELD_NAME_KIND(ArrayNewElem, segment, ModuleItemKind::ElementSegment) DELEGATE_FIELD_CHILD(ArrayNewElem, size) DELEGATE_FIELD_CHILD(ArrayNewElem, offset) DELEGATE_FIELD_CASE_END(ArrayNewElem) DELEGATE_FIELD_CASE_START(ArrayNewFixed) +DELEGATE_IMMEDIATE_TYPED_RESULT(ArrayNewFixed) DELEGATE_FIELD_CHILD_VECTOR(ArrayNewFixed, values) DELEGATE_FIELD_CASE_END(ArrayNewFixed) @@ -866,10 +880,12 @@ DELEGATE_FIELD_CHILD(StringSliceWTF, ref) DELEGATE_FIELD_CASE_END(StringSliceWTF) DELEGATE_FIELD_CASE_START(ContNew) +DELEGATE_IMMEDIATE_TYPED_RESULT(ContNew) DELEGATE_FIELD_CHILD(ContNew, func) DELEGATE_FIELD_CASE_END(ContNew) DELEGATE_FIELD_CASE_START(ContBind) +DELEGATE_IMMEDIATE_TYPED_RESULT(ContBind) DELEGATE_FIELD_IMMEDIATE_TYPED_CHILD(ContBind, cont) DELEGATE_FIELD_CHILD_VECTOR(ContBind, operands) DELEGATE_FIELD_CASE_END(ContBind) @@ -921,6 +937,7 @@ DELEGATE_FIELD_MAIN_END #undef DELEGATE_ID #undef DELEGATE_START #undef DELEGATE_END +#undef DELEGATE_IMMEDIATE_TYPED_RESULT #undef DELEGATE_FIELD_CHILD #undef DELEGATE_FIELD_IMMEDIATE_TYPED_CHILD #undef DELEGATE_FIELD_OPTIONAL_CHILD From 0f4d388de4edb4bf86348717fcb92b2cdf96516a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 20 Apr 2026 09:23:00 -0700 Subject: [PATCH 040/168] Fix and improve unreachable parsing (#8617) The recent change (#8608) that reduced the number of scratch locals introduced by IRBuilder also inadvertently introduced a bug. That commit changed the conditions under which we package multiple expressions into a block to be popped together. We previously did this whenever there was a get of a scratch local to hoist a value to the top of the stack. After #8608, we started using blocks whenever there were multiple expressions hoisted. These conditions are usually identical, but we missed the case where there are multiple expressions and also no `get` because the value is unreachable. The introduction of a block where there was none before invalidated our logic for determining whether or not to pop back past an unreachable expression based on whether the expressions underneath it would satisfy the type constraints of the parent. Simplify that logic by calculating the stack size at which we must avoid popping past an unreachable rather than calculating the index of the unreachable itself. This makes the logic work correctly whether or not the unreachable will be popped as part of a block. That in turn lets us remove an extra unreachable we were conservatively pushing onto the stack when "hoisting" unreachable values. Add tests for the cases where we can and cannot pop past an unreachable that is followed by a none-typed expression. The former case previously parsed incorrectly and the latter case is now parsed without introducing extra unreachable instructions. --- src/ir/properties.h | 19 ++++++++ src/wasm/wasm-ir-builder.cpp | 87 +++++++++++++++++++++------------- test/lit/wat-kitchen-sink.wast | 48 +++++++++++++++++++ 3 files changed, 122 insertions(+), 32 deletions(-) diff --git a/src/ir/properties.h b/src/ir/properties.h index 4b4126d750d..dbfa313af09 100644 --- a/src/ir/properties.h +++ b/src/ir/properties.h @@ -570,6 +570,25 @@ inline bool hasUnwritableTypeImmediate(Expression* curr) { #include "wasm-delegations-fields.def" + if (curr->type == Type::unreachable) { + if (curr->is() || curr->is() || + curr->is() || curr->is() || + curr->is() || curr->is() || + curr->is()) { + return true; + } + if (auto* cast = curr->dynCast()) { + if (!cast->desc) { + return true; + } + if (!cast->desc->type.isRef()) { + return true; + } + if (!cast->desc->type.getHeapType().getDescribedType()) { + return true; + }; + } + } return false; } diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index 55ef512b103..6552df7e1a2 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -87,10 +87,7 @@ MaybeResult IRBuilder::hoistLastValue(bool greedy) { } auto*& expr = stack[valIndex]; if (expr->type == Type::unreachable) { - // Make sure the top of the stack also has an unreachable expression. - if (stack.back()->type != Type::unreachable) { - pushSynthetic(builder.makeUnreachable()); - } + // No need for a scratch local to hoist an unreachable. return HoistedVal{Index(hoistIndex), nullptr}; } // Hoist with a scratch local. Normally the scratch local is the same type as @@ -129,6 +126,11 @@ Result<> IRBuilder::packageHoistedValue(const HoistedVal& hoisted, }; auto type = scope.exprStack.back()->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. + type = Type::unreachable; + } if (type.size() == sizeHint || type.size() <= 1) { if (hoisted.hoistIndex < scope.exprStack.size() - 1) { @@ -280,6 +282,8 @@ void IRBuilder::dump() { if (tryy->name) { std::cerr << " " << tryy->name; } + } else if (scope.getTryTable()) { + std::cerr << "try_table"; } else { WASM_UNREACHABLE("unexpected scope"); } @@ -303,7 +307,8 @@ void IRBuilder::dump() { std::cerr << ":\n"; for (auto* expr : scope.exprStack) { - std::cerr << " " << ShallowExpression{expr} << "\n"; + std::cerr << " " << ShallowExpression{expr} << " (; " + << expr->type.toString() << " ;)\n"; } } #endif // IR_BUILDER_DEBUG @@ -356,32 +361,26 @@ struct IRBuilder::ChildPopper Result<> popConstrainedChildren(std::vector& children) { auto& scope = builder.getScope(); - // The index of the shallowest unreachable instruction on the stack, found - // by checkNeedsUnreachableFallback. - std::optional unreachableIndex; - - // Whether popping the children past the unreachable would produce a type - // mismatch or try to pop from an empty stack. - bool needUnreachableFallback = false; + // The stack size at which we are about to pop an unreachable instruction + // that we must not pop past because the expressions underneath it do not + // have the types we need. + std::optional unreachableFallbackSize; // We only need to check requirements if there is an unreachable. // Otherwise the validator will catch any problems. if (scope.unreachable) { - needUnreachableFallback = - checkNeedsUnreachableFallback(children, unreachableIndex); + unreachableFallbackSize = checkNeedsUnreachableFallback(children); } // We have checked all the constraints, so we are ready to pop children. for (int i = children.size() - 1; i >= 0; --i) { - if (needUnreachableFallback && - scope.exprStack.size() == *unreachableIndex + 1 && i > 0) { + if (unreachableFallbackSize && + scope.exprStack.size() == *unreachableFallbackSize && i > 0) { // The next item on the stack is the unreachable instruction we must - // not pop past. We cannot insert unreachables in front of it because - // it might be a branch we actually have to execute, so this next item - // must be child 0. But we are not ready to pop child 0 yet, so - // synthesize an unreachable instead of popping. The deeper - // instructions that would otherwise have been popped will remain on - // the stack to become prior children of future expressions or to be + // not pop past. We could pop it as child 0, but we are not ready to pop + // child 0 yet, so synthesize an unreachable instead of popping. The + // deeper instructions that would otherwise have been popped will remain + // on the stack to become prior children of future expressions or to be // implicitly dropped at the end of the scope. *children[i].childp = builder.builder.makeUnreachable(); continue; @@ -397,9 +396,10 @@ struct IRBuilder::ChildPopper return Ok{}; } - bool checkNeedsUnreachableFallback(const std::vector& children, - std::optional& unreachableIndex) { + std::optional + checkNeedsUnreachableFallback(const std::vector& children) { auto& scope = builder.getScope(); + std::optional unreachableFallbackSize; // Two-part indices into the stack of available expressions and the vector // of requirements, allowing them to move independently with the granularity @@ -409,6 +409,9 @@ struct IRBuilder::ChildPopper size_t childIndex = children.size(); size_t childTupleIndex = 0; + // Whether we are deeper than some concrete expression. + bool seenConcrete = false; + // Check whether the values on the stack will be able to meet the given // requirements. while (true) { @@ -435,7 +438,7 @@ struct IRBuilder::ChildPopper // the input unreachable instruction is executed first. If we are // not reaching past an unreachable, the error will be caught when // we pop. - return true; + return unreachableFallbackSize; } --stackIndex; stackTupleIndex = scope.exprStack[stackIndex]->type.size() - 1; @@ -450,22 +453,35 @@ struct IRBuilder::ChildPopper } // We have an available type and a constraint. Only check constraints if - // we are past an unreachable, since otherwise we can leave problems to be - // caught by the validator later. + // 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]; - if (unreachableIndex) { + if (unreachableFallbackSize) { auto constraint = children[childIndex].constraint[childTupleIndex]; if (!PrincipalType::matches(type, constraint)) { - return true; + return unreachableFallbackSize; } } - // No problems for children after this unreachable. + // We may need an unreachable fallback if we find violated constraints in + // expressions deeper than this unreachable. Calculate the stack size at + // which this unreachable will be the next thing popped. This size is + // usually one more than the current index, but if there are no shallower + // concrete expressions, then everything down to this unreachable will be + // popped at once immediately at the current stack size. if (type == Type::unreachable) { - unreachableIndex = stackIndex; + unreachableFallbackSize = + seenConcrete ? stackIndex + 1 : scope.exprStack.size(); + } else { + // We skipped none-typed expressions and this isn't unreachable, so it + // must be concrete. + assert(type.isConcrete()); + seenConcrete = true; } } - return false; + + // No unreachable fallback necessary. + return std::nullopt; } // If `greedy`, then we will pop additional none-typed expressions that come @@ -848,6 +864,13 @@ Result IRBuilder::finishScope(Block* block) { return Err{"popping from empty stack"}; } + if (scope.exprStack.back()->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. + pushSynthetic(builder.makeUnreachable()); + } + if (type.isTuple()) { auto hoistedType = scope.exprStack.back()->type; if (hoistedType != Type::unreachable && diff --git a/test/lit/wat-kitchen-sink.wast b/test/lit/wat-kitchen-sink.wast index 7a92d0dcfdf..0b04d385961 100644 --- a/test/lit/wat-kitchen-sink.wast +++ b/test/lit/wat-kitchen-sink.wast @@ -5260,6 +5260,54 @@ ) ) + ;; CHECK: (func $stacky-unreachable-fallback (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block ;; (replaces unreachable StructSet we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $stacky-unreachable-fallback + ;; i32 cannot be the struct.set's ref child, so we cannot pop past the + ;; unreachable. The unreachable and the following nop are popped together. + i32.const 0 + unreachable + nop + struct.set $pair 0 + ) + + ;; CHECK: (func $stacky-unreachable-ok (type $0) + ;; CHECK-NEXT: (struct.set $pair $first + ;; CHECK-NEXT: (struct.new_default $pair) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $stacky-unreachable-ok + ;; Now we can pop past the unreachable. The unreachable and nop are still + ;; popped together. + struct.new_default $pair + unreachable + nop + struct.set $pair 0 + ) + + ;; CHECK: (func $paren-in-string (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (string.const ")") + ;; CHECK-NEXT: ) (func $paren-in-string ;; We should not be tripped up by an extra close parenthesis inside a string. (drop From b3650ea0d6533f66956f9c641ea73e623ed1e57d Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 20 Apr 2026 10:09:09 -0700 Subject: [PATCH 041/168] [NFC] Remove redundant code from bad merge (#8627) When #8617 landed, it introduced this code from an out-of-date version of #8616. The code had been moved in such a way that the merge kept both old and new versions instead of creating a merge conflict. Remove the outdated code. --- src/ir/properties.h | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/ir/properties.h b/src/ir/properties.h index dbfa313af09..4b4126d750d 100644 --- a/src/ir/properties.h +++ b/src/ir/properties.h @@ -570,25 +570,6 @@ inline bool hasUnwritableTypeImmediate(Expression* curr) { #include "wasm-delegations-fields.def" - if (curr->type == Type::unreachable) { - if (curr->is() || curr->is() || - curr->is() || curr->is() || - curr->is() || curr->is() || - curr->is()) { - return true; - } - if (auto* cast = curr->dynCast()) { - if (!cast->desc) { - return true; - } - if (!cast->desc->type.isRef()) { - return true; - } - if (!cast->desc->type.getHeapType().getDescribedType()) { - return true; - }; - } - } return false; } From 93b0629c01852fc8ac0e82accb546a126e1e0049 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 20 Apr 2026 10:30:38 -0700 Subject: [PATCH 042/168] Simplify branch hint instrumentation (#8624) The previous branch hint instrumentation logic would introduce a scratch local to hold the condition so it could be passed into both the logging function and the original branching instruction. The de-instrumentation pass would then need to find this local and attempt to undo the data flow change. Simplify all of this by having the logging function return the condition value so it can interpose between the condition and the branch without any new locals. De-instrumentation can now just replace the call to the log function with its condition parameter. To allow further simplification, also change the order of parameters to the logging function so the condition value is the first parameter. This ensures that we don't need to introduce a scratch local even when the condition is a `pop`, because the pop will remain the leftmost leaf expression in the catch body. --- scripts/fuzz_opt.py | 4 +- scripts/fuzz_shell.js | 5 +- src/passes/InstrumentBranchHints.cpp | 219 ++++-------------- src/tools/execution-results.h | 3 + test/lit/name-overlap.wast | 4 +- .../lit/passes/deinstrument-branch-hints.wast | 169 +------------- test/lit/passes/delete-branch-hints.wast | 96 +++----- test/lit/passes/instrument-branch-hints.wast | 219 ++++++------------ 8 files changed, 158 insertions(+), 561 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 3e41356477e..8af017f956d 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2281,7 +2281,7 @@ def handle(self, wasm): for line in out.splitlines(): if line.startswith(LOG_BRANCH_PREFIX): # (1:-1 strips away the '[', ']' at the edges) - _, _, id_, hint, actual = line[1:-1].split(' ') + _, _, actual, hint, id_ = line[1:-1].split(' ') all_ids.add(id_) if hint != actual: # This hint was misleading. @@ -2443,7 +2443,7 @@ def handle(self, wasm): continue for line in group: if line.startswith(LOG_BRANCH_PREFIX): - _, _, id_, hint, actual = line[1:-1].split(' ') + _, _, actual, hint, id_ = line[1:-1].split(' ') hint = int(hint) actual = int(actual) assert hint in (0, 1) diff --git a/scripts/fuzz_shell.js b/scripts/fuzz_shell.js index 28f5e2f44fb..d16c49624b9 100644 --- a/scripts/fuzz_shell.js +++ b/scripts/fuzz_shell.js @@ -400,8 +400,9 @@ var baseImports = { }); }, - 'log-branch': (id, expected, actual) => { - console.log(`[LoggingExternalInterface log-branch ${id} ${expected} ${actual}]`); + 'log-branch': (actual, expected, id) => { + console.log(`[LoggingExternalInterface log-branch ${actual} ${expected} ${id}]`); + return actual; }, }, // Emscripten support. diff --git a/src/passes/InstrumentBranchHints.cpp b/src/passes/InstrumentBranchHints.cpp index 0fa6f3009b6..c552ddae891 100644 --- a/src/passes/InstrumentBranchHints.cpp +++ b/src/passes/InstrumentBranchHints.cpp @@ -28,9 +28,9 @@ // into // // @metadata.branch.hint B -// ;; log the ID of the condition (123), the prediction (B), and the actual -// ;; runtime result (temp == condition). -// if (temp = condition; log(123, B, temp); temp) { +// ;; log the actual runtime result (condition), the prediction (B), and the +// ;; ID (123), and return that result. +// if (log(condition, B, 123)) { // X // } else { // Y @@ -39,19 +39,20 @@ // Concretely, we emit calls to this logging function: // // (import "fuzzing-support" "log-branch" -// (func $log-branch (param i32 i32 i32)) ;; ID, prediction, actual +// (func $log-branch (param i32 i32 i32) (result i32)) // ) // // This can be used to verify that branch hints are accurate, by implementing // the import like this for example: // -// imports['fuzzing-support']['log-branch'] = (id, prediction, actual) => { +// imports['fuzzing-support']['log-branch'] = (actual, prediction, id) => { // // We only care about truthiness of the expected and actual values. // expected = +!!expected; // actual = +!!actual; // // Throw if the hint said this branch would be taken, but it was not, or // // vice versa. // if (expected != actual) throw `Bad branch hint! (${id})`; +// return actual; // }; // // A pass to delete branch hints is also provided, which finds instrumentations @@ -63,28 +64,28 @@ // would do this transformation: // // @metadata.branch.hint A -// if (temp = condition; log(10, A, temp); temp) { // 10 matches one of 10,20 +// if (log(condition, A, 10)) { // 10 matches one of 10,20 // X // } // @metadata.branch.hint B -// if (temp = condition; log(99, B, temp); temp) { // 99 does not match +// if (log(condition, B, 99)) { // 99 does not match // Y // } // // => // // // Used to be a branch hint here, but it was deleted. -// if (temp = condition; log(10, A, temp); temp) { +// if (log(condition, A, 10)) { // X // } // @metadata.branch.hint B // this one is unmodified. -// if (temp = condition; log(99, B, temp); temp) { +// if (log(condition, B, 99)) { // Y // } // // A pass to undo the instrumentation is also provided, which does // -// if (temp = condition; log(123, A, temp); temp) { +// if (log(condition, A, 123)) { // X // } // @@ -95,14 +96,8 @@ // } // -#include "ir/drop.h" #include "ir/effects.h" -#include "ir/eh-utils.h" -#include "ir/find_all.h" -#include "ir/local-graph.h" #include "ir/names.h" -#include "ir/parents.h" -#include "ir/properties.h" #include "ir/utils.h" #include "pass.h" #include "support/string.h" @@ -133,8 +128,6 @@ int branchId = 1; struct InstrumentBranchHints : public WalkerPass> { - using Super = WalkerPass>; - // The internal name of our import. Name logBranch; @@ -148,8 +141,6 @@ struct InstrumentBranchHints // TODO: BrOn, but the condition there is not an i32 - bool addedInstrumentation = false; - template void processCondition(T* curr) { if (curr->condition->type == Type::unreachable) { // This branch is not even reached. @@ -167,25 +158,11 @@ struct InstrumentBranchHints int id = branchId++; // Instrument the condition. - auto tempLocal = builder.addVar(getFunction(), Type::i32); - auto* set = builder.makeLocalSet(tempLocal, curr->condition); auto* idConst = builder.makeConst(Literal(int32_t(id))); auto* guess = builder.makeConst(Literal(int32_t(*likely))); - auto* get1 = builder.makeLocalGet(tempLocal, Type::i32); - auto* log = builder.makeCall(logBranch, {idConst, guess, get1}, Type::none); - auto* get2 = builder.makeLocalGet(tempLocal, Type::i32); - curr->condition = builder.makeBlock({set, log, get2}); - addedInstrumentation = true; - } - - void doWalkFunction(Function* func) { - Super::doWalkFunction(func); - // Our added blocks may have caused nested pops. - if (addedInstrumentation) { - EHUtils::handleBlockNestedPops(func, *getModule()); - addedInstrumentation = false; - } + curr->condition = + builder.makeCall(logBranch, {curr->condition, guess, idConst}, Type::i32); } void doWalkModule(Module* module) { @@ -193,7 +170,12 @@ struct InstrumentBranchHints // This file already has our import. We nop it out, as whatever the // current code does may be dangerous (it may log incorrect hints). auto* func = module->getFunction(existing); - func->body = Builder(*module).makeNop(); + Builder builder(*module); + if (func->getSig().results == Type::none) { + func->body = builder.makeNop(); + } else { + func->body = builder.makeUnreachable(); + } func->module = func->base = Name(); func->type = func->type.with(Exact); } @@ -201,7 +183,7 @@ struct InstrumentBranchHints // Add our import. auto* func = module->addFunction(Builder::makeFunction( Names::getValidFunctionName(*module, BASE), - Type(Signature({Type::i32, Type::i32, Type::i32}, Type::none), + Type(Signature({Type::i32, Type::i32, Type::i32}, Type::i32), NonNullable, Inexact), {})); @@ -210,7 +192,7 @@ struct InstrumentBranchHints logBranch = func->name; // Walk normally, using logBranch as we go. - Super::doWalkModule(module); + PostWalker::doWalkModule(module); // Update ref.func type changes. ReFinalize().run(getPassRunner(), module); @@ -228,12 +210,6 @@ struct InstrumentationProcessor : public WalkerPass> { // The internal name of our import. Name logBranch; - // A LocalGraph, so we can identify the pattern. - std::unique_ptr localGraph; - - // A map of expressions to their parents, so we can identify the pattern. - std::unique_ptr parents; - Sub* self() { return static_cast(this); } void visitIf(If* curr) { self()->processCondition(curr); } @@ -246,15 +222,6 @@ struct InstrumentationProcessor : public WalkerPass> { // TODO: BrOn, but the condition there is not an i32 - void doWalkFunction(Function* func) { - localGraph = std::make_unique(func, this->getModule()); - localGraph->computeSetInfluences(); - - parents = std::make_unique(func->body); - - Super::doWalkFunction(func); - } - void doWalkModule(Module* module) { logBranch = getLogBranchImport(module); if (!logBranch) { @@ -267,73 +234,14 @@ struct InstrumentationProcessor : public WalkerPass> { // Helpers - // Instrumentation info for a chunk of code that is the result of the - // instrumentation pass. - struct Instrumentation { - // The condition before the instrumentation (a pointer to it, so we can - // replace it). - Expression** originalCondition; - // The local that the original condition is stored in temporarily. - Index tempLocal; - // The call to the logging that the instrumentation added. - Call* call; - }; - - // Check if an expression's condition is an instrumentation, and return the - // info if so. - std::optional getInstrumentation(Expression* condition) { - // We must identify this pattern: - // - // (br_if - // (block - // (local.set $temp (condition)) - // (call $log (id, prediction, (local.get $temp))) - // (local.get $temp) - // ) - // - // The block may vanish during roundtrip though, so we just follow back from - // the last local.get, which appears in the condition: - // - // (local.set $temp (condition)) - // (call $log (id, prediction, (local.get $temp))) - // (br_if - // (local.get $temp) - // - auto* fallthrough = Properties::getFallthrough( - condition, this->getPassOptions(), *this->getModule()); - auto* get = fallthrough->template dynCast(); - if (!get) { - return {}; - } - auto& sets = localGraph->getSets(get); - if (sets.size() != 1) { - return {}; - } - auto* set = *sets.begin(); - if (!set) { - return {}; - } - auto& gets = localGraph->getSetInfluences(set); - if (gets.size() != 2) { - return {}; - } - // The set has two gets: the get in the condition we began at, and - // another. - LocalGet* otherGet = nullptr; - for (auto* get2 : gets) { - if (get2 != get) { - otherGet = get2; - } - } - assert(otherGet); - // See if that other get is used in a logging. The parent should be a - // logging call. - auto* call = parents->getParent(otherGet)->template dynCast(); + // Check if an expression's condition is instrumented, and return the + // instrumentation call if so. Otherwise return null. + Call* getInstrumentation(Expression* condition) { + auto* call = condition->dynCast(); if (!call || call->target != logBranch) { - return {}; + return nullptr; } - // Great, this is indeed a prior instrumentation. - return Instrumentation{&set->value, set->index, call}; + return call; } }; @@ -344,8 +252,8 @@ struct DeleteBranchHints : public InstrumentationProcessor { std::unordered_set idsToDelete; template void processCondition(T* curr) { - if (auto info = getInstrumentation(curr->condition)) { - if (auto* c = info->call->operands[0]->template dynCast()) { + if (auto* call = getInstrumentation(curr->condition)) { + if (auto* c = call->operands[2]->template dynCast()) { auto id = c->value.geti32(); if (idsToDelete.contains(id)) { // Remove the branch hint. @@ -368,78 +276,31 @@ struct DeleteBranchHints : public InstrumentationProcessor { }; struct DeInstrumentBranchHints - : public InstrumentationProcessor { + : public WalkerPass> { - template void processCondition(T* curr) { - if (auto info = getInstrumentation(curr->condition)) { - // Replace the instrumented condition with the original one (swap so that - // the IR remains valid: we cannot use the same expression twice in our - // IR, and the original condition is still used in another place, until - // we remove the logging calls; since we will remove the calls anyhow, we - // just need some valid IR there). - // - // Check for dangerous effects in the condition we are about to replace, - // to avoid a situation where the condition looks like this: - // - // (set $temp (original condition)) - // ..effects.. - // (local.get $temp) - // - // We cannot replace all this with the original condition, as it would - // remove the effects. (Even in that case we will remove the actual call - // to log the branch hint, below, so this just prevents some cleanup that - // is normally safe - the cleanup is mainly useful to allow inspection of - // testcases for debugging.) - EffectAnalyzer effects(getPassOptions(), *getModule(), curr->condition); - // The only condition we allow is a write to the temp local from the - // instrumentation, which getInstrumentation() verified has no other uses - // than us. - effects.localsWritten.erase(info->tempLocal); - if (!effects.hasUnremovableSideEffects()) { - std::swap(curr->condition, *info->originalCondition); - } - } - } + // The internal name of our import. + Name logBranch; - void visitFunction(Function* func) { - if (func->imported()) { - return; - } - // At the very end, remove all logging calls (we use them during the main - // walk to identify instrumentation). - for (auto** callp : FindAllPointers(func->body).list) { - auto* call = (*callp)->cast(); - if (call->target == logBranch) { - Builder builder(*getModule()); - Expression* last; - if (call->type == Type::none) { - last = builder.makeNop(); - } else { - last = builder.makeUnreachable(); - } - *callp = getDroppedChildrenAndAppend(call, - *getModule(), - getPassOptions(), - last, - // We know the call is removable. - DropMode::IgnoreParentEffects); - } + void visitCall(Call* curr) { + if (curr->target == logBranch) { + // Replace the call with its first operand (the original condition). + replaceCurrent(curr->operands[0]); } } void doWalkModule(Module* module) { - auto logBranchImport = getLogBranchImport(module); - if (!logBranchImport) { + logBranch = getLogBranchImport(module); + if (!logBranch) { Fatal() << "No branch hint logging import found. Was this code instrumented?"; } // Mark the log-branch import as having no side effects - we are removing it // entirely here, and its effect should not stop us when we compute effects. - module->getFunction(logBranchImport)->effects = + module->getFunction(logBranch)->effects = std::make_shared(getPassOptions(), *module); - InstrumentationProcessor::doWalkModule(module); + WalkerPass>::doWalkModule(module); } }; diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index 516d9f60603..7717ec23811 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -180,6 +180,9 @@ struct LoggingExternalInterface : public ShellExternalInterface { } } std::cout << "]\n"; + if (import->base == "log-branch") { + return arguments[0]; + } return {}; } else if (import->base == "throw") { // Throw something, depending on the value of the argument. 0 means diff --git a/test/lit/name-overlap.wast b/test/lit/name-overlap.wast index 4caa4785e56..78229488afc 100644 --- a/test/lit/name-overlap.wast +++ b/test/lit/name-overlap.wast @@ -15,11 +15,11 @@ ;; CHECK: (type $1 (func (param f32))) - ;; CHECK: (type $2 (func (param i32 i32 i32))) + ;; CHECK: (type $2 (func (param i32 i32 i32) (result i32))) ;; CHECK: (import "fuzzing-support" "log-i64" (func $fimport$2 (type $0) (param i64))) (import "fuzzing-support" "log-i64" (func $fimport$2 (param i64))) ;; CHECK: (import "fuzzing-support" "log-f32" (func $fimport$3 (type $1) (param f32))) (import "fuzzing-support" "log-f32" (func $fimport$3 (param f32))) ) -;; CHECK: (import "fuzzing-support" "log-branch" (func $fimport$2_2 (type $2) (param i32 i32 i32))) +;; CHECK: (import "fuzzing-support" "log-branch" (func $fimport$2_2 (type $2) (param i32 i32 i32) (result i32))) diff --git a/test/lit/passes/deinstrument-branch-hints.wast b/test/lit/passes/deinstrument-branch-hints.wast index 3f9019028e7..4ff67e3e019 100644 --- a/test/lit/passes/deinstrument-branch-hints.wast +++ b/test/lit/passes/deinstrument-branch-hints.wast @@ -5,15 +5,12 @@ (module ;; CHECK: (type $0 (func)) - ;; CHECK: (type $1 (func (param i32 i32 i32))) + ;; CHECK: (type $1 (func (param i32 i32 i32) (result i32))) - ;; CHECK: (type $2 (func (result i32))) - - ;; CHECK: (import "fuzzing-support" "log-branch" (func $log (type $1) (param i32 i32 i32))) - (import "fuzzing-support" "log-branch" (func $log (param i32 i32 i32))) + ;; CHECK: (import "fuzzing-support" "log-branch" (func $log (type $1) (param i32 i32 i32) (result i32))) + (import "fuzzing-support" "log-branch" (func $log (param i32 i32 i32) (result i32))) ;; CHECK: (func $if (type $0) - ;; CHECK-NEXT: (local $temp i32) ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") ;; CHECK-NEXT: (if ;; CHECK-NEXT: (i32.const 42) @@ -30,21 +27,14 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $if - (local $temp i32) ;; The instrumentation should be removed, and the if's condition should ;; be 42. (@metadata.code.branch_hint "\00") (if - (block (result i32) - (local.set $temp - (i32.const 42) - ) - (call $log - (i32.const 1) - (i32.const 0) - (local.get $temp) - ) - (local.get $temp) + (call $log + (i32.const 42) + (i32.const 0) + (i32.const 1) ) (then (drop @@ -60,7 +50,6 @@ ) ;; CHECK: (func $br (type $0) - ;; CHECK-NEXT: (local $temp i32) ;; CHECK-NEXT: (block $out ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") ;; CHECK-NEXT: (br_if $out @@ -70,151 +59,13 @@ ;; CHECK-NEXT: ) (func $br ;; The same, with a br. - (local $temp i32) - (block $out - (@metadata.code.branch_hint "\01") - (br_if $out - (block (result i32) - (local.set $temp - (i32.const 42) - ) - (call $log - (i32.const 4) - (i32.const 0) - (local.get $temp) - ) - (local.get $temp) - ) - ) - ) - ) - - ;; CHECK: (func $br-before (type $0) - ;; CHECK-NEXT: (local $temp i32) - ;; CHECK-NEXT: (block $out - ;; CHECK-NEXT: (local.set $temp - ;; CHECK-NEXT: (local.get $temp) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") - ;; CHECK-NEXT: (br_if $out - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $br-before - ;; As above, but the instrumentation is before us, leaving only a local.get - ;; in the br's condition. We should still identify the pattern and remove - ;; the logging (but we leave the local.set for other things to clean up). - (local $temp i32) (block $out - (local.set $temp - (i32.const 42) - ) - (call $log - (i32.const 4) - (i32.const 0) - (local.get $temp) - ) (@metadata.code.branch_hint "\01") (br_if $out - (local.get $temp) - ) - ) - ) - - ;; CHECK: (func $br-before-effects (type $0) - ;; CHECK-NEXT: (local $temp i32) - ;; CHECK-NEXT: (local $other i32) - ;; CHECK-NEXT: (block $out - ;; CHECK-NEXT: (local.set $temp - ;; CHECK-NEXT: (local.get $temp) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (block - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.tee $other - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") - ;; CHECK-NEXT: (br_if $out - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $br-before-effects - ;; As above, but there are effects in the call's children that we must - ;; keep. - (local $temp i32) - (local $other i32) - (block $out - (local.set $temp - (i32.const 42) - ) - (call $log - (i32.const 4) - (local.tee $other ;; this tee must be kept around + (call $log + (i32.const 42) (i32.const 0) - ) - (local.get $temp) - ) - (@metadata.code.branch_hint "\01") - (br_if $out - (local.get $temp) - ) - ) - ) - - ;; CHECK: (func $if-unreachable (type $2) (result i32) - ;; CHECK-NEXT: (local $0 i32) - ;; CHECK-NEXT: (block $block (result i32) - ;; CHECK-NEXT: (br_if $block - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (block - ;; CHECK-NEXT: (local.set $0 - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $if-unreachable (result i32) - (local $0 i32) - ;; The unreachable here must be executed. Normally we replace the br_if's - ;; entire condition, but here we only remove the call to $log. - (block $block (result i32) - (br_if $block - (i32.const 0) - (block (result i32) - (block - (local.set $0 - (i32.const 42) - ) - (if - (i32.const 1) - (then - (unreachable) - ) - ) - ) - (call $log - (i32.const 0) - (i32.const 0) - (local.get $0) - ) - (local.get $0) + (i32.const 4) ) ) ) diff --git a/test/lit/passes/delete-branch-hints.wast b/test/lit/passes/delete-branch-hints.wast index 375b10d16c2..bbc59604a53 100644 --- a/test/lit/passes/delete-branch-hints.wast +++ b/test/lit/passes/delete-branch-hints.wast @@ -5,24 +5,17 @@ (module ;; CHECK: (type $0 (func)) - ;; CHECK: (type $1 (func (param i32 i32 i32))) + ;; CHECK: (type $1 (func (param i32 i32 i32) (result i32))) - ;; CHECK: (import "fuzzing-support" "log-branch" (func $log (type $1) (param i32 i32 i32))) - (import "fuzzing-support" "log-branch" (func $log (param i32 i32 i32))) + ;; CHECK: (import "fuzzing-support" "log-branch" (func $log (type $1) (param i32 i32 i32) (result i32))) + (import "fuzzing-support" "log-branch" (func $log (param i32 i32 i32) (result i32))) ;; CHECK: (func $if-10 (type $0) - ;; CHECK-NEXT: (local $temp i32) ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $temp - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log - ;; CHECK-NEXT: (i32.const 10) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $temp) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $temp) + ;; CHECK-NEXT: (call $log + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 10) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (then ;; CHECK-NEXT: (drop @@ -37,21 +30,14 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $if-10 - (local $temp i32) ;; The branch hint should be removed, since the ID "10" is in the list of ;; 10, 30. (@metadata.code.branch_hint "\00") (if - (block (result i32) - (local.set $temp - (i32.const 42) - ) - (call $log - (i32.const 10) - (i32.const 0) - (local.get $temp) - ) - (local.get $temp) + (call $log + (i32.const 42) + (i32.const 0) + (i32.const 10) ) (then (drop @@ -67,19 +53,12 @@ ) ;; CHECK: (func $if-20 (type $0) - ;; CHECK-NEXT: (local $temp i32) ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $temp - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log - ;; CHECK-NEXT: (i32.const 20) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $temp) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $temp) + ;; CHECK-NEXT: (call $log + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 20) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (then ;; CHECK-NEXT: (drop @@ -94,20 +73,13 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $if-20 - (local $temp i32) ;; The branch hint should *not* be removed: 20 is not in the list. (@metadata.code.branch_hint "\00") (if - (block (result i32) - (local.set $temp - (i32.const 42) - ) - (call $log - (i32.const 20) - (i32.const 0) - (local.get $temp) - ) - (local.get $temp) + (call $log + (i32.const 42) + (i32.const 0) + (i32.const 20) ) (then (drop @@ -123,39 +95,25 @@ ) ;; CHECK: (func $br-30 (type $0) - ;; CHECK-NEXT: (local $temp i32) ;; CHECK-NEXT: (block $out ;; CHECK-NEXT: (br_if $out - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $temp - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log - ;; CHECK-NEXT: (i32.const 30) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $temp) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $temp) + ;; CHECK-NEXT: (call $log + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 30) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $br-30 ;; The hint should be removed. - (local $temp i32) (block $out (@metadata.code.branch_hint "\01") (br_if $out - (block (result i32) - (local.set $temp - (i32.const 42) - ) - (call $log - (i32.const 30) - (i32.const 0) - (local.get $temp) - ) - (local.get $temp) + (call $log + (i32.const 42) + (i32.const 0) + (i32.const 30) ) ) ) diff --git a/test/lit/passes/instrument-branch-hints.wast b/test/lit/passes/instrument-branch-hints.wast index 5a6d73f5e0e..8998e3dbdb4 100644 --- a/test/lit/passes/instrument-branch-hints.wast +++ b/test/lit/passes/instrument-branch-hints.wast @@ -11,28 +11,20 @@ ;; CHECK: (type $3 (func (param anyref))) - ;; CHECK: (type $4 (func (param i32 i32 i32))) + ;; CHECK: (type $4 (func (param i32 i32 i32) (result i32))) - ;; CHECK: (import "fuzzing-support" "log-branch" (func $log-branch (type $4) (param i32 i32 i32))) + ;; CHECK: (import "fuzzing-support" "log-branch" (func $log-branch (type $4) (param i32 i32 i32) (result i32))) ;; CHECK: (tag $i32 (type $1) (param i32)) (tag $i32 (param i32)) ;; CHECK: (func $if (type $0) - ;; CHECK-NEXT: (local $0 i32) - ;; CHECK-NEXT: (local $1 i32) ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $0 - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log-branch - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (call $log-branch + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (then ;; CHECK-NEXT: (drop @@ -47,16 +39,10 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $1 - ;; CHECK-NEXT: (i32.const 142) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log-branch - ;; CHECK-NEXT: (i32.const 2) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (call $log-branch + ;; CHECK-NEXT: (i32.const 142) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (then ;; CHECK-NEXT: (drop @@ -95,7 +81,6 @@ ) ;; CHECK: (func $if-2 (type $0) - ;; CHECK-NEXT: (local $0 i32) ;; CHECK-NEXT: (if ;; CHECK-NEXT: (i32.const 242) ;; CHECK-NEXT: (then @@ -111,16 +96,10 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $0 - ;; CHECK-NEXT: (i32.const 342) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log-branch - ;; CHECK-NEXT: (i32.const 3) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (call $log-branch + ;; CHECK-NEXT: (i32.const 342) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (then ;; CHECK-NEXT: (drop @@ -158,21 +137,13 @@ ) ;; CHECK: (func $br (type $0) - ;; CHECK-NEXT: (local $0 i32) - ;; CHECK-NEXT: (local $1 i32) ;; CHECK-NEXT: (block $out ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") ;; CHECK-NEXT: (br_if $out - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $0 - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log-branch - ;; CHECK-NEXT: (i32.const 4) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (call $log-branch + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 4) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop @@ -182,16 +153,10 @@ ;; CHECK-NEXT: (block $out1 ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") ;; CHECK-NEXT: (br_if $out1 - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $1 - ;; CHECK-NEXT: (i32.const 142) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log-branch - ;; CHECK-NEXT: (i32.const 5) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (call $log-branch + ;; CHECK-NEXT: (i32.const 142) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 5) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop @@ -239,22 +204,15 @@ ;; CHECK: (func $br_value (type $2) (result f64) ;; CHECK-NEXT: (local $scratch f64) - ;; CHECK-NEXT: (local $1 i32) ;; CHECK-NEXT: (block $out (result f64) ;; CHECK-NEXT: (local.set $scratch ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") ;; CHECK-NEXT: (br_if $out ;; CHECK-NEXT: (f64.const 3.14159) - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $1 - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log-branch - ;; CHECK-NEXT: (i32.const 6) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (call $log-branch + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 6) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -277,54 +235,33 @@ ) ;; CHECK: (func $nested (type $0) - ;; CHECK-NEXT: (local $0 i32) - ;; CHECK-NEXT: (local $1 i32) - ;; CHECK-NEXT: (local $2 i32) ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $2 - ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") - ;; CHECK-NEXT: (if (result i32) - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $0 - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log-branch - ;; CHECK-NEXT: (i32.const 7) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (i32.const 142) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (else - ;; CHECK-NEXT: (i32.const 242) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $log-branch + ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (call $log-branch + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.const 142) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (i32.const 242) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log-branch - ;; CHECK-NEXT: (i32.const 9) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $2) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 9) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (then ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $1 - ;; CHECK-NEXT: (i32.const 342) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log-branch - ;; CHECK-NEXT: (i32.const 8) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (call $log-branch + ;; CHECK-NEXT: (i32.const 342) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 8) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (then ;; CHECK-NEXT: (drop @@ -385,29 +322,18 @@ ) ;; CHECK: (func $eh-pop (type $0) - ;; CHECK-NEXT: (local $0 i32) - ;; CHECK-NEXT: (local $1 i32) ;; CHECK-NEXT: (block $label ;; CHECK-NEXT: (try ;; CHECK-NEXT: (do ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (catch $i32 - ;; CHECK-NEXT: (local.set $1 - ;; CHECK-NEXT: (pop i32) - ;; CHECK-NEXT: ) ;; CHECK-NEXT: (@metadata.code.branch_hint "\00") ;; CHECK-NEXT: (br_if $label - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $0 - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log-branch - ;; CHECK-NEXT: (i32.const 10) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (local.get $0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (call $log-branch + ;; CHECK-NEXT: (pop i32) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 10) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -423,8 +349,8 @@ (catch $i32 (@metadata.code.branch_hint "\00") (br_if $label - ;; This pop will end up in a block after our instrumentation, which - ;; requires fixups. + ;; This pop will end up as the first parameter of the call, so it + ;; will not need fixups. (pop i32) ) ) @@ -436,43 +362,38 @@ ;; This module has an existing import with our module and base names. We nop it ;; and create a fresh one, to avoid confusion. (module - (import "fuzzing-support" "log-branch" (func $existing (param i32 i32 i32))) + (import "fuzzing-support" "log-branch" (func $existing (param i32 i32 i32) (result i32))) - ;; CHECK: (type $0 (func (param i32 i32 i32))) + ;; CHECK: (type $0 (func (param i32 i32 i32) (result i32))) ;; CHECK: (type $1 (func)) - ;; CHECK: (import "fuzzing-support" "log-branch" (func $log-branch (type $0) (param i32 i32 i32))) + ;; CHECK: (import "fuzzing-support" "log-branch" (func $log-branch (type $0) (param i32 i32 i32) (result i32))) - ;; CHECK: (func $existing (type $0) (param $0 i32) (param $1 i32) (param $2 i32) - ;; CHECK-NEXT: (nop) + ;; CHECK: (func $existing (type $0) (param $0 i32) (param $1 i32) (param $2 i32) (result i32) + ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK: (func $if (type $1) ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (local $1 i32) ;; CHECK-NEXT: (@metadata.code.branch_hint "\01") ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $1 - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $log-branch + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (call $existing ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (call $log-branch - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (then ;; CHECK-NEXT: (drop @@ -489,10 +410,12 @@ (local.set $x (i32.const 42) ) - (call $existing - (i32.const 42) - (i32.const 1) - (local.get $x) + (drop + (call $existing + (i32.const 42) + (i32.const 1) + (local.get $x) + ) ) (local.get $x) ) From 604f547f5ccb51cdc02c1b12fe96a6d045c602d5 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 20 Apr 2026 11:06:15 -0700 Subject: [PATCH 043/168] [StackIR] Optimize the simple case of a multivalue tee and extracts (#8623) When getting a multivalue from e.g. a call and then consuming it immediately, we get this pattern: ```wat (tuple.extract 3 0 (local.tee $temp (call $multivalue-return) ) ) (tuple.extract 3 1 (local.get $temp) ) (tuple.extract 3 2 (local.get $temp) ) ``` We save the entire tuple to a local, then read index by index. In StackIR we can just remove all the tuple operations, as the value is on the stack and ready to be used. Fixes #8618 --- src/wasm/wasm-stack-opts.cpp | 73 +++ .../passes/optimize-stack-ir-multivalue.wast | 582 ++++++++++++++++++ 2 files changed, 655 insertions(+) create mode 100644 test/lit/passes/optimize-stack-ir-multivalue.wast diff --git a/src/wasm/wasm-stack-opts.cpp b/src/wasm/wasm-stack-opts.cpp index eae9b6c681f..0050bd95fe5 100644 --- a/src/wasm/wasm-stack-opts.cpp +++ b/src/wasm/wasm-stack-opts.cpp @@ -272,6 +272,79 @@ void StackIROptimizer::local2Stack() { values.push_back(instIndex); } } + + // Optimize the simple case of a multivalue tee and extract. If an expression + // returns a tuple, and that tuple is immediately consumed, we end up with + // something like this: + // + // local.tee $1 + // tuple.extract 4 0 + // local.get $1 + // tuple.extract 4 1 + // local.get $1 + // tuple.extract 4 2 + // local.get $1 + // tuple.extract 4 3 + // + // The tuple is teed, then we extract the components one by one. If no other + // uses of the tee exist, we can just remove all of this. + for (Index instIndex = 0; instIndex < insts.size(); instIndex++) { + auto* inst = insts[instIndex]; + if (!inst) { + continue; + } + auto* tee = inst->origin->dynCast(); + if (!tee || !tee->type.isTuple()) { + continue; + } + + // The tee must be read by exactly the proper number of gets, and no more, + // which is one less than the tuple size (the tee provides one get). + auto size = tee->type.size(); + auto& setInfluences = localGraph.getSetInfluences(tee); + if (setInfluences.size() != size - 1) { + continue; + } + + // This is a tee of a tuple. Look for the expected extracts/gets. Each + // tuple index has 2 items. + bool ok = true; + for (Index i = 0; i < size; i++) { + // Each tuple index has a pair of items. + auto tupleIndexStart = instIndex + i * 2; + if (tupleIndexStart + 1 >= insts.size()) { + ok = false; + break; + } + auto* first = insts[tupleIndexStart]; + auto* second = insts[tupleIndexStart + 1]; + if (!first || !second) { + ok = false; + break; + } + // The first tuple index has the tee (already validated). Others have a + // get. + if (i != 0) { + auto* get = first->origin->dynCast(); + if (!get || get->index != tee->index) { + ok = false; + break; + } + } + // The second item of the pair is an extract. + auto* extract = second->origin->dynCast(); + if (!extract || extract->index != i) { + ok = false; + break; + } + } + if (ok) { + // Optimize. + for (Index i = 0; i < size * 2; i++) { + insts[instIndex + i] = nullptr; + } + } + } } // There may be unnecessary blocks we can remove: blocks without arriving diff --git a/test/lit/passes/optimize-stack-ir-multivalue.wast b/test/lit/passes/optimize-stack-ir-multivalue.wast new file mode 100644 index 00000000000..0eeb4dee870 --- /dev/null +++ b/test/lit/passes/optimize-stack-ir-multivalue.wast @@ -0,0 +1,582 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: wasm-opt %s -all --optimize-level=3 --generate-stack-ir --optimize-stack-ir --print-stack-ir | filecheck %s + +;; Also test with roundtrip to verify that parsing does not undo this +;; optimization. +;; RUN: wasm-opt %s -all --optimize-level=3 --generate-stack-ir --optimize-stack-ir --roundtrip --print-stack-ir | filecheck %s --check-prefix=ROUNDTRIP + +(module + ;; CHECK: (type $0 (func (result i32 f64 anyref))) + + ;; CHECK: (type $1 (func (param f64) (result i32 f64 anyref))) + + ;; CHECK: (type $2 (func (param i32 f64 anyref))) + + ;; CHECK: (type $3 (func (result i32 f64))) + + ;; CHECK: (type $4 (func (result i32 f64 anyref eqref))) + + ;; CHECK: (type $5 (func (result i32 f64 anyref anyref))) + + ;; CHECK: (type $6 (func (param f64) (result i32 anyref anyref))) + + ;; CHECK: (func $multivalue-return (type $0) (result i32 f64 anyref) + ;; CHECK-NEXT: (local $temp (tuple i32 f64 anyref)) + ;; CHECK-NEXT: call $multivalue-return + ;; CHECK-NEXT: tuple.make 3 + ;; CHECK-NEXT: ) + ;; ROUNDTRIP: (type $0 (func (result i32 f64 anyref))) + + ;; ROUNDTRIP: (type $1 (func (param f64) (result i32 f64 anyref))) + + ;; ROUNDTRIP: (type $2 (func (param i32 f64 anyref))) + + ;; ROUNDTRIP: (type $3 (func (result i32 f64))) + + ;; ROUNDTRIP: (type $4 (func (result i32 f64 anyref eqref))) + + ;; ROUNDTRIP: (type $5 (func (result i32 f64 anyref anyref))) + + ;; ROUNDTRIP: (type $6 (func (param f64) (result i32 anyref anyref))) + + ;; ROUNDTRIP: (func $multivalue-return (type $0) (result i32 f64 anyref) + ;; ROUNDTRIP-NEXT: (local $temp i32) + ;; ROUNDTRIP-NEXT: (local $1 f64) + ;; ROUNDTRIP-NEXT: (local $2 anyref) + ;; ROUNDTRIP-NEXT: call $multivalue-return + ;; ROUNDTRIP-NEXT: ) + (func $multivalue-return (result i32 f64 anyref) + (local $temp (tuple i32 f64 anyref)) + ;; We can remove all these tuple operations after optiming and + ;; roundtripping (though a few locals will be added in roundtripping FIXME). + (tuple.make 3 + (tuple.extract 3 0 + (local.tee $temp + (call $multivalue-return) + ) + ) + (tuple.extract 3 1 + (local.get $temp) + ) + (tuple.extract 3 2 + (local.get $temp) + ) + ) + ) + + ;; CHECK: (func $multivalue-return-too-short (type $3) (result i32 f64) + ;; CHECK-NEXT: (local $temp (tuple i32 f64 anyref)) + ;; CHECK-NEXT: call $multivalue-return + ;; CHECK-NEXT: local.tee $temp + ;; CHECK-NEXT: tuple.extract 3 0 + ;; CHECK-NEXT: local.get $temp + ;; CHECK-NEXT: tuple.extract 3 1 + ;; CHECK-NEXT: tuple.make 2 + ;; CHECK-NEXT: ) + ;; ROUNDTRIP: (func $multivalue-return-too-short (type $3) (result i32 f64) + ;; ROUNDTRIP-NEXT: (local $temp i32) + ;; ROUNDTRIP-NEXT: (local $1 f64) + ;; ROUNDTRIP-NEXT: (local $2 anyref) + ;; ROUNDTRIP-NEXT: (local $scratch (tuple i32 f64 anyref)) + ;; ROUNDTRIP-NEXT: (local $scratch_4 f64) + ;; ROUNDTRIP-NEXT: (local $scratch_5 i32) + ;; ROUNDTRIP-NEXT: call $multivalue-return + ;; ROUNDTRIP-NEXT: local.tee $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 0 + ;; ROUNDTRIP-NEXT: local.get $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 1 + ;; ROUNDTRIP-NEXT: local.get $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 2 + ;; ROUNDTRIP-NEXT: local.set $2 + ;; ROUNDTRIP-NEXT: local.set $1 + ;; ROUNDTRIP-NEXT: local.tee $temp + ;; ROUNDTRIP-NEXT: local.get $1 + ;; ROUNDTRIP-NEXT: tuple.make 2 + ;; ROUNDTRIP-NEXT: ) + (func $multivalue-return-too-short (result i32 f64) + (local $temp (tuple i32 f64 anyref)) + ;; As above, but we only return 2 of the tuple's 3 items (i.e., we are too + ;; short to fit the pattern), so we do not optimize here. + (tuple.make 2 + (tuple.extract 3 0 + (local.tee $temp + (call $multivalue-return) + ) + ) + (tuple.extract 3 1 + (local.get $temp) + ) + ) + ) + + ;; CHECK: (func $multivalue-return-extra (type $4) (result i32 f64 anyref eqref) + ;; CHECK-NEXT: (local $temp (tuple i32 f64 anyref)) + ;; CHECK-NEXT: call $multivalue-return + ;; CHECK-NEXT: ref.null none + ;; CHECK-NEXT: tuple.make 4 + ;; CHECK-NEXT: ) + ;; ROUNDTRIP: (func $multivalue-return-extra (type $4) (result i32 f64 anyref eqref) + ;; ROUNDTRIP-NEXT: (local $temp i32) + ;; ROUNDTRIP-NEXT: (local $1 f64) + ;; ROUNDTRIP-NEXT: (local $2 anyref) + ;; ROUNDTRIP-NEXT: (local $scratch (tuple i32 f64 anyref)) + ;; ROUNDTRIP-NEXT: call $multivalue-return + ;; ROUNDTRIP-NEXT: ref.null none + ;; ROUNDTRIP-NEXT: tuple.make 4 + ;; ROUNDTRIP-NEXT: ) + (func $multivalue-return-extra (result i32 f64 anyref eqref) + (local $temp (tuple i32 f64 anyref)) + ;; As above, but we add an item to the tuple. We can optimize here. + (tuple.make 4 + (tuple.extract 3 0 + (local.tee $temp + (call $multivalue-return) + ) + ) + (tuple.extract 3 1 + (local.get $temp) + ) + (tuple.extract 3 2 + (local.get $temp) + ) + (ref.null eq) + ) + ) + + ;; CHECK: (func $multivalue-return-extra-middle (type $5) (result i32 f64 anyref anyref) + ;; CHECK-NEXT: (local $temp (tuple i32 f64 anyref)) + ;; CHECK-NEXT: call $multivalue-return + ;; CHECK-NEXT: local.tee $temp + ;; CHECK-NEXT: tuple.extract 3 0 + ;; CHECK-NEXT: local.get $temp + ;; CHECK-NEXT: tuple.extract 3 1 + ;; CHECK-NEXT: ref.null none + ;; CHECK-NEXT: local.get $temp + ;; CHECK-NEXT: tuple.extract 3 2 + ;; CHECK-NEXT: tuple.make 4 + ;; CHECK-NEXT: ) + ;; ROUNDTRIP: (func $multivalue-return-extra-middle (type $5) (result i32 f64 anyref anyref) + ;; ROUNDTRIP-NEXT: (local $temp i32) + ;; ROUNDTRIP-NEXT: (local $1 f64) + ;; ROUNDTRIP-NEXT: (local $2 anyref) + ;; ROUNDTRIP-NEXT: (local $scratch (tuple i32 f64 anyref)) + ;; ROUNDTRIP-NEXT: (local $scratch_4 f64) + ;; ROUNDTRIP-NEXT: (local $scratch_5 i32) + ;; ROUNDTRIP-NEXT: call $multivalue-return + ;; ROUNDTRIP-NEXT: local.tee $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 0 + ;; ROUNDTRIP-NEXT: local.get $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 1 + ;; ROUNDTRIP-NEXT: local.get $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 2 + ;; ROUNDTRIP-NEXT: local.set $2 + ;; ROUNDTRIP-NEXT: local.set $1 + ;; ROUNDTRIP-NEXT: local.tee $temp + ;; ROUNDTRIP-NEXT: local.get $1 + ;; ROUNDTRIP-NEXT: ref.null none + ;; ROUNDTRIP-NEXT: local.get $2 + ;; ROUNDTRIP-NEXT: tuple.make 4 + ;; ROUNDTRIP-NEXT: ) + (func $multivalue-return-extra-middle (result i32 f64 anyref anyref) + (local $temp (tuple i32 f64 anyref)) + ;; As the last case, but the extra item is in the middle. We cannot + ;; optimize. + (tuple.make 4 + (tuple.extract 3 0 + (local.tee $temp + (call $multivalue-return) + ) + ) + (tuple.extract 3 1 + (local.get $temp) + ) + (ref.null eq) + (tuple.extract 3 2 + (local.get $temp) + ) + ) + ) + + ;; CHECK: (func $multivalue-return-bad-get (type $1) (param $other f64) (result i32 f64 anyref) + ;; CHECK-NEXT: (local $temp (tuple i32 f64 anyref)) + ;; CHECK-NEXT: call $multivalue-return + ;; CHECK-NEXT: local.tee $temp + ;; CHECK-NEXT: tuple.extract 3 0 + ;; CHECK-NEXT: local.get $other + ;; CHECK-NEXT: local.get $temp + ;; CHECK-NEXT: tuple.extract 3 2 + ;; CHECK-NEXT: tuple.make 3 + ;; CHECK-NEXT: ) + ;; ROUNDTRIP: (func $multivalue-return-bad-get (type $1) (param $other f64) (result i32 f64 anyref) + ;; ROUNDTRIP-NEXT: (local $temp i32) + ;; ROUNDTRIP-NEXT: (local $2 f64) + ;; ROUNDTRIP-NEXT: (local $3 anyref) + ;; ROUNDTRIP-NEXT: (local $scratch (tuple i32 f64 anyref)) + ;; ROUNDTRIP-NEXT: (local $scratch_5 f64) + ;; ROUNDTRIP-NEXT: (local $scratch_6 i32) + ;; ROUNDTRIP-NEXT: call $multivalue-return + ;; ROUNDTRIP-NEXT: local.tee $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 0 + ;; ROUNDTRIP-NEXT: local.get $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 1 + ;; ROUNDTRIP-NEXT: local.get $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 2 + ;; ROUNDTRIP-NEXT: local.set $3 + ;; ROUNDTRIP-NEXT: local.set $2 + ;; ROUNDTRIP-NEXT: local.tee $temp + ;; ROUNDTRIP-NEXT: local.get $other + ;; ROUNDTRIP-NEXT: local.get $3 + ;; ROUNDTRIP-NEXT: tuple.make 3 + ;; ROUNDTRIP-NEXT: ) + (func $multivalue-return-bad-get (param $other f64) (result i32 f64 anyref) + (local $temp (tuple i32 f64 anyref)) + ;; As the first case, but one get has the wrong index, so we do + ;; not optimize. + (tuple.make 3 + (tuple.extract 3 0 + (local.tee $temp + (call $multivalue-return) + ) + ) + (local.get $other) ;; this changed + (tuple.extract 3 2 + (local.get $temp) + ) + ) + ) + + ;; CHECK: (func $multivalue-return-non-get (type $0) (result i32 f64 anyref) + ;; CHECK-NEXT: (local $temp (tuple i32 f64 anyref)) + ;; CHECK-NEXT: call $multivalue-return + ;; CHECK-NEXT: local.tee $temp + ;; CHECK-NEXT: tuple.extract 3 0 + ;; CHECK-NEXT: local.get $temp + ;; CHECK-NEXT: tuple.extract 3 1 + ;; CHECK-NEXT: local.get $temp + ;; CHECK-NEXT: tuple.extract 3 2 + ;; CHECK-NEXT: tuple.make 3 + ;; CHECK-NEXT: ) + ;; ROUNDTRIP: (func $multivalue-return-non-get (type $0) (result i32 f64 anyref) + ;; ROUNDTRIP-NEXT: (local $temp i32) + ;; ROUNDTRIP-NEXT: (local $1 f64) + ;; ROUNDTRIP-NEXT: (local $2 anyref) + ;; ROUNDTRIP-NEXT: (local $scratch (tuple i32 f64 anyref)) + ;; ROUNDTRIP-NEXT: (local $scratch_4 f64) + ;; ROUNDTRIP-NEXT: (local $scratch_5 i32) + ;; ROUNDTRIP-NEXT: call $multivalue-return + ;; ROUNDTRIP-NEXT: local.tee $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 0 + ;; ROUNDTRIP-NEXT: local.get $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 1 + ;; ROUNDTRIP-NEXT: local.get $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 2 + ;; ROUNDTRIP-NEXT: local.set $2 + ;; ROUNDTRIP-NEXT: local.set $1 + ;; ROUNDTRIP-NEXT: local.tee $temp + ;; ROUNDTRIP-NEXT: local.get $1 + ;; ROUNDTRIP-NEXT: local.get $2 + ;; ROUNDTRIP-NEXT: tuple.make 3 + ;; ROUNDTRIP-NEXT: ) + (func $multivalue-return-non-get (result i32 f64 anyref) + (local $temp (tuple i32 f64 anyref)) + ;; As the first case, but one get is replaced by a non-get, so we do + ;; not optimize. + (tuple.make 3 + (tuple.extract 3 0 + (local.tee $temp + (call $multivalue-return) + ) + ) + (tuple.extract 3 1 + (nop) ;; this breaks the pattern, appearing where the get + ;; should be + (local.get $temp) + ) + (tuple.extract 3 2 + (local.get $temp) + ) + ) + ) + + ;; CHECK: (func $multivalue-return-bad-extract (type $6) (param $other f64) (result i32 anyref anyref) + ;; CHECK-NEXT: (local $temp (tuple i32 f64 anyref)) + ;; CHECK-NEXT: call $multivalue-return + ;; CHECK-NEXT: local.tee $temp + ;; CHECK-NEXT: tuple.extract 3 0 + ;; CHECK-NEXT: local.get $temp + ;; CHECK-NEXT: tuple.extract 3 2 + ;; CHECK-NEXT: local.get $temp + ;; CHECK-NEXT: tuple.extract 3 2 + ;; CHECK-NEXT: tuple.make 3 + ;; CHECK-NEXT: ) + ;; ROUNDTRIP: (func $multivalue-return-bad-extract (type $6) (param $other f64) (result i32 anyref anyref) + ;; ROUNDTRIP-NEXT: (local $temp i32) + ;; ROUNDTRIP-NEXT: (local $2 f64) + ;; ROUNDTRIP-NEXT: (local $3 anyref) + ;; ROUNDTRIP-NEXT: (local $scratch (tuple i32 f64 anyref)) + ;; ROUNDTRIP-NEXT: (local $scratch_5 f64) + ;; ROUNDTRIP-NEXT: (local $scratch_6 i32) + ;; ROUNDTRIP-NEXT: call $multivalue-return + ;; ROUNDTRIP-NEXT: local.tee $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 0 + ;; ROUNDTRIP-NEXT: local.get $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 1 + ;; ROUNDTRIP-NEXT: local.get $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 2 + ;; ROUNDTRIP-NEXT: local.set $3 + ;; ROUNDTRIP-NEXT: local.set $2 + ;; ROUNDTRIP-NEXT: local.tee $temp + ;; ROUNDTRIP-NEXT: local.get $3 + ;; ROUNDTRIP-NEXT: local.get $3 + ;; ROUNDTRIP-NEXT: tuple.make 3 + ;; ROUNDTRIP-NEXT: ) + (func $multivalue-return-bad-extract (param $other f64) (result i32 anyref anyref) + (local $temp (tuple i32 f64 anyref)) + ;; As the first case, but one extract has the wrong index, so we + ;; do not optimize. + (tuple.make 3 + (tuple.extract 3 0 + (local.tee $temp + (call $multivalue-return) + ) + ) + (tuple.extract 3 2 ;; this changed from 1 to 2 + (local.get $temp) + ) + (tuple.extract 3 2 + (local.get $temp) + ) + ) + ) + ;; CHECK: (func $multivalue-return-non-extract (type $1) (param $other f64) (result i32 f64 anyref) + ;; CHECK-NEXT: (local $temp (tuple i32 f64 anyref)) + ;; CHECK-NEXT: (local $scratch (tuple i32 f64 anyref)) + ;; CHECK-NEXT: call $multivalue-return + ;; CHECK-NEXT: local.tee $temp + ;; CHECK-NEXT: tuple.extract 3 0 + ;; CHECK-NEXT: local.get $temp + ;; CHECK-NEXT: local.set $scratch + ;; CHECK-NEXT: local.get $scratch + ;; CHECK-NEXT: tuple.extract 3 1 + ;; CHECK-NEXT: local.get $temp + ;; CHECK-NEXT: tuple.extract 3 2 + ;; CHECK-NEXT: tuple.make 3 + ;; CHECK-NEXT: ) + ;; ROUNDTRIP: (func $multivalue-return-non-extract (type $1) (param $other f64) (result i32 f64 anyref) + ;; ROUNDTRIP-NEXT: (local $temp i32) + ;; ROUNDTRIP-NEXT: (local $scratch i32) + ;; ROUNDTRIP-NEXT: (local $3 f64) + ;; ROUNDTRIP-NEXT: (local $4 f64) + ;; ROUNDTRIP-NEXT: (local $5 f64) + ;; ROUNDTRIP-NEXT: (local $6 anyref) + ;; ROUNDTRIP-NEXT: (local $7 anyref) + ;; ROUNDTRIP-NEXT: (local $scratch_8 (tuple i32 f64 anyref)) + ;; ROUNDTRIP-NEXT: (local $scratch_9 f64) + ;; ROUNDTRIP-NEXT: (local $scratch_10 i32) + ;; ROUNDTRIP-NEXT: (local $scratch_11 f64) + ;; ROUNDTRIP-NEXT: (local $scratch_12 i32) + ;; ROUNDTRIP-NEXT: (local $scratch_13 f64) + ;; ROUNDTRIP-NEXT: (local $scratch_14 i32) + ;; ROUNDTRIP-NEXT: call $multivalue-return + ;; ROUNDTRIP-NEXT: local.tee $scratch_8 + ;; ROUNDTRIP-NEXT: tuple.extract 3 0 + ;; ROUNDTRIP-NEXT: local.get $scratch_8 + ;; ROUNDTRIP-NEXT: tuple.extract 3 1 + ;; ROUNDTRIP-NEXT: local.get $scratch_8 + ;; ROUNDTRIP-NEXT: tuple.extract 3 2 + ;; ROUNDTRIP-NEXT: local.set $6 + ;; ROUNDTRIP-NEXT: local.set $3 + ;; ROUNDTRIP-NEXT: local.tee $temp + ;; ROUNDTRIP-NEXT: local.get $temp + ;; ROUNDTRIP-NEXT: local.get $3 + ;; ROUNDTRIP-NEXT: local.get $6 + ;; ROUNDTRIP-NEXT: local.set $7 + ;; ROUNDTRIP-NEXT: local.set $4 + ;; ROUNDTRIP-NEXT: local.get $4 + ;; ROUNDTRIP-NEXT: local.get $7 + ;; ROUNDTRIP-NEXT: drop + ;; ROUNDTRIP-NEXT: local.set $5 + ;; ROUNDTRIP-NEXT: drop + ;; ROUNDTRIP-NEXT: local.get $5 + ;; ROUNDTRIP-NEXT: local.get $6 + ;; ROUNDTRIP-NEXT: tuple.make 3 + ;; ROUNDTRIP-NEXT: ) + (func $multivalue-return-non-extract (param $other f64) (result i32 f64 anyref) + (local $temp (tuple i32 f64 anyref)) + ;; As the first case, but one extract is replaced with something else, so we + ;; do not optimize. + (tuple.make 3 + (tuple.extract 3 0 + (local.tee $temp + (call $multivalue-return) + ) + ) + (tuple.extract 3 1 + (local.get $temp) + (nop) ;; this breaks the pattern, appearing where the tuple.extract + ;; should be + ) + (tuple.extract 3 2 + (local.get $temp) + ) + ) + ) + + ;; CHECK: (func $multiple-multivalue-return (type $2) (param $0 i32) (param $1 f64) (param $2 anyref) + ;; CHECK-NEXT: (local $temp3 (tuple i32 f64 anyref)) + ;; CHECK-NEXT: (local $temp2 (tuple i32 f64)) + ;; CHECK-NEXT: call $multivalue-return + ;; CHECK-NEXT: call $multiple-multivalue-return + ;; CHECK-NEXT: call $multivalue-return-too-short + ;; CHECK-NEXT: ref.null none + ;; CHECK-NEXT: call $multiple-multivalue-return + ;; CHECK-NEXT: call $multivalue-return + ;; CHECK-NEXT: call $multiple-multivalue-return + ;; CHECK-NEXT: ) + ;; ROUNDTRIP: (func $multiple-multivalue-return (type $2) (param $0 i32) (param $1 f64) (param $2 anyref) + ;; ROUNDTRIP-NEXT: (local $temp3 i32) + ;; ROUNDTRIP-NEXT: (local $temp2 i32) + ;; ROUNDTRIP-NEXT: (local $5 f64) + ;; ROUNDTRIP-NEXT: (local $6 f64) + ;; ROUNDTRIP-NEXT: (local $7 anyref) + ;; ROUNDTRIP-NEXT: (local $scratch (tuple i32 f64 anyref)) + ;; ROUNDTRIP-NEXT: (local $scratch_9 (tuple i32 f64)) + ;; ROUNDTRIP-NEXT: (local $scratch_10 (tuple i32 f64 anyref)) + ;; ROUNDTRIP-NEXT: call $multivalue-return + ;; ROUNDTRIP-NEXT: call $multiple-multivalue-return + ;; ROUNDTRIP-NEXT: call $multivalue-return-too-short + ;; ROUNDTRIP-NEXT: ref.null none + ;; ROUNDTRIP-NEXT: call $multiple-multivalue-return + ;; ROUNDTRIP-NEXT: call $multivalue-return + ;; ROUNDTRIP-NEXT: call $multiple-multivalue-return + ;; ROUNDTRIP-NEXT: ) + (func $multiple-multivalue-return (param i32 f64 anyref) + (local $temp3 (tuple i32 f64 anyref)) + (local $temp2 (tuple i32 f64)) + + ;; Multiple optimizations in one function, including a case where we reuse + ;; the local index. + + (call $multiple-multivalue-return + (tuple.extract 3 0 + (local.tee $temp3 + (call $multivalue-return) + ) + ) + (tuple.extract 3 1 + (local.get $temp3) + ) + (tuple.extract 3 2 + (local.get $temp3) + ) + ) + + (call $multiple-multivalue-return + (tuple.extract 2 0 + (local.tee $temp2 + (call $multivalue-return-too-short) + ) + ) + (tuple.extract 2 1 + (local.get $temp2) + ) + (ref.null any) + ) + + (call $multiple-multivalue-return + (tuple.extract 3 0 + (local.tee $temp3 + (call $multivalue-return) + ) + ) + (tuple.extract 3 1 + (local.get $temp3) + ) + (tuple.extract 3 2 + (local.get $temp3) + ) + ) + ) + + ;; CHECK: (func $multiple-multivalue-return-local-reuse (type $2) (param $0 i32) (param $1 f64) (param $2 anyref) + ;; CHECK-NEXT: (local $temp3 (tuple i32 f64 anyref)) + ;; CHECK-NEXT: (local $temp2 (tuple i32 f64)) + ;; CHECK-NEXT: call $multivalue-return + ;; CHECK-NEXT: local.tee $temp3 + ;; CHECK-NEXT: tuple.extract 3 0 + ;; CHECK-NEXT: local.get $temp3 + ;; CHECK-NEXT: tuple.extract 3 1 + ;; CHECK-NEXT: local.get $temp3 + ;; CHECK-NEXT: tuple.extract 3 2 + ;; CHECK-NEXT: call $multiple-multivalue-return + ;; CHECK-NEXT: local.get $temp3 + ;; CHECK-NEXT: tuple.extract 3 0 + ;; CHECK-NEXT: local.get $temp3 + ;; CHECK-NEXT: tuple.extract 3 1 + ;; CHECK-NEXT: local.get $temp3 + ;; CHECK-NEXT: tuple.extract 3 2 + ;; CHECK-NEXT: call $multiple-multivalue-return + ;; CHECK-NEXT: ) + ;; ROUNDTRIP: (func $multiple-multivalue-return-local-reuse (type $2) (param $0 i32) (param $1 f64) (param $2 anyref) + ;; ROUNDTRIP-NEXT: (local $temp3 i32) + ;; ROUNDTRIP-NEXT: (local $temp2 i32) + ;; ROUNDTRIP-NEXT: (local $5 f64) + ;; ROUNDTRIP-NEXT: (local $6 f64) + ;; ROUNDTRIP-NEXT: (local $7 anyref) + ;; ROUNDTRIP-NEXT: (local $scratch (tuple i32 f64 anyref)) + ;; ROUNDTRIP-NEXT: (local $scratch_9 f64) + ;; ROUNDTRIP-NEXT: (local $scratch_10 i32) + ;; ROUNDTRIP-NEXT: call $multivalue-return + ;; ROUNDTRIP-NEXT: local.tee $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 0 + ;; ROUNDTRIP-NEXT: local.get $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 1 + ;; ROUNDTRIP-NEXT: local.get $scratch + ;; ROUNDTRIP-NEXT: tuple.extract 3 2 + ;; ROUNDTRIP-NEXT: local.set $7 + ;; ROUNDTRIP-NEXT: local.set $5 + ;; ROUNDTRIP-NEXT: local.tee $temp3 + ;; ROUNDTRIP-NEXT: local.get $5 + ;; ROUNDTRIP-NEXT: local.get $7 + ;; ROUNDTRIP-NEXT: call $multiple-multivalue-return + ;; ROUNDTRIP-NEXT: local.get $temp3 + ;; ROUNDTRIP-NEXT: local.get $5 + ;; ROUNDTRIP-NEXT: local.get $7 + ;; ROUNDTRIP-NEXT: call $multiple-multivalue-return + ;; ROUNDTRIP-NEXT: ) + (func $multiple-multivalue-return-local-reuse (param i32 f64 anyref) + (local $temp3 (tuple i32 f64 anyref)) + (local $temp2 (tuple i32 f64)) + + ;; As the last case, we have two things to possibly optimize. Here we reuse + ;; the tee'd value after the first one, which prevents any optimizations of + ;; this pattern. + + (call $multiple-multivalue-return + (tuple.extract 3 0 + (local.tee $temp3 + (call $multivalue-return) + ) + ) + (tuple.extract 3 1 + (local.get $temp3) + ) + (tuple.extract 3 2 + (local.get $temp3) + ) + ) + + (call $multiple-multivalue-return + (tuple.extract 3 0 + (local.get $temp3) ;; this changed + ) + (tuple.extract 3 1 + (local.get $temp3) + ) + (tuple.extract 3 2 + (local.get $temp3) + ) + ) + ) +) From 3ef8d1916051fa001a93c37d3270e9b218c45dc9 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 20 Apr 2026 15:22:50 -0700 Subject: [PATCH 044/168] Look at fallthrough when deleting branch hints (#8630) The DeleteBranchHints pass looks at the conditions of branches to find the ID operands of logging calls to determine which branch hints to remove. It previously expected the logging call to be the condition of the branch, but in the presence of stacky code it is possible for the parser to create a block around the logging call. Handle this pattern by looking for the logging call at the condition's fallthrough value. --- src/passes/InstrumentBranchHints.cpp | 41 ++++++++++++------------ test/lit/passes/delete-branch-hints.wast | 39 ++++++++++++++++++++++ 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/passes/InstrumentBranchHints.cpp b/src/passes/InstrumentBranchHints.cpp index c552ddae891..9a80598877f 100644 --- a/src/passes/InstrumentBranchHints.cpp +++ b/src/passes/InstrumentBranchHints.cpp @@ -98,6 +98,7 @@ #include "ir/effects.h" #include "ir/names.h" +#include "ir/properties.h" #include "ir/utils.h" #include "pass.h" #include "support/string.h" @@ -231,18 +232,6 @@ struct InstrumentationProcessor : public WalkerPass> { Super::doWalkModule(module); } - - // Helpers - - // Check if an expression's condition is instrumented, and return the - // instrumentation call if so. Otherwise return null. - Call* getInstrumentation(Expression* condition) { - auto* call = condition->dynCast(); - if (!call || call->target != logBranch) { - return nullptr; - } - return call; - } }; struct DeleteBranchHints : public InstrumentationProcessor { @@ -251,15 +240,27 @@ struct DeleteBranchHints : public InstrumentationProcessor { // The set of IDs to delete. std::unordered_set idsToDelete; + std::optional getBranchID(Expression* condition, + const PassOptions& passOptions, + Module& wasm) { + auto* call = + Properties::getFallthrough(condition, getPassOptions(), *getModule()) + ->dynCast(); + if (!call || call->target != logBranch || call->operands.size() != 3) { + return std::nullopt; + } + auto* c = call->operands[2]->dynCast(); + if (!c || c->type != Type::i32) { + return std::nullopt; + } + return c->value.geti32(); + } + template void processCondition(T* curr) { - if (auto* call = getInstrumentation(curr->condition)) { - if (auto* c = call->operands[2]->template dynCast()) { - auto id = c->value.geti32(); - if (idsToDelete.contains(id)) { - // Remove the branch hint. - getFunction()->codeAnnotations[curr].branchLikely = {}; - } - } + if (auto id = getBranchID(curr->condition, getPassOptions(), *getModule()); + id && idsToDelete.contains(*id)) { + // Remove the branch hint. + getFunction()->codeAnnotations[curr].branchLikely = std::nullopt; } } diff --git a/test/lit/passes/delete-branch-hints.wast b/test/lit/passes/delete-branch-hints.wast index bbc59604a53..c31220fc246 100644 --- a/test/lit/passes/delete-branch-hints.wast +++ b/test/lit/passes/delete-branch-hints.wast @@ -7,6 +7,8 @@ ;; CHECK: (type $1 (func (param i32 i32 i32) (result i32))) + ;; CHECK: (type $2 (func (param i32) (result i32))) + ;; CHECK: (import "fuzzing-support" "log-branch" (func $log (type $1) (param i32 i32 i32) (result i32))) (import "fuzzing-support" "log-branch" (func $log (param i32 i32 i32) (result i32))) @@ -118,4 +120,41 @@ ) ) ) + + ;; CHECK: (func $stacky (type $2) (param $c i32) (result i32) + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (br_if $l + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (call $log + ;; CHECK-NEXT: (local.get $c) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $stacky (param $c i32) (result i32) + block $l (result i32) + i32.const 42 + ;; Because the parser greedily pulls previous none-typed expressions into + ;; block, this condition will be parsed as this: + ;; + ;; (block + ;; (nop) + ;; (call $log-branch ...)) + ;; ) + ;; + ;; We must be able to find and handle this pattern to remove the hint. + nop + local.get $c + i32.const 1 + i32.const 10 + call $log + (@metadata.code.branch_hint "\01") + br_if $l + end + ) ) From 534eab97491642dfb9d09885ccfef5d36b2d9eef Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 21 Apr 2026 10:42:49 -0700 Subject: [PATCH 045/168] [wasm-merge] Preserve function names when writing a profile (#8631) When wasm-merge writes a wasm-split profile, it uses names to identify functions. It is never correct to then throw those names away when writing the binary, because that would make the profile useless. To avoid this problem, have --output-manifest imply --debuginfo and preserve the function names. --- src/tools/wasm-merge.cpp | 6 ++++-- test/lit/help/wasm-merge.test | 3 ++- test/lit/merge/manifest.wat | 27 ++++++++++++++++++++++++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/tools/wasm-merge.cpp b/src/tools/wasm-merge.cpp index 24615804da1..2197bc27356 100644 --- a/src/tools/wasm-merge.cpp +++ b/src/tools/wasm-merge.cpp @@ -697,11 +697,13 @@ Input source maps can be specified by adding an -ism option right after the modu "", "Write a wasm-split manifest to the specified file. This manifest can " "be given to wasm-split to split the merged module along the lines of " - "the original modules.", + "the original modules. Implies --debuginfo to preserve function names " + "in the output module.", WasmMergeOption, Options::Arguments::One, - [&manifestFile](Options* o, const std::string& argument) { + [&](Options* o, const std::string& argument) { manifestFile = argument; + debugInfo = true; }) .add("--rename-export-conflicts", "-rec", diff --git a/test/lit/help/wasm-merge.test b/test/lit/help/wasm-merge.test index c0c4ee726a1..8c94b13745c 100644 --- a/test/lit/help/wasm-merge.test +++ b/test/lit/help/wasm-merge.test @@ -38,7 +38,8 @@ ;; CHECK-NEXT: specified file. This manifest can be ;; CHECK-NEXT: given to wasm-split to split the merged ;; CHECK-NEXT: module along the lines of the original -;; CHECK-NEXT: modules. +;; CHECK-NEXT: modules. Implies --debuginfo to preserve +;; CHECK-NEXT: function names in the output module. ;; CHECK-NEXT: ;; CHECK-NEXT: --rename-export-conflicts,-rec Rename exports to avoid conflicts (rather ;; CHECK-NEXT: than error) diff --git a/test/lit/merge/manifest.wat b/test/lit/merge/manifest.wat index b0d2ef213ad..c93b40cdef9 100644 --- a/test/lit/merge/manifest.wat +++ b/test/lit/merge/manifest.wat @@ -1,5 +1,6 @@ -;; RUN: wasm-merge %s first %s.second second %s.third third --output-manifest %t.manifest -S -o %t.wasm +;; RUN: wasm-merge %s first %s.second second %s.third third --output-manifest %t.manifest -o %t.wasm ;; RUN: cat %t.manifest | filecheck %s +;; RUN: wasm-dis %t.wasm -o - | filecheck %s --check-prefix MERGED ;; The first module is the primary module and does not appear in the manifest. ;; CHECK-NOT: first @@ -12,6 +13,30 @@ ;; CHECK-NEXT: third ;; CHECK-NEXT: qux +;; The binary should contain the original function names. +;; MERGED: (module +;; MERGED-NEXT: (type $0 (func)) +;; MERGED-NEXT: (import "env" "imported_first" (func $imported_first)) +;; MERGED-NEXT: (import "env" "imported_second" (func $imported_second)) +;; MERGED-NEXT: (import "env" "imported_third" (func $imported_third)) +;; MERGED-NEXT: (export "foo" (func $foo)) +;; MERGED-NEXT: (export "bar" (func $bar)) +;; MERGED-NEXT: (export "baz" (func $baz)) +;; MERGED-NEXT: (export "qux" (func $qux)) +;; MERGED-NEXT: (func $foo +;; MERGED-NEXT: (call $imported_first) +;; MERGED-NEXT: ) +;; MERGED-NEXT: (func $bar +;; MERGED-NEXT: (nop) +;; MERGED-NEXT: ) +;; MERGED-NEXT: (func $baz +;; MERGED-NEXT: (call $imported_second) +;; MERGED-NEXT: ) +;; MERGED-NEXT: (func $qux +;; MERGED-NEXT: (call $imported_third) +;; MERGED-NEXT: ) +;; MERGED-NEXT: ) + (module (import "env" "imported_first" (func $imported_first)) (func $foo (export "foo") From 1251efbc1ea471c1311d2726b2bbe061ff2a291c Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 21 Apr 2026 11:31:59 -0700 Subject: [PATCH 046/168] Avoid assertion in BrOn parsing (#8635) The ref and the desc are used as references in `finalize()`, and we assert there if they are e.g. `i32`. Fixes #8633 --- src/wasm/wasm-ir-builder.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index 6552df7e1a2..903e8e87167 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -2093,6 +2093,15 @@ Result<> IRBuilder::makeBrOn(Index label, } CHECK_ERR(visitBrOn(&curr)); + // Validate things that would cause errors later. + if (curr.ref->type != Type::unreachable && !curr.ref->type.isRef()) { + return Err{"br_on* ref must be a ref"}; + } + if (curr.desc && curr.desc->type != Type::unreachable && + !curr.desc->type.isRef()) { + return Err{"br_on_cast_desc* must be a ref"}; + } + // Validate type immediates before we forget them. switch (op) { case BrOnNull: From 40acafeb546c2a9438c61e72a036119624ac6a77 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 22 Apr 2026 03:48:37 +0900 Subject: [PATCH 047/168] Fix comment about --no-auto-initial-contents (#8636) #5943 changed the default from disabled to enabled but comment was left unchanged. --- scripts/test/shared.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test/shared.py b/scripts/test/shared.py index 3d36fb35fa1..6f39c1dc37d 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -95,8 +95,8 @@ def parse_args(args): parser.add_argument( '--no-auto-initial-contents', dest='auto_initial_contents', action='store_false', default=True, - help='Select important initial contents automaticaly in fuzzer. ' - 'Default: disabled.') + help='Disables the automatic selection of important initial contents ' + 'in fuzzer.') return parser.parse_args(args) From 2e86518e16851eaddf1f3a9cde7458e6c6f4d9b3 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 21 Apr 2026 14:02:22 -0700 Subject: [PATCH 048/168] PreserveImportsExportsJS fuzzer: Handle NaNs properly (#8620) 1. De-NaN when that setting is on. 2. Check if we can compare, which depends on NaNs. --- scripts/fuzz_opt.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 8af017f956d..41beca07e6c 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2157,13 +2157,19 @@ def do_handle_pair(self, input, before_wasm, after_wasm, opts): # Modify the initial wat to get the pre-optimizations wasm. pre_wasm = abspath('pre.wasm') - run([in_bin('wasm-opt'), input] + FEATURE_OPTS + [ + gen_args = [ + input, '-ttf', '--fuzz-preserve-imports-exports', '--initial-fuzz=' + wat_file, '-o', pre_wasm, '-g', - ]) + ] + # We do not copy all of GEN_ARGS, as we don't need e.g. legalization. + if not NANS: + # TODO: do we also need this in each reduction step? + gen_args += ['--denan'] + run([in_bin('wasm-opt')] + gen_args + FEATURE_OPTS) # We successfully generated pre_wasm; stash it for possible reduction # purposes later. @@ -2206,8 +2212,9 @@ def do_handle_pair(self, input, before_wasm, after_wasm, opts): post_vm = random.choice(vms) post = self.do_run(post_vm, js_file, post_wasm) - # Compare - compare(pre, post, 'PreserveImportsExportsJS') + # Compare, if we can. + if pre_vm.can_compare_to_other(post_vm): + compare(pre, post, 'PreserveImportsExportsJS') def do_run(self, vm, js, wasm): out = vm.run_js(js, wasm, checked=False) From 595f5af69fb984ce560aad7c61fddaf09939dc72 Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Wed, 22 Apr 2026 12:42:46 -0700 Subject: [PATCH 049/168] Account for global effects in LinearExecutionWalker (#8637) When --enable-exception-handling is true, we previously assumed that every call throws in LinearExecutionWalker which prevented some opportunities for optimizing locals (and potentially other optimizations in passes that use this class). Change the code to make use of global effects when present. The following passes use LinearExecutionWalker: * LocalGraph * LocalCSE * OptimizeCasts * SimplifyGlobals * SimplifyLocals It's also used in ContentOracle which is used in GUFA and TypeRefining. --- src/ir/linear-execution.h | 48 +++++- .../simplify-locals-global-effects-eh.wast | 163 ++++++++++++++++++ 2 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 test/lit/passes/simplify-locals-global-effects-eh.wast diff --git a/src/ir/linear-execution.h b/src/ir/linear-execution.h index e8b1923aacf..167400a0137 100644 --- a/src/ir/linear-execution.h +++ b/src/ir/linear-execution.h @@ -80,11 +80,12 @@ struct LinearExecutionWalker : public PostWalker { static void scan(SubType* self, Expression** currp) { Expression* curr = *currp; - auto handleCall = [&](bool isReturn) { + auto handleCall = [&](bool mayThrow, bool isReturn) { if (!self->connectAdjacentBlocks) { - // Control is nonlinear if we return, or if EH is enabled or may be. - if (isReturn || !self->getModule() || - self->getModule()->features.hasExceptionHandling()) { + // Control is nonlinear if we return or throw. Traps don't need to be + // taken into account since they don't break control flow in a way + // that's observable. + if (mayThrow || isReturn) { self->pushTask(SubType::doNoteNonLinear, currp); } } @@ -153,12 +154,43 @@ struct LinearExecutionWalker : public PostWalker { break; } case Expression::Id::CallId: { - handleCall(curr->cast()->isReturn); - return; + auto* call = curr->cast(); + + bool mayThrow = !self->getModule() || + self->getModule()->features.hasExceptionHandling(); + if (mayThrow && self->getModule()) { + auto* effects = + self->getModule()->getFunction(call->target)->effects.get(); + + if (effects && !effects->throws_) { + mayThrow = false; + } + } + + handleCall(mayThrow, call->isReturn); + break; } case Expression::Id::CallRefId: { - handleCall(curr->cast()->isReturn); - return; + auto* callRef = curr->cast(); + + // TODO: Effect analysis for indirect calls isn't implemented yet. + // Assume any indirect call may throw for now. + bool mayThrow = !self->getModule() || + self->getModule()->features.hasExceptionHandling(); + + handleCall(mayThrow, callRef->isReturn); + break; + } + case Expression::Id::CallIndirectId: { + auto* callIndirect = curr->cast(); + + // TODO: Effect analysis for indirect calls isn't implemented yet. + // Assume any indirect call may throw for now. + bool mayThrow = !self->getModule() || + self->getModule()->features.hasExceptionHandling(); + + handleCall(mayThrow, callIndirect->isReturn); + break; } case Expression::Id::TryId: { self->pushTask(SubType::doVisitTry, currp); diff --git a/test/lit/passes/simplify-locals-global-effects-eh.wast b/test/lit/passes/simplify-locals-global-effects-eh.wast new file mode 100644 index 00000000000..ff11e7487b6 --- /dev/null +++ b/test/lit/passes/simplify-locals-global-effects-eh.wast @@ -0,0 +1,163 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt --closed-world --enable-exception-handling --enable-gc --enable-reference-types --generate-global-effects --simplify-locals -S -o - | filecheck %s + +(module + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (tag $t (type $0)) + (tag $t) + + ;; CHECK: (func $nop (type $0) + ;; CHECK-NEXT: ) + (func $nop + ) + + ;; CHECK: (func $throws (type $0) + ;; CHECK-NEXT: (throw $t) + ;; CHECK-NEXT: ) + (func $throws + (throw $t) + ) + + ;; CHECK: (func $read-g (type $1) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (call $nop) + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + (func $read-g (result i32) + (local $x i32) + (local.set $x (global.get $g)) + + ;; With --global-effects, we can tell that this doesn't throw, so it + ;; doesn't act as a barrier to optimize. The local is optimized away. + (call $nop) + (local.get $x) + ) + + ;; CHECK: (func $read-g-with-throw-in-between (type $1) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $throws) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (func $read-g-with-throw-in-between (result i32) + (local $x i32) + (local.set $x (global.get $g)) + + ;; A potential throw halts our optimizations. + (call $throws) + + (local.get $x) + ) +) + +(module + ;; CHECK: (type $const-type (func (result f32))) + (type $const-type (func (result f32))) + + ;; CHECK: (type $throw-type (func (result f64))) + (type $throw-type (func (result f64))) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (table $t 2 2 funcref) + (table $t 2 2 funcref) + + ;; CHECK: (tag $t (type $2)) + (tag $t) + + ;; CHECK: (func $const (type $const-type) (result f32) + ;; CHECK-NEXT: (f32.const 1) + ;; CHECK-NEXT: ) + (func $const (type $const-type) + (f32.const 1) + ) + (elem declare $const) + + + ;; CHECK: (func $throws (type $throw-type) (result f64) + ;; CHECK-NEXT: (throw $t) + ;; CHECK-NEXT: (f64.const 1) + ;; CHECK-NEXT: ) + (func $throws (type $throw-type) + (throw $t) + (f64.const 1) + ) + (elem declare $throws) + + ;; CHECK: (func $read-g (type $3) (param $ref (ref null $const-type)) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call_ref $const-type + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (func $read-g (param $ref (ref null $const-type)) (result i32) + (local $x i32) + (local.set $x (global.get $g)) + + ;; With more precise effect analysis for indirect calls, we can determine + ;; that the only possible target for this ref is $const in a closed world, + ;; which wouldn't block our optimizations. + ;; TODO: Add effects analysis for indirect calls. + (drop (call_ref $const-type (local.get $ref))) + + (local.get $x) + ) + + ;; CHECK: (func $read-g-with-throw-in-between (type $4) (param $ref (ref $throw-type)) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call_ref $throw-type + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (func $read-g-with-throw-in-between (param $ref (ref $throw-type)) (result i32) + (local $x i32) + (local.set $x (global.get $g)) + + ;; Similar to above, except here we can tell that the indirect call may + ;; throw so optimization is halted. + (drop (call_ref $throw-type (local.get $ref))) + + (local.get $x) + ) + + ;; CHECK: (func $read-g-with-call-indirect-in-between (type $5) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call_indirect $t (type $const-type) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (func $read-g-with-call-indirect-in-between (result i32) + (local $x i32) + (local.set $x (global.get $g)) + + ;; Similar to above with call_indirect instead of call_ref. + ;; TODO: Add effects analysis for indirect calls. + (drop (call_indirect (type $const-type) (i32.const 0))) + + (local.get $x) + ) +) From cd26fc14f11e8cde849e66712a4c44ce04e539b5 Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Wed, 22 Apr 2026 21:57:40 -0700 Subject: [PATCH 050/168] Fix binary writing for test_emit_all_features (#8642) In #8639, the CI failed on Windows 11 because the total number of features became 26 (0x1A), which is the EOF byte in Windows. Write to a temp file to ensure that the file is read as binary. Resolves the CI in the next PR. Part of #8544. --- test/unit/test_features.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/unit/test_features.py b/test/unit/test_features.py index 35b65199a11..2bd3e74f6fc 100644 --- a/test/unit/test_features.py +++ b/test/unit/test_features.py @@ -422,14 +422,15 @@ def test_explicit_detect_features(self): opts=['-mvp', '--detect-features', '--enable-simd']) def test_emit_all_features(self): + temp_path = os.path.join(shared.options.out_dir, 'test_emit_all_features.wasm') p = shared.run_process(shared.WASM_OPT + - ['--emit-target-features', '-all', '-o', '-'], + ['--emit-target-features', '-all', '-o', temp_path], input="(module)", check=False, capture_output=True, decode_output=False) self.assertEqual(p.returncode, 0) p2 = shared.run_process(shared.WASM_OPT + - ['--print-features', '-o', os.devnull], - input=p.stdout, check=False, + ['--print-features', '-o', os.devnull, temp_path], + check=False, capture_output=True) self.assertEqual(p2.returncode, 0) self.assertEqual([ From fb9043e501705257b6e14d3e54fc70c38d52aa1c Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Thu, 23 Apr 2026 09:09:48 -0700 Subject: [PATCH 051/168] Add feature flag for wide arithmetic (#8639) Part of #8544. Based on #8192. --- src/binaryen-c.cpp | 3 +++ src/binaryen-c.h | 1 + src/js/binaryen.js-post.js | 1 + src/tools/tool-options.h | 1 + src/wasm-binary.h | 1 + src/wasm-features.h | 7 ++++++- src/wasm/wasm-binary.cpp | 4 ++++ src/wasm/wasm.cpp | 1 + test/binaryen.js/kitchen-sink.js | 1 + test/binaryen.js/kitchen-sink.js.txt | 3 ++- test/example/c-api-kitchen-sink.c | 2 ++ test/example/c-api-kitchen-sink.txt | 3 ++- test/lit/help/wasm-as.test | 4 ++++ test/lit/help/wasm-ctor-eval.test | 4 ++++ test/lit/help/wasm-dis.test | 4 ++++ test/lit/help/wasm-emscripten-finalize.test | 4 ++++ test/lit/help/wasm-merge.test | 4 ++++ test/lit/help/wasm-metadce.test | 4 ++++ test/lit/help/wasm-opt.test | 4 ++++ test/lit/help/wasm-reduce.test | 4 ++++ test/lit/help/wasm-split.test | 4 ++++ test/lit/help/wasm2js.test | 4 ++++ ...rget-features_roundtrip_print-features_all-features.txt | 1 + test/unit/test_features.py | 1 + 24 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index 133d17e92b4..53926af7680 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -508,6 +508,9 @@ BinaryenFeatures BinaryenFeatureMultibyte(void) { BinaryenFeatures BinaryenFeatureCustomPageSizes(void) { return static_cast(FeatureSet::CustomPageSizes); } +BinaryenFeatures BinaryenFeatureWideArithmetic(void) { + return static_cast(FeatureSet::WideArithmetic); +} BinaryenFeatures BinaryenFeatureAll(void) { return static_cast(FeatureSet::All); } diff --git a/src/binaryen-c.h b/src/binaryen-c.h index 24e0fe3071a..10e01fef3aa 100644 --- a/src/binaryen-c.h +++ b/src/binaryen-c.h @@ -246,6 +246,7 @@ BINARYEN_API BinaryenFeatures BinaryenFeatureCallIndirectOverlong(void); BINARYEN_API BinaryenFeatures BinaryenFeatureRelaxedAtomics(void); BINARYEN_API BinaryenFeatures BinaryenFeatureMultibyte(void); BINARYEN_API BinaryenFeatures BinaryenFeatureCustomPageSizes(void); +BINARYEN_API BinaryenFeatures BinaryenFeatureWideArithmetic(void); BINARYEN_API BinaryenFeatures BinaryenFeatureAll(void); // Modules diff --git a/src/js/binaryen.js-post.js b/src/js/binaryen.js-post.js index 0044b5e7c36..8c965d70ee0 100644 --- a/src/js/binaryen.js-post.js +++ b/src/js/binaryen.js-post.js @@ -194,6 +194,7 @@ function initializeConstants() { 'CallIndirectOverlong', 'RelaxedAtomics', 'CustomPageSizes', + 'WideArithmetic', 'All' ].forEach(name => { Module['Features'][name] = Module['_BinaryenFeature' + name](); diff --git a/src/tools/tool-options.h b/src/tools/tool-options.h index cc39daad4fc..87362d455d2 100644 --- a/src/tools/tool-options.h +++ b/src/tools/tool-options.h @@ -112,6 +112,7 @@ struct ToolOptions : public Options { .addFeature(FeatureSet::RelaxedAtomics, "acquire/release atomic memory operations") .addFeature(FeatureSet::CustomPageSizes, "custom page sizes") + .addFeature(FeatureSet::WideArithmetic, "wide arithmetic") .add("--enable-typed-function-references", "", "Deprecated compatibility flag", diff --git a/src/wasm-binary.h b/src/wasm-binary.h index 386f495a905..abacbda0d13 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -474,6 +474,7 @@ extern const char* CustomDescriptorsFeature; extern const char* RelaxedAtomicsFeature; extern const char* MultibyteFeature; extern const char* CustomPageSizesFeature; +extern const char* WideArithmeticFeature; enum Subsection { NameModule = 0, diff --git a/src/wasm-features.h b/src/wasm-features.h index 056e6a4ab55..833281c0c11 100644 --- a/src/wasm-features.h +++ b/src/wasm-features.h @@ -58,11 +58,12 @@ struct FeatureSet { RelaxedAtomics = 1 << 22, CustomPageSizes = 1 << 23, Multibyte = 1 << 24, + WideArithmetic = 1 << 25, MVP = None, // Keep in sync with llvm default features: // https://github.com/llvm/llvm-project/blob/c7576cb89d6c95f03968076e902d3adfd1996577/clang/lib/Basic/Targets/WebAssembly.cpp#L150-L153 Default = SignExt | MutableGlobals, - All = (1 << 25) - 1, + All = (1 << 26) - 1, }; static std::string toString(Feature f) { @@ -117,6 +118,8 @@ struct FeatureSet { return "custom-page-sizes"; case Multibyte: return "multibyte"; + case WideArithmetic: + return "wide-arithmetic"; case MVP: case Default: case All: @@ -180,6 +183,7 @@ struct FeatureSet { bool hasRelaxedAtomics() const { return (features & RelaxedAtomics) != 0; } bool hasCustomPageSizes() const { return (features & CustomPageSizes) != 0; } bool hasMultibyte() const { return (features & Multibyte) != 0; } + bool hasWideArithmetic() const { return (features & WideArithmetic) != 0; } bool hasAll() const { return (features & All) != 0; } void set(FeatureSet f, bool v = true) { @@ -208,6 +212,7 @@ struct FeatureSet { void setCustomDescriptors(bool v = true) { set(CustomDescriptors, v); } void setRelaxedAtomics(bool v = true) { set(RelaxedAtomics, v); } void setMultibyte(bool v = true) { set(Multibyte, v); } + void setWideArithmetic(bool v = true) { set(WideArithmetic, v); } void setMVP() { features = MVP; } void setAll() { features = All; } diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index da49533f55d..50dbc333bf2 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -1486,6 +1486,8 @@ void WasmBinaryWriter::writeFeaturesSection() { return BinaryConsts::CustomSections::RelaxedAtomicsFeature; case FeatureSet::CustomPageSizes: return BinaryConsts::CustomSections::CustomPageSizesFeature; + case FeatureSet::WideArithmetic: + return BinaryConsts::CustomSections::WideArithmeticFeature; case FeatureSet::None: case FeatureSet::Default: case FeatureSet::All: @@ -5446,6 +5448,8 @@ void WasmBinaryReader::readFeatures(size_t sectionPos, size_t payloadLen) { feature = FeatureSet::RelaxedAtomics; } else if (name == BinaryConsts::CustomSections::CustomPageSizesFeature) { feature = FeatureSet::CustomPageSizes; + } else if (name == BinaryConsts::CustomSections::WideArithmeticFeature) { + feature = FeatureSet::WideArithmetic; } else { // Silently ignore unknown features (this may be and old binaryen running // on a new wasm). diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index 31de2de0362..589f9b33afc 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -79,6 +79,7 @@ const char* CustomDescriptorsFeature = "custom-descriptors"; const char* RelaxedAtomicsFeature = "relaxed-atomics"; const char* MultibyteFeature = "multibyte"; const char* CustomPageSizesFeature = "custom-page-sizes"; +const char* WideArithmeticFeature = "wide-arithmetic"; } // namespace BinaryConsts::CustomSections diff --git a/test/binaryen.js/kitchen-sink.js b/test/binaryen.js/kitchen-sink.js index 89753393591..354668d0c12 100644 --- a/test/binaryen.js/kitchen-sink.js +++ b/test/binaryen.js/kitchen-sink.js @@ -102,6 +102,7 @@ function test_features() { console.log("Features.MultiMemory: " + binaryen.Features.MultiMemory); console.log("Features.RelaxedAtomics: " + binaryen.Features.RelaxedAtomics); console.log("Features.CustomPageSizes: " + binaryen.Features.CustomPageSizes); + console.log("Features.WideArithmetic: " + binaryen.Features.WideArithmetic); console.log("Features.All: " + binaryen.Features.All); } diff --git a/test/binaryen.js/kitchen-sink.js.txt b/test/binaryen.js/kitchen-sink.js.txt index 9ef0ade0bcc..b6f57fa8236 100644 --- a/test/binaryen.js/kitchen-sink.js.txt +++ b/test/binaryen.js/kitchen-sink.js.txt @@ -35,7 +35,8 @@ Features.Strings: 16384 Features.MultiMemory: 32768 Features.RelaxedAtomics: 4194304 Features.CustomPageSizes: 8388608 -Features.All: 33554431 +Features.WideArithmetic: 33554432 +Features.All: 67108863 InvalidId: 0 BlockId: 1 IfId: 2 diff --git a/test/example/c-api-kitchen-sink.c b/test/example/c-api-kitchen-sink.c index 225e5feef58..e046b942303 100644 --- a/test/example/c-api-kitchen-sink.c +++ b/test/example/c-api-kitchen-sink.c @@ -379,6 +379,8 @@ void test_features() { printf("BinaryenFeatureCustomPageSizes: %d\n", BinaryenFeatureCustomPageSizes()); printf("BinaryenFeatureMultibyte: %d\n", BinaryenFeatureMultibyte()); + printf("BinaryenFeatureWideArithmetic: %d\n", + BinaryenFeatureWideArithmetic()); printf("BinaryenFeatureAll: %d\n", BinaryenFeatureAll()); } diff --git a/test/example/c-api-kitchen-sink.txt b/test/example/c-api-kitchen-sink.txt index 5ab868a05f9..cf66a4cba9b 100644 --- a/test/example/c-api-kitchen-sink.txt +++ b/test/example/c-api-kitchen-sink.txt @@ -50,7 +50,8 @@ BinaryenFeatureStrings: 16384 BinaryenFeatureRelaxedAtomics: 4194304 BinaryenFeatureCustomPageSizes: 8388608 BinaryenFeatureMultibyte: 16777216 -BinaryenFeatureAll: 33554431 +BinaryenFeatureWideArithmetic: 33554432 +BinaryenFeatureAll: 67108863 (f32.neg (f32.const -33.61199951171875) ) diff --git a/test/lit/help/wasm-as.test b/test/lit/help/wasm-as.test index 463a8c4270c..35ea0de7868 100644 --- a/test/lit/help/wasm-as.test +++ b/test/lit/help/wasm-as.test @@ -148,6 +148,10 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-custom-page-sizes Disable custom page sizes ;; CHECK-NEXT: +;; CHECK-NEXT: --enable-wide-arithmetic Enable wide arithmetic +;; CHECK-NEXT: +;; CHECK-NEXT: --disable-wide-arithmetic Disable wide arithmetic +;; CHECK-NEXT: ;; CHECK-NEXT: --enable-typed-function-references Deprecated compatibility flag ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-typed-function-references Deprecated compatibility flag diff --git a/test/lit/help/wasm-ctor-eval.test b/test/lit/help/wasm-ctor-eval.test index 5f02cb6d8e9..e56f2b215bc 100644 --- a/test/lit/help/wasm-ctor-eval.test +++ b/test/lit/help/wasm-ctor-eval.test @@ -155,6 +155,10 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-custom-page-sizes Disable custom page sizes ;; CHECK-NEXT: +;; CHECK-NEXT: --enable-wide-arithmetic Enable wide arithmetic +;; CHECK-NEXT: +;; CHECK-NEXT: --disable-wide-arithmetic Disable wide arithmetic +;; CHECK-NEXT: ;; CHECK-NEXT: --enable-typed-function-references Deprecated compatibility flag ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-typed-function-references Deprecated compatibility flag diff --git a/test/lit/help/wasm-dis.test b/test/lit/help/wasm-dis.test index 73a8cd2fe47..3c0fca5a0f4 100644 --- a/test/lit/help/wasm-dis.test +++ b/test/lit/help/wasm-dis.test @@ -141,6 +141,10 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-custom-page-sizes Disable custom page sizes ;; CHECK-NEXT: +;; CHECK-NEXT: --enable-wide-arithmetic Enable wide arithmetic +;; CHECK-NEXT: +;; CHECK-NEXT: --disable-wide-arithmetic Disable wide arithmetic +;; CHECK-NEXT: ;; CHECK-NEXT: --enable-typed-function-references Deprecated compatibility flag ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-typed-function-references Deprecated compatibility flag diff --git a/test/lit/help/wasm-emscripten-finalize.test b/test/lit/help/wasm-emscripten-finalize.test index 21d4c5ef261..6ed8c98771f 100644 --- a/test/lit/help/wasm-emscripten-finalize.test +++ b/test/lit/help/wasm-emscripten-finalize.test @@ -183,6 +183,10 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-custom-page-sizes Disable custom page sizes ;; CHECK-NEXT: +;; CHECK-NEXT: --enable-wide-arithmetic Enable wide arithmetic +;; CHECK-NEXT: +;; CHECK-NEXT: --disable-wide-arithmetic Disable wide arithmetic +;; CHECK-NEXT: ;; CHECK-NEXT: --enable-typed-function-references Deprecated compatibility flag ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-typed-function-references Deprecated compatibility flag diff --git a/test/lit/help/wasm-merge.test b/test/lit/help/wasm-merge.test index 8c94b13745c..f1ff0631278 100644 --- a/test/lit/help/wasm-merge.test +++ b/test/lit/help/wasm-merge.test @@ -178,6 +178,10 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-custom-page-sizes Disable custom page sizes ;; CHECK-NEXT: +;; CHECK-NEXT: --enable-wide-arithmetic Enable wide arithmetic +;; CHECK-NEXT: +;; CHECK-NEXT: --disable-wide-arithmetic Disable wide arithmetic +;; CHECK-NEXT: ;; CHECK-NEXT: --enable-typed-function-references Deprecated compatibility flag ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-typed-function-references Deprecated compatibility flag diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index ea5806e0199..1b0cc7d4569 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -814,6 +814,10 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-custom-page-sizes Disable custom page sizes ;; CHECK-NEXT: +;; CHECK-NEXT: --enable-wide-arithmetic Enable wide arithmetic +;; CHECK-NEXT: +;; CHECK-NEXT: --disable-wide-arithmetic Disable wide arithmetic +;; CHECK-NEXT: ;; CHECK-NEXT: --enable-typed-function-references Deprecated compatibility flag ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-typed-function-references Deprecated compatibility flag diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 1c0a53801e4..d616e1cf085 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -846,6 +846,10 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-custom-page-sizes Disable custom page sizes ;; CHECK-NEXT: +;; CHECK-NEXT: --enable-wide-arithmetic Enable wide arithmetic +;; CHECK-NEXT: +;; CHECK-NEXT: --disable-wide-arithmetic Disable wide arithmetic +;; CHECK-NEXT: ;; CHECK-NEXT: --enable-typed-function-references Deprecated compatibility flag ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-typed-function-references Deprecated compatibility flag diff --git a/test/lit/help/wasm-reduce.test b/test/lit/help/wasm-reduce.test index f403191fed6..48da8b968f3 100644 --- a/test/lit/help/wasm-reduce.test +++ b/test/lit/help/wasm-reduce.test @@ -231,6 +231,10 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-custom-page-sizes Disable custom page sizes ;; CHECK-NEXT: +;; CHECK-NEXT: --enable-wide-arithmetic Enable wide arithmetic +;; CHECK-NEXT: +;; CHECK-NEXT: --disable-wide-arithmetic Disable wide arithmetic +;; CHECK-NEXT: ;; CHECK-NEXT: --enable-typed-function-references Deprecated compatibility flag ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-typed-function-references Deprecated compatibility flag diff --git a/test/lit/help/wasm-split.test b/test/lit/help/wasm-split.test index 39740806aad..950d3e5cea9 100644 --- a/test/lit/help/wasm-split.test +++ b/test/lit/help/wasm-split.test @@ -287,6 +287,10 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-custom-page-sizes Disable custom page sizes ;; CHECK-NEXT: +;; CHECK-NEXT: --enable-wide-arithmetic Enable wide arithmetic +;; CHECK-NEXT: +;; CHECK-NEXT: --disable-wide-arithmetic Disable wide arithmetic +;; CHECK-NEXT: ;; CHECK-NEXT: --enable-typed-function-references Deprecated compatibility flag ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-typed-function-references Deprecated compatibility flag diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index 39bc23554a9..a91d5b5c050 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -778,6 +778,10 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-custom-page-sizes Disable custom page sizes ;; CHECK-NEXT: +;; CHECK-NEXT: --enable-wide-arithmetic Enable wide arithmetic +;; CHECK-NEXT: +;; CHECK-NEXT: --disable-wide-arithmetic Disable wide arithmetic +;; CHECK-NEXT: ;; CHECK-NEXT: --enable-typed-function-references Deprecated compatibility flag ;; CHECK-NEXT: ;; CHECK-NEXT: --disable-typed-function-references Deprecated compatibility flag diff --git a/test/passes/strip-target-features_roundtrip_print-features_all-features.txt b/test/passes/strip-target-features_roundtrip_print-features_all-features.txt index 8172cc77f9c..2272fb36ff1 100644 --- a/test/passes/strip-target-features_roundtrip_print-features_all-features.txt +++ b/test/passes/strip-target-features_roundtrip_print-features_all-features.txt @@ -23,6 +23,7 @@ --enable-relaxed-atomics --enable-custom-page-sizes --enable-multibyte +--enable-wide-arithmetic (module (type $0 (func (result v128 externref))) (func $foo (type $0) (result v128 externref) diff --git a/test/unit/test_features.py b/test/unit/test_features.py index 2bd3e74f6fc..2d693ded087 100644 --- a/test/unit/test_features.py +++ b/test/unit/test_features.py @@ -458,4 +458,5 @@ def test_emit_all_features(self): '--enable-custom-descriptors', '--enable-relaxed-atomics', '--enable-custom-page-sizes', + '--enable-wide-arithmetic', ], p2.stdout.splitlines()) From 2a594b2c4d64f9b4a6fe9499d43c464e4298b1f8 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 23 Apr 2026 11:09:33 -0700 Subject: [PATCH 052/168] Fuzzer: Only run one handler at a time (#8643) Rather than generate one wasm and run it on a few handlers, each wasm gets one handler run on it. This is simpler, in particular during reduction, where only the thing being reduced is run. It is a tradeoff in efficiency. Running the old way vs this for 5 minutes, we generate almost 2x more wasm files per second (good for variety) but run 34% fewer handlers (bad for coverage). This seems reasonable to me. --- scripts/fuzz_opt.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 41beca07e6c..69d51259e92 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2545,24 +2545,19 @@ def test_one(random_input, given_wasm): if len(filtered_handlers) == 0: # pick at least one, to not waste the effort we put into making the wasm filtered_handlers = [random.choice(relevant_handlers)] - # run only some of the pair handling handlers. if we ran them all all the - # time that would mean we have less variety in wasm files and passes run - # on them in the same amount of time. - NUM_PAIR_HANDLERS = 3 - used_handlers = set() - for _ in range(NUM_PAIR_HANDLERS): - testcase_handler = random.choice(filtered_handlers) - if testcase_handler in used_handlers: - continue - used_handlers.add(testcase_handler) - assert testcase_handler.can_run_on_wasm('a.wasm') - print('running testcase handler:', testcase_handler.__class__.__name__) - testcase_handler.increment_runs() - - # let the testcase handler handle this testcase however it wants. in this case we give it - # the input and both wasms. - testcase_handler.handle_pair(input=random_input, before_wasm=abspath('a.wasm'), after_wasm=abspath('b.wasm'), opts=opts + FEATURE_OPTS) - print('') + # run only one of the handlers. this is less efficient in terms of how many + # handlers we run, but more varied in the wasms we see. it also avoids the + # annoyance of running two testcase handlers on the same testcase during + # reduction (it is much simpler to reduce when only the failing thing is + # being run). + testcase_handler = random.choice(filtered_handlers) + assert testcase_handler.can_run_on_wasm('a.wasm') + print('running testcase handler:', testcase_handler.__class__.__name__) + testcase_handler.increment_runs() + + # let the testcase handler handle this testcase however it wants. in this case we give it + # the input and both wasms. + testcase_handler.handle_pair(input=random_input, before_wasm=abspath('a.wasm'), after_wasm=abspath('b.wasm'), opts=opts + FEATURE_OPTS) return bytes From f61c445ad0549ca83089e5629ef9eae6db1c95b5 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Thu, 23 Apr 2026 12:59:14 -0700 Subject: [PATCH 053/168] [FP16] Implement f16x8.demote_{f64x2, f32x4}_zero. (#8580) Specified at https://github.com/WebAssembly/half-precision/blob/main/proposals/half-precision/Overview.md --- scripts/gen-s-parser.py | 2 + src/gen-s-parser.inc | 32 ++++++++++++--- src/ir/child-typer.h | 2 + src/ir/cost.h | 2 + src/literal.h | 2 + src/passes/Print.cpp | 6 +++ src/wasm-binary.h | 2 + src/wasm-interpreter.h | 4 ++ src/wasm.h | 2 + src/wasm/literal.cpp | 24 ++++++++++++ src/wasm/wasm-binary.cpp | 4 ++ src/wasm/wasm-stack.cpp | 8 ++++ src/wasm/wasm-validator.cpp | 2 + src/wasm/wasm.cpp | 2 + test/lit/basic/f16.wast | 42 ++++++++++++++++++++ test/spec/f16.wast | 77 +++++++++++++++++++++++++++++++++++++ 16 files changed, 208 insertions(+), 5 deletions(-) diff --git a/scripts/gen-s-parser.py b/scripts/gen-s-parser.py index d0f08d0546b..aa848ad8a52 100755 --- a/scripts/gen-s-parser.py +++ b/scripts/gen-s-parser.py @@ -550,6 +550,8 @@ ("f16x8.convert_i16x8_s", "makeUnary(UnaryOp::ConvertSVecI16x8ToVecF16x8)"), ("f16x8.convert_i16x8_u", "makeUnary(UnaryOp::ConvertUVecI16x8ToVecF16x8)"), ("f32x4.promote_low_f16x8", "makeUnary(UnaryOp::PromoteLowVecF16x8ToVecF32x4)"), + ("f16x8.demote_f32x4_zero", "makeUnary(UnaryOp::DemoteZeroVecF32x4ToVecF16x8)"), + ("f16x8.demote_f64x2_zero", "makeUnary(UnaryOp::DemoteZeroVecF64x2ToVecF16x8)"), ("f16x8.madd", "makeSIMDTernary(SIMDTernaryOp::MaddVecF16x8)"), ("f16x8.nmadd", "makeSIMDTernary(SIMDTernaryOp::NmaddVecF16x8)"), diff --git a/src/gen-s-parser.inc b/src/gen-s-parser.inc index eca86c6ed77..345afa302aa 100644 --- a/src/gen-s-parser.inc +++ b/src/gen-s-parser.inc @@ -505,12 +505,34 @@ switch (buf[0]) { default: goto parse_error; } } - case 'd': - if (op == "f16x8.div"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::DivVecF16x8)); - return Ok{}; + case 'd': { + switch (buf[7]) { + case 'e': { + switch (buf[14]) { + case '3': + if (op == "f16x8.demote_f32x4_zero"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::DemoteZeroVecF32x4ToVecF16x8)); + return Ok{}; + } + goto parse_error; + case '6': + if (op == "f16x8.demote_f64x2_zero"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::DemoteZeroVecF64x2ToVecF16x8)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; + } + } + case 'i': + if (op == "f16x8.div"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::DivVecF16x8)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } case 'e': { switch (buf[7]) { case 'q': diff --git a/src/ir/child-typer.h b/src/ir/child-typer.h index 385e0fa8290..e223eb2ad59 100644 --- a/src/ir/child-typer.h +++ b/src/ir/child-typer.h @@ -448,6 +448,8 @@ template struct ChildTyper : OverriddenVisitor { case ConvertSVecI16x8ToVecF16x8: case ConvertUVecI16x8ToVecF16x8: case PromoteLowVecF16x8ToVecF32x4: + case DemoteZeroVecF32x4ToVecF16x8: + case DemoteZeroVecF64x2ToVecF16x8: case AnyTrueVec128: case AllTrueVecI8x16: case AllTrueVecI16x8: diff --git a/src/ir/cost.h b/src/ir/cost.h index 0042d27bcb2..7c02dafce7b 100644 --- a/src/ir/cost.h +++ b/src/ir/cost.h @@ -285,6 +285,8 @@ struct CostAnalyzer : public OverriddenVisitor { case ConvertSVecI16x8ToVecF16x8: case ConvertUVecI16x8ToVecF16x8: case PromoteLowVecF16x8ToVecF32x4: + case DemoteZeroVecF32x4ToVecF16x8: + case DemoteZeroVecF64x2ToVecF16x8: ret = 1; break; case InvalidUnary: diff --git a/src/literal.h b/src/literal.h index 4fcb2ee8a2e..686348d1942 100644 --- a/src/literal.h +++ b/src/literal.h @@ -724,6 +724,8 @@ class Literal { Literal demoteZeroToF32x4() const; Literal promoteLowToF64x2() const; Literal promoteLowF16x8ToF32x4() const; + Literal demoteZeroF32x4ToF16x8() const; + Literal demoteZeroF64x2ToF16x8() const; Literal truncSatToSI16x8() const; Literal truncSatToUI16x8() const; Literal convertSToF16x8() const; diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 68981953211..8239edc71be 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -1347,6 +1347,12 @@ struct PrintExpressionContents case PromoteLowVecF16x8ToVecF32x4: o << "f32x4.promote_low_f16x8"; break; + case DemoteZeroVecF32x4ToVecF16x8: + o << "f16x8.demote_f32x4_zero"; + break; + case DemoteZeroVecF64x2ToVecF16x8: + o << "f16x8.demote_f64x2_zero"; + break; case InvalidUnary: WASM_UNREACHABLE("unvalid unary operator"); } diff --git a/src/wasm-binary.h b/src/wasm-binary.h index abacbda0d13..82363964d0b 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -1127,6 +1127,8 @@ enum ASTNodes { I16x8TruncSatF16x8U = 0x146, F16x8ConvertI16x8S = 0x147, F16x8ConvertI16x8U = 0x148, + F16x8DemoteF32x4Zero = 0x149, + F16x8DemoteF64x2Zero = 0x14a, F32x4PromoteLowF16x8 = 0x14b, // bulk memory opcodes diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index e47559b597d..a57f05ea66f 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -1166,6 +1166,10 @@ class ExpressionRunner : public OverriddenVisitor { return value.convertUToF16x8(); case PromoteLowVecF16x8ToVecF32x4: return value.promoteLowF16x8ToF32x4(); + case DemoteZeroVecF32x4ToVecF16x8: + return value.demoteZeroF32x4ToF16x8(); + case DemoteZeroVecF64x2ToVecF16x8: + return value.demoteZeroF64x2ToF16x8(); case InvalidUnary: WASM_UNREACHABLE("invalid unary op"); } diff --git a/src/wasm.h b/src/wasm.h index a6a32bbeb98..e59f99633f5 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -252,6 +252,8 @@ enum UnaryOp { ConvertSVecI16x8ToVecF16x8, ConvertUVecI16x8ToVecF16x8, PromoteLowVecF16x8ToVecF32x4, + DemoteZeroVecF32x4ToVecF16x8, + DemoteZeroVecF64x2ToVecF16x8, InvalidUnary }; diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index b3156fab0b3..551dd7ed738 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -2912,6 +2912,30 @@ Literal Literal::truncSatZeroUToI32x4() const { Literal Literal::demoteZeroToF32x4() const { return unary_zero<4, &Literal::getLanesF64x2, &Literal::demote>(*this); } +Literal Literal::demoteZeroF32x4ToF16x8() const { + auto lanes = getLanesF32x4(); + LaneArray<8> result; + for (size_t i = 0; i < 4; ++i) { + result[i] = Literal(fp16_ieee_from_fp32_value(lanes[i].getf32())); + } + for (size_t i = 4; i < 8; ++i) { + result[i] = Literal(int32_t{0}); + } + return Literal(result); +} + +Literal Literal::demoteZeroF64x2ToF16x8() const { + auto lanes = getLanesF64x2(); + LaneArray<8> result; + for (size_t i = 0; i < 2; ++i) { + result[i] = Literal(fp16_ieee_from_fp32_value(lanes[i].demote().getf32())); + } + for (size_t i = 2; i < 8; ++i) { + result[i] = Literal(int32_t{0}); + } + return Literal(result); +} + Literal Literal::promoteLowToF64x2() const { return extendF32(*this); } diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 50dbc333bf2..d5665fa48ec 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -4476,6 +4476,10 @@ Result<> WasmBinaryReader::readInst() { return builder.makeUnary(ConvertSVecI16x8ToVecF16x8); case BinaryConsts::F16x8ConvertI16x8U: return builder.makeUnary(ConvertUVecI16x8ToVecF16x8); + case BinaryConsts::F16x8DemoteF32x4Zero: + return builder.makeUnary(DemoteZeroVecF32x4ToVecF16x8); + case BinaryConsts::F16x8DemoteF64x2Zero: + return builder.makeUnary(DemoteZeroVecF64x2ToVecF16x8); case BinaryConsts::F32x4PromoteLowF16x8: return builder.makeUnary(PromoteLowVecF16x8ToVecF32x4); case BinaryConsts::I8x16ExtractLaneS: diff --git a/src/wasm/wasm-stack.cpp b/src/wasm/wasm-stack.cpp index 82708ca7f51..cc865b0189d 100644 --- a/src/wasm/wasm-stack.cpp +++ b/src/wasm/wasm-stack.cpp @@ -1457,6 +1457,14 @@ void BinaryInstWriter::visitUnary(Unary* curr) { o << static_cast(BinaryConsts::SIMDPrefix) << U32LEB(BinaryConsts::F16x8ConvertI16x8U); break; + case DemoteZeroVecF32x4ToVecF16x8: + o << static_cast(BinaryConsts::SIMDPrefix) + << U32LEB(BinaryConsts::F16x8DemoteF32x4Zero); + break; + case DemoteZeroVecF64x2ToVecF16x8: + o << static_cast(BinaryConsts::SIMDPrefix) + << U32LEB(BinaryConsts::F16x8DemoteF64x2Zero); + break; case PromoteLowVecF16x8ToVecF32x4: o << static_cast(BinaryConsts::SIMDPrefix) << U32LEB(BinaryConsts::F32x4PromoteLowF16x8); diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index f8c394072dd..3e809d0157d 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -2381,6 +2381,8 @@ void FunctionValidator::visitUnary(Unary* curr) { case DemoteZeroVecF64x2ToVecF32x4: case PromoteLowVecF32x4ToVecF64x2: case PromoteLowVecF16x8ToVecF32x4: + case DemoteZeroVecF32x4ToVecF16x8: + case DemoteZeroVecF64x2ToVecF16x8: case RelaxedTruncSVecF32x4ToVecI32x4: case RelaxedTruncUVecF32x4ToVecI32x4: case RelaxedTruncZeroSVecF64x2ToVecI32x4: diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index 589f9b33afc..9aae6cba291 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -731,6 +731,8 @@ void Unary::finalize() { case ConvertSVecI16x8ToVecF16x8: case ConvertUVecI16x8ToVecF16x8: case PromoteLowVecF16x8ToVecF32x4: + case DemoteZeroVecF32x4ToVecF16x8: + case DemoteZeroVecF64x2ToVecF16x8: type = Type::v128; break; case AnyTrueVec128: diff --git a/test/lit/basic/f16.wast b/test/lit/basic/f16.wast index d5e204d87f7..43ab593ab09 100644 --- a/test/lit/basic/f16.wast +++ b/test/lit/basic/f16.wast @@ -613,6 +613,36 @@ (local.get $0) ) ) + ;; CHECK-TEXT: (func $f16x8.demote_f32x4_zero (type $1) (param $0 v128) (result v128) + ;; CHECK-TEXT-NEXT: (f16x8.demote_f32x4_zero + ;; CHECK-TEXT-NEXT: (local.get $0) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-BIN: (func $f16x8.demote_f32x4_zero (type $1) (param $0 v128) (result v128) + ;; CHECK-BIN-NEXT: (f16x8.demote_f32x4_zero + ;; CHECK-BIN-NEXT: (local.get $0) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + (func $f16x8.demote_f32x4_zero (param $0 v128) (result v128) + (f16x8.demote_f32x4_zero + (local.get $0) + ) + ) + ;; CHECK-TEXT: (func $f16x8.demote_f64x2_zero (type $1) (param $0 v128) (result v128) + ;; CHECK-TEXT-NEXT: (f16x8.demote_f64x2_zero + ;; CHECK-TEXT-NEXT: (local.get $0) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-TEXT-NEXT: ) + ;; CHECK-BIN: (func $f16x8.demote_f64x2_zero (type $1) (param $0 v128) (result v128) + ;; CHECK-BIN-NEXT: (f16x8.demote_f64x2_zero + ;; CHECK-BIN-NEXT: (local.get $0) + ;; CHECK-BIN-NEXT: ) + ;; CHECK-BIN-NEXT: ) + (func $f16x8.demote_f64x2_zero (param $0 v128) (result v128) + (f16x8.demote_f64x2_zero + (local.get $0) + ) + ) ) ;; CHECK-BIN-NODEBUG: (type $0 (func (param v128 v128) (result v128))) @@ -849,3 +879,15 @@ ;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) ;; CHECK-BIN-NODEBUG-NEXT: ) ;; CHECK-BIN-NODEBUG-NEXT: ) + +;; CHECK-BIN-NODEBUG: (func $33 (type $1) (param $0 v128) (result v128) +;; CHECK-BIN-NODEBUG-NEXT: (f16x8.demote_f32x4_zero +;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) + +;; CHECK-BIN-NODEBUG: (func $34 (type $1) (param $0 v128) (result v128) +;; CHECK-BIN-NODEBUG-NEXT: (f16x8.demote_f64x2_zero +;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) +;; CHECK-BIN-NODEBUG-NEXT: ) +;; CHECK-BIN-NODEBUG-NEXT: ) diff --git a/test/spec/f16.wast b/test/spec/f16.wast index a36d5032d4f..9027b598e72 100644 --- a/test/spec/f16.wast +++ b/test/spec/f16.wast @@ -39,6 +39,8 @@ (func (export "f16x8.convert_i16x8_s") (param $0 v128) (result v128) (f16x8.convert_i16x8_s (local.get $0))) (func (export "f16x8.convert_i16x8_u") (param $0 v128) (result v128) (f16x8.convert_i16x8_u (local.get $0))) (func (export "f32x4.promote_low_f16x8") (param $0 v128) (result v128) (f32x4.promote_low_f16x8 (local.get $0))) + (func (export "f16x8.demote_f32x4_zero") (param $0 v128) (result v128) (f16x8.demote_f32x4_zero (local.get $0))) + (func (export "f16x8.demote_f64x2_zero") (param $0 v128) (result v128) (f16x8.demote_f64x2_zero (local.get $0))) ;; Multiple operation tests: (func (export "splat_replace") (result v128) (f16x8.replace_lane 0 (f16x8.splat (f32.const 1)) (f32.const 99)) ) @@ -268,3 +270,78 @@ (v128.const i16x8 0x0001 0 0 0 0 0 0 0)) ;; 2^-24 (v128.const i32x4 0x33800000 0 0 0)) + +(assert_return (invoke "f16x8.demote_f32x4_zero" + ;; 1.0 2.0 3.0 4.0 + (v128.const i32x4 0x3f800000 0x40000000 0x40400000 0x40800000)) + ;; 1.0 2.0 3.0 4.0 0 0 0 0 + (v128.const i16x8 0x3c00 0x4000 0x4200 0x4400 0 0 0 0)) + +(assert_return (invoke "f16x8.demote_f64x2_zero" + ;; 1.0 2.0 + (v128.const i64x2 0x3ff0000000000000 0x4000000000000000)) + ;; 1.0 2.0 0 0 0 0 0 0 + (v128.const i16x8 0x3c00 0x4000 0 0 0 0 0 0)) + +;; Edge cases: Infinities, NaNs, Zeros +(assert_return (invoke "f16x8.demote_f32x4_zero" + ;; inf -inf nan -0.0 + (v128.const i32x4 0x7f800000 0xff800000 0x7fc00000 0x80000000)) + ;; inf -inf nan -0.0 0 0 0 0 + (v128.const i16x8 0x7c00 0xfc00 0x7e00 0x8000 0 0 0 0)) + +;; Edge cases: Overflow +(assert_return (invoke "f16x8.demote_f32x4_zero" + ;; 1e5 -1e5 65504 -65504 + (v128.const i32x4 0x47c35000 0xc7c35000 0x477fe000 0xc77fe000)) + ;; inf -inf 65504 -65504 0 0 0 0 + (v128.const i16x8 0x7c00 0xfc00 0x7bff 0xfbff 0 0 0 0)) + +;; Edge cases: Infinities, NaNs, Zeros +(assert_return (invoke "f16x8.demote_f64x2_zero" + ;; inf -inf + (v128.const i64x2 0x7ff0000000000000 0xfff0000000000000)) + ;; inf -inf 0 0 0 0 0 0 + (v128.const i16x8 0x7c00 0xfc00 0 0 0 0 0 0)) + +(assert_return (invoke "f16x8.demote_f64x2_zero" + ;; nan -0.0 + (v128.const i64x2 0x7ff8000000000000 0x8000000000000000)) + ;; nan -0.0 0 0 0 0 0 0 + (v128.const i16x8 0x7e00 0x8000 0 0 0 0 0 0)) + +;; Edge cases: Overflow +(assert_return (invoke "f16x8.demote_f64x2_zero" + ;; 1e5 -1e5 + (v128.const i64x2 0x40f86a0000000000 0xc0f86a0000000000)) + ;; inf -inf 0 0 0 0 0 0 + (v128.const i16x8 0x7c00 0xfc00 0 0 0 0 0 0)) + +(assert_return (invoke "f16x8.demote_f64x2_zero" + ;; 65504 -65504 + (v128.const i64x2 0x40effc0000000000 0xc0effc0000000000)) + ;; 65504 -65504 0 0 0 0 0 0 + (v128.const i16x8 0x7bff 0xfbff 0 0 0 0 0 0)) + +;; Precision loss cases +(assert_return (invoke "f16x8.demote_f32x4_zero" + ;; 1.000244140625 1.000244140625 1.000244140625 1.000244140625 + (v128.const i32x4 0x3f800800 0x3f800800 0x3f800800 0x3f800800)) + ;; 1.0 1.0 1.0 1.0 0 0 0 0 + (v128.const i16x8 0x3c00 0x3c00 0x3c00 0x3c00 0 0 0 0)) + +(assert_return (invoke "f16x8.demote_f64x2_zero" + ;; 1.000244140625 1.000244140625 + (v128.const i64x2 0x3ff0010000000000 0x3ff0010000000000)) + ;; 1.0 1.0 0 0 0 0 0 0 + (v128.const i16x8 0x3c00 0x3c00 0 0 0 0 0 0)) + +;; TODO: Test Non-canonical NaN cases when an f16 const is supported in wat. +;; (assert_return (invoke "f16x8.demote_f32x4_zero" +;; ;; non-canonical NaN +;; (v128.const i32x4 0x7f800001 0 0 0)) +;; (v128.const f16x8 nan:arithmetic 0 0 0 0 0 0 0)) +;; (assert_return (invoke "f16x8.demote_f64x2_zero" +;; ;; non-canonical NaN +;; (v128.const i64x2 0x7ff0000000000001 0)) +;; (v128.const f16x8 nan:arithmetic 0 0 0 0 0 0 0)) From 35ba23c705ca4422d8f6102a0fe440c64d2bf1de Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Fri, 24 Apr 2026 14:36:46 -0700 Subject: [PATCH 054/168] Compute effects for indirect calls in GlobalEffects (#8609) When running in --closed-world, compute effects for indirect calls by unioning the effects of all potential functions of that type. In --closed-world, we assume that all references originate in our module, so the only possible functions that we don't know about are imports. Previously [we gave up on effects analysis](https://github.com/WebAssembly/binaryen/blob/29b2d42e8a748fbe1095696d58a52b7bf83e2253/src/passes/GlobalEffects.cpp#L83-L87) for indirect calls. Yields a very small byte count reduction in calcworker (3799354 - 3799297 = 57 bytes). Also shows no significant difference in Binaryen runtime: (0.1346069 -> 0.13375045 = <1% improvement, probably within noise). We expect more benefits after we're able to share indirect call effects with other passes, since currently they're only seen one layer up for callers of functions that indirectly call functions (see the newly-added tests for examples). Followups: * Share effect information per type with other passes besides just via Function::effects (#8625) * Exclude functions that don't have an address (i.e. functions that aren't the target of ref.func) from effect analysis () * Compute effects more precisely for exact + nullable/non-nullable references Part of #8615. --- src/passes/GlobalEffects.cpp | 181 +++++-- src/support/graph_traversal.h | 74 +++ test/gtest/CMakeLists.txt | 1 + test/gtest/graph.cpp | 146 ++++++ ...-effects-closed-world-simplify-locals.wast | 98 ++++ .../global-effects-closed-world-tnh.wast | 37 ++ .../passes/global-effects-closed-world.wast | 449 ++++++++++++++++++ test/lit/passes/global-effects.wast | 67 ++- 8 files changed, 994 insertions(+), 59 deletions(-) create mode 100644 src/support/graph_traversal.h create mode 100644 test/gtest/graph.cpp create mode 100644 test/lit/passes/global-effects-closed-world-simplify-locals.wast create mode 100644 test/lit/passes/global-effects-closed-world-tnh.wast create mode 100644 test/lit/passes/global-effects-closed-world.wast diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 7f47cf108eb..1625b551322 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -19,9 +19,12 @@ // PassOptions structure; see more details there. // +#include + #include "ir/effects.h" #include "ir/module-utils.h" #include "pass.h" +#include "support/graph_traversal.h" #include "support/strongly_connected_components.h" #include "wasm.h" @@ -39,6 +42,9 @@ struct FuncInfo { // Directly-called functions from this function. std::unordered_set calledFunctions; + + // Types that are targets of indirect calls. + std::unordered_set indirectCalledTypes; }; std::map analyzeFuncs(Module& module, @@ -83,11 +89,22 @@ std::map analyzeFuncs(Module& module, if (auto* call = curr->dynCast()) { // Note the direct call. funcInfo.calledFunctions.insert(call->target); + } else if (effects.calls && options.closedWorld) { + HeapType type; + if (auto* callRef = curr->dynCast()) { + // call_ref on unreachable does not have a call effect, + // so this must be a HeapType. + type = callRef->target->type.getHeapType(); + } else if (auto* callIndirect = curr->dynCast()) { + type = callIndirect->heapType; + } else { + funcInfo.effects = UnknownEffects; + return; + } + + funcInfo.indirectCalledTypes.insert(type); } else if (effects.calls) { - // This is an indirect call of some sort, so we must assume the - // worst. To do so, clear the effects, which indicates nothing - // is known (so anything is possible). - // TODO: We could group effects by function type etc. + assert(!options.closedWorld); funcInfo.effects = UnknownEffects; } else { // No call here, but update throwing if we see it. (Only do so, @@ -107,22 +124,84 @@ std::map analyzeFuncs(Module& module, return std::move(analysis.map); } -using CallGraph = std::unordered_map>; +using CallGraphNode = std::variant; + +// Call graph for indirect and direct calls. +// +// key (caller) -> value (callee) +// Function -> Function : direct call +// Function -> HeapType : indirect call to the given HeapType +// HeapType -> Function : The function `callee` has the type `caller`. The +// HeapType may essentially 'call' any of its +// potential implementations. +// HeapType -> HeapType : `callee` is a subtype of `caller`. A call_ref +// could target any subtype of the ref, so we need to +// aggregate effects of subtypes of the target type. +// +// If we're running in an open world, we only include Function -> Function +// edges, and don't compute effects for indirect calls, conservatively assuming +// the worst. +using CallGraph = + std::unordered_map>; CallGraph buildCallGraph(const Module& module, - const std::map& funcInfos) { + const std::map& funcInfos, + bool closedWorld) { CallGraph callGraph; - for (const auto& [func, info] : funcInfos) { - if (info.calledFunctions.empty()) { - continue; + if (!closedWorld) { + for (const auto& [caller, callerInfo] : funcInfos) { + auto& callees = callGraph[caller]; + + // Function -> Function + for (Name calleeFunction : callerInfo.calledFunctions) { + callees.insert(module.getFunction(calleeFunction)); + } + } + + return callGraph; + } + + std::unordered_set allFunctionTypes; + for (const auto& [caller, callerInfo] : funcInfos) { + auto& callees = callGraph[caller]; + + // Function -> Function + for (Name calleeFunction : callerInfo.calledFunctions) { + callees.insert(module.getFunction(calleeFunction)); } - auto& callees = callGraph[func]; - for (Name callee : info.calledFunctions) { - callees.insert(module.getFunction(callee)); + // Function -> Type + allFunctionTypes.insert(caller->type.getHeapType()); + for (HeapType calleeType : callerInfo.indirectCalledTypes) { + callees.insert(calleeType); + + // Add the key to ensure the lookup doesn't fail for indirect calls to + // uninhabited types. + callGraph[calleeType]; } + + // Type -> Function + callGraph[caller->type.getHeapType()].insert(caller); } + // Type -> Type + // Do a DFS up the type heirarchy for all function implementations. + // We are essentially walking up each supertype chain and adding edges from + // super -> subtype, but doing it via DFS to avoid repeated work. + Graph superTypeGraph(allFunctionTypes.begin(), + allFunctionTypes.end(), + [&callGraph](auto&& push, HeapType t) { + // Not needed except that during lookup we expect the + // key to exist. + callGraph[t]; + + if (auto super = t.getDeclaredSuperType()) { + callGraph[*super].insert(t); + push(*super); + } + }); + (void)superTypeGraph.traverseDepthFirst(); + return callGraph; } @@ -152,63 +231,60 @@ void propagateEffects(const Module& module, const PassOptions& passOptions, std::map& funcInfos, const CallGraph& callGraph) { + // We only care about Functions that are roots, not types. + // A type would be a root if a function exists with that type, but no-one + // indirect calls the type. + auto funcNodes = std::views::keys(callGraph) | + std::views::filter([](auto node) { + return std::holds_alternative(node); + }) | + std::views::common; + using funcNodesType = decltype(funcNodes); + struct CallGraphSCCs - : SCCs::const_iterator, CallGraphSCCs> { + : SCCs, CallGraphSCCs> { + const std::map& funcInfos; - const std::unordered_map>& - callGraph; + const CallGraph& callGraph; const Module& module; - CallGraphSCCs( - const std::vector& funcs, - const std::map& funcInfos, - const std::unordered_map>& - callGraph, - const Module& module) - : SCCs::const_iterator, CallGraphSCCs>( - funcs.begin(), funcs.end()), + CallGraphSCCs(funcNodesType&& nodes, + const std::map& funcInfos, + const CallGraph& callGraph, + const Module& module) + : SCCs, CallGraphSCCs>( + std::ranges::begin(nodes), std::ranges::end(nodes)), funcInfos(funcInfos), callGraph(callGraph), module(module) {} - void pushChildren(Function* f) { - auto callees = callGraph.find(f); - if (callees == callGraph.end()) { - return; - } - - for (auto* callee : callees->second) { + void pushChildren(CallGraphNode node) { + for (CallGraphNode callee : callGraph.at(node)) { push(callee); } } }; - - std::vector allFuncs; - for (auto& [func, info] : funcInfos) { - allFuncs.push_back(func); - } - CallGraphSCCs sccs(allFuncs, funcInfos, callGraph, module); + CallGraphSCCs sccs(std::move(funcNodes), funcInfos, callGraph, module); std::vector> componentEffects; // Points to an index in componentEffects - std::unordered_map funcComponents; + std::unordered_map nodeComponents; for (auto ccIterator : sccs) { std::optional& ccEffects = componentEffects.emplace_back(std::in_place, passOptions, module); + std::vector cc(ccIterator.begin(), ccIterator.end()); - std::vector ccFuncs(ccIterator.begin(), ccIterator.end()); - - for (Function* f : ccFuncs) { - funcComponents.emplace(f, componentEffects.size() - 1); + std::vector ccFuncs; + for (CallGraphNode node : cc) { + nodeComponents.emplace(node, componentEffects.size() - 1); + if (auto** func = std::get_if(&node)) { + ccFuncs.push_back(*func); + } } std::unordered_set calleeSccs; - for (Function* caller : ccFuncs) { - auto callees = callGraph.find(caller); - if (callees == callGraph.end()) { - continue; - } - for (auto* callee : callees->second) { - calleeSccs.insert(funcComponents.at(callee)); + for (CallGraphNode caller : cc) { + for (CallGraphNode callee : callGraph.at(caller)) { + calleeSccs.insert(nodeComponents.at(callee)); } } @@ -219,11 +295,13 @@ void propagateEffects(const Module& module, } // Add trap effects for potential cycles. - if (ccFuncs.size() > 1) { + if (cc.size() > 1) { if (ccEffects != UnknownEffects) { ccEffects->trap = true; } - } else { + } else if (ccFuncs.size() == 1) { + // It's possible for a CC to only contain 1 type, but that is not a + // cycle in the call graph. auto* func = ccFuncs[0]; if (funcInfos.at(func).calledFunctions.contains(func->name)) { if (ccEffects != UnknownEffects) { @@ -267,7 +345,8 @@ struct GenerateGlobalEffects : public Pass { std::map funcInfos = analyzeFuncs(*module, getPassOptions()); - auto callGraph = buildCallGraph(*module, funcInfos); + auto callGraph = + buildCallGraph(*module, funcInfos, getPassOptions().closedWorld); propagateEffects(*module, getPassOptions(), funcInfos, callGraph); diff --git a/src/support/graph_traversal.h b/src/support/graph_traversal.h new file mode 100644 index 00000000000..cbe5b3a74a4 --- /dev/null +++ b/src/support/graph_traversal.h @@ -0,0 +1,74 @@ +/* + * Copyright 2026 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. + */ + +#include +#include +#include +#include + +namespace wasm { + +// SuccessorFunction should be an invocable that takes a 'push' function (which +// is an invocable that takes a `const T&`), and a `const T&`. i.e. +// SuccessorFunction should call `push` for each neighbor of the T that it's +// called with. +// TODO: We don't have a good way to write this with concepts today. +// Something like this should do it, but we hit an ICE on dwarf symbols in debug +// builds: requires requires(const SuccessorFunction& successors, const T& t) { +// successors([](const T&) { }, t); } +template class Graph { +public: + template Sen> + requires std::convertible_to, T> + Graph(It rootsBegin, Sen rootsEnd, SuccessorFunction successors) + : roots(rootsBegin, rootsEnd), successors(std::move(successors)) {} + + // Traverse the graph depth-first, calling `successors` exactly once for each + // node (unless the node appears multiple times in `roots`). Return the set of + // nodes visited. + std::unordered_set traverseDepthFirst() const { + std::vector stack(roots.begin(), roots.end()); + std::unordered_set visited(roots.begin(), roots.end()); + + auto maybePush = [&](const T& t) { + auto [_, inserted] = visited.insert(t); + if (inserted) { + stack.push_back(t); + } + }; + + while (!stack.empty()) { + auto curr = std::move(stack.back()); + stack.pop_back(); + + successors(maybePush, curr); + } + + return visited; + } + +private: + std::vector roots; + SuccessorFunction successors; +}; + +template Sen, + typename SuccessorFunction> +Graph(It, Sen, SuccessorFunction) + -> Graph, std::decay_t>; + +} // namespace wasm diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt index 41d16f28e92..54055cbaff1 100644 --- a/test/gtest/CMakeLists.txt +++ b/test/gtest/CMakeLists.txt @@ -12,6 +12,7 @@ set(unittest_SOURCES dataflow.cpp dfa_minimization.cpp disjoint_sets.cpp + graph.cpp leaves.cpp glbs.cpp interpreter.cpp diff --git a/test/gtest/graph.cpp b/test/gtest/graph.cpp new file mode 100644 index 00000000000..cf63417c777 --- /dev/null +++ b/test/gtest/graph.cpp @@ -0,0 +1,146 @@ +/* + * Copyright 2026 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. + */ + +#include + +#include "support/graph_traversal.h" +#include "gtest/gtest.h" + +using namespace wasm; + +TEST(GraphTest, Linear) { + // 0 -> 1 -> 2 + std::vector roots = {0}; + std::vector order; + auto successors = [&](const auto& push, int n) { + order.push_back(n); + if (n < 2) { + push(n + 1); + } + }; + + Graph g(roots.begin(), roots.end(), successors); + auto visited = g.traverseDepthFirst(); + + std::vector expectedOrder = {0, 1, 2}; + EXPECT_EQ(order, expectedOrder); + + std::unordered_set expectedVisited = {0, 1, 2}; + EXPECT_EQ(visited, expectedVisited); +} + +TEST(GraphTest, Cycle) { + // 0 -> 1 -> 0 + std::vector roots = {0}; + std::vector order; + auto successors = [&](const auto& push, int n) { + order.push_back(n); + if (n == 0) { + push(1); + } else if (n == 1) { + push(0); + } + }; + + Graph g(roots.begin(), roots.end(), successors); + auto visited = g.traverseDepthFirst(); + + std::vector expectedOrder = {0, 1}; + EXPECT_EQ(order, expectedOrder); + + std::unordered_set expectedVisited = {0, 1}; + EXPECT_EQ(visited, expectedVisited); +} + +TEST(GraphTest, Diamond) { + // 0 -> 1, 2 + // 1 -> 3 + // 2 -> 3 + + std::vector roots = {0}; + std::vector order; + auto successors = [&](const auto& push, int n) { + order.push_back(n); + if (n == 0) { + push(2); + push(1); + } else if (n == 1 || n == 2) { + push(3); + } + }; + + Graph g(roots.begin(), roots.end(), successors); + auto visited = g.traverseDepthFirst(); + + std::vector expectedOrder = {0, 1, 3, 2}; + EXPECT_EQ(order, expectedOrder); + + std::unordered_set expectedVisited = {0, 1, 2, 3}; + EXPECT_EQ(visited, expectedVisited); +} + +TEST(GraphTest, DuplicateRoots) { + // 0 -> 1, 2 + // 1 -> 0 + // 2 -> 0 + // 0 is added as a root 3 times + + std::vector roots = {0, 0, 0}; + std::vector order; + auto successors = [&](const auto& push, int n) { + order.push_back(n); + if (n == 0) { + push(2); + push(1); + } else if (n == 1 || n == 2) { + push(0); + } + }; + + Graph g(roots.begin(), roots.end(), successors); + auto visited = g.traverseDepthFirst(); + + std::vector expectedOrder = {0, 1, 2, 0, 0}; + EXPECT_EQ(order, expectedOrder); + + std::unordered_set expectedVisited = {0, 1, 2}; + EXPECT_EQ(visited, expectedVisited); +} + +TEST(GraphTest, Disjoint) { + // 0 -> 1 + // 2 -> 3 + + std::vector roots = {2, 0}; + std::vector order; + auto successors = [&](const auto& push, int n) { + order.push_back(n); + if (n == 0) { + push(1); + } else if (n == 2) { + push(3); + } + }; + + Graph g(roots.begin(), roots.end(), successors); + auto visited = g.traverseDepthFirst(); + + std::vector expectedOrder = {0, 1, 2, 3}; + EXPECT_EQ(order, expectedOrder); + + std::unordered_set expectedVisited = {0, 1, 2, 3}; + EXPECT_EQ(visited, expectedVisited); +} diff --git a/test/lit/passes/global-effects-closed-world-simplify-locals.wast b/test/lit/passes/global-effects-closed-world-simplify-locals.wast new file mode 100644 index 00000000000..23f5dc17362 --- /dev/null +++ b/test/lit/passes/global-effects-closed-world-simplify-locals.wast @@ -0,0 +1,98 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --closed-world --generate-global-effects --simplify-locals -S -o - | filecheck %s + +;; Tests for aggregating effects from indirect calls in GlobalEffects when +;; --closed-world is true. Continued from global-effects-closed-world.wast. + +(module + ;; CHECK: (type $indirect-type-super (sub (func (param i32)))) + (type $indirect-type-super (sub (func (param i32)))) + + ;; CHECK: (type $1 (func (param (ref $indirect-type-super)))) + + ;; CHECK: (type $indirect-type-sub (sub $indirect-type-super (func (param i32)))) + (type $indirect-type-sub (sub $indirect-type-super (func (param i32)))) + + ;; CHECK: (global $g1 (mut i32) (i32.const 0)) + (global $g1 (mut i32) (i32.const 0)) + ;; CHECK: (global $g2 (mut i32) (i32.const 0)) + (global $g2 (mut i32) (i32.const 0)) + ;; CHECK: (global $g3 (mut i32) (i32.const 0)) + (global $g3 (mut i32) (i32.const 0)) + + ;; CHECK: (export "impl1" (func $impl1)) + + ;; CHECK: (export "impl2" (func $impl2)) + + ;; CHECK: (func $impl1 (type $indirect-type-super) (param $i32 i32) + ;; CHECK-NEXT: (global.set $g1 + ;; CHECK-NEXT: (local.get $i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $impl1 (export "impl1") (type $indirect-type-super) (param $i32 i32) + (global.set $g1 (local.get $i32)) + ) + + ;; CHECK: (func $impl2 (type $indirect-type-sub) (param $i32 i32) + ;; CHECK-NEXT: (global.set $g2 + ;; CHECK-NEXT: (local.get $i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $impl2 (export "impl2") (type $indirect-type-sub) (param $i32 i32) + (global.set $g2 (local.get $i32)) + ) + + ;; CHECK: (func $caller (type $1) (param $ref (ref $indirect-type-super)) + ;; CHECK-NEXT: (call_ref $indirect-type-super + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $caller (param $ref (ref $indirect-type-super)) + ;; This inherits effects from $impl1 and $impl2, so may mutate $g1 and $g2. + (call_ref $indirect-type-super (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $merges-multiple-effects (type $1) (param $ref (ref $indirect-type-super)) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local $z i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (global.get $g1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (global.get $g2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (call $caller + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (global.get $g3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $merges-multiple-effects (param $ref (ref $indirect-type-super)) + (local $x i32) + (local $y i32) + (local $z i32) + + (local.set $x (global.get $g1)) + (local.set $y (global.get $g2)) + (local.set $z (global.get $g3)) + + ;; This acts as a barrier for $x and $y, but not $z because + ;; $ref may write to $g1 (via $impl1) or $g2 (via $impl2) but not $g3. + ;; $z is optimized out and $x and $y are left alone. + (call $caller (local.get $ref)) + + (drop (local.get $x)) + (drop (local.get $y)) + (drop (local.get $z)) + ) +) diff --git a/test/lit/passes/global-effects-closed-world-tnh.wast b/test/lit/passes/global-effects-closed-world-tnh.wast new file mode 100644 index 00000000000..4c4558f8f95 --- /dev/null +++ b/test/lit/passes/global-effects-closed-world-tnh.wast @@ -0,0 +1,37 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --closed-world --traps-never-happen --generate-global-effects --vacuum -S -o - | filecheck %s + +;; Tests for aggregating effects from indirect calls in GlobalEffects when +;; --closed-world is true. Continued from global-effects-closed-world.wast. + +(module + ;; CHECK: (type $nopType (func (param i32))) + (type $nopType (func (param i32))) + + ;; CHECK: (func $nop (type $nopType) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (export "nop") (type $nopType) + (nop) + ) + + ;; CHECK: (func $calls-nop-via-nullable-ref (type $1) (param $ref (ref null $nopType)) + ;; CHECK-NEXT: (call_ref $nopType + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-nop-via-nullable-ref (param $ref (ref null $nopType)) + (call_ref $nopType (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $f (type $1) (param $ref (ref null $nopType)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref null $nopType)) + ;; The only possible implementation of $nopType has no effects. + ;; $calls-nop-via-nullable-ref may trap from a null reference, but + ;; --traps-never-happen is enabled, so we're free to optimize this out. + (call $calls-nop-via-nullable-ref (local.get $ref)) + ) +) diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast new file mode 100644 index 00000000000..77484c63d6d --- /dev/null +++ b/test/lit/passes/global-effects-closed-world.wast @@ -0,0 +1,449 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --closed-world --generate-global-effects --vacuum -S -o - | filecheck %s + +;; Tests for aggregating effects from indirect calls in GlobalEffects when +;; --closed-world is true. Some more complicated tests are in +;; global-effects-closed-world-simplify-locals.wast. + +(module + ;; CHECK: (type $nopType (func (param i32))) + (type $nopType (func (param i32))) + + ;; CHECK: (func $nop (type $nopType) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (export "nop") (type $nopType) + (nop) + ) + + ;; CHECK: (func $calls-nop-via-ref (type $1) (param $ref (ref $nopType)) + ;; CHECK-NEXT: (call_ref $nopType + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-nop-via-ref (param $ref (ref $nopType)) + ;; This can only possibly be a nop in closed-world. + ;; Ideally vacuum could optimize this out but we don't have a way to share + ;; this information with other passes today. + ;; For now, we can at least annotate that the call to this function in $f + ;; has no effects. + ;; TODO: This call_ref could be marked as having no effects, like the call below. + (call_ref $nopType (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $calls-nop-via-nullable-ref (type $2) (param $ref (ref null $nopType)) + ;; CHECK-NEXT: (call_ref $nopType + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-nop-via-nullable-ref (param $ref (ref null $nopType)) + (call_ref $nopType (i32.const 1) (local.get $ref)) + ) + + + ;; CHECK: (func $f (type $1) (param $ref (ref $nopType)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref $nopType)) + ;; $calls-nop-via-ref has no effects because we determined that it can only + ;; call $nop. We can optimize this call out. + (call $calls-nop-via-ref (local.get $ref)) + ) + + ;; CHECK: (func $g (type $2) (param $ref (ref null $nopType)) + ;; CHECK-NEXT: (call $calls-nop-via-nullable-ref + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $g (param $ref (ref null $nopType)) + ;; Similar to $f, but we may still trap here because the ref is null, so we + ;; don't optimize. + (call $calls-nop-via-nullable-ref (local.get $ref)) + ) +) + +;; Same as the above but with call_indirect +(module + ;; CHECK: (type $nopType (func (param i32))) + (type $nopType (func (param i32))) + + (table 1 1 funcref) + + ;; CHECK: (func $nop (type $nopType) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (export "nop") (type $nopType) + (nop) + ) + + ;; CHECK: (func $calls-nop-via-ref (type $1) + ;; CHECK-NEXT: (call_indirect $0 (type $nopType) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-nop-via-ref + ;; This can only possibly be a nop in closed-world. + ;; Ideally vacuum could optimize this out but we don't have a way to share + ;; this information with other passes today. + ;; For now, we can at least annotate that the call to this function in $f + ;; has no effects. + ;; TODO: This call_ref could be marked as having no effects, like the call below. + (call_indirect (type $nopType) (i32.const 1) (i32.const 0)) + ) + + ;; CHECK: (func $f (type $1) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $f + ;; $calls-nop-via-ref has no effects because we determined that it can only + ;; call $nop. We can optimize this call out. + (call $calls-nop-via-ref) + ) +) + +(module + ;; CHECK: (type $maybe-has-effects (func (param i32))) + (type $maybe-has-effects (func (param i32))) + + ;; CHECK: (func $unreachable (type $maybe-has-effects) (param $0 i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $unreachable (export "unreachable") (type $maybe-has-effects) (param i32) + (unreachable) + ) + + ;; CHECK: (func $nop2 (type $maybe-has-effects) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop2 (export "nop2") (type $maybe-has-effects) (param i32) + (nop) + ) + + ;; CHECK: (func $calls-effectful-function-via-ref (type $1) (param $ref (ref $maybe-has-effects)) + ;; CHECK-NEXT: (call_ref $maybe-has-effects + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-effectful-function-via-ref (param $ref (ref $maybe-has-effects)) + (call_ref $maybe-has-effects (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $f (type $1) (param $ref (ref $maybe-has-effects)) + ;; CHECK-NEXT: (call $calls-effectful-function-via-ref + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref $maybe-has-effects)) + ;; This may be a nop or it may trap depending on the ref. + ;; We don't know so don't optimize it out. + (call $calls-effectful-function-via-ref (local.get $ref)) + ) +) + +;; Same as above but with call_indirect +(module + (table 1 1 funcref) + + ;; CHECK: (type $maybe-has-effects (func (param i32))) + (type $maybe-has-effects (func (param i32))) + + ;; CHECK: (func $unreachable (type $maybe-has-effects) (param $0 i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $unreachable (export "unreachable") (type $maybe-has-effects) (param i32) + (unreachable) + ) + + ;; CHECK: (func $nop2 (type $maybe-has-effects) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop2 (export "nop2") (type $maybe-has-effects) (param i32) + (nop) + ) + + ;; CHECK: (func $calls-effectful-function-via-ref (type $1) + ;; CHECK-NEXT: (call_indirect $0 (type $maybe-has-effects) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-effectful-function-via-ref + (call_indirect (type $maybe-has-effects) (i32.const 1) (i32.const 1)) + ) + + ;; CHECK: (func $f (type $1) + ;; CHECK-NEXT: (call $calls-effectful-function-via-ref) + ;; CHECK-NEXT: ) + (func $f + ;; This may be a nop or it may trap depending on the ref. + ;; We don't know so don't optimize it out. + (call $calls-effectful-function-via-ref) + ) +) + +(module + ;; CHECK: (type $uninhabited (func (param i32))) + (type $uninhabited (func (param i32))) + + ;; CHECK: (func $calls-uninhabited (type $1) (param $ref (ref $uninhabited)) + ;; CHECK-NEXT: (call_ref $uninhabited + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-uninhabited (param $ref (ref $uninhabited)) + ;; It's impossible to create a ref to call this function with. + ;; TODO: Optimize this to (unreachable). + (call_ref $uninhabited (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $calls-nullable-uninhabited (type $2) (param $ref (ref null $uninhabited)) + ;; CHECK-NEXT: (call_ref $uninhabited + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-nullable-uninhabited (param $ref (ref null $uninhabited)) + ;; This must be null, so it's guaranteed to trap and can't be optimized out. + ;; TODO: Optimize this to (unreachable). + (call_ref $uninhabited (i32.const 1) (local.get $ref)) + ) + + + ;; CHECK: (func $f (type $1) (param $ref (ref $uninhabited)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref $uninhabited)) + ;; There's no function with this type, so it's impossible to create a ref to + ;; call this function with and there are no effects to aggregate. + ;; Remove this call. + (call $calls-uninhabited (local.get $ref)) + ) + + ;; CHECK: (func $g (type $2) (param $ref (ref null $uninhabited)) + ;; CHECK-NEXT: (call $calls-nullable-uninhabited + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $g (param $ref (ref null $uninhabited)) + ;; Similar to above but we have a nullable reference, so we may trap and + ;; can't optimize the call out. + (call $calls-nullable-uninhabited (local.get $ref)) + ) +) + +(module + ;; CHECK: (type $super (sub (func))) + (type $super (sub (func))) + ;; Subtype + ;; CHECK: (type $sub (sub $super (func))) + (type $sub (sub $super (func))) + + ;; CHECK: (func $nop-with-supertype (type $super) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop-with-supertype (export "nop-with-supertype") (type $super) + ) + + ;; CHECK: (func $effectful-with-subtype (type $sub) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $effectful-with-subtype (export "effectful-with-subtype") (type $sub) + (unreachable) + ) + + ;; CHECK: (func $calls-ref-with-supertype (type $1) (param $func (ref $super)) + ;; CHECK-NEXT: (call_ref $super + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-ref-with-supertype (param $func (ref $super)) + (call_ref $super (local.get $func)) + ) + + ;; CHECK: (func $calls-ref-with-exact-supertype (type $2) (param $func (ref (exact $super))) + ;; CHECK-NEXT: (call_ref $super + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-ref-with-exact-supertype (param $func (ref (exact $super))) + (call_ref $super (local.get $func)) + ) + + ;; CHECK: (func $f (type $1) (param $func (ref $super)) + ;; CHECK-NEXT: (call $calls-ref-with-supertype + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (param $func (ref $super)) + ;; Check that we account for subtyping correctly. + ;; $super has no effects (i.e. the union of all effects of functions with + ;; this type is empty). However, $sub does have effects, and we can call_ref + ;; with that subtype, so we need to include the unreachable effect and we + ;; can't optimize out this call. + (call $calls-ref-with-supertype (local.get $func)) + ) + + ;; CHECK: (func $g (type $2) (param $func (ref (exact $super))) + ;; CHECK-NEXT: (call $calls-ref-with-exact-supertype + ;; CHECK-NEXT: (local.get $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $g (param $func (ref (exact $super))) + ;; Same as above but this time our reference is the exact supertype + ;; so we know not to aggregate effects from the subtype. + ;; TODO: this case doesn't optimize today. Add exact ref support in the pass. + (call $calls-ref-with-exact-supertype (local.get $func)) + ) +) + +(module + ;; CHECK: (type $only-has-effects-in-not-addressable-function (func (param i32))) + (type $only-has-effects-in-not-addressable-function (func (param i32))) + + ;; CHECK: (func $nop (type $only-has-effects-in-not-addressable-function) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (export "nop") (type $only-has-effects-in-not-addressable-function) (param i32) + ) + + ;; CHECK: (func $has-effects-but-not-exported (type $only-has-effects-in-not-addressable-function) (param $0 i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $has-effects-but-not-exported (type $only-has-effects-in-not-addressable-function) (param i32) + (unreachable) + ) + + ;; CHECK: (func $calls-type-with-effects-but-not-addressable (type $1) (param $ref (ref $only-has-effects-in-not-addressable-function)) + ;; CHECK-NEXT: (call_ref $only-has-effects-in-not-addressable-function + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-type-with-effects-but-not-addressable (param $ref (ref $only-has-effects-in-not-addressable-function)) + (call_ref $only-has-effects-in-not-addressable-function (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $f (type $1) (param $ref (ref $only-has-effects-in-not-addressable-function)) + ;; CHECK-NEXT: (call $calls-type-with-effects-but-not-addressable + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref $only-has-effects-in-not-addressable-function)) + ;; The type $has-effects-but-not-exported doesn't have an address because + ;; it's not exported and it's never the target of a ref.func. + ;; We should be able to determine that $ref can only point to $nop. + ;; TODO: Only aggregate effects from functions that are addressed. + (call $calls-type-with-effects-but-not-addressable (local.get $ref)) + ) +) + +(module + ;; CHECK: (type $unreachable-via-direct-call (func (param i32))) + (type $unreachable-via-direct-call (func (param i32))) + + ;; CHECK: (elem declare func $calls-unreachable) + + ;; CHECK: (func $unreachable (type $0) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $unreachable + (unreachable) + ) + + ;; CHECK: (func $calls-unreachable (type $unreachable-via-direct-call) (param $0 i32) + ;; CHECK-NEXT: (call $unreachable) + ;; CHECK-NEXT: ) + (func $calls-unreachable (export "calls-unreachable") (param i32) + (call $unreachable) + ) + + ;; CHECK: (func $calls-unreachable-via-ref-and-direct-call-transtively (type $0) + ;; CHECK-NEXT: (call_ref $unreachable-via-direct-call + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (ref.func $calls-unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-unreachable-via-ref-and-direct-call-transtively + (call_ref $unreachable-via-direct-call (i32.const 0) (ref.func $calls-unreachable)) + ) + + ;; CHECK: (func $f (type $0) + ;; CHECK-NEXT: (call $calls-unreachable-via-ref-and-direct-call-transtively) + ;; CHECK-NEXT: ) + (func $f + ;; Test that we can analyze longer call chains containing both indirect and + ;; direct calls. In this case the call chain hits an unreachable via an + ;; indirect call, then direct call, so we can't optimize this out. + (call $calls-unreachable-via-ref-and-direct-call-transtively) + ) +) + +(module + ;; CHECK: (type $t (func (param i32))) + (type $t (func (param i32))) + + ;; (import "" "" (func $imported-func (type $t))) + ;; CHECK: (import "" "" (func $imported-func (type $t) (param i32))) + (import "" "" (func $imported-func (type $t))) + + (elem declare $imported-func) + + ;; CHECK: (func $nop (type $t) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (param i32) + ) + + ;; CHECK: (func $indirect-calls (type $1) (param $ref (ref $t)) + ;; CHECK-NEXT: (call_ref $t + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $indirect-calls (param $ref (ref $t)) + (call_ref $t (i32.const 1) (local.get $ref)) + ) + + ;; CHECK: (func $f (type $1) (param $ref (ref $t)) + ;; CHECK-NEXT: (call $indirect-calls + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f (param $ref (ref $t)) + ;; $indirect-calls might end up calling an imported function, + ;; so we don't know anything about effects here + (call $indirect-calls (local.get $ref)) + ) +) + +(module + (type $t (func (param i32))) + ;; CHECK: (func $calls-unreachable (type $0) + ;; CHECK-NEXT: (block ;; (replaces unreachable CallRef we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $calls-unreachable (export "calls-unreachable") + (call_ref $t (unreachable)) + ) + + ;; CHECK: (func $f (type $0) + ;; CHECK-NEXT: (call $calls-unreachable) + ;; CHECK-NEXT: ) + (func $f + ;; $t looks like it has no effects, but unreachable is passed in, + ;; so preserve the trap. + (call $calls-unreachable) + ) +) diff --git a/test/lit/passes/global-effects.wast b/test/lit/passes/global-effects.wast index 1125f738e68..86e4988e92c 100644 --- a/test/lit/passes/global-effects.wast +++ b/test/lit/passes/global-effects.wast @@ -13,14 +13,22 @@ ;; INCLUDE: (type $void (func)) (type $void (func)) - ;; WITHOUT: (type $1 (func (result i32))) + ;; WITHOUT: (type $indirect-type (func (param f32))) + ;; INCLUDE: (type $indirect-type (func (param f32))) + (type $indirect-type (func (param f32))) - ;; WITHOUT: (type $2 (func (param i32))) + ;; WITHOUT: (type $2 (func (param (ref $indirect-type)))) + + ;; WITHOUT: (type $3 (func (result i32))) + + ;; WITHOUT: (type $4 (func (param i32))) ;; WITHOUT: (import "a" "b" (func $import (type $void))) - ;; INCLUDE: (type $1 (func (result i32))) + ;; INCLUDE: (type $2 (func (param (ref $indirect-type)))) + + ;; INCLUDE: (type $3 (func (result i32))) - ;; INCLUDE: (type $2 (func (param i32))) + ;; INCLUDE: (type $4 (func (param i32))) ;; INCLUDE: (import "a" "b" (func $import (type $void))) (import "a" "b" (func $import)) @@ -150,7 +158,7 @@ (call $unreachable) ) - ;; WITHOUT: (func $unimportant-effects (type $1) (result i32) + ;; WITHOUT: (func $unimportant-effects (type $3) (result i32) ;; WITHOUT-NEXT: (local $x i32) ;; WITHOUT-NEXT: (local.set $x ;; WITHOUT-NEXT: (i32.const 100) @@ -159,7 +167,7 @@ ;; WITHOUT-NEXT: (local.get $x) ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $unimportant-effects (type $1) (result i32) + ;; INCLUDE: (func $unimportant-effects (type $3) (result i32) ;; INCLUDE-NEXT: (local $x i32) ;; INCLUDE-NEXT: (local.set $x ;; INCLUDE-NEXT: (i32.const 100) @@ -380,7 +388,7 @@ ) ) - ;; WITHOUT: (func $call-throw-or-unreachable-and-catch (type $2) (param $x i32) + ;; WITHOUT: (func $call-throw-or-unreachable-and-catch (type $4) (param $x i32) ;; WITHOUT-NEXT: (block $tryend ;; WITHOUT-NEXT: (try_table (catch_all $tryend) ;; WITHOUT-NEXT: (if @@ -395,7 +403,7 @@ ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) ;; WITHOUT-NEXT: ) - ;; INCLUDE: (func $call-throw-or-unreachable-and-catch (type $2) (param $x i32) + ;; INCLUDE: (func $call-throw-or-unreachable-and-catch (type $4) (param $x i32) ;; INCLUDE-NEXT: (block $tryend ;; INCLUDE-NEXT: (try_table (catch_all $tryend) ;; INCLUDE-NEXT: (if @@ -473,4 +481,47 @@ (call $cycle-with-unknown-call) (call $import) ) + + + ;; WITHOUT: (func $nop-indirect (type $indirect-type) (param $0 f32) + ;; WITHOUT-NEXT: (nop) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $nop-indirect (type $indirect-type) (param $0 f32) + ;; INCLUDE-NEXT: (nop) + ;; INCLUDE-NEXT: ) + (func $nop-indirect (type $indirect-type) (param f32) + ) + + ;; WITHOUT: (func $unknown-indirect-call (type $2) (param $ref (ref $indirect-type)) + ;; WITHOUT-NEXT: (call_ref $indirect-type + ;; WITHOUT-NEXT: (f32.const 1) + ;; WITHOUT-NEXT: (local.get $ref) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $unknown-indirect-call (type $2) (param $ref (ref $indirect-type)) + ;; INCLUDE-NEXT: (call_ref $indirect-type + ;; INCLUDE-NEXT: (f32.const 1) + ;; INCLUDE-NEXT: (local.get $ref) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + (func $unknown-indirect-call (param $ref (ref $indirect-type)) + (call_ref $indirect-type (f32.const 1) (local.get $ref)) + ) + + ;; WITHOUT: (func $calls-unknown-indirect-call (type $2) (param $ref (ref $indirect-type)) + ;; WITHOUT-NEXT: (call $unknown-indirect-call + ;; WITHOUT-NEXT: (local.get $ref) + ;; WITHOUT-NEXT: ) + ;; WITHOUT-NEXT: ) + ;; INCLUDE: (func $calls-unknown-indirect-call (type $2) (param $ref (ref $indirect-type)) + ;; INCLUDE-NEXT: (call $unknown-indirect-call + ;; INCLUDE-NEXT: (local.get $ref) + ;; INCLUDE-NEXT: ) + ;; INCLUDE-NEXT: ) + (func $calls-unknown-indirect-call (param $ref (ref $indirect-type)) + ;; In a closed world, we could determine that the ref can only possibly be + ;; $nop-direct and optimize it out. See global-effects-closed-world.wast + ;; for related tests. + (call $unknown-indirect-call (local.get $ref)) + ) ) From a7fd0520d87d22690fd22691b6af1d3130f55500 Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Sun, 26 Apr 2026 18:56:04 -0700 Subject: [PATCH 055/168] Canonicalize NaNs when casting between float and double (#8645) Fixes #8626. Also part of #8261. The bug is due to the fact that C++ doesn't guarantee a bitwise representation for NaNs that are casted from double -> float or vice versa. From https://en.cppreference.com/cpp/language/implicit_conversion: > Floating-point promotion A [prvalue](https://en.cppreference.com/cpp/language/value_category#prvalue) of type float can be converted to a prvalue of type double. The value does not change. In the case of NaNs, the value (NaN) is preserved regardless of the particular NaN that is picked, so we have no guarantee of the bitwise representation. And for double -> float: > If the conversion is listed under floating-point promotions, it is a promotion and not a conversion. > * If the source value can be represented exactly in the destination type, it does not change. > * If the source value is between two representable values of the destination type, the result is one of those two values (it is implementation-defined which one, although if IEEE arithmetic is supported, rounding defaults [to nearest](https://en.cppreference.com/cpp/numeric/fenv/FE_round)). > * Otherwise, the behavior is undefined. I assume NaN conversions again fall under case 1 meaning that the "value" (NaN) is preserved but not the bitwise representation. Canonicalize NaNs after promotion or demotion to ensure that the quiet bit remains set. --- src/wasm/literal.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index 551dd7ed738..22fc5447e1f 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -864,7 +864,7 @@ Literal Literal::extendToUI64() const { Literal Literal::extendToF64() const { assert(type == Type::f32); - return Literal(double(getf32())); + return standardizeNaN(Literal(static_cast(getf32()))); } Literal Literal::extendS8() const { @@ -1164,7 +1164,7 @@ Literal Literal::sqrt() const { Literal Literal::demote() const { auto f64 = getf64(); if (std::isnan(f64)) { - return Literal(float(f64)); + return standardizeNaN(Literal(static_cast(f64))); } if (std::isinf(f64)) { return Literal(float(f64)); @@ -2786,7 +2786,8 @@ template Literal extendF32(const Literal& vec) { LaneArray<2> result; for (size_t i = 0; i < 2; ++i) { size_t idx = (Side == LaneOrder::Low) ? i : i + 2; - result[i] = Literal((double)lanes[idx].getf32()); + result[i] = Literal::standardizeNaN( + Literal(static_cast(lanes[idx].getf32()))); } return Literal(result); } From 74ca4eb844f17fefc150faffe32b0830f845e8b3 Mon Sep 17 00:00:00 2001 From: walkingeyerobot Date: Mon, 27 Apr 2026 00:51:26 -0400 Subject: [PATCH 056/168] add missing include (#8646) --- src/support/graph_traversal.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/support/graph_traversal.h b/src/support/graph_traversal.h index cbe5b3a74a4..c7ad6ef02c9 100644 --- a/src/support/graph_traversal.h +++ b/src/support/graph_traversal.h @@ -18,6 +18,7 @@ #include #include #include +#include namespace wasm { From 1e36622832c32ad335403065c322781982f1b55c Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 27 Apr 2026 10:00:31 -0700 Subject: [PATCH 057/168] [wasm-reduce] Empty functions with delta debugging (#8640) Delta debugging is an algorithm for finding the minimal set of items necessary to preserve a condition. It generally works by using increasingly fine partitions of the orignal set of items and alternating trying to keep just one of the partitions to make rapid progress and trying to keep the complement of one of the partitions to make smaller changes that are more likely to work. Add a header containing a templatized delta debugging implementation, then use it in wasm-reduce to preserve the minimal number of function bodies necessary to reproduce the reduction condition. This should allow wasm-reduce to make much faster progress on emptying out functions in the common case and leave it much less work to do afterwards. Using delta debugging for deleting functions and performing other reduction operations is left as future work. Deleting functions in particular is challenging because it can involve reloading the module from the working file, potentially changing function names and invalidating the function names that would be stored in the delta debugging partitions. --- src/support/delta_debugging.h | 121 ++++++++++++++++++++ src/tools/wasm-reduce/wasm-reduce.cpp | 159 +++++++++++++++++++------- test/gtest/CMakeLists.txt | 1 + test/gtest/delta_debugging.cpp | 97 ++++++++++++++++ 4 files changed, 335 insertions(+), 43 deletions(-) create mode 100644 src/support/delta_debugging.h create mode 100644 test/gtest/delta_debugging.cpp diff --git a/src/support/delta_debugging.h b/src/support/delta_debugging.h new file mode 100644 index 00000000000..9607d2011d0 --- /dev/null +++ b/src/support/delta_debugging.h @@ -0,0 +1,121 @@ +/* + * Copyright 2026 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. + */ + +#ifndef wasm_support_delta_debugging_h +#define wasm_support_delta_debugging_h + +#include +#include +#include + +namespace wasm { + +// Use the delta debugging algorithm (Zeller 1999, +// https://dl.acm.org/doi/10.1109/32.988498) to find the minimal set of +// items necessary to preserve some property. Returns that minimal set of +// items, preserving their input order. `tryPartition` should have this +// signature: +// +// bool tryPartition(size_t partitionIndex, +// size_t numPartitions, +// const std::vector& partition) +// +// It should return true iff the property is preserved while keeping only +// `partition` items. +template +std::vector deltaDebugging(std::vector items, const F& tryPartition) { + if (items.empty()) { + return items; + } + // First try removing everything. + if (tryPartition(0, 1, {})) { + return {}; + } + size_t numPartitions = 2; + while (numPartitions <= items.size()) { + // Partition the items. + std::vector> partitions; + size_t size = items.size(); + size_t basePartitionSize = size / numPartitions; + size_t rem = size % numPartitions; + size_t idx = 0; + for (size_t i = 0; i < numPartitions; ++i) { + size_t partitionSize = basePartitionSize + (i < rem ? 1 : 0); + if (partitionSize > 0) { + std::vector partition; + partition.reserve(partitionSize); + for (size_t j = 0; j < partitionSize; ++j) { + partition.push_back(items[idx++]); + } + partitions.emplace_back(std::move(partition)); + } + } + assert(numPartitions == partitions.size()); + + bool reduced = false; + + // Try keeping only one partition. Try each partition in turn. + for (size_t i = 0; i < numPartitions; ++i) { + if (tryPartition(i, numPartitions, partitions[i])) { + items = std::move(partitions[i]); + numPartitions = 2; + reduced = true; + break; + } + } + if (reduced) { + continue; + } + + // Otherwise, try keeping the complement of a partition. Do not do this with + // only two partitions because that would be no different from what we + // already tried. + if (numPartitions > 2) { + for (size_t i = 0; i < numPartitions; ++i) { + std::vector complement; + complement.reserve(items.size() - partitions[i].size()); + for (size_t j = 0; j < numPartitions; ++j) { + if (j != i) { + complement.insert( + complement.end(), partitions[j].begin(), partitions[j].end()); + } + } + if (tryPartition(i, numPartitions, complement)) { + items = std::move(complement); + numPartitions = std::max(numPartitions - 1, size_t(2)); + reduced = true; + break; + } + } + if (reduced) { + continue; + } + } + + if (numPartitions == items.size()) { + // Cannot further refine the partitions. We're done. + break; + } + + // Otherwise, make the partitions finer grained. + numPartitions = std::min(items.size(), 2 * numPartitions); + } + return items; +} + +} // namespace wasm + +#endif // wasm_support_delta_debugging_h diff --git a/src/tools/wasm-reduce/wasm-reduce.cpp b/src/tools/wasm-reduce/wasm-reduce.cpp index e002a499545..6722cb45a03 100644 --- a/src/tools/wasm-reduce/wasm-reduce.cpp +++ b/src/tools/wasm-reduce/wasm-reduce.cpp @@ -29,12 +29,12 @@ #include "ir/branch-utils.h" #include "ir/iteration.h" -#include "ir/literal-utils.h" #include "ir/properties.h" #include "ir/utils.h" #include "pass.h" #include "support/colors.h" #include "support/command-line.h" +#include "support/delta_debugging.h" #include "support/file.h" #include "support/hash.h" #include "support/path.h" @@ -894,8 +894,105 @@ struct Reducer } } - // Reduces entire functions at a time. Returns whether we did a significant - // amount of reduction that justifies doing even more. + bool isEmptyBody(Expression* body) { + if (body->is() || body->is()) { + return true; + } + if (auto* block = body->dynCast()) { + return block->list.empty(); + } + return false; + } + + void reduceFunctionBodies() { + std::cerr << "| try to remove function bodies\n"; + // Use function indices to speed up finding the complement of the kept + // partition. + std::vector nontrivialFuncIndices; + nontrivialFuncIndices.reserve(module->functions.size()); + for (Index i = 0; i < module->functions.size(); ++i) { + auto& func = module->functions[i]; + // Skip functions that already have trivial bodies. + if (func->imported() || isEmptyBody(func->body)) { + continue; + } + nontrivialFuncIndices.push_back(i); + } + // TODO: Use something other than an exception to implement early return. + struct EarlyReturn {}; + try { + deltaDebugging( + nontrivialFuncIndices, + [&](Index partitionIndex, + Index numPartitions, + const std::vector& partition) { + // Stop early if the partition size is less than the square root of + // the remaining set. We don't want to waste time on very fine-grained + // partitions when we could switch to another reduction strategy + // instead. + if (size_t sqrtRemaining = std::sqrt(nontrivialFuncIndices.size()); + partition.size() > 0 && partition.size() < sqrtRemaining) { + throw EarlyReturn{}; + } + + std::cerr << "| try partition " << partitionIndex + 1 << " / " + << numPartitions << " (size " << partition.size() << ")\n"; + Index removedSize = nontrivialFuncIndices.size() - partition.size(); + std::vector oldBodies(removedSize); + + // We first need to remove each non-kept function body, and later we + // might need to restore the same function bodies. Abstract the logic + // for iterating over these function bodies. `f` takes a Function* and + // Expression*& for the stashed body. + auto forEachRemovedFuncBody = [&](auto f) { + Index bodyIndex = 0; + Index nontrivialIndex = 0; + Index partitionIndex = 0; + while (nontrivialIndex < nontrivialFuncIndices.size()) { + if (partitionIndex < partition.size() && + nontrivialFuncIndices[nontrivialIndex] == + partition[partitionIndex]) { + // Kept, skip it. + nontrivialIndex++; + partitionIndex++; + } else { + // Removed, process it + Index funcIndex = nontrivialFuncIndices[nontrivialIndex++]; + f(module->functions[funcIndex].get(), oldBodies[bodyIndex++]); + } + } + assert(bodyIndex == removedSize); + assert(partitionIndex == partition.size()); + }; + + // Stash the bodies. + forEachRemovedFuncBody([&](Function* func, Expression*& oldBody) { + oldBody = func->body; + Builder builder(*module); + if (func->getResults() == Type::none) { + func->body = builder.makeNop(); + } else { + func->body = builder.makeUnreachable(); + } + }); + + if (!writeAndTestReduction()) { + // Failure. Restore the bodies. + forEachRemovedFuncBody([](Function* func, Expression*& oldBody) { + func->body = oldBody; + }); + return false; + } + + // Success! + noteReduction(removedSize); + nontrivialFuncIndices = partition; + return true; + }); + } catch (EarlyReturn) { + } + } + bool reduceFunctions() { // try to remove functions std::vector functionNames; @@ -936,11 +1033,9 @@ struct Reducer } std::cerr << "| trying at i=" << i << " of size " << names.size() << "\n"; - // Try to remove functions and/or empty them. Note that - // tryToRemoveFunctions() will reload the module if it fails, which means - // function names may change - for that reason, run it second. - justReduced = tryToEmptyFunctions(names) || tryToRemoveFunctions(names); - if (justReduced) { + // Note that tryToRemoveFunctions() will reload the module if it fails, + // which means function names may change. + if (tryToRemoveFunctions(names)) { noteReduction(names.size()); // Subtract 1 since the loop increments us anyhow by one: we want to // skip over the skipped functions, and not any more. @@ -967,8 +1062,11 @@ struct Reducer assert(curr == module.get()); curr = nullptr; + reduceFunctionBodies(); + // Reduction of entire functions at a time is very effective, and we do it // with exponential growth and backoff, so keep doing it while it works. + // TODO: Figure out how to use delta debugging for this as well. while (reduceFunctions()) { } @@ -1047,41 +1145,6 @@ struct Reducer } } - // Try to empty out the bodies of some functions. - bool tryToEmptyFunctions(std::vector names) { - std::vector oldBodies; - size_t actuallyEmptied = 0; - for (auto name : names) { - auto* func = module->getFunction(name); - auto* oldBody = func->body; - oldBodies.push_back(oldBody); - // Nothing to do for imported functions (body is nullptr) or for bodies - // that have already been as reduced as we can make them. - if (func->imported() || oldBody->is() || - oldBody->is()) { - continue; - } - actuallyEmptied++; - bool useUnreachable = func->getResults() != Type::none; - if (useUnreachable) { - func->body = builder->makeUnreachable(); - } else { - func->body = builder->makeNop(); - } - } - if (actuallyEmptied > 0 && writeAndTestReduction()) { - std::cerr << "| emptied " << actuallyEmptied << " / " - << names.size() << " functions\n"; - return true; - } else { - // Restore the bodies. - for (size_t i = 0; i < names.size(); i++) { - module->getFunction(names[i])->body = oldBodies[i]; - } - return false; - } - } - // Try to actually remove functions. If they are somehow referred to, we will // get a validation error and undo it. bool tryToRemoveFunctions(std::vector names) { @@ -1504,10 +1567,20 @@ More documentation can be found at bool stopping = false; + bool first = true; while (1) { Reducer reducer( command, test, working, binary, deNan, verbose, debugInfo, options); + // For extremely large modules with slow reproduction commands, reducing + // function bodies first can be more effective than running passes. TODO: + // clean this up and reconsider the order of reducers. + if (first) { + reducer.loadWorking(); + reducer.reduceFunctionBodies(); + first = false; + } + // run binaryen optimization passes to reduce. passes are fast to run // and can often reduce large amounts of code efficiently, as opposed // to detructive reduction (i.e., that doesn't preserve correctness as diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt index 54055cbaff1..1c8e2de179f 100644 --- a/test/gtest/CMakeLists.txt +++ b/test/gtest/CMakeLists.txt @@ -10,6 +10,7 @@ set(unittest_SOURCES cast-check.cpp cfg.cpp dataflow.cpp + delta_debugging.cpp dfa_minimization.cpp disjoint_sets.cpp graph.cpp diff --git a/test/gtest/delta_debugging.cpp b/test/gtest/delta_debugging.cpp new file mode 100644 index 00000000000..7e4c8ad4db5 --- /dev/null +++ b/test/gtest/delta_debugging.cpp @@ -0,0 +1,97 @@ +#include "support/delta_debugging.h" +#include "gtest/gtest.h" +#include +#include +#include + +using namespace wasm; + +TEST(DeltaDebuggingTest, EmptyInput) { + std::vector items; + auto result = deltaDebugging( + items, [](size_t, size_t, const std::vector&) { return false; }); + EXPECT_TRUE(result.empty()); +} + +TEST(DeltaDebuggingTest, SingleItem) { + std::vector items = {0, 1, 2, 3, 4, 5, 6, 7}; + auto result = deltaDebugging( + items, [](size_t, size_t, const std::vector& partition) { + return std::find(partition.begin(), partition.end(), 3) != + partition.end(); + }); + std::vector expected = {3}; + EXPECT_EQ(result, expected); +} + +TEST(DeltaDebuggingTest, MultipleItemsAdjacent) { + std::vector items = {0, 1, 2, 3, 4, 5, 6, 7}; + auto result = deltaDebugging( + items, [](size_t, size_t, const std::vector& partition) { + bool has2 = + std::find(partition.begin(), partition.end(), 2) != partition.end(); + bool has3 = + std::find(partition.begin(), partition.end(), 3) != partition.end(); + return has2 && has3; + }); + std::vector expected = {2, 3}; + EXPECT_EQ(result, expected); +} + +TEST(DeltaDebuggingTest, MultipleItemsNonAdjacent) { + std::vector items = {0, 1, 2, 3, 4, 5, 6, 7}; + auto result = deltaDebugging( + items, [](size_t, size_t, const std::vector& partition) { + bool has2 = + std::find(partition.begin(), partition.end(), 2) != partition.end(); + bool has5 = + std::find(partition.begin(), partition.end(), 5) != partition.end(); + return has2 && has5; + }); + std::vector expected = {2, 5}; + EXPECT_EQ(result, expected); +} + +TEST(DeltaDebuggingTest, OrderMaintained) { + std::vector items = {3, 1, 4, 2}; + auto result = deltaDebugging( + items, [](size_t, size_t, const std::vector& partition) { + bool has3 = + std::find(partition.begin(), partition.end(), 3) != partition.end(); + bool has2 = + std::find(partition.begin(), partition.end(), 2) != partition.end(); + return has3 && has2; + }); + std::vector expected = {3, 2}; + EXPECT_EQ(result, expected); +} + +TEST(DeltaDebuggingTest, DifferentTypes) { + std::vector items = {"apple", "banana", "cherry", "date"}; + auto result = deltaDebugging( + items, [](size_t, size_t, const std::vector& partition) { + bool hasBanana = + std::find(partition.begin(), partition.end(), "banana") != + partition.end(); + bool hasDate = std::find(partition.begin(), partition.end(), "date") != + partition.end(); + return hasBanana && hasDate; + }); + std::vector expected = {"banana", "date"}; + EXPECT_EQ(result, expected); +} + +TEST(DeltaDebuggingTest, UnconditionallyTrue) { + std::vector items = {0, 1, 2, 3}; + auto result = deltaDebugging( + items, [](size_t, size_t, const std::vector&) { return true; }); + EXPECT_TRUE(result.empty()); +} + +TEST(DeltaDebuggingTest, UnconditionallyFalse) { + std::vector items = {0, 1, 2, 3}; + auto result = deltaDebugging( + items, [](size_t, size_t, const std::vector&) { return false; }); + std::vector expected = {0, 1, 2, 3}; + EXPECT_EQ(result, expected); +} From 37cf5df49e1148ae07913434c20485f7230e2002 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Mon, 27 Apr 2026 12:58:29 -0700 Subject: [PATCH 058/168] Rename `lld` tests to `finalize` tests. NFC (#8395) These tests are for checking the behavior of `wasm-emscripten-finalize`. --- check.py | 4 ++-- scripts/auto_update_tests.py | 4 ++-- scripts/test/{lld.py => finalize.py} | 6 +++--- ..._lld_tests.py => generate_finalize_tests.py} | 16 ++++++++-------- test/{lld => finalize}/basic_safe_stack.s | 0 test/{lld => finalize}/basic_safe_stack.wat | 0 test/{lld => finalize}/basic_safe_stack.wat.out | 0 test/{lld => finalize}/duplicate_imports.wat | 0 .../{lld => finalize}/duplicate_imports.wat.out | 0 test/{lld => finalize}/em_asm.cpp | 0 test/{lld => finalize}/em_asm.wat | 0 test/{lld => finalize}/em_asm.wat.out | 0 test/{lld => finalize}/em_asm64.cpp | 0 test/{lld => finalize}/em_asm64.wat | 0 test/{lld => finalize}/em_asm64.wat.out | 0 test/{lld => finalize}/em_asm_O0.c | 0 test/{lld => finalize}/em_asm_O0.wat | 0 test/{lld => finalize}/em_asm_O0.wat.out | 0 test/{lld => finalize}/em_asm_main_thread.wat | 0 .../em_asm_main_thread.wat.out | 0 test/{lld => finalize}/em_asm_pthread.cpp | 0 test/{lld => finalize}/em_asm_pthread.wat | 0 test/{lld => finalize}/em_asm_pthread.wat.out | 0 test/{lld => finalize}/em_asm_shared.cpp | 0 test/{lld => finalize}/em_asm_shared.wat | 0 test/{lld => finalize}/em_asm_shared.wat.out | 0 test/{lld => finalize}/em_asm_table.wat | 0 test/{lld => finalize}/em_asm_table.wat.out | 0 test/{lld => finalize}/em_js_O0.wat | 0 test/{lld => finalize}/em_js_O0.wat.out | 0 test/{lld => finalize}/hello_world.c | 0 test/{lld => finalize}/hello_world.passive.wat | 0 .../hello_world.passive.wat.out | 0 test/{lld => finalize}/hello_world.wat | 0 test/{lld => finalize}/hello_world.wat.out | 0 test/{lld => finalize}/init.c | 0 test/{lld => finalize}/init.wat | 0 test/{lld => finalize}/init.wat.out | 0 test/{lld => finalize}/longjmp.c | 0 test/{lld => finalize}/longjmp.wat | 0 test/{lld => finalize}/longjmp.wat.out | 0 test/{lld => finalize}/main_module.wat | 0 test/{lld => finalize}/main_module.wat.out | 0 test/{lld => finalize}/main_module_table.wat | 0 .../{lld => finalize}/main_module_table.wat.out | 0 test/{lld => finalize}/main_module_table_2.wat | 0 .../main_module_table_2.wat.out | 0 test/{lld => finalize}/main_module_table_3.wat | 0 .../main_module_table_3.wat.out | 0 test/{lld => finalize}/main_module_table_4.wat | 0 .../main_module_table_4.wat.out | 0 test/{lld => finalize}/main_module_table_5.wat | 0 .../main_module_table_5.wat.out | 0 test/{lld => finalize}/recursive.c | 0 test/{lld => finalize}/recursive.wat | 0 test/{lld => finalize}/recursive.wat.out | 0 test/{lld => finalize}/recursive_safe_stack.wat | 0 .../recursive_safe_stack.wat.out | 0 test/{lld => finalize}/reserved_func_ptr.cpp | 0 test/{lld => finalize}/reserved_func_ptr.wat | 0 .../{lld => finalize}/reserved_func_ptr.wat.out | 0 .../safe_stack_standalone-wasm.wat | 0 .../safe_stack_standalone-wasm.wat.out | 0 test/{lld => finalize}/shared.cpp | 0 test/{lld => finalize}/shared.wat | 0 test/{lld => finalize}/shared.wat.out | 0 test/{lld => finalize}/shared_add_to_table.wasm | Bin .../shared_add_to_table.wasm.out | 0 test/{lld => finalize}/shared_longjmp.c | 0 test/{lld => finalize}/shared_longjmp.wat | 0 test/{lld => finalize}/shared_longjmp.wat.out | 0 .../standalone-wasm-with-start.wat | 0 .../standalone-wasm-with-start.wat.out | 0 test/{lld => finalize}/standalone-wasm.wat | 0 test/{lld => finalize}/standalone-wasm.wat.out | 0 test/{lld => finalize}/standalone-wasm2.wat | 0 test/{lld => finalize}/standalone-wasm2.wat.out | 0 test/{lld => finalize}/standalone-wasm3.wat | 0 test/{lld => finalize}/standalone-wasm3.wat.out | 0 79 files changed, 15 insertions(+), 15 deletions(-) rename scripts/test/{lld.py => finalize.py} (91%) rename scripts/test/{generate_lld_tests.py => generate_finalize_tests.py} (86%) rename test/{lld => finalize}/basic_safe_stack.s (100%) rename test/{lld => finalize}/basic_safe_stack.wat (100%) rename test/{lld => finalize}/basic_safe_stack.wat.out (100%) rename test/{lld => finalize}/duplicate_imports.wat (100%) rename test/{lld => finalize}/duplicate_imports.wat.out (100%) rename test/{lld => finalize}/em_asm.cpp (100%) rename test/{lld => finalize}/em_asm.wat (100%) rename test/{lld => finalize}/em_asm.wat.out (100%) rename test/{lld => finalize}/em_asm64.cpp (100%) rename test/{lld => finalize}/em_asm64.wat (100%) rename test/{lld => finalize}/em_asm64.wat.out (100%) rename test/{lld => finalize}/em_asm_O0.c (100%) rename test/{lld => finalize}/em_asm_O0.wat (100%) rename test/{lld => finalize}/em_asm_O0.wat.out (100%) rename test/{lld => finalize}/em_asm_main_thread.wat (100%) rename test/{lld => finalize}/em_asm_main_thread.wat.out (100%) rename test/{lld => finalize}/em_asm_pthread.cpp (100%) rename test/{lld => finalize}/em_asm_pthread.wat (100%) rename test/{lld => finalize}/em_asm_pthread.wat.out (100%) rename test/{lld => finalize}/em_asm_shared.cpp (100%) rename test/{lld => finalize}/em_asm_shared.wat (100%) rename test/{lld => finalize}/em_asm_shared.wat.out (100%) rename test/{lld => finalize}/em_asm_table.wat (100%) rename test/{lld => finalize}/em_asm_table.wat.out (100%) rename test/{lld => finalize}/em_js_O0.wat (100%) rename test/{lld => finalize}/em_js_O0.wat.out (100%) rename test/{lld => finalize}/hello_world.c (100%) rename test/{lld => finalize}/hello_world.passive.wat (100%) rename test/{lld => finalize}/hello_world.passive.wat.out (100%) rename test/{lld => finalize}/hello_world.wat (100%) rename test/{lld => finalize}/hello_world.wat.out (100%) rename test/{lld => finalize}/init.c (100%) rename test/{lld => finalize}/init.wat (100%) rename test/{lld => finalize}/init.wat.out (100%) rename test/{lld => finalize}/longjmp.c (100%) rename test/{lld => finalize}/longjmp.wat (100%) rename test/{lld => finalize}/longjmp.wat.out (100%) rename test/{lld => finalize}/main_module.wat (100%) rename test/{lld => finalize}/main_module.wat.out (100%) rename test/{lld => finalize}/main_module_table.wat (100%) rename test/{lld => finalize}/main_module_table.wat.out (100%) rename test/{lld => finalize}/main_module_table_2.wat (100%) rename test/{lld => finalize}/main_module_table_2.wat.out (100%) rename test/{lld => finalize}/main_module_table_3.wat (100%) rename test/{lld => finalize}/main_module_table_3.wat.out (100%) rename test/{lld => finalize}/main_module_table_4.wat (100%) rename test/{lld => finalize}/main_module_table_4.wat.out (100%) rename test/{lld => finalize}/main_module_table_5.wat (100%) rename test/{lld => finalize}/main_module_table_5.wat.out (100%) rename test/{lld => finalize}/recursive.c (100%) rename test/{lld => finalize}/recursive.wat (100%) rename test/{lld => finalize}/recursive.wat.out (100%) rename test/{lld => finalize}/recursive_safe_stack.wat (100%) rename test/{lld => finalize}/recursive_safe_stack.wat.out (100%) rename test/{lld => finalize}/reserved_func_ptr.cpp (100%) rename test/{lld => finalize}/reserved_func_ptr.wat (100%) rename test/{lld => finalize}/reserved_func_ptr.wat.out (100%) rename test/{lld => finalize}/safe_stack_standalone-wasm.wat (100%) rename test/{lld => finalize}/safe_stack_standalone-wasm.wat.out (100%) rename test/{lld => finalize}/shared.cpp (100%) rename test/{lld => finalize}/shared.wat (100%) rename test/{lld => finalize}/shared.wat.out (100%) rename test/{lld => finalize}/shared_add_to_table.wasm (100%) rename test/{lld => finalize}/shared_add_to_table.wasm.out (100%) rename test/{lld => finalize}/shared_longjmp.c (100%) rename test/{lld => finalize}/shared_longjmp.wat (100%) rename test/{lld => finalize}/shared_longjmp.wat.out (100%) rename test/{lld => finalize}/standalone-wasm-with-start.wat (100%) rename test/{lld => finalize}/standalone-wasm-with-start.wat.out (100%) rename test/{lld => finalize}/standalone-wasm.wat (100%) rename test/{lld => finalize}/standalone-wasm.wat.out (100%) rename test/{lld => finalize}/standalone-wasm2.wat (100%) rename test/{lld => finalize}/standalone-wasm2.wat.out (100%) rename test/{lld => finalize}/standalone-wasm3.wat (100%) rename test/{lld => finalize}/standalone-wasm3.wat.out (100%) diff --git a/check.py b/check.py index 0618c59c5e6..2e77eaaf36f 100755 --- a/check.py +++ b/check.py @@ -25,7 +25,7 @@ from multiprocessing.pool import ThreadPool from pathlib import Path -from scripts.test import binaryenjs, lld, shared, support, wasm2js, wasm_opt +from scripts.test import binaryenjs, finalize, shared, support, wasm2js, wasm_opt assert sys.version_info >= (3, 10), 'requires Python 3.10' @@ -436,7 +436,7 @@ def wrapper(*args, **kwargs): 'wasm-metadce': run_wasm_metadce_tests, 'wasm-reduce': run_wasm_reduce_tests, 'spec': run_spec_tests, - 'lld': lld.test_wasm_emscripten_finalize, + 'finalize': finalize.test_wasm_emscripten_finalize, 'wasm2js': wasm2js.test_wasm2js, 'validator': run_validator_tests, 'example': run_example_tests, diff --git a/scripts/auto_update_tests.py b/scripts/auto_update_tests.py index f6ce43cd41f..0c7ae5cb034 100755 --- a/scripts/auto_update_tests.py +++ b/scripts/auto_update_tests.py @@ -18,7 +18,7 @@ import subprocess import sys -from test import binaryenjs, lld, shared, support, wasm2js, wasm_opt +from test import binaryenjs, finalize, shared, support, wasm2js, wasm_opt def update_example_tests(): @@ -166,7 +166,7 @@ def update_lit_tests(): 'wasm-metadce': update_metadce_tests, 'wasm-reduce': update_reduce_tests, 'spec': update_spec_tests, - 'lld': lld.update_lld_tests, + 'finalize': finalize.update_finalize_tests, 'wasm2js': wasm2js.update_wasm2js_tests, 'binaryenjs': binaryenjs.update_binaryen_js_tests, 'lit': update_lit_tests, diff --git a/scripts/test/lld.py b/scripts/test/finalize.py similarity index 91% rename from scripts/test/lld.py rename to scripts/test/finalize.py index 6f4b2be803b..c96bde73e5c 100644 --- a/scripts/test/lld.py +++ b/scripts/test/finalize.py @@ -48,14 +48,14 @@ def run_test(input_path): def test_wasm_emscripten_finalize(): print('\n[ checking wasm-emscripten-finalize testcases... ]\n') - for input_path in shared.get_tests(shared.get_test_dir('lld'), ['.wat', '.wasm']): + for input_path in shared.get_tests(shared.get_test_dir('finalize'), ['.wat', '.wasm']): run_test(input_path) -def update_lld_tests(): +def update_finalize_tests(): print('\n[ updating wasm-emscripten-finalize testcases... ]\n') - for input_path in shared.get_tests(shared.get_test_dir('lld'), ['.wat', '.wasm']): + for input_path in shared.get_tests(shared.get_test_dir('finalize'), ['.wat', '.wasm']): print('..', input_path) extension_arg_map = { '.out': [], diff --git a/scripts/test/generate_lld_tests.py b/scripts/test/generate_finalize_tests.py similarity index 86% rename from scripts/test/generate_lld_tests.py rename to scripts/test/generate_finalize_tests.py index 237b9b9cb72..aebe14382ed 100755 --- a/scripts/test/generate_lld_tests.py +++ b/scripts/test/generate_finalize_tests.py @@ -33,20 +33,20 @@ def files_with_extensions(path, extensions): def generate_wat_files(llvm_bin, emscripten_sysroot): print('\n[ building wat files from C sources... ]\n') - lld_path = os.path.join(shared.options.binaryen_test, 'lld') - for src_file, ext in files_with_extensions(lld_path, ['.c', '.cpp', '.s']): + test_path = os.path.join(shared.options.binaryen_test, 'finalize') + for src_file, ext in files_with_extensions(test_path, ['.c', '.cpp', '.s']): print('..', src_file) obj_file = src_file.replace(ext, '.o') - src_path = os.path.join(lld_path, src_file) - obj_path = os.path.join(lld_path, obj_file) + src_path = os.path.join(test_path, src_file) + obj_path = os.path.join(test_path, obj_file) wasm_file = src_file.replace(ext, '.wasm') wat_file = src_file.replace(ext, '.wat') - obj_path = os.path.join(lld_path, obj_file) - wasm_path = os.path.join(lld_path, wasm_file) - wat_path = os.path.join(lld_path, wat_file) + obj_path = os.path.join(test_path, obj_file) + wasm_path = os.path.join(test_path, wasm_file) + wat_path = os.path.join(test_path, wat_file) is_shared = 'shared' in src_file is_64 = '64' in src_file @@ -104,6 +104,6 @@ def generate_wat_files(llvm_bin, emscripten_sysroot): if __name__ == '__main__': if len(shared.options.positional_args) != 2: - print('Usage: generate_lld_tests.py [llvm/bin/dir] [path/to/emscripten]') + print('Usage: generate_finalize_tests.py [llvm/bin/dir] [path/to/emscripten]') sys.exit(1) generate_wat_files(*shared.options.positional_args) diff --git a/test/lld/basic_safe_stack.s b/test/finalize/basic_safe_stack.s similarity index 100% rename from test/lld/basic_safe_stack.s rename to test/finalize/basic_safe_stack.s diff --git a/test/lld/basic_safe_stack.wat b/test/finalize/basic_safe_stack.wat similarity index 100% rename from test/lld/basic_safe_stack.wat rename to test/finalize/basic_safe_stack.wat diff --git a/test/lld/basic_safe_stack.wat.out b/test/finalize/basic_safe_stack.wat.out similarity index 100% rename from test/lld/basic_safe_stack.wat.out rename to test/finalize/basic_safe_stack.wat.out diff --git a/test/lld/duplicate_imports.wat b/test/finalize/duplicate_imports.wat similarity index 100% rename from test/lld/duplicate_imports.wat rename to test/finalize/duplicate_imports.wat diff --git a/test/lld/duplicate_imports.wat.out b/test/finalize/duplicate_imports.wat.out similarity index 100% rename from test/lld/duplicate_imports.wat.out rename to test/finalize/duplicate_imports.wat.out diff --git a/test/lld/em_asm.cpp b/test/finalize/em_asm.cpp similarity index 100% rename from test/lld/em_asm.cpp rename to test/finalize/em_asm.cpp diff --git a/test/lld/em_asm.wat b/test/finalize/em_asm.wat similarity index 100% rename from test/lld/em_asm.wat rename to test/finalize/em_asm.wat diff --git a/test/lld/em_asm.wat.out b/test/finalize/em_asm.wat.out similarity index 100% rename from test/lld/em_asm.wat.out rename to test/finalize/em_asm.wat.out diff --git a/test/lld/em_asm64.cpp b/test/finalize/em_asm64.cpp similarity index 100% rename from test/lld/em_asm64.cpp rename to test/finalize/em_asm64.cpp diff --git a/test/lld/em_asm64.wat b/test/finalize/em_asm64.wat similarity index 100% rename from test/lld/em_asm64.wat rename to test/finalize/em_asm64.wat diff --git a/test/lld/em_asm64.wat.out b/test/finalize/em_asm64.wat.out similarity index 100% rename from test/lld/em_asm64.wat.out rename to test/finalize/em_asm64.wat.out diff --git a/test/lld/em_asm_O0.c b/test/finalize/em_asm_O0.c similarity index 100% rename from test/lld/em_asm_O0.c rename to test/finalize/em_asm_O0.c diff --git a/test/lld/em_asm_O0.wat b/test/finalize/em_asm_O0.wat similarity index 100% rename from test/lld/em_asm_O0.wat rename to test/finalize/em_asm_O0.wat diff --git a/test/lld/em_asm_O0.wat.out b/test/finalize/em_asm_O0.wat.out similarity index 100% rename from test/lld/em_asm_O0.wat.out rename to test/finalize/em_asm_O0.wat.out diff --git a/test/lld/em_asm_main_thread.wat b/test/finalize/em_asm_main_thread.wat similarity index 100% rename from test/lld/em_asm_main_thread.wat rename to test/finalize/em_asm_main_thread.wat diff --git a/test/lld/em_asm_main_thread.wat.out b/test/finalize/em_asm_main_thread.wat.out similarity index 100% rename from test/lld/em_asm_main_thread.wat.out rename to test/finalize/em_asm_main_thread.wat.out diff --git a/test/lld/em_asm_pthread.cpp b/test/finalize/em_asm_pthread.cpp similarity index 100% rename from test/lld/em_asm_pthread.cpp rename to test/finalize/em_asm_pthread.cpp diff --git a/test/lld/em_asm_pthread.wat b/test/finalize/em_asm_pthread.wat similarity index 100% rename from test/lld/em_asm_pthread.wat rename to test/finalize/em_asm_pthread.wat diff --git a/test/lld/em_asm_pthread.wat.out b/test/finalize/em_asm_pthread.wat.out similarity index 100% rename from test/lld/em_asm_pthread.wat.out rename to test/finalize/em_asm_pthread.wat.out diff --git a/test/lld/em_asm_shared.cpp b/test/finalize/em_asm_shared.cpp similarity index 100% rename from test/lld/em_asm_shared.cpp rename to test/finalize/em_asm_shared.cpp diff --git a/test/lld/em_asm_shared.wat b/test/finalize/em_asm_shared.wat similarity index 100% rename from test/lld/em_asm_shared.wat rename to test/finalize/em_asm_shared.wat diff --git a/test/lld/em_asm_shared.wat.out b/test/finalize/em_asm_shared.wat.out similarity index 100% rename from test/lld/em_asm_shared.wat.out rename to test/finalize/em_asm_shared.wat.out diff --git a/test/lld/em_asm_table.wat b/test/finalize/em_asm_table.wat similarity index 100% rename from test/lld/em_asm_table.wat rename to test/finalize/em_asm_table.wat diff --git a/test/lld/em_asm_table.wat.out b/test/finalize/em_asm_table.wat.out similarity index 100% rename from test/lld/em_asm_table.wat.out rename to test/finalize/em_asm_table.wat.out diff --git a/test/lld/em_js_O0.wat b/test/finalize/em_js_O0.wat similarity index 100% rename from test/lld/em_js_O0.wat rename to test/finalize/em_js_O0.wat diff --git a/test/lld/em_js_O0.wat.out b/test/finalize/em_js_O0.wat.out similarity index 100% rename from test/lld/em_js_O0.wat.out rename to test/finalize/em_js_O0.wat.out diff --git a/test/lld/hello_world.c b/test/finalize/hello_world.c similarity index 100% rename from test/lld/hello_world.c rename to test/finalize/hello_world.c diff --git a/test/lld/hello_world.passive.wat b/test/finalize/hello_world.passive.wat similarity index 100% rename from test/lld/hello_world.passive.wat rename to test/finalize/hello_world.passive.wat diff --git a/test/lld/hello_world.passive.wat.out b/test/finalize/hello_world.passive.wat.out similarity index 100% rename from test/lld/hello_world.passive.wat.out rename to test/finalize/hello_world.passive.wat.out diff --git a/test/lld/hello_world.wat b/test/finalize/hello_world.wat similarity index 100% rename from test/lld/hello_world.wat rename to test/finalize/hello_world.wat diff --git a/test/lld/hello_world.wat.out b/test/finalize/hello_world.wat.out similarity index 100% rename from test/lld/hello_world.wat.out rename to test/finalize/hello_world.wat.out diff --git a/test/lld/init.c b/test/finalize/init.c similarity index 100% rename from test/lld/init.c rename to test/finalize/init.c diff --git a/test/lld/init.wat b/test/finalize/init.wat similarity index 100% rename from test/lld/init.wat rename to test/finalize/init.wat diff --git a/test/lld/init.wat.out b/test/finalize/init.wat.out similarity index 100% rename from test/lld/init.wat.out rename to test/finalize/init.wat.out diff --git a/test/lld/longjmp.c b/test/finalize/longjmp.c similarity index 100% rename from test/lld/longjmp.c rename to test/finalize/longjmp.c diff --git a/test/lld/longjmp.wat b/test/finalize/longjmp.wat similarity index 100% rename from test/lld/longjmp.wat rename to test/finalize/longjmp.wat diff --git a/test/lld/longjmp.wat.out b/test/finalize/longjmp.wat.out similarity index 100% rename from test/lld/longjmp.wat.out rename to test/finalize/longjmp.wat.out diff --git a/test/lld/main_module.wat b/test/finalize/main_module.wat similarity index 100% rename from test/lld/main_module.wat rename to test/finalize/main_module.wat diff --git a/test/lld/main_module.wat.out b/test/finalize/main_module.wat.out similarity index 100% rename from test/lld/main_module.wat.out rename to test/finalize/main_module.wat.out diff --git a/test/lld/main_module_table.wat b/test/finalize/main_module_table.wat similarity index 100% rename from test/lld/main_module_table.wat rename to test/finalize/main_module_table.wat diff --git a/test/lld/main_module_table.wat.out b/test/finalize/main_module_table.wat.out similarity index 100% rename from test/lld/main_module_table.wat.out rename to test/finalize/main_module_table.wat.out diff --git a/test/lld/main_module_table_2.wat b/test/finalize/main_module_table_2.wat similarity index 100% rename from test/lld/main_module_table_2.wat rename to test/finalize/main_module_table_2.wat diff --git a/test/lld/main_module_table_2.wat.out b/test/finalize/main_module_table_2.wat.out similarity index 100% rename from test/lld/main_module_table_2.wat.out rename to test/finalize/main_module_table_2.wat.out diff --git a/test/lld/main_module_table_3.wat b/test/finalize/main_module_table_3.wat similarity index 100% rename from test/lld/main_module_table_3.wat rename to test/finalize/main_module_table_3.wat diff --git a/test/lld/main_module_table_3.wat.out b/test/finalize/main_module_table_3.wat.out similarity index 100% rename from test/lld/main_module_table_3.wat.out rename to test/finalize/main_module_table_3.wat.out diff --git a/test/lld/main_module_table_4.wat b/test/finalize/main_module_table_4.wat similarity index 100% rename from test/lld/main_module_table_4.wat rename to test/finalize/main_module_table_4.wat diff --git a/test/lld/main_module_table_4.wat.out b/test/finalize/main_module_table_4.wat.out similarity index 100% rename from test/lld/main_module_table_4.wat.out rename to test/finalize/main_module_table_4.wat.out diff --git a/test/lld/main_module_table_5.wat b/test/finalize/main_module_table_5.wat similarity index 100% rename from test/lld/main_module_table_5.wat rename to test/finalize/main_module_table_5.wat diff --git a/test/lld/main_module_table_5.wat.out b/test/finalize/main_module_table_5.wat.out similarity index 100% rename from test/lld/main_module_table_5.wat.out rename to test/finalize/main_module_table_5.wat.out diff --git a/test/lld/recursive.c b/test/finalize/recursive.c similarity index 100% rename from test/lld/recursive.c rename to test/finalize/recursive.c diff --git a/test/lld/recursive.wat b/test/finalize/recursive.wat similarity index 100% rename from test/lld/recursive.wat rename to test/finalize/recursive.wat diff --git a/test/lld/recursive.wat.out b/test/finalize/recursive.wat.out similarity index 100% rename from test/lld/recursive.wat.out rename to test/finalize/recursive.wat.out diff --git a/test/lld/recursive_safe_stack.wat b/test/finalize/recursive_safe_stack.wat similarity index 100% rename from test/lld/recursive_safe_stack.wat rename to test/finalize/recursive_safe_stack.wat diff --git a/test/lld/recursive_safe_stack.wat.out b/test/finalize/recursive_safe_stack.wat.out similarity index 100% rename from test/lld/recursive_safe_stack.wat.out rename to test/finalize/recursive_safe_stack.wat.out diff --git a/test/lld/reserved_func_ptr.cpp b/test/finalize/reserved_func_ptr.cpp similarity index 100% rename from test/lld/reserved_func_ptr.cpp rename to test/finalize/reserved_func_ptr.cpp diff --git a/test/lld/reserved_func_ptr.wat b/test/finalize/reserved_func_ptr.wat similarity index 100% rename from test/lld/reserved_func_ptr.wat rename to test/finalize/reserved_func_ptr.wat diff --git a/test/lld/reserved_func_ptr.wat.out b/test/finalize/reserved_func_ptr.wat.out similarity index 100% rename from test/lld/reserved_func_ptr.wat.out rename to test/finalize/reserved_func_ptr.wat.out diff --git a/test/lld/safe_stack_standalone-wasm.wat b/test/finalize/safe_stack_standalone-wasm.wat similarity index 100% rename from test/lld/safe_stack_standalone-wasm.wat rename to test/finalize/safe_stack_standalone-wasm.wat diff --git a/test/lld/safe_stack_standalone-wasm.wat.out b/test/finalize/safe_stack_standalone-wasm.wat.out similarity index 100% rename from test/lld/safe_stack_standalone-wasm.wat.out rename to test/finalize/safe_stack_standalone-wasm.wat.out diff --git a/test/lld/shared.cpp b/test/finalize/shared.cpp similarity index 100% rename from test/lld/shared.cpp rename to test/finalize/shared.cpp diff --git a/test/lld/shared.wat b/test/finalize/shared.wat similarity index 100% rename from test/lld/shared.wat rename to test/finalize/shared.wat diff --git a/test/lld/shared.wat.out b/test/finalize/shared.wat.out similarity index 100% rename from test/lld/shared.wat.out rename to test/finalize/shared.wat.out diff --git a/test/lld/shared_add_to_table.wasm b/test/finalize/shared_add_to_table.wasm similarity index 100% rename from test/lld/shared_add_to_table.wasm rename to test/finalize/shared_add_to_table.wasm diff --git a/test/lld/shared_add_to_table.wasm.out b/test/finalize/shared_add_to_table.wasm.out similarity index 100% rename from test/lld/shared_add_to_table.wasm.out rename to test/finalize/shared_add_to_table.wasm.out diff --git a/test/lld/shared_longjmp.c b/test/finalize/shared_longjmp.c similarity index 100% rename from test/lld/shared_longjmp.c rename to test/finalize/shared_longjmp.c diff --git a/test/lld/shared_longjmp.wat b/test/finalize/shared_longjmp.wat similarity index 100% rename from test/lld/shared_longjmp.wat rename to test/finalize/shared_longjmp.wat diff --git a/test/lld/shared_longjmp.wat.out b/test/finalize/shared_longjmp.wat.out similarity index 100% rename from test/lld/shared_longjmp.wat.out rename to test/finalize/shared_longjmp.wat.out diff --git a/test/lld/standalone-wasm-with-start.wat b/test/finalize/standalone-wasm-with-start.wat similarity index 100% rename from test/lld/standalone-wasm-with-start.wat rename to test/finalize/standalone-wasm-with-start.wat diff --git a/test/lld/standalone-wasm-with-start.wat.out b/test/finalize/standalone-wasm-with-start.wat.out similarity index 100% rename from test/lld/standalone-wasm-with-start.wat.out rename to test/finalize/standalone-wasm-with-start.wat.out diff --git a/test/lld/standalone-wasm.wat b/test/finalize/standalone-wasm.wat similarity index 100% rename from test/lld/standalone-wasm.wat rename to test/finalize/standalone-wasm.wat diff --git a/test/lld/standalone-wasm.wat.out b/test/finalize/standalone-wasm.wat.out similarity index 100% rename from test/lld/standalone-wasm.wat.out rename to test/finalize/standalone-wasm.wat.out diff --git a/test/lld/standalone-wasm2.wat b/test/finalize/standalone-wasm2.wat similarity index 100% rename from test/lld/standalone-wasm2.wat rename to test/finalize/standalone-wasm2.wat diff --git a/test/lld/standalone-wasm2.wat.out b/test/finalize/standalone-wasm2.wat.out similarity index 100% rename from test/lld/standalone-wasm2.wat.out rename to test/finalize/standalone-wasm2.wat.out diff --git a/test/lld/standalone-wasm3.wat b/test/finalize/standalone-wasm3.wat similarity index 100% rename from test/lld/standalone-wasm3.wat rename to test/finalize/standalone-wasm3.wat diff --git a/test/lld/standalone-wasm3.wat.out b/test/finalize/standalone-wasm3.wat.out similarity index 100% rename from test/lld/standalone-wasm3.wat.out rename to test/finalize/standalone-wasm3.wat.out From df8b79d629890d1af88027455d379329a6f533fb Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Mon, 27 Apr 2026 15:01:05 -0700 Subject: [PATCH 059/168] [legalize-js-interface] Don't delete exports (#8649) Instead we can just rely on emscripten's metadce to remove these like all other unused exports. See https://github.com/emscripten-core/emscripten/pull/26793 Also, note that we did not have any tests for this removal anyway. --- src/passes/LegalizeJSInterface.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/passes/LegalizeJSInterface.cpp b/src/passes/LegalizeJSInterface.cpp index 7b6a4c44cdd..ecf1adbc3be 100644 --- a/src/passes/LegalizeJSInterface.cpp +++ b/src/passes/LegalizeJSInterface.cpp @@ -161,9 +161,6 @@ struct LegalizeJSInterface : public Pass { module->removeFunction(pair.first); } } - - module->removeExport(GET_TEMP_RET_EXPORT); - module->removeExport(SET_TEMP_RET_EXPORT); } private: From af1dd5ba3bbcae4b4879257c16ee89a12ef843e3 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 28 Apr 2026 10:20:56 -0700 Subject: [PATCH 060/168] PreserveImportsExportsJS fuzzer: Handle crashes and function ids (#8648) 1. Report VM errors as failures (rather than see they still occur after opts). 2. Ignore function ID differences in logging. --- scripts/fuzz_opt.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 69d51259e92..e74717fba20 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2219,6 +2219,11 @@ def do_handle_pair(self, input, before_wasm, after_wasm, opts): def do_run(self, vm, js, wasm): out = vm.run_js(js, wasm, checked=False) + # VM crashes are actual issues we want to find. + if '(core dumped)' in out or 'Received signal' in out or '== C stack trace ==' in out or '== JS stack trace ==' in out: + raise Exception(f"VM crash:\n\n{out}") + + # Clean up stack traces. cleaned = [] for line in out.splitlines(): if 'RuntimeError:' in line or 'TypeError:' in line: @@ -2240,7 +2245,14 @@ def do_run(self, vm, js, wasm): # Ignore it, as details of traces differ based on optimizations. continue cleaned.append(line) - return '\n'.join(cleaned) + cleaned = '\n'.join(cleaned) + + # Clean up function references, which can differ after opts, things like + # + # function 77() { [native code] } + # + cleaned = re.sub(r'function \d+\(\) ', 'function () ', cleaned) + return cleaned def can_run_on_wasm(self, wasm): return all_disallowed(DISALLOWED_FEATURES_IN_V8) From d951c04a830ba9eac59542b67c7e7b0df865d30a Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Tue, 28 Apr 2026 13:25:09 -0700 Subject: [PATCH 061/168] Enable more ruff checks and remove flake8. NFC (#8654) --- .flake8 | 9 ---- .github/workflows/ci.yml | 1 - .ruff.toml | 21 +++++++++ check.py | 4 +- requirements-dev.txt | 3 +- scripts/bundle_clusterfuzz.py | 5 +- scripts/clusterfuzz/embed_wasms.py | 13 +++--- scripts/clusterfuzz/extract_wasms.py | 8 ++-- scripts/clusterfuzz/run.py | 9 ++-- scripts/fuzz_opt.py | 57 ++++++++++++++++++++--- scripts/fuzz_passes.py | 7 +-- scripts/fuzz_passes_wast.py | 10 ++-- scripts/fuzz_relooper.py | 4 +- scripts/gen-s-parser.py | 5 +- scripts/port_passes_tests_to_lit.py | 3 +- scripts/strip_local_names.py | 6 ++- scripts/test/gen-cast-test.py | 4 +- scripts/test/generate_atomic_spec_test.py | 45 ++++++++++-------- scripts/test/shared.py | 16 +++---- scripts/test/support.py | 11 +++-- scripts/update_help_checks.py | 3 +- scripts/update_lit_checks.py | 4 +- test/unit/test_web_limitations.py | 8 +--- 23 files changed, 156 insertions(+), 100 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index e315e3ff1a2..00000000000 --- a/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -ignore = - ; line too long - E501, - ; space after comma (ignored for list in gen-s-parser.py) - E241, - ; line break after binary operator - W504 -exclude = third_party,./test/emscripten,./test/spec,./test/wasm-install,./test/lit,./_deps,./build,./emcc-build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75ddc691741..209434d4d8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,6 @@ jobs: sudo chmod +x llvm.sh sudo ./llvm.sh ${LLVM_VERSION} sudo apt-get install clang-format clang-format-${LLVM_VERSION} clang-tidy-${LLVM_VERSION} - - run: flake8 - run: ruff check - run: ./scripts/clang-format-diff.sh - name: clang-tidy diff --git a/.ruff.toml b/.ruff.toml index e6fd850fd43..e48daa90671 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,4 +1,5 @@ target-version = "py310" +preview = true exclude = [ 'third_party', @@ -6,11 +7,19 @@ exclude = [ 'test/spec/testsuite', ] +[lint.pylint] +max-locals = 30 +max-positional-args = 7 + +[lint.per-file-ignores] +"test/**.py" = ["PLR6301", "PLW1514", "PLR0914"] + [lint] select = [ "ARG", "ASYNC", "B", + "D", "C4", "C90", "COM", @@ -27,17 +36,29 @@ select = [ ignore = [ "C901", # https://docs.astral.sh/ruff/rules/complex-structure/ + "D100", # https://docs.astral.sh/ruff/rules/undocumented-public-module/ + "D101", # https://docs.astral.sh/ruff/rules/undocumented-public-class/ + "D102", # https://docs.astral.sh/ruff/rules/undocumented-public-method/ + "D103", # https://docs.astral.sh/ruff/rules/undocumented-public-function/ + "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ + "D105", # https://docs.astral.sh/ruff/rules/undocumented-magic-method/ + "D203", # https://docs.astral.sh/ruff/rules/incorrect-blank-line-before-class/ + "D213", # https://docs.astral.sh/ruff/rules/multi-line-summary-second-line/ + "D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/ "B006", # https://docs.astral.sh/ruff/rules/mutable-argument-default/ "B011", # https://docs.astral.sh/ruff/rules/assert-false/ "B023", # https://docs.astral.sh/ruff/rules/function-uses-loop-variable/ "E501", # https://docs.astral.sh/ruff/rules/line-too-long/ "E741", # https://docs.astral.sh/ruff/rules/ambiguous-variable-name/ "PERF401", # https://docs.astral.sh/ruff/rules/manual-list-comprehension/ + "PLR0904", # https://docs.astral.sh/ruff/rules/too-many-public-methods/ "PLR0912", # https://docs.astral.sh/ruff/rules/too-many-branches/ "PLR0913", # https://docs.astral.sh/ruff/rules/too-many-arguments/ "PLR0915", # https://docs.astral.sh/ruff/rules/too-many-statements/ + "PLR1702", # https://docs.astral.sh/ruff/rules/too-many-nested-blocks/ "PLR2004", # https://docs.astral.sh/ruff/rules/magic-value-comparison/ "PLW0603", # https://docs.astral.sh/ruff/rules/global-statement/ "PLW1510", # https://docs.astral.sh/ruff/rules/subprocess-run-without-check/ "PLW2901", # https://docs.astral.sh/ruff/rules/redefined-loop-name/ + "PLW1514", # https://docs.astral.sh/ruff/rules/unspecified-encoding/ ] diff --git a/check.py b/check.py index 2e77eaaf36f..a670bb96001 100755 --- a/check.py +++ b/check.py @@ -265,7 +265,9 @@ def run_one_spec_test(wast: Path, stdout=None): def run_spec_test_with_wrapped_stdout(wast: Path): - """Return (bool, str) where the first element is whether the test was + """Run a single spec test while capturing stdout. + + Return (bool, str) where the first element is whether the test was successful and the second is the combined stdout and stderr of the test. """ out = io.StringIO() diff --git a/requirements-dev.txt b/requirements-dev.txt index 48eeb74ef5c..d64712fe2e8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,9 +1,8 @@ # These requirements are only needed for developers who want to run the test -# suite or flake8, not for end users. +# suite, or CI checks, not for end users. # Install with `pip3 install -r requirements-dev.txt` -flake8==7.3.0 ruff==0.14.1 filecheck==0.0.22 lit==0.11.0.post1 diff --git a/scripts/bundle_clusterfuzz.py b/scripts/bundle_clusterfuzz.py index 60aebd78b7b..70b6ee62ac6 100755 --- a/scripts/bundle_clusterfuzz.py +++ b/scripts/bundle_clusterfuzz.py @@ -1,7 +1,6 @@ #!/usr/bin/python3 -''' -Bundle files for uploading to ClusterFuzz. +"""Bundle files for uploading to ClusterFuzz. Usage: @@ -68,7 +67,7 @@ 3. Check the stats and crashes page (known crashes should at least be showing up). Note that these may take longer to show up than 1 and 2. -''' +""" import glob import os diff --git a/scripts/clusterfuzz/embed_wasms.py b/scripts/clusterfuzz/embed_wasms.py index 84ce6805370..17b4bcaf06d 100644 --- a/scripts/clusterfuzz/embed_wasms.py +++ b/scripts/clusterfuzz/embed_wasms.py @@ -13,11 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' -Reverse script for extract_wasms.py: That one extracts wasm files from a -JavaScript testcase (which has wasm files embedded as arrays of numbers), and -this one re-embeds them back. To do so, we use the magic comments that the -extractor uses: it replaces each wasm array with +"""Reverse of extract_wasms.py. + +extract_wasms.py extracts wasm files from a JavaScript testcase (which has wasm +files embedded as arrays of numbers), and this script re-embeds them back. To +do so, we use the magic comments that the extractor uses: it replaces each +wasm array with 'undefined /* extracted wasm */' @@ -39,7 +40,7 @@ The first argument is the input JS, then the wasm files, then the last argument is the output JS. -''' +""" import re import sys diff --git a/scripts/clusterfuzz/extract_wasms.py b/scripts/clusterfuzz/extract_wasms.py index c1cc429eeb6..2833305b92d 100644 --- a/scripts/clusterfuzz/extract_wasms.py +++ b/scripts/clusterfuzz/extract_wasms.py @@ -13,9 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' -Wasm extractor for testcases generated by the ClusterFuzz run.py script. This is -general enough to also handle Fuzzilli output. +"""Wasm extractor for testcases generated by the ClusterFuzz run.py script. + +This is general enough to also handle Fuzzilli output. Usage: @@ -32,7 +32,7 @@ d8 OUTFILE.js -- OUTFILE.0.wasm That is, the embedded file can now be provided as a filename argument. -''' +""" import re import sys diff --git a/scripts/clusterfuzz/run.py b/scripts/clusterfuzz/run.py index 811c4be85e2..42c7f44fbdd 100755 --- a/scripts/clusterfuzz/run.py +++ b/scripts/clusterfuzz/run.py @@ -13,15 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' -ClusterFuzz run.py script: when run by ClusterFuzz, it uses wasm-opt to generate -a fixed number of testcases. This is a "blackbox fuzzer", see +"""ClusterFuzz run.py script. + +When run by ClusterFuzz, it uses wasm-opt to generate a fixed number of +testcases. This is a "blackbox fuzzer", see https://google.github.io/clusterfuzz/setting-up-fuzzing/blackbox-fuzzing/ This file should be bundled up together with the other files it needs, see bundle_clusterfuzz.py. -''' +""" import getopt import math diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index e74717fba20..5da54d244f9 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 -"""Run various fuzzing operations on random inputs, using wasm-opt. See -"testcase_handlers" below for the list of fuzzing operations. +"""Run various fuzzing operations on random inputs, using wasm-opt. + +See "testcase_handlers" below for the list of fuzzing operations. Usage: @@ -42,6 +43,7 @@ import traceback from datetime import datetime, timedelta, timezone from os.path import abspath +from typing import override from test import fuzzing, shared, support @@ -459,7 +461,7 @@ def get_export_from_export_line(export_line): # compare two strings, strictly def compare(x, y, context, verbose=True): - if x != y and IGNORE not in (x, y): + if x != y and IGNORE not in {x, y}: message = ''.join([a + '\n' for a in difflib.unified_diff(x.splitlines(), y.splitlines(), fromfile='expected', tofile='actual')]) if verbose: raise Exception(f"{context} comparison error, expected to have '{x}' == '{y}', diff:\n\n{message}") @@ -769,12 +771,14 @@ def __init__(self): # If the core handle_pair() method is not overridden, it calls handle() on # each of the items. That is useful if you just want the two wasms and don't # care about their relationship. + @override def handle_pair(self, input, before_wasm, after_wasm, opts): self.handle(before_wasm) # Add some visual space between the independent parts. print('\n') self.handle(after_wasm) + @override def can_run_on_wasm(self, wasm): return True @@ -789,6 +793,7 @@ def count_runs(self): class FuzzExec(TestCaseHandler): frequency = 1 + @override def handle_pair(self, input, before_wasm, after_wasm, opts): run([in_bin('wasm-opt'), before_wasm] + opts + ['--fuzz-exec']) @@ -798,6 +803,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): class BinaryenInterpreter: name = 'binaryen interpreter' + @override def run(self, wasm): output = run_bynterp(wasm, ['--fuzz-exec-before']) if output != IGNORE: @@ -824,12 +830,15 @@ def run(self, wasm): amount=0.5) return output + @override def can_run(self, wasm): return True + @override def can_compare_to_self(self): return True + @override def can_compare_to_other(self, other): return True @@ -842,17 +851,21 @@ class D8: def run_js(self, js, wasm, checked=True): return run_vm([shared.V8, js] + shared.V8_OPTS + get_v8_extra_flags() + self.extra_d8_flags + ['--', wasm], checked=checked) + @override def run(self, wasm): return self.run_js(js=get_fuzz_shell_js(), wasm=wasm) + @override def can_run(self, wasm): return all_disallowed(DISALLOWED_FEATURES_IN_V8) + @override def can_compare_to_self(self): # With nans, VM differences can confuse us, so only very simple VMs # can compare to themselves after opts in that case. return not NANS + @override def can_compare_to_other(self, other): # Relaxed SIMD allows different behavior between VMs, so only # allow comparisons to other d8 variants if it is enabled. @@ -892,6 +905,7 @@ def __init__(self): print('warning: no wabt found:', e) self.wasm2c_dir = None + @override def can_run(self, wasm): if self.wasm2c_dir is None: return False @@ -904,6 +918,7 @@ def can_run(self, wasm): # wasm2c doesn't support most features return all_disallowed(['exception-handling', 'simd', 'threads', 'bulk-memory', 'nontrapping-float-to-int', 'tail-call', 'sign-ext', 'reference-types', 'multivalue', 'gc', 'custom-descriptors', 'relaxed-atomics']) + @override def run(self, wasm): run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) run(['wasm2c', wasm, '-o', 'wasm.c']) @@ -911,11 +926,13 @@ def run(self, wasm): run(compile_cmd) return run_vm(['./a.out']) + @override def can_compare_to_self(self): # The binaryen optimizer changes NaNs in the ways that wasm # expects, but that's not quite what C has return not NANS + @override def can_compare_to_other(self, other): # C won't trap on OOB, and NaNs can differ from wasm VMs return not OOB and not NANS @@ -929,6 +946,7 @@ def __init__(self): self.has_emcc = shared.which('emcc') is not None + @override def run(self, wasm): run([in_bin('wasm-opt'), wasm, '--emit-wasm2c-wrapper=main.c'] + FEATURE_OPTS) run(['wasm2c', wasm, '-o', 'wasm.c']) @@ -955,6 +973,7 @@ def run(self, wasm): run(compile_cmd) return run_d8_js(abspath('a.out.js')) + @override def can_run(self, wasm): # quite slow (more steps), so run it less frequently if random.random() < 0.8: @@ -964,6 +983,7 @@ def can_run(self, wasm): return super().can_run(wasm) and self.has_emcc and \ os.path.getsize(wasm) <= INPUT_SIZE_MEAN + @override def can_compare_to_other(self, other): # NaNs can differ from wasm VMs return not NANS @@ -987,6 +1007,7 @@ def __init__(self): # Wasm2C2Wasm() ] + @override def handle_pair(self, input, before_wasm, after_wasm, opts): before = self.run_vms(before_wasm) @@ -1033,7 +1054,8 @@ def run_vms(self, wasm): return vm_results - def compare_before_and_after(self, before, after): + @staticmethod + def compare_before_and_after(before, after): # compare each VM to itself on the before and after inputs for vm in before.keys(): if vm in after and vm.can_compare_to_self(): @@ -1044,6 +1066,7 @@ def compare_before_and_after(self, before, after): class CheckDeterminism(TestCaseHandler): frequency = 0.2 + @override def handle_pair(self, input, before_wasm, after_wasm, opts): # check for determinism run([in_bin('wasm-opt'), before_wasm, '-o', abspath('b1.wasm')] + opts) @@ -1061,6 +1084,7 @@ def handle_pair(self, input, before_wasm, after_wasm, opts): class Wasm2JS(TestCaseHandler): frequency = 0.1 + @override def handle_pair(self, input, before_wasm, after_wasm, opts): before_wasm_temp = before_wasm + '.temp.wasm' after_wasm_temp = after_wasm + '.temp.wasm' @@ -1173,6 +1197,7 @@ def fix_number(x): interpreter = fix_output_for_js(interpreter) compare_between_vms(before, interpreter, 'Wasm2JS (vs interpreter)') + @override def run(self, wasm): with open(get_fuzz_shell_js()) as f: wrapper = f.read() @@ -1199,6 +1224,7 @@ def run(self, wasm): f.write(wrapper) return run_vm([shared.NODEJS, js_file, abspath('a.wasm')]) + @override def can_run_on_wasm(self, wasm): # TODO: properly handle memory growth. right now the wasm2js handler # uses --emscripten which assumes the Memory is created before, and @@ -1299,6 +1325,7 @@ def wasm_notices_export_changes(wasm): class TrapsNeverHappen(TestCaseHandler): frequency = 0.25 + @override def handle_pair(self, input, before_wasm, after_wasm, opts): before = run_bynterp(before_wasm, ['--fuzz-exec-before']) @@ -1385,6 +1412,7 @@ def ignore_references(out): compare_between_vms(before, after, 'TrapsNeverHappen') + @override def can_run_on_wasm(self, wasm): # If the wasm is sensitive to changes in exports then we cannot alter # them, but we must remove trapping exports (see above), so we cannot @@ -1396,6 +1424,7 @@ def can_run_on_wasm(self, wasm): class CtorEval(TestCaseHandler): frequency = 0.1 + @override def handle(self, wasm): # Get the list of func exports, so we can tell ctor-eval what to eval. func_exports = get_exports(wasm, ['func']) @@ -1436,6 +1465,7 @@ def handle(self, wasm): compare_between_vms(fix_output(wasm_exec), fix_output(evalled_wasm_exec), 'CtorEval') + @override def can_run_on_wasm(self, wasm): # ctor-eval modifies exports, because it assumes they are ctors and so # are only called once (so if it evals them away, they can be @@ -1473,6 +1503,7 @@ def traps_in_instantiation(output): class Merge(TestCaseHandler): frequency = 0.15 + @override def handle(self, wasm): # generate a second wasm file to merge. note that we intentionally pick # a smaller size than the main wasm file, so that reduction is @@ -1569,6 +1600,7 @@ def handle(self, wasm): compare_between_vms(output, merged_output, 'Merge') + @override def can_run_on_wasm(self, wasm): # wasm-merge combines exports, which can alter their indexes and lead to # noticeable differences if the wasm is sensitive to such things, which @@ -1585,6 +1617,7 @@ def can_run_on_wasm(self, wasm): class Split(TestCaseHandler): frequency = 0.1 + @override def handle(self, wasm): # get the list of function names, some of which we will decide to split # out @@ -1680,6 +1713,7 @@ def optimize(name): if not (NANS and optimized): compare_between_vms(output, linked_output, 'Split') + @override def can_run_on_wasm(self, wasm): # to run the split wasm we use JS, that is, JS links the exports of one # to the imports of the other, etc. since we run in JS, the wasm must be @@ -1695,6 +1729,7 @@ def can_run_on_wasm(self, wasm): class RoundtripText(TestCaseHandler): frequency = 0.05 + @override def handle(self, wasm): # use name-types because in wasm GC we can end up truncating the default # names which are very long, causing names to collide and the wast to be @@ -1724,6 +1759,7 @@ class ClusterFuzz(TestCaseHandler): # we generate our own using run.py. If we used handle, we'd be called twice # for each iteration (once for each of the wasm files we ignore), which is # confusing. + @override def handle_pair(self, input, before_wasm, after_wasm, opts): # Do not run ClusterFuzz in the first seconds of fuzzing: the first time # it runs is very slow (to build the bundle), which is annoying when you @@ -1862,6 +1898,7 @@ class Two(TestCaseHandler): # module interactions. frequency = 1 # TODO: We may want even higher priority here + @override def handle(self, wasm): # Generate a second wasm file. (For fuzzing, we may be given one, but we # still do the work to prepare to generate it, as that consumes random @@ -1998,7 +2035,8 @@ def handle(self, wasm): compare(output, optimized_output, 'Two-V8') - def compare_to_merged_output(self, output, merged_output): + @staticmethod + def compare_to_merged_output(output, merged_output): # Comparing the original output from two files to the output after # merging them is not trivial. First, remove the extra logging that # --fuzz-exec-second adds. @@ -2055,6 +2093,7 @@ def compare_to_merged_output(self, output, merged_output): class PreserveImportsExportsRandom(TestCaseHandler): frequency = 0.1 + @override def handle(self, wasm): # We will later verify that no imports or exports changed, by comparing # to the unprocessed original text. @@ -2112,6 +2151,7 @@ def get_relevant_lines(wat): class PreserveImportsExportsJS(TestCaseHandler): frequency = 1 + @override def handle_pair(self, input, before_wasm, after_wasm, opts): try: self.do_handle_pair(input, before_wasm, after_wasm, opts) @@ -2216,7 +2256,8 @@ def do_handle_pair(self, input, before_wasm, after_wasm, opts): if pre_vm.can_compare_to_other(post_vm): compare(pre, post, 'PreserveImportsExportsJS') - def do_run(self, vm, js, wasm): + @staticmethod + def do_run(vm, js, wasm): out = vm.run_js(js, wasm, checked=False) # VM crashes are actual issues we want to find. @@ -2254,6 +2295,7 @@ def do_run(self, vm, js, wasm): cleaned = re.sub(r'function \d+\(\) ', 'function () ', cleaned) return cleaned + @override def can_run_on_wasm(self, wasm): return all_disallowed(DISALLOWED_FEATURES_IN_V8) @@ -2272,6 +2314,7 @@ def can_run_on_wasm(self, wasm): class BranchHintPreservation(TestCaseHandler): frequency = 0.1 + @override def handle(self, wasm): # Generate an instrumented wasm. instrumented = wasm + '.inst.wasm' @@ -2465,7 +2508,7 @@ def handle(self, wasm): _, _, actual, hint, id_ = line[1:-1].split(' ') hint = int(hint) actual = int(actual) - assert hint in (0, 1) + assert hint in {0, 1} # We do not care about the integer value of the condition, # only if it was 0 or non-zero. actual = (actual != 0) diff --git a/scripts/fuzz_passes.py b/scripts/fuzz_passes.py index 528de33e0b6..c2c7910a8dd 100755 --- a/scripts/fuzz_passes.py +++ b/scripts/fuzz_passes.py @@ -14,8 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' -This fuzzes passes, by starting with a working program, then running +"""Script for fuzzing passes. + +Fuzzes passes by starting with a working program, then running random passes on the wast, and seeing if they break something Usage: Provide a base filename for a runnable program, e.g. a.out.js. @@ -23,7 +24,7 @@ be built to run using that wast (BINARYEN_METHOD=interpret-s-expr) Other parameters after the first are used when calling the program. -''' +""" import os diff --git a/scripts/fuzz_passes_wast.py b/scripts/fuzz_passes_wast.py index 19c76d33410..e1bf6be46ff 100755 --- a/scripts/fuzz_passes_wast.py +++ b/scripts/fuzz_passes_wast.py @@ -14,13 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' -This fuzzes passes, by starting with a wast, then running -random passes on the wast, and seeing if they break optimization -or validation +"""Script for fuzzing passes. +Starting with a wast, then running random passes on the wast, and seeing +if they break optimization or validation +""" Usage: Provide the filename of the wast. -''' +""" import os diff --git a/scripts/fuzz_relooper.py b/scripts/fuzz_relooper.py index 7ee6525b160..4fd65e40c8e 100755 --- a/scripts/fuzz_relooper.py +++ b/scripts/fuzz_relooper.py @@ -14,9 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' -This fuzzes the relooper using the C API. -''' +"""Script that fuzzes the relooper using the C API.""" # ruff: noqa: UP031 diff --git a/scripts/gen-s-parser.py b/scripts/gen-s-parser.py index aa848ad8a52..b2a2573c4e4 100755 --- a/scripts/gen-s-parser.py +++ b/scripts/gen-s-parser.py @@ -18,6 +18,8 @@ assert sys.version_info >= (3, 10), 'requires Python 3.10' +# ruff: noqa: E241 + instructions = [ ("unreachable", "makeUnreachable()"), ("nop", "makeNop()"), @@ -713,7 +715,8 @@ def indent(self): # call in a 'with' statement return self - def print_line(self, line): + @staticmethod + def print_line(line): print(" " * CodePrinter.indents + line) diff --git a/scripts/port_passes_tests_to_lit.py b/scripts/port_passes_tests_to_lit.py index ea1dabeebbc..19d5ce6bfc3 100755 --- a/scripts/port_passes_tests_to_lit.py +++ b/scripts/port_passes_tests_to_lit.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Automatically port legacy passes tests to be lit tests -""" +"""Automatically port legacy passes tests to be lit tests.""" import argparse import glob diff --git a/scripts/strip_local_names.py b/scripts/strip_local_names.py index 959e56dd8c1..038fe7cbec5 100644 --- a/scripts/strip_local_names.py +++ b/scripts/strip_local_names.py @@ -1,6 +1,8 @@ -"""Removes local names. When you don't care about local names but do want -to diff for structural changes, this can help. +"""Removes local names. + +When you don't care about local names but do want to diff for structural +changes, this can help. """ import sys diff --git a/scripts/test/gen-cast-test.py b/scripts/test/gen-cast-test.py index 21e469b9858..994ddf40ebd 100755 --- a/scripts/test/gen-cast-test.py +++ b/scripts/test/gen-cast-test.py @@ -1,8 +1,6 @@ #! /usr/bin/python3 -''' -Generate test modules with all interesting casts -''' +"""Generate test modules with all interesting casts.""" import argparse import itertools diff --git a/scripts/test/generate_atomic_spec_test.py b/scripts/test/generate_atomic_spec_test.py index 51b06767028..6ce2f751efe 100644 --- a/scripts/test/generate_atomic_spec_test.py +++ b/scripts/test/generate_atomic_spec_test.py @@ -107,12 +107,12 @@ class Template: def all_combinations() -> Iterator[(Template, (int, ValueType), Ordering)]: """Yield tuples covering all possible combinations of atomic memory operations. + (template, (idx, memory_ptr_type), ordering) where idx is a memory index or None representing an implicit 0 index and memory_ptr_type is i32 or i64 based on the memory being indexed and ordering is an `Ordering` enum or None representing an implicit seqcst ordering. """ - # See the memory section defined in `binary_test` memories = [(None, ValueType.i32), (0, ValueType.i32), (1, ValueType.i64)] @@ -120,7 +120,7 @@ def all_combinations() -> Iterator[(Template, (int, ValueType), Ordering)]: def statement(template, mem_idx: int | None, mem_ptr_type: ValueType, ordering: Ordering | None): - """Return a statement exercising the op in `template` e.g. (i32.atomic.store 1 acqrel (i64.const 42) (i32.const 42))""" + """Return a statement exercising the op in `template` e.g. (i32.atomic.store 1 acqrel (i64.const 42) (i32.const 42)).""" memargs = [] if mem_idx is not None: memargs.append(str(mem_idx)) @@ -139,12 +139,14 @@ def statement(template, mem_idx: int | None, mem_ptr_type: ValueType, ordering: def func(): - """Return a func exercising all ops in `templates` e.g. - (func $test-all-ops - (drop (i32.atomic.load (i32.const 42))) - (drop (i32.atomic.load acqrel (i32.const 42))) - ... - ) + """Return a func exercising all ops in `templates`. + + e.g. + (func $test-all-ops + (drop (i32.atomic.load (i32.const 42))) + (drop (i32.atomic.load acqrel (i32.const 42))) + ... + ) """ return f''';; Memory index must come before memory ordering if present. ;; Both immediates are optional; an omitted memory ordering will be treated as seqcst. @@ -172,7 +174,7 @@ def invalid_text_test(): def bin_to_str(bin: bytes) -> str: - """Return binary formatted for .wast format e.g. \00\61\73\6d\01\00\00\00""" + r"""Return binary formatted for .wast format e.g. \00\61\73\6d\01\00\00\00.""" return ''.join(f'{backslash}{byte:02x}' for byte in bin) @@ -185,7 +187,9 @@ def bin_to_str(bin: bytes) -> str: def bin_statement_lines(template: Template, mem_idx: int, mem_ptr_type: ValueType, ordering: Ordering) -> Iterator[(bytes, str)]: - """Yield (b, comment) where `b` is a part of the statement using `template`, and `comment` explains that part, e.g. + r"""Yield (b, comment) where `b` is a part of the statement using `template`, and `comment` explains that part. + + e.g. (b"\xfe\x11", "i64.atomic.load") The entire iterator represents a complete expression using the `template`. e.g. (drop (i32.atomic.load (i32.const 42))) @@ -226,17 +230,18 @@ def bin_statement_lines(template: Template, mem_idx: int, mem_ptr_type: ValueTyp def bin_statement(template: Template, mem_idx: int, mem_ptr_type: ValueType, ordering: Ordering) -> (bytes, str): - """Return (b, s) where `b` is the binary exercising an instruction, e.g. - (drop (i32.atomic.load (i32.const 42))) - and `s` is a str containing the binary along with comments explaining it, e.g. - "\41\33" ;; (i32.const 51) - "\fe\10" ;; i32.atomic.load - "\42" ;; Alignment of 2 with bit 6 set indicating that a memory index immediate follows - "\00" ;; memory index - "\00" ;; offset - "\1a" ;; drop + r"""Return (b, s) where `b` is the binary exercising an instruction. + + e.g: + (drop (i32.atomic.load (i32.const 42))) + and `s` is a str containing the binary along with comments explaining it, e.g. + "\41\33" ;; (i32.const 51) + "\fe\10" ;; i32.atomic.load + "\42" ;; Alignment of 2 with bit 6 set indicating that a memory index immediate follows + "\00" ;; memory index + "\00" ;; offset + "\1a" ;; drop """ - bins = [] strs = [] diff --git a/scripts/test/shared.py b/scripts/test/shared.py index 6f39c1dc37d..ecb8b6218bc 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -130,7 +130,7 @@ def warn(text): options.binaryen_bin = os.path.normpath(os.path.abspath(options.binaryen_bin)) if not options.binaryen_lib: - options.binaryen_lib = os.path.join(os.path.dirname(options.binaryen_bin), 'lib') + options.binaryen_lib = os.path.join(os.path.dirname(options.binaryen_bin), 'lib') options.binaryen_lib = os.path.normpath(os.path.abspath(options.binaryen_lib)) @@ -344,13 +344,14 @@ def fail_if_not_identical_to_file(actual, expected_file): def get_test_dir(name): - """Returns the test directory located at BINARYEN_ROOT/test/[name].""" + """Return the test directory located at BINARYEN_ROOT/test/[name].""" return os.path.join(options.binaryen_test, name) def get_tests(test_dir, extensions=[], recursive=False): - """Returns the list of test files in a given directory. 'extensions' is a - list of file extensions. If 'extensions' is empty, returns all files. + """Return the list of test files in a given directory. + + 'extensions' is a list of file extensions. If 'extensions' is empty, returns all files. """ tests = [] star = '**/*' if recursive else '*' @@ -461,15 +462,14 @@ def _can_run_spec_test(test): # check utilities -def binary_format_check(wast, verify_final_result=True, wasm_as_args=['-g'], - binary_suffix='.fromBinary', base_name=None, stdout=None): +def binary_format_check(wast, verify_final_result=True, base_name=None, stdout=None): # checks we can convert the wast to binary and back as_file = f"{base_name}-a.wasm" if base_name is not None else "a.wasm" disassembled_file = f"{base_name}-ab.wast" if base_name is not None else "ab.wast" print(' (binary format check)', file=stdout) - cmd = WASM_AS + [wast, '-o', as_file, '-all'] + wasm_as_args + cmd = WASM_AS + [wast, '-o', as_file, '-all', '-g'] print(' ', ' '.join(cmd), file=stdout) if os.path.exists(as_file): os.unlink(as_file) @@ -490,7 +490,7 @@ def binary_format_check(wast, verify_final_result=True, wasm_as_args=['-g'], if verify_final_result: actual = open(disassembled_file).read() - fail_if_not_identical_to_file(actual, wast + binary_suffix) + fail_if_not_identical_to_file(actual, wast + '.fromBinary') return disassembled_file diff --git a/scripts/test/support.py b/scripts/test/support.py index 6af99cabfb5..c79bf26ae6c 100644 --- a/scripts/test/support.py +++ b/scripts/test/support.py @@ -22,10 +22,10 @@ def split_wast(wastFile): - ''' - Returns a list of pairs of module definitions and assertions. + """Return a list of pairs of module definitions and assertions. + Module invalidity tests, as well as (module definition ...) and (module instance ...) are skipped. - ''' + """ # if it's a binary, leave it as is, we can't split it wast = None if not wastFile.endswith('.wasm'): @@ -137,11 +137,12 @@ def _subprocess_run(*args, **kwargs): def run_command(cmd, expected_status=0, stdout=None, stderr=None, expected_err=None, err_contains=False, err_ignore=None): - ''' + """Run a subprocess, returning its output. + stderr - None, subprocess.PIPE, subprocess.STDOUT or a file handle / io.StringIO to write stdout to stdout - File handle to print debug messages to returns the process's stdout - ''' + """ if expected_err is not None: assert stderr == subprocess.PIPE or stderr is None, \ "Can't redirect stderr if using expected_err" diff --git a/scripts/update_help_checks.py b/scripts/update_help_checks.py index 6acaf9ee126..5471f3533a9 100755 --- a/scripts/update_help_checks.py +++ b/scripts/update_help_checks.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""A test case update script for lit help checks. -""" +"""A test case update script for lit help checks.""" import os import subprocess diff --git a/scripts/update_lit_checks.py b/scripts/update_lit_checks.py index 0444b51eca7..5adb37ba514 100755 --- a/scripts/update_lit_checks.py +++ b/scripts/update_lit_checks.py @@ -70,9 +70,7 @@ def warn(msg): def itertests(args): - """ - Yield (filename, lines) for each test specified in the command line args - """ + """Yield (filename, lines) for each test specified in the command line args.""" for pattern in args.tests: tests = glob.glob(pattern, recursive=True) if not tests: diff --git a/test/unit/test_web_limitations.py b/test/unit/test_web_limitations.py index 9f74814770b..7a8f8a25ef6 100644 --- a/test/unit/test_web_limitations.py +++ b/test/unit/test_web_limitations.py @@ -7,9 +7,7 @@ class WebLimitations(utils.BinaryenTestCase): def test_many_params(self): - """Test that we warn on large numbers of parameters, which Web VMs - disallow.""" - + """Test that we warn on large numbers of parameters, which Web VMs disallow.""" params = '(param i32) ' * 1001 module = f''' (module @@ -23,9 +21,7 @@ def test_many_params(self): p.stderr) def test_many_locals(self): - """Test that we warn on large numbers of locals, which Web VMs - disallow.""" - + """Test that we warn on large numbers of locals, which Web VMs disallow.""" params = '(local i32) ' * 50_001 module = f''' (module From 257298c678cd9457e2682ec510a75b315e643b3f Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 28 Apr 2026 14:38:54 -0700 Subject: [PATCH 062/168] Fuzzer: Do not return 0 from upTo when input is finished (#8653) This was added to simplify our output when the random data is small - it makes us pick simpler options, which helps a little there - but the downsides are large, so revert this part. Specifically, we consume all the random input to generate functions. We then do a small amount of random usage at the end, things like export mutation. To handle that, we keep returning random values even after the random input is consumed (using some xor-ing to try to keep things random). But if we just return 0 at that point, we are missing out on a lot of variety. This was added in #7832. All the rest of that PR makes sense - we can check for the end of the random data and do simpler things - but we should not do the simplest possible thing in the generic `upTo` method which would simplify everything, even things we don't want to. --- src/tools/fuzzing/random.cpp | 3 -- test/passes/fuzz_metrics_noprint.bin.txt | 36 ++++++++-------- .../fuzz_metrics_passes_noprint.bin.txt | 42 +++++++++---------- ...e-to-fuzz_all-features_metrics_noprint.txt | 12 +++--- 4 files changed, 45 insertions(+), 48 deletions(-) diff --git a/src/tools/fuzzing/random.cpp b/src/tools/fuzzing/random.cpp index cfcdbdd970e..7af7b412775 100644 --- a/src/tools/fuzzing/random.cpp +++ b/src/tools/fuzzing/random.cpp @@ -66,9 +66,6 @@ float Random::getFloat() { return Literal(get32()).reinterpretf32(); } double Random::getDouble() { return Literal(get64()).reinterpretf64(); } uint32_t Random::upTo(uint32_t x) { - if (finished()) { - return 0; - } if (x == 0) { return 0; } diff --git a/test/passes/fuzz_metrics_noprint.bin.txt b/test/passes/fuzz_metrics_noprint.bin.txt index b243a9e11dc..bc67df3b906 100644 --- a/test/passes/fuzz_metrics_noprint.bin.txt +++ b/test/passes/fuzz_metrics_noprint.bin.txt @@ -9,27 +9,27 @@ total [table-data] : 23 [tables] : 1 [tags] : 0 - [total] : 9732 + [total] : 9511 [vars] : 165 - Binary : 710 - Block : 1566 - Break : 305 - Call : 257 + Binary : 701 + Block : 1509 + Break : 303 + Call : 255 CallIndirect : 109 - Const : 1657 - Drop : 148 - GlobalGet : 787 - GlobalSet : 552 - If : 510 - Load : 177 + Const : 1586 + Drop : 99 + GlobalGet : 784 + GlobalSet : 550 + If : 507 + Load : 174 LocalGet : 802 - LocalSet : 593 - Loop : 217 - Nop : 128 + LocalSet : 585 + Loop : 216 + Nop : 127 RefFunc : 23 - Return : 95 + Return : 93 Select : 88 - Store : 71 + Store : 70 Switch : 4 - Unary : 660 - Unreachable : 273 + Unary : 654 + Unreachable : 272 diff --git a/test/passes/fuzz_metrics_passes_noprint.bin.txt b/test/passes/fuzz_metrics_passes_noprint.bin.txt index 17fb9ad319b..5fe8234b335 100644 --- a/test/passes/fuzz_metrics_passes_noprint.bin.txt +++ b/test/passes/fuzz_metrics_passes_noprint.bin.txt @@ -9,27 +9,27 @@ total [table-data] : 28 [tables] : 1 [tags] : 0 - [total] : 8649 + [total] : 9880 [vars] : 253 - Binary : 597 - Block : 1475 - Break : 252 - Call : 323 - CallIndirect : 46 - Const : 1348 - Drop : 91 - GlobalGet : 732 - GlobalSet : 543 - If : 491 - Load : 135 - LocalGet : 683 - LocalSet : 507 - Loop : 182 - Nop : 131 + Binary : 685 + Block : 1660 + Break : 304 + Call : 343 + CallIndirect : 57 + Const : 1537 + Drop : 102 + GlobalGet : 812 + GlobalSet : 601 + If : 560 + Load : 159 + LocalGet : 820 + LocalSet : 611 + Loop : 210 + Nop : 151 RefFunc : 28 - Return : 78 - Select : 84 - Store : 43 + Return : 84 + Select : 95 + Store : 56 Switch : 2 - Unary : 607 - Unreachable : 271 + Unary : 704 + Unreachable : 299 diff --git a/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt b/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt index af24cbe348c..7f56b028dc0 100644 --- a/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt +++ b/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt @@ -1,6 +1,6 @@ Metrics total - [exports] : 14 + [exports] : 13 [funcs] : 18 [globals] : 2 [imports] : 13 @@ -9,8 +9,8 @@ total [table-data] : 3 [tables] : 2 [tags] : 2 - [total] : 527 - [vars] : 50 + [total] : 525 + [vars] : 51 ArrayNewFixed : 2 AtomicFence : 1 Binary : 27 @@ -18,12 +18,12 @@ total Break : 9 Call : 17 CallRef : 1 - Const : 103 + Const : 101 Drop : 8 GlobalGet : 48 GlobalSet : 44 If : 29 - LocalGet : 14 + LocalGet : 15 LocalSet : 10 Loop : 4 MemoryInit : 1 @@ -45,6 +45,6 @@ total TableSet : 1 TryTable : 2 TupleExtract : 1 - TupleMake : 4 + TupleMake : 3 Unary : 29 Unreachable : 23 From 6216d241739c1a92e500006deec400307e361b23 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Tue, 28 Apr 2026 15:48:27 -0700 Subject: [PATCH 063/168] Fix bad merge from #8654 --- scripts/fuzz_passes_wast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fuzz_passes_wast.py b/scripts/fuzz_passes_wast.py index e1bf6be46ff..97f29a7b415 100755 --- a/scripts/fuzz_passes_wast.py +++ b/scripts/fuzz_passes_wast.py @@ -18,7 +18,7 @@ Starting with a wast, then running random passes on the wast, and seeing if they break optimization or validation -""" + Usage: Provide the filename of the wast. """ From c6a5e65b77a4b6e9d72fa7ba674632aba4b99099 Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Tue, 28 Apr 2026 16:57:13 -0700 Subject: [PATCH 064/168] Support i64.add/sub128 instructions from Wide Arithmetic proposal (#8638) Part of #8544 i64 to i32 lowering isn't implemented yet because supporting multi-value i64 returns requires more work here. Will add this in a separate change. Spec tests are adapted from the upstream proposal repo, with only tests related to i64.add128 and i64.sub128 included. Drive-by changes: * Remove unneeded template params in wasm-validator.cpp * Unconditionally assign to the result in ckd_add / ckd_sub polyfills to match the behavior of the [std implementations](https://en.cppreference.com/cpp/numeric/ckd_add). All of the existing callers don't observe the result when overflow occurred, but for our case we need the value regardless. --- scripts/gen-s-parser.py | 2 + src/gen-s-parser.inc | 42 +++-- src/interpreter/interpreter.cpp | 1 + src/ir/ReFinalize.cpp | 1 + src/ir/child-typer.h | 7 + src/ir/cost.h | 4 + src/ir/effects.h | 1 + src/ir/possible-contents.cpp | 1 + src/ir/subtype-exprs.h | 1 + src/parser/contexts.h | 11 ++ src/parser/parsers.h | 13 ++ src/passes/I64ToI32Lowering.cpp | 4 + src/passes/Print.cpp | 14 ++ src/passes/TypeGeneralizing.cpp | 1 + src/support/stdckdint.h | 20 ++- src/wasm-binary.h | 5 + src/wasm-builder.h | 14 ++ src/wasm-delegations-fields.def | 7 + src/wasm-delegations.def | 1 + src/wasm-interpreter.h | 32 ++++ src/wasm-ir-builder.h | 1 + src/wasm-type.h | 2 + src/wasm.h | 20 +++ src/wasm/wasm-binary.cpp | 4 + src/wasm/wasm-ir-builder.cpp | 9 + src/wasm/wasm-stack.cpp | 14 ++ src/wasm/wasm-type.cpp | 5 + src/wasm/wasm-validator.cpp | 28 ++- src/wasm/wasm.cpp | 12 ++ src/wasm2js.h | 4 + test/spec/wide-arithmetic.wast | 310 ++++++++++++++++++++++++++++++++ 31 files changed, 570 insertions(+), 21 deletions(-) create mode 100644 test/spec/wide-arithmetic.wast diff --git a/scripts/gen-s-parser.py b/scripts/gen-s-parser.py index b2a2573c4e4..d8ebce98e31 100755 --- a/scripts/gen-s-parser.py +++ b/scripts/gen-s-parser.py @@ -148,6 +148,8 @@ ("i64.shr_u", "makeBinary(BinaryOp::ShrUInt64)"), ("i64.rotl", "makeBinary(BinaryOp::RotLInt64)"), ("i64.rotr", "makeBinary(BinaryOp::RotRInt64)"), + ("i64.add128", "makeWideIntAddSub(WideIntAddSubOp::AddInt128)"), + ("i64.sub128", "makeWideIntAddSub(WideIntAddSubOp::SubInt128)"), ("f32.abs", "makeUnary(UnaryOp::AbsFloat32)"), ("f32.neg", "makeUnary(UnaryOp::NegFloat32)"), ("f32.ceil", "makeUnary(UnaryOp::CeilFloat32)"), diff --git a/src/gen-s-parser.inc b/src/gen-s-parser.inc index 345afa302aa..d3d3b99a702 100644 --- a/src/gen-s-parser.inc +++ b/src/gen-s-parser.inc @@ -3427,12 +3427,23 @@ switch (buf[0]) { switch (buf[4]) { case 'a': { switch (buf[5]) { - case 'd': - if (op == "i64.add"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::AddInt64)); - return Ok{}; + case 'd': { + switch (buf[7]) { + case '\0': + if (op == "i64.add"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::AddInt64)); + return Ok{}; + } + goto parse_error; + case '1': + if (op == "i64.add128"sv) { + CHECK_ERR(makeWideIntAddSub(ctx, pos, annotations, WideIntAddSubOp::AddInt128)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } case 'n': if (op == "i64.and"sv) { CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::AndInt64)); @@ -4113,12 +4124,23 @@ switch (buf[0]) { default: goto parse_error; } } - case 'u': - if (op == "i64.sub"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::SubInt64)); - return Ok{}; + case 'u': { + switch (buf[7]) { + case '\0': + if (op == "i64.sub"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::SubInt64)); + return Ok{}; + } + goto parse_error; + case '1': + if (op == "i64.sub128"sv) { + CHECK_ERR(makeWideIntAddSub(ctx, pos, annotations, WideIntAddSubOp::SubInt128)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } default: goto parse_error; } } diff --git a/src/interpreter/interpreter.cpp b/src/interpreter/interpreter.cpp index d15abcfd4a0..e3abe63525d 100644 --- a/src/interpreter/interpreter.cpp +++ b/src/interpreter/interpreter.cpp @@ -216,6 +216,7 @@ struct ExpressionInterpreter : OverriddenVisitor { WASM_UNREACHABLE("TODO"); } } + Flow visitWideIntAddSub(WideIntAddSub* curr) { WASM_UNREACHABLE("TODO"); } Flow visitSelect(Select* curr) { WASM_UNREACHABLE("TODO"); } Flow visitDrop(Drop* curr) { WASM_UNREACHABLE("TODO"); } Flow visitReturn(Return* curr) { WASM_UNREACHABLE("TODO"); } diff --git a/src/ir/ReFinalize.cpp b/src/ir/ReFinalize.cpp index 24afae568c5..6c722cbb03c 100644 --- a/src/ir/ReFinalize.cpp +++ b/src/ir/ReFinalize.cpp @@ -109,6 +109,7 @@ void ReFinalize::visitMemoryFill(MemoryFill* curr) { curr->finalize(); } void ReFinalize::visitConst(Const* curr) { curr->finalize(); } void ReFinalize::visitUnary(Unary* curr) { curr->finalize(); } void ReFinalize::visitBinary(Binary* curr) { curr->finalize(); } +void ReFinalize::visitWideIntAddSub(WideIntAddSub* curr) { curr->finalize(); } void ReFinalize::visitSelect(Select* curr) { curr->finalize(); } void ReFinalize::visitDrop(Drop* curr) { curr->finalize(); } void ReFinalize::visitReturn(Return* curr) { curr->finalize(); } diff --git a/src/ir/child-typer.h b/src/ir/child-typer.h index e223eb2ad59..2aa1a06d549 100644 --- a/src/ir/child-typer.h +++ b/src/ir/child-typer.h @@ -705,6 +705,13 @@ template struct ChildTyper : OverriddenVisitor { } } + void visitWideIntAddSub(WideIntAddSub* curr) { + note(&curr->leftLow, Type::i64); + note(&curr->leftHigh, Type::i64); + note(&curr->rightLow, Type::i64); + note(&curr->rightHigh, Type::i64); + } + void visitSelect(Select* curr, std::optional type = std::nullopt) { if (type) { note(&curr->ifTrue, *type); diff --git a/src/ir/cost.h b/src/ir/cost.h index 7c02dafce7b..7d044a49947 100644 --- a/src/ir/cost.h +++ b/src/ir/cost.h @@ -575,6 +575,10 @@ struct CostAnalyzer : public OverriddenVisitor { } return ret + visit(curr->left) + visit(curr->right); } + CostType visitWideIntAddSub(WideIntAddSub* curr) { + return 1 + visit(curr->leftLow) + visit(curr->leftHigh) + + visit(curr->rightLow) + visit(curr->rightHigh); + } CostType visitSelect(Select* curr) { return 1 + visit(curr->condition) + visit(curr->ifTrue) + visit(curr->ifFalse); diff --git a/src/ir/effects.h b/src/ir/effects.h index af866b9e536..ec96053a13d 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -945,6 +945,7 @@ class EffectAnalyzer { } } } + void visitWideIntAddSub(WideIntAddSub* curr) {} void visitSelect(Select* curr) {} void visitDrop(Drop* curr) {} void visitReturn(Return* curr) { parent.branchesOut = true; } diff --git a/src/ir/possible-contents.cpp b/src/ir/possible-contents.cpp index 4dc6da9c9d9..08cf1cf4f8d 100644 --- a/src/ir/possible-contents.cpp +++ b/src/ir/possible-contents.cpp @@ -634,6 +634,7 @@ struct InfoCollector addRoot(curr); } void visitBinary(Binary* curr) { addRoot(curr); } + void visitWideIntAddSub(WideIntAddSub* curr) { addRoot(curr); } void visitSelect(Select* curr) { receiveChildValue(curr->ifTrue, curr); receiveChildValue(curr->ifFalse, curr); diff --git a/src/ir/subtype-exprs.h b/src/ir/subtype-exprs.h index 8a4677ec9da..b0a910f3fa7 100644 --- a/src/ir/subtype-exprs.h +++ b/src/ir/subtype-exprs.h @@ -213,6 +213,7 @@ struct SubtypingDiscoverer : public OverriddenVisitor { void visitConst(Const* curr) {} void visitUnary(Unary* curr) {} void visitBinary(Binary* curr) {} + void visitWideIntAddSub(WideIntAddSub* curr) {} void visitSelect(Select* curr) { self()->noteSubtype(curr->ifTrue, curr); self()->noteSubtype(curr->ifFalse, curr); diff --git a/src/parser/contexts.h b/src/parser/contexts.h index 88b7d8a941c..e62d3d5869a 100644 --- a/src/parser/contexts.h +++ b/src/parser/contexts.h @@ -476,6 +476,11 @@ struct NullInstrParserCtx { Result<> makeBinary(Index, const std::vector&, BinaryOp) { return Ok{}; } + Result<> + makeWideIntAddSub(Index, const std::vector&, WideIntAddSubOp) { + return Ok{}; + } + Result<> makeUnary(Index, const std::vector&, UnaryOp) { return Ok{}; } @@ -2159,6 +2164,12 @@ struct ParseDefsCtx : TypeParserCtx, AnnotationParserCtx { return withLoc(pos, irBuilder.makeBinary(op)); } + Result<> makeWideIntAddSub(Index pos, + const std::vector& annotations, + WideIntAddSubOp op) { + return withLoc(pos, irBuilder.makeWideIntAddSub(op)); + } + Result<> makeUnary(Index pos, const std::vector& annotations, UnaryOp op) { return withLoc(pos, irBuilder.makeUnary(op)); diff --git a/src/parser/parsers.h b/src/parser/parsers.h index 4a5fb71b771..c0d2804e29f 100644 --- a/src/parser/parsers.h +++ b/src/parser/parsers.h @@ -91,6 +91,11 @@ Result<> makeNop(Ctx&, Index, const std::vector&); template Result<> makeBinary(Ctx&, Index, const std::vector&, BinaryOp op); template +Result<> makeWideIntAddSub(Ctx&, + Index, + const std::vector&, + WideIntAddSubOp op); +template Result<> makeUnary(Ctx&, Index, const std::vector&, UnaryOp op); template Result<> makeSelect(Ctx&, Index, const std::vector&); @@ -1592,6 +1597,14 @@ Result<> makeBinary(Ctx& ctx, return ctx.makeBinary(pos, annotations, op); } +template +Result<> makeWideIntAddSub(Ctx& ctx, + Index pos, + const std::vector& annotations, + WideIntAddSubOp op) { + return ctx.makeWideIntAddSub(pos, annotations, op); +} + template Result<> makeUnary(Ctx& ctx, Index pos, diff --git a/src/passes/I64ToI32Lowering.cpp b/src/passes/I64ToI32Lowering.cpp index 42feafa3861..0724b448674 100644 --- a/src/passes/I64ToI32Lowering.cpp +++ b/src/passes/I64ToI32Lowering.cpp @@ -1553,6 +1553,10 @@ struct I64ToI32Lowering : public WalkerPass> { } } + void visitWideIntAddSub(WideIntAddSub* curr) { + WASM_UNREACHABLE("TODO: wide arithmetic lowering"); + } + void visitSelect(Select* curr) { if (handleUnreachable(curr)) { return; diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 8239edc71be..c926c914720 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -2031,6 +2031,20 @@ struct PrintExpressionContents } restoreNormalColor(o); } + void visitWideIntAddSub(WideIntAddSub* curr) { + prepareColor(o); + switch (curr->op) { + case AddInt128: { + o << "i64.add128"; + break; + } + case SubInt128: { + o << "i64.sub128"; + break; + } + } + restoreNormalColor(o); + } void visitSelect(Select* curr) { prepareColor(o) << "select"; restoreNormalColor(o); diff --git a/src/passes/TypeGeneralizing.cpp b/src/passes/TypeGeneralizing.cpp index 03a0a0f11a7..1cd9a1b7885 100644 --- a/src/passes/TypeGeneralizing.cpp +++ b/src/passes/TypeGeneralizing.cpp @@ -433,6 +433,7 @@ struct TransferFn : OverriddenVisitor { void visitConst(Const* curr) {} void visitUnary(Unary* curr) {} void visitBinary(Binary* curr) {} + void visitWideIntAddSub(WideIntAddSub* curr) {} void visitSelect(Select* curr) { if (curr->type.isRef()) { diff --git a/src/support/stdckdint.h b/src/support/stdckdint.h index 42e87f9a26d..c5132058332 100644 --- a/src/support/stdckdint.h +++ b/src/support/stdckdint.h @@ -29,12 +29,20 @@ template bool ckd_add(T* output, T a, T b) { // Atm this polyfill only supports unsigned types. static_assert(std::is_unsigned_v); - T result = a + b; - if (result < a) { - return true; - } - *output = result; - return false; + *output = a + b; + return *output < a; +#endif +} + +template bool ckd_sub(T* output, T a, T b) { +#if __has_builtin(__builtin_sub_overflow) + return __builtin_sub_overflow(a, b, output); +#else + // Atm this polyfill only supports unsigned types. + static_assert(std::is_unsigned_v); + + *output = a - b; + return *output > a; #endif } diff --git a/src/wasm-binary.h b/src/wasm-binary.h index 82363964d0b..85458e97a3b 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -1138,6 +1138,11 @@ enum ASTNodes { MemoryCopy = 0x0a, MemoryFill = 0x0b, + // wide arithmetic opcodes + + I64Add128 = 0x13, + I64Sub128 = 0x14, + // reference types opcodes TableGrow = 0x0f, diff --git a/src/wasm-builder.h b/src/wasm-builder.h index 30465e9e128..cd9d0c34ddd 100644 --- a/src/wasm-builder.h +++ b/src/wasm-builder.h @@ -660,6 +660,20 @@ class Builder { ret->finalize(); return ret; } + WideIntAddSub* makeWideIntAddSub(WideIntAddSubOp op, + Expression* leftLow, + Expression* leftHigh, + Expression* rightLow, + Expression* rightHigh) { + auto* ret = wasm.allocator.alloc(); + ret->op = op; + ret->leftLow = leftLow; + ret->leftHigh = leftHigh; + ret->rightLow = rightLow; + ret->rightHigh = rightHigh; + ret->finalize(); + return ret; + } Select* makeSelect(Expression* condition, Expression* ifTrue, Expression* ifFalse) { auto* ret = wasm.allocator.alloc(); diff --git a/src/wasm-delegations-fields.def b/src/wasm-delegations-fields.def index f346bfde883..79ac534864f 100644 --- a/src/wasm-delegations-fields.def +++ b/src/wasm-delegations-fields.def @@ -939,6 +939,12 @@ DELEGATE_FIELD_CHILD(WideIntAddSub, leftHigh) DELEGATE_FIELD_CHILD(WideIntAddSub, leftLow) DELEGATE_FIELD_CASE_END(WideIntAddSub) +DELEGATE_FIELD_CASE_START(WideIntMul) +DELEGATE_FIELD_INT(WideIntMul, op) +DELEGATE_FIELD_CHILD(WideIntMul, right) +DELEGATE_FIELD_CHILD(WideIntMul, left) +DELEGATE_FIELD_CASE_END(WideIntMul) + DELEGATE_FIELD_MAIN_END #undef DELEGATE_ID diff --git a/src/wasm-delegations.def b/src/wasm-delegations.def index 92572b01198..8d14135ae54 100644 --- a/src/wasm-delegations.def +++ b/src/wasm-delegations.def @@ -120,5 +120,6 @@ DELEGATE(StackSwitch); DELEGATE(StructWait); DELEGATE(StructNotify); DELEGATE(WideIntAddSub); +DELEGATE(WideIntMul); #undef DELEGATE diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 8dd1bb4f4c6..67101d641e8 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -45,6 +45,7 @@ #include "ir/runtime-table.h" #include "ir/table-utils.h" #include "support/bits.h" +#include "support/int128.h" #include "support/safe_integer.h" #include "support/stdckdint.h" #include "support/string.h" @@ -1833,6 +1834,29 @@ class ExpressionRunner : public OverriddenVisitor { results.push_back(Literal(highResult)); return results; } + Flow visitWideIntMul(WideIntMul* curr) { + VISIT(left, curr->left); + VISIT(right, curr->right); + uint64_t lhs = left.getSingleValue().geti64(); + uint64_t rhs = right.getSingleValue().geti64(); + + Int128 result; + switch (curr->op) { + case MulWideSInt64: { + result = mul_wide_s(lhs, rhs); + break; + } + case MulWideUInt64: { + result = mul_wide_u(lhs, rhs); + break; + } + } + + Literals results; + results.push_back(Literal(result.low)); + results.push_back(Literal(result.high)); + return results; + } Flow visitDrop(Drop* curr) { VISIT(value, curr->value) return Flow(); diff --git a/src/wasm-ir-builder.h b/src/wasm-ir-builder.h index 9b87cba55e2..21ed4650202 100644 --- a/src/wasm-ir-builder.h +++ b/src/wasm-ir-builder.h @@ -191,6 +191,7 @@ class IRBuilder : public UnifiedExpressionVisitor> { Result<> makeUnary(UnaryOp op); Result<> makeBinary(BinaryOp op); Result<> makeWideIntAddSub(WideIntAddSubOp op); + Result<> makeWideIntMul(WideIntMulOp op); Result<> makeSelect(std::optional type = std::nullopt); Result<> makeDrop(); Result<> makeReturn(); diff --git a/src/wasm.h b/src/wasm.h index be7904bb674..6fe34edb08f 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -645,6 +645,11 @@ enum WideIntAddSubOp { SubInt128, }; +enum WideIntMulOp { + MulWideSInt64, + MulWideUInt64, +}; + // // Expressions // @@ -775,6 +780,7 @@ class Expression { StructWaitId, StructNotifyId, WideIntAddSubId, + WideIntMulId, NumExpressionIds }; Id _id; @@ -1320,6 +1326,18 @@ class WideIntAddSub : public SpecificExpression { void finalize(); }; +class WideIntMul : public SpecificExpression { +public: + WideIntMul() = default; + WideIntMul(MixedArena& allocator) {} + + WideIntMulOp op; + Expression* left; + Expression* right; + + void finalize(); +}; + class Select : public SpecificExpression { public: Select() = default; diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 368203ba5ff..61d505bd205 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -4002,6 +4002,10 @@ Result<> WasmBinaryReader::readInst() { return builder.makeWideIntAddSub(AddInt128); case BinaryConsts::I64Sub128: return builder.makeWideIntAddSub(SubInt128); + case BinaryConsts::I64MulWideS: + return builder.makeWideIntMul(MulWideSInt64); + case BinaryConsts::I64MulWideU: + return builder.makeWideIntMul(MulWideUInt64); case BinaryConsts::TableSize: return builder.makeTableSize(getTableName(getU32LEB())); case BinaryConsts::TableGrow: diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index be6148496c0..8e924414587 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -1766,6 +1766,14 @@ Result<> IRBuilder::makeWideIntAddSub(WideIntAddSubOp op) { return Ok{}; } +Result<> IRBuilder::makeWideIntMul(WideIntMulOp op) { + WideIntMul curr; + curr.op = op; + CHECK_ERR(visitWideIntMul(&curr)); + push(builder.makeWideIntMul(op, curr.left, curr.right)); + return Ok{}; +} + Result<> IRBuilder::makeSelect(std::optional type) { Select curr; CHECK_ERR(visitSelect(&curr)); diff --git a/src/wasm/wasm-stack.cpp b/src/wasm/wasm-stack.cpp index 93521cb1e26..8cb99e9cccb 100644 --- a/src/wasm/wasm-stack.cpp +++ b/src/wasm/wasm-stack.cpp @@ -2309,6 +2309,20 @@ void BinaryInstWriter::visitWideIntAddSub(WideIntAddSub* curr) { } } +void BinaryInstWriter::visitWideIntMul(WideIntMul* curr) { + o << static_cast(BinaryConsts::MiscPrefix); + switch (curr->op) { + case MulWideSInt64: { + o << U32LEB(BinaryConsts::I64MulWideS); + break; + } + case MulWideUInt64: { + o << U32LEB(BinaryConsts::I64MulWideU); + break; + } + } +} + void BinaryInstWriter::visitReturn(Return* curr) { o << static_cast(BinaryConsts::Return); } diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index 9f9d6db8c2c..954f0676f55 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -509,6 +509,7 @@ struct FunctionValidator : public WalkerPass> { void visitMemoryFill(MemoryFill* curr); void visitBinary(Binary* curr); void visitWideIntAddSub(WideIntAddSub* curr); + void visitWideIntMul(WideIntMul* curr); void visitUnary(Unary* curr); void visitSelect(Select* curr); void visitDrop(Drop* curr); @@ -2461,6 +2462,20 @@ void FunctionValidator::visitWideIntAddSub(WideIntAddSub* curr) { } } +void FunctionValidator::visitWideIntMul(WideIntMul* curr) { + shouldBeTrue(getModule()->features.hasWideArithmetic(), + curr, + "i64.mul_wide_s / i64.mul_wide_u require wide arithmetic " + "[--enable-wide-arithmetic]"); + + for (auto* operand : {curr->left, curr->right}) { + shouldBeEqualOrFirstIsUnreachable(operand->type, + Type(Type::i64), + curr, + "wide binary child types must be i64"); + } +} + void FunctionValidator::visitDrop(Drop* curr) { shouldBeTrue(curr->value->type.isConcrete() || curr->value->type == Type::unreachable, diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index c2feec54bf3..85c308ea645 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -815,6 +815,15 @@ void WideIntAddSub::finalize() { } } +void WideIntMul::finalize() { + if (left->type == Type::unreachable || right->type == Type::unreachable) { + type = Type::unreachable; + } else { + static Type i64Pair = Types::getI64Pair(); + type = i64Pair; + } +} + void Select::finalize() { assert(ifTrue && ifFalse); if (ifTrue->type == Type::unreachable || ifFalse->type == Type::unreachable || diff --git a/src/wasm2js.h b/src/wasm2js.h index 735669d3d27..08ac6e6e26e 100644 --- a/src/wasm2js.h +++ b/src/wasm2js.h @@ -1964,6 +1964,10 @@ Ref Wasm2JSBuilder::processExpression(Expression* curr, WASM_UNREACHABLE("wide arithmetic is not supported by wasm2js"); } + Ref visitWideIntMul(WideIntMul* curr) { + WASM_UNREACHABLE("wide arithmetic is not supported by wasm2js"); + } + Ref visitSelect(Select* curr) { // If the condition has effects that interact with the operands, we must // reorder it to the start. We must also use locals if the values have diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt index 1c8e2de179f..254f7a38c9f 100644 --- a/test/gtest/CMakeLists.txt +++ b/test/gtest/CMakeLists.txt @@ -14,6 +14,7 @@ set(unittest_SOURCES dfa_minimization.cpp disjoint_sets.cpp graph.cpp + int128.cpp leaves.cpp glbs.cpp interpreter.cpp diff --git a/test/gtest/int128.cpp b/test/gtest/int128.cpp new file mode 100644 index 00000000000..36e0e871153 --- /dev/null +++ b/test/gtest/int128.cpp @@ -0,0 +1,107 @@ +// Copyright 2026 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. + +#include "support/int128.h" +#include "gtest/gtest.h" +#include + +using namespace wasm; + +class Int128MulWideSTest + : public ::testing::TestWithParam {}; + +TEST_P(Int128MulWideSTest, Basic) { + auto mul_s = GetParam(); + + // Simple cases + EXPECT_EQ(mul_s(0, 0), (Int128{0, 0})); + EXPECT_EQ(mul_s(1, 1), (Int128{0, 1})); + + // Mixed sign + EXPECT_EQ(mul_s(-1, 1), + (Int128{0xffffffffffffffffULL, 0xffffffffffffffffULL})); + EXPECT_EQ(mul_s(1, -1), + (Int128{0xffffffffffffffffULL, 0xffffffffffffffffULL})); + + // Double negative + EXPECT_EQ(mul_s(-1, -1), (Int128{0, 1})); + + int64_t maxInt = std::numeric_limits::max(); + // Fits in the lower half because the signed bit now goes in the upper half. + EXPECT_EQ(mul_s(maxInt, 2), (Int128{0, 0xfffffffffffffffeULL})); + EXPECT_EQ(mul_s(maxInt, maxInt), (Int128{0x3fffffffffffffffULL, 1})); + + // Min Ints (0x8000000000000000) + int64_t minInt = std::numeric_limits::min(); + EXPECT_EQ(mul_s(minInt, 2), (Int128{0xffffffffffffffffULL, 0})); + EXPECT_EQ(mul_s(minInt, minInt), (Int128{0x4000000000000000ULL, 0})); +} + +// Test that our fallback implementation is commutative +Int128 mul_wide_s_fallback_reversed(uint64_t lhs, uint64_t rhs) { + return detail::mul_wide_s_fallback(rhs, lhs); +} + +INSTANTIATE_TEST_SUITE_P(Int128, + Int128MulWideSTest, + ::testing::Values(mul_wide_s, + detail::mul_wide_s_fallback, + mul_wide_s_fallback_reversed)); + +class Int128MulWideUTest + : public ::testing::TestWithParam {}; + +TEST_P(Int128MulWideUTest, Basic) { + auto mul_u = GetParam(); + + // Simple cases + EXPECT_EQ(mul_u(0, 0), (Int128{0, 0})); + EXPECT_EQ(mul_u(1, 0), (Int128{0, 0})); + EXPECT_EQ(mul_u(1, 1), (Int128{0, 1})); + + // Max Uint (0xffffffffffffffff) + uint64_t maxUint = std::numeric_limits::max(); + EXPECT_EQ(mul_u(maxUint, 2), (Int128{1, 0xfffffffffffffffeULL})); + EXPECT_EQ(mul_u(maxUint, maxUint), (Int128{0xfffffffffffffffeULL, 1})); + + // Max 32-bit uint (0xffffffff) + EXPECT_EQ(mul_u(0xffffffffULL, 0xffffffffULL), + (Int128{0, 0xfffffffe00000001ULL})); + + // Exactly 2^32 (0x100000000) - Tests a 1 in the lowest bit of the high half + EXPECT_EQ(mul_u(0x100000000ULL, 0x100000000ULL), (Int128{1, 0})); + + // Mixed boundaries + EXPECT_EQ(mul_u(0xffffffffULL, 0x100000000ULL), + (Int128{0, 0xffffffff00000000ULL})); + + // Upper half filled, lower half empty + uint64_t highOnly = 0xffffffff00000000ULL; + EXPECT_EQ(mul_u(highOnly, 2), (Int128{1, 0xfffffffe00000000ULL})); + + // Lower half filled, upper half empty + uint64_t lowOnly = 0x00000000ffffffffULL; + EXPECT_EQ(mul_u(lowOnly, lowOnly), (Int128{0, 0xfffffffe00000001ULL})); +} + +// Test that our fallback implementation is commutative +Int128 mul_wide_u_fallback_reversed(uint64_t lhs, uint64_t rhs) { + return detail::mul_wide_u_fallback(rhs, lhs); +} + +INSTANTIATE_TEST_SUITE_P(Int128, + Int128MulWideUTest, + ::testing::Values(mul_wide_u, + detail::mul_wide_u_fallback, + mul_wide_u_fallback_reversed)); diff --git a/test/spec/wide-arithmetic.wast b/test/spec/wide-arithmetic.wast deleted file mode 100644 index 8c0cca7c368..00000000000 --- a/test/spec/wide-arithmetic.wast +++ /dev/null @@ -1,310 +0,0 @@ -;; Ported from the upstream proposal's tests. -;; TODO: enable the proposal's testsuite and delete this. - -(module - (func (export "i64.add128") (param i64 i64 i64 i64) (result i64 i64) - local.get 0 - local.get 1 - local.get 2 - local.get 3 - i64.add128) - (func (export "i64.sub128") (param i64 i64 i64 i64) (result i64 i64) - local.get 0 - local.get 1 - local.get 2 - local.get 3 - i64.sub128) -) - -;; simple addition -(assert_return (invoke "i64.add128" - (i64.const 0) (i64.const 0) - (i64.const 0) (i64.const 0)) - (i64.const 0) (i64.const 0)) -(assert_return (invoke "i64.add128" - (i64.const 0) (i64.const 1) - (i64.const 1) (i64.const 0)) - (i64.const 1) (i64.const 1)) -(assert_return (invoke "i64.add128" - (i64.const 1) (i64.const 0) - (i64.const -1) (i64.const 0)) - (i64.const 0) (i64.const 1)) -(assert_return (invoke "i64.add128" - (i64.const 1) (i64.const 1) - (i64.const -1) (i64.const -1)) - (i64.const 0) (i64.const 1)) - -;; simple subtraction -(assert_return (invoke "i64.sub128" - (i64.const 0) (i64.const 0) - (i64.const 0) (i64.const 0)) - (i64.const 0) (i64.const 0)) -(assert_return (invoke "i64.sub128" - (i64.const 0) (i64.const 0) - (i64.const 1) (i64.const 0)) - (i64.const -1) (i64.const -1)) -(assert_return (invoke "i64.sub128" - (i64.const 0) (i64.const 1) - (i64.const 1) (i64.const 1)) - (i64.const -1) (i64.const -1)) -(assert_return (invoke "i64.sub128" - (i64.const 0) (i64.const 0) - (i64.const 1) (i64.const 1)) - (i64.const -1) (i64.const -2)) - -;; 20 randomly generated test cases for i64.add128 -(assert_return (invoke "i64.add128" - (i64.const -2418420703207364752) (i64.const -1) - (i64.const -1) (i64.const -1)) - (i64.const -2418420703207364753) (i64.const -1)) -(assert_return (invoke "i64.add128" - (i64.const 0) (i64.const 0) - (i64.const -4579433644172935106) (i64.const -1)) - (i64.const -4579433644172935106) (i64.const -1)) -(assert_return (invoke "i64.add128" - (i64.const 0) (i64.const 0) - (i64.const 1) (i64.const -1)) - (i64.const 1) (i64.const -1)) -(assert_return (invoke "i64.add128" - (i64.const 1) (i64.const 0) - (i64.const 1) (i64.const 0)) - (i64.const 2) (i64.const 0)) -(assert_return (invoke "i64.add128" - (i64.const -1) (i64.const -1) - (i64.const -1) (i64.const -1)) - (i64.const -2) (i64.const -1)) -(assert_return (invoke "i64.add128" - (i64.const 0) (i64.const -1) - (i64.const 1) (i64.const 0)) - (i64.const 1) (i64.const -1)) -(assert_return (invoke "i64.add128" - (i64.const 0) (i64.const 0) - (i64.const 0) (i64.const -1)) - (i64.const 0) (i64.const -1)) -(assert_return (invoke "i64.add128" - (i64.const 1) (i64.const 0) - (i64.const -1) (i64.const -1)) - (i64.const 0) (i64.const 0)) -(assert_return (invoke "i64.add128" - (i64.const 0) (i64.const 6184727276166606191) - (i64.const 0) (i64.const 1)) - (i64.const 0) (i64.const 6184727276166606192)) -(assert_return (invoke "i64.add128" - (i64.const -8434911321912688222) (i64.const -1) - (i64.const 1) (i64.const -1)) - (i64.const -8434911321912688221) (i64.const -2)) -(assert_return (invoke "i64.add128" - (i64.const 1) (i64.const -1) - (i64.const 0) (i64.const -1)) - (i64.const 1) (i64.const -2)) -(assert_return (invoke "i64.add128" - (i64.const 1) (i64.const -5148941131328838092) - (i64.const 0) (i64.const 0)) - (i64.const 1) (i64.const -5148941131328838092)) -(assert_return (invoke "i64.add128" - (i64.const 1) (i64.const 1) - (i64.const 1) (i64.const 0)) - (i64.const 2) (i64.const 1)) -(assert_return (invoke "i64.add128" - (i64.const -1) (i64.const -1) - (i64.const -3636740005180858631) (i64.const -1)) - (i64.const -3636740005180858632) (i64.const -1)) -(assert_return (invoke "i64.add128" - (i64.const -5529682780229988275) (i64.const -1) - (i64.const 0) (i64.const 0)) - (i64.const -5529682780229988275) (i64.const -1)) -(assert_return (invoke "i64.add128" - (i64.const 1) (i64.const -5381447440966559717) - (i64.const 1020031372481336745) (i64.const 1)) - (i64.const 1020031372481336746) (i64.const -5381447440966559716)) -(assert_return (invoke "i64.add128" - (i64.const 1) (i64.const 1) - (i64.const 0) (i64.const 0)) - (i64.const 1) (i64.const 1)) -(assert_return (invoke "i64.add128" - (i64.const -9133888546939907356) (i64.const -1) - (i64.const 1) (i64.const 1)) - (i64.const -9133888546939907355) (i64.const 0)) -(assert_return (invoke "i64.add128" - (i64.const -4612047512704241719) (i64.const -1) - (i64.const 0) (i64.const -1)) - (i64.const -4612047512704241719) (i64.const -2)) -(assert_return (invoke "i64.add128" - (i64.const 414720966820876428) (i64.const -1) - (i64.const 1) (i64.const 0)) - (i64.const 414720966820876429) (i64.const -1)) - - -;; 20 randomly generated test cases for i64.sub128 -(assert_return (invoke "i64.sub128" - (i64.const 0) (i64.const -2459085471354756766) - (i64.const -9151153060221070927) (i64.const -1)) - (i64.const 9151153060221070927) (i64.const -2459085471354756766)) -(assert_return (invoke "i64.sub128" - (i64.const 4566502638724063423) (i64.const -4282658540409485563) - (i64.const -6884077310018979971) (i64.const -1)) - (i64.const -6996164124966508222) (i64.const -4282658540409485563)) -(assert_return (invoke "i64.sub128" - (i64.const 1) (i64.const 3118380319444903041) - (i64.const 0) (i64.const 3283115686417695443)) - (i64.const 1) (i64.const -164735366972792402)) -(assert_return (invoke "i64.sub128" - (i64.const -7208415241680161810) (i64.const -1) - (i64.const 1) (i64.const 0)) - (i64.const -7208415241680161811) (i64.const -1)) -(assert_return (invoke "i64.sub128" - (i64.const 0) (i64.const 3944850126731328706) - (i64.const 1) (i64.const 1)) - (i64.const -1) (i64.const 3944850126731328704)) -(assert_return (invoke "i64.sub128" - (i64.const 1) (i64.const -1) - (i64.const -1) (i64.const -1)) - (i64.const 2) (i64.const -1)) -(assert_return (invoke "i64.sub128" - (i64.const -1) (i64.const -1) - (i64.const 4855833073346115923) (i64.const -6826437637438999645)) - (i64.const -4855833073346115924) (i64.const 6826437637438999644)) -(assert_return (invoke "i64.sub128" - (i64.const 1) (i64.const 0) - (i64.const -1) (i64.const -1)) - (i64.const 2) (i64.const 0)) -(assert_return (invoke "i64.sub128" - (i64.const 1) (i64.const 0) - (i64.const 1) (i64.const 0)) - (i64.const 0) (i64.const 0)) -(assert_return (invoke "i64.sub128" - (i64.const -1) (i64.const -1) - (i64.const 0) (i64.const 0)) - (i64.const -1) (i64.const -1)) -(assert_return (invoke "i64.sub128" - (i64.const 1) (i64.const -1) - (i64.const -6365475388498096428) (i64.const -1)) - (i64.const 6365475388498096429) (i64.const -1)) -(assert_return (invoke "i64.sub128" - (i64.const 6804238617560992346) (i64.const -1) - (i64.const 0) (i64.const -1)) - (i64.const 6804238617560992346) (i64.const 0)) -(assert_return (invoke "i64.sub128" - (i64.const 0) (i64.const 1) - (i64.const 1) (i64.const -7756145513466453619)) - (i64.const -1) (i64.const 7756145513466453619)) -(assert_return (invoke "i64.sub128" - (i64.const 1) (i64.const -1) - (i64.const 1) (i64.const 1)) - (i64.const 0) (i64.const -2)) -(assert_return (invoke "i64.sub128" - (i64.const 0) (i64.const 1) - (i64.const 1) (i64.const 0)) - (i64.const -1) (i64.const 0)) -(assert_return (invoke "i64.sub128" - (i64.const 1) (i64.const 5602881641763648953) - (i64.const -2110589244314239080) (i64.const -1)) - (i64.const 2110589244314239081) (i64.const 5602881641763648953)) -(assert_return (invoke "i64.sub128" - (i64.const 0) (i64.const 1) - (i64.const -1) (i64.const -1)) - (i64.const 1) (i64.const 1)) -(assert_return (invoke "i64.sub128" - (i64.const 0) (i64.const -1) - (i64.const 3553816990259121806) (i64.const -2105235417856431622)) - (i64.const -3553816990259121806) (i64.const 2105235417856431620)) -(assert_return (invoke "i64.sub128" - (i64.const 1861102705894987245) (i64.const 1) - (i64.const 3713781778534059871) (i64.const 1)) - (i64.const -1852679072639072626) (i64.const -1)) -(assert_return (invoke "i64.sub128" - (i64.const 0) (i64.const -1) - (i64.const 1) (i64.const 1832524486821761762)) - (i64.const -1) (i64.const -1832524486821761764)) - -;; assert overlong encodings for each instruction's binary encoding are accepted -(module binary - "\00asm" "\01\00\00\00" - - "\01\0a" ;; type section, 10 bytes - "\01" ;; 1 count - "\60" ;; type0 = function - "\04\7e\7e\7e\7e" ;; 4 params - all i64 - "\02\7e\7e" ;; 2 results - both i64 - - "\03\03" ;; function section, 3 bytes - "\02" ;; 2 count - "\00\00" ;; types of each function (0, 0) - - "\07\1b" ;; export section 0x1b bytes - "\02" ;; 2 count - "\0ai64.add128\00\00" ;; "i64.add128" which is function 0 - "\0ai64.sub128\00\01" ;; "i64.sub128" which is function 1 - - "\0a\1e" ;; code section + byte length (30 bytes = 0x1e) - "\02" ;; 2 count - - "\0e" ;; byte length - "\00" ;; no locals - "\20\00" ;; local.get 0 - "\20\01" ;; local.get 1 - "\20\02" ;; local.get 2 - "\20\03" ;; local.get 3 - "\fc\93\80\00" ;; i64.add128 (overlong) - "\0b" ;; end - - "\0d" ;; byte length - "\00" ;; no locals - "\20\00" ;; local.get 0 - "\20\01" ;; local.get 1 - "\20\02" ;; local.get 2 - "\20\03" ;; local.get 3 - "\fc\94\00" ;; i64.sub128 (overlong) - "\0b" ;; end -) - -(assert_return (invoke "i64.add128" - (i64.const 1) (i64.const 2) - (i64.const 3) (i64.const 4)) - (i64.const 4) (i64.const 6)) -(assert_return (invoke "i64.sub128" - (i64.const 2) (i64.const 5) - (i64.const 1) (i64.const 2)) - (i64.const 1) (i64.const 3)) - -;; some invalid types for these instructions - -(assert_invalid - (module - (func (param i64 i64 i64 i64) (result i64) - local.get 0 - local.get 1 - local.get 2 - local.get 3 - i64.add128) - ) - "type mismatch") -(assert_invalid - (module - (func (param i64 i64 i64) (result i64 i64) - local.get 0 - local.get 1 - local.get 2 - i64.add128) - ) - "type mismatch") - -(assert_invalid - (module - (func (param i64 i64 i64 i64) (result i64) - local.get 0 - local.get 1 - local.get 2 - local.get 3 - i64.sub128) - ) - "type mismatch") -(assert_invalid - (module - (func (param i64 i64 i64) (result i64 i64) - local.get 0 - local.get 1 - local.get 2 - i64.sub128) - ) - "type mismatch") From 35ca7ce5eeea5fa8b710fef11c61365c486939e9 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 4 May 2026 15:04:17 -0700 Subject: [PATCH 071/168] [NFC] Refactor delta debugger utility to a struct (#8651) The delta debugging utility was previously just a free function that took a lambda for testing a partition and saying whether it worked or not. The control flow was entirely in the control of the utility itself, which meant that things like exiting early needed to use exceptions. The user was also not able to e.g. dynamically add new items to the set being reduced. Give users more control and flexibility by refactoring the delta debugging utility into a struct that implements the algorithm as a state machine. The struct provides the current working and test sets, and the user says whether the current test set should be accepted or rejected. --- src/support/delta_debugging.h | 200 ++++++++++++++++---------- src/tools/wasm-reduce/wasm-reduce.cpp | 127 ++++++++-------- test/gtest/delta_debugging.cpp | 195 ++++++++++++++++++------- 3 files changed, 326 insertions(+), 196 deletions(-) diff --git a/src/support/delta_debugging.h b/src/support/delta_debugging.h index 9607d2011d0..7cdafc32554 100644 --- a/src/support/delta_debugging.h +++ b/src/support/delta_debugging.h @@ -21,100 +21,150 @@ #include #include +#include "support/index.h" namespace wasm { -// Use the delta debugging algorithm (Zeller 1999, +// Use the delta debugging algorithm (Zeller 2002, // https://dl.acm.org/doi/10.1109/32.988498) to find the minimal set of -// items necessary to preserve some property. Returns that minimal set of -// items, preserving their input order. `tryPartition` should have this -// signature: -// -// bool tryPartition(size_t partitionIndex, -// size_t numPartitions, -// const std::vector& partition) -// -// It should return true iff the property is preserved while keeping only -// `partition` items. -template -std::vector deltaDebugging(std::vector items, const F& tryPartition) { - if (items.empty()) { - return items; +// items necessary to preserve some property. `working` is the minimal set of +// items found so far and `test` is the smaller set of items that should be +// tested next. After testing, call `accept()`, `reject()`, or `resolve(bool +// accepted)` to update the working and test sets appropriately. +template struct DeltaDebugger { + std::vector working; + std::vector test; + +private: + Index numPartitions = 1; + Index currentPartition = 0; + bool testingComplements = false; + bool triedEmpty = false; + bool isFinished = false; + std::vector> partitions; + +public: + DeltaDebugger(std::vector items) : working(std::move(items)) {} + + bool finished() const { + return isFinished || (triedEmpty && working.size() <= 1); } - // First try removing everything. - if (tryPartition(0, 1, {})) { - return {}; + Index partitionCount() { return numPartitions; } + Index partitionIndex() { return currentPartition; } + + void accept() { + if (finished()) { + return; + } + + if (test.empty()) { + triedEmpty = true; + } + + working = std::move(test); + + // We might be finished now even if we weren't before. + if (finished()) { + return; + } + + if (!testingComplements) { + numPartitions = 2; + } else { + numPartitions = std::max(numPartitions - 1, Index(2)); + } + testingComplements = false; + currentPartition = 0; + updateTest(); } - size_t numPartitions = 2; - while (numPartitions <= items.size()) { - // Partition the items. - std::vector> partitions; - size_t size = items.size(); - size_t basePartitionSize = size / numPartitions; - size_t rem = size % numPartitions; - size_t idx = 0; - for (size_t i = 0; i < numPartitions; ++i) { - size_t partitionSize = basePartitionSize + (i < rem ? 1 : 0); - if (partitionSize > 0) { - std::vector partition; - partition.reserve(partitionSize); - for (size_t j = 0; j < partitionSize; ++j) { - partition.push_back(items[idx++]); + + void reject() { + if (test.empty()) { + triedEmpty = true; + numPartitions = 2; + updateTest(); + return; + } + + if (finished()) { + return; + } + + ++currentPartition; + if (currentPartition >= partitions.size()) { + // No need to test complements if there are only two partitions, since + // that is no different. + if (!testingComplements && numPartitions > 2) { + testingComplements = true; + currentPartition = 0; + } else { + if (numPartitions >= working.size()) { + isFinished = true; + return; } - partitions.emplace_back(std::move(partition)); + // Refine the partitions. + numPartitions = std::min(Index(working.size()), 2 * numPartitions); + testingComplements = false; + currentPartition = 0; } } - assert(numPartitions == partitions.size()); + updateTest(); + } - bool reduced = false; + // Convenience wrapper for when there is already a bool determining whether to + // accept or reject the current test sequence. + void resolve(bool success) { + if (success) { + accept(); + } else { + reject(); + } + } - // Try keeping only one partition. Try each partition in turn. - for (size_t i = 0; i < numPartitions; ++i) { - if (tryPartition(i, numPartitions, partitions[i])) { - items = std::move(partitions[i]); - numPartitions = 2; - reduced = true; - break; - } +private: + void updateTest() { + if (finished()) { + test.clear(); + return; } - if (reduced) { - continue; + + if (currentPartition == 0 && !testingComplements) { + generatePartitions(); } - // Otherwise, try keeping the complement of a partition. Do not do this with - // only two partitions because that would be no different from what we - // already tried. - if (numPartitions > 2) { - for (size_t i = 0; i < numPartitions; ++i) { - std::vector complement; - complement.reserve(items.size() - partitions[i].size()); - for (size_t j = 0; j < numPartitions; ++j) { - if (j != i) { - complement.insert( - complement.end(), partitions[j].begin(), partitions[j].end()); - } - } - if (tryPartition(i, numPartitions, complement)) { - items = std::move(complement); - numPartitions = std::max(numPartitions - 1, size_t(2)); - reduced = true; - break; + if (!testingComplements) { + test = partitions[currentPartition]; + } else { + test.clear(); + test.reserve(working.size() - partitions[currentPartition].size()); + for (size_t i = 0; i < partitions.size(); ++i) { + if (i != currentPartition) { + test.insert(test.end(), partitions[i].begin(), partitions[i].end()); } } - if (reduced) { - continue; - } } + } - if (numPartitions == items.size()) { - // Cannot further refine the partitions. We're done. - break; - } + void generatePartitions() { + partitions.clear(); + size_t size = working.size(); + assert(numPartitions != 0 && numPartitions <= size); - // Otherwise, make the partitions finer grained. - numPartitions = std::min(items.size(), 2 * numPartitions); + size_t basePartitionSize = size / numPartitions; + size_t rem = size % numPartitions; + size_t idx = 0; + for (size_t i = 0; i < numPartitions; ++i) { + size_t partitionSize = basePartitionSize + (i < rem ? 1 : 0); + if (partitionSize > 0) { + std::vector partition; + partition.reserve(partitionSize); + for (size_t j = 0; j < partitionSize; ++j) { + partition.push_back(working[idx++]); + } + partitions.emplace_back(std::move(partition)); + } + } } - return items; -} +}; } // namespace wasm diff --git a/src/tools/wasm-reduce/wasm-reduce.cpp b/src/tools/wasm-reduce/wasm-reduce.cpp index fbb12d6766f..f9cd7b64412 100644 --- a/src/tools/wasm-reduce/wasm-reduce.cpp +++ b/src/tools/wasm-reduce/wasm-reduce.cpp @@ -918,78 +918,67 @@ struct Reducer } nontrivialFuncIndices.push_back(i); } - // TODO: Use something other than an exception to implement early return. - struct EarlyReturn {}; - try { - deltaDebugging( - nontrivialFuncIndices, - [&](Index partitionIndex, - Index numPartitions, - const std::vector& partition) { - // Stop early if the partition size is less than the square root of - // the remaining set. We don't want to waste time on very fine-grained - // partitions when we could switch to another reduction strategy - // instead. - if (size_t sqrtRemaining = std::sqrt(nontrivialFuncIndices.size()); - partition.size() > 0 && partition.size() < sqrtRemaining) { - throw EarlyReturn{}; - } + DeltaDebugger dd(std::move(nontrivialFuncIndices)); + while (!dd.finished()) { + // Stop early if the partition size is less than the square root of + // the remaining set. We don't want to waste time on very fine-grained + // partitions when we could switch to another reduction strategy + // instead. + if (size_t sqrtRemaining = std::sqrt(dd.working.size()); + dd.test.size() > 0 && dd.test.size() < sqrtRemaining) { + break; + } - std::cerr << "| try partition " << partitionIndex + 1 << " / " - << numPartitions << " (size " << partition.size() << ")\n"; - Index removedSize = nontrivialFuncIndices.size() - partition.size(); - std::vector oldBodies(removedSize); - - // We first need to remove each non-kept function body, and later we - // might need to restore the same function bodies. Abstract the logic - // for iterating over these function bodies. `f` takes a Function* and - // Expression*& for the stashed body. - auto forEachRemovedFuncBody = [&](auto f) { - Index bodyIndex = 0; - Index nontrivialIndex = 0; - Index partitionIndex = 0; - while (nontrivialIndex < nontrivialFuncIndices.size()) { - if (partitionIndex < partition.size() && - nontrivialFuncIndices[nontrivialIndex] == - partition[partitionIndex]) { - // Kept, skip it. - nontrivialIndex++; - partitionIndex++; - } else { - // Removed, process it - Index funcIndex = nontrivialFuncIndices[nontrivialIndex++]; - f(module->functions[funcIndex].get(), oldBodies[bodyIndex++]); - } - } - assert(bodyIndex == removedSize); - assert(partitionIndex == partition.size()); - }; - - // Stash the bodies. - forEachRemovedFuncBody([&](Function* func, Expression*& oldBody) { - oldBody = func->body; - Builder builder(*module); - if (func->getResults() == Type::none) { - func->body = builder.makeNop(); - } else { - func->body = builder.makeUnreachable(); - } - }); - - if (!writeAndTestReduction()) { - // Failure. Restore the bodies. - forEachRemovedFuncBody([](Function* func, Expression*& oldBody) { - func->body = oldBody; - }); - return false; + std::cerr << "| try partition " << dd.partitionIndex() + 1 << " / " + << dd.partitionCount() << " (size " << dd.test.size() << ")\n"; + Index removedSize = dd.working.size() - dd.test.size(); + std::vector oldBodies(removedSize); + + // We first need to remove each non-kept function body, and later we + // might need to restore the same function bodies. Abstract the logic + // for iterating over these function bodies. `f` takes a Function* and + // Expression*& for the stashed body. + auto forEachRemovedFuncBody = [&](auto f) { + Index bodyIndex = 0; + Index workingIndex = 0; + Index testIndex = 0; + while (workingIndex < dd.working.size()) { + if (testIndex < dd.test.size() && + dd.working[workingIndex] == dd.test[testIndex]) { + // Kept, skip it. + workingIndex++; + testIndex++; + } else { + // Removed, process it + Index funcIndex = dd.working[workingIndex++]; + f(module->functions[funcIndex].get(), oldBodies[bodyIndex++]); } + } + assert(bodyIndex == removedSize); + assert(testIndex == dd.test.size()); + }; + + // Stash the bodies. + forEachRemovedFuncBody([&](Function* func, Expression*& oldBody) { + oldBody = func->body; + Builder builder(*module); + if (func->getResults() == Type::none) { + func->body = builder.makeNop(); + } else { + func->body = builder.makeUnreachable(); + } + }); - // Success! - noteReduction(removedSize); - nontrivialFuncIndices = partition; - return true; - }); - } catch (EarlyReturn) { + if (!writeAndTestReduction()) { + // Failure. Restore the bodies. + forEachRemovedFuncBody( + [](Function* func, Expression*& oldBody) { func->body = oldBody; }); + dd.reject(); + } else { + // Success! + noteReduction(removedSize); + dd.accept(); + } } } diff --git a/test/gtest/delta_debugging.cpp b/test/gtest/delta_debugging.cpp index 7e4c8ad4db5..7f33d958905 100644 --- a/test/gtest/delta_debugging.cpp +++ b/test/gtest/delta_debugging.cpp @@ -8,90 +8,181 @@ using namespace wasm; TEST(DeltaDebuggingTest, EmptyInput) { std::vector items; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector&) { return false; }); - EXPECT_TRUE(result.empty()); + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(false); + } + EXPECT_TRUE(dd.working.empty()); +} + +TEST(DeltaDebuggingTest, SingleInputEmptySetWorks) { + std::vector items = {42}; + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(dd.test.empty()); + } + EXPECT_TRUE(dd.working.empty()); +} + +TEST(DeltaDebuggingTest, SingleInputEmptySetFails) { + std::vector items = {42}; + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(!dd.test.empty()); + } + std::vector expected = {42}; + EXPECT_EQ(dd.working, expected); } TEST(DeltaDebuggingTest, SingleItem) { std::vector items = {0, 1, 2, 3, 4, 5, 6, 7}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector& partition) { - return std::find(partition.begin(), partition.end(), 3) != - partition.end(); - }); + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(std::find(dd.test.begin(), dd.test.end(), 3) != dd.test.end()); + } std::vector expected = {3}; - EXPECT_EQ(result, expected); + EXPECT_EQ(dd.working, expected); } TEST(DeltaDebuggingTest, MultipleItemsAdjacent) { std::vector items = {0, 1, 2, 3, 4, 5, 6, 7}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector& partition) { - bool has2 = - std::find(partition.begin(), partition.end(), 2) != partition.end(); - bool has3 = - std::find(partition.begin(), partition.end(), 3) != partition.end(); - return has2 && has3; - }); + DeltaDebugger dd(items); + while (!dd.finished()) { + bool has2 = std::find(dd.test.begin(), dd.test.end(), 2) != dd.test.end(); + bool has3 = std::find(dd.test.begin(), dd.test.end(), 3) != dd.test.end(); + dd.resolve(has2 && has3); + } std::vector expected = {2, 3}; - EXPECT_EQ(result, expected); + EXPECT_EQ(dd.working, expected); } TEST(DeltaDebuggingTest, MultipleItemsNonAdjacent) { std::vector items = {0, 1, 2, 3, 4, 5, 6, 7}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector& partition) { - bool has2 = - std::find(partition.begin(), partition.end(), 2) != partition.end(); - bool has5 = - std::find(partition.begin(), partition.end(), 5) != partition.end(); - return has2 && has5; - }); + DeltaDebugger dd(items); + while (!dd.finished()) { + bool has2 = std::find(dd.test.begin(), dd.test.end(), 2) != dd.test.end(); + bool has5 = std::find(dd.test.begin(), dd.test.end(), 5) != dd.test.end(); + dd.resolve(has2 && has5); + } std::vector expected = {2, 5}; - EXPECT_EQ(result, expected); + EXPECT_EQ(dd.working, expected); } TEST(DeltaDebuggingTest, OrderMaintained) { std::vector items = {3, 1, 4, 2}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector& partition) { - bool has3 = - std::find(partition.begin(), partition.end(), 3) != partition.end(); - bool has2 = - std::find(partition.begin(), partition.end(), 2) != partition.end(); - return has3 && has2; - }); + DeltaDebugger dd(items); + while (!dd.finished()) { + bool has3 = std::find(dd.test.begin(), dd.test.end(), 3) != dd.test.end(); + bool has2 = std::find(dd.test.begin(), dd.test.end(), 2) != dd.test.end(); + dd.resolve(has3 && has2); + } std::vector expected = {3, 2}; - EXPECT_EQ(result, expected); + EXPECT_EQ(dd.working, expected); } TEST(DeltaDebuggingTest, DifferentTypes) { std::vector items = {"apple", "banana", "cherry", "date"}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector& partition) { - bool hasBanana = - std::find(partition.begin(), partition.end(), "banana") != - partition.end(); - bool hasDate = std::find(partition.begin(), partition.end(), "date") != - partition.end(); - return hasBanana && hasDate; - }); + DeltaDebugger dd(items); + while (!dd.finished()) { + bool hasBanana = + std::find(dd.test.begin(), dd.test.end(), "banana") != dd.test.end(); + bool hasDate = + std::find(dd.test.begin(), dd.test.end(), "date") != dd.test.end(); + dd.resolve(hasBanana && hasDate); + } std::vector expected = {"banana", "date"}; - EXPECT_EQ(result, expected); + EXPECT_EQ(dd.working, expected); } TEST(DeltaDebuggingTest, UnconditionallyTrue) { std::vector items = {0, 1, 2, 3}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector&) { return true; }); - EXPECT_TRUE(result.empty()); + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(true); + } + EXPECT_TRUE(dd.working.empty()); } TEST(DeltaDebuggingTest, UnconditionallyFalse) { std::vector items = {0, 1, 2, 3}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector&) { return false; }); + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(false); + } std::vector expected = {0, 1, 2, 3}; - EXPECT_EQ(result, expected); + EXPECT_EQ(dd.working, expected); +} + +TEST(DeltaDebuggingTest, StructBasic) { + std::vector items = {0, 1, 2, 3, 4, 5, 6, 7}; + DeltaDebugger dd(items); + while (!dd.finished()) { + bool has3 = std::find(dd.test.begin(), dd.test.end(), 3) != dd.test.end(); + dd.resolve(has3); + } + std::vector expected = {3}; + EXPECT_EQ(dd.working, expected); +} + +TEST(DeltaDebuggingTest, HierarchicalExample) { + std::vector> items = {{1, 10}, {2, 10}, {3, 10}}; + + auto testProperty = [](const std::vector>& lists) { + int sum = 0; + for (const auto& list : lists) { + for (int x : list) { + sum += x; + } + } + return sum >= 20; + }; + + DeltaDebugger> dd(items); + while (!dd.finished()) { + dd.resolve(testProperty(dd.test)); + } + + std::vector> currentLists = dd.working; + + for (size_t i = 0; i < currentLists.size(); ++i) { + std::vector currentList = currentLists[i]; + DeltaDebugger subDd(currentList); + + while (!subDd.finished()) { + std::vector> fullTestSet; + for (size_t j = 0; j < currentLists.size(); ++j) { + if (j == i) { + fullTestSet.push_back(subDd.test); + } else { + fullTestSet.push_back(currentLists[j]); + } + } + subDd.resolve(testProperty(fullTestSet)); + } + currentLists[i] = subDd.working; + } + + std::vector> expected = {{10}, {10}}; + EXPECT_EQ(currentLists, expected); +} + +TEST(DeltaDebuggingTest, ResolveAfterFinished) { + std::vector items = {0, 1, 2, 3}; + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(false); + } + + std::vector expected = {0, 1, 2, 3}; + EXPECT_EQ(dd.working, expected); + EXPECT_TRUE(dd.finished()); + + // Call resolve again + dd.resolve(true); + EXPECT_EQ(dd.working, expected); + EXPECT_TRUE(dd.finished()); + + dd.resolve(false); + EXPECT_EQ(dd.working, expected); + EXPECT_TRUE(dd.finished()); } From 1abcee9eb0842f907ab0530fe263030710fa90ce Mon Sep 17 00:00:00 2001 From: juj Date: Tue, 5 May 2026 10:38:24 +0300 Subject: [PATCH 072/168] Avoid use of C++20 header in GlobalEffects.cpp (#8668) Avoid use of C++20 header in GlobalEffects.cpp, so that the code builds on Clang-14 as well. --- src/passes/GlobalEffects.cpp | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 1625b551322..ca82b2b3aea 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -19,8 +19,6 @@ // PassOptions structure; see more details there. // -#include - #include "ir/effects.h" #include "ir/module-utils.h" #include "pass.h" @@ -234,26 +232,26 @@ void propagateEffects(const Module& module, // We only care about Functions that are roots, not types. // A type would be a root if a function exists with that type, but no-one // indirect calls the type. - auto funcNodes = std::views::keys(callGraph) | - std::views::filter([](auto node) { - return std::holds_alternative(node); - }) | - std::views::common; - using funcNodesType = decltype(funcNodes); + std::vector funcNodes; + for (const auto& [node, _] : callGraph) { + if (std::holds_alternative(node)) { + funcNodes.push_back(node); + } + } struct CallGraphSCCs - : SCCs, CallGraphSCCs> { + : SCCs::iterator, CallGraphSCCs> { const std::map& funcInfos; const CallGraph& callGraph; const Module& module; - CallGraphSCCs(funcNodesType&& nodes, + CallGraphSCCs(std::vector& nodes, const std::map& funcInfos, const CallGraph& callGraph, const Module& module) - : SCCs, CallGraphSCCs>( - std::ranges::begin(nodes), std::ranges::end(nodes)), + : SCCs::iterator, CallGraphSCCs>(nodes.begin(), + nodes.end()), funcInfos(funcInfos), callGraph(callGraph), module(module) {} void pushChildren(CallGraphNode node) { @@ -262,7 +260,7 @@ void propagateEffects(const Module& module, } } }; - CallGraphSCCs sccs(std::move(funcNodes), funcInfos, callGraph, module); + CallGraphSCCs sccs(funcNodes, funcInfos, callGraph, module); std::vector> componentEffects; // Points to an index in componentEffects From d675fbf125c729ca7ad5464d63b7db37a4a392b6 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 5 May 2026 08:04:36 -0700 Subject: [PATCH 073/168] table-utils: Handle table.grow (#8671) This instruction was just not handled. That was mostly ok, as growth only appends, but we did miscompile in some cases: if we call a high index in the table, growth might save us from trapping, but we assumed we still trap in Directize. --- src/ir/table-utils.cpp | 47 ++++--- src/ir/table-utils.h | 19 ++- src/passes/Directize.cpp | 13 +- src/passes/RemoveUnusedModuleElements.cpp | 6 +- test/lit/passes/directize_all-features.wast | 89 ++++++++++++++ .../remove-unused-module-elements-tables.wast | 115 +++++++++++++++++- 6 files changed, 262 insertions(+), 27 deletions(-) diff --git a/src/ir/table-utils.cpp b/src/ir/table-utils.cpp index cb10aff82b9..516b12a215b 100644 --- a/src/ir/table-utils.cpp +++ b/src/ir/table-utils.cpp @@ -95,13 +95,13 @@ TableInfoMap computeTableInfo(Module& wasm, bool initialContentsImmutable) { for (auto& table : wasm.tables) { if (table->imported()) { - tables[table->name].mayBeModified = true; + tables[table->name].hasSet = true; } } for (auto& ex : wasm.exports) { if (ex->kind == ExternalKind::Table) { - tables[*ex->getInternalName()].mayBeModified = true; + tables[*ex->getInternalName()].hasSet = true; } } @@ -109,7 +109,7 @@ TableInfoMap computeTableInfo(Module& wasm, bool initialContentsImmutable) { // might learn anything new. auto hasUnmodifiableTable = false; for (auto& [_, info] : tables) { - if (!info.mayBeModified) { + if (!info.hasSet) { hasUnmodifiableTable = true; break; } @@ -118,39 +118,54 @@ TableInfoMap computeTableInfo(Module& wasm, bool initialContentsImmutable) { return tables; } - using TablesWithSet = std::unordered_set; + // Miniature form of TableInfo, without things we don't need (some of which + // cause compilation errors on the copies below). + struct MiniTableInfo { + bool hasSet = false; + bool hasGrow = false; + }; - ModuleUtils::ParallelFunctionAnalysis analysis( - wasm, [&](Function* func, TablesWithSet& tablesWithSet) { + using MiniTableInfoMap = std::unordered_map; + + ModuleUtils::ParallelFunctionAnalysis analysis( + wasm, [&](Function* func, MiniTableInfoMap& tableInfoMap) { if (func->imported()) { return; } struct Finder : public PostWalker { - TablesWithSet& tablesWithSet; + MiniTableInfoMap& tableInfoMap; - Finder(TablesWithSet& tablesWithSet) : tablesWithSet(tablesWithSet) {} + Finder(MiniTableInfoMap& tableInfoMap) : tableInfoMap(tableInfoMap) {} void visitTableSet(TableSet* curr) { - tablesWithSet.insert(curr->table); + tableInfoMap[curr->table].hasSet = true; } void visitTableFill(TableFill* curr) { - tablesWithSet.insert(curr->table); + tableInfoMap[curr->table].hasSet = true; } void visitTableCopy(TableCopy* curr) { - tablesWithSet.insert(curr->destTable); + tableInfoMap[curr->destTable].hasSet = true; } void visitTableInit(TableInit* curr) { - tablesWithSet.insert(curr->table); + tableInfoMap[curr->table].hasSet = true; + } + void visitTableGrow(TableGrow* curr) { + tableInfoMap[curr->table].hasGrow = true; } }; - Finder(tablesWithSet).walkFunction(func); + Finder(tableInfoMap).walkFunction(func); }); - for (auto& [_, names] : analysis.map) { - for (auto name : names) { - tables[name].mayBeModified = true; + for (auto& [_, tableInfoMap] : analysis.map) { + for (auto& [tableName, info] : tableInfoMap) { + if (info.hasSet) { + tables[tableName].hasSet = true; + } + if (info.hasGrow) { + tables[tableName].hasGrow = true; + } } } diff --git a/src/ir/table-utils.h b/src/ir/table-utils.h index 4e788a2685e..884f2309797 100644 --- a/src/ir/table-utils.h +++ b/src/ir/table-utils.h @@ -122,9 +122,14 @@ bool usesExpressions(ElementSegment* curr, Module* module); // Information about a table's optimizability. struct TableInfo { - // Whether the table may be modified at runtime, either because it is imported - // or exported, or table.set operations exist for it in the code. - bool mayBeModified = false; + // Whether the table has writes to it (anything but a grow, see below). The + // writes may be internal, or through imports and exports. + bool hasSet = false; + + // Whether the table may grow. Growing does modify the table, but it only + // appends, so we track this separately from mayBeModified. This allows more + // optimizations in tables that grow but have no other sets. + bool hasGrow = false; // Whether we can assume that the initial contents are immutable. That is, if // a table looks like [a, b, c] in the wasm, and we see a call to index 1, we @@ -144,6 +149,9 @@ struct TableInfo { std::unique_ptr flatTable; + // Whether the contents may change. + bool mayBeModified() const { return hasSet || hasGrow; } + // Whether we can optimize using this table's data on the entry level, that // is, individual entries in the table are known to us, so calls through the // table with known indexes can be inferred, etc. @@ -154,7 +162,10 @@ struct TableInfo { // contents, even if other things might be appended later, which we // cannot infer). // * The table is flat (so we can see what is in it, by index). - return (!mayBeModified || initialContentsImmutable) && flatTable->valid; + // + // Note that we do not check hasGrow, as we can optimize at least *some* + // entries in that case (growth only appends). + return (!hasSet || initialContentsImmutable) && flatTable->valid; } }; diff --git a/src/passes/Directize.cpp b/src/passes/Directize.cpp index c0cd9e221ab..2c0809b8dba 100644 --- a/src/passes/Directize.cpp +++ b/src/passes/Directize.cpp @@ -127,18 +127,23 @@ struct FunctionDirectizer : public WalkerPass> { // The index is out of bounds for the initial table's content. This may // trap, but it may also not trap if the table is modified later (if a // function is appended to it). - if (!table.mayBeModified) { + if (!table.mayBeModified()) { return CallUtils::Trap{}; } else { // The table may be modified, so it might be appended to. We should only - // get here in the case that the initial contents are immutable, as - // otherwise we have nothing to optimize at all. - assert(table.initialContentsImmutable); + // get here in the case that the initial contents are immutable, or the + // table can grow, as otherwise we have nothing to optimize at all. + assert(table.initialContentsImmutable || table.hasGrow); return CallUtils::Unknown{}; } } auto name = flatTable.names[index]; if (!name.is()) { + // No segment wrote to this part of the initial contents of the table. + // This must trap, as we only get here if we can optimize such cases, + // relying on the fact that the table cannot be modified, or at least the + // initial contents cannot be. + assert(!table.hasSet || table.initialContentsImmutable); return CallUtils::Trap{}; } auto* func = getModule()->getFunction(name); diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 8bba990af8d..fd026490cb1 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -432,11 +432,13 @@ struct Analyzer { // Note a possible call of a function reference as well, if something else // might be written into the table during runtime. - // TODO: Add an option for immutable initial content like Directize? + // TODO: Add an option for immutable initial content like Directize? Can + // also check for grow without set, which leaves initial entries + // fixed. if (!tableInfoMap) { tableInfoMap = TableUtils::computeTableInfo(*module); } - if ((*tableInfoMap)[table].mayBeModified) { + if ((*tableInfoMap)[table].mayBeModified()) { useCallRefType(type); } } diff --git a/test/lit/passes/directize_all-features.wast b/test/lit/passes/directize_all-features.wast index 03ff576245d..adb064e8a96 100644 --- a/test/lit/passes/directize_all-features.wast +++ b/test/lit/passes/directize_all-features.wast @@ -1953,3 +1953,92 @@ ) ) +;; table.grow inhibits some optimizations. +(module + ;; CHECK: (type $func (func)) + ;; IMMUT: (type $func (func)) + (type $func (func)) + + ;; CHECK: (table $table 5 funcref) + ;; IMMUT: (table $table 5 funcref) + (table $table 5 funcref) + + ;; CHECK: (elem $table (i32.const 1) $target) + ;; IMMUT: (elem $table (i32.const 1) $target) + (elem $table (i32.const 1) $target) + + ;; CHECK: (elem declare func $grow) + + ;; CHECK: (export "caller" (func $caller)) + + ;; CHECK: (func $grow (type $func) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (table.grow $table + ;; CHECK-NEXT: (ref.func $grow) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; IMMUT: (elem declare func $grow) + + ;; IMMUT: (export "caller" (func $caller)) + + ;; IMMUT: (func $grow (type $func) + ;; IMMUT-NEXT: (drop + ;; IMMUT-NEXT: (table.grow $table + ;; IMMUT-NEXT: (ref.func $grow) + ;; IMMUT-NEXT: (i32.const 42) + ;; IMMUT-NEXT: ) + ;; IMMUT-NEXT: ) + ;; IMMUT-NEXT: ) + (func $grow + (drop + (table.grow $table + (ref.func $grow) + (i32.const 42) + ) + ) + ) + + ;; CHECK: (func $caller (type $func) + ;; CHECK-NEXT: (call $target) + ;; CHECK-NEXT: (call_indirect $table (type $func) + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call_indirect $table (type $func) + ;; CHECK-NEXT: (i32.const 1000) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; IMMUT: (func $caller (type $func) + ;; IMMUT-NEXT: (call $target) + ;; IMMUT-NEXT: (call_indirect $table (type $func) + ;; IMMUT-NEXT: (i32.const 10) + ;; IMMUT-NEXT: ) + ;; IMMUT-NEXT: (call_indirect $table (type $func) + ;; IMMUT-NEXT: (i32.const 1000) + ;; IMMUT-NEXT: ) + ;; IMMUT-NEXT: ) + (func $caller (export "caller") + ;; This is in the elem segment, so we can optimize it. Growth can only append. + (call_indirect (type $func) + (i32.const 1) + ) + ;; This is in the range that we grow to, if grow() is called, but we don't + ;; know if it will, so we don't optimize. + (call_indirect (type $func) + (i32.const 10) + ) + ;; This is in the range that we grow to, if grow() is called multiple times, + ;; but again we can't optimize. + (call_indirect (type $func) + (i32.const 1000) + ) + ) + + ;; CHECK: (func $target (type $func) + ;; CHECK-NEXT: ) + ;; IMMUT: (func $target (type $func) + ;; IMMUT-NEXT: ) + (func $target + ) +) diff --git a/test/lit/passes/remove-unused-module-elements-tables.wast b/test/lit/passes/remove-unused-module-elements-tables.wast index b0b6bd323a8..a556c09948f 100644 --- a/test/lit/passes/remove-unused-module-elements-tables.wast +++ b/test/lit/passes/remove-unused-module-elements-tables.wast @@ -333,7 +333,7 @@ ) ) -;; As above, but now the table has an initialistion expression +;; As above, but now the table has an initialization expression (module (rec ;; CHECK: (rec @@ -447,3 +447,116 @@ (drop (i32.const 30)) ) ) + +;; As above, but now the table has a table.grow. Like table.set, this prevents +;; optimization. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $foo (func)) + ;; OPEN_WORLD: (rec + ;; OPEN_WORLD-NEXT: (type $foo (func)) + (type $foo (func)) + ;; CHECK: (type $bar (func)) + ;; OPEN_WORLD: (type $bar (func)) + (type $bar (func)) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (table $table 10 funcref) + ;; OPEN_WORLD: (type $2 (func)) + + ;; OPEN_WORLD: (table $table 10 funcref) + (table $table 10 funcref) + ;; CHECK: (elem $table (i32.const 0) $foo-in-table $bar) + ;; OPEN_WORLD: (elem $table (i32.const 0) $foo-in-table $bar) + (elem $table (i32.const 0) $foo-in-table $bar) + + ;; CHECK: (elem declare func $foo-not-in-table) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (func $export (type $2) + ;; CHECK-NEXT: (call_indirect $table (type $foo) + ;; CHECK-NEXT: (i32.const 5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (table.grow $table + ;; CHECK-NEXT: (ref.func $foo-not-in-table) + ;; CHECK-NEXT: (i32.const 7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (elem declare func $foo-not-in-table) + + ;; OPEN_WORLD: (export "export" (func $export)) + + ;; OPEN_WORLD: (func $export (type $2) + ;; OPEN_WORLD-NEXT: (call_indirect $table (type $foo) + ;; OPEN_WORLD-NEXT: (i32.const 5) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (table.grow $table + ;; OPEN_WORLD-NEXT: (ref.func $foo-not-in-table) + ;; OPEN_WORLD-NEXT: (i32.const 7) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $export (export "export") + (call_indirect $table (type $foo) + (i32.const 5) + ) + (drop + (table.grow $table + (ref.func $foo-not-in-table) + (i32.const 7) + ) + ) + ) + + ;; CHECK: (func $foo-in-table (type $foo) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $foo-in-table (type $foo) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (i32.const 10) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $foo-in-table (type $foo) + ;; This is in the table, and might be reached. + (drop (i32.const 10)) + ) + + ;; CHECK: (func $foo-not-in-table (type $foo) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $foo-not-in-table (type $foo) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (i32.const 20) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $foo-not-in-table (type $foo) + ;; The reference taken of this function might be added to the table using + ;; table.grow, so we can do nothing here. + (drop (i32.const 20)) + ) + + ;; CHECK: (func $bar (type $bar) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $bar (type $bar) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (i32.const 30) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $bar (type $bar) + ;; Not even references, so this is unreachable in closed world. + (drop (i32.const 30)) + ) +) + From a0981e5dcdb18f7c92fdd9b046161eeedf7d184d Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 5 May 2026 09:27:58 -0700 Subject: [PATCH 074/168] RemoveExports pass (#8670) Fixes #7976 --- src/passes/CMakeLists.txt | 1 + src/passes/RemoveExports.cpp | 55 +++++++++++++++++++++ src/passes/pass.cpp | 3 ++ src/passes/passes.h | 1 + test/lit/help/wasm-metadce.test | 2 + test/lit/help/wasm-opt.test | 2 + test/lit/help/wasm2js.test | 2 + test/lit/passes/remove-exports.wast | 77 +++++++++++++++++++++++++++++ 8 files changed, 143 insertions(+) create mode 100644 src/passes/RemoveExports.cpp create mode 100644 test/lit/passes/remove-exports.wast diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index c2952e174b8..a61bfb6195c 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -105,6 +105,7 @@ set(passes_SOURCES TraceCalls.cpp RandomizeBranchHints.cpp RedundantSetElimination.cpp + RemoveExports.cpp RemoveImports.cpp RemoveMemoryInit.cpp RemoveNonJSOps.cpp diff --git a/src/passes/RemoveExports.cpp b/src/passes/RemoveExports.cpp new file mode 100644 index 00000000000..99cd0ff641d --- /dev/null +++ b/src/passes/RemoveExports.cpp @@ -0,0 +1,55 @@ +/* + * Copyright 2026 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. + */ + +// +// Remove exports using a wildcard. For example: +// +// --remove-exports=__* +// +// That will remove all exports with names like "__foo" and "__bar". +// + +#include "pass.h" +#include "support/string.h" +#include "wasm.h" + +namespace wasm { + +namespace { + +struct RemoveExports : public Pass { + void run(Module* module) override { + std::string pattern = + getArgument(name, "Usage usage: wasm-opt --" + name + "=WILDCARD"); + + std::vector toRemove; + for (auto& exp : module->exports) { + if (String::wildcardMatch(pattern, exp->name.toString())) { + toRemove.push_back(exp->name); + } + } + + for (auto& name : toRemove) { + module->removeExport(name); + } + } +}; + +} // anonymous namespace + +Pass* createRemoveExportsPass() { return new RemoveExports(); } + +} // namespace wasm diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index e5de76176ba..dc6d91feb4e 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -412,6 +412,9 @@ void PassRegistry::registerPasses() { registerPass("remove-relaxed-simd", "replaces relaxed SIMD instructions with unreachable", createRemoveRelaxedSIMDPass); + registerPass("remove-exports", + "removes exports using a wildcard", + createRemoveExportsPass); registerPass("remove-imports", "removes imports and replaces them with nops", createRemoveImportsPass); diff --git a/src/passes/passes.h b/src/passes/passes.h index be06369a9f8..2fdacd84ab0 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -135,6 +135,7 @@ Pass* createPropagateGlobalsGloballyPass(); Pass* createRandomizeBranchHintsPass(); Pass* createRemoveNonJSOpsPass(); Pass* createRemoveRelaxedSIMDPass(); +Pass* createRemoveExportsPass(); Pass* createRemoveImportsPass(); Pass* createRemoveMemoryInitPass(); Pass* createRemoveUnusedBrsPass(); diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index 1b0cc7d4569..4d5f8e33b89 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -383,6 +383,8 @@ ;; CHECK-NEXT: --propagate-globals-globally propagate global values to other ;; CHECK-NEXT: globals (useful for tests) ;; CHECK-NEXT: +;; CHECK-NEXT: --remove-exports removes exports using a wildcard +;; CHECK-NEXT: ;; CHECK-NEXT: --remove-imports removes imports and replaces ;; CHECK-NEXT: them with nops ;; CHECK-NEXT: diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index d616e1cf085..08e8e5657c3 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -415,6 +415,8 @@ ;; CHECK-NEXT: --propagate-globals-globally propagate global values to other ;; CHECK-NEXT: globals (useful for tests) ;; CHECK-NEXT: +;; CHECK-NEXT: --remove-exports removes exports using a wildcard +;; CHECK-NEXT: ;; CHECK-NEXT: --remove-imports removes imports and replaces ;; CHECK-NEXT: them with nops ;; CHECK-NEXT: diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index a91d5b5c050..88d6504b384 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -347,6 +347,8 @@ ;; CHECK-NEXT: --propagate-globals-globally propagate global values to other ;; CHECK-NEXT: globals (useful for tests) ;; CHECK-NEXT: +;; CHECK-NEXT: --remove-exports removes exports using a wildcard +;; CHECK-NEXT: ;; CHECK-NEXT: --remove-imports removes imports and replaces ;; CHECK-NEXT: them with nops ;; CHECK-NEXT: diff --git a/test/lit/passes/remove-exports.wast b/test/lit/passes/remove-exports.wast new file mode 100644 index 00000000000..0a5af581346 --- /dev/null +++ b/test/lit/passes/remove-exports.wast @@ -0,0 +1,77 @@ +;; 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-exports=__*" -all -S -o - | filecheck %s + +;; foo and bar will be kept as exports, but __foo, __bar, and __ will not. +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (export "foo" (func $foo)) + + ;; CHECK: (export "bar" (func $bar)) + + ;; CHECK: (func $foo (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $foo (export "foo") + (drop (i32.const 1)) + ) + + ;; CHECK: (func $__foo (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $__foo (export "__foo") + (drop (i32.const 2)) + ) + + ;; CHECK: (func $bar (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $bar (export "bar") + (drop (i32.const 3)) + ) + + ;; CHECK: (func $__bar (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $__bar (export "__bar") + (drop (i32.const 4)) + ) + + ;; CHECK: (func $__ (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $__ (export "__") + (drop (i32.const 4)) + ) +) + +;; Test non-function exports. The prefixed __mem and __table exports vanish. +(module + ;; CHECK: (memory $memory 10 20) + (memory $memory 10 20) + + ;; CHECK: (table $table 10 20 funcref) + (table $table 10 20 funcref) + + ;; CHECK: (export "mem" (memory $memory)) + (export "mem" (memory $memory)) + + (export "__mem" (memory $memory)) + + ;; CHECK: (export "tab" (table $table)) + (export "tab" (table $table)) + + (export "__tab" (table $table)) +) From 7f4405403a4c9166cc354725a48001fd764275b8 Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Tue, 5 May 2026 11:53:12 -0700 Subject: [PATCH 075/168] Add Wide Arithmetic C + JS APIs (#8660) Part of #8544. --- CHANGELOG.md | 1 + src/binaryen-c.cpp | 123 ++++++++++++++++++++++++++++ src/binaryen-c.h | 72 ++++++++++++++++ src/js/binaryen.js-post.js | 72 +++++++++++++++- test/binaryen.js/expressions.js | 64 +++++++++++++++ test/binaryen.js/expressions.js.txt | 15 ++++ test/example/c-api-kitchen-sink.c | 43 ++++++++++ test/example/c-api-kitchen-sink.txt | 35 ++++++++ 8 files changed, 424 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 458fefd10bd..5bdd2f6ef92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Current Trunk - Rename `BinaryenCopyMemorySegmentData` to `BinaryenCopyDataSegmentData` in c api. - Rename `module.getNumMemorySegments` to `module.getNumDataSegments` in js api. - Rename `module.getMemorySegmentInfo` to `module.getDataSegmentInfo` in js api. + - Add C and JS APIs for the Wide Arithmetic proposal (#8660). v129 ---- diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index 53926af7680..086f5dda28d 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -690,6 +690,10 @@ BinaryenOp BinaryenLtFloat64(void) { return LtFloat64; } BinaryenOp BinaryenLeFloat64(void) { return LeFloat64; } BinaryenOp BinaryenGtFloat64(void) { return GtFloat64; } BinaryenOp BinaryenGeFloat64(void) { return GeFloat64; } +BinaryenOp BinaryenAddInt128(void) { return AddInt128; } +BinaryenOp BinaryenSubInt128(void) { return SubInt128; } +BinaryenOp BinaryenMulWideSInt64(void) { return MulWideSInt64; } +BinaryenOp BinaryenMulWideUInt64(void) { return MulWideUInt64; } BinaryenOp BinaryenAtomicRMWAdd(void) { return RMWAdd; } BinaryenOp BinaryenAtomicRMWSub(void) { return RMWSub; } BinaryenOp BinaryenAtomicRMWAnd(void) { return RMWAnd; } @@ -1315,6 +1319,26 @@ BinaryenExpressionRef BinaryenBinary(BinaryenModuleRef module, Builder(*(Module*)module) .makeBinary(BinaryOp(op), (Expression*)left, (Expression*)right)); } +BinaryenExpressionRef BinaryenWideIntAddSub(BinaryenModuleRef module, + BinaryenOp op, + BinaryenExpressionRef leftLow, + BinaryenExpressionRef leftHigh, + BinaryenExpressionRef rightLow, + BinaryenExpressionRef rightHigh) { + return Builder(*(Module*)module) + .makeWideIntAddSub(WideIntAddSubOp(op), + (Expression*)leftLow, + (Expression*)leftHigh, + (Expression*)rightLow, + (Expression*)rightHigh); +} +BinaryenExpressionRef BinaryenWideIntMul(BinaryenModuleRef module, + BinaryenOp op, + BinaryenExpressionRef left, + BinaryenExpressionRef right) { + return Builder(*(Module*)module) + .makeWideIntMul(WideIntMulOp(op), (Expression*)left, (Expression*)right); +} BinaryenExpressionRef BinaryenSelect(BinaryenModuleRef module, BinaryenExpressionRef condition, BinaryenExpressionRef ifTrue, @@ -2946,6 +2970,105 @@ void BinaryenBinarySetRight(BinaryenExpressionRef expr, assert(rightExpr); static_cast(expression)->right = (Expression*)rightExpr; } +// WideIntAddSub +BinaryenOp BinaryenWideIntAddSubGetOp(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->op; +} +void BinaryenWideIntAddSubSetOp(BinaryenExpressionRef expr, BinaryenOp op) { + auto* expression = (Expression*)expr; + assert(expression->is()); + static_cast(expression)->op = WideIntAddSubOp(op); +} +BinaryenExpressionRef +BinaryenWideIntAddSubGetLeftLow(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->leftLow; +} +void BinaryenWideIntAddSubSetLeftLow(BinaryenExpressionRef expr, + BinaryenExpressionRef leftLowExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(leftLowExpr); + static_cast(expression)->leftLow = (Expression*)leftLowExpr; +} +BinaryenExpressionRef +BinaryenWideIntAddSubGetLeftHigh(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->leftHigh; +} +void BinaryenWideIntAddSubSetLeftHigh(BinaryenExpressionRef expr, + BinaryenExpressionRef leftHighExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(leftHighExpr); + static_cast(expression)->leftHigh = (Expression*)leftHighExpr; +} +BinaryenExpressionRef +BinaryenWideIntAddSubGetRightLow(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->rightLow; +} +void BinaryenWideIntAddSubSetRightLow(BinaryenExpressionRef expr, + BinaryenExpressionRef rightLowExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(rightLowExpr); + static_cast(expression)->rightLow = (Expression*)rightLowExpr; +} +BinaryenExpressionRef +BinaryenWideIntAddSubGetRightHigh(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->rightHigh; +} +void BinaryenWideIntAddSubSetRightHigh(BinaryenExpressionRef expr, + BinaryenExpressionRef rightHighExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(rightHighExpr); + static_cast(expression)->rightHigh = + (Expression*)rightHighExpr; +} +// WideIntMul +BinaryenOp BinaryenWideIntMulGetOp(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->op; +} +void BinaryenWideIntMulSetOp(BinaryenExpressionRef expr, BinaryenOp op) { + auto* expression = (Expression*)expr; + assert(expression->is()); + static_cast(expression)->op = WideIntMulOp(op); +} +BinaryenExpressionRef BinaryenWideIntMulGetLeft(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->left; +} +void BinaryenWideIntMulSetLeft(BinaryenExpressionRef expr, + BinaryenExpressionRef leftExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(leftExpr); + static_cast(expression)->left = (Expression*)leftExpr; +} +BinaryenExpressionRef BinaryenWideIntMulGetRight(BinaryenExpressionRef expr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + return static_cast(expression)->right; +} +void BinaryenWideIntMulSetRight(BinaryenExpressionRef expr, + BinaryenExpressionRef rightExpr) { + auto* expression = (Expression*)expr; + assert(expression->is()); + assert(rightExpr); + static_cast(expression)->right = (Expression*)rightExpr; +} // Select BinaryenExpressionRef BinaryenSelectGetIfTrue(BinaryenExpressionRef expr) { auto* expression = (Expression*)expr; diff --git a/src/binaryen-c.h b/src/binaryen-c.h index 10e01fef3aa..d18f3638e2d 100644 --- a/src/binaryen-c.h +++ b/src/binaryen-c.h @@ -431,6 +431,10 @@ BINARYEN_API BinaryenOp BinaryenLtFloat64(void); BINARYEN_API BinaryenOp BinaryenLeFloat64(void); BINARYEN_API BinaryenOp BinaryenGtFloat64(void); BINARYEN_API BinaryenOp BinaryenGeFloat64(void); +BINARYEN_API BinaryenOp BinaryenAddInt128(void); +BINARYEN_API BinaryenOp BinaryenSubInt128(void); +BINARYEN_API BinaryenOp BinaryenMulWideSInt64(void); +BINARYEN_API BinaryenOp BinaryenMulWideUInt64(void); BINARYEN_API BinaryenOp BinaryenAtomicRMWAdd(void); BINARYEN_API BinaryenOp BinaryenAtomicRMWSub(void); BINARYEN_API BinaryenOp BinaryenAtomicRMWAnd(void); @@ -841,6 +845,18 @@ BINARYEN_API BinaryenExpressionRef BinaryenBinary(BinaryenModuleRef module, BinaryenExpressionRef left, BinaryenExpressionRef right); BINARYEN_API BinaryenExpressionRef +BinaryenWideIntAddSub(BinaryenModuleRef module, + BinaryenOp op, + BinaryenExpressionRef leftLow, + BinaryenExpressionRef leftHigh, + BinaryenExpressionRef rightLow, + BinaryenExpressionRef rightHigh); +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntMul(BinaryenModuleRef module, + BinaryenOp op, + BinaryenExpressionRef left, + BinaryenExpressionRef right); +BINARYEN_API BinaryenExpressionRef BinaryenSelect(BinaryenModuleRef module, BinaryenExpressionRef condition, BinaryenExpressionRef ifTrue, @@ -1704,6 +1720,62 @@ BinaryenBinaryGetRight(BinaryenExpressionRef expr); BINARYEN_API void BinaryenBinarySetRight(BinaryenExpressionRef expr, BinaryenExpressionRef rightExpr); +// WideIntAddSub + +// Gets the operation being performed by a wide int add/sub expression. +BINARYEN_API BinaryenOp BinaryenWideIntAddSubGetOp(BinaryenExpressionRef expr); +// Sets the operation being performed by a wide int add/sub expression. +BINARYEN_API void BinaryenWideIntAddSubSetOp(BinaryenExpressionRef expr, + BinaryenOp op); +// Gets the left low expression of a wide int add/sub expression. +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntAddSubGetLeftLow(BinaryenExpressionRef expr); +// Sets the left low expression of a wide int add/sub expression. +BINARYEN_API void +BinaryenWideIntAddSubSetLeftLow(BinaryenExpressionRef expr, + BinaryenExpressionRef leftLowExpr); +// Gets the left high expression of a wide int add/sub expression. +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntAddSubGetLeftHigh(BinaryenExpressionRef expr); +// Sets the left high expression of a wide int add/sub expression. +BINARYEN_API void +BinaryenWideIntAddSubSetLeftHigh(BinaryenExpressionRef expr, + BinaryenExpressionRef leftHighExpr); +// Gets the right low expression of a wide int add/sub expression. +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntAddSubGetRightLow(BinaryenExpressionRef expr); +// Sets the right low expression of a wide int add/sub expression. +BINARYEN_API void +BinaryenWideIntAddSubSetRightLow(BinaryenExpressionRef expr, + BinaryenExpressionRef rightLowExpr); +// Gets the right high expression of a wide int add/sub expression. +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntAddSubGetRightHigh(BinaryenExpressionRef expr); +// Sets the right high expression of a wide int add/sub expression. +BINARYEN_API void +BinaryenWideIntAddSubSetRightHigh(BinaryenExpressionRef expr, + BinaryenExpressionRef rightHighExpr); + +// WideIntMul + +// Gets the operation being performed by a wide int mul expression. +BINARYEN_API BinaryenOp BinaryenWideIntMulGetOp(BinaryenExpressionRef expr); +// Sets the operation being performed by a wide int mul expression. +BINARYEN_API void BinaryenWideIntMulSetOp(BinaryenExpressionRef expr, + BinaryenOp op); +// Gets the left expression of a wide int mul expression. +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntMulGetLeft(BinaryenExpressionRef expr); +// Sets the left expression of a wide int mul expression. +BINARYEN_API void BinaryenWideIntMulSetLeft(BinaryenExpressionRef expr, + BinaryenExpressionRef leftExpr); +// Gets the right expression of a wide int mul expression. +BINARYEN_API BinaryenExpressionRef +BinaryenWideIntMulGetRight(BinaryenExpressionRef expr); +// Sets the right expression of a wide int mul expression. +BINARYEN_API void BinaryenWideIntMulSetRight(BinaryenExpressionRef expr, + BinaryenExpressionRef rightExpr); + // Select // Gets the expression becoming selected by a `select` expression if the diff --git a/src/js/binaryen.js-post.js b/src/js/binaryen.js-post.js index 8c965d70ee0..97db8ce7f5c 100644 --- a/src/js/binaryen.js-post.js +++ b/src/js/binaryen.js-post.js @@ -613,7 +613,11 @@ function initializeConstants() { 'StringEncodeLossyUTF8Array', 'StringEncodeWTF16Array', 'StringEqEqual', - 'StringEqCompare' + 'StringEqCompare', + 'AddInt128', + 'SubInt128', + 'MulWideSInt64', + 'MulWideUInt64' ].forEach(name => { Module['Operations'][name] = Module[name] = Module['_Binaryen' + name](); }); @@ -1184,6 +1188,18 @@ function wrapModule(module, self = {}) { 'add'(left, right) { return Module['_BinaryenBinary'](module, Module['AddInt64'], left, right); }, + 'add128'(leftLow, leftHigh, rightLow, rightHigh) { + return Module['_BinaryenWideIntAddSub'](module, Module['AddInt128'], leftLow, leftHigh, rightLow, rightHigh); + }, + 'sub128'(leftLow, leftHigh, rightLow, rightHigh) { + return Module['_BinaryenWideIntAddSub'](module, Module['SubInt128'], leftLow, leftHigh, rightLow, rightHigh); + }, + 'mul_wide_s'(left, right) { + return Module['_BinaryenWideIntMul'](module, Module['MulWideSInt64'], left, right); + }, + 'mul_wide_u'(left, right) { + return Module['_BinaryenWideIntMul'](module, Module['MulWideUInt64'], left, right); + }, 'sub'(left, right) { return Module['_BinaryenBinary'](module, Module['SubInt64'], left, right); }, @@ -4197,6 +4213,60 @@ Module['Binary'] = makeExpressionWrapper(Module['_BinaryenBinaryId'](), { } }); +Module['WideIntAddSub'] = makeExpressionWrapper(Module['_BinaryenWideIntAddSubId'](), { + 'getOp'(expr) { + return Module['_BinaryenWideIntAddSubGetOp'](expr); + }, + 'setOp'(expr, op) { + Module['_BinaryenWideIntAddSubSetOp'](expr, op); + }, + 'getLeftLow'(expr) { + return Module['_BinaryenWideIntAddSubGetLeftLow'](expr); + }, + 'setLeftLow'(expr, leftLowExpr) { + Module['_BinaryenWideIntAddSubSetLeftLow'](expr, leftLowExpr); + }, + 'getLeftHigh'(expr) { + return Module['_BinaryenWideIntAddSubGetLeftHigh'](expr); + }, + 'setLeftHigh'(expr, leftHighExpr) { + Module['_BinaryenWideIntAddSubSetLeftHigh'](expr, leftHighExpr); + }, + 'getRightLow'(expr) { + return Module['_BinaryenWideIntAddSubGetRightLow'](expr); + }, + 'setRightLow'(expr, rightLowExpr) { + Module['_BinaryenWideIntAddSubSetRightLow'](expr, rightLowExpr); + }, + 'getRightHigh'(expr) { + return Module['_BinaryenWideIntAddSubGetRightHigh'](expr); + }, + 'setRightHigh'(expr, rightHighExpr) { + Module['_BinaryenWideIntAddSubSetRightHigh'](expr, rightHighExpr); + } +}); + +Module['WideIntMul'] = makeExpressionWrapper(Module['_BinaryenWideIntMulId'](), { + 'getOp'(expr) { + return Module['_BinaryenWideIntMulGetOp'](expr); + }, + 'setOp'(expr, op) { + Module['_BinaryenWideIntMulSetOp'](expr, op); + }, + 'getLeft'(expr) { + return Module['_BinaryenWideIntMulGetLeft'](expr); + }, + 'setLeft'(expr, leftExpr) { + Module['_BinaryenWideIntMulSetLeft'](expr, leftExpr); + }, + 'getRight'(expr) { + return Module['_BinaryenWideIntMulGetRight'](expr); + }, + 'setRight'(expr, rightExpr) { + Module['_BinaryenWideIntMulSetRight'](expr, rightExpr); + } +}); + Module['Select'] = makeExpressionWrapper(Module['_BinaryenSelectId'](), { 'getIfTrue'(expr) { return Module['_BinaryenSelectGetIfTrue'](expr); diff --git a/test/binaryen.js/expressions.js b/test/binaryen.js/expressions.js index fa56e712de5..a2b7319b9e7 100644 --- a/test/binaryen.js/expressions.js +++ b/test/binaryen.js/expressions.js @@ -968,6 +968,70 @@ console.log("# Binary"); module.dispose(); })(); +console.log("# WideIntAddSub"); +(function testWideIntAddSub() { + const module = new binaryen.Module(); + + var leftLow = module.i64.const(1); + var leftHigh = module.i64.const(2); + var rightLow = module.i64.const(3); + var rightHigh = module.i64.const(4); + const theWideAdd = binaryen.WideIntAddSub(module.i64.add128(leftLow, leftHigh, rightLow, rightHigh)); + assert(theWideAdd instanceof binaryen.WideIntAddSub); + assert(theWideAdd instanceof binaryen.Expression); + assert(theWideAdd.op === binaryen.Operations.AddInt128); + assert(theWideAdd.leftLow === leftLow); + assert(theWideAdd.leftHigh === leftHigh); + assert(theWideAdd.rightLow === rightLow); + assert(theWideAdd.rightHigh === rightHigh); + + theWideAdd.op = binaryen.Operations.SubInt128; + assert(theWideAdd.op === binaryen.Operations.SubInt128); + theWideAdd.leftLow = module.i64.const(5); + theWideAdd.leftHigh = module.i64.const(6); + theWideAdd.rightLow = module.i64.const(7); + theWideAdd.rightHigh = module.i64.const(8); + theWideAdd.finalize(); + + console.log(theWideAdd.toText()); + assert( + theWideAdd.toText() + == + "(i64.sub128\n (i64.const 5)\n (i64.const 6)\n (i64.const 7)\n (i64.const 8)\n)\n" + ); + + module.dispose(); +})(); + +console.log("# WideIntMul"); +(function testWideIntMul() { + const module = new binaryen.Module(); + + var left = module.i64.const(1); + var right = module.i64.const(2); + const theWideMul = binaryen.WideIntMul(module.i64.mul_wide_s(left, right)); + assert(theWideMul instanceof binaryen.WideIntMul); + assert(theWideMul instanceof binaryen.Expression); + assert(theWideMul.op === binaryen.Operations.MulWideSInt64); + assert(theWideMul.left === left); + assert(theWideMul.right === right); + + theWideMul.op = binaryen.Operations.MulWideUInt64; + assert(theWideMul.op === binaryen.Operations.MulWideUInt64); + theWideMul.left = module.i64.const(3); + theWideMul.right = module.i64.const(4); + theWideMul.finalize(); + + console.log(theWideMul.toText()); + assert( + theWideMul.toText() + == + "(i64.mul_wide_u\n (i64.const 3)\n (i64.const 4)\n)\n" + ); + + module.dispose(); +})(); + console.log("# Select"); (function testSelect() { const module = new binaryen.Module(); diff --git a/test/binaryen.js/expressions.js.txt b/test/binaryen.js/expressions.js.txt index f060c0dd33c..ec0c0e518e6 100644 --- a/test/binaryen.js/expressions.js.txt +++ b/test/binaryen.js/expressions.js.txt @@ -103,6 +103,20 @@ (i64.const 4) ) +# WideIntAddSub +(i64.sub128 + (i64.const 5) + (i64.const 6) + (i64.const 7) + (i64.const 8) +) + +# WideIntMul +(i64.mul_wide_u + (i64.const 3) + (i64.const 4) +) + # Select (select (i64.const 5) @@ -459,3 +473,4 @@ (i32.const 7) (ref.func $tiny) ) + diff --git a/test/example/c-api-kitchen-sink.c b/test/example/c-api-kitchen-sink.c index e046b942303..8c9818ece6b 100644 --- a/test/example/c-api-kitchen-sink.c +++ b/test/example/c-api-kitchen-sink.c @@ -2377,6 +2377,48 @@ void test_relaxed_atomics() { BinaryenModulePrint(module); BinaryenModuleDispose(module); } + +void test_wide_arithmetic() { + BinaryenModuleRef module = BinaryenModuleCreate(); + BinaryenModuleSetFeatures(module, BinaryenFeatureWideArithmetic()); + + BinaryenExpressionRef ll = BinaryenConst(module, BinaryenLiteralInt64(1)); + BinaryenExpressionRef lh = BinaryenConst(module, BinaryenLiteralInt64(2)); + BinaryenExpressionRef rl = BinaryenConst(module, BinaryenLiteralInt64(3)); + BinaryenExpressionRef rh = BinaryenConst(module, BinaryenLiteralInt64(4)); + + BinaryenExpressionRef wideAdd = + BinaryenWideIntAddSub(module, BinaryenAddInt128(), ll, lh, rl, rh); + BinaryenExpressionRef wideSub = + BinaryenWideIntAddSub(module, BinaryenSubInt128(), ll, lh, rl, rh); + + BinaryenExpressionRef ml = BinaryenConst(module, BinaryenLiteralInt64(5)); + BinaryenExpressionRef mr = BinaryenConst(module, BinaryenLiteralInt64(6)); + BinaryenExpressionRef wideMulS = + BinaryenWideIntMul(module, BinaryenMulWideSInt64(), ml, mr); + BinaryenExpressionRef wideMulU = + BinaryenWideIntMul(module, BinaryenMulWideUInt64(), ml, mr); + + BinaryenExpressionRef statements[] = {BinaryenDrop(module, wideAdd), + BinaryenDrop(module, wideSub), + BinaryenDrop(module, wideMulS), + BinaryenDrop(module, wideMulU)}; + + BinaryenExpressionRef body = + BinaryenBlock(module, "body", statements, 4, BinaryenTypeAuto()); + + BinaryenAddFunction(module, + "wide-arithmetic-test", + BinaryenTypeNone(), + BinaryenTypeNone(), + NULL, + 0, + body); + + BinaryenModulePrint(module); + BinaryenModuleDispose(module); +} + int main() { test_types(); test_features(); @@ -2392,6 +2434,7 @@ int main() { test_typebuilder(); test_callref_and_types(); test_relaxed_atomics(); + test_wide_arithmetic(); return 0; } diff --git a/test/example/c-api-kitchen-sink.txt b/test/example/c-api-kitchen-sink.txt index cf66a4cba9b..8c1dead6d1f 100644 --- a/test/example/c-api-kitchen-sink.txt +++ b/test/example/c-api-kitchen-sink.txt @@ -3131,3 +3131,38 @@ Cmpxchg memory order: 2 ) ) ) +(module + (type $0 (func)) + (func $wide-arithmetic-test + (block $body + (tuple.drop 2 + (i64.add128 + (i64.const 1) + (i64.const 2) + (i64.const 3) + (i64.const 4) + ) + ) + (tuple.drop 2 + (i64.sub128 + (i64.const 1) + (i64.const 2) + (i64.const 3) + (i64.const 4) + ) + ) + (tuple.drop 2 + (i64.mul_wide_s + (i64.const 5) + (i64.const 6) + ) + ) + (tuple.drop 2 + (i64.mul_wide_u + (i64.const 5) + (i64.const 6) + ) + ) + ) + ) +) From a3b5ce99cb368eb50a5b20ed112687395fde196b Mon Sep 17 00:00:00 2001 From: juj Date: Wed, 6 May 2026 00:25:12 +0300 Subject: [PATCH 076/168] Fix clang 13 build (#8669) Clang-13 advertises C++20 support with `__cplusplus >= 202002L`, but it does not have all the features of C++20. So instead use the C++20 individual feature testing macros to check for the various features: https://en.cppreference.com/cpp/feature_test which Clang-13 accurately reports. Authored on top of PR https://github.com/WebAssembly/binaryen/pull/8668, will rebase before landing, if/when https://github.com/WebAssembly/binaryen/pull/8668 is approved. --- src/analysis/lattice.h | 10 +++++----- src/analysis/lattices/abstraction.h | 4 ++-- src/analysis/lattices/array.h | 6 +++--- src/analysis/lattices/bool.h | 4 ++-- src/analysis/lattices/conetype.h | 2 +- src/analysis/lattices/flat.h | 6 +++--- src/analysis/lattices/int.h | 6 +++--- src/analysis/lattices/inverted.h | 2 +- src/analysis/lattices/lift.h | 2 +- src/analysis/lattices/shared.h | 4 ++-- src/analysis/lattices/stack.h | 2 +- src/analysis/lattices/tuple.h | 2 +- src/analysis/lattices/valtype.h | 2 +- src/analysis/lattices/vector.h | 2 +- src/analysis/transfer-function.h | 15 ++++++++++----- src/support/graph_traversal.h | 16 +++++++++++++++- src/tools/wasm-fuzz-lattices.cpp | 2 +- 17 files changed, 53 insertions(+), 34 deletions(-) diff --git a/src/analysis/lattice.h b/src/analysis/lattice.h index f0de5e07e25..719bae69128 100644 --- a/src/analysis/lattice.h +++ b/src/analysis/lattice.h @@ -17,9 +17,9 @@ #ifndef wasm_analysis_lattice_h #define wasm_analysis_lattice_h -#if __cplusplus >= 202002L +#if __has_include() #include -#endif // __cplusplus >= 202002L +#endif namespace wasm::analysis { @@ -37,7 +37,7 @@ inline LatticeComparison reverseComparison(LatticeComparison comparison) { } } -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) template concept Lattice = requires(const L& lattice, @@ -78,12 +78,12 @@ concept FullLattice = { lattice.meet(elem, constElem) } noexcept -> std::same_as; }; -#else // __cplusplus >= 202002L +#else // defined(__cpp_lib_concepts) #define Lattice typename #define FullLattice typename -#endif // __cplusplus >= 202002L +#endif // defined(__cpp_lib_concepts) } // namespace wasm::analysis diff --git a/src/analysis/lattices/abstraction.h b/src/analysis/lattices/abstraction.h index bc503518c9f..13a7043767e 100644 --- a/src/analysis/lattices/abstraction.h +++ b/src/analysis/lattices/abstraction.h @@ -22,7 +22,7 @@ #include "../lattice.h" #include "support/utilities.h" -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) #include "analysis/lattices/bool.h" #endif @@ -218,7 +218,7 @@ template struct Abstraction { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice>); #endif diff --git a/src/analysis/lattices/array.h b/src/analysis/lattices/array.h index 7ac0273022b..8cc637e93b9 100644 --- a/src/analysis/lattices/array.h +++ b/src/analysis/lattices/array.h @@ -54,7 +54,7 @@ template struct Array { } Element getTop() const noexcept -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) requires FullLattice #endif { @@ -101,7 +101,7 @@ template struct Array { // Pairwise meet on the elements. bool meet(Element& meetee, const Element& meeter) const noexcept -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) requires FullLattice #endif { @@ -113,7 +113,7 @@ template struct Array { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(FullLattice>); static_assert(Lattice, 1>>); #endif diff --git a/src/analysis/lattices/bool.h b/src/analysis/lattices/bool.h index ee1b2149f79..8dba0bdcbc9 100644 --- a/src/analysis/lattices/bool.h +++ b/src/analysis/lattices/bool.h @@ -75,9 +75,9 @@ struct Bool { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice); -#endif // __cplusplus >= 202002L +#endif // defined(__cpp_lib_concepts) } // namespace wasm::analysis diff --git a/src/analysis/lattices/conetype.h b/src/analysis/lattices/conetype.h index 5e5484fc630..2f2bda74b2c 100644 --- a/src/analysis/lattices/conetype.h +++ b/src/analysis/lattices/conetype.h @@ -174,7 +174,7 @@ struct ConeType { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice); static_assert(FullLattice); #endif diff --git a/src/analysis/lattices/flat.h b/src/analysis/lattices/flat.h index aa26b101d99..a26b145c65c 100644 --- a/src/analysis/lattices/flat.h +++ b/src/analysis/lattices/flat.h @@ -21,7 +21,7 @@ #include #include -#if __cplusplus >= 202002L +#if __has_include() #include #endif @@ -30,7 +30,7 @@ namespace wasm::analysis { -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) template concept Flattenable = std::copyable && std::equality_comparable; @@ -118,7 +118,7 @@ struct Flat { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice>); #endif diff --git a/src/analysis/lattices/int.h b/src/analysis/lattices/int.h index 28c39fab6e8..280918099be 100644 --- a/src/analysis/lattices/int.h +++ b/src/analysis/lattices/int.h @@ -26,7 +26,7 @@ namespace wasm::analysis { // The lattice of integers of the given type `T`, ordered by <. The min integer // is the bottom element and the max integer is the top element. -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) template #else template @@ -59,12 +59,12 @@ using UInt32 = Integer; using Int64 = Integer; using UInt64 = Integer; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(FullLattice); static_assert(FullLattice); static_assert(FullLattice); static_assert(FullLattice); -#endif // __cplusplus >= 202002L +#endif // defined(__cpp_lib_concepts) } // namespace wasm::analysis diff --git a/src/analysis/lattices/inverted.h b/src/analysis/lattices/inverted.h index b70e58968e3..917c5710899 100644 --- a/src/analysis/lattices/inverted.h +++ b/src/analysis/lattices/inverted.h @@ -52,7 +52,7 @@ template struct Inverted { // Deduction guide. template Inverted(L&&) -> Inverted; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice>); #endif diff --git a/src/analysis/lattices/lift.h b/src/analysis/lattices/lift.h index ec0f57967a3..ddd1ee96dc6 100644 --- a/src/analysis/lattices/lift.h +++ b/src/analysis/lattices/lift.h @@ -77,7 +77,7 @@ template struct Lift { // Deduction guide. template Lift(L&&) -> Lift; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice>); #endif diff --git a/src/analysis/lattices/shared.h b/src/analysis/lattices/shared.h index f345014b93d..60e627613ec 100644 --- a/src/analysis/lattices/shared.h +++ b/src/analysis/lattices/shared.h @@ -121,9 +121,9 @@ template struct SharedPath { // Deduction guide. template SharedPath(L&&) -> SharedPath; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice>); -#endif // __cplusplus >= 202002L +#endif // defined(__cpp_lib_concepts) } // namespace wasm::analysis diff --git a/src/analysis/lattices/stack.h b/src/analysis/lattices/stack.h index 494d9f037d8..1f3908a4e02 100644 --- a/src/analysis/lattices/stack.h +++ b/src/analysis/lattices/stack.h @@ -185,7 +185,7 @@ template struct Stack { // Deduction guide. template Stack(L&&) -> Stack; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(Lattice>); #endif diff --git a/src/analysis/lattices/tuple.h b/src/analysis/lattices/tuple.h index d63d81f47f0..fe122df0620 100644 --- a/src/analysis/lattices/tuple.h +++ b/src/analysis/lattices/tuple.h @@ -137,7 +137,7 @@ template struct Tuple { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(FullLattice>); static_assert(FullLattice>); #endif diff --git a/src/analysis/lattices/valtype.h b/src/analysis/lattices/valtype.h index d63432ac668..eda53aa567f 100644 --- a/src/analysis/lattices/valtype.h +++ b/src/analysis/lattices/valtype.h @@ -73,7 +73,7 @@ struct ValType { } }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(FullLattice); #endif diff --git a/src/analysis/lattices/vector.h b/src/analysis/lattices/vector.h index 050b0f40812..930d0f934b6 100644 --- a/src/analysis/lattices/vector.h +++ b/src/analysis/lattices/vector.h @@ -154,7 +154,7 @@ template struct Vector { // Deduction guide. template Vector(L&&, size_t) -> Vector; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(FullLattice>); static_assert(Lattice>>); #endif diff --git a/src/analysis/transfer-function.h b/src/analysis/transfer-function.h index 58d2033efdf..02010a85916 100644 --- a/src/analysis/transfer-function.h +++ b/src/analysis/transfer-function.h @@ -17,11 +17,16 @@ #ifndef wasm_analysis_transfer_function_h #define wasm_analysis_transfer_function_h -#if __cplusplus >= 202002L - +#if __has_include() #include -#include +#endif +#if __has_include() #include +#endif + +#if defined(__cpp_lib_concepts) && defined(__cpp_lib_ranges) + +#include #include "cfg.h" #include "lattice.h" @@ -47,10 +52,10 @@ concept TransferFunctionImpl = requires( } // namespace wasm::analysis -#else // __cplusplus >= 202002L +#else // defined(__cpp_lib_concepts) && defined(__cpp_lib_ranges) #define TransferFunction typename -#endif // __cplusplus >= 202002L +#endif // defined(__cpp_lib_concepts) && defined(__cpp_lib_ranges) #endif // wasm_analysis_transfer_function_h diff --git a/src/support/graph_traversal.h b/src/support/graph_traversal.h index c7ad6ef02c9..282aee27256 100644 --- a/src/support/graph_traversal.h +++ b/src/support/graph_traversal.h @@ -14,7 +14,9 @@ * limitations under the License. */ +#if __has_include() #include +#endif #include #include #include @@ -32,10 +34,15 @@ namespace wasm { // successors([](const T&) { }, t); } template class Graph { public: +#if defined(__cpp_lib_concepts) template Sen> requires std::convertible_to, T> +#else + template +#endif Graph(It rootsBegin, Sen rootsEnd, SuccessorFunction successors) - : roots(rootsBegin, rootsEnd), successors(std::move(successors)) {} + : roots(rootsBegin, rootsEnd), successors(std::move(successors)) { + } // Traverse the graph depth-first, calling `successors` exactly once for each // node (unless the node appears multiple times in `roots`). Return the set of @@ -66,10 +73,17 @@ template class Graph { SuccessorFunction successors; }; +#if defined(__cpp_lib_concepts) template Sen, typename SuccessorFunction> Graph(It, Sen, SuccessorFunction) -> Graph, std::decay_t>; +#else +template +Graph(It, Sen, SuccessorFunction) + -> Graph::value_type, + std::decay_t>; +#endif } // namespace wasm diff --git a/src/tools/wasm-fuzz-lattices.cpp b/src/tools/wasm-fuzz-lattices.cpp index efd7a61a24f..4b29e8eb477 100644 --- a/src/tools/wasm-fuzz-lattices.cpp +++ b/src/tools/wasm-fuzz-lattices.cpp @@ -148,7 +148,7 @@ struct RandomLattice { bool join(Element& a, const Element& b) const noexcept; }; -#if __cplusplus >= 202002L +#if defined(__cpp_lib_concepts) static_assert(FullLattice); static_assert(Lattice); #endif From e7f8ce2e4d2831a48759fcb801031bbb9bbe26fc Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 5 May 2026 17:21:11 -0700 Subject: [PATCH 077/168] Rename relaxed SIMD instructions with prefix (#8673) Rename all relaxed SIMD instruction names in Binaryen (such as i16x8.dot_i8x16_i7x16_s and i32x4.dot_i8x16_i7x16_add_s) to prepend the "relaxed_" prefix. This prefix unifies relaxed SIMD instruction naming conventions across the repository, aligns them with standard WebAssembly specifications, and ensures consistent behavior in tools like the S-Expression printer, parser, validator, interpreter, and tests. --- CHANGELOG.md | 8 + scripts/gen-s-parser.py | 12 +- src/binaryen-c.cpp | 24 ++- src/binaryen-c.h | 12 +- src/gen-s-parser.inc | 197 ++++++++++++----------- src/ir/child-typer.h | 2 +- src/ir/cost.h | 12 +- src/js/binaryen.js-post.js | 12 +- src/passes/Print.cpp | 24 +-- src/passes/RemoveRelaxedSIMD.cpp | 12 +- src/wasm-interpreter.h | 12 +- src/wasm.h | 12 +- src/wasm/wasm-binary.cpp | 12 +- src/wasm/wasm-stack.cpp | 12 +- src/wasm/wasm-validator.cpp | 12 +- test/example/c-api-kitchen-sink.c | 12 +- test/example/c-api-kitchen-sink.txt | 12 +- test/lit/basic/relaxed-simd.wast | 84 +++++----- test/lit/exec/relaxed.wast | 6 +- test/lit/passes/remove-relaxed-simd.wast | 4 +- test/spec/dot_product.wast | 36 ++--- 21 files changed, 280 insertions(+), 249 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bdd2f6ef92..49b598836ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,14 @@ full changeset diff at the end of each section. Current Trunk ------------- + - Rename relaxed SIMD instructions to prepend the `relaxed_` prefix. + - Rename C and JS API operations to prepend the `Relaxed` prefix: + - `LaneselectI8x16` to `RelaxedLaneselectI8x16` + - `LaneselectI16x8` to `RelaxedLaneselectI16x8` + - `LaneselectI32x4` to `RelaxedLaneselectI32x4` + - `LaneselectI64x2` to `RelaxedLaneselectI64x2` + - `DotI8x16I7x16AddSToVecI32x4` to `RelaxedDotI8x16I7x16AddSToVecI32x4` + - `DotI8x16I7x16SToVecI16x8` to `RelaxedDotI8x16I7x16SToVecI16x8` - Rename `MemorySegment` functions to `DataSegment` in the c and js apis - Rename `BinaryenGetNumMemorySegments` to `BinaryenGetNumDataSegments` in c api. - Rename `BinaryenGetMemorySegmentByteOffset` to `BinaryenGetDataSegmentByteOffset` in c api. diff --git a/scripts/gen-s-parser.py b/scripts/gen-s-parser.py index 282854757ea..3c0a39a7bb2 100755 --- a/scripts/gen-s-parser.py +++ b/scripts/gen-s-parser.py @@ -571,17 +571,17 @@ ("f32x4.relaxed_nmadd", "makeSIMDTernary(SIMDTernaryOp::RelaxedNmaddVecF32x4)"), ("f64x2.relaxed_madd", "makeSIMDTernary(SIMDTernaryOp::RelaxedMaddVecF64x2)"), ("f64x2.relaxed_nmadd", "makeSIMDTernary(SIMDTernaryOp::RelaxedNmaddVecF64x2)"), - ("i8x16.laneselect", "makeSIMDTernary(SIMDTernaryOp::LaneselectI8x16)"), - ("i16x8.laneselect", "makeSIMDTernary(SIMDTernaryOp::LaneselectI16x8)"), - ("i32x4.laneselect", "makeSIMDTernary(SIMDTernaryOp::LaneselectI32x4)"), - ("i64x2.laneselect", "makeSIMDTernary(SIMDTernaryOp::LaneselectI64x2)"), + ("i8x16.relaxed_laneselect", "makeSIMDTernary(SIMDTernaryOp::RelaxedLaneselectI8x16)"), + ("i16x8.relaxed_laneselect", "makeSIMDTernary(SIMDTernaryOp::RelaxedLaneselectI16x8)"), + ("i32x4.relaxed_laneselect", "makeSIMDTernary(SIMDTernaryOp::RelaxedLaneselectI32x4)"), + ("i64x2.relaxed_laneselect", "makeSIMDTernary(SIMDTernaryOp::RelaxedLaneselectI64x2)"), ("f32x4.relaxed_min", "makeBinary(BinaryOp::RelaxedMinVecF32x4)"), ("f32x4.relaxed_max", "makeBinary(BinaryOp::RelaxedMaxVecF32x4)"), ("f64x2.relaxed_min", "makeBinary(BinaryOp::RelaxedMinVecF64x2)"), ("f64x2.relaxed_max", "makeBinary(BinaryOp::RelaxedMaxVecF64x2)"), ("i16x8.relaxed_q15mulr_s", "makeBinary(BinaryOp::RelaxedQ15MulrSVecI16x8)"), - ("i16x8.dot_i8x16_i7x16_s", "makeBinary(BinaryOp::DotI8x16I7x16SToVecI16x8)"), - ("i32x4.dot_i8x16_i7x16_add_s", "makeSIMDTernary(SIMDTernaryOp::DotI8x16I7x16AddSToVecI32x4)"), + ("i16x8.relaxed_dot_i8x16_i7x16_s", "makeBinary(BinaryOp::RelaxedDotI8x16I7x16SToVecI16x8)"), + ("i32x4.relaxed_dot_i8x16_i7x16_add_s", "makeSIMDTernary(SIMDTernaryOp::RelaxedDotI8x16I7x16AddSToVecI32x4)"), # reference types instructions ("ref.null", "makeRefNull()"), diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index 086f5dda28d..0f8cb4805f9 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -802,12 +802,20 @@ BinaryenOp BinaryenRelaxedMaddVecF32x4(void) { return RelaxedMaddVecF32x4; } BinaryenOp BinaryenRelaxedNmaddVecF32x4(void) { return RelaxedNmaddVecF32x4; } BinaryenOp BinaryenRelaxedMaddVecF64x2(void) { return RelaxedMaddVecF64x2; } BinaryenOp BinaryenRelaxedNmaddVecF64x2(void) { return RelaxedNmaddVecF64x2; } -BinaryenOp BinaryenLaneselectI8x16(void) { return LaneselectI8x16; } -BinaryenOp BinaryenLaneselectI16x8(void) { return LaneselectI16x8; } -BinaryenOp BinaryenLaneselectI32x4(void) { return LaneselectI32x4; } -BinaryenOp BinaryenLaneselectI64x2(void) { return LaneselectI64x2; } -BinaryenOp BinaryenDotI8x16I7x16AddSToVecI32x4(void) { - return DotI8x16I7x16AddSToVecI32x4; +BinaryenOp BinaryenRelaxedLaneselectI8x16(void) { + return RelaxedLaneselectI8x16; +} +BinaryenOp BinaryenRelaxedLaneselectI16x8(void) { + return RelaxedLaneselectI16x8; +} +BinaryenOp BinaryenRelaxedLaneselectI32x4(void) { + return RelaxedLaneselectI32x4; +} +BinaryenOp BinaryenRelaxedLaneselectI64x2(void) { + return RelaxedLaneselectI64x2; +} +BinaryenOp BinaryenRelaxedDotI8x16I7x16AddSToVecI32x4(void) { + return RelaxedDotI8x16I7x16AddSToVecI32x4; } BinaryenOp BinaryenAnyTrueVec128(void) { return AnyTrueVec128; } BinaryenOp BinaryenAbsVecI8x16(void) { return AbsVecI8x16; } @@ -1054,8 +1062,8 @@ BinaryenOp BinaryenRelaxedMaxVecF64x2(void) { return RelaxedMaxVecF64x2; } BinaryenOp BinaryenRelaxedQ15MulrSVecI16x8(void) { return RelaxedQ15MulrSVecI16x8; } -BinaryenOp BinaryenDotI8x16I7x16SToVecI16x8(void) { - return DotI8x16I7x16SToVecI16x8; +BinaryenOp BinaryenRelaxedDotI8x16I7x16SToVecI16x8(void) { + return RelaxedDotI8x16I7x16SToVecI16x8; } BinaryenOp BinaryenRefAsNonNull(void) { return RefAsNonNull; } BinaryenOp BinaryenRefAsExternInternalize(void) { return AnyConvertExtern; } diff --git a/src/binaryen-c.h b/src/binaryen-c.h index d18f3638e2d..fbde9d2a08d 100644 --- a/src/binaryen-c.h +++ b/src/binaryen-c.h @@ -527,11 +527,11 @@ BINARYEN_API BinaryenOp BinaryenRelaxedMaddVecF32x4(void); BINARYEN_API BinaryenOp BinaryenRelaxedNmaddVecF32x4(void); BINARYEN_API BinaryenOp BinaryenRelaxedMaddVecF64x2(void); BINARYEN_API BinaryenOp BinaryenRelaxedNmaddVecF64x2(void); -BINARYEN_API BinaryenOp BinaryenLaneselectI8x16(void); -BINARYEN_API BinaryenOp BinaryenLaneselectI16x8(void); -BINARYEN_API BinaryenOp BinaryenLaneselectI32x4(void); -BINARYEN_API BinaryenOp BinaryenLaneselectI64x2(void); -BINARYEN_API BinaryenOp BinaryenDotI8x16I7x16AddSToVecI32x4(void); +BINARYEN_API BinaryenOp BinaryenRelaxedLaneselectI8x16(void); +BINARYEN_API BinaryenOp BinaryenRelaxedLaneselectI16x8(void); +BINARYEN_API BinaryenOp BinaryenRelaxedLaneselectI32x4(void); +BINARYEN_API BinaryenOp BinaryenRelaxedLaneselectI64x2(void); +BINARYEN_API BinaryenOp BinaryenRelaxedDotI8x16I7x16AddSToVecI32x4(void); BINARYEN_API BinaryenOp BinaryenAnyTrueVec128(void); BINARYEN_API BinaryenOp BinaryenPopcntVecI8x16(void); BINARYEN_API BinaryenOp BinaryenAbsVecI8x16(void); @@ -701,7 +701,7 @@ BINARYEN_API BinaryenOp BinaryenRelaxedMaxVecF32x4(void); BINARYEN_API BinaryenOp BinaryenRelaxedMinVecF64x2(void); BINARYEN_API BinaryenOp BinaryenRelaxedMaxVecF64x2(void); BINARYEN_API BinaryenOp BinaryenRelaxedQ15MulrSVecI16x8(void); -BINARYEN_API BinaryenOp BinaryenDotI8x16I7x16SToVecI16x8(void); +BINARYEN_API BinaryenOp BinaryenRelaxedDotI8x16I7x16SToVecI16x8(void); BINARYEN_API BinaryenOp BinaryenRefAsNonNull(void); BINARYEN_API BinaryenOp BinaryenRefAsExternInternalize(void); BINARYEN_API BinaryenOp BinaryenRefAsExternExternalize(void); diff --git a/src/gen-s-parser.inc b/src/gen-s-parser.inc index ac2ab0a1330..2560826ea80 100644 --- a/src/gen-s-parser.inc +++ b/src/gen-s-parser.inc @@ -5,7 +5,7 @@ // NOLINTBEGIN auto op = *keyword; -char buf[33] = {}; +char buf[36] = {}; // Ensure we do not copy more than the buffer can hold if (op.size() >= sizeof(buf)) { goto parse_error; @@ -1852,12 +1852,6 @@ switch (buf[0]) { return Ok{}; } goto parse_error; - case 'd': - if (op == "i16x8.dot_i8x16_i7x16_s"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::DotI8x16I7x16SToVecI16x8)); - return Ok{}; - } - goto parse_error; case 'e': { switch (buf[7]) { case 'q': @@ -2027,12 +2021,6 @@ switch (buf[0]) { } case 'l': { switch (buf[7]) { - case 'a': - if (op == "i16x8.laneselect"sv) { - CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::LaneselectI16x8)); - return Ok{}; - } - goto parse_error; case 'e': { switch (buf[9]) { case 's': @@ -2162,12 +2150,29 @@ switch (buf[0]) { goto parse_error; case 'r': { switch (buf[8]) { - case 'l': - if (op == "i16x8.relaxed_q15mulr_s"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::RelaxedQ15MulrSVecI16x8)); - return Ok{}; + case 'l': { + switch (buf[14]) { + case 'd': + if (op == "i16x8.relaxed_dot_i8x16_i7x16_s"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::RelaxedDotI8x16I7x16SToVecI16x8)); + return Ok{}; + } + goto parse_error; + case 'l': + if (op == "i16x8.relaxed_laneselect"sv) { + CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::RelaxedLaneselectI16x8)); + return Ok{}; + } + goto parse_error; + case 'q': + if (op == "i16x8.relaxed_q15mulr_s"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::RelaxedQ15MulrSVecI16x8)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } case 'p': if (op == "i16x8.replace_lane"sv) { CHECK_ERR(makeSIMDReplace(ctx, pos, annotations, SIMDReplaceOp::ReplaceLaneVecI16x8, 8)); @@ -2998,23 +3003,12 @@ switch (buf[0]) { return Ok{}; } goto parse_error; - case 'd': { - switch (buf[11]) { - case '1': - if (op == "i32x4.dot_i16x8_s"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::DotSVecI16x8ToVecI32x4)); - return Ok{}; - } - goto parse_error; - case '8': - if (op == "i32x4.dot_i8x16_i7x16_add_s"sv) { - CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::DotI8x16I7x16AddSToVecI32x4)); - return Ok{}; - } - goto parse_error; - default: goto parse_error; + case 'd': + if (op == "i32x4.dot_i16x8_s"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::DotSVecI16x8ToVecI32x4)); + return Ok{}; } - } + goto parse_error; case 'e': { switch (buf[7]) { case 'q': @@ -3173,12 +3167,6 @@ switch (buf[0]) { } case 'l': { switch (buf[7]) { - case 'a': - if (op == "i32x4.laneselect"sv) { - CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::LaneselectI32x4)); - return Ok{}; - } - goto parse_error; case 'e': { switch (buf[9]) { case 's': @@ -3281,38 +3269,55 @@ switch (buf[0]) { case 'r': { switch (buf[8]) { case 'l': { - switch (buf[21]) { - case '3': { - switch (buf[26]) { - case 's': - if (op == "i32x4.relaxed_trunc_f32x4_s"sv) { - CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncSVecF32x4ToVecI32x4)); - return Ok{}; - } - goto parse_error; - case 'u': - if (op == "i32x4.relaxed_trunc_f32x4_u"sv) { - CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncUVecF32x4ToVecI32x4)); - return Ok{}; - } - goto parse_error; - default: goto parse_error; + switch (buf[14]) { + case 'd': + if (op == "i32x4.relaxed_dot_i8x16_i7x16_add_s"sv) { + CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::RelaxedDotI8x16I7x16AddSToVecI32x4)); + return Ok{}; } - } - case '6': { - switch (buf[26]) { - case 's': - if (op == "i32x4.relaxed_trunc_f64x2_s_zero"sv) { - CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncZeroSVecF64x2ToVecI32x4)); - return Ok{}; + goto parse_error; + case 'l': + if (op == "i32x4.relaxed_laneselect"sv) { + CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::RelaxedLaneselectI32x4)); + return Ok{}; + } + goto parse_error; + case 't': { + switch (buf[21]) { + case '3': { + switch (buf[26]) { + case 's': + if (op == "i32x4.relaxed_trunc_f32x4_s"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncSVecF32x4ToVecI32x4)); + return Ok{}; + } + goto parse_error; + case 'u': + if (op == "i32x4.relaxed_trunc_f32x4_u"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncUVecF32x4ToVecI32x4)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; - case 'u': - if (op == "i32x4.relaxed_trunc_f64x2_u_zero"sv) { - CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncZeroUVecF64x2ToVecI32x4)); - return Ok{}; + } + case '6': { + switch (buf[26]) { + case 's': + if (op == "i32x4.relaxed_trunc_f64x2_s_zero"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncZeroSVecF64x2ToVecI32x4)); + return Ok{}; + } + goto parse_error; + case 'u': + if (op == "i32x4.relaxed_trunc_f64x2_u_zero"sv) { + CHECK_ERR(makeUnary(ctx, pos, annotations, UnaryOp::RelaxedTruncZeroUVecF64x2ToVecI32x4)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } default: goto parse_error; } } @@ -4408,12 +4413,6 @@ switch (buf[0]) { } case 'l': { switch (buf[7]) { - case 'a': - if (op == "i64x2.laneselect"sv) { - CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::LaneselectI64x2)); - return Ok{}; - } - goto parse_error; case 'e': if (op == "i64x2.le_s"sv) { CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::LeSVecI64x2)); @@ -4452,12 +4451,23 @@ switch (buf[0]) { default: goto parse_error; } } - case 'r': - if (op == "i64x2.replace_lane"sv) { - CHECK_ERR(makeSIMDReplace(ctx, pos, annotations, SIMDReplaceOp::ReplaceLaneVecI64x2, 2)); - return Ok{}; + case 'r': { + switch (buf[8]) { + case 'l': + if (op == "i64x2.relaxed_laneselect"sv) { + CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::RelaxedLaneselectI64x2)); + return Ok{}; + } + goto parse_error; + case 'p': + if (op == "i64x2.replace_lane"sv) { + CHECK_ERR(makeSIMDReplace(ctx, pos, annotations, SIMDReplaceOp::ReplaceLaneVecI64x2, 2)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } case 's': { switch (buf[7]) { case 'h': { @@ -4637,12 +4647,6 @@ switch (buf[0]) { } case 'l': { switch (buf[7]) { - case 'a': - if (op == "i8x16.laneselect"sv) { - CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::LaneselectI8x16)); - return Ok{}; - } - goto parse_error; case 'e': { switch (buf[9]) { case 's': @@ -4766,12 +4770,23 @@ switch (buf[0]) { goto parse_error; case 'r': { switch (buf[8]) { - case 'l': - if (op == "i8x16.relaxed_swizzle"sv) { - CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::RelaxedSwizzleVecI8x16)); - return Ok{}; + case 'l': { + switch (buf[14]) { + case 'l': + if (op == "i8x16.relaxed_laneselect"sv) { + CHECK_ERR(makeSIMDTernary(ctx, pos, annotations, SIMDTernaryOp::RelaxedLaneselectI8x16)); + return Ok{}; + } + goto parse_error; + case 's': + if (op == "i8x16.relaxed_swizzle"sv) { + CHECK_ERR(makeBinary(ctx, pos, annotations, BinaryOp::RelaxedSwizzleVecI8x16)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } case 'p': if (op == "i8x16.replace_lane"sv) { CHECK_ERR(makeSIMDReplace(ctx, pos, annotations, SIMDReplaceOp::ReplaceLaneVecI8x16, 16)); diff --git a/src/ir/child-typer.h b/src/ir/child-typer.h index b87cd3f9c16..089b21f86e0 100644 --- a/src/ir/child-typer.h +++ b/src/ir/child-typer.h @@ -696,7 +696,7 @@ template struct ChildTyper : OverriddenVisitor { case SwizzleVecI8x16: case RelaxedSwizzleVecI8x16: case RelaxedQ15MulrSVecI16x8: - case DotI8x16I7x16SToVecI16x8: + case RelaxedDotI8x16I7x16SToVecI16x8: note(&curr->left, Type::v128); note(&curr->right, Type::v128); break; diff --git a/src/ir/cost.h b/src/ir/cost.h index 99bf7c05333..1cb70e87aac 100644 --- a/src/ir/cost.h +++ b/src/ir/cost.h @@ -567,7 +567,7 @@ struct CostAnalyzer : public OverriddenVisitor { case SwizzleVecI8x16: case RelaxedSwizzleVecI8x16: case RelaxedQ15MulrSVecI16x8: - case DotI8x16I7x16SToVecI16x8: + case RelaxedDotI8x16I7x16SToVecI16x8: ret = 1; break; case InvalidBinary: @@ -615,17 +615,17 @@ struct CostAnalyzer : public OverriddenVisitor { CostType ret = 0; switch (curr->op) { case Bitselect: - case LaneselectI8x16: - case LaneselectI16x8: - case LaneselectI32x4: - case LaneselectI64x2: + case RelaxedLaneselectI8x16: + case RelaxedLaneselectI16x8: + case RelaxedLaneselectI32x4: + case RelaxedLaneselectI64x2: case MaddVecF16x8: case NmaddVecF16x8: case RelaxedMaddVecF32x4: case RelaxedNmaddVecF32x4: case RelaxedMaddVecF64x2: case RelaxedNmaddVecF64x2: - case DotI8x16I7x16AddSToVecI32x4: + case RelaxedDotI8x16I7x16AddSToVecI32x4: ret = 1; break; } diff --git a/src/js/binaryen.js-post.js b/src/js/binaryen.js-post.js index 97db8ce7f5c..a3494d31ca4 100644 --- a/src/js/binaryen.js-post.js +++ b/src/js/binaryen.js-post.js @@ -422,11 +422,11 @@ function initializeConstants() { 'RelaxedNmaddVecF32x4', 'RelaxedMaddVecF64x2', 'RelaxedNmaddVecF64x2', - 'LaneselectI8x16', - 'LaneselectI16x8', - 'LaneselectI32x4', - 'LaneselectI64x2', - 'DotI8x16I7x16AddSToVecI32x4', + 'RelaxedLaneselectI8x16', + 'RelaxedLaneselectI16x8', + 'RelaxedLaneselectI32x4', + 'RelaxedLaneselectI64x2', + 'RelaxedDotI8x16I7x16AddSToVecI32x4', 'AnyTrueVec128', 'PopcntVecI8x16', 'AbsVecI8x16', @@ -595,7 +595,7 @@ function initializeConstants() { 'RelaxedMinVecF64x2', 'RelaxedMaxVecF64x2', 'RelaxedQ15MulrSVecI16x8', - 'DotI8x16I7x16SToVecI16x8', + 'RelaxedDotI8x16I7x16SToVecI16x8', 'RefAsNonNull', 'RefAsExternInternalize', 'RefAsExternExternalize', diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 4ffdb031dd4..02e555a8c4b 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -722,17 +722,17 @@ struct PrintExpressionContents case Bitselect: o << "v128.bitselect"; break; - case LaneselectI8x16: - o << "i8x16.laneselect"; + case RelaxedLaneselectI8x16: + o << "i8x16.relaxed_laneselect"; break; - case LaneselectI16x8: - o << "i16x8.laneselect"; + case RelaxedLaneselectI16x8: + o << "i16x8.relaxed_laneselect"; break; - case LaneselectI32x4: - o << "i32x4.laneselect"; + case RelaxedLaneselectI32x4: + o << "i32x4.relaxed_laneselect"; break; - case LaneselectI64x2: - o << "i64x2.laneselect"; + case RelaxedLaneselectI64x2: + o << "i64x2.relaxed_laneselect"; break; case MaddVecF16x8: o << "f16x8.madd"; @@ -752,8 +752,8 @@ struct PrintExpressionContents case RelaxedNmaddVecF64x2: o << "f64x2.relaxed_nmadd"; break; - case DotI8x16I7x16AddSToVecI32x4: - o << "i32x4.dot_i8x16_i7x16_add_s"; + case RelaxedDotI8x16I7x16AddSToVecI32x4: + o << "i32x4.relaxed_dot_i8x16_i7x16_add_s"; break; } restoreNormalColor(o); @@ -2022,8 +2022,8 @@ struct PrintExpressionContents case RelaxedQ15MulrSVecI16x8: o << "i16x8.relaxed_q15mulr_s"; break; - case DotI8x16I7x16SToVecI16x8: - o << "i16x8.dot_i8x16_i7x16_s"; + case RelaxedDotI8x16I7x16SToVecI16x8: + o << "i16x8.relaxed_dot_i8x16_i7x16_s"; break; case InvalidBinary: diff --git a/src/passes/RemoveRelaxedSIMD.cpp b/src/passes/RemoveRelaxedSIMD.cpp index b09319d8fce..61f65830d12 100644 --- a/src/passes/RemoveRelaxedSIMD.cpp +++ b/src/passes/RemoveRelaxedSIMD.cpp @@ -64,7 +64,7 @@ struct RemoveRelaxedSIMD : WalkerPass> { case RelaxedMinVecF64x2: case RelaxedMaxVecF64x2: case RelaxedQ15MulrSVecI16x8: - case DotI8x16I7x16SToVecI16x8: + case RelaxedDotI8x16I7x16SToVecI16x8: replace(curr); return; default: @@ -78,11 +78,11 @@ struct RemoveRelaxedSIMD : WalkerPass> { case RelaxedNmaddVecF32x4: case RelaxedMaddVecF64x2: case RelaxedNmaddVecF64x2: - case LaneselectI8x16: - case LaneselectI16x8: - case LaneselectI32x4: - case LaneselectI64x2: - case DotI8x16I7x16AddSToVecI32x4: + case RelaxedLaneselectI8x16: + case RelaxedLaneselectI16x8: + case RelaxedLaneselectI32x4: + case RelaxedLaneselectI64x2: + case RelaxedDotI8x16I7x16AddSToVecI32x4: replace(curr); return; default: diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 67101d641e8..c018ca4484e 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -1653,7 +1653,7 @@ class ExpressionRunner : public OverriddenVisitor { case SwizzleVecI8x16: return left.swizzleI8x16(right); - case DotI8x16I7x16SToVecI16x8: + case RelaxedDotI8x16I7x16SToVecI16x8: return left.dotSI8x16toI16x8(right); case InvalidBinary: @@ -1725,10 +1725,10 @@ class ExpressionRunner : public OverriddenVisitor { Literal c = flow.getSingleValue(); switch (curr->op) { case Bitselect: - case LaneselectI8x16: - case LaneselectI16x8: - case LaneselectI32x4: - case LaneselectI64x2: + case RelaxedLaneselectI8x16: + case RelaxedLaneselectI16x8: + case RelaxedLaneselectI32x4: + case RelaxedLaneselectI64x2: return c.bitselectV128(a, b); case MaddVecF16x8: @@ -1755,7 +1755,7 @@ class ExpressionRunner : public OverriddenVisitor { return NONCONSTANT_FLOW; } return a.relaxedNmaddF64x2(b, c); - case DotI8x16I7x16AddSToVecI32x4: + case RelaxedDotI8x16I7x16AddSToVecI32x4: if (relaxedBehavior == RelaxedBehavior::NonConstant) { return NONCONSTANT_FLOW; } diff --git a/src/wasm.h b/src/wasm.h index 6fe34edb08f..df0c19669d3 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -516,7 +516,7 @@ enum BinaryOp { RelaxedMinVecF64x2, RelaxedMaxVecF64x2, RelaxedQ15MulrSVecI16x8, - DotI8x16I7x16SToVecI16x8, + RelaxedDotI8x16I7x16SToVecI16x8, InvalidBinary }; @@ -594,11 +594,11 @@ enum SIMDTernaryOp { RelaxedNmaddVecF32x4, RelaxedMaddVecF64x2, RelaxedNmaddVecF64x2, - LaneselectI8x16, - LaneselectI16x8, - LaneselectI32x4, - LaneselectI64x2, - DotI8x16I7x16AddSToVecI32x4, + RelaxedLaneselectI8x16, + RelaxedLaneselectI16x8, + RelaxedLaneselectI32x4, + RelaxedLaneselectI64x2, + RelaxedDotI8x16I7x16AddSToVecI32x4, // FP16 MaddVecF16x8, NmaddVecF16x8, diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 61d505bd205..2ee7975da1d 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -4321,7 +4321,7 @@ Result<> WasmBinaryReader::readInst() { case BinaryConsts::I16x8RelaxedQ15MulrS: return builder.makeBinary(RelaxedQ15MulrSVecI16x8); case BinaryConsts::I16x8DotI8x16I7x16S: - return builder.makeBinary(DotI8x16I7x16SToVecI16x8); + return builder.makeBinary(RelaxedDotI8x16I7x16SToVecI16x8); case BinaryConsts::I8x16Splat: return builder.makeUnary(SplatVecI8x16); case BinaryConsts::I16x8Splat: @@ -4534,13 +4534,13 @@ Result<> WasmBinaryReader::readInst() { case BinaryConsts::V128Bitselect: return builder.makeSIMDTernary(Bitselect); case BinaryConsts::I8x16Laneselect: - return builder.makeSIMDTernary(LaneselectI8x16); + return builder.makeSIMDTernary(RelaxedLaneselectI8x16); case BinaryConsts::I16x8Laneselect: - return builder.makeSIMDTernary(LaneselectI16x8); + return builder.makeSIMDTernary(RelaxedLaneselectI16x8); case BinaryConsts::I32x4Laneselect: - return builder.makeSIMDTernary(LaneselectI32x4); + return builder.makeSIMDTernary(RelaxedLaneselectI32x4); case BinaryConsts::I64x2Laneselect: - return builder.makeSIMDTernary(LaneselectI64x2); + return builder.makeSIMDTernary(RelaxedLaneselectI64x2); case BinaryConsts::F16x8Madd: return builder.makeSIMDTernary(MaddVecF16x8); case BinaryConsts::F16x8Nmadd: @@ -4554,7 +4554,7 @@ Result<> WasmBinaryReader::readInst() { case BinaryConsts::F64x2RelaxedNmadd: return builder.makeSIMDTernary(RelaxedNmaddVecF64x2); case BinaryConsts::I32x4DotI8x16I7x16AddS: - return builder.makeSIMDTernary(DotI8x16I7x16AddSToVecI32x4); + return builder.makeSIMDTernary(RelaxedDotI8x16I7x16AddSToVecI32x4); case BinaryConsts::I8x16Shl: return builder.makeSIMDShift(ShlVecI8x16); case BinaryConsts::I8x16ShrS: diff --git a/src/wasm/wasm-stack.cpp b/src/wasm/wasm-stack.cpp index 8cb99e9cccb..13f52570b22 100644 --- a/src/wasm/wasm-stack.cpp +++ b/src/wasm/wasm-stack.cpp @@ -715,16 +715,16 @@ void BinaryInstWriter::visitSIMDTernary(SIMDTernary* curr) { case Bitselect: o << U32LEB(BinaryConsts::V128Bitselect); break; - case LaneselectI8x16: + case RelaxedLaneselectI8x16: o << U32LEB(BinaryConsts::I8x16Laneselect); break; - case LaneselectI16x8: + case RelaxedLaneselectI16x8: o << U32LEB(BinaryConsts::I16x8Laneselect); break; - case LaneselectI32x4: + case RelaxedLaneselectI32x4: o << U32LEB(BinaryConsts::I32x4Laneselect); break; - case LaneselectI64x2: + case RelaxedLaneselectI64x2: o << U32LEB(BinaryConsts::I64x2Laneselect); break; case MaddVecF16x8: @@ -745,7 +745,7 @@ void BinaryInstWriter::visitSIMDTernary(SIMDTernary* curr) { case RelaxedNmaddVecF64x2: o << U32LEB(BinaryConsts::F64x2RelaxedNmadd); break; - case DotI8x16I7x16AddSToVecI32x4: + case RelaxedDotI8x16I7x16AddSToVecI32x4: o << U32LEB(BinaryConsts::I32x4DotI8x16I7x16AddS); break; } @@ -2272,7 +2272,7 @@ void BinaryInstWriter::visitBinary(Binary* curr) { o << static_cast(BinaryConsts::SIMDPrefix) << U32LEB(BinaryConsts::I16x8RelaxedQ15MulrS); break; - case DotI8x16I7x16SToVecI16x8: + case RelaxedDotI8x16I7x16SToVecI16x8: o << static_cast(BinaryConsts::SIMDPrefix) << U32LEB(BinaryConsts::I16x8DotI8x16I7x16S); break; diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index 954f0676f55..b099926112c 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -1579,15 +1579,15 @@ void FunctionValidator::visitSIMDShuffle(SIMDShuffle* curr) { void FunctionValidator::visitSIMDTernary(SIMDTernary* curr) { FeatureSet required = FeatureSet::None; switch (curr->op) { - case LaneselectI8x16: - case LaneselectI16x8: - case LaneselectI32x4: - case LaneselectI64x2: + case RelaxedLaneselectI8x16: + case RelaxedLaneselectI16x8: + case RelaxedLaneselectI32x4: + case RelaxedLaneselectI64x2: case RelaxedMaddVecF32x4: case RelaxedNmaddVecF32x4: case RelaxedMaddVecF64x2: case RelaxedNmaddVecF64x2: - case DotI8x16I7x16AddSToVecI32x4: + case RelaxedDotI8x16I7x16AddSToVecI32x4: required |= FeatureSet::RelaxedSIMD | FeatureSet::SIMD; break; case MaddVecF16x8: @@ -2095,7 +2095,7 @@ void FunctionValidator::visitBinary(Binary* curr) { case SwizzleVecI8x16: case RelaxedSwizzleVecI8x16: case RelaxedQ15MulrSVecI16x8: - case DotI8x16I7x16SToVecI16x8: { + case RelaxedDotI8x16I7x16SToVecI16x8: { shouldBeEqualOrFirstIsUnreachable( curr->left->type, Type(Type::v128), curr, "v128 op"); shouldBeEqualOrFirstIsUnreachable( diff --git a/test/example/c-api-kitchen-sink.c b/test/example/c-api-kitchen-sink.c index 8c9818ece6b..a03f670034d 100644 --- a/test/example/c-api-kitchen-sink.c +++ b/test/example/c-api-kitchen-sink.c @@ -856,7 +856,7 @@ void test_core() { makeBinary(module, BinaryenRelaxedMinVecF64x2(), v128), makeBinary(module, BinaryenRelaxedMaxVecF64x2(), v128), makeBinary(module, BinaryenRelaxedQ15MulrSVecI16x8(), v128), - makeBinary(module, BinaryenDotI8x16I7x16SToVecI16x8(), v128), + makeBinary(module, BinaryenRelaxedDotI8x16I7x16SToVecI16x8(), v128), // SIMD lane manipulation makeSIMDExtract(module, BinaryenExtractLaneSVecI8x16()), makeSIMDExtract(module, BinaryenExtractLaneUVecI8x16()), @@ -982,11 +982,11 @@ void test_core() { makeSIMDTernary(module, BinaryenRelaxedNmaddVecF32x4()), makeSIMDTernary(module, BinaryenRelaxedMaddVecF64x2()), makeSIMDTernary(module, BinaryenRelaxedNmaddVecF64x2()), - makeSIMDTernary(module, BinaryenLaneselectI8x16()), - makeSIMDTernary(module, BinaryenLaneselectI16x8()), - makeSIMDTernary(module, BinaryenLaneselectI32x4()), - makeSIMDTernary(module, BinaryenLaneselectI64x2()), - makeSIMDTernary(module, BinaryenDotI8x16I7x16AddSToVecI32x4()), + makeSIMDTernary(module, BinaryenRelaxedLaneselectI8x16()), + makeSIMDTernary(module, BinaryenRelaxedLaneselectI16x8()), + makeSIMDTernary(module, BinaryenRelaxedLaneselectI32x4()), + makeSIMDTernary(module, BinaryenRelaxedLaneselectI64x2()), + makeSIMDTernary(module, BinaryenRelaxedDotI8x16I7x16AddSToVecI32x4()), // Bulk memory makeMemoryInit(module), makeDataDrop(module), diff --git a/test/example/c-api-kitchen-sink.txt b/test/example/c-api-kitchen-sink.txt index 8c1dead6d1f..4907ac0cdd0 100644 --- a/test/example/c-api-kitchen-sink.txt +++ b/test/example/c-api-kitchen-sink.txt @@ -1627,7 +1627,7 @@ BinaryenFeatureAll: 67108863 ) ) (drop - (i16x8.dot_i8x16_i7x16_s + (i16x8.relaxed_dot_i8x16_i7x16_s (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) ) @@ -1922,35 +1922,35 @@ BinaryenFeatureAll: 67108863 ) ) (drop - (i8x16.laneselect + (i8x16.relaxed_laneselect (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) ) ) (drop - (i16x8.laneselect + (i16x8.relaxed_laneselect (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) ) ) (drop - (i32x4.laneselect + (i32x4.relaxed_laneselect (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) ) ) (drop - (i64x2.laneselect + (i64x2.relaxed_laneselect (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) ) ) (drop - (i32x4.dot_i8x16_i7x16_add_s + (i32x4.relaxed_dot_i8x16_i7x16_add_s (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) (v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d) diff --git a/test/lit/basic/relaxed-simd.wast b/test/lit/basic/relaxed-simd.wast index 1624d18b4d8..fcecba768c6 100644 --- a/test/lit/basic/relaxed-simd.wast +++ b/test/lit/basic/relaxed-simd.wast @@ -199,88 +199,88 @@ ) ) - ;; CHECK-TEXT: (func $i8x16.laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - ;; CHECK-TEXT-NEXT: (i8x16.laneselect + ;; CHECK-TEXT: (func $i8x16.relaxed_laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + ;; CHECK-TEXT-NEXT: (i8x16.relaxed_laneselect ;; CHECK-TEXT-NEXT: (local.get $0) ;; CHECK-TEXT-NEXT: (local.get $1) ;; CHECK-TEXT-NEXT: (local.get $2) ;; CHECK-TEXT-NEXT: ) ;; CHECK-TEXT-NEXT: ) - ;; CHECK-BIN: (func $i8x16.laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - ;; CHECK-BIN-NEXT: (i8x16.laneselect + ;; CHECK-BIN: (func $i8x16.relaxed_laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + ;; CHECK-BIN-NEXT: (i8x16.relaxed_laneselect ;; CHECK-BIN-NEXT: (local.get $0) ;; CHECK-BIN-NEXT: (local.get $1) ;; CHECK-BIN-NEXT: (local.get $2) ;; CHECK-BIN-NEXT: ) ;; CHECK-BIN-NEXT: ) - (func $i8x16.laneselect (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - (i8x16.laneselect + (func $i8x16.relaxed_laneselect (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + (i8x16.relaxed_laneselect (local.get $0) (local.get $1) (local.get $2) ) ) - ;; CHECK-TEXT: (func $i16x8.laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - ;; CHECK-TEXT-NEXT: (i16x8.laneselect + ;; CHECK-TEXT: (func $i16x8.relaxed_laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + ;; CHECK-TEXT-NEXT: (i16x8.relaxed_laneselect ;; CHECK-TEXT-NEXT: (local.get $0) ;; CHECK-TEXT-NEXT: (local.get $1) ;; CHECK-TEXT-NEXT: (local.get $2) ;; CHECK-TEXT-NEXT: ) ;; CHECK-TEXT-NEXT: ) - ;; CHECK-BIN: (func $i16x8.laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - ;; CHECK-BIN-NEXT: (i16x8.laneselect + ;; CHECK-BIN: (func $i16x8.relaxed_laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + ;; CHECK-BIN-NEXT: (i16x8.relaxed_laneselect ;; CHECK-BIN-NEXT: (local.get $0) ;; CHECK-BIN-NEXT: (local.get $1) ;; CHECK-BIN-NEXT: (local.get $2) ;; CHECK-BIN-NEXT: ) ;; CHECK-BIN-NEXT: ) - (func $i16x8.laneselect (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - (i16x8.laneselect + (func $i16x8.relaxed_laneselect (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + (i16x8.relaxed_laneselect (local.get $0) (local.get $1) (local.get $2) ) ) - ;; CHECK-TEXT: (func $i32x4.laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - ;; CHECK-TEXT-NEXT: (i32x4.laneselect + ;; CHECK-TEXT: (func $i32x4.relaxed_laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + ;; CHECK-TEXT-NEXT: (i32x4.relaxed_laneselect ;; CHECK-TEXT-NEXT: (local.get $0) ;; CHECK-TEXT-NEXT: (local.get $1) ;; CHECK-TEXT-NEXT: (local.get $2) ;; CHECK-TEXT-NEXT: ) ;; CHECK-TEXT-NEXT: ) - ;; CHECK-BIN: (func $i32x4.laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - ;; CHECK-BIN-NEXT: (i32x4.laneselect + ;; CHECK-BIN: (func $i32x4.relaxed_laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + ;; CHECK-BIN-NEXT: (i32x4.relaxed_laneselect ;; CHECK-BIN-NEXT: (local.get $0) ;; CHECK-BIN-NEXT: (local.get $1) ;; CHECK-BIN-NEXT: (local.get $2) ;; CHECK-BIN-NEXT: ) ;; CHECK-BIN-NEXT: ) - (func $i32x4.laneselect (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - (i32x4.laneselect + (func $i32x4.relaxed_laneselect (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + (i32x4.relaxed_laneselect (local.get $0) (local.get $1) (local.get $2) ) ) - ;; CHECK-TEXT: (func $i64x2.laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - ;; CHECK-TEXT-NEXT: (i64x2.laneselect + ;; CHECK-TEXT: (func $i64x2.relaxed_laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + ;; CHECK-TEXT-NEXT: (i64x2.relaxed_laneselect ;; CHECK-TEXT-NEXT: (local.get $0) ;; CHECK-TEXT-NEXT: (local.get $1) ;; CHECK-TEXT-NEXT: (local.get $2) ;; CHECK-TEXT-NEXT: ) ;; CHECK-TEXT-NEXT: ) - ;; CHECK-BIN: (func $i64x2.laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - ;; CHECK-BIN-NEXT: (i64x2.laneselect + ;; CHECK-BIN: (func $i64x2.relaxed_laneselect (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + ;; CHECK-BIN-NEXT: (i64x2.relaxed_laneselect ;; CHECK-BIN-NEXT: (local.get $0) ;; CHECK-BIN-NEXT: (local.get $1) ;; CHECK-BIN-NEXT: (local.get $2) ;; CHECK-BIN-NEXT: ) ;; CHECK-BIN-NEXT: ) - (func $i64x2.laneselect (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - (i64x2.laneselect + (func $i64x2.relaxed_laneselect (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + (i64x2.relaxed_laneselect (local.get $0) (local.get $1) (local.get $2) @@ -382,41 +382,41 @@ ) ) - ;; CHECK-TEXT: (func $i16x8.dot_i8x16_i7x16_s (type $1) (param $0 v128) (param $1 v128) (result v128) - ;; CHECK-TEXT-NEXT: (i16x8.dot_i8x16_i7x16_s + ;; CHECK-TEXT: (func $i16x8.relaxed_dot_i8x16_i7x16_s (type $1) (param $0 v128) (param $1 v128) (result v128) + ;; CHECK-TEXT-NEXT: (i16x8.relaxed_dot_i8x16_i7x16_s ;; CHECK-TEXT-NEXT: (local.get $0) ;; CHECK-TEXT-NEXT: (local.get $1) ;; CHECK-TEXT-NEXT: ) ;; CHECK-TEXT-NEXT: ) - ;; CHECK-BIN: (func $i16x8.dot_i8x16_i7x16_s (type $1) (param $0 v128) (param $1 v128) (result v128) - ;; CHECK-BIN-NEXT: (i16x8.dot_i8x16_i7x16_s + ;; CHECK-BIN: (func $i16x8.relaxed_dot_i8x16_i7x16_s (type $1) (param $0 v128) (param $1 v128) (result v128) + ;; CHECK-BIN-NEXT: (i16x8.relaxed_dot_i8x16_i7x16_s ;; CHECK-BIN-NEXT: (local.get $0) ;; CHECK-BIN-NEXT: (local.get $1) ;; CHECK-BIN-NEXT: ) ;; CHECK-BIN-NEXT: ) - (func $i16x8.dot_i8x16_i7x16_s (param $0 v128) (param $1 v128) (result v128) - (i16x8.dot_i8x16_i7x16_s + (func $i16x8.relaxed_dot_i8x16_i7x16_s (param $0 v128) (param $1 v128) (result v128) + (i16x8.relaxed_dot_i8x16_i7x16_s (local.get $0) (local.get $1) ) ) -;; CHECK-TEXT: (func $i32x4.dot_i8x16_i7x16_add_s (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) -;; CHECK-TEXT-NEXT: (i32x4.dot_i8x16_i7x16_add_s +;; CHECK-TEXT: (func $i32x4.relaxed_dot_i8x16_i7x16_add_s (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) +;; CHECK-TEXT-NEXT: (i32x4.relaxed_dot_i8x16_i7x16_add_s ;; CHECK-TEXT-NEXT: (local.get $0) ;; CHECK-TEXT-NEXT: (local.get $1) ;; CHECK-TEXT-NEXT: (local.get $2) ;; CHECK-TEXT-NEXT: ) ;; CHECK-TEXT-NEXT: ) -;; CHECK-BIN: (func $i32x4.dot_i8x16_i7x16_add_s (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) -;; CHECK-BIN-NEXT: (i32x4.dot_i8x16_i7x16_add_s +;; CHECK-BIN: (func $i32x4.relaxed_dot_i8x16_i7x16_add_s (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) +;; CHECK-BIN-NEXT: (i32x4.relaxed_dot_i8x16_i7x16_add_s ;; CHECK-BIN-NEXT: (local.get $0) ;; CHECK-BIN-NEXT: (local.get $1) ;; CHECK-BIN-NEXT: (local.get $2) ;; CHECK-BIN-NEXT: ) ;; CHECK-BIN-NEXT: ) -(func $i32x4.dot_i8x16_i7x16_add_s (param $0 v128) (param $1 v128) (param $2 v128) (result v128) - (i32x4.dot_i8x16_i7x16_add_s +(func $i32x4.relaxed_dot_i8x16_i7x16_add_s (param $0 v128) (param $1 v128) (param $2 v128) (result v128) + (i32x4.relaxed_dot_i8x16_i7x16_add_s (local.get $0) (local.get $1) (local.get $2) @@ -495,7 +495,7 @@ ;; CHECK-BIN-NODEBUG-NEXT: ) ;; CHECK-BIN-NODEBUG: (func $9 (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) -;; CHECK-BIN-NODEBUG-NEXT: (i8x16.laneselect +;; CHECK-BIN-NODEBUG-NEXT: (i8x16.relaxed_laneselect ;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) ;; CHECK-BIN-NODEBUG-NEXT: (local.get $1) ;; CHECK-BIN-NODEBUG-NEXT: (local.get $2) @@ -503,7 +503,7 @@ ;; CHECK-BIN-NODEBUG-NEXT: ) ;; CHECK-BIN-NODEBUG: (func $10 (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) -;; CHECK-BIN-NODEBUG-NEXT: (i16x8.laneselect +;; CHECK-BIN-NODEBUG-NEXT: (i16x8.relaxed_laneselect ;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) ;; CHECK-BIN-NODEBUG-NEXT: (local.get $1) ;; CHECK-BIN-NODEBUG-NEXT: (local.get $2) @@ -511,7 +511,7 @@ ;; CHECK-BIN-NODEBUG-NEXT: ) ;; CHECK-BIN-NODEBUG: (func $11 (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) -;; CHECK-BIN-NODEBUG-NEXT: (i32x4.laneselect +;; CHECK-BIN-NODEBUG-NEXT: (i32x4.relaxed_laneselect ;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) ;; CHECK-BIN-NODEBUG-NEXT: (local.get $1) ;; CHECK-BIN-NODEBUG-NEXT: (local.get $2) @@ -519,7 +519,7 @@ ;; CHECK-BIN-NODEBUG-NEXT: ) ;; CHECK-BIN-NODEBUG: (func $12 (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) -;; CHECK-BIN-NODEBUG-NEXT: (i64x2.laneselect +;; CHECK-BIN-NODEBUG-NEXT: (i64x2.relaxed_laneselect ;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) ;; CHECK-BIN-NODEBUG-NEXT: (local.get $1) ;; CHECK-BIN-NODEBUG-NEXT: (local.get $2) @@ -562,14 +562,14 @@ ;; CHECK-BIN-NODEBUG-NEXT: ) ;; CHECK-BIN-NODEBUG: (func $18 (type $1) (param $0 v128) (param $1 v128) (result v128) -;; CHECK-BIN-NODEBUG-NEXT: (i16x8.dot_i8x16_i7x16_s +;; CHECK-BIN-NODEBUG-NEXT: (i16x8.relaxed_dot_i8x16_i7x16_s ;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) ;; CHECK-BIN-NODEBUG-NEXT: (local.get $1) ;; CHECK-BIN-NODEBUG-NEXT: ) ;; CHECK-BIN-NODEBUG-NEXT: ) ;; CHECK-BIN-NODEBUG: (func $19 (type $0) (param $0 v128) (param $1 v128) (param $2 v128) (result v128) -;; CHECK-BIN-NODEBUG-NEXT: (i32x4.dot_i8x16_i7x16_add_s +;; CHECK-BIN-NODEBUG-NEXT: (i32x4.relaxed_dot_i8x16_i7x16_add_s ;; CHECK-BIN-NODEBUG-NEXT: (local.get $0) ;; CHECK-BIN-NODEBUG-NEXT: (local.get $1) ;; CHECK-BIN-NODEBUG-NEXT: (local.get $2) diff --git a/test/lit/exec/relaxed.wast b/test/lit/exec/relaxed.wast index f78d11c125a..664a7141afc 100644 --- a/test/lit/exec/relaxed.wast +++ b/test/lit/exec/relaxed.wast @@ -5,15 +5,15 @@ (module (import "fuzzing-support" "log-i32" (func $log (param i32))) - ;; CHECK: [fuzz-exec] export i32x4.dot_i8x16_i7x16_add_s + ;; CHECK: [fuzz-exec] export i32x4.relaxed_dot_i8x16_i7x16_add_s ;; CHECK-NEXT: [LoggingExternalInterface logging 8] ;; CHECK-NEXT: [LoggingExternalInterface logging 14] ;; CHECK-NEXT: [LoggingExternalInterface logging 22] ;; CHECK-NEXT: [LoggingExternalInterface logging 32] - (func $i32x4.dot_i8x16_i7x16_add_s (export "i32x4.dot_i8x16_i7x16_add_s") + (func $i32x4.relaxed_dot_i8x16_i7x16_add_s (export "i32x4.relaxed_dot_i8x16_i7x16_add_s") (local $v v128) (local.set $v - (i32x4.dot_i8x16_i7x16_add_s + (i32x4.relaxed_dot_i8x16_i7x16_add_s (v128.const i32x4 0 1 2 3) (v128.const i32x4 4 5 6 7) (v128.const i32x4 8 9 10 11) diff --git a/test/lit/passes/remove-relaxed-simd.wast b/test/lit/passes/remove-relaxed-simd.wast index 045a4a190a9..93f68abcda4 100644 --- a/test/lit/passes/remove-relaxed-simd.wast +++ b/test/lit/passes/remove-relaxed-simd.wast @@ -91,7 +91,7 @@ (drop (f64x2.relaxed_min (local.get 0) (local.get 1))) (drop (f64x2.relaxed_max (local.get 0) (local.get 1))) (drop (i16x8.relaxed_q15mulr_s (local.get 0) (local.get 1))) - (drop (i16x8.dot_i8x16_i7x16_s (local.get 0) (local.get 1))) + (drop (i16x8.relaxed_dot_i8x16_i7x16_s (local.get 0) (local.get 1))) ;; Normal SIMD instruction (drop (v128.xor (local.get 0) (local.get 1))) ) @@ -131,7 +131,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $ternary (param v128 v128 v128) - (drop (i32x4.dot_i8x16_i7x16_add_s (local.get 0) (local.get 1) (local.get 2))) + (drop (i32x4.relaxed_dot_i8x16_i7x16_add_s (local.get 0) (local.get 1) (local.get 2))) (drop (f32x4.relaxed_madd (local.get 0) (local.get 1) (local.get 2))) (drop (f32x4.relaxed_nmadd (local.get 0) (local.get 1) (local.get 2))) (drop (f64x2.relaxed_madd (local.get 0) (local.get 1) (local.get 2))) diff --git a/test/spec/dot_product.wast b/test/spec/dot_product.wast index ff512bb855b..43330e9853a 100644 --- a/test/spec/dot_product.wast +++ b/test/spec/dot_product.wast @@ -6,27 +6,27 @@ ;; test). (module - (func (export "i16x8.dot_i8x16_i7x16_s") (param v128 v128) (result v128) (i16x8.dot_i8x16_i7x16_s (local.get 0) (local.get 1))) - (func (export "i32x4.dot_i8x16_i7x16_add_s") (param v128 v128 v128) (result v128) (i32x4.dot_i8x16_i7x16_add_s (local.get 0) (local.get 1) (local.get 2))) + (func (export "i16x8.relaxed_dot_i8x16_i7x16_s") (param v128 v128) (result v128) (i16x8.relaxed_dot_i8x16_i7x16_s (local.get 0) (local.get 1))) + (func (export "i32x4.relaxed_dot_i8x16_i7x16_add_s") (param v128 v128 v128) (result v128) (i32x4.relaxed_dot_i8x16_i7x16_add_s (local.get 0) (local.get 1) (local.get 2))) - (func (export "i16x8.dot_i8x16_i7x16_s_cmp") (param v128 v128) (result v128) + (func (export "i16x8.relaxed_dot_i8x16_i7x16_s_cmp") (param v128 v128) (result v128) (i16x8.eq - (i16x8.dot_i8x16_i7x16_s (local.get 0) (local.get 1)) - (i16x8.dot_i8x16_i7x16_s (local.get 0) (local.get 1)))) - (func (export "i32x4.dot_i8x16_i7x16_add_s_cmp") (param v128 v128 v128) (result v128) + (i16x8.relaxed_dot_i8x16_i7x16_s (local.get 0) (local.get 1)) + (i16x8.relaxed_dot_i8x16_i7x16_s (local.get 0) (local.get 1)))) + (func (export "i32x4.relaxed_dot_i8x16_i7x16_add_s_cmp") (param v128 v128 v128) (result v128) (i16x8.eq - (i32x4.dot_i8x16_i7x16_add_s (local.get 0) (local.get 1) (local.get 2)) - (i32x4.dot_i8x16_i7x16_add_s (local.get 0) (local.get 1) (local.get 2)))) + (i32x4.relaxed_dot_i8x16_i7x16_add_s (local.get 0) (local.get 1) (local.get 2)) + (i32x4.relaxed_dot_i8x16_i7x16_add_s (local.get 0) (local.get 1) (local.get 2)))) ) ;; Simple values to ensure things are functional. -(assert_return (invoke "i16x8.dot_i8x16_i7x16_s" +(assert_return (invoke "i16x8.relaxed_dot_i8x16_i7x16_s" (v128.const i8x16 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15) (v128.const i8x16 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15)) (v128.const i16x8 1 13 41 85 145 221 313 421)) ;; Test max and min i8 values; -(assert_return (invoke "i16x8.dot_i8x16_i7x16_s" +(assert_return (invoke "i16x8.relaxed_dot_i8x16_i7x16_s" (v128.const i8x16 -128 -128 127 127 0 0 0 0 0 0 0 0 0 0 0 0) (v128.const i8x16 127 127 127 127 0 0 0 0 0 0 0 0 0 0 0 0)) (v128.const i16x8 -32512 32258 0 0 0 0 0 0)) @@ -34,13 +34,13 @@ ;; signed * unsigned : -128 * 129 * 2 = -33,024 saturated to -32,768 ;; signed * signed : -128 * -127 * 2 = 32,512 ;; unsigned * unsigned : 128 * 129 * 2 = 33,024 -(assert_return (invoke "i16x8.dot_i8x16_i7x16_s" +(assert_return (invoke "i16x8.relaxed_dot_i8x16_i7x16_s" (v128.const i8x16 -128 -128 0 0 0 0 0 0 0 0 0 0 0 0 0 0) (v128.const i8x16 -127 -127 0 0 0 0 0 0 0 0 0 0 0 0 0 0)) (v128.const i16x8 32512 0 0 0 0 0 0 0)) ;; Simple values to ensure things are functional. -(assert_return (invoke "i32x4.dot_i8x16_i7x16_add_s" +(assert_return (invoke "i32x4.relaxed_dot_i8x16_i7x16_add_s" (v128.const i8x16 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15) (v128.const i8x16 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15) (v128.const i32x4 0 1 2 3)) @@ -48,7 +48,7 @@ (v128.const i32x4 14 127 368 737)) ;; Test max and min i8 values; -(assert_return (invoke "i32x4.dot_i8x16_i7x16_add_s" +(assert_return (invoke "i32x4.relaxed_dot_i8x16_i7x16_add_s" (v128.const i8x16 -128 -128 -128 -128 127 127 127 127 0 0 0 0 0 0 0 0) (v128.const i8x16 127 127 127 127 127 127 127 127 0 0 0 0 0 0 0 0) (v128.const i32x4 1 2 3 4)) @@ -61,7 +61,7 @@ ;; -32768 + -32768 = -65536 (+ 1) ;; signed * signed : -128 * -127 * 4 = 65,024 (+ 1) ;; unsigned * unsigned : 128 * 129 * 2 = 66,048 (+ 1) -(assert_return (invoke "i32x4.dot_i8x16_i7x16_add_s" +(assert_return (invoke "i32x4.relaxed_dot_i8x16_i7x16_add_s" (v128.const i8x16 -128 -128 -128 -128 0 0 0 0 0 0 0 0 0 0 0 0) (v128.const i8x16 -127 -127 -127 -127 0 0 0 0 0 0 0 0 0 0 0 0) (v128.const i32x4 1 2 3 4)) @@ -70,13 +70,13 @@ ;; Check that multiple calls to the relaxed instruction with same inputs returns same results. ;; Test max and min i8 values; -(assert_return (invoke "i16x8.dot_i8x16_i7x16_s_cmp" +(assert_return (invoke "i16x8.relaxed_dot_i8x16_i7x16_s_cmp" (v128.const i8x16 -128 -128 127 127 0 0 0 0 0 0 0 0 0 0 0 0) (v128.const i8x16 127 127 127 127 0 0 0 0 0 0 0 0 0 0 0 0)) (v128.const i16x8 -1 -1 -1 -1 -1 -1 -1 -1)) ;; Test max and min i8 values; -(assert_return (invoke "i32x4.dot_i8x16_i7x16_add_s_cmp" +(assert_return (invoke "i32x4.relaxed_dot_i8x16_i7x16_add_s_cmp" (v128.const i8x16 -128 -128 -128 -128 127 127 127 127 0 0 0 0 0 0 0 0) (v128.const i8x16 127 127 127 127 127 127 127 127 0 0 0 0 0 0 0 0) (v128.const i32x4 1 2 3 4)) @@ -86,7 +86,7 @@ ;; signed * unsigned : -128 * 129 * 2 = -33,024 saturated to -32,768 ;; signed * signed : -128 * -127 * 2 = 32,512 ;; unsigned * unsigned : 128 * 129 * 2 = 33,024 -(assert_return (invoke "i16x8.dot_i8x16_i7x16_s_cmp" +(assert_return (invoke "i16x8.relaxed_dot_i8x16_i7x16_s_cmp" (v128.const i8x16 -128 -128 0 0 0 0 0 0 0 0 0 0 0 0 0 0) (v128.const i8x16 -127 -127 0 0 0 0 0 0 0 0 0 0 0 0 0 0)) (v128.const i16x8 -1 -1 -1 -1 -1 -1 -1 -1)) @@ -97,7 +97,7 @@ ;; -32768 + -32768 = -65536 (+ 1) ;; signed * signed : -128 * -127 * 4 = 65,024 (+ 1) ;; unsigned * unsigned : 128 * 129 * 2 = 66,048 (+ 1) -(assert_return (invoke "i32x4.dot_i8x16_i7x16_add_s_cmp" +(assert_return (invoke "i32x4.relaxed_dot_i8x16_i7x16_add_s_cmp" (v128.const i8x16 -128 -128 -128 -128 0 0 0 0 0 0 0 0 0 0 0 0) (v128.const i8x16 -127 -127 -127 -127 0 0 0 0 0 0 0 0 0 0 0 0) (v128.const i32x4 1 2 3 4)) From d2415b6045c470a8cad449167a3a4fdc21bdfb90 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 6 May 2026 10:12:20 -0700 Subject: [PATCH 078/168] [NFC] Use pascal-style string storage for IString/Name (#8662) Previously our interned strings were std::string_view, which means a pointer and a size. Instead, store the size as a header alongside the string data. As we have many views on the same data, this reduces memory usage, basically anywhere we use a Name, which is every Call, GlobalSet, Load, etc. I see a 5% RAM reduction. Changes to instructions, branches, and wall time are very very small. This removes the "reuse" optimization where we could reuse a string from the input. That was error-prone and a bad idea anyhow. In practice, it might have helped a little, but this new model is simpler and saves a lot more. (We can't reuse now since we need to convert to pascal-style storage anyhow.) --- src/ir/names.h | 2 +- src/ir/possible-contents.cpp | 3 +- src/parser/contexts.h | 2 +- src/passes/Asyncify.cpp | 2 +- src/passes/J2CLOpts.cpp | 2 +- src/passes/MinifyImportsAndExports.cpp | 10 +-- src/passes/Print.cpp | 8 +-- src/passes/StringLifting.cpp | 8 +-- src/passes/StringLowering.cpp | 6 +- src/support/istring.cpp | 80 +++++++++++---------- src/support/istring.h | 79 +++++++++++++++------ src/support/json.cpp | 2 +- src/support/name.cpp | 1 + src/support/name.h | 11 ++- src/tools/execution-results.h | 2 +- src/tools/wasm-metadce.cpp | 2 +- src/tools/wasm2c-wrapper.h | 2 +- src/wasm-interpreter.h | 6 +- src/wasm/wasm-binary.cpp | 50 ++++++------- src/wasm2js.h | 11 ++- test/gtest/CMakeLists.txt | 1 + test/gtest/istring.cpp | 97 ++++++++++++++++++++++++++ 22 files changed, 266 insertions(+), 121 deletions(-) create mode 100644 test/gtest/istring.cpp diff --git a/src/ir/names.h b/src/ir/names.h index 61fc137f5a8..083a54f0ef5 100644 --- a/src/ir/names.h +++ b/src/ir/names.h @@ -56,7 +56,7 @@ inline Name getValidName(Name root, if (check(root)) { return root; } - auto prefixed = std::string(root.str) + separator; + auto prefixed = std::string(root.view()) + separator; Index num = hint; while (1) { auto name = prefixed + std::to_string(num); diff --git a/src/ir/possible-contents.cpp b/src/ir/possible-contents.cpp index 3e1213cd05b..4b76f467825 100644 --- a/src/ir/possible-contents.cpp +++ b/src/ir/possible-contents.cpp @@ -1211,8 +1211,7 @@ struct InfoCollector addRoot(curr, PossibleContents::exactType(curr->type)); } void visitStringConst(StringConst* curr) { - addRoot(curr, - PossibleContents::literal(Literal(std::string(curr->string.str)))); + addRoot(curr, PossibleContents::literal(Literal(curr->string.view()))); } void visitStringMeasure(StringMeasure* curr) { // TODO: optimize when possible diff --git a/src/parser/contexts.h b/src/parser/contexts.h index eac35a543c5..06a515e32d1 100644 --- a/src/parser/contexts.h +++ b/src/parser/contexts.h @@ -1996,7 +1996,7 @@ struct ParseDefsCtx : TypeParserCtx, AnnotationParserCtx { void setSrcLoc(const std::vector& annotations) { const Annotation* annotation = nullptr; for (auto& a : annotations) { - if (a.kind.str == std::string_view("src")) { + if (a.kind.view() == std::string_view("src")) { annotation = &a; } } diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp index a8410a24c05..feea9adf7c2 100644 --- a/src/passes/Asyncify.cpp +++ b/src/passes/Asyncify.cpp @@ -1730,7 +1730,7 @@ struct AsyncifyLocals : public WalkerPass> { } // anonymous namespace static std::string getFullImportName(Name module, Name base) { - return std::string(module.str) + '.' + base.toString(); + return module.toString() + '.' + base.toString(); } struct Asyncify : public Pass { diff --git a/src/passes/J2CLOpts.cpp b/src/passes/J2CLOpts.cpp index 759ef7cc287..0474a31ef74 100644 --- a/src/passes/J2CLOpts.cpp +++ b/src/passes/J2CLOpts.cpp @@ -197,7 +197,7 @@ class ConstantHoister : public WalkerPass> { } Name getEnclosingClass(Name name) { - return Name(name.str.substr(name.str.find_last_of('@'))); + return Name(name.view().substr(name.view().find_last_of('@'))); } AssignmentCountMap& assignmentCounts; diff --git a/src/passes/MinifyImportsAndExports.cpp b/src/passes/MinifyImportsAndExports.cpp index 882f9a8b5d9..8c43ee694c3 100644 --- a/src/passes/MinifyImportsAndExports.cpp +++ b/src/passes/MinifyImportsAndExports.cpp @@ -112,9 +112,9 @@ struct MinifyImportsAndExports : public Pass { std::cout << ','; } std::cout << "\n ["; - String::printEscaped(std::cout, key.first.str) << ", "; - String::printEscaped(std::cout, key.second.str) << ", "; - String::printEscaped(std::cout, new_.str) << "]"; + String::printEscaped(std::cout, key.first.view()) << ", "; + String::printEscaped(std::cout, key.second.view()) << ", "; + String::printEscaped(std::cout, new_.view()) << "]"; } } std::cout << "\n ],\n\"exports\": ["; @@ -127,8 +127,8 @@ struct MinifyImportsAndExports : public Pass { std::cout << ','; } std::cout << "\n ["; - String::printEscaped(std::cout, key.second.str) << ", "; - String::printEscaped(std::cout, new_.str) << "]"; + String::printEscaped(std::cout, key.second.view()) << ", "; + String::printEscaped(std::cout, new_.view()) << "]"; } } std::cout << "\n ]\n"; diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 02e555a8c4b..d560ca49547 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -2566,7 +2566,7 @@ struct PrintExpressionContents // Re-encode from WTF-16 to WTF-8. std::stringstream wtf8; [[maybe_unused]] bool valid = - String::convertWTF16ToWTF8(wtf8, curr->string.str); + String::convertWTF16ToWTF8(wtf8, curr->string.view()); assert(valid); // TODO: Use wtf8.view() once we have C++20. String::printEscaped(o, wtf8.str()); @@ -3190,7 +3190,7 @@ void PrintSExpression::visitExport(Export* curr) { o << '('; printMedium(o, "export "); std::stringstream escaped; - String::printEscaped(escaped, curr->name.str); + String::printEscaped(escaped, curr->name.view()); printText(o, escaped.str(), false) << " ("; switch (curr->kind) { case ExternalKind::Function: @@ -3219,8 +3219,8 @@ void PrintSExpression::visitExport(Export* curr) { void PrintSExpression::emitImportHeader(Importable* curr) { printMedium(o, "import "); std::stringstream escapedModule, escapedBase; - String::printEscaped(escapedModule, curr->module.str); - String::printEscaped(escapedBase, curr->base.str); + String::printEscaped(escapedModule, curr->module.view()); + String::printEscaped(escapedBase, curr->base.view()); printText(o, escapedModule.str(), false) << ' '; printText(o, escapedBase.str(), false) << ' '; } diff --git a/src/passes/StringLifting.cpp b/src/passes/StringLifting.cpp index cd3a8ffabb6..ac15d2a8614 100644 --- a/src/passes/StringLifting.cpp +++ b/src/passes/StringLifting.cpp @@ -70,11 +70,11 @@ struct StringLifting : public Pass { // Encode from WTF-8 to WTF-16. auto wtf8 = global->base; std::stringstream wtf16; - bool valid = String::convertWTF8ToWTF16(wtf16, wtf8.str); + bool valid = String::convertWTF8ToWTF16(wtf16, wtf8.view()); if (!valid) { Fatal() << "Bad string to lift: " << wtf8; } - importedStrings[global->name] = wtf16.str(); + importedStrings[global->name] = wtf16.view(); found = true; } } @@ -101,7 +101,7 @@ struct StringLifting : public Pass { continue; } // The index in the array is the basename. - Index index = std::stoi(std::string(global->base.str)); + Index index = std::stoi(std::string(global->base.view())); if (index >= array.size()) { Fatal() << "StringLifting: bad index in string.const section"; } @@ -222,7 +222,7 @@ struct StringLifting : public Pass { auto iter = parent.importedStrings.find(curr->name); if (iter != parent.importedStrings.end()) { auto wtf16 = iter->second; - replaceCurrent(Builder(*getModule()).makeStringConst(wtf16.str)); + replaceCurrent(Builder(*getModule()).makeStringConst(wtf16.view())); modified = true; } } diff --git a/src/passes/StringLowering.cpp b/src/passes/StringLowering.cpp index b4641bda134..bd753efaf91 100644 --- a/src/passes/StringLowering.cpp +++ b/src/passes/StringLowering.cpp @@ -153,7 +153,7 @@ struct StringGathering : public Pass { // Re-encode from WTF-16 to WTF-8 to make the name easier to read. std::stringstream wtf8; [[maybe_unused]] bool valid = - String::convertWTF16ToWTF8(wtf8, string.str); + String::convertWTF16ToWTF8(wtf8, string.view()); assert(valid); // Then escape it because identifiers must be valid UTF-8. // TODO: Use wtf8.view() and escaped.view() once we have C++20. @@ -246,7 +246,7 @@ struct StringLowering : public StringGathering { if (auto* c = global->init->dynCast()) { std::stringstream utf8; if (useMagicImports && - String::convertUTF16ToUTF8(utf8, c->string.str)) { + String::convertUTF16ToUTF8(utf8, c->string.view())) { global->module = stringConstsModule; global->base = Name(utf8.str()); } else { @@ -263,7 +263,7 @@ struct StringLowering : public StringGathering { } else { json << ','; } - String::printEscapedJSON(json, c->string.str); + String::printEscapedJSON(json, c->string.view()); jsonImportIndex++; } global->init = nullptr; diff --git a/src/support/istring.cpp b/src/support/istring.cpp index 60c8b59be6d..d254dcc4cff 100644 --- a/src/support/istring.cpp +++ b/src/support/istring.cpp @@ -14,32 +14,39 @@ * limitations under the License. */ +#include +#include + #include "istring.h" #include "mixed_arena.h" namespace wasm { -std::string_view IString::interned(std::string_view s, bool reuse) { - // We need a set of string_views that can be modified in-place to minimize - // the number of lookups we do. Since set elements cannot normally be - // modified, wrap the string_views in a container that provides mutability - // even through a const reference. - struct MutStringView { - mutable std::string_view str; - MutStringView(std::string_view str) : str(str) {} - }; - struct MutStringViewHash { - size_t operator()(const MutStringView& mut) const { - return std::hash{}(mut.str); +const char* IString::interned(std::string_view s) { + // A set of interned Views, i.e., that contains our pascal-style strings. We + // need to query this using a std::string_view, as that is what we receive as + // input (turning it into pascal-style storage would add overhead). To do so, + // use overloading in the hash and equality functions (which works thanks to + // `is_transparent`). + struct InternedHash { + using is_transparent = void; + size_t operator()(View v) const { + return std::hash{}(v.view()); + } + size_t operator()(std::string_view sv) const { + return std::hash{}(sv); } }; - struct MutStringViewEqual { - bool operator()(const MutStringView& a, const MutStringView& b) const { - return a.str == b.str; + struct InternedEqual { + using is_transparent = void; + bool operator()(View a, View b) const { return a.view() == b.view(); } + bool operator()(std::string_view a, View b) const { return a == b.view(); } + bool operator()(View a, std::string_view b) const { return a.view() == b; } + bool operator()(std::string_view a, std::string_view b) const { + return a == b; } }; - using StringSet = - std::unordered_set; + using StringSet = std::unordered_set; // The authoritative global set of interned string views. static StringSet globalStrings; @@ -54,34 +61,37 @@ std::string_view IString::interned(std::string_view s, bool reuse) { // A thread-local cache of strings to reduce contention. thread_local static StringSet localStrings; - auto [localIt, localInserted] = localStrings.insert(s); - if (!localInserted) { + if (auto it = localStrings.find(s); it != localStrings.end()) { // We already had a local copy of this string. - return localIt->str; + return it->internal; } // No copy yet in the local cache. Check the global cache. std::unique_lock lock(mutex); - auto [globalIt, globalInserted] = globalStrings.insert(s); - if (!globalInserted) { + if (auto it = globalStrings.find(s); it != globalStrings.end()) { // We already had a global copy of this string. Cache it locally. - localIt->str = globalIt->str; - return localIt->str; + localStrings.insert(*it); + return it->internal; } - if (!reuse) { - // We have a new string, but it doesn't have a stable address. Create a copy - // of the data at a stable address we can use. Make sure it is null - // terminated so legacy uses that get a C string still work. - char* data = (char*)arena.allocSpace(s.size() + 1, 1); - std::copy(s.begin(), s.end(), data); - data[s.size()] = '\0'; - s = std::string_view(data, s.size()); - } + // We have a new string. Create a copy of the data at a stable address with a + // header we can use. Make sure it is null terminated so legacy uses that get + // a C string still work. + size_t size = s.size(); + // The string's size must fit in 32 bits. + assert(size <= std::numeric_limits::max()); + char* buffer = + (char*)arena.allocSpace(sizeof(uint32_t) + size + 1, alignof(uint32_t)); + *(uint32_t*)(buffer) = size; + char* data = buffer + sizeof(uint32_t); + std::copy(s.begin(), s.end(), data); + data[size] = '\0'; // Intern our new string. - localIt->str = globalIt->str = s; - return s; + View v{data}; + globalStrings.insert(v); + localStrings.insert(v); + return data; } } // namespace wasm diff --git a/src/support/istring.h b/src/support/istring.h index dad20b8fb00..a567eb82f12 100644 --- a/src/support/istring.h +++ b/src/support/istring.h @@ -33,21 +33,49 @@ namespace wasm { struct IString { private: - static std::string_view interned(std::string_view s, bool reuse = true); + static const char* interned(std::string_view s); public: - const std::string_view str; + // Strings are stored in Pascal style: a size followed by the characters. We + // keep the internal pointer pointing to the data, so that data() is a no-op; + // computing the size, which is more rare, requires looking back and doing a + // load. + // + // The size is limited to 4 bytes, so the maximum string we support is 4GB. + // + // The alternative approach of using a string_view here, i.e., keeping the + // pointer and size in the IString, uses more more memory. That is, this + // optimization saves a lot of space, because while it adds 4 bytes to each + // interned string itself, we tend to have many views on each. + // + // We provide a View here, which is a simple interface. Users that need more + // convert to a std::string_view with .view() or a cast. + struct View { + const char* internal = nullptr; + const char* data() const { return internal; } + size_t size() const { + return internal ? *(const uint32_t*)(internal - 4) : 0; + } + char operator[](size_t x) const { return internal[x]; } + std::string_view view() const { + if (!internal) { + // No size to read. + return {}; + } + return {internal, size()}; + } + }; + const View str; + + std::string_view view() const { return str.view(); } IString() = default; - // TODO: This is a wildly unsafe default inherited from the previous - // implementation. Change it? - IString(std::string_view str, bool reuse = true) - : str(interned(str, reuse)) {} + IString(View v) : str(v) {} - // But other C strings generally do need to be copied. - IString(const char* str) : str(interned(str, false)) {} - IString(const std::string& str) : str(interned(str, false)) {} + IString(std::string_view s) : str{interned(s)} {} + IString(const char* str) : str{interned(str)} {} + IString(const std::string& str) : str{interned(str)} {} IString(const IString& other) = default; @@ -57,17 +85,24 @@ struct IString { bool operator==(const IString& other) const { // Fast! No need to compare contents due to interning - return str.data() == other.str.data(); + return str.internal == other.str.internal; } bool operator!=(const IString& other) const { return !(*this == other); } - bool operator<(const IString& other) const { return str < other.str; } - bool operator<=(const IString& other) const { return str <= other.str; } - bool operator>(const IString& other) const { return str > other.str; } - bool operator>=(const IString& other) const { return str >= other.str; } + bool operator<(const IString& other) const { + if (str.internal == other.str.internal) { + return false; + } + return view() < other.view(); + } + bool operator<=(const IString& other) const { + return *this == other || *this < other; + } + bool operator>(const IString& other) const { return !(*this <= other); } + bool operator>=(const IString& other) const { return !(*this < other); } char operator[](int x) const { return str[x]; } - explicit operator bool() const { return str.data() != nullptr; } + explicit operator bool() const { return str.internal != nullptr; } // TODO: deprecate? bool is() const { return bool(*this); } @@ -75,13 +110,13 @@ struct IString { std::string toString() const { return {str.data(), str.size()}; } - bool equals(std::string_view other) const { return str == other; } + bool equals(std::string_view other) const { return str.view() == other; } bool startsWith(std::string_view prefix) const { // TODO: Use C++20 `starts_with`. - return str.substr(0, prefix.size()) == prefix; + return view().substr(0, prefix.size()) == prefix; } - bool startsWith(IString str) const { return startsWith(str.str); } + bool startsWith(IString other) const { return startsWith(other.view()); } // Disambiguate for string literals. template bool startsWith(const char (&str)[N]) const { @@ -93,9 +128,9 @@ struct IString { if (suffix.size() > str.size()) { return false; } - return str.substr(str.size() - suffix.size()) == suffix; + return view().substr(str.size() - suffix.size()) == suffix; } - bool endsWith(IString str) const { return endsWith(str.str); } + bool endsWith(IString other) const { return endsWith(other.view()); } // Disambiguate for string literals. template bool endsWith(const char (&str)[N]) const { @@ -103,7 +138,7 @@ struct IString { } IString substr(size_t pos, size_t len = std::string_view::npos) const { - return IString(str.substr(pos, len)); + return IString(view().substr(pos, len)); } size_t size() const { return str.size(); } @@ -120,7 +155,7 @@ template<> struct hash { }; inline std::ostream& operator<<(std::ostream& os, const wasm::IString& str) { - return os << str.str; + return os << str.view(); } } // namespace std diff --git a/src/support/json.cpp b/src/support/json.cpp index dd94719d47d..ff393317410 100644 --- a/src/support/json.cpp +++ b/src/support/json.cpp @@ -23,7 +23,7 @@ void Value::stringify(std::ostream& os, bool pretty) { if (isString()) { std::stringstream wtf16; [[maybe_unused]] bool valid = - wasm::String::convertWTF8ToWTF16(wtf16, getIString().str); + wasm::String::convertWTF8ToWTF16(wtf16, getIString().view()); assert(valid); // TODO: Use wtf16.view() once we have C++20. wasm::String::printEscapedJSON(os, wtf16.str()); diff --git a/src/support/name.cpp b/src/support/name.cpp index 4c599defca0..34b7546fba9 100644 --- a/src/support/name.cpp +++ b/src/support/name.cpp @@ -46,6 +46,7 @@ std::ostream& Name::print(std::ostream& o) const { // TODO: This is not spec-compliant since the spec does not yet support // quoted identifiers and has a limited set of valid idchars. o << '$'; + auto str = view(); if (size() >= 1 && std::all_of(str.begin(), str.end(), isIDChar)) { return o << str; } else { diff --git a/src/support/name.h b/src/support/name.h index 8e3f7a291d5..d6c71f732c9 100644 --- a/src/support/name.h +++ b/src/support/name.h @@ -33,9 +33,10 @@ namespace wasm { struct Name : public IString { Name() : IString() {} - Name(std::string_view str) : IString(str, false) {} - Name(const char* str) : IString(str, false) {} + Name(std::string_view str) : IString(str) {} + Name(const char* str) : IString(str) {} Name(IString str) : IString(str) {} + Name(IString::View str) : IString(str) {} Name(const std::string& str) : IString(str) {} // String literals do not need to be copied. Note: Not safe to construct from @@ -50,13 +51,11 @@ struct Name : public IString { } } - static Name fromInt(size_t i) { - return IString(std::to_string(i).c_str(), false); - } + static Name fromInt(size_t i) { return IString(std::to_string(i).c_str()); } bool hasSubstring(IString substring) { // TODO: Use C++23 `contains`. - return str.find(substring.str) != std::string_view::npos; + return view().find(substring.view()) != std::string_view::npos; } std::ostream& print(std::ostream& o) const; diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index 7717ec23811..09f7657ee96 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -412,7 +412,7 @@ class FuzzerImportResolver // fuzz_shell.js. Index payload = 0; for (auto name : {name.module, name.name}) { - for (auto c : name.str) { + for (auto c : name.view()) { payload = (payload + static_cast(c)) % 251; } } diff --git a/src/tools/wasm-metadce.cpp b/src/tools/wasm-metadce.cpp index 96b2063df4b..29bba15aaf8 100644 --- a/src/tools/wasm-metadce.cpp +++ b/src/tools/wasm-metadce.cpp @@ -78,7 +78,7 @@ struct MetaDCEGraph { // to be kept alive. module = ENV; } - return std::string(module.str) + " (*) " + std::string(base.str); + return std::string(module.view()) + " (*) " + std::string(base.view()); } ImportId getImportId(ModuleItemKind kind, Name name) { diff --git a/src/tools/wasm2c-wrapper.h b/src/tools/wasm2c-wrapper.h index 242442a1d8a..e39bf54a3d2 100644 --- a/src/tools/wasm2c-wrapper.h +++ b/src/tools/wasm2c-wrapper.h @@ -30,7 +30,7 @@ namespace wasm { inline std::string wasm2cMangle(Name name, Signature sig) { const char escapePrefix = 'Z'; std::string mangled = "Z_"; - for (unsigned char c : name.str) { + for (unsigned char c : name.view()) { if ((isalnum(c) && c != escapePrefix) || c == '_') { // This character is ok to emit as it is. mangled += c; diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index c018ca4484e..447c22497d3 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -121,7 +121,7 @@ class Flow { } friend std::ostream& operator<<(std::ostream& o, const Flow& flow) { - o << "(flow " << (flow.breakTo.is() ? flow.breakTo.str : "-") << " : {"; + o << "(flow " << (flow.breakTo.is() ? flow.breakTo.view() : "-") << " : {"; for (size_t i = 0; i < flow.values.size(); ++i) { if (i > 0) { o << ", "; @@ -2727,7 +2727,9 @@ class ExpressionRunner : public OverriddenVisitor { return Flow(NONCONSTANT_FLOW); } } - Flow visitStringConst(StringConst* curr) { return Literal(curr->string.str); } + Flow visitStringConst(StringConst* curr) { + return Literal(curr->string.view()); + } Flow visitStringMeasure(StringMeasure* curr) { // For now we only support JS-style strings. diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 2ee7975da1d..2adb6ba83d4 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -207,7 +207,7 @@ void WasmBinaryWriter::writeStart() { return; } auto start = startSection(BinaryConsts::Section::Start); - o << U32LEB(getFunctionIndex(wasm->start.str)); + o << U32LEB(getFunctionIndex(wasm->start.view())); finishSection(start); } @@ -339,8 +339,8 @@ void WasmBinaryWriter::writeImports() { auto start = startSection(BinaryConsts::Section::Import); o << U32LEB(num); auto writeImportHeader = [&](Importable* import) { - writeInlineString(import->module.str); - writeInlineString(import->base.str); + writeInlineString(import->module.view()); + writeInlineString(import->base.view()); }; ModuleUtils::iterImportedFunctions(*wasm, [&](Function* func) { writeImportHeader(func); @@ -581,7 +581,8 @@ void WasmBinaryWriter::writeStrings() { for (auto& string : sorted) { // Re-encode from WTF-16 to WTF-8. std::stringstream wtf8; - [[maybe_unused]] bool valid = String::convertWTF16ToWTF8(wtf8, string.str); + [[maybe_unused]] bool valid = + String::convertWTF16ToWTF8(wtf8, string.view()); assert(valid); // TODO: Use wtf8.view() once we have C++20. writeInlineString(wtf8.str()); @@ -640,7 +641,7 @@ void WasmBinaryWriter::writeExports() { auto start = startSection(BinaryConsts::Section::Export); o << U32LEB(wasm->exports.size()); for (auto& curr : wasm->exports) { - writeInlineString(curr->name.str); + writeInlineString(curr->name.view()); o << U32LEB(int32_t(curr->kind)); switch (curr->kind) { case ExternalKind::Function: @@ -917,7 +918,7 @@ void WasmBinaryWriter::writeNames() { if (emitModuleName && wasm->name.is()) { auto substart = startSubsection(BinaryConsts::CustomSections::Subsection::NameModule); - writeEscapedName(wasm->name.str); + writeEscapedName(wasm->name.view()); finishSubsection(substart); } @@ -946,7 +947,7 @@ void WasmBinaryWriter::writeNames() { o << U32LEB(functionsWithNames.size()); for (auto& [index, global] : functionsWithNames) { o << U32LEB(index); - writeEscapedName(global->name.str); + writeEscapedName(global->name.view()); } finishSubsection(substart); } @@ -1006,7 +1007,7 @@ void WasmBinaryWriter::writeNames() { o << U32LEB(localsWithNames.size()); for (auto& [indexInBinary, name] : localsWithNames) { o << U32LEB(indexInBinary); - writeEscapedName(name.str); + writeEscapedName(name.view()); } emitted++; } @@ -1029,7 +1030,7 @@ void WasmBinaryWriter::writeNames() { o << U32LEB(namedTypes.size()); for (auto type : namedTypes) { o << U32LEB(indexedTypes.indices[type]); - writeEscapedName(wasm->typeNames[type].name.str); + writeEscapedName(wasm->typeNames[type].name.view()); } finishSubsection(substart); } @@ -1056,7 +1057,7 @@ void WasmBinaryWriter::writeNames() { for (auto& [index, table] : tablesWithNames) { o << U32LEB(index); - writeEscapedName(table->name.str); + writeEscapedName(table->name.view()); } finishSubsection(substart); @@ -1082,7 +1083,7 @@ void WasmBinaryWriter::writeNames() { o << U32LEB(memoriesWithNames.size()); for (auto& [index, memory] : memoriesWithNames) { o << U32LEB(index); - writeEscapedName(memory->name.str); + writeEscapedName(memory->name.view()); } finishSubsection(substart); } @@ -1107,7 +1108,7 @@ void WasmBinaryWriter::writeNames() { o << U32LEB(globalsWithNames.size()); for (auto& [index, global] : globalsWithNames) { o << U32LEB(index); - writeEscapedName(global->name.str); + writeEscapedName(global->name.view()); } finishSubsection(substart); } @@ -1132,7 +1133,7 @@ void WasmBinaryWriter::writeNames() { for (auto& [index, elem] : elemsWithNames) { o << U32LEB(index); - writeEscapedName(elem->name.str); + writeEscapedName(elem->name.view()); } finishSubsection(substart); @@ -1156,7 +1157,7 @@ void WasmBinaryWriter::writeNames() { auto& seg = wasm->dataSegments[i]; if (seg->hasExplicitName) { o << U32LEB(i); - writeEscapedName(seg->name.str); + writeEscapedName(seg->name.view()); } } finishSubsection(substart); @@ -1187,7 +1188,7 @@ void WasmBinaryWriter::writeNames() { o << U32LEB(fieldNames.size()); for (auto& [index, name] : fieldNames) { o << U32LEB(index); - writeEscapedName(name.str); + writeEscapedName(name.view()); } } finishSubsection(substart); @@ -1213,7 +1214,7 @@ void WasmBinaryWriter::writeNames() { o << U32LEB(tagsWithNames.size()); for (auto& [index, tag] : tagsWithNames) { o << U32LEB(index); - writeEscapedName(tag->name.str); + writeEscapedName(tag->name.view()); } finishSubsection(substart); } @@ -1232,7 +1233,8 @@ void WasmBinaryWriter::writeSourceMapUrl() { void WasmBinaryWriter::writeSymbolMap() { std::ofstream file(symbolMap); auto write = [&](Function* func) { - file << getFunctionIndex(func->name) << ":" << func->name.str << std::endl; + file << getFunctionIndex(func->name) << ":" << func->name.view() + << std::endl; }; ModuleUtils::iterImportedFunctions(*wasm, write); ModuleUtils::iterDefinedFunctions(*wasm, write); @@ -1523,7 +1525,7 @@ void WasmBinaryWriter::writeLegacyDylinkSection() { o << U32LEB(wasm->dylinkSection->tableAlignment); o << U32LEB(wasm->dylinkSection->neededDynlibs.size()); for (auto& neededDynlib : wasm->dylinkSection->neededDynlibs) { - writeInlineString(neededDynlib.str); + writeInlineString(neededDynlib.view()); } finishSection(start); } @@ -1554,7 +1556,7 @@ void WasmBinaryWriter::writeDylinkSection() { startSubsection(BinaryConsts::CustomSections::Subsection::DylinkNeeded); o << U32LEB(wasm->dylinkSection->neededDynlibs.size()); for (auto& neededDynlib : wasm->dylinkSection->neededDynlibs) { - writeInlineString(neededDynlib.str); + writeInlineString(neededDynlib.view()); } finishSubsection(substart); } @@ -1744,7 +1746,7 @@ std::optional WasmBinaryWriter::writeExpressionHints( // We found data: emit the section. buffer << uint8_t(BinaryConsts::Custom); auto lebPos = buffer.writeU32LEBPlaceholder(); - buffer.writeInlineString(sectionName.str); + buffer.writeInlineString(sectionName.view()); buffer << U32LEB(funcHintsVec.size()); for (auto& funcHints : funcHintsVec) { @@ -2279,7 +2281,7 @@ void WasmBinaryReader::readCustomSection(size_t payloadLen) { } wasm.customSections.resize(wasm.customSections.size() + 1); auto& section = wasm.customSections.back(); - section.name = sectionName.str; + section.name = sectionName.view(); auto data = getByteView(payloadLen); section.data = {data.begin(), data.end()}; } @@ -4926,7 +4928,7 @@ void WasmBinaryReader::readStrings() { auto string = getInlineString(false); // Re-encode from WTF-8 to WTF-16. std::stringstream wtf16; - if (!String::convertWTF8ToWTF16(wtf16, string.str)) { + if (!String::convertWTF8ToWTF16(wtf16, string.view())) { throwError("invalid string constant"); } // TODO: Use wtf16.view() once we have C++20. @@ -5208,7 +5210,7 @@ static char formatNibble(int nibble) { Name WasmBinaryReader::escape(Name name) { bool allIdChars = true; - for (char c : name.str) { + for (char c : name.view()) { if (!(allIdChars = isIdChar(c))) { break; } @@ -5218,7 +5220,7 @@ Name WasmBinaryReader::escape(Name name) { } // encode name, if at least one non-idchar (per WebAssembly spec) was found std::string escaped; - for (char c : name.str) { + for (char c : name.view()) { if (isIdChar(c)) { escaped.push_back(c); continue; diff --git a/src/wasm2js.h b/src/wasm2js.h index 08ac6e6e26e..5eb937b0f88 100644 --- a/src/wasm2js.h +++ b/src/wasm2js.h @@ -126,7 +126,7 @@ bool needsBufferView(Module& wasm) { return need; } -IString stringToIString(std::string str) { return IString(str.c_str(), false); } +IString stringToIString(std::string str) { return IString(str.c_str()); } // Used when taking a wasm name and generating a JS identifier. Each scope here // is used to ensure that all names have a unique name but the same wasm name @@ -215,8 +215,7 @@ class Wasm2JSBuilder { auto index = temps[type]++; ret = IString((std::string("wasm2js_") + type.toString() + "$" + std::to_string(index)) - .c_str(), - false); + .c_str()); ret = fromName(ret, NameScope::Local); } if (func->localIndices.find(ret) == func->localIndices.end()) { @@ -427,7 +426,7 @@ Ref Wasm2JSBuilder::processWasm(Module* wasm, Name funcName) { Output out(flags.symbolsFile, wasm::Flags::Text); Index i = 0; for (auto& func : wasm->functions) { - out.getStream() << i++ << ':' << func->name.str << '\n'; + out.getStream() << i++ << ':' << func->name.view() << '\n'; } } @@ -600,7 +599,7 @@ void Wasm2JSBuilder::addBasics(Ref ast, Module* wasm) { static bool needsQuoting(Name name) { auto mangled = asmangle(name.toString()); - return mangled != name.str; + return mangled != name.view(); } void Wasm2JSBuilder::ensureModuleVar(Ref ast, const Importable& imp) { @@ -1587,7 +1586,7 @@ Ref Wasm2JSBuilder::processExpression(Expression* curr, std::ostringstream out; out << lo << "," << hi; std::string os = out.str(); - IString name(os.c_str(), false); + IString name(os.c_str()); return ValueBuilder::makeName(name); } case Type::f32: { diff --git a/test/gtest/CMakeLists.txt b/test/gtest/CMakeLists.txt index 254f7a38c9f..05ce44006a6 100644 --- a/test/gtest/CMakeLists.txt +++ b/test/gtest/CMakeLists.txt @@ -19,6 +19,7 @@ set(unittest_SOURCES glbs.cpp interpreter.cpp intervals.cpp + istring.cpp json.cpp lattices.cpp local-graph.cpp diff --git a/test/gtest/istring.cpp b/test/gtest/istring.cpp new file mode 100644 index 00000000000..18b99464cb3 --- /dev/null +++ b/test/gtest/istring.cpp @@ -0,0 +1,97 @@ +// Copyright 2026 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. + +#include "support/istring.h" +#include "gtest/gtest.h" + +using namespace wasm; + +using IStringTest = ::testing::Test; + +TEST_F(IStringTest, Empty) { + // Null and empty strings differ. + auto null = IString(); + auto empty = IString(""); + EXPECT_NE(null, empty); + + EXPECT_FALSE(null.is()); + EXPECT_TRUE(empty.is()); + + // But they are equal to themselves. + EXPECT_EQ(null, null); + EXPECT_EQ(empty, empty); + + // A default string_view is the empty string, and has data == nullptr. + auto stdViewDefault1 = std::string_view(); + EXPECT_EQ(stdViewDefault1.data(), nullptr); + auto stdViewDefault2 = std::string_view{}; + EXPECT_EQ(stdViewDefault2.data(), nullptr); + + EXPECT_EQ(empty, stdViewDefault1); + EXPECT_EQ(empty, stdViewDefault2); + + // The same when going through the IString constructor. + EXPECT_EQ(empty, IString(std::string_view{})); + + // An empty string_view is equal to those, even though its data != nullptr. + auto stdViewEmpty = std::string_view(""); + EXPECT_NE(stdViewEmpty.data(), nullptr); + EXPECT_EQ(empty, stdViewEmpty); +} + +TEST_F(IStringTest, Interning) { + // The same string interned twice is equal. + auto foo1 = IString("foo"); + auto foo2 = IString("foo"); + EXPECT_EQ(foo1, foo2); + + // The internal pointers are equal too. + EXPECT_EQ(foo1.str.data(), foo2.str.data()); + + // Other things are different. + auto bar = IString("bar"); + EXPECT_NE(foo1, bar); + EXPECT_NE(foo2, bar); + + // Things are equal to themselves. + EXPECT_EQ(foo1, foo1); + EXPECT_EQ(foo2, foo2); + EXPECT_EQ(bar, bar); +} + +TEST_F(IStringTest, StartsWith) { + auto foo = IString("foo"); + EXPECT_TRUE(foo.startsWith("f")); + EXPECT_TRUE(foo.startsWith("fo")); + EXPECT_TRUE(foo.startsWith("foo")); + + EXPECT_FALSE(foo.startsWith("oo")); + EXPECT_FALSE(foo.startsWith("o")); + + EXPECT_FALSE(foo.startsWith("foobar")); + EXPECT_FALSE(foo.startsWith("bar")); +} + +TEST_F(IStringTest, EndsWith) { + auto foo = IString("foo"); + EXPECT_TRUE(foo.endsWith("o")); + EXPECT_TRUE(foo.endsWith("oo")); + EXPECT_TRUE(foo.endsWith("foo")); + + EXPECT_FALSE(foo.endsWith("f")); + EXPECT_FALSE(foo.endsWith("fo")); + + EXPECT_FALSE(foo.endsWith("foobar")); + EXPECT_FALSE(foo.endsWith("bar")); +} From d1ee405538bb150bd27de4556655ec2f55196851 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 6 May 2026 10:26:58 -0700 Subject: [PATCH 079/168] RemoveExports: Support comma separation and a response file (#8674) Helps #7976 --- src/passes/RemoveExports.cpp | 20 ++++++++++--- test/lit/passes/remove-exports-file.txt | 2 ++ test/lit/passes/remove-exports-file.wast | 37 +++++++++++++++++++++++ test/lit/passes/remove-exports-list.wast | 38 ++++++++++++++++++++++++ test/lit/passes/remove-exports.wast | 1 - 5 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 test/lit/passes/remove-exports-file.txt create mode 100644 test/lit/passes/remove-exports-file.wast create mode 100644 test/lit/passes/remove-exports-list.wast diff --git a/src/passes/RemoveExports.cpp b/src/passes/RemoveExports.cpp index 99cd0ff641d..5207a6f2e1a 100644 --- a/src/passes/RemoveExports.cpp +++ b/src/passes/RemoveExports.cpp @@ -19,10 +19,14 @@ // // --remove-exports=__* // -// That will remove all exports with names like "__foo" and "__bar". +// In this case we will remove all exports with names like "__foo" and "__bar". +// +// Exports can also be specified as a comma-separated list, and can be a +// response file. // #include "pass.h" +#include "support/file.h" #include "support/string.h" #include "wasm.h" @@ -32,13 +36,21 @@ namespace { struct RemoveExports : public Pass { void run(Module* module) override { - std::string pattern = + std::string param = getArgument(name, "Usage usage: wasm-opt --" + name + "=WILDCARD"); + param = String::trim(read_possible_response_file(param)); + + String::Split patterns(param, String::Split::NewLineOr(",")); + patterns = handleBracketingOperators(patterns); + std::vector toRemove; for (auto& exp : module->exports) { - if (String::wildcardMatch(pattern, exp->name.toString())) { - toRemove.push_back(exp->name); + for (auto& pattern : patterns) { + if (String::wildcardMatch(pattern, exp->name.toString())) { + toRemove.push_back(exp->name); + break; + } } } diff --git a/test/lit/passes/remove-exports-file.txt b/test/lit/passes/remove-exports-file.txt new file mode 100644 index 00000000000..3bd1f0e2974 --- /dev/null +++ b/test/lit/passes/remove-exports-file.txt @@ -0,0 +1,2 @@ +foo +bar diff --git a/test/lit/passes/remove-exports-file.wast b/test/lit/passes/remove-exports-file.wast new file mode 100644 index 00000000000..b333ff20038 --- /dev/null +++ b/test/lit/passes/remove-exports-file.wast @@ -0,0 +1,37 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: foreach %s %t wasm-opt --remove-exports=@%S/remove-exports-file.txt -all -S -o - | filecheck %s + +;; Test a response file as the input to this pass. foo and bar will be removed. +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (export "keep" (func $keep)) + + ;; CHECK: (func $foo (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $foo (export "foo") + (drop (i32.const 1)) + ) + + ;; CHECK: (func $bar (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $bar (export "bar") + (drop (i32.const 2)) + ) + + ;; CHECK: (func $keep (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $keep (export "keep") + (drop (i32.const 3)) + ) +) diff --git a/test/lit/passes/remove-exports-list.wast b/test/lit/passes/remove-exports-list.wast new file mode 100644 index 00000000000..7f850ee59fa --- /dev/null +++ b/test/lit/passes/remove-exports-list.wast @@ -0,0 +1,38 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: foreach %s %t wasm-opt "--remove-exports=foo,bar" -all -S -o - | filecheck %s + +;; The two exports mentioned will be removed (note the handling of comma +;; separation, taking into account bracketing). The other will remain. +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (export "keep" (func $keep)) + + ;; CHECK: (func $"foo" (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $"foo" (export "foo") + (drop (i32.const 1)) + ) + + ;; CHECK: (func $bar (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $bar (export "bar") + (drop (i32.const 2)) + ) + + ;; CHECK: (func $keep (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $keep (export "keep") + (drop (i32.const 3)) + ) +) diff --git a/test/lit/passes/remove-exports.wast b/test/lit/passes/remove-exports.wast index 0a5af581346..7629afa38f7 100644 --- a/test/lit/passes/remove-exports.wast +++ b/test/lit/passes/remove-exports.wast @@ -1,5 +1,4 @@ ;; 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-exports=__*" -all -S -o - | filecheck %s From 3c9c6f314ff85c4ebea8602839702c0d46928f53 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 6 May 2026 11:02:19 -0700 Subject: [PATCH 080/168] SignatureRefining: Do not refine results of functions used in continuations (#8675) This can break with `cont.bind`, see testcase. --- src/passes/SignatureRefining.cpp | 7 ++- test/lit/passes/signature-refining-cont.wast | 48 ++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 test/lit/passes/signature-refining-cont.wast diff --git a/src/passes/SignatureRefining.cpp b/src/passes/SignatureRefining.cpp index 792aedd9d5c..c1d72bc8d77 100644 --- a/src/passes/SignatureRefining.cpp +++ b/src/passes/SignatureRefining.cpp @@ -168,13 +168,12 @@ struct SignatureRefining : public Pass { false; } - // Continuations must not have params refined, because we do not update - // their users (e.g. cont.bind, resume) with new types. - // TODO: support refining continuations + // Continuations must not have params or results refined, because we do not + // update their users (e.g. cont.bind, resume) with new types. if (module->features.hasStackSwitching()) { for (auto type : ModuleUtils::collectHeapTypes(*module)) { if (type.isContinuation()) { - allInfo[type.getContinuation().type].canModifyParams = false; + allInfo[type.getContinuation().type].canModify = false; } } } diff --git a/test/lit/passes/signature-refining-cont.wast b/test/lit/passes/signature-refining-cont.wast new file mode 100644 index 00000000000..060dba2026e --- /dev/null +++ b/test/lit/passes/signature-refining-cont.wast @@ -0,0 +1,48 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: wasm-opt %s --signature-refining -all -S -o - | filecheck %s + +;; cont.bind places restrictions on signature refining. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (func (result (ref null $cont-A)))) + (type $A (func (result (ref null $cont-A)))) + + ;; CHECK: (type $B (func (result (ref null $cont-A)))) + (type $B (func (result (ref null $cont-A)))) + + ;; CHECK: (type $cont-A (cont $A)) + (type $cont-A (cont $A)) + + ;; CHECK: (type $cont-B (cont $B)) + (type $cont-B (cont $B)) + ) + + ;; CHECK: (elem declare func $0) + + ;; CHECK: (func $1 (type $A) (result (ref null $cont-A)) + ;; CHECK-NEXT: (cont.bind $cont-B $cont-A + ;; CHECK-NEXT: (cont.new $cont-B + ;; CHECK-NEXT: (ref.func $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $1 (type $A) (result (ref null $cont-A)) + ;; cont.bind requires that the continuation type's results match, so we + ;; cannot refine the type $A: if we make it return an exact result, we would + ;; be binding a continuation that returns an inexact result to an exact one. + (cont.bind $cont-B $cont-A + (cont.new $cont-B + (ref.func $0) + ) + ) + ) + + ;; CHECK: (func $0 (type $B) (result (ref null $cont-A)) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $0 (type $B) (result (ref null $cont-A)) + (unreachable) + ) +) + From 56a304912bd726de2c29da86a582d6af1ea8a429 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Wed, 6 May 2026 12:50:27 -0700 Subject: [PATCH 081/168] Support fuzzing more Relaxed SIMD instructions (#8676) Extend the binary and ternary WebAssembly fuzzer generators to cover more Relaxed SIMD. --- src/tools/fuzzing/fuzzing.cpp | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index e7696532472..fceeecf331d 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -4790,6 +4790,14 @@ Expression* TranslateToFuzzReader::makeBinary(Type type) { // SIMD Swizzle SwizzleVecI8x16) + .add(FeatureSet::RelaxedSIMD, + RelaxedSwizzleVecI8x16, + RelaxedMinVecF32x4, + RelaxedMaxVecF32x4, + RelaxedMinVecF64x2, + RelaxedMaxVecF64x2, + RelaxedQ15MulrSVecI16x8, + RelaxedDotI8x16I7x16SToVecI16x8) .add(FeatureSet::FP16, EqVecF16x8, EqVecF16x8, @@ -5113,13 +5121,18 @@ Expression* TranslateToFuzzReader::makeSIMDShuffle() { } Expression* TranslateToFuzzReader::makeSIMDTernary() { - // TODO: Enable qfma/qfms once it is implemented in V8 and the interpreter - // SIMDTernaryOp op = pick(Bitselect, - // QFMAF32x4, - // QFMSF32x4, - // QFMAF64x2, - // QFMSF64x2); - SIMDTernaryOp op = Bitselect; + SIMDTernaryOp op = pick(FeatureOptions() + .add(FeatureSet::SIMD, Bitselect) + .add(FeatureSet::RelaxedSIMD, + RelaxedMaddVecF32x4, + RelaxedNmaddVecF32x4, + RelaxedMaddVecF64x2, + RelaxedNmaddVecF64x2, + RelaxedLaneselectI8x16, + RelaxedLaneselectI16x8, + RelaxedLaneselectI32x4, + RelaxedLaneselectI64x2, + RelaxedDotI8x16I7x16AddSToVecI32x4)); Expression* a = make(Type::v128); Expression* b = make(Type::v128); Expression* c = make(Type::v128); From f6f01de88f35e40308d5fa6eaa45789627e6902f Mon Sep 17 00:00:00 2001 From: Changqing Jing Date: Thu, 7 May 2026 04:28:42 +0800 Subject: [PATCH 082/168] [NFC] Avoid O(N^2) exiting-branch checks in CodeFolding (#8599) Follow up PR of #8586 to optimize CodeFolding `optimizeTerminatingTails` calls `EffectAnalyzer` per tail item, each walking the full subtree. On deeply nested blocks this is O(N^2). Replace the per-item walks with a single O(N) bottom-up `PostWalker` (`populateExitingBranchCache`) that pre-computes exiting-branch results for every node, making subsequent lookups O(1). --- src/passes/CodeFolding.cpp | 92 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/src/passes/CodeFolding.cpp b/src/passes/CodeFolding.cpp index e53cd4f880d..1ccd0737f61 100644 --- a/src/passes/CodeFolding.cpp +++ b/src/passes/CodeFolding.cpp @@ -63,6 +63,7 @@ #include "ir/effects.h" #include "ir/eh-utils.h" #include "ir/find_all.h" +#include "ir/iteration.h" #include "ir/label-utils.h" #include "ir/utils.h" #include "pass.h" @@ -299,6 +300,7 @@ struct CodeFolding returnTails.clear(); unoptimizables.clear(); modifieds.clear(); + exitingBranchCache.clear(); if (needEHFixups) { EHUtils::handleBlockNestedPops(func, *getModule()); } @@ -306,6 +308,92 @@ struct CodeFolding } private: + // Cache of exiting branch names, populated on demand. Only queried roots + // are stored. An empty set means no exiting branches. + std::unordered_map> exitingBranchCache; + + bool hasExitingBranches(Expression* expr) { + auto it = exitingBranchCache.find(expr); + if (it != exitingBranchCache.end()) { + return !it->second.empty(); + } + return !populateExitingBranchCache(expr).empty(); + } + + // Walk |root| bottom-up computing exiting branches. Name sets are kept + // transiently (moved from children, erased after merge). Only the root's + // name set is persisted. Already-cached subtrees are skipped via scan(), + // and their cached names are merged in precisely. + // Returns a reference to the root's cached set (which may be empty). + const std::unordered_set& populateExitingBranchCache(Expression* root) { + struct CachePopulator + : public PostWalker> { + std::unordered_map>& resultCache; + std::unordered_map> nameSets; + + CachePopulator( + std::unordered_map>& resultCache) + : resultCache(resultCache) {} + + static void scan(CachePopulator* self, Expression** currp) { + auto* curr = *currp; + if (self->resultCache.count(curr)) { + return; + } + PostWalker>::scan(self, currp); + } + + void visitExpression(Expression* curr) { + std::unordered_set targets; + + ChildIterator children(curr); + for (auto* child : children) { + auto it = nameSets.find(child); + if (it != nameSets.end()) { + if (targets.empty()) { + targets = std::move(it->second); + } else { + targets.merge(it->second); + } + nameSets.erase(it); + } else { + // Child was skipped by scan() — merge its cached names. + auto cacheIt = resultCache.find(child); + if (cacheIt != resultCache.end() && !cacheIt->second.empty()) { + if (targets.empty()) { + targets = cacheIt->second; + } else { + targets.insert(cacheIt->second.begin(), cacheIt->second.end()); + } + } + } + } + + BranchUtils::operateOnScopeNameUses( + curr, [&](Name& name) { targets.insert(name); }); + + BranchUtils::operateOnScopeNameDefs(curr, [&](Name& name) { + if (name.is()) { + targets.erase(name); + } + }); + + if (!targets.empty()) { + nameSets[curr] = std::move(targets); + } + } + }; + CachePopulator populator(exitingBranchCache); + populator.walk(root); + auto it = populator.nameSets.find(root); + if (it != populator.nameSets.end()) { + return exitingBranchCache[root] = std::move(it->second); + } + return exitingBranchCache[root] = {}; + } + // check if we can move a list of items out of another item. we can't do so // if one of the items has a branch to something inside outOf that is not // inside that item @@ -637,9 +725,7 @@ struct CodeFolding // TODO: this should not be a problem in // *non*-terminating tails, but // double-verify that - if (EffectAnalyzer( - getPassOptions(), *getModule(), newItem) - .hasExternalBreakTargets()) { + if (hasExitingBranches(newItem)) { return true; } return false; From 9851810ef43454866157e40136249c8a6fe8eb34 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Wed, 6 May 2026 16:55:20 -0700 Subject: [PATCH 083/168] Add fuzzing support for more FP16 instructions (#8678) This adds fuzzing support for several FP16 half-precision vector instructions: SplatVecF16x8, MaddVecF16x8, NmaddVecF16x8, DemoteZeroVecF32x4ToVecF16x8, and DemoteZeroVecF64x2ToVecF16x8. --- src/tools/fuzzing/fuzzing.cpp | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index fceeecf331d..aba3afe33e1 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -4458,7 +4458,10 @@ Expression* TranslateToFuzzReader::makeUnary(Type type) { case 1: return buildUnary({SplatVecI64x2, make(Type::i64)}); case 2: - return buildUnary({SplatVecF32x4, make(Type::f32)}); + return buildUnary({pick(FeatureOptions() + .add(FeatureSet::SIMD, SplatVecF32x4) + .add(FeatureSet::FP16, SplatVecF16x8)), + make(Type::f32)}); case 3: return buildUnary({SplatVecF64x2, make(Type::f64)}); case 4: @@ -4531,7 +4534,9 @@ Expression* TranslateToFuzzReader::makeUnary(Type type) { TruncSatUVecF16x8ToVecI16x8, ConvertSVecI16x8ToVecF16x8, ConvertUVecI16x8ToVecF16x8, - PromoteLowVecF16x8ToVecF32x4)), + PromoteLowVecF16x8ToVecF32x4, + DemoteZeroVecF32x4ToVecF16x8, + DemoteZeroVecF64x2ToVecF16x8)), make(Type::v128)}); } WASM_UNREACHABLE("invalid value"); @@ -5121,18 +5126,20 @@ Expression* TranslateToFuzzReader::makeSIMDShuffle() { } Expression* TranslateToFuzzReader::makeSIMDTernary() { - SIMDTernaryOp op = pick(FeatureOptions() - .add(FeatureSet::SIMD, Bitselect) - .add(FeatureSet::RelaxedSIMD, - RelaxedMaddVecF32x4, - RelaxedNmaddVecF32x4, - RelaxedMaddVecF64x2, - RelaxedNmaddVecF64x2, - RelaxedLaneselectI8x16, - RelaxedLaneselectI16x8, - RelaxedLaneselectI32x4, - RelaxedLaneselectI64x2, - RelaxedDotI8x16I7x16AddSToVecI32x4)); + SIMDTernaryOp op = + pick(FeatureOptions() + .add(FeatureSet::SIMD, Bitselect) + .add(FeatureSet::RelaxedSIMD, + RelaxedMaddVecF32x4, + RelaxedNmaddVecF32x4, + RelaxedMaddVecF64x2, + RelaxedNmaddVecF64x2, + RelaxedLaneselectI8x16, + RelaxedLaneselectI16x8, + RelaxedLaneselectI32x4, + RelaxedLaneselectI64x2, + RelaxedDotI8x16I7x16AddSToVecI32x4) + .add(FeatureSet::FP16, MaddVecF16x8, NmaddVecF16x8)); Expression* a = make(Type::v128); Expression* b = make(Type::v128); Expression* c = make(Type::v128); From 6b06b59bf477298676d628faea03c9de7131a7d0 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Wed, 6 May 2026 18:03:42 -0700 Subject: [PATCH 084/168] Flush stdout/stderr between wasm-opt tests (#8680) Without this the github CI often shows the errors in confusing locations interspersed with the test names. I confirmed in #8679 that this addresses the issue. Maybe there is a better place to do this but this is good improvement for now. --- scripts/test/wasm_opt.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/test/wasm_opt.py b/scripts/test/wasm_opt.py index 6e45d3bd6ac..e1848519a30 100644 --- a/scripts/test/wasm_opt.py +++ b/scripts/test/wasm_opt.py @@ -15,6 +15,7 @@ import os import shutil import subprocess +import sys from . import shared, support @@ -60,6 +61,12 @@ def test_wasm_opt(): opts = [('--' + p if not p.startswith('O') and p != 'g' else '-' + p) for p in passes] actual = '' for module, asserts in support.split_wast(t): + # Flush stdout/stderr between each test. This prevent confusing + # interleaving in output of github CI + # TODO: Find a better, more systematic way to achieve this that + # works for all test suites. + sys.stdout.flush() + sys.stderr.flush() assert len(asserts) == 0 support.write_wast('split.wast', module) cmd = shared.WASM_OPT + opts + ['split.wast', '-q'] From c37aca5ff30c7e842035ff9e2b0c7c5695b239ff Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 7 May 2026 12:52:16 -0700 Subject: [PATCH 085/168] New fuzzer mode: Fuzz against JavaScript (#8655) The new fuzzer flag --fuzz-against-js tells the fuzzer we will only run the wasm against JS - not link it to wasm or something else. This lets it make changes that are valid from JS's point of view, like refining things on the boundary while not changing the arity. For example, if we sent JS an anyref, but the actual type we send is (ref $A) then we can refine to that type (or any type between it and anyref). We can do this for both export results and import params, as in both cases we send things to JS and know their type. This is useful for fuzzers that generate JS and let Binaryen mutate the wasm: they can emit anyrefs on the boundary, and Binaryen will be able to add new GC types in the module and even refine the boundary to those types. Such a fuzzer does not even need to emit GC types itself (it can emit anyref and send only nulls). --- scripts/fuzz_opt.py | 1 + src/tools/fuzzing.h | 10 ++ src/tools/fuzzing/fuzzing.cpp | 239 ++++++++++++++++++++++++++++++++ src/tools/wasm-opt.cpp | 9 ++ test/lit/help/wasm-opt.test | 4 + test/unit/input/fuzz.wat | 69 +++++++++ test/unit/test_fuzz_preserve.py | 194 ++++++++++++++++++++++++++ 7 files changed, 526 insertions(+) create mode 100644 test/unit/input/fuzz.wat create mode 100644 test/unit/test_fuzz_preserve.py diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index b002151e0f2..4ef1910852f 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2202,6 +2202,7 @@ def do_handle_pair(self, input, before_wasm, after_wasm, opts): input, '-ttf', '--fuzz-preserve-imports-exports', + '--fuzz-against-js', '--initial-fuzz=' + wat_file, '-o', pre_wasm, '-g', diff --git a/src/tools/fuzzing.h b/src/tools/fuzzing.h index 78057877031..e06160332b0 100644 --- a/src/tools/fuzzing.h +++ b/src/tools/fuzzing.h @@ -132,6 +132,7 @@ class TranslateToFuzzReader { void setPreserveImportsAndExports(bool preserveImportsAndExports_) { preserveImportsAndExports = preserveImportsAndExports_; } + void setAgainstJS(bool againstJS_) { againstJS = againstJS_; } void setImportedModule(std::string importedModuleName); void build(); @@ -159,6 +160,11 @@ class TranslateToFuzzReader { // existing testcase (using initial-content). bool preserveImportsAndExports = false; + // Whether the wasm will be used from JS and in no other way. This lets us + // modify the wasm in ways that keep it valid from JS's point of view, but + // which might cause issues when linked against wasm or used otherwise. + bool againstJS = false; + // An optional module to import from. std::optional importedModule; @@ -409,6 +415,10 @@ class TranslateToFuzzReader { void fixAfterChanges(Function* func); void modifyInitialFunctions(); + // Mutate the JS boundary, that is, make changes on the wasm side that JS + // would not be broken by (JS does not care about types). + void mutateJSBoundary(); + // Note a global for use during code generation. void useGlobalLater(Global* global); diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index aba3afe33e1..f5c332ad6d3 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -19,6 +19,7 @@ #include "ir/glbs.h" #include "ir/iteration.h" #include "ir/local-structural-dominance.h" +#include "ir/lubs.h" #include "ir/module-utils.h" #include "ir/names.h" #include "ir/subtype-exprs.h" @@ -413,6 +414,10 @@ void TranslateToFuzzReader::build() { PassRunner runner(&wasm); ReFinalize().run(&runner, &wasm); ReFinalize().walkModuleCode(&wasm); + + if (againstJS) { + mutateJSBoundary(); + } } void TranslateToFuzzReader::setupMemory() { @@ -2389,6 +2394,240 @@ void TranslateToFuzzReader::modifyInitialFunctions() { } } +void TranslateToFuzzReader::mutateJSBoundary() { + assert(againstJS); + + // Scan to find functions whose address is taken. We cannot modify their + // signatures at all. + + struct FunctionInfo { + // Whether there are references to this function itself. + bool reffed = false; + + // Calls to imports from this function. + std::vector callImports; + }; + + using NameInfoMap = std::unordered_map; + + struct FunctionInfoScanner + : public WalkerPass> { + // Not parallel for simplicity, see the map update below. + + bool modifiesBinaryenIR() override { return false; } + + NameInfoMap& map; + + FunctionInfoScanner(NameInfoMap& map) : map(map) {} + + std::unique_ptr create() override { + return std::make_unique(map); + } + + void visitCall(Call* curr) { + if (getModule()->getFunction(curr->target)->imported()) { + map[curr->target].callImports.push_back(curr); + } + + // Return calls add a dependency similar to references: we cannot refine + // the callee without coordination with the caller. + if (curr->isReturn) { + map[curr->target].reffed = true; + } + } + + void visitRefFunc(RefFunc* curr) { map[curr->func].reffed = true; } + }; + + NameInfoMap map; + FunctionInfoScanner scanner(map); + PassRunner runner(&wasm); + scanner.setModule(&wasm); + scanner.run(&runner, &wasm); + scanner.walkModuleCode(&wasm); + + // If a function does not have its address taken, we can refine types. This is + // safe because we will still send and receive the right number of values (we + // are not changing the arity, which JS might notice). Each place we may + // refine, we are given the maximum refinement and pick a random type between + // it and the old type. + auto maybeRefine = [&](Type old, Type new_) { + if (!old.isRef()) { + return old; + } + + // If this is unreachable code, we can still refine to the bottom. + if (new_ == Type::unreachable) { + new_ = Type(old.getHeapType().getBottom(), NonNullable); + } + + // Find all heap types between the old and new, starting from new. + auto oldHeapType = old.getHeapType(); + auto newHeapType = new_.getHeapType(); + assert(HeapType::isSubType(newHeapType, oldHeapType)); + std::vector options; + while (1) { + options.push_back(newHeapType); + // We cannot look at a bottom type's supers (there can be many, and the + // getSuperType() API doesn't return them), but can use + // interestingHeapSubTypes on the top. + if (newHeapType.isBottom()) { + for (auto type : interestingHeapSubTypes[newHeapType.getTop()]) { + options.push_back(type); + } + break; + } + // Continue until we reach the old type. + if (newHeapType == oldHeapType) { + break; + } + auto next = newHeapType.getSuperType(); + assert(next); + newHeapType = *next; + } + newHeapType = pick(options); + + // Pick the nullability. + auto oldNullability = old.getNullability(); + auto newNullability = new_.getNullability(); + if (newNullability != oldNullability) { + newNullability = getNullability(); + } + + // Pick the exactness. + auto oldExactness = old.getExactness(); + auto newExactness = new_.getExactness(); + // We can only be exact if we are using the new heap type: that type is + // exactly what is sent here, and no intermediate heap type would be valid. + // For example, given $A :> $B :> $C, then maybeRefine($A, exact $C) can + // return exact $C, but cannot return exact $B. + // + // Also, basic heap types cannot be exact. + if (newHeapType != new_.getHeapType() || newHeapType.isBasic()) { + newExactness = Inexact; + } else if (newExactness != oldExactness) { + // TODO: once getExactness() is fixed (see there), use that + newExactness = oneIn(2) ? Exact : Inexact; + } + + return Type(newHeapType, newNullability, newExactness); + }; + + // Given a set of types (all params or all results), and an index among them, + // refine that index if we can. It is possible that no new types exist at all, + // if the code was unreachable and we noted nothing. + auto maybeRefineIndex = [&](Type oldTypes, LUBFinder newLUB, Index index) { + auto lub = + newLUB.noted() ? newLUB.getLUB()[index] : Type(Type::unreachable); + return maybeRefine(oldTypes[index], lub); + }; + + // First, refine params sent to imports. Gather the LUB sent to each import, + // and then refine. + std::unordered_map paramLUBs; + for (auto& [_, info] : map) { + for (auto* call : info.callImports) { + auto declaredParams = wasm.getFunction(call->target)->getParams(); + std::vector sent; + for (Index i = 0; i < call->operands.size(); i++) { + auto type = call->operands[i]->type; + if (type == Type::unreachable) { + // Nothing sent here. What we refine to must still validate, even + // though this call is unreachable. Using the non-nullable bottom type + // is valid, and has the fewest restrictions. + type = declaredParams[i]; + if (type.isRef()) { + type = Type(type.getHeapType().getBottom(), NonNullable); + } + } + sent.push_back(type); + } + paramLUBs[call->target].note(Type(sent)); + } + } + + for (auto& func : wasm.functions) { + if (!func->imported()) { + continue; + } + // TODO: In the referenced case, we could consider using import/export + // wrappers and refining just there. + if (map[func->name].reffed) { + continue; + } + // Do not alter the signature of configureAll or other VM builtins. Changing + // these to something the VM does not expect will just cause it to + // immediately reject the module by trapping. + if (func->module.startsWith("wasm:")) { + continue; + } + + auto oldParams = func->getParams(); + if (oldParams == Type::none) { + continue; + } + + // Refine. + auto lub = paramLUBs[func->name]; + auto lubType = lub.getLUB(); + // Either the LUB has the right data shape, or nothing was noted (this is + // unreachable). + assert(oldParams.size() == lubType.size() || !lub.noted()); + std::vector newParams; + for (Index i = 0; i < lubType.size(); i++) { + newParams.push_back(maybeRefineIndex(oldParams, lub, i)); + } + func->setParams(Type(newParams)); + } + + // Second, refine results sent from exports. + for (auto& exp : wasm.exports) { + if (exp->kind != ExternalKind::Function) { + continue; + } + auto name = *exp->getInternalName(); + if (map[name].reffed) { + continue; + } + + auto* func = wasm.getFunction(name); + auto oldResults = func->getResults(); + if (oldResults == Type::none) { + continue; + } + + // Refine. + auto lub = LUB::getResultsLUB(func, wasm); + auto lubType = lub.getLUB(); + assert(oldResults.size() == lubType.size() || !lub.noted()); + std::vector newResults; + for (Index i = 0; i < lubType.size(); i++) { + newResults.push_back(maybeRefineIndex(oldResults, lub, i)); + } + func->setResults(Type(newResults)); + } + + // Update return types from calls to exports whose results we refined. + struct CallUpdater : public WalkerPass> { + bool isFunctionParallel() override { return true; } + + std::unique_ptr create() override { + return std::make_unique(); + } + + void visitCall(Call* curr) { + if (curr->type != Type::unreachable) { + curr->type = getModule()->getFunction(curr->target)->getResults(); + } + } + } updater; + updater.setModule(&wasm); + updater.run(&runner, &wasm); + + // Propagate after our changes. + ReFinalize().run(&runner, &wasm); +} + void TranslateToFuzzReader::dropToLog(Function* func) { // Don't always do this. if (oneIn(2)) { diff --git a/src/tools/wasm-opt.cpp b/src/tools/wasm-opt.cpp index 5c2807c25e4..f593428d2b6 100644 --- a/src/tools/wasm-opt.cpp +++ b/src/tools/wasm-opt.cpp @@ -87,6 +87,7 @@ int main(int argc, const char* argv[]) { bool fuzzMemory = true; bool fuzzOOB = true; bool fuzzPreserveImportsAndExports = false; + bool fuzzAgainstJS = false; std::string fuzzImport; std::string emitSpecWrapper; std::string emitWasm2CWrapper; @@ -212,6 +213,13 @@ For more on how to optimize effectively, see [&](Options* o, const std::string& arguments) { fuzzPreserveImportsAndExports = true; }) + .add( + "--fuzz-against-js", + "", + "modify the wasm in valid ways that assume it is used only from JS", + WasmOptOption, + Options::Arguments::Zero, + [&](Options* o, const std::string& arguments) { fuzzAgainstJS = true; }) .add( "--fuzz-import", "", @@ -349,6 +357,7 @@ For more on how to optimize effectively, see reader.setAllowMemory(fuzzMemory); reader.setAllowOOB(fuzzOOB); reader.setPreserveImportsAndExports(fuzzPreserveImportsAndExports); + reader.setAgainstJS(fuzzAgainstJS); if (!fuzzImport.empty()) { reader.setImportedModule(fuzzImport); } diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 08e8e5657c3..8566645db87 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -72,6 +72,10 @@ ;; CHECK-NEXT: --fuzz-preserve-imports-exports don't add imports and exports in ;; CHECK-NEXT: -ttf mode, and keep the start ;; CHECK-NEXT: +;; CHECK-NEXT: --fuzz-against-js modify the wasm in valid ways +;; CHECK-NEXT: that assume it is used only from +;; CHECK-NEXT: JS +;; CHECK-NEXT: ;; CHECK-NEXT: --fuzz-import a module to use as an import in ;; CHECK-NEXT: -ttf mode ;; CHECK-NEXT: diff --git a/test/unit/input/fuzz.wat b/test/unit/input/fuzz.wat new file mode 100644 index 00000000000..e102107e37f --- /dev/null +++ b/test/unit/input/fuzz.wat @@ -0,0 +1,69 @@ +(module + ;; Two structs, A and B, each of which has a subtype. + (rec + (type $A (sub (struct))) + (type $A2 (sub $A (struct))) + + (type $B (sub (struct))) + (type $B2 (sub $B(struct))) + ) + + ;; Two imports, one which will be referenced. + (import "module" "base" (func $import (param i32 anyref) (result eqref))) + (import "module" "base" (func $import-reffed (param i32 anyref) (result eqref))) + + ;; Two exports, one which will be referenced. + + (func $export (export "export") (param $0 i32) (param $1 anyref) (result eqref) + ;; Add the refs. + (drop + (ref.func $import-reffed) + ) + (drop + (ref.func $export-reffed) + ) + + ;; Call the imports. + (drop + (call $import + (i32.const 10) + ;; Send $A. We can refine the anyref to $A or $A2 (but not $B or $B2). + (struct.new $A) + ) + ) + (drop + (call $import-reffed + (i32.const 20) + (struct.new $A) + ) + ) + + ;; Return $B. We can refine the eqref to $B or $B2 (but not $A or $A2). + (struct.new $B) + ) + + (func $export-reffed (export "export-reffed") (param $0 i32) (param $1 anyref) (result eqref) + (struct.new $A) + ) + + ;; An export without a ref.func but that has a tail call, which also prevents + ;; us from refining its results. The called function and the caller both + ;; return nullref initially, and each might be refined to a conflicting type, + ;; if we are not careful here. + + (func $tail-called (export "tail-called") (result nullref) + (ref.null $A) + ) + + (func $tail-caller (export "tail-caller") (param $x i32) (result nullref) + (if + (local.get $x) + (then + (return + (ref.null $B) + ) + ) + ) + (return_call $tail-called) + ) +) diff --git a/test/unit/test_fuzz_preserve.py b/test/unit/test_fuzz_preserve.py new file mode 100644 index 00000000000..e1a3dc2fe93 --- /dev/null +++ b/test/unit/test_fuzz_preserve.py @@ -0,0 +1,194 @@ +import os +import random +import subprocess +import tempfile +import time + +from scripts.test import shared + +from . import utils + + +# Runs the fuzzer many times and allows checking for specific variety in the +# output. Calls hooks: +# +# self.found_variety() - checks if we found what we are looking for +# self.process_wat(wat) - receives the current fuzz wat +# +class FuzzerVarietyTester: + # Run until we find what we want. Stop only if we reached a max number + # of iterations and a timeout. + max_time = 60 + min_iters = 200 + + # The maximum size of the wasm-generating input + max_size = 1024 + + def __init__(self, initial): + self.initial = initial + + def test(self): + start_time = time.time() + stop_time = start_time + self.max_time + + self.temp_dir = tempfile.TemporaryDirectory() + + i = 0 + while True: + i += 1 + + # Stop early if we found what we are looking for. + if self.found_variety(): + print(f"{i} iterations {round(time.time() - start_time, 2)} seconds)") + print(f'proper import_params : {self.import_params}') + print(f'proper export_results: {self.export_results}') + return + + if i > self.min_iters and time.time() > stop_time: + raise Exception('looked too long and still failed') + + # Generate raw random data. + size = random.randint(1, self.max_size) + temp_dat = os.path.join(self.temp_dir.name, f'temp_{i}.dat') + with open(temp_dat, 'wb') as f: + f.write(bytes([random.randint(0, 255) for x in range(size)])) + + # Generate the fuzz testcase from the random data + the initial + # contents. + args = ['-ttf', temp_dat, '--initial-fuzz=' + self.initial, '-all'] + args += self.ttf_args + args += ['--print'] + wat = shared.run_process(shared.WASM_OPT + args, + stdout=subprocess.PIPE).stdout + + self.process_wat(wat) + + +class FuzzAgainstJSVarietyTester(FuzzerVarietyTester): + # When --fuzz-against-js is used, the wasm is only going to be fuzzed + # against JS, so the fuzzer mutates the boundary in valid ways, even if + # --fuzz-preserve-imports-exports is set. + # + # Testing this deterministically is too hard (as the fuzzer evolves, it + # will handle random data differently, and the test would constantly get + # out of date). Instead, test randomly, but in a way that the chance of + # a flake is unrealistic. + ttf_args = ['--fuzz-preserve-imports-exports', '--fuzz-against-js'] + + def __init__(self, initial): + super().__init__(initial) + + # The set of all params we see, for the import that is refinable. Ditto + # for export results. + self.import_params = set() + self.export_results = set() + + def found_variety(self): + return self.found_expected(self.import_params) and self.found_expected(self.export_results) + + def process_wat(self, wat): + # The things that begin reffed might end up not reffed, if mutation + # removes the refs. Check for that. + import_reffed_is_reffed = '(ref.func $import-reffed)' in wat + export_reffed_is_reffed = '(ref.func $export-reffed)' in wat + + # Find the params/results that might be refined. + for line in wat.splitlines(): + if line.startswith(' (import "module" "base" (func $import '): + params, results = self.parse_params_results(line) + self.import_params.add(params) + assert results == '(result eqref)', 'cannot refine import result' + elif line.startswith(' (import "module" "base" (func $import-reffed '): + params, results = self.parse_params_results(line) + if import_reffed_is_reffed: + assert params == '(param i32 anyref)', 'cannot refine reffed stuff' + assert results == '(result eqref)', 'cannot refine import result' + if line.startswith(' (func $export '): + params, results = self.parse_params_results(line) + assert params == '(param $0 i32) (param $1 anyref)', 'cannot refine export params' + self.export_results.add(results) + if line.startswith(' (func $export-reffed '): + params, results = self.parse_params_results(line) + assert params == '(param $0 i32) (param $1 anyref)', 'cannot refine export params' + if export_reffed_is_reffed: + assert results == '(result eqref)', 'cannot refine reffed stuff' + + # Given the types we saw for params or results, look in detail for the + # things we expect to see. + def found_expected(self, data): + # The many returns here seem to be the best way to write this code. + # ruff: noqa: PLR0911 + + # Look for significant variety. + if len(data) < 5: + return False + + string = str(data) + + # Each of the following has a 50% chance to get emitted each time, so + # over many iterations, the chance of failing to find them goes + # exponentially to nothing. + + # There must be nullable types. + if '(ref null' not in string: + return False + + # There must be non-nullable types. + if '(ref (' not in string and '(ref $' not in string: + return False + + string = string.replace('null ', '') + + # There must be defined types. + if ' $' not in string: + return False + + # There must be exact types. + if '(exact ' not in string: + return False + + # There must be inexact types. + if '(ref $' not in string: + return False + + return True + + # Given a line with wat params and results, parse and return them. + def parse_params_results(self, line): + # Find either params or results. + def get(what, line): + ret = '' + pos = 0 + + while True: + # Find the thing we are looking for. + start = line.find(what, pos) + if start < 0: + break + + # Find the end paren. + parens = 1 + end = start + 1 + while parens > 0: + if line[end] == '(': + parens += 1 + elif line[end] == ')': + parens -= 1 + end += 1 + + # Add (separated by a space). + if ret: + ret += ' ' + ret += line[start:end] + + # Keep looking. + pos = end + + return ret + + return get('(param', line), get('(result', line) + + +class PreserveFuzzTest(utils.BinaryenTestCase): + def test_against_js(self): + FuzzAgainstJSVarietyTester(self.input_path('fuzz.wat')).test() From 5f3595803cdf044d94e7951638f78b150dd19b10 Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Thu, 7 May 2026 16:32:19 -0700 Subject: [PATCH 086/168] Enable fuzzing for relaxed atomics (#8664) Part of #8165. V8 now supports relaxed atomics with the --experimental-wasm-acquire-release flag. cc @rmahdav --- scripts/bundle_clusterfuzz.py | 1 - scripts/clusterfuzz/run.py | 3 +-- scripts/fuzz_opt.py | 1 - scripts/test/shared.py | 1 + 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/bundle_clusterfuzz.py b/scripts/bundle_clusterfuzz.py index 3d90e76bcc5..86ea91a11d5 100755 --- a/scripts/bundle_clusterfuzz.py +++ b/scripts/bundle_clusterfuzz.py @@ -109,7 +109,6 @@ '--disable-fp16', '--disable-strings', '--disable-stack-switching', - '--disable-relaxed-atomics', '--disable-multibyte', '--disable-wide-arithmetic', ] diff --git a/scripts/clusterfuzz/run.py b/scripts/clusterfuzz/run.py index b155d656f25..7fe9979b3d2 100755 --- a/scripts/clusterfuzz/run.py +++ b/scripts/clusterfuzz/run.py @@ -33,7 +33,7 @@ # The V8 flags we put in the "fuzzer flags" files, which tell ClusterFuzz how to # run V8. By default we apply all staging flags. -FUZZER_FLAGS = '--wasm-staging --experimental-wasm-custom-descriptors --experimental-wasm-js-interop' +FUZZER_FLAGS = '--wasm-staging --experimental-wasm-custom-descriptors --experimental-wasm-js-interop --experimental-wasm-acquire-release' # Optional V8 flags to add to FUZZER_FLAGS, some of the time. OPTIONAL_FUZZER_FLAGS = [ @@ -94,7 +94,6 @@ '--disable-fp16', '--disable-strings', '--disable-stack-switching', - '--disable-relaxed-atomics', '--disable-wide-arithmetic', ] diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 4ef1910852f..c1a4d02c6d5 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -75,7 +75,6 @@ 'fp16', 'strings', 'stack-switching', - 'relaxed-atomics', 'multibyte', 'wide-arithmetic', ] diff --git a/scripts/test/shared.py b/scripts/test/shared.py index 8f872676a96..4a41a046bde 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -254,6 +254,7 @@ def has_shell_timeout(): '--experimental-wasm-fp16', '--experimental-wasm-custom-descriptors', '--experimental-wasm-js-interop', + '--experimental-wasm-acquire-release', ] # external tools From 3141b1a6909cbe59ebda3424fb56343565b69881 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Thu, 7 May 2026 16:51:31 -0700 Subject: [PATCH 087/168] Remove guesswork from getStackPointerGlobal. NFC (#8679) We were just assuming that if no `__stack_pointer` was imported then the first global must be the `__stack_pointer`. Instead we can just require that the global be names correctly in the name section. This will require an emscripten-side change to ensure names are generated when running this pass. Needed for fixing https://github.com/emscripten-core/emscripten/issues/24964. --- src/wasm/wasm-emscripten.cpp | 13 +- test/finalize/recursive_safe_stack.wat | 14 +- test/finalize/recursive_safe_stack.wat.out | 14 +- test/finalize/safe_stack_standalone-wasm.wat | 14 +- .../safe_stack_standalone-wasm.wat.out | 14 +- test/lit/passes/stack-check-memory64.wast | 16 +- test/passes/spill-pointers.txt | 178 +++++++++--------- test/passes/spill-pointers.wast | 6 +- 8 files changed, 131 insertions(+), 138 deletions(-) diff --git a/src/wasm/wasm-emscripten.cpp b/src/wasm/wasm-emscripten.cpp index 592020e1fef..ebc2df9bc2c 100644 --- a/src/wasm/wasm-emscripten.cpp +++ b/src/wasm/wasm-emscripten.cpp @@ -40,17 +40,10 @@ void addExportedFunction(Module& wasm, Function* function) { } Global* getStackPointerGlobal(Module& wasm) { - // Assumption: The stack pointer is either imported as __stack_pointer or - // we just assume it's the first non-imported global. - // TODO(sbc): Find a better way to discover the stack pointer. Perhaps the - // linker could export it by name? + // Assumption: The stack pointer is either be an imported global called + // __stack_pointer or a defined global with that name. for (auto& g : wasm.globals) { - if (g->imported() && g->base == STACK_POINTER) { - return g.get(); - } - } - for (auto& g : wasm.globals) { - if (!g->imported()) { + if (g->base == STACK_POINTER || g->name == STACK_POINTER) { return g.get(); } } diff --git a/test/finalize/recursive_safe_stack.wat b/test/finalize/recursive_safe_stack.wat index 67f7f3914a0..4d0d20923d0 100644 --- a/test/finalize/recursive_safe_stack.wat +++ b/test/finalize/recursive_safe_stack.wat @@ -6,7 +6,7 @@ (memory $0 2) (data (i32.const 568) "%d:%d\n\00Result: %d\n\00") (table $0 1 1 funcref) - (global $global$0 (mut i32) (i32.const 66128)) + (global $__stack_pointer (mut i32) (i32.const 66128)) (global $global$1 i32 (i32.const 66128)) (global $global$2 i32 (i32.const 587)) (export "memory" (memory $0)) @@ -18,10 +18,10 @@ ) (func $foo (; 2 ;) (type $0) (param $0 i32) (param $1 i32) (result i32) (local $2 i32) - (global.set $global$0 + (global.set $__stack_pointer (local.tee $2 (i32.sub - (global.get $global$0) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -40,7 +40,7 @@ (local.get $2) ) ) - (global.set $global$0 + (global.set $__stack_pointer (i32.add (local.get $2) (i32.const 16) @@ -53,10 +53,10 @@ ) (func $__original_main (; 3 ;) (type $2) (result i32) (local $0 i32) - (global.set $global$0 + (global.set $__stack_pointer (local.tee $0 (i32.sub - (global.get $global$0) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -74,7 +74,7 @@ (local.get $0) ) ) - (global.set $global$0 + (global.set $__stack_pointer (i32.add (local.get $0) (i32.const 16) diff --git a/test/finalize/recursive_safe_stack.wat.out b/test/finalize/recursive_safe_stack.wat.out index f042d8e1522..8cd234e9f9d 100644 --- a/test/finalize/recursive_safe_stack.wat.out +++ b/test/finalize/recursive_safe_stack.wat.out @@ -6,7 +6,7 @@ (type $4 (func (param i32 i32))) (import "env" "printf" (func $printf (param i32 i32) (result i32))) (import "env" "__handle_stack_overflow" (func $__handle_stack_overflow (param i32))) - (global $global$0 (mut i32) (i32.const 66128)) + (global $__stack_pointer (mut i32) (i32.const 66128)) (global $global$1 i32 (i32.const 66128)) (global $global$2 i32 (i32.const 587)) (global $__stack_base (mut i32) (i32.const 0)) @@ -33,7 +33,7 @@ (local.tee $3 (local.tee $2 (i32.sub - (global.get $global$0) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -51,7 +51,7 @@ ) ) ) - (global.set $global$0 + (global.set $__stack_pointer (local.get $3) ) ) @@ -92,7 +92,7 @@ ) ) ) - (global.set $global$0 + (global.set $__stack_pointer (local.get $4) ) ) @@ -112,7 +112,7 @@ (local.tee $1 (local.tee $0 (i32.sub - (global.get $global$0) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -130,7 +130,7 @@ ) ) ) - (global.set $global$0 + (global.set $__stack_pointer (local.get $1) ) ) @@ -170,7 +170,7 @@ ) ) ) - (global.set $global$0 + (global.set $__stack_pointer (local.get $2) ) ) diff --git a/test/finalize/safe_stack_standalone-wasm.wat b/test/finalize/safe_stack_standalone-wasm.wat index e2d8a79de07..ca2a9a2909f 100644 --- a/test/finalize/safe_stack_standalone-wasm.wat +++ b/test/finalize/safe_stack_standalone-wasm.wat @@ -6,7 +6,7 @@ (memory $0 2) (data (i32.const 568) "%d:%d\n\00Result: %d\n\00") (table $0 1 1 funcref) - (global $global$0 (mut i32) (i32.const 66128)) + (global $__stack_pointer (mut i32) (i32.const 66128)) (global $global$1 i32 (i32.const 66128)) (global $global$2 i32 (i32.const 587)) (export "memory" (memory $0)) @@ -18,10 +18,10 @@ ) (func $foo (; 2 ;) (type $0) (param $0 i32) (param $1 i32) (result i32) (local $2 i32) - (global.set $global$0 + (global.set $__stack_pointer (local.tee $2 (i32.sub - (global.get $global$0) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -40,7 +40,7 @@ (local.get $2) ) ) - (global.set $global$0 + (global.set $__stack_pointer (i32.add (local.get $2) (i32.const 16) @@ -53,10 +53,10 @@ ) (func $__original_main (; 3 ;) (type $2) (result i32) (local $0 i32) - (global.set $global$0 + (global.set $__stack_pointer (local.tee $0 (i32.sub - (global.get $global$0) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -74,7 +74,7 @@ (local.get $0) ) ) - (global.set $global$0 + (global.set $__stack_pointer (i32.add (local.get $0) (i32.const 16) diff --git a/test/finalize/safe_stack_standalone-wasm.wat.out b/test/finalize/safe_stack_standalone-wasm.wat.out index 2820566b3ee..c417e461181 100644 --- a/test/finalize/safe_stack_standalone-wasm.wat.out +++ b/test/finalize/safe_stack_standalone-wasm.wat.out @@ -4,7 +4,7 @@ (type $2 (func (result i32))) (type $3 (func (param i32 i32))) (import "env" "printf" (func $printf (param i32 i32) (result i32))) - (global $global$0 (mut i32) (i32.const 66128)) + (global $__stack_pointer (mut i32) (i32.const 66128)) (global $global$1 i32 (i32.const 66128)) (global $global$2 i32 (i32.const 587)) (global $__stack_base (mut i32) (i32.const 0)) @@ -31,7 +31,7 @@ (local.tee $3 (local.tee $2 (i32.sub - (global.get $global$0) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -47,7 +47,7 @@ (unreachable) ) ) - (global.set $global$0 + (global.set $__stack_pointer (local.get $3) ) ) @@ -86,7 +86,7 @@ (unreachable) ) ) - (global.set $global$0 + (global.set $__stack_pointer (local.get $4) ) ) @@ -106,7 +106,7 @@ (local.tee $1 (local.tee $0 (i32.sub - (global.get $global$0) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -122,7 +122,7 @@ (unreachable) ) ) - (global.set $global$0 + (global.set $__stack_pointer (local.get $1) ) ) @@ -160,7 +160,7 @@ (unreachable) ) ) - (global.set $global$0 + (global.set $__stack_pointer (local.get $2) ) ) diff --git a/test/lit/passes/stack-check-memory64.wast b/test/lit/passes/stack-check-memory64.wast index 807e733e7e4..e9ccd062536 100644 --- a/test/lit/passes/stack-check-memory64.wast +++ b/test/lit/passes/stack-check-memory64.wast @@ -9,8 +9,8 @@ ;; CHECK: (type $1 (func (param i64 i64))) - ;; CHECK: (global $sp (mut i64) (i64.const 0)) - (global $sp (mut i64) (i64.const 0)) + ;; CHECK: (global $__stack_pointer (mut i64) (i64.const 0)) + (global $__stack_pointer (mut i64) (i64.const 0)) ;; CHECK: (global $__stack_base (mut i64) (i64.const 0)) ;; CHECK: (global $__stack_limit (mut i64) (i64.const 0)) @@ -41,15 +41,15 @@ ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (global.set $sp + ;; CHECK-NEXT: (global.set $__stack_pointer ;; CHECK-NEXT: (local.get $0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (global.get $sp) + ;; CHECK-NEXT: (global.get $__stack_pointer) ;; CHECK-NEXT: ) (func $use_stack (export "use_stack") (result i64) - (global.set $sp (i64.const 42)) - (global.get $sp) + (global.set $__stack_pointer (i64.const 42)) + (global.get $__stack_pointer) ) ) ;; CHECK: (func $__set_stack_limits (param $0 i64) (param $1 i64) @@ -67,8 +67,8 @@ ;; CHECK: (type $1 (func (param i64 i64))) - ;; CHECK: (global $sp (mut i64) (i64.const 0)) - (global $sp (mut i64) (i64.const 0)) + ;; CHECK: (global $__stack_pointer (mut i64) (i64.const 0)) + (global $__stack_pointer (mut i64) (i64.const 0)) ;; CHECK: (global $__stack_base (mut i64) (i64.const 0)) (global $__stack_base (mut i64) (i64.const 0)) ;; CHECK: (global $__stack_limit (mut i64) (i64.const 0)) diff --git a/test/passes/spill-pointers.txt b/test/passes/spill-pointers.txt index 52d7b19a16b..98650041a3a 100644 --- a/test/passes/spill-pointers.txt +++ b/test/passes/spill-pointers.txt @@ -7,7 +7,7 @@ (type $5 (func (param f64))) (import "env" "STACKTOP" (global $STACKTOP$asm2wasm$import i32)) (import "env" "segfault" (func $segfault (param i32))) - (global $stack_ptr (mut i32) (global.get $STACKTOP$asm2wasm$import)) + (global $__stack_pointer (mut i32) (global.get $STACKTOP$asm2wasm$import)) (memory $0 10) (table $0 1 1 funcref) (elem $0 (i32.const 0)) @@ -23,10 +23,10 @@ (func $spill (local $x i32) (local $1 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $1 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -43,7 +43,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -56,10 +56,10 @@ (local $z f32) (local $w f64) (local $4 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $4 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -97,7 +97,7 @@ (local.get $w) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $4) (i32.const 16) @@ -110,10 +110,10 @@ (local $z i32) (local $w i32) (local $4 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $4 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -163,7 +163,7 @@ (local.get $w) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $4) (i32.const 16) @@ -177,10 +177,10 @@ (local $w i32) (local $a i32) (local $5 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $5 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 32) ) ) @@ -240,7 +240,7 @@ (local.get $a) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $5) (i32.const 32) @@ -251,10 +251,10 @@ (local $x i32) (local $y i32) (local $2 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $2 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -271,7 +271,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $2) (i32.const 16) @@ -283,10 +283,10 @@ (local $3 i32) (local $4 i32) (local $5 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $3 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -312,7 +312,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $3) (i32.const 16) @@ -325,10 +325,10 @@ (local $2 i32) (local $3 i32) (local $4 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $1 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -351,7 +351,7 @@ (local.set $2 (i32.const 2) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -365,7 +365,7 @@ (local.set $3 (i32.const 3) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -379,7 +379,7 @@ (i32.const 4) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -391,10 +391,10 @@ (local $x i32) (local $1 i32) (local $2 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $1 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -414,7 +414,7 @@ (unreachable) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -431,10 +431,10 @@ (local $3 i32) (local $4 i32) (local $5 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $2 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -469,7 +469,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $2) (i32.const 16) @@ -504,10 +504,10 @@ (local $1 i32) (local $2 i32) (local $3 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $1 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -526,7 +526,7 @@ ) (drop (block - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -537,7 +537,7 @@ (local.set $2 (i32.const 1) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -553,7 +553,7 @@ (i32.const 0) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -565,10 +565,10 @@ (local $x i32) (local $2 i32) (local $3 f64) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $2 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -590,7 +590,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $2) (i32.const 16) @@ -603,10 +603,10 @@ (local $2 i32) (local $3 i32) (local $4 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $1 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -636,7 +636,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -647,10 +647,10 @@ (local $x i32) (local $1 i32) (local $2 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $1 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -672,7 +672,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -688,13 +688,13 @@ (type $4 (func (param i32))) (type $5 (func (param f64))) (import "env" "segfault" (func $segfault (param i32))) - (global $stack_ptr (mut i32) (i32.const 1716592)) + (global $__stack_pointer (mut i32) (i32.const 1716592)) (memory $0 10) (table $0 1 1 funcref) (elem $0 (i32.const 0)) (export "stackSave" (func $stack_save)) (func $stack_save (result i32) - (global.get $stack_ptr) + (global.get $__stack_pointer) ) (func $nothing ) @@ -708,10 +708,10 @@ (func $spill (local $x i32) (local $1 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $1 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -728,7 +728,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -741,10 +741,10 @@ (local $z f32) (local $w f64) (local $4 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $4 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -782,7 +782,7 @@ (local.get $w) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $4) (i32.const 16) @@ -795,10 +795,10 @@ (local $z i32) (local $w i32) (local $4 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $4 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -848,7 +848,7 @@ (local.get $w) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $4) (i32.const 16) @@ -862,10 +862,10 @@ (local $w i32) (local $a i32) (local $5 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $5 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 32) ) ) @@ -925,7 +925,7 @@ (local.get $a) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $5) (i32.const 32) @@ -936,10 +936,10 @@ (local $x i32) (local $y i32) (local $2 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $2 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -956,7 +956,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $2) (i32.const 16) @@ -968,10 +968,10 @@ (local $3 i32) (local $4 i32) (local $5 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $3 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -997,7 +997,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $3) (i32.const 16) @@ -1010,10 +1010,10 @@ (local $2 i32) (local $3 i32) (local $4 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $1 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -1036,7 +1036,7 @@ (local.set $2 (i32.const 2) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -1050,7 +1050,7 @@ (local.set $3 (i32.const 3) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -1064,7 +1064,7 @@ (i32.const 4) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -1076,10 +1076,10 @@ (local $x i32) (local $1 i32) (local $2 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $1 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -1099,7 +1099,7 @@ (unreachable) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -1116,10 +1116,10 @@ (local $3 i32) (local $4 i32) (local $5 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $2 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -1154,7 +1154,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $2) (i32.const 16) @@ -1189,10 +1189,10 @@ (local $1 i32) (local $2 i32) (local $3 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $1 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -1211,7 +1211,7 @@ ) (drop (block - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -1222,7 +1222,7 @@ (local.set $2 (i32.const 1) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -1238,7 +1238,7 @@ (i32.const 0) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -1250,10 +1250,10 @@ (local $x i32) (local $2 i32) (local $3 f64) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $2 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -1275,7 +1275,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $2) (i32.const 16) @@ -1288,10 +1288,10 @@ (local $2 i32) (local $3 i32) (local $4 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $1 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -1321,7 +1321,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) @@ -1332,10 +1332,10 @@ (local $x i32) (local $1 i32) (local $2 i32) - (global.set $stack_ptr + (global.set $__stack_pointer (local.tee $1 (i32.sub - (global.get $stack_ptr) + (global.get $__stack_pointer) (i32.const 16) ) ) @@ -1357,7 +1357,7 @@ (local.get $x) ) ) - (global.set $stack_ptr + (global.set $__stack_pointer (i32.add (local.get $1) (i32.const 16) diff --git a/test/passes/spill-pointers.wast b/test/passes/spill-pointers.wast index b9c59b2c50e..de15ffb001a 100644 --- a/test/passes/spill-pointers.wast +++ b/test/passes/spill-pointers.wast @@ -5,7 +5,7 @@ (type $ii (func (param i32 i32))) (table 1 1 funcref) (elem (i32.const 0)) - (global $stack_ptr (mut i32) (global.get $STACKTOP$asm2wasm$import)) + (global $__stack_pointer (mut i32) (global.get $STACKTOP$asm2wasm$import)) (func $nothing ) @@ -176,10 +176,10 @@ (type $ii (func (param i32 i32))) (table 1 1 funcref) (elem (i32.const 0)) - (global $stack_ptr (mut i32) (i32.const 1716592)) + (global $__stack_pointer (mut i32) (i32.const 1716592)) (export "stackSave" (func $stack_save)) (func $stack_save (result i32) - (global.get $stack_ptr) + (global.get $__stack_pointer) ) (func $nothing From dc6762753219d0b5fd92d46d5986e64b16b0a955 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Fri, 8 May 2026 09:42:25 -0700 Subject: [PATCH 088/168] Add `print_heading` testsuite helper. NFC (#8650) --- check.py | 32 +++++++++++++++++--------------- scripts/auto_update_tests.py | 19 +++++++++++-------- scripts/test/binaryenjs.py | 5 +++-- scripts/test/finalize.py | 5 +++-- scripts/test/shared.py | 4 ++++ scripts/test/wasm2js.py | 5 +++-- scripts/test/wasm_opt.py | 15 ++++++++------- 7 files changed, 49 insertions(+), 36 deletions(-) diff --git a/check.py b/check.py index a670bb96001..3bf3bafcf5b 100755 --- a/check.py +++ b/check.py @@ -26,6 +26,7 @@ from pathlib import Path from scripts.test import binaryenjs, finalize, shared, support, wasm2js, wasm_opt +from scripts.test.shared import print_heading assert sys.version_info >= (3, 10), 'requires Python 3.10' @@ -41,7 +42,7 @@ def get_changelog_version(): def run_version_tests(): - print('[ checking --version ... ]\n') + print_heading('checking --version ...') not_executable_suffix = ['.DS_Store', '.txt', '.js', '.ilk', '.pdb', '.dll', '.wasm', '.manifest'] executable_prefix = ['wasm'] @@ -67,7 +68,7 @@ def run_version_tests(): def run_wasm_dis_tests(): - print('\n[ checking wasm-dis on provided binaries... ]\n') + print_heading('checking wasm-dis on provided binaries...') for t in shared.get_tests(shared.options.binaryen_test, ['.wasm']): print('..', os.path.basename(t)) @@ -85,7 +86,7 @@ def run_wasm_dis_tests(): def run_crash_tests(): - print("\n[ checking we don't crash on tricky inputs... ]\n") + print_heading("checking we don't crash on tricky inputs...") for t in shared.get_tests(shared.get_test_dir('crash'), ['.wast', '.wasm']): print('..', os.path.basename(t)) @@ -95,7 +96,7 @@ def run_crash_tests(): def run_dylink_tests(): - print("\n[ we emit dylink sections properly... ]\n") + print_heading('we emit dylink sections properly...') dylink_tests = glob.glob(os.path.join(shared.options.binaryen_test, 'dylib*.wasm')) for t in sorted(dylink_tests): @@ -109,7 +110,7 @@ def run_dylink_tests(): def run_ctor_eval_tests(): - print('\n[ checking wasm-ctor-eval... ]\n') + print_heading('checking wasm-ctor-eval...') for t in shared.get_tests(shared.get_test_dir('ctor-eval'), ['.wast', '.wasm']): print('..', os.path.basename(t)) @@ -126,7 +127,7 @@ def run_ctor_eval_tests(): def run_wasm_metadce_tests(): - print('\n[ checking wasm-metadce ]\n') + print_heading('checking wasm-metadce') for t in shared.get_tests(shared.get_test_dir('metadce'), ['.wast', '.wasm']): print('..', os.path.basename(t)) @@ -141,10 +142,10 @@ def run_wasm_metadce_tests(): def run_wasm_reduce_tests(): if not shared.has_shell_timeout(): - print('\n[ skipping wasm-reduce testcases]\n') + print_heading('skipping wasm-reduce testcases') return - print('\n[ checking wasm-reduce testcases]\n') + print_heading('checking wasm-reduce testcases') # fixed testcases for t in shared.get_tests(shared.get_test_dir('reduce'), ['.wast']): @@ -161,7 +162,7 @@ def run_wasm_reduce_tests(): # run on a nontrivial fuzz testcase, for general coverage # this is very slow in ThreadSanitizer, so avoid it there if 'fsanitize=thread' not in str(os.environ): - print('\n[ checking wasm-reduce fuzz testcase ]\n') + print_heading('checking wasm-reduce fuzz testcase') # TODO: re-enable multivalue once it is better optimized support.run_command(shared.WASM_OPT + [os.path.join(shared.options.binaryen_test, 'lit/basic/signext.wast'), '-ttf', '-Os', '-o', 'a.wasm', '--detect-features', '--disable-multivalue']) before = os.stat('a.wasm').st_size @@ -296,7 +297,7 @@ def red_stderr(): def run_spec_tests(): - print('\n[ checking wasm-shell spec testcases... ]\n') + print_heading('checking wasm-shell spec testcases...') worker_count = os.cpu_count() print("Running with", worker_count, "workers") @@ -328,7 +329,7 @@ def run_spec_tests(): def run_validator_tests(): - print('\n[ running validation tests... ]\n') + print_heading('running validation tests...') # Ensure the tests validate by default cmd = shared.WASM_AS + [os.path.join(shared.get_test_dir('validator'), 'invalid_export.wast'), '-o', 'a.wasm'] support.run_command(cmd) @@ -345,7 +346,7 @@ def run_validator_tests(): def run_example_tests(): - print('\n[ checking native example testcases...]\n') + print_heading('checking native example testcases...') if not shared.NATIVECC or not shared.NATIVEXX: shared.fail_with_error('Native compiler (e.g. gcc/g++) was not found in PATH!') return @@ -387,7 +388,7 @@ def run_example_tests(): def run_unittest(): - print('\n[ checking unit tests...]\n') + print_heading('checking unit tests...') # equivalent to `python -m unittest discover -s ./test -v` suite = unittest.defaultTestLoader.discover(os.path.dirname(shared.options.binaryen_test)) @@ -471,16 +472,17 @@ def main(): for test in shared.requested: TEST_SUITES[test]() + print() # Check/display the results if shared.num_failures == 0: - print('\n[ success! ]') + print_heading('success!') if shared.warnings: print('\n' + '\n'.join(shared.warnings)) if shared.num_failures > 0: - print('\n[ ' + str(shared.num_failures) + ' failures! ]') + print_heading(f'{shared.num_failures} failures!') return 1 return 0 diff --git a/scripts/auto_update_tests.py b/scripts/auto_update_tests.py index 0c7ae5cb034..43f318055be 100755 --- a/scripts/auto_update_tests.py +++ b/scripts/auto_update_tests.py @@ -18,11 +18,13 @@ import subprocess import sys +from test.shared import print_heading + from test import binaryenjs, finalize, shared, support, wasm2js, wasm_opt def update_example_tests(): - print('\n[ checking example testcases... ]\n') + print_heading('checking example testcases...') for src in shared.get_tests(shared.get_test_dir('example')): basename = os.path.basename(src) output_file = os.path.join(shared.options.binaryen_bin, 'example') @@ -64,7 +66,7 @@ def update_example_tests(): def update_wasm_dis_tests(): - print('\n[ checking wasm-dis on provided binaries... ]\n') + print_heading('checking wasm-dis on provided binaries...') for t in shared.get_tests(shared.options.binaryen_test, ['.wasm']): print('..', os.path.basename(t)) cmd = shared.WASM_DIS + [t] @@ -76,7 +78,7 @@ def update_wasm_dis_tests(): def update_ctor_eval_tests(): - print('\n[ checking wasm-ctor-eval... ]\n') + print_heading('checking wasm-ctor-eval...') for t in shared.get_tests(shared.get_test_dir('ctor-eval'), ['.wast', '.wasm']): print('..', os.path.basename(t)) ctors = open(t + '.ctors').read().strip() @@ -93,7 +95,7 @@ def update_ctor_eval_tests(): def update_metadce_tests(): - print('\n[ checking wasm-metadce... ]\n') + print_heading('checking wasm-metadce...') for t in shared.get_tests(shared.get_test_dir('metadce'), ['.wast', '.wasm']): print('..', os.path.basename(t)) graph = t + '.graph.txt' @@ -108,7 +110,7 @@ def update_metadce_tests(): def update_reduce_tests(): - print('\n[ checking wasm-reduce ]\n') + print_heading('checking wasm-reduce') for t in shared.get_tests(shared.get_test_dir('reduce'), ['.wast']): print('..', os.path.basename(t)) # convert to wasm @@ -120,7 +122,7 @@ def update_reduce_tests(): def update_spec_tests(): - print('\n[ updating wasm-shell spec testcases... ]\n') + print_heading('updating wasm-shell spec testcases...') for t in shared.options.spec_tests: print('..', os.path.basename(t)) @@ -138,7 +140,7 @@ def update_spec_tests(): def update_lit_tests(): - print('\n[ updating lit testcases... ]\n') + print_heading('updating lit testcases...') script = os.path.join(shared.options.binaryen_root, 'scripts', 'update_lit_checks.py') @@ -187,8 +189,9 @@ def main(): for test in shared.requested: TEST_SUITES[test]() + print() - print('\n[ success! ]') + print_heading('success!') if __name__ == '__main__': diff --git a/scripts/test/binaryenjs.py b/scripts/test/binaryenjs.py index 97d84bb4f0f..aa9542be594 100644 --- a/scripts/test/binaryenjs.py +++ b/scripts/test/binaryenjs.py @@ -16,6 +16,7 @@ import subprocess from . import shared, support +from .shared import print_heading def make_js_test_header(binaryen_js): @@ -52,7 +53,7 @@ def test_binaryen_js(): if not os.path.exists(shared.BINARYEN_JS): shared.fail_with_error('no ' + shared.BINARYEN_JS + ' build to test') - print('\n[ checking binaryen.js testcases (' + shared.BINARYEN_JS + ')... ]\n') + print_heading(f'checking binaryen.js testcases ({shared.BINARYEN_JS})...') for s in shared.get_tests(shared.get_test_dir('binaryen.js'), ['.js']): outname = make_js_test(s, shared.BINARYEN_JS) @@ -87,7 +88,7 @@ def update_binaryen_js_tests(): print('no binaryen.js build to test') return - print('\n[ checking binaryen.js testcases... ]\n') + print_heading('checking binaryen.js testcases...') node_has_wasm = shared.NODEJS and support.node_has_webassembly(shared.NODEJS) for s in shared.get_tests(shared.get_test_dir('binaryen.js'), ['.js']): outname = make_js_test(s, shared.BINARYEN_JS) diff --git a/scripts/test/finalize.py b/scripts/test/finalize.py index c96bde73e5c..d5f3c2ebd0d 100644 --- a/scripts/test/finalize.py +++ b/scripts/test/finalize.py @@ -15,6 +15,7 @@ import os from . import shared, support +from .shared import print_heading def args_for_finalize(filename): @@ -46,14 +47,14 @@ def run_test(input_path): def test_wasm_emscripten_finalize(): - print('\n[ checking wasm-emscripten-finalize testcases... ]\n') + print_heading('checking wasm-emscripten-finalize testcases...') for input_path in shared.get_tests(shared.get_test_dir('finalize'), ['.wat', '.wasm']): run_test(input_path) def update_finalize_tests(): - print('\n[ updating wasm-emscripten-finalize testcases... ]\n') + print_heading('updating wasm-emscripten-finalize testcases...') for input_path in shared.get_tests(shared.get_test_dir('finalize'), ['.wat', '.wasm']): print('..', input_path) diff --git a/scripts/test/shared.py b/scripts/test/shared.py index 4a41a046bde..4786bf2c755 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -114,6 +114,10 @@ def warn(text): print('warning:', text, file=sys.stderr) +def print_heading(msg): + print(f'[ {msg} ]') + + # setup # Locate Binaryen build artifacts directory (bin/ by default) diff --git a/scripts/test/wasm2js.py b/scripts/test/wasm2js.py index 4af2c55d837..76b788841e9 100644 --- a/scripts/test/wasm2js.py +++ b/scripts/test/wasm2js.py @@ -16,6 +16,7 @@ import subprocess from . import shared, support +from .shared import print_heading basic_tests = shared.get_tests(os.path.join(shared.options.binaryen_test, 'lit', 'basic')) # memory64 is not supported in wasm2js yet (but may be with BigInt eventually). @@ -159,7 +160,7 @@ def test_asserts_output(): def test_wasm2js(): - print('\n[ checking wasm2js testcases... ]\n') + print_heading('checking wasm2js testcases...') check_for_stale_files() if shared.skip_if_on_windows('wasm2js'): return @@ -168,7 +169,7 @@ def test_wasm2js(): def update_wasm2js_tests(): - print('\n[ checking wasm2js ]\n') + print_heading('checking wasm2js') for opt in (0, 1): for wasm in basic_tests + spec_tests + wasm2js_tests: diff --git a/scripts/test/wasm_opt.py b/scripts/test/wasm_opt.py index e1848519a30..de51497139c 100644 --- a/scripts/test/wasm_opt.py +++ b/scripts/test/wasm_opt.py @@ -18,10 +18,11 @@ import sys from . import shared, support +from .shared import print_heading def test_wasm_opt(): - print('\n[ checking wasm-opt -o notation... ]\n') + print_heading('checking wasm-opt -o notation...') for extra_args in [[], ['--no-validation']]: wast = os.path.join(shared.options.binaryen_test, 'hello_world.wat') @@ -31,7 +32,7 @@ def test_wasm_opt(): support.run_command(cmd) shared.fail_if_not_identical_to_file(open(out).read(), wast) - print('\n[ checking wasm-opt binary reading/writing... ]\n') + print_heading('checking wasm-opt binary reading/writing...') shutil.copyfile(os.path.join(shared.options.binaryen_test, 'hello_world.wat'), 'a.wat') shared.delete_from_orbit('a.wasm') @@ -41,7 +42,7 @@ def test_wasm_opt(): support.run_command(shared.WASM_OPT + ['a.wasm', '-o', 'b.wast', '-S', '-q']) assert open('b.wast', 'rb').read()[0] != 0, 'we emit text with -S' - print('\n[ checking wasm-opt passes... ]\n') + print_heading('checking wasm-opt passes...') for t in shared.get_tests(shared.get_test_dir('passes'), ['.wast', '.wasm']): print('..', os.path.basename(t)) @@ -94,7 +95,7 @@ def test_wasm_opt(): with open('a.wat') as actual: shared.fail_if_not_identical_to_file(actual.read(), t + '.wat') - print('\n[ checking wasm-opt parsing & printing... ]\n') + print_heading('checking wasm-opt parsing & printing...') for t in shared.get_tests(shared.get_test_dir('print'), ['.wast']): print('..', os.path.basename(t)) @@ -111,13 +112,13 @@ def test_wasm_opt(): def update_wasm_opt_tests(): - print('\n[ updating wasm-opt -o notation... ]\n') + print_heading('updating wasm-opt -o notation...') wast = os.path.join(shared.options.binaryen_test, 'hello_world.wat') cmd = shared.WASM_OPT + [wast, '-o', 'a.wast', '-S'] support.run_command(cmd) open(wast, 'w').write(open('a.wast').read()) - print('\n[ updating wasm-opt parsing & printing... ]\n') + print_heading('updating wasm-opt parsing & printing...') for t in shared.get_tests(shared.get_test_dir('print'), ['.wast']): print('..', os.path.basename(t)) wasm = t.replace('.wast', '') @@ -133,7 +134,7 @@ def update_wasm_opt_tests(): with open(wasm + '.minified.txt', 'wb') as o: o.write(actual) - print('\n[ updating wasm-opt passes... ]\n') + print_heading('updating wasm-opt passes...') for t in shared.get_tests(shared.get_test_dir('passes'), ['.wast', '.wasm']): print('..', os.path.basename(t)) # windows has some failures that need to be investigated: From 3180c6f73e9f9598016b6a8a8346b81af0b9a5f2 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 8 May 2026 09:45:11 -0700 Subject: [PATCH 089/168] [NFC] Inline more core HeapType methods (#8681) This makes Unsubtyping over 2x faster on a large Dart testcase. This was the slowest pass there by far. On -Os this saves 5.5% of total time. The key slowdowns this fixes are HeapType::getShared() took 33% (!) of total runtime in Unsubtyping and HeapType::getKind() took 6%, all due to the call overhead that inlining can fix. --- src/wasm-type.h | 118 ++++++++++++++++++++++++++++++++++++----- src/wasm/wasm-type.cpp | 72 ------------------------- 2 files changed, 105 insertions(+), 85 deletions(-) diff --git a/src/wasm-type.h b/src/wasm-type.h index 97ccd98108e..972688928b8 100644 --- a/src/wasm-type.h +++ b/src/wasm-type.h @@ -151,23 +151,17 @@ class HeapType { HeapTypeKind getKind() const; constexpr bool isBasic() const { return id <= _last_basic_type; } - bool isFunction() const { - return isMaybeShared(func) || getKind() == HeapTypeKind::Func; - } - bool isData() const { - auto kind = getKind(); - return isMaybeShared(string) || kind == HeapTypeKind::Struct || - kind == HeapTypeKind::Array; - } - bool isSignature() const { return getKind() == HeapTypeKind::Func; } - bool isContinuation() const { return getKind() == HeapTypeKind::Cont; } - bool isStruct() const { return getKind() == HeapTypeKind::Struct; } - bool isArray() const { return getKind() == HeapTypeKind::Array; } + bool isFunction() const; + bool isData() const; + bool isSignature() const; + bool isContinuation() const; + bool isStruct() const; + bool isArray() const; bool isExn() const { return isMaybeShared(HeapType::exn); } bool isString() const { return isMaybeShared(HeapType::string); } bool isBottom() const; bool isOpen() const; - bool isShared() const { return getShared() == Shared; } + bool isShared() const; Shareability getShared() const; @@ -1117,6 +1111,104 @@ std::ostream& operator<<(std::ostream&, const TypeBuilder::ErrorReason&); // Inline some nontrivial methods here for performance reasons. +using RecGroupInfo = std::vector; + +struct HeapTypeInfo { + using type_t = HeapType; + // Used in assertions to ensure that temporary types don't leak into the + // global store. + bool isTemp = false; + bool isOpen = false; + Shareability share = Unshared; + // The supertype of this HeapType, if it exists. + HeapTypeInfo* supertype = nullptr; + // The descriptor of this HeapType, if it exists. + HeapTypeInfo* descriptor = nullptr; + // The HeapType described by this one, if it exists. + HeapTypeInfo* described = nullptr; + // The recursion group of this type or null if the recursion group is trivial + // (i.e. contains only this type). + RecGroupInfo* recGroup = nullptr; + size_t recGroupIndex = 0; + HeapTypeKind kind; + union { + Signature signature; + Continuation continuation; + Struct struct_; + Array array; + }; + + HeapTypeInfo(Signature sig) : kind(HeapTypeKind::Func), signature(sig) {} + HeapTypeInfo(Continuation continuation) + : kind(HeapTypeKind::Cont), continuation(continuation) {} + HeapTypeInfo(const Struct& struct_) + : kind(HeapTypeKind::Struct), struct_(struct_) {} + HeapTypeInfo(Struct&& struct_) + : kind(HeapTypeKind::Struct), struct_(std::move(struct_)) {} + HeapTypeInfo(Array array) : kind(HeapTypeKind::Array), array(array) {} + ~HeapTypeInfo(); + + constexpr bool isSignature() const { return kind == HeapTypeKind::Func; } + constexpr bool isContinuation() const { return kind == HeapTypeKind::Cont; } + constexpr bool isStruct() const { return kind == HeapTypeKind::Struct; } + constexpr bool isArray() const { return kind == HeapTypeKind::Array; } + constexpr bool isData() const { return isStruct() || isArray(); } +}; + +inline HeapTypeInfo* getHeapTypeInfo(HeapType ht) { + assert(!ht.isBasic()); + return (HeapTypeInfo*)ht.getID(); +} + +inline HeapTypeKind HeapType::getKind() const { + if (isBasic()) { + return HeapTypeKind::Basic; + } + return getHeapTypeInfo(*this)->kind; +} + +inline bool HeapType::isFunction() const { + return isMaybeShared(func) || getKind() == HeapTypeKind::Func; +} + +inline bool HeapType::isData() const { + auto kind = getKind(); + return isMaybeShared(string) || kind == HeapTypeKind::Struct || + kind == HeapTypeKind::Array; +} + +inline bool HeapType::isSignature() const { + return getKind() == HeapTypeKind::Func; +} + +inline bool HeapType::isContinuation() const { + return getKind() == HeapTypeKind::Cont; +} + +inline bool HeapType::isStruct() const { + return getKind() == HeapTypeKind::Struct; +} + +inline bool HeapType::isArray() const { + return getKind() == HeapTypeKind::Array; +} + +inline bool HeapType::isOpen() const { + if (isBasic()) { + return false; + } + return getHeapTypeInfo(*this)->isOpen; +} + +inline bool HeapType::isShared() const { return getShared() == Shared; } + +inline Shareability HeapType::getShared() const { + if (isBasic()) { + return (getID() & SharedMask) != 0 ? Shared : Unshared; + } + return getHeapTypeInfo(*this)->share; +} + inline bool HeapType::isBottom() const { if (isBasic()) { switch (getBasic(Unshared)) { diff --git a/src/wasm/wasm-type.cpp b/src/wasm/wasm-type.cpp index 685aace64b4..ae4983daaff 100644 --- a/src/wasm/wasm-type.cpp +++ b/src/wasm/wasm-type.cpp @@ -38,50 +38,6 @@ namespace wasm { namespace { -using RecGroupInfo = std::vector; - -struct HeapTypeInfo { - using type_t = HeapType; - // Used in assertions to ensure that temporary types don't leak into the - // global store. - bool isTemp = false; - bool isOpen = false; - Shareability share = Unshared; - // The supertype of this HeapType, if it exists. - HeapTypeInfo* supertype = nullptr; - // The descriptor of this HeapType, if it exists. - HeapTypeInfo* descriptor = nullptr; - // The HeapType described by this one, if it exists. - HeapTypeInfo* described = nullptr; - // The recursion group of this type or null if the recursion group is trivial - // (i.e. contains only this type). - RecGroupInfo* recGroup = nullptr; - size_t recGroupIndex = 0; - HeapTypeKind kind; - union { - Signature signature; - Continuation continuation; - Struct struct_; - Array array; - }; - - HeapTypeInfo(Signature sig) : kind(HeapTypeKind::Func), signature(sig) {} - HeapTypeInfo(Continuation continuation) - : kind(HeapTypeKind::Cont), continuation(continuation) {} - HeapTypeInfo(const Struct& struct_) - : kind(HeapTypeKind::Struct), struct_(struct_) {} - HeapTypeInfo(Struct&& struct_) - : kind(HeapTypeKind::Struct), struct_(std::move(struct_)) {} - HeapTypeInfo(Array array) : kind(HeapTypeKind::Array), array(array) {} - ~HeapTypeInfo(); - - constexpr bool isSignature() const { return kind == HeapTypeKind::Func; } - constexpr bool isContinuation() const { return kind == HeapTypeKind::Cont; } - constexpr bool isStruct() const { return kind == HeapTypeKind::Struct; } - constexpr bool isArray() const { return kind == HeapTypeKind::Array; } - constexpr bool isData() const { return isStruct() || isArray(); } -}; - // Helper for finding the equirecursive least upper bound of two types. // Helper for printing types. struct TypePrinter { @@ -210,11 +166,6 @@ template class equal_to> { namespace wasm { namespace { -HeapTypeInfo* getHeapTypeInfo(HeapType ht) { - assert(!ht.isBasic()); - return (HeapTypeInfo*)ht.getID(); -} - HeapType asHeapType(std::unique_ptr& info) { return HeapType(uintptr_t(info.get())); } @@ -881,29 +832,6 @@ HeapType::HeapType(Array array) { HeapType(globalRecGroupStore.insert(std::make_unique(array))); } -HeapTypeKind HeapType::getKind() const { - if (isBasic()) { - return HeapTypeKind::Basic; - } - return getHeapTypeInfo(*this)->kind; -} - -bool HeapType::isOpen() const { - if (isBasic()) { - return false; - } else { - return getHeapTypeInfo(*this)->isOpen; - } -} - -Shareability HeapType::getShared() const { - if (isBasic()) { - return (id & SharedMask) != 0 ? Shared : Unshared; - } else { - return getHeapTypeInfo(*this)->share; - } -} - bool HeapType::isCastable() { return !isContinuation() && !isMaybeShared(HeapType::cont) && !isMaybeShared(HeapType::nocont); From e9b4b4c45684882f5756411c64c603e4716bdd73 Mon Sep 17 00:00:00 2001 From: Changqing Jing Date: Sat, 9 May 2026 01:30:57 +0800 Subject: [PATCH 090/168] [NFC] cache repeated tree walks to avoid O(N^2) in optimizeTerminatingTails in CodeFolding (#8602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cache the result of getBranchTargets(getFunction()->body) in optimizeTerminatingTails so that recursive calls share the same computed set rather than each re-walking the entire function body. This avoids O(N²) behavior where N is the size of the function body, since the recursive calls previously each performed an O(N) tree walk. The cached targets are computed lazily on first need and passed through to the canMove overload that accepts pre-computed branch targets. ## Benmark data For the test case in https://github.com/WebAssembly/binaryen/issues/7319#issuecomment-2678393304 Main head: ```shell time ./build/bin/wasm-opt --code-folding --enable-bulk-memory --enable-multivalue --enable-reference-types --enable-gc --enable-tail-call --enable-exception-handling -o /dev/null ./test3.wasm real 5m45.996s user 6m6.267s sys 0m3.798s ``` This PR: ```shell time ./build/bin/wasm-opt --code-folding --enable-bulk-memory --enable-multivalue --enable-reference-types --enable-gc --enable-tail-call --enable-exception-handling -o /dev/null ./test3.wasm real 2m2.380s user 2m25.700s sys 0m2.449s ``` ## Benchmark regression test Test case: https://jetbrains.github.io/kotlinconf-app/73cbe24d7cf5a54d37ad.wasm On main ```shell Performance counter stats for 'build/bin/wasm-opt 73cbe24d7cf5a54d37ad.wasm -all --code-folding -o /dev/null' (10 runs): 4837936912 task-clock # 1.445 CPUs utilized ( +- 0.51% ) 114 context-switches # 23.564 /sec ( +- 7.58% ) 7 cpu-migrations # 1.447 /sec ( +- 16.88% ) 46271 page-faults # 9.564 K/sec ( +- 0.00% ) 13431328103 instructions # 1.21 insn per cycle ( +- 0.01% ) 11125222873 cycles # 2.300 GHz ( +- 0.51% ) 64641504 branch-misses ( +- 1.26% ) 3.3484 +- 0.0221 seconds time elapsed ( +- 0.66% ) ``` On current PR ```shell Performance counter stats for 'build/bin/wasm-opt 73cbe24d7cf5a54d37ad.wasm -all --code-folding -o /dev/null' (10 runs): 4802304211 task-clock # 1.437 CPUs utilized ( +- 0.47% ) 125 context-switches # 26.029 /sec ( +- 6.50% ) 8 cpu-migrations # 1.666 /sec ( +- 14.20% ) 46272 page-faults # 9.635 K/sec ( +- 0.00% ) 13391520427 instructions # 1.21 insn per cycle ( +- 0.01% ) 11043221889 cycles # 2.300 GHz ( +- 0.47% ) 59021679 branch-misses ( +- 1.24% ) 3.3427 +- 0.0207 seconds time elapsed ( +- 0.62% ) ``` --- src/passes/CodeFolding.cpp | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/passes/CodeFolding.cpp b/src/passes/CodeFolding.cpp index 1ccd0737f61..7964818121a 100644 --- a/src/passes/CodeFolding.cpp +++ b/src/passes/CodeFolding.cpp @@ -398,7 +398,14 @@ struct CodeFolding // if one of the items has a branch to something inside outOf that is not // inside that item bool canMove(const std::vector& items, Expression* outOf) { - auto allTargets = BranchUtils::getBranchTargets(outOf); + return canMove(items, outOf, BranchUtils::getBranchTargets(outOf)); + } + + // Overload that accepts pre-computed branch targets to avoid redundant + // O(N) getBranchTargets calls. + bool canMove(const std::vector& items, + Expression* outOf, + const BranchUtils::NameSet& allTargets) { for (auto* item : items) { auto exiting = BranchUtils::getExitingBranches(item); std::vector intersection; @@ -632,11 +639,18 @@ struct CodeFolding // we are just starting; num > 0 means that tails is guaranteed to be // equal in the last num items, so we can merge there, but we look for // deeper merges first. + // bodyTargets is lazily computed on first need and then passed to recursive + // calls to avoid repeated O(N) getBranchTargets walks over the function body. // returns whether we optimized something. - bool optimizeTerminatingTails(std::vector& tails, Index num = 0) { + bool optimizeTerminatingTails(std::vector& tails, + Index num = 0, + BranchUtils::NameSet* bodyTargets = nullptr) { if (tails.size() < 2) { return false; } + // Storage for body branch targets, declared here so it outlives the + // pointer stored in bodyTargets. + BranchUtils::NameSet localBodyTargets; // remove things that are untoward and cannot be optimized tails.erase( std::remove_if(tails.begin(), @@ -697,9 +711,11 @@ struct CodeFolding // can be removed, though cost += WORTH_ADDING_BLOCK_TO_REMOVE_THIS_MUCH; // if we cannot merge to the end, then we definitely need 2 blocks, - // and a branch - // TODO: efficiency, entire body - if (!canMove(items, getFunction()->body)) { + // and a branch. Use the pre-computed bodyTargets to avoid repeated + // O(N) getBranchTargets calls. + assert(bodyTargets); + bool canMoveItems = canMove(items, getFunction()->body, *bodyTargets); + if (!canMoveItems) { cost += 1 + WORTH_ADDING_BLOCK_TO_REMOVE_THIS_MUCH; // TODO: to do this, we need to maintain a map of element=>parent, // so that we can insert the new blocks in the right place @@ -795,7 +811,14 @@ struct CodeFolding // as the changes may influence us. we leave further opts to further // passes (as this is rare in practice, it's generally not a perf // issue, but TODO optimize) - if (optimizeTerminatingTails(explore, num + 1)) { + // Compute body branch targets once and share across recursive + // calls to avoid repeated O(N) tree walks. + if (!bodyTargets) { + localBodyTargets = + BranchUtils::getBranchTargets(getFunction()->body); + bodyTargets = &localBodyTargets; + } + if (optimizeTerminatingTails(explore, num + 1, bodyTargets)) { return true; } } From 6c3212577de63ee451ef2784836d5a5dfce3d37f Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 8 May 2026 10:31:10 -0700 Subject: [PATCH 091/168] Fuzzer: Fix subtyping of bottom types when fuzzing against JS (#8683) maybeRefine should refine between old and new, and we were missing a check for being refined enough when traversing all subtypes of bottom. As a result, we could un-refine, which can break if a call exists. --- src/tools/fuzzing/fuzzing.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index f5c332ad6d3..7dc4a9051f7 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -2470,9 +2470,9 @@ void TranslateToFuzzReader::mutateJSBoundary() { options.push_back(newHeapType); // We cannot look at a bottom type's supers (there can be many, and the // getSuperType() API doesn't return them), but can use - // interestingHeapSubTypes on the top. + // interestingHeapSubTypes: any subtype of old is valid. if (newHeapType.isBottom()) { - for (auto type : interestingHeapSubTypes[newHeapType.getTop()]) { + for (auto type : interestingHeapSubTypes[oldHeapType]) { options.push_back(type); } break; From 79d54a16a1ce488dbc4fde1c80c438cad4c89df8 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 8 May 2026 16:19:11 -0700 Subject: [PATCH 092/168] Fix crash in MinimizeRecGroups (#8685) We previously did not store type indices for public types. This caused a crash when the logic for comparing rec groups came across references to public types and assumed there would be indices to compare them by. Fixes #8682. --- src/passes/MinimizeRecGroups.cpp | 2 +- test/lit/passes/minimize-rec-groups.wast | 40 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/passes/MinimizeRecGroups.cpp b/src/passes/MinimizeRecGroups.cpp index 306426af5b9..a4d5e8211ac 100644 --- a/src/passes/MinimizeRecGroups.cpp +++ b/src/passes/MinimizeRecGroups.cpp @@ -311,10 +311,10 @@ struct MinimizeRecGroups : Pass { // generate new groups with the same shape. std::unordered_set publicGroups; for (auto& [type, info] : typeInfo) { + typeIndices.insert({type, typeIndices.size()}); if (info.visibility == ModuleUtils::Visibility::Private) { // We can optimize private types. types.push_back(type); - typeIndices.insert({type, typeIndices.size()}); } else { publicGroups.insert(type.getRecGroup()); } diff --git a/test/lit/passes/minimize-rec-groups.wast b/test/lit/passes/minimize-rec-groups.wast index 3483d1c9de3..963f655f20d 100644 --- a/test/lit/passes/minimize-rec-groups.wast +++ b/test/lit/passes/minimize-rec-groups.wast @@ -563,4 +563,44 @@ ;; CHECK: (global $privateB (ref null $privateB) (ref.null none)) (global $privateB (ref null $privateB) (ref.null none)) ) + +;; Regression test for a bug where we crashed when comparing rec groups with +;; references to public types because we did not store type indices for public +;; types. ;; CHECK: (export "g" (global $public)) +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $public (sub (descriptor $desc) (struct))) + (type $public (sub (descriptor $desc) (struct))) + ;; CHECK: (type $desc (sub (describes $public) (struct))) + (type $desc (sub (describes $public) (struct))) + ) + + ;; We should not crash when comparing these identical connected components. + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $private1 (sub $public (descriptor $desc1) (struct))) + (type $private1 (sub $public (descriptor $desc1) (struct))) + ;; CHECK: (type $desc1 (sub $desc (describes $private1) (struct))) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $4 (struct)) + + ;; CHECK: (type $private2 (sub $public (descriptor $desc2) (struct))) + (type $private2 (sub $public (descriptor $desc2) (struct))) + (type $desc1 (sub $desc (describes $private1) (struct))) + ;; CHECK: (type $desc2 (sub $desc (describes $private2) (struct))) + (type $desc2 (sub $desc (describes $private2) (struct))) + ) + + ;; CHECK: (global $use1 (ref null $private1) (ref.null none)) + (global $use1 (ref null $private1) (ref.null none)) + ;; CHECK: (global $use2 (ref null $private2) (ref.null none)) + (global $use2 (ref null $private2) (ref.null none)) + ;; CHECK: (global $public (ref null $public) (ref.null none)) + (global $public (ref null $public) (ref.null none)) + ;; CHECK: (export "public" (global $public)) + (export "public" (global $public)) +) + From d9fd5da61afc0e540b0275f21b8cbf802cf860de Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 11 May 2026 11:57:32 -0700 Subject: [PATCH 093/168] [wasm-split] Move shareImportableItems (NFC) (#8686) This just moves the function up in the file. A follow-up PR will change the order of functions and run it before `indirectReferencesToSecondaryFunctions`, so I'd like to match the order of the functions in the file to match that order, but if I move the function in that PR, it is hard to see what changes in the function. --- src/ir/module-splitting.cpp | 1070 +++++++++++++++++------------------ 1 file changed, 535 insertions(+), 535 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index f5825a93bf0..21d82d013a4 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -558,310 +558,6 @@ static void walkSegments(Walker& walker, Module* module) { } } -void ModuleSplitter::indirectReferencesToSecondaryFunctions() { - // Turn references to secondary functions into references to thunks that - // perform a direct call to the original referent. The direct calls in the - // thunks will be handled like all other cross-module calls later, in - // |indirectCallsToSecondaryFunctions|. - struct Gatherer : public PostWalker { - ModuleSplitter& parent; - - Gatherer(ModuleSplitter& parent) : parent(parent) {} - - // Collect RefFuncs in a map from the function name to all RefFuncs that - // refer to it. We only collect this for secondary funcs. - InsertOrderedMap> map; - - void visitRefFunc(RefFunc* curr) { - Module* currModule = getModule(); - // Add ref.func to the map when - // 1. ref.func's target func is in one of the secondary modules and - // 2. the current module is a different module (either the primary module - // or a different secondary module) - if (parent.allSecondaryFuncs.contains(curr->func) && - (currModule == &parent.primary || - parent.secondaries.at(parent.funcToSecondaryIndex.at(curr->func)) - .get() != currModule)) { - map[curr->func].push_back(curr); - } - } - } gatherer(*this); - // We shouldn't use collector.walkModuleCode here, because we don't want to - // walk global initializers. At this point, all globals are still in the - // primary module, so if we walk global initializers here, it will create - // unnecessary trampolines. - // - // For example, we have (global $a funcref (ref.func $foo)), and $foo was - // split into a secondary module. Because $a is at this point still in the - // primary module, $foo will be considered to exist in a different module, so - // this will create a trampoline for $foo. But it is possible that later we - // find out $a is exclusively used by that secondary module and move $a there. - // In that case, $a can just reference $foo locally, but if we scan global - // initializers here, we would have created an unnecessary trampoline for - // $foo. - walkSegments(gatherer, &primary); - for (auto& curr : primary.functions) { - if (!curr->imported()) { - gatherer.walkFunction(curr.get()); - } - } - for (auto& secondaryPtr : secondaries) { - gatherer.walkModule(secondaryPtr.get()); - } - - // Ignore references to secondary functions that occur in the active segment - // that will contain the imported placeholders. Indirect calls to table slots - // initialized by that segment will already go to the right place once the - // secondary module has been loaded and the table has been patched. - std::unordered_set ignore; - if (tableManager.activeSegment) { - for (auto* expr : tableManager.activeSegment->data) { - if (auto* ref = expr->dynCast()) { - ignore.insert(ref); - } - } - } - - // Fix up what we found: Generate trampolines as described earlier, and apply - // them. - Builder builder(primary); - // Generate the new trampoline function and add it to the module. - for (auto& [name, refFuncs] : gatherer.map) { - // Find the relevant (non-ignored) RefFuncs. If there are none, we can skip - // creating a thunk entirely. - std::vector relevantRefFuncs; - for (auto* refFunc : refFuncs) { - assert(refFunc->func == name); - if (!ignore.contains(refFunc)) { - relevantRefFuncs.push_back(refFunc); - } - } - if (relevantRefFuncs.empty()) { - continue; - } - - Name trampoline = getTrampoline(name); - // Update RefFuncs to refer to it. - for (auto* refFunc : relevantRefFuncs) { - refFunc->func = trampoline; - } - } -} - -void ModuleSplitter::indirectCallsToSecondaryFunctions() { - // Update direct calls of secondary functions to be indirect calls of their - // corresponding table indices instead. - struct CallIndirector : public PostWalker { - ModuleSplitter& parent; - CallIndirector(ModuleSplitter& parent) : parent(parent) {} - void visitCall(Call* curr) { - // Return if the call's target is not in one of the secondary module. - if (!parent.allSecondaryFuncs.contains(curr->target)) { - return; - } - // Return if the current module is the same module as the call's target, - // because we don't need a call_indirect within the same module. - Module* currModule = getModule(); - if (currModule != &parent.primary && - parent.secondaries.at(parent.funcToSecondaryIndex.at(curr->target)) - .get() == currModule) { - return; - } - - Builder builder(*getModule()); - Index secIndex = parent.funcToSecondaryIndex.at(curr->target); - auto* func = parent.secondaries.at(secIndex)->getFunction(curr->target); - auto tableSlot = - parent.tableManager.getSlot(curr->target, func->type.getHeapType()); - - replaceCurrent( - builder.makeCallIndirect(tableSlot.tableName, - tableSlot.makeExpr(parent.primary), - curr->operands, - func->type.getHeapType(), - curr->isReturn)); - } - }; - CallIndirector callIndirector(*this); - callIndirector.walkModule(&primary); - for (auto& secondaryPtr : secondaries) { - callIndirector.walkModule(secondaryPtr.get()); - } -} - -void ModuleSplitter::exportImportCalledPrimaryFunctions() { - // Find primary functions called/referred to from the secondary modules. - using CalledPrimaryToModules = std::map>; - for (auto& secondaryPtr : secondaries) { - Module* secondary = secondaryPtr.get(); - ModuleUtils::ParallelFunctionAnalysis callCollector( - *secondary, - [&](Function* func, CalledPrimaryToModules& calledPrimaryToModules) { - struct CallCollector : PostWalker { - const std::unordered_set& primaryFuncs; - CalledPrimaryToModules& calledPrimaryToModules; - CallCollector(const std::unordered_set& primaryFuncs, - CalledPrimaryToModules& calledPrimaryToModules) - : primaryFuncs(primaryFuncs), - calledPrimaryToModules(calledPrimaryToModules) {} - void visitCall(Call* curr) { - if (primaryFuncs.contains(curr->target)) { - calledPrimaryToModules[curr->target].insert(getModule()); - } - } - void visitRefFunc(RefFunc* curr) { - if (primaryFuncs.contains(curr->func)) { - calledPrimaryToModules[curr->func].insert(getModule()); - } - } - }; - CallCollector(primaryFuncs, calledPrimaryToModules) - .walkFunctionInModule(func, secondary); - }); - - CalledPrimaryToModules calledPrimaryToModules; - for (auto& [_, map] : callCollector.map) { - calledPrimaryToModules.merge(map); - } - - // Ensure each called primary function is exported and imported - for (auto& [func, modules] : calledPrimaryToModules) { - exportImportFunction(func, modules); - } - } -} - -void ModuleSplitter::setupTablePatching() { - if (!tableManager.activeTable) { - return; - } - - std::map> moduleToReplacedElems; - // Replace table references to secondary functions with an imported - // placeholder that encodes the table index in its name: - // `importNamespace`.`index`. - forEachElement( - primary, [&](Name table, Name, Index index, Expression*& elem) { - auto* ref = elem->dynCast(); - if (!ref) { - return; - } - if (!allSecondaryFuncs.contains(ref->func)) { - return; - } - assert(table == tableManager.activeTable->name); - - placeholderMap[table][index] = ref->func; - Index secondaryIndex = funcToSecondaryIndex.at(ref->func); - Module& secondary = *secondaries.at(secondaryIndex); - Name secondaryName = config.secondaryNames.at(secondaryIndex); - auto* secondaryFunc = secondary.getFunction(ref->func); - moduleToReplacedElems[&secondary][index] = secondaryFunc; - if (!config.usePlaceholders) { - // TODO: This can create active element segments with lots of nulls. We - // should optimize them like we do data segments with zeros. - elem = Builder(primary).makeRefNull(HeapType::nofunc); - return; - } - auto placeholder = std::make_unique(); - placeholder->module = config.placeholderNamespacePrefix.toString() + "." + - secondaryName.toString(); - placeholder->base = std::to_string(index); - placeholder->name = Names::getValidFunctionName( - primary, std::string("placeholder_") + placeholder->base.toString()); - placeholder->hasExplicitName = true; - placeholder->type = secondaryFunc->type.with(Inexact); - elem = Builder(primary).makeRefFunc(placeholder->name, placeholder->type); - primary.addFunction(std::move(placeholder)); - }); - - if (moduleToReplacedElems.size() == 0) { - // No placeholders to patch out of the table - return; - } - - for (auto& [secondaryPtr, replacedElems] : moduleToReplacedElems) { - Module& secondary = *secondaryPtr; - auto secondaryTable = - ModuleUtils::copyTable(tableManager.activeTable, secondary); - - if (tableManager.activeBase.global.size()) { - assert(tableManager.activeTableSegments.size() == 1 && - "Unexpected number of segments with non-const base"); - assert(secondary.tables.size() == 1 && secondary.elementSegments.empty()); - // Since addition is not currently allowed in initializer expressions, we - // need to start the new secondary segment where the primary segment - // starts. The secondary segment will contain the same primary functions - // as the primary module except in positions where it needs to overwrite a - // placeholder function. All primary functions in the table therefore need - // to be imported into the second module. TODO: use better strategies - // here, such as using ref.func in the start function or standardizing - // addition in initializer expressions. - ElementSegment* primarySeg = tableManager.activeTableSegments.front(); - std::vector secondaryElems; - secondaryElems.reserve(primarySeg->data.size()); - - // Copy functions from the primary segment to the secondary segment, - // replacing placeholders and creating new exports and imports as - // necessary. - auto replacement = replacedElems.begin(); - for (Index i = 0; - i < primarySeg->data.size() && replacement != replacedElems.end(); - ++i) { - if (replacement->first == i) { - // primarySeg->data[i] is a placeholder, so use the secondary - // function. - auto* func = replacement->second; - auto* ref = Builder(secondary).makeRefFunc(func->name, func->type); - secondaryElems.push_back(ref); - ++replacement; - } else if (auto* get = primarySeg->data[i]->dynCast()) { - exportImportFunction(get->func, {&secondary}); - auto* copied = - ExpressionManipulator::copy(primarySeg->data[i], secondary); - secondaryElems.push_back(copied); - } - } - - auto offset = ExpressionManipulator::copy(primarySeg->offset, secondary); - auto secondarySeg = std::make_unique( - secondaryTable->name, offset, secondaryTable->type, secondaryElems); - secondarySeg->setName(primarySeg->name, primarySeg->hasExplicitName); - secondary.addElementSegment(std::move(secondarySeg)); - return; - } - - // Create active table segments in the secondary module to patch in the - // original functions when it is instantiated. - Index currBase = replacedElems.begin()->first; - std::vector currData; - auto finishSegment = [&]() { - auto* offset = Builder(secondary).makeConst( - Literal::makeFromInt32(currBase, secondaryTable->addressType)); - auto secondarySeg = std::make_unique( - secondaryTable->name, offset, secondaryTable->type, currData); - Name name = Names::getValidElementSegmentName( - secondary, Name::fromInt(secondary.elementSegments.size())); - secondarySeg->setName(name, false); - secondary.addElementSegment(std::move(secondarySeg)); - }; - for (auto curr = replacedElems.begin(); curr != replacedElems.end(); - ++curr) { - if (curr->first != currBase + currData.size()) { - finishSegment(); - currBase = curr->first; - currData.clear(); - } - auto* func = curr->second; - currData.push_back( - Builder(secondary).makeRefFunc(func->name, func->type)); - } - if (currData.size()) { - finishSegment(); - } - } -} - void ModuleSplitter::shareImportableItems() { // Map internal names to (one of) their corresponding export names. Don't // consider functions because they have already been imported and exported as @@ -947,278 +643,582 @@ void ModuleSplitter::shareImportableItems() { } \ } -#include "wasm-delegations-fields.def" +#include "wasm-delegations-fields.def" + } + }; + + // Given a module, collect names used in the module + auto getUsedNames = [&](Module& module) { + UsedNames used; + ModuleUtils::ParallelFunctionAnalysis nameCollector( + module, [&](Function* func, UsedNames& used) { + if (!func->imported()) { + NameCollector(used).walk(func->body); + } + }); + + for (auto& [_, funcUsed] : nameCollector.map) { + used.globals.insert(funcUsed.globals.begin(), funcUsed.globals.end()); + used.memories.insert(funcUsed.memories.begin(), funcUsed.memories.end()); + used.tables.insert(funcUsed.tables.begin(), funcUsed.tables.end()); + used.tags.insert(funcUsed.tags.begin(), funcUsed.tags.end()); + } + + NameCollector collector(used); + // We shouldn't use collector.walkModuleCode here, because we don't want to + // walk global initializers. At this point, all globals are still in the + // primary module, so if we walk global initializers here, other globals + // appearing in their initializers will all be marked as used in the primary + // module, which is not what we want. + // + // For example, we have (global $a i32 (global.get $b)). Because $a is at + // this point still in the primary module, $b will be marked as "used" in + // the primary module. But $a can be moved to a secondary module later if it + // is used exclusively by that module. Then $b can be also moved, in case it + // doesn't have other uses. But if it is marked as "used" in the primary + // module, it can't. + walkSegments(collector, &module); + for (auto& segment : module.dataSegments) { + if (segment->memory.is()) { + used.memories.insert(segment->memory); + } + } + for (auto& segment : module.elementSegments) { + if (segment->table.is()) { + used.tables.insert(segment->table); + } + } + + // If primary module has exports, they are "used" in it. Secondary modules + // don't have exports, so this only applies to the primary module. + for (auto& ex : module.exports) { + switch (ex->kind) { + case ExternalKind::Global: + used.globals.insert(*ex->getInternalName()); + break; + case ExternalKind::Memory: + used.memories.insert(*ex->getInternalName()); + break; + case ExternalKind::Table: + used.tables.insert(*ex->getInternalName()); + break; + case ExternalKind::Tag: + used.tags.insert(*ex->getInternalName()); + break; + default: + break; + } + } + return used; + }; + + UsedNames primaryUsed = getUsedNames(primary); + std::vector secondaryUsed; + for (auto& secondaryPtr : secondaries) { + secondaryUsed.push_back(getUsedNames(*secondaryPtr)); + } + + // Compute the transitive closure of globals referenced in other globals' + // initializers. Since globals can reference other globals, we must ensure + // that if a global is used in a module, all its dependencies are also marked + // as used. + auto computeTransitiveGlobals = [&](UsedNames& used) { + UniqueNonrepeatingDeferredQueue worklist; + for (auto global : used.globals) { + worklist.push(global); + } + while (!worklist.empty()) { + Name name = worklist.pop(); + // At this point all globals are still in the primary module, so this + // exists + auto* global = primary.getGlobal(name); + if (!global->imported() && global->init) { + for (auto* get : FindAll(global->init).list) { + worklist.push(get->name); + used.globals.insert(get->name); + } + } + } + }; + + computeTransitiveGlobals(primaryUsed); + for (auto& used : secondaryUsed) { + computeTransitiveGlobals(used); + } + + // Given a name and module item kind, returns the list of secondary modules + // using that name + auto getUsingSecondaries = [&](const Name& name, auto UsedNames::* field) { + std::vector usingModules; + for (size_t i = 0; i < secondaries.size(); ++i) { + if ((secondaryUsed[i].*field).contains(name)) { + usingModules.push_back(secondaries[i].get()); + } + } + return usingModules; + }; + + // Share module items with secondary modules. + // 1. Only share an item with the modules that use it + // 2. If an item is used by only a single secondary module, move the item to + // that secondary module. If an item is used by multiple modules (including + // the primary and secondary modules), export the item from the primary and + // import it from the using secondary modules. + + std::vector memoriesToRemove; + for (auto& memory : primary.memories) { + auto usingSecondaries = + getUsingSecondaries(memory->name, &UsedNames::memories); + bool usedInPrimary = primaryUsed.memories.contains(memory->name); + + if (!usedInPrimary && usingSecondaries.size() == 1) { + auto* secondary = usingSecondaries[0]; + ModuleUtils::copyMemory(memory.get(), *secondary); + memoriesToRemove.push_back(memory->name); + } else { + for (auto* secondary : usingSecondaries) { + auto* secondaryMemory = + ModuleUtils::copyMemory(memory.get(), *secondary); + makeImportExport( + *memory, *secondaryMemory, "memory", ExternalKind::Memory); + } + } + } + for (auto& name : memoriesToRemove) { + primary.removeMemory(name); + } + + std::vector tablesToRemove; + for (auto& table : primary.tables) { + auto usingSecondaries = + getUsingSecondaries(table->name, &UsedNames::tables); + bool usedInPrimary = primaryUsed.tables.contains(table->name); + + if (!usedInPrimary && usingSecondaries.size() == 1) { + auto* secondary = usingSecondaries[0]; + // In case we copied this table to this secondary module in + // setupTablePatching(), !usedInPrimary can't be satisfied, because the + // primary module should have an element segment that refers to this + // table. + assert(!secondary->getTableOrNull(table->name)); + ModuleUtils::copyTable(table.get(), *secondary); + tablesToRemove.push_back(table->name); + } else { + for (auto* secondary : usingSecondaries) { + // 1. In case we copied this table to this secondary module in + // setupTablePatching(), secondary.getTableOrNull(table->name) is not + // null, and we need to import it. + // 2. As in the case with other module elements, if the table is used in + // the secondary module's instructions, we need to export it. + auto secondaryTable = secondary->getTableOrNull(table->name); + if (!secondaryTable) { + secondaryTable = ModuleUtils::copyTable(table.get(), *secondary); + } + makeImportExport(*table, *secondaryTable, "table", ExternalKind::Table); + } + } + } + for (auto& name : tablesToRemove) { + primary.removeTable(name); + } + + std::vector globalsToRemove; + for (auto& global : primary.globals) { + if (global->mutable_) { + assert(primary.features.hasMutableGlobals() && + "TODO: add wrapper functions for disallowed mutable globals"); + } + + auto usingSecondaries = + getUsingSecondaries(global->name, &UsedNames::globals); + bool inPrimary = primaryUsed.globals.contains(global->name); + + if (!inPrimary && usingSecondaries.empty()) { + // It's not used anywhere, so delete it. Unlike other unused module items + // (memories, tables, and tags) that can just sit in the primary module + // and later be DCE'ed by another pass, we should remove it here, because + // an unused global can contain an initializer that refers to another + // global that will be moved to a secondary module, like + // (global $unused i32 (global.get $a)) // $a is moved to a secondary + globalsToRemove.push_back(global->name); + + } else if (!inPrimary && usingSecondaries.size() == 1) { + // We are moving this global to this secondary module + auto* secondary = usingSecondaries[0]; + auto* secondaryGlobal = ModuleUtils::copyGlobal(global.get(), *secondary); + globalsToRemove.push_back(global->name); + + if (secondaryGlobal->init) { + // When a global's initializer contains ref.func + for (auto* ref : FindAll(secondaryGlobal->init).list) { + // If ref.func's function is in a different secondary module, we + // create a trampoline here. + if (auto targetIndexIt = funcToSecondaryIndex.find(ref->func); + targetIndexIt != funcToSecondaryIndex.end()) { + if (secondaries[targetIndexIt->second].get() != secondary) { + ref->func = getTrampoline(ref->func); + } + } + // 1. If ref.func's function is in the primary module, we export it + // here. + // 2. If ref.func's function is in a different secondary module and we + // just created a trampoline for it in the primary module above, we + // export the trampoline here. + if (primary.getFunctionOrNull(ref->func)) { + exportImportFunction(ref->func, {secondary}); + } + // If ref.func's function is in the same secondary module, we don't + // need to do anything. The ref.func can directly reference the + // function. + } + } + + } else { // We are NOT moving this global to the secondary module + if (global->init) { + for (auto* ref : FindAll(global->init).list) { + // If we are exporting this global from the primary module, we should + // create a trampoline here, because we skipped doing it for global + // initializers in indirectReferencesToSecondaryFunctions. + if (allSecondaryFuncs.contains(ref->func)) { + ref->func = getTrampoline(ref->func); + } + } + } + + for (auto* secondary : usingSecondaries) { + auto* secondaryGlobal = + ModuleUtils::copyGlobal(global.get(), *secondary); + makeImportExport( + *global, *secondaryGlobal, "global", ExternalKind::Global); + } + } + } + for (auto& name : globalsToRemove) { + primary.removeGlobal(name); + } + + std::vector tagsToRemove; + for (auto& tag : primary.tags) { + auto usingSecondaries = getUsingSecondaries(tag->name, &UsedNames::tags); + bool usedInPrimary = primaryUsed.tags.contains(tag->name); + + if (!usedInPrimary && usingSecondaries.size() == 1) { + auto* secondary = usingSecondaries[0]; + ModuleUtils::copyTag(tag.get(), *secondary); + tagsToRemove.push_back(tag->name); + } else { + for (auto* secondary : usingSecondaries) { + auto* secondaryTag = ModuleUtils::copyTag(tag.get(), *secondary); + makeImportExport(*tag, *secondaryTag, "tag", ExternalKind::Tag); + } } - }; + } + for (auto& name : tagsToRemove) { + primary.removeTag(name); + } +} - // Given a module, collect names used in the module - auto getUsedNames = [&](Module& module) { - UsedNames used; - ModuleUtils::ParallelFunctionAnalysis nameCollector( - module, [&](Function* func, UsedNames& used) { - if (!func->imported()) { - NameCollector(used).walk(func->body); - } - }); +void ModuleSplitter::indirectReferencesToSecondaryFunctions() { + // Turn references to secondary functions into references to thunks that + // perform a direct call to the original referent. The direct calls in the + // thunks will be handled like all other cross-module calls later, in + // |indirectCallsToSecondaryFunctions|. + struct Gatherer : public PostWalker { + ModuleSplitter& parent; - for (auto& [_, funcUsed] : nameCollector.map) { - used.globals.insert(funcUsed.globals.begin(), funcUsed.globals.end()); - used.memories.insert(funcUsed.memories.begin(), funcUsed.memories.end()); - used.tables.insert(funcUsed.tables.begin(), funcUsed.tables.end()); - used.tags.insert(funcUsed.tags.begin(), funcUsed.tags.end()); - } + Gatherer(ModuleSplitter& parent) : parent(parent) {} - NameCollector collector(used); - // We shouldn't use collector.walkModuleCode here, because we don't want to - // walk global initializers. At this point, all globals are still in the - // primary module, so if we walk global initializers here, other globals - // appearing in their initializers will all be marked as used in the primary - // module, which is not what we want. - // - // For example, we have (global $a i32 (global.get $b)). Because $a is at - // this point still in the primary module, $b will be marked as "used" in - // the primary module. But $a can be moved to a secondary module later if it - // is used exclusively by that module. Then $b can be also moved, in case it - // doesn't have other uses. But if it is marked as "used" in the primary - // module, it can't. - walkSegments(collector, &module); - for (auto& segment : module.dataSegments) { - if (segment->memory.is()) { - used.memories.insert(segment->memory); + // Collect RefFuncs in a map from the function name to all RefFuncs that + // refer to it. We only collect this for secondary funcs. + InsertOrderedMap> map; + + void visitRefFunc(RefFunc* curr) { + Module* currModule = getModule(); + // Add ref.func to the map when + // 1. ref.func's target func is in one of the secondary modules and + // 2. the current module is a different module (either the primary module + // or a different secondary module) + if (parent.allSecondaryFuncs.contains(curr->func) && + (currModule == &parent.primary || + parent.secondaries.at(parent.funcToSecondaryIndex.at(curr->func)) + .get() != currModule)) { + map[curr->func].push_back(curr); } } - for (auto& segment : module.elementSegments) { - if (segment->table.is()) { - used.tables.insert(segment->table); - } + } gatherer(*this); + // We shouldn't use collector.walkModuleCode here, because we don't want to + // walk global initializers. At this point, all globals are still in the + // primary module, so if we walk global initializers here, it will create + // unnecessary trampolines. + // + // For example, we have (global $a funcref (ref.func $foo)), and $foo was + // split into a secondary module. Because $a is at this point still in the + // primary module, $foo will be considered to exist in a different module, so + // this will create a trampoline for $foo. But it is possible that later we + // find out $a is exclusively used by that secondary module and move $a there. + // In that case, $a can just reference $foo locally, but if we scan global + // initializers here, we would have created an unnecessary trampoline for + // $foo. + walkSegments(gatherer, &primary); + for (auto& curr : primary.functions) { + if (!curr->imported()) { + gatherer.walkFunction(curr.get()); } + } + for (auto& secondaryPtr : secondaries) { + gatherer.walkModule(secondaryPtr.get()); + } - // If primary module has exports, they are "used" in it. Secondary modules - // don't have exports, so this only applies to the primary module. - for (auto& ex : module.exports) { - switch (ex->kind) { - case ExternalKind::Global: - used.globals.insert(*ex->getInternalName()); - break; - case ExternalKind::Memory: - used.memories.insert(*ex->getInternalName()); - break; - case ExternalKind::Table: - used.tables.insert(*ex->getInternalName()); - break; - case ExternalKind::Tag: - used.tags.insert(*ex->getInternalName()); - break; - default: - break; + // Ignore references to secondary functions that occur in the active segment + // that will contain the imported placeholders. Indirect calls to table slots + // initialized by that segment will already go to the right place once the + // secondary module has been loaded and the table has been patched. + std::unordered_set ignore; + if (tableManager.activeSegment) { + for (auto* expr : tableManager.activeSegment->data) { + if (auto* ref = expr->dynCast()) { + ignore.insert(ref); } } - return used; - }; - - UsedNames primaryUsed = getUsedNames(primary); - std::vector secondaryUsed; - for (auto& secondaryPtr : secondaries) { - secondaryUsed.push_back(getUsedNames(*secondaryPtr)); } - // Compute the transitive closure of globals referenced in other globals' - // initializers. Since globals can reference other globals, we must ensure - // that if a global is used in a module, all its dependencies are also marked - // as used. - auto computeTransitiveGlobals = [&](UsedNames& used) { - UniqueNonrepeatingDeferredQueue worklist; - for (auto global : used.globals) { - worklist.push(global); - } - while (!worklist.empty()) { - Name name = worklist.pop(); - // At this point all globals are still in the primary module, so this - // exists - auto* global = primary.getGlobal(name); - if (!global->imported() && global->init) { - for (auto* get : FindAll(global->init).list) { - worklist.push(get->name); - used.globals.insert(get->name); - } + // Fix up what we found: Generate trampolines as described earlier, and apply + // them. + Builder builder(primary); + // Generate the new trampoline function and add it to the module. + for (auto& [name, refFuncs] : gatherer.map) { + // Find the relevant (non-ignored) RefFuncs. If there are none, we can skip + // creating a thunk entirely. + std::vector relevantRefFuncs; + for (auto* refFunc : refFuncs) { + assert(refFunc->func == name); + if (!ignore.contains(refFunc)) { + relevantRefFuncs.push_back(refFunc); } } - }; + if (relevantRefFuncs.empty()) { + continue; + } - computeTransitiveGlobals(primaryUsed); - for (auto& used : secondaryUsed) { - computeTransitiveGlobals(used); + Name trampoline = getTrampoline(name); + // Update RefFuncs to refer to it. + for (auto* refFunc : relevantRefFuncs) { + refFunc->func = trampoline; + } } +} - // Given a name and module item kind, returns the list of secondary modules - // using that name - auto getUsingSecondaries = [&](const Name& name, auto UsedNames::* field) { - std::vector usingModules; - for (size_t i = 0; i < secondaries.size(); ++i) { - if ((secondaryUsed[i].*field).contains(name)) { - usingModules.push_back(secondaries[i].get()); +void ModuleSplitter::indirectCallsToSecondaryFunctions() { + // Update direct calls of secondary functions to be indirect calls of their + // corresponding table indices instead. + struct CallIndirector : public PostWalker { + ModuleSplitter& parent; + CallIndirector(ModuleSplitter& parent) : parent(parent) {} + void visitCall(Call* curr) { + // Return if the call's target is not in one of the secondary module. + if (!parent.allSecondaryFuncs.contains(curr->target)) { + return; + } + // Return if the current module is the same module as the call's target, + // because we don't need a call_indirect within the same module. + Module* currModule = getModule(); + if (currModule != &parent.primary && + parent.secondaries.at(parent.funcToSecondaryIndex.at(curr->target)) + .get() == currModule) { + return; } + + Builder builder(*getModule()); + Index secIndex = parent.funcToSecondaryIndex.at(curr->target); + auto* func = parent.secondaries.at(secIndex)->getFunction(curr->target); + auto tableSlot = + parent.tableManager.getSlot(curr->target, func->type.getHeapType()); + + replaceCurrent( + builder.makeCallIndirect(tableSlot.tableName, + tableSlot.makeExpr(parent.primary), + curr->operands, + func->type.getHeapType(), + curr->isReturn)); } - return usingModules; }; + CallIndirector callIndirector(*this); + callIndirector.walkModule(&primary); + for (auto& secondaryPtr : secondaries) { + callIndirector.walkModule(secondaryPtr.get()); + } +} - // Share module items with secondary modules. - // 1. Only share an item with the modules that use it - // 2. If an item is used by only a single secondary module, move the item to - // that secondary module. If an item is used by multiple modules (including - // the primary and secondary modules), export the item from the primary and - // import it from the using secondary modules. +void ModuleSplitter::exportImportCalledPrimaryFunctions() { + // Find primary functions called/referred to from the secondary modules. + using CalledPrimaryToModules = std::map>; + for (auto& secondaryPtr : secondaries) { + Module* secondary = secondaryPtr.get(); + ModuleUtils::ParallelFunctionAnalysis callCollector( + *secondary, + [&](Function* func, CalledPrimaryToModules& calledPrimaryToModules) { + struct CallCollector : PostWalker { + const std::unordered_set& primaryFuncs; + CalledPrimaryToModules& calledPrimaryToModules; + CallCollector(const std::unordered_set& primaryFuncs, + CalledPrimaryToModules& calledPrimaryToModules) + : primaryFuncs(primaryFuncs), + calledPrimaryToModules(calledPrimaryToModules) {} + void visitCall(Call* curr) { + if (primaryFuncs.contains(curr->target)) { + calledPrimaryToModules[curr->target].insert(getModule()); + } + } + void visitRefFunc(RefFunc* curr) { + if (primaryFuncs.contains(curr->func)) { + calledPrimaryToModules[curr->func].insert(getModule()); + } + } + }; + CallCollector(primaryFuncs, calledPrimaryToModules) + .walkFunctionInModule(func, secondary); + }); - std::vector memoriesToRemove; - for (auto& memory : primary.memories) { - auto usingSecondaries = - getUsingSecondaries(memory->name, &UsedNames::memories); - bool usedInPrimary = primaryUsed.memories.contains(memory->name); + CalledPrimaryToModules calledPrimaryToModules; + for (auto& [_, map] : callCollector.map) { + calledPrimaryToModules.merge(map); + } - if (!usedInPrimary && usingSecondaries.size() == 1) { - auto* secondary = usingSecondaries[0]; - ModuleUtils::copyMemory(memory.get(), *secondary); - memoriesToRemove.push_back(memory->name); - } else { - for (auto* secondary : usingSecondaries) { - auto* secondaryMemory = - ModuleUtils::copyMemory(memory.get(), *secondary); - makeImportExport( - *memory, *secondaryMemory, "memory", ExternalKind::Memory); - } + // Ensure each called primary function is exported and imported + for (auto& [func, modules] : calledPrimaryToModules) { + exportImportFunction(func, modules); } } - for (auto& name : memoriesToRemove) { - primary.removeMemory(name); - } +} - std::vector tablesToRemove; - for (auto& table : primary.tables) { - auto usingSecondaries = - getUsingSecondaries(table->name, &UsedNames::tables); - bool usedInPrimary = primaryUsed.tables.contains(table->name); +void ModuleSplitter::setupTablePatching() { + if (!tableManager.activeTable) { + return; + } - if (!usedInPrimary && usingSecondaries.size() == 1) { - auto* secondary = usingSecondaries[0]; - // In case we copied this table to this secondary module in - // setupTablePatching(), !usedInPrimary can't be satisfied, because the - // primary module should have an element segment that refers to this - // table. - assert(!secondary->getTableOrNull(table->name)); - ModuleUtils::copyTable(table.get(), *secondary); - tablesToRemove.push_back(table->name); - } else { - for (auto* secondary : usingSecondaries) { - // 1. In case we copied this table to this secondary module in - // setupTablePatching(), secondary.getTableOrNull(table->name) is not - // null, and we need to import it. - // 2. As in the case with other module elements, if the table is used in - // the secondary module's instructions, we need to export it. - auto secondaryTable = secondary->getTableOrNull(table->name); - if (!secondaryTable) { - secondaryTable = ModuleUtils::copyTable(table.get(), *secondary); - } - makeImportExport(*table, *secondaryTable, "table", ExternalKind::Table); + std::map> moduleToReplacedElems; + // Replace table references to secondary functions with an imported + // placeholder that encodes the table index in its name: + // `importNamespace`.`index`. + forEachElement( + primary, [&](Name table, Name, Index index, Expression*& elem) { + auto* ref = elem->dynCast(); + if (!ref) { + return; } - } - } - for (auto& name : tablesToRemove) { - primary.removeTable(name); - } + if (!allSecondaryFuncs.contains(ref->func)) { + return; + } + assert(table == tableManager.activeTable->name); - std::vector globalsToRemove; - for (auto& global : primary.globals) { - if (global->mutable_) { - assert(primary.features.hasMutableGlobals() && - "TODO: add wrapper functions for disallowed mutable globals"); - } + placeholderMap[table][index] = ref->func; + Index secondaryIndex = funcToSecondaryIndex.at(ref->func); + Module& secondary = *secondaries.at(secondaryIndex); + Name secondaryName = config.secondaryNames.at(secondaryIndex); + auto* secondaryFunc = secondary.getFunction(ref->func); + moduleToReplacedElems[&secondary][index] = secondaryFunc; + if (!config.usePlaceholders) { + // TODO: This can create active element segments with lots of nulls. We + // should optimize them like we do data segments with zeros. + elem = Builder(primary).makeRefNull(HeapType::nofunc); + return; + } + auto placeholder = std::make_unique(); + placeholder->module = config.placeholderNamespacePrefix.toString() + "." + + secondaryName.toString(); + placeholder->base = std::to_string(index); + placeholder->name = Names::getValidFunctionName( + primary, std::string("placeholder_") + placeholder->base.toString()); + placeholder->hasExplicitName = true; + placeholder->type = secondaryFunc->type.with(Inexact); + elem = Builder(primary).makeRefFunc(placeholder->name, placeholder->type); + primary.addFunction(std::move(placeholder)); + }); - auto usingSecondaries = - getUsingSecondaries(global->name, &UsedNames::globals); - bool inPrimary = primaryUsed.globals.contains(global->name); + if (moduleToReplacedElems.size() == 0) { + // No placeholders to patch out of the table + return; + } - if (!inPrimary && usingSecondaries.empty()) { - // It's not used anywhere, so delete it. Unlike other unused module items - // (memories, tables, and tags) that can just sit in the primary module - // and later be DCE'ed by another pass, we should remove it here, because - // an unused global can contain an initializer that refers to another - // global that will be moved to a secondary module, like - // (global $unused i32 (global.get $a)) // $a is moved to a secondary - globalsToRemove.push_back(global->name); + for (auto& [secondaryPtr, replacedElems] : moduleToReplacedElems) { + Module& secondary = *secondaryPtr; + auto secondaryTable = + ModuleUtils::copyTable(tableManager.activeTable, secondary); - } else if (!inPrimary && usingSecondaries.size() == 1) { - // We are moving this global to this secondary module - auto* secondary = usingSecondaries[0]; - auto* secondaryGlobal = ModuleUtils::copyGlobal(global.get(), *secondary); - globalsToRemove.push_back(global->name); + if (tableManager.activeBase.global.size()) { + assert(tableManager.activeTableSegments.size() == 1 && + "Unexpected number of segments with non-const base"); + assert(secondary.tables.size() == 1 && secondary.elementSegments.empty()); + // Since addition is not currently allowed in initializer expressions, we + // need to start the new secondary segment where the primary segment + // starts. The secondary segment will contain the same primary functions + // as the primary module except in positions where it needs to overwrite a + // placeholder function. All primary functions in the table therefore need + // to be imported into the second module. TODO: use better strategies + // here, such as using ref.func in the start function or standardizing + // addition in initializer expressions. + ElementSegment* primarySeg = tableManager.activeTableSegments.front(); + std::vector secondaryElems; + secondaryElems.reserve(primarySeg->data.size()); - if (secondaryGlobal->init) { - // When a global's initializer contains ref.func - for (auto* ref : FindAll(secondaryGlobal->init).list) { - // If ref.func's function is in a different secondary module, we - // create a trampoline here. - if (auto targetIndexIt = funcToSecondaryIndex.find(ref->func); - targetIndexIt != funcToSecondaryIndex.end()) { - if (secondaries[targetIndexIt->second].get() != secondary) { - ref->func = getTrampoline(ref->func); - } - } - // 1. If ref.func's function is in the primary module, we export it - // here. - // 2. If ref.func's function is in a different secondary module and we - // just created a trampoline for it in the primary module above, we - // export the trampoline here. - if (primary.getFunctionOrNull(ref->func)) { - exportImportFunction(ref->func, {secondary}); - } - // If ref.func's function is in the same secondary module, we don't - // need to do anything. The ref.func can directly reference the + // Copy functions from the primary segment to the secondary segment, + // replacing placeholders and creating new exports and imports as + // necessary. + auto replacement = replacedElems.begin(); + for (Index i = 0; + i < primarySeg->data.size() && replacement != replacedElems.end(); + ++i) { + if (replacement->first == i) { + // primarySeg->data[i] is a placeholder, so use the secondary // function. + auto* func = replacement->second; + auto* ref = Builder(secondary).makeRefFunc(func->name, func->type); + secondaryElems.push_back(ref); + ++replacement; + } else if (auto* get = primarySeg->data[i]->dynCast()) { + exportImportFunction(get->func, {&secondary}); + auto* copied = + ExpressionManipulator::copy(primarySeg->data[i], secondary); + secondaryElems.push_back(copied); } } - } else { // We are NOT moving this global to the secondary module - if (global->init) { - for (auto* ref : FindAll(global->init).list) { - // If we are exporting this global from the primary module, we should - // create a trampoline here, because we skipped doing it for global - // initializers in indirectReferencesToSecondaryFunctions. - if (allSecondaryFuncs.contains(ref->func)) { - ref->func = getTrampoline(ref->func); - } - } - } - - for (auto* secondary : usingSecondaries) { - auto* secondaryGlobal = - ModuleUtils::copyGlobal(global.get(), *secondary); - makeImportExport( - *global, *secondaryGlobal, "global", ExternalKind::Global); - } + auto offset = ExpressionManipulator::copy(primarySeg->offset, secondary); + auto secondarySeg = std::make_unique( + secondaryTable->name, offset, secondaryTable->type, secondaryElems); + secondarySeg->setName(primarySeg->name, primarySeg->hasExplicitName); + secondary.addElementSegment(std::move(secondarySeg)); + return; } - } - for (auto& name : globalsToRemove) { - primary.removeGlobal(name); - } - - std::vector tagsToRemove; - for (auto& tag : primary.tags) { - auto usingSecondaries = getUsingSecondaries(tag->name, &UsedNames::tags); - bool usedInPrimary = primaryUsed.tags.contains(tag->name); - if (!usedInPrimary && usingSecondaries.size() == 1) { - auto* secondary = usingSecondaries[0]; - ModuleUtils::copyTag(tag.get(), *secondary); - tagsToRemove.push_back(tag->name); - } else { - for (auto* secondary : usingSecondaries) { - auto* secondaryTag = ModuleUtils::copyTag(tag.get(), *secondary); - makeImportExport(*tag, *secondaryTag, "tag", ExternalKind::Tag); + // Create active table segments in the secondary module to patch in the + // original functions when it is instantiated. + Index currBase = replacedElems.begin()->first; + std::vector currData; + auto finishSegment = [&]() { + auto* offset = Builder(secondary).makeConst( + Literal::makeFromInt32(currBase, secondaryTable->addressType)); + auto secondarySeg = std::make_unique( + secondaryTable->name, offset, secondaryTable->type, currData); + Name name = Names::getValidElementSegmentName( + secondary, Name::fromInt(secondary.elementSegments.size())); + secondarySeg->setName(name, false); + secondary.addElementSegment(std::move(secondarySeg)); + }; + for (auto curr = replacedElems.begin(); curr != replacedElems.end(); + ++curr) { + if (curr->first != currBase + currData.size()) { + finishSegment(); + currBase = curr->first; + currData.clear(); } + auto* func = curr->second; + currData.push_back( + Builder(secondary).makeRefFunc(func->name, func->type)); + } + if (currData.size()) { + finishSegment(); } - } - for (auto& name : tagsToRemove) { - primary.removeTag(name); } } From 36b703354301c89e7cb3bc6396a572562746669d Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 12 May 2026 09:31:26 -0700 Subject: [PATCH 094/168] Testing: Add verbose logging feature, and stop excessive spec logging (#8684) --- check.py | 6 +++--- scripts/test/shared.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/check.py b/check.py index 3bf3bafcf5b..d1e1f49103a 100755 --- a/check.py +++ b/check.py @@ -192,7 +192,7 @@ def run_opt_test(wast, stdout=None): def check_expected(actual, expected, stdout=None): if expected and os.path.exists(expected): expected = open(expected).read() - print(' (using expected output)', file=stdout) + shared.verbose_log(' (using expected output)', file=stdout) actual = actual.strip() expected = expected.strip() if actual != expected: @@ -229,7 +229,7 @@ def run_one_spec_test(wast: Path, stdout=None): actual = run_spec_test(str(wast), stdout=stdout) except Exception as e: if ('wasm-validator error' in str(e) or 'error: ' in str(e)) and '.fail.' in test_name: - print('<< test failed as expected >>', file=stdout) + shared.verbose_log('<< test failed as expected >>', file=stdout) return # don't try all the binary format stuff TODO else: shared.fail_with_error(str(e)) @@ -249,7 +249,7 @@ def run_one_spec_test(wast: Path, stdout=None): if not module: # Skip any initial assertions that don't have a module continue - print(f' testing split module {i}', file=stdout) + shared.verbose_log(f' testing split module {i}', file=stdout) split_name = base_name + f'_split{i}.wast' support.write_wast(split_name, module) run_opt_test(split_name, stdout=stdout) # also that our optimizer doesn't break on it diff --git a/scripts/test/shared.py b/scripts/test/shared.py index 4786bf2c755..d8211d8143a 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -97,6 +97,9 @@ def parse_args(args): action='store_false', default=True, help='Disables the automatic selection of important initial contents ' 'in fuzzer.') + parser.add_argument( + '--verbose', action='store_true', default=False, + help='Enables verbose logging.') return parser.parse_args(args) @@ -118,6 +121,11 @@ def print_heading(msg): print(f'[ {msg} ]') +def verbose_log(*args, **kwargs): + if options.verbose: + print(*args, **kwargs) + + # setup # Locate Binaryen build artifacts directory (bin/ by default) @@ -472,16 +480,16 @@ def binary_format_check(wast, verify_final_result=True, base_name=None, stdout=N as_file = f"{base_name}-a.wasm" if base_name is not None else "a.wasm" disassembled_file = f"{base_name}-ab.wast" if base_name is not None else "ab.wast" - print(' (binary format check)', file=stdout) + verbose_log(' (binary format check)', file=stdout) cmd = WASM_AS + [wast, '-o', as_file, '-all', '-g'] - print(' ', ' '.join(cmd), file=stdout) + verbose_log(' ', ' '.join(cmd), file=stdout) if os.path.exists(as_file): os.unlink(as_file) subprocess.check_call(cmd, stdout=subprocess.PIPE) assert os.path.exists(as_file) cmd = WASM_DIS + [as_file, '-o', disassembled_file, '-all'] - print(' ', ' '.join(cmd), file=stdout) + verbose_log(' ', ' '.join(cmd), file=stdout) if os.path.exists(disassembled_file): os.unlink(disassembled_file) subprocess.check_call(cmd, stdout=subprocess.PIPE) @@ -489,7 +497,7 @@ def binary_format_check(wast, verify_final_result=True, base_name=None, stdout=N # make sure it is a valid wast cmd = WASM_OPT + [disassembled_file, '-all', '-q'] - print(' ', ' '.join(cmd), file=stdout) + verbose_log(' ', ' '.join(cmd), file=stdout) subprocess.check_call(cmd, stdout=subprocess.PIPE) if verify_final_result: From 2f1a08b8f84282f401be1ad406904355150b5ac3 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 13 May 2026 16:15:16 -0700 Subject: [PATCH 095/168] Fuzzer: Do not emit calls to table.get when trivialNesting is set (#8700) `trivialNesting` means we were asked to emit something as trivial as possible. The other option in this code, to emit a `ref.func` for the `funcref` type, is far simpler. --- src/tools/fuzzing/fuzzing.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index 7dc4a9051f7..91531904a0d 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -4143,8 +4143,8 @@ Expression* TranslateToFuzzReader::makeBasicRef(Type type) { case HeapType::func: { // Rarely, emit a call to imported table.get (when nullable, unshared, and // where we can emit a call). - if (type.isNullable() && share == Unshared && funcContext && - tableGetImportName && !oneIn(3)) { + if (!trivialNesting && type.isNullable() && share == Unshared && + funcContext && tableGetImportName && !oneIn(3)) { return makeImportTableGet(); } return makeRefFuncConst(type); From 66c5d775459849b8d69204df95c92f768c7ee125 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 13 May 2026 17:07:57 -0700 Subject: [PATCH 096/168] [wasm-merge] Handle merging of start functions with control flow (#8697) When the merged modules both had start functions, we'd inline one into the other. However, we didn't use the full inlining logic with handles control flow, so a return in the code caused a problem: ```wat (func $start-a A (return) ) (func $start-b B ) ;; naively-merged starts (func $merged A (return) ;; this ends up skipping B B ) ``` As a fix, avoid attempts to be clever and just make a new function with two calls to the starts, in order. Fixes #8692 --- src/tools/wasm-merge.cpp | 68 +++++++++++++++++--------- test/lit/merge/start-return.wat | 39 +++++++++++++++ test/lit/merge/start-return.wat.second | 10 ++++ test/lit/merge/start3.wat | 22 +++------ 4 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 test/lit/merge/start-return.wat create mode 100644 test/lit/merge/start-return.wat.second diff --git a/src/tools/wasm-merge.cpp b/src/tools/wasm-merge.cpp index 2197bc27356..66800ed08ab 100644 --- a/src/tools/wasm-merge.cpp +++ b/src/tools/wasm-merge.cpp @@ -117,6 +117,11 @@ namespace { // have it as a global rather than pass it around all the time. Module merged; +// Everything we merge is accumulated into |merged|, aside from the start +// functions. To avoid incrementally adding a call each time, which can end up +// nested, we add them here and generate a series of flat calls at the end. +std::vector startFunctions; + // Name conflicts on functions etc. are resolved by renaming things in a way // that only matters internally. Conflicting export names, however, are // observable, and so the user must decide how they want wasm-merge to handle @@ -363,28 +368,9 @@ void copyModuleContents(Module& input, Name inputName) { merged.addExport(std::move(copy)); } - // Start functions must be merged. - if (input.start.is()) { - if (!merged.start.is()) { - // No previous start; just refer to the new one. - merged.start = input.start; - } else { - // Merge them, keeping the order. We copy both functions to avoid issues - // with other references to them, and just call the second one, leaving - // inlining to the optimizer if that makes sense to do. - auto copiedOldName = - Names::getValidFunctionName(merged, "merged.start.old"); - auto copiedNewName = - Names::getValidFunctionName(merged, "merged.start.new"); - auto* copiedOld = ModuleUtils::copyFunction( - merged.getFunction(merged.start), merged, copiedOldName); - ModuleUtils::copyFunction( - merged.getFunction(input.start), merged, copiedNewName); - Builder builder(merged); - copiedOld->body = builder.makeSequence( - copiedOld->body, builder.makeCall(copiedNewName, {}, Type::none)); - merged.start = copiedOldName; - } + // Start functions are accumulated till the end. + if (input.start) { + startFunctions.push_back(input.start); } // TODO: type names, features, debug info, custom sections, dylink info, etc. @@ -596,6 +582,34 @@ void updateTypes(Module& wasm) { updater.runOnModuleCode(&runner, &wasm); } +// Merge the start functions, keeping the order. We add a new function that +// calls them in sequence (leaving proper inlining, including handling of +// control flow etc., to the optimizer). +void mergeStartFunctions() { + if (startFunctions.empty()) { + return; + } + + if (startFunctions.size() == 1) { + // Avoid adding a call here. + merged.start = startFunctions[0]; + return; + } + + auto combinedName = + Names::getValidFunctionName(merged, "merged.start.combined"); + Builder builder(merged); + std::vector calls; + for (auto start : startFunctions) { + calls.push_back(builder.makeCall(start, {}, Type::none)); + } + auto* body = builder.makeBlock(calls); + auto combined = builder.makeFunction( + combinedName, Signature(Type::none, Type::none), {}, body); + merged.addFunction(std::move(combined)); + merged.start = combinedName; +} + // Merges an input module into an existing target module. The input module can // be modified, as it will no longer be needed (so it is intentionally not // marked as const here). @@ -794,6 +808,13 @@ Input source maps can be specified by adding an -ism option right after the modu for (auto& curr : merged.exports) { exportModuleMap[curr.get()] = ExportInfo{inputFileName, curr->name}; } + + // Start functions are accumulated till the end. + if (merged.start) { + startFunctions.push_back(merged.start); + merged.start = Name(); + } + } else { // This is a later module: do a full merge. mergeInto(*currModule, inputFileName); @@ -827,6 +848,9 @@ Input source maps can be specified by adding an -ism option right after the modu // Update types after combing and linking everything. updateTypes(merged); + // Merge the start functions, after everything else is set up. + mergeStartFunctions(); + { PassRunner passRunner(&merged); // We might have made some globals read from others that now appear after diff --git a/test/lit/merge/start-return.wat b/test/lit/merge/start-return.wat new file mode 100644 index 00000000000..3b6f8ea1cc4 --- /dev/null +++ b/test/lit/merge/start-return.wat @@ -0,0 +1,39 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: wasm-merge %s first %s.second second --rename-export-conflicts -all -S -o - | filecheck %s + +;; Test that we properly merge start functions with control flow. The two +;; start functions have returns, and a naive merge of their bodies would end up +;; skipping the second (after the first return). + +(module + (start $start-a) + + ;; CHECK: (type $0 (func)) + + ;; CHECK: (start $merged.start.combined) + + ;; CHECK: (func $start-a (type $0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + (func $start-a + (drop + (i32.const 0) + ) + (return) + ) +) + +;; CHECK: (func $start-b (type $0) +;; CHECK-NEXT: (drop +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (return) +;; CHECK-NEXT: ) + +;; CHECK: (func $merged.start.combined (type $0) +;; CHECK-NEXT: (call $start-a) +;; CHECK-NEXT: (call $start-b) +;; CHECK-NEXT: ) diff --git a/test/lit/merge/start-return.wat.second b/test/lit/merge/start-return.wat.second new file mode 100644 index 00000000000..040c0805162 --- /dev/null +++ b/test/lit/merge/start-return.wat.second @@ -0,0 +1,10 @@ +(module + (start $start-b) + + (func $start-b + (drop + (i32.const 1) + ) + (return) + ) +) diff --git a/test/lit/merge/start3.wat b/test/lit/merge/start3.wat index cba29737ca4..223ff2910f2 100644 --- a/test/lit/merge/start3.wat +++ b/test/lit/merge/start3.wat @@ -13,7 +13,7 @@ ;; CHECK: (export "user" (func $user)) -;; CHECK: (start $merged.start.old) +;; CHECK: (start $merged.start.combined) ;; CHECK: (func $start (type $0) ;; CHECK-NEXT: (local $x i32) @@ -30,20 +30,7 @@ ;; CHECK-NEXT: (call $start) ;; CHECK-NEXT: ) -;; CHECK: (func $merged.start.old (type $0) -;; CHECK-NEXT: (local $x i32) -;; CHECK-NEXT: (block -;; CHECK-NEXT: (drop -;; CHECK-NEXT: (local.get $x) -;; CHECK-NEXT: ) -;; CHECK-NEXT: (drop -;; CHECK-NEXT: (i32.const 1) -;; CHECK-NEXT: ) -;; CHECK-NEXT: ) -;; CHECK-NEXT: (call $merged.start.new) -;; CHECK-NEXT: ) - -;; CHECK: (func $merged.start.new (type $0) +;; CHECK: (func $start_2 (type $0) ;; CHECK-NEXT: (local $x f64) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $x) @@ -52,3 +39,8 @@ ;; CHECK-NEXT: (i32.const 2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + +;; CHECK: (func $merged.start.combined (type $0) +;; CHECK-NEXT: (call $start) +;; CHECK-NEXT: (call $start_2) +;; CHECK-NEXT: ) From a8df1c08bd807fb7308bf9163034b9023b0d6f95 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 13 May 2026 17:27:04 -0700 Subject: [PATCH 097/168] [wasm-reduce] Remove functions with delta debugging (#8690) Also take care not to remove functions that are referenced from module-level code, since in general those references cannot be replaced with anything else. When we remove functions, use ChildLocalizer to keep any children with side effects, since they might be important for reproducing the issue. Also try to replace calls to removed functions with constants if possible to avoid inserting unnecessary traps. Unlike the old implementation, this reduction scheme avoids wasted work by ensuring that the reduced module is still valid. --- src/tools/wasm-reduce/wasm-reduce.cpp | 263 +++++++++++++++----------- 1 file changed, 150 insertions(+), 113 deletions(-) diff --git a/src/tools/wasm-reduce/wasm-reduce.cpp b/src/tools/wasm-reduce/wasm-reduce.cpp index f9cd7b64412..bba5cda3cb1 100644 --- a/src/tools/wasm-reduce/wasm-reduce.cpp +++ b/src/tools/wasm-reduce/wasm-reduce.cpp @@ -29,6 +29,7 @@ #include "ir/branch-utils.h" #include "ir/iteration.h" +#include "ir/localize.h" #include "ir/properties.h" #include "ir/utils.h" #include "pass.h" @@ -930,7 +931,8 @@ struct Reducer } std::cerr << "| try partition " << dd.partitionIndex() + 1 << " / " - << dd.partitionCount() << " (size " << dd.test.size() << ")\n"; + << dd.partitionCount() << " (size " << dd.test.size() << " / " + << dd.working.size() << ")\n"; Index removedSize = dd.working.size() - dd.test.size(); std::vector oldBodies(removedSize); @@ -982,66 +984,160 @@ struct Reducer } } - bool reduceFunctions() { - // try to remove functions - std::vector functionNames; - for (auto& func : module->functions) { - functionNames.push_back(func->name); + void reduceFunctions() { + std::cerr << "| try to remove functions\n"; + + // Find functions referenced from module code (i.e. global initializers). We + // will not attempt to remove these functions because we cannot generally + // replace their references with something valid. + // TODO: Look at how the function references are used. If they can be + // nullable, we can still consider deleting the functions. + struct UnremovableFinder : public PostWalker { + std::unordered_set unremovable; + void visitRefFunc(RefFunc* curr) { unremovable.insert(curr->func); } + }; + UnremovableFinder finder; + finder.walkModuleCode(module.get()); + + // Find the indices of functions we can consider removing or must not + // remove. + std::vector unremovableIndices; + std::vector initialCandidates; + initialCandidates.reserve(module->functions.size() - + finder.unremovable.size()); + for (Index i = 0; i < module->functions.size(); ++i) { + if (finder.unremovable.contains(module->functions[i]->name)) { + unremovableIndices.push_back(i); + } else { + initialCandidates.push_back(i); + } } - auto numFuncs = functionNames.size(); - if (numFuncs == 0) { - return false; + + if (initialCandidates.empty()) { + return; } - uint64_t skip = 1; - uint64_t maxSkip = 1; - // If we just removed some functions in the previous iteration, keep trying - // to remove more as this is one of the most efficient ways to reduce. - bool justReduced = true; - // Start from a new place each time. - size_t base = deterministicRandom(numFuncs); - std::cerr << "| try to remove functions (base: " << base - << ", decisionCounter: " << decisionCounter << ", numFuncs " - << numFuncs << ")\n"; - for (size_t x = 0; x < functionNames.size(); x++) { - size_t i = (base + x) % numFuncs; - if (!justReduced && functionsWeTriedToRemove.contains(functionNames[i]) && - !shouldTryToReduce(std::max((factor / 5) + 1, uint64_t(20000)))) { - continue; + + // Indices will change as we remove functions. Map the original indices to + // the present indices so we can use the original indices as stable + // identifiers. (Function names are not necessarily preserved through + // round-tripping.) + std::vector> currentIndices; + currentIndices.reserve(module->functions.size()); + for (Index i = 0; i < module->functions.size(); ++i) { + currentIndices.push_back(i); + } + + DeltaDebugger dd(std::move(initialCandidates)); + while (!dd.finished()) { + // Exit early if the test set size is less than the square root of the + // working set size. We don't want to waste time on very fine-grained + // partitions when we could switch to a different reduction strategy + // instead. + if (size_t sqrtRemaining = std::sqrt(dd.working.size()); + dd.test.size() > 0 && dd.test.size() < sqrtRemaining) { + break; } - std::vector names; - for (size_t j = 0; names.size() < skip && i + j < functionNames.size(); - j++) { - auto name = functionNames[i + j]; - if (module->getFunctionOrNull(name)) { - names.push_back(name); - functionsWeTriedToRemove.insert(name); + + std::cerr << "| try partition " << dd.partitionIndex() + 1 << " / " + << dd.partitionCount() << " (size " << dd.test.size() << " / " + << dd.working.size() << ")\n"; + + std::unordered_set keptIndices; + for (Index i : unremovableIndices) { + keptIndices.insert(*currentIndices[i]); + } + for (Index i : dd.test) { + keptIndices.insert(*currentIndices[i]); + } + + // Get the list of kept functions and the new index mapping we will have + // to use if this reduction works. + std::vector> newFuncs; + newFuncs.reserve(keptIndices.size()); + std::vector> newCurrentIndices; + newCurrentIndices.reserve(currentIndices.size()); + for (size_t i = 0; i < currentIndices.size(); ++i) { + if (auto currIndex = currentIndices[i]; + currIndex && keptIndices.contains(*currIndex)) { + newCurrentIndices.push_back(newFuncs.size()); + newFuncs.emplace_back(std::move(module->functions[*currIndex])); + } else { + newCurrentIndices.push_back(std::nullopt); } } - if (names.size() == 0) { - continue; + + module->functions = std::move(newFuncs); + module->updateFunctionsMap(); + + // Remove exports for functions we have removed. + std::vector exportsToRemove; + for (auto& exp : module->exports) { + if (exp->kind == ExternalKind::Function && + !module->getFunctionOrNull(*exp->getInternalName())) { + exportsToRemove.push_back(exp->name); + } } - std::cerr << "| trying at i=" << i << " of size " << names.size() - << "\n"; - // Note that tryToRemoveFunctions() will reload the module if it fails, - // which means function names may change. - if (tryToRemoveFunctions(names)) { - noteReduction(names.size()); - // Subtract 1 since the loop increments us anyhow by one: we want to - // skip over the skipped functions, and not any more. - x += skip - 1; - skip = std::min(factor, 2 * skip); - maxSkip = std::max(skip, maxSkip); + for (auto expName : exportsToRemove) { + module->removeExport(expName); + } + + // We may have removed the start function. + if (module->start && !module->getFunctionOrNull(module->start)) { + module->start = Name(); + } + + struct FunctionReplacer + : public WalkerPass> { + bool isFunctionParallel() override { return true; } + bool requiresNonNullableLocalFixups() override { return false; } + std::unique_ptr create() override { + return std::make_unique(); + }; + void visitCall(Call* curr) { + // Replace calls to functions we have removed. + if (getModule()->getFunctionOrNull(curr->target)) { + return; + } + Builder builder(*getModule()); + auto* block = + ChildLocalizer(curr, getFunction(), *getModule(), getPassOptions()) + .getChildrenReplacement(); + auto* replacement = builder.replaceWithIdenticalType(curr); + // We may have failed to come up with a replacement (e.g. for + // non-nullable references), so manually add an `unreachable` in that + // case. + if (replacement == curr) { + replacement = builder.makeUnreachable(); + } + block->list.push_back(replacement); + block->type = curr->type; + replaceCurrent(block); + } + void visitRefFunc(RefFunc* curr) { + // Replace references to functions we have removed. + if (getModule()->getFunctionOrNull(curr->func)) { + return; + } + Builder builder(*getModule()); + replaceCurrent( + builder.makeBlock({builder.makeUnreachable()}, curr->type)); + } + }; + PassRunner runner(module.get()); + runner.add(std::make_unique()); + runner.run(); + + assert(WasmValidator().validate( + *module, WasmValidator::Globally | WasmValidator::Quiet)); + if (writeAndTestReduction()) { + noteReduction(dd.working.size() - dd.test.size()); + currentIndices = std::move(newCurrentIndices); + dd.accept(); } else { - skip = std::max(skip / 2, uint64_t(1)); // or 1? - x += factor / 100; + loadWorking(); + dd.reject(); } } - // If maxSkip is 1 then we never reduced at all. If it is 2 then we did - // manage to reduce individual functions, but all our attempts at - // exponential growth failed. Only suggest doing a new iteration of this - // function if we did in fact manage to grow, which indicated there are lots - // of opportunities here, and it is worth focusing on this. - return maxSkip > 2; } void visitModule([[maybe_unused]] Module* curr) { @@ -1052,12 +1148,7 @@ struct Reducer curr = nullptr; reduceFunctionBodies(); - - // Reduction of entire functions at a time is very effective, and we do it - // with exponential growth and backoff, so keep doing it while it works. - // TODO: Figure out how to use delta debugging for this as well. - while (reduceFunctions()) { - } + reduceFunctions(); shrinkElementSegments(); @@ -1134,61 +1225,6 @@ struct Reducer } } - // Try to actually remove functions. If they are somehow referred to, we will - // get a validation error and undo it. - bool tryToRemoveFunctions(std::vector names) { - for (auto name : names) { - module->removeFunction(name); - } - - // remove all references to them - struct FunctionReferenceRemover - : public PostWalker { - std::unordered_set names; - std::vector exportsToRemove; - - FunctionReferenceRemover(std::vector& vec) { - for (auto name : vec) { - names.insert(name); - } - } - void visitCall(Call* curr) { - if (names.contains(curr->target)) { - replaceCurrent(Builder(*getModule()).replaceWithIdenticalType(curr)); - } - } - void visitRefFunc(RefFunc* curr) { - if (names.contains(curr->func)) { - replaceCurrent(Builder(*getModule()).replaceWithIdenticalType(curr)); - } - } - void visitExport(Export* curr) { - if (auto* name = curr->getInternalName(); - name && names.contains(*name)) { - exportsToRemove.push_back(curr->name); - } - } - void doWalkModule(Module* module) { - PostWalker::doWalkModule(module); - for (auto name : exportsToRemove) { - module->removeExport(name); - } - } - }; - FunctionReferenceRemover referenceRemover(names); - referenceRemover.walkModule(module.get()); - - if (WasmValidator().validate( - *module, WasmValidator::Globally | WasmValidator::Quiet) && - writeAndTestReduction()) { - std::cerr << "| removed " << names.size() << " functions\n"; - return true; - } else { - loadWorking(); // restore it from orbit - return false; - } - } - // helpers // try to replace condition with always true and always false @@ -1567,6 +1603,7 @@ More documentation can be found at if (first) { reducer.loadWorking(); reducer.reduceFunctionBodies(); + reducer.reduceFunctions(); first = false; } From bb6ead889c9fd0bde9ad7717182d16d6e41e92a7 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 14 May 2026 08:28:41 -0700 Subject: [PATCH 098/168] execution-results: Handle a trap during start (#8699) Before, the entire process crashed on a thrown exception there, as it was not handled. --- src/tools/execution-results.h | 2 ++ test/lit/exec/start.wast | 55 +++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 test/lit/exec/start.wast diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index 09f7657ee96..0cda208792b 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -487,6 +487,8 @@ struct ExecutionResults { // This should be ignored and not compared with, as optimizations can // change whether a host limit is reached. ignore = true; + } catch (const WasmException&) { + std::cout << "[exception thrown: start]\n"; } } diff --git a/test/lit/exec/start.wast b/test/lit/exec/start.wast new file mode 100644 index 00000000000..edd11152421 --- /dev/null +++ b/test/lit/exec/start.wast @@ -0,0 +1,55 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --output=fuzz-exec and should not be edited. + +;; RUN: foreach %s %t wasm-opt -all --fuzz-exec-before | filecheck %s + +;; $start runs before the first export, so we print 1 here. +(module + (global $global (mut i32) (i32.const 0)) + + (start $start) + + (func $start + (global.set $global + (i32.const 1) + ) + ) + + ;; CHECK: [fuzz-exec] export run + ;; CHECK-NEXT: [fuzz-exec] note result: run => 1 + ;; CHECK-NEXT: [trap unreachable] + ;; CHECK-NEXT: [exception thrown: start] + (func $run (export "run") (result i32) + ;; Due to limitations of the auto-updater, the trap and exception from the + ;; following two modules gets logged here. (There is at least no + ;; ambiguity: we first see that we finished ok and returned a value.) + (global.get $global) + ) +) + +;; A trapping start prevents any export from running. +(module + (start $trap) + + (func $trap + (unreachable) + ) + + (func $run (export "run") (result i32) + (i32.const 42) + ) +) + +;; A throwing start prevents any export from running. +(module + (tag $tag) + + (start $throw) + + (func $throw + (throw $tag) + ) + + (func $run (export "run") (result i32) + (i32.const 42) + ) +) From 02f114bd79dde59a3185eac203eef6a872f0e34a Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 14 May 2026 12:58:03 -0700 Subject: [PATCH 099/168] JSON: Implement printing for all types (#8701) Also add minor quality-of-life improvements like nicer constructors. --- src/support/json.cpp | 93 +++++++++++++++++++++++++++++++++----------- src/support/json.h | 24 +++++++++++- test/gtest/json.cpp | 57 ++++++++++++++++++++++++++- 3 files changed, 149 insertions(+), 25 deletions(-) diff --git a/src/support/json.cpp b/src/support/json.cpp index ff393317410..94f3df082e7 100644 --- a/src/support/json.cpp +++ b/src/support/json.cpp @@ -19,30 +19,79 @@ namespace json { -void Value::stringify(std::ostream& os, bool pretty) { - if (isString()) { - std::stringstream wtf16; - [[maybe_unused]] bool valid = - wasm::String::convertWTF8ToWTF16(wtf16, getIString().view()); - assert(valid); - // TODO: Use wtf16.view() once we have C++20. - wasm::String::printEscapedJSON(os, wtf16.str()); - } else if (isArray()) { - os << '['; - auto first = true; - for (auto& item : getArray()) { - if (first) { - first = false; - } else { - // TODO pretty whitespace - os << ','; +void Value::stringify(std::ostream& os, bool pretty, int indent) { + auto doIndent = [&]() { + for (int i = 0; i < indent; i++) { + os << ' '; + } + }; + + auto maybeNewline = [&]() { + if (pretty) { + os << '\n'; + doIndent(); + } + }; + + switch (type) { + case String: { + std::stringstream wtf16; + [[maybe_unused]] bool valid = + wasm::String::convertWTF8ToWTF16(wtf16, getIString().view()); + assert(valid); + wasm::String::printEscapedJSON(os, wtf16.view()); + return; + } + case Array: { + os << '['; + indent++; + auto first = true; + for (auto& item : getArray()) { + if (first) { + first = false; + } else { + os << ','; + } + maybeNewline(); + item->stringify(os, pretty, indent); + } + indent--; + maybeNewline(); + os << ']'; + return; + } + case Object: { + os << '{'; + indent++; + auto first = true; + for (auto& [key, value] : getObject()) { + if (first) { + first = false; + } else { + os << ','; + } + maybeNewline(); + os << "\"" << key << "\":"; + if (pretty) { + os << ' '; + } + value->stringify(os, pretty, indent); } - item->stringify(os, pretty); + indent--; + maybeNewline(); + os << '}'; + return; } - os << ']'; - } else { - WASM_UNREACHABLE("TODO: stringify all of JSON"); - } + case Number: + os << getNumber(); + return; + case Null: + os << "null"; + return; + case Bool: + os << (getBool() ? "true" : "false"); + return; + }; } } // namespace json diff --git a/src/support/json.h b/src/support/json.h index 23d0749f045..98e46b063fd 100644 --- a/src/support/json.h +++ b/src/support/json.h @@ -37,6 +37,7 @@ #include #include +#include "support/insert_ordered.h" #include "support/istring.h" #include "support/safe_integer.h" #include "support/string.h" @@ -67,7 +68,18 @@ struct Value { Ref& operator[](IString x) { return (*this->get())[x]; } }; + static Ref make() { return Ref(new Value); } template static Ref make(T t) { return Ref(new Value(t)); } + static Ref makeArray() { + Ref ret(new Value); + ret->setArray(); + return ret; + } + static Ref makeObject() { + Ref ret(new Value); + ret->setObject(); + return ret; + } enum Type { String = 0, @@ -81,7 +93,7 @@ struct Value { Type type = Null; using ArrayStorage = std::vector; - using ObjectStorage = std::unordered_map; + using ObjectStorage = wasm::InsertOrderedMap; // MSVC does not allow unrestricted unions: // http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2544.pdf @@ -102,6 +114,10 @@ struct Value { // constructors all copy their input Value() {} explicit Value(const char* s) : type(Null) { setString(s); } + explicit Value(const std::string& s) : type(Null) { setString(s.c_str()); } + explicit Value(const std::string_view& s) : type(Null) { + setString(std::string(s)); + } explicit Value(double n) : type(Null) { setNumber(n); } explicit Value(ArrayStorage& a) : type(Null) { setArray(); @@ -202,6 +218,10 @@ struct Value { assert(isArray()); return *arr; } + ObjectStorage& getObject() { + assert(isObject()); + return *obj; + } bool& getBool() { assert(isBool()); return boo; @@ -378,7 +398,7 @@ struct Value { return curr; } - void stringify(std::ostream& os, bool pretty = false); + void stringify(std::ostream& os, bool pretty = false, int indent = 0); // String operations diff --git a/test/gtest/json.cpp b/test/gtest/json.cpp index 626861a626a..5cd737ac6df 100644 --- a/test/gtest/json.cpp +++ b/test/gtest/json.cpp @@ -3,7 +3,7 @@ using JSONTest = ::testing::Test; -TEST_F(JSONTest, Stringify) { +TEST_F(JSONTest, RoundtripString) { // TODO: change the API to not require a copy auto input = "[\"hello\",\"world\"]"; auto* copy = strdup(input); @@ -14,3 +14,58 @@ TEST_F(JSONTest, Stringify) { EXPECT_EQ(ss.str(), input); free(copy); } + +static void +checkOutput(json::Value::Ref ref, std::string expected, bool pretty = false) { + std::stringstream ss; + ref->stringify(ss, pretty); + EXPECT_EQ(ss.str(), expected); +} + +static void checkPrettyOutput(json::Value::Ref ref, std::string expected) { + checkOutput(ref, expected, true); +} + +TEST_F(JSONTest, StringifyArray) { + auto array = json::Value::makeArray(); + array->push_back(json::Value::make(42)); + array->push_back(json::Value::make("1337")); + array->push_back(json::Value::make()); // null + checkOutput(array, "[42,\"1337\",null]"); + checkPrettyOutput(array, R"([ + 42, + "1337", + null +])"); +} + +TEST_F(JSONTest, StringifyObject) { + auto object = json::Value::makeObject(); + object["foo"] = json::Value::make(42); + object["bar"] = json::Value::make("1337"); + checkOutput(object, "{\"foo\":42,\"bar\":\"1337\"}"); + checkPrettyOutput(object, R"({ + "foo": 42, + "bar": "1337" +})"); +} + +TEST_F(JSONTest, StringifyNesting) { + auto array = json::Value::makeArray(); + auto object = json::Value::makeObject(); + auto array1 = json::Value::makeArray(); + auto object1 = json::Value::makeObject(); + array->push_back(object); + object["body"] = array1; + array1->push_back(object1); + object1["value"] = json::Value::make(42); + checkPrettyOutput(array, R"([ + { + "body": [ + { + "value": 42 + } + ] + } +])"); +} From 2f1f55aef6d9adfa6fdc2c25e46d202232dbf6e2 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 14 May 2026 13:34:32 -0700 Subject: [PATCH 100/168] [wasm-split] Split module elements early (#8688) Before #8443, we scanned `ref.func`s in global initializers early in `indirectReferencesToSecondaryFunctions` and created trampolines for them and replaced `ref.func $func`s with `ref.func $trampoline_func` if `func` was set to move to a secondary module. But in case the global containing `ref.func $trampoline_func` also ends up moving to the same secondary module, creating trampoline and using it was not necessary, because the global can simply use `ref.func $func` because `func` is in the same secondary module. To fix this, in #8443, we postponed creating trampolines for `ref.func`s in global initializers until `shareImportableItems`. This had a problem, because we end up creating new trampolines late in `shareImportableItems`. But trampolines were designed to go through `indirectCallsToSecondaryFunctions` and `setupTablePatching`, so those late trampolines were invalid, like ```wast (func $trampoline_foo (call $foo) ) ``` when `foo` was in a secondary module. This was supposed to be converted to a `call_indirect` in `indirectCallsToSecondaryFunctions` and the table elements were supposed to set up in `setupTablePatching`. --- This moves `shareImportableItems` before `indirectReferencesToSecondaryFunctions`. Turns out, except for the active table and its base global, we can do all splitting before we make most of the changes related to splitting. This also simplifies `shareImportableItems` because we can now delete code handling the consequences of various transformations. Because the active table and its base global may not be registered as "used" in `shareImportableItems` before `setupTablePatching`, we make sure they are correctly shared with secondary modules in `setupTablePatching`. `active-table-base-global-used-elsewhere.wast` has some edge cases that I feel easy to miss, because now we specially handle the active table and the base global in `setupTablePatching`. `global-reffunc.wast` is the same but just renamed expectation rewritten. `global-reffunc2.wast` is the failing case simplified from #8510. Other test changes are just the changes in the creation order of module elements and not meaningful. Replaces #8542 and fixes #8510. --- scripts/fuzz_opt.py | 2 +- src/ir/module-splitting.cpp | 260 ++++++++---------- ...tive-table-base-global-used-elsewhere.wast | 63 +++++ test/lit/wasm-split/global-funcref.wast | 43 --- test/lit/wasm-split/global-reffunc.wast | 45 +++ test/lit/wasm-split/global-reffunc2.wast | 52 ++++ test/lit/wasm-split/ref.func.wast | 6 +- test/lit/wasm-split/split-module-items.wast | 8 +- 8 files changed, 290 insertions(+), 189 deletions(-) create mode 100644 test/lit/wasm-split/active-table-base-global-used-elsewhere.wast delete mode 100644 test/lit/wasm-split/global-funcref.wast create mode 100644 test/lit/wasm-split/global-reffunc.wast create mode 100644 test/lit/wasm-split/global-reffunc2.wast diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index c1a4d02c6d5..b089ec7f1f6 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2525,7 +2525,7 @@ def handle(self, wasm): TrapsNeverHappen(), CtorEval(), Merge(), - # Split(), # https://github.com/WebAssembly/binaryen/issues/8510 + Split(), RoundtripText(), ClusterFuzz(), Two(), diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 21d82d013a4..6ae18645128 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -26,28 +26,28 @@ // placeholder function (and eventually to the original secondary // function), allocating a new table slot for the placeholder if necessary. // -// 4. Replace all references to each secondary module's functions in the +// 4. Export globals, tags, tables, and memories from the primary module and +// import them in the secondary modules. If possible, move those module +// items instead to the secondary modules. +// +// 5. Replace all references to each secondary module's functions in the // primary module's and each other secondary module's table segments with // references to imported placeholder functions. // -// 5. Rewrite direct calls from primary functions to secondary functions to be +// 6. Rewrite direct calls from primary functions to secondary functions to be // indirect calls to their placeholder functions (and eventually to their // original secondary functions), allocating new table slots for the // placeholders if necessary. // -// 6. For each primary function directly called from a secondary function, +// 7. For each primary function directly called from a secondary function, // export the primary function if it is not already exported and import it // into each secondary module using it. // -// 7. For each secondary module, create new active table segments in the +// 8. For each secondary module, create new active table segments in the // module that will replace all the placeholder function references in the // table with references to their corresponding secondary functions upon // instantiation. // -// 8. Export globals, tags, tables, and memories from the primary module and -// import them in the secondary modules. If possible, move those module -// items instead to the secondary modules. -// // Functions can be used or referenced three ways in a WebAssembly module: they // can be exported, called, or referenced with ref.func. The above procedure // introduces a layer of indirection to each of those mechanisms that removes @@ -73,7 +73,6 @@ // from the IR before splitting. // #include "ir/module-splitting.h" -#include "ir/export-utils.h" #include "ir/find_all.h" #include "ir/module-utils.h" #include "ir/names.h" @@ -311,6 +310,10 @@ struct ModuleSplitter { // names. std::unordered_map exportedPrimaryFuncs; + // Map from to their corresponding export names for + // non-function items. + std::unordered_map, Name> exportedPrimaryItems; + // For each table, map placeholder indices to the names of the functions they // replace. std::unordered_map> placeholderMap; @@ -322,32 +325,39 @@ struct ModuleSplitter { static std::unique_ptr initSecondary(const Module& primary); static std::unordered_map initExportedPrimaryFuncs(const Module& primary); + static std::unordered_map, Name> + initExportedPrimaryItems(const Module& primary); // Other helpers void exportImportFunction(Name func, const std::set& modules); + void makeImportExport(Importable& primaryItem, + Importable& secondaryItem, + const std::string& genericExportName, + ExternalKind kind); Name getTrampoline(Name funcName); // Main splitting steps void classifyFunctions(); void moveSecondaryFunctions(); void thunkExportedSecondaryFunctions(); + void shareImportableItems(); void indirectReferencesToSecondaryFunctions(); void indirectCallsToSecondaryFunctions(); void exportImportCalledPrimaryFunctions(); void setupTablePatching(); - void shareImportableItems(); ModuleSplitter(Module& primary, const Config& config) : config(config), primary(primary), tableManager(primary), - exportedPrimaryFuncs(initExportedPrimaryFuncs(primary)) { + exportedPrimaryFuncs(initExportedPrimaryFuncs(primary)), + exportedPrimaryItems(initExportedPrimaryItems(primary)) { classifyFunctions(); moveSecondaryFunctions(); thunkExportedSecondaryFunctions(); + shareImportableItems(); indirectReferencesToSecondaryFunctions(); indirectCallsToSecondaryFunctions(); exportImportCalledPrimaryFunctions(); setupTablePatching(); - shareImportableItems(); } }; @@ -443,6 +453,41 @@ ModuleSplitter::initExportedPrimaryFuncs(const Module& primary) { return functionExportNames; } +std::unordered_map, Name> +ModuleSplitter::initExportedPrimaryItems(const Module& primary) { + std::unordered_map, Name> exports; + for (auto& ex : primary.exports) { + if (ex->kind != ExternalKind::Function) { + if (auto* name = ex->getInternalName()) { + exports[std::make_pair(ex->kind, *name)] = ex->name; + } + } + } + return exports; +} + +void ModuleSplitter::makeImportExport(Importable& primaryItem, + Importable& secondaryItem, + const std::string& genericExportName, + ExternalKind kind) { + secondaryItem.name = primaryItem.name; + secondaryItem.hasExplicitName = primaryItem.hasExplicitName; + secondaryItem.module = config.importNamespace; + auto exportIt = exportedPrimaryItems.find({kind, primaryItem.name}); + if (exportIt != exportedPrimaryItems.end()) { + secondaryItem.base = exportIt->second; + } else { + std::string baseName = + config.newExportPrefix + + (config.minimizeNewExportNames ? minified.getName() : genericExportName); + Name exportName = Names::getValidExportName(primary, baseName); + primary.addExport( + std::make_unique(exportName, kind, primaryItem.name)); + secondaryItem.base = exportName; + exportedPrimaryItems[{kind, primaryItem.name}] = exportName; + } +} + void ModuleSplitter::exportImportFunction(Name funcName, const std::set& modules) { Name exportName; @@ -508,7 +553,9 @@ Name ModuleSplitter::getTrampoline(Name funcName) { primary, std::string("trampoline_") + funcName.toString()); it->second = trampoline; - // Generate the call and the function. + // Generate the call and the function. We generate a direct call here, but + // this will be converted to a call_indirect in + // indirectCallsToSecondaryFunctions. std::vector args; for (Index i = 0; i < oldFunc->getNumParams(); i++) { args.push_back(builder.makeLocalGet(i, oldFunc->getLocalType(i))); @@ -559,39 +606,6 @@ static void walkSegments(Walker& walker, Module* module) { } void ModuleSplitter::shareImportableItems() { - // Map internal names to (one of) their corresponding export names. Don't - // consider functions because they have already been imported and exported as - // necessary. - std::unordered_map, Name> exports; - for (auto& ex : primary.exports) { - if (ex->kind != ExternalKind::Function) { - if (auto* name = ex->getInternalName()) { - exports[std::make_pair(ex->kind, *name)] = ex->name; - } - } - } - - auto makeImportExport = [&](Importable& primaryItem, - Importable& secondaryItem, - const std::string& genericExportName, - ExternalKind kind) { - secondaryItem.name = primaryItem.name; - secondaryItem.hasExplicitName = primaryItem.hasExplicitName; - secondaryItem.module = config.importNamespace; - auto exportIt = exports.find(std::make_pair(kind, primaryItem.name)); - if (exportIt != exports.end()) { - secondaryItem.base = exportIt->second; - } else { - std::string baseName = - config.newExportPrefix + (config.minimizeNewExportNames - ? minified.getName() - : genericExportName); - Name exportName = Names::getValidExportName(primary, baseName); - primary.addExport(new Export(exportName, kind, primaryItem.name)); - secondaryItem.base = exportName; - exports[std::make_pair(kind, primaryItem.name)] = exportName; - } - }; struct UsedNames { std::unordered_set globals; @@ -718,6 +732,15 @@ void ModuleSplitter::shareImportableItems() { secondaryUsed.push_back(getUsedNames(*secondaryPtr)); } + // We need to assume the active table and its base global are used in the + // primary module, because we will create segments there later. + if (tableManager.activeTable) { + primaryUsed.tables.insert(tableManager.activeTable->name); + } + if (tableManager.activeBase.global.size()) { + primaryUsed.globals.insert(tableManager.activeBase.global); + } + // Compute the transitive closure of globals referenced in other globals' // initializers. Since globals can reference other globals, we must ensure // that if a global is used in a module, all its dependencies are also marked @@ -796,24 +819,12 @@ void ModuleSplitter::shareImportableItems() { if (!usedInPrimary && usingSecondaries.size() == 1) { auto* secondary = usingSecondaries[0]; - // In case we copied this table to this secondary module in - // setupTablePatching(), !usedInPrimary can't be satisfied, because the - // primary module should have an element segment that refers to this - // table. assert(!secondary->getTableOrNull(table->name)); ModuleUtils::copyTable(table.get(), *secondary); tablesToRemove.push_back(table->name); } else { for (auto* secondary : usingSecondaries) { - // 1. In case we copied this table to this secondary module in - // setupTablePatching(), secondary.getTableOrNull(table->name) is not - // null, and we need to import it. - // 2. As in the case with other module elements, if the table is used in - // the secondary module's instructions, we need to export it. - auto secondaryTable = secondary->getTableOrNull(table->name); - if (!secondaryTable) { - secondaryTable = ModuleUtils::copyTable(table.get(), *secondary); - } + auto* secondaryTable = ModuleUtils::copyTable(table.get(), *secondary); makeImportExport(*table, *secondaryTable, "table", ExternalKind::Table); } } @@ -841,50 +852,11 @@ void ModuleSplitter::shareImportableItems() { // global that will be moved to a secondary module, like // (global $unused i32 (global.get $a)) // $a is moved to a secondary globalsToRemove.push_back(global->name); - } else if (!inPrimary && usingSecondaries.size() == 1) { - // We are moving this global to this secondary module auto* secondary = usingSecondaries[0]; - auto* secondaryGlobal = ModuleUtils::copyGlobal(global.get(), *secondary); + ModuleUtils::copyGlobal(global.get(), *secondary); globalsToRemove.push_back(global->name); - - if (secondaryGlobal->init) { - // When a global's initializer contains ref.func - for (auto* ref : FindAll(secondaryGlobal->init).list) { - // If ref.func's function is in a different secondary module, we - // create a trampoline here. - if (auto targetIndexIt = funcToSecondaryIndex.find(ref->func); - targetIndexIt != funcToSecondaryIndex.end()) { - if (secondaries[targetIndexIt->second].get() != secondary) { - ref->func = getTrampoline(ref->func); - } - } - // 1. If ref.func's function is in the primary module, we export it - // here. - // 2. If ref.func's function is in a different secondary module and we - // just created a trampoline for it in the primary module above, we - // export the trampoline here. - if (primary.getFunctionOrNull(ref->func)) { - exportImportFunction(ref->func, {secondary}); - } - // If ref.func's function is in the same secondary module, we don't - // need to do anything. The ref.func can directly reference the - // function. - } - } - - } else { // We are NOT moving this global to the secondary module - if (global->init) { - for (auto* ref : FindAll(global->init).list) { - // If we are exporting this global from the primary module, we should - // create a trampoline here, because we skipped doing it for global - // initializers in indirectReferencesToSecondaryFunctions. - if (allSecondaryFuncs.contains(ref->func)) { - ref->func = getTrampoline(ref->func); - } - } - } - + } else { for (auto* secondary : usingSecondaries) { auto* secondaryGlobal = ModuleUtils::copyGlobal(global.get(), *secondary); @@ -946,25 +918,7 @@ void ModuleSplitter::indirectReferencesToSecondaryFunctions() { } } } gatherer(*this); - // We shouldn't use collector.walkModuleCode here, because we don't want to - // walk global initializers. At this point, all globals are still in the - // primary module, so if we walk global initializers here, it will create - // unnecessary trampolines. - // - // For example, we have (global $a funcref (ref.func $foo)), and $foo was - // split into a secondary module. Because $a is at this point still in the - // primary module, $foo will be considered to exist in a different module, so - // this will create a trampoline for $foo. But it is possible that later we - // find out $a is exclusively used by that secondary module and move $a there. - // In that case, $a can just reference $foo locally, but if we scan global - // initializers here, we would have created an unnecessary trampoline for - // $foo. - walkSegments(gatherer, &primary); - for (auto& curr : primary.functions) { - if (!curr->imported()) { - gatherer.walkFunction(curr.get()); - } - } + gatherer.walkModule(&primary); for (auto& secondaryPtr : secondaries) { gatherer.walkModule(secondaryPtr.get()); } @@ -1052,29 +1006,30 @@ void ModuleSplitter::indirectCallsToSecondaryFunctions() { void ModuleSplitter::exportImportCalledPrimaryFunctions() { // Find primary functions called/referred to from the secondary modules. using CalledPrimaryToModules = std::map>; + struct CallCollector : PostWalker { + const std::unordered_set& primaryFuncs; + CalledPrimaryToModules& calledPrimaryToModules; + CallCollector(const std::unordered_set& primaryFuncs, + CalledPrimaryToModules& calledPrimaryToModules) + : primaryFuncs(primaryFuncs), + calledPrimaryToModules(calledPrimaryToModules) {} + void visitCall(Call* curr) { + if (primaryFuncs.contains(curr->target)) { + calledPrimaryToModules[curr->target].insert(getModule()); + } + } + void visitRefFunc(RefFunc* curr) { + if (primaryFuncs.contains(curr->func)) { + calledPrimaryToModules[curr->func].insert(getModule()); + } + } + }; + for (auto& secondaryPtr : secondaries) { Module* secondary = secondaryPtr.get(); ModuleUtils::ParallelFunctionAnalysis callCollector( *secondary, [&](Function* func, CalledPrimaryToModules& calledPrimaryToModules) { - struct CallCollector : PostWalker { - const std::unordered_set& primaryFuncs; - CalledPrimaryToModules& calledPrimaryToModules; - CallCollector(const std::unordered_set& primaryFuncs, - CalledPrimaryToModules& calledPrimaryToModules) - : primaryFuncs(primaryFuncs), - calledPrimaryToModules(calledPrimaryToModules) {} - void visitCall(Call* curr) { - if (primaryFuncs.contains(curr->target)) { - calledPrimaryToModules[curr->target].insert(getModule()); - } - } - void visitRefFunc(RefFunc* curr) { - if (primaryFuncs.contains(curr->func)) { - calledPrimaryToModules[curr->func].insert(getModule()); - } - } - }; CallCollector(primaryFuncs, calledPrimaryToModules) .walkFunctionInModule(func, secondary); }); @@ -1084,6 +1039,9 @@ void ModuleSplitter::exportImportCalledPrimaryFunctions() { calledPrimaryToModules.merge(map); } + CallCollector collector(primaryFuncs, calledPrimaryToModules); + collector.walkModuleCode(secondary); + // Ensure each called primary function is exported and imported for (auto& [func, modules] : calledPrimaryToModules) { exportImportFunction(func, modules); @@ -1142,10 +1100,36 @@ void ModuleSplitter::setupTablePatching() { for (auto& [secondaryPtr, replacedElems] : moduleToReplacedElems) { Module& secondary = *secondaryPtr; + // Import and export the active table if necessary. Unless we use an + // existing table as an active table (e.g. because reference-types is + // disabled) and that table was already being used by an existing indirect + // call, shareImportableItems wasn't able to mark it as used in secondaries, + // so we should export and import the active table here. auto secondaryTable = - ModuleUtils::copyTable(tableManager.activeTable, secondary); + secondary.getTableOrNull(tableManager.activeTable->name); + if (!secondaryTable) { + secondaryTable = + ModuleUtils::copyTable(tableManager.activeTable, secondary); + makeImportExport(*tableManager.activeTable, + *secondaryTable, + "table", + ExternalKind::Table); + } if (tableManager.activeBase.global.size()) { + // Import and export the active table's base global if necessary. Unless + // the base global was already being used elsewhere in secondaries, + // shareImportableItems wasn't able to mark it as used in secondaries, so + // we should export and import it here. + auto* primaryGlobal = primary.getGlobal(tableManager.activeBase.global); + auto* secondaryGlobal = + secondary.getGlobalOrNull(tableManager.activeBase.global); + if (!secondaryGlobal) { + secondaryGlobal = ModuleUtils::copyGlobal(primaryGlobal, secondary); + } + makeImportExport( + *primaryGlobal, *secondaryGlobal, "global", ExternalKind::Global); + assert(tableManager.activeTableSegments.size() == 1 && "Unexpected number of segments with non-const base"); assert(secondary.tables.size() == 1 && secondary.elementSegments.empty()); diff --git a/test/lit/wasm-split/active-table-base-global-used-elsewhere.wast b/test/lit/wasm-split/active-table-base-global-used-elsewhere.wast new file mode 100644 index 00000000000..6c68499e732 --- /dev/null +++ b/test/lit/wasm-split/active-table-base-global-used-elsewhere.wast @@ -0,0 +1,63 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; We need to disable reference-types to reuse the existing table as the active +;; table +;; RUN: wasm-split %s --disable-reference-types --split-funcs=split -g -o1 %t.1.wasm -o2 %t.2.wasm +;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-dis %t.2.wasm | filecheck %s --check-prefix SECONDARY + +;; This tests the case when an existing table is used as the active table, and +;; the active table and its base global already has existing uses in the +;; secondary module. + +(module + ;; PRIMARY: (type $0 (func)) + ;; SECONDARY: (type $0 (func)) + (type $0 (func)) + (global $base (import "env" "base") i32) + ;; PRIMARY: (import "env" "base" (global $base i32)) + + ;; PRIMARY: (import "placeholder.deferred" "1" (func $placeholder_1)) + + ;; PRIMARY: (table $table 2 funcref) + (table $table 1 funcref) + (elem (global.get $base) $keep) + ;; PRIMARY: (elem $0 (global.get $base) $keep $placeholder_1) + + ;; PRIMARY: (export "table" (table $table)) + + ;; PRIMARY: (export "global" (global $base)) + + ;; PRIMARY: (export "keep" (func $keep)) + + ;; PRIMARY: (func $keep + ;; PRIMARY-NEXT: (call_indirect (type $0) + ;; PRIMARY-NEXT: (i32.add + ;; PRIMARY-NEXT: (global.get $base) + ;; PRIMARY-NEXT: (i32.const 1) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: ) + (func $keep + (call $split) + ) + ;; SECONDARY: (import "primary" "table" (table $table 1 funcref)) + + ;; SECONDARY: (import "primary" "global" (global $base i32)) + + ;; SECONDARY: (import "primary" "keep" (func $keep)) + + ;; SECONDARY: (elem $0 (global.get $base) $keep $split) + + ;; SECONDARY: (func $split + ;; SECONDARY-NEXT: (drop + ;; SECONDARY-NEXT: (global.get $base) + ;; SECONDARY-NEXT: ) + ;; SECONDARY-NEXT: (call_indirect (type $0) + ;; SECONDARY-NEXT: (i32.const 0) + ;; SECONDARY-NEXT: ) + ;; SECONDARY-NEXT: ) + (func $split + (drop (global.get $base)) + (call_indirect (type $0) (i32.const 0)) + ) +) diff --git a/test/lit/wasm-split/global-funcref.wast b/test/lit/wasm-split/global-funcref.wast deleted file mode 100644 index 3faf339546c..00000000000 --- a/test/lit/wasm-split/global-funcref.wast +++ /dev/null @@ -1,43 +0,0 @@ -;; RUN: wasm-split %s -all -g -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 - -;; When a split global ($a here)'s initializer contains a ref.func of a split -;; function, we should NOT create any trampolines, and the split global should -;; direclty refer to the function. - -(module - (global $a funcref (ref.func $split)) - (global $b funcref (ref.func $keep)) - - ;; PRIMARY: (export "keep" (func $keep)) - - ;; PRIMARY-NOT: (export "trampoline_split" - ;; PRIMARY-NOT: (func $trampoline_split - - ;; SECONDARY: (import "primary" "keep" (func $keep (exact))) - - ;; SECONDARY: (global $a funcref (ref.func $split)) - ;; SECONDARY: (global $b funcref (ref.func $keep)) - - ;; PRIMARY: (func $keep - ;; PRIMARY-NEXT: ) - (func $keep) - - ;; SECONDARY: (func $split - ;; SECONDARY-NEXT: (drop - ;; SECONDARY-NEXT: (global.get $a) - ;; SECONDARY-NEXT: ) - ;; SECONDARY-NEXT: (drop - ;; SECONDARY-NEXT: (global.get $b) - ;; SECONDARY-NEXT: ) - ;; SECONDARY-NEXT: ) - (func $split - (drop - (global.get $a) - ) - (drop - (global.get $b) - ) - ) -) diff --git a/test/lit/wasm-split/global-reffunc.wast b/test/lit/wasm-split/global-reffunc.wast new file mode 100644 index 00000000000..5d5259ebd33 --- /dev/null +++ b/test/lit/wasm-split/global-reffunc.wast @@ -0,0 +1,45 @@ +;; RUN: wasm-split %s -all -g -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 + +;; When a split global ($a here)'s initializer contains a ref.func of a split +;; function, we should NOT create any trampolines, and the split global should +;; directly refer to the function. + +(module + (global $a funcref (ref.func $split)) + (global $b funcref (ref.func $keep)) + + (func $keep) + + (func $split + (drop + (global.get $a) + ) + (drop + (global.get $b) + ) + ) +) + +;; PRIMARY: (module +;; PRIMARY-NEXT: (type $0 (func)) +;; PRIMARY-NEXT: (export "keep" (func $keep)) +;; PRIMARY-NEXT: (func $keep +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) + +;; SECONDARY: (module +;; SECONDARY-NEXT: (type $0 (func)) +;; SECONDARY-NEXT: (import "primary" "keep" (func $keep (exact))) +;; SECONDARY-NEXT: (global $a funcref (ref.func $split)) +;; SECONDARY-NEXT: (global $b funcref (ref.func $keep)) +;; SECONDARY-NEXT: (func $split +;; SECONDARY-NEXT: (drop +;; SECONDARY-NEXT: (global.get $a) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: (drop +;; SECONDARY-NEXT: (global.get $b) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) diff --git a/test/lit/wasm-split/global-reffunc2.wast b/test/lit/wasm-split/global-reffunc2.wast new file mode 100644 index 00000000000..80ce2275b25 --- /dev/null +++ b/test/lit/wasm-split/global-reffunc2.wast @@ -0,0 +1,52 @@ +;; RUN: wasm-split %s -all -g -o1 %t.1.wasm -o2 %t.2.wasm --split-funcs=split1,split2 +;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-dis %t.2.wasm | filecheck %s --check-prefix SECONDARY + +;; Global $g1 is used (exported) in the primary module so it can't move, and +;; global $g2 is only used in the secondary module so it will move there. + +(module + (global $g1 funcref (ref.func $split1)) + (global $g2 funcref (ref.func $split2)) + (export "g1" (global $g1)) + + (func $split1 + (unreachable) + ) + + (func $split2 + (drop + (global.get $g2) + ) + ) +) + +;; PRIMARY: (module +;; PRIMARY-NEXT: (type $0 (func)) +;; PRIMARY-NEXT: (import "placeholder.deferred" "0" (func $placeholder_0)) +;; PRIMARY-NEXT: (global $g1 funcref (ref.func $trampoline_split1)) +;; PRIMARY-NEXT: (table $0 1 funcref) +;; PRIMARY-NEXT: (elem $0 (i32.const 0) $placeholder_0) +;; PRIMARY-NEXT: (export "g1" (global $g1)) +;; PRIMARY-NEXT: (export "table" (table $0)) +;; PRIMARY-NEXT: (func $trampoline_split1 +;; PRIMARY-NEXT: (call_indirect (type $0) +;; PRIMARY-NEXT: (i32.const 0) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) + +;; SECONDARY: (module +;; SECONDARY-NEXT: (type $0 (func)) +;; SECONDARY-NEXT: (import "primary" "table" (table $timport$0 1 funcref)) +;; SECONDARY-NEXT: (global $g2 funcref (ref.func $split2)) +;; SECONDARY-NEXT: (elem $0 (i32.const 0) $split1) +;; SECONDARY-NEXT: (func $split1 +;; SECONDARY-NEXT: (unreachable) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: (func $split2 +;; SECONDARY-NEXT: (drop +;; SECONDARY-NEXT: (global.get $g2) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) diff --git a/test/lit/wasm-split/ref.func.wast b/test/lit/wasm-split/ref.func.wast index 38eea498d72..5cc261dc4cd 100644 --- a/test/lit/wasm-split/ref.func.wast +++ b/test/lit/wasm-split/ref.func.wast @@ -73,7 +73,7 @@ ;; SECONDARY: (import "primary" "prime" (func $prime (exact (type $0)))) - ;; SECONDARY: (elem $0 (i32.const 0) $second-in-table $second) + ;; SECONDARY: (elem $0 (i32.const 0) $second $second-in-table) ;; SECONDARY: (elem declare func $prime) @@ -109,13 +109,13 @@ ;; (but we will get a placeholder, as all split-out functions do). ) ) -;; PRIMARY: (func $trampoline_second-in-table (type $0) +;; PRIMARY: (func $trampoline_second (type $0) ;; PRIMARY-NEXT: (call_indirect $1 (type $0) ;; PRIMARY-NEXT: (i32.const 0) ;; PRIMARY-NEXT: ) ;; PRIMARY-NEXT: ) -;; PRIMARY: (func $trampoline_second (type $0) +;; PRIMARY: (func $trampoline_second-in-table (type $0) ;; PRIMARY-NEXT: (call_indirect $1 (type $0) ;; PRIMARY-NEXT: (i32.const 1) ;; PRIMARY-NEXT: ) diff --git a/test/lit/wasm-split/split-module-items.wast b/test/lit/wasm-split/split-module-items.wast index 340fe27dac8..fdca4737a28 100644 --- a/test/lit/wasm-split/split-module-items.wast +++ b/test/lit/wasm-split/split-module-items.wast @@ -34,16 +34,16 @@ ;; PRIMARY: (tag $keep-tag (type $1) (param i32)) ;; PRIMARY-NEXT: (tag $shared-tag (type $1) (param i32)) - ;; PRIMARY: (export "keep" (func $keep)) - ;; PRIMARY-NEXT: (export "memory" (memory $shared-memory)) + ;; PRIMARY: (export "memory" (memory $shared-memory)) ;; PRIMARY-NEXT: (export "table" (table $shared-table)) - ;; PRIMARY-NEXT: (export "table_3" (table $2)) ;; PRIMARY-NEXT: (export "global" (global $shared-global)) ;; PRIMARY-NEXT: (export "tag" (tag $shared-tag)) + ;; PRIMARY-NEXT: (export "keep" (func $keep)) + ;; PRIMARY-NEXT: (export "table_5" (table $2)) ;; SECONDARY: (import "primary" "memory" (memory $shared-memory 1 1)) - ;; SECONDARY-NEXT: (import "primary" "table_3" (table $timport$0 1 funcref)) ;; SECONDARY-NEXT: (import "primary" "table" (table $shared-table 1 1 funcref)) + ;; SECONDARY-NEXT: (import "primary" "table_5" (table $timport$1 1 funcref)) ;; SECONDARY-NEXT: (import "primary" "global" (global $shared-global i32)) ;; SECONDARY-NEXT: (import "primary" "keep" (func $keep (exact (param i32) (result i32)))) ;; SECONDARY-NEXT: (import "primary" "tag" (tag $shared-tag (type $1) (param i32))) From 332a49f12888baac1479e721187f660dc018e1e4 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 15 May 2026 13:01:19 -0700 Subject: [PATCH 101/168] PrintBoundary pass, emitting a JSON summary of the API boundary of the module (#8703) --- src/passes/CMakeLists.txt | 1 + src/passes/PrintBoundary.cpp | 180 ++++++++++++++++++++++++++++ src/passes/pass.cpp | 2 + src/passes/passes.h | 1 + test/lit/help/wasm-metadce.test | 2 + test/lit/help/wasm-opt.test | 2 + test/lit/help/wasm2js.test | 2 + test/lit/passes/print-boundary.wast | 72 +++++++++++ 8 files changed, 262 insertions(+) create mode 100644 src/passes/PrintBoundary.cpp create mode 100644 test/lit/passes/print-boundary.wast diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index a61bfb6195c..d6f9100aad0 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -89,6 +89,7 @@ set(passes_SOURCES PostEmscripten.cpp Precompute.cpp Print.cpp + PrintBoundary.cpp PrintCallGraph.cpp PrintFeatures.cpp PrintFunctionMap.cpp diff --git a/src/passes/PrintBoundary.cpp b/src/passes/PrintBoundary.cpp new file mode 100644 index 00000000000..371e642e2d8 --- /dev/null +++ b/src/passes/PrintBoundary.cpp @@ -0,0 +1,180 @@ +/* + * Copyright 2026 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. + */ + +// +// Prints the boundary - the imports and exports - in a convenient JSON format. +// Only enough information for JavaScript is provided (for the full info, parse +// the wat or wasm). +// +// Usage: +// +// wasm-opt --print-boundary=OUTFILE +// +// If OUTFILE is not provided, prints to stdout. +// +// Example: +// +// { +// 'imports': [ +// { +// 'module': 'foo', // foo.bar +// 'base': 'bar', +// 'kind': 'func', +// 'type': { +// 'params': ['i32', '(ref func)'], +// 'results': ['f64'] +// }, +// }, +// [..] +// ], +// 'exports': [ +// { +// 'name': 'foo', +// 'kind': 'global', +// 'type': 'i32', +// }, +// [..] +// ] +// } +// + +#include "ir/module-utils.h" +#include "pass.h" +#include "support/file.h" +#include "support/json.h" +#include "wasm.h" + +namespace wasm { + +struct PrintBoundary : public Pass { + bool modifiesBinaryenIR() override { return false; } + + void run(Module* module) override { + std::string target = getArgumentOrDefault("print-boundary", ""); + + // Imports. + auto imports = json::Value::makeArray(); + + ModuleUtils::iterImportable( + *module, [&](ExternalKind kind, Importable* import) { + auto item = json::Value::makeObject(); + item["module"] = json::Value::make(import->module.view()); + item["base"] = json::Value::make(import->base.view()); + item["kind"] = getKindName(kind); + item["type"] = getExternalType(kind, import->name, *module); + imports->push_back(item); + }); + + // Exports. + auto exports = json::Value::makeArray(); + + for (auto& exp : module->exports) { + auto item = json::Value::makeObject(); + item["name"] = json::Value::make(exp->name.view()); + item["kind"] = getKindName(exp->kind); + item["type"] = + getExternalType(exp->kind, *exp->getInternalName(), *module); + exports->push_back(item); + } + + // Emit the final structure + json::Value root; + root.setObject(); + root["imports"] = imports; + root["exports"] = exports; + + Output output(target, Flags::BinaryOption::Text); + root.stringify(output.getStream(), true /* pretty */); + } + + // Emits an array of multivalue types. For a signature, emits params and + // results. + // + // We emit an array only when needed, unless forceArray is set. + json::Value::Ref getTypes(Type type, bool forceArray = false) { + if (type.isRef()) { + auto heapType = type.getHeapType(); + if (heapType.isSignature()) { + auto sig = heapType.getSignature(); + auto ret = json::Value::makeObject(); + // Always emit arrays for params and results. + ret["params"] = getTypes(sig.params, true); + ret["results"] = getTypes(sig.results, true); + return ret; + } + } + + // Simplify the output, avoiding an array for a single value. + if (!forceArray && type.size() == 1) { + return json::Value::make(type.toString()); + } + + auto ret = json::Value::makeArray(); + for (auto t : type) { + ret->push_back(json::Value::make(t.toString())); + } + return ret; + } + + // For an imported or exported thing (something external), and its name, + // return the type info we report for it. + json::Value::Ref getExternalType(ExternalKind kind, Name name, Module& wasm) { + switch (kind) { + case ExternalKind::Function: + return getTypes(wasm.getFunction(name)->type); + break; + case ExternalKind::Table: + break; + case ExternalKind::Memory: + break; + case ExternalKind::Global: + return getTypes(wasm.getGlobal(name)->type); + case ExternalKind::Tag: + break; + case ExternalKind::Invalid: + WASM_UNREACHABLE("invalid ExternalKind"); + } + return {}; + } + + json::Value::Ref getKindName(ExternalKind kind) { + const char* name = nullptr; + switch (kind) { + case ExternalKind::Function: + name = "func"; + break; + case ExternalKind::Table: + name = "table"; + break; + case ExternalKind::Memory: + name = "memory"; + break; + case ExternalKind::Global: + name = "global"; + break; + case ExternalKind::Tag: + name = "tag"; + break; + case ExternalKind::Invalid: + WASM_UNREACHABLE("invalid ExternalKind"); + } + return json::Value::make(name); + } +}; + +Pass* createPrintBoundaryPass() { return new PrintBoundary(); } + +} // namespace wasm diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index dc6d91feb4e..d29a6fcebf5 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -390,6 +390,8 @@ void PassRegistry::registerPasses() { createPrintFeaturesPass); registerPass( "print-full", "print in full s-expression format", createFullPrinterPass); + registerPass( + "print-boundary", "print boundary in JSON format", createPrintBoundaryPass); registerPass( "print-call-graph", "print call graph", createPrintCallGraphPass); diff --git a/src/passes/passes.h b/src/passes/passes.h index 2fdacd84ab0..681a259a831 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -128,6 +128,7 @@ Pass* createPostEmscriptenPass(); Pass* createPrecomputePass(); Pass* createPrecomputePropagatePass(); Pass* createPrinterPass(); +Pass* createPrintBoundaryPass(); Pass* createPrintCallGraphPass(); Pass* createPrintFeaturesPass(); Pass* createPrintFunctionMapPass(); diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index 4d5f8e33b89..b35982035d0 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -362,6 +362,8 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --print print in s-expression format ;; CHECK-NEXT: +;; CHECK-NEXT: --print-boundary print boundary in JSON format +;; CHECK-NEXT: ;; CHECK-NEXT: --print-call-graph print call graph ;; CHECK-NEXT: ;; CHECK-NEXT: --print-features print options for enabled diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 8566645db87..1565b9686b7 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -398,6 +398,8 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --print print in s-expression format ;; CHECK-NEXT: +;; CHECK-NEXT: --print-boundary print boundary in JSON format +;; CHECK-NEXT: ;; CHECK-NEXT: --print-call-graph print call graph ;; CHECK-NEXT: ;; CHECK-NEXT: --print-features print options for enabled diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index 88d6504b384..32a1f60ed3e 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -326,6 +326,8 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --print print in s-expression format ;; CHECK-NEXT: +;; CHECK-NEXT: --print-boundary print boundary in JSON format +;; CHECK-NEXT: ;; CHECK-NEXT: --print-call-graph print call graph ;; CHECK-NEXT: ;; CHECK-NEXT: --print-features print options for enabled diff --git a/test/lit/passes/print-boundary.wast b/test/lit/passes/print-boundary.wast new file mode 100644 index 00000000000..850618f07b1 --- /dev/null +++ b/test/lit/passes/print-boundary.wast @@ -0,0 +1,72 @@ +(module + (type $struct (struct)) + + (import "module" "base" (func $foo (param i32) (param f64) (result anyref))) + + (import "module2" "other" (func $bar (result i32 f32))) + + (global $g (mut i32) (i32.const 42)) + + (export "one" (func $one)) + + (export "glob" (global $g)) + + (func $one (param $x (ref $struct)) (result i32 i32 i32) + (unreachable) + ) +) + +;; RUN: wasm-opt %s -all --print-boundary -S -o - | filecheck %s + +;; CHECK: { +;; CHECK-NEXT: "imports": [ +;; CHECK-NEXT: { +;; CHECK-NEXT: "module": "module", +;; CHECK-NEXT: "base": "base", +;; CHECK-NEXT: "kind": "func", +;; CHECK-NEXT: "type": { +;; CHECK-NEXT: "params": [ +;; CHECK-NEXT: "i32", +;; CHECK-NEXT: "f64" +;; CHECK-NEXT: ], +;; CHECK-NEXT: "results": [ +;; CHECK-NEXT: "anyref" +;; CHECK-NEXT: ] +;; CHECK-NEXT: } +;; CHECK-NEXT: }, +;; CHECK-NEXT: { +;; CHECK-NEXT: "module": "module2", +;; CHECK-NEXT: "base": "other", +;; CHECK-NEXT: "kind": "func", +;; CHECK-NEXT: "type": { +;; CHECK-NEXT: "params": [ +;; CHECK-NEXT: ], +;; CHECK-NEXT: "results": [ +;; CHECK-NEXT: "i32", +;; CHECK-NEXT: "f32" +;; CHECK-NEXT: ] +;; CHECK-NEXT: } +;; CHECK-NEXT: } +;; CHECK-NEXT: ], +;; CHECK-NEXT: "exports": [ +;; CHECK-NEXT: { +;; CHECK-NEXT: "name": "one", +;; CHECK-NEXT: "kind": "func", +;; CHECK-NEXT: "type": { +;; CHECK-NEXT: "params": [ +;; CHECK-NEXT: "(ref $struct.0)" +;; CHECK-NEXT: ], +;; CHECK-NEXT: "results": [ +;; CHECK-NEXT: "i32", +;; CHECK-NEXT: "i32", +;; CHECK-NEXT: "i32" +;; CHECK-NEXT: ] +;; CHECK-NEXT: } +;; CHECK-NEXT: }, +;; CHECK-NEXT: { +;; CHECK-NEXT: "name": "glob", +;; CHECK-NEXT: "kind": "global", +;; CHECK-NEXT: "type": "i32" +;; CHECK-NEXT: } +;; CHECK-NEXT: ] +;; CHECK-NEXT: } From 2458c41db802168a3d7f26c060f0332b416d89c7 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 15 May 2026 13:57:13 -0700 Subject: [PATCH 102/168] Supply fuzz imports to second modules as well (#8705) Update execution-results.h to supply standard fuzzing imports, including imported externref globals materialized on demand, to second modules. This matches the behavior of fuzz_shell.js, which already makes these imports available to both modules. --- src/tools/execution-results.h | 5 ++++- test/lit/exec/fuzz-exec-second-import.wast | 16 ++++++++++++++++ .../lit/exec/fuzz-exec-second-import.wast.second | 7 +++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 test/lit/exec/fuzz-exec-second-import.wast create mode 100644 test/lit/exec/fuzz-exec-second-import.wast.second diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index 0cda208792b..b1690dc9d20 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -470,7 +470,10 @@ struct ExecutionResults { secondInterface = std::make_unique( loggings, *second, linkedInstances); secondInstance = std::make_shared( - *second, secondInterface.get(), linkedInstances); + *second, + secondInterface.get(), + linkedInstances, + std::make_shared(linkedInstances)); instantiate(*secondInstance, *secondInterface); } diff --git a/test/lit/exec/fuzz-exec-second-import.wast b/test/lit/exec/fuzz-exec-second-import.wast new file mode 100644 index 00000000000..765b63e8505 --- /dev/null +++ b/test/lit/exec/fuzz-exec-second-import.wast @@ -0,0 +1,16 @@ +;; RUN: wasm-opt %s -all --fuzz-exec-before --fuzz-exec-second=%s.second -q -o /dev/null 2>&1 | filecheck %s + +;; Check that imported externref globals work in second modules as well. + +(module + (import "__fuzz_import" "extern$" (global $gimport$0 externref)) + + (func (export "check") (result i32) + (ref.is_null (global.get $gimport$0)) + ) +) + +;; CHECK: [fuzz-exec] export check +;; CHECK-NEXT: [fuzz-exec] note result: check => 0 +;; CHECK: [fuzz-exec] export check_second +;; CHECK-NEXT: [fuzz-exec] note result: check_second => 0 diff --git a/test/lit/exec/fuzz-exec-second-import.wast.second b/test/lit/exec/fuzz-exec-second-import.wast.second new file mode 100644 index 00000000000..0b3e09d59c0 --- /dev/null +++ b/test/lit/exec/fuzz-exec-second-import.wast.second @@ -0,0 +1,7 @@ +(module + (import "__fuzz_import" "extern$" (global $gimport$0 externref)) + + (func (export "check_second") (result i32) + (ref.is_null (global.get $gimport$0)) + ) +) From e7987f62a5b31cd165cf4ce9fb6ff40633150315 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 15 May 2026 14:01:50 -0700 Subject: [PATCH 103/168] [wasm-split] Use Name() for nonexistent global base (NFC) (#8704) Rather than using an empty string, this uses `Name()` when an active table does not have a base global. This allows us to check for the existence of the global by just ```cpp if (tableManager.activeBase.global) ``` rather than ```cpp if (tableManager.activeBase.global.size()) ``` --- src/ir/module-splitting.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 6ae18645128..db016dc9cef 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -87,7 +87,7 @@ namespace { template void forEachElement(Module& module, F f) { ModuleUtils::iterActiveElementSegments(module, [&](ElementSegment* segment) { - Name base = ""; + Name base; Index offset = 0; if (auto* c = segment->offset->dynCast()) { offset = c->value.getInteger(); @@ -134,7 +134,7 @@ Expression* TableSlotManager::Slot::makeExpr(Module& module) { auto makeIndex = [&]() { return builder.makeConst(Literal::makeFromInt32(index, table->addressType)); }; - if (global.size()) { + if (global) { Expression* getBase = builder.makeGlobalGet(global, table->addressType); auto addOp = table->is64() ? AddInt64 : AddInt32; return index == 0 ? getBase @@ -191,7 +191,7 @@ TableSlotManager::TableSlotManager(Module& module) : module(module) { if (activeTableSegments.empty()) { // There are no active segments, so we will lazily create one and start // filling it at index 0. - activeBase = {activeTable->name, "", 0}; + activeBase = {activeTable->name, Name(), 0}; } else if (activeTableSegments.size() == 1 && activeTableSegments[0]->type == funcref && !activeTableSegments[0]->offset->is()) { @@ -218,7 +218,7 @@ TableSlotManager::TableSlotManager(Module& module) : module(module) { if (segmentBase + segment->data.size() >= maxIndex) { maxIndex = segmentBase + segment->data.size(); activeSegment = segment; - activeBase = {activeTable->name, "", segmentBase}; + activeBase = {activeTable->name, Name(), segmentBase}; } } } @@ -257,7 +257,7 @@ TableSlotManager::Slot TableSlotManager::getSlot(Name func, HeapType type) { if (activeSegment == nullptr) { if (activeTable == nullptr) { activeTable = makeTable(); - activeBase = {activeTable->name, "", 0}; + activeBase = {activeTable->name, Name(), 0}; } // None of the existing segments should refer to the active table @@ -737,7 +737,7 @@ void ModuleSplitter::shareImportableItems() { if (tableManager.activeTable) { primaryUsed.tables.insert(tableManager.activeTable->name); } - if (tableManager.activeBase.global.size()) { + if (tableManager.activeBase.global) { primaryUsed.globals.insert(tableManager.activeBase.global); } @@ -1116,7 +1116,7 @@ void ModuleSplitter::setupTablePatching() { ExternalKind::Table); } - if (tableManager.activeBase.global.size()) { + if (tableManager.activeBase.global) { // Import and export the active table's base global if necessary. Unless // the base global was already being used elsewhere in secondaries, // shareImportableItems wasn't able to mark it as used in secondaries, so From b5b9ebd748027515445ac7f98ffb69ad7204be75 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 15 May 2026 14:47:51 -0700 Subject: [PATCH 104/168] [wasm-split] Sync secondary tables' initial/max (#8714) When an existing table is used as the active table, after we increate the size of the table by placing placeholders, we should sync it to the secondary modules' imports of the table. Fixes https://github.com/emscripten-core/emscripten/issues/26959. --- src/ir/module-splitting.cpp | 6 +++++- .../wasm-split/active-table-base-global-used-elsewhere.wast | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index db016dc9cef..3385bc7602a 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -1107,7 +1107,11 @@ void ModuleSplitter::setupTablePatching() { // so we should export and import the active table here. auto secondaryTable = secondary.getTableOrNull(tableManager.activeTable->name); - if (!secondaryTable) { + if (secondaryTable) { + // In case it's already in the secondary module, sync the initial/max + secondaryTable->initial = tableManager.activeTable->initial; + secondaryTable->max = tableManager.activeTable->max; + } else { secondaryTable = ModuleUtils::copyTable(tableManager.activeTable, secondary); makeImportExport(*tableManager.activeTable, diff --git a/test/lit/wasm-split/active-table-base-global-used-elsewhere.wast b/test/lit/wasm-split/active-table-base-global-used-elsewhere.wast index 6c68499e732..ca3380ce405 100644 --- a/test/lit/wasm-split/active-table-base-global-used-elsewhere.wast +++ b/test/lit/wasm-split/active-table-base-global-used-elsewhere.wast @@ -18,8 +18,8 @@ ;; PRIMARY: (import "placeholder.deferred" "1" (func $placeholder_1)) - ;; PRIMARY: (table $table 2 funcref) - (table $table 1 funcref) + ;; PRIMARY: (table $table 2 2 funcref) + (table $table 1 1 funcref) (elem (global.get $base) $keep) ;; PRIMARY: (elem $0 (global.get $base) $keep $placeholder_1) @@ -40,7 +40,7 @@ (func $keep (call $split) ) - ;; SECONDARY: (import "primary" "table" (table $table 1 funcref)) + ;; SECONDARY: (import "primary" "table" (table $table 2 2 funcref)) ;; SECONDARY: (import "primary" "global" (global $base i32)) From d3029d2b975488acdf9253eb2994a3fc55bd3549 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 15 May 2026 15:34:18 -0700 Subject: [PATCH 105/168] [wasm-split] Fix table naming conflicts (#8708) After #8688, we split module elements, including tables, earlier than `indirectCallsToSecondaryFunctions`, which calls `getSlot`, which calls `makeTable`. But when making a table, we only try to get a valid name within the primary module: https://github.com/WebAssembly/binaryen/blob/2f1f55aef6d9adfa6fdc2c25e46d202232dbf6e2/src/ir/module-splitting.cpp#L235-L238 If an existing table's name was `0` and it was moved to a secondary module in `shareImportable` already, this will happily create an active table with the name `0` again. And in `setupTablePatching`, because the secondary module already has `0`, the active table will not be exported / imported there: https://github.com/WebAssembly/binaryen/blob/2f1f55aef6d9adfa6fdc2c25e46d202232dbf6e2/src/ir/module-splitting.cpp#L1103-L1117 But this existing table is NOT the active table, and this table's type may not even be `funcref`. This fixes `makeTable` so that it makes a table name that does not collide with any table names not only in the primary module but all secondary modules. This also disables `Split` fuzzer for now; I'm finding more bugs, so I'll reenable it after it is more stabilized. --- scripts/fuzz_opt.py | 2 +- src/ir/module-splitting.cpp | 31 +++++++++++--- test/lit/wasm-split/table-name-conflict.wast | 45 ++++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 test/lit/wasm-split/table-name-conflict.wast diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index b089ec7f1f6..4173058abf8 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2525,7 +2525,7 @@ def handle(self, wasm): TrapsNeverHappen(), CtorEval(), Merge(), - Split(), +# Split(), # Will reenable after stabilized RoundtripText(), ClusterFuzz(), Two(), diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 3385bc7602a..9659121d6fe 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -112,13 +112,15 @@ struct TableSlotManager { Expression* makeExpr(Module& module); }; Module& module; + const std::vector>& secondaries; Table* activeTable = nullptr; ElementSegment* activeSegment = nullptr; Slot activeBase; std::map funcIndices; std::vector activeTableSegments; - TableSlotManager(Module& module); + TableSlotManager(Module& module, + const std::vector>& secondaries); Table* makeTable(); ElementSegment* makeElementSegment(); @@ -149,7 +151,9 @@ void TableSlotManager::addSlot(Name func, Slot slot) { funcIndices.insert({func, slot}); } -TableSlotManager::TableSlotManager(Module& module) : module(module) { +TableSlotManager::TableSlotManager( + Module& module, const std::vector>& secondaries) + : module(module), secondaries(secondaries) { // If possible, just create a new table to manage all primary-to-secondary // calls lazily. Do not re-use slots for functions that will already be in // existing tables, since that is not correct in the face of table mutations. @@ -233,8 +237,25 @@ TableSlotManager::TableSlotManager(Module& module) : module(module) { } Table* TableSlotManager::makeTable() { - return module.addTable( - Builder::makeTable(Names::getValidTableName(module, Name::fromInt(0)))); + // Because the active table will be imported in secondary modules, its name + // should not collide with any existing tables in primary and secondary + // modules. + std::unordered_set secondaryTableNames; + for (auto& secondary : secondaries) { + for (auto& table : secondary->tables) { + secondaryTableNames.insert(table->name); + } + } + Name name = Names::getValidName("0", [&](Name test) { + if (module.getTableOrNull(test)) { + return false; + } + if (secondaryTableNames.contains(test)) { + return false; + } + return true; + }); + return module.addTable(Builder::makeTable(name)); } ElementSegment* TableSlotManager::makeElementSegment() { @@ -347,7 +368,7 @@ struct ModuleSplitter { void setupTablePatching(); ModuleSplitter(Module& primary, const Config& config) - : config(config), primary(primary), tableManager(primary), + : config(config), primary(primary), tableManager(primary, secondaries), exportedPrimaryFuncs(initExportedPrimaryFuncs(primary)), exportedPrimaryItems(initExportedPrimaryItems(primary)) { classifyFunctions(); diff --git a/test/lit/wasm-split/table-name-conflict.wast b/test/lit/wasm-split/table-name-conflict.wast new file mode 100644 index 00000000000..983234b8854 --- /dev/null +++ b/test/lit/wasm-split/table-name-conflict.wast @@ -0,0 +1,45 @@ +;; RUN: wasm-split %s -all -g -S -o1 %t.1.wast -o2 %t.2.wast --split-funcs=split +;; RUN: cat %t.1.wast | filecheck %s --check-prefix PRIMARY +;; RUN: cat %t.2.wast | filecheck %s --check-prefix SECONDARY + +;; Regression test for a bug when an existing table, which is to be split to the +;; secondary module, has the name '0'. The newly created active table should +;; have a different name. + +(module + (table $0 0 externref) + (export "split" (func $split)) + (func $split + (table.set $0 + (i32.const 0) + (ref.null extern) + ) + ) +) + +;; PRIMARY: (module +;; PRIMARY-NEXT: (type $0 (func)) +;; PRIMARY-NEXT: (import "placeholder.deferred" "0" (func $placeholder_0 (type $0))) +;; PRIMARY-NEXT: (table $0_0 1 funcref) +;; PRIMARY-NEXT: (elem $0 (i32.const 0) $placeholder_0) +;; PRIMARY-NEXT: (export "split" (func $trampoline_split)) +;; PRIMARY-NEXT: (export "table" (table $0_0)) +;; PRIMARY-NEXT: (func $trampoline_split (type $0) +;; PRIMARY-NEXT: (call_indirect $0_0 (type $0) +;; PRIMARY-NEXT: (i32.const 0) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: ) + +;; SECONDARY: (module +;; SECONDARY-NEXT: (type $0 (func)) +;; SECONDARY-NEXT: (import "primary" "table" (table $0_0 1 funcref)) +;; SECONDARY-NEXT: (table $0 0 externref) +;; SECONDARY-NEXT: (elem $0 (table $0_0) (i32.const 0) func $split) +;; SECONDARY-NEXT: (func $split (type $0) +;; SECONDARY-NEXT: (table.set $0 +;; SECONDARY-NEXT: (i32.const 0) +;; SECONDARY-NEXT: (ref.null noextern) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) +;; SECONDARY-NEXT: ) From aef8534cd359bd8e811bcd61ca5c874d8bfb407f Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 18 May 2026 12:27:03 -0700 Subject: [PATCH 106/168] Fix validation error in wasm-reduce (#8715) When removing functions, wasm-reduce replaces calls to those functions using `builder.replaceWithIdenticalType`. That method can modify the input expression in-place to have a different types (e.g. replacing a reference with a null), so when the previous code read `curr->type` to set the type of the replacement block, it was possible to get a more refined type, leading to validation failures. Fix the bug by explicitly using the original type. Fixes #8713. --- src/tools/wasm-reduce/wasm-reduce.cpp | 3 ++- .../wasm-reduce/reduce-validation-error.wast | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 test/lit/wasm-reduce/reduce-validation-error.wast diff --git a/src/tools/wasm-reduce/wasm-reduce.cpp b/src/tools/wasm-reduce/wasm-reduce.cpp index bba5cda3cb1..04676109acf 100644 --- a/src/tools/wasm-reduce/wasm-reduce.cpp +++ b/src/tools/wasm-reduce/wasm-reduce.cpp @@ -1102,6 +1102,7 @@ struct Reducer auto* block = ChildLocalizer(curr, getFunction(), *getModule(), getPassOptions()) .getChildrenReplacement(); + auto originalType = curr->type; auto* replacement = builder.replaceWithIdenticalType(curr); // We may have failed to come up with a replacement (e.g. for // non-nullable references), so manually add an `unreachable` in that @@ -1110,7 +1111,7 @@ struct Reducer replacement = builder.makeUnreachable(); } block->list.push_back(replacement); - block->type = curr->type; + block->type = originalType; replaceCurrent(block); } void visitRefFunc(RefFunc* curr) { diff --git a/test/lit/wasm-reduce/reduce-validation-error.wast b/test/lit/wasm-reduce/reduce-validation-error.wast new file mode 100644 index 00000000000..5b941fc8a18 --- /dev/null +++ b/test/lit/wasm-reduce/reduce-validation-error.wast @@ -0,0 +1,20 @@ +;; This is a regression test for a crash in wasm-reduce where in-place mutation +;; of a Call node during replaceWithIdenticalType caused the replacement block +;; type to be set incorrectly to nullref instead of the original type, leading +;; to a validation error. + +;; TODO: Why does this fail on CI without --force? +;; RUN: wasm-reduce %s -t %t.t.wast -w %t.w.wast --force \ +;; RUN: --command='wasm-opt %t.t.wast -all --fuzz-exec' + +(module + (func $to_remove (result anyref) + (ref.null none) + ) + + (func $main (export "main") (result anyref) + ;; This will be replaced with a nullref. This should not cause validation + ;; failures and cause wasm-reduce to crash. + (call $to_remove) + ) +) From 731a4732986d9c560be8ffde40cbd4f3accc0267 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Mon, 18 May 2026 12:40:45 -0700 Subject: [PATCH 107/168] Fix implicitly-deleted default constructor error on older Xcode version. (#8719) Older versions of Clang (particularly those shipped with older Xcode versions) are more strict about the initialization of const members. According to the C++ standard, if a class has a const member that is not initialized at the point of declaration and lacks a user-provided default constructor that initializes it, the default constructor is implicitly deleted. While newer versions of Clang and other compilers may allow this if the member has a trivial default constructor (like our View struct, which has an inline initializer for its only member), older versions of Apple Clang have been known to reject this, leading to build failures when inheriting from IString (e.g., in the Name class). This change provides an explicit default constructor for IString that initializes the 'str' member, ensuring compatibility across a wider range of compiler versions. --- src/support/istring.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/support/istring.h b/src/support/istring.h index a567eb82f12..bbb8028eb0f 100644 --- a/src/support/istring.h +++ b/src/support/istring.h @@ -69,7 +69,13 @@ struct IString { std::string_view view() const { return str.view(); } - IString() = default; + // Use an explicit constructor instead of `= default` because some older + // compilers (e.g. Apple Clang in older Xcode versions) delete the default + // constructor if there is a const member without an in-class initializer, + // even if that member's type has a default constructor. + // FIXME: Use `= default` once we bump the min clang/Xcode version that + // we support. + IString() : str({nullptr}) {} IString(View v) : str(v) {} From 0005bc8686fa3a639d9b514ebc341c41face684f Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Mon, 18 May 2026 13:15:24 -0700 Subject: [PATCH 108/168] Use C++20 string_view starts_with/ends_with methods. NFC (#8722) Also, remove the extra suffix size check from ends_with. (I could alternatively add this to `starts_with` instead, but I assumes its not a useful optimization?). --- src/support/istring.h | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/support/istring.h b/src/support/istring.h index bbb8028eb0f..bc7d2380dd2 100644 --- a/src/support/istring.h +++ b/src/support/istring.h @@ -119,8 +119,7 @@ struct IString { bool equals(std::string_view other) const { return str.view() == other; } bool startsWith(std::string_view prefix) const { - // TODO: Use C++20 `starts_with`. - return view().substr(0, prefix.size()) == prefix; + return view().starts_with(prefix); } bool startsWith(IString other) const { return startsWith(other.view()); } @@ -130,11 +129,7 @@ struct IString { } bool endsWith(std::string_view suffix) const { - // TODO: Use C++20 `ends_with`. - if (suffix.size() > str.size()) { - return false; - } - return view().substr(str.size() - suffix.size()) == suffix; + return view().ends_with(suffix); } bool endsWith(IString other) const { return endsWith(other.view()); } From b66e6c9ed07ed02a664f59a1e0031864d80f58d0 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Mon, 18 May 2026 14:17:21 -0700 Subject: [PATCH 109/168] Use C++20 requires clauses in TypeNameGeneratorBase (#8552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This addresses the TODOs in src/wasm-type-printing.h by utilizing C++20 concepts and requires clauses. - Replaced the manual SFINAE/macro-based check in TypeNameGeneratorBase with a static_assert(requires { ... }) to ensure subclasses implement getNames correctly. This is cleaner and more robust. - Updated the ModuleTypeNameGenerator constructor to use a requires clause instead of std::enable_if_t for its default constructor, improving readability. - Added #include as required. Verfied locally by remove a `getNames` impl: ``` /usr/local/google/home/sbc/dev/wasm/binaryen/src/wasm-type-printing.h: In instantiation of ‘constexpr void wasm::TypeNameGeneratorBase::assertValidUsage() [with Subclass = wasm::PrintSExpression::TypePrinter]’: /usr/local/google/home/sbc/dev/wasm/binaryen/src/wasm-type-printing.h:36:29: required from ‘wasm::TypeNameGeneratorBase::TypeNameGeneratorBase() [with Subclass = wasm::PrintSExpression::TypePrinter]’ 36 | TypeNameGeneratorBase() { assertValidUsage(); } | ^~~~~~~~~~~~~~~~ /usr/local/google/home/sbc/dev/wasm/binaryen/src/passes/Print.cpp:179:22: required from here 179 | : parent(parent) { | ^ /usr/local/google/home/sbc/dev/wasm/binaryen/src/wasm-type-printing.h:50:7: error: static assertion failed: Derived class must implement getNames 50 | requires(Subclass& s, HeapType ht) { | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 51 | { s.getNames(ht) } -> std::same_as; | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 52 | }, "Derived class must implement getNames"); | ~ /usr/local/google/home/sbc/dev/wasm/binaryen/src/wasm-type-printing.h:50:7: note: ‘false’ evaluates to false ``` --- src/wasm-type-printing.h | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/wasm-type-printing.h b/src/wasm-type-printing.h index 483c48cf0a3..f3a5ebfffbf 100644 --- a/src/wasm-type-printing.h +++ b/src/wasm-type-printing.h @@ -17,6 +17,7 @@ #ifndef wasm_wasm_type_printing_h #define wasm_wasm_type_printing_h +#include #include #include #include @@ -34,9 +35,6 @@ namespace wasm { template struct TypeNameGeneratorBase { TypeNameGeneratorBase() { assertValidUsage(); } - TypeNames getNames(HeapType type) { - WASM_UNREACHABLE("Derived class must implement getNames"); - } HeapType::Printed operator()(HeapType type) { return type.print( [&](HeapType ht) { return static_cast(this)->getNames(ht); }); @@ -48,16 +46,10 @@ template struct TypeNameGeneratorBase { private: constexpr void assertValidUsage() { - // This check current causes a crash on MSVC - // TODO: Convert to C++20 requires check -#if !defined(_MSC_VER) && (!defined(__GNUC__) || __GNUC__ >= 14) - // Check that the subclass provides `getNames` with the correct type. - using Self = TypeNameGeneratorBase; static_assert( - static_cast(&Self::getNames) != - static_cast(&Subclass::getNames), - "Derived class must implement getNames"); -#endif + requires(Subclass& s, HeapType ht) { + { s.getNames(ht) } -> std::same_as; + }, "Derived class must implement getNames"); } }; @@ -123,11 +115,8 @@ struct ModuleTypeNameGenerator ModuleTypeNameGenerator(const Module& wasm, FallbackGenerator& fallback) : wasm(wasm), fallback(fallback) {} - // TODO: Use C++20 `requires` to clean this up. - template - ModuleTypeNameGenerator( - const Module& wasm, - std::enable_if_t>* = nullptr) + ModuleTypeNameGenerator(const Module& wasm) + requires std::is_same_v : ModuleTypeNameGenerator(wasm, defaultGenerator) {} TypeNames getNames(HeapType type) { From 7b7959369c91d82a14f6b9518575687ba803f9e8 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Mon, 18 May 2026 15:59:51 -0700 Subject: [PATCH 110/168] [CI] Build on macos-14 rather than macos-latest (#8721) In emsdk we actually build on macos-13, but sadly there is not macos-13 available in github actions. In doing this I'm trying to get binaryen CI to reproduce the current issue we are seeing in emsdk CI: ``` In file included from /Users/distiller/project/binaryen/main/src/ir/import-names.h:23: /Users/distiller/project/binaryen/main/src/support/name.h:35:12: error: call to implicitly-deleted default constructor of 'wasm::IString' Name() : IString() {} ^ /Users/distiller/project/binaryen/main/src/support/istring.h:72:3: note: explicitly defaulted function was implicitly deleted here IString() = default; ^ /Users/distiller/project/binaryen/main/src/support/istring.h:68:14: note: default constructor of 'IString' is implicitly deleted because field 'str' of const-qualified type 'const wasm::IString::View' would not be initialized const View str; ^ ``` Split out from #8719 --- .github/workflows/ci.yml | 9 ++++++--- .github/workflows/create_release.yml | 12 ++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 209434d4d8b..ed840da7f48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,10 @@ jobs: strategy: matrix: # Test on the oldest support Ubuntu version in addition to `latest`. - os: [ubuntu-22.04, ubuntu-latest, macos-latest, windows-latest, windows-11-arm] + # Build using the oldest support macOS version. For emsdk this is + # currently macos-13, but unfortunately that doesn't exist in github + # actions so we settle for macos-14. + os: [ubuntu-22.04, ubuntu-latest, macos-14, windows-latest, windows-11-arm] steps: - uses: actions/setup-python@v5 with: @@ -74,7 +77,7 @@ jobs: - name: install ninja (macos) run: brew install ninja - if: matrix.os == 'macos-latest' + if: startsWith(matrix.os, 'macos') - name: install ninja (win) run: choco install ninja @@ -94,7 +97,7 @@ jobs: - name: cmake (macos) run: cmake -S . -B out -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=out/install '-DCMAKE_OSX_ARCHITECTURES=x86_64;arm64' - if: matrix.os == 'macos-latest' + if: startsWith(matrix.os, 'macos') - name: cmake (win) # -G "Visual Studio 15 2017" diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 07cbdc8fef5..3dc36f80cbd 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-latest, windows-latest, windows-11-arm] + os: [macos-14, windows-latest, windows-11-arm] defaults: run: shell: bash @@ -27,7 +27,7 @@ jobs: - name: install ninja (macos) run: brew install ninja - if: matrix.os == 'macos-latest' + if: startsWith(matrix.os, 'macos') - name: install ninja (win) run: choco install ninja @@ -40,7 +40,7 @@ jobs: run: | cmake -S . -B out -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=out/install -DCMAKE_OSX_ARCHITECTURES=x86_64 cmake -S . -B out-arm64 -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=out-arm64/install -DCMAKE_OSX_ARCHITECTURES=arm64 - if: matrix.os == 'macos-latest' + if: startsWith(matrix.os, 'macos') - name: cmake (win) # -G "Visual Studio 15 2017" @@ -57,7 +57,7 @@ jobs: - name: build-arm64 run: cmake --build out-arm64 -v --config Release --target install - if: matrix.os == 'macos-latest' + if: startsWith(matrix.os, 'macos') - name: strip run: find out*/install/ -type f -perm -u=x -exec strip -x {} + @@ -83,7 +83,7 @@ jobs: - name: archive-arm64 id: archive-arm64 run: | - OSNAME=$(echo ${{ matrix.os }} | sed 's/-latest//' | sed 's/-11-arm//') + OSNAME=$(echo ${{ matrix.os }} | sed 's/-11-arm//' | sed 's/-14//') VERSION=$GITHUB_REF_NAME PKGNAME="binaryen-$VERSION-arm64-$OSNAME" TARBALL=$PKGNAME.tar.gz @@ -95,7 +95,7 @@ jobs: cmake -E sha256sum $TARBALL > $SHASUM echo "TARBALL=$TARBALL" >> $GITHUB_OUTPUT echo "SHASUM=$SHASUM" >> $GITHUB_OUTPUT - if: ${{ matrix.os == 'macos-latest' || matrix.os == 'windows-11-arm' }} + if: ${{ matrix.os == 'macos-14' || matrix.os == 'windows-11-arm' }} - name: upload tarball uses: softprops/action-gh-release@v1 From 2c2509b5bfe399df400b2185caa370f56e563d32 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 18 May 2026 17:05:55 -0700 Subject: [PATCH 111/168] Fix macos 13 compilation errors with stringstream.view() (#8725) Fixes https://github.com/WebAssembly/binaryen/issues/8723 --- src/passes/StringLifting.cpp | 3 ++- src/support/json.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/passes/StringLifting.cpp b/src/passes/StringLifting.cpp index ac15d2a8614..2c8572c025a 100644 --- a/src/passes/StringLifting.cpp +++ b/src/passes/StringLifting.cpp @@ -74,7 +74,8 @@ struct StringLifting : public Pass { if (!valid) { Fatal() << "Bad string to lift: " << wtf8; } - importedStrings[global->name] = wtf16.view(); + // TODO: Use wtf16.view() once we have C++20. + importedStrings[global->name] = wtf16.str(); found = true; } } diff --git a/src/support/json.cpp b/src/support/json.cpp index 94f3df082e7..7858cfba319 100644 --- a/src/support/json.cpp +++ b/src/support/json.cpp @@ -39,7 +39,8 @@ void Value::stringify(std::ostream& os, bool pretty, int indent) { [[maybe_unused]] bool valid = wasm::String::convertWTF8ToWTF16(wtf16, getIString().view()); assert(valid); - wasm::String::printEscapedJSON(os, wtf16.view()); + // TODO: Use wtf16.view() once we have C++20. + wasm::String::printEscapedJSON(os, wtf16.str()); return; } case Array: { From 5fcc1af12ae19032339ee5e56889e08b6912e8cb Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 19 May 2026 14:37:04 -0700 Subject: [PATCH 112/168] [wasm-split] Tidy up indirectCallsToSecondaryFunctions (NFC) (#8726) This avoids computing repeated expression multiple times and removes an unnecessary if condition. --- src/ir/module-splitting.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 9659121d6fe..6b233c5e624 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -997,18 +997,17 @@ void ModuleSplitter::indirectCallsToSecondaryFunctions() { // Return if the current module is the same module as the call's target, // because we don't need a call_indirect within the same module. Module* currModule = getModule(); - if (currModule != &parent.primary && - parent.secondaries.at(parent.funcToSecondaryIndex.at(curr->target)) - .get() == currModule) { + Module* calleeModule = + parent.secondaries.at(parent.funcToSecondaryIndex.at(curr->target)) + .get(); + if (currModule == calleeModule) { return; } - Builder builder(*getModule()); - Index secIndex = parent.funcToSecondaryIndex.at(curr->target); - auto* func = parent.secondaries.at(secIndex)->getFunction(curr->target); + Builder builder(*currModule); + auto* func = calleeModule->getFunction(curr->target); auto tableSlot = parent.tableManager.getSlot(curr->target, func->type.getHeapType()); - replaceCurrent( builder.makeCallIndirect(tableSlot.tableName, tableSlot.makeExpr(parent.primary), @@ -1151,9 +1150,9 @@ void ModuleSplitter::setupTablePatching() { secondary.getGlobalOrNull(tableManager.activeBase.global); if (!secondaryGlobal) { secondaryGlobal = ModuleUtils::copyGlobal(primaryGlobal, secondary); + makeImportExport( + *primaryGlobal, *secondaryGlobal, "global", ExternalKind::Global); } - makeImportExport( - *primaryGlobal, *secondaryGlobal, "global", ExternalKind::Global); assert(tableManager.activeTableSegments.size() == 1 && "Unexpected number of segments with non-const base"); From c974a59509729273425d562356accad27cd123ea Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 19 May 2026 17:51:54 -0700 Subject: [PATCH 113/168] [wasm-split] Share active table with caller modules (#8728) The way we currently share the active table with secondary modules https://github.com/WebAssembly/binaryen/blob/2c2509b5bfe399df400b2185caa370f56e563d32/src/ir/module-splitting.cpp#L1122-L1156 has missed an important case in multi-split. This is one of the unexpected consequences of #8688. Here the `secondary` module in this loop is a secondary module whose functions have been put in the active table, mostly by `getTrampoline` function. When we have a call from `$A` to `$B` and `$B` is split, `getTrampoline` puts `$B` on the active table, which will be converted to a placeholder in `setupTablePatcing`. So the code above share the active table (and its base global, if exists) with `$B`'s module. The problem is we didn't share the active global with `$A`'s module. In a two-way split this is not a problem because `$A` is in the primary module. But in multi-split, `$A`'s module itself is another secondary module, and this module needs to access the active table because it should call the placeholder for `$B`. This does it in `indirectCallsToSecondaryFunctions`. --- I wish we can do the active table sharing for callee modules in `indirectCallsToSecondaryFunctions` too and get done with it, but we still need to share it in `shareImportableItems` for secondary modules that have functions to be replaced in the active table, because there is a case of existing secondary function names in the active table. See `KEEP-NONE` RUN line in the test below: https://github.com/WebAssembly/binaryen/blob/main/test/lit/wasm-split/basic.wast In this test, we reuse the existing table as the active table, which already has `$foo` in its element section. And that `foo` will be split. Because both `bar` and `foo` are split there is no cross-module call between them to process in `indirectCallsToSecondaryFunctions`. This won't be converted to trampolines because https://github.com/WebAssembly/binaryen/blob/5fcc1af12ae19032339ee5e56889e08b6912e8cb/src/ir/module-splitting.cpp#L947-L958 and because there is no trampoline, this doesn't get to have a call instruction from primary->secondary, but we still need to share the active table with the secondary module, and we can't handle it in `indirectCallsToSecondaryFunctions`. --- This fixes the error discussed in https://github.com/WebAssembly/binaryen/pull/8711#discussion_r3251422502. --- src/ir/module-splitting.cpp | 73 +++++++++++++++------------ test/lit/wasm-split/multi-split2.wast | 67 ++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 32 deletions(-) create mode 100644 test/lit/wasm-split/multi-split2.wast diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 6b233c5e624..af31ff20b37 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -342,6 +342,8 @@ struct ModuleSplitter { // Map from original secondary function name to its trampoline std::unordered_map trampolineMap; + void shareActiveTable(Module* secondary); + // Initialization helpers static std::unique_ptr initSecondary(const Module& primary); static std::unordered_map @@ -590,6 +592,32 @@ Name ModuleSplitter::getTrampoline(Name funcName) { return trampoline; } +void ModuleSplitter::shareActiveTable(Module* secondary) { + assert(tableManager.activeTable); + auto secondaryTable = + secondary->getTableOrNull(tableManager.activeTable->name); + if (secondaryTable) { + // In case it's already in the secondary module, sync the initial/max + secondaryTable->initial = tableManager.activeTable->initial; + secondaryTable->max = tableManager.activeTable->max; + } else { + secondaryTable = + ModuleUtils::copyTable(tableManager.activeTable, *secondary); + makeImportExport( + *tableManager.activeTable, *secondaryTable, "table", ExternalKind::Table); + } + if (tableManager.activeBase.global) { + auto* primaryGlobal = primary.getGlobal(tableManager.activeBase.global); + auto* secondaryGlobal = + secondary->getGlobalOrNull(tableManager.activeBase.global); + if (!secondaryGlobal) { + secondaryGlobal = ModuleUtils::copyGlobal(primaryGlobal, *secondary); + makeImportExport( + *primaryGlobal, *secondaryGlobal, "global", ExternalKind::Global); + } + } +} + void ModuleSplitter::thunkExportedSecondaryFunctions() { // Update exports of secondary functions in the primary module to export // wrapper functions that indirectly call the secondary functions. We are @@ -988,6 +1016,7 @@ void ModuleSplitter::indirectCallsToSecondaryFunctions() { // corresponding table indices instead. struct CallIndirector : public PostWalker { ModuleSplitter& parent; + std::unordered_set activeTableUsingSecondaries; CallIndirector(ModuleSplitter& parent) : parent(parent) {} void visitCall(Call* curr) { // Return if the call's target is not in one of the secondary module. @@ -1014,6 +1043,12 @@ void ModuleSplitter::indirectCallsToSecondaryFunctions() { curr->operands, func->type.getHeapType(), curr->isReturn)); + + // Share the active table with the current module (caller). We share the + // active table with with calleeModule later in setupTablePathing. + if (currModule != &parent.primary) { + activeTableUsingSecondaries.insert(currModule); + } } }; CallIndirector callIndirector(*this); @@ -1021,6 +1056,10 @@ void ModuleSplitter::indirectCallsToSecondaryFunctions() { for (auto& secondaryPtr : secondaries) { callIndirector.walkModule(secondaryPtr.get()); } + + for (auto* secondary : callIndirector.activeTableUsingSecondaries) { + shareActiveTable(secondary); + } } void ModuleSplitter::exportImportCalledPrimaryFunctions() { @@ -1120,40 +1159,10 @@ void ModuleSplitter::setupTablePatching() { for (auto& [secondaryPtr, replacedElems] : moduleToReplacedElems) { Module& secondary = *secondaryPtr; - // Import and export the active table if necessary. Unless we use an - // existing table as an active table (e.g. because reference-types is - // disabled) and that table was already being used by an existing indirect - // call, shareImportableItems wasn't able to mark it as used in secondaries, - // so we should export and import the active table here. - auto secondaryTable = - secondary.getTableOrNull(tableManager.activeTable->name); - if (secondaryTable) { - // In case it's already in the secondary module, sync the initial/max - secondaryTable->initial = tableManager.activeTable->initial; - secondaryTable->max = tableManager.activeTable->max; - } else { - secondaryTable = - ModuleUtils::copyTable(tableManager.activeTable, secondary); - makeImportExport(*tableManager.activeTable, - *secondaryTable, - "table", - ExternalKind::Table); - } + shareActiveTable(&secondary); + auto* secondaryTable = secondary.getTable(tableManager.activeTable->name); if (tableManager.activeBase.global) { - // Import and export the active table's base global if necessary. Unless - // the base global was already being used elsewhere in secondaries, - // shareImportableItems wasn't able to mark it as used in secondaries, so - // we should export and import it here. - auto* primaryGlobal = primary.getGlobal(tableManager.activeBase.global); - auto* secondaryGlobal = - secondary.getGlobalOrNull(tableManager.activeBase.global); - if (!secondaryGlobal) { - secondaryGlobal = ModuleUtils::copyGlobal(primaryGlobal, secondary); - makeImportExport( - *primaryGlobal, *secondaryGlobal, "global", ExternalKind::Global); - } - assert(tableManager.activeTableSegments.size() == 1 && "Unexpected number of segments with non-const base"); assert(secondary.tables.size() == 1 && secondary.elementSegments.empty()); diff --git a/test/lit/wasm-split/multi-split2.wast b/test/lit/wasm-split/multi-split2.wast new file mode 100644 index 00000000000..73578fd814e --- /dev/null +++ b/test/lit/wasm-split/multi-split2.wast @@ -0,0 +1,67 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: wasm-split -all -g --multi-split %s --manifest %S/multi-split.wast.manifest --out-prefix=%t -o %t.wasm +;; RUN: wasm-dis %t.wasm | filecheck %s --check-prefix=PRIMARY +;; RUN: wasm-dis %t1.wasm | filecheck %s --check-prefix=MOD1 +;; RUN: wasm-dis %t2.wasm | filecheck %s --check-prefix=MOD2 +;; RUN: wasm-dis %t3.wasm | filecheck %s --check-prefix=MOD3 + +;; A regresion test for the case the active table was not correctly shared with +;; MOD1. Because func $A is not called by any function, MOD1 is a secondary +;; module who only acts as a caller. + +(module + ;; MOD1: (type $0 (func)) + + ;; MOD1: (import "primary" "table" (table $timport$0 2 funcref)) + + ;; MOD1: (func $A + ;; MOD1-NEXT: (call_indirect (type $0) + ;; MOD1-NEXT: (i32.const 0) + ;; MOD1-NEXT: ) + ;; MOD1-NEXT: (call_indirect (type $0) + ;; MOD1-NEXT: (i32.const 1) + ;; MOD1-NEXT: ) + ;; MOD1-NEXT: ) + (func $A + (call $B) + (call $C) + ) + + ;; MOD2: (type $0 (func)) + + ;; MOD2: (import "primary" "table" (table $timport$0 2 funcref)) + + ;; MOD2: (elem $0 (i32.const 0) $B) + + ;; MOD2: (func $B + ;; MOD2-NEXT: (call_indirect (type $0) + ;; MOD2-NEXT: (i32.const 1) + ;; MOD2-NEXT: ) + ;; MOD2-NEXT: ) + (func $B + (call $C) + ) + + ;; MOD3: (type $0 (func)) + + ;; MOD3: (import "primary" "table" (table $timport$0 2 funcref)) + + ;; MOD3: (elem $0 (i32.const 1) $C) + + ;; MOD3: (func $C + ;; MOD3-NEXT: ) + (func $C + ) +) +;; PRIMARY: (type $0 (func)) + +;; PRIMARY: (import "placeholder.2" "0" (func $placeholder_0)) + +;; PRIMARY: (import "placeholder.3" "1" (func $placeholder_1)) + +;; PRIMARY: (table $0 2 funcref) + +;; PRIMARY: (elem $0 (i32.const 0) $placeholder_0 $placeholder_1) + +;; PRIMARY: (export "table" (table $0)) From c82a630d5ad354eb2225e40b2563489c4b70524f Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 19 May 2026 18:43:11 -0700 Subject: [PATCH 114/168] Skip folding branches of unreachable If (#8724) OptimizeInstructions generally skips optimizing unreachable code, leaving that to DCE instead. But it did not have this check before folding identical instructions out of If arms. The fuzzer found a case where cont.new instructions were hoisted out of an unreachable If but not refinalized, resulting in the concretely-typed cont.new having an unreachable child. This caused an assertion failure in the validator. Fix the root problem by skipping this optimization for unreachable Ifs, but also fix the validator so that it will no longer crash on such invalid IR. --- src/passes/OptimizeInstructions.cpp | 2 +- src/wasm/wasm-validator.cpp | 3 +- test/gtest/validator.cpp | 25 +++++++++++++++ ...egalize-js-interface-exported-helpers.wast | 6 ++-- test/lit/passes/optimize-instructions-gc.wast | 12 ++++--- .../lit/passes/optimize-instructions-mvp.wast | 31 +++++++++++++++++++ 6 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index 9ea8a7aa982..e61deed16aa 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -5761,7 +5761,7 @@ struct OptimizeInstructions } } - if (!neverFold) { + if (!neverFold && curr->condition->type != Type::unreachable) { // Identical code on both arms can be folded out, e.g. // // (select diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index b099926112c..64b67e51edd 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -4340,7 +4340,8 @@ void FunctionValidator::visitContNew(ContNew* curr) { auto cont = curr->type.getHeapType().getContinuation(); assert(cont.type.isSignature()); - shouldBeTrue(HeapType::isSubType(curr->func->type.getHeapType(), cont.type), + shouldBeTrue(curr->func->type.isRef() && + HeapType::isSubType(curr->func->type.getHeapType(), cont.type), curr, "cont.new function reference must be a subtype"); } diff --git a/test/gtest/validator.cpp b/test/gtest/validator.cpp index e984945226c..906e9369464 100644 --- a/test/gtest/validator.cpp +++ b/test/gtest/validator.cpp @@ -90,3 +90,28 @@ TEST(ValidatorTest, UnreachableCastDescEq) { WasmValidator::FlagValues::Globally | WasmValidator::FlagValues::Quiet; EXPECT_FALSE(WasmValidator{}.validate(func.get(), module, flags)); } + +TEST(ValidatorTest, ContNewUnreachable) { + Module module; + module.features = FeatureSet::All; + Builder builder(module); + + auto sig = Signature(Type::none, Type::none); + module.addFunction(builder.makeFunction( + "f", {}, Signature(Type::none, Type::none), {}, builder.makeUnreachable())); + + auto contType = HeapType(Continuation(sig)); + auto contRefType = Type(contType, NonNullable); + + // Create a cont.new with a concrete type despite an unreachable child. This + // is not valid, but we should not crash while validating it. + auto* contNew = builder.makeContNew(contType, builder.makeUnreachable()); + contNew->type = contRefType; + + auto testFunc = builder.makeFunction( + "test", {}, Signature(Type::none, contRefType), {}, contNew); + + auto flags = + WasmValidator::FlagValues::Globally | WasmValidator::FlagValues::Quiet; + EXPECT_FALSE(WasmValidator{}.validate(testFunc.get(), module, flags)); +} diff --git a/test/lit/passes/legalize-js-interface-exported-helpers.wast b/test/lit/passes/legalize-js-interface-exported-helpers.wast index bed768f1a0d..18b93cfc987 100644 --- a/test/lit/passes/legalize-js-interface-exported-helpers.wast +++ b/test/lit/passes/legalize-js-interface-exported-helpers.wast @@ -8,8 +8,6 @@ (module (export "get_i64" (func $get_i64)) (import "env" "imported" (func $imported (result i64))) - (export "__set_temp_ret" (func $__set_temp_ret)) - (export "__get_temp_ret" (func $__get_temp_ret)) ;; CHECK: (type $0 (func (result i32))) ;; CHECK: (type $1 (func (result i64))) @@ -20,6 +18,10 @@ ;; CHECK: (export "get_i64" (func $legalstub$get_i64)) + ;; CHECK: (export "__set_temp_ret" (func $__set_temp_ret)) + (export "__set_temp_ret" (func $__set_temp_ret)) + ;; CHECK: (export "__get_temp_ret" (func $__get_temp_ret)) + (export "__get_temp_ret" (func $__get_temp_ret)) ;; CHECK: (func $get_i64 (result i64) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (call $legalfunc$imported) diff --git a/test/lit/passes/optimize-instructions-gc.wast b/test/lit/passes/optimize-instructions-gc.wast index 9dc52715724..c8532cf7ae4 100644 --- a/test/lit/passes/optimize-instructions-gc.wast +++ b/test/lit/passes/optimize-instructions-gc.wast @@ -3695,13 +3695,15 @@ ;; CHECK: (func $comp-i31-struct-unreachable-if (type $4) ;; CHECK-NEXT: (ref.eq - ;; CHECK-NEXT: (ref.i31 - ;; CHECK-NEXT: (if (result i32) - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (if (result (ref i31)) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (ref.i31 ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (else + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (ref.i31 ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) diff --git a/test/lit/passes/optimize-instructions-mvp.wast b/test/lit/passes/optimize-instructions-mvp.wast index a301cc6858c..021b868bcc1 100644 --- a/test/lit/passes/optimize-instructions-mvp.wast +++ b/test/lit/passes/optimize-instructions-mvp.wast @@ -16046,6 +16046,37 @@ ) ) ) + ;; CHECK: (func $ternary-identical-arms-if-unreachable (param $x i32) (param $y i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ternary-identical-arms-if-unreachable (param $x i32) (param $y i32) + (drop + ;; Leave this to DCE instead of optimizing. + (if (result i32) + (unreachable) + (then + (i32.eqz (local.get $x)) + ) + (else + (i32.eqz (local.get $y)) + ) + ) + ) + ) ;; CHECK: (func $ternary-identical-arms-type-change (param $x f64) (param $y f64) (param $z i32) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (f32.demote_f64 From 8e2e403604b1ed11b53f7a88b233f5994413635e Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 20 May 2026 08:56:02 -0700 Subject: [PATCH 115/168] Run non-nullable local fixups when reducing functions (#8730) It's not entirely clear how FunctionReplacer was producing IR that with invalid nullable locals, but this was observed in practice in issue #8720. Fix it by running the non-nullable local fixups after the pass. Fixes #8720. --- src/tools/wasm-reduce/wasm-reduce.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tools/wasm-reduce/wasm-reduce.cpp b/src/tools/wasm-reduce/wasm-reduce.cpp index 04676109acf..0dd8e0dda99 100644 --- a/src/tools/wasm-reduce/wasm-reduce.cpp +++ b/src/tools/wasm-reduce/wasm-reduce.cpp @@ -1089,7 +1089,6 @@ struct Reducer struct FunctionReplacer : public WalkerPass> { bool isFunctionParallel() override { return true; } - bool requiresNonNullableLocalFixups() override { return false; } std::unique_ptr create() override { return std::make_unique(); }; From dd958c220e19c63054d2122037d9dec04ddf21e6 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 20 May 2026 09:02:11 -0700 Subject: [PATCH 116/168] wasm-ctor-eval: Remove the start function when we succeed (#8702) If we remove even one ctor that we were asked to, then we ran the start function, and we evalled it too (if it trapped or such, we would not have managed to eval that one ctor). But we left the start function there, which meant it would run again, even though we evalled it and baked its results into the wasm already. Basically, the start function is like a ctor, and after evalling it successfully we need to empty it out, so it doesn't re-execute, or else we would be changing the program behavior. --- src/tools/wasm-ctor-eval.cpp | 13 ++++ test/lit/ctor-eval/gc-cycle.wast | 33 +++++----- test/lit/ctor-eval/start-bad-2.wast | 65 +++++++++++++++++++ test/lit/ctor-eval/start-bad.wast | 62 ++++++++++++++++++ test/lit/ctor-eval/start-partrun.wast | 89 ++++++++++++++++++++++++++ test/lit/ctor-eval/start-rerun.wast | 92 +++++++++++++++++++++++++++ test/lit/ctor-eval/start.wast | 68 ++++++++++++++++++++ 7 files changed, 406 insertions(+), 16 deletions(-) create mode 100644 test/lit/ctor-eval/start-bad-2.wast create mode 100644 test/lit/ctor-eval/start-bad.wast create mode 100644 test/lit/ctor-eval/start-partrun.wast create mode 100644 test/lit/ctor-eval/start-rerun.wast create mode 100644 test/lit/ctor-eval/start.wast diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index 93e8d4ae51f..c8fe1d5e385 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -312,9 +312,22 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { linkedInstances.swap(linkedInstances_); } + bool firstApplication = true; + // Called when we want to apply the current state of execution to the Module. // Until this is called the Module is never changed. void applyToModule() { + if (firstApplication) { + // The first time we apply things to the module, we can remove the start + // function: we evalled it successfully, if we got to here (and we must + // not execute it again later, which would mean it runs twice). We do not + // do this after the first application because we start to build up a new + // start function with the things we need, unrelated to the original one + // (see addStartFixup). + wasm->start = Name(); + firstApplication = false; + } + clearApplyState(); // If nothing was ever written to memories then there is nothing to update. diff --git a/test/lit/ctor-eval/gc-cycle.wast b/test/lit/ctor-eval/gc-cycle.wast index 0af9fbf10b3..faf26c4adec 100644 --- a/test/lit/ctor-eval/gc-cycle.wast +++ b/test/lit/ctor-eval/gc-cycle.wast @@ -1158,7 +1158,9 @@ ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) (module - ;; The start function already exists here. We must prepend to it. + ;; The start function already exists here. We must *not* prepend to it: it gets + ;; evalled away too (we execute it before the first ctor, and we should not + ;; eval those contents twice). ;; CHECK: (type $A (struct (field (mut (ref null $A))) (field i32))) (type $A (struct (field (mut (ref null $A))) (field i32))) @@ -1178,11 +1180,6 @@ ;; CHECK: (global $b (mut (ref null $A)) (ref.null none)) (global $b (mut (ref null $A)) (ref.null $A)) - ;; CHECK: (export "test" (func $test_3)) - - ;; CHECK: (export "keepalive" (func $keepalive)) - - ;; CHECK: (start $start) (start $start) (func $test (export "test") @@ -1201,6 +1198,12 @@ ) ) + ;; CHECK: (export "test" (func $test_4)) + + ;; CHECK: (export "keepalive" (func $keepalive)) + + ;; CHECK: (start $start_3) + ;; CHECK: (func $keepalive (type $2) (result i32) ;; CHECK-NEXT: (i32.add ;; CHECK-NEXT: (struct.get $A 1 @@ -1222,15 +1225,6 @@ ) ) - ;; CHECK: (func $start (type $1) - ;; CHECK-NEXT: (struct.set $A 0 - ;; CHECK-NEXT: (global.get $ctor-eval$global_4) - ;; CHECK-NEXT: (global.get $ctor-eval$global_4) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (global.set $b - ;; CHECK-NEXT: (global.get $a) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) (func $start (global.set $b (global.get $a) @@ -1238,7 +1232,14 @@ ) ) -;; CHECK: (func $test_3 (type $1) +;; CHECK: (func $start_3 (type $1) +;; CHECK-NEXT: (struct.set $A 0 +;; CHECK-NEXT: (global.get $ctor-eval$global_4) +;; CHECK-NEXT: (global.get $ctor-eval$global_4) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $test_4 (type $1) ;; CHECK-NEXT: (local $a (ref $A)) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) diff --git a/test/lit/ctor-eval/start-bad-2.wast b/test/lit/ctor-eval/start-bad-2.wast new file mode 100644 index 00000000000..2b38a836a62 --- /dev/null +++ b/test/lit/ctor-eval/start-bad-2.wast @@ -0,0 +1,65 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-ctor-eval --ctors=test --kept-exports=test --quiet -all -S -o - | filecheck %s + +;; We fail to eval away test (due to infinite recursion). As a result, we do +;; not update either global - not the one it modifies or even the one that the +;; start function modifies, and the start function remains as the start. +;; TODO: We could perhaps eval away the start in such cases, even when nothing +;; else gets optimized. + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (type $1 (func (result i32))) + + ;; CHECK: (global $global1 (mut i32) (i32.const 0)) + (global $global1 (mut i32) (i32.const 0)) + + ;; CHECK: (global $global2 (mut i32) (i32.const 0)) + (global $global2 (mut i32) (i32.const 0)) + + ;; CHECK: (export "test" (func $test)) + + ;; CHECK: (export "keepalive" (func $keepalive)) + + ;; CHECK: (start $start) + (start $start) + + ;; CHECK: (func $start (type $0) + ;; CHECK-NEXT: (global.set $global2 + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $start + (global.set $global2 + (i32.const 42) + ) + ) + + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: (global.set $global1 + ;; CHECK-NEXT: (i32.const 1337) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (export "test") + (call $test) + (global.set $global1 + (i32.const 1337) + ) + ) + + ;; CHECK: (func $keepalive (type $1) (result i32) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $global1) + ;; CHECK-NEXT: (global.get $global2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $keepalive (export "keepalive") (result i32) + ;; Keep the globals alive to show changes. + (i32.add + (global.get $global1) + (global.get $global2) + ) + ) +) diff --git a/test/lit/ctor-eval/start-bad.wast b/test/lit/ctor-eval/start-bad.wast new file mode 100644 index 00000000000..bf9e2d7c900 --- /dev/null +++ b/test/lit/ctor-eval/start-bad.wast @@ -0,0 +1,62 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-ctor-eval --ctors=test --kept-exports=test --quiet -all -S -o - | filecheck %s + +;; The start function traps here, so we cannot eval anything. None of the +;; globals should change. + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (type $1 (func (result i32))) + + ;; CHECK: (global $global1 (mut i32) (i32.const 0)) + (global $global1 (mut i32) (i32.const 0)) + + ;; CHECK: (global $global2 (mut i32) (i32.const 0)) + (global $global2 (mut i32) (i32.const 0)) + + ;; CHECK: (export "test" (func $test)) + + ;; CHECK: (export "keepalive" (func $keepalive)) + + ;; CHECK: (start $start) + (start $start) + + ;; CHECK: (func $start (type $0) + ;; CHECK-NEXT: (global.set $global2 + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $start + (global.set $global2 + (i32.const 42) + ) + (unreachable) + ) + + ;; CHECK: (func $test (type $0) + ;; CHECK-NEXT: (global.set $global1 + ;; CHECK-NEXT: (i32.const 1337) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (export "test") + (global.set $global1 + (i32.const 1337) + ) + ) + + ;; CHECK: (func $keepalive (type $1) (result i32) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $global1) + ;; CHECK-NEXT: (global.get $global2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $keepalive (export "keepalive") (result i32) + ;; Keep the globals alive to show changes. + (i32.add + (global.get $global1) + (global.get $global2) + ) + ) +) diff --git a/test/lit/ctor-eval/start-partrun.wast b/test/lit/ctor-eval/start-partrun.wast new file mode 100644 index 00000000000..064c8a13682 --- /dev/null +++ b/test/lit/ctor-eval/start-partrun.wast @@ -0,0 +1,89 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-ctor-eval --ctors=ok,trap,s --kept-exports=s,trap --quiet -all -S -o - | filecheck %s + +;; Similar to start-rerun.wast, but rather than $s executing twice, the second +;; time fails to eval, so only the usage in the start function ends up baked in. +;; Specifically, the start function evals fine, as does $ok, but $trap stops us +;; before we get to $s. The start function's increment of the global will leave +;; it as 1, and $ok adds 1000, so it ends up at 1001 but not 1002. + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (shared (struct (field (mut (ref null $B))))))) + (type $A (sub (shared (struct (field (mut (ref null $B))))))) + ;; CHECK: (type $B (sub (shared (struct (field (mut (ref null (shared any)))))))) + (type $B (sub (shared (struct (field (mut (ref null (shared any)))))))) + ) + + (global $global (ref $A) (struct.new $A + (struct.new_default $B) + )) + + ;; A counter to show how $s executes twice (each time adding 1), and $t once + ;; (adding 1000). + ;; CHECK: (type $2 (func)) + + ;; CHECK: (global $ctor-eval$global (ref (exact $A)) (struct.new $A + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: )) + + ;; CHECK: (global $ctor-eval$global_3 (ref (exact $B)) (struct.new $B + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: )) + + ;; CHECK: (global $counter (mut i32) (i32.const 1001)) + (global $counter (mut i32) (i32.const 0)) + + ;; CHECK: (export "s" (func $s)) + (export "s" (func $s)) + + (export "ok" (func $ok)) + + ;; CHECK: (export "trap" (func $trap)) + (export "trap" (func $trap)) + + (start $s) + + ;; CHECK: (start $start) + + ;; CHECK: (func $s (type $2) + ;; CHECK-NEXT: (global.set $counter + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $counter) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $s + (global.set $counter + (i32.add + (global.get $counter) + (i32.const 1) + ) + ) + ) + + (func $ok + (global.set $counter + (i32.add + (global.get $counter) + (i32.const 1000) + ) + ) + ) + + ;; CHECK: (func $trap (type $2) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $trap + (unreachable) + ) +) + +;; CHECK: (func $start (type $2) +;; CHECK-NEXT: (struct.set $A 0 +;; CHECK-NEXT: (global.get $ctor-eval$global) +;; CHECK-NEXT: (global.get $ctor-eval$global_3) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) diff --git a/test/lit/ctor-eval/start-rerun.wast b/test/lit/ctor-eval/start-rerun.wast new file mode 100644 index 00000000000..3acc802c701 --- /dev/null +++ b/test/lit/ctor-eval/start-rerun.wast @@ -0,0 +1,92 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-ctor-eval --ctors=t,s --kept-exports=t,s --quiet -all -S -o - | filecheck %s + +;; A corner case where we export the start function and consider it a ctor. +;; That it executes twice should not cause an internal error. +;; +;; The $counter shows how $s executes twice (each time adding 1), and $t once +;; (adding 1000). + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (shared (struct (field (mut (ref null $B))))))) + (type $A (sub (shared (struct (field (mut (ref null $B))))))) + ;; CHECK: (type $B (sub (shared (struct (field (mut (ref null (shared any)))))))) + (type $B (sub (shared (struct (field (mut (ref null (shared any)))))))) + ) + + (global $global (ref $A) (struct.new $A + (struct.new_default $B) + )) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (type $3 (func (result i32))) + + ;; CHECK: (global $ctor-eval$global_4 (ref (exact $A)) (struct.new $A + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: )) + + ;; CHECK: (global $ctor-eval$global_5 (ref (exact $B)) (struct.new $B + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: )) + + ;; CHECK: (global $counter (mut i32) (i32.const 1002)) + (global $counter (mut i32) (i32.const 0)) + + (export "s" (func $s)) + + (export "t" (func $t)) + + (start $s) + + (func $s + (global.set $counter + (i32.add + (global.get $counter) + (i32.const 1) + ) + ) + ) + + (func $t + (global.set $counter + (i32.add + (global.get $counter) + (i32.const 1000) + ) + ) + ) + + ;; CHECK: (export "keepalive" (func $keepalive)) + + ;; CHECK: (export "s" (func $s_5)) + + ;; CHECK: (export "t" (func $t_4)) + + ;; CHECK: (start $start) + + ;; CHECK: (func $keepalive (type $3) (result i32) + ;; CHECK-NEXT: (global.get $counter) + ;; CHECK-NEXT: ) + (func $keepalive (export "keepalive") (result i32) + ;; Keep the counter alive to show the result. + (global.get $counter) + ) +) + +;; CHECK: (func $start (type $2) +;; CHECK-NEXT: (struct.set $A 0 +;; CHECK-NEXT: (global.get $ctor-eval$global_4) +;; CHECK-NEXT: (global.get $ctor-eval$global_5) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $t_4 (type $2) +;; CHECK-NEXT: (nop) +;; CHECK-NEXT: ) + +;; CHECK: (func $s_5 (type $2) +;; CHECK-NEXT: (nop) +;; CHECK-NEXT: ) diff --git a/test/lit/ctor-eval/start.wast b/test/lit/ctor-eval/start.wast new file mode 100644 index 00000000000..31f787e3dce --- /dev/null +++ b/test/lit/ctor-eval/start.wast @@ -0,0 +1,68 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-ctor-eval --ctors=test --kept-exports=test --quiet -all -S -o - | filecheck %s + +;; This code does the following: +;; +;; * start writes global2, and traps if global1 is set. +;; * test sets global1. +;; +;; We must eval away the start function when we eval away the other. That is, +;; there should be no start function afterwards. Otherwise, if it remains as the +;; start, it will trap when it reads the modified global. +;; +;; While doing so we must apply the changes of the start function, to global2. +;; So both globals end up modified. + +(module + ;; CHECK: (type $0 (func (result i32))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (global $global1 (mut i32) (i32.const 1337)) + (global $global1 (mut i32) (i32.const 0)) + + ;; CHECK: (global $global2 (mut i32) (i32.const 42)) + (global $global2 (mut i32) (i32.const 0)) + + (start $start) + + (func $start + (global.set $global2 + (i32.const 42) + ) + (if + (global.get $global1) + (then + (unreachable) + ) + ) + ) + + (func $test (export "test") + (global.set $global1 + (i32.const 1337) + ) + ) + + ;; CHECK: (export "test" (func $test_3)) + + ;; CHECK: (export "keepalive" (func $keepalive)) + + ;; CHECK: (func $keepalive (type $0) (result i32) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (global.get $global1) + ;; CHECK-NEXT: (global.get $global2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $keepalive (export "keepalive") (result i32) + ;; Keep the globals alive to show changes. + (i32.add + (global.get $global1) + (global.get $global2) + ) + ) +) + +;; CHECK: (func $test_3 (type $1) +;; CHECK-NEXT: (nop) +;; CHECK-NEXT: ) From 7ac78f76ffd988605b11f5493a1074480d92a86c Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 20 May 2026 09:02:25 -0700 Subject: [PATCH 117/168] Fuzzer: Export tags (#8729) This is the only exportable thing we did not already export. --- src/tools/fuzzing.h | 3 + src/tools/fuzzing/fuzzing.cpp | 11 +++ ...e-to-fuzz_all-features_metrics_noprint.txt | 85 ++++++++++--------- 3 files changed, 59 insertions(+), 40 deletions(-) diff --git a/src/tools/fuzzing.h b/src/tools/fuzzing.h index e06160332b0..803e13d5d0b 100644 --- a/src/tools/fuzzing.h +++ b/src/tools/fuzzing.h @@ -376,6 +376,9 @@ class TranslateToFuzzReader { bool isValidPublicType(Type type) { return publicTypeValidator.isValidPublicType(type); } + bool isValidPublicType(HeapType type) { + return publicTypeValidator.isValidPublicType(type); + } // Function operations. The main processFunctions() loop will call addFunction // as well as modFunction(). diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index 91531904a0d..759061da88f 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -846,6 +846,17 @@ void TranslateToFuzzReader::setupTags() { jsTag->base = "jstag"; wasm.addTag(std::move(jsTag)); } + + // Export some tags, sometimes. + if (!preserveImportsAndExports) { + for (auto& tag : wasm.tags) { + if (isValidPublicType(tag->type) && oneIn(2)) { + auto exportName = Names::getValidExportName(wasm, tag->name); + wasm.addExport( + Builder::makeExport(exportName, tag->name, ExternalKind::Tag)); + } + } + } } void TranslateToFuzzReader::addTag() { diff --git a/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt b/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt index 7f56b028dc0..961f8e1e8bc 100644 --- a/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt +++ b/test/passes/translate-to-fuzz_all-features_metrics_noprint.txt @@ -1,50 +1,55 @@ Metrics total - [exports] : 13 - [funcs] : 18 + [exports] : 10 + [funcs] : 5 [globals] : 2 [imports] : 13 [memories] : 1 [memory-data] : 16 - [table-data] : 3 + [table-data] : 2 [tables] : 2 - [tags] : 2 - [total] : 525 - [vars] : 51 - ArrayNewFixed : 2 - AtomicFence : 1 - Binary : 27 - Block : 97 - Break : 9 - Call : 17 - CallRef : 1 - Const : 101 - Drop : 8 - GlobalGet : 48 - GlobalSet : 44 - If : 29 - LocalGet : 15 - LocalSet : 10 - Loop : 4 + [tags] : 3 + [total] : 704 + [vars] : 26 + ArrayNewFixed : 6 + AtomicFence : 3 + Binary : 30 + Block : 130 + BrOn : 6 + Break : 23 + Call : 30 + CallRef : 2 + Const : 103 + Drop : 10 + GlobalGet : 44 + GlobalSet : 42 + I31Get : 3 + If : 39 + Load : 6 + LocalGet : 25 + LocalSet : 27 + Loop : 16 MemoryInit : 1 Nop : 7 - RefAs : 1 - RefEq : 2 - RefFunc : 7 - RefI31 : 6 - RefNull : 5 - Return : 4 - SIMDExtract : 2 - Select : 2 - Store : 1 - StringConst : 5 - StringEncode : 1 + Pop : 6 + RefEq : 1 + RefFunc : 11 + RefI31 : 10 + RefNull : 10 + RefTest : 7 + Return : 3 + Select : 1 + Store : 2 + StringConst : 7 StringEq : 1 - StringMeasure : 1 - StructNew : 7 - TableSet : 1 - TryTable : 2 - TupleExtract : 1 - TupleMake : 3 - Unary : 29 - Unreachable : 23 + StringMeasure : 2 + StringWTF16Get : 2 + StructNew : 8 + TableSet : 2 + Throw : 2 + Try : 6 + TryTable : 6 + TupleExtract : 3 + TupleMake : 5 + Unary : 35 + Unreachable : 21 From 072bcc4df2869a013609ba5941c51e960cdfede8 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 20 May 2026 10:18:29 -0700 Subject: [PATCH 118/168] [wasm-split] Do not split out the start function (#8711) Like imports, automatically keep it in the primary module. --- src/tools/wasm-split/wasm-split.cpp | 45 +++++++--- test/lit/wasm-split/multi-split-start.wast | 86 +++++++++++++++++++ .../multi-split-start.wast.manifest | 9 ++ test/lit/wasm-split/start.wast | 28 ++++++ 4 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 test/lit/wasm-split/multi-split-start.wast create mode 100644 test/lit/wasm-split/multi-split-start.wast.manifest create mode 100644 test/lit/wasm-split/start.wast diff --git a/src/tools/wasm-split/wasm-split.cpp b/src/tools/wasm-split/wasm-split.cpp index fd53dd18b5e..82a3e9ab860 100644 --- a/src/tools/wasm-split/wasm-split.cpp +++ b/src/tools/wasm-split/wasm-split.cpp @@ -242,6 +242,33 @@ void setCommonSplitConfigs(ModuleSplitting::Config& config, } } +// Returns whether it is valid to split a function out from the main module. +bool canSplitFunc(Function* func, + const Module& wasm, + const WasmSplitOptions& options) { + if (!func) { + if (!options.quiet) { + std::cerr << "warning: function " << func->name << " does not exist\n"; + } + return false; + } + if (func->imported()) { + if (!options.quiet) { + std::cerr << "warning: cannot split out imported function " << func->name + << "\n"; + } + return false; + } + if (func->name == wasm.start) { + if (!options.quiet) { + std::cerr << "warning: cannot split out start function " << func->name + << "\n"; + } + return false; + } + return true; +} + void splitModule(const WasmSplitOptions& options) { Module wasm; parseInput(wasm, options); @@ -283,17 +310,7 @@ void splitModule(const WasmSplitOptions& options) { // Use the explicitly provided `splitFuncs`. for (auto& func : options.splitFuncs) { auto* function = wasm.getFunctionOrNull(func); - if (!function) { - if (!options.quiet) { - std::cerr << "warning: function " << func << " does not exist\n"; - } - continue; - } - if (function->imported()) { - if (!options.quiet) { - std::cerr << "warning: cannot split out imported function " << func - << "\n"; - } + if (!canSplitFunc(function, wasm, options)) { continue; } if (!options.quiet && options.keepFuncs.contains(func)) { @@ -435,6 +452,9 @@ void multiSplitModule(const WasmSplitOptions& options) { continue; } assert(currFuncs); + if (!canSplitFunc(wasm.getFunctionOrNull(name), wasm, options)) { + continue; + } currFuncs->insert(name); auto [it, inserted] = funcModules.insert({name, currModule}); if (!inserted && it->second != currModule) { @@ -442,9 +462,6 @@ void multiSplitModule(const WasmSplitOptions& options) { << currModule << "; it is already assigned to module " << it->second << '\n'; } - if (inserted && !options.quiet && !wasm.getFunctionOrNull(name)) { - std::cerr << "warning: Function " << name << " does not exist\n"; - } } if (options.emitModuleNames && !wasm.name) { diff --git a/test/lit/wasm-split/multi-split-start.wast b/test/lit/wasm-split/multi-split-start.wast new file mode 100644 index 00000000000..8a0bb1af728 --- /dev/null +++ b/test/lit/wasm-split/multi-split-start.wast @@ -0,0 +1,86 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: wasm-split -all -g --multi-split %s --manifest %s.manifest --out-prefix=%t -o %t.wasm +;; RUN: wasm-dis %t.wasm | filecheck %s --check-prefix=PRIMARY +;; RUN: wasm-dis %t1.wasm | filecheck %s --check-prefix=MOD1 +;; RUN: wasm-dis %t2.wasm | filecheck %s --check-prefix=MOD2 +;; RUN: wasm-dis %t3.wasm | filecheck %s --check-prefix=MOD3 + +;; The start function, $C, cannot be split it, and will remain in the primary. + +(module + ;; PRIMARY: (type $0 (func)) + + ;; PRIMARY: (import "placeholder.2" "0" (func $placeholder_0)) + + ;; PRIMARY: (table $0 1 funcref) + + ;; PRIMARY: (elem $0 (i32.const 0) $placeholder_0) + + ;; PRIMARY: (export "table" (table $0)) + + ;; PRIMARY: (export "C" (func $C)) + + ;; PRIMARY: (start $C) + (start $C) + + ;; MOD1: (type $0 (func)) + + ;; MOD1: (import "primary" "table" (table $timport$0 1 funcref)) + + ;; MOD1: (import "primary" "C" (func $C (exact))) + + ;; MOD1: (func $A + ;; MOD1-NEXT: (call $A) + ;; MOD1-NEXT: (call_indirect (type $0) + ;; MOD1-NEXT: (i32.const 0) + ;; MOD1-NEXT: ) + ;; MOD1-NEXT: (call $C) + ;; MOD1-NEXT: ) + (func $A + (call $A) + (call $B) + (call $C) + ) + + ;; MOD2: (type $0 (func)) + + ;; MOD2: (import "primary" "table" (table $timport$0 1 funcref)) + + ;; MOD2: (elem $0 (i32.const 0) $B) + + ;; MOD2: (func $B + ;; MOD2-NEXT: (drop + ;; MOD2-NEXT: (i32.const 42) + ;; MOD2-NEXT: ) + ;; MOD2-NEXT: ) + (func $B + (drop + (i32.const 42) + ) + ) + + ;; PRIMARY: (func $C + ;; PRIMARY-NEXT: (drop + ;; PRIMARY-NEXT: (i32.const 1337) + ;; PRIMARY-NEXT: ) + ;; PRIMARY-NEXT: ) + (func $C + (drop + (i32.const 1337) + ) + ) + + ;; MOD3: (type $0 (func)) + + ;; MOD3: (func $D + ;; MOD3-NEXT: (drop + ;; MOD3-NEXT: (i32.const 999999) + ;; MOD3-NEXT: ) + ;; MOD3-NEXT: ) + (func $D + (drop + (i32.const 999999) + ) + ) +) diff --git a/test/lit/wasm-split/multi-split-start.wast.manifest b/test/lit/wasm-split/multi-split-start.wast.manifest new file mode 100644 index 00000000000..ae2c2b78c5a --- /dev/null +++ b/test/lit/wasm-split/multi-split-start.wast.manifest @@ -0,0 +1,9 @@ +1: +A + +2: +B + +3: +C +D diff --git a/test/lit/wasm-split/start.wast b/test/lit/wasm-split/start.wast new file mode 100644 index 00000000000..381a1e8707a --- /dev/null +++ b/test/lit/wasm-split/start.wast @@ -0,0 +1,28 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: wasm-split %s --split-funcs=start_func,other -o1 %t.1.wasm -o2 %t.2.wasm -g 2>&1 +;; RUN: wasm-dis -all %t.1.wasm | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-dis -all %t.2.wasm | filecheck %s --check-prefix SECONDARY + +;; Do not error on trying to split out the start function. It cannot be +;; split out, keep it in the primary module. + +(module + ;; PRIMARY: (type $0 (func)) + + ;; PRIMARY: (start $start_func) + + ;; PRIMARY: (func $start_func (type $0) + ;; PRIMARY-NEXT: ) + (func $start_func) + + (start $start_func) + + ;; SECONDARY: (type $0 (func)) + + ;; SECONDARY: (func $other (type $0) + ;; SECONDARY-NEXT: ) + (func $other) +) + + From f3e100813546dc5b5d87ee05015e582f85116468 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 20 May 2026 14:04:01 -0700 Subject: [PATCH 119/168] Fix OOB errors in the lexer (#8734) Fix an OOB string_view access that was just trying to get a one-past-the-end pointer. Fix two locations where we could have been peeking empty input. Fixes #8732. --- src/parser/lexer.h | 10 +++++++++- test/gtest/wat-lexer.cpp | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/parser/lexer.h b/src/parser/lexer.h index 2f5cb7a0291..e998c6db820 100644 --- a/src/parser/lexer.h +++ b/src/parser/lexer.h @@ -624,6 +624,9 @@ inline std::optional Lexer::takeHexnum(OverflowBehavior behavior) { } inline Lexer::Sign Lexer::takeSign() { + if (empty()) { + return NoSign; + } auto c = peek(); if (c == '+') { take(1); @@ -812,7 +815,8 @@ inline std::optional Lexer::takeFloat() { // we need to strip any underscores since `std::strtod` does not understand // them. std::stringstream ss; - for (const char *curr = &buffer[startPos], *end = &buffer[pos]; curr != end; + for (const char *curr = buffer.data() + startPos, *end = buffer.data() + pos; + curr != end; ++curr) { if (*curr != '_') { ss << *curr; @@ -853,6 +857,10 @@ inline std::optional Lexer::takeStr() { // Escape sequences ensureBuildingEscaped(); take(1); + if (empty()) { + pos = startPos; + return std::nullopt; + } auto c = peek(); take(1); switch (c) { diff --git a/test/gtest/wat-lexer.cpp b/test/gtest/wat-lexer.cpp index 3a4cd49e246..38ad63dedca 100644 --- a/test/gtest/wat-lexer.cpp +++ b/test/gtest/wat-lexer.cpp @@ -22,6 +22,14 @@ using namespace wasm::WATParser; using namespace std::string_view_literals; +TEST(LexerTest, EmptyInput) { + EXPECT_TRUE(Lexer(""sv).empty()); + EXPECT_EQ(Lexer(""sv).takeI32(), std::nullopt); + EXPECT_EQ(Lexer(""sv).takeF32(), std::nullopt); + EXPECT_EQ(Lexer(""sv).takeString(), std::nullopt); + EXPECT_EQ(Lexer(""sv).takeID(), std::nullopt); +} + TEST(LexerTest, LexWhitespace) { Lexer lexer(" 1\t2\n3\r4 \n\n\t 5 "sv); @@ -915,6 +923,7 @@ TEST(LexerTest, LexString) { "_$_\xC2\xA3_\xE2\x82\xAC_\xF0\x90\x8D\x88_"s); EXPECT_FALSE(Lexer("\"unterminated"sv).takeString()); + EXPECT_FALSE(Lexer("\"foo\\"sv).takeString()); EXPECT_FALSE(Lexer("\"unescaped nul\0\""sv).takeString()); EXPECT_FALSE(Lexer("\"unescaped U+19\x19\""sv).takeString()); EXPECT_FALSE(Lexer("\"unescaped U+7f\x7f\""sv).takeString()); From 355af661ff7e9370685f674df0894aebb9877d18 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 20 May 2026 15:07:02 -0700 Subject: [PATCH 120/168] [NFC] Return optional in Lexer::peek (#8737) This prevents errors where we peek the buffer without checking that there is more input to peek at. --- src/parser/lexer.h | 109 +++++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 49 deletions(-) diff --git a/src/parser/lexer.h b/src/parser/lexer.h index e998c6db820..a65bac4eb85 100644 --- a/src/parser/lexer.h +++ b/src/parser/lexer.h @@ -69,6 +69,8 @@ struct Lexer { std::vector annotations; std::optional file; + static bool isSpacechar(uint8_t c); + public: std::string_view buffer; @@ -93,11 +95,11 @@ struct Lexer { std::optional peekChar() const; - bool peekLParen() { return !empty() && peek() == '('; } + bool peekLParen() { return peek() == uint8_t('('); } bool takeLParen(); - bool peekRParen() { return !empty() && peek() == ')'; } + bool peekRParen() { return peek() == uint8_t(')'); } bool takeRParen(); @@ -134,7 +136,12 @@ struct Lexer { std::string_view next() const { return buffer.substr(pos); } - uint8_t peek() const { return buffer[pos]; } + std::optional peek() const { + if (empty()) { + return std::nullopt; + } + return uint8_t(buffer[pos]); + } void advance() { annotations.clear(); @@ -247,8 +254,8 @@ inline Lexer::Lexer(std::string_view buffer, std::optional file) } inline std::optional Lexer::peekChar() const { - if (!empty()) { - return peek(); + if (auto c = peek()) { + return char(*c); } return std::nullopt; } @@ -298,16 +305,12 @@ inline std::optional Lexer::takeID() { } inline std::optional Lexer::peekKeyword() { - if (empty()) { + auto start = peek(); + if (!start || *start < 'a' || *start > 'z') { return std::nullopt; } auto startPos = pos; - uint8_t start = peek(); - if ('a' <= start && start <= 'z') { - take(1); - } else { - return std::nullopt; - } + take(1); while (idchar()) { take(1); } @@ -544,23 +547,21 @@ inline bool Lexer::takePrefix(std::string_view sv) { } inline std::optional Lexer::takeDigit() { - if (empty()) { - return std::nullopt; - } - if (auto d = getDigit(peek())) { - take(1); - return d; + if (auto c = peek()) { + if (auto d = getDigit(*c)) { + take(1); + return d; + } } return std::nullopt; } inline std::optional Lexer::takeHexdigit() { - if (empty()) { - return std::nullopt; - } - if (auto h = getHexDigit(peek())) { - take(1); - return h; + if (auto c = peek()) { + if (auto h = getHexDigit(*c)) { + take(1); + return h; + } } return std::nullopt; } @@ -624,17 +625,15 @@ inline std::optional Lexer::takeHexnum(OverflowBehavior behavior) { } inline Lexer::Sign Lexer::takeSign() { - if (empty()) { - return NoSign; - } - auto c = peek(); - if (c == '+') { - take(1); - return Pos; - } - if (c == '-') { - take(1); - return Neg; + if (auto c = peek()) { + if (*c == '+') { + take(1); + return Pos; + } + if (*c == '-') { + take(1); + return Neg; + } } return NoSign; } @@ -862,8 +861,12 @@ inline std::optional Lexer::takeStr() { return std::nullopt; } auto c = peek(); + if (!c) { + pos = startPos; + return std::nullopt; + } take(1); - switch (c) { + switch (*c) { case 't': *escapeBuilder << '\t'; break; @@ -909,7 +912,7 @@ inline std::optional Lexer::takeStr() { default: { // Byte escape: \hh // We already took the first h as c. - auto first = getHexDigit(c); + auto first = getHexDigit(*c); auto second = takeHexdigit(); if (!first || !second) { // TODO: Add error production for unrecognized escape sequence. @@ -921,7 +924,8 @@ inline std::optional Lexer::takeStr() { } } else { // Normal characters - if (uint8_t c = peek(); c >= 0x20 && c != 0x7F) { + uint8_t c = *peek(); + if (c >= 0x20 && c != 0x7F) { if (escapeBuilder) { *escapeBuilder << c; } @@ -941,17 +945,17 @@ inline std::optional Lexer::takeStr() { } inline bool Lexer::idchar() { - if (empty()) { + auto c = peek(); + if (!c) { return false; } - uint8_t c = peek(); // All the allowed characters lie in the range '!' to '~', and within that // range the vast majority of characters are allowed, so it is significantly // faster to check for the disallowed characters instead. - if (c < '!' || c > '~') { + if (*c < '!' || *c > '~') { return false; } - switch (c) { + switch (*c) { case '"': case '(': case ')': @@ -999,11 +1003,8 @@ inline std::optional Lexer::takeIdent() { return std::nullopt; } -inline bool Lexer::spacechar() { - if (empty()) { - return false; - } - switch (peek()) { +inline bool Lexer::isSpacechar(uint8_t c) { + switch (c) { case ' ': case '\n': case '\r': @@ -1014,6 +1015,13 @@ inline bool Lexer::spacechar() { } } +inline bool Lexer::spacechar() { + if (auto c = peek()) { + return isSpacechar(*c); + } + return false; +} + inline bool Lexer::takeSpacechar() { if (spacechar()) { take(1); @@ -1160,8 +1168,11 @@ inline bool Lexer::canFinish() { // actually want to parse more than a couple characters of space, so check // for individual space chars or comment starts instead. using namespace std::string_view_literals; - return empty() || spacechar() || peek() == '(' || peek() == ')' || - startsWith(";;"sv); + auto c = peek(); + if (!c) { + return true; + } + return isSpacechar(*c) || *c == '(' || *c == ')' || startsWith(";;"sv); } } // namespace wasm::WATParser From 17a90787bae4f4544e25b3e7833da076e0d543de Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 20 May 2026 16:03:27 -0700 Subject: [PATCH 121/168] MarkJSCalled pass (#8733) This adds `@binaryen.js.called` to functions called from any `configureAll`, even one not from the start function. Addresses the issue in #8727, where fuzzing the start function can lead to situations where a function is referred to from configureAll but not marked as js-called, which can break optimizations. By using this in the fuzzer, we can fuzz even files with interesting configureAll calls. --- scripts/fuzz_opt.py | 9 ++- src/ir/intrinsics.cpp | 9 +++ src/passes/CMakeLists.txt | 1 + src/passes/MarkJSCalled.cpp | 79 +++++++++++++++++++++ src/passes/pass.cpp | 3 + src/passes/passes.h | 1 + test/lit/help/wasm-metadce.test | 3 + test/lit/help/wasm-opt.test | 3 + test/lit/help/wasm2js.test | 3 + test/lit/passes/mark-js-called.wast | 102 ++++++++++++++++++++++++++++ 10 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/passes/MarkJSCalled.cpp create mode 100644 test/lit/passes/mark-js-called.wast diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 4173058abf8..72df5a75dbb 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2232,9 +2232,16 @@ def do_handle_pair(self, input, before_wasm, after_wasm, opts): pre_vm = random.choice(vms) pre = self.do_run(pre_vm, js_file, pre_wasm) + # We are about to optimize, and do not trust the given wasm file to + # have marked all js-called methods properly. In particular, it could + # have a configureAll that is not in the start function. + full_opts = opts + [ + '--mark-js-called', + ] + # Optimize. post_wasm = abspath('post.wasm') - cmd = [in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + opts + FEATURE_OPTS + cmd = [in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + full_opts + FEATURE_OPTS print(' '.join(cmd)) proc = subprocess.run(cmd, capture_output=True, text=True) if proc.returncode: diff --git a/src/ir/intrinsics.cpp b/src/ir/intrinsics.cpp index 6dd4c072324..2069b1288e6 100644 --- a/src/ir/intrinsics.cpp +++ b/src/ir/intrinsics.cpp @@ -110,6 +110,15 @@ std::vector Intrinsics::getJSCalledFunctions() { } // ConfigureAlls in a start function make their functions callable. + // + // TODO: Rather than scan the start, which does not handle all cases + // (configureAll can be called from an export), we could remove this and + // expect users to mark all functions as jsCalled. The MarkJSCalled pass scans + // for configureAlls and emits that annotation, so users could basically run + // it, if they don't want to manually annotate. Then the code here could + // get unified into that pass. The errors above (like the elem segment not + // having the right size etc.) could then be improved and/or turned into + // warnings. if (module.start) { auto* start = module.getFunction(module.start); if (!start->imported()) { diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index d6f9100aad0..9a3e1738e93 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -65,6 +65,7 @@ set(passes_SOURCES LocalSubtyping.cpp LogExecution.cpp LoopInvariantCodeMotion.cpp + MarkJSCalled.cpp Memory64Lowering.cpp MemoryPacking.cpp MergeBlocks.cpp diff --git a/src/passes/MarkJSCalled.cpp b/src/passes/MarkJSCalled.cpp new file mode 100644 index 00000000000..c05c75062c5 --- /dev/null +++ b/src/passes/MarkJSCalled.cpp @@ -0,0 +1,79 @@ +/* + * Copyright 2026 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. + */ + +// +// Users should mark JS-called functions using @binaryen.js.called. This pass +// helps by auto-marking them where possible. The main thing this does is to +// find any configureAll calls and mark the functions referred to there. +// +// We do automatically handle configureAll in the start function (in +// intrinsics.cpp), so this pass is only needed for other uses of configureAll, +// like from an export. +// + +#include "ir/find_all.h" +#include "ir/intrinsics.h" +#include "ir/module-utils.h" +#include "pass.h" +#include "wasm.h" + +namespace wasm { + +struct MarkJSCalled : public Pass { + void run(Module* module) override { + Intrinsics intrinsics(*module); + + // See if there even is a configureAll. + auto hasConfigureAll = false; + for (auto& func : module->functions) { + if (intrinsics.isConfigureAll(func.get())) { + hasConfigureAll = true; + break; + } + } + if (!hasConfigureAll) { + return; + } + + using JSCalledSet = std::unordered_set; + + ModuleUtils::ParallelFunctionAnalysis analysis( + *module, [&](Function* func, JSCalledSet& jsCalled) { + if (func->imported()) { + return; + } + + FindAll calls(func->body); + for (auto* call : calls.list) { + if (intrinsics.isConfigureAll(call)) { + for (auto name : intrinsics.getConfigureAllFunctions(call)) { + jsCalled.insert(name); + } + } + } + }); + + for (auto& [_, jsCalled] : analysis.map) { + for (auto name : jsCalled) { + module->getFunction(name)->funcAnnotations.jsCalled = true; + } + } + } +}; + +Pass* createMarkJSCalledPass() { return new MarkJSCalled(); } + +} // namespace wasm diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index d29a6fcebf5..d47812e0869 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -277,6 +277,9 @@ void PassRegistry::registerPasses() { registerPass("limit-segments", "attempt to merge segments to fit within web limits", createLimitSegmentsPass); + registerPass("mark-js-called", + "mark js called functions (using configureAll) as doing so", + createMarkJSCalledPass); registerPass("memory64-lowering", "lower loads and stores to a 64-bit memory to instead use a " "32-bit one", diff --git a/src/passes/passes.h b/src/passes/passes.h index 681a259a831..0e53028144e 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -87,6 +87,7 @@ Pass* createInstrumentLocalsPass(); Pass* createInstrumentMemoryPass(); Pass* createLLVMMemoryCopyFillLoweringPass(); Pass* createLoopInvariantCodeMotionPass(); +Pass* createMarkJSCalledPass(); Pass* createMemory64LoweringPass(); Pass* createMemoryPackingPass(); Pass* createMergeBlocksPass(); diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index b35982035d0..5cd70538ba0 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -256,6 +256,9 @@ ;; CHECK-NEXT: --log-execution instrument the build with ;; CHECK-NEXT: logging of where execution goes ;; CHECK-NEXT: +;; CHECK-NEXT: --mark-js-called mark js called functions (using +;; CHECK-NEXT: configureAll) as doing so +;; CHECK-NEXT: ;; CHECK-NEXT: --memory-packing packs memory into separate ;; CHECK-NEXT: segments, skipping zeros ;; CHECK-NEXT: diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 1565b9686b7..50aa71f196b 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -292,6 +292,9 @@ ;; CHECK-NEXT: --log-execution instrument the build with ;; CHECK-NEXT: logging of where execution goes ;; CHECK-NEXT: +;; CHECK-NEXT: --mark-js-called mark js called functions (using +;; CHECK-NEXT: configureAll) as doing so +;; CHECK-NEXT: ;; CHECK-NEXT: --memory-packing packs memory into separate ;; CHECK-NEXT: segments, skipping zeros ;; CHECK-NEXT: diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index 32a1f60ed3e..1f89eedb6d0 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -220,6 +220,9 @@ ;; CHECK-NEXT: --log-execution instrument the build with ;; CHECK-NEXT: logging of where execution goes ;; CHECK-NEXT: +;; CHECK-NEXT: --mark-js-called mark js called functions (using +;; CHECK-NEXT: configureAll) as doing so +;; CHECK-NEXT: ;; CHECK-NEXT: --memory-packing packs memory into separate ;; CHECK-NEXT: segments, skipping zeros ;; CHECK-NEXT: diff --git a/test/lit/passes/mark-js-called.wast b/test/lit/passes/mark-js-called.wast new file mode 100644 index 00000000000..10a2ca57ddd --- /dev/null +++ b/test/lit/passes/mark-js-called.wast @@ -0,0 +1,102 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: foreach %s %t wasm-opt --mark-js-called -all -S -o - | filecheck %s + +;; $configured will be marked as @binaryen.js.called. $already is already marked, +;; and nothing changess. $unconfigured* are not in configureAll so they are left +;; alone. + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (type $externs (array (mut externref))) + (type $externs (array (mut externref))) + + ;; CHECK: (type $funcs (array (mut funcref))) + (type $funcs (array (mut funcref))) + + ;; CHECK: (type $bytes (array (mut i8))) + (type $bytes (array (mut i8))) + + ;; CHECK: (type $configureAll (func (param (ref null $externs) (ref null $funcs) (ref null $bytes) externref))) + (type $configureAll (func (param (ref null $externs)) (param (ref null $funcs)) (param (ref null $bytes)) (param externref))) + + ;; CHECK: (import "wasm:js-prototypes" "configureAll" (func $configureAll (type $configureAll) (param (ref null $externs) (ref null $funcs) (ref null $bytes) externref))) + (import "wasm:js-prototypes" "configureAll" (func $configureAll (type $configureAll))) + + ;; CHECK: (data $bytes "12345678") + (data $bytes "12345678") + + ;; CHECK: (elem $externs externref (item (ref.null noextern))) + (elem $externs externref + (ref.null extern) + ) + + ;; CHECK: (elem $funcs func $configured $already) + (elem $funcs funcref + (ref.func $configured) + (ref.func $already) + ) + + ;; CHECK: (elem $other func $unconfigured) + (elem $other funcref + (ref.func $unconfigured) + ) + + ;; CHECK: (start $start) + (start $start) + + ;; CHECK: (func $start (type $0) + ;; CHECK-NEXT: (call $configureAll + ;; CHECK-NEXT: (array.new_elem $externs $externs + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (array.new_elem $funcs $funcs + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (array.new_data $bytes $bytes + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null noextern) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $start + (call $configureAll + (array.new_elem $externs $externs + (i32.const 0) (i32.const 1)) + (array.new_elem $funcs $funcs + (i32.const 0) (i32.const 2)) + (array.new_data $bytes $bytes + (i32.const 0) (i32.const 8)) + (ref.null extern) + ) + ) + + ;; CHECK: (@binaryen.js.called) + ;; CHECK-NEXT: (func $configured (type $0) + ;; CHECK-NEXT: ) + (func $configured + ) + + ;; CHECK: (@binaryen.js.called) + ;; CHECK-NEXT: (func $already (type $0) + ;; CHECK-NEXT: ) + (@binaryen.js.called) + (func $already + ) + + ;; CHECK: (func $unconfigured (type $0) + ;; CHECK-NEXT: ) + (func $unconfigured + ) + + ;; CHECK: (@binaryen.js.called) + ;; CHECK-NEXT: (func $unconfigured-already (type $0) + ;; CHECK-NEXT: ) + (@binaryen.js.called) + (func $unconfigured-already + ) +) From 84ace4aa2cab2fed7b2fd12a1e91e5a2ce342811 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 21 May 2026 08:44:56 -0700 Subject: [PATCH 122/168] PrintBoundary: Handle types for all externable things (#8739) The old code gave memories, tables, and tags, a null Type as a placeholder. But that actually crashes in printing. This adds a proper type for each. --- src/passes/PrintBoundary.cpp | 13 ++++++----- test/lit/passes/print-boundary.wast | 34 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/passes/PrintBoundary.cpp b/src/passes/PrintBoundary.cpp index 371e642e2d8..2bd63f256e3 100644 --- a/src/passes/PrintBoundary.cpp +++ b/src/passes/PrintBoundary.cpp @@ -135,19 +135,20 @@ struct PrintBoundary : public Pass { switch (kind) { case ExternalKind::Function: return getTypes(wasm.getFunction(name)->type); - break; case ExternalKind::Table: - break; + return getTypes(wasm.getTable(name)->type); case ExternalKind::Memory: - break; + return getTypes(wasm.getMemory(name)->addressType); case ExternalKind::Global: return getTypes(wasm.getGlobal(name)->type); case ExternalKind::Tag: - break; + // Wrap it in a Type so that getTypes can handle it. That will print the + // params and results as we expect. + return getTypes(Type(wasm.getTag(name)->type, NonNullable)); case ExternalKind::Invalid: - WASM_UNREACHABLE("invalid ExternalKind"); + break; } - return {}; + WASM_UNREACHABLE("invalid ExternalKind"); } json::Value::Ref getKindName(ExternalKind kind) { diff --git a/test/lit/passes/print-boundary.wast b/test/lit/passes/print-boundary.wast index 850618f07b1..27578366cb3 100644 --- a/test/lit/passes/print-boundary.wast +++ b/test/lit/passes/print-boundary.wast @@ -5,10 +5,22 @@ (import "module2" "other" (func $bar (result i32 f32))) + (memory $mem 10 20) + + (table 10 20 funcref) + + (tag $e (param i32)) + (global $g (mut i32) (i32.const 42)) (export "one" (func $one)) + (export "m" (memory $mem)) + + (export "tab" (table 0)) + + (export "tag" (tag $e)) + (export "glob" (global $g)) (func $one (param $x (ref $struct)) (result i32 i32 i32) @@ -64,9 +76,31 @@ ;; CHECK-NEXT: } ;; CHECK-NEXT: }, ;; CHECK-NEXT: { +;; CHECK-NEXT: "name": "m", +;; CHECK-NEXT: "kind": "memory", +;; CHECK-NEXT: "type": "i32" +;; CHECK-NEXT: }, +;; CHECK-NEXT: { +;; CHECK-NEXT: "name": "tab", +;; CHECK-NEXT: "kind": "table", +;; CHECK-NEXT: "type": "funcref" +;; CHECK-NEXT: }, +;; CHECK-NEXT: { +;; CHECK-NEXT: "name": "tag", +;; CHECK-NEXT: "kind": "tag", +;; CHECK-NEXT: "type": { +;; CHECK-NEXT: "params": [ +;; CHECK-NEXT: "i32" +;; CHECK-NEXT: ], +;; CHECK-NEXT: "results": [ +;; CHECK-NEXT: ] +;; CHECK-NEXT: } +;; CHECK-NEXT: }, +;; CHECK-NEXT: { ;; CHECK-NEXT: "name": "glob", ;; CHECK-NEXT: "kind": "global", ;; CHECK-NEXT: "type": "i32" ;; CHECK-NEXT: } ;; CHECK-NEXT: ] ;; CHECK-NEXT: } + From 371faf73decf05acde4cd0bd0f298fd2be90f1e7 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 21 May 2026 09:30:18 -0700 Subject: [PATCH 123/168] Fix test_cluster_fuzz.py V8 flags mismatch (#8746) When running test_cluster_fuzz.py, it was launching V8 with a hardcoded list of flags, instead of reading the flags from the generated flags file. This caused V8 to fail compilation of test cases that included relaxed atomics (which are generated by default now since relaxed atomics fuzzing was enabled), because V8 requires --experimental-wasm-acquire-release flag for them, and this flag was not in the hardcoded list. This CL fixes this by reading the flags from the generated flags file to faithfully simulate how ClusterFuzz runs V8. --- test/unit/test_cluster_fuzz.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/unit/test_cluster_fuzz.py b/test/unit/test_cluster_fuzz.py index 5dae09cc06f..e40c449be4b 100644 --- a/test/unit/test_cluster_fuzz.py +++ b/test/unit/test_cluster_fuzz.py @@ -435,11 +435,14 @@ def test_file_contents(self): valid_executions = 0 for i in range(1, N + 1): fuzz_file = os.path.join(temp_dir.name, f'fuzz-binaryen-{i}.js') + flags_file = os.path.join(temp_dir.name, f'flags-binaryen-{i}.js') + # Read flags from flags file to faithfully simulate how + # ClusterFuzz runs V8. + with open(flags_file) as f: + flags = f.read().strip().split() # Add --fuzzing to allow legacy and standard EH to coexist - cmd = [shared.V8, - '--wasm-staging', - '--experimental-wasm-custom-descriptors', + cmd = [shared.V8] + flags + [ '--fuzzing', fuzz_file] # Capture stderr even though we will not read it. It may From 2f63efb3681c47da15c5e3f5edbb7319c40d9227 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 21 May 2026 10:36:59 -0700 Subject: [PATCH 124/168] Require linux on the wasm-reduce lit test (#8749) For some reason this test is crashing on the Windows CI and possibly causing timeouts. Only run it on linux for now to avoid blocking other PRs from going in. --- test/lit/wasm-reduce/reduce-validation-error.wast | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/lit/wasm-reduce/reduce-validation-error.wast b/test/lit/wasm-reduce/reduce-validation-error.wast index 5b941fc8a18..31c918ba78a 100644 --- a/test/lit/wasm-reduce/reduce-validation-error.wast +++ b/test/lit/wasm-reduce/reduce-validation-error.wast @@ -3,7 +3,8 @@ ;; type to be set incorrectly to nullref instead of the original type, leading ;; to a validation error. -;; TODO: Why does this fail on CI without --force? +;; TODO: Why does this fail on CI without --force? Why does it crash on Windows? +;; REQUIRES: linux ;; RUN: wasm-reduce %s -t %t.t.wast -w %t.w.wast --force \ ;; RUN: --command='wasm-opt %t.t.wast -all --fuzz-exec' From cfc3d50b89e5745fd03db29d650b726bcb146f6c Mon Sep 17 00:00:00 2001 From: Sertonix Date: Thu, 21 May 2026 18:54:45 +0000 Subject: [PATCH 125/168] Fix i64x2 shift on big-endian (#8748) Since the i64x2.sh* functions use i32 as shift argument the access to `other.i64` may not use the correct union member. On big-endian systems this causes tests in `test/spec/testsuite/simd_bit_shift.wast` and a few other places to fail. Ref https://github.com/WebAssembly/binaryen/issues/2983 --- src/wasm/literal.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index 22fc5447e1f..8c5074e1c74 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -1483,8 +1483,8 @@ Literal Literal::shl(const Literal& other) const { return Literal(uint32_t(i32) << Bits::getEffectiveShifts(other.i32, Type::i32)); case Type::i64: - return Literal(uint64_t(i64) - << Bits::getEffectiveShifts(other.i64, Type::i64)); + return Literal(uint64_t(i64) << Bits::getEffectiveShifts( + other.getInteger(), Type::i64)); default: WASM_UNREACHABLE("unexpected type"); } @@ -1495,7 +1495,8 @@ Literal Literal::shrS(const Literal& other) const { case Type::i32: return Literal(i32 >> Bits::getEffectiveShifts(other.i32, Type::i32)); case Type::i64: - return Literal(i64 >> Bits::getEffectiveShifts(other.i64, Type::i64)); + return Literal(i64 >> + Bits::getEffectiveShifts(other.getInteger(), Type::i64)); default: WASM_UNREACHABLE("unexpected type"); } @@ -1508,7 +1509,7 @@ Literal Literal::shrU(const Literal& other) const { Bits::getEffectiveShifts(other.i32, Type::i32)); case Type::i64: return Literal(uint64_t(i64) >> - Bits::getEffectiveShifts(other.i64, Type::i64)); + Bits::getEffectiveShifts(other.getInteger(), Type::i64)); default: WASM_UNREACHABLE("unexpected type"); } From d215f0303a4d449b3664419bbdadbb42d63a508a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 21 May 2026 14:14:38 -0700 Subject: [PATCH 126/168] Fix ContinuationStore desynchronization (#8741) When a primary module execution traps or suspends to the host, its continuation store is cleared. Previously, this was done by reassigning the shared_ptr to a new ContinuationStore instance. However, secondary (linked) modules that were instantiated prior to this still hold the original shared_ptr to the old ContinuationStore. This led to desynchronization, where the secondary module would run with stale continuation state (including leaked continuations and resuming flags), eventually causing crashes like assertion failures in visitSuspend. This fix changes clearContinuationStore to clear the ContinuationStore in-place (clearing the continuations vector and resetting resuming flag) instead of reassigning the shared_ptr, ensuring all linked modules continue to share the same cleared state. Added a lit test to verify the fix and prevent regression. --- src/wasm-interpreter.h | 9 ++++++- test/lit/exec/continuation-leak.wast | 26 +++++++++++++++++++++ test/lit/exec/continuation-leak.wast.second | 7 ++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 test/lit/exec/continuation-leak.wast create mode 100644 test/lit/exec/continuation-leak.wast.second diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 447c22497d3..8d793045dfb 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -304,6 +304,13 @@ struct ContinuationStore { // Set when we are resuming execution, that is, re-winding the stack. bool resuming = false; + + // On traps or other errors that unwind the stack, we reset the continuation + // store to return to a clean state ahead of further calls to exports. + void clear() { + continuations.clear(); + resuming = false; + } }; // Execute an expression @@ -537,7 +544,7 @@ class ExpressionRunner : public OverriddenVisitor { #if WASM_INTERPRETER_DEBUG std::cout << indent() << "clear continuations\n"; #endif - continuationStore = std::make_shared(); + continuationStore->clear(); } } diff --git a/test/lit/exec/continuation-leak.wast b/test/lit/exec/continuation-leak.wast new file mode 100644 index 00000000000..b760d95ccff --- /dev/null +++ b/test/lit/exec/continuation-leak.wast @@ -0,0 +1,26 @@ +;; RUN: wasm-opt %s -all --fuzz-exec-before --fuzz-exec-second=%s.second -q -o /dev/null 2>&1 | filecheck %s + +;; Check that clearing the continuation store in linked modules clears it in-place, +;; so that continuations leaked from the primary module do not affect the second module. + +(module + (type $func_t (func)) + (type $cont_t (cont $func_t)) + (tag $tag) + + (func $f_suspend + (suspend $tag) + ) + + (func $test1 (export "test1") + (local $c (ref $cont_t)) + (local.set $c (cont.new $cont_t (ref.func $f_suspend))) + (resume $cont_t (local.get $c)) + ) +) + +;; CHECK: [fuzz-exec] export test1 +;; CHECK-NEXT: [exception thrown: unhandled suspend] +;; CHECK: [fuzz-exec] running second module +;; CHECK-NEXT: [fuzz-exec] export test2 +;; CHECK-NEXT: [exception thrown: unhandled suspend] diff --git a/test/lit/exec/continuation-leak.wast.second b/test/lit/exec/continuation-leak.wast.second new file mode 100644 index 00000000000..70a276176ae --- /dev/null +++ b/test/lit/exec/continuation-leak.wast.second @@ -0,0 +1,7 @@ +(module + (tag $tag) + + (func $test2 (export "test2") + (suspend $tag) + ) +) From 1215ea4eaa5146cd2cb5a7318a5f512ac3fe5928 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 21 May 2026 14:31:26 -0700 Subject: [PATCH 127/168] [Fuzzer] Make Split's exports a list of strings (#8731) When the export name contains a comma, such as `foo`, the v8 command does not work: ```colsole v8 ... exports:foo,bar,... --fuzz-split ``` This generates an `exports` as a list of strings within quotes, supporting export names with commas: ```colsole v8 ... exports:["foo","bar",...] --fuzz-split ``` This still parses the old format of `exports` in case it doesn't contain commas, which I think might be useful for handwritten command lines. --- scripts/fuzz_opt.py | 4 ++-- scripts/fuzz_shell.js | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 72df5a75dbb..be1b208b456 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -1693,9 +1693,9 @@ def optimize(name): # prepare the list of exports to call. the format is # - # exports:A,B,C + # exports:["A","B","C"] # - exports_to_call = 'exports:' + ','.join(exports) + exports_to_call = 'exports:' + json.dumps(exports) # get the output from the split modules, linking them using JS # TODO run liftoff/turboshaft/etc. diff --git a/scripts/fuzz_shell.js b/scripts/fuzz_shell.js index d16c49624b9..06a1030a215 100644 --- a/scripts/fuzz_shell.js +++ b/scripts/fuzz_shell.js @@ -57,7 +57,12 @@ var fuzzSplit = false; for (var i = 0; i < argv.length; i++) { var curr = argv[i]; if (curr.startsWith('exports:')) { - exportsToCall = curr.substr('exports:'.length).split(','); + var payload = curr.substr('exports:'.length); + if (payload.startsWith('[')) { + exportsToCall = JSON.parse(payload); + } else { + exportsToCall = payload ? payload.split(',') : []; + } argv.splice(i, 1); i--; } else if (curr == '--fuzz-split') { From 86910224b8632751b705a39938023a2132ce75f5 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 21 May 2026 14:31:34 -0700 Subject: [PATCH 128/168] [wasm-split] Don't split trapping globals (#8742) Trapping globals, which became possible with the custom-descriptor proposal, should stay in the primary module because we have to preserve their trapping behavior upon instantiation. Also, note that we remove unused globals here: https://github.com/WebAssembly/binaryen/blob/17a90787bae4f4544e25b3e7833da076e0d543de/src/ir/module-splitting.cpp#L896-L903 But trapping globals shouldn't be removed even when they are unused to preserve the trapping behavior. In case someone wants to split modules assuming traps never happen, this also adds `--traps-never-happen` option to wasm-split, in which case we can freely split or remove trapping globals. --- src/ir/module-splitting.cpp | 11 +++++++++++ src/ir/module-splitting.h | 3 +++ src/tools/wasm-split/split-options.cpp | 10 ++++++++++ src/tools/wasm-split/wasm-split.cpp | 1 + test/lit/help/wasm-split.test | 5 +++++ test/lit/wasm-split/trapping-global.wast | 24 ++++++++++++++++++++++++ 6 files changed, 54 insertions(+) create mode 100644 test/lit/wasm-split/trapping-global.wast diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index af31ff20b37..735e747938f 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -73,6 +73,7 @@ // from the IR before splitting. // #include "ir/module-splitting.h" +#include "ir/effects.h" #include "ir/find_all.h" #include "ir/module-utils.h" #include "ir/names.h" @@ -790,6 +791,16 @@ void ModuleSplitter::shareImportableItems() { primaryUsed.globals.insert(tableManager.activeBase.global); } + // Trapping globals should stay in the primary module to preserve the trapping + // behavior upon instantiation. + for (auto& global : primary.globals) { + if (global->init && + EffectAnalyzer(config.passOptions, primary, global->init) + .hasUnremovableSideEffects()) { + primaryUsed.globals.insert(global->name); + } + } + // Compute the transitive closure of globals referenced in other globals' // initializers. Since globals can reference other globals, we must ensure // that if a global is used in a module, all its dependencies are also marked diff --git a/src/ir/module-splitting.h b/src/ir/module-splitting.h index 8260feb6b8d..fd21d052061 100644 --- a/src/ir/module-splitting.h +++ b/src/ir/module-splitting.h @@ -44,11 +44,14 @@ #ifndef wasm_ir_module_splitting_h #define wasm_ir_module_splitting_h +#include "pass.h" #include "wasm.h" namespace wasm::ModuleSplitting { struct Config { + // Pass options to use for effects analysis + PassOptions passOptions; // A vector of set of functions to split into that secondary. Each function // set belongs to a single secondary module. All others are kept in the // primary module. Must not include the start function if it exists. May or diff --git a/src/tools/wasm-split/split-options.cpp b/src/tools/wasm-split/split-options.cpp index dcea3bcf227..d62d22ad765 100644 --- a/src/tools/wasm-split/split-options.cpp +++ b/src/tools/wasm-split/split-options.cpp @@ -358,6 +358,16 @@ WasmSplitOptions::WasmSplitOptions() {Mode::Split, Mode::MultiSplit, Mode::Instrument}, Options::Arguments::Zero, [&](Options* o, const std::string& arguments) { stripDebug = true; }) + .add("--traps-never-happen", + "-tnh", + "Split under the helpful assumption that no trap is reached at " + "runtime (from load, div/mod, etc.)", + WasmSplitOption, + {Mode::Split, Mode::MultiSplit}, + Options::Arguments::Zero, + [&](Options* o, const std::string& arguments) { + passOptions.trapsNeverHappen = true; + }) .add("--output", "-o", "Output file.", diff --git a/src/tools/wasm-split/wasm-split.cpp b/src/tools/wasm-split/wasm-split.cpp index 82a3e9ab860..8faa1bef488 100644 --- a/src/tools/wasm-split/wasm-split.cpp +++ b/src/tools/wasm-split/wasm-split.cpp @@ -229,6 +229,7 @@ void writePlaceholderMap( void setCommonSplitConfigs(ModuleSplitting::Config& config, const WasmSplitOptions& options) { + config.passOptions = options.passOptions; config.usePlaceholders = options.usePlaceholders; config.minimizeNewExportNames = !options.passOptions.debugInfo; if (options.importNamespace) { diff --git a/test/lit/help/wasm-split.test b/test/lit/help/wasm-split.test index 950d3e5cea9..3503a46675d 100644 --- a/test/lit/help/wasm-split.test +++ b/test/lit/help/wasm-split.test @@ -151,6 +151,11 @@ ;; CHECK-NEXT: --strip-debug [split, multi-split, instrument] Strip ;; CHECK-NEXT: debug info (including the names section) ;; CHECK-NEXT: +;; CHECK-NEXT: --traps-never-happen,-tnh [split, multi-split] Split under the +;; CHECK-NEXT: helpful assumption that no trap is +;; CHECK-NEXT: reached at runtime (from load, div/mod, +;; CHECK-NEXT: etc.) +;; CHECK-NEXT: ;; CHECK-NEXT: --output,-o [instrument, merge-profiles, multi-split] ;; CHECK-NEXT: Output file. ;; CHECK-NEXT: diff --git a/test/lit/wasm-split/trapping-global.wast b/test/lit/wasm-split/trapping-global.wast new file mode 100644 index 00000000000..388973d835d --- /dev/null +++ b/test/lit/wasm-split/trapping-global.wast @@ -0,0 +1,24 @@ +;; RUN: wasm-split %s -all -g -o1 %t.1.wasm -o2 %t.2.wasm --split-funcs=split +;; RUN: wasm-dis -all %t.1.wasm | filecheck %s --check-prefix PRIMARY +;; RUN: wasm-split %s -all -g -o1 %t.tnh.1.wasm -o2 %t.tnh.2.wasm --split-funcs=split --traps-never-happen +;; RUN: wasm-dis -all %t.tnh.1.wasm | filecheck %s --check-prefix PRIMARY-TNH + +;; This unused global should NOT be removed by wasm-split because its +;; initializer contains a side effect (a trap due to a null descriptor). +;; However, if we pass --traps-never-happen, we assume traps never occur, so the +;; global will be considered to have no side effects and will be removed. +(module + (rec + (type $struct (descriptor $desc) (struct)) + (type $desc (describes $struct) (struct)) + ) + ;; PRIMARY: (global $trap (ref $struct) + ;; PRIMARY-TNH-NOT: (global $trap (ref $struct) + (global $trap (ref $struct) + (struct.new_desc $struct + (ref.null none) + ) + ) + + (func $split) +) From f526098d83d9b2928bdc140d37f3763cdbf81961 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 21 May 2026 14:45:44 -0700 Subject: [PATCH 129/168] [Stack Switching] Fix continuation execution from a start function (#8751) Rather than set the module after creation - which is after the start is called - do it during init, which is also simpler. --- src/tools/execution-results.h | 8 +++--- test/lit/exec/cont_start.wast | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 test/lit/exec/cont_start.wast diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index b1690dc9d20..9918b845cf8 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -143,6 +143,11 @@ struct LoggingExternalInterface : public ShellExternalInterface { } } + void init(Module& wasm, ModuleRunner& instance_) override { + ShellExternalInterface::init(wasm, instance_); + instance = &instance_; + } + Literal getImportedFunction(Function* import) override { if (linkedInstances.contains(import->module)) { return getImportInstance(import)->getExportedFunction(import->base); @@ -366,8 +371,6 @@ struct LoggingExternalInterface : public ShellExternalInterface { } return false; } - - void setModuleRunner(ModuleRunner* instance_) { instance = instance_; } }; class FuzzerImportResolver @@ -501,7 +504,6 @@ struct ExecutionResults { // SIMD instructions. instance.setRelaxedBehavior(ModuleRunner::RelaxedBehavior::Execute); instance.instantiate(); - interface.setModuleRunner(&instance); } void callExports(Module& wasm, ModuleRunner& instance) { diff --git a/test/lit/exec/cont_start.wast b/test/lit/exec/cont_start.wast new file mode 100644 index 00000000000..e10ac5778d8 --- /dev/null +++ b/test/lit/exec/cont_start.wast @@ -0,0 +1,46 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --output=fuzz-exec and should not be edited. + +;; RUN: foreach %s %t wasm-opt -all --fuzz-exec-before -q -o /dev/null 2>&1 | filecheck %s + +;; Resume a continuation from the start function. It should execute without +;; error. + +(module + (type $void (func)) + (type $void_cont (cont $void)) + (type $i32_pair (func (param i32 i32))) + + (import "fuzzing-support" "call-export" (func $call_export (type $i32_pair) (param i32 i32))) + + (tag $susp_tag (type $void)) + + (export "suspend" (func $suspend)) + + (start $start) + + ;; CHECK: [fuzz-exec] export suspend + (func $suspend (type $void) + (nop) + ) + + (func $start (type $void) + (drop + (block $handler (result (ref $void_cont)) + (resume $void_cont (on $susp_tag $handler) + (cont.new $void_cont + (ref.func $run) + ) + ) + (return) + ) + ) + ) + + (func $run (type $void) + (call $call_export + (i32.const 0) + (i32.const 0) + ) + ) +) + From affe9b72db4845624e8a62750e449bfa66156f8e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 21 May 2026 14:45:58 -0700 Subject: [PATCH 130/168] RemoveUnusedModuleElements: Referenced elems refer to their offsets (#8750) --- src/passes/RemoveUnusedModuleElements.cpp | 3 ++ .../remove-unused-module-elements_tnh.wast | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index fd026490cb1..679e3224029 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -764,6 +764,9 @@ struct Analyzer { } else if (kind == ModuleElementKind::ElementSegment) { // TODO: We could empty out parts of the segment we don't need. auto* segment = module->getElementSegment(value); + if (segment->offset) { + addReferences(segment->offset); + } for (auto* item : segment->data) { addReferences(item); } diff --git a/test/lit/passes/remove-unused-module-elements_tnh.wast b/test/lit/passes/remove-unused-module-elements_tnh.wast index 76275742c63..a4d06d53154 100644 --- a/test/lit/passes/remove-unused-module-elements_tnh.wast +++ b/test/lit/passes/remove-unused-module-elements_tnh.wast @@ -236,3 +236,45 @@ ;; T_N_H: (export "mem" (memory $mem)) (export "mem" (memory $mem)) ) + +;; The exported function has a call_indirect, which refers to the table and +;; elem. The elem refers to the global, so it must not be removed (in either TNH +;; or not). +(module + ;; CHECK: (type $nop (func)) + ;; T_N_H: (type $nop (func)) + (type $nop (func)) + + ;; CHECK: (global $g i32 (i32.const 0)) + ;; T_N_H: (global $g i32 (i32.const 0)) + (global $g i32 (i32.const 0)) + + ;; CHECK: (table $table 50 50 funcref) + ;; T_N_H: (table $table 50 50 funcref) + (table $table 50 50 funcref) + + ;; CHECK: (elem $elem (global.get $g) $func) + ;; T_N_H: (elem $elem (global.get $g) $func) + (elem $elem (global.get $g) $func) + + ;; CHECK: (export "func" (func $func)) + + ;; CHECK: (func $func (type $nop) + ;; CHECK-NEXT: (call_indirect $table (type $nop) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; T_N_H: (export "func" (func $func)) + + ;; T_N_H: (func $func (type $nop) + ;; T_N_H-NEXT: (call_indirect $table (type $nop) + ;; T_N_H-NEXT: (i32.const 0) + ;; T_N_H-NEXT: ) + ;; T_N_H-NEXT: ) + (func $func (export "func") (type $nop) + (call_indirect (type $nop) + (i32.const 0) + ) + ) +) + From 9aaee88b765c163be93702baa91364ae4ba6e6e1 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 21 May 2026 15:37:25 -0700 Subject: [PATCH 131/168] Fuzzer: Mark js-called functions before optimizing (#8752) Marking them after does not work... since the point is to fix things for optimizations. --- scripts/fuzz_opt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index be1b208b456..93b609dbe92 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2235,9 +2235,9 @@ def do_handle_pair(self, input, before_wasm, after_wasm, opts): # We are about to optimize, and do not trust the given wasm file to # have marked all js-called methods properly. In particular, it could # have a configureAll that is not in the start function. - full_opts = opts + [ + full_opts = [ '--mark-js-called', - ] + ] + opts # Optimize. post_wasm = abspath('post.wasm') From 58e27a24d5940211ec54cb587ab8a93b931ec5fd Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 21 May 2026 16:18:25 -0700 Subject: [PATCH 132/168] LICM: Migrate from invalidates to orderedBefore (#8743) Replace the coarse `invalidates` check and the coarse global state check in LICM with more precise `orderedBefore` checks. This allows LICM to move memory accesses past release stores, while still correctly blocking them from moving past acquire loads. Add a lit test to verify the asymmetrical reordering behavior with release/acquire atomics on shared memory/GC structs. --- src/ir/effects.h | 21 +++---- src/passes/LoopInvariantCodeMotion.cpp | 10 ++-- test/lit/passes/licm-atomics.wast | 68 +++++++++++++++++++++ test/lit/passes/licm.wast | 82 +++++++++++++++++++++++++- 4 files changed, 165 insertions(+), 16 deletions(-) create mode 100644 test/lit/passes/licm-atomics.wast diff --git a/src/ir/effects.h b/src/ir/effects.h index 44cc8031f45..666765364f7 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -303,10 +303,8 @@ class EffectAnalyzer { // (e.g., if we write, we must remain ordered before someone that reads). // // This assumes the things whose effects we are comparing will both execute, - // at least if neither of them transfers control flow away. That is, we assume - // that there is no transfer of control flow *between* them: we are comparing - // things appear after each other, perhaps with some other code in the middle, - // but that code does not transfer control flow. It is not valid to call this + // at least if neither of them transfers control flow away. We assume there is + // no transfer of control flow *between* them. It is not valid to call this // method in other situations, like this: // // A @@ -326,12 +324,15 @@ class EffectAnalyzer { // ;; control flow transfer // B // - // That the things being compared both execute only matters in the case of - // traps-never-happen: in that mode we can move traps but only if doing so - // would not make them start to appear when they did not. In the second - // example we can't reorder A and B if B traps, but in the first example we - // can reorder them even if B traps (even if A has a global effect like a - // global.set, since we assume B does not trap in traps-never-happen). + // (Note that if they appear in inside a loop, A and B may overlap or even be + // the same expression; this is fine because A still executes before B, even + // if also executes during and after B across different loop iterations.) That + // A and B both execute only matters in the case of traps-never-happen: in + // that mode we can move traps but only if doing so would not make them start + // to appear when they did not previously. In the example with the br_if we + // can't reorder A and B if B traps, but in the valid examples we can reorder + // them even if B traps (even if A has a global effect like a global.set, + // since we assume B does not trap in traps-never-happen). bool orderedBefore(const EffectAnalyzer& other) const { // Cannot reorder control flow and side effects. if ((transfersControlFlow() && other.hasSideEffects()) || diff --git a/src/passes/LoopInvariantCodeMotion.cpp b/src/passes/LoopInvariantCodeMotion.cpp index 6add5134a79..c524b82ae9d 100644 --- a/src/passes/LoopInvariantCodeMotion.cpp +++ b/src/passes/LoopInvariantCodeMotion.cpp @@ -65,11 +65,14 @@ struct LoopInvariantCodeMotion // is ok to do so. EffectAnalyzer effectsSoFar(getPassOptions(), *getModule()); // The loop's total effects also matter. For example, a store - // in the loop means we can't move a load outside. + // in the loop means we can't move a load outside. We discard the local + // reads and writes because we analyze them separately. // FIXME: we look at the loop "tail" area too, after the last // possible branch back, which can cause false positives // for bad effect interactions. EffectAnalyzer loopEffects(getPassOptions(), *getModule(), loop); + loopEffects.localsRead.clear(); + loopEffects.localsWritten.clear(); // Note all the sets in each loop, and how many per index. Currently // EffectAnalyzer can't do that, and we need it to know if we // can move a set out of the loop (if there is another set @@ -123,9 +126,8 @@ struct LoopInvariantCodeMotion // take into account global state like interacting loads and // stores. bool unsafeToMove = effects.writesGlobalState() || - effectsSoFar.invalidates(effects) || - (effects.readsMutableGlobalState() && - loopEffects.writesGlobalState()); + effectsSoFar.orderedBefore(effects) || + loopEffects.orderedBefore(effects); // TODO: look into optimizing this with exceptions. for now, disallow if (effects.throws() || loopEffects.throws()) { unsafeToMove = true; diff --git a/test/lit/passes/licm-atomics.wast b/test/lit/passes/licm-atomics.wast new file mode 100644 index 00000000000..43beb342a0c --- /dev/null +++ b/test/lit/passes/licm-atomics.wast @@ -0,0 +1,68 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --licm -S -o - | filecheck %s + +(module + ;; CHECK: (type $struct (shared (struct (field (mut i32))))) + (type $struct (shared (struct (field (mut i32))))) + + ;; CHECK: (memory $mem 1 1 shared) + (memory $mem 1 1 shared) + + ;; Test 1: Allowed reordering (GC read moved before Wasm release store) + ;; CHECK: (func $allowed (type $1) (param $x (ref $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $allowed (param $x (ref $struct)) + (loop $loop + ;; X: release store (Wasm memory) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + ;; E: memory access (shared GC read) + (drop + (struct.get $struct 0 (local.get $x)) + ) + (br $loop) + ) + ) + + ;; Test 2: Disallowed reordering (GC read moved before Wasm acquire load) + ;; CHECK: (func $disallowed (type $1) (param $x (ref $struct)) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $disallowed (param $x (ref $struct)) + (loop $loop + ;; X: acquire load (Wasm memory) + (drop + (i32.atomic.load acqrel (i32.const 0)) + ) + ;; E: memory access (shared GC read) + (drop + (struct.get $struct 0 (local.get $x)) + ) + (br $loop) + ) + ) +) diff --git a/test/lit/passes/licm.wast b/test/lit/passes/licm.wast index 0d82ad4a943..d1918c95491 100644 --- a/test/lit/passes/licm.wast +++ b/test/lit/passes/licm.wast @@ -7,11 +7,13 @@ ;; CHECK: (type $0 (func (param i32))) - ;; CHECK: (type $1 (func)) + ;; CHECK: (type $1 (func (param i32) (result i32))) + + ;; CHECK: (type $2 (func)) ;; CHECK: (memory $0 10 20) - ;; CHECK: (func $unreachable-get (type $1) + ;; CHECK: (func $unreachable-get (type $2) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $x) @@ -104,4 +106,80 @@ (br $loop) ) ) + + ;; CHECK: (func $bug-inversion (type $1) (param $z i32) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $loop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (func $bug-inversion (param $z i32) (result i32) + (local $x i32) + (local $y i32) + (local.set $y (i32.const 0)) + (local.set $x (i32.const 0)) + (loop $loop + (local.set $y (i32.const 2)) + (local.set $x (i32.add (local.get $y) (i32.const 1))) + (br_if $loop (i32.const 0)) + ) + (local.get $x) + ) + + ;; CHECK: (func $bug-cross-statement-dependency (type $1) (param $z i32) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $z) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $loop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + (func $bug-cross-statement-dependency (param $z i32) (result i32) + (local $x i32) + (local $y i32) + (local.set $x (i32.const 0)) + (local.set $y (i32.const 0)) + (loop $loop + (local.set $y (local.get $x)) + (local.set $x (i32.add (local.get $z) (i32.const 1))) + (br_if $loop (i32.const 0)) + ) + (local.get $y) + ) ) From da3bff5a8a18d8856aa77745e735f3b68bbb0f9b Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 21 May 2026 19:05:24 -0700 Subject: [PATCH 133/168] LocalCSE: Migrate from invalidates to orderedBefore (#8744) Replace the coarse `invalidates` check in `LocalCSE` with `orderedBefore`. This allows `LocalCSE` to reuse expression values across release stores, while still correctly blocking reuse across acquire loads. Add a lit test to verify the asymmetrical reordering behavior with release/acquire atomics on shared GC structs and Wasm memory. --- src/passes/LocalCSE.cpp | 4 +- test/lit/passes/local-cse-atomics.wast | 58 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 test/lit/passes/local-cse-atomics.wast diff --git a/src/passes/LocalCSE.cpp b/src/passes/LocalCSE.cpp index 0233d17061d..2ad15b731fe 100644 --- a/src/passes/LocalCSE.cpp +++ b/src/passes/LocalCSE.cpp @@ -497,7 +497,9 @@ struct Checker continue; } auto& originalInfo = kv.second; - if (effects.invalidates(originalInfo.effects)) { + // Check whether curr must remain before COPY. We use ORIGINAL's effects + // in the check because we know they are the same as COPY's effects. + if (effects.orderedBefore(originalInfo.effects)) { invalidated.push_back(original); } } diff --git a/test/lit/passes/local-cse-atomics.wast b/test/lit/passes/local-cse-atomics.wast new file mode 100644 index 00000000000..15553ec9bd4 --- /dev/null +++ b/test/lit/passes/local-cse-atomics.wast @@ -0,0 +1,58 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt -all --local-cse -S -o - %s | filecheck %s + +(module + ;; CHECK: (type $struct (shared (struct (field (mut i32))))) + (type $struct (shared (struct (field (mut i32))))) + + ;; CHECK: (memory $mem 1 1 shared) + (memory $mem 1 1 shared) + + ;; Test 1: Allowed reordering (GC read reused across Wasm release store) + ;; CHECK: (func $allowed (type $1) (param $x (ref $struct)) (result i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (local.tee $2 + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + (func $allowed (param $x (ref $struct)) (result i32) + (local $y i32) + (local.set $y (struct.get $struct 0 (local.get $x))) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + (struct.get $struct 0 (local.get $x)) + ) + + ;; Test 2: Disallowed reordering (GC read NOT reused across Wasm acquire load) + ;; CHECK: (func $disallowed (type $1) (param $x (ref $struct)) (result i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $disallowed (param $x (ref $struct)) (result i32) + (local $y i32) + (local.set $y (struct.get $struct 0 (local.get $x))) + (drop (i32.atomic.load acqrel (i32.const 0))) + (struct.get $struct 0 (local.get $x)) + ) +) From f404918453cef6c22774f71a4945cdb187f8e95a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 21 May 2026 21:15:02 -0700 Subject: [PATCH 134/168] CodePushing: Migrate from invalidates to orderedBefore (#8745) Replace the coarse `invalidates` check in `CodePushing` with `orderedBefore`. This allows `CodePushing` to push expressions (like GC reads) past acquire loads, while still correctly blocking them from being pushed past release stores. Add a lit test to verify the asymmetrical reordering behavior with release/acquire atomics on shared GC structs and Wasm memory. --- src/passes/CodePushing.cpp | 4 +- test/lit/passes/code-pushing-atomics.wast | 145 ++++++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 test/lit/passes/code-pushing-atomics.wast diff --git a/src/passes/CodePushing.cpp b/src/passes/CodePushing.cpp index 57dd9993417..31aaf151e28 100644 --- a/src/passes/CodePushing.cpp +++ b/src/passes/CodePushing.cpp @@ -204,7 +204,7 @@ class Pusher { auto* pushable = isPushable(list[i]); if (pushable) { const auto& effects = getPushableEffects(pushable); - if (cumulativeEffects.invalidates(effects)) { + if (effects.orderedBefore(cumulativeEffects)) { // we can't push this, so further pushables must pass it cumulativeEffects.mergeIn(effects); } else { @@ -354,7 +354,7 @@ class Pusher { const auto& effects = getPushableEffects(pushable); - if (cumulativeEffects.invalidates(effects)) { + if (effects.orderedBefore(cumulativeEffects)) { // This can't be moved forward. Add it to the things that are not // moving. cumulativeEffects.walk(list[i]); diff --git a/test/lit/passes/code-pushing-atomics.wast b/test/lit/passes/code-pushing-atomics.wast new file mode 100644 index 00000000000..81046d3b02d --- /dev/null +++ b/test/lit/passes/code-pushing-atomics.wast @@ -0,0 +1,145 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt -all --code-pushing -S -o - %s | filecheck %s + +(module + ;; CHECK: (type $struct (shared (struct (field (mut i32))))) + (type $struct (shared (struct (field (mut i32))))) + + ;; CHECK: (memory $mem 1 1 shared) + (memory $mem 1 1 shared) + + ;; Test 1: Allowed reordering into If (GC read pushed past Wasm acquire load into If arm) + ;; CHECK: (func $allowed (type $1) (param $x (ref $struct)) (param $cond i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $cond) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $allowed (param $x (ref $struct)) (param $cond i32) + (local $y i32) + (local.set $y (struct.get $struct 0 (local.get $x))) + (drop (i32.atomic.load acqrel (i32.const 0))) + (if (local.get $cond) + (then + (drop (local.get $y)) + ) + ) + ) + + ;; Test 2: Disallowed reordering into If (GC read NOT pushed past Wasm release store into If arm) + ;; CHECK: (func $disallowed (type $1) (param $x (ref $struct)) (param $cond i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $cond) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $disallowed (param $x (ref $struct)) (param $cond i32) + (local $y i32) + (local.set $y (struct.get $struct 0 (local.get $x))) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + (if (local.get $cond) + (then + (drop (local.get $y)) + ) + ) + ) + + ;; Test 3: Allowed segment reordering (GC read pushed past Wasm acquire load AND target if block, as it is read after the if) + ;; CHECK: (func $allowed_segment (type $1) (param $x (ref $struct)) (param $cond i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $cond) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $allowed_segment (param $x (ref $struct)) (param $cond i32) + (local $y i32) + (local.set $y (struct.get $struct 0 (local.get $x))) + (drop (i32.atomic.load acqrel (i32.const 0))) + (if (local.get $cond) + (then + (nop) + ) + ) + (drop (local.get $y)) + ) + + ;; Test 4: Disallowed segment reordering (GC read NOT pushed past Wasm release store, even if it is read after the if) + ;; CHECK: (func $disallowed_segment (type $1) (param $x (ref $struct)) (param $cond i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $cond) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $disallowed_segment (param $x (ref $struct)) (param $cond i32) + (local $y i32) + (local.set $y (struct.get $struct 0 (local.get $x))) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + (if (local.get $cond) + (then + (nop) + ) + ) + (drop (local.get $y)) + ) +) From 77679499aee2fa2480416b099651f81e0b151f1e Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Thu, 21 May 2026 22:01:39 -0700 Subject: [PATCH 135/168] Effect analysis for indirect call expressions (#8625) Part of #8615. After #8609, we compute effects for indirect call expressions, but only reflect this in the call-site via the effects of the `Function` that contains the indirect call. That let us reason about effects only one layer of indirection away, for example in the following module: ```wasm (func $a (call_ref $t (...)) ) (func $b (call $a) ) ``` If we know that an indirect call to $t can't possibly have any effects (e.g. its only potential target is a nop), we'd be able to optimize away `(call $a)` but not the `(call_ref)` itself, since the effects only got stored in the effects of `$a`. This PR lets us reason about indirect call effects at the expression level within function bodies by adding a map from HeapType to effects `typeEffects` in `wasm::Module`. As a result we can completely optimize out the `call_ref` in the above example. Drive-by fixes: * Set an unconditional trap effect on call_indirect when the call type doesn't match the target table. * ~~Correctly set `branchesOut` for `return_call` on `call.without.effects`. Previously this would not have a `branchesOut` effect which may have allowed incorrect reorderings (we shouldn't move an effectful expression above a `return_call` but we would have allowed this).~~ Will follow up in #8693. --- src/ir/effects.h | 190 ++++++++------- src/ir/type-updating.cpp | 23 ++ src/passes/GlobalEffects.cpp | 82 ++++--- src/support/utilities.h | 13 + src/wasm.h | 28 ++- ...ts-closed-world-ignore-implicit-traps.wast | 82 +++++++ .../global-effects-closed-world-tnh.wast | 24 +- .../passes/global-effects-closed-world.wast | 227 +++--------------- .../passes/global-effects-indirect-merge.wast | 84 +++++++ 9 files changed, 432 insertions(+), 321 deletions(-) create mode 100644 test/lit/passes/global-effects-closed-world-ignore-implicit-traps.wast create mode 100644 test/lit/passes/global-effects-indirect-merge.wast diff --git a/src/ir/effects.h b/src/ir/effects.h index 666765364f7..f28c8c27a24 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -23,6 +23,7 @@ #include "ir/intrinsics.h" #include "pass.h" #include "support/name.h" +#include "support/utilities.h" #include "wasm-traversal.h" #include "wasm-type.h" #include "wasm.h" @@ -671,27 +672,34 @@ class EffectAnalyzer { } } + // Handle effects due to a null type arriving in a place where a null input + // causes trapping. That is, handle the case of the type proving that the + // input is null. + // Returns true iff there is no need to consider further effects. + bool trapOnNull(Type type) { + if (type == Type::unreachable) { + return true; + } + assert(type.isRef()); + if (type.isNull()) { + parent.trap = true; + return true; + } + if (type.isNullable()) { + parent.implicitTrap = true; + } + + return false; + } + // Handle effects due to an explicit null check of the operands in `exprs`. // Returns true iff there is no need to consider further effects. bool trapOnNull(std::initializer_list exprs) { for (auto* expr : exprs) { - if (expr && expr->type == Type::unreachable) { + if (expr && trapOnNull(expr->type)) { return true; } } - for (auto* expr : exprs) { - assert(!expr || expr->type.isRef()); - if (expr && expr->type.isNull()) { - parent.trap = true; - return true; - } - } - for (auto* expr : exprs) { - if (expr && expr->type.isNullable()) { - parent.implicitTrap = true; - break; - } - } return false; } @@ -717,71 +725,52 @@ class EffectAnalyzer { } void visitCall(Call* curr) { - // call.without.effects has no effects. if (Intrinsics(parent.module).isCallWithoutEffects(curr)) { return; } - // Get the target's effects, if they exist. Note that we must handle the - // case of the function not yet existing (we may be executed in the middle - // of a pass, which may have built up calls but not the targets of those - // calls; in such a case, we do not find the targets and therefore assume - // we know nothing about the effects, which is safe). - const EffectAnalyzer* targetEffects = nullptr; - if (auto* target = parent.module.getFunctionOrNull(curr->target)) { - targetEffects = target->effects.get(); + const EffectAnalyzer* callTargetEffects = nullptr; + if (auto* target = parent.module.getFunctionOrNull(curr->target); + target && target->effects) { + callTargetEffects = target->effects.get(); } - - if (curr->isReturn) { - parent.branchesOut = true; - // When EH is enabled, any call can throw. - if (parent.features.hasExceptionHandling() && - (!targetEffects || targetEffects->throws())) { - parent.hasReturnCallThrow = true; - } + addCallEffects(curr, callTargetEffects); + } + void visitCallIndirect(CallIndirect* curr) { + auto* table = parent.module.getTable(curr->table); + if (trapOnNull(table->type)) { + return; } - if (targetEffects) { - // We have effect information for this call target, and can just use - // that. The one change we may want to make is to remove throws_, if the - // target function throws and we know that will be caught anyhow, the - // same as the code below for the general path. We can always filter out - // throws for return calls because they are already more precisely - // captured by `branchesOut`, which models the return, and - // `hasReturnCallThrow`, which models the throw that will happen after - // the return. - if (targetEffects->throws_ && (parent.tryDepth > 0 || curr->isReturn)) { - auto filteredEffects = *targetEffects; - filteredEffects.throws_ = false; - parent.mergeIn(filteredEffects); - } else { - // Just merge in all the effects. - parent.mergeIn(*targetEffects); - } + if (!Type::isSubType(Type(curr->heapType, Nullability::NonNullable), + table->type)) { + parent.trap = true; return; } - parent.calls = true; - // When EH is enabled, any call can throw. Skip this for return calls - // because the throw is already more precisely captured by the combination - // of `hasReturnCallThrow` and `branchesOut`. - if (parent.features.hasExceptionHandling() && parent.tryDepth == 0 && - !curr->isReturn) { - parent.throws_ = true; + // Due to index out of bounds. Type-related traps are handled above and + // may set either implicitTrap or trap (or neither). + parent.implicitTrap = true; + + const EffectAnalyzer* callTargetEffects = nullptr; + if (auto it = parent.module.indirectCallEffects.find(curr->heapType); + it != parent.module.indirectCallEffects.end()) { + callTargetEffects = it->second.get(); } + addCallEffects(curr, callTargetEffects); } - void visitCallIndirect(CallIndirect* curr) { - parent.calls = true; - if (curr->isReturn) { - parent.branchesOut = true; - if (parent.features.hasExceptionHandling()) { - parent.hasReturnCallThrow = true; - } + void visitCallRef(CallRef* curr) { + if (trapOnNull(curr->target)) { + return; } - if (parent.features.hasExceptionHandling() && - (parent.tryDepth == 0 && !curr->isReturn)) { - parent.throws_ = true; + + const EffectAnalyzer* callTargetEffects = nullptr; + if (auto it = parent.module.indirectCallEffects.find( + curr->target->type.getHeapType()); + it != parent.module.indirectCallEffects.end()) { + callTargetEffects = it->second.get(); } + addCallEffects(curr, callTargetEffects); } void visitLocalGet(LocalGet* curr) { parent.localsRead.insert(curr->index); @@ -1039,22 +1028,6 @@ class EffectAnalyzer { void visitTupleExtract(TupleExtract* curr) {} void visitRefI31(RefI31* curr) {} void visitI31Get(I31Get* curr) { trapOnNull(curr->i31); } - void visitCallRef(CallRef* curr) { - if (trapOnNull(curr->target)) { - return; - } - if (curr->isReturn) { - parent.branchesOut = true; - if (parent.features.hasExceptionHandling()) { - parent.hasReturnCallThrow = true; - } - } - parent.calls = true; - if (parent.features.hasExceptionHandling() && - (parent.tryDepth == 0 && !curr->isReturn)) { - parent.throws_ = true; - } - } void visitRefTest(RefTest* curr) {} void visitRefCast(RefCast* curr) { @@ -1336,6 +1309,61 @@ class EffectAnalyzer { parent.throws_ = true; } } + + private: + // Populate a call's effects using effects computed from GlobalEffects. Note + // that calls may have other effects that aren't captured by the function + // body of the target (e.g. a call_ref may trap on null refs). + template + void addCallEffectsFromGlobalEffects(const CallType* curr, + const EffectAnalyzer& funcEffects) { + if (curr->isReturn) { + if (funcEffects.throws()) { + parent.hasReturnCallThrow = true; + } + } + + if (funcEffects.throws_ && (parent.tryDepth > 0 || curr->isReturn)) { + // We can ignore a throw here, as the parent catches it. + // + // Also, we can filter out throws for return calls because they are + // already more precisely captured by `branchesOut`, which models the + // return, and `hasReturnCallThrow`, which models the throw that will + // happen after the return. + auto filteredEffects = funcEffects; + filteredEffects.throws_ = false; + parent.mergeIn(filteredEffects); + } else { + parent.mergeIn(funcEffects); + } + } + + // Common effects logic for the 3 types of call: `call`, `call_indirect`, + // and `call_ref`. + template + void addCallEffects(const CallType* curr, + const EffectAnalyzer* callTargetEffects) { + if (curr->isReturn) { + parent.branchesOut = true; + } + + if (callTargetEffects) { + addCallEffectsFromGlobalEffects(curr, *callTargetEffects); + return; + } + + parent.calls = true; + // If EH is enabled and we don't have global effects information, + // assume that the call target may throw. + if (parent.features.hasExceptionHandling()) { + if (curr->isReturn) { + parent.hasReturnCallThrow = true; + } + if (parent.tryDepth == 0 && !curr->isReturn) { + parent.throws_ = true; + } + } + } }; public: diff --git a/src/ir/type-updating.cpp b/src/ir/type-updating.cpp index 69f29101c86..7fcd6a4935c 100644 --- a/src/ir/type-updating.cpp +++ b/src/ir/type-updating.cpp @@ -324,6 +324,29 @@ void GlobalTypeRewriter::mapTypes(const TypeMap& oldToNewTypes) { for (auto& tag : wasm.tags) { tag->type = updater.getNew(tag->type); } + + // Update indirect call effects per type. + // When A is rewritten to B, B inherits the effects of A and A loses its + // effects. + std::unordered_map> + newTypeEffects; + for (auto& [oldType, oldEffects] : wasm.indirectCallEffects) { + if (!oldEffects) { + continue; + } + + auto newType = updater.getNew(oldType); + std::shared_ptr& targetEffects = + newTypeEffects[newType]; + if (!targetEffects) { + targetEffects = oldEffects; + } else { + auto merged = std::make_shared(*targetEffects); + merged->mergeIn(*oldEffects); + targetEffects = merged; + } + } + wasm.indirectCallEffects = std::move(newTypeEffects); } void GlobalTypeRewriter::mapTypeNamesAndIndices(const TypeMap& oldToNewTypes) { diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index ca82b2b3aea..88fb4c00907 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -24,17 +24,16 @@ #include "pass.h" #include "support/graph_traversal.h" #include "support/strongly_connected_components.h" +#include "support/utilities.h" #include "wasm.h" namespace wasm { namespace { -constexpr auto UnknownEffects = std::nullopt; - struct FuncInfo { - // Effects in this function. nullopt / UnknownEffects means that we don't know - // what effects this function has, so we conservatively assume all effects. + // Effects in this function. nullopt means that we don't know what effects + // this function has, so we conservatively assume all effects. // Nullopt cases won't be copied to Function::effects. std::optional effects; @@ -96,14 +95,14 @@ std::map analyzeFuncs(Module& module, } else if (auto* callIndirect = curr->dynCast()) { type = callIndirect->heapType; } else { - funcInfo.effects = UnknownEffects; + funcInfo.effects = std::nullopt; return; } funcInfo.indirectCalledTypes.insert(type); } else if (effects.calls) { assert(!options.closedWorld); - funcInfo.effects = UnknownEffects; + funcInfo.effects = std::nullopt; } else { // No call here, but update throwing if we see it. (Only do so, // however, if we have effects; if we cleared it - see before - @@ -203,12 +202,17 @@ CallGraph buildCallGraph(const Module& module, return callGraph; } -void mergeMaybeEffects(std::optional& dest, - const std::optional& src) { +constexpr auto UnknownEffects = nullptr; + +// Merges effects from another connected component (const EffectAnalyzer*) or a +// function (std::optional&). +template +void mergeMaybeEffects(std::shared_ptr& dest, + const EffectAnalyzerPtr& src) { if (dest == UnknownEffects) { return; } - if (src == UnknownEffects) { + if (!src) { dest = UnknownEffects; return; } @@ -225,10 +229,13 @@ void mergeMaybeEffects(std::optional& dest, // - Merge all of the effects of functions within the CC // - Also merge the (already computed) effects of each callee CC // - Add trap effects for potentially recursive call chains -void propagateEffects(const Module& module, - const PassOptions& passOptions, - std::map& funcInfos, - const CallGraph& callGraph) { +void propagateEffects( + const Module& module, + const PassOptions& passOptions, + std::map& funcInfos, + std::unordered_map>& + typeEffects, + const CallGraph& callGraph) { // We only care about Functions that are roots, not types. // A type would be a root if a function exists with that type, but no-one // indirect calls the type. @@ -262,13 +269,13 @@ void propagateEffects(const Module& module, }; CallGraphSCCs sccs(funcNodes, funcInfos, callGraph, module); - std::vector> componentEffects; + std::vector> componentEffects; // Points to an index in componentEffects std::unordered_map nodeComponents; for (auto ccIterator : sccs) { - std::optional& ccEffects = - componentEffects.emplace_back(std::in_place, passOptions, module); + auto& ccEffects = componentEffects.emplace_back( + std::make_shared(passOptions, module)); std::vector cc(ccIterator.begin(), ccIterator.end()); std::vector ccFuncs; @@ -289,7 +296,7 @@ void propagateEffects(const Module& module, // Merge in effects from callees for (int calleeScc : calleeSccs) { const auto& calleeComponentEffects = componentEffects.at(calleeScc); - mergeMaybeEffects(ccEffects, calleeComponentEffects); + mergeMaybeEffects(ccEffects, calleeComponentEffects.get()); } // Add trap effects for potential cycles. @@ -306,6 +313,14 @@ void propagateEffects(const Module& module, ccEffects->trap = true; } } + } else if (ccFuncs.empty() && calleeSccs.empty()) { + // This node came from an indirect call to an uninhabited type. + // This CC must consist of exactly one type, because an uninhabited type + // can't make any indirect calls to other types. + // + // Since the type is uninhabited, this call must trap. + assert(cc.size() == 1); + ccEffects->trap = true; } // Aggregate effects within this CC @@ -317,27 +332,18 @@ void propagateEffects(const Module& module, } // Assign each function's effects to its CC effects. - for (Function* f : ccFuncs) { - if (!ccEffects) { - funcInfos.at(f).effects = UnknownEffects; - } else { - funcInfos.at(f).effects.emplace(*ccEffects); - } + for (auto node : cc) { + std::visit(overloaded{[&](HeapType type) { + if (ccEffects != UnknownEffects) { + typeEffects[type] = ccEffects; + } + }, + [&](Function* f) { f->effects = ccEffects; }}, + node); } } } -void copyEffectsToFunctions(const std::map& funcInfos) { - for (auto& [func, info] : funcInfos) { - func->effects.reset(); - if (!info.effects) { - continue; - } - - func->effects = std::make_shared(*info.effects); - } -} - struct GenerateGlobalEffects : public Pass { void run(Module* module) override { std::map funcInfos = @@ -346,9 +352,11 @@ struct GenerateGlobalEffects : public Pass { auto callGraph = buildCallGraph(*module, funcInfos, getPassOptions().closedWorld); - propagateEffects(*module, getPassOptions(), funcInfos, callGraph); - - copyEffectsToFunctions(funcInfos); + propagateEffects(*module, + getPassOptions(), + funcInfos, + module->indirectCallEffects, + callGraph); } }; diff --git a/src/support/utilities.h b/src/support/utilities.h index 3f40111c451..ae8822bf4e2 100644 --- a/src/support/utilities.h +++ b/src/support/utilities.h @@ -94,6 +94,19 @@ class Fatal { #define WASM_UNREACHABLE(msg) wasm::handle_unreachable() #endif +// Helper to create an invocable with an overloaded operator(), for use with +// std::visit e.g. +// std::visit( +// overloaded{ +// [](const A& a) { ... }, +// [](const B& b) { ... }}, +// variant) +template struct overloaded : Ts... { + using Ts::operator()...; +}; + +template overloaded(Ts...) -> overloaded; + } // namespace wasm #endif // wasm_support_utilities_h diff --git a/src/wasm.h b/src/wasm.h index df0c19669d3..40cdf896c19 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2463,12 +2463,14 @@ class Function : public Importable { // about the function-level annotations. CodeAnnotation funcAnnotations; - // The effects for this function, if they have been computed. We use a shared - // ptr here to avoid compilation errors with the forward-declared - // EffectAnalyzer. + // The effects for this function, if they have been computed. + // Effects are shared within connected components of the function call graph. + // e.g. if A calls B and B calls A, then A and B's effects are exactly the + // same and they share the same EffectAnalyzer. The same applies for indirect + // calls when --closed-world is enabled (see Module::indirectCallEffects). // // See addsEffects() in pass.h for more details. - std::shared_ptr effects; + std::shared_ptr effects; // Inlining metadata: whether to disallow full and/or partial inlining. This // is a toolchain-level hint. For more details, see Inlining.cpp. @@ -2722,6 +2724,24 @@ class Module { std::unordered_map typeNames; std::unordered_map typeIndices; + // Potential effects for bodies of indirect calls to this type. Populated by + // GlobalEffects when --closed-world is enabled. e.g. when we have a call to + // HeapType $A and functions $foo and $bar have types that are subtypes of $A, + // then an indirect call to $A has effects equal to the union of $foo and + // $bar. + // + // This is stored as a shared_ptr because effects are always shared within + // each connected component in the module's call graph. e.g. if A calls B + // and B calls A (directly or indirectly), then A and B have the same effects + // and can share an EffectAnalyzer. Also see Function::effects. + // + // This data is only meaningful for indirect calls. If no indirect call + // exists to a function, the data can be out of date (no effort is made to + // clean up the data if e.g. all indirect calls to a function are removed). + // TODO: Account for exactness here. + std::unordered_map> + indirectCallEffects; + MixedArena allocator; private: diff --git a/test/lit/passes/global-effects-closed-world-ignore-implicit-traps.wast b/test/lit/passes/global-effects-closed-world-ignore-implicit-traps.wast new file mode 100644 index 00000000000..888c3edc07a --- /dev/null +++ b/test/lit/passes/global-effects-closed-world-ignore-implicit-traps.wast @@ -0,0 +1,82 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --closed-world --ignore-implicit-traps --generate-global-effects --vacuum -S -o - | filecheck %s + +;; Tests for aggregating effects from indirect calls in GlobalEffects when +;; --closed-world is true. Continued from global-effects-closed-world.wast but +;; adding --ignore-implicit-traps. + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $nopType (func (param i32))) + (type $nopType (func (param i32))) + ;; CHECK: (type $otherNopType (func (param i32))) + (type $otherNopType (func (param i32))) + ) + + (table 1 1 (ref null $nopType)) + + ;; CHECK: (func $nop (type $nopType) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (export "nop") (type $nopType) + (nop) + ) + + ;; CHECK: (func $indirect-call-correct-type (type $0) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $indirect-call-correct-type + ;; Only implicit traps are possible here since the type is correct. + ;; A trap will happen if the index is out of bounds or contains null. + (call_indirect (type $nopType) (i32.const 1) (i32.const 0)) + ) + + ;; CHECK: (func $indirect-call-wrong-type (type $0) + ;; CHECK-NEXT: (call_indirect $0 (type $otherNopType) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $indirect-call-wrong-type + ;; This is guaranteed to trap because the type does not match the table. + (call_indirect (type $otherNopType) (i32.const 1) (i32.const 0)) + ) +) + +(module + (table 1 1 funcref) + + ;; CHECK: (type $maybe-has-effects (func (param i32))) + (type $maybe-has-effects (func (param i32))) + + ;; CHECK: (func $unreachable (type $maybe-has-effects) (param $0 i32) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $unreachable (export "unreachable") (type $maybe-has-effects) (param i32) + (unreachable) + ) + + ;; CHECK: (func $nop (type $maybe-has-effects) (param $0 i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $nop (export "nop") (type $maybe-has-effects) (param i32) + (nop) + ) + + ;; CHECK: (func $call-indirect-effectful-function (type $1) + ;; CHECK-NEXT: (call_indirect $0 (type $maybe-has-effects) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call-indirect-effectful-function + ;; This may be a nop or it may trap depending on the ref. + ;; We don't know so we don't optimize it out. + ;; Similar to the test in global-effects-closed-world.wast, but in this case + ;; we don't optimize the call out solely because we don't know what effects + ;; the target function will have and not because of potential implicit + ;; traps. + (call_indirect (type $maybe-has-effects) (i32.const 1) (i32.const 1)) + ) +) diff --git a/test/lit/passes/global-effects-closed-world-tnh.wast b/test/lit/passes/global-effects-closed-world-tnh.wast index 4c4558f8f95..39508c0c565 100644 --- a/test/lit/passes/global-effects-closed-world-tnh.wast +++ b/test/lit/passes/global-effects-closed-world-tnh.wast @@ -1,13 +1,16 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. -;; RUN: foreach %s %t wasm-opt -all --closed-world --traps-never-happen --generate-global-effects --vacuum -S -o - | filecheck %s +;; RUN: wasm-opt %s -all --closed-world --traps-never-happen --generate-global-effects --vacuum -S -o - | filecheck %s ;; Tests for aggregating effects from indirect calls in GlobalEffects when -;; --closed-world is true. Continued from global-effects-closed-world.wast. +;; --closed-world is true. Continued from global-effects-closed-world.wast, but +;; adding traps-never-happen. (module ;; CHECK: (type $nopType (func (param i32))) (type $nopType (func (param i32))) + (table 1 1 funcref) + ;; CHECK: (func $nop (type $nopType) (param $0 i32) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) @@ -16,22 +19,19 @@ ) ;; CHECK: (func $calls-nop-via-nullable-ref (type $1) (param $ref (ref null $nopType)) - ;; CHECK-NEXT: (call_ref $nopType - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) (func $calls-nop-via-nullable-ref (param $ref (ref null $nopType)) + ;; We would trap if $ref is null, but otherwise this has no effects. (call_ref $nopType (i32.const 1) (local.get $ref)) ) - ;; CHECK: (func $f (type $1) (param $ref (ref null $nopType)) + ;; CHECK: (func $calls-nop-via-ref (type $2) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) - (func $f (param $ref (ref null $nopType)) - ;; The only possible implementation of $nopType has no effects. - ;; $calls-nop-via-nullable-ref may trap from a null reference, but - ;; --traps-never-happen is enabled, so we're free to optimize this out. - (call $calls-nop-via-nullable-ref (local.get $ref)) + (func $calls-nop-via-ref + ;; We may trap due to index out of bounds or the function type not matching + ;; the table, but otherwise this has no possible effects. + (call_indirect (type $nopType) (i32.const 1) (i32.const 0)) ) ) diff --git a/test/lit/passes/global-effects-closed-world.wast b/test/lit/passes/global-effects-closed-world.wast index 77484c63d6d..7b1945fc3a0 100644 --- a/test/lit/passes/global-effects-closed-world.wast +++ b/test/lit/passes/global-effects-closed-world.wast @@ -6,6 +6,8 @@ ;; global-effects-closed-world-simplify-locals.wast. (module + (table 1 1 funcref) + ;; CHECK: (type $nopType (func (param i32))) (type $nopType (func (param i32))) @@ -17,18 +19,12 @@ ) ;; CHECK: (func $calls-nop-via-ref (type $1) (param $ref (ref $nopType)) - ;; CHECK-NEXT: (call_ref $nopType - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) (func $calls-nop-via-ref (param $ref (ref $nopType)) ;; This can only possibly be a nop in closed-world. - ;; Ideally vacuum could optimize this out but we don't have a way to share - ;; this information with other passes today. - ;; For now, we can at least annotate that the call to this function in $f - ;; has no effects. - ;; TODO: This call_ref could be marked as having no effects, like the call below. + ;; The equivalent for call_indirect is tested in + ;; test/lit/passes/global-effects-closed-world-tnh.wast. (call_ref $nopType (i32.const 1) (local.get $ref)) ) @@ -42,69 +38,27 @@ (call_ref $nopType (i32.const 1) (local.get $ref)) ) - - ;; CHECK: (func $f (type $1) (param $ref (ref $nopType)) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: ) - (func $f (param $ref (ref $nopType)) - ;; $calls-nop-via-ref has no effects because we determined that it can only - ;; call $nop. We can optimize this call out. - (call $calls-nop-via-ref (local.get $ref)) - ) - - ;; CHECK: (func $g (type $2) (param $ref (ref null $nopType)) - ;; CHECK-NEXT: (call $calls-nop-via-nullable-ref - ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $g (param $ref (ref null $nopType)) - ;; Similar to $f, but we may still trap here because the ref is null, so we - ;; don't optimize. - (call $calls-nop-via-nullable-ref (local.get $ref)) - ) -) - -;; Same as the above but with call_indirect -(module - ;; CHECK: (type $nopType (func (param i32))) - (type $nopType (func (param i32))) - - (table 1 1 funcref) - - ;; CHECK: (func $nop (type $nopType) (param $0 i32) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: ) - (func $nop (export "nop") (type $nopType) - (nop) - ) - - ;; CHECK: (func $calls-nop-via-ref (type $1) + ;; CHECK: (func $call-indirect-nop (type $3) ;; CHECK-NEXT: (call_indirect $0 (type $nopType) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $calls-nop-via-ref - ;; This can only possibly be a nop in closed-world. - ;; Ideally vacuum could optimize this out but we don't have a way to share - ;; this information with other passes today. - ;; For now, we can at least annotate that the call to this function in $f - ;; has no effects. - ;; TODO: This call_ref could be marked as having no effects, like the call below. + (func $call-indirect-nop + ;; The call body is guaranteed to have no effects, however the call_indirect + ;; itself may trap if the index is out of bounds or if the function we + ;; lookup doesn't match $nopType. The call can't be optimized out. + ;; This could be optimized out if --traps-never-happens or + ;; --ignore-implicit-traps (in fewer cases) is set. See + ;; global-effects-closed-world-tnh.wast and + ;; global-effects-closed-world-ignore-implicit-traps.wast. (call_indirect (type $nopType) (i32.const 1) (i32.const 0)) ) - - ;; CHECK: (func $f (type $1) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: ) - (func $f - ;; $calls-nop-via-ref has no effects because we determined that it can only - ;; call $nop. We can optimize this call out. - (call $calls-nop-via-ref) - ) ) (module + (table 1 1 funcref) + ;; CHECK: (type $maybe-has-effects (func (param i32))) (type $maybe-has-effects (func (param i32))) @@ -115,10 +69,10 @@ (unreachable) ) - ;; CHECK: (func $nop2 (type $maybe-has-effects) (param $0 i32) + ;; CHECK: (func $nop (type $maybe-has-effects) (param $0 i32) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) - (func $nop2 (export "nop2") (type $maybe-has-effects) (param i32) + (func $nop (export "nop") (type $maybe-has-effects) (param i32) (nop) ) @@ -129,60 +83,23 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $calls-effectful-function-via-ref (param $ref (ref $maybe-has-effects)) - (call_ref $maybe-has-effects (i32.const 1) (local.get $ref)) - ) - - ;; CHECK: (func $f (type $1) (param $ref (ref $maybe-has-effects)) - ;; CHECK-NEXT: (call $calls-effectful-function-via-ref - ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $f (param $ref (ref $maybe-has-effects)) - ;; This may be a nop or it may trap depending on the ref. + ;; This may be a nop or it may trap depending on the ref ;; We don't know so don't optimize it out. - (call $calls-effectful-function-via-ref (local.get $ref)) - ) -) - -;; Same as above but with call_indirect -(module - (table 1 1 funcref) - - ;; CHECK: (type $maybe-has-effects (func (param i32))) - (type $maybe-has-effects (func (param i32))) - - ;; CHECK: (func $unreachable (type $maybe-has-effects) (param $0 i32) - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) - (func $unreachable (export "unreachable") (type $maybe-has-effects) (param i32) - (unreachable) - ) - - ;; CHECK: (func $nop2 (type $maybe-has-effects) (param $0 i32) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: ) - (func $nop2 (export "nop2") (type $maybe-has-effects) (param i32) - (nop) + (call_ref $maybe-has-effects (i32.const 1) (local.get $ref)) ) - ;; CHECK: (func $calls-effectful-function-via-ref (type $1) + ;; CHECK: (func $call-indirect-effectful-function (type $2) ;; CHECK-NEXT: (call_indirect $0 (type $maybe-has-effects) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $calls-effectful-function-via-ref + (func $call-indirect-effectful-function + ;; As above, this may trap or be a nop depending on what's in the table. + ;; In addition, this may trap due to a type mismatch of index out of bounds. + ;; Since we don't know any of these things, don't optimize the call out. (call_indirect (type $maybe-has-effects) (i32.const 1) (i32.const 1)) ) - - ;; CHECK: (func $f (type $1) - ;; CHECK-NEXT: (call $calls-effectful-function-via-ref) - ;; CHECK-NEXT: ) - (func $f - ;; This may be a nop or it may trap depending on the ref. - ;; We don't know so don't optimize it out. - (call $calls-effectful-function-via-ref) - ) ) (module @@ -196,8 +113,9 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $calls-uninhabited (param $ref (ref $uninhabited)) - ;; It's impossible to create a ref to call this function with. - ;; TODO: Optimize this to (unreachable). + ;; There's no function with this type, so it's impossible to create a ref to + ;; call this function with. If this code is reached, it must trap. + ;; TODO: Optimize this to (unreachable) by creating a 'must trap' effect. (call_ref $uninhabited (i32.const 1) (local.get $ref)) ) @@ -209,31 +127,9 @@ ;; CHECK-NEXT: ) (func $calls-nullable-uninhabited (param $ref (ref null $uninhabited)) ;; This must be null, so it's guaranteed to trap and can't be optimized out. - ;; TODO: Optimize this to (unreachable). + ;; TODO: Optimize this to (unreachable) by creating a 'must trap' effect. (call_ref $uninhabited (i32.const 1) (local.get $ref)) ) - - - ;; CHECK: (func $f (type $1) (param $ref (ref $uninhabited)) - ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: ) - (func $f (param $ref (ref $uninhabited)) - ;; There's no function with this type, so it's impossible to create a ref to - ;; call this function with and there are no effects to aggregate. - ;; Remove this call. - (call $calls-uninhabited (local.get $ref)) - ) - - ;; CHECK: (func $g (type $2) (param $ref (ref null $uninhabited)) - ;; CHECK-NEXT: (call $calls-nullable-uninhabited - ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $g (param $ref (ref null $uninhabited)) - ;; Similar to above but we have a nullable reference, so we may trap and - ;; can't optimize the call out. - (call $calls-nullable-uninhabited (local.get $ref)) - ) ) (module @@ -256,48 +152,30 @@ (unreachable) ) - ;; CHECK: (func $calls-ref-with-supertype (type $1) (param $func (ref $super)) + ;; CHECK: (func $calls-ref-with-supertype (type $2) (param $func (ref $super)) ;; CHECK-NEXT: (call_ref $super ;; CHECK-NEXT: (local.get $func) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $calls-ref-with-supertype (param $func (ref $super)) - (call_ref $super (local.get $func)) - ) - - ;; CHECK: (func $calls-ref-with-exact-supertype (type $2) (param $func (ref (exact $super))) - ;; CHECK-NEXT: (call_ref $super - ;; CHECK-NEXT: (local.get $func) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $calls-ref-with-exact-supertype (param $func (ref (exact $super))) - (call_ref $super (local.get $func)) - ) - - ;; CHECK: (func $f (type $1) (param $func (ref $super)) - ;; CHECK-NEXT: (call $calls-ref-with-supertype - ;; CHECK-NEXT: (local.get $func) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $f (param $func (ref $super)) ;; Check that we account for subtyping correctly. ;; $super has no effects (i.e. the union of all effects of functions with ;; this type is empty). However, $sub does have effects, and we can call_ref ;; with that subtype, so we need to include the unreachable effect and we ;; can't optimize out this call. - (call $calls-ref-with-supertype (local.get $func)) + (call_ref $super (local.get $func)) ) - ;; CHECK: (func $g (type $2) (param $func (ref (exact $super))) - ;; CHECK-NEXT: (call $calls-ref-with-exact-supertype + ;; CHECK: (func $calls-ref-with-exact-supertype (type $3) (param $func (ref (exact $super))) + ;; CHECK-NEXT: (call_ref $super ;; CHECK-NEXT: (local.get $func) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $g (param $func (ref (exact $super))) + (func $calls-ref-with-exact-supertype (param $func (ref (exact $super))) ;; Same as above but this time our reference is the exact supertype ;; so we know not to aggregate effects from the subtype. ;; TODO: this case doesn't optimize today. Add exact ref support in the pass. - (call $calls-ref-with-exact-supertype (local.get $func)) + (call_ref $super (local.get $func)) ) ) @@ -325,21 +203,12 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $calls-type-with-effects-but-not-addressable (param $ref (ref $only-has-effects-in-not-addressable-function)) - (call_ref $only-has-effects-in-not-addressable-function (i32.const 1) (local.get $ref)) - ) - - ;; CHECK: (func $f (type $1) (param $ref (ref $only-has-effects-in-not-addressable-function)) - ;; CHECK-NEXT: (call $calls-type-with-effects-but-not-addressable - ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $f (param $ref (ref $only-has-effects-in-not-addressable-function)) ;; The type $has-effects-but-not-exported doesn't have an address because ;; it's not exported and it's never the target of a ref.func. ;; We should be able to determine that $ref can only point to $nop. ;; TODO: Only aggregate effects from functions that are addressed. - (call $calls-type-with-effects-but-not-addressable (local.get $ref)) - ) + (call_ref $only-has-effects-in-not-addressable-function (i32.const 1) (local.get $ref)) + ) ) (module @@ -406,18 +275,9 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $indirect-calls (param $ref (ref $t)) - (call_ref $t (i32.const 1) (local.get $ref)) - ) - - ;; CHECK: (func $f (type $1) (param $ref (ref $t)) - ;; CHECK-NEXT: (call $indirect-calls - ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $f (param $ref (ref $t)) - ;; $indirect-calls might end up calling an imported function, + ;; This might end up calling an imported function, ;; so we don't know anything about effects here - (call $indirect-calls (local.get $ref)) + (call_ref $t (i32.const 1) (local.get $ref)) ) ) @@ -435,15 +295,8 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $calls-unreachable (export "calls-unreachable") - (call_ref $t (unreachable)) - ) - - ;; CHECK: (func $f (type $0) - ;; CHECK-NEXT: (call $calls-unreachable) - ;; CHECK-NEXT: ) - (func $f ;; $t looks like it has no effects, but unreachable is passed in, ;; so preserve the trap. - (call $calls-unreachable) + (call_ref $t (unreachable)) ) ) diff --git a/test/lit/passes/global-effects-indirect-merge.wast b/test/lit/passes/global-effects-indirect-merge.wast new file mode 100644 index 00000000000..e7ca31ea11b --- /dev/null +++ b/test/lit/passes/global-effects-indirect-merge.wast @@ -0,0 +1,84 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --all-features --closed-world --generate-global-effects --vacuum --type-merging --remove-unused-types -S -o - | filecheck %s --check-prefix VACUUM_FIRST +;; RUN: wasm-opt %s --all-features --closed-world --generate-global-effects --type-merging --remove-unused-types --vacuum -S -o - | filecheck %s --check-prefix MERGE_FIRST + +;; Test that indirect call effects are preserved when types are rewritten +;; globally. When we rewrite $effectful and $not-effectful into the same type, +;; the resulting type has the same effects as the union of the two. This is +;; pessemistic since indirect calls that targeted $not-effectful now look like +;; they may target $effectful as well which is not true in practice. This is the +;; best we can do without preserving extra information before rewriting. + +(module + (rec + ;; VACUUM_FIRST: (type $effectful (func (result i32))) + ;; MERGE_FIRST: (type $effectful (func (result i32))) + (type $effectful (func (result i32))) + (type $not-effectful (func (result i32))) + ) + + ;; VACUUM_FIRST: (func $unreachable (type $effectful) (result i32) + ;; VACUUM_FIRST-NEXT: (unreachable) + ;; VACUUM_FIRST-NEXT: ) + ;; MERGE_FIRST: (func $unreachable (type $effectful) (result i32) + ;; MERGE_FIRST-NEXT: (unreachable) + ;; MERGE_FIRST-NEXT: ) + (func $unreachable (type $effectful) + (unreachable) + ) + + ;; VACUUM_FIRST: (func $const (type $effectful) (result i32) + ;; VACUUM_FIRST-NEXT: (i32.const 0) + ;; VACUUM_FIRST-NEXT: ) + ;; MERGE_FIRST: (func $const (type $effectful) (result i32) + ;; MERGE_FIRST-NEXT: (i32.const 0) + ;; MERGE_FIRST-NEXT: ) + (func $const (type $not-effectful) + (i32.const 0) + ) + + ;; VACUUM_FIRST: (func $f (type $1) + ;; VACUUM_FIRST-NEXT: (nop) + ;; VACUUM_FIRST-NEXT: ) + ;; MERGE_FIRST: (func $f (type $1) + ;; MERGE_FIRST-NEXT: (nop) + ;; MERGE_FIRST-NEXT: ) + (func $f + ;; Reference the functions in a ref.func so that it's possible that they're + ;; the target of indirect calls. + (drop (ref.func $unreachable)) + (drop (ref.func $const)) + ) + + ;; VACUUM_FIRST: (func $test (type $0) (param $effectful-ref (ref $effectful)) (param $not-effectful-ref (ref $effectful)) + ;; VACUUM_FIRST-NEXT: (drop + ;; VACUUM_FIRST-NEXT: (call_ref $effectful + ;; VACUUM_FIRST-NEXT: (local.get $effectful-ref) + ;; VACUUM_FIRST-NEXT: ) + ;; VACUUM_FIRST-NEXT: ) + ;; VACUUM_FIRST-NEXT: ) + ;; MERGE_FIRST: (func $test (type $0) (param $effectful-ref (ref $effectful)) (param $not-effectful-ref (ref $effectful)) + ;; MERGE_FIRST-NEXT: (drop + ;; MERGE_FIRST-NEXT: (call_ref $effectful + ;; MERGE_FIRST-NEXT: (local.get $not-effectful-ref) + ;; MERGE_FIRST-NEXT: ) + ;; MERGE_FIRST-NEXT: ) + ;; MERGE_FIRST-NEXT: (drop + ;; MERGE_FIRST-NEXT: (call_ref $effectful + ;; MERGE_FIRST-NEXT: (local.get $effectful-ref) + ;; MERGE_FIRST-NEXT: ) + ;; MERGE_FIRST-NEXT: ) + ;; MERGE_FIRST-NEXT: ) + (func $test (param $effectful-ref (ref $effectful)) (param $not-effectful-ref (ref $not-effectful)) + ;; If we run global effects followed by vacuum, we can tell that this call + ;; can't possibly have any effects and remove it. But if we run global + ;; effects, then merge types, we can no longer distinguish this from + ;; $effectful, so we have to conservatively not optimize this out. + (drop + (call_ref $not-effectful (local.get $not-effectful-ref)) + ) + (drop + (call_ref $effectful (local.get $effectful-ref)) + ) + ) +) From 0312a6f9c1737d86a010d503ba3a95d569924c27 Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Thu, 21 May 2026 22:42:42 -0700 Subject: [PATCH 136/168] Set branchesOut effect for call.without.effects (#8695) Fix for #8693 --- src/ir/effects.h | 5 +++++ test/lit/passes/vacuum-intrinsics.wast | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/ir/effects.h b/src/ir/effects.h index f28c8c27a24..f76fbd5b85e 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -726,6 +726,11 @@ class EffectAnalyzer { void visitCall(Call* curr) { if (Intrinsics(parent.module).isCallWithoutEffects(curr)) { + // The only effect this can have is to branch (which is an effect of the + // return, not the call). + if (curr->isReturn) { + parent.branchesOut = true; + } return; } diff --git a/test/lit/passes/vacuum-intrinsics.wast b/test/lit/passes/vacuum-intrinsics.wast index 8ae5c0a3317..8f62ea36d46 100644 --- a/test/lit/passes/vacuum-intrinsics.wast +++ b/test/lit/passes/vacuum-intrinsics.wast @@ -258,4 +258,25 @@ ;; Helper function for the above. (nop) ) + + ;; CHECK: (func $const (type $1) (result i32) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + (func $const (result i32) + (i32.const 1) + ) + + ;; CHECK: (func $sets-branch-effect (type $1) (result i32) + ;; CHECK-NEXT: (return_call $call.without.effects + ;; CHECK-NEXT: (ref.func $const) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $sets-branch-effect (result i32) + ;; $call-without-effects still has a branch effect, even though the callee + ;; body is assumed to have no effects. + ;; Without a branch effect, we'd be free to optimize this to (unreachable). + ;; Also, we're free to optimize away the (unreachable). + (return_call $call.without.effects (ref.func $const)) + (unreachable) + ) ) From 5cbd7f09bc5087e1c339f4df72fc01e3fd6aaf89 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 21 May 2026 23:31:10 -0700 Subject: [PATCH 137/168] Fix "pessemistic" => "pessimistic" (#8756) Found by my local spell checker git hook during a merge. --- test/lit/passes/global-effects-indirect-merge.wast | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lit/passes/global-effects-indirect-merge.wast b/test/lit/passes/global-effects-indirect-merge.wast index e7ca31ea11b..96669cf0212 100644 --- a/test/lit/passes/global-effects-indirect-merge.wast +++ b/test/lit/passes/global-effects-indirect-merge.wast @@ -5,7 +5,7 @@ ;; Test that indirect call effects are preserved when types are rewritten ;; globally. When we rewrite $effectful and $not-effectful into the same type, ;; the resulting type has the same effects as the union of the two. This is -;; pessemistic since indirect calls that targeted $not-effectful now look like +;; pessimistic since indirect calls that targeted $not-effectful now look like ;; they may target $effectful as well which is not true in practice. This is the ;; best we can do without preserving extra information before rewriting. From 17d9262e2163446b0f3421474109eaed50bda54e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 22 May 2026 09:55:29 -0700 Subject: [PATCH 138/168] [NFC] Document reference construction in closed world (#8761) See #8754 --- src/pass.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pass.h b/src/pass.h index f5088e5e7ee..02af3f57eb5 100644 --- a/src/pass.h +++ b/src/pass.h @@ -199,8 +199,8 @@ struct PassOptions { // Assume code outside of the module does not inspect or interact with GC and // function references, with the goal of being able to aggressively optimize // all user-defined types. The outside may hold on to references and pass them - // back in, but may not inspect their contents, call them, or reflect on their - // types in any way. + // back in, but may not inspect their contents, call them, construct them, or + // reflect on their types in any way. // // By default we do not make this assumption, and assume anything that escapes // to the outside may be inspected in detail, which prevents us from e.g. From 505dae4a6fa9fe4eee8e9c92ab7380699e73717f Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Fri, 22 May 2026 11:09:34 -0700 Subject: [PATCH 139/168] Set `BUILD_STATIC_LIB` under emscripten (#8762) This fixes the following warning when building with emscripten: ``` CMake Warning (dev) at CMakeLists.txt:460 (add_library): ADD_LIBRARY called with SHARED option but the target platform does not support dynamic linking. Building a STATIC library instead. This may lead to problems. This warning is for project developers. Use -Wno-dev to suppress it. ``` --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 97863ffb52b..109d5c8f784 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,8 +53,9 @@ if(EMSCRIPTEN) endif() option(BUILD_STATIC_LIB "Build as a static library" OFF) -if(MSVC) +if(MSVC OR EMSCRIPTEN) # We don't have dllexport declarations set up for Windows yet. + # With emscripten we require a static library to create binaryen_js correctly. set(BUILD_STATIC_LIB ON) endif() From 36ef125bc7bb702790fde39e4fdaa14e767f053e Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 22 May 2026 15:49:29 -0700 Subject: [PATCH 140/168] Fix block-nested Pop in GUFA (#8747) In GUFA, when optimizing RefEq or RefTest expressions, they are replaced by constants if the optimizer can prove the two sides have no intersection. However, if the expression contains side effects (like a Pop), getDroppedChildrenAndAppend is used to preserve them, which wraps the side-effect expressions in a Block. If the Pop is nested inside this Block, it becomes invalidly nested within the catch block, which violates validation rules. GUFA has a fixup pass (EHUtils::handleBlockNestedPops) designed to fix this by spilling the Pop to a local, but it was not being run because visitRefEq and visitRefTest failed to set the `optimized = true` flag. Fix the issue by setting `optimized = true` when RefEq or RefTest are optimized, ensuring the post-optimization cleanups (including the nested pop fixup) are executed. Add a regression test to gufa-cast-all.wast. --- src/passes/GUFA.cpp | 6 +-- test/lit/passes/gufa-cast-all.wast | 62 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/passes/GUFA.cpp b/src/passes/GUFA.cpp index a4567aaea6d..addba6b7ee3 100644 --- a/src/passes/GUFA.cpp +++ b/src/passes/GUFA.cpp @@ -97,8 +97,8 @@ struct GUFAOptimizer std::unordered_map newContents; Expression* replaceCurrent(Expression* rep) { + optimized = true; newContents[rep] = oracle.getContents(getCurrent()); - return WalkerPass< PostWalker>>::replaceCurrent(rep); @@ -140,7 +140,6 @@ struct GUFAOptimizer // code. replaceCurrent(getDroppedChildrenAndAppend( curr, wasm, options, builder.makeUnreachable())); - optimized = true; return; } @@ -169,7 +168,6 @@ struct GUFAOptimizer // valid here. if (Type::isSubType(c->type, curr->type)) { replaceCurrent(getDroppedChildrenAndAppend(curr, wasm, options, c)); - optimized = true; } else { // The type is not compatible: we cannot place |c| in this location, even // though we have proven it is the only value possible here. @@ -214,7 +212,6 @@ struct GUFAOptimizer assert(Properties::isConstantExpression(c)); replaceCurrent(getDroppedChildrenAndAppend( curr, wasm, options, builder.makeUnreachable())); - optimized = true; } } } @@ -391,7 +388,6 @@ struct GUFAOptimizer if (oracleType.isRef() && oracleType != curr->type && Type::isSubType(oracleType, curr->type)) { replaceCurrent(Builder(*getModule()).makeRefCast(curr, oracleType)); - optimized = true; } } }; diff --git a/test/lit/passes/gufa-cast-all.wast b/test/lit/passes/gufa-cast-all.wast index 28248674883..bffd88e7714 100644 --- a/test/lit/passes/gufa-cast-all.wast +++ b/test/lit/passes/gufa-cast-all.wast @@ -392,3 +392,65 @@ ) ) + +;; Regression test for bug where optimizing expressions containing a Pop could +;; result in the Pop becoming invalidly nested inside a Block (created to +;; preserve side effects), and the optimizer failed to run the nested pop fixup +;; because it didn't mark the function as optimized. +(module + ;; CHECK: (type $array (sub (array anyref))) + (type $array (sub (array (ref null any)))) + ;; CHECK: (type $tag-sig (func (param (ref null $array)))) + (type $tag-sig (func (param (ref null $array)))) + ;; CHECK: (type $2 (func)) + + ;; CHECK: (tag $tag (type $tag-sig) (param (ref null $array))) + (tag $tag (type $tag-sig) (param (ref null $array))) + + ;; CHECK: (func $test (type $2) + ;; CHECK-NEXT: (local $0 (ref null $array)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (try (result (ref i31)) + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (ref.i31 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $tag + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (pop (ref null $array)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.i31 + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + (drop + (try (result (ref i31)) + (do + ;; This doesn't throw, so everything in the catch is unreachable. + (ref.i31 (i32.const 0)) + ) + (catch $tag + (ref.i31 + (ref.eq + (pop (ref null $array)) + (ref.i31 (i32.const 0)) + ) + ) + ) + ) + ) + ) +) From f19f8e13b9f1df43db1adacccbca6ef9003ba601 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 22 May 2026 16:01:00 -0700 Subject: [PATCH 141/168] Fix typo in mark-js-called.wast (#8755) Found by my local spellchecking git hook when performing a merge. --- test/lit/passes/mark-js-called.wast | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lit/passes/mark-js-called.wast b/test/lit/passes/mark-js-called.wast index 10a2ca57ddd..10b4bfc0ebd 100644 --- a/test/lit/passes/mark-js-called.wast +++ b/test/lit/passes/mark-js-called.wast @@ -3,7 +3,7 @@ ;; RUN: foreach %s %t wasm-opt --mark-js-called -all -S -o - | filecheck %s ;; $configured will be marked as @binaryen.js.called. $already is already marked, -;; and nothing changess. $unconfigured* are not in configureAll so they are left +;; and nothing changes. $unconfigured* are not in configureAll so they are left ;; alone. (module From 962bd54ee9f5c32a1209d2ad63dbfafaf9231960 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 22 May 2026 16:40:46 -0700 Subject: [PATCH 142/168] HeapStoreOptimization: Migrate from invalidates to orderedBefore (#8758) Replace the coarse `invalidates` check in `HeapStoreOptimization` with `orderedBefore`. This allows `HeapStoreOptimization` to optimize heap stores across release stores (e.g., moving a GC read before a release store), while still correctly blocking them from being moved before acquire loads. Added a lit test to verify the asymmetrical reordering behavior with release/acquire atomics on shared GC structs and Wasm memory. TAG=agy --- src/passes/HeapStoreOptimization.cpp | 8 +- test/lit/passes/heap-store-atomics.wast | 311 ++++++++++++++++++++++++ 2 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 test/lit/passes/heap-store-atomics.wast diff --git a/src/passes/HeapStoreOptimization.cpp b/src/passes/HeapStoreOptimization.cpp index c720c5f0a41..6c6e729e744 100644 --- a/src/passes/HeapStoreOptimization.cpp +++ b/src/passes/HeapStoreOptimization.cpp @@ -181,7 +181,7 @@ struct HeapStoreOptimization // effects. auto firstEffects = effects(list[i]); auto secondEffects = effects(list[j]); - if (secondEffects.invalidates(firstEffects)) { + if (firstEffects.orderedBefore(secondEffects)) { return false; } @@ -241,7 +241,7 @@ struct HeapStoreOptimization if (!new_->isWithDefault()) { for (Index i = index + 1; i < operands.size(); i++) { auto operandEffects = effects(operands[i]); - if (operandEffects.invalidates(setValueEffects)) { + if (operandEffects.orderedBefore(setValueEffects)) { // TODO: we could use locals to reorder everything return false; } @@ -252,7 +252,7 @@ struct HeapStoreOptimization // if it exists. if (new_->desc) { auto descEffects = effects(new_->desc); - if (descEffects.invalidates(setValueEffects)) { + if (descEffects.orderedBefore(setValueEffects)) { // TODO: we could use locals to reorder everything return false; } @@ -264,7 +264,7 @@ struct HeapStoreOptimization // the optimization X' would happen first. ShallowEffectAnalyzer structNewEffects( getPassOptions(), *getModule(), new_); - if (structNewEffects.invalidates(setValueEffects)) { + if (structNewEffects.orderedBefore(setValueEffects)) { return false; } diff --git a/test/lit/passes/heap-store-atomics.wast b/test/lit/passes/heap-store-atomics.wast new file mode 100644 index 00000000000..a5254d2408e --- /dev/null +++ b/test/lit/passes/heap-store-atomics.wast @@ -0,0 +1,311 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --heap-store-optimization -all -S -o - | filecheck %s + +(module + ;; CHECK: (type $struct (shared (struct (field (mut i32)) (field (mut i32))))) + (type $struct (shared (struct (field (mut i32)) (field (mut i32))))) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $described (shared (descriptor $desc) (struct (field (mut i32))))) + (type $described (shared (descriptor $desc) (struct (field (mut i32))))) + ;; CHECK: (type $desc (shared (describes $described) (struct))) + (type $desc (shared (describes $described) (struct))) + ) + + ;; CHECK: (memory $mem 1 1 shared) + (memory $mem 1 1 shared) + + ;; Test 1: Disallowed reordering (GC read NOT moved before Wasm acquire load) + ;; CHECK: (func $disallowed (type $3) (param $x (ref $struct)) (param $other (ref $struct)) + ;; CHECK-NEXT: (local $ref (ref $struct)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new $struct + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.set $struct 0 + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: (struct.get $struct 1 + ;; CHECK-NEXT: (local.get $other) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $disallowed (param $x (ref $struct)) (param $other (ref $struct)) + (local $ref (ref $struct)) + (local.set $ref + (struct.new $struct + (i32.const 0) + ;; Acquire load (returns i32 via block) + (block (result i32) + (drop (i32.atomic.load acqrel (i32.const 0))) + (i32.const 0) + ) + ) + ) + ;; struct.set with GC read value. + (struct.set $struct 0 + (local.get $ref) + (struct.get $struct 1 (local.get $other)) + ) + ) + + ;; Test 2: Allowed reordering (GC read moved before Wasm release store) + ;; CHECK: (func $allowed (type $3) (param $x (ref $struct)) (param $other (ref $struct)) + ;; CHECK-NEXT: (local $ref (ref $struct)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new $struct + ;; CHECK-NEXT: (struct.get $struct 1 + ;; CHECK-NEXT: (local.get $other) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $allowed (param $x (ref $struct)) (param $other (ref $struct)) + (local $ref (ref $struct)) + (local.set $ref + (struct.new $struct + (i32.const 0) + ;; Release store (returns i32 via block) + (block (result i32) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + (i32.const 0) + ) + ) + ) + ;; struct.set with GC read value + (struct.set $struct 0 + (local.get $ref) + (struct.get $struct 1 (local.get $other)) + ) + ) + ;; Test 3: Swap allowed (GC read in struct.new swapped with subsequent acquire load) + ;; CHECK: (func $swap_allowed (type $4) (param $other (ref $struct)) + ;; CHECK-NEXT: (local $ref (ref $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new $struct + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $swap_allowed (param $other (ref $struct)) + (local $ref (ref $struct)) + (local.set $ref + (struct.new $struct + (struct.get $struct 1 (local.get $other)) + (i32.const 0) + ) + ) + (drop (i32.atomic.load acqrel (i32.const 0))) + (struct.set $struct 0 + (local.get $ref) + (i32.const 42) + ) + ) + + ;; Test 4: Swap disallowed (struct.new with acquire load NOT swapped with subsequent GC read) + ;; CHECK: (func $swap_disallowed (type $4) (param $other (ref $struct)) + ;; CHECK-NEXT: (local $ref (ref $struct)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new $struct + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $struct 1 + ;; CHECK-NEXT: (local.get $other) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.set $struct 0 + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $swap_disallowed (param $other (ref $struct)) + (local $ref (ref $struct)) + (local.set $ref + (struct.new $struct + (block (result i32) + (drop (i32.atomic.load acqrel (i32.const 0))) + (i32.const 0) + ) + (i32.const 0) + ) + ) + (drop (struct.get $struct 1 (local.get $other))) + (struct.set $struct 0 + (local.get $ref) + (i32.const 42) + ) + ) + ;; Test 5: GC read in struct.set value CAN move before release store in descriptor + ;; CHECK: (func $desc_allowed (type $5) (param $other (ref $struct)) (param $d (ref (exact $desc))) + ;; CHECK-NEXT: (local $ref (ref $described)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new_desc $described + ;; CHECK-NEXT: (struct.get $struct 1 + ;; CHECK-NEXT: (local.get $other) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result (ref (exact $desc))) + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $d) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $desc_allowed (param $other (ref $struct)) (param $d (ref (exact $desc))) + (local $ref (ref $described)) + (local.set $ref + (struct.new_desc $described + (i32.const 0) + (block (result (ref (exact $desc))) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + (local.get $d) + ) + ) + ) + (struct.set $described 0 + (local.get $ref) + (struct.get $struct 1 (local.get $other)) + ) + ) + + ;; Test 6: GC read in struct.set value CANNOT move before acquire load in descriptor + ;; CHECK: (func $desc_disallowed (type $5) (param $other (ref $struct)) (param $d (ref (exact $desc))) + ;; CHECK-NEXT: (local $ref (ref $described)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new_desc $described + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (block (result (ref (exact $desc))) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $d) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.set $described 0 + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: (struct.get $struct 1 + ;; CHECK-NEXT: (local.get $other) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $desc_disallowed (param $other (ref $struct)) (param $d (ref (exact $desc))) + (local $ref (ref $described)) + (local.set $ref + (struct.new_desc $described + (i32.const 0) + (block (result (ref (exact $desc))) + (drop (i32.atomic.load acqrel (i32.const 0))) + (local.get $d) + ) + ) + ) + (struct.set $described 0 + (local.get $ref) + (struct.get $struct 1 (local.get $other)) + ) + ) + + ;; Test 7: Memory load in struct.set value CAN move before shallow trap (nullable desc) of struct.new + ;; CHECK: (func $shallow_allowed (type $6) (param $d (ref (exact $desc))) + ;; CHECK-NEXT: (local $ref (ref $described)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new_desc $described + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $shallow_allowed (param $d (ref (exact $desc))) + (local $ref (ref $described)) + (local.set $ref + (struct.new_desc $described + (i32.const 0) + (ref.null $desc) + ) + ) + (struct.set $described 0 + (local.get $ref) + (i32.load (i32.const 0)) + ) + ) + + ;; Test 8: Memory store in struct.set value CANNOT move before shallow trap (nullable desc) of struct.new + ;; CHECK: (func $shallow_disallowed (type $6) (param $d (ref (exact $desc))) + ;; CHECK-NEXT: (local $ref (ref $described)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new_desc $described + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.set $described 0 + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1337) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $shallow_disallowed (param $d (ref (exact $desc))) + (local $ref (ref $described)) + (local.set $ref + (struct.new_desc $described + (i32.const 0) + (ref.null $desc) + ) + ) + (struct.set $described 0 + (local.get $ref) + (block (result i32) + (i32.store (i32.const 0) (i32.const 42)) + (i32.const 1337) + ) + ) + ) +) From a69c666279b7bc9df3f83895ada44f84ff56eb7c Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 22 May 2026 16:59:59 -0700 Subject: [PATCH 143/168] [wasm-split] Generate fillers in MVP + --no-placenolders (#8759) When --no-placeholder is given and reference-types is disabled, we cannot fill the table with `ref.nulls`. Previously we were generating `ref.null`s anyway, creating invalid modules. This generates filler functions in place of `ref.null`s. --- src/ir/module-splitting.cpp | 48 ++++++++++++++------- test/lit/wasm-split/minimized-exports.wast | 7 ++- test/lit/wasm-split/no-reftypes-filler.wast | 32 ++++++++++++++ 3 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 test/lit/wasm-split/no-reftypes-filler.wast diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 735e747938f..27b7302c105 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -1125,6 +1125,8 @@ void ModuleSplitter::setupTablePatching() { } std::map> moduleToReplacedElems; + Name fillerName; + Type fillerType = Type(Signature(Type::none, Type::none), NonNullable, Exact); // Replace table references to secondary functions with an imported // placeholder that encodes the table index in its name: // `importNamespace`.`index`. @@ -1145,22 +1147,38 @@ void ModuleSplitter::setupTablePatching() { Name secondaryName = config.secondaryNames.at(secondaryIndex); auto* secondaryFunc = secondary.getFunction(ref->func); moduleToReplacedElems[&secondary][index] = secondaryFunc; - if (!config.usePlaceholders) { - // TODO: This can create active element segments with lots of nulls. We - // should optimize them like we do data segments with zeros. - elem = Builder(primary).makeRefNull(HeapType::nofunc); - return; + + if (config.usePlaceholders) { + auto placeholder = std::make_unique(); + placeholder->module = config.placeholderNamespacePrefix.toString() + + "." + secondaryName.toString(); + placeholder->base = std::to_string(index); + placeholder->name = Names::getValidFunctionName( + primary, std::string("placeholder_") + placeholder->base.toString()); + placeholder->hasExplicitName = true; + placeholder->type = secondaryFunc->type.with(Inexact); + elem = + Builder(primary).makeRefFunc(placeholder->name, placeholder->type); + primary.addFunction(std::move(placeholder)); + + } else { // !config.usePlaceholders + if (primary.features.hasReferenceTypes()) { + // TODO: This can create active element segments with lots of nulls. + // We should optimize them like we do data segments with zeros. + elem = Builder(primary).makeRefNull(HeapType::nofunc); + return; + } + // When reference-types is not enabled, we can't use a ref.null. Put a + // filler function that contains an unreachable. + if (!fillerName) { + fillerName = Names::getValidFunctionName(primary, "filler"); + auto filler = Builder::makeFunction( + fillerName, fillerType, {}, Builder(primary).makeUnreachable()); + filler->hasExplicitName = true; + primary.addFunction(std::move(filler)); + } + elem = Builder(primary).makeRefFunc(fillerName, fillerType); } - auto placeholder = std::make_unique(); - placeholder->module = config.placeholderNamespacePrefix.toString() + "." + - secondaryName.toString(); - placeholder->base = std::to_string(index); - placeholder->name = Names::getValidFunctionName( - primary, std::string("placeholder_") + placeholder->base.toString()); - placeholder->hasExplicitName = true; - placeholder->type = secondaryFunc->type.with(Inexact); - elem = Builder(primary).makeRefFunc(placeholder->name, placeholder->type); - primary.addFunction(std::move(placeholder)); }); if (moduleToReplacedElems.size() == 0) { diff --git a/test/lit/wasm-split/minimized-exports.wast b/test/lit/wasm-split/minimized-exports.wast index b7d28afab60..5463407b719 100644 --- a/test/lit/wasm-split/minimized-exports.wast +++ b/test/lit/wasm-split/minimized-exports.wast @@ -5,8 +5,8 @@ ;; PRIMARY: (module ;; PRIMARY-NEXT: (type $0 (func)) ;; PRIMARY-NEXT: (table $0 1 funcref) -;; PRIMARY-NEXT: (elem $0 (table $0) (i32.const 0) funcref (item (ref.null nofunc))) -;; PRIMARY-NEXT: (export "baz" (func $2) +;; PRIMARY-NEXT: (elem $0 (i32.const 0) $3) +;; PRIMARY-NEXT: (export "baz" (func $2)) ;; PRIMARY-NEXT: (export "%a" (func $1)) ;; PRIMARY-NEXT: (export "%b" (func $0)) ;; PRIMARY-NEXT: (export "%c" (table $0)) @@ -21,6 +21,9 @@ ;; PRIMARY-NEXT: (i32.const 0) ;; PRIMARY-NEXT: ) ;; PRIMARY-NEXT: ) +;; PRIMARY-NEXT: (func $3 +;; PRIMARY-NEXT: (unreachable) +;; PRIMARY-NEXT: ) ;; PRIMARY-NEXT: ) ;; SECONDARY: (module diff --git a/test/lit/wasm-split/no-reftypes-filler.wast b/test/lit/wasm-split/no-reftypes-filler.wast new file mode 100644 index 00000000000..955e74a88eb --- /dev/null +++ b/test/lit/wasm-split/no-reftypes-filler.wast @@ -0,0 +1,32 @@ +;; RUN: wasm-split %s --disable-reference-types --keep-funcs=keep --export-prefix='%' -o1 %t.1.wasm -o2 %t.2.wasm --no-placeholders -g +;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY + +;; When --no-placeholder is given and reference-types is disabled, we cannot +;; fill the table with ref.nulls. Use a filler function. + +(module + ;; PRIMARY: (table $table 4 4 funcref) + ;; PRIMARY: (elem $0 (i32.const 0) $filler $filler $filler $keep) + (table $table 4 4 funcref) + (elem (i32.const 0) $split1 $split2 $split3 $keep) + + ;; PRIMARY: (func $filler + ;; PRIMARY-NEXT: (unreachable) + ;; PRIMARY-NEXT: ) + + (func $keep + (nop) + ) + + (func $split1 + (nop) + ) + + (func $split2 (param i32) + (nop) + ) + + (func $split3 (param i32) (result i32) + (i32.const 0) + ) +) From abfdaeee8c8bd7eb9db773771374c3ad536675c4 Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Fri, 22 May 2026 17:38:57 -0700 Subject: [PATCH 144/168] Use indirect call effects in LinearExecutionWalker (#8738) Part of #8615. Follows up from #8637 now that global effects for indirect calls has been added. Allows LinearExecutionWalker to *not* halt and continue scanning when an indirect call is encountered in the case the global effects have been computed under `--closed-world` and we determined that the indirect call can't have any throw effects. LinearExecutionWalker is used in the following passes: * LocalGraph * LocalCSE * OptimizeCasts * SimplifyGlobals * SimplifyLocals * GUFA * TypeRefining --- src/ir/linear-execution.h | 60 ++++++++----- src/support/utilities.h | 8 ++ .../simplify-locals-global-effects-eh.wast | 84 ++++++++++++++----- 3 files changed, 111 insertions(+), 41 deletions(-) diff --git a/src/ir/linear-execution.h b/src/ir/linear-execution.h index 167400a0137..9e69405ff7c 100644 --- a/src/ir/linear-execution.h +++ b/src/ir/linear-execution.h @@ -80,7 +80,11 @@ struct LinearExecutionWalker : public PostWalker { static void scan(SubType* self, Expression** currp) { Expression* curr = *currp; - auto handleCall = [&](bool mayThrow, bool isReturn) { + auto handleCall = [&](bool isReturn, bool refutesThrowEffect) { + bool mayThrow = !self->getModule() || + self->getModule()->features.hasExceptionHandling(); + mayThrow = mayThrow && !refutesThrowEffect; + if (!self->connectAdjacentBlocks) { // Control is nonlinear if we return or throw. Traps don't need to be // taken into account since they don't break control flow in a way @@ -156,40 +160,54 @@ struct LinearExecutionWalker : public PostWalker { case Expression::Id::CallId: { auto* call = curr->cast(); - bool mayThrow = !self->getModule() || - self->getModule()->features.hasExceptionHandling(); - if (mayThrow && self->getModule()) { - auto* effects = - self->getModule()->getFunction(call->target)->effects.get(); - - if (effects && !effects->throws_) { - mayThrow = false; + bool refutesThrowEffect = false; + if (self->getModule()) { + auto* func = self->getModule()->getFunctionOrNull(call->target); + // TODO: `func` might not exist here because of #8753. Fix this + // and remove the null check. + if (func && func->effects) { + refutesThrowEffect = !func->effects->throws_; } } - handleCall(mayThrow, call->isReturn); + handleCall(call->isReturn, refutesThrowEffect); break; } case Expression::Id::CallRefId: { auto* callRef = curr->cast(); - // TODO: Effect analysis for indirect calls isn't implemented yet. - // Assume any indirect call may throw for now. - bool mayThrow = !self->getModule() || - self->getModule()->features.hasExceptionHandling(); + bool refutesThrowEffect = [&]() { + if (!self->getModule()) { + return false; + } + if (!callRef->target->type.isRef()) { + // This is an unreachable, so no throws effect. + return true; + } - handleCall(mayThrow, callRef->isReturn); + auto* effects = find_or_null(self->getModule()->indirectCallEffects, + callRef->target->type.getHeapType()); + if (!effects) { + return false; + } + return !(*effects)->throws_; + }(); + + handleCall(callRef->isReturn, refutesThrowEffect); break; } case Expression::Id::CallIndirectId: { auto* callIndirect = curr->cast(); - // TODO: Effect analysis for indirect calls isn't implemented yet. - // Assume any indirect call may throw for now. - bool mayThrow = !self->getModule() || - self->getModule()->features.hasExceptionHandling(); - - handleCall(mayThrow, callIndirect->isReturn); + bool refutesThrowEffect = false; + if (self->getModule()) { + if (auto* effects = find_or_null( + self->getModule()->indirectCallEffects, callIndirect->heapType); + effects) { + refutesThrowEffect = !(*effects)->throws_; + } + } + handleCall(callIndirect->isReturn, refutesThrowEffect); break; } case Expression::Id::TryId: { diff --git a/src/support/utilities.h b/src/support/utilities.h index ae8822bf4e2..0aad86c94e1 100644 --- a/src/support/utilities.h +++ b/src/support/utilities.h @@ -107,6 +107,14 @@ template struct overloaded : Ts... { template overloaded(Ts...) -> overloaded; +// Lookup a value from `map` and return a pointer to the underlying value +// or nullptr if not present. Returns a const pointer if `map` is const and +// non-const otherwise +auto* find_or_null(auto& map, const auto& key) { + auto it = map.find(key); + return it != map.end() ? &it->second : nullptr; +} + } // namespace wasm #endif // wasm_support_utilities_h diff --git a/test/lit/passes/simplify-locals-global-effects-eh.wast b/test/lit/passes/simplify-locals-global-effects-eh.wast index ff11e7487b6..f616208a9b1 100644 --- a/test/lit/passes/simplify-locals-global-effects-eh.wast +++ b/test/lit/passes/simplify-locals-global-effects-eh.wast @@ -56,10 +56,11 @@ ) (module + ;; CHECK: (type $throw-type (func (result f64))) + ;; CHECK: (type $const-type (func (result f32))) (type $const-type (func (result f32))) - ;; CHECK: (type $throw-type (func (result f64))) (type $throw-type (func (result f64))) ;; CHECK: (global $g (mut i32) (i32.const 0)) @@ -68,7 +69,7 @@ ;; CHECK: (table $t 2 2 funcref) (table $t 2 2 funcref) - ;; CHECK: (tag $t (type $2)) + ;; CHECK: (tag $t (type $4)) (tag $t) ;; CHECK: (func $const (type $const-type) (result f32) @@ -90,32 +91,48 @@ ) (elem declare $throws) - ;; CHECK: (func $read-g (type $3) (param $ref (ref null $const-type)) (result i32) + ;; CHECK: (func $read-g-with-nop-call-ref (type $5) (param $ref (ref null $const-type)) (result i32) ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (global.get $g) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (call_ref $const-type ;; CHECK-NEXT: (local.get $ref) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (global.get $g) ;; CHECK-NEXT: ) - (func $read-g (param $ref (ref null $const-type)) (result i32) + (func $read-g-with-nop-call-ref (param $ref (ref null $const-type)) (result i32) (local $x i32) (local.set $x (global.get $g)) - ;; With more precise effect analysis for indirect calls, we can determine - ;; that the only possible target for this ref is $const in a closed world, - ;; which wouldn't block our optimizations. - ;; TODO: Add effects analysis for indirect calls. + ;; With --closed-world enabled, we can tell that this can only possibly call + ;; $const, which doesn't block our optimizations. (drop (call_ref $const-type (local.get $ref))) (local.get $x) ) - ;; CHECK: (func $read-g-with-throw-in-between (type $4) (param $ref (ref $throw-type)) (result i32) + ;; CHECK: (func $read-g-with-nop-call-indirect (type $2) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call_indirect $t (type $const-type) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + (func $read-g-with-nop-call-indirect (result i32) + (local $x i32) + (local.set $x (global.get $g)) + + ;; Similar to above with call_indirect instead of call_ref. + (drop (call_indirect (type $const-type) (i32.const 0))) + + (local.get $x) + ) + + ;; CHECK: (func $read-g-with-effectful-call-ref (type $3) (param $ref (ref $throw-type)) (result i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (local.set $x ;; CHECK-NEXT: (global.get $g) @@ -127,7 +144,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) - (func $read-g-with-throw-in-between (param $ref (ref $throw-type)) (result i32) + (func $read-g-with-effectful-call-ref (param $ref (ref $throw-type)) (result i32) (local $x i32) (local.set $x (global.get $g)) @@ -138,25 +155,52 @@ (local.get $x) ) - ;; CHECK: (func $read-g-with-call-indirect-in-between (type $5) (result i32) + ;; CHECK: (func $read-g-with-effectful-call-indirect (type $3) (param $ref (ref $throw-type)) (result i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (local.set $x ;; CHECK-NEXT: (global.get $g) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (call_indirect $t (type $const-type) + ;; CHECK-NEXT: (call_indirect $t (type $throw-type) ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) - (func $read-g-with-call-indirect-in-between (result i32) + (func $read-g-with-effectful-call-indirect (param $ref (ref $throw-type)) (result i32) (local $x i32) (local.set $x (global.get $g)) - ;; Similar to above with call_indirect instead of call_ref. - ;; TODO: Add effects analysis for indirect calls. - (drop (call_indirect (type $const-type) (i32.const 0))) + ;; Similar to above, except here we can tell that the indirect call may + ;; throw so optimization is halted. + (drop (call_indirect (type $throw-type) (i32.const 0))) + + (local.get $x) + ) + + ;; CHECK: (func $read-g-with-unreachable-call-ref (type $2) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block ;; (replaces unreachable CallRef we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + (func $read-g-with-unreachable-call-ref (result i32) + (local $x i32) + (local.set $x (global.get $g)) + + ;; This is guaranteed to trap, and the type immediate doesn't matter. + ;; TODO: we should be able to optimize this, but something is likely missing + ;; in SimplifyGlobals (LinearExecutionWalker handles this case correctly). + (drop (call_ref $throw-type (unreachable))) (local.get $x) ) From 8b61409636f76adb679d90370c1da867fc9d61ec Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 22 May 2026 19:34:27 -0700 Subject: [PATCH 145/168] [wasm-split] Scan trapping globals if custom descriptors is enabled (NFC) (#8768) We don't need to compute effects for all global initializers unless custom-descriptors is not enabled. Should have done this with #8742. --- src/ir/module-splitting.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 27b7302c105..78141676375 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -791,13 +791,16 @@ void ModuleSplitter::shareImportableItems() { primaryUsed.globals.insert(tableManager.activeBase.global); } - // Trapping globals should stay in the primary module to preserve the trapping - // behavior upon instantiation. - for (auto& global : primary.globals) { - if (global->init && - EffectAnalyzer(config.passOptions, primary, global->init) - .hasUnremovableSideEffects()) { - primaryUsed.globals.insert(global->name); + // If custom-descirptors is enabled, global initializers can trap. Trapping + // globals should stay in the primary module to preserve the trapping behavior + // upon instantiation. + if (primary.features.hasCustomDescriptors()) { + for (auto& global : primary.globals) { + if (global->init && + EffectAnalyzer(config.passOptions, primary, global->init) + .hasUnremovableSideEffects()) { + primaryUsed.globals.insert(global->name); + } } } From 67c63733dc5d496fb63a4f470a2f9d3b4235701c Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Sat, 23 May 2026 19:51:00 -0700 Subject: [PATCH 146/168] NFC: Use `const EffectAnalyzer&` in ostream operator (#8769) Allows printing `const EffectAnalyzer`s. Also move the ostream operator into the `wasm` namespace to avoid UB from extending std: https://en.cppreference.com/cpp/language/extending_std --- src/ir/effects.cpp | 10 +++++----- src/ir/effects.h | 6 ++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/ir/effects.cpp b/src/ir/effects.cpp index 2f9dbddad7e..017057a1349 100644 --- a/src/ir/effects.cpp +++ b/src/ir/effects.cpp @@ -17,9 +17,9 @@ #include "ir/effects.h" #include "wasm.h" -namespace std { +namespace wasm { -std::ostream& operator<<(std::ostream& o, wasm::EffectAnalyzer& effects) { +std::ostream& operator<<(std::ostream& o, const EffectAnalyzer& effects) { o << "EffectAnalyzer {\n"; if (effects.branchesOut) { o << "branchesOut\n"; @@ -87,10 +87,10 @@ std::ostream& operator<<(std::ostream& o, wasm::EffectAnalyzer& effects) { if (effects.implicitTrap) { o << "implicitTrap\n"; } - if (effects.readOrder != wasm::MemoryOrder::Unordered) { + if (effects.readOrder != MemoryOrder::Unordered) { o << "readOrder " << effects.readOrder << "\n"; } - if (effects.writeOrder != wasm::MemoryOrder::Unordered) { + if (effects.writeOrder != MemoryOrder::Unordered) { o << "writeOrder " << effects.writeOrder << "\n"; } if (effects.throws_) { @@ -160,4 +160,4 @@ std::ostream& operator<<(std::ostream& o, wasm::EffectAnalyzer& effects) { return o; } -} // namespace std +} // namespace wasm diff --git a/src/ir/effects.h b/src/ir/effects.h index f76fbd5b85e..56cea0ad4fc 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -1501,10 +1501,8 @@ class ShallowEffectAnalyzer : public EffectAnalyzer { } }; -} // namespace wasm +std::ostream& operator<<(std::ostream& o, const EffectAnalyzer& effects); -namespace std { -std::ostream& operator<<(std::ostream& o, wasm::EffectAnalyzer& effects); -} // namespace std +} // namespace wasm #endif // wasm_ir_effects_h From fe6e02c58e3898e3a8140cf9bb8e13d2e130f368 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 26 May 2026 08:25:25 -0700 Subject: [PATCH 147/168] Support resuming into try-catch in interpreter (#8757) Enable resuming execution into `try` and `catch` blocks in the interpreter. For catch blocks, preserve the exception stack state so that `rethrow` and other exception operations work correctly after resumption. TAG=agy --- src/wasm-interpreter.h | 74 ++++++++++--- test/lit/exec/try-catch-resume.wast | 160 ++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 16 deletions(-) create mode 100644 test/lit/exec/try-catch-resume.wast diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 8d793045dfb..0c4e9f02b77 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -4874,29 +4874,40 @@ class ModuleRunnerBase : public ExpressionRunner { return {}; } Flow visitTry(Try* curr) { - assert(!self()->isResuming()); // TODO - try { - return self()->visit(curr->body); - } catch (const WasmException& e) { - // If delegation is in progress and the current try is not the target of - // the delegation, don't handle it and just rethrow. - if (scope->currDelegateTarget.is()) { - if (scope->currDelegateTarget == curr->name) { - scope->currDelegateTarget = Name{}; - } else { - throw; - } + auto suspend = [&](Index resumeIndex, + std::optional exn = std::nullopt) { + if (exn) { + self()->pushResumeEntry({Literal(int32_t(resumeIndex)), *exn}, "try"); + } else { + self()->pushResumeEntry({Literal(int32_t(resumeIndex))}, "try"); + } + }; + + Index resumeIndex = -1; + std::optional resumedExn; + if (self()->isResuming()) { + auto entry = self()->popResumeEntry("try"); + assert(entry.size() == 1 || entry.size() == 2); + resumeIndex = entry[0].geti32(); + if (entry.size() == 2) { + resumedExn = entry[1]; } + } - auto processCatchBody = [&](Expression* catchBody) { + auto processCatchBody = + [&](Index i, Expression* catchBody, const WasmException& currentExn) { // Push the current exception onto the exceptionStack in case // 'rethrow's use it - exceptionStack.push_back(std::make_pair(e, curr->name)); + exceptionStack.push_back({currentExn, curr->name}); // We need to pop exceptionStack in either case: when the catch body // exits normally or when a new exception is thrown Flow ret; try { ret = self()->visit(catchBody); + if (ret.suspendTag) { + suspend(1 + i, currentExn.exn); + return ret; + } } catch (const WasmException&) { exceptionStack.pop_back(); throw; @@ -4905,16 +4916,46 @@ class ModuleRunnerBase : public ExpressionRunner { return ret; }; + if (self()->isResuming() && resumeIndex >= 1) { + Index i = resumeIndex - 1; + assert(i < curr->catchBodies.size()); + assert(resumedExn); + return processCatchBody( + i, curr->catchBodies[i], WasmException{*resumedExn}); + } + + Flow flow; + try { + if (!self()->isResuming() || resumeIndex == 0) { + flow = self()->visit(curr->body); + if (flow.suspendTag) { + suspend(0); + return flow; + } + return flow; + } + } catch (const WasmException& e) { + // If delegation is in progress and the current try is not the target of + // the delegation, don't handle it and just rethrow. + if (scope->currDelegateTarget.is()) { + if (scope->currDelegateTarget == curr->name) { + scope->currDelegateTarget = Name{}; + } else { + throw; + } + } + auto exnData = e.exn.getExnData(); for (size_t i = 0; i < curr->catchTags.size(); i++) { auto* tag = allTags[curr->catchTags[i]]; if (tag == exnData->tag) { multiValues.push_back(exnData->payload); - return processCatchBody(curr->catchBodies[i]); + return processCatchBody(i, curr->catchBodies[i], e); } } if (curr->hasCatchAll()) { - return processCatchBody(curr->catchBodies.back()); + return processCatchBody( + curr->catchBodies.size() - 1, curr->catchBodies.back(), e); } if (curr->isDelegate()) { scope->currDelegateTarget = curr->delegateTarget; @@ -4922,6 +4963,7 @@ class ModuleRunnerBase : public ExpressionRunner { // This exception is not caught by this try-catch. Rethrow it. throw; } + return flow; } Flow visitTryTable(TryTable* curr) { try { diff --git a/test/lit/exec/try-catch-resume.wast b/test/lit/exec/try-catch-resume.wast new file mode 100644 index 00000000000..25ac7375ca8 --- /dev/null +++ b/test/lit/exec/try-catch-resume.wast @@ -0,0 +1,160 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --output=fuzz-exec and should not be edited. +;; RUN: wasm-opt %s -all --fuzz-exec-before -o /dev/null 2>&1 | filecheck %s + +(module + (type $type_void (func)) + (type $i32 (func (result i32))) + (type $cont (cont $i32)) + (type $throw_func (func)) + (type $f_catch_sig (func (param (ref $throw_func)) (result i32))) + (type $cont_catch (cont $f_catch_sig)) + + (elem declare func $try_f1 $f_catch $f_rethrow $throw_tag1 $throw_tag2 $throw_tag3) + + (tag $tag_suspend (type $type_void)) + (tag $tag1 (type $type_void)) + (tag $tag2 (type $type_void)) + (tag $tag3 (type $type_void)) + + ;; CHECK: [fuzz-exec] export run_try_resume + ;; CHECK-NEXT: [fuzz-exec] note result: run_try_resume => 42 + (func $run_try_resume (export "run_try_resume") (result i32) + (local $cont (ref $cont)) + (local.set $cont (cont.new $cont (ref.func $try_f1))) + ;; Resume into the try blocks in $try_f1 and $try_f2. + (resume $cont + (block $block (result (ref $cont)) + ;; Run until the suspend in $try_f3. + (drop (resume $cont (on $tag_suspend $block) (local.get $cont))) + (unreachable) + ) + ) + ) + + (func $try_f1 (result i32) + (try (result i32) + (do (call $try_f2)) + (catch_all (unreachable)) + ) + ) + + (func $try_f2 (result i32) + (try (result i32) + (do (call $try_f3)) + (catch_all (unreachable)) + ) + ) + + (func $try_f3 (result i32) + (suspend $tag_suspend) + (i32.const 42) + ) + + (func $throw_tag1 (type $throw_func) + (throw $tag1) + ) + (func $throw_tag2 (type $throw_func) + (throw $tag2) + ) + (func $throw_tag3 (type $throw_func) + (throw $tag3) + ) + + (func $run_catch_test (param $throw (ref $throw_func)) (result i32) + (local $cont (ref $cont)) + (local.set $cont + (cont.bind $cont_catch $cont + (local.get $throw) + (cont.new $cont_catch (ref.func $f_catch)) + ) + ) + ;; Resume into the catch block. + (resume $cont + (block $block (result (ref $cont)) + ;; Run until the suspend inside a catch. + (drop (resume $cont (on $tag_suspend $block) (local.get $cont))) + (unreachable) + ) + ) + ) + + ;; CHECK: [fuzz-exec] export run_catch_1 + ;; CHECK-NEXT: [fuzz-exec] note result: run_catch_1 => 101 + (func $run_catch_1 (export "run_catch_1") (result i32) + (call $run_catch_test (ref.func $throw_tag1)) + ) + + ;; CHECK: [fuzz-exec] export run_catch_2 + ;; CHECK-NEXT: [fuzz-exec] note result: run_catch_2 => 102 + (func $run_catch_2 (export "run_catch_2") (result i32) + (call $run_catch_test (ref.func $throw_tag2)) + ) + + ;; CHECK: [fuzz-exec] export run_catch_all + ;; CHECK-NEXT: [fuzz-exec] note result: run_catch_all => 103 + (func $run_catch_all (export "run_catch_all") (result i32) + (call $run_catch_test (ref.func $throw_tag3)) + ) + + (func $f_catch (param $throw (ref $throw_func)) (result i32) + (try (result i32) + (do + (call_ref $throw_func (local.get $throw)) + (unreachable) + ) + (catch $tag1 + (suspend $tag_suspend) + (i32.const 101) + ) + (catch $tag2 + (suspend $tag_suspend) + (i32.const 102) + ) + (catch_all + (suspend $tag_suspend) + (i32.const 103) + ) + ) + ) + + (func $run_rethrow_test (result i32) + (local $cont (ref $cont)) + (local.set $cont (cont.new $cont (ref.func $f_rethrow))) + ;; Resume into the catch block. + (resume $cont + (block $block (result (ref $cont)) + ;; Run until the suspend inside a catch. + (drop (resume $cont (on $tag_suspend $block) (local.get $cont))) + (unreachable) + ) + ) + ) + + + ;; CHECK: [fuzz-exec] export run_rethrow + ;; CHECK-NEXT: [fuzz-exec] note result: run_rethrow => 201 + ;; CHECK-NEXT: warning: no passes specified, not doing any work + (func $run_rethrow (export "run_rethrow") (result i32) + (call $run_rethrow_test) + ) + + (func $f_rethrow (result i32) + (try $outer (result i32) + (do + (try $inner (result i32) + (do + (throw $tag1) + ) + (catch $tag1 + (suspend $tag_suspend) + ;; Check that the exception stack is properly restored. + (rethrow $inner) + ) + ) + ) + (catch $tag1 + (i32.const 201) + ) + ) + ) +) From 9d2c168dd525b3e843c39547fbf86723082c48d4 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 26 May 2026 08:45:21 -0700 Subject: [PATCH 148/168] [NFC] Simplify externref literal representation (#8765) Instead of using a complex bit-packing scheme to represent externref payloads and differentiate them from externalized internal references, simply store the payload in the GCData. Externalized internal references are also stored in the GCData, but can be differentiated from externref payloads because the latter have type i32 and the former are always reference types. Besides being complex, the bit packing scheme was also incorrect on big-endian architectures. --- src/literal.h | 61 ++++++++++++++++++++++---------------------- src/wasm/literal.cpp | 34 ++++++++++-------------- 2 files changed, 45 insertions(+), 50 deletions(-) diff --git a/src/literal.h b/src/literal.h index 686348d1942..e982dae68aa 100644 --- a/src/literal.h +++ b/src/literal.h @@ -43,28 +43,18 @@ class Literal { // Note: i31 is stored in the |i32| field, with the lower 31 bits containing // the value if there is one, and the highest bit containing whether there // is a value. Thus, a null is |i32 === 0|. - // - // Externref payloads, which serve to differentiate different external - // references but are otherwise meaningless, are also stored in the i32 - // field, with their low bit set to differentiate an externref with a - // payload from an externalized internal reference, which uses the gcData - // field instead. This scheme supports 31 bits of payload for externrefs, - // which should be sufficient for spec test and fuzzing purposes, but if we - // need more bits we can use the i64 field instead. This scheme also depends - // on the low bit of a shared_ptr not being used. int32_t i32; int64_t i64; uint8_t v128[16]; // A reference to Function data. std::shared_ptr funcData; - // A reference to GC data, either a Struct or an Array. For both of those we - // store the referred data as a Literals object (which is natural for an - // Array, and for a Struct, is just the fields in order). The type is used - // to indicate whether this is a Struct or an Array, and of what type. We - // also use this to store String data, as it is similarly stored on the - // heap. For externalized or internalized references (including strings), - // gcData holds a single value, which is the wrapped internal or external - // reference. + // A reference to GC data, used for structs, arrays, strings, externrefs, + // and internalized externrefs. The GCData contains the struct or array + // fields, or the characters in the string. Externrefs are either + // externalized internal references, in which case the GCData will contain + // the internal reference, or a host reference, in which case the GCData + // will contain an i32 payload. Internalized references contain the wrapped + // externref in the GCData. std::shared_ptr gcData; // A reference to Exn data. std::shared_ptr exnData; @@ -266,11 +256,7 @@ class Literal { lit.i32 = value | 0x80000000; return lit; } - static Literal makeExtern(int32_t payload, Shareability share) { - auto lit = Literal(Type(HeapTypes::ext.getBasic(share), NonNullable)); - lit.i32 = (payload << 1) | 1; - return lit; - } + static Literal makeExtern(int32_t payload, Shareability share); // Wasm has nondeterministic rules for NaN propagation in some operations. For // example. f32.neg is deterministic and just flips the sign, even of a NaN, // but f32.add is nondeterministic, and if one or more of the inputs is a NaN, @@ -308,14 +294,8 @@ class Literal { // Cast to unsigned for the left shift to avoid undefined behavior. return signed_ ? int32_t((uint32_t(i32) << 1)) >> 1 : (i32 & 0x7fffffff); } - bool hasExternPayload() const { - assert(type.getHeapType().isMaybeShared(HeapType::ext)); - return (i32 & 1) == 1; - } - int32_t getExternPayload() const { - assert(hasExternPayload()); - return int32_t(uint32_t(i32) >> 1); - } + bool hasExternPayload() const; + int32_t getExternPayload() const; int64_t geti64() const { assert(type == Type::i64); return i64; @@ -813,6 +793,19 @@ struct GCData { : values(std::move(values)), desc(desc) {} }; +inline bool Literal::hasExternPayload() const { + if (isNull()) { + return false; + } + assert(type.getHeapType().isMaybeShared(HeapType::ext)); + return gcData->values[0].type == Type::i32; +} + +inline int32_t Literal::getExternPayload() const { + assert(hasExternPayload()); + return gcData->values[0].geti32(); +} + } // namespace wasm namespace std { @@ -857,6 +850,14 @@ template<> struct hash { wasm::rehash(digest, a.geti31(true)); return digest; } + if (type.isMaybeShared(wasm::HeapType::ext)) { + if (a.hasExternPayload()) { + wasm::rehash(digest, a.getExternPayload()); + return digest; + } + wasm::rehash(digest, (*this)(a.internalize())); + return digest; + } if (type.isMaybeShared(wasm::HeapType::any)) { // This may be an extern string that was internalized to |any|. Undo // that to get the actual value. (Rehash here with the existing digest, diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index 8c5074e1c74..d20921bccbf 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -66,7 +66,8 @@ Literal::Literal(Type type) : type(type) { if (type.isRef() && type.getHeapType().isMaybeShared(HeapType::ext)) { assert(type.isNonNullable()); - i32 = 1; + new (&gcData) std::shared_ptr( + std::make_shared(Literals{Literal(int32_t(0))})); return; } @@ -92,6 +93,11 @@ Literal Literal::makeFunc(Name func, Module& wasm) { return makeFunc(func, wasm.getFunction(func)->type); } +Literal Literal::makeExtern(int32_t payload, Shareability share) { + auto ext = HeapTypes::ext.getBasic(share); + return Literal(std::make_shared(Literals{Literal(payload)}), ext); +} + Literal::Literal(std::shared_ptr gcData, HeapType type) : gcData(gcData), type(type, gcData ? NonNullable : Nullable, @@ -175,17 +181,9 @@ Literal::Literal(const Literal& other) : type(other.type) { case HeapType::exn: new (&exnData) std::shared_ptr(other.exnData); return; - case HeapType::ext: { - if (other.hasExternPayload()) { - i32 = other.i32; - } else { - // Externalized internal reference. - new (&gcData) std::shared_ptr(other.gcData); - } - return; - } + case HeapType::ext: case HeapType::any: - // Internalized external reference or string. + // Externalized or internalized reference/payload. new (&gcData) std::shared_ptr(other.gcData); return; case HeapType::none: @@ -210,12 +208,8 @@ Literal::~Literal() { if (type.isBasic()) { return; } - if (type.getHeapType().isMaybeShared(HeapType::ext) && !hasExternPayload()) { - // Externalized internal reference. - gcData.~shared_ptr(); - return; - } - if (isNull() || isData() || type.getHeapType().isMaybeShared(HeapType::any)) { + if (isNull() || isData() || type.getHeapType().isMaybeShared(HeapType::any) || + type.getHeapType().isMaybeShared(HeapType::ext)) { gcData.~shared_ptr(); } else if (isFunction()) { funcData.~shared_ptr(); @@ -506,10 +500,10 @@ bool Literal::operator==(const Literal& other) const { return i32 == other.i32; } if (heapType.isMaybeShared(HeapType::ext)) { + if (hasExternPayload() != other.hasExternPayload()) { + return false; + } if (hasExternPayload()) { - if (!other.hasExternPayload()) { - return false; - } return getExternPayload() == other.getExternPayload(); } return internalize() == other.internalize(); From d78dba37bca6dac681124e565318e655a44aea49 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 26 May 2026 08:48:15 -0700 Subject: [PATCH 149/168] MergeBlocks: Migrate from invalidates to orderedBefore (#8767) Replace the coarse 'invalidates' check in 'MergeBlocks' with 'orderedBefore'. This allows 'MergeBlocks' to move expressions (like GC reads) out of blocks past other sibling expressions that have been left behind. Also clean up dead code in 'optimize()' by removing the unused 'dependency1' and 'dependency2' parameters and their checks (which were the other two replaced 'invalidates' calls that are no longer active). TAG=agy --- src/passes/MergeBlocks.cpp | 24 +------ test/lit/passes/merge-blocks-atomics.wast | 83 +++++++++++++++++++++++ 2 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 test/lit/passes/merge-blocks-atomics.wast diff --git a/src/passes/MergeBlocks.cpp b/src/passes/MergeBlocks.cpp index 3013f67ac9b..7010d6d15f7 100644 --- a/src/passes/MergeBlocks.cpp +++ b/src/passes/MergeBlocks.cpp @@ -498,29 +498,11 @@ struct MergeBlocks // ) // at which point the block is on the outside and potentially mergeable with // an outer block - Block* optimize(Expression* curr, - Expression*& child, - Block* outer = nullptr, - Expression** dependency1 = nullptr, - Expression** dependency2 = nullptr) { + Block* + optimize(Expression* curr, Expression*& child, Block* outer = nullptr) { if (!child) { return outer; } - if ((dependency1 && *dependency1) || (dependency2 && *dependency2)) { - // there are dependencies, things we must be reordered through. make sure - // no problems there - EffectAnalyzer childEffects(getPassOptions(), *getModule(), child); - if (dependency1 && *dependency1 && - EffectAnalyzer(getPassOptions(), *getModule(), *dependency1) - .invalidates(childEffects)) { - return outer; - } - if (dependency2 && *dependency2 && - EffectAnalyzer(getPassOptions(), *getModule(), *dependency2) - .invalidates(childEffects)) { - return outer; - } - } if (auto* block = child->dynCast()) { if (!block->name.is() && block->list.size() >= 2) { auto* back = block->list.back(); @@ -665,7 +647,7 @@ struct MergeBlocks EffectAnalyzer blockChildEffects( getPassOptions(), *getModule(), blockChild); for (auto& effects : childEffects) { - if (blockChildEffects.invalidates(effects)) { + if (effects.orderedBefore(blockChildEffects)) { fail = true; break; } diff --git a/test/lit/passes/merge-blocks-atomics.wast b/test/lit/passes/merge-blocks-atomics.wast new file mode 100644 index 00000000000..6435a9d7202 --- /dev/null +++ b/test/lit/passes/merge-blocks-atomics.wast @@ -0,0 +1,83 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --merge-blocks -all -S -o - | filecheck %s + +(module + ;; CHECK: (type $struct (shared (struct (field (mut i32))))) + (type $struct (shared (struct (field (mut i32))))) + + ;; CHECK: (memory $mem 1 1 shared) + (memory $mem 1 1 shared) + + ;; CHECK: (func $foo (type $2) (param $0 i32) (param $1 i32) + ;; CHECK-NEXT: ) + (func $foo (param i32 i32)) + + ;; CHECK: (func $disallowed (type $1) (param $x (ref $struct)) + ;; CHECK-NEXT: (call $foo + ;; CHECK-NEXT: (block $label1 (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $disallowed (param $x (ref $struct)) + ;; Test 1: Disallowed reordering (GC read NOT moved before acquire load). + (call $foo + ;; This block is left behind because it is named. + (block $block (result i32) + (drop (i32.atomic.load acqrel (i32.const 0))) + (i32.const 0) + ) + ;; This block tries to move back past $block and out of $foo, but cannot. + (block (result i32) + (drop (struct.get $struct 0 (local.get $x))) + (i32.const 0) + ) + ) + ) + + ;; CHECK: (func $allowed (type $1) (param $x (ref $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $foo + ;; CHECK-NEXT: (block $label2 (result i32) + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $allowed (param $x (ref $struct)) + ;; Test 2: Allowed reordering (GC read moved before Wasm release store) + (call $foo + ;; This block is left behind because it is named. + (block $block (result i32) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + (i32.const 0) + ) + ;; This block can move back past $block and out of $foo. + (block (result i32) + (drop (struct.get $struct 0 (local.get $x))) + (i32.const 0) + ) + ) + ) +) From aea97f8506268d5e341858d372ef20858aeefa9d Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 26 May 2026 08:54:26 -0700 Subject: [PATCH 150/168] Fix refinalization for --gufa-cast-all (#8773) In #8747 we refactored GUFA's `replaceCurrent` to set `optimized = true` and removed the now-redundant `optimized = true` at all the call sites. But we were too aggressive and removed an `optimized = true` in `Adder`, which had a separate `replaceCurrent` that did not set `optimized = true`. This caused a regression where we no longer refinalized after adding casts, but the regression was not caught by any tests. Fix the bug and add a test that depends on refinalization after adding casts. --- src/passes/GUFA.cpp | 1 + test/lit/passes/gufa-cast-all.wast | 41 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/passes/GUFA.cpp b/src/passes/GUFA.cpp index addba6b7ee3..533c033524e 100644 --- a/src/passes/GUFA.cpp +++ b/src/passes/GUFA.cpp @@ -388,6 +388,7 @@ struct GUFAOptimizer if (oracleType.isRef() && oracleType != curr->type && Type::isSubType(oracleType, curr->type)) { replaceCurrent(Builder(*getModule()).makeRefCast(curr, oracleType)); + optimized = true; } } }; diff --git a/test/lit/passes/gufa-cast-all.wast b/test/lit/passes/gufa-cast-all.wast index bffd88e7714..a1675982f08 100644 --- a/test/lit/passes/gufa-cast-all.wast +++ b/test/lit/passes/gufa-cast-all.wast @@ -454,3 +454,44 @@ ) ) ) + +(module + ;; CHECK: (type $struct (struct)) + (type $struct (struct)) + + ;; CHECK: (type $1 (func (result (ref $struct)))) + + ;; CHECK: (global $g (mut (ref null $struct)) (struct.new_default $struct)) + (global $g (mut (ref null $struct)) (struct.new $struct)) + + ;; CHECK: (func $test (type $1) (result (ref $struct)) + ;; CHECK-NEXT: (ref.cast (ref (exact $struct)) + ;; CHECK-NEXT: (block $block (result (ref (exact $struct))) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.cast (ref (exact $struct)) + ;; CHECK-NEXT: (br_on_cast $block (ref (exact $struct)) (ref (exact $struct)) + ;; CHECK-NEXT: (ref.cast (ref (exact $struct)) + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (result (ref $struct)) + (block $block (result (ref $struct)) + (drop + (br_on_cast $block (ref null $struct) (ref $struct) + ;; This will be cast to (ref (exact $struct)). We must refinalize to + ;; update the br_on_cast output to (ref (exact $struct)) as well to + ;; maintain the validation condition that the cast output is a subtype + ;; of its input. + (global.get $g) + ) + ) + (unreachable) + ) + ) +) From 6a7249360da5499f0e135a288ac92c334079ad26 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Tue, 26 May 2026 09:04:12 -0700 Subject: [PATCH 151/168] Use cmake standard `BUILD_SHARED_LIBS` setting (#8764) See https://cmake.org/cmake/help/latest/variable/BUILD_SHARED_LIBS.html I'm not sure why we made up our own setting here rather than using the standard one. --- .github/workflows/ci.yml | 2 +- .github/workflows/create_release.yml | 2 +- CMakeLists.txt | 26 +++++++++++++------------- src/binaryen-c.h | 4 +++- third_party/CMakeLists.txt | 14 +++++++------- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed840da7f48..c3513897b1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -265,7 +265,7 @@ jobs: # size for pthreads is tiny, # https://github.com/WebAssembly/binaryen/issues/8594 run: | - ./alpine.sh cmake . -G Ninja -DCMAKE_CXX_FLAGS="-static" -DCMAKE_C_FLAGS="-static" -DCMAKE_EXE_LINKER_FLAGS="-Wl,-z,stack-size=8388608" -DCMAKE_BUILD_TYPE=Release -DBUILD_STATIC_LIB=ON -DBUILD_MIMALLOC=ON -DCMAKE_INSTALL_PREFIX=install + ./alpine.sh cmake . -G Ninja -DCMAKE_CXX_FLAGS="-static" -DCMAKE_C_FLAGS="-static" -DCMAKE_EXE_LINKER_FLAGS="-Wl,-z,stack-size=8388608" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DBUILD_MIMALLOC=ON -DCMAKE_INSTALL_PREFIX=install - name: build run: | diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 3dc36f80cbd..723257fc47d 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -150,7 +150,7 @@ jobs: # size for pthreads is tiny, # https://github.com/WebAssembly/binaryen/issues/8594 run: | - ./alpine.sh cmake . -G Ninja -DCMAKE_CXX_FLAGS="-static" -DCMAKE_C_FLAGS="-static" -DCMAKE_EXE_LINKER_FLAGS="-Wl,-z,stack-size=8388608" -DCMAKE_BUILD_TYPE=Release -DBUILD_STATIC_LIB=ON -DBUILD_MIMALLOC=ON -DCMAKE_INSTALL_PREFIX=install + ./alpine.sh cmake . -G Ninja -DCMAKE_CXX_FLAGS="-static" -DCMAKE_C_FLAGS="-static" -DCMAKE_EXE_LINKER_FLAGS="-Wl,-z,stack-size=8388608" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DBUILD_MIMALLOC=ON -DCMAKE_INSTALL_PREFIX=install - name: build run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 109d5c8f784..f45a805098b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,11 +52,11 @@ if(EMSCRIPTEN) set(BUILD_LLVM_DWARF OFF) endif() -option(BUILD_STATIC_LIB "Build as a static library" OFF) +option(BUILD_SHARED_LIBS "Build using shared libraries" ON) if(MSVC OR EMSCRIPTEN) # We don't have dllexport declarations set up for Windows yet. # With emscripten we require a static library to create binaryen_js correctly. - set(BUILD_STATIC_LIB ON) + set(BUILD_SHARED_LIBS OFF) endif() # Advised to turn on when statically linking against musl libc (e.g., in the @@ -451,18 +451,18 @@ else() # MSVC endif() # Declare libbinaryen +# This will be either be STATIC or SHARED depending on BUILD_SHARED_LIBS +add_library(binaryen) -if(BUILD_STATIC_LIB) - message(STATUS "Building libbinaryen as statically linked library.") - add_library(binaryen STATIC) - add_definitions(-DBUILD_STATIC_LIBRARY) -else() +if(BUILD_SHARED_LIBS) + add_definitions(-DBUILD_SHARED_LIBS) message(STATUS "Building libbinaryen as shared library.") - add_library(binaryen SHARED) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") # Disable interposition and resolve Binaryen symbols locally. add_link_flag("-Bsymbolic") endif() +else() + message(STATUS "Building libbinaryen as statically linked library.") endif() target_link_libraries(binaryen PUBLIC Threads::Threads) binaryen_setup_rpath(binaryen) @@ -475,13 +475,13 @@ if(BUILD_MIMALLOC) message(FATAL_ERROR "Linking mimalloc is only supported on Linux.") endif() message(STATUS "Building with mimalloc allocator.") - if(BUILD_STATIC_LIB) + if(BUILD_SHARED_LIBS) + target_link_options(mimalloc PRIVATE "-Wl,--as-needed") + target_link_libraries(binaryen PRIVATE mimalloc) + else() target_link_libraries(binaryen PRIVATE "-Wl,--push-state,--as-needed") target_link_libraries(binaryen PRIVATE mimalloc-static) target_link_libraries(binaryen PRIVATE "-Wl,--pop-state") - else() - target_link_options(mimalloc PRIVATE "-Wl,--as-needed") - target_link_libraries(binaryen PRIVATE mimalloc) endif() endif() @@ -518,7 +518,7 @@ set(binaryen_SOURCES ) target_sources(binaryen PRIVATE ${binaryen_SOURCES}) -if(INSTALL_LIBS OR NOT BUILD_STATIC_LIB) +if(INSTALL_LIBS OR BUILD_SHARED_LIBS) install(TARGETS binaryen RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} diff --git a/src/binaryen-c.h b/src/binaryen-c.h index fbde9d2a08d..f798496ad1c 100644 --- a/src/binaryen-c.h +++ b/src/binaryen-c.h @@ -58,7 +58,9 @@ #if defined(__EMSCRIPTEN__) #include #define BINARYEN_API EMSCRIPTEN_KEEPALIVE -#elif defined(_MSC_VER) && !defined(BUILD_STATIC_LIBRARY) +#elif defined(_MSC_VER) && defined(BUILD_SHARED_LIBS) +// TODO: This is not yet used since we disabled BUILD_SHARED_LIBS under +// _MSC_VER in CMakeLists.txt #define BINARYEN_API __declspec(dllexport) #else #define BINARYEN_API diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 2888d8ec7b8..5c77330e4fa 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -18,9 +18,9 @@ endif() if(BUILD_MIMALLOC) # Match static/dynamic linking between libbinaryen and mimalloc - set(MI_BUILD_STATIC ${BUILD_STATIC_LIB}) - if (BUILD_STATIC_LIB) - set(MI_BUILD_SHARED OFF) + set(MI_BUILD_SHARED ${BUILD_SHARED_LIBS}) + if (BUILD_SHARED_LIBS) + set(MI_BUILD_STATIC OFF) endif() set(MI_BUILD_OBJECT OFF) set(MI_BUILD_TESTS OFF) @@ -35,11 +35,11 @@ if(BUILD_MIMALLOC) # Do not show debug and warning messages of the allocator by default. # (They can still be enabled via MIMALLOC_VERBOSE=1 wasm-opt ...) add_compile_definitions(MI_DEBUG=0) - - if(BUILD_STATIC_LIB) + + if(BUILD_SHARED_LIBS) + add_subdirectory(mimalloc) + else() # No need to install libmimalloc.a when it's linked statically into the tools. add_subdirectory(mimalloc EXCLUDE_FROM_ALL) - else() - add_subdirectory(mimalloc) endif() endif() From 95e7dfa3cffa0cb6792a116ac28c58bb788d086e Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 26 May 2026 11:48:17 -0700 Subject: [PATCH 152/168] [NFC] Refactor closedWorld to WorldMode enum (#8740) Replace the `closedWorld` boolean in `PassOptions` with a `WorldMode` enum which has `Closed` and `Open` variants. Also introduce `ModuleUtils::getExposedPublicHeapTypes` to collect directly exposed public types, and updates `getPublicHeapTypes` to take `WorldMode` as a parameter. These refactorings will make a subsequent commit changing how we collect public heap types in open-world mode simpler. --- src/binaryen-c.cpp | 8 +- src/ir/module-utils.cpp | 133 +++++++++++++++------- src/ir/module-utils.h | 18 ++- src/ir/possible-contents.cpp | 12 +- src/ir/type-updating.cpp | 3 +- src/ir/type-updating.h | 18 +-- src/pass.h | 48 ++++---- src/passes/AbstractTypeRefining.cpp | 14 ++- src/passes/ConstantFieldPropagation.cpp | 2 +- src/passes/DeadArgumentElimination2.cpp | 8 +- src/passes/GlobalEffects.cpp | 11 +- src/passes/GlobalRefining.cpp | 2 +- src/passes/GlobalStructInference.cpp | 2 +- src/passes/GlobalTypeOptimization.cpp | 9 +- src/passes/J2CLItableMerging.cpp | 5 +- src/passes/MinimizeRecGroups.cpp | 3 +- src/passes/RemoveUnusedModuleElements.cpp | 6 +- src/passes/RemoveUnusedTypes.cpp | 4 +- src/passes/ReorderTypes.cpp | 9 +- src/passes/SignaturePruning.cpp | 8 +- src/passes/SignatureRefining.cpp | 6 +- src/passes/StringLowering.cpp | 2 +- src/passes/TypeFinalizing.cpp | 6 +- src/passes/TypeMerging.cpp | 10 +- src/passes/TypeRefining.cpp | 8 +- src/passes/Unsubtyping.cpp | 9 +- src/passes/pass.cpp | 8 +- src/tools/fuzzing.h | 6 +- src/tools/fuzzing/fuzzing.cpp | 25 ++-- src/tools/tool-options.h | 2 +- src/tools/wasm-opt.cpp | 2 +- src/wasm/wasm-validator.cpp | 2 +- 32 files changed, 252 insertions(+), 157 deletions(-) diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index 0f8cb4805f9..7c8dcab97be 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -5888,9 +5888,13 @@ void BinaryenSetTrapsNeverHappen(bool on) { globalPassOptions.trapsNeverHappen = on; } -bool BinaryenGetClosedWorld(void) { return globalPassOptions.closedWorld; } +bool BinaryenGetClosedWorld(void) { + return globalPassOptions.worldMode == WorldMode::Closed; +} -void BinaryenSetClosedWorld(bool on) { globalPassOptions.closedWorld = on; } +void BinaryenSetClosedWorld(bool on) { + globalPassOptions.worldMode = on ? WorldMode::Closed : WorldMode::Open; +} bool BinaryenGetLowMemoryUnused(void) { return globalPassOptions.lowMemoryUnused; diff --git a/src/ir/module-utils.cpp b/src/ir/module-utils.cpp index 6ee94d35d65..4d4a565a7ce 100644 --- a/src/ir/module-utils.cpp +++ b/src/ir/module-utils.cpp @@ -481,12 +481,16 @@ struct CodeScanner : PostWalker { }; void classifyTypeVisibility(Module& wasm, - InsertOrderedMap& types); + InsertOrderedMap& types, + WorldMode worldMode); } // anonymous namespace -InsertOrderedMap collectHeapTypeInfo( - Module& wasm, TypeInclusion inclusion, VisibilityHandling visibility) { +InsertOrderedMap +collectHeapTypeInfo(Module& wasm, + WorldMode worldMode, + TypeInclusion inclusion, + VisibilityHandling visibility) { // Collect module-level info. TypeInfos info; CodeScanner(wasm, info).walkModuleCode(&wasm); @@ -593,7 +597,7 @@ InsertOrderedMap collectHeapTypeInfo( } if (visibility == VisibilityHandling::FindVisibility) { - classifyTypeVisibility(wasm, info.info); + classifyTypeVisibility(wasm, info.info, worldMode); } return std::move(info.info); @@ -602,8 +606,9 @@ InsertOrderedMap collectHeapTypeInfo( namespace { void classifyTypeVisibility(Module& wasm, - InsertOrderedMap& types) { - for (auto type : getPublicHeapTypes(wasm)) { + InsertOrderedMap& types, + WorldMode worldMode) { + for (auto type : getPublicHeapTypes(wasm, worldMode)) { if (auto it = types.find(type); it != types.end()) { it->second.visibility = Visibility::Public; } @@ -615,6 +620,64 @@ void classifyTypeVisibility(Module& wasm, } } +// Collects all heap types transitively reachable from a root set of types. +// Options are provided to customize the traversal: +// - `includeSupertypes`: if true, declared supertypes are also traversed. +// - `includeRecGroups`: if true, all types in the same recursion group +// are also traversed. +std::vector +getTransitivelyReachable(const std::vector& roots, + bool includeSupertypes, + bool includeRecGroups) { + std::vector result; + std::vector worklist; + std::unordered_set seen; + std::unordered_set seenRecGroups; + + auto note = [&](HeapType type) { + if (type.isBasic()) { + if (seen.insert(type).second) { + result.push_back(type); + } + return; + } + + if (includeRecGroups) { + auto group = type.getRecGroup(); + if (seenRecGroups.insert(group).second) { + for (auto member : group) { + result.push_back(member); + worklist.push_back(member); + } + } + } else { + if (seen.insert(type).second) { + result.push_back(type); + worklist.push_back(type); + } + } + }; + + for (auto type : roots) { + note(type); + } + + while (!worklist.empty()) { + auto curr = worklist.back(); + worklist.pop_back(); + std::optional super = + includeSupertypes ? std::nullopt : curr.getDeclaredSuperType(); + for (auto t : curr.getReferencedHeapTypes()) { + if (super && t == *super) { + continue; + } + note(t); + } + } + + return result; +} + void setIndices(IndexedHeapTypes& indexedTypes) { for (Index i = 0; i < indexedTypes.types.size(); i++) { indexedTypes.indices[indexedTypes.types[i]] = i; @@ -624,7 +687,7 @@ void setIndices(IndexedHeapTypes& indexedTypes) { } // anonymous namespace std::vector collectHeapTypes(Module& wasm) { - auto info = collectHeapTypeInfo(wasm); + auto info = collectHeapTypeInfo(wasm, WorldMode::Open); std::vector types; types.reserve(info.size()); for (auto& [type, _] : info) { @@ -633,27 +696,16 @@ std::vector collectHeapTypes(Module& wasm) { return types; } -std::vector getPublicHeapTypes(Module& wasm) { - // Look at the types of imports as exports to get an initial set of public - // types, then traverse the types used by public types and collect the - // transitively reachable public types as well. - std::vector workList; - std::unordered_set publicGroups; - - // The collected types. +std::vector getExposedPublicHeapTypes(Module& wasm) { + // Look at the types of imports and exports to get an initial set of public + // types. std::vector publicTypes; + std::unordered_set seenTypes; auto notePublic = [&](HeapType type) { - if (type.isBasic()) { - return; + if (seenTypes.insert(type).second) { + publicTypes.push_back(type); } - auto group = type.getRecGroup(); - if (!publicGroups.insert(group).second) { - // The groups in this type have already been marked public. - return; - } - publicTypes.insert(publicTypes.end(), group.begin(), group.end()); - workList.insert(workList.end(), group.begin(), group.end()); }; ModuleUtils::iterImportedTags(wasm, [&](Tag* tag) { notePublic(tag->type); }); @@ -710,24 +762,28 @@ std::vector getPublicHeapTypes(Module& wasm) { notePublic(type); } - // Find all the other public types reachable from directly publicized types. - while (!workList.empty()) { - auto curr = workList.back(); - workList.pop_back(); - for (auto t : curr.getReferencedHeapTypes()) { - notePublic(t); + return publicTypes; +} + +std::vector getPublicHeapTypes(Module& wasm, WorldMode worldMode) { + auto directlyExposed = getExposedPublicHeapTypes(wasm); + auto transitivelyExposed = getTransitivelyReachable( + directlyExposed, /*includeSupertypes=*/true, /*includeRecGroups=*/true); + std::vector publicTypes; + publicTypes.reserve(transitivelyExposed.size()); + for (auto type : transitivelyExposed) { + if (!type.isBasic()) { + publicTypes.push_back(type); } } - - // TODO: In an open world, we need to consider subtypes of public types public - // as well, or potentially even consider all types to be public unless - // otherwise annotated. return publicTypes; } -std::vector getPrivateHeapTypes(Module& wasm) { - auto info = collectHeapTypeInfo( - wasm, TypeInclusion::UsedIRTypes, VisibilityHandling::FindVisibility); +std::vector getPrivateHeapTypes(Module& wasm, WorldMode worldMode) { + auto info = collectHeapTypeInfo(wasm, + worldMode, + TypeInclusion::UsedIRTypes, + VisibilityHandling::FindVisibility); std::vector types; types.reserve(info.size()); for (auto& [type, typeInfo] : info) { @@ -739,7 +795,8 @@ std::vector getPrivateHeapTypes(Module& wasm) { } IndexedHeapTypes getOptimizedIndexedHeapTypes(Module& wasm) { - auto counts = collectHeapTypeInfo(wasm, TypeInclusion::BinaryTypes); + auto counts = + collectHeapTypeInfo(wasm, WorldMode::Open, TypeInclusion::BinaryTypes); // Collect the rec groups. std::unordered_map groupIndices; diff --git a/src/ir/module-utils.h b/src/ir/module-utils.h index 50b67df7cea..09b7900d3c9 100644 --- a/src/ir/module-utils.h +++ b/src/ir/module-utils.h @@ -472,6 +472,7 @@ struct HeapTypeInfo { InsertOrderedMap collectHeapTypeInfo( Module& wasm, + WorldMode worldMode, TypeInclusion inclusion = TypeInclusion::AllTypes, VisibilityHandling visibility = VisibilityHandling::NoVisibility); @@ -479,13 +480,18 @@ InsertOrderedMap collectHeapTypeInfo( // module, i.e. the types that would appear in the type section. std::vector collectHeapTypes(Module& wasm); -// Collect all the heap types visible on the module boundary that cannot be -// changed. TODO: For open world use cases, this needs to include all subtypes -// of public types as well. -std::vector getPublicHeapTypes(Module& wasm); +// Get the types directly made public by imported or exported module items. For +// example, the types of imported or exported globals or functions, but not +// other types reachable from those types. Includes abstract heap types. +std::vector getExposedPublicHeapTypes(Module& wasm); -// getHeapTypes - getPublicHeapTypes -std::vector getPrivateHeapTypes(Module& wasm); +// Collect all the defined heap types visible on the module boundary that cannot +// be changed, e.g. the defined types from getExposedPublicHeapTypes and those +// they reach. +std::vector getPublicHeapTypes(Module& wasm, WorldMode worldMode); + +// All the defined heap types that are not public. +std::vector getPrivateHeapTypes(Module& wasm, WorldMode worldMode); struct IndexedHeapTypes { std::vector types; diff --git a/src/ir/possible-contents.cpp b/src/ir/possible-contents.cpp index 4b76f467825..a8a01841fd6 100644 --- a/src/ir/possible-contents.cpp +++ b/src/ir/possible-contents.cpp @@ -684,7 +684,7 @@ struct InfoCollector SignatureResultLocation{func->type.getHeapType(), i}}); } - if (!options.closedWorld) { + if (options.worldMode == WorldMode::Open) { info.calledFromOutside.insert(curr->func); } } @@ -1711,7 +1711,7 @@ void TNHOracle::scan(Function* func, void visitCallRef(CallRef* curr) { // We can only optimize call_ref in closed world, as otherwise the // call can go somewhere we can't see. - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { info.callRefs.push_back(curr); } } @@ -1834,7 +1834,7 @@ void TNHOracle::infer() { // that type or a subtype, i.e., might be called when that type is seen in a // call_ref target. std::unordered_map> typeFunctions; - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { for (auto& func : wasm.functions) { auto type = func->type; auto& info = map[wasm.getFunction(func->name)]; @@ -1895,7 +1895,7 @@ void TNHOracle::infer() { // We should only get here in a closed world, in which we know which // functions might be called (the scan phase only notes callRefs if we are // in fact in a closed world). - assert(options.closedWorld); + assert(options.worldMode == WorldMode::Closed); auto iter = typeFunctions.find(targetType.getHeapType()); if (iter == typeFunctions.end()) { @@ -2535,8 +2535,8 @@ Flower::Flower(Module& wasm, const PassOptions& options) } // In open world, public heap types may be written to from the outside. - if (!options.closedWorld) { - for (auto type : ModuleUtils::getPublicHeapTypes(wasm)) { + if (options.worldMode == WorldMode::Open) { + for (auto type : ModuleUtils::getPublicHeapTypes(wasm, options.worldMode)) { if (type.isStruct()) { auto& fields = type.getStruct().fields; for (Index i = 0; i < fields.size(); i++) { diff --git a/src/ir/type-updating.cpp b/src/ir/type-updating.cpp index 7fcd6a4935c..be9f125e451 100644 --- a/src/ir/type-updating.cpp +++ b/src/ir/type-updating.cpp @@ -26,7 +26,7 @@ namespace wasm { -GlobalTypeRewriter::GlobalTypeRewriter(Module& wasm) +GlobalTypeRewriter::GlobalTypeRewriter(Module& wasm, WorldMode worldMode) : wasm(wasm), publicGroups(wasm.features) { // Find the heap types that are not publicly observable. Even in a closed // world scenario, don't modify public types because we assume that they may @@ -34,6 +34,7 @@ GlobalTypeRewriter::GlobalTypeRewriter(Module& wasm) // will be located in the builder. typeInfo = ModuleUtils::collectHeapTypeInfo( wasm, + worldMode, ModuleUtils::TypeInclusion::UsedIRTypes, ModuleUtils::VisibilityHandling::FindVisibility); diff --git a/src/ir/type-updating.h b/src/ir/type-updating.h index 0e050becc59..7aaf02a8234 100644 --- a/src/ir/type-updating.h +++ b/src/ir/type-updating.h @@ -358,7 +358,7 @@ class GlobalTypeRewriter { // private types do not conflict with public types. UniqueRecGroups publicGroups; - GlobalTypeRewriter(Module& wasm); + GlobalTypeRewriter(Module& wasm, WorldMode worldMode); virtual ~GlobalTypeRewriter() {} // Main entry point. This performs the entire process of creating new heap @@ -427,7 +427,9 @@ class GlobalTypeRewriter { // Helper for the repeating pattern of just updating Signature types using a // map of old heap type => new Signature. - static void updateSignatures(const SignatureUpdates& updates, Module& wasm) { + static void updateSignatures(const SignatureUpdates& updates, + Module& wasm, + WorldMode worldMode) { if (updates.empty()) { return; } @@ -436,8 +438,10 @@ class GlobalTypeRewriter { const SignatureUpdates& updates; public: - SignatureRewriter(Module& wasm, const SignatureUpdates& updates) - : GlobalTypeRewriter(wasm), updates(updates) { + SignatureRewriter(Module& wasm, + const SignatureUpdates& updates, + WorldMode worldMode) + : GlobalTypeRewriter(wasm, worldMode), updates(updates) { update(); } @@ -448,7 +452,7 @@ class GlobalTypeRewriter { sig.results = getTempType(iter->second.results); } } - } rewriter(wasm, updates); + } rewriter(wasm, updates, worldMode); } protected: @@ -473,8 +477,8 @@ class TypeMapper : public GlobalTypeRewriter { const TypeUpdates& mapping; - TypeMapper(Module& wasm, const TypeUpdates& mapping) - : GlobalTypeRewriter(wasm), mapping(mapping) {} + TypeMapper(Module& wasm, const TypeUpdates& mapping, WorldMode worldMode) + : GlobalTypeRewriter(wasm, worldMode), mapping(mapping) {} void map() { // Update the internals of types (struct fields, signatures, etc.) to diff --git a/src/pass.h b/src/pass.h index 02af3f57eb5..8bbf9612a90 100644 --- a/src/pass.h +++ b/src/pass.h @@ -110,6 +110,30 @@ struct InliningOptions { Index partialInliningIfs = 0; }; +// Assume code outside of the module does not inspect or interact with GC and +// function references, with the goal of being able to aggressively optimize all +// user-defined types. The outside may hold on to references and pass them back +// in, but may not inspect their contents, call them, construct them, or reflect +// on their types in any way. +// +// By default we do not make this assumption, and assume anything that escapes +// to the outside may be inspected in detail, which prevents us from e.g. +// changing the type of any value that may escape except by refining it (so we +// can't remove or refine fields on an escaping struct type, for example, +// unless the new type declares the original type as a supertype). +// +// Note that the module can still have imports and exports - otherwise it +// could do nothing at all! - so the meaning of "closed world" is a little +// subtle here. We do still want to keep imports and exports unchanged, as +// they form a contract with the outside world. For example, if an import has +// two parameters, we can't remove one of them. A nuance regarding that is how +// type equality works between wasm modules using the isorecursive type +// system: not only do we need to not remove a parameter as just mentioned, +// but we also want to keep types of things on the boundary unchanged. For +// example, we should not change an exported function's signature, as the +// outside may need that type to properly call the export. +enum class WorldMode { Open, Closed }; + struct PassOptions { friend Pass; @@ -196,29 +220,7 @@ struct PassOptions { // creates it and we know it is all zeros right before the active segments are // applied.) bool zeroFilledMemory = false; - // Assume code outside of the module does not inspect or interact with GC and - // function references, with the goal of being able to aggressively optimize - // all user-defined types. The outside may hold on to references and pass them - // back in, but may not inspect their contents, call them, construct them, or - // reflect on their types in any way. - // - // By default we do not make this assumption, and assume anything that escapes - // to the outside may be inspected in detail, which prevents us from e.g. - // changing the type of any value that may escape except by refining it (so we - // can't remove or refine fields on an escaping struct type, for example, - // unless the new type declares the original type as a supertype). - // - // Note that the module can still have imports and exports - otherwise it - // could do nothing at all! - so the meaning of "closed world" is a little - // subtle here. We do still want to keep imports and exports unchanged, as - // they form a contract with the outside world. For example, if an import has - // two parameters, we can't remove one of them. A nuance regarding that is how - // type equality works between wasm modules using the isorecursive type - // system: not only do we need to not remove a parameter as just mentioned, - // but we also want to keep types of things on the boundary unchanged. For - // example, we should not change an exported function's signature, as the - // outside may need that type to properly call the export. - bool closedWorld = false; + WorldMode worldMode = WorldMode::Open; // Whether to try to preserve debug info through, which are special calls. bool debugInfo = false; // Whether to generate StackIR during binary writing. This is on by default diff --git a/src/passes/AbstractTypeRefining.cpp b/src/passes/AbstractTypeRefining.cpp index b75153e3d2d..a22488dd1c4 100644 --- a/src/passes/AbstractTypeRefining.cpp +++ b/src/passes/AbstractTypeRefining.cpp @@ -87,7 +87,7 @@ struct AbstractTypeRefining : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "AbstractTypeRefining requires --closed-world"; } @@ -116,7 +116,8 @@ struct AbstractTypeRefining : public Pass { // module, given closed world, but we'd also need to make sure that // we don't need to make any changes to public types that refer to // them. - for (auto type : ModuleUtils::getPublicHeapTypes(*module)) { + for (auto type : + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode)) { createdTypes.insert(type); } @@ -289,8 +290,10 @@ struct AbstractTypeRefining : public Pass { // that for Unsubtyping. class AbstractTypeRefiningTypeMapper : public TypeMapper { public: - AbstractTypeRefiningTypeMapper(Module& wasm, const TypeUpdates& mapping) - : TypeMapper(wasm, mapping) {} + AbstractTypeRefiningTypeMapper(Module& wasm, + const TypeUpdates& mapping, + WorldMode worldMode) + : TypeMapper(wasm, mapping, worldMode) {} std::optional getDeclaredSuperType(HeapType oldType) override { // We do not want to update subtype relationships. @@ -298,7 +301,8 @@ struct AbstractTypeRefining : public Pass { } }; - AbstractTypeRefiningTypeMapper(*module, mapping).map(); + AbstractTypeRefiningTypeMapper(*module, mapping, getPassOptions().worldMode) + .map(); // Refinalize to propagate the type changes we made. For example, a refined // cast may lead to a struct.get reading a more refined type using that diff --git a/src/passes/ConstantFieldPropagation.cpp b/src/passes/ConstantFieldPropagation.cpp index 0063a8d3a69..66f45bb6047 100644 --- a/src/passes/ConstantFieldPropagation.cpp +++ b/src/passes/ConstantFieldPropagation.cpp @@ -542,7 +542,7 @@ struct ConstantFieldPropagation : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "CFP requires --closed-world"; } diff --git a/src/passes/DeadArgumentElimination2.cpp b/src/passes/DeadArgumentElimination2.cpp index 11dc2cec1fb..1ebc576fa9a 100644 --- a/src/passes/DeadArgumentElimination2.cpp +++ b/src/passes/DeadArgumentElimination2.cpp @@ -235,7 +235,7 @@ struct DAE2 : public Pass { } optimizeReferencedFuncs = - getPassOptions().closedWorld && wasm->features.hasGC(); + getPassOptions().worldMode == WorldMode::Closed && wasm->features.hasGC(); TIME(Timer timer); @@ -579,7 +579,8 @@ void DAE2::analyzeModule() { // // TODO: Analyze tags and remove their unused parameters. std::unordered_set unrewritableRoots; - publicHeapTypes = ModuleUtils::getPublicHeapTypes(*wasm); + publicHeapTypes = + ModuleUtils::getPublicHeapTypes(*wasm, getPassOptions().worldMode); for (auto type : publicHeapTypes) { if (type.isSignature()) { unrewritableRoots.insert(getRootType(type)); @@ -728,7 +729,8 @@ void DAE2::computeFixedPoint() { struct DAETypeUpdater : GlobalTypeRewriter { DAE2& parent; DAETypeUpdater(DAE2& parent) - : GlobalTypeRewriter(*parent.wasm), parent(parent) {} + : GlobalTypeRewriter(*parent.wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifySignature(HeapType oldType, Signature& sig) override { // All signature types in a type tree will have the same parameters removed diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp index 88fb4c00907..720752372aa 100644 --- a/src/passes/GlobalEffects.cpp +++ b/src/passes/GlobalEffects.cpp @@ -86,7 +86,8 @@ std::map analyzeFuncs(Module& module, if (auto* call = curr->dynCast()) { // Note the direct call. funcInfo.calledFunctions.insert(call->target); - } else if (effects.calls && options.closedWorld) { + } else if (effects.calls && + options.worldMode == WorldMode::Closed) { HeapType type; if (auto* callRef = curr->dynCast()) { // call_ref on unreachable does not have a call effect, @@ -101,7 +102,7 @@ std::map analyzeFuncs(Module& module, funcInfo.indirectCalledTypes.insert(type); } else if (effects.calls) { - assert(!options.closedWorld); + assert(options.worldMode == WorldMode::Open); funcInfo.effects = std::nullopt; } else { // No call here, but update throwing if we see it. (Only do so, @@ -143,9 +144,9 @@ using CallGraph = CallGraph buildCallGraph(const Module& module, const std::map& funcInfos, - bool closedWorld) { + WorldMode worldMode) { CallGraph callGraph; - if (!closedWorld) { + if (worldMode == WorldMode::Open) { for (const auto& [caller, callerInfo] : funcInfos) { auto& callees = callGraph[caller]; @@ -350,7 +351,7 @@ struct GenerateGlobalEffects : public Pass { analyzeFuncs(*module, getPassOptions()); auto callGraph = - buildCallGraph(*module, funcInfos, getPassOptions().closedWorld); + buildCallGraph(*module, funcInfos, getPassOptions().worldMode); propagateEffects(*module, getPassOptions(), diff --git a/src/passes/GlobalRefining.cpp b/src/passes/GlobalRefining.cpp index 87dc5b259cf..95389f8ccfc 100644 --- a/src/passes/GlobalRefining.cpp +++ b/src/passes/GlobalRefining.cpp @@ -80,7 +80,7 @@ struct GlobalRefining : public Pass { std::unordered_set exportedGlobals(exportedGlobalsVec.begin(), exportedGlobalsVec.end()); for (auto* global : exportedGlobalsVec) { - if (getPassOptions().closedWorld || global->mutable_) { + if (getPassOptions().worldMode == WorldMode::Closed || global->mutable_) { unoptimizable.insert(global->name); } } diff --git a/src/passes/GlobalStructInference.cpp b/src/passes/GlobalStructInference.cpp index bb5a077648a..8de7f20fa50 100644 --- a/src/passes/GlobalStructInference.cpp +++ b/src/passes/GlobalStructInference.cpp @@ -109,7 +109,7 @@ struct GlobalStructInference : public Pass { subTypes = std::make_unique(*module); } - if (getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Closed) { analyzeClosedWorld(module); } diff --git a/src/passes/GlobalTypeOptimization.cpp b/src/passes/GlobalTypeOptimization.cpp index 8171ce1c501..46eb698eaa4 100644 --- a/src/passes/GlobalTypeOptimization.cpp +++ b/src/passes/GlobalTypeOptimization.cpp @@ -153,8 +153,7 @@ struct GlobalTypeOptimization : public Pass { if (!module->features.hasGC()) { return; } - - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "GTO requires --closed-world"; } @@ -207,7 +206,8 @@ struct GlobalTypeOptimization : public Pass { propagator.propagateToSubTypes(dataFromSupersMap); // Find the public types, which we must not modify. - auto publicTypes = ModuleUtils::getPublicHeapTypes(*module); + auto publicTypes = + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode); std::unordered_set publicTypesSet(publicTypes.begin(), publicTypes.end()); @@ -479,7 +479,8 @@ struct GlobalTypeOptimization : public Pass { public: TypeRewriter(Module& wasm, GlobalTypeOptimization& parent) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifyStruct(HeapType oldStructType, Struct& struct_) override { auto& newFields = struct_.fields; diff --git a/src/passes/J2CLItableMerging.cpp b/src/passes/J2CLItableMerging.cpp index 68e610755d2..55bcfc6a917 100644 --- a/src/passes/J2CLItableMerging.cpp +++ b/src/passes/J2CLItableMerging.cpp @@ -72,7 +72,7 @@ struct J2CLItableMerging : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "--merge-j2cl-itables requires --closed-world"; } @@ -384,7 +384,8 @@ struct J2CLItableMerging : public Pass { public: TypeRewriter(Module& wasm, J2CLItableMerging& parent) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifyStruct(HeapType oldStructType, Struct& struct_) override { auto structInfoIt = parent.structInfoByVtableType.find(oldStructType); diff --git a/src/passes/MinimizeRecGroups.cpp b/src/passes/MinimizeRecGroups.cpp index a4d5e8211ac..e6757c8ac96 100644 --- a/src/passes/MinimizeRecGroups.cpp +++ b/src/passes/MinimizeRecGroups.cpp @@ -302,6 +302,7 @@ struct MinimizeRecGroups : Pass { auto typeInfo = ModuleUtils::collectHeapTypeInfo( *module, + getPassOptions().worldMode, ModuleUtils::TypeInclusion::AllTypes, ModuleUtils::VisibilityHandling::FindVisibility); @@ -771,7 +772,7 @@ struct MinimizeRecGroups : Pass { ++i; } } - GlobalTypeRewriter rewriter(wasm); + GlobalTypeRewriter rewriter(wasm, getPassOptions().worldMode); rewriter.mapTypes(oldToNew); rewriter.mapTypeNamesAndIndices(oldToNew); } diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 679e3224029..22f9b3ff536 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -444,7 +444,7 @@ struct Analyzer { } void useRefFunc(Name func) { - if (!options.closedWorld) { + if (options.worldMode == WorldMode::Open) { // The world is open, so assume the worst and something (inside or outside // of the module) can call this. use({ModuleElementKind::Function, func}); @@ -610,8 +610,8 @@ struct Analyzer { // outside of the code we can see), and when it is reached (if it's // unreachable then we don't know the type, and can defer that to DCE to // remove). - if (!options.closedWorld || curr->type == Type::unreachable || - !curr->is()) { + if (options.worldMode == WorldMode::Open || + curr->type == Type::unreachable || !curr->is()) { for (auto* child : ChildIterator(curr)) { use(child); } diff --git a/src/passes/RemoveUnusedTypes.cpp b/src/passes/RemoveUnusedTypes.cpp index b3b0f4a6dd9..3e4e2f83b88 100644 --- a/src/passes/RemoveUnusedTypes.cpp +++ b/src/passes/RemoveUnusedTypes.cpp @@ -39,14 +39,14 @@ struct RemoveUnusedTypes : Pass { // would change the identity of $A. Currently we would incorrectly remove // $unused. To fix that, we need to fix our collection of public types to // consider $A (and $unused) public in an open world. - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "RemoveUnusedTypes requires --closed-world"; } // We're not changing the contents of any of the types, so we just round // trip them through GlobalTypeRewriter which will put all the private types // in a single new rec group and leave out all the unused types. - GlobalTypeRewriter(*module).update(); + GlobalTypeRewriter(*module, getPassOptions().worldMode).update(); } }; diff --git a/src/passes/ReorderTypes.cpp b/src/passes/ReorderTypes.cpp index e120822da51..0b394890aab 100644 --- a/src/passes/ReorderTypes.cpp +++ b/src/passes/ReorderTypes.cpp @@ -41,8 +41,8 @@ struct ReorderingTypeRewriter : GlobalTypeRewriter { static constexpr float maxFactor = 1.0; static constexpr Index numFactors = 21; - ReorderingTypeRewriter(Module& wasm, bool forTesting) - : GlobalTypeRewriter(wasm), forTesting(forTesting) {} + ReorderingTypeRewriter(Module& wasm, bool forTesting, WorldMode worldMode) + : GlobalTypeRewriter(wasm, worldMode), forTesting(forTesting) {} std::vector getSortedTypes(PredecessorGraph preds) override { auto numTypes = preds.size(); @@ -142,11 +142,12 @@ struct ReorderTypes : Pass { } // See note in RemoveUnusedTypes. - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "ReorderTypes requires --closed-world"; } - ReorderingTypeRewriter(*module, forTesting).update(); + ReorderingTypeRewriter(*module, forTesting, getPassOptions().worldMode) + .update(); } }; diff --git a/src/passes/SignaturePruning.cpp b/src/passes/SignaturePruning.cpp index 4670d1c0015..fc95a66bad8 100644 --- a/src/passes/SignaturePruning.cpp +++ b/src/passes/SignaturePruning.cpp @@ -65,7 +65,7 @@ struct SignaturePruning : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "SignaturePruning requires --closed-world"; } @@ -187,7 +187,8 @@ struct SignaturePruning : public Pass { } // Find the public types, which cannot be modified. - for (auto type : ModuleUtils::getPublicHeapTypes(*module)) { + for (auto type : + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode)) { if (type.isFunction()) { allInfo[type].optimizable = false; } @@ -339,7 +340,8 @@ struct SignaturePruning : public Pass { } // Rewrite the types. - GlobalTypeRewriter::updateSignatures(newSignatures, *module); + GlobalTypeRewriter::updateSignatures( + newSignatures, *module, getPassOptions().worldMode); if (callTargetsToLocalize.empty()) { return false; diff --git a/src/passes/SignatureRefining.cpp b/src/passes/SignatureRefining.cpp index c1d72bc8d77..5137b1899a5 100644 --- a/src/passes/SignatureRefining.cpp +++ b/src/passes/SignatureRefining.cpp @@ -156,7 +156,8 @@ struct SignatureRefining : public Pass { } // Find the public types, which we must not modify. - for (auto type : ModuleUtils::getPublicHeapTypes(*module)) { + for (auto type : + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode)) { if (type.isFunction()) { allInfo[type].canModify = false; } @@ -337,7 +338,8 @@ struct SignatureRefining : public Pass { CodeUpdater(*this, *module).run(getPassRunner(), module); // Rewrite the types. - GlobalTypeRewriter::updateSignatures(newSignatures, *module); + GlobalTypeRewriter::updateSignatures( + newSignatures, *module, getPassOptions().worldMode); // Update intrinsics. updateIntrinsics(module, allInfo); diff --git a/src/passes/StringLowering.cpp b/src/passes/StringLowering.cpp index bd753efaf91..c9e836aefe8 100644 --- a/src/passes/StringLowering.cpp +++ b/src/passes/StringLowering.cpp @@ -338,7 +338,7 @@ struct StringLowering : public StringGathering { // Strings turn into externref. updates[HeapType::string] = HeapType::ext; - TypeMapper(*module, updates).map(); + TypeMapper(*module, updates, getPassOptions().worldMode).map(); } // Imported string functions. diff --git a/src/passes/TypeFinalizing.cpp b/src/passes/TypeFinalizing.cpp index 5ba3459da46..85d218da327 100644 --- a/src/passes/TypeFinalizing.cpp +++ b/src/passes/TypeFinalizing.cpp @@ -52,7 +52,8 @@ struct TypeFinalizing : public Pass { // Note we don't need to worry about signature-called functions here // (configureAll) because such calls don't care about finality. - auto privateTypes = ModuleUtils::getPrivateHeapTypes(*module); + auto privateTypes = + ModuleUtils::getPrivateHeapTypes(*module, getPassOptions().worldMode); for (auto type : privateTypes) { // If we are finalizing types then we can only do that to leaf types. If // we are unfinalizing, we can do that unconditionally. @@ -66,7 +67,8 @@ struct TypeFinalizing : public Pass { public: TypeRewriter(Module& wasm, TypeFinalizing& parent) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifyTypeBuilderEntry(TypeBuilder& typeBuilder, Index i, diff --git a/src/passes/TypeMerging.cpp b/src/passes/TypeMerging.cpp index 8ca33699716..64a2df6bb7d 100644 --- a/src/passes/TypeMerging.cpp +++ b/src/passes/TypeMerging.cpp @@ -243,13 +243,14 @@ void TypeMerging::run(Module* module_) { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "TypeMerging requires --closed-world"; } // First, find all the cast types and private types. We will need these to // determine whether types are eligible to be merged. - mergeable = ModuleUtils::getPrivateHeapTypes(*module); + mergeable = + ModuleUtils::getPrivateHeapTypes(*module, getPassOptions().worldMode); privateTypes = std::unordered_set(mergeable.begin(), mergeable.end()); auto casts = findCastTypes(); @@ -303,7 +304,8 @@ bool TypeMerging::merge(MergeKind kind) { Partitions partitions; #if TYPE_MERGING_DEBUG - auto printedPrivateTypes = ModuleUtils::getPrivateHeapTypes(*module); + auto printedPrivateTypes = + ModuleUtils::getPrivateHeapTypes(*module, getPassOptions().worldMode); using Fallback = IndexedTypeNameGenerator; Fallback printPrivate(printedPrivateTypes, "private."); ModuleTypeNameGenerator print(*module, printPrivate); @@ -640,7 +642,7 @@ void TypeMerging::applyMerges() { // We found things to optimize! Rewrite types in the module to apply those // changes. - TypeMapper(*module, replacements).map(); + TypeMapper(*module, replacements, getPassOptions().worldMode).map(); } bool shapeEq(HeapType a, HeapType b) { diff --git a/src/passes/TypeRefining.cpp b/src/passes/TypeRefining.cpp index 201360e5aca..720233cf9ae 100644 --- a/src/passes/TypeRefining.cpp +++ b/src/passes/TypeRefining.cpp @@ -145,7 +145,7 @@ struct TypeRefining : public Pass { return; } - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "TypeRefining requires --closed-world"; } @@ -262,7 +262,8 @@ struct TypeRefining : public Pass { bool canOptimize = false; // We cannot modify public types. - auto publicTypes = ModuleUtils::getPublicHeapTypes(*module); + auto publicTypes = + ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode); std::unordered_set publicTypesSet(publicTypes.begin(), publicTypes.end()); @@ -454,7 +455,8 @@ struct TypeRefining : public Pass { public: TypeRewriter(Module& wasm, TypeRefining& parent) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} void modifyStruct(HeapType oldStructType, Struct& struct_) override { const auto& oldFields = oldStructType.getStruct().fields; diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index f3165b8147c..866e32ba58a 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -567,8 +567,7 @@ struct Unsubtyping : Pass, Noter { if (!wasm->features.hasGC()) { return; } - - if (!getPassOptions().closedWorld) { + if (getPassOptions().worldMode == WorldMode::Open) { Fatal() << "Unsubtyping requires --closed-world"; } @@ -635,7 +634,8 @@ struct Unsubtyping : Pass, Noter { void analyzePublicTypes(Module& wasm) { // We cannot change supertypes for anything public. - for (auto type : ModuleUtils::getPublicHeapTypes(wasm)) { + for (auto type : + ModuleUtils::getPublicHeapTypes(wasm, getPassOptions().worldMode)) { if (auto super = type.getDeclaredSuperType()) { noteSubtype(type, *super); } @@ -1038,7 +1038,8 @@ struct Unsubtyping : Pass, Noter { struct Rewriter : GlobalTypeRewriter { Unsubtyping& parent; Rewriter(Unsubtyping& parent, Module& wasm) - : GlobalTypeRewriter(wasm), parent(parent) {} + : GlobalTypeRewriter(wasm, parent.getPassOptions().worldMode), + parent(parent) {} std::optional getDeclaredSuperType(HeapType type) override { if (auto super = parent.types.getSupertype(type); super && !super->isBasic()) { diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index d47812e0869..df1b3cb809e 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -763,7 +763,7 @@ void PassRunner::addDefaultGlobalOptimizationPrePasses() { addIfNoDWARFIssues("once-reduction"); } if (wasm->features.hasGC() && options.optimizeLevel >= 2) { - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { addIfNoDWARFIssues("type-refining"); addIfNoDWARFIssues("signature-pruning"); addIfNoDWARFIssues("signature-refining"); @@ -773,11 +773,11 @@ void PassRunner::addDefaultGlobalOptimizationPrePasses() { // remove ref.funcs that were once assigned to vtables but are no longer // needed, which can allow more code to be removed globally. After those, // constant field propagation can be more effective. - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { addIfNoDWARFIssues("gto"); } addIfNoDWARFIssues("remove-unused-module-elements"); - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { addIfNoDWARFIssues("remove-unused-types"); // Allow ref.tests in cfp if we are aggressively optimizing for speed. if (options.optimizeLevel >= 3) { @@ -787,7 +787,7 @@ void PassRunner::addDefaultGlobalOptimizationPrePasses() { } } addIfNoDWARFIssues("gsi"); - if (options.closedWorld) { + if (options.worldMode == WorldMode::Closed) { addIfNoDWARFIssues("abstract-type-refining"); addIfNoDWARFIssues("unsubtyping"); } diff --git a/src/tools/fuzzing.h b/src/tools/fuzzing.h index 803e13d5d0b..fa819e20772 100644 --- a/src/tools/fuzzing.h +++ b/src/tools/fuzzing.h @@ -121,10 +121,10 @@ class TranslateToFuzzReader { public: TranslateToFuzzReader(Module& wasm, std::vector&& input, - bool closedWorld = false); + WorldMode worldMode = WorldMode::Open); TranslateToFuzzReader(Module& wasm, std::string& filename, - bool closedWorld = false); + WorldMode worldMode = WorldMode::Open); void pickPasses(OptimizationOptions& options); void setAllowMemory(bool allowMemory_) { allowMemory = allowMemory_; } @@ -141,7 +141,7 @@ class TranslateToFuzzReader { private: // Whether the module will be tested in a closed-world environment. - bool closedWorld; + WorldMode worldMode; Builder builder; Random random; Intrinsics intrinsics; diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index 759061da88f..69e97fa5aa2 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -62,8 +62,8 @@ std::vector getMemoryOrders(const FeatureSet& features) { TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, std::vector&& input, - bool closedWorld) - : wasm(wasm), closedWorld(closedWorld), builder(wasm), + WorldMode worldMode) + : wasm(wasm), worldMode(worldMode), builder(wasm), random(std::move(input), wasm.features), intrinsics(wasm), loggableTypes(getLoggableTypes(wasm.features)), atomicMemoryOrders(getMemoryOrders(wasm.features)), @@ -123,10 +123,9 @@ TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm, std::string& filename, - bool closedWorld) - : TranslateToFuzzReader(wasm, - read_file>(filename, Flags::Binary), - closedWorld) {} + WorldMode worldMode) + : TranslateToFuzzReader( + wasm, read_file>(filename, Flags::Binary), worldMode) {} void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { // Pick random passes to further shape the wasm. This is similar to how we @@ -274,8 +273,8 @@ void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { // Most of these depend on closed world, so just set that. Set it both // on the global pass options, and in the internal state of this // TranslateToFuzzReader instance. - options.passOptions.closedWorld = true; - closedWorld = true; + options.passOptions.worldMode = WorldMode::Closed; + worldMode = WorldMode::Closed; switch (upTo(16)) { case 0: @@ -343,8 +342,8 @@ void TranslateToFuzzReader::pickPasses(OptimizationOptions& options) { options.passOptions.shrinkLevel = upTo(3); } - if (!options.passOptions.closedWorld && oneIn(2)) { - options.passOptions.closedWorld = true; + if (options.passOptions.worldMode == WorldMode::Open && oneIn(2)) { + options.passOptions.worldMode = WorldMode::Closed; } // Prune things that error in JS if we call them (like SIMD), some of the @@ -1683,7 +1682,7 @@ void TranslateToFuzzReader::processFunctions() { // Also fix up closed world, if we need to. We must do this at the end, so // nothing can break the closed world assumptions after. - if (closedWorld) { + if (worldMode == WorldMode::Closed) { for (auto& func : wasm.functions) { if (!func->imported()) { fixClosedWorld(func.get()); @@ -2194,7 +2193,7 @@ void TranslateToFuzzReader::mutate(Function* func) { } void TranslateToFuzzReader::fixClosedWorld(Function* func) { - assert(closedWorld); + assert(worldMode == WorldMode::Closed); struct Fixer : public ExpressionStackWalker> { @@ -6699,7 +6698,7 @@ bool TranslateToFuzzReader::isValidRefFuncTarget(Name func) { // reference, but in that mode we must only pass in jsCalled functions. We // handle direct calls in fixClosedWorld, but cannot handle indirect ones // easily, so just disallow taking references of those functions. - if (!closedWorld) { + if (worldMode == WorldMode::Open) { return true; } return !isCallRefImport(func); diff --git a/src/tools/tool-options.h b/src/tools/tool-options.h index 87362d455d2..1f3ec266bc7 100644 --- a/src/tools/tool-options.h +++ b/src/tools/tool-options.h @@ -173,7 +173,7 @@ struct ToolOptions : public Options { ToolOptionsCategory, Options::Arguments::Zero, [this](Options*, const std::string&) { - passOptions.closedWorld = true; + passOptions.worldMode = WorldMode::Closed; }) .add( "--preserve-type-order", diff --git a/src/tools/wasm-opt.cpp b/src/tools/wasm-opt.cpp index f593428d2b6..9bdfa4b06f7 100644 --- a/src/tools/wasm-opt.cpp +++ b/src/tools/wasm-opt.cpp @@ -353,7 +353,7 @@ For more on how to optimize effectively, see } if (translateToFuzz) { TranslateToFuzzReader reader( - wasm, options.extra["infile"], options.passOptions.closedWorld); + wasm, options.extra["infile"], options.passOptions.worldMode); reader.setAllowMemory(fuzzMemory); reader.setAllowOOB(fuzzOOB); reader.setPreserveImportsAndExports(fuzzPreserveImportsAndExports); diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index 64b67e51edd..a8c9f40e188 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -250,7 +250,7 @@ void validateExactReferences(Module& module, ValidationInfo& info) { return; } - for (auto type : ModuleUtils::getPublicHeapTypes(module)) { + for (auto type : ModuleUtils::getExposedPublicHeapTypes(module)) { for (auto child : type.getTypeChildren()) { if (child.isExact()) { std::string typeName; From 865b60914e35d66bb40f3de733f733ab5f020e08 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 26 May 2026 13:48:00 -0700 Subject: [PATCH 153/168] Update merge-blocks-atomics.wast expectations (#8777) This incorrect test expectation was force-landed past a CI infra failure, so is causing failures on main. Fix it. --- test/lit/passes/merge-blocks-atomics.wast | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/lit/passes/merge-blocks-atomics.wast b/test/lit/passes/merge-blocks-atomics.wast index 6435a9d7202..e8ed209d7ef 100644 --- a/test/lit/passes/merge-blocks-atomics.wast +++ b/test/lit/passes/merge-blocks-atomics.wast @@ -14,7 +14,7 @@ ;; CHECK: (func $disallowed (type $1) (param $x (ref $struct)) ;; CHECK-NEXT: (call $foo - ;; CHECK-NEXT: (block $label1 (result i32) + ;; CHECK-NEXT: (block $block (result i32) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (i32.atomic.load acqrel ;; CHECK-NEXT: (i32.const 0) @@ -55,7 +55,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (call $foo - ;; CHECK-NEXT: (block $label2 (result i32) + ;; CHECK-NEXT: (block $block (result i32) ;; CHECK-NEXT: (i32.atomic.store acqrel ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: (i32.const 42) From 818de5e9c9e959e470c145f9f562991cf45fa952 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 26 May 2026 15:53:38 -0700 Subject: [PATCH 154/168] OptimizeCasts: Migrate from invalidates to orderedBefore (#8774) Replace the coarse 'invalidates' check in 'OptimizeCasts' with 'orderedBefore'. This is a cleanup to use the more precise directional effects check. Also add a new lit test to verify that casts (which have trap effects) are correctly allowed to move past global state reads but blocked by global state writes. TAG=agy --- src/passes/OptimizeCasts.cpp | 6 +- test/lit/passes/optimize-casts-order.wast | 149 ++++++++++++++++++++++ 2 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 test/lit/passes/optimize-casts-order.wast diff --git a/src/passes/OptimizeCasts.cpp b/src/passes/OptimizeCasts.cpp index e2e7c8c43ab..a6ea8fa2ac2 100644 --- a/src/passes/OptimizeCasts.cpp +++ b/src/passes/OptimizeCasts.cpp @@ -237,16 +237,16 @@ struct EarlyCastFinder void visitExpression(Expression* curr) { // A new one is instantiated for each expression to determine - // if a cast can be moved past it. + // if a cast can be moved backward past it. ShallowEffectAnalyzer currAnalyzer(options, *getModule(), curr); - if (testRefCast.invalidates(currAnalyzer)) { + if (currAnalyzer.orderedBefore(testRefCast)) { for (size_t i = 0; i < numLocals; i++) { flushRefCastResult(i, *getModule()); } } - if (testRefAs.invalidates(currAnalyzer)) { + if (currAnalyzer.orderedBefore(testRefAs)) { for (size_t i = 0; i < numLocals; i++) { flushRefAsResult(i, *getModule()); } diff --git a/test/lit/passes/optimize-casts-order.wast b/test/lit/passes/optimize-casts-order.wast new file mode 100644 index 00000000000..c74eea29572 --- /dev/null +++ b/test/lit/passes/optimize-casts-order.wast @@ -0,0 +1,149 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --optimize-casts -all -S -o - | filecheck %s + +(module + ;; CHECK: (type $A (sub (struct))) + (type $A (sub (struct))) + ;; CHECK: (type $B (sub $A (struct))) + (type $B (sub $A (struct))) + + (memory 1) + + ;; CHECK: (global $g (mut i32) (i32.const 0)) + (global $g (mut i32) (i32.const 0)) + + ;; CHECK: (func $cast_allowed (type $2) (param $x (ref null $A)) + ;; CHECK-NEXT: (local $temp (ref null $A)) + ;; CHECK-NEXT: (local $2 (ref null $B)) + ;; CHECK-NEXT: (local.set $temp + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $2 + ;; CHECK-NEXT: (ref.cast (ref null $B) + ;; CHECK-NEXT: (local.get $temp) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.cast (ref null $B) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cast_allowed (param $x (ref null $A)) + ;; ref.cast can move back past load. + (local $temp (ref null $A)) + (local.set $temp (local.get $x)) + (block + (drop (local.get $temp)) + (drop (i32.load (i32.const 0))) + (drop (ref.cast (ref null $B) (local.get $temp))) + ) + ) + + ;; CHECK: (func $cast_disallowed (type $2) (param $x (ref null $A)) + ;; CHECK-NEXT: (local $temp (ref null $A)) + ;; CHECK-NEXT: (local.set $temp + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $temp) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.cast (ref null $B) + ;; CHECK-NEXT: (local.get $temp) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cast_disallowed (param $x (ref null $A)) + ;; ref.cast cannot move back past a store. + (local $temp (ref null $A)) + (local.set $temp (local.get $x)) + (block + (drop (local.get $temp)) + (i32.store (i32.const 0) (i32.const 42)) + (drop (ref.cast (ref null $B) (local.get $temp))) + ) + ) + + ;; Test 3: ref.as positive (moves past mutable global read) + ;; CHECK: (func $as_allowed (type $3) (param $x anyref) + ;; CHECK-NEXT: (local $temp anyref) + ;; CHECK-NEXT: (local $2 (ref any)) + ;; CHECK-NEXT: (local.set $temp + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $2 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $temp) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (global.get $g) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $as_allowed (param $x anyref) + ;; ref.as_non_null can move back past a global.get. + (local $temp anyref) + (local.set $temp (local.get $x)) + (block + (drop (local.get $temp)) + (drop (global.get $g)) + (drop (ref.as_non_null (local.get $temp))) + ) + ) + + ;; Test 4: ref.as negative (blocked by mutable global write) + ;; CHECK: (func $as_disallowed (type $3) (param $x anyref) + ;; CHECK-NEXT: (local $temp anyref) + ;; CHECK-NEXT: (local.set $temp + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $temp) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $temp) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $as_disallowed (param $x anyref) + ;; ref.as_non_null cannot move back past a global.set. + (local $temp anyref) + (local.set $temp (local.get $x)) + (block + (drop (local.get $temp)) + (global.set $g (i32.const 42)) + (drop (ref.as_non_null (local.get $temp))) + ) + ) +) From 83f68e13b2cd22e52a8e8c19b9a48cc11416c99a Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 26 May 2026 16:04:10 -0700 Subject: [PATCH 155/168] [wasm-ctor-eval] Avoid local and global state getting out of sync (#8776) Simplify this by clearing before any serialization. Before, if we partially evalled, the local and global state could diverge, leading to locals referring to the wrong globals. Changes to the existing test are NFC, just reordering of names (sadly). --- src/tools/wasm-ctor-eval.cpp | 23 ++- test/lit/ctor-eval/gc-cycle.wast | 264 +++++++++++++-------------- test/lit/ctor-eval/partial-local.wat | 78 ++++++++ 3 files changed, 223 insertions(+), 142 deletions(-) create mode 100644 test/lit/ctor-eval/partial-local.wat diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index c8fe1d5e385..0dbeb57772a 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -288,6 +288,9 @@ std::unique_ptr buildEnvModule(Module& wasm) { // that there are not arguments passed to main, etc. static bool ignoreExternalInput = false; +// Whether to emit informative logging to stdout about the eval process. +static bool quiet = false; + struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { Module* wasm; EvallingModuleRunner* instance; @@ -328,8 +331,6 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { firstApplication = false; } - clearApplyState(); - // If nothing was ever written to memories then there is nothing to update. if (!memories.empty()) { applyMemoryToModule(); @@ -529,12 +530,12 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { return Bits::readLE(getMemory(address, memoryName, sizeof(T))); } +public: // Clear the state of the operation of applying the interpreter's runtime - // information into the module. - // - // This happens each time we apply contents to the module, which is basically - // once per ctor function, but can be more fine-grained also if we execute a - // line at a time. + // information into the module. This must be done before we start to serialize + // content (as the serialization uses this state - defining globals must be + // set and are latter used, etc.). After this, serialization can happen, and + // after that, a call to applyToModule() can be done. void clearApplyState() { // The process of allocating "defining globals" begins here, from scratch // each time (things live before may no longer be). @@ -546,6 +547,7 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { clearStartBlock(); } +private: void applyMemoryToModule() { // Memory must have already been flattened into the standard form: one // segment at offset 0, or none. @@ -1062,9 +1064,6 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { } }; -// Whether to emit informative logging to stdout about the eval process. -static bool quiet = false; - // The outcome of evalling a ctor is one of three states: // // 1. We failed to eval it completely (but perhaps we succeeded partially). In @@ -1203,6 +1202,10 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, break; } + // We are about to serialize content (the code paths below call + // getSerialization). Clear the state. + interface.clearApplyState(); + if (flow.breakTo == RETURN_CALL_FLOW) { // The return-called function is stored in the last value. func = wasm.getFunction(flow.values.back().getFunc()); diff --git a/test/lit/ctor-eval/gc-cycle.wast b/test/lit/ctor-eval/gc-cycle.wast index faf26c4adec..6d4f0726730 100644 --- a/test/lit/ctor-eval/gc-cycle.wast +++ b/test/lit/ctor-eval/gc-cycle.wast @@ -9,12 +9,12 @@ ;; CHECK: (type $2 (func (result i32))) - ;; CHECK: (global $ctor-eval$global_3 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_2 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: )) - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_3)) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_2)) (global $a (mut (ref null $A)) (ref.null $A)) (func $test (export "test") @@ -64,8 +64,8 @@ ;; CHECK: (func $start (type $1) ;; CHECK-NEXT: (struct.set $A 0 -;; CHECK-NEXT: (global.get $ctor-eval$global_3) -;; CHECK-NEXT: (global.get $ctor-eval$global_3) +;; CHECK-NEXT: (global.get $ctor-eval$global_2) +;; CHECK-NEXT: (global.get $ctor-eval$global_2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -84,12 +84,12 @@ ;; CHECK: (type $2 (func (result i32))) - ;; CHECK: (global $ctor-eval$global_3 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_2 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: )) - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_3)) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_2)) (global $a (mut (ref null $A)) (ref.null $A)) (func $test (export "test") @@ -132,8 +132,8 @@ ;; CHECK: (func $start (type $1) ;; CHECK-NEXT: (struct.set $A 1 -;; CHECK-NEXT: (global.get $ctor-eval$global_3) -;; CHECK-NEXT: (global.get $ctor-eval$global_3) +;; CHECK-NEXT: (global.get $ctor-eval$global_2) +;; CHECK-NEXT: (global.get $ctor-eval$global_2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -151,20 +151,20 @@ ;; CHECK: (type $2 (func (result i32))) - ;; CHECK: (global $ctor-eval$global_7 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_5 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_8 (ref (exact $A)) (struct.new $A - ;; CHECK-NEXT: (global.get $ctor-eval$global_7) + ;; CHECK: (global $ctor-eval$global_6 (ref (exact $A)) (struct.new $A + ;; CHECK-NEXT: (global.get $ctor-eval$global_5) ;; CHECK-NEXT: (i32.const 1337) ;; CHECK-NEXT: )) - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_7)) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_5)) (global $a (mut (ref null $A)) (ref.null $A)) - ;; CHECK: (global $b (mut (ref null $A)) (global.get $ctor-eval$global_8)) + ;; CHECK: (global $b (mut (ref null $A)) (global.get $ctor-eval$global_6)) (global $b (mut (ref null $A)) (ref.null $A)) (func $test (export "test") @@ -223,8 +223,8 @@ ;; CHECK: (func $start (type $1) ;; CHECK-NEXT: (struct.set $A 0 -;; CHECK-NEXT: (global.get $ctor-eval$global_7) -;; CHECK-NEXT: (global.get $ctor-eval$global_8) +;; CHECK-NEXT: (global.get $ctor-eval$global_5) +;; CHECK-NEXT: (global.get $ctor-eval$global_6) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -250,20 +250,20 @@ ;; CHECK: (type $3 (func (result i32))) - ;; CHECK: (global $ctor-eval$global_7 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_5 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_8 (ref (exact $B)) (struct.new $B - ;; CHECK-NEXT: (global.get $ctor-eval$global_7) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_5)) + (global $a (mut (ref null $A)) (ref.null $A)) + + ;; CHECK: (global $ctor-eval$global_6 (ref (exact $B)) (struct.new $B + ;; CHECK-NEXT: (global.get $ctor-eval$global_5) ;; CHECK-NEXT: (i32.const 1337) ;; CHECK-NEXT: )) - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_7)) - (global $a (mut (ref null $A)) (ref.null $A)) - - ;; CHECK: (global $b (mut (ref null $B)) (global.get $ctor-eval$global_8)) + ;; CHECK: (global $b (mut (ref null $B)) (global.get $ctor-eval$global_6)) (global $b (mut (ref null $B)) (ref.null $B)) (func $test (export "test") @@ -321,8 +321,8 @@ ;; CHECK: (func $start (type $2) ;; CHECK-NEXT: (struct.set $A 0 -;; CHECK-NEXT: (global.get $ctor-eval$global_7) -;; CHECK-NEXT: (global.get $ctor-eval$global_8) +;; CHECK-NEXT: (global.get $ctor-eval$global_5) +;; CHECK-NEXT: (global.get $ctor-eval$global_6) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -348,19 +348,19 @@ ;; CHECK: (type $3 (func (result i32))) - ;; CHECK: (global $ctor-eval$global_7 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_5 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_8 (ref (exact $B)) (struct.new $B - ;; CHECK-NEXT: (global.get $ctor-eval$global_7) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_5)) + + ;; CHECK: (global $ctor-eval$global_6 (ref (exact $B)) (struct.new $B + ;; CHECK-NEXT: (global.get $ctor-eval$global_5) ;; CHECK-NEXT: (i32.const 1337) ;; CHECK-NEXT: )) - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_7)) - - ;; CHECK: (global $b (mut (ref null $B)) (global.get $ctor-eval$global_8)) + ;; CHECK: (global $b (mut (ref null $B)) (global.get $ctor-eval$global_6)) (global $b (mut (ref null $B)) (ref.null $B)) (global $a (mut (ref null $A)) (ref.null $A)) @@ -420,8 +420,8 @@ ;; CHECK: (func $start (type $2) ;; CHECK-NEXT: (struct.set $A 0 -;; CHECK-NEXT: (global.get $ctor-eval$global_7) -;; CHECK-NEXT: (global.get $ctor-eval$global_8) +;; CHECK-NEXT: (global.get $ctor-eval$global_5) +;; CHECK-NEXT: (global.get $ctor-eval$global_6) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -446,17 +446,12 @@ ;; CHECK: (type $3 (func (result i32))) - ;; CHECK: (global $ctor-eval$global_7 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_5 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_8 (ref (exact $B)) (struct.new $B - ;; CHECK-NEXT: (global.get $ctor-eval$global_7) - ;; CHECK-NEXT: (i32.const 1337) - ;; CHECK-NEXT: )) - - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_7)) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_5)) (global $a (mut (ref null $A)) (ref.null $A)) (global $b (mut (ref null $B)) (ref.null $B)) @@ -486,6 +481,11 @@ ) ) + ;; CHECK: (global $ctor-eval$global_6 (ref (exact $B)) (struct.new $B + ;; CHECK-NEXT: (global.get $ctor-eval$global_5) + ;; CHECK-NEXT: (i32.const 1337) + ;; CHECK-NEXT: )) + ;; CHECK: (export "test" (func $test_3)) ;; CHECK: (export "keepalive" (func $keepalive)) @@ -506,8 +506,8 @@ ;; CHECK: (func $start (type $2) ;; CHECK-NEXT: (struct.set $A 0 -;; CHECK-NEXT: (global.get $ctor-eval$global_7) -;; CHECK-NEXT: (global.get $ctor-eval$global_8) +;; CHECK-NEXT: (global.get $ctor-eval$global_5) +;; CHECK-NEXT: (global.get $ctor-eval$global_6) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -536,17 +536,12 @@ ;; CHECK: (type $3 (func (result i32))) - ;; CHECK: (global $ctor-eval$global_7 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_5 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_8 (ref (exact $B)) (struct.new $B - ;; CHECK-NEXT: (global.get $ctor-eval$global_7) - ;; CHECK-NEXT: (i32.const 1337) - ;; CHECK-NEXT: )) - - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_7)) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_5)) (global $a (mut (ref null $A)) (ref.null $A)) (func $test (export "test") @@ -574,6 +569,11 @@ ) ) + ;; CHECK: (global $ctor-eval$global_6 (ref (exact $B)) (struct.new $B + ;; CHECK-NEXT: (global.get $ctor-eval$global_5) + ;; CHECK-NEXT: (i32.const 1337) + ;; CHECK-NEXT: )) + ;; CHECK: (export "test" (func $test_3)) ;; CHECK: (export "keepalive" (func $keepalive)) @@ -594,8 +594,8 @@ ;; CHECK: (func $start (type $2) ;; CHECK-NEXT: (struct.set $A 0 -;; CHECK-NEXT: (global.get $ctor-eval$global_7) -;; CHECK-NEXT: (global.get $ctor-eval$global_8) +;; CHECK-NEXT: (global.get $ctor-eval$global_5) +;; CHECK-NEXT: (global.get $ctor-eval$global_6) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -614,22 +614,22 @@ ;; CHECK: (type $2 (func (result i32))) - ;; CHECK: (global $ctor-eval$global_12 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_9 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_14 (ref (exact $A)) (struct.new $A - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) + ;; CHECK: (global $ctor-eval$global_11 (ref (exact $A)) (struct.new $A + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) ;; CHECK-NEXT: (i32.const 1337) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_13 (ref (exact $A)) (struct.new $A - ;; CHECK-NEXT: (global.get $ctor-eval$global_14) + ;; CHECK: (global $ctor-eval$global_10 (ref (exact $A)) (struct.new $A + ;; CHECK-NEXT: (global.get $ctor-eval$global_11) ;; CHECK-NEXT: (i32.const 99999) ;; CHECK-NEXT: )) - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_12)) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_9)) (global $a (mut (ref null $A)) (ref.null $A)) (global $b (mut (ref null $A)) (ref.null $A)) @@ -690,8 +690,8 @@ ;; CHECK: (func $start (type $1) ;; CHECK-NEXT: (struct.set $A 0 -;; CHECK-NEXT: (global.get $ctor-eval$global_12) -;; CHECK-NEXT: (global.get $ctor-eval$global_13) +;; CHECK-NEXT: (global.get $ctor-eval$global_9) +;; CHECK-NEXT: (global.get $ctor-eval$global_10) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -725,25 +725,12 @@ ;; CHECK: (type $4 (func (result i32))) - ;; CHECK: (global $ctor-eval$global_12 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_9 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_14 (ref (exact $B)) (array.new_fixed $B 10 - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: )) - - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_12)) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_9)) (global $a (mut (ref null $A)) (ref.null $A)) (func $test (export "test") @@ -780,9 +767,22 @@ ) ) - ;; CHECK: (global $ctor-eval$global_13 (ref (exact $C)) (array.new_fixed $C 2 - ;; CHECK-NEXT: (global.get $ctor-eval$global_14) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) + ;; CHECK: (global $ctor-eval$global_11 (ref (exact $B)) (array.new_fixed $B 10 + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: )) + + ;; CHECK: (global $ctor-eval$global_10 (ref (exact $C)) (array.new_fixed $C 2 + ;; CHECK-NEXT: (global.get $ctor-eval$global_11) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) ;; CHECK-NEXT: )) ;; CHECK: (export "test" (func $test_3)) @@ -805,8 +805,8 @@ ;; CHECK: (func $start (type $3) ;; CHECK-NEXT: (struct.set $A 0 -;; CHECK-NEXT: (global.get $ctor-eval$global_12) -;; CHECK-NEXT: (global.get $ctor-eval$global_13) +;; CHECK-NEXT: (global.get $ctor-eval$global_9) +;; CHECK-NEXT: (global.get $ctor-eval$global_10) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -833,25 +833,12 @@ ;; CHECK: (type $4 (func (result i32))) - ;; CHECK: (global $ctor-eval$global_12 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_9 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_14 (ref (exact $B)) (array.new_fixed $B 10 - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) - ;; CHECK-NEXT: )) - - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_12)) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_9)) (global $a (mut (ref null $A)) (ref.null $A)) (global $b (mut (ref null $B)) (ref.null $B)) @@ -892,9 +879,22 @@ ) ) - ;; CHECK: (global $ctor-eval$global_13 (ref (exact $C)) (array.new_fixed $C 2 - ;; CHECK-NEXT: (global.get $ctor-eval$global_14) - ;; CHECK-NEXT: (global.get $ctor-eval$global_12) + ;; CHECK: (global $ctor-eval$global_11 (ref (exact $B)) (array.new_fixed $B 10 + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) + ;; CHECK-NEXT: )) + + ;; CHECK: (global $ctor-eval$global_10 (ref (exact $C)) (array.new_fixed $C 2 + ;; CHECK-NEXT: (global.get $ctor-eval$global_11) + ;; CHECK-NEXT: (global.get $ctor-eval$global_9) ;; CHECK-NEXT: )) ;; CHECK: (export "test" (func $test_3)) @@ -917,8 +917,8 @@ ;; CHECK: (func $start (type $3) ;; CHECK-NEXT: (struct.set $A 0 -;; CHECK-NEXT: (global.get $ctor-eval$global_12) -;; CHECK-NEXT: (global.get $ctor-eval$global_13) +;; CHECK-NEXT: (global.get $ctor-eval$global_9) +;; CHECK-NEXT: (global.get $ctor-eval$global_10) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -944,7 +944,7 @@ ;; CHECK: (type $3 (func (result anyref))) - ;; CHECK: (global $ctor-eval$global_17 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_11 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (ref.null none) @@ -956,13 +956,13 @@ ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_18 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_15 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: )) - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_14)) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_11)) (global $a (mut (ref null $A)) (ref.null $A)) (global $b (mut (ref null $B)) (ref.null $B)) @@ -994,15 +994,15 @@ ) ) - ;; CHECK: (global $ctor-eval$global_16 (ref (exact $B)) (array.new_fixed $B 3 - ;; CHECK-NEXT: (global.get $ctor-eval$global_17) + ;; CHECK: (global $ctor-eval$global_13 (ref (exact $B)) (array.new_fixed $B 3 ;; CHECK-NEXT: (global.get $ctor-eval$global_14) - ;; CHECK-NEXT: (global.get $ctor-eval$global_18) + ;; CHECK-NEXT: (global.get $ctor-eval$global_11) + ;; CHECK-NEXT: (global.get $ctor-eval$global_15) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_15 (ref (exact $B)) (array.new_fixed $B 0)) + ;; CHECK: (global $ctor-eval$global_12 (ref (exact $B)) (array.new_fixed $B 0)) - ;; CHECK: (global $ctor-eval$global_19 (ref (exact $B)) (array.new_fixed $B 0)) + ;; CHECK: (global $ctor-eval$global_16 (ref (exact $B)) (array.new_fixed $B 0)) ;; CHECK: (export "test" (func $test_3)) @@ -1024,16 +1024,16 @@ ;; CHECK: (func $start (type $2) ;; CHECK-NEXT: (struct.set $A 0 -;; CHECK-NEXT: (global.get $ctor-eval$global_14) -;; CHECK-NEXT: (global.get $ctor-eval$global_15) +;; CHECK-NEXT: (global.get $ctor-eval$global_11) +;; CHECK-NEXT: (global.get $ctor-eval$global_12) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $A 1 -;; CHECK-NEXT: (global.get $ctor-eval$global_14) -;; CHECK-NEXT: (global.get $ctor-eval$global_16) +;; CHECK-NEXT: (global.get $ctor-eval$global_11) +;; CHECK-NEXT: (global.get $ctor-eval$global_13) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $A 2 -;; CHECK-NEXT: (global.get $ctor-eval$global_14) -;; CHECK-NEXT: (global.get $ctor-eval$global_19) +;; CHECK-NEXT: (global.get $ctor-eval$global_11) +;; CHECK-NEXT: (global.get $ctor-eval$global_16) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -1056,23 +1056,23 @@ ;; CHECK: (type $3 (func (result anyref))) - ;; CHECK: (global $ctor-eval$global_17 (ref (exact $B)) (array.new_fixed $B 0)) - - ;; CHECK: (global $ctor-eval$global_14 (ref (exact $B)) (array.new_fixed $B 3 + ;; CHECK: (global $ctor-eval$global_11 (ref (exact $B)) (array.new_fixed $B 3 ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_18 (ref (exact $B)) (array.new_fixed $B 0)) + ;; CHECK: (global $ctor-eval$global_14 (ref (exact $B)) (array.new_fixed $B 0)) - ;; CHECK: (global $ctor-eval$global_16 (ref (exact $A)) (struct.new $A - ;; CHECK-NEXT: (global.get $ctor-eval$global_17) + ;; CHECK: (global $ctor-eval$global_15 (ref (exact $B)) (array.new_fixed $B 0)) + + ;; CHECK: (global $ctor-eval$global_13 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (global.get $ctor-eval$global_14) - ;; CHECK-NEXT: (global.get $ctor-eval$global_18) + ;; CHECK-NEXT: (global.get $ctor-eval$global_11) + ;; CHECK-NEXT: (global.get $ctor-eval$global_15) ;; CHECK-NEXT: )) - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_16)) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_13)) (global $a (mut (ref null $A)) (ref.null $A)) (global $b (mut (ref null $B)) (ref.null $B)) @@ -1105,13 +1105,13 @@ ) ) - ;; CHECK: (global $ctor-eval$global_15 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_12 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: )) - ;; CHECK: (global $ctor-eval$global_19 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_16 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (ref.null none) @@ -1137,19 +1137,19 @@ ;; CHECK: (func $start (type $2) ;; CHECK-NEXT: (array.set $B -;; CHECK-NEXT: (global.get $ctor-eval$global_14) +;; CHECK-NEXT: (global.get $ctor-eval$global_11) ;; CHECK-NEXT: (i32.const 0) -;; CHECK-NEXT: (global.get $ctor-eval$global_15) +;; CHECK-NEXT: (global.get $ctor-eval$global_12) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (array.set $B -;; CHECK-NEXT: (global.get $ctor-eval$global_14) +;; CHECK-NEXT: (global.get $ctor-eval$global_11) ;; CHECK-NEXT: (i32.const 1) -;; CHECK-NEXT: (global.get $ctor-eval$global_16) +;; CHECK-NEXT: (global.get $ctor-eval$global_13) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (array.set $B -;; CHECK-NEXT: (global.get $ctor-eval$global_14) +;; CHECK-NEXT: (global.get $ctor-eval$global_11) ;; CHECK-NEXT: (i32.const 2) -;; CHECK-NEXT: (global.get $ctor-eval$global_19) +;; CHECK-NEXT: (global.get $ctor-eval$global_16) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -1169,12 +1169,12 @@ ;; CHECK: (type $2 (func (result i32))) - ;; CHECK: (global $ctor-eval$global_4 (ref (exact $A)) (struct.new $A + ;; CHECK: (global $ctor-eval$global_3 (ref (exact $A)) (struct.new $A ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (i32.const 42) ;; CHECK-NEXT: )) - ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_4)) + ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_3)) (global $a (mut (ref null $A)) (ref.null $A)) ;; CHECK: (global $b (mut (ref null $A)) (ref.null none)) @@ -1234,8 +1234,8 @@ ;; CHECK: (func $start_3 (type $1) ;; CHECK-NEXT: (struct.set $A 0 -;; CHECK-NEXT: (global.get $ctor-eval$global_4) -;; CHECK-NEXT: (global.get $ctor-eval$global_4) +;; CHECK-NEXT: (global.get $ctor-eval$global_3) +;; CHECK-NEXT: (global.get $ctor-eval$global_3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) diff --git a/test/lit/ctor-eval/partial-local.wat b/test/lit/ctor-eval/partial-local.wat new file mode 100644 index 00000000000..a1bc8a6725b --- /dev/null +++ b/test/lit/ctor-eval/partial-local.wat @@ -0,0 +1,78 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: wasm-ctor-eval %s --ctors=test --kept-exports=test --quiet -all -S -o - | filecheck %s + +(module + ;; CHECK: (type $0 (sub (struct (field i32) (field (mut (ref null $0)))))) + (type $0 (sub (struct (field i32) (field (mut (ref null $0)))))) + + (import "__fuzz_import" "extern$" (global $gimport externref)) + + (global $global (ref null $0) (struct.new $0 + (i32.const 0) + (struct.new_default $0) + )) + + (export "test" (func $0)) + + (func $0 + (local $temp (ref null $0)) + + ;; wasm-ctor-eval evals away the get of $global, since it sees all the data. + (local.set $temp + (global.get $global) + ) + + ;; It stops at the read of the imported global, since that value depends on + ;; runtime info. + ;; + ;; We should leave the module in a valid state: globals appear, and a start + ;; function which applies one to the other. It should apply to the same global + ;; that is then read from the remaining code in the export. + ;; + ;; Note that this get of the import vanishes in the final output, not because + ;; we eval it, but because we run vacuum after. + (drop + (global.get $gimport) + ) + + (drop + (ref.as_non_null + (struct.get $0 1 + (local.get $temp) + ) + ) + ) + ) +) +;; CHECK: (type $1 (func)) + +;; CHECK: (global $ctor-eval$global (ref (exact $0)) (struct.new $0 +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: (ref.null none) +;; CHECK-NEXT: )) + +;; CHECK: (global $ctor-eval$global_3 (ref (exact $0)) (struct.new $0 +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: (ref.null none) +;; CHECK-NEXT: )) + +;; CHECK: (export "test" (func $0_2)) + +;; CHECK: (start $start) + +;; CHECK: (func $start (type $1) +;; CHECK-NEXT: (struct.set $0 1 +;; CHECK-NEXT: (global.get $ctor-eval$global) +;; CHECK-NEXT: (global.get $ctor-eval$global_3) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $0_2 (type $1) +;; CHECK-NEXT: (drop +;; CHECK-NEXT: (ref.as_non_null +;; CHECK-NEXT: (struct.get $0 1 +;; CHECK-NEXT: (global.get $ctor-eval$global) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) From 9f2642a28f6d2963a7dbe134dee80cb5163c67e9 Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Tue, 26 May 2026 19:36:10 -0700 Subject: [PATCH 156/168] NFC: Simplify test with improved indirect call effects (#8780) This was missed after #8625 and #8738. We don't need the extra indirection of a direct call in the test since we're testing effect analysis for indirect calls and call chains containing both indirect and direct calls is already tested [here](https://github.com/WebAssembly/binaryen/blob/83f68e13b2cd22e52a8e8c19b9a48cc11416c99a/test/lit/passes/global-effects-closed-world.wast#L248-L251). --- ...-effects-closed-world-simplify-locals.wast | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/test/lit/passes/global-effects-closed-world-simplify-locals.wast b/test/lit/passes/global-effects-closed-world-simplify-locals.wast index 23f5dc17362..534e6476172 100644 --- a/test/lit/passes/global-effects-closed-world-simplify-locals.wast +++ b/test/lit/passes/global-effects-closed-world-simplify-locals.wast @@ -8,11 +8,11 @@ ;; CHECK: (type $indirect-type-super (sub (func (param i32)))) (type $indirect-type-super (sub (func (param i32)))) - ;; CHECK: (type $1 (func (param (ref $indirect-type-super)))) - ;; CHECK: (type $indirect-type-sub (sub $indirect-type-super (func (param i32)))) (type $indirect-type-sub (sub $indirect-type-super (func (param i32)))) + ;; CHECK: (type $2 (func (param (ref $indirect-type-super)))) + ;; CHECK: (global $g1 (mut i32) (i32.const 0)) (global $g1 (mut i32) (i32.const 0)) ;; CHECK: (global $g2 (mut i32) (i32.const 0)) @@ -42,18 +42,7 @@ (global.set $g2 (local.get $i32)) ) - ;; CHECK: (func $caller (type $1) (param $ref (ref $indirect-type-super)) - ;; CHECK-NEXT: (call_ref $indirect-type-super - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $caller (param $ref (ref $indirect-type-super)) - ;; This inherits effects from $impl1 and $impl2, so may mutate $g1 and $g2. - (call_ref $indirect-type-super (i32.const 1) (local.get $ref)) - ) - - ;; CHECK: (func $merges-multiple-effects (type $1) (param $ref (ref $indirect-type-super)) + ;; CHECK: (func $merges-multiple-effects (type $2) (param $ref (ref $indirect-type-super)) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (local $y i32) ;; CHECK-NEXT: (local $z i32) @@ -64,7 +53,8 @@ ;; CHECK-NEXT: (global.get $g2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (nop) - ;; CHECK-NEXT: (call $caller + ;; CHECK-NEXT: (call_ref $indirect-type-super + ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (local.get $ref) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop @@ -89,7 +79,7 @@ ;; This acts as a barrier for $x and $y, but not $z because ;; $ref may write to $g1 (via $impl1) or $g2 (via $impl2) but not $g3. ;; $z is optimized out and $x and $y are left alone. - (call $caller (local.get $ref)) + (call_ref $indirect-type-super (i32.const 1) (local.get $ref)) (drop (local.get $x)) (drop (local.get $y)) From 04adfce58972427b08972ae954449bb514b32766 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Wed, 27 May 2026 10:15:58 -0700 Subject: [PATCH 157/168] [wasm-ctor-eval] Do not error on non-constant code in the start function (#8778) Before, we did not catch the Nonconstant exception. --- src/tools/wasm-ctor-eval.cpp | 7 ++++- test/lit/ctor-eval/start-nonconstant.wast | 38 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 test/lit/ctor-eval/start-nonconstant.wast diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index 0dbeb57772a..b503b0a818a 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -1485,11 +1485,16 @@ void evalCtors(Module& wasm, } } } catch (FailToEvalException& fail) { - // that's it, we failed to even create the instance + // That's it, we failed to even create the instance. if (!quiet) { std::cout << " ...stopping since could not create module instance: " << fail.why << "\n"; } + } catch (NonconstantException& fail) { + // We can also fail during start due to a non-constant operation. + if (!quiet) { + std::cout << " ...stopping since non-constant in start\n"; + } } catch (TopologicalSort::CycleException e) { // We use a topological sort for GC globals. If there is a non-breakable // cycle there, we will hit an error (we can break cycles in nullable and diff --git a/test/lit/ctor-eval/start-nonconstant.wast b/test/lit/ctor-eval/start-nonconstant.wast new file mode 100644 index 00000000000..4d6baa668ab --- /dev/null +++ b/test/lit/ctor-eval/start-nonconstant.wast @@ -0,0 +1,38 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-ctor-eval --ctors=test --kept-exports=test --quiet -all -S -o - | filecheck %s + +;; A non-constant (relaxed SIMD) operation in the start function. We should not +;; error. +(module + ;; CHECK: (type $0 (func (param v128))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (import "a" "b" (func $import (type $0) (param v128))) + (import "a" "b" (func $import (param v128))) + + ;; CHECK: (export "test" (func $0)) + (export "test" (func $0)) + + ;; CHECK: (start $0) + (start $0) + + ;; CHECK: (func $0 (type $1) + ;; CHECK-NEXT: (call $import + ;; CHECK-NEXT: (f32x4.relaxed_min + ;; CHECK-NEXT: (v128.const i32x4 0x00000000 0x00000000 0x00000000 0x00000000) + ;; CHECK-NEXT: (v128.const i32x4 0x00000000 0x00000000 0x00000000 0x00000000) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $0 + ;; The import avoids vacuum from removing the entire body. + (call $import + (f32x4.relaxed_min + (v128.const i32x4 0x00000000 0x00000000 0x00000000 0x00000000) + (v128.const i32x4 0x00000000 0x00000000 0x00000000 0x00000000) + ) + ) + ) +) + From ac6c001b75f7ace237e94dcd522cdc6462e768e8 Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Wed, 27 May 2026 13:16:54 -0700 Subject: [PATCH 158/168] Fuzz wide arithmetic instructions (#8661) Part of #8544. Continued in #8781. Drive-by fix: ensure that tuples aren't generated with a size larger than TUPLE_MAX_SIZE: ```diff - size_t maxElements = 2 + upTo(fuzzParams->MAX_TUPLE_SIZE - 1); + size_t maxElements = 2 + upTo(fuzzParams->MAX_TUPLE_SIZE - 2); ``` After increasing the seed file size, the wide arithmetic instructions [were generated](https://github.com/WebAssembly/binaryen/actions/runs/26316945566/job/77478064626) (ctrl + f "wideint"), but fails in CI due to #8770. For now I leave the seed file unchanged without showing that the wide arithmetic instructions are generated in the golden file. Also generated a new seed for test/lit/fuzz-import.wast since it was failing after these changes, seemingly due to bad luck (which the test file mentions is a possibility). --- src/tools/fuzzing.h | 3 + src/tools/fuzzing/fuzzing.cpp | 39 +++++++- src/tools/fuzzing/heap-types.cpp | 2 +- src/tools/fuzzing/parameters.cpp | 2 +- test/lit/fuzz-import.wast.dat | Bin 4023 -> 4096 bytes ...e-to-fuzz_all-features_metrics_noprint.txt | 89 +++++++++--------- 6 files changed, 86 insertions(+), 49 deletions(-) diff --git a/src/tools/fuzzing.h b/src/tools/fuzzing.h index fa819e20772..da3daced9dd 100644 --- a/src/tools/fuzzing.h +++ b/src/tools/fuzzing.h @@ -477,6 +477,9 @@ class TranslateToFuzzReader { Expression* makeGlobalGet(Type type); Expression* makeGlobalSet(Type type); Expression* makeTupleMake(Type type); + Expression* makeWideIntAddSub(Type type); + Expression* makeWideIntMul(Type type); + Expression* makeWideIntExpression(Type type); Expression* makeTupleExtract(Type type); Expression* makePointer(); Expression* makeNonAtomicLoad(Type type); diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index 69e97fa5aa2..fa5772a221f 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -2812,7 +2812,11 @@ Expression* TranslateToFuzzReader::_makeConcrete(Type type) { &Self::makeStringGet); } if (type.isTuple()) { - options.add(FeatureSet::Multivalue, &Self::makeTupleMake); + if (type == Types::getI64Pair() && oneIn(2)) { + options.add(FeatureSet::WideArithmetic, &Self::makeWideIntExpression); + } else { + options.add(FeatureSet::Multivalue, &Self::makeTupleMake); + } } if (type.isRef()) { auto heapType = type.getHeapType(); @@ -3495,6 +3499,30 @@ Expression* TranslateToFuzzReader::makeTupleMake(Type type) { return builder.makeTupleMake(std::move(elements)); } +Expression* TranslateToFuzzReader::makeWideIntAddSub(Type type) { + assert(wasm.features.hasWideArithmetic()); + assert(type == Types::getI64Pair()); + auto op = oneIn(2) ? AddInt128 : SubInt128; + auto* leftLow = make(Type::i64); + auto* leftHigh = make(Type::i64); + auto* rightLow = make(Type::i64); + auto* rightHigh = make(Type::i64); + return builder.makeWideIntAddSub(op, leftLow, leftHigh, rightLow, rightHigh); +} + +Expression* TranslateToFuzzReader::makeWideIntMul(Type type) { + assert(wasm.features.hasWideArithmetic()); + assert(type == Types::getI64Pair()); + auto op = oneIn(2) ? MulWideSInt64 : MulWideUInt64; + auto* left = make(Type::i64); + auto* right = make(Type::i64); + return builder.makeWideIntMul(op, left, right); +} + +Expression* TranslateToFuzzReader::makeWideIntExpression(Type type) { + return oneIn(2) ? makeWideIntAddSub(type) : makeWideIntMul(type); +} + Expression* TranslateToFuzzReader::makeTupleExtract(Type type) { // Tuples can require locals in binary format conversions. if (!type.isDefaultable()) { @@ -6425,9 +6453,14 @@ Type TranslateToFuzzReader::getMVPType() { } Type TranslateToFuzzReader::getTupleType() { + // Give a significant chance to an i64 pair, for wide arithmetic. + if (wasm.features.hasWideArithmetic() && oneIn(5)) { + return Types::getI64Pair(); + } + std::vector elements; - size_t maxElements = 2 + upTo(fuzzParams->MAX_TUPLE_SIZE - 1); - for (size_t i = 0; i < maxElements; ++i) { + size_t numElements = 2 + upTo(fuzzParams->MAX_TUPLE_SIZE - 2); + for (size_t i = 0; i < numElements; ++i) { auto type = getSingleConcreteType(); // Don't add a non-defaultable type into a tuple, as currently we can't // spill them into locals (that would require a "let"). diff --git a/src/tools/fuzzing/heap-types.cpp b/src/tools/fuzzing/heap-types.cpp index e2b6b552623..41d3a1cf084 100644 --- a/src/tools/fuzzing/heap-types.cpp +++ b/src/tools/fuzzing/heap-types.cpp @@ -422,7 +422,7 @@ struct HeapTypeGeneratorImpl { } Type generateTupleType(Shareability share) { - std::vector types(2 + rand.upTo(params.MAX_TUPLE_SIZE - 1)); + std::vector types(2 + rand.upTo(params.MAX_TUPLE_SIZE - 2)); for (auto& type : types) { type = generateSingleType(share); } diff --git a/src/tools/fuzzing/parameters.cpp b/src/tools/fuzzing/parameters.cpp index 3220f9625d3..423cad941d5 100644 --- a/src/tools/fuzzing/parameters.cpp +++ b/src/tools/fuzzing/parameters.cpp @@ -26,7 +26,7 @@ void FuzzParams::setDefaults() { MAX_GLOBALS = 30; - MAX_TUPLE_SIZE = 6; + MAX_TUPLE_SIZE = 7; MAX_STRUCT_SIZE = 6; diff --git a/test/lit/fuzz-import.wast.dat b/test/lit/fuzz-import.wast.dat index 922d620d004e4070ddff5e69b942e5153ab4a22e..172f50c7db9d70c54e517469f8f9221435690b68 100644 GIT binary patch literal 4096 zcmV+b5dZJ%nC7)wvU*7rC0jLP#w8Kwdb{JA{k|&yf*O#ZoNOBQ0HaOq~7pxRQd!a29 zGGSMl$6qK@PQa)vZ50fGUoLPx(&d!Rlj&kSBKQ*$a1A%rmcj{%h!>nPCK#=tX7i$I zaK3qe&J|xhsV2Kl9T*eO5eP8#Ad6^!M&zG982b@4JANQ+{UJlHn>d#xEzbRVq4vZT z3l=6VnSqL->ts^@}kcb7N4l_Ou&OejOailcU~J26Dy*|)O<=RI_qhM)g_pTwHcoCnLZ8EI%* zOJ{&Dw)(R9r&;XxS%hU*e#sXE@z)ULid$axkR4qoz)}|BOY#^E3IJLsIr&`P3flU zAg;#dypUxMy$kxt#Qbj9F|)t&XZNHBSmSLvv>vY@U*=D?M)T15*B`S{&V zYdqG32n8(a4|V{Qg}!a4OOL#A>~!ih5+QRFG@Y_KrGFNfHAt_bPd%gF=}C`z^^4N* zNOhNNYDh{D4wXs`?vNWRIR!1MgAx^6;8>9hu>tpP#HqXBJo)&#B;QD(j0Us_FM_OG zT@%&Zcc^A)7p(fAIH>n>?{%ybK-2;%aECFjoK6iYA7YW1sD&Ta(9?s-kAXwj=|73H zDV*FN*$-wr4;1WWyrr};ih&7syr+8Zyr+-H{A0hWHTis@-+Ja53;;!q7D}FoC+5O+v(CZ zKsWVLopQ|kj>;{Vp6}qgM}k(P^Ea_oOfjyy_Z6TfY{%3c3-T${JM=f-Owzpz&#?pe z1ngzZR3oeFp}7#OtQWI*MRH4fBRV7dJ|w8A@BDG`!O2YU<1V%VSeFt(?KFO+-;WX6 zyxX|jYnX?}0*Z2Z+!&y(kjthc%-H4{&_BVUghM-e7a)^ASU%p_pD9#GBW3yKTor{$ zSQmaiZsG2w#rk<=k% zb)^H^RdFnNzvytOvs|p!uwnk<5ta-)wAx-oKk_!j9Gg3-&5+Kbua7bFJb}SF?8#Wb zG2i}PqKExJripE~v%A)H-jL|ikp-`4^zuiWSBI5SCHXZMKkpI&=>vF$O!|$*+Q?!o zu%0d=FYT%QoHpc%CW2R~d<%u3n(gy&JSM)mH{`he^1bbKLw@f`17fwTu5t;p?dD(7 z_?dyFKc;vaXxyx2i_^to)SziOoiViknpM3XBN%Uk5b8juSLA;{81)2K+E_rt%}<(aaI3_f3@1%TU+&CNOCCtnf*jqD0ZTkIK$1$T8oG_aMNA6Y~OuoWUgeD z0483r0K@a!m{t$(MHk8Hj;=cs$Zi!?`9T9W(Pz{Y&XW!raYST5dNl(ecZi@~M#Ik9dueD=nSRhc%Ep|Z_j1L?a!?Ldc@B=Nvd_R9OHp4Iv zvyejeLSsJS{pKyE>Tc}b)-aBCl$m%F^KLsG6%m26s)M$8Ki=d1_=nr@g5XB zWs0ptjz=eLqoFrS1%W2=GX#%+>t4+Fvt_GliMd>ylOu&+Fe zLJ&a>xoie8*Ql8s3B7xM(S+gxy{t6$>LJ5~ztfwdqA(Hzm}qevdD-w5w#hH46Z0By ziq|_tK=LPI!3gcI)=IZ1BuEias5msqO#ZPTKH(u3YpIN)S>}u?3rADH#n>+`h z)xK$T2zRnkNG3b%8hqU%;s_lrljZ+WU#uGQ)V5oQP^%-YL5x2Li&QeLzj}(FWp2ff zWdx(>kHFC(zF;EBo#v<$d?R*pIw*Mya>93?bP;BbT$P_FyW1gh$y{knJWwepEoz#S zI32D?Au3}@Y>BjBUf|ttkt|L!zpKzZ$mUo>DHcsII=QX^9a<#?!NOiZa4(1)fIKl6 zh^aVuXE9sg=~L=Gwsd@SSR~3bJ49eJaiF_nS89;v9)}p;21%A*B9!QMZjY7+=Ew{G zook{`DU8rJV0xzNDUT5OJ?X6mHOtYuV#5k;O&A~+e;KJJxvv@-+hO{1hsxJLXV(vn zNXQaw%A!=vd9igo3S-?5zu3@ZduZUCI48<|pdMdq_OBWG^b!SQKxI|dJYMJoc=op~ z>>~^d3<*_s>R;y8Mfc2~CV7&>DP$FK<*`o#9-hGO@hbj1m#7ZhH52U(P)goFjgVuS zHl0BYeixw>#(2=3n8DCld^p$}mVzg2x=9|dC7j#ya|0d*ITrCUeF2G?&|&JKDh+TV z!go4uKNBMBN?fd^VIlOzz`D&c`iSlq%&Q@%=SS|Y_r)loZ&n@TwKk7c0k|2F zi@g6|qd+KhJF$)llC9kmDK@8q)j@FBgXy3|xK9w(^wA_5dmc8*Je4#oYJr7BlvSYk z(CKSEO=NrYAVjyl$6gtqQRORrxwfsUVmnee>;1d+1@=mb12U^YX4ue8W~K3ISj;Zo zgYY&(#Yg%GQmL7`am}j9u2xbWv^Ah7T6$ zjv*nI)-B2Jf9EG?K=Z77>AFf7B|bBv*&Q3DmWD6I!^@~LSHs-HyBrbipPK=dP}`bd z$)+>q0DzHND^*!&H@gtihiA1@5{VlgTbBMx3t#c_h$p(Z&L;`qs_jW@XgLB@J zXbWSeCd&3@Y5Zj#(Y>+LptAr)^_9t9_af0^`OqGFY~x zI1$V?b#FW zA0ohi+P%U%8h1TD3$p(0nq7*gzHJX&t-!|r_?pee;FOF#7y{tnB)bODlafW;{))Il zA!RCo5yYv-rZ4=3=Mpwt)8Oewjs_)~%78R1?&z)!KR7zp#?u&vG0#@vn&=C$YoD#0 zRDXYJlTgBg2j;{z3rT*bqp5I%va#mxHdDi!30}th2s*f$wy16Uk|ms!M`EyvMbvEJ z1k>~SL@auw9_ydt7 zAQU3#^c15mUvACjmmMWQ_yR^M@S^I^TEPvU_!Ta|3G--_COx8)Gijem33%whBVj@v ze{PkGI%&wNGRV)~yUDP{naELutMQ>$onTzUs^>4Q27Km0_F&n_#UNfsU~oO(2{+|C zKIwVh&0}Q6NF!Z=Gi_QUJ*#?Ivn=2alYIrO_3GpK7rBjD*o~{Rhfh#*^~X=_2HsCg z_v3QFVw9=0+-)1uUXZrza6@!4L;q5PVu5P;60eG3Clo(}e+Dk9 z*)KLp>zY&*nTz$zDm#SfI9JOo8lUHvdC7=}o=Mm6e zaYumdyQVarD&x3 z@y>_X%+6adFq%rqz)J6oIB@N(6P5j%F^RxjxGPL74dinG8ae?&Cj=TMYiAbv7aU`1Xn#)37G{~MB}dAf_2N|mHifTD(*IvsPZceGZU3xym$ z)e=wZf8gXki!YsnENp!*1NU`pSBGQ|gvSx2cGb_yB)v}9u80-mb%FnRe`|kBe;3Qq zurja2r=o82iM7NOgRMPYwMx#qU0tEf(Kj7{_1}!i7hISX$u!C*b$GX1#w(}Ls_IqM zeh?4VS;q9xAI~z%hmMqG(VaUl^zOKVhA65QZ$GiUT9D!?Zi>>L=pA9zpIHSmDG}Q{ z#r>`MC#@hF^%M3hj%px5rrdo=dtJSjd9LP<8qM}d(D>BA!-fRk!2`!T6De&4m7 zBbWX{UB9ASB&6kk$mwjU!Eb(SfGIcHMFFeKM5$Cp@m#zR=X`@Lr2{VejE&S{yCc0& zr1`7BDO6zhl}Z?6p+aVD$2tMpKO$=aqtPH~^DIxD-^_`^6I71w%ml6;WstVimD-*V zI$&*;uf6zX)9qhb{`T=odI`Z*#)&_xN4wYP^jaEhqj^!8%bDdQ0Cu%K5_DWS+)i|n zka-@I#`bNB@;LY^7J!AfcF6!&EM;EwzpA2AZpmM;e`Ag3I@q=m1N+uue1R{<(;7Ie z5+hot#@7!g?&ns(9fvB;npu({f;i#r1rTYqtoSg>CO|;UzNdYq+JGCAd26xosDB&5 yB;?9Pi>T{0Hyv>vXrQ$~TEA32jQ3--yuoE?cK+0$kp8 literal 4023 zcmV;o4@mI6@*xKBgm+@_^UFQg-h><@a_V+EYBGOF?mfF-j78k*(j(8HD11C2SWzdy|PZh18xJJRi>%}yR+{W{w6u=;3m*!-fa#c{TDo4Ky@#2>H4Q_aafy1TU*0h^q^fe@Rw%4* z=8AJY5n-WSj;<@PLf4gw?XlIA*3lBvPi6x2-f@-jTNS_S!WP^czW$-t*6RsujSOk2I^KQ~CJ*O_1H_MciKHeATJGl+`B z8$}iMihi}K&gCPt{y9yGGdVe4VAaS$O3PzcTW5g1mJj35#G#)4nfDK20XKL1SnFkD zY!c%+kxoVLEg22CcPIY|t)-~2L@5y{uW2A=-*Nb%HxU!h@*>!3`kM>w>doL> z=@Wyyz2&*hasg|Ey-Dk1QELlV&Q=abI9B5OULFh1<)Y9K8hXwY%lr*+doaiHVN|L+ zu|FD=jP9BGe~1sPu%a&=-Yo$&4=xE;680Nlsb=)9^|KMfoXISfUeh|qtIk=)w_dr_ z;C18nu!&a2`4vuA3-2#6qj?+{=I*M!03>6~HLagG<|%7?7*0tAIdzk2aMq(>?^-Ao zISeyqf-5)f%yKvQvCiZ;mE*!`I|J>RC$6z#_$0d6B5i-OAFFuW;46S{rBrEe?bf_u z(f82s5tRvEF%C_ZAMq4pYK{Bd6R_`OlSwq=nbPAYX-@f(z`j!4pY8H-xRa6(?Z3B} zd<6$XObS#P0BG*Dxe@wadIpW%tr{@qpN4+_)`hJW58D$|enr!)M2`qo7YO(s>+jUP zSC(BRM>Np1BMH2TbWg&h-X3@9no7l2T(rFzl<;K z;igJ&rRl$-x3I2~n2KFLajt)m-iYTy^s8Fp8Y9)&l^%J+#B3(JZ5;eO+{d{FnZCWu z;J!8eN~LfBy&wQbDQl$OkYo=IpyRr2;jKNvbMr@?l3_epE8GsV!vp$W@{C8vXx*6K zk})bE;-n!2ruEn`sn&C)XUHuY4SFkFcJRuPu<3MXn6E^-wV`O_9^Ic~{BB+~={?JA zpLu$T*7Pn_SQEw&q+mlQ?tWtzVEGR}W11iYr;)YrQBaqZr0}!mI zxil7R#s%Z2a^dHB0t5%uMVZ0 z_Js0-X}2+k_1E-gtQ~q7&3naMqcU6Pg51}x!Q2$O$-2mka7J`w57RZx^z^~qX7b7% zQVU}-M#?)rHEotb*xpJ|ZrJ{z;tqlducF0mK9FTAX~%qVs&y}_n{H~$0GN78jwFDv zXp~Q|Ze}~^+JGF&hm&UJXrHnzHrLFsd4NAY3|OPER&u8pI-(-btCR});^0>I06kZ| z^^U`3>%j84y9KEA{pSVoC7fS(I#^kSpeVTlajFL!!c;crviJn+-YGXOD`z#2Dyj}^ z8Q~K@ZKva#qz7ULZ{9(&|d!m)BuveG9C0@mw>CfD5GqVG_;T-CK z{rh5=EG78j%2a2DM1}xcW_jp5l)Pdy0H)!Z`WB;>TkHOx^RIg#XA-(4u>)HZ@=&KR zSGs!h9|d)zNRofDvcOE}+s_^C&I>7s)||RO1Y9uC7RuOvC*Bo@yk6U~(+YRCcf`H5 zlTmcK0DVyShGRzZ;#ALq!^kgb!8U8TwuGC(-cGP4lBAAfV#{rAcfW?^empn{(-5DZ zwm?IJSn|b%dQ$Z&W3>kP9@pJF$f44jwaq8(V1EGOJs94X0=u< zR?6uMHKl69)NwZ#Eh3v_UE^LXvnBXWNO^Pkj?W)v=uTQgethK=08F#X5M_R4pg+4_SGos_^TTCpWhr3b&yWB3pz#4Uq5y*TflsuTQjS zBBYtZPsf8QZTc?9oFt)sV{ystlu1qjdsyhc)XU(S&%&E1p*ASR*oEzwF?Q(pQ#Zo> zY~Yv*la(b#U=SE3aC!b1y>ho9&b`%OPSl zC3}FOXINJ_Ph_v9c_vuQ1$~d67{2Jes++9gGOc5U_jBZ&WxF=>JN{j*Oc|;li~)F? zy@gxjK}?~#Zl0vsDb;sBf;UD8`QU`4kZO|5CkQ;+ElN`7dyA;s9m#qHz>JWBP_zkg zC|E#lCe;Mu(eipPr-ZQ$YEmkA}|Gcq~9`h$yYG4%G6{0jtNdfB_=O`(&&WL1X%(k>)i$EI`2 z6BLAe7pgZVk*sX-Ht>okW)x9w%Oo@p#o!H$^KweL;-~41ywJ!R%;_<_K?{jzTr7u2 z_k4*zR*_f=micGN3h`xx=qKmEm#+J7zB_a(Eg@;$GH!ZXR-YzYkef{ByciThemKUn{@ zz2D-(yRKCe!SS2MCRXJdCRdhmNR>*QUy-=BA*fZ_p^g#o*Xx%yTNBe$`OlU-aXxje zD9F@;Ok)+4|2O)C9dJk~+Ypq5kVm2mb47ps-yTi0>n+?c=zn*LY%m2>wj=?g;4xZE zeVj?ak!M6?A2-u`A^lBv=0h`^-WLoE7ks}DgabVN;W~ZK8C%UQ@flx%h)>$~Nc4w;UVh~2L%=40grFxRkjf&+?e(hQ>LhBI-4k_+^rcHEH<@a>);%HLNT=}h(dtsP;eJ-c1le+qzQdWO-sI|F(N%Y0K5ZByKN z-l8V}I&+SFAVMG7Wp}pM8~`GF5+?slj6T%;54I@sFE^qCq&TocYm}O5*Ql(e@qRo{ z&?#)a5M65(dy79oShv(Z!{o%Kl_u_iwgpVyA~rmO#H#&oB^e7_50+0V*(03M;s2o( z<(gFt6EKn1T3ouXmQbqOwLbbttOK+8ZIst`=vFY3??67s7)j7R>m&dur(hkJvcTKd zF*p=s&5}gkl}I6dsahWmVFgix$#tf#KD3Tp5BAK#{2-ppr6)r29F_~roxih<*CzLc zjei22ph1{rM!+6}1n#Q9jlR#;)aR|yb2G(W;C4lBO#M7A`@sN@bn{EvjH6J3ND7RG zJ9ea!yYqM$V8@K0I7jvQCBn$_dP`=talWWY7N3k=P#6S`rLS^uCZeLi+LS^vx)TVU zng!v@m%DJ#ddzlkg5aITD{iU>^|G;lc9I;sA@Z$exj{2?>w26a#k}q4cd`qUStxX^ zVtN6>W-$qcZ7Apx1we+MP{Xw>pqgoBVDZ?{&2}M5;t$D9^tD7XaMC?uqZg}FbOk7c z`K86}+V`8bd0;T#NAK;4)hU>YHeicZ_OGA;a857BI!zzG27_}HotR%{(h~QwdeS(- zCZC3Js;ml2FtRA`;#;Kml|?^T20NsvU!&92eIKatO%TdW1LC&N4S(S2d@CmgV^pi3 zbdS5f+FFNm3)&1N322sZaJMf02pk5YMXXPCy+KVykAaZ|V~Sj;l>uxvLfPux5!h?D z3QU^No122J|CuTxMx<=G8PIZ7M=+Xoqn4?3WCaMa!!F)!G9H3i-2(nLtPMv6#~jaT zeD_86?H2TDE5=UTq1qpUuxiH;tqfc1!o`)Ah@LGb(ecqOy~9G&%f)L4%p=G;?WL1o z8T%s6_?He*{c!1Jm~~P9zj2Ym#RH{5%~J;H+8=Y*Ff*uyIn8uUp&z`qJF0yO+ Date: Wed, 27 May 2026 15:47:05 -0700 Subject: [PATCH 159/168] Enable fuzzing for wide arithmetic (#8781) Part of #8544. The V8 implementation is ready now under an experimental flag. --- scripts/bundle_clusterfuzz.py | 1 - scripts/clusterfuzz/run.py | 3 +-- scripts/fuzz_opt.py | 1 - scripts/test/shared.py | 1 + 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/bundle_clusterfuzz.py b/scripts/bundle_clusterfuzz.py index 86ea91a11d5..648c3f33a8e 100755 --- a/scripts/bundle_clusterfuzz.py +++ b/scripts/bundle_clusterfuzz.py @@ -110,7 +110,6 @@ '--disable-strings', '--disable-stack-switching', '--disable-multibyte', - '--disable-wide-arithmetic', ] with tarfile.open(output_file, "w:gz") as tar: diff --git a/scripts/clusterfuzz/run.py b/scripts/clusterfuzz/run.py index 7fe9979b3d2..b98c5658c50 100755 --- a/scripts/clusterfuzz/run.py +++ b/scripts/clusterfuzz/run.py @@ -33,7 +33,7 @@ # The V8 flags we put in the "fuzzer flags" files, which tell ClusterFuzz how to # run V8. By default we apply all staging flags. -FUZZER_FLAGS = '--wasm-staging --experimental-wasm-custom-descriptors --experimental-wasm-js-interop --experimental-wasm-acquire-release' +FUZZER_FLAGS = '--wasm-staging --experimental-wasm-custom-descriptors --experimental-wasm-js-interop --experimental-wasm-acquire-release --experimental-wasm-wide-arithmetic' # Optional V8 flags to add to FUZZER_FLAGS, some of the time. OPTIONAL_FUZZER_FLAGS = [ @@ -94,7 +94,6 @@ '--disable-fp16', '--disable-strings', '--disable-stack-switching', - '--disable-wide-arithmetic', ] diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 93b609dbe92..8bc451b962c 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -76,7 +76,6 @@ 'strings', 'stack-switching', 'multibyte', - 'wide-arithmetic', ] diff --git a/scripts/test/shared.py b/scripts/test/shared.py index d8211d8143a..5e3cd208270 100644 --- a/scripts/test/shared.py +++ b/scripts/test/shared.py @@ -267,6 +267,7 @@ def has_shell_timeout(): '--experimental-wasm-custom-descriptors', '--experimental-wasm-js-interop', '--experimental-wasm-acquire-release', + '--experimental-wasm-wide-arithmetic', ] # external tools From f499c8468541ac0a693898fa5a21b81d49e39ce4 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 28 May 2026 11:24:13 -0700 Subject: [PATCH 160/168] Add isActive/isPassive methods to segments (NFC) (#8782) Currently `DataSegment` class has `isPassive` member variable but `ElementSegment` doesn't. `DataSegment`'s `isPassive` variable is redundant anyway because we can test whether its `memory` is set, as we do for `ElementSegment`'s `table`. This removes `DataSegment::isPassive` variable, and instead adds `isPassive()` and `isActive()` methods to both classes and use them instead of things like `if (segment->table)`. This also removes `isPassive` parameter from `makeDataSegment` in `Builder`. --- src/binaryen-c.cpp | 30 +++++++++++------------ src/ir/memory-utils.cpp | 2 +- src/ir/memory-utils.h | 2 +- src/ir/module-splitting.cpp | 4 +-- src/ir/module-utils.cpp | 5 ++-- src/ir/module-utils.h | 6 ++--- src/parser/context-decls.cpp | 1 - src/parser/context-defs.cpp | 3 +-- src/passes/LLVMMemoryCopyFillLowering.cpp | 4 +-- src/passes/Memory64Lowering.cpp | 4 +-- src/passes/MemoryPacking.cpp | 21 ++++++++-------- src/passes/PostEmscripten.cpp | 2 +- src/passes/Print.cpp | 6 ++--- src/passes/RemoveUnusedModuleElements.cpp | 6 ++--- src/passes/SeparateDataSegments.cpp | 2 +- src/tools/fuzzing/fuzzing.cpp | 8 +++--- src/wasm-builder.h | 4 +-- src/wasm-interpreter.h | 2 +- src/wasm-traversal.h | 4 +-- src/wasm.h | 7 +++++- src/wasm/wasm-binary.cpp | 16 ++++++------ src/wasm/wasm-emscripten.cpp | 2 +- src/wasm/wasm-validator.cpp | 5 ++-- src/wasm2js.h | 8 +++--- 24 files changed, 75 insertions(+), 79 deletions(-) diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index 7c8dcab97be..a0f613aa5a2 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -5557,7 +5557,7 @@ BinaryenIndex BinaryenGetNumElementSegments(BinaryenModuleRef module) { } BinaryenExpressionRef BinaryenElementSegmentGetOffset(BinaryenElementSegmentRef elem) { - if (((ElementSegment*)elem)->table.isNull()) { + if (((ElementSegment*)elem)->isPassive()) { Fatal() << "elem segment is passive."; } return ((ElementSegment*)elem)->offset; @@ -5611,12 +5611,12 @@ void BinaryenSetMemory(BinaryenModuleRef module, for (BinaryenIndex i = 0; i < numSegments; i++) { auto explicitName = segmentNames && segmentNames[i]; auto name = explicitName ? Name(segmentNames[i]) : Name::fromInt(i); - auto curr = Builder::makeDataSegment(name, - memory->name, - segmentPassives[i], - (Expression*)segmentOffsets[i], - segmentDatas[i], - segmentSizes[i]); + auto curr = + Builder::makeDataSegment(name, + segmentPassives[i] ? Name() : memory->name, + (Expression*)segmentOffsets[i], + segmentDatas[i], + segmentSizes[i]); curr->hasExplicitName = explicitName; ((Module*)module)->addDataSegment(std::move(curr)); } @@ -5766,7 +5766,7 @@ size_t BinaryenGetDataSegmentByteLength(BinaryenDataSegmentRef segment) { return ((DataSegment*)segment)->data.size(); } bool BinaryenGetDataSegmentPassive(BinaryenDataSegmentRef segment) { - return ((DataSegment*)segment)->isPassive; + return ((DataSegment*)segment)->isPassive(); } void BinaryenCopyDataSegmentData(BinaryenDataSegmentRef segment, char* buffer) { std::copy(((DataSegment*)segment)->data.cbegin(), @@ -5783,12 +5783,12 @@ void BinaryenAddDataSegment(BinaryenModuleRef module, auto* wasm = (Module*)module; auto name = segmentName ? Name(segmentName) : Name::fromInt(wasm->dataSegments.size()); - auto curr = Builder::makeDataSegment(name, - memoryName ? memoryName : "0", - segmentPassive, - (Expression*)segmentOffset, - segmentData, - segmentSize); + auto curr = Builder::makeDataSegment( + name, + segmentPassive ? Name() : (memoryName ? memoryName : "0"), + (Expression*)segmentOffset, + segmentData, + segmentSize); curr->hasExplicitName = segmentName ? true : false; wasm->addDataSegment(std::move(curr)); } @@ -6333,7 +6333,7 @@ void BinaryenElementSegmentSetTable(BinaryenElementSegmentRef elem, ((ElementSegment*)elem)->table = table; } bool BinaryenElementSegmentIsPassive(BinaryenElementSegmentRef elem) { - return ((ElementSegment*)elem)->table.isNull(); + return ((ElementSegment*)elem)->isPassive(); } // diff --git a/src/ir/memory-utils.cpp b/src/ir/memory-utils.cpp index 4bd629e2cbc..6c6a75ebbcb 100644 --- a/src/ir/memory-utils.cpp +++ b/src/ir/memory-utils.cpp @@ -94,7 +94,7 @@ bool flatten(Module& wasm) { std::vector data; for (auto& segment : dataSegments) { - if (segment->isPassive) { + if (segment->isPassive()) { return false; } auto* offset = segment->offset->dynCast(); diff --git a/src/ir/memory-utils.h b/src/ir/memory-utils.h index 2929d17ca9f..db9ff2bcba8 100644 --- a/src/ir/memory-utils.h +++ b/src/ir/memory-utils.h @@ -81,7 +81,7 @@ ensureLimitedSegments(Module& module, numDynamic++; } } - hasPassiveSegments |= segment->isPassive; + hasPassiveSegments |= segment->isPassive(); } if (hasPassiveSegments) { diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 78141676375..95abc3306da 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -743,12 +743,12 @@ void ModuleSplitter::shareImportableItems() { // module, it can't. walkSegments(collector, &module); for (auto& segment : module.dataSegments) { - if (segment->memory.is()) { + if (segment->isActive()) { used.memories.insert(segment->memory); } } for (auto& segment : module.elementSegments) { - if (segment->table.is()) { + if (segment->isActive()) { used.tables.insert(segment->table); } } diff --git a/src/ir/module-utils.cpp b/src/ir/module-utils.cpp index 4d4a565a7ce..b17d6c772b9 100644 --- a/src/ir/module-utils.cpp +++ b/src/ir/module-utils.cpp @@ -146,7 +146,7 @@ ElementSegment* copyElementSegment(const ElementSegment* segment, Module& out) { return out.addElementSegment(std::move(ret)); }; - if (segment->table.isNull()) { + if (segment->isPassive()) { return copy(std::make_unique()); } else { auto offset = ExpressionManipulator::copy(segment->offset, out); @@ -188,8 +188,7 @@ DataSegment* copyDataSegment(const DataSegment* segment, Module& out) { ret->name = segment->name; ret->hasExplicitName = segment->hasExplicitName; ret->memory = segment->memory; - ret->isPassive = segment->isPassive; - if (!segment->isPassive) { + if (segment->isActive()) { auto offset = ExpressionManipulator::copy(segment->offset, out); ret->offset = offset; } diff --git a/src/ir/module-utils.h b/src/ir/module-utils.h index 09b7900d3c9..860672beef8 100644 --- a/src/ir/module-utils.h +++ b/src/ir/module-utils.h @@ -95,7 +95,7 @@ template inline void iterDefinedMemories(Module& wasm, T visitor) { template inline void iterMemorySegments(Module& wasm, Name memory, T visitor) { for (auto& segment : wasm.dataSegments) { - if (!segment->isPassive && segment->memory == memory) { + if (segment->isActive() && segment->memory == memory) { visitor(segment.get()); } } @@ -104,7 +104,7 @@ inline void iterMemorySegments(Module& wasm, Name memory, T visitor) { template inline void iterActiveDataSegments(Module& wasm, T visitor) { for (auto& segment : wasm.dataSegments) { - if (!segment->isPassive) { + if (segment->isActive()) { visitor(segment.get()); } } @@ -142,7 +142,7 @@ inline void iterTableSegments(Module& wasm, Name table, T visitor) { template inline void iterActiveElementSegments(Module& wasm, T visitor) { for (auto& segment : wasm.elementSegments) { - if (segment->table.is()) { + if (segment->isActive()) { visitor(segment.get()); } } diff --git a/src/parser/context-decls.cpp b/src/parser/context-decls.cpp index 3afa960ee5a..0298f1cee9a 100644 --- a/src/parser/context-decls.cpp +++ b/src/parser/context-decls.cpp @@ -181,7 +181,6 @@ Result<> ParseDeclsCtx::addImplicitData(DataStringT&& data) { auto& mem = *wasm.memories.back(); auto d = std::make_unique(); d->memory = mem.name; - d->isPassive = false; d->offset = Builder(wasm).makeConstPtr(0, mem.addressType); d->data = std::move(data); d->name = Names::getValidDataSegmentName(wasm, "implicit-data"); diff --git a/src/parser/context-defs.cpp b/src/parser/context-defs.cpp index c69c428d5bc..d0d170ed2c5 100644 --- a/src/parser/context-defs.cpp +++ b/src/parser/context-defs.cpp @@ -108,7 +108,6 @@ Result<> ParseDefsCtx::addData( Name, Name* mem, std::optional offset, DataStringT, Index pos) { auto& d = wasm.dataSegments[index]; if (offset) { - d->isPassive = false; d->offset = *offset; if (mem) { d->memory = *mem; @@ -118,7 +117,7 @@ Result<> ParseDefsCtx::addData( return in.err(pos, "active data segment with no memory"); } } else { - d->isPassive = true; + d->memory = Name(); } return Ok{}; } diff --git a/src/passes/LLVMMemoryCopyFillLowering.cpp b/src/passes/LLVMMemoryCopyFillLowering.cpp index 9ccf2934a5a..e61e9891d16 100644 --- a/src/passes/LLVMMemoryCopyFillLowering.cpp +++ b/src/passes/LLVMMemoryCopyFillLowering.cpp @@ -59,13 +59,13 @@ struct LLVMMemoryCopyFillLowering // Check for the presence of any passive data or table segments. for (auto& segment : module->dataSegments) { - if (segment->isPassive) { + if (segment->isPassive()) { Fatal() << "memory.copy lowering should only be run on modules with " "no passive segments"; } } for (auto& segment : module->elementSegments) { - if (!segment->table.is()) { + if (segment->isPassive()) { Fatal() << "memory.copy lowering should only be run on modules with" " no passive segments"; } diff --git a/src/passes/Memory64Lowering.cpp b/src/passes/Memory64Lowering.cpp index a3877254df7..67409fc3ab5 100644 --- a/src/passes/Memory64Lowering.cpp +++ b/src/passes/Memory64Lowering.cpp @@ -192,7 +192,7 @@ struct Memory64Lowering : public WalkerPass> { auto& module = *getModule(); // passive segments don't have any offset to adjust - if (segment->isPassive || !module.getMemory(segment->memory)->is64()) { + if (segment->isPassive() || !module.getMemory(segment->memory)->is64()) { return; } @@ -300,7 +300,7 @@ struct Memory64Lowering : public WalkerPass> { auto& module = *getModule(); // Passive segments don't have any offset to update. - if (segment->table.isNull() || !module.getTable(segment->table)->is64()) { + if (segment->isPassive() || !module.getTable(segment->table)->is64()) { return; } diff --git a/src/passes/MemoryPacking.cpp b/src/passes/MemoryPacking.cpp index 83fb1b6e494..b43abac2787 100644 --- a/src/passes/MemoryPacking.cpp +++ b/src/passes/MemoryPacking.cpp @@ -215,7 +215,7 @@ bool MemoryPacking::canOptimize( // Check if it is ok for us to optimize. Address maxAddress = 0; for (auto& segment : dataSegments) { - if (!segment->isPassive) { + if (segment->isActive()) { auto* c = segment->offset->dynCast(); // If an active segment has a non-constant offset, then what gets written // cannot be known until runtime. That is, the active segments are written @@ -250,7 +250,7 @@ bool MemoryPacking::canOptimize( // TODO: optimize in the trampling case DisjointSpans space; for (auto& segment : dataSegments) { - if (!segment->isPassive) { + if (segment->isActive()) { auto* c = segment->offset->cast(); Address start = c->value.getUnsigned(); DisjointSpans::Span span{start, start + segment->data.size()}; @@ -283,7 +283,7 @@ bool MemoryPacking::canSplit(const std::unique_ptr& segment, for (auto* referrer : referrers) { if (auto* curr = referrer->dynCast()) { - if (segment->isPassive) { + if (segment->isPassive()) { // Do not try to split if there is a nonconstant offset or size if (!curr->offset->is() || !curr->size->is()) { return false; @@ -296,7 +296,7 @@ bool MemoryPacking::canSplit(const std::unique_ptr& segment, } // Active segments can only be split if they have constant offsets - return segment->isPassive || segment->offset->is(); + return segment->isPassive() || segment->offset->is(); } void MemoryPacking::calculateRanges(Module* module, @@ -351,7 +351,7 @@ void MemoryPacking::calculateRanges(Module* module, // entire segment and that all its arguments are constants. These assumptions // are true of all memory.inits generated by the tools. size_t threshold = 0; - if (segment->isPassive) { + if (segment->isPassive()) { // Passive segment metadata size threshold += 2; // Zeroes on the edge do not increase the number of segments or data.drops, @@ -450,7 +450,7 @@ void MemoryPacking::optimizeSegmentOps(Module* module) { void visitMemoryInit(MemoryInit* curr) { Builder builder(*getModule()); auto* segment = getModule()->getDataSegment(curr->segment); - size_t maxRuntimeSize = segment->isPassive ? segment->data.size() : 0; + size_t maxRuntimeSize = segment->isPassive() ? segment->data.size() : 0; bool mustNop = false; bool mustTrap = false; auto* offset = curr->offset->dynCast(); @@ -483,7 +483,7 @@ void MemoryPacking::optimizeSegmentOps(Module* module) { builder.makeDrop(curr->size), builder.makeUnreachable())); needsRefinalizing = true; - } else if (!segment->isPassive) { + } else if (segment->isActive()) { // trap if (dest > memory.size | offset | size) != 0 replaceCurrent(builder.makeIf( builder.makeBinary( @@ -494,7 +494,7 @@ void MemoryPacking::optimizeSegmentOps(Module* module) { } } void visitDataDrop(DataDrop* curr) { - if (!getModule()->getDataSegment(curr->segment)->isPassive) { + if (getModule()->getDataSegment(curr->segment)->isActive()) { ExpressionManipulator::nop(curr); } } @@ -569,7 +569,7 @@ void MemoryPacking::dropUnusedSegments( bool used = false; auto referrersIt = referrers.find(segments[i]->name); bool hasReferrers = referrersIt != referrers.end(); - if (segments[i]->isPassive) { + if (segments[i]->isPassive()) { if (hasReferrers) { for (auto* referrer : referrersIt->second) { if (!referrer->is()) { @@ -623,7 +623,7 @@ void MemoryPacking::createSplitSegments( continue; } Expression* offset = nullptr; - if (!segment->isPassive) { + if (segment->isActive()) { if (auto* c = segment->offset->dynCast()) { if (c->value.type == Type::i32) { offset = addStartAndOffset( @@ -663,7 +663,6 @@ void MemoryPacking::createSplitSegments( } auto curr = Builder::makeDataSegment(name, segment->memory, - segment->isPassive, offset, segment->data.data() + range.start, range.end - range.start); diff --git a/src/passes/PostEmscripten.cpp b/src/passes/PostEmscripten.cpp index 12ea129173d..533e5258ba6 100644 --- a/src/passes/PostEmscripten.cpp +++ b/src/passes/PostEmscripten.cpp @@ -107,7 +107,7 @@ static void calcSegmentOffsets(Module& wasm, } for (unsigned i = 0; i < wasm.dataSegments.size(); ++i) { auto& segment = wasm.dataSegments[i]; - if (segment->isPassive) { + if (segment->isPassive()) { auto it = passiveOffsets.find(segment->name); if (it != passiveOffsets.end()) { segmentOffsets.push_back(it->second); diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index d560ca49547..86ce8595683 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -3443,7 +3443,7 @@ void PrintSExpression::visitElementSegment(ElementSegment* curr) { printMedium(o, "elem "); curr->name.print(o); - if (curr->table.is()) { + if (curr->isActive()) { if (usesExpressions || currModule->tables.size() > 1) { // tableuse o << " (table "; @@ -3523,7 +3523,7 @@ void PrintSExpression::visitMemory(Memory* curr) { } void PrintSExpression::visitDataSegment(DataSegment* curr) { - if (!curr->isPassive && !curr->offset) { + if (curr->isActive() && !curr->offset) { // This data segment must have been created from the datacount section but // not parsed yet. Skip it. return; @@ -3533,7 +3533,7 @@ void PrintSExpression::visitDataSegment(DataSegment* curr) { printMajor(o, "data "); curr->name.print(o); o << ' '; - if (!curr->isPassive) { + if (curr->isActive()) { assert(!currModule || currModule->memories.size() > 0); if (!currModule || curr->memory != currModule->memories[0]->name) { o << "(memory "; diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 22f9b3ff536..fe1c6c91685 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -314,7 +314,7 @@ struct Analyzer { void prepare() { for (auto& elem : module->elementSegments) { - if (!elem->table) { + if (elem->isPassive()) { continue; } auto& flatTableInfo = flatTableInfoMap[elem->table]; @@ -862,7 +862,7 @@ struct RemoveUnusedModuleElements : public Pass { } }; ModuleUtils::iterActiveDataSegments(*module, [&](DataSegment* segment) { - if (segment->memory.is()) { + if (segment->isActive()) { auto* memory = module->getMemory(segment->memory); maybeRootSegment(ModuleElementKind::DataSegment, segment->name, @@ -874,7 +874,7 @@ struct RemoveUnusedModuleElements : public Pass { }); ModuleUtils::iterActiveElementSegments( *module, [&](ElementSegment* segment) { - if (segment->table.is()) { + if (segment->isActive()) { auto* table = module->getTable(segment->table); maybeRootSegment(ModuleElementKind::ElementSegment, segment->name, diff --git a/src/passes/SeparateDataSegments.cpp b/src/passes/SeparateDataSegments.cpp index bd684dbe82c..34a7855f12b 100644 --- a/src/passes/SeparateDataSegments.cpp +++ b/src/passes/SeparateDataSegments.cpp @@ -44,7 +44,7 @@ struct SeparateDataSegments : public Pass { Address base = std::stoi(baseStr); size_t lastEnd = 0; for (auto& seg : module->dataSegments) { - if (seg->isPassive) { + if (seg->isPassive()) { Fatal() << "separating passive segments not implemented"; } if (!seg->offset->is()) { diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index fa5772a221f..457a371a008 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -450,13 +450,13 @@ void TranslateToFuzzReader::setupMemory() { auto segment = builder.makeDataSegment(); segment->setName(Names::getValidDataSegmentName(wasm, Name::fromInt(i)), false); - segment->isPassive = bool(upTo(2)); + bool isPassive = bool(upTo(2)); size_t segSize = upTo(fuzzParams->USABLE_MEMORY * 2); segment->data.resize(segSize); for (size_t j = 0; j < segSize; j++) { segment->data[j] = upTo(512); } - if (!segment->isPassive) { + if (!isPassive) { segment->offset = builder.makeConst( Literal::makeFromInt32(memCovered, memory->addressType)); memCovered += segSize; @@ -643,7 +643,7 @@ void TranslateToFuzzReader::setupTables() { std::any_of(wasm.elementSegments.begin(), wasm.elementSegments.end(), [&](auto& segment) { - return segment->table.is() && segment->type == funcref; + return segment->isActive() && segment->type == funcref; }); auto addressType = wasm.getTable(funcrefTableName)->addressType; if (!hasFuncrefElemSegment) { @@ -869,7 +869,7 @@ void TranslateToFuzzReader::finalizeMemory() { auto& memory = wasm.memories[0]; for (auto& segment : wasm.dataSegments) { Address maxOffset = segment->data.size(); - if (!segment->isPassive) { + if (segment->isActive()) { if (!wasm.features.hasGC()) { // Using a non-imported global in a segment offset is not valid in wasm // unless GC is enabled. This can occur due to us adding a local diff --git a/src/wasm-builder.h b/src/wasm-builder.h index 1c8894c4094..0bf270ab8ef 100644 --- a/src/wasm-builder.h +++ b/src/wasm-builder.h @@ -151,15 +151,13 @@ class Builder { static std::unique_ptr makeDataSegment(Name name = "", - Name memory = "", - bool isPassive = false, + Name memory = Name(), Expression* offset = nullptr, const char* init = "", Address size = 0) { auto seg = std::make_unique(); seg->name = name; seg->memory = memory; - seg->isPassive = isPassive; seg->offset = offset; seg->data.resize(size); std::copy_n(init, size, seg->data.begin()); diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 0c4e9f02b77..dd8a68866f9 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -3848,7 +3848,7 @@ class ModuleRunnerBase : public ExpressionRunner { // apply active memory segments for (size_t i = 0, e = wasm.dataSegments.size(); i < e; ++i) { auto& segment = wasm.dataSegments[i]; - if (segment->isPassive) { + if (segment->isPassive()) { continue; } diff --git a/src/wasm-traversal.h b/src/wasm-traversal.h index 9717c053d8c..550a9306b0e 100644 --- a/src/wasm-traversal.h +++ b/src/wasm-traversal.h @@ -172,7 +172,7 @@ struct Walker : public VisitorType { void doWalkFunction(Function* func) { walk(func->body); } void walkElementSegment(ElementSegment* segment) { - if (segment->table.is()) { + if (segment->isActive()) { walk(segment->offset); } for (auto* expr : segment->data) { @@ -189,7 +189,7 @@ struct Walker : public VisitorType { } void walkDataSegment(DataSegment* segment) { - if (!segment->isPassive) { + if (segment->isActive()) { walk(segment->offset); } static_cast(this)->visitDataSegment(segment); diff --git a/src/wasm.h b/src/wasm.h index 40cdf896c19..9f81336144d 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2571,6 +2571,9 @@ class ElementSegment : public Named { Type type = Type(HeapType::func, Nullable); std::vector data; + bool isActive() const { return bool(table); } + bool isPassive() const { return !table; } + ElementSegment() = default; ElementSegment(Name table, Expression* offset, @@ -2610,9 +2613,11 @@ class Table : public Importable { class DataSegment : public Named { public: Name memory; - bool isPassive = false; Expression* offset = nullptr; std::vector data; // TODO: optimize + + bool isActive() const { return bool(memory); } + bool isPassive() const { return !memory; } }; class Memory : public Importable { diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 2adb6ba83d4..49497af127a 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -689,7 +689,7 @@ void WasmBinaryWriter::writeDataSegments() { for (auto& segment : wasm->dataSegments) { uint32_t flags = 0; Index memoryIndex = 0; - if (segment->isPassive) { + if (segment->isPassive()) { flags |= BinaryConsts::IsPassive; } else { memoryIndex = getMemoryIndex(segment->memory); @@ -698,7 +698,7 @@ void WasmBinaryWriter::writeDataSegments() { } } o << U32LEB(flags); - if (!segment->isPassive) { + if (segment->isActive()) { if (memoryIndex) { o << U32LEB(memoryIndex); } @@ -825,7 +825,6 @@ void WasmBinaryWriter::writeElementSegments() { for (auto& segment : wasm->elementSegments) { Index tableIdx = 0; - bool isPassive = segment->table.isNull(); // If the segment is MVP, we can use the shorter form. bool usesExpressions = TableUtils::usesExpressions(segment.get(), wasm); @@ -834,7 +833,7 @@ void WasmBinaryWriter::writeElementSegments() { // supported by the MVP, which also did not support table indices in the // segment encoding. bool hasTableIndex = false; - if (!isPassive) { + if (segment->isActive()) { tableIdx = getTableIndex(segment->table); hasTableIndex = tableIdx > 0 || wasm->getTable(segment->table)->type != funcref; @@ -844,14 +843,14 @@ void WasmBinaryWriter::writeElementSegments() { if (usesExpressions) { flags |= BinaryConsts::UsesExpressions; } - if (isPassive) { + if (segment->isPassive()) { flags |= BinaryConsts::IsPassive; } else if (hasTableIndex) { flags |= BinaryConsts::HasIndex; } o << U32LEB(flags); - if (!isPassive) { + if (segment->isActive()) { if (hasTableIndex) { o << U32LEB(tableIdx); } @@ -859,7 +858,7 @@ void WasmBinaryWriter::writeElementSegments() { o << int8_t(BinaryConsts::End); } - if (isPassive || hasTableIndex) { + if (segment->isPassive() || hasTableIndex) { if (usesExpressions) { // elemType writeType(segment->type); @@ -5023,8 +5022,7 @@ void WasmBinaryReader::readDataSegments() { throwError("bad segment flags, must be 0, 1, or 2, not " + std::to_string(flags)); } - curr->isPassive = flags & BinaryConsts::IsPassive; - if (curr->isPassive) { + if (flags & BinaryConsts::IsPassive) { curr->memory = Name(); curr->offset = nullptr; } else { diff --git a/src/wasm/wasm-emscripten.cpp b/src/wasm/wasm-emscripten.cpp index ebc2df9bc2c..8c2c272c8b9 100644 --- a/src/wasm/wasm-emscripten.cpp +++ b/src/wasm/wasm-emscripten.cpp @@ -140,7 +140,7 @@ class StringConstantTracker { } for (unsigned i = 0; i < wasm.dataSegments.size(); ++i) { auto& segment = wasm.dataSegments[i]; - if (segment->isPassive) { + if (segment->isPassive()) { auto it = passiveOffsets.find(segment->name); if (it != passiveOffsets.end()) { segmentOffsets.push_back(it->second); diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index a8c9f40e188..8a5a63ca0af 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -5146,7 +5146,7 @@ void validateMemories(Module& module, ValidationInfo& info) { void validateDataSegments(Module& module, ValidationInfo& info) { for (auto& segment : module.dataSegments) { - if (segment->isPassive) { + if (segment->isPassive()) { info.shouldBeTrue( module.features.hasBulkMemory(), segment->offset, @@ -5274,8 +5274,7 @@ void validateTables(Module& module, ValidationInfo& info) { << getMissingFeaturesList(module, typeFeats) << '\n'; } - bool isPassive = !segment->table.is(); - if (isPassive) { + if (segment->isPassive()) { info.shouldBeTrue( !segment->offset, "elem", "passive segment should not have an offset"); } else { diff --git a/src/wasm2js.h b/src/wasm2js.h index 5eb937b0f88..c5bfe650ecc 100644 --- a/src/wasm2js.h +++ b/src/wasm2js.h @@ -97,7 +97,7 @@ bool isTableExported(Module& wasm) { bool hasActiveSegments(Module& wasm) { for (Index i = 0; i < wasm.dataSegments.size(); i++) { - if (!wasm.dataSegments[i]->isPassive) { + if (wasm.dataSegments[i]->isActive()) { return true; } } @@ -2870,7 +2870,7 @@ void Wasm2JSGlue::emitMemory() { // If we have passive memory segments, we need to store those. for (auto& seg : wasm.dataSegments) { - if (seg->isPassive) { + if (seg->isPassive()) { out << " var memorySegments = {};\n"; break; } @@ -2907,7 +2907,7 @@ void Wasm2JSGlue::emitMemory() { for (Index i = 0; i < wasm.dataSegments.size(); i++) { auto& seg = wasm.dataSegments[i]; - if (seg->isPassive) { + if (seg->isPassive()) { // Fancy passive segments are decoded into typed arrays on the side, for // later copying. out << "memorySegments[" << i @@ -2934,7 +2934,7 @@ void Wasm2JSGlue::emitMemory() { out << "function initActiveSegments(imports) {\n"; for (Index i = 0; i < wasm.dataSegments.size(); i++) { auto& seg = wasm.dataSegments[i]; - if (!seg->isPassive) { + if (seg->isActive()) { // Plain active segments are decoded directly into the main memory. out << " base64DecodeToExistingUint8Array(bufferView, " << globalOffset(*seg) << ", \"" << base64Encode(seg->data) From 513aa1f662009ba743cd2495f3d69f0c32b2d509 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 28 May 2026 11:24:32 -0700 Subject: [PATCH 161/168] [wasm-split] Fix error printing when func does not exist (#8784) When `func` is null, we can't query `func->name`. --- src/tools/wasm-split/wasm-split.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/tools/wasm-split/wasm-split.cpp b/src/tools/wasm-split/wasm-split.cpp index 8faa1bef488..14293b779af 100644 --- a/src/tools/wasm-split/wasm-split.cpp +++ b/src/tools/wasm-split/wasm-split.cpp @@ -244,25 +244,26 @@ void setCommonSplitConfigs(ModuleSplitting::Config& config, } // Returns whether it is valid to split a function out from the main module. -bool canSplitFunc(Function* func, +bool canSplitFunc(Name funcName, const Module& wasm, const WasmSplitOptions& options) { + auto* func = wasm.getFunctionOrNull(funcName); if (!func) { if (!options.quiet) { - std::cerr << "warning: function " << func->name << " does not exist\n"; + std::cerr << "warning: function " << funcName << " does not exist\n"; } return false; } if (func->imported()) { if (!options.quiet) { - std::cerr << "warning: cannot split out imported function " << func->name + std::cerr << "warning: cannot split out imported function " << funcName << "\n"; } return false; } if (func->name == wasm.start) { if (!options.quiet) { - std::cerr << "warning: cannot split out start function " << func->name + std::cerr << "warning: cannot split out start function " << funcName << "\n"; } return false; @@ -310,8 +311,7 @@ void splitModule(const WasmSplitOptions& options) { // Use the explicitly provided `splitFuncs`. for (auto& func : options.splitFuncs) { - auto* function = wasm.getFunctionOrNull(func); - if (!canSplitFunc(function, wasm, options)) { + if (!canSplitFunc(func, wasm, options)) { continue; } if (!options.quiet && options.keepFuncs.contains(func)) { @@ -453,7 +453,7 @@ void multiSplitModule(const WasmSplitOptions& options) { continue; } assert(currFuncs); - if (!canSplitFunc(wasm.getFunctionOrNull(name), wasm, options)) { + if (!canSplitFunc(name, wasm, options)) { continue; } currFuncs->insert(name); From a72691c422a631f177af86fd929751f4c5847047 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 28 May 2026 12:41:36 -0700 Subject: [PATCH 162/168] Skip d8 lit tests on windows (#8788) It is not clear why v8 is not installed on CI specifically on windows-arm, but apparently it isn't. As this just affects 4 tests, skip them on windows. Example error: https://github.com/WebAssembly/binaryen/actions/runs/26543251282/job/78189409612 ``` Failed Tests (4): Binaryen lit tests :: d8/fuzz_shell.wast Binaryen lit tests :: d8/fuzz_shell_exceptions.wast Binaryen lit tests :: d8/fuzz_shell_jspi.wast Binaryen lit tests :: d8/fuzz_shell_sleep.wast ``` --- test/lit/d8/fuzz_shell.wast | 4 ++++ test/lit/d8/fuzz_shell_exceptions.wast | 4 ++++ test/lit/d8/fuzz_shell_jspi.wast | 4 ++++ test/lit/d8/fuzz_shell_sleep.wast | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/test/lit/d8/fuzz_shell.wast b/test/lit/d8/fuzz_shell.wast index 40b2ad1329a..68d97d9f157 100644 --- a/test/lit/d8/fuzz_shell.wast +++ b/test/lit/d8/fuzz_shell.wast @@ -1,5 +1,9 @@ ;; Test running a wasm file in fuzz_shell.js. +;; This fails on windows-ARM on CI for unclear reasons. v8 is somehow not +;; properly installed. +;; REQUIRES: linux + (module (func $test (export "test") (result i32) (i32.const 42) diff --git a/test/lit/d8/fuzz_shell_exceptions.wast b/test/lit/d8/fuzz_shell_exceptions.wast index 9bad7bf6117..f4d5d082d85 100644 --- a/test/lit/d8/fuzz_shell_exceptions.wast +++ b/test/lit/d8/fuzz_shell_exceptions.wast @@ -1,5 +1,9 @@ ;; Test throwing from JS by calling the throw import. +;; This fails on windows-ARM on CI for unclear reasons. v8 is somehow not +;; properly installed. +;; REQUIRES: linux + (module (import "fuzzing-support" "throw" (func $throw (param i32))) diff --git a/test/lit/d8/fuzz_shell_jspi.wast b/test/lit/d8/fuzz_shell_jspi.wast index eb105483766..41903c4f5c0 100644 --- a/test/lit/d8/fuzz_shell_jspi.wast +++ b/test/lit/d8/fuzz_shell_jspi.wast @@ -1,3 +1,7 @@ +;; This fails on windows-ARM on CI for unclear reasons. v8 is somehow not +;; properly installed. +;; REQUIRES: linux + (module (import "fuzzing-support" "log-i32" (func $log (param i32))) diff --git a/test/lit/d8/fuzz_shell_sleep.wast b/test/lit/d8/fuzz_shell_sleep.wast index 6243b21d326..33114743fd7 100644 --- a/test/lit/d8/fuzz_shell_sleep.wast +++ b/test/lit/d8/fuzz_shell_sleep.wast @@ -1,3 +1,7 @@ +;; This fails on windows-ARM on CI for unclear reasons. v8 is somehow not +;; properly installed. +;; REQUIRES: linux + (module (import "fuzzing-support" "sleep" (func $sleep (param i32 i32) (result i32))) From ab2d66cf85b4933b1624bc187ab21fdedb441cf6 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 28 May 2026 13:50:00 -0700 Subject: [PATCH 163/168] [StackSwitching] Error properly on an unhandled resume during start (#8787) Before, we asserted on stale state. Fix the auto-updater script to not crash on such output (an exec line before any function). That happens to improve one existing test output. --- scripts/update_lit_checks.py | 37 ++++++++++++------------- src/wasm-interpreter.h | 5 +++- test/lit/exec/cont_start_unhandled.wast | 32 +++++++++++++++++++++ test/lit/exec/host-limit.wast | 1 + 4 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 test/lit/exec/cont_start_unhandled.wast diff --git a/scripts/update_lit_checks.py b/scripts/update_lit_checks.py index 5adb37ba514..1b1b8e1cc4e 100755 --- a/scripts/update_lit_checks.py +++ b/scripts/update_lit_checks.py @@ -183,7 +183,7 @@ def parse_output_modules(text): return modules -def parse_output_fuzz_exec(text): +def parse_output_fuzz_exec(text, first_named_item): # Returns the same data as `parse_output_modules`, but can't tell where # module boundaries are, so always just returns items for a single module. items = [] @@ -194,19 +194,18 @@ def parse_output_fuzz_exec(text): # in the input. name = '$' + func.group("name") items.append((('func', name), [line])) - elif line.startswith('[host limit'): - # Skip mentions of host limits that we hit. This can happen even - # before we reach the execution of a function (if it happens during - # instantiation of the module), in which case |items| may be empty, - # and we'd error on the code below. - pass elif line: - assert items, 'unexpected non-invocation line' - items[-1][1].append(line) + if not items: + # Early output before any export was executed. Associate it with + # the first named item, so it appears before everything else + # (which is when it executes). + items.append((first_named_item, [line])) + else: + items[-1][1].append(line) return [items] -def get_command_output(args, kind, test, lines, tmp): +def get_command_output(args, kind, test, lines, tmp, named_items): # Return list of maps from prefixes to lists of module items of the form # ((kind, name), [line]). The outer list has an entry for each module. command_output = [] @@ -233,7 +232,7 @@ def get_command_output(args, kind, test, lines, tmp): if kind == 'wat': module_outputs = parse_output_modules(output) elif kind == 'fuzz-exec': - module_outputs = parse_output_fuzz_exec(output) + module_outputs = parse_output_fuzz_exec(output, named_items[0]) else: assert False, "unknown output kind" for i in range(len(module_outputs)): @@ -259,7 +258,14 @@ def update_test(args, test, lines, tmp): # Skip the notice if it is already in the output lines = lines[1:] - command_output = get_command_output(args, output_kind, test, lines, tmp) + named_items = [] + for line in lines: + match = ITEM_RE.match(line) + if match: + _, kind, name = indentKindName(match) + named_items.append((kind, name)) + + command_output = get_command_output(args, output_kind, test, lines, tmp, named_items) prefixes = {prefix for module_output in command_output for prefix in module_output.keys()} check_line_re = re.compile(r'^\s*;;\s*(' + '|'.join(prefixes) + @@ -275,13 +281,6 @@ def update_test(args, test, lines, tmp): filtered.append(lines[-1]) lines = filtered - named_items = [] - for line in lines: - match = ITEM_RE.match(line) - if match: - _, kind, name = indentKindName(match) - named_items.append((kind, name)) - notice_args = '' if all_items: notice_args += ' --all-items' diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index dd8a68866f9..fb95d7ecb0b 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -3449,7 +3449,10 @@ class ModuleRunnerBase : public ExpressionRunner { // run start, if present if (wasm.start.is()) { Literals arguments; - callFunction(wasm.start, arguments); + auto flow = callFunction(wasm.start, arguments); + if (flow.suspendTag) { + trap("unhandled suspend in start function"); + } } } diff --git a/test/lit/exec/cont_start_unhandled.wast b/test/lit/exec/cont_start_unhandled.wast new file mode 100644 index 00000000000..5b80edcbba2 --- /dev/null +++ b/test/lit/exec/cont_start_unhandled.wast @@ -0,0 +1,32 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --output=fuzz-exec and should not be edited. + +;; RUN: foreach %s %t wasm-opt -all --fuzz-exec-before -q -o /dev/null 2>&1 | filecheck %s + +;; A start function suspends to the host. This is an unhandled suspend in the +;; start function, which should trap and not leave a stale continuation on the +;; stack (which would cause subsequent assertions to fail). + +(module + ;; CHECK: [trap unhandled suspend in start function] + (type $void_func (func)) + (type $cont (cont $void_func)) + + (tag $tag_suspend (type $void_func)) + + (start $start) + + (func $start + (local $c (ref $cont)) + (local.set $c (cont.new $cont (ref.func $suspending_func))) + (resume $cont (local.get $c)) + ) + + (func $suspending_func + (suspend $tag_suspend) + ) + + (func $run (export "run") + ;; Another function, to show that the trap during start does not get here. + ) +) + diff --git a/test/lit/exec/host-limit.wast b/test/lit/exec/host-limit.wast index 2f6298ca675..871e8a9ce7f 100644 --- a/test/lit/exec/host-limit.wast +++ b/test/lit/exec/host-limit.wast @@ -7,6 +7,7 @@ ;; fuzz exec and not error. (module + ;; CHECK: [host limit allocation failure] (type $type$0 (array i8)) (import "fuzzing-support" "log-i32" (func $log (param i32))) From ec73e2ba8a6cefc3efe1e714333afdf790ffb9d7 Mon Sep 17 00:00:00 2001 From: Steven Fontanella Date: Thu, 28 May 2026 13:51:09 -0700 Subject: [PATCH 164/168] NFC: Require override keyword (#8789) --- CMakeLists.txt | 1 + src/passes/Outlining.cpp | 2 +- src/passes/Poppify.cpp | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f45a805098b..aa6797b600e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -419,6 +419,7 @@ else() # MSVC add_compile_flag("-Wswitch") # we explicitly expect this in the code add_compile_flag("-Wimplicit-fallthrough") add_compile_flag("-Wnon-virtual-dtor") + add_compile_flag("-Wsuggest-override") if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") # Google style requires this, so make sure we compile cleanly with it. diff --git a/src/passes/Outlining.cpp b/src/passes/Outlining.cpp index c52213fdc61..840a84bad37 100644 --- a/src/passes/Outlining.cpp +++ b/src/passes/Outlining.cpp @@ -662,7 +662,7 @@ struct ReconstructStringifyWalker }; struct Outlining : public Pass { - void run(Module* module) { + void run(Module* module) override { HashStringifyWalker stringify; // Walk the module and create a "string representation" of the program. stringify.walkModule(module); diff --git a/src/passes/Poppify.cpp b/src/passes/Poppify.cpp index a7f3e5cccd7..c12c7203c0d 100644 --- a/src/passes/Poppify.cpp +++ b/src/passes/Poppify.cpp @@ -456,7 +456,7 @@ class PoppifyFunctionsPass : public Pass { } // anonymous namespace class PoppifyPass : public Pass { - void run(Module* module) { + void run(Module* module) override { PassRunner subRunner(getPassRunner()); subRunner.add(std::make_unique()); // TODO: Enable this once it handles Poppy blocks correctly From a6f59c6bc0302eafe3568415b0057d8919ecdfb0 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 28 May 2026 14:00:57 -0700 Subject: [PATCH 165/168] Make areConsecutiveInputsEqual more precise (#8783) Collect the interfering effects specifically from the expressions that are evaluated in between the LHS and RHS fallthrough values. The previous code overapproximated this by collecting all effects from the LHS and RHS expressions, whether or not the effects were due to subexpressions that would have been evaluated before or after the LHS and RHS values. Also use a direction-aware effect ordering check. --- src/passes/OptimizeInstructions.cpp | 134 ++-- .../optimize-instructions-struct-rmw.wast | 674 +++++++++++++++++- 2 files changed, 750 insertions(+), 58 deletions(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index e61deed16aa..47964f33713 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -2788,69 +2788,89 @@ struct OptimizeInstructions // simple peephole optimizations - all we care about is a single instruction // at a time, and its inputs). bool areConsecutiveInputsEqual(Expression* left, Expression* right) { - // When we look for a tee/get pair, we can consider the fallthrough values - // for the first, as the fallthrough happens last (however, we must use - // NoTeeBrIf as we do not want to look through the tee). We cannot do this - // on the second, however, as there could be effects in the middle. - // TODO: Use effects here perhaps. - left = - Properties::getFallthrough(left, - getPassOptions(), - *getModule(), - Properties::FallthroughBehavior::NoTeeBrIf); - if (areMatchingTeeAndGet(left, right)) { - return true; + // The fallthrough expression of `left` produces its value. That value may + // depend on effects from other non-fallthrough expressions in `left`, but + // those expressions are generally executed before the fallthrough value and + // will affect the values of `left` and `right` equally, so we can ignore + // them. The exceptions are `local.tee` instructions and br_if conditions, + // which execute after the fallthrough and might affect only the value of + // `right`. + // TODO: We should use a custom getFallthrough that ignores whether br_if + // conditions and values can be reordered, since we can handle that more + // precisely here. + // TODO: When the fallthrough is an If (meaning the other branch must never + // return), we should ignore effects in that non-returning branch. + EffectAnalyzer interferingEffects(getPassOptions(), *getModule()); + bool matchingTeeAndGet = false; + while (true) { + left = + Properties::getFallthrough(left, + getPassOptions(), + *getModule(), + Properties::FallthroughBehavior::NoTeeBrIf); + if (auto* tee = left->dynCast()) { + assert(tee->isTee()); + // If `right` reads directly from this local.tee, then we know their + // values are the same. We know no children of this tee will be executed + // after it, so we need not look for further effects. But there might be + // interfering sets in previous br_if conditions, so we cannot just + // return here. + // TODO: Calculate `right`'s fallthrough first in case the fallthrough + // is the matching get. + if (areMatchingTeeAndGet(left, right)) { + matchingTeeAndGet = true; + left = getFallthrough(left); + break; + } + interferingEffects.visit(tee); + left = tee->value; + continue; + } + if (auto* br = left->dynCast(); br && br->condition) { + assert(br->value); + // NB: We don't need to worry about the branch effect because any branch + // at runtime must skip past the parent expression, so it would not + // matter how that parent expression gets optimized. + interferingEffects.walk(br->condition); + left = br->value; + continue; + } + // We have found the real fallthrough expression. + break; } - // Ignore extraneous things and compare them syntactically. We can also - // look at the full fallthrough for both sides now. - auto* originalLeft = left; - left = getFallthrough(left); - auto* originalRight = right; - right = getFallthrough(right); - if (!ExpressionAnalyzer::equal(left, right)) { - return false; + // We similarly want to find the fallthrough expression of `right`. But this + // time, it is the expressions that execute before, not after, the + // fallthrough that can affect its value. + while (true) { + auto* next = Properties::getImmediateFallthrough( + right, getPassOptions(), *getModule()); + if (next == right) { + // We have found the fallthrough expression. + break; + } + // Gather the effects of all the non-fallthrough children of the + // container. + for (auto* child : ChildIterator(right)) { + if (child == next) { + // Skip children that execute after the fallthrough value, such as + // br_if conditions. + break; + } + interferingEffects.walk(child); + } + right = next; } - // We must also not have non-fallthrough effects that invalidate us, such as - // this situation: - // - // (local.get $x) - // (block - // (local.set $x ..) - // (local.get $x) - // ) - // - // The fallthroughs are identical, but the set may cause us to read a - // different value. - if (originalRight != right) { - // TODO: We could be more precise here and ignore right itself in - // originalRightEffects. - auto originalRightEffects = effects(originalRight); - auto rightEffects = effects(right); - if (originalRightEffects.invalidates(rightEffects)) { - return false; - } + // We have both fallthrough expressions. See if they look the same. + if (!matchingTeeAndGet && !ExpressionAnalyzer::equal(left, right)) { + return false; } - // The same, with left, as we can have this situation: - // - // (local.tee $x ..) - // (something using $x) - // ) - // (something using $x) - // - // The fallthroughs are identical, but the tee may cause us to read a - // different value. - if (originalLeft != left) { - auto originalLeftEffects = effects(originalLeft); - // |left == right| here (we would have exited early, otherwise, above), so - // we could compute either. Compute |left| as it might have better cache - // locality. - auto leftEffects = effects(left); - if (originalLeftEffects.invalidates(leftEffects)) { - return false; - } + // They do look the same! Make sure nothing executed in between them can + // affect the value of `right` and make it different from `left`. + if (interferingEffects.orderedBefore(effects(right))) { + return false; } // To be equal, they must also be known to return the same result diff --git a/test/lit/passes/optimize-instructions-struct-rmw.wast b/test/lit/passes/optimize-instructions-struct-rmw.wast index d8ea93ef4b4..19893460361 100644 --- a/test/lit/passes/optimize-instructions-struct-rmw.wast +++ b/test/lit/passes/optimize-instructions-struct-rmw.wast @@ -1,6 +1,6 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. -;; RUN: wasm-opt %s -all --optimize-instructions --preserve-type-order -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt -all --optimize-instructions --preserve-type-order -S -o - | filecheck %s (module ;; CHECK: (type $i32 (shared (struct (field (mut i32))))) @@ -1476,3 +1476,675 @@ ) ) ) + +;; Test the effect analysis in areConsecutiveInputsEqual by optimizing (or not) +;; cmpxchg operations with matching expected and replacement operands. +(module + ;; CHECK: (type $struct (shared (struct (field (mut i32))))) + (type $struct (shared (struct (field (mut i32))))) + ;; CHECK: (type $array (shared (array (mut i32)))) + (type $array (shared (array (mut i32)))) + + ;; CHECK: (func $release-store-before (type $2) (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + ;; CHECK-NEXT: (local $temp i32) + ;; CHECK-NEXT: (struct.atomic.get acqrel $struct 0 + ;; CHECK-NEXT: (block (result (ref $struct)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $temp + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (array.atomic.set acqrel $array + ;; CHECK-NEXT: (local.get $array) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $release-store-before (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + (local $temp i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + ;; This tee does not affect the value of the right-hand side, so we can + ;; ignore it. + (local.tee $temp + (block (result i32) + ;; This release store is not ordered before the struct.get, so we know + ;; it cannot influence the struct.get's value, so we can optimize. + (array.atomic.set acqrel $array (local.get $array) (i32.const 0) (i32.const 0)) + (struct.get $struct 0 (local.get $struct)) + ) + ) + (struct.get $struct 0 (local.get $struct)) + ) + ) + + ;; CHECK: (func $acquire-load-before (type $2) (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + ;; CHECK-NEXT: (local $temp i32) + ;; CHECK-NEXT: (struct.atomic.get acqrel $struct 0 + ;; CHECK-NEXT: (block (result (ref $struct)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $temp + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (array.atomic.get acqrel $array + ;; CHECK-NEXT: (local.get $array) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $acquire-load-before (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + (local $temp i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + ;; This tee does not affect the value of the right-hand side, so we can + ;; ignore it. + (local.tee $temp + (block (result i32) + ;; This acquire load is ordered before the struct.get, so can affect + ;; its value. However, it affects both struct.gets equally, so we can + ;; optimize anyway. (The code used to do a coarser check for ordering + ;; here that would prevent optimization.) + (drop (array.atomic.get acqrel $array (local.get $array) (i32.const 0))) + (struct.get $struct 0 (local.get $struct)) + ) + ) + (struct.get $struct 0 (local.get $struct)) + ) + ) + + ;; CHECK: (func $release-store-middle-lhs (type $2) (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (struct.atomic.get acqrel $struct 0 + ;; CHECK-NEXT: (block (result (ref $struct)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (br_if $l + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (array.atomic.set acqrel $array + ;; CHECK-NEXT: (local.get $array) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $release-store-middle-lhs (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + (block $l (result i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (br_if $l + ;; This is the fallthrough value. + (struct.get $struct 0 (local.get $struct)) + ;; The condition is executed after the fallthrough, so its effects can + ;; affect the right-hand side in principle. However, the release store + ;; does not affect the value, so we can still optimize. + (block (result i32) + (array.atomic.set acqrel $array (local.get $array) (i32.const 0) (i32.const 0)) + (i32.const 0) + ) + ) + (struct.get $struct 0 (local.get $struct)) + ) + ) + ) + + ;; CHECK: (func $acquire-load-middle-lhs (type $2) (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: (br_if $l + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (array.atomic.get acqrel $array + ;; CHECK-NEXT: (local.get $array) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $acquire-load-middle-lhs (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + (block $l (result i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (br_if $l + ;; This is the fallthrough value. + (struct.get $struct 0 (local.get $struct)) + ;; The acquire load could form a synchronization edge that forces the + ;; the right-hand struct.get to observe a different value, so it + ;; blocks optimization. + (block (result i32) + (drop (array.atomic.get acqrel $array (local.get $array) (i32.const 0))) + (i32.const 0) + ) + ) + (struct.get $struct 0 (local.get $struct)) + ) + ) + ) + + ;; CHECK: (func $release-store-middle-rhs (type $2) (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + ;; CHECK-NEXT: (struct.atomic.get acqrel $struct 0 + ;; CHECK-NEXT: (block (result (ref $struct)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (array.atomic.set acqrel $array + ;; CHECK-NEXT: (local.get $array) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $release-store-middle-rhs (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (struct.get $struct 0 (local.get $struct)) + (block (result i32) + ;; This release store is executed between the LHS and RHS struct.gets, + ;; but does not change the value observed by the RHS, so does not block + ;; optimization. + (array.atomic.set acqrel $array (local.get $array) (i32.const 0) (i32.const 0)) + (struct.get $struct 0 (local.get $struct)) + ) + ) + ) + + ;; CHECK: (func $acquire-load-middle-rhs (type $2) (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + ;; CHECK-NEXT: (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (array.atomic.get acqrel $array + ;; CHECK-NEXT: (local.get $array) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $acquire-load-middle-rhs (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (struct.get $struct 0 (local.get $struct)) + (block (result i32) + ;; This acquire load is executed between the LHS and RHS struct.gets and + ;; can create a synchronization edge forcing the RHS to observe a + ;; different value, so it blocks optimization. + (drop (array.atomic.get acqrel $array (local.get $array) (i32.const 0))) + (struct.get $struct 0 (local.get $struct)) + ) + ) + ) + + ;; CHECK: (func $release-store-after (type $2) (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $l + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (array.atomic.set acqrel $array + ;; CHECK-NEXT: (local.get $array) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $release-store-after (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + (block $l (result i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (struct.get $struct 0 (local.get $struct)) + (br_if $l + ;; This is the RHS value. + (struct.get $struct 0 (local.get $struct)) + ;; The condition is executed after the RHS value is computed, so its + ;; effects cannot affect the values seen by the cmpxchg and can never + ;; block optimization. + ;; TODO: getImmediateFallthroughPtr "helpfully" only allows + ;; fallthrough via br_if when the value and condition can be + ;; reordered, so unnecessarily blocks this optimization. + (block (result i32) + (array.atomic.set acqrel $array (local.get $array) (i32.const 0) (i32.const 0)) + (i32.const 0) + ) + ) + ) + ) + ) + + ;; CHECK: (func $acquire-load-after (type $2) (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $l + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (array.atomic.get acqrel $array + ;; CHECK-NEXT: (local.get $array) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $acquire-load-after (param $struct (ref $struct)) (param $array (ref $array)) (result i32) + (block $l (result i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (struct.get $struct 0 (local.get $struct)) + (br_if $l + ;; This is the RHS value. + (struct.get $struct 0 (local.get $struct)) + ;; The condition is executed after the RHS value is computed, so its + ;; effects cannot affect the values seen by the cmpxchg and can never + ;; block optimization. + ;; TODO: getImmediateFallthroughPtr "helpfully" only allows + ;; fallthrough via br_if when the value and condition can be + ;; reordered. In this case the value and condition _can_ be reordered, + ;; but the reordering check is currently symmetric, so the + ;; optimization is still blocked. + (block (result i32) + (drop (array.atomic.get acqrel $array (local.get $array) (i32.const 0))) + (i32.const 0) + ) + ) + ) + ) + ) + + ;; CHECK: (func $outer-tee-match (type $3) (param $struct (ref $struct)) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (struct.atomic.get acqrel $struct 0 + ;; CHECK-NEXT: (block (result (ref $struct)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $x + ;; CHECK-NEXT: (local.tee $y + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $outer-tee-match (param $struct (ref $struct)) (result i32) + (local $x i32) + (local $y i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (local.tee $x + (local.tee $y + (i32.const 0) + ) + ) + ;; We recognize that this comes from the LHS tee and can optimize. + (local.get $x) + ) + ) + + ;; CHECK: (func $inner-tee-match (type $3) (param $struct (ref $struct)) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (struct.atomic.get acqrel $struct 0 + ;; CHECK-NEXT: (block (result (ref $struct)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $x + ;; CHECK-NEXT: (local.tee $y + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $inner-tee-match (param $struct (ref $struct)) (result i32) + (local $x i32) + (local $y i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (local.tee $x + (local.tee $y + (i32.const 0) + ) + ) + ;; We recognize that this comes from the LHS tee and can optimize. + (local.get $y) + ) + ) + + ;; CHECK: (func $br_if-after-tee (type $3) (param $struct (ref $struct)) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: (br_if $l + ;; CHECK-NEXT: (local.tee $x + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_if-after-tee (param $struct (ref $struct)) (result i32) + (local $x i32) + (block $l (result i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (br_if $l + (local.tee $x + (i32.const 0) + ) + ;; The br_if condition is evaluated after the local.tee, so it + ;; interferes and blocks optimization. + (block (result i32) + (local.set $x (i32.const 2)) + (i32.const 0) + ) + ) + (local.get $x) + ) + ) + ) + + ;; CHECK: (func $tee-interference (type $3) (param $struct (ref $struct)) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: (local.tee $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $tee-interference (param $struct (ref $struct)) (result i32) + (local $x i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (local.tee $x + ;; The fallthrough values depend on $x, so the optimization is blocked. + (i32.add (local.get $x) (i32.const 1)) + ) + (i32.add (local.get $x) (i32.const 1)) + ) + ) + + ;; CHECK: (func $tee-set-before (type $3) (param $struct (ref $struct)) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (struct.atomic.get acqrel $struct 0 + ;; CHECK-NEXT: (block (result (ref $struct)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $x + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $tee-set-before (param $struct (ref $struct)) (result i32) + (local $x i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (local.tee $x + (block (result i32) + ;; This set is executed before the tee, so it does not block + ;; optimization. + (local.set $x (i32.const 1)) + (i32.const 0) + ) + ) + (local.get $x) + ) + ) + + ;; CHECK: (func $tee-set-middle-lhs (type $3) (param $struct (ref $struct)) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (struct.atomic.get acqrel $struct 0 + ;; CHECK-NEXT: (block (result (ref $struct)) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $x + ;; CHECK-NEXT: (br_if $l + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $tee-set-middle-lhs (param $struct (ref $struct)) (result i32) + (local $x i32) + (block $l (result i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (local.tee $x + (br_if $l + (i32.const 0) + ;; The condition is evaluated after the LHS value, so this would + ;; normally block optimization. However, it is evaluated before the + ;; local.tee, so actually it is fine. + (block (result i32) + (local.set $x (i32.const 1)) + (i32.const 0) + ) + ) + ) + (local.get $x) + ) + ) + ) + + ;; CHECK: (func $tee-set-middle-rhs (type $3) (param $struct (ref $struct)) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: (local.tee $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $tee-set-middle-rhs (param $struct (ref $struct)) (result i32) + (local $x i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (local.tee $x + (i32.const 1) + ) + (block (result i32) + ;; This set changes the value of $x and blocks optimization. + (local.set $x (i32.const 2)) + (local.get $x) + ) + ) + ) + + ;; CHECK: (func $tee-set-after (type $3) (param $struct (ref $struct)) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $l (result i32) + ;; CHECK-NEXT: (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: (local.tee $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $l + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $tee-set-after (param $struct (ref $struct)) (result i32) + (local $x i32) + (block $l (result i32) + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.get $struct) + (local.tee $x + (i32.const 1) + ) + (br_if $l + (local.get $x) + ;; The condition is evaluated after the value, so its effects do not + ;; block optimization. + ;; TODO: getImmediateFallthroughPtr once again blocks this + ;; optimization. + (block (result i32) + (local.set $x (i32.const 2)) + (i32.const 0) + ) + ) + ) + ) + ) +) From 8531324a9d2f5c04ae69d769f68c4d5910f28fe1 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 28 May 2026 22:31:29 -0700 Subject: [PATCH 166/168] [wasm-split] Move computeTransitiveGlobals into getUsedNames (NFC) (#8790) This doesn't have to be a separate function outside `getUsedNames`. --- src/ir/module-splitting.cpp | 49 ++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/src/ir/module-splitting.cpp b/src/ir/module-splitting.cpp index 95abc3306da..619b00d92b6 100644 --- a/src/ir/module-splitting.cpp +++ b/src/ir/module-splitting.cpp @@ -773,6 +773,27 @@ void ModuleSplitter::shareImportableItems() { break; } } + + // Compute the transitive closure of globals referenced in other globals' + // initializers. Since globals can reference other globals, we must ensure + // that if a global is used in a module, all its dependencies are also + // marked as used. + UniqueNonrepeatingDeferredQueue worklist; + for (auto global : used.globals) { + worklist.push(global); + } + while (!worklist.empty()) { + Name name = worklist.pop(); + // At this point all globals are still in the primary module, so this + // exists + auto* global = primary.getGlobal(name); + if (!global->imported() && global->init) { + for (auto* get : FindAll(global->init).list) { + worklist.push(get->name); + used.globals.insert(get->name); + } + } + } return used; }; @@ -804,34 +825,6 @@ void ModuleSplitter::shareImportableItems() { } } - // Compute the transitive closure of globals referenced in other globals' - // initializers. Since globals can reference other globals, we must ensure - // that if a global is used in a module, all its dependencies are also marked - // as used. - auto computeTransitiveGlobals = [&](UsedNames& used) { - UniqueNonrepeatingDeferredQueue worklist; - for (auto global : used.globals) { - worklist.push(global); - } - while (!worklist.empty()) { - Name name = worklist.pop(); - // At this point all globals are still in the primary module, so this - // exists - auto* global = primary.getGlobal(name); - if (!global->imported() && global->init) { - for (auto* get : FindAll(global->init).list) { - worklist.push(get->name); - used.globals.insert(get->name); - } - } - } - }; - - computeTransitiveGlobals(primaryUsed); - for (auto& used : secondaryUsed) { - computeTransitiveGlobals(used); - } - // Given a name and module item kind, returns the list of secondary modules // using that name auto getUsingSecondaries = [&](const Name& name, auto UsedNames::* field) { From 806fab60bbdbe87794a13cd18b0efca2a13b854a Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 29 May 2026 16:38:22 -0700 Subject: [PATCH 167/168] update_lit_tests.py: Do not leak results between modules (#8791) Use an internal separator when doing `foreach`, i.e., when there are multiple modules. --- scripts/update_lit_checks.py | 82 +++++++++++++++++++++++++++++++----- test/lit/exec/start.wast | 7 +-- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/scripts/update_lit_checks.py b/scripts/update_lit_checks.py index 1b1b8e1cc4e..d399fae8493 100755 --- a/scripts/update_lit_checks.py +++ b/scripts/update_lit_checks.py @@ -27,6 +27,10 @@ import sys import tempfile +from test import support + +INTERNAL_SEPARATOR = '[update-lit-checks-separator]' + script_dir = os.path.dirname(__file__) script_name = os.path.basename(__file__) @@ -108,8 +112,27 @@ def run_command(args, test, tmp, command): command = command.replace('%s', test) command = command.replace('%S', os.path.dirname(test)) command = command.replace('%t', tmp) - command = command.replace('foreach', os.path.join(script_dir, 'foreach.py')) - return subprocess.check_output(command, shell=True, env=env).decode('utf-8') + + match = re.match(r'^(.*)\s*foreach\s+(\S+)\s+(\S+)\s+(.*)$', command) + if match: + prefix = match.group(1) + infile = match.group(2) + tempfile = match.group(3) + cmd_rest = match.group(4) + + outputs = [] + for i, (module, _asserts) in enumerate(support.split_wast(infile)): + tempname = tempfile + '.' + str(i) + with open(tempname, 'w') as temp: + print(module, file=temp) + new_command = prefix + ' ' + cmd_rest + ' ' + tempname + out = subprocess.check_output(new_command, shell=True, env=env).decode('utf-8') + outputs.append(out) + + return f"\n{INTERNAL_SEPARATOR}\n".join(outputs) + else: + assert 'foreach' not in command, 'bad foreach matching: ' + command + return subprocess.check_output(command, shell=True, env=env).decode('utf-8') def find_end(module, start): @@ -205,10 +228,31 @@ def parse_output_fuzz_exec(text, first_named_item): return [items] -def get_command_output(args, kind, test, lines, tmp, named_items): +def split_outputs(output): + return re.split(r'\n?' + re.escape(INTERNAL_SEPARATOR) + r'\n?', output) + + +def get_modules_named_items(lines): + # Return a list, one entry per module, each entry being the named items for + # that module. + modules_text = split_modules('\n'.join(lines)) + modules_named_items = [] + for module_text in modules_text: + named_items = [] + for line in module_text.split('\n'): + match = ITEM_RE.match(line) + if match: + _, kind, name = indentKindName(match) + named_items.append((kind, name)) + modules_named_items.append(named_items) + return modules_named_items + + +def get_command_output(args, kind, test, lines, tmp): # Return list of maps from prefixes to lists of module items of the form # ((kind, name), [line]). The outer list has an entry for each module. command_output = [] + modules_named_items = get_modules_named_items(lines) for line in find_run_lines(test, lines): commands = [cmd.strip() for cmd in line.rsplit('|', 1)] if (len(commands) > 2 or @@ -229,12 +273,30 @@ def get_command_output(args, kind, test, lines, tmp, named_items): output = run_command(args, test, tmp, commands[0]) if prefix: - if kind == 'wat': - module_outputs = parse_output_modules(output) - elif kind == 'fuzz-exec': - module_outputs = parse_output_fuzz_exec(output, named_items[0]) - else: - assert False, "unknown output kind" + outputs = split_outputs(output) + module_outputs = [] + if len(outputs) != len(modules_named_items): + warn(f'Mismatch between output parts ({len(outputs)}) and ' + f'input modules ({len(modules_named_items)}).') + for i, out in enumerate(outputs): + if i >= len(modules_named_items): + break + mod_named_items = modules_named_items[i] + first_named_item = mod_named_items[0] if mod_named_items else None + if kind == 'wat': + mod_out = parse_output_modules(out) + if mod_out: + module_outputs.append(mod_out[0]) + else: + module_outputs.append([]) + elif kind == 'fuzz-exec': + mod_out = parse_output_fuzz_exec(out, first_named_item) + if mod_out: + module_outputs.append(mod_out[0]) + else: + module_outputs.append([]) + else: + assert False, "unknown output kind" for i in range(len(module_outputs)): if len(command_output) == i: command_output.append({}) @@ -265,7 +327,7 @@ def update_test(args, test, lines, tmp): _, kind, name = indentKindName(match) named_items.append((kind, name)) - command_output = get_command_output(args, output_kind, test, lines, tmp, named_items) + command_output = get_command_output(args, output_kind, test, lines, tmp) prefixes = {prefix for module_output in command_output for prefix in module_output.keys()} check_line_re = re.compile(r'^\s*;;\s*(' + '|'.join(prefixes) + diff --git a/test/lit/exec/start.wast b/test/lit/exec/start.wast index edd11152421..cdb44617ac6 100644 --- a/test/lit/exec/start.wast +++ b/test/lit/exec/start.wast @@ -16,18 +16,14 @@ ;; CHECK: [fuzz-exec] export run ;; CHECK-NEXT: [fuzz-exec] note result: run => 1 - ;; CHECK-NEXT: [trap unreachable] - ;; CHECK-NEXT: [exception thrown: start] (func $run (export "run") (result i32) - ;; Due to limitations of the auto-updater, the trap and exception from the - ;; following two modules gets logged here. (There is at least no - ;; ambiguity: we first see that we finished ok and returned a value.) (global.get $global) ) ) ;; A trapping start prevents any export from running. (module + ;; CHECK: [trap unreachable] (start $trap) (func $trap @@ -41,6 +37,7 @@ ;; A throwing start prevents any export from running. (module + ;; CHECK: [exception thrown: start] (tag $tag) (start $throw) From 5d704ad52bc77a258e8fa3f9d34fcc5e8799c1c3 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 1 Jun 2026 14:00:57 -0700 Subject: [PATCH 168/168] Version 130 (#8793) --- CHANGELOG.md | 14 +++++++++++--- CMakeLists.txt | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b598836ed..12a2e838529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,16 @@ full changeset diff at the end of each section. Current Trunk ------------- - - Rename relaxed SIMD instructions to prepend the `relaxed_` prefix. +v130 +---- + + - MarkJSCalled pass, to help configureAll users. (#8733) + - RemoveExports pass, to allow easy export removal (e.g. after merge) (#8670) + - Wide Arithmetic support (#8544) + - New fuzzer mode: PreserveImportsExportsJS (#8592) + - New fuzzer mode: Fuzz against JavaScript (#8655) + - Increase Alpine stack size to 8MB for release builds (#8595) + - Rename relaxed SIMD instructions to prepend the `relaxed_` prefix. (#8673) - Rename C and JS API operations to prepend the `Relaxed` prefix: - `LaneselectI8x16` to `RelaxedLaneselectI8x16` - `LaneselectI16x8` to `RelaxedLaneselectI16x8` @@ -23,7 +32,7 @@ Current Trunk - `LaneselectI64x2` to `RelaxedLaneselectI64x2` - `DotI8x16I7x16AddSToVecI32x4` to `RelaxedDotI8x16I7x16AddSToVecI32x4` - `DotI8x16I7x16SToVecI16x8` to `RelaxedDotI8x16I7x16SToVecI16x8` - - Rename `MemorySegment` functions to `DataSegment` in the c and js apis + - [JS & C API] Rename MemorySegment functions to DataSegment (#8576) - Rename `BinaryenGetNumMemorySegments` to `BinaryenGetNumDataSegments` in c api. - Rename `BinaryenGetMemorySegmentByteOffset` to `BinaryenGetDataSegmentByteOffset` in c api. - Rename `BinaryenGetMemorySegmentByteLength` to `BinaryenGetDataSegmentByteLength` in c api. @@ -31,7 +40,6 @@ Current Trunk - Rename `BinaryenCopyMemorySegmentData` to `BinaryenCopyDataSegmentData` in c api. - Rename `module.getNumMemorySegments` to `module.getNumDataSegments` in js api. - Rename `module.getMemorySegmentInfo` to `module.getDataSegmentInfo` in js api. - - Add C and JS APIs for the Wide Arithmetic proposal (#8660). v129 ---- diff --git a/CMakeLists.txt b/CMakeLists.txt index aa6797b600e..fb4bb241a77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.16.3) # Needed for C++17 (std::path) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") -project(binaryen LANGUAGES C CXX VERSION 129) +project(binaryen LANGUAGES C CXX VERSION 130) include(GNUInstallDirs) # The C++ standard whose features are required to build Binaryen.