Files
el/lang/runtime/channel.el
T
2026-05-05 01:38:51 -05:00

165 lines
6.4 KiB
EmacsLisp

// channel.el Go-style channels for El
//
// Channels are the communication primitive for concurrent El programs.
// Threads send values into a channel; other threads receive them.
// Channels are typed by convention all values are Strings.
//
// Backed by four seed primitives in el_runtime.c:
// __channel_new(capacity) -> Int create channel; cap=0 = unbounded
// __channel_send(ch, msg) push msg; blocks if bounded and full
// __channel_recv(ch) -> String pop msg; blocks until available; "" on close
// __channel_try_recv(ch) -> String non-blocking pop; "" if empty
// __channel_close(ch) mark closed; wake all blocked recvers
//
// Usage:
// let ch: Int = channel_new(10) // buffered channel, capacity 10
// spawn("producer", int_to_str(ch))
// let msg: String = channel_recv(ch)
// Core channel API
// channel_new create a channel with the given buffer capacity.
//
// capacity: 0 = unbounded (never blocks sender)
// N = bounded buffer of N messages (sender blocks when full)
//
// Returns a channel handle (Int) to pass to send/recv/close.
fn channel_new(capacity: Int) -> Int {
return __channel_new(capacity)
}
// channel_send send a message into the channel.
//
// Blocks if the channel is bounded and full.
// No-op if the channel is already closed.
fn channel_send(ch: Int, msg: String) {
__channel_send(ch, msg)
}
// channel_recv receive the next message from the channel.
//
// Blocks until a message is available.
// Returns "" when the channel is closed and all buffered messages are drained.
// The "" sentinel signals end-of-stream to consumers in a loop.
fn channel_recv(ch: Int) -> String {
return __channel_recv(ch)
}
// channel_try_recv non-blocking receive.
//
// Returns the next message if one is available, or "" if the channel is empty.
// Does not block. Callers must distinguish "" (empty) from a legitimate ""
// message by convention use a non-empty sentinel in the message protocol.
fn channel_try_recv(ch: Int) -> String {
return __channel_try_recv(ch)
}
// channel_close signal that no more messages will be sent.
//
// After close, channel_recv continues to drain buffered messages then
// returns "" on every subsequent call. channel_send on a closed channel
// is a no-op (the message is dropped).
fn channel_close(ch: Int) {
__channel_close(ch)
}
// channel_pipeline
// channel_pipeline producer/consumer pipeline with parallel workers.
//
// Reads messages from in_ch, applies fn_name to each, writes results to out_ch.
// Spawns `workers` concurrent worker threads each drains in_ch independently,
// so messages are processed in arrival order within each worker but not globally.
//
// fn_name must be an El fn with signature (String) -> String.
//
// Call channel_close(in_ch) to signal EOF. Workers exit when they receive "".
// The caller must also close out_ch after all workers finish (via join).
//
// let in_ch: Int = channel_new(0)
// let out_ch: Int = channel_new(0)
// channel_pipeline(in_ch, out_ch, "process_item", 4)
// channel_send(in_ch, "work-1")
// channel_close(in_ch)
// let result: String = channel_recv(out_ch)
fn channel_pipeline(in_ch: Int, out_ch: Int, fn_name: String, workers: Int) {
let i: Int = 0
while i < workers {
let arg: String = "{\"in_ch\":" + int_to_str(in_ch) +
",\"out_ch\":" + int_to_str(out_ch) +
",\"fn\":\"" + fn_name + "\"}"
let _tid: Int = spawn("_channel_worker", arg)
let i = i + 1
}
}
// _channel_worker internal worker for channel_pipeline.
//
// Reads messages from in_ch until it receives "" (closed+empty), applies
// fn_name to each, and writes results to out_ch. Runs in its own thread
// (spawned by channel_pipeline).
fn _channel_worker(arg: String) -> String {
let in_ch: Int = str_to_int(json_get(arg, "in_ch"))
let out_ch: Int = str_to_int(json_get(arg, "out_ch"))
let fn_name: String = json_get(arg, "fn")
let running: Bool = true
while running {
let msg: String = channel_recv(in_ch)
if str_eq(msg, "") {
let running = false
} else {
// Spawn fn_name in a child thread so it cannot block the worker loop.
let tid: Int = spawn(fn_name, msg)
let result: String = join(tid)
channel_send(out_ch, result)
}
}
return ""
}
// channel_drain
// channel_drain collect all messages from ch into a list.
//
// Reads until the channel is closed and empty (recv returns "").
// Returns a [String] of all messages received.
//
// Typical usage: close the channel from the producer side, then call
// channel_drain from the consumer to collect results.
fn channel_drain(ch: Int) -> [String] {
let results: [String] = el_list_empty()
let running: Bool = true
while running {
let msg: String = channel_recv(ch)
if str_eq(msg, "") {
let running = false
} else {
let results = el_list_append(results, msg)
}
}
return results
}
// channel_fan_out
// channel_fan_out send every item in a list into a channel.
//
// items: [String] items to send
// ch: Int destination channel
//
// Sends all items then closes the channel to signal end-of-stream.
// Intended for the producer side of a pipeline:
//
// channel_fan_out(items, in_ch)
// let results: [String] = channel_drain(out_ch)
fn channel_fan_out(items: [String], ch: Int) {
let n: Int = el_list_len(items)
let i: Int = 0
while i < n {
let item: String = el_list_get(items, i)
channel_send(ch, item)
let i = i + 1
}
channel_close(ch)
}