current commit
261021.3 · Add a --constant-arrays-to-data pass (#9274)
/*
 * 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.
 */

//
// Converts array.new_fixed of constant numeric values in function bodies into
// array.new_data from a passive data segment, when that is smaller.
//
// array.new_fixed needs a full *.const instruction per element, while a data
// segment stores the raw bytes, so large arrays, or arrays whose values have
// long encodings, are smaller as array.new_data. On the other hand,
// array.new_fixed is a constant expression that many optimizations understand
// (and OptimizeInstructions lowers array.new_data into it for that reason), so
// this pass is only useful at the very end of the pipeline, where nothing can
// benefit from array.new_fixed anymore. The default pipeline runs it as part of
// the last set of optimizations.
//
// Global initializers are not modified, as array.new_data is not a constant
// expression.
//
// Arrays with identical payloads share one copy of the bytes, from an existing
// passive segment with identical contents that is never dropped, or else from a
// new segment. Each payload gets its own new segment, so that if its arrays are
// lowered to array.new_fixed and converted again later (e.g. with --converge),
// they reuse that segment rather than duplicate it. A payload is converted if
// that reduces the estimated binary size, including the size of its segment.
//
// TODO: Also reuse payloads that appear inside other data. That needs an
//       efficient substring search, to avoid quadratic behavior.
// TODO: Convert arrays of constant references to array.new_elem.
//

#include <cstring>
#include <optional>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>

#include "ir/debuginfo.h"
#include "ir/module-utils.h"
#include "ir/names.h"
#include "pass.h"
#include "support/insert_ordered.h"
#include "wasm-binary.h"
#include "wasm-builder.h"
#include "wasm-traversal.h"
#include "wasm.h"

namespace wasm {

namespace {

// Binary size estimates.

// The number of bytes |thing| (e.g. a LEB) takes in the binary.
template<typename T> size_t getWrittenSize(const T& thing) {
  BufferWithRandomAccess buffer;
  buffer << thing;
  return buffer.size();
}

// The size of a *.const instruction.
size_t getConstSize(const Literal& value) {
  switch (value.type.getBasic()) {
    case Type::i32:
      return 1 + getWrittenSize(S32LEB(value.geti32()));
    case Type::i64:
      return 1 + getWrittenSize(S64LEB(value.geti64()));
    case Type::f32:
    case Type::f64:
      return 1 + value.type.getByteSize();
    case Type::v128:
      // The SIMD prefix and opcode, and the value.
      return 2 + 16;
    case Type::none:
    case Type::unreachable:
      break;
  }
  WASM_UNREACHABLE("unexpected const type");
}

// The size of an array.new_fixed of constants, apart from the prefix, opcode
// and type index, which are the same in array.new_data.
size_t getArrayNewFixedSize(ArrayNewFixed* curr) {
  size_t size = getWrittenSize(U32LEB(curr->values.size()));
  for (auto* value : curr->values) {
    size += getConstSize(value->cast<Const>()->value);
  }
  return size;
}

// The size of an array.new_data that reads all of a segment, apart from the
// prefix, opcode and type index.
size_t getArrayNewDataSize(Index segment, Index size) {
  // The segment index, an i32.const 0 for the offset (2 bytes), and an
  // i32.const for the size.
  return getWrittenSize(U32LEB(segment)) + 2 +
         getConstSize(Literal(int32_t(size)));
}

// The size of a new passive data segment with the given amount of data.
size_t getNewSegmentSize(const Module& wasm, size_t dataSize) {
  // The flags, the size of the data, and the data itself.
  size_t size = 1 + getWrittenSize(U32LEB(dataSize)) + dataSize;
  if (wasm.dataSegments.empty()) {
    // The first segment also adds the data count and data sections, each with
    // an id, a size and a count.
    size += 2 * 3;
  }
  return size;
}

// Candidates for conversion.

// Returns a view of the bytes, to use them as a key in an unordered map
// (std::hash supports views of strings, but not vectors).
std::string_view getView(const std::vector<char>& bytes) {
  return std::string_view(bytes.data(), bytes.size());
}

// Returns the bytes the array would have in a data segment, if its values are
// numeric constants.
std::optional<std::vector<char>> getPayload(ArrayNewFixed* curr) {
  // An unreachable array has no array type (and its values are not all
  // constants anyway), and an empty one is smaller as it is.
  if (curr->type == Type::unreachable || curr->values.empty()) {
    return std::nullopt;
  }
  const auto& element = curr->type.getHeapType().getArray().element;
  // array.new_data only supports numeric elements.
  if (!element.type.isNumber()) {
    return std::nullopt;
  }
  auto elemBytes = element.getByteSize();
  std::vector<char> payload(curr->values.size() * elemBytes);
  for (Index i = 0; i < curr->values.size(); i++) {
    auto* c = curr->values[i]->dynCast<Const>();
    if (!c) {
      return std::nullopt;
    }
    // Like stores to arrays in the interpreter, write the low bytes of the
    // value, which truncates it for packed fields.
    uint8_t bits[16];
    c->value.getBits(bits);
    memcpy(&payload[i * elemBytes], bits, elemBytes);
  }
  return payload;
}

// An array.new_fixed of numeric constants.
struct Candidate {
  Function* func;
  // Where the array.new_fixed is.
  Expression** location;
  std::vector<char> payload;
};

// Candidates with identical payloads, which can share one copy of the bytes.
struct PayloadGroup {
  std::vector<Candidate*> candidates;

  const std::vector<char>& getPayload() const { return candidates[0]->payload; }
};

struct ConstantArraysToData : public Pass {
  // We only replace allocations with other allocations of the same type.
  bool requiresNonNullableLocalFixups() override { return false; }

  Module* wasm = nullptr;

  // All the candidates, in a deterministic order. Payload groups point into
  // them, so they must not change after grouping.
  std::vector<Candidate> candidates;

  // Segments that are dropped somewhere. We cannot reuse them, as
  // array.new_data traps once its segment is dropped (unless it reads nothing).
  std::unordered_set<Name> droppedSegments;

  void run(Module* module) override {
    wasm = module;
    // array.new_data requires bulk memory.
    if (!wasm->features.hasGC() || !wasm->features.hasBulkMemory()) {
      return;
    }
    findCandidates();
    if (candidates.empty()) {
      return;
    }
    auto groups = groupByPayload();
    convert(groups);
  }

  void findCandidates() {
    struct Info {
      std::vector<Candidate> candidates;
      std::unordered_set<Name> droppedSegments;
    };

    struct Finder : public PostWalker<Finder> {
      Info& info;

      Finder(Info& info) : info(info) {}

      void visitArrayNewFixed(ArrayNewFixed* curr) {
        if (auto payload = getPayload(curr)) {
          info.candidates.push_back(
            {getFunction(), getCurrentPointer(), std::move(*payload)});
        }
      }

      void visitDataDrop(DataDrop* curr) {
        info.droppedSegments.insert(curr->segment);
      }
    };

    ModuleUtils::ParallelFunctionAnalysis<Info, Immutable, InsertOrderedMap>
      analysis(*wasm, [](Function* func, Info& info) {
        if (!func->imported()) {
          Finder(info).walkFunction(func);
        }
      });

    // The map is in function order (see ParallelFunctionAnalysis), so this
    // order is deterministic.
    for (auto& [func, info] : analysis.map) {
      for (auto& candidate : info.candidates) {
        candidates.push_back(std::move(candidate));
      }
      droppedSegments.insert(info.droppedSegments.begin(),
                             info.droppedSegments.end());
    }
  }

  std::vector<PayloadGroup> groupByPayload() {
    std::vector<PayloadGroup> groups;
    // The keys are views of the candidates' payloads.
    std::unordered_map<std::string_view, Index> groupIndexes;
    for (auto& candidate : candidates) {
      auto [it, inserted] =
        groupIndexes.insert({getView(candidate.payload), groups.size()});
      if (inserted) {
        groups.emplace_back();
      }
      groups[it->second].candidates.push_back(&candidate);
    }
    return groups;
  }

  // Converts the arrays of each payload group to read from an existing segment
  // with identical contents, or else from a new segment, if that is smaller.
  void convert(std::vector<PayloadGroup>& groups) {
    auto reusableSegments = getReusableSegments();
    for (auto& group : groups) {
      const auto& payload = group.getPayload();
      if (auto it = reusableSegments.find(getView(payload));
          it != reusableSegments.end()) {
        if (getCodeSavings(group, it->second) > 0) {
          replaceArrays(group, it->second);
        }
        continue;
      }
      // A new segment goes at the end. It is worth adding if the code shrinks
      // by more than the size of the segment.
      Index segment = wasm->dataSegments.size();
      if (getCodeSavings(group, segment) >
          int64_t(getNewSegmentSize(*wasm, payload.size()))) {
        addSegment(payload);
        replaceArrays(group, segment);
      }
    }
  }

  // Returns the passive segments that are never dropped, by their contents (the
  // first one, if several are identical). The keys are views of the segments'
  // data, which stays in place as we add more segments.
  std::unordered_map<std::string_view, Index> getReusableSegments() {
    std::unordered_map<std::string_view, Index> reusableSegments;
    for (Index i = 0; i < wasm->dataSegments.size(); i++) {
      auto& segment = wasm->dataSegments[i];
      if (segment->isPassive() && !droppedSegments.contains(segment->name)) {
        reusableSegments.insert({getView(segment->data), i});
      }
    }
    return reusableSegments;
  }

  // The bytes saved in the code if the group's arrays read from the segment.
  int64_t getCodeSavings(const PayloadGroup& group, Index segment) {
    // Arrays with identical payloads can differ in size: their element types
    // can differ (e.g. 8 i8s or 2 i32s), and so can the constants for packed
    // fields, where only the low bits matter (e.g. -1 and 255 in an i8 array).
    // So we measure each array.
    int64_t savings = 0;
    for (auto* candidate : group.candidates) {
      auto* array = (*candidate->location)->cast<ArrayNewFixed>();
      savings += int64_t(getArrayNewFixedSize(array)) -
                 int64_t(getArrayNewDataSize(segment, array->values.size()));
    }
    return savings;
  }

  void addSegment(const std::vector<char>& payload) {
    auto name = Names::getValidDataSegmentName(*wasm, "constant-array-data");
    // Without a memory and an offset, the segment is passive.
    wasm->addDataSegment(Builder::makeDataSegment(
      name, Name(), nullptr, payload.data(), payload.size()));
  }

  // Replaces the group's arrays with array.new_data of all of the segment. That
  // cannot trap, as the segment is never dropped.
  void replaceArrays(const PayloadGroup& group, Index segment) {
    Builder builder(*wasm);
    auto name = wasm->dataSegments[segment]->name;
    for (auto* candidate : group.candidates) {
      auto* array = (*candidate->location)->cast<ArrayNewFixed>();
      auto* replacement = builder.makeArrayNewData(
        array->type.getHeapType(),
        name,
        builder.makeConst(int32_t(0)),
        builder.makeConst(int32_t(array->values.size())));
      assert(replacement->type == array->type);
      debuginfo::copyOriginalToReplacement(array, replacement, candidate->func);
      *candidate->location = replacement;
    }
  }
};

} // anonymous namespace

Pass* createConstantArraysToDataPass() { return new ConstantArraysToData(); }

} // namespace wasm