diff --git a/README.md b/README.md index 8198b49cc76..4d2e4206ba6 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,8 @@ overview of some of the relevant ones: removes unneeded parts, etc. * **MergeBlocks** - Merge a `block` to an outer one where possible, reducing their number. +* **MergeDataSegments** - Merge active data segments with adjacent offsets into + a single data segment. * **MergeLocals** - When two locals have the same value in part of their overlap, pick in a way to help CoalesceLocals do better later (split off from CoalesceLocals to keep the latter simple). diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index b002151e0f2..89571298af9 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -2671,6 +2671,7 @@ def write_commands(commands, filename): ("--generate-stack-ir",), ("--licm",), ("--local-subtyping",), + ("--merge-data-segments",), ("--memory-packing",), ("--merge-blocks",), ('--merge-locals',), diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index c2952e174b8..d03d26fe47a 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -68,6 +68,7 @@ set(passes_SOURCES Memory64Lowering.cpp MemoryPacking.cpp MergeBlocks.cpp + MergeDataSegments.cpp MergeSimilarFunctions.cpp MergeLocals.cpp Metrics.cpp diff --git a/src/passes/MergeDataSegments.cpp b/src/passes/MergeDataSegments.cpp new file mode 100644 index 00000000000..1856bd41b70 --- /dev/null +++ b/src/passes/MergeDataSegments.cpp @@ -0,0 +1,580 @@ +/* + * 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. + */ + +// +// Merges adjacent active data segments into a single data segment. The name of +// the merged segment is the name of the input segment with the lowest offset. +// If the memory is known to be zero-initialized, we can also merge +// near-adjacent data segments according to a size heuristic. We must be careful +// to flush all merged segments for a memory before adding a segment of +// non-constant offset. Unless TNH is enabled, we must also be careful to flush +// all merged segments for all memories before adding a segment that may cause +// an out-of-bounds trap. +// + +#include + +#include "pass.h" +#include "support/stdckdint.h" +#include "wasm-builder.h" +#include "wasm.h" + +namespace wasm { + +namespace { + +// The maximum size possible for a single data segment. +constexpr uint64_t MAX_SEG_SIZE = std::numeric_limits::max(); + +struct SegmentEntry { + Address start; + Name name; + mutable std::vector data; + Address end() const { return start + data.size(); } + + struct CompareStart { + using is_transparent = void; + + bool operator()(const SegmentEntry& lhs, const SegmentEntry& rhs) const { + return lhs.start < rhs.start; + } + + bool operator()(const SegmentEntry& lhs, Address rhs) const { + return lhs.start < rhs; + } + + bool operator()(Address lhs, const SegmentEntry& rhs) const { + return lhs < rhs.start; + } + }; + + // Check if we can merge this entry while respecting MAX_SEG_SIZE. + bool canMergeInto(std::set& map) const { + if (data.empty()) { + return true; + } + + size_t mergedSize = data.size(); + auto it = map.upper_bound(start); + if (it != map.begin()) { + --it; + if (start <= it->end()) { + mergedSize += start - it->start; + } + } + it = map.upper_bound(end()); + if (it != map.begin()) { + --it; + if (end() <= it->end()) { + mergedSize += it->end() - end(); + } + } + + return mergedSize <= MAX_SEG_SIZE; + } + + // Simple merge algorithm, joining together adjacent entries. + void mergeInto(std::set& map) const { + if (data.empty()) { + return; + } + + // If there exists an overlapping or adjacent entry before the new entry, + // then subsume the new entry into the old entry. Otherwise, simply add the + // new entry to the map. + auto it = map.upper_bound(start); + auto merged = it; + if (it != map.begin()) { + --merged; + if (start <= merged->end()) { + auto head = start - merged->start; + auto tail = merged->data.size() - head; + // Copy all bytes up to the old entry's size, then append any remaining + // bytes. + if (data.size() <= tail) { + std::copy(data.begin(), data.end(), merged->data.begin() + head); + } else { + std::copy( + data.begin(), data.begin() + tail, merged->data.begin() + head); + merged->data.insert( + merged->data.end(), data.begin() + tail, data.end()); + } + } else { + merged = map.emplace_hint(it, *this); + } + } else { + merged = map.emplace_hint(it, *this); + } + + // Subsume any further overlapping or adjacent entries into the merged + // entry. + while (it != map.end() && it->start <= merged->end()) { + if (merged->end() < it->end()) { + merged->data.insert(merged->data.end(), + it->data.begin() + (merged->end() - it->start), + it->data.end()); + } + it = map.erase(it); + } + } +}; + +using SegmentMap = std::set; + +// Information about the bounds check that triggered the previous flush. The +// segment name is used as a hint when synthesizing an empty segment. +struct BoundsCheck { + Name mem; + Name seg; + Address lastPageStart; +}; + +// Bytes needed to represent a nonnegative integer in the unsigned LEB encoding. +size_t ulebSize(uint64_t x) { return (std::bit_width(x) + 6) / 7; } +// Bytes needed to represent a nonnegative integer in the signed LEB encoding. +size_t slebSize(uint64_t x) { return (std::bit_width(x) + 7) / 7; } + +enum InBounds { No, Maybe, Yes }; + +struct MergeInfo { + Memory* mem; + Address knownSize; + SegmentMap flushedSegments; + SegmentMap newSegments; + bool zeroFilled; + + // Determine whether the initialization of a new data segment can possibly + // succeed, and update the known size of the memory accordingly. If this + // method returns No, then initializing the data segment will invariably + // result in a trap during instantiation. This method should return Maybe or + // Yes before the segment is added to a SegmentMap, otherwise address + // overflows could occur in the merge algorithm. + InBounds inBounds(Address start, size_t size) { + if (size > MAX_SEG_SIZE) { + return InBounds::No; + } + bool end64 = false; + uint64_t end, lastAddr = std::numeric_limits::max(); + if (std::ckd_add(&end, start, size)) { + // The spec permits a segment to end at address 2^64 exactly, but we + // cannot handle it, so either return No or throw an error. + if (end != 0) { + return InBounds::No; + } + end64 = true; + } else { + if (end == 0) { + return InBounds::Yes; + } + lastAddr = end - 1; + } + uint64_t lastPage = lastAddr >> mem->pageSizeLog2; + if (lastPage < knownSize) { + if (end64) { + Fatal() << "MergeDataSegments does not support offset 2^64-1"; + } + return InBounds::Yes; + } else if (!mem->imported() || (mem->hasMax() && lastPage >= mem->max)) { + return InBounds::No; + } else { + if (end64) { + Fatal() << "MergeDataSegments does not support offset 2^64-1"; + } + knownSize = lastPage + 1; + return InBounds::Maybe; + } + } + + // Retrieve a range of backing data from flushedSegments. Returns true if all + // bytes could be retrieved without any gaps. + bool flushedData(std::vector& dest, Address start, size_t size) { + dest.clear(); + dest.reserve(size); + Address end = start + size; + + auto it = flushedSegments.upper_bound(start); + if (it != flushedSegments.begin()) { + auto preIt = it; + --preIt; + if (start < preIt->end()) { + if (end <= preIt->end()) { + dest.assign(preIt->data.begin() + (start - preIt->start), + preIt->data.begin() + (end - preIt->start)); + return true; + } + dest.assign(preIt->data.begin() + (start - preIt->start), + preIt->data.end()); + } + } + + while (it != flushedSegments.end()) { + if (dest.size() < it->start - start) { + if (!zeroFilled) { + return false; + } + dest.resize(it->start - start); + } + if (end <= it->end()) { + dest.insert( + dest.end(), it->data.begin(), it->data.begin() + (end - it->start)); + return true; + } + dest.insert(dest.end(), it->data.begin(), it->data.end()); + ++it; + } + if (!zeroFilled) { + return false; + } + dest.resize(size); + return true; + } + + // Merge near-adjacent entries in newSegments according to a size heuristic. + void mergeNearAdjacent() { + if (newSegments.size() < 2) { + return; + } + // Pessimistically assume that all data segments use the implicit memory 0 + // encoding. Then, the total size of a data segment is 3 + slebSize(offset) + // + ulebSize(size) + size. We greedily attempt to merge segments in a + // single pass from lower to higher addresses. + auto left = newSegments.begin(); + auto right = left; + ++right; + std::vector gapData; + while (right != newSegments.end()) { + uint64_t leftSize = left->data.size(); + uint64_t rightSize = right->data.size(); + uint64_t gapSize = right->start - left->end(); + uint64_t mergedSize = leftSize + gapSize + rightSize; + if (mergedSize > MAX_SEG_SIZE) { + left = right++; + continue; + } + + uint64_t leftSegSize = + leftSize + 3 + slebSize(left->start) + ulebSize(leftSize); + uint64_t rightSegSize = + rightSize + 3 + slebSize(right->start) + ulebSize(rightSize); + uint64_t mergedSegSize = + mergedSize + 3 + slebSize(left->start) + ulebSize(mergedSize); + if (leftSegSize + rightSegSize < mergedSegSize) { + left = right++; + continue; + } + if (!flushedData(gapData, left->end(), gapSize)) { + left = right++; + continue; + } + + left->data.insert(left->data.end(), gapData.begin(), gapData.end()); + left->data.insert( + left->data.end(), right->data.begin(), right->data.end()); + right = newSegments.erase(right); + } + } + + void flushBoundsCheck(Module* module, + const BoundsCheck& boundsCheck, + bool clearFlushed) { + // Flush the first merged segment that overlaps the bounds-check page, so + // that the bounds check is triggered before any other segments are added. + assert(knownSize != 0); + auto it = newSegments.upper_bound(boundsCheck.lastPageStart); + bool hasEntry = false; + SegmentEntry entry; + if (it != newSegments.begin()) { + auto preIt = it; + --preIt; + if (boundsCheck.lastPageStart < preIt->end()) { + hasEntry = true; + entry = std::move(newSegments.extract(preIt).value()); + } + } + if (!hasEntry && it != newSegments.end()) { + hasEntry = true; + entry = std::move(newSegments.extract(it).value()); + } + if (hasEntry && !clearFlushed) { + entry.mergeInto(flushedSegments); + } + // If the last known page has no nonempty segments, synthesize a new empty + // segment. + if (!hasEntry) { + entry.start = boundsCheck.lastPageStart + 1; + entry.name = boundsCheck.seg; + } + flushEntry(module, std::move(entry)); + } + + void flush(Module* module, bool clearFlushed) { + // If the flush is triggered by a segment of non-constant offset, clear all + // previous data. + if (clearFlushed) { + flushedSegments.clear(); + } else { + for (const auto& seg : newSegments) { + seg.mergeInto(flushedSegments); + } + } + // Flush merged segments to the module in order. + while (!newSegments.empty()) { + flushEntry(module, + std::move(newSegments.extract(newSegments.begin()).value())); + } + } + + void flushEntry(Module* module, SegmentEntry&& entry) { + // Finish flushing an entry into a data segment in the underlying module. + auto* c = Builder(*module).makeConst( + Literal::makeFromInt64(entry.start, mem->addressType)); + auto seg = Builder::makeDataSegment(entry.name, mem->name, false, c); + seg->data = std::move(entry.data); + module->dataSegments.push_back(std::move(seg)); + } +}; + +void flushAll(Module* module, + std::unordered_map& infos, + std::optional& boundsCheck, + std::optional clearFlushedMem) { + for (const auto& mem : module->memories) { + infos[mem->name].mergeNearAdjacent(); + } + if (boundsCheck) { + infos[boundsCheck->mem].flushBoundsCheck( + module, *boundsCheck, boundsCheck->mem == clearFlushedMem); + boundsCheck.reset(); + } + for (const auto& mem : module->memories) { + infos[mem->name].flush(module, mem->name == clearFlushedMem); + } +} + +} // namespace + +struct MergeDataSegments : public Pass { + // This pass only modifies data segments and data-segment indices. + bool requiresNonNullableLocalFixups() override { return false; } + + void run(Module* module) override { + bool trapsNeverHappen = getPassOptions().trapsNeverHappen; + bool zeroFilledMemory = getPassOptions().zeroFilledMemory; + + if (module->dataSegments.empty()) { + return; + } + + // Initialize the MergeInfo list with each memory in the module. + std::unordered_map infos; + for (const auto& mem : module->memories) { + auto& info = infos[mem->name]; + info.mem = mem.get(); + info.knownSize = mem->initial; + info.zeroFilled = zeroFilledMemory || !mem->imported(); + } + + // Gather all active segments from the module, leaving passive segments + // behind. Also gather all active segment names for the final renaming step. + std::vector> activeSegments; + std::unordered_set activeNames; + for (auto& seg : module->dataSegments) { + if (!seg->isPassive) { + activeNames.insert(seg->name); + activeSegments.push_back(std::move(seg)); + } + } + std::erase(module->dataSegments, nullptr); + + // To avoid changing observable behavior, we flush all existing data before + // adding a new data segment that may be out-of-bounds. Between flushes, we + // use boundsCheck to lazily keep track of which memory last triggered a + // bounds check, so that we can flush a corresponding bounds-check segment + // before flushing any other data. + std::optional boundsCheck = std::nullopt; + // Retain an emptySegment in case we need a target for the renaming step, + // but no active segments remain after removing empty segments. + std::unique_ptr emptySegment = nullptr; + // If a segment is guaranteed to cause an out-of-bounds trap, then we flush + // all prior segments, copy it verbatim, then drop all remaining segments. + std::unique_ptr trapSegment = nullptr; + + for (auto& seg : activeSegments) { + auto& info = infos[seg->memory]; + + if (auto* c = seg->offset->dynCast()) { + Address start = c->value.getUnsigned(); + auto inBounds = info.inBounds(start, seg->data.size()); + if (inBounds == InBounds::No) { + trapSegment = std::move(seg); + break; + } + + SegmentEntry entry; + entry.start = start; + entry.name = seg->name; + if (!seg->data.empty()) { + entry.data = std::move(seg->data); + } else if (!emptySegment) { + emptySegment = std::move(seg); + } + + // If a constant-offset segment is not statically in-bounds, flush all + // memories and mark its page as the next bounds-check page. + if (!trapsNeverHappen && inBounds != InBounds::Yes) { + auto neededSize = info.knownSize; + assert(neededSize != 0); + flushAll(module, infos, boundsCheck, std::nullopt); + boundsCheck = BoundsCheck(); + boundsCheck->mem = info.mem->name; + boundsCheck->seg = entry.name; + boundsCheck->lastPageStart = (neededSize - 1) + << info.mem->pageSizeLog2; + } + + // As a special fallback, flush the memory early if the merged segment + // would not respect MAX_SEG_SIZE. + if (!entry.canMergeInto(info.newSegments)) { + if (boundsCheck) { + infos[boundsCheck->mem].mergeNearAdjacent(); + infos[boundsCheck->mem].flushBoundsCheck( + module, *boundsCheck, boundsCheck->mem == seg->memory); + boundsCheck.reset(); + } + info.mergeNearAdjacent(); + info.flush(module, false); + } + + entry.mergeInto(info.newSegments); + } else { + if (!seg->data.empty()) { + // A nonempty non-constant-offset segment always flushes its own + // memory and invalidates all previous data. Unless TNH is enabled, it + // also requires all other memories to be flushed due to the bounds + // check. + if (trapsNeverHappen) { + if (boundsCheck) { + infos[boundsCheck->mem].mergeNearAdjacent(); + infos[boundsCheck->mem].flushBoundsCheck( + module, *boundsCheck, boundsCheck->mem == seg->memory); + boundsCheck.reset(); + } + info.mergeNearAdjacent(); + info.flush(module, true); + } else { + flushAll(module, infos, boundsCheck, seg->memory); + } + info.zeroFilled = false; + + // For the bounds check, conservatively assume that the offset is 0. + if (info.inBounds(0, seg->data.size()) == InBounds::No) { + trapSegment = std::move(seg); + break; + } + module->dataSegments.push_back(std::move(seg)); + } else { + // An empty non-constant-offset segment only triggers a bounds check. + if (!trapsNeverHappen) { + flushAll(module, infos, boundsCheck, std::nullopt); + module->dataSegments.push_back(std::move(seg)); + } + } + } + } + + // If there were no active segments in the input, then we have no more work + // to do after regenerating the module's map. + if (activeNames.empty()) { + module->updateDataSegmentsMap(); + return; + } + + // Flush all remaining segments, then copy any trap segment. + flushAll(module, infos, boundsCheck, std::nullopt); + if (trapSegment) { + module->dataSegments.push_back(std::move(trapSegment)); + } + module->updateDataSegmentsMap(); + + // Determine a target segment for any instructions that refer to an active + // segment. If there are no active segments left in the output, then there + // must have been an empty active segment in the input, which we have + // retained in emptySegment. + std::optional firstActive = std::nullopt; + for (const auto& seg : module->dataSegments) { + if (!seg->isPassive) { + firstActive = seg->name; + break; + } + } + assert(firstActive || emptySegment); + Name targetName = firstActive ? *firstActive : emptySegment->name; + + struct ActiveSegmentRenamer + : public WalkerPass> { + // This pass only modifies data-segment indices. + bool requiresNonNullableLocalFixups() override { return false; } + + std::unordered_set srcNames; + Name targetName; + bool targetUsed = false; + + ActiveSegmentRenamer(std::unordered_set srcNames, Name targetName) + : srcNames(std::move(srcNames)), targetName(targetName) {} + + void visitMemoryInit(MemoryInit* curr) { + if (srcNames.contains(curr->segment)) { + curr->segment = targetName; + targetUsed = true; + } + } + + void visitDataDrop(DataDrop* curr) { + if (srcNames.contains(curr->segment)) { + curr->segment = targetName; + targetUsed = true; + } + } + + void visitArrayNewData(ArrayNewData* curr) { + if (srcNames.contains(curr->segment)) { + curr->segment = targetName; + targetUsed = true; + } + } + + void visitArrayInitData(ArrayInitData* curr) { + if (srcNames.contains(curr->segment)) { + curr->segment = targetName; + targetUsed = true; + } + } + }; + + // Replace the names, then add an empty target segment if needed. + ActiveSegmentRenamer renamer(std::move(activeNames), targetName); + renamer.run(getPassRunner(), module); + renamer.runOnModuleCode(getPassRunner(), module); + if (renamer.targetUsed && !firstActive) { + module->dataSegments.push_back(std::move(emptySegment)); + module->updateDataSegmentsMap(); + } + } +}; + +Pass* createMergeDataSegmentsPass() { return new MergeDataSegments(); } + +} // namespace wasm diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index e5de76176ba..ddffa335edf 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -293,6 +293,9 @@ void PassRegistry::registerPasses() { createMemoryPackingPass); registerPass( "merge-blocks", "merges blocks to their parents", createMergeBlocksPass); + registerPass("merge-data-segments", + "merges adjacent active data segments into a single segment", + createMergeDataSegmentsPass); registerPass("merge-similar-functions", "merges similar functions when benefical", createMergeSimilarFunctionsPass); @@ -821,6 +824,7 @@ void PassRunner::addDefaultGlobalOptimizationPostPasses() { } else { addIfNoDWARFIssues("simplify-globals"); } + addIfNoDWARFIssues("merge-data-segments"); addIfNoDWARFIssues("remove-unused-module-elements"); if (options.optimizeLevel >= 2 && wasm->features.hasStrings()) { // Gather strings to globals right before reorder-globals, which will then diff --git a/src/passes/passes.h b/src/passes/passes.h index be06369a9f8..cc7ca31e2ba 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -90,6 +90,7 @@ Pass* createLoopInvariantCodeMotionPass(); Pass* createMemory64LoweringPass(); Pass* createMemoryPackingPass(); Pass* createMergeBlocksPass(); +Pass* createMergeDataSegmentsPass(); Pass* createMergeSimilarFunctionsPass(); Pass* createMergeLocalsPass(); Pass* createMinifiedPrinterPass(); diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index 1b0cc7d4569..71c145b0f37 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -265,6 +265,9 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --merge-blocks merges blocks to their parents ;; CHECK-NEXT: +;; CHECK-NEXT: --merge-data-segments merges adjacent active data +;; CHECK-NEXT: segments into a single segment +;; CHECK-NEXT: ;; CHECK-NEXT: --merge-j2cl-itables Merges itable structures into ;; CHECK-NEXT: vtables to make types more ;; CHECK-NEXT: compact diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index d616e1cf085..4e72a3f967e 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -297,6 +297,9 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --merge-blocks merges blocks to their parents ;; CHECK-NEXT: +;; CHECK-NEXT: --merge-data-segments merges adjacent active data +;; CHECK-NEXT: segments into a single segment +;; CHECK-NEXT: ;; CHECK-NEXT: --merge-j2cl-itables Merges itable structures into ;; CHECK-NEXT: vtables to make types more ;; CHECK-NEXT: compact diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index a91d5b5c050..3307e117d53 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -229,6 +229,9 @@ ;; CHECK-NEXT: ;; CHECK-NEXT: --merge-blocks merges blocks to their parents ;; CHECK-NEXT: +;; CHECK-NEXT: --merge-data-segments merges adjacent active data +;; CHECK-NEXT: segments into a single segment +;; CHECK-NEXT: ;; CHECK-NEXT: --merge-j2cl-itables Merges itable structures into ;; CHECK-NEXT: vtables to make types more ;; CHECK-NEXT: compact diff --git a/test/lit/passes/merge-data-segments-tnh.wast b/test/lit/passes/merge-data-segments-tnh.wast new file mode 100644 index 00000000000..cb8c941719b --- /dev/null +++ b/test/lit/passes/merge-data-segments-tnh.wast @@ -0,0 +1,164 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --merge-data-segments -tnh -S -o - | filecheck %s + +;; Guaranteed traps remain guaranteed under TNH. It's nontrivial to "drop +;; absolutely everything" from a module that can never be instantiated, so to +;; simplify the implementation, we just reuse the non-TNH behavior. Thus, these +;; are identical to the non-TNH tests for guaranteed traps. We use a second +;; memory for the dead segment to distinguish between the trap behavior and the +;; drop behavior. + +;; Segment $1 ends outside memory $0's max size. +(module + ;; CHECK: (memory $0 0 0) + (memory $0 0 0) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + (data $0 (i32.const 0) "") + ;; CHECK: (data $1 (i32.const 1) "") + (data $1 (i32.const 1) "") + (data $2 (memory $1) (i32.const 0) "dead") +) + +;; Segment $1 ends outside memory $0's max size. +(module + ;; CHECK: (import "" "" (memory $0 0 0)) + (import "" "" (memory $0 0 0)) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + (data $0 (i32.const 0) "") + ;; CHECK: (data $1 (i32.const 1) "") + (data $1 (i32.const 1) "") + (data $2 (memory $1) (i32.const 0) "dead") +) + +;; Segment $1 ends outside memory $0's initial size, and the memory cannot be +;; any larger since it is not imported. +(module + ;; CHECK: (memory $0 0) + (memory $0 0) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + (data $0 (i32.const 0) "") + ;; CHECK: (data $1 (i32.const 1) "") + (data $1 (i32.const 1) "") + (data $2 (memory $1) (i32.const 0) "dead") +) + +;; Empty non-constant-offset segments are dropped under TNH. +(module + ;; CHECK: (global $0 i32 (i32.const 0)) + (global $0 i32 (i32.const 0)) + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 0) "foobar") + (data $0 (i32.const 0) "foo") + (data $1 (global.get $0) "") + (data $2 (i32.const 3) "bar") +) + +;; Nonempty non-constant-offset segments trigger no bounds checks under TNH: +;; segment $1 overwriting memory $1 does not cause memory $0 to be flushed, so +;; the other segments are merged freely. +(module + ;; CHECK: (global $0 i32 (i32.const 0)) + (global $0 i32 (i32.const 0)) + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (data $1 (memory $1) (global.get $0) "bar") + + ;; CHECK: (data $0 (i32.const 0) "foopez") + (data $0 (i32.const 0) "foo") + (data $1 (memory $1) (global.get $0) "bar") + (data $2 (i32.const 3) "pez") +) + +;; Nonempty non-constant-offset segments still flush their own memory: segment +;; $1 causes memory $0 to be flushed. +(module + ;; CHECK: (global $0 i32 (i32.const 0)) + (global $0 i32 (i32.const 0)) + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 0) "foo") + (data $0 (i32.const 0) "foo") + ;; CHECK: (data $1 (global.get $0) "bar") + (data $1 (global.get $0) "bar") + ;; CHECK: (data $2 (i32.const 3) "pez") + (data $2 (i32.const 3) "pez") +) + +;; Nonempty non-constant-offset segments still invalidate flushed data: the data +;; in segment $0 cannot be used to merge the near-adjacent segments $2 and $3. +(module + ;; CHECK: (global $0 i32 (i32.const 0)) + (global $0 i32 (i32.const 0)) + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 3) "bar") + (data $0 (i32.const 3) "bar") + ;; CHECK: (data $1 (global.get $0) "\00") + (data $1 (global.get $0) "\00") + ;; CHECK: (data $2 (i32.const 0) "foo") + (data $2 (i32.const 0) "foo") + ;; CHECK: (data $3 (i32.const 6) "pez") + (data $3 (i32.const 6) "pez") +) + +;; Bounds checks within memory limits are assumed to succeed under TNH: no +;; flushes occur, and segments are merged freely. +(module + ;; CHECK: (import "" "" (memory $0 0)) + (import "" "" (memory $0 0)) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (data $1 (i32.const 0) "fooQUX") + + ;; CHECK: (data $3 (i32.const 65536) "bar") + + ;; CHECK: (data $5 (i32.const 131072) "pez") + + ;; CHECK: (data $7 (i32.const 196608) "qux") + + ;; CHECK: (data $0 (memory $1) (i32.const 0) "post") + (data $0 (memory $1) (i32.const 0) "pre") + (data $1 (i32.const 0) "foo") + (data $2 (i32.const 3) "FOO") + (data $3 (i32.const 65536) "bar") + (data $4 (i32.const 3) "BAR") + (data $5 (i32.const 131072) "pez") + (data $6 (i32.const 3) "PEZ") + (data $7 (i32.const 196608) "qux") + (data $8 (i32.const 3) "QUX") + (data $9 (memory $1) (i32.const 0) "post") +) + +;; Since no flushes occur, reordering input segments in the previous test does +;; not change the shape of the final data segments, only their contents. +(module + ;; CHECK: (import "" "" (memory $0 0)) + (import "" "" (memory $0 0)) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (data $7 (i32.const 0) "fooFOO") + + ;; CHECK: (data $5 (i32.const 65536) "bar") + + ;; CHECK: (data $3 (i32.const 131072) "pez") + + ;; CHECK: (data $1 (i32.const 196608) "qux") + + ;; CHECK: (data $0 (memory $1) (i32.const 0) "post") + (data $0 (memory $1) (i32.const 0) "pre") + (data $1 (i32.const 196608) "qux") + (data $2 (i32.const 3) "QUX") + (data $3 (i32.const 131072) "pez") + (data $4 (i32.const 3) "PEZ") + (data $5 (i32.const 65536) "bar") + (data $6 (i32.const 3) "BAR") + (data $7 (i32.const 0) "foo") + (data $8 (i32.const 3) "FOO") + (data $9 (memory $1) (i32.const 0) "post") +) diff --git a/test/lit/passes/merge-data-segments.wast b/test/lit/passes/merge-data-segments.wast new file mode 100644 index 00000000000..e1b8c3e2e7f --- /dev/null +++ b/test/lit/passes/merge-data-segments.wast @@ -0,0 +1,939 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --merge-data-segments -S -o - | filecheck %s + +;; Basic tests for the merge algorithm: it should merge adjacent and overlapping +;; segments in their order of appearance. The name of the merged segment should +;; be the name of an input segment with the lowest start address. As a +;; tiebreaker, when multiple input segments have the same start address, the +;; name is taken from the first-appearing input segment with the lowest start +;; address. + +;; A typical forward merge of adjacent segments. Segments that are directly +;; adjacent can always be merged, except in the edge case that the merged data +;; would be too long for a single data segment. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 3) "foobarpezqux") + (data $0 (i32.const 3) "foo") + (data $1 (i32.const 6) "bar") + (data $2 (i32.const 9) "pez") + (data $3 (i32.const 12) "qux") +) + +;; The same merge, but with the segments appearing in reverse order. The +;; resulting data should be the same, since adjacent merge respects address +;; order instead of appearance order. Even though segment $3 appears last, the +;; merged segment takes its name since it has the lowest start address. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + (data $0 (i32.const 12) "qux") + (data $1 (i32.const 9) "pez") + (data $2 (i32.const 6) "bar") + ;; CHECK: (data $3 (i32.const 3) "foobarpezqux") + (data $3 (i32.const 3) "foo") +) + +;; The following tests cover overlapping merge cases. Per the spec, active data +;; segments are written to their memories in order of appearance, so when they +;; overlap, we should take the data from the last-appearing segment for each +;; address. + +;; Growing segments with a common start address. We take the name of segment $0 +;; since it is the first-appearing segment at address 0. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 0) "quuuux") + (data $0 (i32.const 0) "foo") + (data $1 (i32.const 0) "baar") + (data $2 (i32.const 0) "peeez") + (data $3 (i32.const 0) "quuuux") +) + +;; Growing segments with a common end address. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + (data $0 (i32.const 3) "foo") + (data $1 (i32.const 2) "baar") + (data $2 (i32.const 1) "peeez") + ;; CHECK: (data $3 (i32.const 0) "quuuux") + (data $3 (i32.const 0) "quuuux") +) + +;; Growing segments with differing start and end addresses. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + (data $0 (i32.const 3) "foo") + (data $1 (i32.const 2) "baaar") + (data $2 (i32.const 1) "peeeeez") + ;; CHECK: (data $3 (i32.const 0) "quuuuuuux") + (data $3 (i32.const 0) "quuuuuuux") +) + +;; Shrinking segments with a common start address. Again, we take the name of +;; segment $0 since it is the first-appearing segment at address 0. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 0) "foorzx") + (data $0 (i32.const 0) "quuuux") + (data $1 (i32.const 0) "peeez") + (data $2 (i32.const 0) "baar") + (data $3 (i32.const 0) "foo") +) + +;; Shrinking segments with a common end address. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 0) "qpbfoo") + (data $0 (i32.const 0) "quuuux") + (data $1 (i32.const 1) "peeez") + (data $2 (i32.const 2) "baar") + (data $3 (i32.const 3) "foo") +) + +;; Shrinking segments with differing start and end addresses. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 0) "qpbfoorzx") + (data $0 (i32.const 0) "quuuuuuux") + (data $1 (i32.const 1) "peeeeez") + (data $2 (i32.const 2) "baaar") + (data $3 (i32.const 3) "foo") +) + +;; Long forward chain of partially overlapping merges. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 0) "fobapequFOBAPEQUX") + (data $0 (i32.const 0) "foo") + (data $1 (i32.const 2) "bar") + (data $2 (i32.const 4) "pez") + (data $3 (i32.const 6) "qux") + (data $4 (i32.const 8) "FOO") + (data $5 (i32.const 10) "BAR") + (data $6 (i32.const 12) "PEZ") + (data $7 (i32.const 14) "QUX") +) + +;; Long reverse chain of partially overlapping merges. We take the name of +;; segment $7 since it has the lowest start address. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + (data $0 (i32.const 14) "QUX") + (data $1 (i32.const 12) "PEZ") + (data $2 (i32.const 10) "BAR") + (data $3 (i32.const 8) "FOO") + (data $4 (i32.const 6) "qux") + (data $5 (i32.const 4) "pez") + (data $6 (i32.const 2) "bar") + ;; CHECK: (data $7 (i32.const 0) "fooarezuxOOAREZUX") + (data $7 (i32.const 0) "foo") +) + +;; Repeatedly overwriting different parts of a merged segment. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 0) "QUXPEZ") + (data $0 (i32.const 0) "foo") + (data $1 (i32.const 3) "bar") + (data $2 (i32.const 0) "pez") + (data $3 (i32.const 3) "qux") + (data $4 (i32.const 3) "FOO") + (data $5 (i32.const 0) "BAR") + (data $6 (i32.const 3) "PEZ") + (data $7 (i32.const 0) "QUX") +) + +;; Overwriting a merged segment while expanding it on both sides. We use the +;; name of segment $2 since it has the lowest start address. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + (data $0 (i32.const 3) "foobar") + (data $1 (i32.const 2) "qux") + ;; CHECK: (data $2 (i32.const 1) "quxxobfooux") + (data $2 (i32.const 1) "qux") + (data $3 (i32.const 9) "qux") + (data $4 (i32.const 7) "foo") +) + +;; Interleaving merges between two different merged segments, in forward and +;; reverse. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 0) "fobapez") + (data $0 (i32.const 0) "foo") + (data $1 (i32.const 20) "PEZ") + (data $2 (i32.const 2) "bar") + (data $3 (i32.const 18) "BAR") + (data $4 (i32.const 4) "pez") + ;; CHECK: (data $5 (i32.const 16) "FOOAREZ") + (data $5 (i32.const 16) "FOO") +) + +;; The merge algorithm should operate independently on different memories, and +;; it should ensure that the offset types on merged segments correspond to their +;; respective memories. +(module + ;; CHECK: (memory $0 i64 1 1) + (memory $0 i64 1 1) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + (data $0 (memory $1) (i32.const 5) "PEZ") + ;; CHECK: (data $1 (i64.const 0) "fobapez") + (data $1 (i64.const 0) "foo") + (data $2 (memory $1) (i32.const 3) "BAR") + (data $3 (i64.const 2) "bar") + ;; CHECK: (data $4 (memory $1) (i32.const 1) "FOOAREZ") + (data $4 (memory $1) (i32.const 1) "FOO") + (data $5 (i64.const 4) "pez") +) + +;; More shuffling between data segments of different memories. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (memory $2 1 1) + (memory $2 1 1) + ;; CHECK: (memory $3 1 1) + (memory $3 1 1) + ;; CHECK: (data $4 (i32.const 0) "foobar") + + ;; CHECK: (data $5 (memory $1) (i32.const 1) "foor") + + ;; CHECK: (data $1 (memory $2) (i32.const 1) "bfoo") + + ;; CHECK: (data $0 (memory $3) (i32.const 0) "barfoo") + (data $0 (memory $3) (i32.const 0) "bar") + (data $1 (memory $2) (i32.const 1) "bar") + (data $2 (memory $1) (i32.const 2) "bar") + (data $3 (i32.const 3) "bar") + (data $4 (i32.const 0) "foo") + (data $5 (memory $1) (i32.const 1) "foo") + (data $6 (memory $2) (i32.const 2) "foo") + (data $7 (memory $3) (i32.const 3) "foo") +) + +;; Tests for passive segments and instruction rewriting. The spec demands that +;; every active segment effectively go through data.drop after initialization, +;; so every time an explicit instruction refers to an active segment, it has the +;; same effect as referring to an empty passive segment. When manipulating +;; segments, we just have to find some active segment name in the output to +;; replace all active segment names appearing in instructions in the input. + +;; Basic rewriting: All passive segments appear first in the output, and +;; references to them are unmodified; references to active segments are renamed +;; to the first-appearing active segment in the output. +(module + ;; CHECK: (type $1 (array (mut i8))) + + ;; CHECK: (type $0 (func)) + (type $0 (func)) + (type $1 (array (mut i8))) + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + (data $0 (i32.const 3) "qux") + ;; CHECK: (data $1 "foo") + (data $1 "foo") + ;; CHECK: (data $3 "pez") + + ;; CHECK: (data $2 (i32.const 0) "barqux") + (data $2 (i32.const 0) "bar") + (data $3 "pez") + ;; CHECK: (data $4 (i32.const 16) "FOO") + (data $4 (i32.const 16) "FOO") + ;; CHECK: (func $0 (type $0) + ;; CHECK-NEXT: (memory.init $3 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (data.drop $1) + ;; CHECK-NEXT: (array.init_data $1 $1 + ;; CHECK-NEXT: (array.new_data $1 $3 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (memory.init $2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (data.drop $2) + ;; CHECK-NEXT: (array.init_data $1 $2 + ;; CHECK-NEXT: (array.new_data $1 $2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $0 (type $0) + (memory.init $3 + (i32.const 0) + (i32.const 0) + (i32.const 0) + ) + (data.drop $1) + (array.init_data $1 $1 + (array.new_data $1 $3 + (i32.const 0) + (i32.const 0) + ) + (i32.const 0) + (i32.const 0) + (i32.const 0) + ) + (memory.init $2 + (i32.const 0) + (i32.const 0) + (i32.const 0) + ) + (data.drop $0) + (array.init_data $1 $0 + (array.new_data $1 $2 + (i32.const 0) + (i32.const 0) + ) + (i32.const 0) + (i32.const 0) + (i32.const 0) + ) + ) +) + +;; Usage of the empty target segment. Again, all passive segments appear first +;; in the output, and references to them are left unmodified. But in this case, +;; no active segments are left in the output to act as the target segment to +;; rename into. So if the renamer detects a reference to an active segment, we +;; add back the first-appearing empty active segment in the input to act as the +;; target, in this case segment $1. +(module + ;; CHECK: (type $1 (array (mut i8))) + + ;; CHECK: (type $0 (func)) + (type $0 (func)) + (type $1 (array (mut i8))) + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 "foo") + (data $0 "foo") + ;; CHECK: (data $5 "bar") + + ;; CHECK: (data $1 (i32.const 64) "") + (data $1 (i32.const 64) "") + (data $2 (i32.const 48) "") + (data $3 (i32.const 32) "") + (data $4 (i32.const 16) "") + (data $5 "bar") + ;; CHECK: (func $0 (type $0) + ;; CHECK-NEXT: (memory.init $5 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (data.drop $0) + ;; CHECK-NEXT: (array.init_data $1 $0 + ;; CHECK-NEXT: (array.new_data $1 $5 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (memory.init $1 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (data.drop $1) + ;; CHECK-NEXT: (array.init_data $1 $1 + ;; CHECK-NEXT: (array.new_data $1 $1 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $0 (type $0) + (memory.init $5 + (i32.const 0) + (i32.const 0) + (i32.const 0) + ) + (data.drop $0) + (array.init_data $1 $0 + (array.new_data $1 $5 + (i32.const 0) + (i32.const 0) + ) + (i32.const 0) + (i32.const 0) + (i32.const 0) + ) + (memory.init $4 + (i32.const 0) + (i32.const 0) + (i32.const 0) + ) + (data.drop $3) + (array.init_data $1 $2 + (array.new_data $1 $1 + (i32.const 0) + (i32.const 0) + ) + (i32.const 0) + (i32.const 0) + (i32.const 0) + ) + ) +) + +;; Tests for guaranteed traps. A "trap segment" is an active segment which is +;; statically out-of-bounds and whose initialization will always raise a trap +;; during module instantiation. After a trap segment, all remaining active +;; segments should be dropped. In most of these tests, we use a second memory +;; for the dead segment to distinguish between the trap behavior and the drop +;; behavior, since dropping should not depend on the memory. + +;; Passive segments following a trap segment should be retained, and references +;; to dead active segments should be renamed to a live segment. +(module + ;; CHECK: (type $0 (func)) + (type $0 (func)) + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $2 "bar") + + ;; CHECK: (data $4 "qux") + + ;; CHECK: (data $0 (i32.const 0) "foo") + (data $0 (i32.const 0) "foo") + ;; CHECK: (data $1 (i32.const 65536) "trap") + (data $1 (i32.const 65536) "trap") + (data $2 "bar") + (data $3 (i32.const 3) "pez") + (data $4 "qux") + ;; CHECK: (func $0 (type $0) + ;; CHECK-NEXT: (data.drop $4) + ;; CHECK-NEXT: (data.drop $0) + ;; CHECK-NEXT: ) + (func $0 (type $0) + (data.drop $4) + (data.drop $3) + ) +) + +;; Segment $1 ends outside memory $0's max size. +(module + ;; CHECK: (memory $0 0 0) + (memory $0 0 0) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + (data $0 (i32.const 0) "") + ;; CHECK: (data $1 (i32.const 1) "") + (data $1 (i32.const 1) "") + (data $2 (memory $1) (i32.const 0) "dead") +) + +;; An imported segment's max size must be no greater than its declared max size. +;; Thus, segment $1 must still end outside the imported memory $0's max size. +(module + ;; CHECK: (import "" "" (memory $0 0 0)) + (import "" "" (memory $0 0 0)) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + (data $0 (i32.const 0) "") + ;; CHECK: (data $1 (i32.const 1) "") + (data $1 (i32.const 1) "") + (data $2 (memory $1) (i32.const 0) "dead") +) + +;; Segment $1 ends outside memory $0's initial size. Since the memory is not +;; imported, its actual size during initialization is equal to its initial size, +;; so the trap is still guaranteed. +(module + ;; CHECK: (memory $0 0) + (memory $0 0) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + (data $0 (i32.const 0) "") + ;; CHECK: (data $1 (i32.const 1) "") + (data $1 (i32.const 1) "") + (data $2 (memory $1) (i32.const 0) "dead") +) + +;; Not a guaranteed trap: the initial size of an imported memory may be greater +;; than its initial size. When simulating bounds-check behavior, we use the +;; "known size" of a memory to track how large it must presently be, for all +;; previous initializations to have succeeded. +(module + ;; CHECK: (import "" "" (memory $0 0)) + (import "" "" (memory $0 0)) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + (data $0 (i32.const 0) "") + ;; CHECK: (data $1 (i32.const 1) "") + (data $1 (i32.const 1) "") + ;; CHECK: (data $2 (memory $1) (i32.const 0) "dead") + (data $2 (memory $1) (i32.const 0) "dead") +) + +;; Two in-bounds segments and an out-of-bounds segment for a small memory. +(module + ;; CHECK: (global $0 i32 (i32.const 64)) + (global $0 i32 (i32.const 64)) + ;; CHECK: (memory $0 4 4 (pagesize 1)) + (memory $0 4 4 (pagesize 1)) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (data $0 (global.get $0) "foo") + (data $0 (global.get $0) "foo") + ;; CHECK: (data $1 (global.get $0) "baar") + (data $1 (global.get $0) "baar") + ;; CHECK: (data $2 (global.get $0) "peeez") + (data $2 (global.get $0) "peeez") + (data $3 (memory $1) (i32.const 0) "dead") +) + +;; Merged segment, trap segment, and two dead segments. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 65532) "ffoo") + (data $0 (i32.const 65532) "foo") + (data $1 (i32.const 65533) "foo") + ;; CHECK: (data $2 (i32.const 65534) "foo") + (data $2 (i32.const 65534) "foo") + (data $3 (i32.const 65535) "foo") + (data $4 (i32.const 65536) "foo") +) + +;; Test for address overflow. Note that a module may validly contain a 2^64-byte +;; memory and active data segments ending at offset 2^64 exactly, but the pass +;; rejects that particular edge case as a fatal error. +(module + ;; CHECK: (import "" "" (memory $0 i64 0)) + (import "" "" (memory $0 i64 0)) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (data $0 (i64.const 0) "foo") + (data $0 (i64.const 0) "foo") + ;; CHECK: (data $1 (i64.const -4) "bar") + (data $1 (i64.const -4) "bar") + ;; CHECK: (data $2 (i64.const -4) "barpez") + (data $2 (i64.const -4) "barpez") + (data $3 (memory $1) (i32.const 0) "dead") +) + +;; Tests for bounds checks and flushing. We use a separate memory to distinguish +;; between bounds-check behavior and flushing behavior, since after a potential +;; bounds-check trap, all memories should be flushed at once. + +;; Before each point that a bounds-check trap may occur, all merged segments in +;; all memories should be flushed. After a trap, the embedder can observe any +;; prior modifications to imported memories, and all of these modifications +;; should be preserved to avoid differences in behavior. To simplify the logic, +;; we flush all memories, not just imported memories. +(module + ;; CHECK: (import "" "" (memory $0 0)) + (import "" "" (memory $0 0)) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (data $0 (memory $1) (i32.const 0) "pre") + (data $0 (memory $1) (i32.const 0) "pre") + ;; CHECK: (data $1 (i32.const 0) "fooFOO") + (data $1 (i32.const 0) "foo") + (data $2 (i32.const 3) "FOO") + ;; CHECK: (data $3 (i32.const 65536) "bar") + (data $3 (i32.const 65536) "bar") + ;; CHECK: (data $4 (i32.const 3) "BAR") + (data $4 (i32.const 3) "BAR") + ;; CHECK: (data $5 (i32.const 131072) "pez") + (data $5 (i32.const 131072) "pez") + ;; CHECK: (data $6 (i32.const 3) "PEZ") + (data $6 (i32.const 3) "PEZ") + ;; CHECK: (data $7 (i32.const 196608) "qux") + (data $7 (i32.const 196608) "qux") + ;; CHECK: (data $8 (i32.const 3) "QUX") + (data $8 (i32.const 3) "QUX") + ;; CHECK: (data $9 (memory $1) (i32.const 0) "post") + (data $9 (memory $1) (i32.const 0) "post") +) + +;; The same test, but writing in reverse order. Only segment $1 may trigger a +;; bounds-check trap: all future segments end earlier than $1 does. +(module + ;; CHECK: (import "" "" (memory $0 0)) + (import "" "" (memory $0 0)) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (data $0 (memory $1) (i32.const 0) "pre") + (data $0 (memory $1) (i32.const 0) "pre") + ;; CHECK: (data $1 (i32.const 196608) "qux") + (data $1 (i32.const 196608) "qux") + (data $2 (i32.const 3) "QUX") + ;; CHECK: (data $7 (i32.const 0) "fooFOO") + + ;; CHECK: (data $5 (i32.const 65536) "bar") + + ;; CHECK: (data $3 (i32.const 131072) "pez") + (data $3 (i32.const 131072) "pez") + (data $4 (i32.const 3) "PEZ") + (data $5 (i32.const 65536) "bar") + (data $6 (i32.const 3) "BAR") + (data $7 (i32.const 0) "foo") + (data $8 (i32.const 3) "FOO") + ;; CHECK: (data $9 (memory $1) (i32.const 0) "post") + (data $9 (memory $1) (i32.const 0) "post") +) + +;; Segments written in forward and reverse order. Segments $1 to $4 each cause a +;; flush: but after segment $5 causes a flush, segments $5 to $8 may be freely +;; merged. +(module + ;; CHECK: (import "" "" (memory $0 0 (pagesize 1))) + (import "" "" (memory $0 0 (pagesize 1))) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (data $0 (memory $1) (i32.const 0) "pre") + (data $0 (memory $1) (i32.const 0) "pre") + ;; CHECK: (data $1 (i32.const 0) "foo") + (data $1 (i32.const 0) "foo") + ;; CHECK: (data $2 (i32.const 1) "bar") + (data $2 (i32.const 1) "bar") + ;; CHECK: (data $3 (i32.const 2) "pez") + (data $3 (i32.const 2) "pez") + ;; CHECK: (data $4 (i32.const 3) "qux") + (data $4 (i32.const 3) "qux") + (data $5 (i32.const 19) "QUX") + (data $6 (i32.const 18) "PEZ") + (data $7 (i32.const 17) "BAR") + ;; CHECK: (data $8 (i32.const 16) "FOORZX") + (data $8 (i32.const 16) "FOO") + ;; CHECK: (data $9 (memory $1) (i32.const 0) "post") + (data $9 (memory $1) (i32.const 0) "post") +) + +;; Bounds-check behavior approaching the offset 2^64-1 boundary. +(module + ;; CHECK: (import "" "" (memory $0 i64 0 (pagesize 1))) + (import "" "" (memory $0 i64 0 (pagesize 1))) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (data $0 (memory $1) (i32.const 0) "pre") + (data $0 (memory $1) (i32.const 0) "pre") + ;; CHECK: (data $1 (i64.const -7) "foo") + (data $1 (i64.const -7) "foo") + ;; CHECK: (data $2 (i64.const -6) "bar") + (data $2 (i64.const -6) "bar") + ;; CHECK: (data $3 (i64.const -5) "pez") + (data $3 (i64.const -5) "pez") + (data $4 (i64.const -4) "qux") + ;; CHECK: (data $5 (i64.const -5) "PEZx") + (data $5 (i64.const -5) "PEZ") + ;; CHECK: (data $6 (memory $1) (i32.const 0) "post") + (data $6 (memory $1) (i32.const 0) "post") +) + +;; After previous segments are flushed, the data in a bounds-check segment need +;; not be flushed immediately: it can be freely merged with and overwritten by +;; later segments. But at the next flush, we must be careful to output this +;; merged segment before any other segments, so that it has the same +;; bounds-check behavior. We track this based on the start address of the last +;; page asserted to exist from a bounds check. +(module + ;; CHECK: (import "" "" (memory $0 0 (pagesize 1))) + (import "" "" (memory $0 0 (pagesize 1))) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (data $0 (memory $1) (i32.const 0) "pre") + (data $0 (memory $1) (i32.const 0) "pre") + ;; CHECK: (data $1 (i32.const 0) "fooo") + (data $1 (i32.const 0) "fooo") + (data $2 (i32.const 12) "QUUX") + (data $3 (i32.const 8) "PEEZ") + (data $4 (i32.const 4) "BAAR") + ;; CHECK: (data $5 (i32.const 0) "FOOOBAARPEEZquux") + (data $5 (i32.const 0) "FOOO") + (data $6 (i32.const 12) "quux") + ;; CHECK: (data $7 (memory $1) (i32.const 0) "post") + (data $7 (memory $1) (i32.const 0) "post") +) + +;; Tests for near-adjacent merge algorithm and zero-fill tracking. + +;; Basic near-adjacent merge, in forward and reverse order. It should operate +;; independently on different memories. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (data $0 (i32.const 0) "foo\00bar\00pez\00qux") + (data $0 (i32.const 0) "foo") + (data $1 (memory $1) (i32.const 12) "QUX") + (data $2 (i32.const 4) "bar") + (data $3 (memory $1) (i32.const 8) "PEZ") + (data $4 (i32.const 8) "pez") + (data $5 (memory $1) (i32.const 4) "BAR") + (data $6 (i32.const 12) "qux") + ;; CHECK: (data $7 (memory $1) (i32.const 0) "FOO\00BAR\00PEZ\00QUX") + (data $7 (memory $1) (i32.const 0) "FOO") +) + +;; Since the memories are imported in this case, we don't know what data lies +;; in the gaps, so a near-adjacent merge is not possible. +(module + ;; CHECK: (import "" "" (memory $0 1 1)) + (import "" "" (memory $0 1 1)) + ;; CHECK: (import "" "" (memory $1 1 1)) + (import "" "" (memory $1 1 1)) + ;; CHECK: (data $0 (i32.const 0) "foo") + (data $0 (i32.const 0) "foo") + ;; CHECK: (data $2 (i32.const 4) "bar") + + ;; CHECK: (data $4 (i32.const 8) "pez") + + ;; CHECK: (data $6 (i32.const 12) "qux") + + ;; CHECK: (data $7 (memory $1) (i32.const 0) "FOO") + + ;; CHECK: (data $5 (memory $1) (i32.const 4) "BAR") + + ;; CHECK: (data $3 (memory $1) (i32.const 8) "PEZ") + + ;; CHECK: (data $1 (memory $1) (i32.const 12) "QUX") + (data $1 (memory $1) (i32.const 12) "QUX") + (data $2 (i32.const 4) "bar") + (data $3 (memory $1) (i32.const 8) "PEZ") + (data $4 (i32.const 8) "pez") + (data $5 (memory $1) (i32.const 4) "BAR") + (data $6 (i32.const 12) "qux") + (data $7 (memory $1) (i32.const 0) "FOO") +) + +;; Near-adjacent merge, in forward and reverse order, is possible before a +;; nonempty non-constant-offset segment, but not afterward, since we don't know +;; where it may have written its data. +(module + ;; CHECK: (global $0 i32 (i32.const 0)) + (global $0 i32 (i32.const 0)) + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (memory $1 1 1) + (memory $1 1 1) + ;; CHECK: (data $0 (i32.const 0) "foo\00bar") + (data $0 (i32.const 0) "foo") + (data $1 (memory $1) (i32.const 12) "QUX") + (data $2 (i32.const 4) "bar") + ;; CHECK: (data $3 (memory $1) (i32.const 8) "PEZ\00QUX") + (data $3 (memory $1) (i32.const 8) "PEZ") + ;; CHECK: (data $4 (global.get $0) "\00") + (data $4 (global.get $0) "\00") + ;; CHECK: (data $5 (memory $1) (global.get $0) "\00") + (data $5 (memory $1) (global.get $0) "\00") + ;; CHECK: (data $6 (i32.const 8) "pez") + (data $6 (i32.const 8) "pez") + ;; CHECK: (data $8 (i32.const 12) "qux") + + ;; CHECK: (data $9 (memory $1) (i32.const 0) "FOO") + + ;; CHECK: (data $7 (memory $1) (i32.const 4) "BAR") + (data $7 (memory $1) (i32.const 4) "BAR") + (data $8 (i32.const 12) "qux") + (data $9 (memory $1) (i32.const 0) "FOO") +) + +;; Tests for near-adjacent merge algorithm and size heuristic. + +;; Near-adjacent merge is universally applied for small gaps, acting as an +;; extension of overlapping and adjacent merge. It works in forward and reverse +;; order. +(module + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 0) "bar") + (data $0 (i32.const 0) "foo") + (data $1 (i32.const 0) "bar") + (data $2 (i32.const 17) "bar") + ;; CHECK: (data $3 (i32.const 16) "foor") + (data $3 (i32.const 16) "foo") + ;; CHECK: (data $4 (i32.const 32) "fobar") + (data $4 (i32.const 32) "foo") + (data $5 (i32.const 34) "bar") + (data $6 (i32.const 51) "bar") + ;; CHECK: (data $7 (i32.const 48) "foobar") + (data $7 (i32.const 48) "foo") + ;; CHECK: (data $8 (i32.const 64) "foo\00bar") + (data $8 (i32.const 64) "foo") + (data $9 (i32.const 68) "bar") + (data $10 (i32.const 85) "bar") + ;; CHECK: (data $11 (i32.const 80) "foo\00\00bar") + (data $11 (i32.const 80) "foo") + ;; CHECK: (data $12 (i32.const 96) "foo\00\00\00bar") + (data $12 (i32.const 96) "foo") + (data $13 (i32.const 102) "bar") + (data $14 (i32.const 119) "bar") + ;; CHECK: (data $15 (i32.const 112) "foo\00\00\00\00bar") + (data $15 (i32.const 112) "foo") +) + +;; For large gaps, the size heuristic depends on the encoded size of the offset. +;; In this case, we influence the merge decision by alternately increasing the +;; gap size and increasing the offset size. (The heuristic similarly depends on +;; the encoded size of the data length, but that is more difficult to test.) +(module + ;; CHECK: (memory $0 17 17) + (memory $0 17 17) + ;; CHECK: (data $0 (i32.const 0) "foo\00\00\00\00\00bar") + (data $0 (i32.const 0) "foo") + (data $1 (i32.const 8) "bar") + ;; CHECK: (data $3 (i32.const 32) "foo") + + ;; CHECK: (data $2 (i32.const 41) "bar") + (data $2 (i32.const 41) "bar") + (data $3 (i32.const 32) "foo") + ;; CHECK: (data $4 (i32.const 64) "foo\00\00\00\00\00\00bar") + (data $4 (i32.const 64) "foo") + (data $5 (i32.const 73) "bar") + ;; CHECK: (data $7 (i32.const 96) "foo") + + ;; CHECK: (data $6 (i32.const 106) "bar") + (data $6 (i32.const 106) "bar") + (data $7 (i32.const 96) "foo") + ;; CHECK: (data $8 (i32.const 4096) "foo") + (data $8 (i32.const 4096) "foo") + ;; CHECK: (data $9 (i32.const 4106) "bar") + (data $9 (i32.const 4106) "bar") + (data $10 (i32.const 8202) "bar") + ;; CHECK: (data $11 (i32.const 8192) "foo\00\00\00\00\00\00\00bar") + (data $11 (i32.const 8192) "foo") + ;; CHECK: (data $12 (i32.const 8224) "foo") + (data $12 (i32.const 8224) "foo") + ;; CHECK: (data $13 (i32.const 8235) "bar") + (data $13 (i32.const 8235) "bar") + ;; CHECK: (data $15 (i32.const 524288) "foo") + + ;; CHECK: (data $14 (i32.const 524299) "bar") + (data $14 (i32.const 524299) "bar") + (data $15 (i32.const 524288) "foo") + ;; CHECK: (data $16 (i32.const 1048576) "foo\00\00\00\00\00\00\00\00bar") + (data $16 (i32.const 1048576) "foo") + (data $17 (i32.const 1048587) "bar") + ;; CHECK: (data $19 (i32.const 1048608) "foo") + + ;; CHECK: (data $18 (i32.const 1048620) "bar") + (data $18 (i32.const 1048620) "bar") + (data $19 (i32.const 1048608) "foo") +) + +;; Tests for near-adjacent merge algorithm and flushing. We use empty +;; non-constant-offset segments simply to force a flush. + +;; Near-adjacent merge uses data from previously flushed segments to fill gaps. +(module + ;; CHECK: (global $0 i32 (i32.const 0)) + (global $0 i32 (i32.const 0)) + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 4) "bar") + (data $0 (i32.const 4) "bar") + ;; CHECK: (data $1 (global.get $0) "") + (data $1 (global.get $0) "") + (data $2 (i32.const 8) "pez") + ;; CHECK: (data $3 (i32.const 0) "foo\00bar\00pez") + (data $3 (i32.const 0) "foo") +) + +;; Flushed segment data is combined across repeated flushes. +(module + ;; CHECK: (global $0 i32 (i32.const 0)) + (global $0 i32 (i32.const 0)) + ;; CHECK: (memory $0 1 1) + (memory $0 1 1) + ;; CHECK: (data $0 (i32.const 6) "B") + (data $0 (i32.const 6) "B") + ;; CHECK: (data $1 (global.get $0) "") + (data $1 (global.get $0) "") + ;; CHECK: (data $2 (i32.const 4) "A") + (data $2 (i32.const 4) "A") + ;; CHECK: (data $3 (global.get $0) "") + (data $3 (global.get $0) "") + (data $4 (i32.const 8) "bar") + ;; CHECK: (data $5 (i32.const 0) "foo\00A\00B\00bar") + (data $5 (i32.const 0) "foo") +) + +;; In this case, the memory is imported, and the flushed segment data is too +;; short to fill the gap, so near-adjacent merge is not possible. +(module + ;; CHECK: (import "" "" (memory $0 1 1)) + (import "" "" (memory $0 1 1)) + ;; CHECK: (global $0 i32 (i32.const 0)) + (global $0 i32 (i32.const 0)) + ;; CHECK: (data $0 (i32.const 4) "B") + (data $0 (i32.const 4) "B") + ;; CHECK: (data $1 (global.get $0) "") + (data $1 (global.get $0) "") + ;; CHECK: (data $2 (i32.const 0) "foo") + (data $2 (i32.const 0) "foo") + ;; CHECK: (data $3 (i32.const 6) "pez") + (data $3 (i32.const 6) "pez") +) + +;; In this case, even though the memory is imported, the flushed segment data +;; completely fills the gap, so near-adjacent merge is possible. +(module + ;; CHECK: (import "" "" (memory $0 1 1)) + (import "" "" (memory $0 1 1)) + ;; CHECK: (global $0 i32 (i32.const 0)) + (global $0 i32 (i32.const 0)) + ;; CHECK: (data $0 (i32.const 3) "bar") + (data $0 (i32.const 3) "bar") + ;; CHECK: (data $1 (global.get $0) "") + (data $1 (global.get $0) "") + ;; CHECK: (data $2 (i32.const 0) "foobarpez") + (data $2 (i32.const 0) "foo") + (data $3 (i32.const 6) "pez") +) + +;; Additional segments can be merged into a bounds-check segment via +;; near-adjacent merge, and it is still flushed before any other segments. +(module + ;; CHECK: (import "" "" (memory $0 1)) + (import "" "" (memory $0 1)) + ;; CHECK: (global $0 i32 (i32.const 0)) + (global $0 i32 (i32.const 0)) + ;; CHECK: (data $0 (i32.const 65535) ".") + (data $0 (i32.const 65535) ".") + ;; CHECK: (data $1 (global.get $0) "") + (data $1 (global.get $0) "") + (data $2 (i32.const 65536) "pez") + ;; CHECK: (data $3 (i32.const 65532) "bar.pez") + (data $3 (i32.const 65532) "bar") + ;; CHECK: (data $4 (i32.const 0) "foo") + (data $4 (i32.const 0) "foo") +)