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 @@ -2766,6 +2766,7 @@ def write_commands(commands, filename):
("--simplify-locals-notee",),
("--simplify-locals-notee-nostructure",),
("--ssa",),
("--strip-refining-casts",),
("--tail-call",),
("--tuple-optimization",),
("--type-finalizing",),
Expand Down
4 changes: 2 additions & 2 deletions src/ir/subtype-exprs.h
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ struct SubtypingDiscoverer : public OverriddenVisitor<SubType> {
void visitElemDrop(ElemDrop* curr) {}
void visitTry(Try* curr) {
self()->noteSubtype(curr->body, curr);
for (auto* body : curr->catchBodies) {
for (auto*& body : curr->catchBodies) {
self()->noteSubtype(body, curr);
}
}
Expand Down Expand Up @@ -423,7 +423,7 @@ struct SubtypingDiscoverer : public OverriddenVisitor<SubType> {
return;
}
auto array = curr->type.getHeapType().getArray();
for (auto* value : curr->values) {
for (auto*& value : curr->values) {
self()->noteSubtype(value, array.element.type);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/passes/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ set(passes_SOURCES
StringLifting.cpp
StringLowering.cpp
Strip.cpp
StripRefiningCasts.cpp
StripTargetFeatures.cpp
StripToolchainAnnotations.cpp
TraceCalls.cpp
Expand Down
175 changes: 175 additions & 0 deletions src/passes/StripRefiningCasts.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* 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.
*/

//
// Removes casts (ref.cast and ref.as_non_null) on values that flow into a slot
// whose declared type the uncast value already satisfies. For example:
//
// (func $callee (param $s (ref $Super)) ..)
// ..
// (call $callee
// (ref.cast (ref $Sub) (local.get $x)) ;; $x is (ref $Super)
// )
//
// The cast is not needed for validation, and in traps-never-happen mode it can
// be assumed to succeed, so it can be removed:
//
// (call $callee
// (local.get $x)
// )
//
// All value-flowing slots discovered by SubtypingDiscoverer (locals, globals,
// call parameters, function results, struct fields, array elements, control
// flow branches and fallthroughs, etc.) are handled. Casts on the input of a
// ref.test whose outcome is not determined by the input's type are removed as
// well, as they do not affect the result.
//
// Unlike the cast removals done in OptimizeInstructions, which never lose type
// information, the casts removed here may narrow the value beyond the slot
// type. That narrower type is information that refining passes (GUFA,
// signature-refining, type-refining, local-subtyping, etc.) could use to refine
// the slot itself, so this pass is meant to run late in the pipeline, after
// those passes had their chance. It should still be followed by a round of
// general optimizations, as removing the casts can make functions identical
// (helping duplicate-function-elimination) or smaller (helping inlining).
//
// Requires traps-never-happen mode, as removing a cast removes a possible trap.
//

#include <unordered_map>

#include "ir/effects.h"
#include "ir/gc-type-utils.h"
#include "ir/intrinsics.h"
#include "ir/subtype-exprs.h"
#include "pass.h"
#include "passes/passes.h"
#include "wasm-traversal.h"
#include "wasm.h"

namespace wasm {

namespace {

struct StripRefiningCasts
: public WalkerPass<
ControlFlowWalker<StripRefiningCasts,
SubtypingDiscoverer<StripRefiningCasts>>> {
using Super =
WalkerPass<ControlFlowWalker<StripRefiningCasts,
SubtypingDiscoverer<StripRefiningCasts>>>;

bool isFunctionParallel() override { return true; }

std::unique_ptr<Pass> create() override {
return std::make_unique<StripRefiningCasts>();
}

void runOnFunction(Module* module, Function* func) override {
if (getPassOptions().trapsNeverHappen) {
Super::runOnFunction(module, func);
}
}

// Map from expression slot to the greatest lower bound of all types it must
// be a subtype of (e.g. a switch value must be a subtype of all its targets).
std::unordered_map<Expression**, Type> requiredTypes;

void noteSubtype(Type, Type) {}
void noteSubtype(HeapType, HeapType) {}
void noteSubtype(Type, Expression*) {}
void noteSubtype(Expression*& sub, Type super) {
auto [it, inserted] = requiredTypes.insert({&sub, super});
if (!inserted) {
it->second = Type::getGreatestLowerBound(it->second, super);
}
}
void noteSubtype(Expression*& sub, Expression* super) {
// TODO: Propagate the required type of |super| itself through fallthrough
// expressions (e.g. blocks, loops, ifs, selects, br_ifs) and re-finalize
// them, rather than constraining |sub| by |super|'s current refined type.
noteSubtype(sub, super->type);
}
void noteNonFlowSubtype(Expression*, Type) {}
void noteCast(HeapType, Type) {}
void noteCast(Expression*, Type) {}
void noteCast(Expression*, Expression*) {}

// Skips casts on |input| while the uncast value is a subtype of |slotType|.
void skipCasts(Expression*& input, Type slotType) {
while (true) {
if (auto* as = input->dynCast<RefAs>()) {
if (as->op == RefAsNonNull &&
Type::isSubType(as->value->type, slotType)) {
input = as->value;
continue;
}
} else if (auto* cast = input->dynCast<RefCast>()) {
// Removing a descriptor cast also removes the descriptor operand, which
// we can only do if it has no side effects.
// TODO: Use ChildLocalizer to preserve side-effecting descriptors in a
// block and still remove the cast.
if ((!cast->desc ||
!EffectAnalyzer(getPassOptions(), *getModule(), cast->desc)
.hasSideEffects()) &&
Type::isSubType(cast->ref->type, slotType)) {
input = cast->ref;
continue;
}
}
break;
}
}

void visitCall(Call* curr) {
// call.without.effects operands must also satisfy the signature of the
// target function operand, not just the import's declared signature.
if (Intrinsics(*getModule()).isCallWithoutEffects(curr)) {
return;
}
Super::visitCall(curr);
}

void visitRefTest(RefTest* curr) {
// If the type of the input determines the outcome, leave the test to
// OptimizeInstructions, which will use that type to resolve it.
// (This also leaves exact tests alone when custom descriptors are disabled,
// as those are then only valid on inputs of the same exact type, which
// determines the outcome.)
if (GCTypeUtils::evaluateCastCheck(curr->ref->type, curr->castType) !=
GCTypeUtils::Unknown) {
return;
}
// Any input in the hierarchy of the cast type is valid. Nullability does
// not matter either: a removed cast is assumed to succeed, so it only ever
// passes its input through unchanged.
noteSubtype(curr->ref,
Type(curr->castType.getHeapType().getTop(), Nullable));
}

void visitFunction(Function* func) {
Super::visitFunction(func);
for (auto& [slot, type] : requiredTypes) {
skipCasts(*slot, type);
}
}
};

} // anonymous namespace

Pass* createStripRefiningCastsPass() { return new StripRefiningCasts(); }

} // namespace wasm
4 changes: 4 additions & 0 deletions src/passes/pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,10 @@ void PassRegistry::registerPasses() {
"strip debug info (including the names section)",
createStripDebugPass);
registerPass("strip-dwarf", "strip dwarf debug info", createStripDWARFPass);
registerPass("strip-refining-casts",
"remove casts on values flowing into slots whose declared type "
"the uncast value already satisfies",
createStripRefiningCastsPass);
registerPass("strip-producers",
"strip the wasm producers section",
createStripProducersPass);
Expand Down
1 change: 1 addition & 0 deletions src/passes/passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ Pass* createStripDebugPass();
Pass* createStripDWARFPass();
Pass* createStripEHPass();
Pass* createStripProducersPass();
Pass* createStripRefiningCastsPass();
Pass* createStripTargetFeaturesPass();
Pass* createStripToolchainAnnotationsPass();
Pass* createStubUnsupportedJSOpsPass();
Expand Down
5 changes: 5 additions & 0 deletions test/lit/help/wasm-metadce.test
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,11 @@
;; CHECK-EMPTY:
;; CHECK-NEXT: --strip-producers strip the wasm producers section
;; CHECK-EMPTY:
;; CHECK-NEXT: --strip-refining-casts remove casts on values flowing
;; CHECK-NEXT: into slots whose declared type
;; CHECK-NEXT: the uncast value already
;; CHECK-NEXT: satisfies
;; CHECK-EMPTY:
;; CHECK-NEXT: --strip-target-features strip the wasm target features
;; CHECK-NEXT: section
;; CHECK-EMPTY:
Expand Down
5 changes: 5 additions & 0 deletions test/lit/help/wasm-opt.test
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,11 @@
;; CHECK-EMPTY:
;; CHECK-NEXT: --strip-producers strip the wasm producers section
;; CHECK-EMPTY:
;; CHECK-NEXT: --strip-refining-casts remove casts on values flowing
;; CHECK-NEXT: into slots whose declared type
;; CHECK-NEXT: the uncast value already
;; CHECK-NEXT: satisfies
;; CHECK-EMPTY:
;; CHECK-NEXT: --strip-target-features strip the wasm target features
;; CHECK-NEXT: section
;; CHECK-EMPTY:
Expand Down
5 changes: 5 additions & 0 deletions test/lit/help/wasm2js.test
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,11 @@
;; CHECK-EMPTY:
;; CHECK-NEXT: --strip-producers strip the wasm producers section
;; CHECK-EMPTY:
;; CHECK-NEXT: --strip-refining-casts remove casts on values flowing
;; CHECK-NEXT: into slots whose declared type
;; CHECK-NEXT: the uncast value already
;; CHECK-NEXT: satisfies
;; CHECK-EMPTY:
;; CHECK-NEXT: --strip-target-features strip the wasm target features
;; CHECK-NEXT: section
;; CHECK-EMPTY:
Expand Down
Loading
Loading