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}