From e6469c966037a208bd93b7ee99595d3bf8fd5169 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 8 Oct 2026 13:00:32 -0700 Subject: [PATCH 01/12] yolo --- src/ir/function-utils.h | 14 +- src/pass.h | 4 + src/passes/DuplicateFunctionElimination.cpp | 7 +- src/tools/optimization-options.h | 42 +++- test/lit/help/wasm-metadce.test | 5 + test/lit/help/wasm-opt.test | 5 + test/lit/help/wasm2js.test | 5 + ...cate-function-elimination_annotations.wast | 230 ++++++++++++++++-- 8 files changed, 282 insertions(+), 30 deletions(-) diff --git a/src/ir/function-utils.h b/src/ir/function-utils.h index 2d3cf9d94fd..43e0647032b 100644 --- a/src/ir/function-utils.h +++ b/src/ir/function-utils.h @@ -26,7 +26,9 @@ namespace wasm::FunctionUtils { // Checks if two functions are equal in all functional aspects, // everything but their name (which can't be the same, in the same // module!) - same params, vars, body, result, etc. -inline bool equal(Function* left, Function* right) { +inline bool equal(Function* left, + Function* right, + const PassOptions& option) { if (left->type != right->type) { return false; } @@ -44,10 +46,16 @@ inline bool equal(Function* left, Function* right) { // only in metadata (following LLVM's example). If we have a non-optimization // reason for comparing metadata here then we could add a flag for it. We do, // however, compare metadata that changes semantics (not debug info, but - // things like jsCalled). We do that check conservatively, erasing the non- - // semantic-altering ones and then comparing (so that new annotations + // things like removableIfUnused). auto leftAnnotations = Intrinsics::getAnnotations(left); auto rightAnnotations = Intrinsics::getAnnotations(right); + if (options.lastOpts) { + // If these are the last toolchain opts, we do not need to worry about + // inhibiting others by merging a jsCalled function with a non-jsCalled one + // (after the merge, both will be annotated, preventing optimizations that + // would be a problem in the presence of JS calls). + leftAnnotations.jsCalled = rightAnnotations.jsCalled = false; + } if (!leftAnnotations.equalOnSemanticsAltering(rightAnnotations)) { return false; } diff --git a/src/pass.h b/src/pass.h index 8bbf9612a90..310c3e517f1 100644 --- a/src/pass.h +++ b/src/pass.h @@ -239,6 +239,10 @@ struct PassOptions { // that needs lowering, then we lower it, then we can optimize it again to the // original form). bool targetJS = false; + // Whether we are in the last sequence of optimizations being run. If we are, + // then we can perform optimizations that only make sense to do at the very + // end (because they might inhibit further ones after). + bool lastOpts = true; // Arbitrary string arguments from the commandline, which we forward to // passes. std::unordered_map arguments; diff --git a/src/passes/DuplicateFunctionElimination.cpp b/src/passes/DuplicateFunctionElimination.cpp index 88253e00583..b004c6a3e71 100644 --- a/src/passes/DuplicateFunctionElimination.cpp +++ b/src/passes/DuplicateFunctionElimination.cpp @@ -83,10 +83,13 @@ struct DuplicateFunctionElimination : public Pass { if (duplicates.contains(second->name)) { continue; } - if (FunctionUtils::equal(first, second)) { - // great, we can replace the second with the first! + if (FunctionUtils::equal(first, second, options)) { + // Great, we can replace the second with the first! replacements[second->name] = first->name; duplicates.insert(second->name); + if (second->funcAnnotations.jsCalled) { + first->funcAnnotations.jsCalled = true; + } } } } diff --git a/src/tools/optimization-options.h b/src/tools/optimization-options.h index a43c92c3fe7..a8a75aa74de 100644 --- a/src/tools/optimization-options.h +++ b/src/tools/optimization-options.h @@ -67,6 +67,11 @@ struct OptimizationOptions : public ToolOptions { std::vector passes; + // Normally we assume no wasm-opt or other toolchain opts happen after us, + // but the user can tell us otherwise, which inhibits some opts (ones that + // limit subsequent opts). + bool expectLaterToolchainOpts = false; + // Add a request to run all the default opt passes. They are run with the // current opt and shrink levels specified, which are read from passOptions. // @@ -319,6 +324,15 @@ struct OptimizationOptions : public ToolOptions { [this](Options*, const std::string&) { passOptions.zeroFilledMemory = true; }) + .add("--expect-later-toolchain-opts", + "", + "Expect further invocations of wasm-opt later, avoiding " + "optimizations that might inhibit them", + OptimizationOptionsCategory, + Options::Arguments::Zero, + [this](Options*, const std::string&) { + expectLaterToolchainOpts = true; + }) .add("--skip-pass", "-sp", "Skip a pass (do not run it)", @@ -410,7 +424,28 @@ struct OptimizationOptions : public ToolOptions { passRunner.clear(); }; - for (auto& pass : passes) { + // If we do not expect later toolchain opts, then this invocation of + // wasm-opt is the last one, and the last invocation of the default opts is + // the last one. Find that invocation and mark lastOpts=true there, which + // will then remain true until the end. + Index lastDefaultOptsIndex = Index(-1); + if (!expectLaterToolchainOpts) { + for (Index i = 0; i < passes.size(); ++i) { + if (passes[i].name == DEFAULT_OPT_PASSES) { + lastDefaultOptsIndex = i; + } + } + + if (lastDefaultOptsIndex == Index(-1)) { + // No default opts run at all. Just mark lastOpts now, so it applies to + // the non-default opts (same as it would apply to all non-default opts + // after the last default ones). + passRunner.options.lastOpts = true; + } + } + + for (Index i = 0; i < passes.size(); ++i) { + auto& pass = passes[i]; if (pass.name == DEFAULT_OPT_PASSES) { // This is something like -O3 or -Oz. We must run this now, in order to // set the proper opt and shrink levels. To do that, first reset the @@ -428,6 +463,11 @@ struct OptimizationOptions : public ToolOptions { passRunner.options.optimizeLevel = *pass.optimizeLevel; passRunner.options.shrinkLevel = *pass.shrinkLevel; + // If these are the last default opts, mark that. + if (i == lastDefaultOptsIndex) { + passRunner.options.lastOpts = true; + } + // Run our optimizations now with the custom levels. passRunner.addDefaultOptimizationPasses(); flush(); diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index 38b112e5820..ea45a3ce6fe 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -696,6 +696,11 @@ ;; CHECK-NEXT: --zero-filled-memory,-uim Assume that an imported memory ;; CHECK-NEXT: will be zero-initialized ;; CHECK-EMPTY: +;; CHECK-NEXT: --expect-later-toolchain-opts Expect further invocations of +;; CHECK-NEXT: wasm-opt later, avoiding +;; CHECK-NEXT: optimizations that might inhibit +;; CHECK-NEXT: them +;; CHECK-EMPTY: ;; CHECK-NEXT: --skip-pass,-sp Skip a pass (do not run it) ;; CHECK-EMPTY: ;; CHECK-EMPTY: diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 058e63d144d..90c3b2990a3 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -741,6 +741,11 @@ ;; CHECK-NEXT: --zero-filled-memory,-uim Assume that an imported memory ;; CHECK-NEXT: will be zero-initialized ;; CHECK-EMPTY: +;; CHECK-NEXT: --expect-later-toolchain-opts Expect further invocations of +;; CHECK-NEXT: wasm-opt later, avoiding +;; CHECK-NEXT: optimizations that might inhibit +;; CHECK-NEXT: them +;; CHECK-EMPTY: ;; CHECK-NEXT: --skip-pass,-sp Skip a pass (do not run it) ;; CHECK-EMPTY: ;; CHECK-EMPTY: diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index fa0be35fa25..fdce36ac338 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -660,6 +660,11 @@ ;; CHECK-NEXT: --zero-filled-memory,-uim Assume that an imported memory ;; CHECK-NEXT: will be zero-initialized ;; CHECK-EMPTY: +;; CHECK-NEXT: --expect-later-toolchain-opts Expect further invocations of +;; CHECK-NEXT: wasm-opt later, avoiding +;; CHECK-NEXT: optimizations that might inhibit +;; CHECK-NEXT: them +;; CHECK-EMPTY: ;; CHECK-NEXT: --skip-pass,-sp Skip a pass (do not run it) ;; CHECK-EMPTY: ;; CHECK-EMPTY: diff --git a/test/lit/passes/duplicate-function-elimination_annotations.wast b/test/lit/passes/duplicate-function-elimination_annotations.wast index 64ed8c0f471..d6816fa650c 100644 --- a/test/lit/passes/duplicate-function-elimination_annotations.wast +++ b/test/lit/passes/duplicate-function-elimination_annotations.wast @@ -1,18 +1,20 @@ ;; 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 --duplicate-function-elimination --all-features -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt --duplicate-function-elimination --all-features -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt --duplicate-function-elimination --all-features --expect-later-toolchain-opts -S -o - | filecheck %s --check-prefix=LATER ;; We should not merge functions differing in semantics-altering annotations -;; like js.called and removable.if.unused. +;; like removable.if.unused, or js.called when expecting later toolchain opts. -;; One function has the js.called hint, so we do not merge. +;; One function has the js.called hint, so we merge only if we do not expect +;; later toolchain opts (and keep the hint on the merged function). (module ;; CHECK: (type $0 (func (param i32))) ;; CHECK: (export "a" (func $a)) - ;; CHECK: (export "b" (func $b)) + ;; CHECK: (export "b" (func $a)) ;; CHECK: (@binaryen.js.called) ;; CHECK-NEXT: (func $a (type $0) (param $x i32) @@ -23,6 +25,21 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (type $0 (func (param i32))) + + ;; LATER: (export "a" (func $a)) + + ;; LATER: (export "b" (func $b)) + + ;; LATER: (@binaryen.js.called) + ;; LATER-NEXT: (func $a (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (@binaryen.js.called) (func $a (export "a") (param $x i32) (if @@ -33,14 +50,14 @@ ) ) - ;; CHECK: (func $b (type $0) (param $x i32) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) + ;; LATER: (func $b (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (func $b (export "b") (param $x i32) (if (local.get $x) @@ -51,15 +68,17 @@ ) ) -;; Flipped, now other has it. We still do not merge. +;; Flipped, now other has it. Again, we merge only if we do not expect later +;; toolchain opts (and keep the hint on the merged function). (module ;; CHECK: (type $0 (func (param i32))) ;; CHECK: (export "a" (func $a)) - ;; CHECK: (export "b" (func $b)) + ;; CHECK: (export "b" (func $a)) - ;; CHECK: (func $a (type $0) (param $x i32) + ;; CHECK: (@binaryen.js.called) + ;; CHECK-NEXT: (func $a (type $0) (param $x i32) ;; CHECK-NEXT: (if ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: (then @@ -67,6 +86,20 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (type $0 (func (param i32))) + + ;; LATER: (export "a" (func $a)) + + ;; LATER: (export "b" (func $b)) + + ;; LATER: (func $a (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (func $a (export "a") (param $x i32) (if (local.get $x) @@ -76,15 +109,15 @@ ) ) - ;; CHECK: (@binaryen.js.called) - ;; CHECK-NEXT: (func $b (type $0) (param $x i32) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) + ;; LATER: (@binaryen.js.called) + ;; LATER-NEXT: (func $b (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (@binaryen.js.called) (func $b (export "b") (param $x i32) (if @@ -113,6 +146,21 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (type $0 (func (param i32))) + + ;; LATER: (export "a" (func $a)) + + ;; LATER: (export "b" (func $a)) + + ;; LATER: (@binaryen.js.called) + ;; LATER-NEXT: (func $a (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (@binaryen.js.called) (func $a (export "a") (param $x i32) (if @@ -151,6 +199,21 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (type $0 (func (param i32))) + + ;; LATER: (export "a" (func $a)) + + ;; LATER: (export "b" (func $b)) + + ;; LATER: (@binaryen.removable.if.unused) + ;; LATER-NEXT: (func $a (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (@binaryen.removable.if.unused) (func $a (export "a") (param $x i32) (if @@ -169,6 +232,14 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (func $b (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (func $b (export "b") (param $x i32) (if (local.get $x) @@ -196,6 +267,21 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (type $0 (func (param i32))) + + ;; LATER: (export "a" (func $a)) + + ;; LATER: (export "b" (func $b)) + + ;; LATER: (@binaryen.removable.if.unused) + ;; LATER-NEXT: (func $a (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (@binaryen.removable.if.unused) (func $a (export "a") (param $x i32) (if @@ -215,6 +301,15 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (@binaryen.js.called) + ;; LATER-NEXT: (func $b (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (@binaryen.js.called) (func $b (export "b") (param $x i32) (if @@ -244,6 +339,22 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (type $0 (func (param i32))) + + ;; LATER: (export "a" (func $a)) + + ;; LATER: (export "b" (func $b)) + + ;; LATER: (@binaryen.removable.if.unused) + ;; LATER-NEXT: (@binaryen.js.called) + ;; LATER-NEXT: (func $a (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (@binaryen.removable.if.unused) (@binaryen.js.called) (func $a (export "a") (param $x i32) @@ -264,6 +375,15 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (@binaryen.js.called) + ;; LATER-NEXT: (func $b (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (@binaryen.js.called) (func $b (export "b") (param $x i32) (if @@ -294,6 +414,22 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (type $0 (func (param i32))) + + ;; LATER: (export "a" (func $a)) + + ;; LATER: (export "b" (func $a)) + + ;; LATER: (@binaryen.removable.if.unused) + ;; LATER-NEXT: (@binaryen.js.called) + ;; LATER-NEXT: (func $a (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (@binaryen.removable.if.unused) (@binaryen.js.called) (func $a (export "a") (param $x i32) @@ -334,6 +470,21 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (type $0 (func (param i32))) + + ;; LATER: (export "a" (func $a)) + + ;; LATER: (export "b" (func $b)) + + ;; LATER: (@binaryen.idempotent) + ;; LATER-NEXT: (func $a (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (@binaryen.idempotent) (func $a (export "a") (param $x i32) (if @@ -352,6 +503,14 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (func $b (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (func $b (export "b") (param $x i32) (if (local.get $x) @@ -379,6 +538,21 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (type $0 (func (param i32))) + + ;; LATER: (export "a" (func $a)) + + ;; LATER: (export "b" (func $b)) + + ;; LATER: (@binaryen.noreturn) + ;; LATER-NEXT: (func $a (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (@binaryen.noreturn) (func $a (export "a") (param $x i32) (if @@ -397,6 +571,14 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LATER: (func $b (type $0) (param $x i32) + ;; LATER-NEXT: (if + ;; LATER-NEXT: (local.get $x) + ;; LATER-NEXT: (then + ;; LATER-NEXT: (unreachable) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) + ;; LATER-NEXT: ) (func $b (export "b") (param $x i32) (if (local.get $x) From 68b6f8cbec0e1f3e8d9f6a153b009219612a06a6 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 8 Oct 2026 13:00:45 -0700 Subject: [PATCH 02/12] yolo --- src/ir/function-utils.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ir/function-utils.h b/src/ir/function-utils.h index 43e0647032b..007ba56da76 100644 --- a/src/ir/function-utils.h +++ b/src/ir/function-utils.h @@ -26,9 +26,7 @@ namespace wasm::FunctionUtils { // Checks if two functions are equal in all functional aspects, // everything but their name (which can't be the same, in the same // module!) - same params, vars, body, result, etc. -inline bool equal(Function* left, - Function* right, - const PassOptions& option) { +inline bool equal(Function* left, Function* right, const PassOptions& option) { if (left->type != right->type) { return false; } From 5deac42690068cead91d18184df19e16bc3cd4b1 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 8 Oct 2026 13:01:25 -0700 Subject: [PATCH 03/12] yolo --- src/ir/function-utils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ir/function-utils.h b/src/ir/function-utils.h index 007ba56da76..92e814c6434 100644 --- a/src/ir/function-utils.h +++ b/src/ir/function-utils.h @@ -26,7 +26,7 @@ namespace wasm::FunctionUtils { // Checks if two functions are equal in all functional aspects, // everything but their name (which can't be the same, in the same // module!) - same params, vars, body, result, etc. -inline bool equal(Function* left, Function* right, const PassOptions& option) { +inline bool equal(Function* left, Function* right, const PassOptions& options) { if (left->type != right->type) { return false; } From 59866910513ccc820c3ed4adf34048e74bfee1b3 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 8 Oct 2026 13:09:25 -0700 Subject: [PATCH 04/12] test --- .../duplicate-function-elimination_annotations.wast | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/lit/passes/duplicate-function-elimination_annotations.wast b/test/lit/passes/duplicate-function-elimination_annotations.wast index d6816fa650c..d70c3d5f400 100644 --- a/test/lit/passes/duplicate-function-elimination_annotations.wast +++ b/test/lit/passes/duplicate-function-elimination_annotations.wast @@ -4,9 +4,6 @@ ;; RUN: foreach %s %t wasm-opt --duplicate-function-elimination --all-features -S -o - | filecheck %s ;; RUN: foreach %s %t wasm-opt --duplicate-function-elimination --all-features --expect-later-toolchain-opts -S -o - | filecheck %s --check-prefix=LATER -;; We should not merge functions differing in semantics-altering annotations -;; like removable.if.unused, or js.called when expecting later toolchain opts. - ;; One function has the js.called hint, so we merge only if we do not expect ;; later toolchain opts (and keep the hint on the merged function). (module @@ -129,7 +126,7 @@ ) ) -;; Both have it, so we do merge. +;; Both have it, so we always merge. (module ;; CHECK: (type $0 (func (param i32))) @@ -182,7 +179,10 @@ ) ) -;; Just one function has removable.if.unused, so we do not merge. +;; Just one function has removable.if.unused, so we do not merge. Unlike +;; js.called, we cannot merge and apply the annotation, as the annotation alters +;; semantics (whereas js.called just warns about something we should not break, +;; so applying it to more places can inhibit opts, but not break things). (module ;; CHECK: (type $0 (func (param i32))) From daa90c92fba00d21a2ca7fe038e2142efd9e8fd3 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 8 Oct 2026 13:11:21 -0700 Subject: [PATCH 05/12] yolo --- scripts/fuzz_opt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 5908a63a569..5365181619d 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2865,6 +2865,9 @@ def get_random_opts(): # value (the same used in j2wasm atm) if random.random() < 0.5: ret += ['-pii=4'] + # fuzz both with and without expecting later toolchain opts + if random.random() < 0.5: + ret += ['--expect-later-toolchain-opts'] # test both closed and open world if CLOSED_WORLD: ret += [CLOSED_WORLD_FLAG] From e37bf6e98ceb18b924f4e6f68173385717ce1084 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 8 Oct 2026 14:56:42 -0700 Subject: [PATCH 06/12] commentses --- src/pass.h | 5 +++-- src/tools/optimization-options.h | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/pass.h b/src/pass.h index 310c3e517f1..54a86f995e0 100644 --- a/src/pass.h +++ b/src/pass.h @@ -239,9 +239,10 @@ struct PassOptions { // that needs lowering, then we lower it, then we can optimize it again to the // original form). bool targetJS = false; - // Whether we are in the last sequence of optimizations being run. If we are, + // Whether we are in the last major sequence of optimizations being run. If so // then we can perform optimizations that only make sense to do at the very - // end (because they might inhibit further ones after). + // end (because they might inhibit further ones after). For example, if we run + // -O3 -O3 then in the second -O3 we might do more things. bool lastOpts = true; // Arbitrary string arguments from the commandline, which we forward to // passes. diff --git a/src/tools/optimization-options.h b/src/tools/optimization-options.h index a8a75aa74de..908687a9d94 100644 --- a/src/tools/optimization-options.h +++ b/src/tools/optimization-options.h @@ -428,6 +428,19 @@ struct OptimizationOptions : public ToolOptions { // wasm-opt is the last one, and the last invocation of the default opts is // the last one. Find that invocation and mark lastOpts=true there, which // will then remain true until the end. + // + // That is, imagine we are called with this: + // + // wasm-opt --dce -O3 --rse -Oz --licm + // ^^ + // Then lastOpts is set here --^^ + // And it applies from then on: [.......] + // That is, we mark lastOpts on the -Oz (after --dce -O3 --rse) and it + // applies to the -Oz and the --licm. + // + // The idea is that the last -O3/-Oz/etc. is the last big cycle of opts, and + // we can assume it is the last (and other specific passes are assumed to be + // "last" if they are after it). Index lastDefaultOptsIndex = Index(-1); if (!expectLaterToolchainOpts) { for (Index i = 0; i < passes.size(); ++i) { From 3b1fecc6836e5778a57e203f5b3c861b678ca068 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 8 Oct 2026 15:16:28 -0700 Subject: [PATCH 07/12] fix --- src/passes/DuplicateFunctionElimination.cpp | 1 + src/tools/optimization-options.h | 1 + test/lit/passes/last-opts.wast | 53 +++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 test/lit/passes/last-opts.wast diff --git a/src/passes/DuplicateFunctionElimination.cpp b/src/passes/DuplicateFunctionElimination.cpp index b004c6a3e71..5436e42d5bb 100644 --- a/src/passes/DuplicateFunctionElimination.cpp +++ b/src/passes/DuplicateFunctionElimination.cpp @@ -42,6 +42,7 @@ struct DuplicateFunctionElimination : public Pass { // we see the functions C1 and C2 that they call are in fact identical. // Rarely, such "chains" can be very long, so we limit how many we do. auto& options = getPassOptions(); +std::cout << "last? " << options.lastOpts << '\n'; Index limit; if (options.optimizeLevel >= 3 || options.shrinkLevel >= 1) { limit = module->functions.size(); // no limit diff --git a/src/tools/optimization-options.h b/src/tools/optimization-options.h index 908687a9d94..360514e97cc 100644 --- a/src/tools/optimization-options.h +++ b/src/tools/optimization-options.h @@ -441,6 +441,7 @@ struct OptimizationOptions : public ToolOptions { // The idea is that the last -O3/-Oz/etc. is the last big cycle of opts, and // we can assume it is the last (and other specific passes are assumed to be // "last" if they are after it). + passRunner.options.lastOpts = false; Index lastDefaultOptsIndex = Index(-1); if (!expectLaterToolchainOpts) { for (Index i = 0; i < passes.size(); ++i) { diff --git a/test/lit/passes/last-opts.wast b/test/lit/passes/last-opts.wast new file mode 100644 index 00000000000..dd6e440ee43 --- /dev/null +++ b/test/lit/passes/last-opts.wast @@ -0,0 +1,53 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; Run DFE early, before the last set of opts. At that early time, we do not +;; ignore the js.called intrinsic, so we do not merge and b. We then extract b, +;; which will remain without the annotation (if we merged, the single remaining +;; function would be annotated). We then run -O3, which is the last set of opts +;; (this defines which the last set of opts is). As this is after the +;; extraction, so there is nothing to merge at that point, so we end up with +;; b, without the js.called annotation. +;; +;; If we considered the first DFE to be in the last set of opts, we'd merge a +;; and b, and the extraction would error on not finding b. +;; +;; RUN: foreach %s %t wasm-opt --duplicate-function-elimination --extract-function=b -O3 -S -o - | filecheck %s + +;; The same thing happens with an -O3 instead of the DFE (-O3 runs that DFE, and +;; it is not in the set of last opts). +;; RUN: foreach %s %t wasm-opt -O3 --extract-function=b -O3 -S -o - | filecheck %s + +(module + (@binaryen.js.called) + (func $a (export "a") (param $x i32) + (if + (local.get $x) + (then + (unreachable) + ) + ) + ) + + ;; CHECK: (type $0 (func (param i32))) + + ;; CHECK: (export "b" (func $b)) + + ;; CHECK: (func $b (param $0 i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $b (export "b") (param $x i32) + ;; b survives, and does not have the js.called annotation. + (if + (local.get $x) + (then + (unreachable) + ) + ) + ) +) + From 401423ed660b42812eef1c8586764cd17599a39e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 8 Oct 2026 15:16:41 -0700 Subject: [PATCH 08/12] fix --- src/passes/DuplicateFunctionElimination.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/passes/DuplicateFunctionElimination.cpp b/src/passes/DuplicateFunctionElimination.cpp index 5436e42d5bb..b004c6a3e71 100644 --- a/src/passes/DuplicateFunctionElimination.cpp +++ b/src/passes/DuplicateFunctionElimination.cpp @@ -42,7 +42,6 @@ struct DuplicateFunctionElimination : public Pass { // we see the functions C1 and C2 that they call are in fact identical. // Rarely, such "chains" can be very long, so we limit how many we do. auto& options = getPassOptions(); -std::cout << "last? " << options.lastOpts << '\n'; Index limit; if (options.optimizeLevel >= 3 || options.shrinkLevel >= 1) { limit = module->functions.size(); // no limit From cf59ab0c4844590211c46b5ad8492160d113ab70 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 8 Oct 2026 15:43:52 -0700 Subject: [PATCH 09/12] safer --- src/pass.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pass.h b/src/pass.h index 54a86f995e0..b92926c076a 100644 --- a/src/pass.h +++ b/src/pass.h @@ -243,7 +243,7 @@ struct PassOptions { // then we can perform optimizations that only make sense to do at the very // end (because they might inhibit further ones after). For example, if we run // -O3 -O3 then in the second -O3 we might do more things. - bool lastOpts = true; + bool lastOpts = false; // Arbitrary string arguments from the commandline, which we forward to // passes. std::unordered_map arguments; From 2c44b54d5032e3b08fc11e45d2ef1c54b042f8e6 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 8 Oct 2026 15:59:42 -0700 Subject: [PATCH 10/12] typo --- test/lit/passes/last-opts.wast | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lit/passes/last-opts.wast b/test/lit/passes/last-opts.wast index dd6e440ee43..095c39e4d89 100644 --- a/test/lit/passes/last-opts.wast +++ b/test/lit/passes/last-opts.wast @@ -1,7 +1,7 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. ;; Run DFE early, before the last set of opts. At that early time, we do not -;; ignore the js.called intrinsic, so we do not merge and b. We then extract b, +;; ignore the js.called intrinsic, so we do not merge a and b. We then extract b, ;; which will remain without the annotation (if we merged, the single remaining ;; function would be annotated). We then run -O3, which is the last set of opts ;; (this defines which the last set of opts is). As this is after the From 579741740386824faf8c41bf29f29efc5512f7b4 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 9 Oct 2026 09:15:22 -0700 Subject: [PATCH 11/12] suggested TODO --- src/tools/optimization-options.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tools/optimization-options.h b/src/tools/optimization-options.h index 360514e97cc..fe8af0e82d6 100644 --- a/src/tools/optimization-options.h +++ b/src/tools/optimization-options.h @@ -441,6 +441,9 @@ struct OptimizationOptions : public ToolOptions { // The idea is that the last -O3/-Oz/etc. is the last big cycle of opts, and // we can assume it is the last (and other specific passes are assumed to be // "last" if they are after it). + // + // TODO: We could maybe add a --begin-last-opts flag (whose position on the + // commandline matters) if we needed more fine-grained control here. passRunner.options.lastOpts = false; Index lastDefaultOptsIndex = Index(-1); if (!expectLaterToolchainOpts) { From dec09782fdfb16cf04879a9c5ee9c4f2fafbac3e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 9 Oct 2026 09:17:49 -0700 Subject: [PATCH 12/12] second.todo --- .../lit/passes/duplicate-function-elimination_annotations.wast | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/lit/passes/duplicate-function-elimination_annotations.wast b/test/lit/passes/duplicate-function-elimination_annotations.wast index d70c3d5f400..804eb6c6c57 100644 --- a/test/lit/passes/duplicate-function-elimination_annotations.wast +++ b/test/lit/passes/duplicate-function-elimination_annotations.wast @@ -182,7 +182,8 @@ ;; Just one function has removable.if.unused, so we do not merge. Unlike ;; js.called, we cannot merge and apply the annotation, as the annotation alters ;; semantics (whereas js.called just warns about something we should not break, -;; so applying it to more places can inhibit opts, but not break things). +;; so applying it to more places can inhibit opts, but not break things). We +;; could *remove* it after merging, however TODO (module ;; CHECK: (type $0 (func (param i32)))