current commit
261006.1 · Add StripRefiningCasts pass to eliminate redundant subtype casts and … (#9188)
/*
 * 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