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}