/*
* 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