Skip to main content

logicaffeine_compile/tensor_graph/
front.rs

1//! The front end of the one IR: the **whole-program walk** — every tensor statement of a program lifted, in order,
2//! into a typed spec over the shared recognizers, with its dataflow and its carried state named. Every tensor backend
3//! enters here (the CUDA lane, the older forward, the spatial pipeline, the D5005 engine); it is also reached by its
4//! older path, `spatial::passes::recognize_stages`.
5//!
6//! `recognize_any` in [`crate::spatial::pipeline`] lifts ONE op out of a source; an authored forward is a hundred.
7//! This walk reads the program's top-level statements one at a time (each tensor sugar desugars to one
8//! `Stmt::Splice`, so a statement is an op) and tries the recognizers in [`RECOGNIZER_ORDER`] — the CUDA
9//! orchestrator's order, composite blocks first so their inner loops are never claimed by a simpler shape.
10//! What it cannot lift it RETURNS: `unrecognized` lists the statement by index and binder, so a composition
11//! refuses by name and a ledger can print the gap. Nothing is skipped silently.
12//!
13//! The result names everything by variable: [`links`] is the dataflow (the producer stage of every tensor
14//! and the stages that read it) and [`state_tensors`] the carried state (what a step reads AND writes in
15//! place — the conv rings, the GDN memories; the KV cache joins through the masked attention form). These
16//! are the inputs the section composer takes; the lowering of each spec is the composer's other half.
17
18use std::collections::{BTreeMap, BTreeSet, HashMap};
19
20use logicaffeine_language::ast::stmt::{Expr, Literal, Stmt};
21
22use crate::spatial::passes::SpatialReject;
23use crate::intern::{Interner, Symbol};
24use crate::tensor_recognize::qwen::{
25    recognize_argmax, recognize_attention, recognize_causal_attention, recognize_causal_conv,
26    recognize_decay, recognize_full_attention, recognize_gated_delta, recognize_gelu, recognize_head_slice,
27    recognize_int_clamp, recognize_int_requant, recognize_join, recognize_l2norm, recognize_lut_attention, recognize_moe,
28    recognize_relu, recognize_rope, recognize_row_gather, recognize_rows_gather, recognize_scale, recognize_set_row,
29    recognize_sigmoid, recognize_silu, recognize_softcap, recognize_transpose, ArgMaxSpec, AttentionSpec, CausalConvSpec,
30    DecaySpec,
31    FullAttentionSpec, GatedDeltaSpec, GeluSpec, HeadSliceSpec, IntClampSpec, IntRequantSpec, JoinSpec, L2NormSpec,
32    LutAttentionSpec, MoeSpec, ReluSpec, RopeSpec, RowGatherSpec, RowIndex, RowsGatherSpec, ScaleSpec, SetRowSpec,
33    SigmoidSpec, SiluSpec, SoftcapSpec, TransposeSpec,
34};
35use crate::tensor_recognize::{
36    recognize_elementwise, recognize_fused_row, recognize_head_matmul, recognize_matmul, Fills,
37    recognize_swiglu, ElementwiseSpec, Fill, FusedRowSpec, HeadMatmulSpec, MatmulSpec, SwiGluSpec,
38};
39
40/// One recognized tensor op — the typed spec the shared recognizers lift. The variant set is the language's
41/// tensor sugar; the lowering of each to a spatial template lives with the composer.
42#[derive(Clone, Debug)]
43pub enum TensorOpSpec {
44    Matmul(MatmulSpec),
45    Elementwise(ElementwiseSpec),
46    SwiGlu(SwiGluSpec),
47    FusedRow(FusedRowSpec),
48    Rope(RopeSpec),
49    Silu(SiluSpec),
50    Sigmoid(SigmoidSpec),
51    Gelu(GeluSpec),
52    Scale(ScaleSpec),
53    Softcap(SoftcapSpec),
54    Decay(DecaySpec),
55    L2Norm(L2NormSpec),
56    Transpose(TransposeSpec),
57    HeadSlice(HeadSliceSpec),
58    Join(JoinSpec),
59    HeadMatmul(HeadMatmulSpec),
60    CausalConv(CausalConvSpec),
61    GatedDelta(GatedDeltaSpec),
62    Attention(AttentionSpec),
63    FullAttention(FullAttentionSpec),
64    Moe(MoeSpec),
65    RowGather(RowGatherSpec),
66    RowsGather(RowsGatherSpec),
67    ArgMax(ArgMaxSpec),
68    SetRow(SetRowSpec),
69    IntRequant(IntRequantSpec),
70    IntClamp(IntClampSpec),
71    Relu(ReluSpec),
72    LutAttention(LutAttentionSpec),
73}
74
75impl TensorOpSpec {
76    /// The variant's name — the kind a ledger or a schedule reads.
77    pub fn kind(&self) -> &'static str {
78        match self {
79            TensorOpSpec::Matmul(_) => "Matmul",
80            TensorOpSpec::Elementwise(_) => "Elementwise",
81            TensorOpSpec::SwiGlu(_) => "SwiGlu",
82            TensorOpSpec::FusedRow(_) => "FusedRow",
83            TensorOpSpec::Rope(_) => "Rope",
84            TensorOpSpec::Silu(_) => "Silu",
85            TensorOpSpec::Sigmoid(_) => "Sigmoid",
86            TensorOpSpec::Gelu(_) => "Gelu",
87            TensorOpSpec::Scale(_) => "Scale",
88            TensorOpSpec::Softcap(_) => "Softcap",
89            TensorOpSpec::Decay(_) => "Decay",
90            TensorOpSpec::L2Norm(_) => "L2Norm",
91            TensorOpSpec::Transpose(_) => "Transpose",
92            TensorOpSpec::HeadSlice(_) => "HeadSlice",
93            TensorOpSpec::Join(_) => "Join",
94            TensorOpSpec::HeadMatmul(_) => "HeadMatmul",
95            TensorOpSpec::CausalConv(_) => "CausalConv",
96            TensorOpSpec::GatedDelta(_) => "GatedDelta",
97            TensorOpSpec::Attention(_) => "Attention",
98            TensorOpSpec::FullAttention(_) => "FullAttention",
99            TensorOpSpec::Moe(_) => "Moe",
100            TensorOpSpec::RowGather(_) => "RowGather",
101            TensorOpSpec::RowsGather(_) => "RowsGather",
102            TensorOpSpec::ArgMax(_) => "ArgMax",
103            TensorOpSpec::SetRow(_) => "SetRow",
104            TensorOpSpec::IntRequant(_) => "IntRequant",
105            TensorOpSpec::IntClamp(_) => "IntClamp",
106            TensorOpSpec::Relu(_) => "Relu",
107            TensorOpSpec::LutAttention(_) => "LutAttention",
108        }
109    }
110
111    /// The tensor the op binds.
112    pub fn out(&self) -> Symbol {
113        match self {
114            TensorOpSpec::Matmul(m) => m.c,
115            TensorOpSpec::Elementwise(e) => e.c,
116            TensorOpSpec::SwiGlu(s) => s.y,
117            TensorOpSpec::FusedRow(r) => r.y,
118            TensorOpSpec::Rope(r) => r.out,
119            TensorOpSpec::Silu(s) => s.out,
120            TensorOpSpec::Sigmoid(s) => s.out,
121            TensorOpSpec::Gelu(g) => g.out,
122            TensorOpSpec::Scale(s) => s.out,
123            TensorOpSpec::Softcap(s) => s.out,
124            TensorOpSpec::Decay(d) => d.out,
125            TensorOpSpec::L2Norm(l) => l.out,
126            TensorOpSpec::Transpose(t) => t.out,
127            TensorOpSpec::HeadSlice(s) => s.out,
128            TensorOpSpec::Join(j) => j.out,
129            TensorOpSpec::HeadMatmul(m) => m.out,
130            TensorOpSpec::CausalConv(c) => c.out,
131            TensorOpSpec::GatedDelta(g) => g.out,
132            TensorOpSpec::Attention(a) => a.out,
133            TensorOpSpec::FullAttention(a) => a.out,
134            TensorOpSpec::Moe(m) => m.out,
135            TensorOpSpec::RowGather(r) => r.out,
136            TensorOpSpec::RowsGather(r) => r.out,
137            TensorOpSpec::ArgMax(a) => a.out,
138            TensorOpSpec::SetRow(r) => r.table,
139            TensorOpSpec::IntRequant(r) => r.out,
140            TensorOpSpec::IntClamp(c) => c.out,
141            TensorOpSpec::Relu(r) => r.out,
142            TensorOpSpec::LutAttention(a) => a.out,
143        }
144    }
145
146    /// The tensors the op reads, in the spec's operand order: matmul `[a, b]`; elementwise `[a, b]`; swiglu
147    /// `[gate, up]`; fused row `[x, weight?, residual?, bias?]` (the present ones); rope `[q, rc, rs, position?]`; the
148    /// unary maps and a scale `[x]`; decay `[ar, dt, alog]`; l2norm / transpose / head slice `[x]`; join `[a, b]`;
149    /// per-head matmul `[x, w]`; causal conv `[x, wc, cs]`; gated
150    /// delta `[q, k, v, z, b, a, st, gn]`; attention `[q, k, v, position?, sinks?]` and full attention `[q, k, v]`; moe `[x, wr, wg, wu, wd, router bias?, gate, up and down biases?]`;
151    /// row gather `[table, index?]`; rows gather `[table, indices]`; arg-max `[x]`; set row `[table, source, index?]` (a NAMED index is an
152    /// operand — the data the row comes from; a literal one is part of the op).
153    pub fn ins(&self) -> Vec<Symbol> {
154        match self {
155            TensorOpSpec::Matmul(m) => vec![m.a, m.b],
156            TensorOpSpec::Elementwise(e) => vec![e.a, e.b],
157            TensorOpSpec::SwiGlu(s) => vec![s.gate, s.up],
158            TensorOpSpec::FusedRow(r) => std::iter::once(r.x)
159                .chain(r.weight)
160                .chain(r.residual)
161                .chain(r.bias)
162                .collect(),
163            TensorOpSpec::Rope(r) => [r.q, r.rc, r.rs]
164                .into_iter()
165                .chain(r.from.and_then(|(p, _)| named(p)))
166                .collect(),
167            TensorOpSpec::Silu(s) => vec![s.x],
168            TensorOpSpec::Sigmoid(s) => vec![s.x],
169            TensorOpSpec::Gelu(g) => vec![g.x],
170            TensorOpSpec::Scale(s) => vec![s.x],
171            TensorOpSpec::Softcap(s) => vec![s.x],
172            TensorOpSpec::Decay(d) => vec![d.ar, d.dt, d.alog],
173            TensorOpSpec::L2Norm(l) => vec![l.x],
174            TensorOpSpec::Transpose(t) => vec![t.x],
175            TensorOpSpec::HeadSlice(s) => vec![s.x],
176            TensorOpSpec::Join(j) => vec![j.a, j.b],
177            TensorOpSpec::HeadMatmul(m) => vec![m.x, m.w],
178            TensorOpSpec::CausalConv(c) => vec![c.x, c.wc, c.cs],
179            TensorOpSpec::GatedDelta(g) => vec![g.q, g.k, g.v, g.z, g.b, g.a, g.st, g.gn],
180            TensorOpSpec::Attention(a) => [a.q, a.kc, a.vc]
181                .into_iter()
182                .chain(a.pos.and_then(named))
183                .chain(a.sinks)
184                .collect(),
185            TensorOpSpec::FullAttention(a) => vec![a.q, a.k, a.v],
186            TensorOpSpec::Moe(m) => [m.x, m.wr, m.wg, m.wu, m.wd]
187                .into_iter()
188                .chain(m.router_bias)
189                .chain(m.biases.into_iter().flatten())
190                .chain(m.selection_bias)
191                .collect(),
192            TensorOpSpec::RowGather(r) => std::iter::once(r.table).chain(named(r.index)).collect(),
193            TensorOpSpec::RowsGather(r) => vec![r.table, r.indices],
194            TensorOpSpec::ArgMax(a) => vec![a.x],
195            TensorOpSpec::SetRow(r) => [r.table, r.source]
196                .into_iter()
197                .chain(named(r.index))
198                .collect(),
199            TensorOpSpec::IntRequant(r) => vec![r.x],
200            TensorOpSpec::IntClamp(c) => vec![c.x],
201            TensorOpSpec::Relu(r) => vec![r.x],
202            TensorOpSpec::LutAttention(a) => [a.q, a.kc, a.vc, a.table]
203                .into_iter()
204                .chain(a.pos.and_then(named))
205                .collect(),
206        }
207    }
208
209    /// The positions in [`Self::ins`] of the operands the op writes back in place — its carried state: the
210    /// conv ring, the GDN memory, and the table a row is written into (the KV cache's append). An attention only
211    /// READS its cache — masked or not — so it carries nothing; the cache is carried because `Set row` writes it.
212    pub fn state_inputs(&self) -> Vec<usize> {
213        match self {
214            TensorOpSpec::CausalConv(_) => vec![2],
215            TensorOpSpec::GatedDelta(_) => vec![6],
216            TensorOpSpec::SetRow(_) => vec![0],
217            _ => Vec::new(),
218        }
219    }
220
221    /// The rows of a result the op writes a row at a time into a table it builds, or `None`: a block gated delta,
222    /// whose rows each take the step in turn and write their output row. The table is state inside the step — each
223    /// row's write updates it in place — which the step assembles whole.
224    pub fn assembled_rows(&self) -> Option<usize> {
225        match self {
226            TensorOpSpec::GatedDelta(g) if g.rows > 1 => Some(g.rows),
227            _ => None,
228        }
229    }
230}
231
232/// One lifted statement: the spec, where it sits in the top-level statement list, and its binder and operands
233/// by name (in [`TensorOpSpec::ins`] order).
234#[derive(Clone, Debug)]
235pub struct RecognizedStage {
236    pub op: TensorOpSpec,
237    pub stmt_index: usize,
238    pub out: String,
239    pub ins: Vec<String>,
240}
241
242/// A tensor the program declares (`Let A be a R by C tensor of T`): its flat length and the fill the
243/// program gives it (the declaration's zero fill plus every `Set item` override).
244#[derive(Clone, Debug, PartialEq, Eq)]
245pub struct DeclaredTensor {
246    pub name: String,
247    pub len: usize,
248    pub fill: Fill,
249}
250
251/// The walk's result: the stages in program order, the tensor statements it could not lift (by top-level
252/// index and binder), the declared tensors, and the names of every symbol the specs mention.
253#[derive(Clone, Debug, Default)]
254pub struct RecognizedProgram {
255    pub stages: Vec<RecognizedStage>,
256    pub unrecognized: Vec<(usize, String)>,
257    /// Top-level writes to a declared tensor that no section computes — `(statement, tensor, why)`. Each is a
258    /// reason the program is NOT lowered: dropping it would leave the tensor holding values the program never had.
259    pub unlifted_writes: Vec<(usize, String, &'static str)>,
260    pub declared: Vec<DeclaredTensor>,
261    pub symbols: HashMap<Symbol, String>,
262    /// Every tensor the program binds, by name, as `(rows, cols)` — the parser's table; the desugared statements
263    /// carry only flat lengths.
264    pub shapes: HashMap<String, (usize, usize)>,
265    /// Every name the program's top-level statements touch, by name, with the statements that touch it — a `Show`, a
266    /// scalar read and a stage alike. What reads a value is here, so a pass can know a value nothing else reads.
267    pub mentions: HashMap<String, std::collections::BTreeSet<usize>>,
268    /// The top-level statements that run foreign code, which may touch any name.
269    pub opaque: Vec<usize>,
270    /// The top-level statements that only declare a tensor or a scalar, or give a declared tensor a literal value before
271    /// any op reads it: the statements that make the program's inputs, which observe nothing.
272    pub quiet: BTreeSet<usize>,
273}
274
275impl RecognizedProgram {
276    /// The `(rows, cols)` the program gives tensor `name`, when that accounts for the `n` elements a recognizer
277    /// counted — and the `1 × n` row otherwise.
278    pub fn shape_of(&self, name: &str, n: usize) -> (usize, usize) {
279        match self.shapes.get(name) {
280            Some(&(r, c)) if r * c == n => (r, c),
281            _ => (1, n),
282        }
283    }
284
285    /// The name of a symbol one of this program's specs mentions.
286    pub fn name(&self, s: Symbol) -> &str {
287        self.symbols
288            .get(&s)
289            .map(String::as_str)
290            .unwrap_or_else(|| panic!("{s:?} is not a symbol of this program's specs"))
291    }
292}
293
294/// Which tensors of a walked program hold FLOAT data, propagated forward from the declarations, each name as its last
295/// write leaves it.
296///
297/// ★ AN ACTIVATION HAS NO FILL TO READ. A backend decides between an integer and a float datapath from the declared
298/// fill of its operands — right for a declared tensor, blind for one another op produces. The walk knows: a sugar that
299/// computes an exponential or a normalization is float by construction, and everything else is float exactly when
300/// something feeding it is.
301pub fn float_tensors(rp: &RecognizedProgram) -> BTreeSet<String> {
302    let mut float: BTreeSet<String> = rp
303        .declared
304        .iter()
305        .filter(|d| matches!(d.fill, Fill::ConstF(_) | Fill::FloatOverrides { .. } | Fill::RealPattern { .. }))
306        .map(|d| d.name.clone())
307        .collect();
308    for st in &rp.stages {
309        let is_float = match st.op {
310            // An index is an integer whatever it indexes.
311            TensorOpSpec::ArgMax(_) => false,
312            // A row moved out of — or into — a table has the TABLE's element kind; the index is not data here.
313            TensorOpSpec::RowGather(_) | TensorOpSpec::RowsGather(_) | TensorOpSpec::SetRow(_) => float.contains(&st.ins[0]),
314            TensorOpSpec::Matmul(_)
315            | TensorOpSpec::Elementwise(_)
316            | TensorOpSpec::Transpose(_)
317            | TensorOpSpec::HeadSlice(_)
318            | TensorOpSpec::Join(_)
319            | TensorOpSpec::HeadMatmul(_) => st.ins.iter().any(|n| float.contains(n)),
320            // An integer model's forms compute integers by construction; a ReLU keeps its operand's kind.
321            TensorOpSpec::IntRequant(_) | TensorOpSpec::IntClamp(_) | TensorOpSpec::LutAttention(_) => false,
322            TensorOpSpec::Relu(ref r) => !r.int,
323            _ => true,
324        };
325        if is_float {
326            float.insert(st.out.clone());
327        } else {
328            float.remove(&st.out);
329        }
330    }
331    float
332}
333
334/// A tensor's producer stage and the stages that read it — the dataflow edge by variable name. A stage whose
335/// output nothing reads (the program's result) has no consumers.
336#[derive(Clone, Debug, PartialEq, Eq)]
337pub struct Link {
338    pub producer: usize,
339    pub consumers: Vec<usize>,
340    pub tensor: String,
341}
342
343/// The operand a row index contributes: a named scalar is data, a literal is part of the op.
344fn named(index: RowIndex) -> Option<Symbol> {
345    match index {
346        RowIndex::Named(s) => Some(s),
347        RowIndex::Literal(_) => None,
348    }
349}
350
351type Recognizer = fn(&[Stmt], &Interner) -> Option<TensorOpSpec>;
352
353fn r_moe(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
354    recognize_moe(s).map(TensorOpSpec::Moe)
355}
356fn r_attention(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
357    recognize_attention(s)
358        .or_else(|| recognize_causal_attention(s))
359        .map(TensorOpSpec::Attention)
360}
361fn r_lut_attention(s: &[Stmt], i: &Interner) -> Option<TensorOpSpec> {
362    recognize_lut_attention(s, i).map(TensorOpSpec::LutAttention)
363}
364fn r_int_requant(s: &[Stmt], i: &Interner) -> Option<TensorOpSpec> {
365    recognize_int_requant(s, i).map(TensorOpSpec::IntRequant)
366}
367fn r_int_clamp(s: &[Stmt], i: &Interner) -> Option<TensorOpSpec> {
368    recognize_int_clamp(s, i).map(TensorOpSpec::IntClamp)
369}
370fn r_relu(s: &[Stmt], i: &Interner) -> Option<TensorOpSpec> {
371    recognize_relu(s, i).map(TensorOpSpec::Relu)
372}
373fn r_full_attention(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
374    recognize_full_attention(s).map(TensorOpSpec::FullAttention)
375}
376fn r_row_gather(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
377    recognize_row_gather(s).map(TensorOpSpec::RowGather)
378}
379fn r_rows_gather(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
380    recognize_rows_gather(s).map(TensorOpSpec::RowsGather)
381}
382fn r_argmax(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
383    recognize_argmax(s).map(TensorOpSpec::ArgMax)
384}
385fn r_set_row(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
386    recognize_set_row(s).map(TensorOpSpec::SetRow)
387}
388fn r_matmul(s: &[Stmt], i: &Interner) -> Option<TensorOpSpec> {
389    recognize_matmul(s, i).ok().map(TensorOpSpec::Matmul)
390}
391fn r_elementwise(s: &[Stmt], i: &Interner) -> Option<TensorOpSpec> {
392    recognize_elementwise(s, i)
393        .ok()
394        .map(TensorOpSpec::Elementwise)
395}
396fn r_rope(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
397    recognize_rope(s).map(TensorOpSpec::Rope)
398}
399fn r_silu(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
400    recognize_silu(s).map(TensorOpSpec::Silu)
401}
402fn r_sigmoid(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
403    recognize_sigmoid(s).map(TensorOpSpec::Sigmoid)
404}
405fn r_gelu(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
406    recognize_gelu(s).map(TensorOpSpec::Gelu)
407}
408fn r_scale(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
409    recognize_scale(s).map(TensorOpSpec::Scale)
410}
411fn r_softcap(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
412    recognize_softcap(s).map(TensorOpSpec::Softcap)
413}
414fn r_decay(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
415    recognize_decay(s).map(TensorOpSpec::Decay)
416}
417fn r_causal_conv(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
418    recognize_causal_conv(s).map(TensorOpSpec::CausalConv)
419}
420fn r_gated_delta(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
421    recognize_gated_delta(s).map(TensorOpSpec::GatedDelta)
422}
423fn r_l2norm(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
424    recognize_l2norm(s).map(TensorOpSpec::L2Norm)
425}
426fn r_fused_row(s: &[Stmt], i: &Interner) -> Option<TensorOpSpec> {
427    recognize_fused_row(s, i).ok().map(TensorOpSpec::FusedRow)
428}
429fn r_transpose(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
430    recognize_transpose(s).map(TensorOpSpec::Transpose)
431}
432fn r_head_slice(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
433    recognize_head_slice(s).map(TensorOpSpec::HeadSlice)
434}
435fn r_join(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
436    recognize_join(s).map(TensorOpSpec::Join)
437}
438fn r_head_matmul(s: &[Stmt], _: &Interner) -> Option<TensorOpSpec> {
439    recognize_head_matmul(s).map(TensorOpSpec::HeadMatmul)
440}
441fn r_swiglu(s: &[Stmt], i: &Interner) -> Option<TensorOpSpec> {
442    recognize_swiglu(s, i).ok().map(TensorOpSpec::SwiGlu)
443}
444
445/// The recognizers a statement is tried against, in order — the CUDA orchestrator's order, extended with the
446/// exact-shape recognizers it has no stage for. The composite blocks (moe, attention) go first: they contain
447/// matmul-like loops and a softmax the simpler recognizers would otherwise claim.
448pub const RECOGNIZER_ORDER: &[(&str, Recognizer)] = &[
449    ("moe", r_moe),
450    ("lut_attention", r_lut_attention),
451    ("attention", r_attention),
452    ("full_attention", r_full_attention),
453    ("row_gather", r_row_gather),
454    ("rows_gather", r_rows_gather),
455    ("argmax", r_argmax),
456    ("set_row", r_set_row),
457    ("head_matmul", r_head_matmul),
458    ("matmul", r_matmul),
459    ("softcap", r_softcap),
460    ("elementwise", r_elementwise),
461    ("int_requant", r_int_requant),
462    ("int_clamp", r_int_clamp),
463    ("relu", r_relu),
464    ("rope", r_rope),
465    ("silu", r_silu),
466    ("sigmoid", r_sigmoid),
467    ("gelu", r_gelu),
468    ("scale", r_scale),
469    ("decay", r_decay),
470    ("causal_conv", r_causal_conv),
471    ("gated_delta", r_gated_delta),
472    ("l2norm", r_l2norm),
473    ("fused_row", r_fused_row),
474    ("transpose", r_transpose),
475    ("head_slice", r_head_slice),
476    ("join", r_join),
477    ("swiglu", r_swiglu),
478];
479
480/// Walk a program's top-level statements into typed specs. `Err` only when the source does not parse; a
481/// statement that parses but is no op the walk knows is reported in `unrecognized`.
482pub fn recognize_stages(source: &str) -> Result<RecognizedProgram, SpatialReject> {
483    crate::ui_bridge::with_parsed_tensor_program(source, |parsed, interner, shapes| {
484        let (stmts, _types, _policies) = parsed.map_err(SpatialReject::Parse)?;
485        let mut rp = walk(stmts, interner);
486        rp.shapes = shapes
487            .iter()
488            .filter(|(_, (r, c))| *r > 0 && *c > 0)
489            .map(|(sym, (r, c))| {
490                (
491                    interner.resolve(*sym).to_string(),
492                    (*r as usize, *c as usize),
493                )
494            })
495            .collect();
496        Ok(rp)
497    })
498}
499
500/// [`recognize_stages`] over statements already parsed — every backend's front end: the CUDA forward refuses what
501/// this walk reports as `unrecognized` or `unlifted_writes`, so both lanes agree on what a program's statements are.
502/// The shape table is the caller's to add (it comes from the parse, not the statements).
503pub fn walk(stmts: &[Stmt], interner: &Interner) -> RecognizedProgram {
504    let mut rp = RecognizedProgram::default();
505    let mut tensors: std::collections::HashSet<Symbol> = Default::default();
506    let (fills, assigned) = (Fills::of(stmts), assigned(stmts));
507    // The names declared, and the names a recognized stage reads or writes.
508    let (mut declared, mut staged): (std::collections::HashSet<String>, std::collections::HashSet<String>) = Default::default();
509    for (i, s) in stmts.iter().enumerate() {
510        let touched = crate::optimize::mentions::Mentions::of_stmt(s);
511        if touched.is_opaque() {
512            rp.opaque.push(i);
513        }
514        for sym in touched.symbols() {
515            rp.mentions.entry(interner.resolve(sym).to_string()).or_default().insert(i);
516        }
517        // ★ A NAME IS BOUND ONCE. Every section reads the one value a name was declared with — its fill is folded
518        // from the whole statement list — so a second binding of a declared or computed name is refused, never
519        // lowered as the first.
520        let bound = |t: Symbol| {
521            let name = interner.resolve(t);
522            declared.contains(name) || staged.contains(name)
523        };
524        let declaration = tensor_declaration(s)
525            .map(|(var, len)| (var, len, None))
526            .or_else(|| scalar_declaration(s, &assigned).map(|(var, fill)| (var, 1, Some(fill))));
527        if let Some((var, len, scalar)) = declaration {
528            if bound(var) {
529                rp.unlifted_writes
530                    .push((i, interner.resolve(var).to_string(), SECOND_BINDING));
531                continue;
532            }
533            let fill = scalar.unwrap_or_else(|| {
534                tensors.insert(var);
535                fills.fill(var)
536            });
537            declared.insert(interner.resolve(var).to_string());
538            rp.quiet.insert(i);
539            rp.declared.push(DeclaredTensor {
540                name: interner.resolve(var).to_string(),
541                len,
542                fill,
543            });
544            continue;
545        }
546        if !is_compound(s) {
547            let touched = |t: Symbol| staged.contains(interner.resolve(t));
548            match unlifted_write(s, &tensors, &bound, &touched) {
549                Some((t, why)) => rp.unlifted_writes.push((i, interner.resolve(t).to_string(), why)),
550                None if matches!(s, Stmt::SetIndex { collection: Expr::Identifier(t), .. } if tensors.contains(t)) => {
551                    rp.quiet.insert(i);
552                }
553                None => {}
554            }
555            continue;
556        }
557        let one = std::slice::from_ref(s);
558        match RECOGNIZER_ORDER.iter().find_map(|(_, r)| r(one, interner)) {
559            Some(op) => {
560                let out = op.out();
561                let ins = op.ins();
562                for sym in std::iter::once(out).chain(ins.iter().copied()) {
563                    rp.symbols
564                        .entry(sym)
565                        .or_insert_with(|| interner.resolve(sym).to_string());
566                }
567                staged.extend(std::iter::once(out).chain(ins.iter().copied()).map(|t| interner.resolve(t).to_string()));
568                rp.stages.push(RecognizedStage {
569                    out: interner.resolve(out).to_string(),
570                    ins: ins
571                        .iter()
572                        .map(|s| interner.resolve(*s).to_string())
573                        .collect(),
574                    op,
575                    stmt_index: i,
576                });
577            }
578            None => rp.unrecognized.push((i, describe(s, interner))),
579        }
580    }
581    rp
582}
583
584/// `Let A be a R by C tensor of T` — the declaration's desugar: the binder, a zero counter, and one counted
585/// loop pushing the element type's zero `R·C` times. Returns `(A, R·C)`.
586fn tensor_declaration(s: &Stmt) -> Option<(Symbol, usize)> {
587    let Stmt::Splice { body } = s else {
588        return None;
589    };
590    let [Stmt::Let {
591        var, value: new, ..
592    }, Stmt::Let {
593        var: ctr,
594        value: start,
595        ..
596    }, Stmt::While {
597        cond, body: fill, ..
598    }] = body
599    else {
600        return None;
601    };
602    if !matches!(new, Expr::New { .. }) || !matches!(start, Expr::Literal(Literal::Number(0))) {
603        return None;
604    }
605    let Expr::BinaryOp { left, right, .. } = cond else {
606        return None;
607    };
608    let (Expr::Identifier(iv), Expr::Literal(Literal::Number(len))) = (*left, *right) else {
609        return None;
610    };
611    let [Stmt::Push { value, collection }, Stmt::Set { .. }] = fill else {
612        return None;
613    };
614    let zero = matches!(value, Expr::Literal(Literal::Number(0)))
615        || matches!(value, Expr::Literal(Literal::Float(f)) if *f == 0.0);
616    (iv == ctr && zero && matches!(collection, Expr::Identifier(c) if c == var) && *len >= 0)
617        .then_some((*var, *len as usize))
618}
619
620/// `Let tok be 2.` at the top level — an Int scalar, the one-element tensor a row sugar's NAMED index reads.
621/// Its value is a compile-time constant only if nothing assigns it again; a scalar that is re-`Set` is
622/// data the compiler does not know (`Fill::Unknown`), never a stale constant.
623fn scalar_declaration(s: &Stmt, assigned: &std::collections::HashSet<Symbol>) -> Option<(Symbol, Fill)> {
624    let Stmt::Let {
625        var,
626        value: Expr::Literal(Literal::Number(n)),
627        ..
628    } = s
629    else {
630        return None;
631    };
632    Some((
633        *var,
634        if assigned.contains(var) {
635            Fill::Unknown
636        } else {
637            Fill::Const(*n)
638        },
639    ))
640}
641
642/// Every name a `Set` assigns anywhere in the statements, nested blocks and loops included.
643fn assigned(stmts: &[Stmt]) -> std::collections::HashSet<Symbol> {
644    fn walk(stmts: &[Stmt], into: &mut std::collections::HashSet<Symbol>) {
645        for s in stmts {
646            match s {
647                Stmt::Set { target, .. } => {
648                    into.insert(*target);
649                }
650                Stmt::Splice { body } | Stmt::While { body, .. } | Stmt::Repeat { body, .. } => walk(body, into),
651                Stmt::If {
652                    then_block,
653                    else_block,
654                    ..
655                } => {
656                    walk(then_block, into);
657                    if let Some(eb) = else_block {
658                        walk(eb, into);
659                    }
660                }
661                _ => {}
662            }
663        }
664    }
665    let mut into = std::collections::HashSet::new();
666    walk(stmts, &mut into);
667    into
668}
669
670/// Why a declaration or `Let` of a name already bound is refused.
671const SECOND_BINDING: &str = "a second binding of a name already bound — every section reads the value it was first bound to";
672
673/// ★ A WRITE THE WALK WOULD SKIP IS A WRITE THE PROGRAM WOULD LOSE. Top-level simple statements are passed over,
674/// and a tensor's declared fill is the value it holds when an op first reads it — so the only writes that fill may
675/// absorb are literal `Set item i of T to v` statements made BEFORE any op touches `T` (`recognize_fill` folds
676/// exactly those). Any other top-level write to a declared tensor is data no section computes; it is reported, and
677/// the program is refused by name, rather than lowered with the tensor holding values the program never had. So is
678/// any `Let` of a name already `bound`, and a `Set` of a value an op has already read (`touched`): a scalar the
679/// sections read is one value for the whole program.
680fn unlifted_write(
681    s: &Stmt,
682    tensors: &std::collections::HashSet<Symbol>,
683    bound: &dyn Fn(Symbol) -> bool,
684    touched: &dyn Fn(Symbol) -> bool,
685) -> Option<(Symbol, &'static str)> {
686    let ident = |e: &Expr| match e {
687        Expr::Identifier(t) => Some(*t),
688        _ => None,
689    };
690    match s {
691        Stmt::SetIndex {
692            collection,
693            index,
694            value,
695        } => {
696            let t = ident(collection).filter(|t| tensors.contains(t))?;
697            let literal = matches!(index, Expr::Literal(Literal::Number(_)))
698                && matches!(value, Expr::Literal(Literal::Number(_) | Literal::Float(_)));
699            if !literal {
700                Some((
701                    t,
702                    "a write whose position or value is not a literal — data no section computes",
703                ))
704            } else if touched(t) {
705                Some((t, "a literal write after an op has used the tensor — folding it into the fill would move it before that op"))
706            } else {
707                None
708            }
709        }
710        Stmt::Push { collection, .. } => ident(collection)
711            .filter(|t| tensors.contains(t))
712            .map(|t| (t, "a push that grows a declared tensor")),
713        Stmt::Set { target, .. } | Stmt::Let { var: target, .. } if tensors.contains(target) => {
714            Some((*target, "a rebinding of a declared tensor"))
715        }
716        Stmt::Let { var, .. } if bound(*var) => Some((*var, SECOND_BINDING)),
717        Stmt::Set { target, .. } if touched(*target) => {
718            Some((*target, "a write to a value an op has already read"))
719        }
720        _ => None,
721    }
722}
723
724/// A statement that can hold a tensor op: a desugar's `Splice`, or a hand-written loop nest or branch.
725fn is_compound(s: &Stmt) -> bool {
726    matches!(
727        s,
728        Stmt::Splice { .. } | Stmt::While { .. } | Stmt::Repeat { .. } | Stmt::If { .. }
729    )
730}
731
732/// How an unrecognized statement is reported: the binder of a desugared `Let`, or the loop variable of a
733/// hand-written loop.
734fn describe(s: &Stmt, interner: &Interner) -> String {
735    match s {
736        Stmt::Splice { body } => body
737            .iter()
738            .find_map(|st| match st {
739                Stmt::Let { var, .. } => Some(interner.resolve(*var).to_string()),
740                _ => None,
741            })
742            .unwrap_or_else(|| "Splice".to_string()),
743        Stmt::While { cond, .. } => match cond {
744            Expr::BinaryOp {
745                left: Expr::Identifier(iv),
746                ..
747            } => format!("While {}", interner.resolve(*iv)),
748            _ => "While".to_string(),
749        },
750        Stmt::Repeat { .. } => "Repeat".to_string(),
751        Stmt::If { .. } => "If".to_string(),
752        _ => "statement".to_string(),
753    }
754}
755
756/// The dataflow by variable name: for every stage, the later stages that read its output.
757pub fn links(stages: &[RecognizedStage]) -> Vec<Link> {
758    let mut readers: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
759    for (j, s) in stages.iter().enumerate() {
760        for name in &s.ins {
761            let r = readers.entry(name.as_str()).or_default();
762            if r.last() != Some(&j) {
763                r.push(j);
764            }
765        }
766    }
767    stages
768        .iter()
769        .enumerate()
770        .map(|(i, s)| Link {
771            producer: i,
772            consumers: readers
773                .get(s.out.as_str())
774                .map(|r| r.iter().copied().filter(|&j| j > i).collect())
775                .unwrap_or_default(),
776            tensor: s.out.clone(),
777        })
778        .collect()
779}
780
781/// The state a step CARRIES to the next — [`state_tensors`] without the tables the step assembles whole. A table every
782/// row of which the step writes at a literal row (`Set row 2 of X`, `Set rows from 1 of X`, a block gated delta's
783/// output) before any stage reads it begins each step with nothing the step can see: a chunk's stacked rows, its RoPE
784/// block. The KV cache is carried —
785/// it is written at a position the data gives and read far beyond it — and so are the conv rings and the memories,
786/// which every step reads before it writes.
787pub fn carried_state(rp: &RecognizedProgram) -> Vec<String> {
788    let assembled = |table: &str| -> bool {
789        let mut rows: Option<usize> = None;
790        let mut covered: BTreeSet<usize> = BTreeSet::new();
791        for s in &rp.stages {
792            let writes = matches!(&s.op, TensorOpSpec::SetRow(_)) && s.ins[0] == table;
793            let reads = s
794                .ins
795                .iter()
796                .enumerate()
797                .any(|(k, n)| n == table && !(writes && k == 0));
798            if reads && rows.is_none_or(|r| covered.len() < r) {
799                return false;
800            }
801            if let (true, TensorOpSpec::SetRow(r)) = (writes, &s.op) {
802                let RowIndex::Literal(first) = r.index else {
803                    return false;
804                };
805                rows = Some(r.rows);
806                covered.extend(
807                    (first as usize).saturating_sub(1)
808                        ..(first as usize).saturating_sub(1) + r.count,
809                );
810            }
811            if let Some(n) = s.op.assembled_rows().filter(|_| s.out == table) {
812                rows = Some(n);
813                covered.extend(0..n);
814            }
815        }
816        rows.is_some_and(|r| (0..r).all(|y| covered.contains(&y)))
817    };
818    state_tensors(&rp.stages)
819        .into_iter()
820        .filter(|t| !assembled(t))
821        .collect()
822}
823
824/// The state, in first-appearance order: every tensor some stage reads and writes back in place, and every table an
825/// op assembles a row at a time ([`TensorOpSpec::assembled_rows`]).
826pub fn state_tensors(stages: &[RecognizedStage]) -> Vec<String> {
827    let mut seen: BTreeSet<&str> = BTreeSet::new();
828    let mut out: Vec<String> = Vec::new();
829    for s in stages {
830        for pos in s.op.state_inputs() {
831            let name = &s.ins[pos];
832            if seen.insert(name.as_str()) {
833                out.push(name.clone());
834            }
835        }
836        if s.op.assembled_rows().is_some() && seen.insert(s.out.as_str()) {
837            out.push(s.out.clone());
838        }
839    }
840    out
841}