Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
5daee4c
go
kripken Jan 21, 2026
7ce3151
work
kripken Jan 21, 2026
477e77b
work
kripken Jan 21, 2026
83583cb
work
kripken Jan 21, 2026
073b39e
fmt
kripken Jan 21, 2026
f129e51
test
kripken Jan 21, 2026
075fd15
work
kripken Jan 21, 2026
d540a96
work
kripken Jan 21, 2026
4d8b7a5
builds
kripken Jan 21, 2026
e38e29c
progress
kripken Jan 21, 2026
c78dc41
finish
kripken Jan 21, 2026
8ad0110
works
kripken Jan 21, 2026
d876266
UNDO effects.h
kripken Jan 21, 2026
31f6e5c
yolo
kripken Jan 21, 2026
879dc79
works
kripken Jan 21, 2026
eadd3dc
rename
kripken Jan 21, 2026
a49fe26
rename
kripken Jan 21, 2026
c2c1d9d
Merge remote-tracking branch 'origin/main' into callsIfMoved
kripken Jan 27, 2026
dce83d8
refactor
kripken Jan 27, 2026
93234e2
fix
kripken Jan 27, 2026
eeba297
fmrt
kripken Jan 27, 2026
902a233
work
kripken Jan 28, 2026
2e287a2
work
kripken Jan 28, 2026
45650e1
work
kripken Jan 28, 2026
017f8e5
work
kripken Jan 28, 2026
4097eec
work
kripken Jan 28, 2026
694e4f9
work
kripken Jan 28, 2026
a815fb7
simpl
kripken Jan 28, 2026
8214606
Merge remote-tracking branch 'origin/main' into callsIfMoved
kripken Jan 29, 2026
6a88e93
fmrt
kripken Jan 29, 2026
c4ec997
UNDO
kripken Jan 30, 2026
4080447
renamings
kripken Jan 30, 2026
817dc64
finish
kripken Jan 30, 2026
130e5a1
finish
kripken Jan 30, 2026
d1eec83
finish
kripken Jan 30, 2026
a266a05
work
kripken Jan 30, 2026
e474857
work
kripken Jan 30, 2026
8c24e14
Merge remote-tracking branch 'origin/main' into callsIfMoved
kripken Feb 3, 2026
8e89cb4
Merge remote-tracking branch 'origin/main' into callsIfMoved
kripken Feb 4, 2026
0b4d4f9
hhow?
kripken Feb 4, 2026
7cac194
hhow?
kripken Feb 4, 2026
0658143
finish
kripken Feb 5, 2026
a2aea65
fix
kripken Feb 5, 2026
a8857b5
fix
kripken Feb 5, 2026
9b33ec1
fuzz
kripken Feb 5, 2026
ed1466f
test roundtripping of function-level annotation
kripken Feb 5, 2026
dafbdbe
simpler
kripken Feb 5, 2026
708f8b2
just use bool
kripken Feb 5, 2026
f3c73c6
fix
kripken Feb 5, 2026
1ad327c
unfuzz
kripken Feb 5, 2026
6544c89
default
kripken Feb 6, 2026
5baae1f
Merge remote-tracking branch 'origin/main' into callsIfMoved
kripken Feb 9, 2026
3be3f48
rename
kripken Feb 9, 2026
926f3a7
format
kripken Feb 9, 2026
0746c4b
fix
kripken Feb 9, 2026
6fcfc79
fix
kripken Feb 9, 2026
c2d17c3
fix
kripken Feb 9, 2026
871b46e
fix
kripken Feb 9, 2026
c06f0e9
rename tests
kripken Feb 9, 2026
a4b3b6f
Update src/passes/Vacuum.cpp
kripken Feb 10, 2026
7ff7795
Update test/lit/passes/vacuum-removable-if-unused-func.wast
kripken Feb 10, 2026
e98bf88
fmrt
kripken Feb 10, 2026
4c6fb3c
add link to wiki
kripken Feb 10, 2026
1fe24ac
Merge remote-tracking branch 'origin/main' into callsIfMoved
kripken Feb 10, 2026
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
6 changes: 6 additions & 0 deletions scripts/test/fuzzing.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@
# Contains a name with "__fuzz_split", indicating it is emitted by
# wasm-split, confusing the fuzzer because it is in the initial content.
'fuzz_shell_second.wast',
# We cannot fuzz semantics-altering intrinsics, as when we optimize the
# behavior changes.
'removable-if-unused.wast',
'removable-if-unused-func.wast',
'vacuum-removable-if-unused.wast',
'vacuum-removable-if-unused-func.wast',
]


