code wiki / (root) / channel.nx

channel.nx source

↩ module page · 118 lines · 4073 B

1// channel.nx -- typed message-passing primitive (Phase M1 seed). 2// 3// Research: Honda 1993 ("Types for Dyadic Interaction"), Pony 4// actor-channel model, Go channels with session-type extensions. 5// The long-term destination is full session types (Honda 1998) 6// where the channel's type describes the entire protocol; this 7// file ships the foundation on which session-type checking will 8// stack in a future parse.nx pass. 9// 10// Today's capabilities: 11// * Typed FIFO channel with bounded capacity 12// * Non-blocking try_send / try_recv 13// * Blocking send / recv (spin-on-empty/full; MCU-friendly) 14// * Single-producer single-consumer (SPSC); MPSC / MPMC variants 15// ship in a follow-up once we have atomic primitives 16// 17// Currently specialised to i64 message type; generalises to Chan<T> 18// when full multi-parameter generics land in parse.nx. 19// 20// Scaling claim: same channel.nx source compiles on MCU (no kernel 21// threads, cooperative yield via __wfi between poll attempts) and 22// on supercomputer (the scheduler's context switch uses the same 23// primitive). Nothing about the API changes. 24 25import "syscalls.nx" 26 27// Ring-buffer backed channel. All SPSC ordering is maintained by 28// the head/tail indices; atomicity comes later via memory fences 29// (`__fence()`) once we run on multi-core. 30struct Chan { 31 buf: *i64, // ring storage; `cap` i64 slots 32 cap: i64, // capacity (must be power of 2 for index wrap) 33 head: i64, // next write position 34 tail: i64, // next read position 35} 36 37// Allocate a new channel with `cap` slots. cap is rounded up to the 38// next power of 2 so the head/tail wrap is a single AND mask instead 39// of a modulo. 40func chan_new(cap_hint: i64) -> *Chan { 41 var cap: i64 = 1 42 while cap < cap_hint { cap = cap * 2 } 43 if cap < 2 { cap = 2 } 44 let raw: *u8 = sys_mmap(64 + cap * 8 + 16) 45 let c: *Chan = raw as *Chan 46 c.buf = (raw as i64 + 64) as *i64 47 c.cap = cap 48 c.head = 0 49 c.tail = 0 50 return c 51} 52 53// Returns 1 if the channel is empty right now. 54func chan_is_empty(c: *Chan) -> i64 { 55 if c.head == c.tail { return 1 } 56 return 0 57} 58 59// Returns 1 if the channel is full right now. Full = one slot open 60// stays reserved so we can distinguish full from empty using only 61// head/tail. 62func chan_is_full(c: *Chan) -> i64 { 63 let next: i64 = (c.head + 1) & (c.cap - 1) 64 if next == c.tail { return 1 } 65 return 0 66} 67 68// Non-blocking send. Returns 1 on success, 0 when channel was full. 69func chan_try_send(c: *Chan, v: i64) -> i64 { 70 if chan_is_full(c) == 1 { return 0 } 71 c.buf[c.head] = v 72 // Memory fence so the store is visible before head advances -- 73 // matters on multi-core; no-op on single-hart. 74 __fence() 75 c.head = (c.head + 1) & (c.cap - 1) 76 return 1 77} 78 79// Non-blocking receive. Returns 1 and writes value to *out on 80// success; returns 0 when channel was empty (*out untouched). 81func chan_try_recv(c: *Chan, out: *i64) -> i64 { 82 if chan_is_empty(c) == 1 { return 0 } 83 *out = c.buf[c.tail] 84 __fence() 85 c.tail = (c.tail + 1) & (c.cap - 1) 86 return 1 87} 88 89// Blocking send -- spins until space. On MCU / kernel contexts a 90// `__wfi()` yields the hart between attempts. On multi-threaded 91// userspace the caller should integrate with the scheduler rather 92// than busy-looping. 93func chan_send(c: *Chan, v: i64) -> i64 { 94 var sent: i64 = 0 95 while sent == 0 { 96 sent = chan_try_send(c, v) 97 if sent == 0 { __wfi() } 98 } 99 return 0 100} 101 102// Blocking receive -- spins until a message arrives. 103func chan_recv(c: *Chan) -> i64 { 104 let out_raw: *u8 = sys_mmap(16) 105 let out: *i64 = out_raw as *i64 106 *out = 0 107 var got: i64 = 0 108 while got == 0 { 109 got = chan_try_recv(c, out) 110 if got == 0 { __wfi() } 111 } 112 return *out 113} 114 115// Returns current message count (head - tail, wrapped to capacity). 116func chan_len(c: *Chan) -> i64 { 117 return (c.head - c.tail) & (c.cap - 1) 118}