diff --git a/CHANGELOG.md b/CHANGELOG.md index 42537ea3595..e8bffa8bda3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ Current Trunk - Add a new wasm-embed tool for extracting Wasm modules embedded as byte arrays in JS source files or replacing such modules in the JS source (#9127) - Support reading and writing label names in the name section (#9166) + - Add a `--constant-arrays-to-data` pass that converts `array.new_fixed` of + constant numeric values to `array.new_data` when that is smaller. It runs + in the last set of default optimizations (when + `--expect-later-toolchain-opts` is not passed). v133 ---- diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 3339df8e4fe..c97d9cb252e 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2692,6 +2692,7 @@ def write_commands(commands, filename): ("--code-pushing",), ("--code-folding",), ("--const-hoisting",), + ("--constant-arrays-to-data",), ("--constraint-analysis",), ("--dae",), ("--dae-optimizing",), diff --git a/src/ir/module-utils.h b/src/ir/module-utils.h index f232b76e025..70297c7cee2 100644 --- a/src/ir/module-utils.h +++ b/src/ir/module-utils.h @@ -264,6 +264,9 @@ inline void iterModuleItems(const Module& wasm, auto&& visitor) { // default - otherwise, set the Mutability to Mutable. (This is not enforced at // compile time - TODO find a way - but at runtime in pass-debug mode it is // checked.) +// The map has an entry for each function, inserted in the order of +// wasm.functions, so with MapT = InsertOrderedMap, iterating over it visits the +// functions in a deterministic order. template using DefaultMap = std::map; template +#include +#include +#include +#include +#include + +#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 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()->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& 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> 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 payload(curr->values.size() * elemBytes); + for (Index i = 0; i < curr->values.size(); i++) { + auto* c = curr->values[i]->dynCast(); + 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 payload; +}; + +// Candidates with identical payloads, which can share one copy of the bytes. +struct PayloadGroup { + std::vector candidates; + + const std::vector& 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 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 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 candidates; + std::unordered_set droppedSegments; + }; + + struct Finder : public PostWalker { + 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 + 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 groupByPayload() { + std::vector groups; + // The keys are views of the candidates' payloads. + std::unordered_map 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& 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 getReusableSegments() { + std::unordered_map 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(); + savings += int64_t(getArrayNewFixedSize(array)) - + int64_t(getArrayNewDataSize(segment, array->values.size())); + } + return savings; + } + + void addSegment(const std::vector& 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(); + 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 diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index 55b20e05e7d..8481bbda927 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -129,6 +129,10 @@ void PassRegistry::registerPasses() { registerPass("cfp-reftest", "propagate constant struct field values, using ref.test", createConstantFieldPropagationRefTestPass); + registerPass("constant-arrays-to-data", + "convert constant array.new_fixed to array.new_data when it is " + "smaller", + createConstantArraysToDataPass); registerPass("constraint-analysis", "finds and uses mathematical constraints on locals", createConstraintAnalysisPass); @@ -853,6 +857,14 @@ void PassRunner::addDefaultGlobalOptimizationPostPasses() { addIfNoDWARFIssues("simplify-globals"); } addIfNoDWARFIssues("remove-unused-module-elements"); + if (options.lastOpts && wasm->features.hasGC() && + wasm->features.hasBulkMemory() && + (options.optimizeLevel >= 2 || options.shrinkLevel >= 1)) { + // Converting array.new_fixed to array.new_data inhibits optimizations that + // need constant expressions or know the contents of the array (and + // OptimizeInstructions may undo it), so only do it at the very end. + addIfNoDWARFIssues("constant-arrays-to-data"); + } if (options.optimizeLevel >= 2 && wasm->features.hasStrings()) { // Gather strings to globals right before reorder-globals, which will then // sort them properly. diff --git a/src/passes/passes.h b/src/passes/passes.h index 1324edf1af5..d7eac10bd60 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -31,6 +31,7 @@ Pass* createCoalesceLocalsWithLearningPass(); Pass* createCodeFoldingPass(); Pass* createCodePushingPass(); Pass* createConstHoistingPass(); +Pass* createConstantArraysToDataPass(); Pass* createConstantFieldPropagationPass(); Pass* createConstantFieldPropagationRefTestPass(); Pass* createConstraintAnalysisPass(); diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index ea45a3ce6fe..15c0452507d 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -109,6 +109,10 @@ ;; CHECK-NEXT: --const-hoisting hoist repeated constants to a ;; CHECK-NEXT: local ;; CHECK-EMPTY: +;; CHECK-NEXT: --constant-arrays-to-data convert constant array.new_fixed +;; CHECK-NEXT: to array.new_data when it is +;; CHECK-NEXT: smaller +;; CHECK-EMPTY: ;; CHECK-NEXT: --constraint-analysis finds and uses mathematical ;; CHECK-NEXT: constraints on locals ;; CHECK-EMPTY: diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 90c3b2990a3..bc304c4f679 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -154,6 +154,10 @@ ;; CHECK-NEXT: --const-hoisting hoist repeated constants to a ;; CHECK-NEXT: local ;; CHECK-EMPTY: +;; CHECK-NEXT: --constant-arrays-to-data convert constant array.new_fixed +;; CHECK-NEXT: to array.new_data when it is +;; CHECK-NEXT: smaller +;; CHECK-EMPTY: ;; CHECK-NEXT: --constraint-analysis finds and uses mathematical ;; CHECK-NEXT: constraints on locals ;; CHECK-EMPTY: diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index fdce36ac338..fec9a3487a7 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -73,6 +73,10 @@ ;; CHECK-NEXT: --const-hoisting hoist repeated constants to a ;; CHECK-NEXT: local ;; CHECK-EMPTY: +;; CHECK-NEXT: --constant-arrays-to-data convert constant array.new_fixed +;; CHECK-NEXT: to array.new_data when it is +;; CHECK-NEXT: smaller +;; CHECK-EMPTY: ;; CHECK-NEXT: --constraint-analysis finds and uses mathematical ;; CHECK-NEXT: constraints on locals ;; CHECK-EMPTY: diff --git a/test/lit/passes/constant-arrays-to-data-converge.wast b/test/lit/passes/constant-arrays-to-data-converge.wast new file mode 100644 index 00000000000..d4a5089294c --- /dev/null +++ b/test/lit/passes/constant-arrays-to-data-converge.wast @@ -0,0 +1,72 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; With --converge, the last set of opts runs in every iteration. Arrays that +;; OptimizeInstructions lowers back to array.new_fixed are then converted again, +;; and must not duplicate their bytes. + +;; RUN: wasm-opt %s -O2 --converge --traps-never-happen -all -S -o - | filecheck %s + +(module + ;; CHECK: (type $i32 (array (mut i32))) + (type $i32 (array (mut i32))) + + ;; CHECK: (type $1 (func (result (ref $i32)))) + + ;; CHECK: (data $constant-array-data "\00\00\00@\01\00\00@\02\00\00@\03\00\00@\04\00\00@\05\00\00@\06\00\00@\07\00\00@\08\00\00@\t\00\00@\n\00\00@\0b\00\00@\0c\00\00@\r\00\00@\0e\00\00@\0f\00\00@\10\00\00@\11\00\00@\12\00\00@\13\00\00@\14\00\00@\15\00\00@\16\00\00@\17\00\00@\18\00\00@\19\00\00@\1a\00\00@\1b\00\00@\1c\00\00@\1d\00\00@\1e\00\00@\1f\00\00@ \00\00@!\00\00@\"\00\00@#\00\00@$\00\00@%\00\00@&\00\00@\'\00\00@(\00\00@)\00\00@*\00\00@+\00\00@,\00\00@-\00\00@.\00\00@/\00\00@0\00\00@1\00\00@2\00\00@3\00\00@4\00\00@5\00\00@6\00\00@7\00\00@8\00\00@9\00\00@:\00\00@;\00\00@<\00\00@=\00\00@>\00\00@?\00\00@@\00\00@") + + ;; CHECK: (data $constant-array-data_1 "\00\01\00@\01\01\00@\02\01\00@\03\01\00@\04\01\00@\05\01\00@\06\01\00@\07\01\00@") + + ;; CHECK: (export "large" (func $large)) + + ;; CHECK: (export "small" (func $small)) + + ;; CHECK: (func $large (type $1) (result (ref $i32)) + ;; CHECK-NEXT: (array.new_data $i32 $constant-array-data + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 65) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $large (export "large") (result (ref $i32)) + ;; This has more than 64 elements, so OptimizeInstructions does not lower + ;; it back to array.new_fixed, and its segment remains. + (array.new_fixed $i32 65 + (i32.const 0x40000000) (i32.const 0x40000001) (i32.const 0x40000002) + (i32.const 0x40000003) (i32.const 0x40000004) (i32.const 0x40000005) + (i32.const 0x40000006) (i32.const 0x40000007) (i32.const 0x40000008) + (i32.const 0x40000009) (i32.const 0x4000000a) (i32.const 0x4000000b) + (i32.const 0x4000000c) (i32.const 0x4000000d) (i32.const 0x4000000e) + (i32.const 0x4000000f) (i32.const 0x40000010) (i32.const 0x40000011) + (i32.const 0x40000012) (i32.const 0x40000013) (i32.const 0x40000014) + (i32.const 0x40000015) (i32.const 0x40000016) (i32.const 0x40000017) + (i32.const 0x40000018) (i32.const 0x40000019) (i32.const 0x4000001a) + (i32.const 0x4000001b) (i32.const 0x4000001c) (i32.const 0x4000001d) + (i32.const 0x4000001e) (i32.const 0x4000001f) (i32.const 0x40000020) + (i32.const 0x40000021) (i32.const 0x40000022) (i32.const 0x40000023) + (i32.const 0x40000024) (i32.const 0x40000025) (i32.const 0x40000026) + (i32.const 0x40000027) (i32.const 0x40000028) (i32.const 0x40000029) + (i32.const 0x4000002a) (i32.const 0x4000002b) (i32.const 0x4000002c) + (i32.const 0x4000002d) (i32.const 0x4000002e) (i32.const 0x4000002f) + (i32.const 0x40000030) (i32.const 0x40000031) (i32.const 0x40000032) + (i32.const 0x40000033) (i32.const 0x40000034) (i32.const 0x40000035) + (i32.const 0x40000036) (i32.const 0x40000037) (i32.const 0x40000038) + (i32.const 0x40000039) (i32.const 0x4000003a) (i32.const 0x4000003b) + (i32.const 0x4000003c) (i32.const 0x4000003d) (i32.const 0x4000003e) + (i32.const 0x4000003f) (i32.const 0x40000040) + ) + ) + + ;; CHECK: (func $small (type $1) (result (ref $i32)) + ;; CHECK-NEXT: (array.new_data $i32 $constant-array-data_1 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $small (export "small") (result (ref $i32)) + ;; OptimizeInstructions lowers this back to array.new_fixed in the next + ;; iteration, and then we convert it again. + (array.new_fixed $i32 8 + (i32.const 0x40000100) (i32.const 0x40000101) (i32.const 0x40000102) + (i32.const 0x40000103) (i32.const 0x40000104) (i32.const 0x40000105) + (i32.const 0x40000106) (i32.const 0x40000107) + ) + ) +) diff --git a/test/lit/passes/constant-arrays-to-data-last-opts.wast b/test/lit/passes/constant-arrays-to-data-last-opts.wast new file mode 100644 index 00000000000..893e3dcbbd7 --- /dev/null +++ b/test/lit/passes/constant-arrays-to-data-last-opts.wast @@ -0,0 +1,55 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; The default pipeline runs constant-arrays-to-data only in the last set of +;; opts, as converting to array.new_data inhibits other opts. + +;; RUN: wasm-opt %s -O2 -all -S -o - | filecheck %s --check-prefix=LAST +;; RUN: wasm-opt %s -O2 --expect-later-toolchain-opts -all -S -o - | filecheck %s --check-prefix=NOT-LAST +;; RUN: wasm-opt %s -O2 --type-finalizing -all -S -o - | filecheck %s --check-prefix=LAST +;; RUN: wasm-opt %s -O2 -O2 -all -S -o - | filecheck %s --check-prefix=LAST + +(module + ;; LAST: (type $i32 (array (mut i32))) + ;; NOT-LAST: (type $i32 (array (mut i32))) + (type $i32 (array (mut i32))) + + ;; LAST: (type $1 (func (result (ref $i32)))) + + ;; LAST: (data $constant-array-data "\00\ca\9a;\00l\ca\88\00\ab\90A\00\8c\86G\00m|M\00NrS\00/hY\00\10^_") + + ;; LAST: (export "i32" (func $i32)) + + ;; LAST: (func $i32 (type $1) (result (ref $i32)) + ;; LAST-NEXT: (array.new_data $i32 $constant-array-data + ;; LAST-NEXT: (i32.const 0) + ;; LAST-NEXT: (i32.const 8) + ;; LAST-NEXT: ) + ;; LAST-NEXT: ) + ;; NOT-LAST: (type $1 (func (result (ref $i32)))) + + ;; NOT-LAST: (export "i32" (func $i32)) + + ;; NOT-LAST: (func $i32 (type $1) (result (ref $i32)) + ;; NOT-LAST-NEXT: (array.new_fixed $i32 8 + ;; NOT-LAST-NEXT: (i32.const 1000000000) + ;; NOT-LAST-NEXT: (i32.const -2000000000) + ;; NOT-LAST-NEXT: (i32.const 1100000000) + ;; NOT-LAST-NEXT: (i32.const 1200000000) + ;; NOT-LAST-NEXT: (i32.const 1300000000) + ;; NOT-LAST-NEXT: (i32.const 1400000000) + ;; NOT-LAST-NEXT: (i32.const 1500000000) + ;; NOT-LAST-NEXT: (i32.const 1600000000) + ;; NOT-LAST-NEXT: ) + ;; NOT-LAST-NEXT: ) + (func $i32 (export "i32") (result (ref $i32)) + (array.new_fixed $i32 8 + (i32.const 1000000000) + (i32.const -2000000000) + (i32.const 1100000000) + (i32.const 1200000000) + (i32.const 1300000000) + (i32.const 1400000000) + (i32.const 1500000000) + (i32.const 1600000000) + ) + ) +) diff --git a/test/lit/passes/constant-arrays-to-data.wast b/test/lit/passes/constant-arrays-to-data.wast new file mode 100644 index 00000000000..30a9e338317 --- /dev/null +++ b/test/lit/passes/constant-arrays-to-data.wast @@ -0,0 +1,409 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-opt --constant-arrays-to-data -all -S -o - | filecheck %s + +(module + ;; CHECK: (type $i32 (array (mut i32))) + + ;; CHECK: (type $1 (func (result (ref $i32)))) + + ;; CHECK: (type $i8 (array (mut i8))) + (type $i8 (array (mut i8))) + ;; CHECK: (type $i16 (array (mut i16))) + (type $i16 (array (mut i16))) + (type $i32 (array (mut i32))) + ;; CHECK: (type $i64 (array (mut i64))) + (type $i64 (array (mut i64))) + ;; CHECK: (type $f32 (array (mut f32))) + (type $f32 (array (mut f32))) + ;; CHECK: (type $f64 (array (mut f64))) + (type $f64 (array (mut f64))) + ;; CHECK: (type $v128 (array (mut v128))) + (type $v128 (array (mut v128))) + ;; CHECK: (type $ref (array (mut anyref))) + (type $ref (array (mut anyref))) + + ;; Global initializers must remain constant expressions. + ;; CHECK: (type $9 (func (param i32) (result (ref $i32)))) + + ;; CHECK: (type $10 (func (result (ref $i8)))) + + ;; CHECK: (type $11 (func (result (ref $i16)))) + + ;; CHECK: (type $12 (func (result (ref $i64)))) + + ;; CHECK: (type $13 (func (result (ref $f32)))) + + ;; CHECK: (type $14 (func (result (ref $f64)))) + + ;; CHECK: (type $15 (func (result (ref $v128)))) + + ;; CHECK: (type $16 (func (result (ref $ref)))) + + ;; CHECK: (global $global (ref $i32) (array.new_fixed $i32 4 + ;; CHECK-NEXT: (i32.const 100000) + ;; CHECK-NEXT: (i32.const 200000) + ;; CHECK-NEXT: (i32.const 300000) + ;; CHECK-NEXT: (i32.const 400000) + ;; CHECK-NEXT: )) + (global $global (ref $i32) (array.new_fixed $i32 4 + (i32.const 100000) + (i32.const 200000) + (i32.const 300000) + (i32.const 400000) + )) + + ;; An existing passive segment, which can be reused. It contains the i32s + ;; 10000, 20000, 30000 and 40000. + ;; CHECK: (data $existing "\10\'\00\00 N\00\000u\00\00@\9c\00\00") + (data $existing "\10\27\00\00\20\4e\00\00\30\75\00\00\40\9c\00\00") + + ;; A segment that is dropped, which cannot be reused. It contains the i32s + ;; 50000, 60000, 70000 and 80000. + ;; CHECK: (data $dropped "P\c3\00\00`\ea\00\00p\11\01\00\808\01\00") + (data $dropped "\50\c3\00\00\60\ea\00\00\70\11\01\00\80\38\01\00") + + ;; CHECK: (data $constant-array-data "@B\0f\00\80{\e1\ff\c0\c6-\00\00\t=\00@KL\00\80\8d[\00\c0\cfj\00\00\12z\00") + + ;; CHECK: (data $constant-array-data_3 "\c8\c9\ca\cb\cc\cd\ff4") + + ;; CHECK: (data $constant-array-data_4 "@\9cA\9cB\9cC\9c") + + ;; CHECK: (data $constant-array-data_5 "\ff\ff\ff\ff\ff\ff\ff\7f\fe\ff\ff\ff\ff\ff\ff\7f\fd\ff\ff\ff\ff\ff\ff\7f\fc\ff\ff\ff\ff\ff\ff\7f") + + ;; CHECK: (data $constant-array-data_6 "\00\00\00?\00\00\c0?\00\00 @\00\00`@\00\00\90@\00\00\b0@\00\00\d0@\00\00\f0@") + + ;; CHECK: (data $constant-array-data_7 "\9a\99\99\99\99\99\b9?\9a\99\99\99\99\99\c9?333333\d3?\9a\99\99\99\99\99\d9?\00\00\00\00\00\00\e0?333333\e3?ffffff\e6?\04\00\00\00\00\00\f0\7f") + + ;; CHECK: (data $constant-array-data_8 "\01\00\00\00\02\00\00\00\03\00\00\00\04\00\00\00\05\00\00\00\06\00\00\00\07\00\00\00\08\00\00\00\t\00\00\00\n\00\00\00\0b\00\00\00\0c\00\00\00\r\00\00\00\0e\00\00\00\0f\00\00\00\10\00\00\00") + + ;; CHECK: (export "i32" (func $i32)) + + ;; CHECK: (export "i32-same" (func $i32-same)) + + ;; CHECK: (export "i32-subsequence" (func $i32-subsequence)) + + ;; CHECK: (export "i32-existing" (func $i32-existing)) + + ;; CHECK: (export "i32-dropped" (func $i32-dropped)) + + ;; CHECK: (export "i32-small" (func $i32-small)) + + ;; CHECK: (export "i32-nonconstant" (func $i32-nonconstant)) + + ;; CHECK: (export "i8" (func $i8)) + + ;; CHECK: (export "i16" (func $i16)) + + ;; CHECK: (export "i64" (func $i64)) + + ;; CHECK: (export "f32" (func $f32)) + + ;; CHECK: (export "f64" (func $f64)) + + ;; CHECK: (export "v128" (func $v128)) + + ;; CHECK: (export "ref" (func $ref)) + + ;; CHECK: (export "empty" (func $empty)) + + ;; CHECK: (func $i32 (type $1) (result (ref $i32)) + ;; CHECK-NEXT: ;;@ src.cpp:10:5 + ;; CHECK-NEXT: (array.new_data $i32 $constant-array-data + ;; CHECK-NEXT: ;;@ + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ;;@ + ;; CHECK-NEXT: (i32.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $i32 (export "i32") (result (ref $i32)) + ;; This is smaller as array.new_data. The debug location is kept. + ;;@ src.cpp:10:5 + (array.new_fixed $i32 8 + (i32.const 1000000) + (i32.const -2000000) + (i32.const 3000000) + (i32.const 4000000) + (i32.const 5000000) + (i32.const 6000000) + (i32.const 7000000) + (i32.const 8000000) + ) + ) + + ;; CHECK: (func $i32-same (type $1) (result (ref $i32)) + ;; CHECK-NEXT: (array.new_data $i32 $constant-array-data + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $i32-same (export "i32-same") (result (ref $i32)) + ;; The same payload as before, which reuses the same segment. + (array.new_fixed $i32 8 + (i32.const 1000000) + (i32.const -2000000) + (i32.const 3000000) + (i32.const 4000000) + (i32.const 5000000) + (i32.const 6000000) + (i32.const 7000000) + (i32.const 8000000) + ) + ) + + ;; CHECK: (func $i32-subsequence (type $1) (result (ref $i32)) + ;; CHECK-NEXT: (array.new_fixed $i32 2 + ;; CHECK-NEXT: (i32.const 3000000) + ;; CHECK-NEXT: (i32.const 4000000) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $i32-subsequence (export "i32-subsequence") (result (ref $i32)) + ;; A subsequence of the previous payload. We do not look for payloads inside + ;; other data (see the TODO in the pass), and alone this is not worth + ;; converting. + (array.new_fixed $i32 2 + (i32.const 3000000) + (i32.const 4000000) + ) + ) + + ;; CHECK: (func $i32-existing (type $1) (result (ref $i32)) + ;; CHECK-NEXT: (array.new_data $i32 $existing + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $i32-existing (export "i32-existing") (result (ref $i32)) + ;; An existing segment with identical contents is reused. + (array.new_fixed $i32 4 + (i32.const 10000) + (i32.const 20000) + (i32.const 30000) + (i32.const 40000) + ) + ) + + ;; CHECK: (func $i32-dropped (type $1) (result (ref $i32)) + ;; CHECK-NEXT: (data.drop $dropped) + ;; CHECK-NEXT: (array.new_fixed $i32 4 + ;; CHECK-NEXT: (i32.const 50000) + ;; CHECK-NEXT: (i32.const 60000) + ;; CHECK-NEXT: (i32.const 70000) + ;; CHECK-NEXT: (i32.const 80000) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $i32-dropped (export "i32-dropped") (result (ref $i32)) + ;; The dropped segment cannot be reused, and it is not worth adding a new + ;; one. + (data.drop $dropped) + (array.new_fixed $i32 4 + (i32.const 50000) + (i32.const 60000) + (i32.const 70000) + (i32.const 80000) + ) + ) + + ;; CHECK: (func $i32-small (type $1) (result (ref $i32)) + ;; CHECK-NEXT: (array.new_fixed $i32 4 + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: (i32.const 5) + ;; CHECK-NEXT: (i32.const 7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $i32-small (export "i32-small") (result (ref $i32)) + ;; Small values have short encodings, so this is smaller as it is. + (array.new_fixed $i32 4 + (i32.const 1) + (i32.const 3) + (i32.const 5) + (i32.const 7) + ) + ) + + ;; CHECK: (func $i32-nonconstant (type $9) (param $x i32) (result (ref $i32)) + ;; CHECK-NEXT: (array.new_fixed $i32 4 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 2000000) + ;; CHECK-NEXT: (i32.const 3000000) + ;; CHECK-NEXT: (i32.const 4000000) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $i32-nonconstant (export "i32-nonconstant") (param $x i32) (result (ref $i32)) + ;; Non-constant values prevent the optimization. + (array.new_fixed $i32 4 + (local.get $x) + (i32.const 2000000) + (i32.const 3000000) + (i32.const 4000000) + ) + ) + + ;; CHECK: (func $i8 (type $10) (result (ref $i8)) + ;; CHECK-NEXT: (array.new_data $i8 $constant-array-data_3 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $i8 (export "i8") (result (ref $i8)) + ;; Packed fields store the low byte of each value, so the new segment has + ;; the bytes c8 c9 ca cb cc cd ff 34. + (array.new_fixed $i8 8 + (i32.const 0xc8) + (i32.const 0xc9) + (i32.const 0xca) + (i32.const 0xcb) + (i32.const 0xcc) + (i32.const 0xcd) + (i32.const 0xffffffff) + (i32.const 0x1234) + ) + ) + + ;; CHECK: (func $i16 (type $11) (result (ref $i16)) + ;; CHECK-NEXT: (array.new_data $i16 $constant-array-data_4 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $i16 (export "i16") (result (ref $i16)) + ;; The new segment has the bytes 40 9c 41 9c 42 9c 43 9c. + (array.new_fixed $i16 4 + (i32.const 0x9c40) + (i32.const 0x9c41) + (i32.const 0x9c42) + (i32.const 0x9c43) + ) + ) + + ;; CHECK: (func $i64 (type $12) (result (ref $i64)) + ;; CHECK-NEXT: (array.new_data $i64 $constant-array-data_5 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $i64 (export "i64") (result (ref $i64)) + (array.new_fixed $i64 4 + (i64.const 0x7fffffffffffffff) + (i64.const 0x7ffffffffffffffe) + (i64.const 0x7ffffffffffffffd) + (i64.const 0x7ffffffffffffffc) + ) + ) + + ;; CHECK: (func $f32 (type $13) (result (ref $f32)) + ;; CHECK-NEXT: (array.new_data $f32 $constant-array-data_6 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f32 (export "f32") (result (ref $f32)) + (array.new_fixed $f32 8 + (f32.const 0.5) + (f32.const 1.5) + (f32.const 2.5) + (f32.const 3.5) + (f32.const 4.5) + (f32.const 5.5) + (f32.const 6.5) + (f32.const 7.5) + ) + ) + + ;; CHECK: (func $f64 (type $14) (result (ref $f64)) + ;; CHECK-NEXT: (array.new_data $f64 $constant-array-data_7 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $f64 (export "f64") (result (ref $f64)) + (array.new_fixed $f64 8 + (f64.const 0.1) + (f64.const 0.2) + (f64.const 0.3) + (f64.const 0.4) + (f64.const 0.5) + (f64.const 0.6) + (f64.const 0.7) + (f64.const nan:0x4) + ) + ) + + ;; CHECK: (func $v128 (type $15) (result (ref $v128)) + ;; CHECK-NEXT: (array.new_data $v128 $constant-array-data_8 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $v128 (export "v128") (result (ref $v128)) + (array.new_fixed $v128 4 + (v128.const i32x4 1 2 3 4) + (v128.const i32x4 5 6 7 8) + (v128.const i32x4 9 10 11 12) + (v128.const i32x4 13 14 15 16) + ) + ) + + ;; CHECK: (func $ref (type $16) (result (ref $ref)) + ;; CHECK-NEXT: (array.new_fixed $ref 2 + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref (export "ref") (result (ref $ref)) + ;; Non-numeric arrays are not optimized. + (array.new_fixed $ref 2 + (ref.null none) + (ref.null none) + ) + ) + + ;; CHECK: (func $empty (type $1) (result (ref $i32)) + ;; CHECK-NEXT: (array.new_fixed $i32 0) + ;; CHECK-NEXT: ) + (func $empty (export "empty") (result (ref $i32)) + ;; Empty arrays are not optimized. + (array.new_fixed $i32 0) + ) +) + +;; A new segment must pay for its flags and size. (There is already a segment, +;; so it does not also need to pay for the data count and data sections.) +(module + ;; CHECK: (type $i32 (array (mut i32))) + (type $i32 (array (mut i32))) + + ;; CHECK: (type $1 (func (result (ref $i32)))) + + ;; CHECK: (data $unrelated "\00") + (data $unrelated "\00") + + ;; CHECK: (export "new" (func $new)) + + ;; CHECK: (func $new (type $1) (result (ref $i32)) + ;; CHECK-NEXT: (array.new_fixed $i32 8 + ;; CHECK-NEXT: (i32.const 1000000) + ;; CHECK-NEXT: (i32.const -1000000) + ;; CHECK-NEXT: (i32.const 1000001) + ;; CHECK-NEXT: (i32.const 4000000) + ;; CHECK-NEXT: (i32.const 5000000) + ;; CHECK-NEXT: (i32.const 6000000) + ;; CHECK-NEXT: (i32.const 7000000) + ;; CHECK-NEXT: (i32.const 8000000) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $new (export "new") (result (ref $i32)) + ;; This would save 1 byte, which does not pay for the new segment's flags + ;; and size. + (array.new_fixed $i32 8 + (i32.const 1000000) + (i32.const -1000000) + (i32.const 1000001) + (i32.const 4000000) + (i32.const 5000000) + (i32.const 6000000) + (i32.const 7000000) + (i32.const 8000000) + ) + ) +)