Files
wasm2go/testdata/cg_threads_m64.wat
0d0e628b3a Compose wasi-threads with memory64: addr64 atomics, shared-mem64 modules (#49)
The threads support and the memory64 support each worked alone but
were kept apart by two gates: shared memory64 modules were rejected
at translation, and every 0xFE atomic was rejected on a memory64
memory. A wasm64-wasip1-threads toolchain now exists, so compose
them.

- atomicEA64 mirrors simdEA64's overflow-guarded address math; every
  atomic op body moves into an ea-keyed core, the wasm32 helpers
  become one-line wrappers through atomicEA, and each gets an _m64
  twin through atomicEA64 (loads/stores/RMW families, wait/notify).
  Contended fast paths and memory orders are moved, not rewritten.
- The 0xFE lowering decodes the memarg offset as u64 under memory64
  (the one place still reading u32), passes it as Const64, and
  selects the _m64 helper names; the inline atomic fast path becomes
  width-correct instead of truncating.
- wasi_thread_spawn under memory64 passes the start_arg as i64 (it is
  a linear-memory pointer): threadSpawn splits into a shared launch
  core with per-width entries, and the module wires a threadStart64
  field for the widened wasi_thread_start export.
- memoryGrow64 gains the no-relocation branch for shared memories a
  grow must never move the backing array out from under peer agents.
  A shared memory64 requires a declared maximum: the reservation is
  the mapping ceiling, and the memory64 hard cap is not a ceiling.
- WasiStubs gains the trivial Sched_yield64 LP64 twin (musl's spin
  paths import sched_yield; single-threaded modules never did).
- New shared+i64 fixtures mirror the wasm32 threads fixtures and run
  under the race detector; an atomicEA64 unit test covers beyond-4GiB
  addresses, overflow and misalignment; a regression test pins that
  unshared memory64 modules may use atomics. Generated output for
  wasm32 modules is byte-identical.

Verified end to end: a wasm64-wasip1-threads C program
(pthread_create x4 over a shared counter) transpiles and runs
correctly on goroutines, including under -race; llama.cpp built for
wasm64-wasip1-threads decodes correctly with ggml n_threads
parallelism.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-22 00:31:05 +09:00

47 lines
2.3 KiB
WebAssembly Text Format

;; cg_threads_m64.wat — the shared-memory wasi_thread_spawn scenario of
;; cg_threads.wat on a memory64: i64 addressing throughout, an i64 start_arg
;; (a linear-memory pointer), and memory.size/grow in i64 pages. Exercises the
;; goroutine-backed spawn, cross-goroutine _m64 atomics on shared memory64,
;; and the wait/notify parking lot with 64-bit effective addresses.
(module
(import "wasi" "thread-spawn" (func $thread_spawn (param i64) (result i32)))
(memory (export "memory") i64 1 2 shared)
;; wasi-libc's thread entry: called on a fresh agent with (tid, arg).
;; arg is the address of the counter to bump — an i64 pointer here.
(func (export "wasi_thread_start") (param $tid i32) (param $arg i64)
(drop (i32.atomic.rmw.add (local.get $arg) (i32.const 1)))
(drop (memory.atomic.notify (local.get $arg) (i32.const -1))))
;; Spawn n agents on counter address 64 and spin-wait (with a bounded
;; atomic wait, so a lost notify shows up as a timeout, not a hang)
;; until the counter reaches n. Returns the final counter.
(func (export "spawn_and_join") (param $n i32) (result i32)
(local $i i32)
(i32.atomic.store (i64.const 64) (i32.const 0))
(block $spawned
(loop $spawn
(br_if $spawned (i32.ge_s (local.get $i) (local.get $n)))
(drop (call $thread_spawn (i64.const 64)))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $spawn)))
(block $done
(loop $wait
(br_if $done (i32.ge_s (i32.atomic.load (i64.const 64)) (local.get $n)))
;; wait while the counter still holds its current value (1s cap)
(drop (memory.atomic.wait32 (i64.const 64)
(i32.atomic.load (i64.const 64))
(i64.const 1000000000)))
(br $wait)))
(i32.atomic.load (i64.const 64)))
;; memory.grow on a shared memory must not relocate: the pointer other
;; agents deref stays valid. Returns new size in bytes plus the sentinel
;; written before the grow.
(func (export "grow_shared") (result i64)
(i32.atomic.store (i64.const 128) (i32.const 0xbeef))
(drop (memory.grow (i64.const 1)))
;; data written before the grow must still be there, and the new size visible
(i64.add (i64.mul (memory.size) (i64.const 65536))
(i64.extend_i32_u (i32.atomic.load (i64.const 128))))))