@ 261106.8
current
/*
 * 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.
 */

//
// Simple multi-allocation escape analysis, aiming to cleanly separate multiple
// allocations written to a single local.
//
// This pass identifies a simple case of refs not escaping, like Heap2Local, but
// allows more than a single allocation (struct.new), which is better, and
// only handles a far simpler-to-identify situation, which is worse. To see the
// motivation, consider overlapping allocations like this:
//
//    x = new Foo(42);
//    if (condition) x = new Foo(1337);
//    else x.value = -1;
//    bar(x.value, x.value);
//
// Heap2Local cannot optimize this due to the overlapping live ranges (it sees
// some x.value operations are not exclusive to any single allocation). But this
// pass can optimize into this:
//
//    x_value = (new Foo(42)).value;
//    if (condition) x_value = (new Foo(1337)).value;
//    else x_value = -1;
//    bar(x_value, x_value);
//
// Note how we now operate on x_value, a new local with data. Basically, each
// struct.get/struct.set is turned into an operation on that new local. This is
// safe despite the multiple allocations because the local x is always used
// immediately, as data: rather than store a reference, we can just focus on the
// data. That is, each struct.new just writes a bunch of fields, and it all
// could have been unboxed. After verifying that nothing observes x as a
// reference (no ref.eq), and it does not escape, we can optimize this.
// Heap2Local will then see 'untangled' allocations, each one immediately
// consumed, and remove them all.
//
// Note that Heap2Local could safely optimize in this example, even before our
// changes: while the `x.value` operations are not exclusive to allocations, the
// live ranges of the allocations join exactly there - they do not overlap in a
// dangerous way. But here is an example MultiHeap2Local can optimize that
// Heap2Local cannot:
//
//   (local.set $a
//     (struct.new $struct
//       (..value..)
//     )
//   )
//   (struct.get $ref 0
//     (select
//       (local.get $a)
//       (local.tee $a
//         (struct.new_default $struct)
//       )
//       (..condition..)
//     )
//   )
//
// The struct.get at the end may read ..value.. from set at the top, or it may
// read the default value from the tee at the bottom. But here we cannot
// optimize while using a single local `a_value` because of the live range
// overlap: both allocations are alive as the select executes, and it picks
// between them, so we must store both of them somewhere in the interim.
// Detecting such overlaps is not trivial as it involves locals as well as the
// wasm value stack, which is why this pass is useful, as it handles common and
// obviously-optimizable cases.
//

#include "ir/bits.h"
#include "ir/find_all.h"
#include "ir/local-graph.h"
#include "ir/properties.h"
#include "pass.h"
#include "wasm-builder.h"
#include "wasm-traversal.h"
#include "wasm.h"

namespace wasm {

namespace {

struct MultiHeap2Local : public WalkerPass<PostWalker<MultiHeap2Local>> {
  bool isFunctionParallel() override { return true; }

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

  // Information about a local index, used to decide whether we can optimize.
  struct LocalInfo {
    // The number of gets for the local, tracked so that we can see if all the
    // local.gets are consumed by struct.get/sets.
    Index numGets = 0;

    // The number of sets for the local, tracked as we only optimize when we see
    // 2 or more local.sets (a single one is trivially optimized by Heap2Local,
    // so we would be doing pointless work).
    Index numSets = 0;

    // The number of tees for the local, tracked for the same reason as gets:
    // they flow out the reference, so we must see them consumed by struct
    // gets/sets.
    Index numTees = 0;

    // All struct.get/struct.set operations that read a get/tee of this local.
    std::vector<Expression*> structOperations;

    // Certain conditions prevent us from optimizing.
    bool unoptimizable = false;
  };

  // For each local index, its info.
  std::vector<LocalInfo> localInfos;

  // Checks if a local index is relevant for us: if it refers to a struct.
  bool isStructLocal(Index index) {
    auto type = getFunction()->getLocalType(index);
    return type.isRef() && type.getHeapType().isStruct();
  }

  void visitLocalGet(LocalGet* curr) {
    if (isStructLocal(curr->index)) {
      localInfos[curr->index].numGets++;
    }
  }

  void visitLocalSet(LocalSet* curr) {
    if (isStructLocal(curr->index)) {
      localInfos[curr->index].numSets++;
      if (curr->isTee()) {
        localInfos[curr->index].numTees++;
      }
      // All sets must write a struct.new, otherwise we cannot analyze this
      // local.
      if (!curr->value->is<StructNew>()) {
        localInfos[curr->index].unoptimizable = true;
      }
    }
  }

  void visitStructGet(StructGet* curr) { handleStructOperation(curr); }
  void visitStructSet(StructSet* curr) { handleStructOperation(curr); }

