Skip to main content

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;