summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/passes/CMakeLists.txt1
-rw-r--r--src/passes/MultiMemoryLowering.cpp421
-rw-r--r--src/passes/pass.cpp3
-rw-r--r--src/passes/passes.h1
-rw-r--r--src/wasm.h2
-rw-r--r--test/lit/help/wasm-opt.test3
-rw-r--r--test/lit/help/wasm2js.test3
-rw-r--r--test/lit/passes/multi-memory-lowering.wast247
8 files changed, 681 insertions, 0 deletions
diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt
index 152594431..cedb7d764 100644
--- a/src/passes/CMakeLists.txt
+++ b/src/passes/CMakeLists.txt
@@ -62,6 +62,7 @@ set(passes_SOURCES
MergeLocals.cpp
Metrics.cpp
MinifyImportsAndExports.cpp
+ MultiMemoryLowering.cpp
NameList.cpp
NameTypes.cpp
OnceReduction.cpp
diff --git a/src/passes/MultiMemoryLowering.cpp b/src/passes/MultiMemoryLowering.cpp
new file mode 100644
index 000000000..9e9637b36
--- /dev/null
+++ b/src/passes/MultiMemoryLowering.cpp
@@ -0,0 +1,421 @@
+/*
+ * Copyright 2022 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.
+ */
+
+//
+// Condensing a module with multiple memories into a module with a single memory
+// for browsers that don’t support multiple memories.
+//
+// This pass also disables multi-memories so that the target features section in
+// the emitted module does not report the use of MultiMemories. Disabling the
+// multi-memories feature also prevents later passes from adding additional
+// memories.
+//
+// Also worth noting that we are diverging from the spec with regards to
+// handling load and store instructions. We are not trapping if the offset +
+// write size is larger than the length of the memory's data. Warning:
+// out-of-bounds loads and stores can read junk out of or corrupt other
+// memories instead of trapping.
+
+#include "ir/module-utils.h"
+#include "ir/names.h"
+#include "wasm-builder.h"
+#include <pass.h>
+#include <wasm.h>
+
+namespace wasm {
+
+struct MultiMemoryLowering : public Pass {
+ Module* wasm = nullptr;
+ // The name of the single memory that exists after this pass is run
+ Name combinedMemory;
+ // The type of the single memory
+ Type pointerType;
+ // Used to indicate the type of the single memory when creating instructions
+ // (memory.grow, memory.size) for that memory
+ Builder::MemoryInfo memoryInfo;
+ // If the combined memory is shared
+ bool isShared;
+ // The initial page size of the combined memory
+ Address totalInitialPages;
+ // The max page size of the combined memory
+ Address totalMaxPages;
+ // There is no offset for the first memory, so offsetGlobalNames will always
+ // have a size that is one less than the count of memories at the time this
+ // pass is run. Use helper getOffsetGlobal(Index) to index the vector
+ // conveniently without having to manipulate the index directly
+ std::vector<Name> offsetGlobalNames;
+ // Maps from the name of the memory to its index as seen in the
+ // module->memories vector
+ std::unordered_map<Name, Index> memoryIdxMap;
+ // A vector of the memory size function names that were created proactively
+ // for each memory
+ std::vector<Name> memorySizeNames;
+ // A vector of the memory grow functions that were created proactively for
+ // each memory
+ std::vector<Name> memoryGrowNames;
+
+ void run(Module* module) override {
+ module->features.disable(FeatureSet::MultiMemories);
+
+ // If there are no memories or 1 memory, skip this pass
+ if (module->memories.size() <= 1) {
+ return;
+ }
+
+ this->wasm = module;
+
+ prepCombinedMemory();
+ addOffsetGlobals();
+ adjustActiveDataSegmentOffsets();
+ createMemorySizeFunctions();
+ createMemoryGrowFunctions();
+ removeExistingMemories();
+ addCombinedMemory();
+
+ struct Replacer : public WalkerPass<PostWalker<Replacer>> {
+ MultiMemoryLowering& parent;
+ Builder builder;
+ Replacer(MultiMemoryLowering& parent, Module& wasm)
+ : parent(parent), builder(wasm) {}
+ // Avoid visiting the custom functions added by the parent pass
+ // MultiMemoryLowering
+ void walkFunction(Function* func) {
+ for (Name funcName : parent.memorySizeNames) {
+ if (funcName == func->name) {
+ return;
+ }
+ }
+ for (Name funcName : parent.memoryGrowNames) {
+ if (funcName == func->name) {
+ return;
+ }
+ }
+ super::walkFunction(func);
+ }
+
+ void visitMemoryGrow(MemoryGrow* curr) {
+ auto idx = parent.memoryIdxMap.at(curr->memory);
+ Name funcName = parent.memoryGrowNames[idx];
+ replaceCurrent(builder.makeCall(funcName, {curr->delta}, curr->type));
+ }
+
+ void visitMemorySize(MemorySize* curr) {
+ auto idx = parent.memoryIdxMap.at(curr->memory);
+ Name funcName = parent.memorySizeNames[idx];
+ replaceCurrent(builder.makeCall(funcName, {}, curr->type));
+ }
+
+ // TODO: Add an option to add bounds checks.
+ void visitLoad(Load* curr) {
+ auto idx = parent.memoryIdxMap.at(curr->memory);
+ auto global = parent.getOffsetGlobal(idx);
+ curr->memory = parent.combinedMemory;
+ if (!global) {
+ return;
+ }
+ curr->ptr = builder.makeBinary(
+ Abstract::getBinary(parent.pointerType, Abstract::Add),
+ builder.makeGlobalGet(global, parent.pointerType),
+ curr->ptr);
+ }
+
+ // We diverge from the spec here and are not trapping if the offset + type
+ // / 8 is larger than the length of the memory's data. Warning,
+ // out-of-bounds loads and stores can read junk out of or corrupt other
+ // memories instead of trapping
+ void visitStore(Store* curr) {
+ auto idx = parent.memoryIdxMap.at(curr->memory);
+ auto global = parent.getOffsetGlobal(idx);
+ curr->memory = parent.combinedMemory;
+ if (!global) {
+ return;
+ }
+ curr->ptr = builder.makeBinary(
+ Abstract::getBinary(parent.pointerType, Abstract::Add),
+ builder.makeGlobalGet(global, parent.pointerType),
+ curr->ptr);
+ }
+ };
+ Replacer(*this, *wasm).run(getPassRunner(), wasm);
+ }
+
+ // Returns the global name for the given idx. There is no global for the first
+ // idx, so an empty name is returned
+ Name getOffsetGlobal(Index idx) {
+ // There is no offset global for the first memory
+ if (idx == 0) {
+ return Name();
+ }
+
+ // Since there is no offset global for the first memory, we need to
+ // subtract one when indexing into the offsetGlobalName vector
+ return offsetGlobalNames[idx - 1];
+ }
+
+ // Whether the idx represents the last memory. Since there is no offset global
+ // for the first memory, the last memory is represented by the size of
+ // offsetGlobalNames
+ bool isLastMemory(Index idx) { return idx == offsetGlobalNames.size(); }
+
+ void prepCombinedMemory() {
+ pointerType = wasm->memories[0]->indexType;
+ memoryInfo = pointerType == Type::i32 ? Builder::MemoryInfo::Memory32
+ : Builder::MemoryInfo::Memory64;
+ isShared = wasm->memories[0]->shared;
+ for (auto& memory : wasm->memories) {
+ // We are assuming that each memory is configured the same as the first
+ // and assert if any of the memories does not match this configuration
+ assert(memory->shared == isShared);
+ assert(memory->indexType == pointerType);
+
+ // Calculating the total initial and max page size for the combined memory
+ // by totaling the initial and max page sizes for the memories in the
+ // module
+ totalInitialPages = totalInitialPages + memory->initial;
+ if (memory->hasMax()) {
+ totalMaxPages = totalMaxPages + memory->max;
+ }
+ }
+ // Ensuring valid initial and max page sizes that do not exceed the number
+ // of pages addressable by the pointerType
+ Address maxSize =
+ pointerType == Type::i32 ? Memory::kMaxSize32 : Memory::kMaxSize64;
+ if (totalMaxPages > maxSize || totalMaxPages == 0) {
+ totalMaxPages = Memory::kUnlimitedSize;
+ }
+ if (totalInitialPages > totalMaxPages) {
+ totalInitialPages = totalMaxPages;
+ }
+
+ // Creating the combined memory name so we can reference the combined memory
+ // in subsequent instructions before it is added to the module
+ combinedMemory = Names::getValidMemoryName(*wasm, "combined_memory");
+ }
+
+ void addOffsetGlobals() {
+ auto addGlobal = [&](Name name, size_t offset) {
+ auto global = Builder::makeGlobal(
+ name,
+ pointerType,
+ Builder(*wasm).makeConst(Literal::makeFromInt64(offset, pointerType)),
+ Builder::Mutable);
+ wasm->addGlobal(std::move(global));
+ };
+
+ size_t offsetRunningTotal = 0;
+ for (Index i = 0; i < wasm->memories.size(); i++) {
+ auto& memory = wasm->memories[i];
+ memoryIdxMap[memory->name] = i;
+ // We don't need a page offset global for the first memory as it's always
+ // 0
+ if (i != 0) {
+ Name name = Names::getValidGlobalName(
+ *wasm, memory->name.toString() + "_byte_offset");
+ offsetGlobalNames.push_back(std::move(name));
+ addGlobal(name, offsetRunningTotal * Memory::kPageSize);
+ }
+ offsetRunningTotal += memory->initial;
+ }
+ }
+
+ void adjustActiveDataSegmentOffsets() {
+ Builder builder(*wasm);
+ ModuleUtils::iterActiveDataSegments(*wasm, [&](DataSegment* dataSegment) {
+ assert(dataSegment->offset->is<Const>() &&
+ "TODO: handle non-const segment offsets");
+ auto idx = memoryIdxMap.at(dataSegment->memory);
+ dataSegment->memory = combinedMemory;
+ // No need to update the offset of data segments for the first memory
+ if (idx != 0) {
+ auto offsetGlobalName = getOffsetGlobal(idx);
+ assert(wasm->features.hasExtendedConst());
+ dataSegment->offset = builder.makeBinary(
+ Abstract::getBinary(pointerType, Abstract::Add),
+ builder.makeGlobalGet(offsetGlobalName, pointerType),
+ dataSegment->offset);
+ }
+ });
+ }
+
+ void createMemorySizeFunctions() {
+ for (Index i = 0; i < wasm->memories.size(); i++) {
+ auto function = memorySize(i, wasm->memories[i]->name);
+ memorySizeNames.push_back(function->name);
+ wasm->addFunction(std::move(function));
+ }
+ }
+
+ void createMemoryGrowFunctions() {
+ for (Index i = 0; i < wasm->memories.size(); i++) {
+ auto function = memoryGrow(i, wasm->memories[i]->name);
+ memoryGrowNames.push_back(function->name);
+ wasm->addFunction(std::move(function));
+ }
+ }
+
+ // This function replaces memory.grow instruction calls in the wasm module.
+ // Because the multiple discrete memories are lowered into a single memory,
+ // we need to adjust offsets as a particular memory receives an
+ // instruction to grow.
+ std::unique_ptr<Function> memoryGrow(Index memIdx, Name memoryName) {
+ Builder builder(*wasm);
+ Name name = memoryName.toString() + "_grow";
+ Name functionName = Names::getValidFunctionName(*wasm, name);
+ auto function = Builder::makeFunction(
+ functionName, Signature(pointerType, pointerType), {});
+ function->setLocalName(0, "page_delta");
+ auto pageSizeConst = [&]() {
+ return builder.makeConst(Literal(Memory::kPageSize));
+ };
+ auto getOffsetDelta = [&]() {
+ return builder.makeBinary(Abstract::getBinary(pointerType, Abstract::Mul),
+ builder.makeLocalGet(0, pointerType),
+ pageSizeConst());
+ };
+ auto getMoveSource = [&](Name global) {
+ return builder.makeGlobalGet(global, pointerType);
+ };
+ Expression* functionBody;
+ Index sizeLocal = -1;
+
+ Index returnLocal =
+ Builder::addVar(function.get(), "return_size", pointerType);
+ functionBody = builder.blockify(builder.makeLocalSet(
+ returnLocal, builder.makeCall(memorySizeNames[memIdx], {}, pointerType)));
+
+ if (!isLastMemory(memIdx)) {
+ sizeLocal = Builder::addVar(function.get(), "memory_size", pointerType);
+ functionBody = builder.blockify(
+ functionBody,
+ builder.makeLocalSet(
+ sizeLocal, builder.makeMemorySize(combinedMemory, memoryInfo)));
+ }
+
+ // TODO: Check the result of makeMemoryGrow for errors and return the error
+ // instead
+ functionBody = builder.blockify(
+ functionBody,
+ builder.makeDrop(builder.makeMemoryGrow(
+ builder.makeLocalGet(0, pointerType), combinedMemory, memoryInfo)));
+
+ // If we are not growing the last memory, then we need to copy data,
+ // shifting it over to accomodate the increase from page_delta
+ if (!isLastMemory(memIdx)) {
+ // This offset is the starting pt for copying
+ auto offsetGlobalName = getOffsetGlobal(memIdx + 1);
+ functionBody = builder.blockify(
+ functionBody,
+ builder.makeMemoryCopy(
+ // destination
+ builder.makeBinary(Abstract::getBinary(pointerType, Abstract::Add),
+ getMoveSource(offsetGlobalName),
+ getOffsetDelta()),
+ // source
+ getMoveSource(offsetGlobalName),
+ // size
+ builder.makeBinary(
+ Abstract::getBinary(pointerType, Abstract::Sub),
+ builder.makeBinary(Abstract::getBinary(pointerType, Abstract::Mul),
+ builder.makeLocalGet(sizeLocal, pointerType),
+ pageSizeConst()),
+ getMoveSource(offsetGlobalName)),
+ combinedMemory,
+ combinedMemory));
+ }
+
+ // Adjust the offsets of the globals impacted by the memory.grow call
+ for (Index i = memIdx; i < offsetGlobalNames.size(); i++) {
+ auto& offsetGlobalName = offsetGlobalNames[i];
+ functionBody = builder.blockify(
+ functionBody,
+ builder.makeGlobalSet(
+ offsetGlobalName,
+ builder.makeBinary(Abstract::getBinary(pointerType, Abstract::Add),
+ getMoveSource(offsetGlobalName),
+ getOffsetDelta())));
+ }
+
+ functionBody = builder.blockify(
+ functionBody, builder.makeLocalGet(returnLocal, pointerType));
+
+ function->body = functionBody;
+ return function;
+ }
+
+ // This function replaces memory.size instructions with a function that can
+ // return the size of each memory as if each was discrete and separate.
+ std::unique_ptr<Function> memorySize(Index memIdx, Name memoryName) {
+ Builder builder(*wasm);
+ Name name = memoryName.toString() + "_size";
+ Name functionName = Names::getValidFunctionName(*wasm, name);
+ auto function = Builder::makeFunction(
+ functionName, Signature(Type::none, pointerType), {});
+ Expression* functionBody;
+ auto pageSizeConst = [&]() {
+ return builder.makeConst(Literal(Memory::kPageSize));
+ };
+ auto getOffsetInPageUnits = [&](Name global) {
+ return builder.makeBinary(
+ Abstract::getBinary(pointerType, Abstract::DivU),
+ builder.makeGlobalGet(global, pointerType),
+ pageSizeConst());
+ };
+
+ // offsetGlobalNames does not keep track of a global for the offset of
+ // wasm->memories[0] because it's always 0. As a result, the below
+ // calculations that involve offsetGlobalNames are intrinsically "offset".
+ // Thus, offsetGlobalNames[0] is the offset for wasm->memories[1] and
+ // the size of wasm->memories[0].
+ if (memIdx == 0) {
+ auto offsetGlobalName = getOffsetGlobal(1);
+ functionBody = builder.blockify(
+ builder.makeReturn(getOffsetInPageUnits(offsetGlobalName)));
+ } else if (isLastMemory(memIdx)) {
+ auto offsetGlobalName = getOffsetGlobal(memIdx);
+ functionBody = builder.blockify(builder.makeReturn(
+ builder.makeBinary(Abstract::getBinary(pointerType, Abstract::Sub),
+ builder.makeMemorySize(combinedMemory, memoryInfo),
+ getOffsetInPageUnits(offsetGlobalName))));
+ } else {
+ auto offsetGlobalName = getOffsetGlobal(memIdx);
+ auto nextOffsetGlobalName = getOffsetGlobal(memIdx + 1);
+ functionBody = builder.blockify(builder.makeReturn(
+ builder.makeBinary(Abstract::getBinary(pointerType, Abstract::Sub),
+ getOffsetInPageUnits(nextOffsetGlobalName),
+ getOffsetInPageUnits(offsetGlobalName))));
+ }
+
+ function->body = functionBody;
+ return function;
+ }
+
+ void removeExistingMemories() {
+ wasm->removeMemories([&](Memory* curr) { return true; });
+ }
+
+ void addCombinedMemory() {
+ auto memory = Builder::makeMemory(combinedMemory);
+ memory->shared = isShared;
+ memory->indexType = pointerType;
+ memory->initial = totalInitialPages;
+ memory->max = totalMaxPages;
+ wasm->addMemory(std::move(memory));
+ }
+};
+
+Pass* createMultiMemoryLoweringPass() { return new MultiMemoryLowering(); }
+
+} // namespace wasm
diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp
index ae9f34584..c6d94ddf7 100644
--- a/src/passes/pass.cpp
+++ b/src/passes/pass.cpp
@@ -272,6 +272,9 @@ void PassRegistry::registerPasses() {
registerPass("mod-asyncify-never-unwind",
"apply the assumption that asyncify never unwinds",
createModAsyncifyNeverUnwindPass);
+ registerPass("multi-memory-lowering",
+ "combines multiple memories into a single memory",
+ createMultiMemoryLoweringPass);
registerPass("nm", "name list", createNameListPass);
registerPass("name-types", "(re)name all heap types", createNameTypesPass);
registerPass("once-reduction",
diff --git a/src/passes/passes.h b/src/passes/passes.h
index 99ee87553..20dfe0249 100644
--- a/src/passes/passes.h
+++ b/src/passes/passes.h
@@ -84,6 +84,7 @@ Pass* createMinifyImportsPass();
Pass* createMinifyImportsAndExportsPass();
Pass* createMinifyImportsAndExportsAndModulesPass();
Pass* createMetricsPass();
+Pass* createMultiMemoryLoweringPass();
Pass* createNameListPass();
Pass* createNameTypesPass();
Pass* createOnceReductionPass();
diff --git a/src/wasm.h b/src/wasm.h
index 46b5040c2..2b890177e 100644
--- a/src/wasm.h
+++ b/src/wasm.h
@@ -2069,6 +2069,8 @@ public:
// In wasm32, the maximum memory size is limited by a 32-bit pointer: 4GB
static const Address::address32_t kMaxSize32 =
(uint64_t(4) * 1024 * 1024 * 1024) / kPageSize;
+ // in wasm64, the maximum number of pages
+ static const Address::address64_t kMaxSize64 = 1ull << (64 - 16);
Address initial = 0; // sizes are in pages
Address max = kMaxSize32;
diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test
index b8679a62d..7ee35ab1a 100644
--- a/test/lit/help/wasm-opt.test
+++ b/test/lit/help/wasm-opt.test
@@ -273,6 +273,9 @@
;; CHECK-NEXT: --mod-asyncify-never-unwind apply the assumption that
;; CHECK-NEXT: asyncify never unwinds
;; CHECK-NEXT:
+;; CHECK-NEXT: --multi-memory-lowering combines multiple memories into
+;; CHECK-NEXT: a single memory
+;; CHECK-NEXT:
;; CHECK-NEXT: --name-types (re)name all heap types
;; CHECK-NEXT:
;; CHECK-NEXT: --nm name list
diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test
index 4df281803..0d61348b8 100644
--- a/test/lit/help/wasm2js.test
+++ b/test/lit/help/wasm2js.test
@@ -232,6 +232,9 @@
;; CHECK-NEXT: --mod-asyncify-never-unwind apply the assumption that
;; CHECK-NEXT: asyncify never unwinds
;; CHECK-NEXT:
+;; CHECK-NEXT: --multi-memory-lowering combines multiple memories into
+;; CHECK-NEXT: a single memory
+;; CHECK-NEXT:
;; CHECK-NEXT: --name-types (re)name all heap types
;; CHECK-NEXT:
;; CHECK-NEXT: --nm name list
diff --git a/test/lit/passes/multi-memory-lowering.wast b/test/lit/passes/multi-memory-lowering.wast
new file mode 100644
index 000000000..6bf1ecc7c
--- /dev/null
+++ b/test/lit/passes/multi-memory-lowering.wast
@@ -0,0 +1,247 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+;; RUN: wasm-opt %s --enable-multi-memories --multi-memory-lowering --enable-bulk-memory --enable-extended-const -S -o - | filecheck %s
+
+(module
+ (memory $memory1 1)
+ (memory $memory2 2)
+ (memory $memory3 3)
+ (data (memory $memory1) (i32.const 0) "a")
+ (data (memory $memory3) (i32.const 1) "123")
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (type $i32_=>_i32 (func (param i32) (result i32)))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $memory2_byte_offset (mut i32) (i32.const 65536))
+
+ ;; CHECK: (global $memory3_byte_offset (mut i32) (i32.const 196608))
+
+ ;; CHECK: (memory $combined_memory 6)
+
+ ;; CHECK: (data (i32.const 0) "a")
+
+ ;; CHECK: (data (i32.add
+ ;; CHECK-NEXT: (global.get $memory3_byte_offset)
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: ) "123")
+
+ ;; CHECK: (func $loads
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.load
+ ;; CHECK-NEXT: (i32.const 10)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.load
+ ;; CHECK-NEXT: (i32.add
+ ;; CHECK-NEXT: (global.get $memory2_byte_offset)
+ ;; CHECK-NEXT: (i32.const 11)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.load
+ ;; CHECK-NEXT: (i32.add
+ ;; CHECK-NEXT: (global.get $memory3_byte_offset)
+ ;; CHECK-NEXT: (i32.const 12)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $loads
+ (drop
+ (i32.load $memory1
+ (i32.const 10)
+ )
+ )
+ (drop
+ (i32.load $memory2
+ (i32.const 11)
+ )
+ )
+ (drop
+ (i32.load $memory3
+ (i32.const 12)
+ )
+ )
+ )
+ ;; CHECK: (func $stores
+ ;; CHECK-NEXT: (i32.store
+ ;; CHECK-NEXT: (i32.const 10)
+ ;; CHECK-NEXT: (i32.const 115)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (i32.store
+ ;; CHECK-NEXT: (i32.add
+ ;; CHECK-NEXT: (global.get $memory2_byte_offset)
+ ;; CHECK-NEXT: (i32.const 11)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (i32.const 115)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (i32.store
+ ;; CHECK-NEXT: (i32.add
+ ;; CHECK-NEXT: (global.get $memory3_byte_offset)
+ ;; CHECK-NEXT: (i32.const 12)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (i32.const 115)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $stores
+ (i32.store $memory1
+ (i32.const 10)
+ (i32.const 115)
+ )
+ (i32.store $memory2
+ (i32.const 11)
+ (i32.const 115)
+ )
+ (i32.store $memory3
+ (i32.const 12)
+ (i32.const 115)
+ )
+ )
+)
+
+;; CHECK: (func $memory1_size (result i32)
+;; CHECK-NEXT: (return
+;; CHECK-NEXT: (i32.div_u
+;; CHECK-NEXT: (global.get $memory2_byte_offset)
+;; CHECK-NEXT: (i32.const 65536)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $memory2_size (result i32)
+;; CHECK-NEXT: (return
+;; CHECK-NEXT: (i32.sub
+;; CHECK-NEXT: (i32.div_u
+;; CHECK-NEXT: (global.get $memory3_byte_offset)
+;; CHECK-NEXT: (i32.const 65536)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (i32.div_u
+;; CHECK-NEXT: (global.get $memory2_byte_offset)
+;; CHECK-NEXT: (i32.const 65536)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $memory3_size (result i32)
+;; CHECK-NEXT: (return
+;; CHECK-NEXT: (i32.sub
+;; CHECK-NEXT: (memory.size)
+;; CHECK-NEXT: (i32.div_u
+;; CHECK-NEXT: (global.get $memory3_byte_offset)
+;; CHECK-NEXT: (i32.const 65536)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $memory1_grow (param $page_delta i32) (result i32)
+;; CHECK-NEXT: (local $return_size i32)
+;; CHECK-NEXT: (local $memory_size i32)
+;; CHECK-NEXT: (local.set $return_size
+;; CHECK-NEXT: (call $memory1_size)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (local.set $memory_size
+;; CHECK-NEXT: (memory.size)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (memory.grow
+;; CHECK-NEXT: (local.get $page_delta)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (memory.copy
+;; CHECK-NEXT: (i32.add
+;; CHECK-NEXT: (global.get $memory2_byte_offset)
+;; CHECK-NEXT: (i32.mul
+;; CHECK-NEXT: (local.get $page_delta)
+;; CHECK-NEXT: (i32.const 65536)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (global.get $memory2_byte_offset)
+;; CHECK-NEXT: (i32.sub
+;; CHECK-NEXT: (i32.mul
+;; CHECK-NEXT: (local.get $memory_size)
+;; CHECK-NEXT: (i32.const 65536)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (global.get $memory2_byte_offset)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (global.set $memory2_byte_offset
+;; CHECK-NEXT: (i32.add
+;; CHECK-NEXT: (global.get $memory2_byte_offset)
+;; CHECK-NEXT: (i32.mul
+;; CHECK-NEXT: (local.get $page_delta)
+;; CHECK-NEXT: (i32.const 65536)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (global.set $memory3_byte_offset
+;; CHECK-NEXT: (i32.add
+;; CHECK-NEXT: (global.get $memory3_byte_offset)
+;; CHECK-NEXT: (i32.mul
+;; CHECK-NEXT: (local.get $page_delta)
+;; CHECK-NEXT: (i32.const 65536)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (local.get $return_size)
+;; CHECK-NEXT: )
+
+;; CHECK: (func $memory2_grow (param $page_delta i32) (result i32)
+;; CHECK-NEXT: (local $return_size i32)
+;; CHECK-NEXT: (local $memory_size i32)
+;; CHECK-NEXT: (local.set $return_size
+;; CHECK-NEXT: (call $memory2_size)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (local.set $memory_size
+;; CHECK-NEXT: (memory.size)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (memory.grow
+;; CHECK-NEXT: (local.get $page_delta)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (memory.copy
+;; CHECK-NEXT: (i32.add
+;; CHECK-NEXT: (global.get $memory3_byte_offset)
+;; CHECK-NEXT: (i32.mul
+;; CHECK-NEXT: (local.get $page_delta)
+;; CHECK-NEXT: (i32.const 65536)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (global.get $memory3_byte_offset)
+;; CHECK-NEXT: (i32.sub
+;; CHECK-NEXT: (i32.mul
+;; CHECK-NEXT: (local.get $memory_size)
+;; CHECK-NEXT: (i32.const 65536)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (global.get $memory3_byte_offset)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (global.set $memory3_byte_offset
+;; CHECK-NEXT: (i32.add
+;; CHECK-NEXT: (global.get $memory3_byte_offset)
+;; CHECK-NEXT: (i32.mul
+;; CHECK-NEXT: (local.get $page_delta)
+;; CHECK-NEXT: (i32.const 65536)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (local.get $return_size)
+;; CHECK-NEXT: )
+
+;; CHECK: (func $memory3_grow (param $page_delta i32) (result i32)
+;; CHECK-NEXT: (local $return_size i32)
+;; CHECK-NEXT: (local.set $return_size
+;; CHECK-NEXT: (call $memory3_size)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (drop
+;; CHECK-NEXT: (memory.grow
+;; CHECK-NEXT: (local.get $page_delta)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (local.get $return_size)
+;; CHECK-NEXT: )