  template<typename T> void handleStructOperation(T* curr) {
    // Avoid unreachable code.
    if (curr->type == Type::unreachable) {
      return;
    }

    // See if the reference is one of the patterns we recognize.
    // TODO fallthrough, but careful of br_if which can escape the value
    if (auto* get = curr->ref->template dynCast<LocalGet>()) {
      localInfos[get->index].structOperations.push_back(curr);

      // Also handle the value for a struct.set: a struct.set has two inputs,
      // and if the value modifies the local, then two allocations can be
      // live at once:
      //
      //   (struct.set
      //     (local.get $x)
      //     (block
      //       (local.set $x ..)
      //       ..
      //
      // Here $x is put on the value stack, but then we write something new into
      // $x before the struct.set, which means the struct.set is writing to the
      // old allocation - one that no struct.get will later read. When we see
      // such live range overlaps, give up.
      if (auto* structSet = curr->template dynCast<StructSet>()) {
        for (auto* localSet : FindAll<LocalSet>(structSet->value).list) {
          if (localSet->index == get->index) {
            localInfos[get->index].unoptimizable = true;
            break;
          }
        }
      }
    } else if (auto* tee = curr->ref->template dynCast<LocalSet>()) {
      // TODO: Handle a tee for struct.set. This is a little complex to handle
      // below, and not really needed, since
      //
      //   (struct.set $A k
      //     (local.tee
      //       (struct.new $A ..)
      //     )
      //     (value)
      //   )
      //
      // can be folded into a single struct.new (that is, we are writing all the
      // fields, then writing an additional one, and other passes make us do
      // them all at once anyhow).
      if (curr->template is<StructSet>()) {
        localInfos[tee->index].unoptimizable = true;
      } else {
        // Otherwise, this is a valid struct operation.
        localInfos[tee->index].structOperations.push_back(curr);
      }
    }
    // TODO: handle more things flowing into struct.get, like an If
  }

  void doWalkFunction(Function* func) {
    localInfos.resize(func->getNumLocals());

    WalkerPass<PostWalker<MultiHeap2Local>>::doWalkFunction(func);

    pickLocalsToOptimize(func);

    if (!optimizedLocals.empty()) {
      optimize(func);
    }
  }

  // For each optimized local, maps each used field index to the new local
  // index that stores that field. Note that the inner map is sorted, both for
  // deterministic ordering when we create new locals, and also because the
  // number of used fields tends to be small (so hashing is overkill).
  std::unordered_map<Index, std::map<Index, Index>> optimizedLocals;

  void pickLocalsToOptimize(Function* func) {
    auto* module = getModule();
    Builder builder(*module);
    LazyLocalGraph localGraph(func, module);

    for (Index localIndex = 0; localIndex < localInfos.size(); ++localIndex) {
      auto& info = localInfos[localIndex];
      if (info.unoptimizable) {
        continue;
      }

      if (info.numSets < 2) {
        // We only care about the case of 2 or more sets for a local: otherwise,
        // Heap2Local can handle things perfectly for a single set.
        continue;
      }

      // Check if each local.get is consumed by a struct.get/set, which is the
      // case if the counts match: the sum of local.gets/local.tees (things
      // that flow out the reference) must match the number of struct.gets/sets
      // (things that consume the reference).
      if (info.numGets + info.numTees != info.structOperations.size()) {
        continue;
      }

      // Finally, if the initial value of the local from the function entry is
      // used, do not optimize. For a var, that means the default of zero, which
      // will trap when accessed, while for params it means an unknown value. We
      // therefore must always check for params, but for vars, if we know the
      // default cannot be accessed, we can skip this (which is the case for
      // a non-nullable local, and if trapsNeverHappen as a trap on the null
      // default can be ignored).
      auto localType = func->getLocalType(localIndex);
      auto canReadDefault =
        localType.isNullableRef() && !getPassOptions().trapsNeverHappen;
      if (func->isParam(localIndex) || canReadDefault) {
        bool ok = true;
        for (auto* op : info.structOperations) {
          Expression* ref;
          if (auto* get = op->dynCast<StructGet>()) {
            ref = get->ref;
          } else if (auto* set = op->dynCast<StructSet>()) {
            ref = set->ref;
          } else {
            WASM_UNREACHABLE("bad op");
          }
          if (auto* get = ref->dynCast<LocalGet>()) {
            if (localGraph.getSets(get).contains(nullptr)) {
              ok = false;
              break;
            }
          }
        }
        if (!ok) {
          continue;
        }
      }

      // We can optimize!

      // Gather all the struct field indexes operated on from this local.
      std::set<Index> usedFields;
      for (auto* op : info.structOperations) {
        usedFields.insert(getFieldIndex(op));
      }

      // Allocate new locals to store each field.
      auto& fields = localType.getHeapType().getStruct().fields;
      for (auto fieldIndex : usedFields) {
        optimizedLocals[localIndex][fieldIndex] =
          builder.addVar(func, fields[fieldIndex].type);
      }
    }
  }

