Skip to main content

logicaffeine_compile/codegen_tt/
concurrency_mesh.rs

1//! LOGOS **concurrency pattern RECOGNIZERS** — the source-reading half of the concurrency tier.
2//!
3//! A `Launch a task to producer with ch` + `Send v into ch` (in the task) + `Receive x from ch` (in main)
4//! is CSP dataflow, and this module is what READS that out of a LOGOS program: the pipe, the fan-out, the
5//! loop-driven producer, the select, and the two racing selects, each reduced to a spec of exactly what
6//! the source said. A non-matching program is DECLINED (the caller falls back to the sound refusal),
7//! never miscompiled.
8//!
9//! ★ THE EMITTERS ARE GONE; THE RECOGNIZERS ARE THE POINT. This module used to end each recognizer with a
10//! hand-written pair of baby-RISC-V kernels around a circular buffer — the producer pushing, the consumer
11//! waiting and popping, with the recognized values baked into the C++ as a `const uint32_t vals[N]`. Those
12//! kernels are now PRINTED FROM THE SPATIAL IR like every other lowering (`spatial::passes::from_tensor_op`
13//! builds the template, the Tensix printer emits it), because a pattern that is spelled out by hand is a
14//! claim about a kernel while a pattern that is lowered is a claim about a program: the same numbers, but
15//! reachable by every pass, the census and the evaluator instead of sealed inside text.
16//!
17//! ★ EVERY RESOLUTION HERE IS COMPILE-TIME, WHICH IS WHY THE IR STAYS A MARKED GRAPH. A select does not
18//! race: the VM's virtual-time scheduler fires whichever event has the smaller virtual time, and both
19//! times are literals of the source. So the compiler RESOLVES the arm and lowers the resolved program,
20//! rather than teaching the IR a free choice — which would cost the confluence that makes
21//! `eval_choreography_confluent` a proof rather than a spot-check, for every program sharing the IR.
22
23use logicaffeine_language::ast::stmt::{BinaryOpKind, Expr, Literal, Pattern, SelectBranch, Stmt};
24
25use crate::intern::{Interner, Symbol};
26
27/// What the concurrency tier produces for a recognized pattern: what the SOURCE said, and the spatial
28/// program it lowers to.
29///
30/// `program` is `None` when the compiler's compile-time resolution makes the answer a CONSTANT with no
31/// dataflow — a select whose timeout arm wins prints a fixed string, and a constant has no placement. The
32/// RESOLUTION is still a fact about the source, and it is what the ≡VM anchors check: the compiler and the
33/// VM's virtual-time scheduler must agree on which arm fires.
34#[derive(Debug, Clone)]
35pub struct MeshLowering<S> {
36    pub spec: S,
37    pub program: Option<crate::spatial::ir::SpatialProgram>,
38}
39
40/// A recognized producer/consumer pipe program: a task sends a fixed sequence of integers into a pipe, and
41/// main receives them and folds them (first increment: an integer sum) into a shown result.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ConcurrencySpec {
44    /// The task function that sends into the pipe.
45    pub producer: Symbol,
46    /// The integer values the task sends, in order.
47    pub sends: Vec<i64>,
48    /// How many values main receives (must equal `sends.len()` for a matched program).
49    pub receives: usize,
50}
51
52fn as_ident(e: &Expr) -> Option<Symbol> {
53    if let Expr::Identifier(s) = e {
54        Some(*s)
55    } else {
56        None
57    }
58}
59
60fn as_int(e: &Expr) -> Option<i64> {
61    if let Expr::Literal(Literal::Number(n)) = e {
62        Some(*n)
63    } else {
64        None
65    }
66}
67
68/// Collect the top-level function bodies and the "main" body from a parsed program.
69fn functions<'a>(
70    stmts: &'a [Stmt<'a>],
71    interner: &Interner,
72) -> (Vec<(Symbol, &'a [Stmt<'a>])>, Option<&'a [Stmt<'a>]>) {
73    let mut fns = Vec::new();
74    let mut main = None;
75    for s in stmts {
76        if let Stmt::FunctionDef { name, body, .. } = s {
77            if interner.resolve(*name).eq_ignore_ascii_case("main") {
78                main = Some(*body);
79            } else {
80                fns.push((*name, *body));
81            }
82        }
83    }
84    // Some front-ends leave `## Main` statements at the top level rather than in a `main` fn.
85    if main.is_none() {
86        main = Some(stmts);
87    }
88    (fns, main)
89}
90
91/// Recognize the producer/consumer pipe pattern and extract its `ConcurrencySpec`, or `None` if the program
92/// is not this shape (the caller then falls back to the sound single-core refusal).
93pub fn recognize_producer_consumer(stmts: &[Stmt], interner: &Interner) -> Option<ConcurrencySpec> {
94    let (fns, main) = functions(stmts, interner);
95    let main = main?;
96
97    // main must launch exactly one task and receive from a pipe.
98    let launched: Vec<Symbol> = main
99        .iter()
100        .filter_map(|s| match s {
101            Stmt::LaunchTask { function, .. } => Some(*function),
102            _ => None,
103        })
104        .collect();
105    if launched.len() != 1 {
106        return None;
107    }
108    let producer = launched[0];
109
110    let receives = main
111        .iter()
112        .filter(|s| matches!(s, Stmt::ReceivePipe { .. }))
113        .count();
114    if receives == 0 {
115        return None;
116    }
117
118    // The producer's body: a straight run of `Send <int literal> into ch`.
119    let body = fns.iter().find(|(n, _)| *n == producer).map(|(_, b)| *b)?;
120    let mut sends = Vec::new();
121    for s in body {
122        match s {
123            Stmt::SendPipe { value, .. } => sends.push(as_int(value)?), // non-literal ⇒ decline (increment 1)
124            // `Sleep` in a blocking producer is OUTPUT-IRRELEVANT (the receiver blocks and gets the value
125            // whenever it arrives), so we drop it — the mesh result stays ≡VM.
126            Stmt::Sleep { .. } => {}
127            // Ignore the parameter/setup noise a body may carry; anything ELSE that isn't a send declines.
128            Stmt::Return { value: None } => {}
129            _ => return None,
130        }
131    }
132    if sends.is_empty() || sends.len() != receives {
133        return None;
134    }
135
136    // main must fold the receives and show a result (we don't over-constrain the fold; the FIFO order +
137    // the shown value are what the mesh reproduces).
138    let shows = main.iter().any(|s| matches!(s, Stmt::Show { .. }));
139    let _ = shows; // a Show is expected but its target is validated by the ≡VM check, not structurally here.
140    let _ = as_ident; // (kept for the next increment: computed sends / handle plumbing)
141
142    Some(ConcurrencySpec {
143        producer,
144        sends,
145        receives,
146    })
147}
148
149/// A recognized **dynamic-channel** producer: `Repeat for i from lo to hi: Send <expr> into ch`. The pipe is
150/// fed by a LOOP, and each sent value is a (possibly computed) expression of the loop variable.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct DynamicSpec {
153    pub lo: i64,
154    pub hi: i64,
155    /// The values the loop actually sends, `f(lo)..=f(hi)`. The bounds and the body are literals of the
156    /// source, so the stream is known at compile time — which is what lets the loop lower to the same
157    /// spatial reduction as a static pipe instead of needing a device-side loop over an opaque expression.
158    pub values: Vec<i64>,
159    /// The C expression for the sent value, in terms of the loop variable `i`.
160    pub send_expr_c: String,
161}
162
163/// Lower a simple integer expression over the loop variable to C (the loop variable becomes `i`). Handles
164/// literals, the loop variable, and `+ - * / %` — enough for computed send values like `i * i`, `2 * i + 1`.
165fn expr_to_c(e: &Expr, loop_var: Symbol) -> Option<String> {
166    match e {
167        Expr::Literal(Literal::Number(n)) => Some(n.to_string()),
168        Expr::Identifier(s) if *s == loop_var => Some("i".to_string()),
169        Expr::BinaryOp { op, left, right } => {
170            let l = expr_to_c(left, loop_var)?;
171            let r = expr_to_c(right, loop_var)?;
172            let o = match op {
173                BinaryOpKind::Add => "+",
174                BinaryOpKind::Subtract => "-",
175                BinaryOpKind::Multiply => "*",
176                BinaryOpKind::Modulo => "%",
177                BinaryOpKind::Divide | BinaryOpKind::FloorDivide => "/",
178                _ => return None,
179            };
180            Some(format!("({l} {o} {r})"))
181        }
182        _ => None,
183    }
184}
185
186/// [`expr_to_c`]'s twin: the same expression evaluated at `i`, so the compiler knows the STREAM and not
187/// only its printed form. Kept adjacent on purpose — an arm added to one and not the other is a send whose
188/// emitted text and lowered value disagree.
189fn expr_eval(e: &Expr, loop_var: Symbol, i: i64) -> Option<i64> {
190    match e {
191        Expr::Literal(Literal::Number(n)) => n.to_string().parse::<i64>().ok(),
192        Expr::Identifier(s) if *s == loop_var => Some(i),
193        Expr::BinaryOp { op, left, right } => {
194            let l = expr_eval(left, loop_var, i)?;
195            let r = expr_eval(right, loop_var, i)?;
196            match op {
197                BinaryOpKind::Add => l.checked_add(r),
198                BinaryOpKind::Subtract => l.checked_sub(r),
199                BinaryOpKind::Multiply => l.checked_mul(r),
200                BinaryOpKind::Modulo => (r != 0).then(|| l.wrapping_rem(r)),
201                BinaryOpKind::Divide | BinaryOpKind::FloorDivide => {
202                    (r != 0).then(|| l.div_euclid(r))
203                }
204                _ => None,
205            }
206        }
207        _ => None,
208    }
209}
210
211/// Recognize a loop-driven producer: `main` launches a task whose body is a single
212/// `Repeat for i from lo to hi: Send <expr> into ch`. Extracts the bounds + the C send expression.
213pub fn recognize_dynamic_producer(stmts: &[Stmt], interner: &Interner) -> Option<DynamicSpec> {
214    let _ = interner;
215    let (fns, main) = functions(stmts, interner);
216    let main = main?;
217    let producer = main.iter().find_map(|s| match s {
218        Stmt::LaunchTask { function, .. } => Some(*function),
219        _ => None,
220    })?;
221    let body = fns.iter().find(|(n, _)| *n == producer).map(|(_, b)| *b)?;
222    // The producer body must be a single counted `Repeat` loop (no static sends — that is increment 1).
223    let (pattern, iterable, loop_body) = body.iter().find_map(|s| match s {
224        Stmt::Repeat {
225            pattern,
226            iterable,
227            body,
228        } => Some((pattern, *iterable, *body)),
229        _ => None,
230    })?;
231    let iv = match pattern {
232        Pattern::Identifier(s) => *s,
233        _ => return None,
234    };
235    let (lo, hi) = match iterable {
236        Expr::Range { start, end } => (as_int(start)?, as_int(end)?),
237        _ => return None,
238    };
239    let send_expr = loop_body.iter().find_map(|s| match s {
240        Stmt::SendPipe { value, .. } => Some(*value),
241        _ => None,
242    })?;
243    let send_expr_c = expr_to_c(send_expr, iv)?;
244    let values: Vec<i64> = (lo..=hi)
245        .map(|i| expr_eval(send_expr, iv, i))
246        .collect::<Option<_>>()?;
247    Some(DynamicSpec {
248        lo,
249        hi,
250        values,
251        send_expr_c,
252    })
253}
254
255/// A recognized **racy select**: a task sleeps then sends; main awaits the first of {receive, timeout}. The
256/// VM's deterministic scheduler fires the smaller-virtual-time event, so the outcome is a COMPARISON of the
257/// send's virtual time (the task's cumulative sleep) against the timeout — reproducible on the mesh by a
258/// coordinator core, independent of real hardware timing.
259#[derive(Debug, Clone, PartialEq, Eq)]
260pub struct RacySelectSpec {
261    /// The value the task sends.
262    pub value: i64,
263    /// The send's virtual time = the task's cumulative sleep, in milliseconds.
264    pub send_vtime_ms: i64,
265    /// The select's timeout, in milliseconds.
266    pub timeout_ms: i64,
267}
268
269/// A duration literal in milliseconds: a `Duration` (nanos) is divided down; a bare number is taken as ms.
270fn duration_ms(e: &Expr) -> Option<i64> {
271    match e {
272        Expr::Literal(Literal::Duration(nanos)) => Some(nanos / 1_000_000),
273        Expr::Literal(Literal::Number(n)) => Some(*n),
274        _ => None,
275    }
276}
277
278/// Recognize a racy select: main launches a task (which sleeps then sends one literal) and awaits the first
279/// of a receive + a timeout, with NO pre-send in main. Extracts the send's virtual time + the timeout.
280pub fn recognize_racy_select(stmts: &[Stmt], interner: &Interner) -> Option<RacySelectSpec> {
281    let (fns, main) = functions(stmts, interner);
282    let main = main?;
283    // A pre-send would make this the READY case (recognize_select), not the racy case.
284    if main.iter().any(|s| matches!(s, Stmt::SendPipe { .. })) {
285        return None;
286    }
287    let producer = main.iter().find_map(|s| match s {
288        Stmt::LaunchTask { function, .. } => Some(*function),
289        _ => None,
290    })?;
291    let branches = main.iter().find_map(|s| match s {
292        Stmt::Select { branches } => Some(branches),
293        _ => None,
294    })?;
295    if !branches
296        .iter()
297        .any(|b| matches!(b, SelectBranch::Receive { .. }))
298    {
299        return None;
300    }
301    let timeout_ms = branches.iter().find_map(|b| match b {
302        SelectBranch::Timeout { milliseconds, .. } => duration_ms(milliseconds),
303        _ => None,
304    })?;
305
306    // The task: sum its sleeps (virtual time) + find the single sent literal.
307    let body = fns.iter().find(|(n, _)| *n == producer).map(|(_, b)| *b)?;
308    let mut send_vtime_ms = 0i64;
309    let mut value = None;
310    for s in body {
311        match s {
312            Stmt::Sleep { milliseconds } => send_vtime_ms += duration_ms(milliseconds)?,
313            Stmt::SendPipe { value: v, .. } => value = Some(as_int(v)?),
314            Stmt::Return { value: None } => {}
315            _ => return None,
316        }
317    }
318    Some(RacySelectSpec {
319        value: value?,
320        send_vtime_ms,
321        timeout_ms,
322    })
323}
324
325/// A recognized **multi-task race**: N tasks each sleep then send; main awaits receive-or-timeout. The
326/// deterministic scheduler fires the global-minimum-virtual-time event.
327#[derive(Debug, Clone, PartialEq, Eq)]
328pub struct RacyMultiSpec {
329    /// `(value, send_vtime_ms)` per racing task, in launch order.
330    pub tasks: Vec<(i64, i64)>,
331    /// The timeout arm's virtual time, in milliseconds.
332    pub timeout_ms: i64,
333}
334
335/// Recognize a multi-task race: main launches ≥2 tasks (each sleeps then sends one literal, no pre-send in
336/// main) and awaits the first of a receive + a timeout. Extracts each task's `(value, send-vtime)` + timeout.
337pub fn recognize_multi_racy(stmts: &[Stmt], interner: &Interner) -> Option<RacyMultiSpec> {
338    let (fns, main) = functions(stmts, interner);
339    let main = main?;
340    if main.iter().any(|s| matches!(s, Stmt::SendPipe { .. })) {
341        return None;
342    }
343    let launched: Vec<Symbol> = main
344        .iter()
345        .filter_map(|s| match s {
346            Stmt::LaunchTask { function, .. } => Some(*function),
347            _ => None,
348        })
349        .collect();
350    if launched.len() < 2 {
351        return None; // one task is the single-task racy case
352    }
353    let branches = main.iter().find_map(|s| match s {
354        Stmt::Select { branches } => Some(branches),
355        _ => None,
356    })?;
357    if !branches
358        .iter()
359        .any(|b| matches!(b, SelectBranch::Receive { .. }))
360    {
361        return None;
362    }
363    let timeout_ms = branches.iter().find_map(|b| match b {
364        SelectBranch::Timeout { milliseconds, .. } => duration_ms(milliseconds),
365        _ => None,
366    })?;
367
368    let mut tasks = Vec::new();
369    for name in &launched {
370        let body = fns.iter().find(|(n, _)| n == name).map(|(_, b)| *b)?;
371        let mut vtime = 0i64;
372        let mut value = None;
373        for s in body {
374            match s {
375                Stmt::Sleep { milliseconds } => vtime += duration_ms(milliseconds)?,
376                Stmt::SendPipe { value: v, .. } => value = Some(as_int(v)?),
377                Stmt::Return { value: None } => {}
378                _ => return None,
379            }
380        }
381        tasks.push((value?, vtime));
382    }
383    Some(RacyMultiSpec { tasks, timeout_ms })
384}
385
386/// A recognized **select** (`Await the first of { Receive … / After … }`) on a READY channel: main
387/// pre-sends `presend` into the pipe, so the deterministic scheduler takes the RECEIVE arm; `timeout_ticks`
388/// is the (dead-on-this-path) timeout duration.
389#[derive(Debug, Clone, PartialEq, Eq)]
390pub struct SelectSpec {
391    /// The value main sends before the select — makes the channel ready.
392    pub presend: i64,
393    /// The timeout arm's duration literal (unused on the ready path; kept for the emitted fallback arm).
394    pub timeout_ticks: i64,
395}
396
397/// Recognize a select on a ready channel: `Send v into ch` then `Await the first of { Receive x from ch: …;
398/// After n …: … }`. Returns the spec, or `None` if not this shape.
399pub fn recognize_select(stmts: &[Stmt], interner: &Interner) -> Option<SelectSpec> {
400    let (_fns, main) = functions(stmts, interner);
401    let main = main?;
402
403    // The pre-send that makes the channel ready.
404    let presend = main.iter().find_map(|s| match s {
405        Stmt::SendPipe { value, .. } => as_int(value),
406        _ => None,
407    })?;
408
409    // The select must have a Receive arm and a Timeout arm.
410    let branches = main.iter().find_map(|s| match s {
411        Stmt::Select { branches } => Some(branches),
412        _ => None,
413    })?;
414    let has_receive = branches
415        .iter()
416        .any(|b| matches!(b, SelectBranch::Receive { .. }));
417    let timeout_ticks = branches.iter().find_map(|b| match b {
418        SelectBranch::Timeout { milliseconds, .. } => match milliseconds {
419            Expr::Literal(Literal::Number(n)) => Some(*n),
420            Expr::Literal(Literal::Duration(nanos)) => Some((nanos / 1_000_000_000).max(1)),
421            _ => Some(1),
422        },
423        _ => None,
424    })?;
425    if !has_receive {
426        return None;
427    }
428    Some(SelectSpec {
429        presend,
430        timeout_ticks,
431    })
432}
433
434/// A recognized **fan-out**: N distinct tasks each send one value into a shared pipe, and main receives all
435/// N and folds them (commutative sum — so the mesh's arrival order doesn't matter and it stays ≡VM).
436#[derive(Debug, Clone, PartialEq, Eq)]
437pub struct FanoutSpec {
438    /// The value each task sends, one per task (task `i` runs on its own core and contributes `values[i]`).
439    pub values: Vec<i64>,
440}
441
442/// Recognize the fan-out pattern: `main` launches N (≥2) distinct tasks, each of which sends exactly one
443/// integer literal into a shared pipe, and `main` receives N values. `None` if the program is not this shape.
444pub fn recognize_fanout(stmts: &[Stmt], interner: &Interner) -> Option<FanoutSpec> {
445    let (fns, main) = functions(stmts, interner);
446    let main = main?;
447
448    // The launched task functions, in launch order.
449    let launched: Vec<Symbol> = main
450        .iter()
451        .filter_map(|s| match s {
452            Stmt::LaunchTask { function, .. } => Some(*function),
453            _ => None,
454        })
455        .collect();
456    if launched.len() < 2 {
457        return None; // one task is the producer/consumer case, not fan-out
458    }
459
460    // Each launched task must send exactly one integer literal into the pipe.
461    let mut values = Vec::new();
462    for name in &launched {
463        let body = fns.iter().find(|(n, _)| n == name).map(|(_, b)| *b)?;
464        let sends: Vec<i64> = body
465            .iter()
466            .filter_map(|s| match s {
467                Stmt::SendPipe { value, .. } => as_int(value),
468                _ => None,
469            })
470            .collect();
471        // exactly one send, and nothing but sends/returns in the body (increment 2 keeps it simple).
472        let only_sends_or_ret = body
473            .iter()
474            .all(|s| matches!(s, Stmt::SendPipe { .. } | Stmt::Return { value: None }));
475        if sends.len() != 1 || !only_sends_or_ret {
476            return None;
477        }
478        values.push(sends[0]);
479    }
480
481    // main must receive one value per task.
482    let receives = main
483        .iter()
484        .filter(|s| matches!(s, Stmt::ReceivePipe { .. }))
485        .count();
486    if receives != values.len() {
487        return None;
488    }
489    let _ = as_ident;
490    Some(FanoutSpec { values })
491}