Expand Down
42 changes: 42 additions & 0 deletions src/ir/intrinsics.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,48 @@ class Intrinsics {
std::vector<Name> getConfigureAllFunctions(Call* call);
// As above, but looks through the module to find the configureAll.
std::vector<Name> getConfigureAllFunctions();

// Get the code annotations for an expression in a function.
CodeAnnotation getAnnotations(Expression* curr, Function* func) {
auto& annotations = func->codeAnnotations;
auto iter = annotations.find(curr);
if (iter != annotations.end()) {
return iter->second;
}
return {};
}

// Get the code annotations for a function itself.
CodeAnnotation getAnnotations(Function* func) {
return getAnnotations(nullptr, func);
}

// Given a call in a function, return all the annotations for it. The call may
// be annotated itself (which takes precedence), or the function it calls be
// annotated.
CodeAnnotation getCallAnnotations(Call* call, Function* func) {
// Combine annotations from the call itself and from the called function.
auto ret = getAnnotations(call, func);

// Check on the called function, if it exists (it may not if the IR is still
// being built up).
if (auto* target = module.getFunctionOrNull(call->target)) {
auto funcAnnotations = getAnnotations(target);

// Merge them, giving precedence for the call annotation.
if (!ret.branchLikely) {
ret.branchLikely = funcAnnotations.branchLikely;
}
if (!ret.inline_) {
ret.inline_ = funcAnnotations.inline_;
}
if (!ret.removableIfUnused) {
ret.removableIfUnused = funcAnnotations.removableIfUnused;
}
}

return ret;
}
};

} // namespace wasm
Expand Down
2 changes: 2 additions & 0 deletions src/parser/contexts.h
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,8 @@ struct AnnotationParserCtx {
branchHint = &a;
} else if (a.kind == Annotations::InlineHint) {
inlineHint = &a;
} else if (a.kind == Annotations::RemovableIfUnusedHint) {
ret.removableIfUnused = true;
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/passes/Print.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2791,6 +2791,12 @@ void PrintSExpression::printCodeAnnotations(Expression* curr) {
restoreNormalColor(o);
doIndent(o, indent);
}
if (annotation.removableIfUnused) {
Colors::grey(o);
o << "(@" << Annotations::RemovableIfUnusedHint << ")\n";
restoreNormalColor(o);
doIndent(o, indent);
}
}
}

Expand Down
27 changes: 23 additions & 4 deletions src/passes/Vacuum.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <ir/branch-hints.h>
#include <ir/drop.h>
#include <ir/effects.h>
#include <ir/intrinsics.h>
#include <ir/iteration.h>
#include <ir/literal-utils.h>
#include <ir/utils.h>
Expand Down Expand Up @@ -91,10 +92,8 @@ struct Vacuum : public WalkerPass<ExpressionStackWalker<Vacuum>> {
curr->is<Loop>() || curr->is<Try>() || curr->is<TryTable>()) {
return curr;
}
// Check if this expression itself has side effects, ignoring children.
EffectAnalyzer self(getPassOptions(), *getModule());
self.visit(curr);
if (self.hasUnremovableSideEffects()) {
// Check if this expression itself must be kept.
if (mustKeepUnusedParent(curr)) {
return curr;
}
// The result isn't used, and this has no side effects itself, so we can
Expand Down Expand Up @@ -130,6 +129,26 @@ struct Vacuum : public WalkerPass<ExpressionStackWalker<Vacuum>> {
}
}

// Check if a parent expression must be kept around, given the knowledge that
// its result is unused (dropped). This is basically just a call to
// ShallowEffectAnalyzer to see if we can remove it, except that given the
// result is unused, the relevant hint may help us. (This just checks the
// parent itself: it may have children that the caller must check and keep
// around if so.)
bool mustKeepUnusedParent(Expression* curr) {
if (auto* call = curr->dynCast<Call>()) {
// If |curr| is marked as removable if unused, then it is removable
// without even checking effects.
if (Intrinsics(*getModule())
.getCallAnnotations(call, getFunction())
.removableIfUnused) {
return false;
}
}
ShallowEffectAnalyzer self(getPassOptions(), *getModule(), curr);
return self.hasUnremovableSideEffects();
}

