Skip to content

Commit 49d24c2

Browse files
feat: add atomic instructions
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 785be0e commit 49d24c2

10 files changed

Lines changed: 357 additions & 7 deletions

File tree

‎crates/parser/src/lib.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ impl Parser {
174174
#[cfg(feature = "validate")]
175175
{
176176
let features = WasmFeatures::WASM3
177-
.difference(WasmFeatures::THREADS)
178177
.union(WasmFeatures::CUSTOM_PAGE_SIZES)
179178
.union(WasmFeatures::WIDE_ARITHMETIC)
180179
.union(WasmFeatures::COMPACT_IMPORTS);

‎crates/parser/src/macros.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ pub(crate) mod visit {
203203
(@@function_references $($rest:tt)* ) => {};
204204
(@@gc $($rest:tt)* ) => {};
205205
(@@exceptions $($rest:tt)* ) => {};
206+
(@@threads $($rest:tt)* ) => {};
206207

207208
(@@$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*)) => {
208209
fn $visit(&mut self $($(,_: $argty)*)?) -> Self::Output {

‎crates/parser/src/visit.rs‎

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ use alloc::{
1111
vec::Vec,
1212
};
1313
use tinywasm_types::{
14-
BinOp, BinOp128, CmpOp, ExceptionHandler, Global, Import, ImportKind, Instruction, MemoryType, Operand64,
15-
Operand64Idx, Operand128, Operand128Idx, StorageType, TableDefinition, TagType, TypeSection, ValueCounts,
16-
ValueLane, WasmFunctionData,
14+
AtomicArg, AtomicOp, AtomicWidth, BinOp, BinOp128, CmpOp, ExceptionHandler, Global, Import, ImportKind,
15+
Instruction, MemoryType, Operand64, Operand64Idx, Operand128, Operand128Idx, StorageType, TableDefinition, TagType,
16+
TypeSection, ValueCounts, ValueLane, WasmFunctionData,
1717
};
1818
use wasmparser::{FunctionBody, OperatorsReader, OperatorsReaderAllocations, VisitSimdOperator};
1919

@@ -497,6 +497,35 @@ pub(crate) fn process_operators_and_validate(
497497
Ok((instructions, builder.data, builder.uses_local_memory, validator.into_allocations(), reader.into_allocations()))
498498
}
499499

500+
macro_rules! atomic_visitors {
501+
($($op:ident, $inputs:tt => $outputs:tt: $(($visit:ident, $width:literal)),+;)*) => {
502+
$(atomic_visitors!(@group $op, $inputs => $outputs: $(($visit, $width)),+);)*
503+
};
504+
(@group $op:ident, $inputs:tt => $outputs:tt: $(($visit:ident, $width:literal)),+) => {
505+
$(atomic_visitors!(@one $op, $inputs => $outputs: $visit, $width);)+
506+
};
507+
(@one $op:ident, [$($input:ident),*] => [$($output:ident),*]: $visit:ident, $width:literal) => {
508+
fn $visit(&mut self, memarg: wasmparser::MemArg) -> Self::Output {
509+
if memarg.align != ($width as u32).trailing_zeros() as u8 {
510+
return Err(crate::ParseError::Other("invalid atomic alignment".into()));
511+
}
512+
let address = self.metadata.memory_size(memarg.memory)?;
513+
self.mark_memory(memarg.memory);
514+
let memory = self.push128(Operand128::<tinywasm_types::MemoryOperand>::new(memarg.offset, memarg.memory))?;
515+
self.emit(
516+
&[$(atomic_visitors!(@size $input, address)),*],
517+
&[$(atomic_visitors!(@size $output, address)),*],
518+
Instruction::Atomic(AtomicArg::new(memory, AtomicWidth::from_bytes($width), atomic_visitors!(@is64 [$($input),*] [$($output),*]), AtomicOp::$op)),
519+
)
520+
}
521+
};
522+
(@size Addr, $address:ident) => { $address };
523+
(@size $lane:ident, $address:ident) => { ValueLane::$lane };
524+
(@is64 [Addr, S64 $(, S64)*] $outputs:tt) => { true };
525+
(@is64 [Addr] [S64]) => { true };
526+
(@is64 $inputs:tt $outputs:tt) => { false };
527+
}
528+
500529
impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> {
501530
type Output = Result<()>;
502531

@@ -506,6 +535,41 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> {
506535

507536
wasmparser::for_each_visit_operator!(impl_visit_operator);
508537

538+
fn visit_atomic_fence(&mut self) -> Self::Output {
539+
self.emit(&[], &[], Instruction::AtomicFence)
540+
}
541+
542+
fn visit_memory_atomic_notify(&mut self, _: wasmparser::MemArg) -> Self::Output {
543+
Err(crate::ParseError::UnsupportedOperator("memory.atomic.notify".into()))
544+
}
545+
fn visit_memory_atomic_wait32(&mut self, _: wasmparser::MemArg) -> Self::Output {
546+
Err(crate::ParseError::UnsupportedOperator("memory.atomic.wait32".into()))
547+
}
548+
fn visit_memory_atomic_wait64(&mut self, _: wasmparser::MemArg) -> Self::Output {
549+
Err(crate::ParseError::UnsupportedOperator("memory.atomic.wait64".into()))
550+
}
551+
552+
atomic_visitors! {
553+
Load, [Addr] => [S32]: (visit_i32_atomic_load, 4), (visit_i32_atomic_load8_u, 1), (visit_i32_atomic_load16_u, 2);
554+
Load, [Addr] => [S64]: (visit_i64_atomic_load, 8), (visit_i64_atomic_load8_u, 1), (visit_i64_atomic_load16_u, 2), (visit_i64_atomic_load32_u, 4);
555+
Store, [Addr, S32] => []: (visit_i32_atomic_store, 4), (visit_i32_atomic_store8, 1), (visit_i32_atomic_store16, 2);
556+
Store, [Addr, S64] => []: (visit_i64_atomic_store, 8), (visit_i64_atomic_store8, 1), (visit_i64_atomic_store16, 2), (visit_i64_atomic_store32, 4);
557+
Add, [Addr, S32] => [S32]: (visit_i32_atomic_rmw_add, 4), (visit_i32_atomic_rmw8_add_u, 1), (visit_i32_atomic_rmw16_add_u, 2);
558+
Add, [Addr, S64] => [S64]: (visit_i64_atomic_rmw_add, 8), (visit_i64_atomic_rmw8_add_u, 1), (visit_i64_atomic_rmw16_add_u, 2), (visit_i64_atomic_rmw32_add_u, 4);
559+
Sub, [Addr, S32] => [S32]: (visit_i32_atomic_rmw_sub, 4), (visit_i32_atomic_rmw8_sub_u, 1), (visit_i32_atomic_rmw16_sub_u, 2);
560+
Sub, [Addr, S64] => [S64]: (visit_i64_atomic_rmw_sub, 8), (visit_i64_atomic_rmw8_sub_u, 1), (visit_i64_atomic_rmw16_sub_u, 2), (visit_i64_atomic_rmw32_sub_u, 4);
561+
And, [Addr, S32] => [S32]: (visit_i32_atomic_rmw_and, 4), (visit_i32_atomic_rmw8_and_u, 1), (visit_i32_atomic_rmw16_and_u, 2);
562+
And, [Addr, S64] => [S64]: (visit_i64_atomic_rmw_and, 8), (visit_i64_atomic_rmw8_and_u, 1), (visit_i64_atomic_rmw16_and_u, 2), (visit_i64_atomic_rmw32_and_u, 4);
563+
Or, [Addr, S32] => [S32]: (visit_i32_atomic_rmw_or, 4), (visit_i32_atomic_rmw8_or_u, 1), (visit_i32_atomic_rmw16_or_u, 2);
564+
Or, [Addr, S64] => [S64]: (visit_i64_atomic_rmw_or, 8), (visit_i64_atomic_rmw8_or_u, 1), (visit_i64_atomic_rmw16_or_u, 2), (visit_i64_atomic_rmw32_or_u, 4);
565+
Xor, [Addr, S32] => [S32]: (visit_i32_atomic_rmw_xor, 4), (visit_i32_atomic_rmw8_xor_u, 1), (visit_i32_atomic_rmw16_xor_u, 2);
566+
Xor, [Addr, S64] => [S64]: (visit_i64_atomic_rmw_xor, 8), (visit_i64_atomic_rmw8_xor_u, 1), (visit_i64_atomic_rmw16_xor_u, 2), (visit_i64_atomic_rmw32_xor_u, 4);
567+
Xchg, [Addr, S32] => [S32]: (visit_i32_atomic_rmw_xchg, 4), (visit_i32_atomic_rmw8_xchg_u, 1), (visit_i32_atomic_rmw16_xchg_u, 2);
568+
Xchg, [Addr, S64] => [S64]: (visit_i64_atomic_rmw_xchg, 8), (visit_i64_atomic_rmw8_xchg_u, 1), (visit_i64_atomic_rmw16_xchg_u, 2), (visit_i64_atomic_rmw32_xchg_u, 4);
569+
Cmpxchg, [Addr, S32, S32] => [S32]: (visit_i32_atomic_rmw_cmpxchg, 4), (visit_i32_atomic_rmw8_cmpxchg_u, 1), (visit_i32_atomic_rmw16_cmpxchg_u, 2);
570+
Cmpxchg, [Addr, S64, S64] => [S64]: (visit_i64_atomic_rmw_cmpxchg, 8), (visit_i64_atomic_rmw8_cmpxchg_u, 1), (visit_i64_atomic_rmw16_cmpxchg_u, 2), (visit_i64_atomic_rmw32_cmpxchg_u, 4);
571+
}
572+
509573
lowering_ops! {
510574
memory [Addr] => [S32] {
511575
visit_i32_load => I32Load [load(Instruction::LoadLocal32)],

‎crates/tinywasm/src/error.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ pub enum Trap {
131131
max: usize,
132132
},
133133

134+
/// An atomic memory access was not naturally aligned.
135+
UnalignedAtomic,
136+
134137
/// An out-of-bounds table access occurred
135138
TableOutOfBounds {
136139
/// The offset of the access
@@ -216,6 +219,7 @@ impl Trap {
216219
match self {
217220
Self::Unreachable => "unreachable",
218221
Self::MemoryOutOfBounds { .. } => "out of bounds memory access",
222+
Self::UnalignedAtomic => "unaligned atomic",
219223
Self::TableOutOfBounds { .. } => "out of bounds table access",
220224
Self::ArrayOutOfBounds => "out of bounds array access",
221225
Self::DivisionByZero => "integer divide by zero",
@@ -324,6 +328,7 @@ impl Display for Trap {
324328
Self::MemoryOutOfBounds { offset, len, max } => {
325329
write!(f, "out of bounds memory access: offset={offset}, len={len}, max={max}")
326330
}
331+
Self::UnalignedAtomic => write!(f, "unaligned atomic"),
327332
Self::TableOutOfBounds { offset, len, max } => {
328333
write!(f, "out of bounds table access: offset={offset}, len={len}, max={max}")
329334
}

‎crates/tinywasm/src/interpreter/executor/instructions.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,8 @@ macro_rules! instruction_handlers {
323323
ArrayInitElem(idx) => executor.exec_array_init_elem(*idx)?,
324324
MemorySize(addr) => executor.exec_memory_size(*addr)?,
325325
MemoryGrow(addr) => executor.exec_memory_grow(*addr)?,
326+
Atomic(arg) => executor.exec_atomic(*arg)?,
327+
AtomicFence => crate::shared::fence(crate::shared::Ordering::SeqCst),
326328

327329
// Bulk memory operations
328330
MemoryCopy(idx) => executor.exec_memory_copy(*idx)?,

‎crates/tinywasm/src/interpreter/executor/mod.rs‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,74 @@ impl<'store> Executor<'store> {
14301430
TARGET::stack_push(&mut self.store.value_stack, cast(value))
14311431
}
14321432

1433+
fn exec_atomic(&mut self, arg: AtomicArg) -> Result<(), Trap> {
1434+
match arg.width() {
1435+
AtomicWidth::Bytes1 => self.exec_atomic_width::<1>(arg),
1436+
AtomicWidth::Bytes2 => self.exec_atomic_width::<2>(arg),
1437+
AtomicWidth::Bytes4 => self.exec_atomic_width::<4>(arg),
1438+
AtomicWidth::Bytes8 => self.exec_atomic_width::<8>(arg),
1439+
}
1440+
}
1441+
1442+
fn exec_atomic_width<const N: usize>(&mut self, arg: AtomicArg) -> Result<(), Trap> {
1443+
let op = arg.op();
1444+
let value = if op == AtomicOp::Load {
1445+
0
1446+
} else if arg.is_64() {
1447+
u64::stack_pop(&mut self.store.value_stack)
1448+
} else {
1449+
u32::stack_pop(&mut self.store.value_stack) as u64
1450+
};
1451+
let expected = if op == AtomicOp::Cmpxchg {
1452+
if arg.is_64() {
1453+
u64::stack_pop(&mut self.store.value_stack)
1454+
} else {
1455+
u32::stack_pop(&mut self.store.value_stack) as u64
1456+
}
1457+
} else {
1458+
0
1459+
};
1460+
let memory = arg.memory.resolve(&self.func.data);
1461+
let mem = self.store.state.get_mem_mut(self.mem_addr(memory.memory()));
1462+
let base = self.store.value_stack.pop_memory_operand(mem.kind.arch())?;
1463+
let addr = cold_err!(mem.effective_addr::<N>(base, memory.offset()))?;
1464+
if addr % N != 0 {
1465+
return cold!(Err(Trap::UnalignedAtomic));
1466+
}
1467+
if op == AtomicOp::Store {
1468+
let mut bytes = [0u8; N];
1469+
bytes.copy_from_slice(&value.to_le_bytes()[..N]);
1470+
return cold_err!(mem.inner.write_fixed::<N>(addr, &bytes));
1471+
}
1472+
let bytes = cold_err!(mem.inner.read_fixed::<N>(addr))?;
1473+
let mut padded = [0u8; 8];
1474+
padded[..N].copy_from_slice(&bytes);
1475+
let old = u64::from_le_bytes(padded);
1476+
let mask = if N == 8 { u64::MAX } else { (1u64 << (N * 8)) - 1 };
1477+
let next = match op {
1478+
AtomicOp::Load => None,
1479+
AtomicOp::Store => unreachable!(),
1480+
AtomicOp::Xchg => Some(value),
1481+
AtomicOp::Add => Some(old.wrapping_add(value)),
1482+
AtomicOp::Sub => Some(old.wrapping_sub(value)),
1483+
AtomicOp::And => Some(old & value),
1484+
AtomicOp::Or => Some(old | value),
1485+
AtomicOp::Xor => Some(old ^ value),
1486+
AtomicOp::Cmpxchg => (old == (expected & mask)).then_some(value),
1487+
};
1488+
if let Some(next) = next {
1489+
let mut bytes = [0u8; N];
1490+
bytes.copy_from_slice(&next.to_le_bytes()[..N]);
1491+
cold_err!(mem.inner.write_fixed::<N>(addr, &bytes))?;
1492+
}
1493+
if arg.is_64() {
1494+
u64::stack_push(&mut self.store.value_stack, old)?;
1495+
} else {
1496+
u32::stack_push(&mut self.store.value_stack, old as u32)?;
1497+
}
1498+
Ok(())
1499+
}
1500+
14331501
#[inline(always)]
14341502
fn exec_mem_store_lane<U: MemValue<N> + Copy, const N: usize>(&mut self, arg: MemoryLaneArg) -> Result<(), Trap> {
14351503
let bytes = Value128::stack_pop(&mut self.store.value_stack).to_mem_bytes();

‎crates/tinywasm/src/shared.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ pub(crate) use alloc::rc::Rc as StoreShared;
44
pub(crate) use tinywasm_types::Shared as StoreShared;
55

66
#[cfg(not(feature = "portable-atomic"))]
7-
pub(crate) use core::sync::atomic::{AtomicU32, Ordering};
7+
pub(crate) use core::sync::atomic::{AtomicU32, Ordering, fence};
88
#[cfg(feature = "portable-atomic")]
9-
pub(crate) use portable_atomic::{AtomicU32, Ordering};
9+
pub(crate) use portable_atomic::{AtomicU32, Ordering, fence};

‎crates/tinywasm/tests/atomics.rs‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use tinywasm::{Error, ModuleInstance, Store, Trap};
2+
3+
type TestResult = Result<(), Box<dyn core::error::Error>>;
4+
5+
#[test]
6+
fn atomic_operations_on_ordinary_memory() -> TestResult {
7+
let wasm = wat::parse_str(
8+
r#"
9+
(module
10+
(memory (export "memory") 1)
11+
(func (export "run") (result i64)
12+
i32.const 12
13+
i32.const 255
14+
i32.atomic.store8
15+
i32.const 12
16+
i32.const 2
17+
i32.atomic.rmw8.add_u
18+
drop
19+
i32.const 12
20+
i32.const 1
21+
i32.const 7
22+
i32.atomic.rmw8.cmpxchg_u
23+
drop
24+
i32.const 16
25+
i64.const -1
26+
i64.atomic.store32
27+
i32.const 16
28+
i64.atomic.load32_u
29+
atomic.fence))
30+
"#,
31+
)?;
32+
let module = tinywasm::parse_bytes(&wasm)?;
33+
let mut store = Store::default();
34+
let instance = ModuleInstance::instantiate(&mut store, &module, None)?;
35+
assert_eq!(instance.func::<(), i64>(&store, "run")?.call(&mut store, ())?, 0xffff_ffff);
36+
let memory = instance.memory("memory")?;
37+
assert_eq!(memory.read_vec(&store, 12, 1)?, [7]);
38+
Ok(())
39+
}
40+
41+
#[test]
42+
fn atomic_alignment_and_bounds() -> TestResult {
43+
let wasm = wat::parse_str(
44+
r#"
45+
(module (memory 1)
46+
(func (export "load") (param i32) (result i32)
47+
local.get 0
48+
i32.atomic.load offset=2))
49+
"#,
50+
)?;
51+
let module = tinywasm::parse_bytes(&wasm)?;
52+
let mut store = Store::default();
53+
let instance = ModuleInstance::instantiate(&mut store, &module, None)?;
54+
let load = instance.func::<i32, i32>(&store, "load")?;
55+
assert!(matches!(load.call(&mut store, 0), Err(Error::Trap(Trap::UnalignedAtomic))));
56+
assert!(matches!(load.call(&mut store, 65534), Err(Error::Trap(Trap::MemoryOutOfBounds { .. }))));
57+
58+
let invalid_alignment = wat::parse_str("(module (memory 1) (func i32.const 0 i32.atomic.load align=1 drop))")?;
59+
assert!(tinywasm::parse_bytes(&invalid_alignment).is_err());
60+
Ok(())
61+
}
62+
63+
#[test]
64+
fn atomic_rmw_operations_wrap_at_byte_width() -> TestResult {
65+
for (op, expected, updated) in [
66+
("add", 0xff, 1),
67+
("sub", 0xff, 0xfd),
68+
("and", 0xff, 2),
69+
("or", 0xff, 0xff),
70+
("xor", 0xff, 0xfd),
71+
("xchg", 0xff, 2),
72+
] {
73+
let wat = format!(
74+
r#"
75+
(module (memory 1)
76+
(func (export "run") (result i32)
77+
i32.const 0
78+
i32.const 255
79+
i32.atomic.store8
80+
i32.const 0
81+
i32.const 2
82+
i32.atomic.rmw8.{op}_u)
83+
(func (export "read") (result i32)
84+
i32.const 0
85+
i32.atomic.load8_u))
86+
"#
87+
);
88+
let module = tinywasm::parse_bytes(&wat::parse_str(&wat)?)?;
89+
let mut store = Store::default();
90+
let instance = ModuleInstance::instantiate(&mut store, &module, None)?;
91+
assert_eq!(instance.func::<(), i32>(&store, "run")?.call(&mut store, ())?, expected);
92+
assert_eq!(instance.func::<(), i32>(&store, "read")?.call(&mut store, ())?, updated);
93+
}
94+
Ok(())
95+
}
96+
97+
#[test]
98+
fn narrow_cmpxchg_truncates_expected_and_preserves_old_value() -> TestResult {
99+
let wasm = wat::parse_str(
100+
r#"
101+
(module
102+
(memory (export "memory") 1)
103+
(func (export "exchange") (param i64 i64) (result i64)
104+
i32.const 4
105+
local.get 0
106+
local.get 1
107+
i64.atomic.rmw32.cmpxchg_u))
108+
"#,
109+
)?;
110+
let module = tinywasm::parse_bytes(&wasm)?;
111+
let mut store = Store::default();
112+
let instance = ModuleInstance::instantiate(&mut store, &module, None)?;
113+
let memory = instance.memory("memory")?;
114+
let exchange = instance.func::<(i64, i64), i64>(&store, "exchange")?;
115+
memory.copy_from_slice(&mut store, 4, &0x89ab_cdef_u32.to_le_bytes())?;
116+
117+
assert_eq!(exchange.call(&mut store, (0x1234_89ab_cdef, 7))?, 0x89ab_cdef);
118+
assert_eq!(exchange.call(&mut store, (0x89ab_cdef, 9))?, 7);
119+
assert_eq!(memory.read_vec(&store, 4, 4)?, 7u32.to_le_bytes());
120+
Ok(())
121+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
0.9.0,357,262,[{"name":"atomic.wast","passed":48,"failed":249},{"name":"exports.wast","passed":82,"failed":6},{"name":"imports.wast","passed":147,"failed":5},{"name":"memory.wast","passed":80,"failed":2}]
22
0.10.0,357,262,[{"name":"atomic.wast","passed":48,"failed":249},{"name":"exports.wast","passed":82,"failed":6},{"name":"imports.wast","passed":147,"failed":5},{"name":"memory.wast","passed":80,"failed":2}]
3-
0.11.0-pre.0,357,262,[{"name":"atomic.wast","passed":48,"failed":249},{"name":"exports.wast","passed":82,"failed":6},{"name":"imports.wast","passed":147,"failed":5},{"name":"memory.wast","passed":80,"failed":2}]
3+
0.11.0,609,10,[{"name":"atomic.wast","passed":292,"failed":5},{"name":"exports.wast","passed":88,"failed":0},{"name":"imports.wast","passed":147,"failed":5},{"name":"memory.wast","passed":82,"failed":0}]

0 commit comments

Comments
 (0)