  void optimize(Function* func) {
    struct Optimizer : PostWalker<Optimizer> {
      MultiHeap2Local& parent;

      Optimizer(MultiHeap2Local& parent) : parent(parent) {}

      void visitLocalSet(LocalSet* curr) {
        if (!curr->isTee() && parent.optimizedLocals.contains(curr->index)) {
          // local.set now stores the used fields (tee is handled below).
          replaceCurrent(makeFieldSets(curr));
        }
      }

      void visitStructGet(StructGet* curr) {
        if (auto* get = curr->ref->dynCast<LocalGet>()) {
          if (parent.optimizedLocals.contains(get->index)) {
            // struct.get of a local.get reads the field from a new local.
            replaceCurrent(
              getFieldFromLocal(get->index, curr->index, curr->signed_));
          }
        } else if (auto* tee = curr->ref->dynCast<LocalSet>()) {
          if (parent.optimizedLocals.contains(tee->index)) {
            // struct.get of a local.tee first writes the fields, then reads.
            replaceCurrent(makeFieldSets(tee, curr));
          }
        }
      }

      void visitStructSet(StructSet* curr) {
        if (auto* get = curr->ref->dynCast<LocalGet>()) {
          if (parent.optimizedLocals.contains(get->index)) {
            // struct.set of a local.get writes the field into a new local.
            replaceCurrent(
              setFieldToLocal(get->index, curr->index, curr->value));
          }
        }
      }

      Expression*
      getFieldFromLocal(Index localIndex, Index fieldIndex, bool signed_) {
        auto& field = getFunction()
                        ->getLocalType(localIndex)
                        .getHeapType()
                        .getStruct()
                        .fields[fieldIndex];
        // Read from the appropriate new local, handling sign extension as
        // needed.
        auto newLocal = parent.optimizedLocals[localIndex][fieldIndex];
        Builder builder(*getModule());
        Expression* ret =
          builder.makeLocalGet(newLocal, getFunction()->getLocalType(newLocal));
        return Bits::makePackedFieldGet(ret, field, signed_, *getModule());
      }

      Expression*
      setFieldToLocal(Index localIndex, Index fieldIndex, Expression* value) {
        // Write to the appropriate new local.
        auto newLocal = parent.optimizedLocals[localIndex][fieldIndex];
        Builder builder(*getModule());
        return builder.makeLocalSet(newLocal, value);
      }

      Expression* makeFieldSets(LocalSet* set, StructGet* teeParent = nullptr) {
        auto localIndex = set->index;
        auto localType = getFunction()->getLocalType(localIndex);
        auto& fields = localType.getHeapType().getStruct().fields;

        // Store the reference, then store struct.gets of all used fields in the
        // new locals.
        Builder builder(*getModule());
        std::vector<Expression*> list = {
          builder.makeLocalSet(localIndex, set->value)};
        for (auto [fieldIndex, newLocal] : parent.optimizedLocals[localIndex]) {
          auto& field = fields[fieldIndex];
          // Note that the sign does not matter here: we just store the bits,
          // and will handle the sign when reading back out (like in
          // Heap2Local).
          auto* sget =
            builder.makeStructGet(fieldIndex,
                                  builder.makeLocalGet(localIndex, localType),
                                  MemoryOrder::Unordered,
                                  field.type,
                                  false);
          list.push_back(builder.makeLocalSet(newLocal, sget));
        }

        // Finally, if this is a tee, then it was consumed by a struct.get, and
        // we can read that field and return it.
        if (teeParent) {
          list.push_back(getFieldFromLocal(
            localIndex, teeParent->index, teeParent->signed_));
        }
        return builder.makeBlock(list);
      }
    } optimizer(*this);
    optimizer.setModule(getModule());
    optimizer.walkFunction(func);
  }

  // Given a struct.get or struct.set, return the field index.
  Index getFieldIndex(Expression* curr) {
    if (auto* get = curr->dynCast<StructGet>()) {
      return get->index;
    } else if (auto* set = curr->dynCast<StructSet>()) {
      return set->index;
    } else {
      WASM_UNREACHABLE("bad op");
    }
  }
};

} // anonymous namespace

Pass* createMultiHeap2LocalPass() { return new MultiHeap2Local(); }

} // namespace wasm