void visitBlock(Block* curr) {
auto& list = curr->list;

Expand Down
1 change: 1 addition & 0 deletions src/wasm-annotations.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace wasm::Annotations {

extern const Name BranchHint;
extern const Name InlineHint;
extern const Name RemovableIfUnusedHint;

} // namespace wasm::Annotations

Expand Down
2 changes: 2 additions & 0 deletions src/wasm-binary.h
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,7 @@ class WasmBinaryWriter {

std::optional<BufferWithRandomAccess> getBranchHintsBuffer();
std::optional<BufferWithRandomAccess> getInlineHintsBuffer();
std::optional<BufferWithRandomAccess> getRemovableIfUnusedHintsBuffer();

// helpers
void writeInlineString(std::string_view name);
Expand Down Expand Up @@ -1733,6 +1734,7 @@ class WasmBinaryReader {

void readBranchHints(size_t payloadLen);
void readInlineHints(size_t payloadLen);
void readremovableIfUnusedHints(size_t payloadLen);

std::tuple<Address, Address, Index, MemoryOrder>
readMemoryAccess(bool isAtomic, bool isRMW);
Expand Down
13 changes: 11 additions & 2 deletions src/wasm.h
Original file line number Diff line number Diff line change
Expand Up @@ -2233,7 +2233,9 @@ struct BinaryLocations {
// Forward declaration for FuncEffectsMap.
class EffectAnalyzer;

// Code annotations for VMs.
// Annotation for a particular piece of code. This includes std::optionals for
// all possible annotations, with the ones present being filled in (or just a
// bool for an annotation with one possible value).
struct CodeAnnotation {
// Branch Hinting proposal: Whether the branch is likely, or unlikely.
std::optional<bool> branchLikely;
Expand All @@ -2243,8 +2245,15 @@ struct CodeAnnotation {
static const uint8_t AlwaysInline = 127;
std::optional<uint8_t> inline_;

// Toolchain hint: If this expression's result is unused, then the entire
// thing can be considered dead and removable. See
//
// https://github.com/WebAssembly/binaryen/wiki/Optimizer-Cookbook#intrinsics
bool removableIfUnused = false;

bool operator==(const CodeAnnotation& other) const {
return branchLikely == other.branchLikely && inline_ == other.inline_;
return branchLikely == other.branchLikely && inline_ == other.inline_ &&
removableIfUnused == other.removableIfUnused;
}
};

Expand Down
35 changes: 34 additions & 1 deletion src/wasm/wasm-binary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1627,6 +1627,7 @@ std::optional<BufferWithRandomAccess> WasmBinaryWriter::writeCodeAnnotations() {

append(getBranchHintsBuffer());
append(getInlineHintsBuffer());
append(getRemovableIfUnusedHintsBuffer());
return ret;
}

Expand Down Expand Up @@ -1769,6 +1770,19 @@ std::optional<BufferWithRandomAccess> WasmBinaryWriter::getInlineHintsBuffer() {
});
}

std::optional<BufferWithRandomAccess>
WasmBinaryWriter::getRemovableIfUnusedHintsBuffer() {
return writeExpressionHints(
Annotations::RemovableIfUnusedHint,
[](const CodeAnnotation& annotation) {
return annotation.removableIfUnused;
},
[](const CodeAnnotation& annotation, BufferWithRandomAccess& buffer) {
// Hint size, always empty.
buffer << U32LEB(0);
});
}

void WasmBinaryWriter::writeData(const char* data, size_t size) {
for (size_t i = 0; i < size; i++) {
o << int8_t(data[i]);
Expand Down Expand Up @@ -2040,7 +2054,8 @@ void WasmBinaryReader::preScan() {
auto sectionName = getInlineString();

if (sectionName == Annotations::BranchHint ||
sectionName == Annotations::InlineHint) {
sectionName == Annotations::InlineHint ||
sectionName == Annotations::RemovableIfUnusedHint) {
// Code annotations require code locations.
// TODO: We could note which functions require code locations, as an
// optimization.
Expand Down Expand Up @@ -2197,6 +2212,11 @@ void WasmBinaryReader::readCustomSection(size_t payloadLen) {
} else if (sectionName == Annotations::InlineHint) {
deferredAnnotationSections.push_back(AnnotationSectionInfo{
pos, [this, payloadLen]() { this->readInlineHints(payloadLen); }});
} else if (sectionName == Annotations::RemovableIfUnusedHint) {
deferredAnnotationSections.push_back(
AnnotationSectionInfo{pos, [this, payloadLen]() {
this->readremovableIfUnusedHints(payloadLen);
}});
} else {
// an unfamiliar custom section
if (sectionName.equals(BinaryConsts::CustomSections::Linking)) {
Expand Down Expand Up @@ -5507,6 +5527,19 @@ void WasmBinaryReader::readInlineHints(size_t payloadLen) {
});
}

void WasmBinaryReader::readremovableIfUnusedHints(size_t payloadLen) {
readExpressionHints(Annotations::RemovableIfUnusedHint,
payloadLen,
[&](CodeAnnotation& annotation) {
auto size = getU32LEB();
if (size != 0) {
throwError("bad removableIfUnusedHint size");
}

annotation.removableIfUnused = true;
});
}

std::tuple<Address, Address, Index, MemoryOrder>
WasmBinaryReader::readMemoryAccess(bool isAtomic, bool isRMW) {
auto rawAlignment = getU32LEB();
Expand Down
6 changes: 6 additions & 0 deletions src/wasm/wasm-ir-builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2661,6 +2661,12 @@ void IRBuilder::applyAnnotations(Expression* expr,
assert(func);
func->codeAnnotations[expr].inline_ = annotation.inline_;
}

if (annotation.removableIfUnused) {
// Only possible inside functions.
assert(func);
func->codeAnnotations[expr].removableIfUnused = true;
}
}

} // namespace wasm
1 change: 1 addition & 0 deletions src/wasm/wasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ namespace Annotations {

const Name BranchHint = "metadata.code.branch_hint";
const Name InlineHint = "metadata.code.inline";
const Name RemovableIfUnusedHint = "binaryen.removable.if.unused";

} // namespace Annotations

Expand Down
88 changes: 88 additions & 0 deletions test/lit/passes/vacuum-removable-if-unused-func.wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
;; RUN: wasm-opt -all --vacuum %s -S -o - | filecheck %s
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we autogenerate the output for this one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looked into this a little but it would be a huge refactoring of the update script. The issue is that module-level things like functions are all tracked by name, but this would not be a named thing, so a very different regex is needed, and different tracking to match it up to the right thing in the output. I did some experimentation, but it got very messy... Though maybe you know that script better and have an idea?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. That's unfortunate. I can take a look at updating the script to handle annotations. We'll definitely want that for e.g. type annotations in the future.


;; Test the function-level annotation of removable.if.unused.
(module
(@binaryen.removable.if.unused)
(func $calls-marked (param $x i32) (result i32)
;; The function is marked as removable if unused, and this is dropped, so optimize.
(drop
(call $calls-marked
(i32.const 0)
)
)
;; Not dropped, so keep it.
(local.set $x
(call $calls-marked
(i32.const 1)
)
)
(i32.const 2)
)

(func $calls-unmarked (param $x i32) (result i32)
;; As above, but unmarked with the hint. We change nothing here.
(drop
(call $calls-unmarked
(i32.const 0)
)
)
(local.set $x
(call $calls-unmarked
(i32.const 1)
)
)
(i32.const 2)
)

(func $calls-other (param $x i32) (result i32)
;; As above, but calling another function, to check we look for annotations in
;; the right place. Both calls are dropped, and only the one to the marked
;; function should be removed.
(drop
(call $calls-marked
(i32.const 0)
)
)
(drop
(call $calls-unmarked
(i32.const 1)
)
)
(i32.const 2)
)
)

;; CHECK: (module
;; CHECK-NEXT: (type $0 (func (param i32) (result i32)))
;; CHECK-NEXT: (@binaryen.removable.if.unused)
;; CHECK-NEXT: (func $calls-marked (type $0) (param $x i32) (result i32)
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (call $calls-marked
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (i32.const 2)
;; CHECK-NEXT: )
;; CHECK-NEXT: (func $calls-unmarked (type $0) (param $x i32) (result i32)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (call $calls-unmarked
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (call $calls-unmarked
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (i32.const 2)
;; CHECK-NEXT: )
;; CHECK-NEXT: (func $calls-other (type $0) (param $x i32) (result i32)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (call $calls-unmarked
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (i32.const 2)
;; CHECK-NEXT: )
;; CHECK-NEXT: )

Loading
Loading