From 998f4521c9bb31c5b1eefaf087657cc9ea1d1d98 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 18 Nov 2025 01:10:12 +0000 Subject: [PATCH] [WIP][multibyte] Add multibyte array store instructions. Prototype implementation of the new multibyte array proposal that reuses the existing memory instructions. The syntax for the new instructions is as follows: .store type=array Some spec related TODOs: - Decide on new instructions vs existing memory instructions with memarg flag. - If using existing instructions: - What does the wat syntax look like? e.g. `i32.store type=array x y` or `i32.store array x y` or ??? - Do we support immediate offsets? - Do we support i32.store8? (effectively the same as array.set on i8) - Do we support atomic instructions? Some implementation TODOs: - Add v128.store. - Go through the various `visitArrayStore` functions and implement them or make sure the copied versions makes sense. - Add fuzzing support. - Make it so there's only one `WasmBinaryReader::getMemarg` - Add JS API --- src/interpreter/interpreter.cpp | 1 + src/ir/ReFinalize.cpp | 1 + src/ir/child-typer.h | 16 ++ src/ir/cost.h | 4 + src/ir/effects.h | 9 + src/ir/possible-contents.cpp | 8 + src/ir/subtype-exprs.h | 7 + src/parser/contexts.h | 9 +- src/parser/parsers.h | 15 +- src/passes/Print.cpp | 42 ++-- src/passes/TypeGeneralizing.cpp | 2 + src/wasm-binary.h | 4 +- src/wasm-builder.h | 15 ++ src/wasm-delegations-fields.def | 9 + src/wasm-delegations.def | 1 + src/wasm-interpreter.h | 55 +++++ src/wasm-ir-builder.h | 7 + src/wasm-stack.h | 1 + src/wasm.h | 22 ++ src/wasm/wasm-binary.cpp | 84 ++++--- src/wasm/wasm-ir-builder.cpp | 28 +++ src/wasm/wasm-stack.cpp | 135 +++++----- src/wasm/wasm-validator.cpp | 24 ++ src/wasm/wasm.cpp | 9 + src/wasm2js.h | 4 + test/lit/array-multibyte.wast | 126 ++++++++++ test/spec/array-multibyte.wast | 423 ++++++++++++++++++++++++++++++++ 27 files changed, 952 insertions(+), 109 deletions(-) create mode 100644 test/lit/array-multibyte.wast create mode 100644 test/spec/array-multibyte.wast diff --git a/src/interpreter/interpreter.cpp b/src/interpreter/interpreter.cpp index 8007f011dcc..c24cc3b4d8b 100644 --- a/src/interpreter/interpreter.cpp +++ b/src/interpreter/interpreter.cpp @@ -260,6 +260,7 @@ struct ExpressionInterpreter : OverriddenVisitor { Flow visitArrayNewFixed(ArrayNewFixed* curr) { WASM_UNREACHABLE("TODO"); } Flow visitArrayGet(ArrayGet* curr) { WASM_UNREACHABLE("TODO"); } Flow visitArraySet(ArraySet* curr) { WASM_UNREACHABLE("TODO"); } + Flow visitArrayStore(ArrayStore* curr) { WASM_UNREACHABLE("TODO"); } Flow visitArrayLen(ArrayLen* curr) { WASM_UNREACHABLE("TODO"); } Flow visitArrayCopy(ArrayCopy* curr) { WASM_UNREACHABLE("TODO"); } Flow visitArrayFill(ArrayFill* curr) { WASM_UNREACHABLE("TODO"); } diff --git a/src/ir/ReFinalize.cpp b/src/ir/ReFinalize.cpp index f7544e3cf51..3ce915a5833 100644 --- a/src/ir/ReFinalize.cpp +++ b/src/ir/ReFinalize.cpp @@ -165,6 +165,7 @@ void ReFinalize::visitArrayNewElem(ArrayNewElem* curr) { curr->finalize(); } void ReFinalize::visitArrayNewFixed(ArrayNewFixed* curr) { curr->finalize(); } void ReFinalize::visitArrayGet(ArrayGet* curr) { curr->finalize(); } void ReFinalize::visitArraySet(ArraySet* curr) { curr->finalize(); } +void ReFinalize::visitArrayStore(ArrayStore* curr) { curr->finalize(); } void ReFinalize::visitArrayLen(ArrayLen* curr) { curr->finalize(); } void ReFinalize::visitArrayCopy(ArrayCopy* curr) { curr->finalize(); } void ReFinalize::visitArrayFill(ArrayFill* curr) { curr->finalize(); } diff --git a/src/ir/child-typer.h b/src/ir/child-typer.h index 649f7192783..958e13aada2 100644 --- a/src/ir/child-typer.h +++ b/src/ir/child-typer.h @@ -1067,6 +1067,22 @@ template struct ChildTyper : OverriddenVisitor { note(&curr->value, type); } + void visitArrayStore(ArrayStore* curr, + std::optional ht = std::nullopt) { + // TODO Figure out if this is correct. This is copied from visitArraySet. + if (!ht) { + if (!curr->ref->type.isRef()) { + self().noteUnknown(); + return; + } + ht = curr->ref->type.getHeapType(); + } + auto type = ht->getArray().element.type; + note(&curr->ref, Type(*ht, Nullable)); + note(&curr->index, Type::i32); + note(&curr->value, type); + } + void visitArrayLen(ArrayLen* curr) { note(&curr->ref, Type(HeapType::array, Nullable)); } diff --git a/src/ir/cost.h b/src/ir/cost.h index 175cf722e2e..87949860f68 100644 --- a/src/ir/cost.h +++ b/src/ir/cost.h @@ -745,6 +745,10 @@ struct CostAnalyzer : public OverriddenVisitor { return 2 + nullCheckCost(curr->ref) + visit(curr->ref) + visit(curr->index) + visit(curr->value); } + CostType visitArrayStore(ArrayStore* curr) { + return 2 + nullCheckCost(curr->ref) + visit(curr->ref) + + visit(curr->index) + visit(curr->value); + } CostType visitArrayLen(ArrayLen* curr) { return 1 + nullCheckCost(curr->ref) + visit(curr->ref); } diff --git a/src/ir/effects.h b/src/ir/effects.h index cbab700b94e..2947aaed037 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -979,6 +979,15 @@ class EffectAnalyzer { // traps when the arg is null or the index out of bounds parent.implicitTrap = true; } + void visitArrayStore(ArrayStore* curr) { + if (curr->ref->type.isNull()) { + parent.trap = true; + return; + } + parent.writesArray = true; + // traps when the arg is null or the index out of bounds + parent.implicitTrap = true; + } void visitArrayLen(ArrayLen* curr) { if (curr->ref->type.isNull()) { parent.trap = true; diff --git a/src/ir/possible-contents.cpp b/src/ir/possible-contents.cpp index 6562e4578eb..e41d5089279 100644 --- a/src/ir/possible-contents.cpp +++ b/src/ir/possible-contents.cpp @@ -1063,6 +1063,13 @@ struct InfoCollector addChildParentLink(curr->ref, curr); addChildParentLink(curr->value, curr); } + void visitArrayStore(ArrayStore* curr) { + if (curr->ref->type == Type::unreachable) { + return; + } + addChildParentLink(curr->ref, curr); + addChildParentLink(curr->value, curr); + } void visitArrayLen(ArrayLen* curr) { // TODO: optimize when possible (perhaps we can infer a Literal for the @@ -1696,6 +1703,7 @@ void TNHOracle::scan(Function* func, } void visitArrayGet(ArrayGet* curr) { notePossibleTrap(curr->ref); } void visitArraySet(ArraySet* curr) { notePossibleTrap(curr->ref); } + void visitArrayStore(ArrayStore* curr) { notePossibleTrap(curr->ref); } void visitArrayLen(ArrayLen* curr) { notePossibleTrap(curr->ref); } void visitArrayCopy(ArrayCopy* curr) { notePossibleTrap(curr->srcRef); diff --git a/src/ir/subtype-exprs.h b/src/ir/subtype-exprs.h index a333f88b79a..503b0b1d12f 100644 --- a/src/ir/subtype-exprs.h +++ b/src/ir/subtype-exprs.h @@ -403,6 +403,13 @@ struct SubtypingDiscoverer : public OverriddenVisitor { auto array = curr->ref->type.getHeapType().getArray(); self()->noteSubtype(curr->value, array.element.type); } + void visitArrayStore(ArrayStore* curr) { + if (!curr->ref->type.isArray()) { + return; + } + auto array = curr->ref->type.getHeapType().getArray(); + self()->noteSubtype(curr->value, array.element.type); + } void visitArrayLen(ArrayLen* curr) {} void visitArrayCopy(ArrayCopy* curr) { if (!curr->srcRef->type.isArray() || !curr->destRef->type.isArray()) { diff --git a/src/parser/contexts.h b/src/parser/contexts.h index d43967bffaa..f1e5ef7b0a2 100644 --- a/src/parser/contexts.h +++ b/src/parser/contexts.h @@ -564,7 +564,8 @@ struct NullInstrParserCtx { int, bool, MemoryIdxT*, - MemargT) { + MemargT, + BackingType) { return Ok{}; } Result<> makeAtomicRMW(Index, @@ -2245,7 +2246,11 @@ struct ParseDefsCtx : TypeParserCtx, AnnotationParserCtx { int bytes, bool isAtomic, Name* mem, - Memarg memarg) { + Memarg memarg, + BackingType backingType) { + if (backingType == BackingType::Array) { + return withLoc(pos, irBuilder.makeArrayStore(bytes, type)); + } auto m = getMemory(pos, mem); CHECK_ERR(m); if (isAtomic) { diff --git a/src/parser/parsers.h b/src/parser/parsers.h index 3733761bc2c..c66a071e194 100644 --- a/src/parser/parsers.h +++ b/src/parser/parsers.h @@ -1737,6 +1737,8 @@ Result<> makeLoad(Ctx& ctx, bool signed_, int bytes, bool isAtomic) { + auto backing = backingType(ctx); + CHECK_ERR(backing); auto mem = maybeMemidx(ctx); CHECK_ERR(mem); auto arg = memarg(ctx, bytes); @@ -1752,12 +1754,14 @@ Result<> makeStore(Ctx& ctx, Type type, int bytes, bool isAtomic) { + auto backing = backingType(ctx); + CHECK_ERR(backing); auto mem = maybeMemidx(ctx); CHECK_ERR(mem); auto arg = memarg(ctx, bytes); CHECK_ERR(arg); return ctx.makeStore( - pos, annotations, type, bytes, isAtomic, mem.getPtr(), *arg); + pos, annotations, type, bytes, isAtomic, mem.getPtr(), *arg, *backing); } template @@ -2853,6 +2857,15 @@ MaybeResult maybeTableuse(Ctx& ctx) { return *idx; } +template Result backingType(Ctx& ctx) { + // TODO this should probably parse out the "type" and value separately, but + // for now this works for a prototype. + if (ctx.in.takeKeyword("type=array"sv)) { + return BackingType::Array; + } + return BackingType::Memory; +} + // memidx ::= x:u32 => x // | v:id => x (if memories[x] = v) template diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 02c094c358d..edf98102090 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -436,6 +436,25 @@ struct PrintExpressionContents return parent.printBlockType(sig); } + std::ostream& printStorePostfix(uint8_t bytes, Type valueType) { + if (bytes < 4 || (valueType == Type::i64 && bytes < 8)) { + if (bytes == 1) { + o << '8'; + } else if (bytes == 2) { + if (valueType == Type::f32) { + o << "_f16"; + } else { + o << "16"; + } + } else if (bytes == 4) { + o << "32"; + } else { + abort(); + } + } + return o; + } + void visitBlock(Block* curr) { printMedium(o, "block"); if (curr->name.is()) { @@ -587,21 +606,7 @@ struct PrintExpressionContents o << ".atomic"; } o << ".store"; - if (curr->bytes < 4 || (curr->valueType == Type::i64 && curr->bytes < 8)) { - if (curr->bytes == 1) { - o << '8'; - } else if (curr->bytes == 2) { - if (curr->valueType == Type::f32) { - o << "_f16"; - } else { - o << "16"; - } - } else if (curr->bytes == 4) { - o << "32"; - } else { - abort(); - } - } + printStorePostfix(curr->bytes, curr->valueType); restoreNormalColor(o); printMemoryName(curr->memory, o, wasm); if (curr->offset) { @@ -2446,6 +2451,13 @@ struct PrintExpressionContents printMemoryOrder(curr->order); printHeapTypeName(curr->ref->type.getHeapType()); } + void visitArrayStore(ArrayStore* curr) { + prepareColor(o) << forceConcrete(curr->valueType); + o << ".store"; + printStorePostfix(curr->bytes, curr->valueType); + restoreNormalColor(o); + o << " type=array"; + } void visitArrayLen(ArrayLen* curr) { printMedium(o, "array.len"); } void visitArrayCopy(ArrayCopy* curr) { printMedium(o, "array.copy "); diff --git a/src/passes/TypeGeneralizing.cpp b/src/passes/TypeGeneralizing.cpp index 7ed65c602c7..da58ef4f760 100644 --- a/src/passes/TypeGeneralizing.cpp +++ b/src/passes/TypeGeneralizing.cpp @@ -794,6 +794,8 @@ struct TransferFn : OverriddenVisitor { } } + void visitArrayStore(ArrayStore* curr) { WASM_UNREACHABLE("TODO"); } + void visitArrayLen(ArrayLen* curr) { // The input must be an array. push(Type(HeapType::array, Nullable)); diff --git a/src/wasm-binary.h b/src/wasm-binary.h index 3117ca4c97f..5b24264999e 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -1725,7 +1725,9 @@ class WasmBinaryReader { size_t inlineHintsLen = 0; void readInlineHints(size_t payloadLen); - Index readMemoryAccess(Address& alignment, Address& offset); + Index + readMemoryAccess(Address& alignment, Address& offset, BackingType& backing); + std::tuple getMemargWithBacking(); std::tuple getMemarg(); MemoryOrder getMemoryOrder(bool isRMW = false); diff --git a/src/wasm-builder.h b/src/wasm-builder.h index d14e716f12b..cda04ae17a4 100644 --- a/src/wasm-builder.h +++ b/src/wasm-builder.h @@ -1114,6 +1114,21 @@ class Builder { ret->finalize(); return ret; } + ArrayStore* makeArrayStore(unsigned bytes, + Type valueType, + Expression* ref, + Expression* index, + Expression* value) { + auto* ret = wasm.allocator.alloc(); + ret->bytes = bytes; + ret->valueType = valueType; + ret->ref = ref; + ret->index = index; + ret->value = value; + // ret->order = order needed??? + ret->finalize(); + return ret; + } ArrayLen* makeArrayLen(Expression* ref) { auto* ret = wasm.allocator.alloc(); ret->ref = ref; diff --git a/src/wasm-delegations-fields.def b/src/wasm-delegations-fields.def index 376648311f3..f062782a240 100644 --- a/src/wasm-delegations-fields.def +++ b/src/wasm-delegations-fields.def @@ -743,6 +743,15 @@ DELEGATE_FIELD_IMMEDIATE_TYPED_CHILD(ArraySet, ref) DELEGATE_FIELD_INT(ArraySet, order) DELEGATE_FIELD_CASE_END(ArraySet) +DELEGATE_FIELD_CASE_START(ArrayStore) +DELEGATE_FIELD_CHILD(ArrayStore, value) +DELEGATE_FIELD_CHILD(ArrayStore, index) +DELEGATE_FIELD_IMMEDIATE_TYPED_CHILD(ArrayStore, ref) +DELEGATE_FIELD_INT(ArrayStore, bytes) +DELEGATE_FIELD_TYPE(ArrayStore, valueType) +DELEGATE_FIELD_INT(ArrayStore, order) +DELEGATE_FIELD_CASE_END(ArrayStore) + DELEGATE_FIELD_CASE_START(ArrayLen) DELEGATE_FIELD_CHILD(ArrayLen, ref) DELEGATE_FIELD_CASE_END(ArrayLen) diff --git a/src/wasm-delegations.def b/src/wasm-delegations.def index dcec0e6e938..cfafa4fe3b4 100644 --- a/src/wasm-delegations.def +++ b/src/wasm-delegations.def @@ -92,6 +92,7 @@ DELEGATE(ArrayNewElem); DELEGATE(ArrayNewFixed); DELEGATE(ArrayGet); DELEGATE(ArraySet); +DELEGATE(ArrayStore); DELEGATE(ArrayLen); DELEGATE(ArrayCopy); DELEGATE(ArrayFill); diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 2b98c12af31..588d06ba6a0 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -345,6 +345,15 @@ class ExpressionRunner : public OverriddenVisitor { return Literal(allocation); } + // TODO find a better spot for this or see if a helper already exists. + template + void writeBytes(T value, int numBytes, size_t index, Literals& values) { + for (int i = 0; i < numBytes; ++i) { + values[index + i] = + Literal(static_cast((value >> (i * 8)) & 0xff)); + } + } + public: // Indicates no limit of maxDepth or maxLoopIterations. static const Index NO_LIMIT = 0; @@ -2291,6 +2300,7 @@ class ExpressionRunner : public OverriddenVisitor { if (!data) { trap("null ref"); } + Index i = index.getSingleValue().geti32(); if (i >= data->values.size()) { trap("array oob"); @@ -2299,6 +2309,51 @@ class ExpressionRunner : public OverriddenVisitor { data->values[i] = truncateForPacking(value.getSingleValue(), field); return Flow(); } + Flow visitArrayStore(ArrayStore* curr) { + VISIT(ref, curr->ref) + VISIT(index, curr->index) + VISIT(value, curr->value) + auto data = ref.getSingleValue().getGCData(); + if (!data) { + trap("null ref"); + } + + Index i = index.getSingleValue().geti32(); + size_t size = data->values.size(); + // Use subtraction to avoid overflow. + if (i >= size || curr->bytes > (size - i)) { + trap("array oob"); + } + switch (curr->valueType.getBasic()) { + case Type::i32: + writeBytes( + value.getSingleValue().geti32(), curr->bytes, i, data->values); + break; + case Type::i64: + writeBytes( + value.getSingleValue().geti64(), curr->bytes, i, data->values); + break; + case Type::f32: + writeBytes(value.getSingleValue().reinterpreti32(), + curr->bytes, + i, + data->values); + break; + case Type::f64: + writeBytes(value.getSingleValue().reinterpreti64(), + curr->bytes, + i, + data->values); + break; + case Type::v128: + // TODO + WASM_UNREACHABLE("todo"); + case Type::none: + case Type::unreachable: + WASM_UNREACHABLE("unimp basic type"); + } + return Flow(); + } Flow visitArrayLen(ArrayLen* curr) { VISIT(ref, curr->ref) auto data = ref.getSingleValue().getGCData(); diff --git a/src/wasm-ir-builder.h b/src/wasm-ir-builder.h index b79061d8ab9..9392ee1a8f4 100644 --- a/src/wasm-ir-builder.h +++ b/src/wasm-ir-builder.h @@ -156,6 +156,12 @@ class IRBuilder : public UnifiedExpressionVisitor> { Name mem); Result<> makeStore( unsigned bytes, Address offset, unsigned align, Type type, Name mem); + Result<> makeStore(BackingType backing, + unsigned bytes, + Address offset, + unsigned align, + Type type, + Name mem); Result<> makeAtomicLoad(unsigned bytes, Address offset, Type type, Name mem); Result<> makeAtomicStore(unsigned bytes, Address offset, Type type, Name mem); Result<> makeAtomicRMW( @@ -244,6 +250,7 @@ class IRBuilder : public UnifiedExpressionVisitor> { Result<> makeArrayNewFixed(HeapType type, uint32_t arity); Result<> makeArrayGet(HeapType type, bool signed_, MemoryOrder order); Result<> makeArraySet(HeapType type, MemoryOrder order); + Result<> makeArrayStore(unsigned bytes, Type type); Result<> makeArrayLen(); Result<> makeArrayCopy(HeapType destType, HeapType srcType); Result<> makeArrayFill(HeapType type); diff --git a/src/wasm-stack.h b/src/wasm-stack.h index 41118d1391e..95f0ee62a83 100644 --- a/src/wasm-stack.h +++ b/src/wasm-stack.h @@ -130,6 +130,7 @@ class BinaryInstWriter : public OverriddenVisitor { size_t bytes, uint64_t offset, Name memory); + void emitStore(uint8_t bytes, Type ValueType); int32_t getBreakIndex(Name name); WasmBinaryWriter& parent; diff --git a/src/wasm.h b/src/wasm.h index 605c7025395..26926d574a4 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -71,6 +71,11 @@ enum class MemoryOrder { AcqRel, }; +enum class BackingType { + Memory, + Array, +}; + enum class IRProfile { Normal, Poppy }; // Operators @@ -735,6 +740,7 @@ class Expression { ArrayNewFixedId, ArrayGetId, ArraySetId, + ArrayStoreId, ArrayLenId, ArrayCopyId, ArrayFillId, @@ -1827,6 +1833,22 @@ class ArraySet : public SpecificExpression { void finalize(); }; +class ArrayStore : public SpecificExpression { +public: + ArrayStore() = default; + ArrayStore(MixedArena& allocator) {} + + uint8_t bytes; + Expression* ref; + Expression* index; + Expression* value; + Type valueType; + // TODO is this needed? + MemoryOrder order = MemoryOrder::Unordered; + + void finalize(); +}; + class ArrayLen : public SpecificExpression { public: ArrayLen() = default; diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 21d9732d398..8c5d2ed902d 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -3560,40 +3560,40 @@ Result<> WasmBinaryReader::readInst() { return builder.makeLoad(8, false, offset, align, Type::f64, mem); } case BinaryConsts::I32StoreMem8: { - auto [mem, align, offset] = getMemarg(); - return builder.makeStore(1, offset, align, Type::i32, mem); + auto [mem, align, offset, backing] = getMemargWithBacking(); + return builder.makeStore(backing, 1, offset, align, Type::i32, mem); } case BinaryConsts::I32StoreMem16: { - auto [mem, align, offset] = getMemarg(); - return builder.makeStore(2, offset, align, Type::i32, mem); + auto [mem, align, offset, backing] = getMemargWithBacking(); + return builder.makeStore(backing, 2, offset, align, Type::i32, mem); } case BinaryConsts::I32StoreMem: { - auto [mem, align, offset] = getMemarg(); - return builder.makeStore(4, offset, align, Type::i32, mem); + auto [mem, align, offset, backing] = getMemargWithBacking(); + return builder.makeStore(backing, 4, offset, align, Type::i32, mem); } case BinaryConsts::I64StoreMem8: { - auto [mem, align, offset] = getMemarg(); - return builder.makeStore(1, offset, align, Type::i64, mem); + auto [mem, align, offset, backing] = getMemargWithBacking(); + return builder.makeStore(backing, 1, offset, align, Type::i64, mem); } case BinaryConsts::I64StoreMem16: { - auto [mem, align, offset] = getMemarg(); - return builder.makeStore(2, offset, align, Type::i64, mem); + auto [mem, align, offset, backing] = getMemargWithBacking(); + return builder.makeStore(backing, 2, offset, align, Type::i64, mem); } case BinaryConsts::I64StoreMem32: { - auto [mem, align, offset] = getMemarg(); - return builder.makeStore(4, offset, align, Type::i64, mem); + auto [mem, align, offset, backing] = getMemargWithBacking(); + return builder.makeStore(backing, 4, offset, align, Type::i64, mem); } case BinaryConsts::I64StoreMem: { - auto [mem, align, offset] = getMemarg(); - return builder.makeStore(8, offset, align, Type::i64, mem); + auto [mem, align, offset, backing] = getMemargWithBacking(); + return builder.makeStore(backing, 8, offset, align, Type::i64, mem); } case BinaryConsts::F32StoreMem: { - auto [mem, align, offset] = getMemarg(); - return builder.makeStore(4, offset, align, Type::f32, mem); + auto [mem, align, offset, backing] = getMemargWithBacking(); + return builder.makeStore(backing, 4, offset, align, Type::f32, mem); } case BinaryConsts::F64StoreMem: { - auto [mem, align, offset] = getMemarg(); - return builder.makeStore(8, offset, align, Type::f64, mem); + auto [mem, align, offset, backing] = getMemargWithBacking(); + return builder.makeStore(backing, 8, offset, align, Type::f64, mem); } case BinaryConsts::AtomicPrefix: { auto op = getU32LEB(); @@ -5428,9 +5428,12 @@ void WasmBinaryReader::readInlineHints(size_t payloadLen) { }); } -Index WasmBinaryReader::readMemoryAccess(Address& alignment, Address& offset) { +Index WasmBinaryReader::readMemoryAccess(Address& alignment, + Address& offset, + BackingType& backing) { auto rawAlignment = getU32LEB(); bool hasMemIdx = false; + backing = BackingType::Memory; Index memIdx = 0; // Check bit 6 in the alignment to know whether a memory index is present per: // https://github.com/WebAssembly/multi-memory/blob/main/proposals/multi-memory/Overview.md @@ -5440,28 +5443,51 @@ Index WasmBinaryReader::readMemoryAccess(Address& alignment, Address& offset) { rawAlignment = rawAlignment & ~(1 << 6); } + if (rawAlignment & (1 << (7))) { + backing = BackingType::Array; + // Clear the bit before we parse alignment + rawAlignment = rawAlignment & ~(1 << 7); + } + if (rawAlignment > 8) { throwError("Alignment must be of a reasonable size"); } alignment = Bits::pow2(rawAlignment); - if (hasMemIdx) { - memIdx = getU32LEB(); - } - if (memIdx >= wasm.memories.size()) { - throwError("Memory index out of range while reading memory alignment."); + // TODO: don't allow memIdx when backing typ is array. + if (backing == BackingType::Memory) { + if (hasMemIdx) { + memIdx = getU32LEB(); + } + if (memIdx >= wasm.memories.size()) { + throwError("Memory index out of range while reading memory alignment."); + } + auto* memory = wasm.memories[memIdx].get(); + offset = memory->addressType == Type::i32 ? getU32LEB() : getU64LEB(); } - auto* memory = wasm.memories[memIdx].get(); - offset = memory->addressType == Type::i32 ? getU32LEB() : getU64LEB(); return memIdx; } // TODO: make this the only version -std::tuple WasmBinaryReader::getMemarg() { +// TODO: remove me - temporary hack to use a second getMemarg function so i +// don't have to update everywhere that uses getMemarg +std::tuple +WasmBinaryReader::getMemargWithBacking() { Address alignment, offset; - auto memIdx = readMemoryAccess(alignment, offset); - return {getMemoryName(memIdx), alignment, offset}; + BackingType backing; + auto memIdx = readMemoryAccess(alignment, offset, backing); + if (backing == BackingType::Array) { + // ??? how does binaryen usually handle empty names or maybe we shouldn't + // return a name? + return {{}, alignment, offset, backing}; + } + return {getMemoryName(memIdx), alignment, offset, backing}; +} + +std::tuple WasmBinaryReader::getMemarg() { + auto [memoryName, alignment, offset, backing] = getMemargWithBacking(); + return {memoryName, alignment, offset}; } MemoryOrder WasmBinaryReader::getMemoryOrder(bool isRMW) { diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index e63bb9d488d..93dfd688220 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -543,6 +543,13 @@ struct IRBuilder::ChildPopper return popConstrainedChildren(children); } + Result<> visitArrayStore(ArrayStore* curr, + std::optional ht = std::nullopt) { + std::vector children; + ConstraintCollector{builder, children}.visitArrayStore(curr, ht); + return popConstrainedChildren(children); + } + Result<> visitArrayCopy(ArrayCopy* curr, std::optional dest = std::nullopt, std::optional src = std::nullopt) { @@ -1475,6 +1482,18 @@ Result<> IRBuilder::makeStore( return Ok{}; } +Result<> IRBuilder::makeStore(BackingType backing, + unsigned bytes, + Address offset, + unsigned align, + Type type, + Name mem) { + if (backing == BackingType::Memory) { + return makeStore(bytes, offset, align, type, mem); + } + return makeArrayStore(bytes, type); +} + Result<> IRBuilder::makeAtomicLoad(unsigned bytes, Address offset, Type type, Name mem) { Load curr; @@ -2264,6 +2283,15 @@ Result<> IRBuilder::makeArraySet(HeapType type, MemoryOrder order) { return Ok{}; } +Result<> IRBuilder::makeArrayStore(unsigned bytes, Type type) { + ArrayStore curr; + curr.valueType = type; + CHECK_ERR( + ChildPopper{*this}.visitArrayStore(&curr, HeapTypes::getMutI8Array())); + push(builder.makeArrayStore(bytes, type, curr.ref, curr.index, curr.value)); + return Ok{}; +} + Result<> IRBuilder::makeArrayLen() { ArrayLen curr; CHECK_ERR(visitArrayLen(&curr)); diff --git a/src/wasm/wasm-stack.cpp b/src/wasm/wasm-stack.cpp index 33c3d7e3973..68c8e2442f1 100644 --- a/src/wasm/wasm-stack.cpp +++ b/src/wasm/wasm-stack.cpp @@ -56,6 +56,69 @@ void BinaryInstWriter::emitIfElse(If* curr) { o << int8_t(BinaryConsts::Else); } +void BinaryInstWriter::emitStore(uint8_t bytes, Type valueType) { + switch (valueType.getBasic()) { + case Type::i32: { + switch (bytes) { + case 1: + o << int8_t(BinaryConsts::I32StoreMem8); + break; + case 2: + o << int8_t(BinaryConsts::I32StoreMem16); + break; + case 4: + o << int8_t(BinaryConsts::I32StoreMem); + break; + default: + abort(); + } + break; + } + case Type::i64: { + switch (bytes) { + case 1: + o << int8_t(BinaryConsts::I64StoreMem8); + break; + case 2: + o << int8_t(BinaryConsts::I64StoreMem16); + break; + case 4: + o << int8_t(BinaryConsts::I64StoreMem32); + break; + case 8: + o << int8_t(BinaryConsts::I64StoreMem); + break; + default: + abort(); + } + break; + } + case Type::f32: { + switch (bytes) { + case 2: + o << int8_t(BinaryConsts::MiscPrefix) + << U32LEB(BinaryConsts::F32_F16StoreMem); + break; + case 4: + o << int8_t(BinaryConsts::F32StoreMem); + break; + default: + WASM_UNREACHABLE("invalid store size"); + } + break; + } + case Type::f64: + o << int8_t(BinaryConsts::F64StoreMem); + break; + case Type::v128: + o << int8_t(BinaryConsts::SIMDPrefix) << U32LEB(BinaryConsts::V128Store); + break; + case Type::none: + case Type::unreachable: + WASM_UNREACHABLE("unexpected type"); + } +} + void BinaryInstWriter::visitLoop(Loop* curr) { breakStack.push_back(curr->name); o << int8_t(BinaryConsts::Loop); @@ -366,67 +429,7 @@ void BinaryInstWriter::visitLoad(Load* curr) { void BinaryInstWriter::visitStore(Store* curr) { if (!curr->isAtomic) { - switch (curr->valueType.getBasic()) { - case Type::i32: { - switch (curr->bytes) { - case 1: - o << int8_t(BinaryConsts::I32StoreMem8); - break; - case 2: - o << int8_t(BinaryConsts::I32StoreMem16); - break; - case 4: - o << int8_t(BinaryConsts::I32StoreMem); - break; - default: - abort(); - } - break; - } - case Type::i64: { - switch (curr->bytes) { - case 1: - o << int8_t(BinaryConsts::I64StoreMem8); - break; - case 2: - o << int8_t(BinaryConsts::I64StoreMem16); - break; - case 4: - o << int8_t(BinaryConsts::I64StoreMem32); - break; - case 8: - o << int8_t(BinaryConsts::I64StoreMem); - break; - default: - abort(); - } - break; - } - case Type::f32: { - switch (curr->bytes) { - case 2: - o << int8_t(BinaryConsts::MiscPrefix) - << U32LEB(BinaryConsts::F32_F16StoreMem); - break; - case 4: - o << int8_t(BinaryConsts::F32StoreMem); - break; - default: - WASM_UNREACHABLE("invalid store size"); - } - break; - } - case Type::f64: - o << int8_t(BinaryConsts::F64StoreMem); - break; - case Type::v128: - o << int8_t(BinaryConsts::SIMDPrefix) - << U32LEB(BinaryConsts::V128Store); - break; - case Type::none: - case Type::unreachable: - WASM_UNREACHABLE("unexpected type"); - } + emitStore(curr->bytes, curr->valueType); } else { o << int8_t(BinaryConsts::AtomicPrefix); switch (curr->valueType.getBasic()) { @@ -2548,6 +2551,16 @@ void BinaryInstWriter::visitArraySet(ArraySet* curr) { parent.writeIndexedHeapType(curr->ref->type.getHeapType()); } +void BinaryInstWriter::visitArrayStore(ArrayStore* curr) { + if (curr->ref->type.isNull()) { + emitUnreachable(); + return; + } + emitStore(curr->bytes, curr->valueType); + uint32_t alignmentBits = 1 << 7; + o << U32LEB(alignmentBits); +} + void BinaryInstWriter::visitArrayLen(ArrayLen* curr) { o << int8_t(BinaryConsts::GCPrefix) << U32LEB(BinaryConsts::ArrayLen); } diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index 0a1df50dbb9..04182040a04 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -512,6 +512,7 @@ struct FunctionValidator : public WalkerPass> { void visitArrayNewFixed(ArrayNewFixed* curr); void visitArrayGet(ArrayGet* curr); void visitArraySet(ArraySet* curr); + void visitArrayStore(ArrayStore* curr); void visitArrayLen(ArrayLen* curr); void visitArrayCopy(ArrayCopy* curr); void visitArrayFill(ArrayFill* curr); @@ -3584,6 +3585,29 @@ void FunctionValidator::visitArraySet(ArraySet* curr) { shouldBeTrue(element.mutable_, curr, "array.set type must be mutable"); } +void FunctionValidator::visitArrayStore(ArrayStore* curr) { + // TODO add feature for multibyte? + // shouldBeTrue( + // getModule()->features.hasGC(), curr, "array.set requires gc [--enable-gc]"); + shouldBeEqualOrFirstIsUnreachable( + curr->index->type, Type(Type::i32), curr, "array store index must be an i32"); + if (curr->type == Type::unreachable) { + return; + } + const char* mustBeArray = "array store target should be an array reference"; + if (curr->type == Type::unreachable || + !shouldBeTrue(curr->ref->type.isRef(), curr, mustBeArray) || + curr->ref->type.getHeapType().isBottom() || + !shouldBeTrue(curr->ref->type.isArray(), curr, mustBeArray)) { + return; + } + + auto heapType = curr->ref->type.getHeapType(); + const auto& element = heapType.getArray().element; + shouldBeTrue(element.packedType == Field::i8, curr, "array store type must be i8"); + shouldBeTrue(element.mutable_, curr, "array store type must be mutable"); +} + void FunctionValidator::visitArrayLen(ArrayLen* curr) { shouldBeTrue( getModule()->features.hasGC(), curr, "array.len requires gc [--enable-gc]"); diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index 44c1daea622..92edd3ba53b 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -1323,6 +1323,15 @@ void ArraySet::finalize() { } } +void ArrayStore::finalize() { + if (ref->type == Type::unreachable || index->type == Type::unreachable || + value->type == Type::unreachable) { + type = Type::unreachable; + } else { + type = Type::none; + } +} + void ArrayLen::finalize() { if (ref->type == Type::unreachable) { type = Type::unreachable; diff --git a/src/wasm2js.h b/src/wasm2js.h index d6e0e4d6560..9cf3cf846e4 100644 --- a/src/wasm2js.h +++ b/src/wasm2js.h @@ -2365,6 +2365,10 @@ Ref Wasm2JSBuilder::processExpression(Expression* curr, unimplemented(curr); WASM_UNREACHABLE("unimp"); } + Ref visitArrayStore(ArrayStore* curr) { + unimplemented(curr); + WASM_UNREACHABLE("unimp"); + } Ref visitArrayLen(ArrayLen* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); diff --git a/test/lit/array-multibyte.wast b/test/lit/array-multibyte.wast new file mode 100644 index 00000000000..ed418bedad1 --- /dev/null +++ b/test/lit/array-multibyte.wast @@ -0,0 +1,126 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; Check that array types and operations are emitted properly in the binary format. + +;; RUN: wasm-opt %s -all -S -o - | filecheck %s +;; RUN: wasm-opt %s -all --roundtrip -S -o - | filecheck %s --check-prefix=ROUNDTRIP + +;; Check that we can roundtrip through the text format as well. + +;; RUN: wasm-opt %s -all -S -o - | wasm-opt -all -S -o - | filecheck %s + +(module + ;; CHECK: (type $i8_array (array (mut i8))) + ;; ROUNDTRIP: (type $i8_array (array (mut i8))) + (type $i8_array (array (mut i8))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (global $arr (ref $i8_array) (array.new_default $i8_array + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: )) + ;; ROUNDTRIP: (type $1 (func)) + + ;; ROUNDTRIP: (global $arr (ref $i8_array) (array.new_default $i8_array + ;; ROUNDTRIP-NEXT: (i32.const 4) + ;; ROUNDTRIP-NEXT: )) + (global $arr (ref $i8_array) + (array.new_default $i8_array (i32.const 4)) + ) + + ;; CHECK: (func $stores (type $1) + ;; CHECK-NEXT: (i32.store8 type=array + ;; CHECK-NEXT: (global.get $arr) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store16 type=array + ;; CHECK-NEXT: (global.get $arr) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store type=array + ;; CHECK-NEXT: (global.get $arr) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (f32.store type=array + ;; CHECK-NEXT: (global.get $arr) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i64.store8 type=array + ;; CHECK-NEXT: (global.get $arr) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i64.store16 type=array + ;; CHECK-NEXT: (global.get $arr) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i64.store32 type=array + ;; CHECK-NEXT: (global.get $arr) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (f64.store type=array + ;; CHECK-NEXT: (global.get $arr) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (f64.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; ROUNDTRIP: (func $stores (type $1) + ;; ROUNDTRIP-NEXT: (i32.store8 type=array + ;; ROUNDTRIP-NEXT: (global.get $arr) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: ) + ;; ROUNDTRIP-NEXT: (i32.store16 type=array + ;; ROUNDTRIP-NEXT: (global.get $arr) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: ) + ;; ROUNDTRIP-NEXT: (i32.store type=array + ;; ROUNDTRIP-NEXT: (global.get $arr) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: ) + ;; ROUNDTRIP-NEXT: (f32.store type=array + ;; ROUNDTRIP-NEXT: (global.get $arr) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: (f32.const 0) + ;; ROUNDTRIP-NEXT: ) + ;; ROUNDTRIP-NEXT: (i64.store8 type=array + ;; ROUNDTRIP-NEXT: (global.get $arr) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: ) + ;; ROUNDTRIP-NEXT: (i64.store16 type=array + ;; ROUNDTRIP-NEXT: (global.get $arr) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: ) + ;; ROUNDTRIP-NEXT: (i64.store32 type=array + ;; ROUNDTRIP-NEXT: (global.get $arr) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: ) + ;; ROUNDTRIP-NEXT: (f64.store type=array + ;; ROUNDTRIP-NEXT: (global.get $arr) + ;; ROUNDTRIP-NEXT: (i32.const 0) + ;; ROUNDTRIP-NEXT: (f64.const 0) + ;; ROUNDTRIP-NEXT: ) + ;; ROUNDTRIP-NEXT: ) + (func $stores + (i32.store8 type=array (global.get $arr) (i32.const 0) (i32.const 0)) + (i32.store16 type=array (global.get $arr) (i32.const 0) (i32.const 0)) + (i32.store type=array (global.get $arr) (i32.const 0) (i32.const 0)) + (f32.store type=array (global.get $arr) (i32.const 0) (f32.const 0)) + + (i64.store8 type=array (global.get $arr) (i32.const 0) (i32.const 0)) + (i64.store16 type=array (global.get $arr) (i32.const 0) (i32.const 0)) + (i64.store32 type=array (global.get $arr) (i32.const 0) (i32.const 0)) + (f64.store type=array (global.get $arr) (i32.const 0) (f64.const 0)) + ) +) diff --git a/test/spec/array-multibyte.wast b/test/spec/array-multibyte.wast new file mode 100644 index 00000000000..f3439eca65c --- /dev/null +++ b/test/spec/array-multibyte.wast @@ -0,0 +1,423 @@ +;; Current Syntax: type=array +;; +;; (func $test2 (export "test2") +;; (i32.store type=array +;; (global.get $arr_4) +;; (i32.const 1) +;; (i32.const 1337) +;; ) +;; ) + +;; Alternative Syntax 1: array +;; +;; (func $test2 (export "test2") +;; (i32.store array +;; (global.get $arr_4) +;; (i32.const 1) +;; (i32.const 1337) +;; ) +;; ) + +;; Alternative Syntax 2: new opcodes +;; +;; (func $test2 (export "test2") +;; (i32.array.store +;; (global.get $arr_4) +;; (i32.const 1) +;; (i32.const 1337) +;; ) +;; ) + +(module + (type $i8_array (array (mut i8))) + + (global $arr_4 (ref $i8_array) + (array.new_default $i8_array (i32.const 4)) + ) + + (global $arr_8 (ref $i8_array) + (array.new_default $i8_array (i32.const 8)) + ) + + ;; Read i32 from an i8 array using regular array get instructions. + ;; TODO remove this and use the load instructions when implemented. + (func $load_i32_from_array + (export "load_i32_from_array") + (param $arr (ref $i8_array)) + (param $idx i32) + (result i32) + + ;; 1. Load the first byte (least significant) + (array.get_u $i8_array (local.get $arr) (local.get $idx)) + + ;; 2. Load the second byte and shift it left by 8 bits + (array.get_u $i8_array (local.get $arr) (i32.add (local.get $idx) (i32.const 1))) + (i32.const 8) + (i32.shl) + (i32.or) + + ;; 3. Load the third byte and shift it left by 16 bits + (array.get_u $i8_array (local.get $arr) (i32.add (local.get $idx) (i32.const 2))) + (i32.const 16) + (i32.shl) + (i32.or) + + ;; 4. Load the fourth byte (most significant) and shift it left by 24 bits + (array.get_u $i8_array (local.get $arr) (i32.add (local.get $idx) (i32.const 3))) + (i32.const 24) + (i32.shl) + (i32.or) + ) + + ;; TODO remove this and use the load instructions when implemented. + (func $load_i64_from_array + (export "load_i64_from_array") + (param $arr (ref $i8_array)) + (param $idx i32) + (result i64) + (call $load_i32_from_array (local.get $arr) (local.get $idx)) + (i64.extend_i32_u) + + (call $load_i32_from_array (local.get $arr) (i32.add (local.get $idx) (i32.const 4))) + (i64.extend_i32_u) + (i64.const 32) + (i64.shl) + (i64.or) + ) + + (func $i32_set_i8 (export "i32_set_i8") (param $index i32) (param $value i32) + (i32.store8 type=array + (global.get $arr_4) + (local.get $index) + (local.get $value) + ) + ) + + (func $i32_set_i16 (export "i32_set_i16") (param $index i32) (param $value i32) + (i32.store16 type=array + (global.get $arr_4) + (local.get $index) + (local.get $value) + ) + ) + + (func $i32_set_i32 (export "i32_set_i32") (param $index i32) (param $value i32) + (i32.store type=array + (global.get $arr_4) + (local.get $index) + (local.get $value) + ) + ) + + (func $f32_set (export "f32_set") (param $index i32) (param $value f32) + (f32.store type=array + (global.get $arr_4) + (local.get $index) + (local.get $value) + ) + ) + + (func $i64_set_i8 (export "i64_set_i8") (param $index i32) (param $value i64) + (i64.store8 type=array + (global.get $arr_8) + (local.get $index) + (local.get $value) + ) + ) + + (func $i64_set_i16 (export "i64_set_i16") (param $index i32) (param $value i64) + (i64.store16 type=array + (global.get $arr_8) + (local.get $index) + (local.get $value) + ) + ) + + (func $i64_set_i32 (export "i64_set_i32") (param $index i32) (param $value i64) + (i64.store32 type=array + (global.get $arr_8) + (local.get $index) + (local.get $value) + ) + ) + + (func $i64_set_i64 (export "i64_set_i64") (param $index i32) (param $value i64) + (i64.store type=array + (global.get $arr_8) + (local.get $index) + (local.get $value) + ) + ) + + (func $f64_set (export "f64_set") (param $index i32) (param $value f64) + (f64.store type=array + (global.get $arr_8) + (local.get $index) + (local.get $value) + ) + ) + + ;; ??? Do we even want to spec out this instruction since array.store is the + ;; same thing? + (func $i32_set_and_get_i8 (export "i32_set_and_get_i8") (param $value i32) (result i32) + (i32.store8 type=array + (global.get $arr_4) + (i32.const 0) + (local.get $value) + ) + ;; TODO: when multibyte get is supported use that instead here. + (call $load_i32_from_array (global.get $arr_4) (i32.const 0)) + ) + + (func $i32_set_and_get_i16 (export "i32_set_and_get_i16") (param $value i32) (result i32) + (i32.store16 type=array + (global.get $arr_4) + (i32.const 0) + (local.get $value) + ) + ;; TODO: when multibyte get is supported use that instead here. + (call $load_i32_from_array (global.get $arr_4) (i32.const 0)) + ) + + (func $i32_set_and_get_i32 (export "i32_set_and_get_i32") (param $value i32) (result i32) + (i32.store type=array + (global.get $arr_4) + (i32.const 0) + (local.get $value) + ) + ;; TODO: when multibyte get is supported use that instead here. + (call $load_i32_from_array (global.get $arr_4) (i32.const 0)) + ) + + (func $set_and_get_f32 (export "set_and_get_f32") (param $value f32) (result f32) + (f32.store type=array + (global.get $arr_4) + (i32.const 0) + (local.get $value) + ) + ;; TODO: when multibyte get is supported use that instead here. + (call $load_i32_from_array (global.get $arr_4) (i32.const 0)) + (f32.reinterpret_i32) + ) + + (func $i64_set_and_get_i8 (export "i64_set_and_get_i8") (param $value i64) (result i64) + (i64.store8 type=array + (global.get $arr_8) + (i32.const 0) + (local.get $value) + ) + ;; TODO: when multibyte get is supported use that instead here. + (call $load_i64_from_array (global.get $arr_8) (i32.const 0)) + ) + + (func $i64_set_and_get_i16 (export "i64_set_and_get_i16") (param $value i64) (result i64) + (i64.store16 type=array + (global.get $arr_8) + (i32.const 0) + (local.get $value) + ) + ;; TODO: when multibyte get is supported use that instead here. + (call $load_i64_from_array (global.get $arr_8) (i32.const 0)) + ) + + (func $i64_set_and_get_i32 (export "i64_set_and_get_i32") (param $value i64) (result i64) + (i64.store32 type=array + (global.get $arr_8) + (i32.const 0) + (local.get $value) + ) + ;; TODO: when multibyte get is supported use that instead here. + (call $load_i64_from_array (global.get $arr_8) (i32.const 0)) + ) + + (func $i64_set_and_get_i64 (export "i64_set_and_get_i64") (param $value i64) (result i64) + (i64.store type=array + (global.get $arr_8) + (i32.const 0) + (local.get $value) + ) + ;; TODO: when multibyte get is supported use that instead here. + (call $load_i64_from_array (global.get $arr_8) (i32.const 0)) + ) + + (func $set_and_get_f64 (export "set_and_get_f64") (param $value f64) (result f64) + (f64.store type=array + (global.get $arr_8) + (i32.const 0) + (local.get $value) + ) + ;; TODO: when multibyte get is supported use that instead here. + (call $load_i64_from_array (global.get $arr_8) (i32.const 0)) + (f64.reinterpret_i64) + ) +) + +;; +;; 32 bit round trip tests +;; + +(assert_return (invoke "i32_set_and_get_i8" (i32.const 0)) (i32.const 0)) +(assert_return (invoke "i32_set_and_get_i8" (i32.const 255)) (i32.const 255)) +;; ensure high bits are ignored +(assert_return (invoke "i32_set_and_get_i8" (i32.const 0xFFFFFF00)) (i32.const 0)) + +(assert_return (invoke "i32_set_and_get_i16" (i32.const 0)) (i32.const 0)) +(assert_return (invoke "i32_set_and_get_i16" (i32.const 65535)) (i32.const 65535)) +;; ensure high bits are ignored +(assert_return (invoke "i32_set_and_get_i16" (i32.const 0xFFFF0000)) (i32.const 0)) + +(assert_return (invoke "i32_set_and_get_i32" (i32.const 0)) (i32.const 0)) +(assert_return (invoke "i32_set_and_get_i32" (i32.const 1)) (i32.const 1)) +(assert_return (invoke "i32_set_and_get_i32" (i32.const 256)) (i32.const 256)) +(assert_return (invoke "i32_set_and_get_i32" (i32.const -1)) (i32.const -1)) +(assert_return (invoke "i32_set_and_get_i32" (i32.const 2147483647)) (i32.const 2147483647)) +(assert_return (invoke "i32_set_and_get_i32" (i32.const -2147483648)) (i32.const -2147483648)) + +(assert_return (invoke "set_and_get_f32" (f32.const 0)) (f32.const 0)) +(assert_return (invoke "set_and_get_f32" (f32.const -1)) (f32.const -1)) +(assert_return (invoke "set_and_get_f32" (f32.const 3.3)) (f32.const 3.3)) +(assert_return (invoke "set_and_get_f32" (f32.const -2.000000238418579)) (f32.const -2.000000238418579)) +(assert_return (invoke "set_and_get_f32" (f32.const nan)) (f32.const nan)) + +;; +;; 64 bit round trip tests +;; + +(assert_return (invoke "i64_set_and_get_i8" (i64.const 0)) (i64.const 0)) +(assert_return (invoke "i64_set_and_get_i8" (i64.const 255)) (i64.const 255)) +;; ensure high bits are ignored +(assert_return (invoke "i64_set_and_get_i8" (i64.const 0xFFFFFFFFFFFFFF00)) (i64.const 0)) + +(assert_return (invoke "i64_set_and_get_i16" (i64.const 0)) (i64.const 0)) +(assert_return (invoke "i64_set_and_get_i16" (i64.const 65535)) (i64.const 65535)) +;; ensure high bits are ignored +(assert_return (invoke "i64_set_and_get_i16" (i64.const 0xFFFFFFFFFFFF0000)) (i64.const 0)) + +(assert_return (invoke "i64_set_and_get_i32" (i64.const 0)) (i64.const 0)) +(assert_return (invoke "i64_set_and_get_i32" (i64.const 2147483647)) (i64.const 2147483647)) +;; unsigned extend +(assert_return (invoke "i64_set_and_get_i32" (i64.const -2147483648)) (i64.const 2147483648)) +;; ensure high bits are ignored +(assert_return (invoke "i64_set_and_get_i32" (i64.const 0xFFFFFFFF00000000)) (i64.const 0)) + +(assert_return (invoke "i64_set_and_get_i64" (i64.const 0)) (i64.const 0)) +(assert_return (invoke "i64_set_and_get_i64" (i64.const 9223372036854775807)) (i64.const 9223372036854775807)) +(assert_return (invoke "i64_set_and_get_i64" (i64.const -9223372036854775808)) (i64.const -9223372036854775808)) + +(assert_return (invoke "set_and_get_f64" (f64.const 0)) (f64.const 0)) +(assert_return (invoke "set_and_get_f64" (f64.const -1)) (f64.const -1)) +(assert_return (invoke "set_and_get_f64" (f64.const 3.3)) (f64.const 3.3)) +(assert_return (invoke "set_and_get_f64" (f64.const -2.00000000000000044409)) (f64.const -2.00000000000000044409)) +(assert_return (invoke "set_and_get_f64" (f64.const nan)) (f64.const nan)) + +;; +;; Bounds checks (32 bit with a 4-byte array) +;; + +;; i32_set_i8: Writes 1 byte +;; Valid range: [0, 3] +(assert_trap (invoke "i32_set_i8" (i32.const -1) (i32.const 0)) "out of bounds") +(assert_return (invoke "i32_set_i8" (i32.const 0) (i32.const 0))) +(assert_return (invoke "i32_set_i8" (i32.const 1) (i32.const 0))) +(assert_return (invoke "i32_set_i8" (i32.const 2) (i32.const 0))) +(assert_return (invoke "i32_set_i8" (i32.const 3) (i32.const 0))) +(assert_trap (invoke "i32_set_i8" (i32.const 4) (i32.const 0)) "out of bounds") + +;; i32_set_i16: Writes 2 bytes +;; Valid range: offset + 2 <= 4 -> Max offset 2 +(assert_trap (invoke "i32_set_i16" (i32.const -1) (i32.const 0)) "out of bounds") +(assert_return (invoke "i32_set_i16" (i32.const 0) (i32.const 0))) +(assert_return (invoke "i32_set_i16" (i32.const 1) (i32.const 0))) +(assert_return (invoke "i32_set_i16" (i32.const 2) (i32.const 0))) +(assert_trap (invoke "i32_set_i16" (i32.const 3) (i32.const 0)) "out of bounds") + +;; i32_set_i32: Writes 4 bytes +;; Valid range: offset + 4 <= 4 -> Max offset 0 +(assert_trap (invoke "i32_set_i32" (i32.const -1) (i32.const 0)) "out of bounds") +(assert_return (invoke "i32_set_i32" (i32.const 0) (i32.const 0))) +(assert_trap (invoke "i32_set_i32" (i32.const 1) (i32.const 0)) "out of bounds") + +;; f32_set: Writes 4 bytes +;; Valid range: offset + 4 <= 4 -> Max offset 0 +(assert_trap (invoke "f32_set" (i32.const -1) (f32.const 0)) "out of bounds") +(assert_return (invoke "f32_set" (i32.const 0) (f32.const 0))) +(assert_trap (invoke "f32_set" (i32.const 1) (f32.const 0)) "out of bounds") + +;; +;; Bounds checks (64 bit with an 8-byte array) +;; + +;; i64_set_i8: Writes 1 byte +;; Valid range: [0, 7] +(assert_trap (invoke "i64_set_i8" (i32.const -1) (i64.const 0)) "out of bounds") +(assert_return (invoke "i64_set_i8" (i32.const 0) (i64.const 0))) +(assert_return (invoke "i64_set_i8" (i32.const 1) (i64.const 0))) +(assert_return (invoke "i64_set_i8" (i32.const 6) (i64.const 0))) +(assert_return (invoke "i64_set_i8" (i32.const 7) (i64.const 0))) +(assert_trap (invoke "i64_set_i8" (i32.const 8) (i64.const 0)) "out of bounds") + +;; i64_set_i16: Writes 2 bytes +;; Valid range: offset + 2 <= 8 -> Max offset 6 +(assert_trap (invoke "i64_set_i16" (i32.const -1) (i64.const 0)) "out of bounds") +(assert_return (invoke "i64_set_i16" (i32.const 0) (i64.const 0))) +(assert_return (invoke "i64_set_i16" (i32.const 1) (i64.const 0))) +(assert_return (invoke "i64_set_i16" (i32.const 5) (i64.const 0))) +(assert_return (invoke "i64_set_i16" (i32.const 6) (i64.const 0))) +(assert_trap (invoke "i64_set_i16" (i32.const 7) (i64.const 0)) "out of bounds") + +;; i64_set_i32: Writes 4 bytes +;; Valid range: offset + 4 <= 8 -> Max offset 4 +(assert_trap (invoke "i64_set_i32" (i32.const -1) (i64.const 0)) "out of bounds") +(assert_return (invoke "i64_set_i32" (i32.const 0) (i64.const 0))) +(assert_return (invoke "i64_set_i32" (i32.const 1) (i64.const 0))) +(assert_return (invoke "i64_set_i32" (i32.const 3) (i64.const 0))) +(assert_return (invoke "i64_set_i32" (i32.const 4) (i64.const 0))) +(assert_trap (invoke "i64_set_i32" (i32.const 5) (i64.const 0)) "out of bounds") + +;; i64_set_i64: Writes 8 bytes +;; Valid range: offset + 8 <= 8 -> Max offset 0 +(assert_trap (invoke "i64_set_i64" (i32.const -1) (i64.const 0)) "out of bounds") +(assert_return (invoke "i64_set_i64" (i32.const 0) (i64.const 0))) +(assert_trap (invoke "i64_set_i64" (i32.const 1) (i64.const 0)) "out of bounds") + +;; f64_set: Writes 8 bytes +;; Valid range: offset + 8 <= 8 -> Max offset 0 +(assert_trap (invoke "f64_set" (i32.const -1) (f64.const 0)) "out of bounds") +(assert_return (invoke "f64_set" (i32.const 0) (f64.const 0))) +(assert_trap (invoke "f64_set" (i32.const 1) (f64.const 0)) "out of bounds") + + +(assert_invalid + (module + (type $a (array i8)) + (func (export "i32_set_immutable") (param $a (ref $a)) + (i32.store type=array (local.get $a) (i32.const 0) (i32.const 1)) + ) + ) + "array is immutable" +) + +(assert_invalid + (module + (type $a (array (mut i16))) + (func (export "i32_set_mut_i16") (param $a (ref $a)) + (i32.store type=array (local.get $a) (i32.const 0) (i32.const 1)) + ) + ) + "array element type must be i8" +) + +;; Null dereference + +(module + (type $t (array (mut i8))) + ;; (func (export "array.get-null") + ;; (local (ref null $t)) (drop (array.get $t (local.get 0) (i32.const 0))) + ;; ) + (func (export "i32.store_array_null") + (local (ref null $t)) (i32.store type=array (local.get 0) (i32.const 0) (i32.const 0)) + ) +) + +;; (assert_trap (invoke "array.get-null") "null array") +(assert_trap (invoke "i32.store_array_null") "null array")