Repository navigation
Add a MarkNoReturn pass #9229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a MarkNoReturn pass #9229
Changes from all commits
4237b45
3b26cd5
bc08b58
3c1c385
852050e
0cff222
6e5bdc7
cb34d93
761311a
f29a01c
0a81475
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,7 +65,20 @@ namespace LUB { | |
| // it does a refinalize in order to be able to compute the new types. We could | ||
| // roll back that change, but it's not harmful and can help, so we keep it | ||
| // regardless. | ||
| LUBFinder getResultsLUB(Function* func, Module& wasm); | ||
| enum Mode { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess this is a performance optimization? How much does it matter? It would be nice to unify the handling if possible.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is significant, I think. Without this flag, we scan the entire module unnecessarily, rather than just functions returning a GC type. |
||
| // In the default mode, we only look for types we can refine: GC types. This | ||
| // is used in passes that refine function results. | ||
| RefinableTypesOnly, | ||
|
|
||
| // We can also look for all results, including MVP types and none (no result). | ||
| // These cannot be normally refined, but we may find that that there is no | ||
| // return at all, in which case their effective return type is unreachable, | ||
| // and callers know that calls to them do not return. | ||
| AllResults | ||
| }; | ||
|
|
||
| LUBFinder | ||
| getResultsLUB(Function* func, Module& wasm, Mode mode = RefinableTypesOnly); | ||
|
|
||
| } // namespace LUB | ||
|
|
||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be better to do this as a part of the existing passes that refine result types, since they can find the fixed point in the case of cycles.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Heh, that was my first idea, which you can see here: https://github.com/WebAssembly/binaryen/compare/main...kripken:binaryen:dae.noreturn?expand=1 Unfortunately it ends up adding a bunch of work inside each pass. It seems cleaner and simpler to just mark first, independently. But I do agree cycles etc. are worth handling, I intend to add that part in those passes later. That is, after this pass marks functions, specific passes can use that information to handle cycles. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
|
|
||
| // | ||
| // Applies @binaryen.noreturn to relevant functions. The OptimizeNoReturn pass | ||
| // can then use that information. | ||
| // | ||
| // This pass is not run by default, as then the annotations would persist if the | ||
| // user forgets to strip them. TODO: perhaps run this by default but also clean | ||
| // up the annotations at the end of the pipeline? | ||
| // | ||
|
|
||
| #include "ir/lubs.h" | ||
| #include "pass.h" | ||
| #include "wasm.h" | ||
|
|
||
| namespace wasm { | ||
|
|
||
| struct MarkNoReturn : public Pass { | ||
| void run(Module* module) override { | ||
| // MarkNoReturn itself cannot be function-parallel, as it adds | ||
| // annotations, which could race with passes that read them. We are | ||
| // therefore a module-level pass, internally parallel. | ||
| struct Inner : public WalkerPass<PostWalker<Inner>> { | ||
| bool isFunctionParallel() override { return true; } | ||
|
|
||
| std::unique_ptr<Pass> create() override { | ||
| return std::make_unique<Inner>(); | ||
| } | ||
|
|
||
| void doWalkFunction(Function* func) { | ||
| if (!LUB::getResultsLUB(func, *getModule(), LUB::AllResults).noted()) { | ||
| func->funcAnnotations.noReturn = true; | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| Inner().run(getPassRunner(), module); | ||
| } | ||
| }; | ||
|
|
||
| Pass* createMarkNoReturnPass() { return new MarkNoReturn(); } | ||
|
|
||
| } // namespace wasm |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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-opt --mark-noreturn -all -S -o - | filecheck %s | ||
|
|
||
| (module | ||
| ;; CHECK: (type $0 (func)) | ||
|
|
||
| ;; CHECK: (type $1 (func (result i32))) | ||
|
|
||
| ;; CHECK: (type $2 (func (result anyref))) | ||
|
|
||
| ;; CHECK: (func $none (type $0) | ||
| ;; CHECK-NEXT: (nop) | ||
| ;; CHECK-NEXT: ) | ||
| (func $none | ||
| ;; Nothing happens here. | ||
| (nop) | ||
| ) | ||
|
|
||
| ;; CHECK: (@binaryen.noreturn) | ||
| ;; CHECK-NEXT: (func $none-noreturn (type $0) | ||
| ;; CHECK-NEXT: (unreachable) | ||
| ;; CHECK-NEXT: ) | ||
| (func $none-noreturn | ||
| ;; This never returns, and will be annotated with @binaryen.noreturn. | ||
| (unreachable) | ||
| ) | ||
|
|
||
| ;; CHECK: (func $i32 (type $1) (result i32) | ||
| ;; CHECK-NEXT: (i32.const 42) | ||
| ;; CHECK-NEXT: ) | ||
| (func $i32 (result i32) | ||
| ;; Nothing happens here. | ||
| (i32.const 42) | ||
| ) | ||
|
|
||
| ;; CHECK: (@binaryen.noreturn) | ||
| ;; CHECK-NEXT: (func $i32-noreturn (type $1) (result i32) | ||
| ;; CHECK-NEXT: (unreachable) | ||
| ;; CHECK-NEXT: ) | ||
| (func $i32-noreturn (result i32) | ||
| ;; This never returns, and will be annotated with @binaryen.noreturn. | ||
| (unreachable) | ||
| ) | ||
|
|
||
| ;; CHECK: (func $gc (type $2) (result anyref) | ||
| ;; CHECK-NEXT: (ref.null none) | ||
| ;; CHECK-NEXT: ) | ||
| (func $gc (result anyref) | ||
| ;; Nothing happens here. | ||
| (ref.null any) | ||
| ) | ||
|
|
||
| ;; CHECK: (@binaryen.noreturn) | ||
| ;; CHECK-NEXT: (func $gc-noreturn (type $2) (result anyref) | ||
| ;; CHECK-NEXT: (unreachable) | ||
| ;; CHECK-NEXT: ) | ||
| (func $gc-noreturn (result anyref) | ||
| ;; This never returns, and will be annotated with @binaryen.noreturn. | ||
| (unreachable) | ||
| ) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't going to handle cases where the tail-called function itself does not return or has refinable results.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I'll add a TODO for propagating noreturn