seq_runtime/channel.rs
1//! Channel operations for CSP-style concurrency.
2//!
3//! Channels are the primary communication mechanism between strands.
4//! They use May's MPMC channels with cooperative blocking.
5//!
6//! ## Zero-Mutex Design
7//!
8//! Channels are passed directly as `Value::Channel` on the stack. There is NO
9//! global registry and NO mutex contention. Send/receive operations work
10//! directly on the channel handles. The `closed` flag is a single atomic
11//! load on the send hot path; no locking.
12//!
13//! ## Non-Blocking Guarantee
14//!
15//! All channel operations (`send`, `receive`) cooperatively block using May's
16//! scheduler. They NEVER block OS threads — May handles scheduling other
17//! strands while waiting.
18//!
19//! ## Multi-Consumer Support
20//!
21//! Channels support multiple producers AND multiple consumers (MPMC). Each
22//! message is delivered to exactly one receiver (work-stealing semantics).
23//!
24//! ## `chan.close` Semantics
25//!
26//! Issue #499: `chan.close` is real, not "equivalent to drop". The
27//! implementation uses the same typed-sentinel pattern as `WeaveChannelData`
28//! — see `crates/core/src/value.rs::ChannelMsg` and
29//! `docs/design/CHAN_CLOSE_SEMANTICS.md`.
30//!
31//! - `chan.close` atomically sets a shared `closed` flag (CAS) and, on the
32//! first close, enqueues a single `ChannelMsg::Closed` sentinel.
33//! - `chan.send` short-circuits to `false` when the flag is set.
34//! - `chan.receive` returns `( value true )` on `ChannelMsg::Value`. On
35//! `ChannelMsg::Closed` it re-broadcasts the sentinel (so the next blocked
36//! receiver also wakes — Go-style propagation across an unknown number of
37//! MPMC consumers) and returns `( default false )`.
38//!
39//! The user-facing API is unchanged: programs still write `Channel`, still
40//! call `chan.make`/`chan.send`/`chan.receive`/`chan.close`, still see the
41//! `( value Bool )` success-flag shape.
42//!
43//! ## Stack Effects
44//!
45//! - `chan.make`: ( -- Channel )
46//! - `chan.send`: ( value Channel -- Bool ) consumes the channel
47//! - `chan.receive`: ( Channel -- value Bool ) consumes the channel
48//! - `chan.close`: ( Channel -- ) consumes the channel
49
50use crate::stack::{Stack, pop, push};
51use crate::value::{ChannelData, ChannelMsg, Value};
52use may::sync::mpmc;
53use std::sync::Arc;
54use std::sync::atomic::{AtomicBool, Ordering};
55
56#[cfg(feature = "diagnostics")]
57use std::sync::atomic::AtomicU64;
58
59#[cfg(feature = "diagnostics")]
60pub static TOTAL_MESSAGES_SENT: AtomicU64 = AtomicU64::new(0);
61#[cfg(feature = "diagnostics")]
62pub static TOTAL_MESSAGES_RECEIVED: AtomicU64 = AtomicU64::new(0);
63
64/// Create a new channel.
65///
66/// Stack effect: ( -- Channel )
67///
68/// Returns a Channel value that can be used with send/receive operations.
69/// The channel can be duplicated (`dup`) to share between strands; each
70/// clone shares the same underlying `mpmc` queue and closed flag.
71///
72/// # Safety
73/// Always safe to call.
74#[unsafe(no_mangle)]
75pub unsafe extern "C" fn patch_seq_make_channel(stack: Stack) -> Stack {
76 let (sender, receiver) = mpmc::channel::<ChannelMsg>();
77 let channel = Arc::new(ChannelData {
78 sender,
79 receiver,
80 closed: Arc::new(AtomicBool::new(false)),
81 });
82 unsafe { push(stack, Value::Channel(channel)) }
83}
84
85/// Close a channel.
86///
87/// Stack effect: ( Channel -- )
88///
89/// Atomically marks the channel closed (idempotent across multiple
90/// `chan.close` callers via CAS) and, on the first close, enqueues one
91/// `ChannelMsg::Closed` sentinel. Any blocked `chan.receive` calls wake
92/// up: the first one consumes the sentinel and re-broadcasts it,
93/// propagating the close through the MPMC fan-out lazily as each
94/// blocked receiver is scheduled.
95///
96/// # Safety
97/// Stack must have a Channel on top.
98#[unsafe(no_mangle)]
99pub unsafe extern "C" fn patch_seq_close_channel(stack: Stack) -> Stack {
100 assert!(!stack.is_null(), "chan.close: stack is empty");
101
102 let (rest, channel_value) = unsafe { pop(stack) };
103 let channel = match channel_value {
104 Value::Channel(ch) => ch,
105 other => panic!("chan.close: expected Channel on stack, got {:?}", other),
106 };
107
108 // First-to-close wins the CAS and is responsible for enqueueing the
109 // sentinel. Subsequent closes are no-ops (idempotent).
110 if channel
111 .closed
112 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
113 .is_ok()
114 {
115 // Best-effort: if the queue is somehow broken, there's nothing
116 // meaningful to do — the closed flag is the durable signal that
117 // chan.send checks; chan.receive will also see Err from recv()
118 // when every Arc<ChannelData> drops.
119 let _ = channel.sender.send(ChannelMsg::Closed);
120 }
121
122 rest
123}
124
125/// Send a value through a channel.
126///
127/// Stack effect: ( value Channel -- Bool )
128///
129/// Returns `true` on success, `false` if the channel is closed (either via
130/// `chan.close` or because every receiver has been dropped).
131///
132/// # Safety
133/// Stack must have a Channel on top and a value below it.
134#[unsafe(no_mangle)]
135pub unsafe extern "C" fn patch_seq_chan_send(stack: Stack) -> Stack {
136 assert!(!stack.is_null(), "chan.send: stack is empty");
137
138 let (stack, channel_value) = unsafe { pop(stack) };
139 let channel = match channel_value {
140 Value::Channel(ch) => ch,
141 _ => {
142 // Wrong type — consume value (if any) and return failure.
143 if !stack.is_null() {
144 let (rest, _value) = unsafe { pop(stack) };
145 return unsafe { push(rest, Value::Bool(false)) };
146 }
147 return unsafe { push(stack, Value::Bool(false)) };
148 }
149 };
150
151 if stack.is_null() {
152 return unsafe { push(stack, Value::Bool(false)) };
153 }
154
155 let (rest, value) = unsafe { pop(stack) };
156
157 // Closed gate: short-circuit on the fast path so user code reliably
158 // sees `false` after close.
159 if channel.closed.load(Ordering::Acquire) {
160 return unsafe { push(rest, Value::Bool(false)) };
161 }
162
163 let global_value = value.clone();
164 match channel.sender.send(ChannelMsg::Value(global_value)) {
165 Ok(()) => {
166 #[cfg(feature = "diagnostics")]
167 TOTAL_MESSAGES_SENT.fetch_add(1, Ordering::Relaxed);
168 unsafe { push(rest, Value::Bool(true)) }
169 }
170 Err(_) => unsafe { push(rest, Value::Bool(false)) },
171 }
172}
173
174/// Receive a value from a channel.
175///
176/// Stack effect: ( Channel -- value Bool )
177///
178/// Blocks cooperatively until a value arrives or the channel is closed and
179/// drained. Returns `( value true )` on success, `( Int(0) false )` on
180/// close. The closed-sentinel is re-broadcast before returning so other
181/// blocked receivers wake up too — see module doc.
182///
183/// # Safety
184/// Stack must have a Channel on top.
185#[unsafe(no_mangle)]
186pub unsafe extern "C" fn patch_seq_chan_receive(stack: Stack) -> Stack {
187 assert!(!stack.is_null(), "chan.receive: stack is empty");
188
189 let (rest, channel_value) = unsafe { pop(stack) };
190 let channel = match channel_value {
191 Value::Channel(ch) => ch,
192 _ => {
193 let stack = unsafe { push(rest, Value::Int(0)) };
194 return unsafe { push(stack, Value::Bool(false)) };
195 }
196 };
197
198 match channel.receiver.recv() {
199 Ok(ChannelMsg::Value(value)) => {
200 #[cfg(feature = "diagnostics")]
201 TOTAL_MESSAGES_RECEIVED.fetch_add(1, Ordering::Relaxed);
202 let stack = unsafe { push(rest, value) };
203 unsafe { push(stack, Value::Bool(true)) }
204 }
205 Ok(ChannelMsg::Closed) => {
206 // Propagate the close to other waiters. Ignore the result —
207 // the queue may be in any state; the closed flag is the
208 // durable signal, this is just a wake-up nudge.
209 let _ = channel.sender.send(ChannelMsg::Closed);
210 let stack = unsafe { push(rest, Value::Int(0)) };
211 unsafe { push(stack, Value::Bool(false)) }
212 }
213 Err(_) => {
214 // All Arc<ChannelData> instances dropped — every sender clone
215 // and every receiver clone is gone. Treat as closed.
216 let stack = unsafe { push(rest, Value::Int(0)) };
217 unsafe { push(stack, Value::Bool(false)) }
218 }
219 }
220}
221
222// Public re-exports with short names for internal use
223pub use patch_seq_chan_receive as receive;
224pub use patch_seq_chan_send as send;
225pub use patch_seq_close_channel as close_channel;
226pub use patch_seq_make_channel as make_channel;
227
228#[cfg(test)]
229mod tests;