Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/fuzz_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2723,6 +2723,7 @@ def write_commands(commands, filename):
("--generate-stack-ir",),
("--licm",),
("--local-subtyping",),
("--mark-noreturn",),
("--memory-packing",),
("--merge-blocks",),
('--merge-locals',),
Expand Down
26 changes: 17 additions & 9 deletions src/ir/lubs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,20 @@ namespace wasm {

namespace LUB {

LUBFinder getResultsLUB(Function* func, Module& wasm) {
LUBFinder getResultsLUB(Function* func, Module& wasm, Mode mode) {
LUBFinder lub;

if (!wasm.features.hasGC()) {
return lub;
}

Type originalType = func->getResults();
if (!originalType.hasRef()) {
// Nothing to refine.
return lub;

if (mode == RefinableTypesOnly) {
if (!wasm.features.hasGC()) {
return lub;
}

if (!originalType.hasRef()) {
// Nothing to refine.
return lub;
}
}

// Before we do anything, we must refinalize the function, because otherwise
Expand All @@ -57,8 +60,13 @@ LUBFinder getResultsLUB(Function* func, Module& wasm) {

Finder(Module& wasm, LUBFinder& lub) : wasm(wasm), lub(lub) {}

void visitReturn(Return* curr) { lub.note(curr->value->type); }
void visitReturn(Return* curr) {
lub.note(curr->value ? curr->value->type : Type::none);
}
void visitCall(Call* curr) {
// TODO: propagation through calls (if the called function does not
// return, we do not; also the tail-called function's results can
// be taken into account)
if (curr->isReturn) {
lub.note(wasm.getFunction(curr->target)->getResults());

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Member Author

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

}
Expand Down
15 changes: 14 additions & 1 deletion src/ir/lubs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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

Expand Down
1 change: 1 addition & 0 deletions src/passes/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ set(passes_SOURCES
LoopInvariantCodeMotion.cpp
MakeSharedObjects.cpp
MarkJSCalled.cpp
MarkNoReturn.cpp
Memory64Lowering.cpp
MemoryPacking.cpp
MergeBlocks.cpp
Expand Down
57 changes: 57 additions & 0 deletions src/passes/MarkNoReturn.cpp

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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
3 changes: 3 additions & 0 deletions src/passes/pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,9 @@ void PassRegistry::registerPasses() {
registerPass("mark-js-called",
"mark js called functions (using configureAll) as doing so",
createMarkJSCalledPass);
registerPass("mark-noreturn",
"mark functions that do not return, enabling later opts",
createMarkNoReturnPass);
registerPass("memory64-lowering",
"lower loads and stores to a 64-bit memory to instead use a "
"32-bit one",
Expand Down
1 change: 1 addition & 0 deletions src/passes/passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Pass* createLLVMMemoryCopyFillLoweringPass();
Pass* createLoopInvariantCodeMotionPass();
Pass* createMakeSharedObjectsPass();
Pass* createMarkJSCalledPass();
Pass* createMarkNoReturnPass();
Pass* createMemory64LoweringPass();
Pass* createMemoryPackingPass();
Pass* createMergeBlocksPass();
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm-metadce.test
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@
;; CHECK-NEXT: --mark-js-called mark js called functions (using
;; CHECK-NEXT: configureAll) as doing so
;; CHECK-EMPTY:
;; CHECK-NEXT: --mark-noreturn mark functions that do not
;; CHECK-NEXT: return, enabling later opts
;; CHECK-EMPTY:
;; CHECK-NEXT: --memory-packing packs memory into separate
;; CHECK-NEXT: segments, skipping zeros
;; CHECK-EMPTY:
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm-opt.test
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,9 @@
;; CHECK-NEXT: --mark-js-called mark js called functions (using
;; CHECK-NEXT: configureAll) as doing so
;; CHECK-EMPTY:
;; CHECK-NEXT: --mark-noreturn mark functions that do not
;; CHECK-NEXT: return, enabling later opts
;; CHECK-EMPTY:
;; CHECK-NEXT: --memory-packing packs memory into separate
;; CHECK-NEXT: segments, skipping zeros
;; CHECK-EMPTY:
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm2js.test
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@
;; CHECK-NEXT: --mark-js-called mark js called functions (using
;; CHECK-NEXT: configureAll) as doing so
;; CHECK-EMPTY:
;; CHECK-NEXT: --mark-noreturn mark functions that do not
;; CHECK-NEXT: return, enabling later opts
;; CHECK-EMPTY:
;; CHECK-NEXT: --memory-packing packs memory into separate
;; CHECK-NEXT: segments, skipping zeros
;; CHECK-EMPTY:
Expand Down
62 changes: 62 additions & 0 deletions test/lit/passes/mark-noreturn.wast
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)
)
)
Loading