Skip to main content

logicaffeine_compile/tensor_recognize/
mod.rs

1//! Backend-agnostic tensor-op **recognition** — LOGOS AST -> typed op specs, shared by every codegen backend.
2//!
3//! The recognizers here read an ordinary (desugared) LOGOS program and lift it into a typed spec — a matmul, an
4//! elementwise op, a reduction / fused-row normalization, or a SwiGLU map — or REFUSE (typed). They are
5//! **backend-neutral**: the CUDA ([`crate::codegen_cuda`]), Tenstorrent ([`crate::codegen_tt`]), and RISC-V
6//! ([`crate::codegen_riscv`]) tiers all consume the SAME spec and each emit their own kernel from it. The
7//! per-backend EMITTERS (`lower_*_to_tensix`, the CUDA `emit_*` fns, ...) live in their backend module; only the
8//! recognition + spec types are shared, and they live HERE rather than under any one backend.
9
10use crate::intern::{Interner, Symbol};
11use logicaffeine_language::ast::stmt::{BinaryOpKind, Expr, Literal, Stmt, TypeExpr};
12use std::collections::HashMap;
13
14pub mod qwen;
15
16/// A recognized matmul: `C(M×N) = A(M×K) · B(K×N)`, row-major, over the named `Seq of Int` arrays.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct MatmulSpec {
19    pub m: usize,
20    pub n: usize,
21    pub k: usize,
22    pub a: Symbol,
23    pub b: Symbol,
24    pub c: Symbol,
25}
26
27impl MatmulSpec {
28    /// Output tiles high (32×32 Tensix tiles, ceil).
29    pub fn mt(&self) -> usize {
30        self.m.div_ceil(32)
31    }
32    /// Output tiles wide.
33    pub fn nt(&self) -> usize {
34        self.n.div_ceil(32)
35    }
36    /// Contraction tiles.
37    pub fn kt(&self) -> usize {
38        self.k.div_ceil(32)
39    }
40}
41
42/// A recognized per-head matmul, `Y = X matmul W across H heads`: each head's slice of every row of `X` times its own
43/// block of `W`'s rows. `X` is `m × (heads·a)`, `W` `(heads·a) × b`, `Y` `m × (heads·b)`, and
44/// `Y[t][h·b + r] = Σ_e X[t][h·a + e]·W[h·a + e][r]` — a block-diagonal product, one `a × b` block per head.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct HeadMatmulSpec {
47    pub x: Symbol,
48    pub w: Symbol,
49    pub out: Symbol,
50    pub m: usize,
51    pub heads: usize,
52    pub a: usize,
53    pub b: usize,
54}
55
56/// Why a program is not a recognizable tile-matmul — a typed refusal, never a miscompile.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum MatmulReject {
59    /// No `While` loop at the top of the scanned region.
60    NoLoopNest,
61    /// Fewer than three nested counted loops.
62    NotTripleNested,
63    /// No accumulating indexed store `Set item _ of C to (item _ of C) + …`.
64    NoAccumulatingStore,
65    /// An index expression is not affine in the induction variables.
66    NonAffineIndex,
67    /// The affine indices do not match the row-major GEMM layout (against the loop bounds).
68    NotGemmLayout,
69}
70
71/// An affine form `Σ coeff·iv + cst` over induction/loop variables (keyed by their `Symbol`).
72#[derive(Clone, Default, Debug, PartialEq, Eq)]
73struct Affine {
74    coeff: HashMap<Symbol, i64>,
75    cst: i64,
76}
77
78impl Affine {
79    fn constant(c: i64) -> Self {
80        Affine {
81            coeff: HashMap::new(),
82            cst: c,
83        }
84    }
85    fn var(s: Symbol) -> Self {
86        let mut coeff = HashMap::new();
87        coeff.insert(s, 1);
88        Affine { coeff, cst: 0 }
89    }
90    fn add(&self, o: &Affine) -> Affine {
91        let mut coeff = self.coeff.clone();
92        for (k, v) in &o.coeff {
93            *coeff.entry(*k).or_default() += v;
94        }
95        coeff.retain(|_, v| *v != 0);
96        Affine {
97            coeff,
98            cst: self.cst + o.cst,
99        }
100    }
101    fn sub(&self, o: &Affine) -> Affine {
102        let mut coeff = self.coeff.clone();
103        for (k, v) in &o.coeff {
104            *coeff.entry(*k).or_default() -= v;
105        }
106        coeff.retain(|_, v| *v != 0);
107        Affine {
108            coeff,
109            cst: self.cst - o.cst,
110        }
111    }
112    fn scale(&self, f: i64) -> Affine {
113        Affine {
114            coeff: self.coeff.iter().map(|(k, v)| (*k, v * f)).collect(),
115            cst: self.cst * f,
116        }
117    }
118    fn is_const(&self) -> bool {
119        self.coeff.is_empty()
120    }
121    fn coeff_of(&self, s: Symbol) -> i64 {
122        *self.coeff.get(&s).unwrap_or(&0)
123    }
124}
125
126/// Evaluate an expression to an affine form over the induction variables, resolving top-level integer
127/// constants and inner `Let`-bound index expressions. Returns `None` if the expression is non-affine
128/// (e.g. iv×iv) or references an unsupported construct.
129fn eval_affine(
130    e: &Expr,
131    consts: &HashMap<Symbol, i64>,
132    lets: &HashMap<Symbol, Affine>,
133) -> Option<Affine> {
134    match e {
135        Expr::Literal(Literal::Number(n)) => Some(Affine::constant(*n)),
136        Expr::Identifier(s) => {
137            if let Some(c) = consts.get(s) {
138                Some(Affine::constant(*c))
139            } else if let Some(a) = lets.get(s) {
140                Some(a.clone())
141            } else {
142                Some(Affine::var(*s))
143            }
144        }
145        Expr::BinaryOp { op, left, right } => {
146            let l = eval_affine(left, consts, lets)?;
147            let r = eval_affine(right, consts, lets)?;
148            match op {
149                BinaryOpKind::Add => Some(l.add(&r)),
150                BinaryOpKind::Subtract => Some(l.sub(&r)),
151                BinaryOpKind::Multiply => {
152                    if l.is_const() {
153                        Some(r.scale(l.cst))
154                    } else if r.is_const() {
155                        Some(l.scale(r.cst))
156                    } else {
157                        None // iv×iv — non-affine
158                    }
159                }
160                _ => None,
161            }
162        }
163        _ => None,
164    }
165}
166
167/// The first `While` in a block, as `(condition, body)`.
168fn first_while<'a>(stmts: &'a [Stmt<'a>]) -> Option<(&'a Expr<'a>, &'a [Stmt<'a>])> {
169    stmts.iter().find_map(|s| match s {
170        Stmt::While { cond, body, .. } => Some((*cond, *body)),
171        _ => None,
172    })
173}
174
175/// A counted loop's `(induction variable, trip count)` from `While iv is less than BOUND` (or `at most`).
176fn loop_iv_bound(cond: &Expr, consts: &HashMap<Symbol, i64>) -> Option<(Symbol, i64)> {
177    let empty = HashMap::new();
178    if let Expr::BinaryOp { op, left, right } = cond {
179        if let Expr::Identifier(iv) = left {
180            let b = eval_affine(right, consts, &empty)?;
181            if b.is_const() {
182                return match op {
183                    BinaryOpKind::Lt => Some((*iv, b.cst)),
184                    BinaryOpKind::LtEq => Some((*iv, b.cst + 1)),
185                    _ => None,
186                };
187            }
188        }
189    }
190    None
191}
192
193fn as_ident(e: &Expr) -> Option<Symbol> {
194    if let Expr::Identifier(s) = e {
195        Some(*s)
196    } else {
197        None
198    }
199}
200
201fn as_index<'a>(e: &'a Expr<'a>) -> Option<(&'a Expr<'a>, &'a Expr<'a>)> {
202    if let Expr::Index { collection, index } = e {
203        Some((collection, index))
204    } else {
205        None
206    }
207}
208
209fn as_binop<'a>(e: &'a Expr<'a>, want: BinaryOpKind) -> Option<(&'a Expr<'a>, &'a Expr<'a>)> {
210    if let Expr::BinaryOp { op, left, right } = e {
211        if *op == want {
212            return Some((left, right));
213        }
214    }
215    None
216}
217
218/// Every integer constant `Let x be <number>` anywhere in the tree, so loop bounds / index strides that
219/// reference a named size (`n`) resolve regardless of scope.
220fn collect_consts_rec(stmts: &[Stmt], out: &mut HashMap<Symbol, i64>) {
221    for s in stmts {
222        match s {
223            // Only IMMUTABLE bindings are dimension constants; a `Let mutable i be 0` is a loop counter and
224            // must remain a free induction variable, not get frozen to its initial value.
225            Stmt::Let {
226                var,
227                value,
228                mutable: false,
229                ..
230            } => {
231                if let Expr::Literal(Literal::Number(n)) = value {
232                    out.insert(*var, *n);
233                }
234            }
235            Stmt::While { body, .. } | Stmt::Repeat { body, .. } | Stmt::Splice { body } => {
236                collect_consts_rec(body, out)
237            }
238            Stmt::If {
239                then_block,
240                else_block,
241                ..
242            } => {
243                collect_consts_rec(then_block, out);
244                if let Some(eb) = else_block {
245                    collect_consts_rec(eb, out);
246                }
247            }
248            _ => {}
249        }
250    }
251}
252
253fn reject_rank(r: &MatmulReject) -> u8 {
254    match r {
255        MatmulReject::NoLoopNest => 1,
256        MatmulReject::NotTripleNested => 2,
257        MatmulReject::NoAccumulatingStore => 3,
258        MatmulReject::NonAffineIndex => 4,
259        MatmulReject::NotGemmLayout => 5,
260    }
261}
262
263/// Recognize a row-major GEMM idiom anywhere in a LOGOS statement list and extract its `MatmulSpec`.
264///
265/// Structure required: three nested counted `While` loops over induction variables `i, k, j`, with an
266/// accumulating store `Set item _ of C to (item _ of C) + (item _ of A) * (item _ of B)` in the innermost
267/// body. The three index expressions must be affine and match the row-major layout — verified against the
268/// loop bounds — i.e. `C` indexed `i·N + j`, `A` indexed `i·K + k`, `B` indexed `k·N + j`. Loops that are
269/// not the matmul nest (e.g. an array-build loop) are skipped; the search returns the most-specific refusal
270/// if nothing matches.
271pub fn recognize_matmul(stmts: &[Stmt], _interner: &Interner) -> Result<MatmulSpec, MatmulReject> {
272    let mut consts: HashMap<Symbol, i64> = HashMap::new();
273    collect_consts_rec(stmts, &mut consts);
274    search_matmul(stmts, &consts)
275}
276
277/// Recognize `Let Y be X matmul W across H heads` — the matmul's nest with a head loop between the row loop and the
278/// contraction: `Y[t·(H·b) + h·b + r + 1] += X[t·(H·a) + h·a + e + 1] · W[(h·a + e)·b + r + 1]`, the loops `t`, `h`, `e`,
279/// `r` over `m`, `H`, `a`, `b`.
280pub fn recognize_head_matmul(stmts: &[Stmt]) -> Option<HeadMatmulSpec> {
281    let mut consts: HashMap<Symbol, i64> = HashMap::new();
282    collect_consts_rec(stmts, &mut consts);
283    search_head_matmul(stmts, &consts)
284}
285
286fn search_head_matmul(stmts: &[Stmt], consts: &HashMap<Symbol, i64>) -> Option<HeadMatmulSpec> {
287    for s in stmts {
288        let found = match s {
289            Stmt::While { cond, body, .. } => {
290                head_matmul_at(cond, body, consts).or_else(|| search_head_matmul(body, consts))
291            }
292            Stmt::Splice { body } => search_head_matmul(body, consts),
293            Stmt::If { then_block, else_block, .. } => search_head_matmul(then_block, consts)
294                .or_else(|| else_block.and_then(|eb| search_head_matmul(eb, consts))),
295            _ => None,
296        };
297        if found.is_some() {
298            return found;
299        }
300    }
301    None
302}
303
304/// The per-head nest with the given `While` as its row loop.
305fn head_matmul_at(cond_t: &Expr, body_t: &[Stmt], consts: &HashMap<Symbol, i64>) -> Option<HeadMatmulSpec> {
306    let (t, m) = loop_iv_bound(cond_t, consts)?;
307    let (cond_h, body_h) = first_while(body_t)?;
308    let (h, heads) = loop_iv_bound(cond_h, consts)?;
309    let (cond_e, body_e) = first_while(body_h)?;
310    let (e, a) = loop_iv_bound(cond_e, consts)?;
311    let (cond_r, body_r) = first_while(body_e)?;
312    let (r, b) = loop_iv_bound(cond_r, consts)?;
313    let none = HashMap::new();
314    let (out, idx, value) = body_r.iter().find_map(|s| match s {
315        Stmt::SetIndex { collection, index, value } => Some((as_ident(collection)?, *index, *value)),
316        _ => None,
317    })?;
318    let y = eval_affine(idx, consts, &none)?;
319    let (acc, prod) = as_binop(value, BinaryOpKind::Add)?;
320    let (acc_c, acc_i) = as_index(acc)?;
321    if as_ident(acc_c)? != out || eval_affine(acc_i, consts, &none)? != y {
322        return None;
323    }
324    let (xr, wr) = as_binop(prod, BinaryOpKind::Multiply)?;
325    let (xc, xi) = as_index(xr)?;
326    let (wc, wi) = as_index(wr)?;
327    let (x, w) = (as_ident(xc)?, as_ident(wc)?);
328    let (xi, wi) = (eval_affine(xi, consts, &none)?, eval_affine(wi, consts, &none)?);
329    let shaped = |f: &Affine, coeffs: [i64; 4]| {
330        [t, h, e, r].iter().zip(coeffs).all(|(v, c)| f.coeff_of(*v) == c) && f.coeff.len() == coeffs.iter().filter(|c| **c != 0).count() && f.cst == 1
331    };
332    let ok = shaped(&y, [heads * b, b, 0, 1]) && shaped(&xi, [heads * a, a, 1, 0]) && shaped(&wi, [0, a * b, b, 1]);
333    (ok && m >= 1 && heads >= 1 && a >= 1 && b >= 1).then(|| HeadMatmulSpec {
334        x,
335        w,
336        out,
337        m: m as usize,
338        heads: heads as usize,
339        a: a as usize,
340        b: b as usize,
341    })
342}
343
344/// How an array is FILLED by its build loop — recognized from `While iv is less than N: Push <expr> to xs`,
345/// so a backend host can reproduce the source's data and a numerics claim can bound the operand range.
346#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
347pub enum Fill {
348    /// Every element is the same constant.
349    Const(i64),
350    /// `element[p] = (p % modulus) + shift` over the loop counter `p`.
351    Pattern { modulus: i64, shift: i64 },
352    /// A recognized base fill followed by constant element overrides `Set item i of xs to v` (0-based here).
353    Overrides {
354        base: Box<Fill>,
355        sets: Vec<(usize, i64)>,
356    },
357    /// Every element is the same float constant (its bits, so the fill stays hashable).
358    ConstF(u64),
359    /// A float base constant (bits) followed by float element overrides (0-based index, value bits).
360    FloatOverrides { base: u64, sets: Vec<(usize, u64)> },
361    /// `element[p] = ((p % modulus) + shift) / divisor` over the loop counter `p`, a `Real` (`divisor` by its bits).
362    RealPattern { modulus: i64, shift: i64, divisor: u64 },
363    /// Not a recognized fill shape (data is external to the compiler).
364    Unknown,
365}
366
367impl Fill {
368    /// The float element overrides of a float fill, as values.
369    pub fn float_sets(&self) -> Option<(f64, Vec<(usize, f64)>)> {
370        match self {
371            Fill::ConstF(b) => Some((f64::from_bits(*b), Vec::new())),
372            Fill::FloatOverrides { base, sets } => Some((
373                f64::from_bits(*base),
374                sets.iter().map(|(i, b)| (*i, f64::from_bits(*b))).collect(),
375            )),
376            _ => None,
377        }
378    }
379
380    /// Element `p` of a fill that is the same at every length — `None` for an override list, whose length the fill
381    /// does not carry past its last override, and for data the compiler cannot see.
382    pub fn element(&self, p: i64) -> Option<Scalar> {
383        match self {
384            Fill::Const(c) => Some(Scalar::Int(*c)),
385            Fill::Pattern { modulus, shift } => Some(Scalar::Int(p % modulus + shift)),
386            Fill::ConstF(b) => Some(Scalar::Real(f64::from_bits(*b))),
387            Fill::RealPattern { modulus, shift, divisor } => {
388                Some(Scalar::Real((p % modulus + shift) as f64 / f64::from_bits(*divisor)))
389            }
390            Fill::Overrides { .. } | Fill::FloatOverrides { .. } | Fill::Unknown => None,
391        }
392    }
393}
394
395/// Recognize the fill of `tensor` anywhere in the statement list: the FIRST loop whose body pushes onto it.
396pub fn recognize_fill(stmts: &[Stmt], tensor: Symbol) -> Fill {
397    Fills::of(stmts).fill(tensor)
398}
399
400/// Every tensor's fill in a statement list at once ([`recognize_fill`] of each), from one walk of the list: for each
401/// tensor the first loop pushing onto it — a loop's own pushes met before the loops nested in it, as `recognize_fill`
402/// meets them — and its top-level literal writes, in order.
403pub struct Fills<'s, 'a> {
404    first: std::collections::HashMap<Symbol, Fill>,
405    writes: std::collections::HashMap<Symbol, Vec<&'s Stmt<'a>>>,
406}
407
408impl<'s, 'a> Fills<'s, 'a> {
409    pub fn of(stmts: &'s [Stmt<'a>]) -> Self {
410        fn scan(stmts: &[Stmt], first: &mut std::collections::HashMap<Symbol, Fill>) {
411            for s in stmts {
412                match s {
413                    Stmt::While { cond, body, .. } => {
414                        let iv = as_binop_any(cond).and_then(|(l, _)| as_ident(l));
415                        for b in body.iter() {
416                            if let Stmt::Push { value, collection } = b {
417                                if let Some(t) = as_ident(collection) {
418                                    first.entry(t).or_insert_with(|| fill_of(value, iv));
419                                }
420                            }
421                        }
422                        scan(body, first);
423                    }
424                    Stmt::If { then_block, else_block, .. } => {
425                        scan(then_block, first);
426                        if let Some(eb) = else_block {
427                            scan(eb, first);
428                        }
429                    }
430                    Stmt::Splice { body } => scan(body, first),
431                    _ => {}
432                }
433            }
434        }
435        let mut first = std::collections::HashMap::new();
436        scan(stmts, &mut first);
437        let mut writes: std::collections::HashMap<Symbol, Vec<&Stmt>> = std::collections::HashMap::new();
438        for s in stmts {
439            if let Stmt::SetIndex { collection, .. } = s {
440                if let Some(t) = as_ident(collection) {
441                    writes.entry(t).or_default().push(s);
442                }
443            }
444        }
445        Fills { first, writes }
446    }
447
448    /// `tensor`'s fill: its first fill loop's, folded with its literal writes.
449    pub fn fill(&self, tensor: Symbol) -> Fill {
450        let base = self.first.get(&tensor).cloned().unwrap_or(Fill::Unknown);
451        if base == Fill::Unknown {
452            return base;
453        }
454        let writes: &[&Stmt] = self.writes.get(&tensor).map_or(&[], Vec::as_slice);
455        if !writes.is_empty() && matches!(base, Fill::RealPattern { .. }) {
456            return Fill::Unknown;
457        }
458        let mut sets = Vec::new();
459        let mut float_sets: Vec<(usize, u64)> = Vec::new();
460        for s in writes {
461            if let Stmt::SetIndex {
462                index: Expr::Literal(Literal::Number(i)),
463                value,
464                ..
465            } = s
466            {
467                if *i >= 1 {
468                    match value {
469                        Expr::Literal(Literal::Number(v)) => sets.push((*i as usize - 1, *v)),
470                        Expr::Literal(Literal::Float(v)) => float_sets.push((*i as usize - 1, v.to_bits())),
471                        _ => {}
472                    }
473                }
474            }
475        }
476        let float_base = match &base {
477            Fill::ConstF(b) => Some(*b),
478            Fill::Const(c) if !float_sets.is_empty() => Some((*c as f64).to_bits()),
479            _ => None,
480        };
481        if let Some(fb) = float_base {
482            let mut all: Vec<(usize, u64)> = sets
483                .iter()
484                .map(|(i, v)| (*i, (*v as f64).to_bits()))
485                .collect();
486            all.extend(float_sets);
487            return if all.is_empty() {
488                Fill::ConstF(fb)
489            } else {
490                Fill::FloatOverrides {
491                    base: fb,
492                    sets: all,
493                }
494            };
495        }
496        if sets.is_empty() {
497            base
498        } else {
499            Fill::Overrides {
500                base: Box::new(base),
501                sets,
502            }
503        }
504    }
505}
506
507fn as_binop_any<'a>(e: &'a Expr<'a>) -> Option<(&'a Expr<'a>, &'a Expr<'a>)> {
508    if let Expr::BinaryOp { left, right, .. } = e {
509        Some((left, right))
510    } else {
511        None
512    }
513}
514
515/// The fill an expression over the loop counter `iv` denotes: a number literal, `iv % m`, `(iv % m) ± c`, or either
516/// of the last two over a `Real` divisor.
517fn fill_of(value: &Expr, iv: Option<Symbol>) -> Fill {
518    match value {
519        Expr::BinaryOp {
520            op: BinaryOpKind::Divide,
521            left,
522            right: Expr::Literal(Literal::Float(d)),
523        } if d.is_finite() && *d != 0.0 => match fill_of(left, iv) {
524            Fill::Pattern { modulus, shift } => Fill::RealPattern { modulus, shift, divisor: d.to_bits() },
525            _ => Fill::Unknown,
526        },
527        Expr::Literal(Literal::Number(n)) => Fill::Const(*n),
528        Expr::Literal(Literal::Float(f)) => Fill::ConstF(f.to_bits()),
529        Expr::BinaryOp {
530            op: BinaryOpKind::Modulo,
531            left,
532            right,
533        } => match (as_ident(left), right) {
534            (Some(v), Expr::Literal(Literal::Number(m))) if Some(v) == iv && *m > 0 => {
535                Fill::Pattern {
536                    modulus: *m,
537                    shift: 0,
538                }
539            }
540            _ => Fill::Unknown,
541        },
542        Expr::BinaryOp { op, left, right }
543            if matches!(op, BinaryOpKind::Add | BinaryOpKind::Subtract) =>
544        {
545            match (fill_of(left, iv), right) {
546                (Fill::Pattern { modulus, shift }, Expr::Literal(Literal::Number(c))) => {
547                    let c = if *op == BinaryOpKind::Add { *c } else { -*c };
548                    Fill::Pattern {
549                        modulus,
550                        shift: shift + c,
551                    }
552                }
553                _ => Fill::Unknown,
554            }
555        }
556        _ => Fill::Unknown,
557    }
558}
559
560/// An elementwise tensor op — `C[p] = A[p] ⊕ B[p]` over a shared flat length.
561#[derive(
562    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
563)]
564pub enum ElementwiseOp {
565    Add,
566    Mul,
567}
568
569impl ElementwiseOp {
570    /// The C/CUDA infix operator token.
571    pub fn c_op(self) -> &'static str {
572        match self {
573            ElementwiseOp::Add => "+",
574            ElementwiseOp::Mul => "*",
575        }
576    }
577}
578
579/// The recognized shape of an elementwise tensor op: two source tensors `a`, `b`, a destination `c`, the
580/// operator, and the shared element count `n`.
581#[derive(Debug, Clone, Copy)]
582pub struct ElementwiseSpec {
583    pub a: Symbol,
584    pub b: Symbol,
585    pub c: Symbol,
586    pub op: ElementwiseOp,
587    pub n: usize,
588    /// `B` is one row of this width, combined with every row of `A` (a bias over a block); `None` when the two
589    /// operands have one shape.
590    pub row: Option<usize>,
591}
592
593/// Why a program is not a recognizable elementwise op.
594#[derive(Debug, Clone, Copy, PartialEq, Eq)]
595pub enum ElementwiseReject {
596    NotElementwise,
597}
598
599/// Recognize an elementwise idiom `Push (item p of A) ⊕ (item p of B) to C` inside a counted loop — the
600/// exact shape the `plus`/`hadamard` sugar desugars to — anywhere in the statement list (through `Splice`).
601pub fn recognize_elementwise(
602    stmts: &[Stmt],
603    _interner: &Interner,
604) -> Result<ElementwiseSpec, ElementwiseReject> {
605    let mut consts: HashMap<Symbol, i64> = HashMap::new();
606    collect_consts_rec(stmts, &mut consts);
607    search_elementwise(stmts, &consts, None).ok_or(ElementwiseReject::NotElementwise)
608}
609
610fn search_elementwise(
611    stmts: &[Stmt],
612    consts: &HashMap<Symbol, i64>,
613    binder: Option<Symbol>,
614) -> Option<ElementwiseSpec> {
615    for s in stmts {
616        match s {
617            Stmt::While { cond, body, .. } => {
618                if let Some(spec) = try_elementwise_at(cond, body, consts, binder) {
619                    return Some(spec);
620                }
621                if let Some(spec) = search_elementwise(body, consts, binder) {
622                    return Some(spec);
623                }
624            }
625            Stmt::If {
626                then_block,
627                else_block,
628                ..
629            } => {
630                if let Some(spec) = search_elementwise(then_block, consts, binder) {
631                    return Some(spec);
632                }
633                if let Some(eb) = else_block {
634                    if let Some(spec) = search_elementwise(eb, consts, binder) {
635                        return Some(spec);
636                    }
637                }
638            }
639            Stmt::Splice { body } => {
640                if let Some(spec) = search_elementwise(body, consts, splice_binder(body).or(binder))
641                {
642                    return Some(spec);
643                }
644            }
645            _ => {}
646        }
647    }
648    None
649}
650
651/// `1..=N` counted loop over `iv` → `(iv, N)`; the elementwise sugar counts `p` from 1 with `p <= N`.
652fn ew_loop_bound(cond: &Expr, consts: &HashMap<Symbol, i64>) -> Option<(Symbol, i64)> {
653    let empty = HashMap::new();
654    if let Expr::BinaryOp { op, left, right } = cond {
655        if let Expr::Identifier(iv) = left {
656            let b = eval_affine(right, consts, &empty)?;
657            if b.is_const() {
658                return match op {
659                    BinaryOpKind::LtEq => Some((*iv, b.cst)), // p <= N (p from 1) → N elements
660                    BinaryOpKind::Lt => Some((*iv, b.cst - 1)), // p <  N (p from 1) → N-1 elements
661                    _ => None,
662                };
663            }
664        }
665    }
666    None
667}
668
669fn try_elementwise_at(
670    cond: &Expr,
671    body: &[Stmt],
672    consts: &HashMap<Symbol, i64>,
673    binder: Option<Symbol>,
674) -> Option<ElementwiseSpec> {
675    let (iv, n) = ew_loop_bound(cond, consts)?;
676    if n <= 0 {
677        return None;
678    }
679    for st in body {
680        let Stmt::Push { value, collection } = st else {
681            continue;
682        };
683        let c = as_ident(collection)?;
684        if !pushes_into_binder(c, binder) {
685            continue;
686        }
687        let Expr::BinaryOp { op, left, right } = value else {
688            continue;
689        };
690        let ewop = match op {
691            BinaryOpKind::Add => ElementwiseOp::Add,
692            BinaryOpKind::Multiply => ElementwiseOp::Mul,
693            _ => continue,
694        };
695        let (a_coll, a_idx) = as_index(left)?;
696        let (b_coll, b_idx) = as_index(right)?;
697        // `A` reads at the loop variable; `B` at it too (one shape), or at its column `((p − 1) mod W) + 1` (a row).
698        let row = match as_ident(b_idx) {
699            Some(i) if i == iv => Some(None),
700            _ => broadcast_width(b_idx, iv).map(Some),
701        };
702        if let (Some(row), true) = (row, as_ident(a_idx) == Some(iv)) {
703            return Some(ElementwiseSpec {
704                a: as_ident(a_coll)?,
705                b: as_ident(b_coll)?,
706                c,
707                op: ewop,
708                n: n as usize,
709                row,
710            });
711        }
712    }
713    None
714}
715
716/// `((iv − 1) mod W) + 1` — the column of a row `W` wide that element `iv` (1-based) of a block falls in. Returns `W`.
717fn broadcast_width(e: &Expr, iv: Symbol) -> Option<usize> {
718    let (m, one) = as_binop(e, BinaryOpKind::Add)?;
719    let (shifted, w) = as_binop(m, BinaryOpKind::Modulo)?;
720    let (i, one2) = as_binop(shifted, BinaryOpKind::Subtract)?;
721    let lit = |e: &Expr| match e {
722        Expr::Literal(Literal::Number(n)) => Some(*n),
723        _ => None,
724    };
725    (lit(one)? == 1 && lit(one2)? == 1 && as_ident(i)? == iv).then_some(())?;
726    lit(w).filter(|w| *w > 0).map(|w| w as usize)
727}
728
729/// A float reduction tensor op — `rmsnorm` (unit-RMS rescale) or `softmax` (probability distribution).
730#[derive(
731    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
732)]
733pub enum ReductionKind {
734    Rmsnorm,
735    Softmax,
736}
737
738impl ReductionKind {
739    pub fn name(self) -> &'static str {
740        match self {
741            ReductionKind::Rmsnorm => "rmsnorm",
742            ReductionKind::Softmax => "softmax",
743        }
744    }
745}
746
747/// The recognized shape of a reduction: source tensor `x`, destination `y`, element count `n`, and (an rmsnorm's) the
748/// divisor its normalize step divides by, whose assignment holds its epsilon.
749#[derive(Debug, Clone, Copy)]
750pub struct ReductionSpec {
751    pub kind: ReductionKind,
752    pub x: Symbol,
753    pub y: Symbol,
754    pub n: usize,
755    pub denom: Option<Symbol>,
756}
757
758/// The reduction performed per row in a **fused row op** — the extensible core of the fused-row family
759/// (LayerNorm / fused-add-RMSNorm join here as they land). Each is a shared-memory reduction over one row.
760#[derive(
761    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
762)]
763pub enum RowReduceKind {
764    /// `Y[r,i] = X[r,i] / sqrt(mean_i(X[r,·]²) + eps)` (× `weight[i]` when present) — unit-RMS rescale.
765    RmsNorm,
766    /// `Y[r,i] = exp(X[r,i]-max) / Σ exp(X[r,·]-max)` — a per-row probability distribution.
767    Softmax,
768    /// `Y[r,i] = (X[r,i]-mean_i)/sqrt(var_i+eps) * W[i] + B[i]` — per-row mean/variance normalize, scale + shift.
769    LayerNorm,
770}
771
772impl RowReduceKind {
773    pub fn name(self) -> &'static str {
774        match self {
775            RowReduceKind::RmsNorm => "rmsnorm",
776            RowReduceKind::Softmax => "softmax",
777            RowReduceKind::LayerNorm => "layernorm",
778        }
779    }
780}
781
782/// A norm's epsilon — the `E` of `sqrt(… + E)` — kept as its bits, so a spec holding one stays `Eq` and hashable.
783#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
784pub struct Epsilon(u64);
785
786impl Epsilon {
787    /// The epsilon a norm sugar takes when the program gives none.
788    pub const DEFAULT: Epsilon = Epsilon(1e-6f64.to_bits());
789
790    pub fn new(v: f64) -> Epsilon {
791        Epsilon(v.to_bits())
792    }
793
794    pub fn get(self) -> f64 {
795        f64::from_bits(self.0)
796    }
797}
798
799/// A constant factor an op multiplies by, kept as its bits, so a spec holding one stays `Eq` and hashable.
800#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
801pub struct Factor(u64);
802
803impl Factor {
804    pub fn new(v: f64) -> Factor {
805        Factor(v.to_bits())
806    }
807
808    pub fn get(self) -> f64 {
809        f64::from_bits(self.0)
810    }
811}
812
813/// The epsilon of the norm whose divisor is `denom`: the `E` of the `sqrt(… + E)` the norm's desugar assigns to it.
814pub fn epsilon_of(stmts: &[Stmt], denom: Symbol) -> Option<Epsilon> {
815    for s in stmts {
816        let found = match s {
817            Stmt::Let { var, value, .. } if *var == denom => sqrt_epsilon(value),
818            Stmt::Set { target, value } if *target == denom => sqrt_epsilon(value),
819            Stmt::While { body, .. } | Stmt::Splice { body } => epsilon_of(body, denom),
820            Stmt::If { then_block, else_block, .. } => {
821                epsilon_of(then_block, denom).or_else(|| else_block.and_then(|e| epsilon_of(e, denom)))
822            }
823            _ => None,
824        };
825        if found.is_some() {
826            return found;
827        }
828    }
829    None
830}
831
832/// The `E` of `sqrt(… + E)`.
833fn sqrt_epsilon(value: &Expr) -> Option<Epsilon> {
834    let Expr::Call { args, .. } = value else { return None };
835    let [arg] = args.as_slice() else { return None };
836    let (_, eps) = as_binop(arg, BinaryOpKind::Add)?;
837    match eps {
838        Expr::Literal(Literal::Float(f)) if *f > 0.0 => Some(Epsilon::new(*f)),
839        _ => None,
840    }
841}
842
843/// A recognized **fused row op** — a per-row reduction over a `rows × cols` tensor: one output row per input
844/// row, each normalized independently over its `cols` entries. This is the transformer-normalization shape
845/// (RMSNorm / softmax / LayerNorm over the hidden dim, batched across rows). The flat single-vector reduction
846/// [`ReductionSpec`] is exactly the `rows == 1` special case; this spec is what the one-block-per-row CUDA
847/// emitter consumes, and it grows `weight`/`residual` as the fused variants land — the durable foundation.
848#[derive(Debug, Clone, PartialEq, Eq)]
849pub struct FusedRowSpec {
850    pub rows: usize,
851    pub cols: usize,
852    pub reduce: RowReduceKind,
853    pub x: Symbol,
854    pub y: Symbol,
855    /// Per-column weight applied after the reduction (`None` for the weightless sugar).
856    pub weight: Option<Symbol>,
857    /// How many rows of `cols` the weight holds: 1, one row shared by every row; `L`, the lanes of a hyper-connected
858    /// residual, row `r` taking the weight's row `r mod L` (`rmsnorm of H in L lanes with weight W`).
859    pub weight_rows: usize,
860    /// A fused residual added element-wise BEFORE the reduction — the reduced value is `X[idx] + residual[idx]`
861    /// (`None` for plain RMSNorm). This is the "add" of fused-add-RMSNorm (FIB `001_fused_add_rmsnorm`).
862    pub residual: Option<Symbol>,
863    /// Per-column bias ADDED after the scale (`Y = … * weight + bias`) — the shift of LayerNorm (`None` for
864    /// RMSNorm, which has no bias).
865    pub bias: Option<Symbol>,
866    /// The norm's epsilon, as the program gives it ([`Epsilon::DEFAULT`] for a softmax, which has none).
867    pub eps: Epsilon,
868}
869
870/// Recognize the fused row-op idiom. First it lifts the PER-ROW WEIGHTED RMSNorm shape (a row loop whose
871/// normalize body `Push (X[idx] / denom) * W[col]` gives `x`, `weight`, `y`, and whose enclosing loop bounds
872/// give `rows`/`cols`); failing that it falls back to the weightless flat `rmsnorm`/`softmax` sugar
873/// (`rows == 1`, `cols == n`) via [`recognize_reduction`]. Backend-agnostic (CUDA + Tenstorrent).
874pub fn recognize_fused_row(
875    stmts: &[Stmt],
876    interner: &Interner,
877) -> Result<FusedRowSpec, ReductionReject> {
878    let mut consts: HashMap<Symbol, i64> = HashMap::new();
879    collect_consts_rec(stmts, &mut consts);
880    if let Some((mut spec, denom)) = search_weighted_rmsnorm(stmts, &consts, &mut Vec::new(), None) {
881        if let Some(denom) = denom {
882            spec.eps = epsilon_of(stmts, denom).ok_or(ReductionReject::NotReduction)?;
883        }
884        return Ok(spec);
885    }
886    let r = recognize_reduction(stmts, interner)?;
887    let eps = match r.kind {
888        ReductionKind::Rmsnorm => r.denom.and_then(|d| epsilon_of(stmts, d)).ok_or(ReductionReject::NotReduction)?,
889        ReductionKind::Softmax => Epsilon::DEFAULT,
890    };
891    Ok(FusedRowSpec {
892        rows: 1,
893        cols: r.n,
894        reduce: match r.kind {
895            ReductionKind::Rmsnorm => RowReduceKind::RmsNorm,
896            ReductionKind::Softmax => RowReduceKind::Softmax,
897        },
898        x: r.x,
899        y: r.y,
900        weight: None,
901        weight_rows: 1,
902        residual: None,
903        bias: None,
904        eps,
905    })
906}
907
908/// A LayerNorm-normalize body `((X[idx] - mean) / denom) * W[col] + B[col]`. Returns `(x, weight, bias, denom)`;
909/// `None` for any other shape. The top-level `+ B[col]` (bias shift) is what distinguishes it from RMSNorm.
910fn match_layernorm_push(value: &Expr) -> Option<(Symbol, Symbol, Symbol, Symbol)> {
911    let (scaled, b_index) = as_binop(value, BinaryOpKind::Add)?;
912    let (b_coll, _) = as_index(b_index)?;
913    let (div, w_index) = as_binop(scaled, BinaryOpKind::Multiply)?;
914    let (w_coll, _) = as_index(w_index)?;
915    let (sub, denom) = as_binop(div, BinaryOpKind::Divide)?;
916    // The divisor is the row's `sqrt(var + eps)` scalar — a plain identifier, never a per-element expression.
917    let denom = as_ident(denom)?;
918    let (x_read, mean) = as_binop(sub, BinaryOpKind::Subtract)?;
919    as_ident(mean)?;
920    let (x_coll, _) = as_index(x_read)?;
921    Some((as_ident(x_coll)?, as_ident(w_coll)?, as_ident(b_coll)?, denom))
922}
923
924/// A weighted-normalize body `(REDUCED / denom) * W[..]`, where `REDUCED` is `X[idx]` (plain RMSNorm) or
925/// `X[idx] + residual[idx]` (fused-add-RMSNorm). Returns `(x, residual, weight, denom)`; `None` for any other shape.
926/// Specific enough that only the (fused-add) weighted-RMSNorm desugar matches.
927fn match_weighted_push(value: &Expr) -> Option<(Symbol, Option<Symbol>, Symbol, Symbol)> {
928    let (div, w_index) = as_binop(value, BinaryOpKind::Multiply)?;
929    let (reduced, denom) = as_binop(div, BinaryOpKind::Divide)?;
930    // The divisor must be the scalar `denom` binding (a plain identifier), NOT a per-element expression — this
931    // rejects e.g. SwiGLU's `(gate / (1 + e^-gate)) * up`, whose divisor is a binop, not the row's rsqrt scalar.
932    let denom = as_ident(denom)?;
933    let (w_coll, _widx) = as_index(w_index)?;
934    let weight = as_ident(w_coll)?;
935    // Fused-add: the reduced value is `X[idx] + residual[idx]`.
936    if let Some((a, b)) = as_binop(reduced, BinaryOpKind::Add) {
937        let (x_coll, _) = as_index(a)?;
938        let (r_coll, _) = as_index(b)?;
939        return Some((as_ident(x_coll)?, Some(as_ident(r_coll)?), weight, denom));
940    }
941    // Plain: the reduced value is `X[idx]`.
942    let (x_coll, _xidx) = as_index(reduced)?;
943    Some((as_ident(x_coll)?, None, weight, denom))
944}
945
946/// Search for the weighted per-row RMSNorm shape (and the LayerNorm and per-row softmax shapes beside it), tracking
947/// the stack of enclosing loop bounds so the innermost bound is `cols` and the next-out is `rows`. Returns the spec
948/// and the norm's divisor, whose assignment holds its epsilon (none for a softmax).
949fn search_weighted_rmsnorm(
950    stmts: &[Stmt],
951    consts: &HashMap<Symbol, i64>,
952    bounds: &mut Vec<i64>,
953    binder: Option<Symbol>,
954) -> Option<(FusedRowSpec, Option<Symbol>)> {
955    for s in stmts {
956        match s {
957            Stmt::While { cond, body, .. } => {
958                let pushed = ew_loop_bound(cond, consts)
959                    .map(|(_, n)| bounds.push(n))
960                    .is_some();
961                if let Some(spec) = search_weighted_rmsnorm(body, consts, bounds, binder) {
962                    return Some(spec);
963                }
964                if pushed {
965                    bounds.pop();
966                }
967            }
968            Stmt::If {
969                then_block,
970                else_block,
971                ..
972            } => {
973                if let Some(spec) = search_weighted_rmsnorm(then_block, consts, bounds, binder) {
974                    return Some(spec);
975                }
976                if let Some(eb) = else_block {
977                    if let Some(spec) = search_weighted_rmsnorm(eb, consts, bounds, binder) {
978                        return Some(spec);
979                    }
980                }
981            }
982            Stmt::Splice { body } => {
983                if let Some(spec) =
984                    search_weighted_rmsnorm(body, consts, bounds, splice_binder(body).or(binder))
985                {
986                    return Some(spec);
987                }
988            }
989            Stmt::Push { value, collection } => {
990                let y = as_ident(collection)?;
991                if !pushes_into_binder(y, binder) {
992                    continue;
993                }
994                let cols = *bounds.last()?;
995                let rows = if bounds.len() >= 2 {
996                    bounds[bounds.len() - 2]
997                } else {
998                    1
999                };
1000                if cols == 0 || rows == 0 {
1001                    continue;
1002                }
1003                // LayerNorm first (its `+ B[col]` shift is the most specific shape), then weighted RMSNorm.
1004                if let Some((x, weight, bias, denom)) = match_layernorm_push(value) {
1005                    let spec = FusedRowSpec {
1006                        rows: rows as usize,
1007                        cols: cols as usize,
1008                        reduce: RowReduceKind::LayerNorm,
1009                        x,
1010                        y,
1011                        weight: Some(weight),
1012                        weight_rows: 1,
1013                        residual: None,
1014                        bias: Some(bias),
1015                        eps: Epsilon::DEFAULT,
1016                    };
1017                    return Some((spec, Some(denom)));
1018                }
1019                if let Some(x) = match_softmax_push(value) {
1020                    let spec = FusedRowSpec {
1021                        rows: rows as usize,
1022                        cols: cols as usize,
1023                        reduce: RowReduceKind::Softmax,
1024                        x,
1025                        y,
1026                        weight: None,
1027                        weight_rows: 1,
1028                        residual: None,
1029                        bias: None,
1030                        eps: Epsilon::DEFAULT,
1031                    };
1032                    return Some((spec, None));
1033                }
1034                if let Some((x, residual, weight, denom)) = match_weighted_push(value) {
1035                    let spec = FusedRowSpec {
1036                        rows: rows as usize,
1037                        cols: cols as usize,
1038                        reduce: RowReduceKind::RmsNorm,
1039                        x,
1040                        y,
1041                        weight: Some(weight),
1042                        weight_rows: 1,
1043                        residual,
1044                        bias: None,
1045                        eps: Epsilon::DEFAULT,
1046                    };
1047                    return Some((spec, Some(denom)));
1048                }
1049            }
1050            _ => {}
1051        }
1052    }
1053    None
1054}
1055
1056/// A per-row softmax normalize body `e^(X[idx] - m) / s` — the value the row loop pushes. Returns the
1057/// source tensor. `m` (the row max) and `s` (the row's sum of exponentials) must be plain identifiers:
1058/// they are the row scalars, and requiring them rejects any per-element divisor.
1059fn match_softmax_push(value: &Expr) -> Option<Symbol> {
1060    let (expo, s_denom) = as_binop(value, BinaryOpKind::Divide)?;
1061    as_ident(s_denom)?;
1062    let (base, shifted) = as_binop(expo, BinaryOpKind::Pow)?;
1063    if !is_euler(base) {
1064        return None;
1065    }
1066    let (x_index, m) = as_binop(shifted, BinaryOpKind::Subtract)?;
1067    as_ident(m)?;
1068    let (x_coll, _) = as_index(x_index)?;
1069    as_ident(x_coll)
1070}
1071
1072/// A recognized **SwiGLU** map: `Y[i] = silu(gate[i]) * up[i]`, `silu(g) = g / (1 + e^-g)`, over `total`
1073/// elements — the gated MLP activation (not a row-reduction; a pure two-input elementwise map).
1074#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1075pub struct SwiGluSpec {
1076    pub total: usize,
1077    pub gate: Symbol,
1078    pub up: Symbol,
1079    pub y: Symbol,
1080}
1081
1082/// Recognize the SwiGLU idiom the sugar desugars to — `Push (gate[iv] / (1 + E^(0 - gate[iv]))) * up[iv]` in a
1083/// counted loop. Backend-agnostic (CUDA + Tenstorrent).
1084pub fn recognize_swiglu(
1085    stmts: &[Stmt],
1086    _interner: &Interner,
1087) -> Result<SwiGluSpec, ReductionReject> {
1088    let mut consts: HashMap<Symbol, i64> = HashMap::new();
1089    collect_consts_rec(stmts, &mut consts);
1090    search_swiglu(stmts, &consts, None).ok_or(ReductionReject::NotReduction)
1091}
1092
1093fn search_swiglu(
1094    stmts: &[Stmt],
1095    consts: &HashMap<Symbol, i64>,
1096    binder: Option<Symbol>,
1097) -> Option<SwiGluSpec> {
1098    for s in stmts {
1099        match s {
1100            Stmt::While { cond, body, .. } => {
1101                if let Some((iv, n)) = ew_loop_bound(cond, consts) {
1102                    if n > 0 {
1103                        for st in *body {
1104                            if let Stmt::Push { value, collection } = st {
1105                                let y = as_ident(collection)?;
1106                                if !pushes_into_binder(y, binder) {
1107                                    continue;
1108                                }
1109                                if let Some((gate, up)) = match_swiglu_push(value, iv) {
1110                                    return Some(SwiGluSpec {
1111                                        total: n as usize,
1112                                        gate,
1113                                        up,
1114                                        y,
1115                                    });
1116                                }
1117                            }
1118                        }
1119                    }
1120                }
1121                if let Some(spec) = search_swiglu(body, consts, binder) {
1122                    return Some(spec);
1123                }
1124            }
1125            Stmt::If {
1126                then_block,
1127                else_block,
1128                ..
1129            } => {
1130                if let Some(spec) = search_swiglu(then_block, consts, binder) {
1131                    return Some(spec);
1132                }
1133                if let Some(eb) = else_block {
1134                    if let Some(spec) = search_swiglu(eb, consts, binder) {
1135                        return Some(spec);
1136                    }
1137                }
1138            }
1139            Stmt::Splice { body } => {
1140                if let Some(spec) = search_swiglu(body, consts, splice_binder(body).or(binder)) {
1141                    return Some(spec);
1142                }
1143            }
1144            _ => {}
1145        }
1146    }
1147    None
1148}
1149
1150/// `Push (gate[iv] / (1 + E^(0 - gate[iv]))) * up[iv]` → `(gate, up)`; both element reads index by `iv`.
1151fn match_swiglu_push(value: &Expr, iv: Symbol) -> Option<(Symbol, Symbol)> {
1152    let (silu, up_index) = as_binop(value, BinaryOpKind::Multiply)?;
1153    let (up_coll, up_idx) = as_index(up_index)?;
1154    if as_ident(up_idx) != Some(iv) {
1155        return None;
1156    }
1157    // silu(g) = g / (1 + e^(0 - g))
1158    let (g_num, denom) = as_binop(silu, BinaryOpKind::Divide)?;
1159    let (g_coll, g_idx) = as_index(g_num)?;
1160    if as_ident(g_idx) != Some(iv) {
1161        return None;
1162    }
1163    // denom = 1 + Pow(E, Subtract(0, gate[iv])) — confirm the sigmoid shape (an Add whose RHS is a Pow).
1164    let (_one, powe) = as_binop(denom, BinaryOpKind::Add)?;
1165    as_binop(powe, BinaryOpKind::Pow)?;
1166    Some((as_ident(g_coll)?, as_ident(up_coll)?))
1167}
1168
1169/// Why a program is not a recognizable reduction op.
1170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1171pub enum ReductionReject {
1172    NotReduction,
1173}
1174
1175/// Recognize the `rmsnorm` / `softmax` normalize-loop idiom the sugar desugars to — a counted loop that
1176/// `Push`es `X[p] / denom` (rmsnorm) or `e^(X[p]-m) / s` (softmax) into `Y` — anywhere (through `Splice`).
1177pub fn recognize_reduction(
1178    stmts: &[Stmt],
1179    _interner: &Interner,
1180) -> Result<ReductionSpec, ReductionReject> {
1181    let mut consts: HashMap<Symbol, i64> = HashMap::new();
1182    collect_consts_rec(stmts, &mut consts);
1183    search_reduction(stmts, &consts, None).ok_or(ReductionReject::NotReduction)
1184}
1185
1186fn search_reduction(
1187    stmts: &[Stmt],
1188    consts: &HashMap<Symbol, i64>,
1189    binder: Option<Symbol>,
1190) -> Option<ReductionSpec> {
1191    for s in stmts {
1192        match s {
1193            Stmt::While { cond, body, .. } => {
1194                if let Some(spec) = try_reduction_at(cond, body, consts, stmts, binder) {
1195                    return Some(spec);
1196                }
1197                if let Some(spec) = search_reduction(body, consts, binder) {
1198                    return Some(spec);
1199                }
1200            }
1201            Stmt::If {
1202                then_block,
1203                else_block,
1204                ..
1205            } => {
1206                if let Some(spec) = search_reduction(then_block, consts, binder) {
1207                    return Some(spec);
1208                }
1209                if let Some(eb) = else_block {
1210                    if let Some(spec) = search_reduction(eb, consts, binder) {
1211                        return Some(spec);
1212                    }
1213                }
1214            }
1215            Stmt::Splice { body } => {
1216                if let Some(spec) = search_reduction(body, consts, splice_binder(body).or(binder)) {
1217                    return Some(spec);
1218                }
1219            }
1220            _ => {}
1221        }
1222    }
1223    None
1224}
1225
1226/// The flat normalize loop of `rmsnorm` / `softmax`, with the divisor checked for the form it claims: an
1227/// rmsnorm divides by the root-mean-square scalar this block binds (`Let denom be sqrt(ss / n + eps)`), a
1228/// softmax by the row's exponential sum — never by a per-element expression, which is how `silu`'s and
1229/// `gelu`'s `x / (1 + e^…)` loops read otherwise. `scope` is the statement list the loop lives in.
1230fn try_reduction_at(
1231    cond: &Expr,
1232    body: &[Stmt],
1233    consts: &HashMap<Symbol, i64>,
1234    scope: &[Stmt],
1235    binder: Option<Symbol>,
1236) -> Option<ReductionSpec> {
1237    let (iv, n) = ew_loop_bound(cond, consts)?;
1238    if n <= 0 {
1239        return None;
1240    }
1241    for st in body {
1242        let Stmt::Push { value, collection } = st else {
1243            continue;
1244        };
1245        let y = as_ident(collection)?;
1246        if !pushes_into_binder(y, binder) {
1247            continue;
1248        }
1249        let (num, den) = as_binop(value, BinaryOpKind::Divide)?;
1250        // rmsnorm: X[iv] / denom, the divisor bound in this block to `sqrt(mean + eps)`.
1251        if let Some((coll, idx)) = as_index(num) {
1252            if as_ident(idx) == Some(iv)
1253                && as_ident(den).is_some_and(|d| binds_rms_denominator(scope, d))
1254            {
1255                return Some(ReductionSpec {
1256                    kind: ReductionKind::Rmsnorm,
1257                    x: as_ident(coll)?,
1258                    y,
1259                    n: n as usize,
1260                    denom: as_ident(den),
1261                });
1262            }
1263        }
1264        // softmax: e^(X[iv] - m) / s, with `m` (the row max) and `s` (the exponential sum) the row scalars.
1265        if let Some((base, exponent)) = as_binop(num, BinaryOpKind::Pow) {
1266            if is_euler(base) && as_ident(den).is_some() {
1267                if let Some((subl, m)) = as_binop(exponent, BinaryOpKind::Subtract) {
1268                    if as_ident(m).is_some() {
1269                        if let Some((coll, idx)) = as_index(subl) {
1270                            if as_ident(idx) == Some(iv) {
1271                                return Some(ReductionSpec {
1272                                    kind: ReductionKind::Softmax,
1273                                    x: as_ident(coll)?,
1274                                    y,
1275                                    n: n as usize,
1276                                    denom: None,
1277                                });
1278                            }
1279                        }
1280                    }
1281                }
1282            }
1283        }
1284    }
1285    None
1286}
1287
1288/// The tensor a desugar binds — the first `Let var be a new Seq` of its `Splice` body. Every Push-based
1289/// recognizer lifts only pushes into that binder while inside the block, so a loop that is a fragment of a
1290/// larger sugar (the router softmax inside `moe`) is never taken for the whole program.
1291fn splice_binder(body: &[Stmt]) -> Option<Symbol> {
1292    body.iter().find_map(|s| match s {
1293        Stmt::Let { var, value, .. } if matches!(value, Expr::New { .. }) => Some(*var),
1294        _ => None,
1295    })
1296}
1297
1298fn pushes_into_binder(collection: Symbol, binder: Option<Symbol>) -> bool {
1299    binder.map(|b| b == collection).unwrap_or(true)
1300}
1301
1302fn is_euler(e: &Expr) -> bool {
1303    matches!(e, Expr::Literal(Literal::Float(v)) if (*v - std::f64::consts::E).abs() < 1e-9)
1304}
1305
1306/// Does `scope` bind `d` to the rmsnorm denominator `sqrt(<sum> / <n> + <eps>)`?
1307fn binds_rms_denominator(scope: &[Stmt], d: Symbol) -> bool {
1308    scope.iter().any(|s| match s {
1309        Stmt::Let {
1310            var,
1311            value: Expr::Call { args, .. },
1312            ..
1313        } if *var == d => match args.as_slice() {
1314            [arg] => as_binop(arg, BinaryOpKind::Add)
1315                .and_then(|(mean, eps)| Some((as_binop(mean, BinaryOpKind::Divide)?, eps)))
1316                .map(|((ss, _n), eps)| {
1317                    as_ident(ss).is_some() && matches!(eps, Expr::Literal(Literal::Float(_)))
1318                })
1319                .unwrap_or(false),
1320            _ => false,
1321        },
1322        _ => false,
1323    })
1324}
1325
1326/// Try every loop nest (and nested blocks) as a matmul root; return the first match, else the most-advanced
1327/// refusal seen (so a triple-loop-with-wrong-indices reports `NotGemmLayout`, not `NoLoopNest`).
1328fn search_matmul(
1329    stmts: &[Stmt],
1330    consts: &HashMap<Symbol, i64>,
1331) -> Result<MatmulSpec, MatmulReject> {
1332    let mut best = MatmulReject::NoLoopNest;
1333    for s in stmts {
1334        match s {
1335            Stmt::While { cond, body, .. } => {
1336                match try_matmul_at(cond, body, consts) {
1337                    Ok(spec) => return Ok(spec),
1338                    Err(e) => {
1339                        if reject_rank(&e) > reject_rank(&best) {
1340                            best = e;
1341                        }
1342                    }
1343                }
1344                if let Ok(spec) = search_matmul(body, consts) {
1345                    return Ok(spec);
1346                }
1347            }
1348            Stmt::If {
1349                then_block,
1350                else_block,
1351                ..
1352            } => {
1353                if let Ok(spec) = search_matmul(then_block, consts) {
1354                    return Ok(spec);
1355                }
1356                if let Some(eb) = else_block {
1357                    if let Ok(spec) = search_matmul(eb, consts) {
1358                        return Ok(spec);
1359                    }
1360                }
1361            }
1362            // Parser desugars (tensor sugar, tuple destructure, …) emit their statements inside a transparent
1363            // `Stmt::Splice`; look through it so a matmul written as `Let C be A matmul B` is recognized.
1364            Stmt::Splice { body } => {
1365                if let Ok(spec) = search_matmul(body, consts) {
1366                    return Ok(spec);
1367                }
1368            }
1369            _ => {}
1370        }
1371    }
1372    Err(best)
1373}
1374
1375/// Try to recognize a matmul with the given `While` as the outer (row / `i`) loop.
1376fn try_matmul_at(
1377    cond_i: &Expr,
1378    body_i: &[Stmt],
1379    consts: &HashMap<Symbol, i64>,
1380) -> Result<MatmulSpec, MatmulReject> {
1381    let (iv_i, m) = loop_iv_bound(cond_i, consts).ok_or(MatmulReject::NoLoopNest)?;
1382    let (cond_k, body_k) = first_while(body_i).ok_or(MatmulReject::NotTripleNested)?;
1383    let (iv_k, k) = loop_iv_bound(cond_k, consts).ok_or(MatmulReject::NotTripleNested)?;
1384    let (cond_j, body_j) = first_while(body_k).ok_or(MatmulReject::NotTripleNested)?;
1385    let (iv_j, n) = loop_iv_bound(cond_j, consts).ok_or(MatmulReject::NotTripleNested)?;
1386
1387    // Inner `Let` bindings (e.g. `Let idx be i*n + j + 1.`) captured as affine forms so the store's index
1388    // expressions resolve even when they are named.
1389    let mut lets: HashMap<Symbol, Affine> = HashMap::new();
1390    for s in body_j {
1391        if let Stmt::Let { var, value, .. } = s {
1392            if let Some(a) = eval_affine(value, consts, &lets) {
1393                lets.insert(*var, a);
1394            }
1395        }
1396    }
1397
1398    // The accumulating indexed store into C.
1399    let (store_coll, store_idx, store_val) = body_j
1400        .iter()
1401        .find_map(|s| match s {
1402            Stmt::SetIndex {
1403                collection,
1404                index,
1405                value,
1406            } => Some((*collection, *index, *value)),
1407            _ => None,
1408        })
1409        .ok_or(MatmulReject::NoAccumulatingStore)?;
1410
1411    let c = as_ident(store_coll).ok_or(MatmulReject::NotGemmLayout)?;
1412    let c_idx = eval_affine(store_idx, consts, &lets).ok_or(MatmulReject::NonAffineIndex)?;
1413
1414    // value = (item c_idx of C) + (item a_idx of A) * (item b_idx of B)
1415    let (add_l, add_r) =
1416        as_binop(store_val, BinaryOpKind::Add).ok_or(MatmulReject::NoAccumulatingStore)?;
1417    let (acc_coll, acc_idx) = as_index(add_l).ok_or(MatmulReject::NoAccumulatingStore)?;
1418    if as_ident(acc_coll) != Some(c) {
1419        return Err(MatmulReject::NoAccumulatingStore);
1420    }
1421    let acc_idx_aff = eval_affine(acc_idx, consts, &lets).ok_or(MatmulReject::NonAffineIndex)?;
1422    if acc_idx_aff != c_idx {
1423        return Err(MatmulReject::NoAccumulatingStore); // must be C[idx] += … (same index read + written)
1424    }
1425
1426    let (mul_l, mul_r) =
1427        as_binop(add_r, BinaryOpKind::Multiply).ok_or(MatmulReject::NotGemmLayout)?;
1428    let (a_coll, a_idx_e) = as_index(mul_l).ok_or(MatmulReject::NotGemmLayout)?;
1429    let (b_coll, b_idx_e) = as_index(mul_r).ok_or(MatmulReject::NotGemmLayout)?;
1430    let a = as_ident(a_coll).ok_or(MatmulReject::NotGemmLayout)?;
1431    let b = as_ident(b_coll).ok_or(MatmulReject::NotGemmLayout)?;
1432    let a_idx = eval_affine(a_idx_e, consts, &lets).ok_or(MatmulReject::NonAffineIndex)?;
1433    let b_idx = eval_affine(b_idx_e, consts, &lets).ok_or(MatmulReject::NonAffineIndex)?;
1434
1435    // Row-major GEMM layout, verified against the loop bounds:
1436    //   C[i·N + j]  (i-stride N, j-stride 1, no k)
1437    //   A[i·K + k]  (i-stride K, k-stride 1, no j)
1438    //   B[k·N + j]  (k-stride N, j-stride 1, no i)
1439    let n_i = n;
1440    let k_i = k;
1441    let gemm_c =
1442        c_idx.coeff_of(iv_i) == n_i && c_idx.coeff_of(iv_j) == 1 && c_idx.coeff_of(iv_k) == 0;
1443    let gemm_a =
1444        a_idx.coeff_of(iv_i) == k_i && a_idx.coeff_of(iv_k) == 1 && a_idx.coeff_of(iv_j) == 0;
1445    let gemm_b =
1446        b_idx.coeff_of(iv_k) == n_i && b_idx.coeff_of(iv_j) == 1 && b_idx.coeff_of(iv_i) == 0;
1447    if !(gemm_c && gemm_a && gemm_b) {
1448        return Err(MatmulReject::NotGemmLayout);
1449    }
1450
1451    Ok(MatmulSpec {
1452        m: m as usize,
1453        n: n as usize,
1454        k: k as usize,
1455        a,
1456        b,
1457        c,
1458    })
1459}
1460
1461/// A recognized dot product `acc = Σ_{t<n} A[t]·B[t]`: one counted loop accumulating the product of two
1462/// declared `Seq of Int` arrays read at the same unit-stride index.
1463#[derive(Debug, Clone, PartialEq, Eq)]
1464pub struct DotSpec {
1465    pub n: usize,
1466    pub a: Symbol,
1467    pub b: Symbol,
1468    pub acc: Symbol,
1469}
1470
1471/// Why a program is not a recognizable dot product — a typed refusal, never a miscompile. Ordered from the
1472/// least to the most advanced, so a search reports the refusal of the loop that came closest: what a loop's
1473/// body computes ranks before whether its counter is provable, so a dot product with an unprovable counter
1474/// outranks a loop that accumulates nothing.
1475#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1476pub enum DotReject {
1477    /// No `While counter is less than BOUND` loop.
1478    NoLoop,
1479    /// No `Set s to s + …` in the loop body.
1480    NoAccumulation,
1481    /// The accumulated term is not `item _ of A * item _ of B`.
1482    NoProduct,
1483    /// A product, but not of two declared `Seq of Int` arrays — the integer dot product the app computes.
1484    NotInt,
1485    /// An index is not affine in the counter.
1486    NonAffineIndex,
1487    /// The two operands are not read at one unit-stride index of the counter.
1488    NotAligned,
1489    /// The counter is not set to a constant right before the loop, or is not stepped by exactly one.
1490    NotCounted,
1491    /// The loop body does more than the dot product.
1492    ExtraWork,
1493    /// The accumulator is not set to zero right before the loop.
1494    NotFromZero,
1495}
1496
1497/// A recognized outer product `C[i·n + j] = A[i]·B[j]` over `i < m`, `j < n`: a counted nest storing (not
1498/// accumulating) the product of two `Seq of Int` arrays into a row-major `m × n` array.
1499#[derive(Debug, Clone, PartialEq, Eq)]
1500pub struct OuterSpec {
1501    pub m: usize,
1502    pub n: usize,
1503    pub a: Symbol,
1504    pub b: Symbol,
1505    pub c: Symbol,
1506}
1507
1508/// Why a program is not a recognizable outer product, least to most advanced.
1509#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1510pub enum OuterReject {
1511    /// No `While counter is less than BOUND` loop.
1512    NoLoopNest,
1513    /// A counted loop with no counted loop directly inside it.
1514    NotDoubleNested,
1515    /// No `Set item _ of C to …` in the inner loop.
1516    NoStore,
1517    /// The stored value is not `item _ of A * item _ of B`.
1518    NoProduct,
1519    /// A product, but `A`, `B` and `C` are not all declared `Seq of Int` arrays — the integer product the app computes.
1520    NotInt,
1521    /// `C[idx] := C[idx] + A·B` — a rank-1 update, not an outer product.
1522    Accumulates,
1523    /// `C[idx] := A·B` where a rank-1 update (`C[idx] := C[idx] + A·B`) is read.
1524    Stores,
1525    /// An index is not affine in the counters.
1526    NonAffineIndex,
1527    /// A counter not set to a constant right before its loop, or not stepped by exactly one.
1528    NotCounted,
1529    /// Not `C[i·n + j]`, `A[i]`, `B[j]` with unit strides against the loop bounds.
1530    NotOuterLayout,
1531    /// A loop body does more than the outer product.
1532    ExtraWork,
1533}
1534
1535/// A counted loop: its counter and how many times its body runs.
1536struct Counted {
1537    iv: Symbol,
1538    trips: i64,
1539}
1540
1541/// The constant each variable holds as the statement at `stmts[at]` begins, read from the contiguous run of
1542/// `Let`/`Set` statements right before it: the nearest write wins, and a non-constant write hides every
1543/// earlier one.
1544fn entry_values(
1545    stmts: &[Stmt],
1546    at: usize,
1547    consts: &HashMap<Symbol, i64>,
1548) -> HashMap<Symbol, Option<i64>> {
1549    let empty = HashMap::new();
1550    let mut out: HashMap<Symbol, Option<i64>> = HashMap::new();
1551    for s in stmts[..at].iter().rev() {
1552        let (var, value) = match s {
1553            Stmt::Let { var, value, .. } => (*var, *value),
1554            Stmt::Set { target, value } => (*target, *value),
1555            _ => break,
1556        };
1557        out.entry(var).or_insert_with(|| {
1558            eval_affine(value, consts, &empty)
1559                .filter(Affine::is_const)
1560                .map(|a| a.cst)
1561        });
1562    }
1563    out
1564}
1565
1566/// `Set iv to iv + 1` (either operand order).
1567fn is_unit_step(s: &Stmt, iv: Symbol) -> bool {
1568    match s {
1569        Stmt::Set { target, value } if *target == iv => as_binop(value, BinaryOpKind::Add)
1570            .map_or(false, |(l, r)| {
1571                let one = |e: &Expr| matches!(e, Expr::Literal(Literal::Number(1)));
1572                (as_ident(l) == Some(iv) && one(r)) || (one(l) && as_ident(r) == Some(iv))
1573            }),
1574        _ => false,
1575    }
1576}
1577
1578/// The loop `While iv is less than BOUND` at `stmts[at]` as a counted loop: `iv` set to a constant right
1579/// before it, and its one top-level write in the body `Set iv to iv + 1`.
1580fn counted_at(
1581    stmts: &[Stmt],
1582    at: usize,
1583    (iv, bound): (Symbol, i64),
1584    body: &[Stmt],
1585    consts: &HashMap<Symbol, i64>,
1586) -> Option<Counted> {
1587    let start = entry_values(stmts, at, consts).get(&iv).copied().flatten()?;
1588    let writes = |s: &&Stmt| {
1589        matches!(s, Stmt::Let { var, .. } if *var == iv)
1590            || matches!(s, Stmt::Set { target, .. } if *target == iv)
1591    };
1592    let steps = body.iter().filter(|s| is_unit_step(s, iv)).count();
1593    (steps == 1 && body.iter().filter(writes).count() == 1 && bound > start).then(|| Counted {
1594        iv,
1595        trips: bound - start,
1596    })
1597}
1598
1599/// Try `at` on every `While` of a block and of the blocks nested in it (loop bodies, an `If`'s arms, a
1600/// `Splice`): the first match wins, else the most advanced refusal seen (`floor` when no loop was tried).
1601fn search_loops<'a, T, R: Ord + Copy>(
1602    stmts: &'a [Stmt<'a>],
1603    floor: R,
1604    at: &impl Fn(&'a [Stmt<'a>], usize, &'a Expr<'a>, &'a [Stmt<'a>]) -> Result<T, R>,
1605) -> Result<T, R> {
1606    let mut best = floor;
1607    for (k, s) in stmts.iter().enumerate() {
1608        let nested: Vec<&'a [Stmt<'a>]> = match s {
1609            Stmt::While { cond, body, .. } => {
1610                match at(stmts, k, cond, body) {
1611                    Ok(spec) => return Ok(spec),
1612                    Err(e) => best = best.max(e),
1613                }
1614                vec![*body]
1615            }
1616            Stmt::If {
1617                then_block,
1618                else_block,
1619                ..
1620            } => std::iter::once(*then_block).chain(*else_block).collect(),
1621            Stmt::Splice { body } => vec![*body],
1622            _ => Vec::new(),
1623        };
1624        for block in nested {
1625            match search_loops(block, floor, at) {
1626                Ok(spec) => return Ok(spec),
1627                Err(e) => best = best.max(e),
1628            }
1629        }
1630    }
1631    Err(best)
1632}
1633
1634/// The immutable `Let` bindings in scope, in order — an outer loop's before its inner loop's — as affine
1635/// forms (for indices) and as expressions (for an operand or product named before it is used).
1636fn body_lets<'a>(
1637    scope: impl IntoIterator<Item = &'a Stmt<'a>>,
1638    consts: &HashMap<Symbol, i64>,
1639) -> (HashMap<Symbol, Affine>, HashMap<Symbol, &'a Expr<'a>>) {
1640    let mut affine = HashMap::new();
1641    let mut exprs = HashMap::new();
1642    for s in scope {
1643        if let Stmt::Let {
1644            var,
1645            value,
1646            mutable: false,
1647            ..
1648        } = s
1649        {
1650            if let Some(a) = eval_affine(value, consts, &affine) {
1651                affine.insert(*var, a);
1652            }
1653            exprs.insert(*var, *value);
1654        }
1655    }
1656    (affine, exprs)
1657}
1658
1659/// `e` with a body-`Let` name replaced by the expression it was bound to.
1660fn through_lets<'a>(e: &'a Expr<'a>, exprs: &HashMap<Symbol, &'a Expr<'a>>) -> &'a Expr<'a> {
1661    as_ident(e)
1662        .and_then(|s| exprs.get(&s).copied())
1663        .unwrap_or(e)
1664}
1665
1666/// `item IA of A * item IB of B` (through body `Let`s), as `(A, IA, B, IB)`.
1667fn indexed_product<'a>(
1668    e: &'a Expr<'a>,
1669    exprs: &HashMap<Symbol, &'a Expr<'a>>,
1670) -> Option<(Symbol, &'a Expr<'a>, Symbol, &'a Expr<'a>)> {
1671    let (l, r) = as_binop(through_lets(e, exprs), BinaryOpKind::Multiply)?;
1672    let (ac, ai) = as_index(through_lets(l, exprs))?;
1673    let (bc, bi) = as_index(through_lets(r, exprs))?;
1674    Some((as_ident(ac)?, ai, as_ident(bc)?, bi))
1675}
1676
1677/// Whether a body statement is an index or product name: an immutable `Let` that rebinds none of `kept`.
1678fn is_body_let(s: &Stmt, kept: &[Symbol]) -> bool {
1679    matches!(s, Stmt::Let { var, mutable: false, .. } if !kept.contains(var))
1680}
1681
1682/// Recognize a dot product anywhere in a LOGOS statement list:
1683///
1684/// ```text
1685/// Let mutable s be 0.
1686/// Let mutable i be 0.
1687/// While i is less than n:
1688///     Set s to s + item (i + 1) of a * item (i + 1) of b.
1689///     Set i to i + 1.
1690/// ```
1691///
1692/// A dot product is the [zip sum](recognize_zip_sum) whose term is the product of two reads: the loop is
1693/// counted, the accumulator starts at zero right before it, both operands are read at the same unit-stride
1694/// index, and the body holds nothing else but immutable index or product names. The search returns the
1695/// refusal of the loop that came closest when nothing matches.
1696pub fn recognize_dot(stmts: &[Stmt], interner: &Interner) -> Result<DotSpec, DotReject> {
1697    let mut consts: HashMap<Symbol, i64> = HashMap::new();
1698    collect_consts_rec(stmts, &mut consts);
1699    let program = Program::of(stmts, interner);
1700    search_loops(stmts, DotReject::NoLoop, &|block, at, cond, body| {
1701        let zip = try_zip_at(block, at, cond, body, &consts, &program).map_err(DotReject::from)?;
1702        match zip.term {
1703            ZipTerm::Op(ZipOp::Mul, ref l, ref r) => match (l.as_ref(), r.as_ref()) {
1704                (ZipTerm::Read(x), ZipTerm::Read(y)) => Ok(DotSpec {
1705                    n: zip.n,
1706                    a: zip.operands[*x],
1707                    b: zip.operands[*y],
1708                    acc: zip.acc,
1709                }),
1710                _ => Err(DotReject::NoProduct),
1711            },
1712            _ => Err(DotReject::NoProduct),
1713        }
1714    })
1715}
1716
1717impl From<ZipReject> for DotReject {
1718    fn from(z: ZipReject) -> DotReject {
1719        match z {
1720            ZipReject::NoLoop => DotReject::NoLoop,
1721            ZipReject::NoAccumulation => DotReject::NoAccumulation,
1722            ZipReject::UnsupportedTerm => DotReject::NoProduct,
1723            ZipReject::NotInt => DotReject::NotInt,
1724            ZipReject::NonAffineIndex => DotReject::NonAffineIndex,
1725            ZipReject::NotAligned => DotReject::NotAligned,
1726            ZipReject::NotCounted => DotReject::NotCounted,
1727            ZipReject::ExtraWork => DotReject::ExtraWork,
1728            ZipReject::NotFromZero => DotReject::NotFromZero,
1729        }
1730    }
1731}
1732
1733/// An operator of a [`ZipTerm`]: arithmetic, a comparison (a boolean), or a boolean connective. On two `Int`s
1734/// arithmetic is `Int` (`/` truncating toward zero, `**` exact); with a `Real` operand it is `Real` — the VM's rules.
1735#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1736pub enum ZipOp {
1737    Add,
1738    Sub,
1739    Mul,
1740    Div,
1741    Pow,
1742    Lt,
1743    Gt,
1744    Le,
1745    Ge,
1746    Eq,
1747    Ne,
1748    And,
1749    Or,
1750}
1751
1752impl ZipOp {
1753    fn of(op: BinaryOpKind) -> Option<ZipOp> {
1754        Some(match op {
1755            BinaryOpKind::Add => ZipOp::Add,
1756            BinaryOpKind::Subtract => ZipOp::Sub,
1757            BinaryOpKind::Multiply => ZipOp::Mul,
1758            BinaryOpKind::Divide => ZipOp::Div,
1759            BinaryOpKind::Pow => ZipOp::Pow,
1760            BinaryOpKind::Lt => ZipOp::Lt,
1761            BinaryOpKind::Gt => ZipOp::Gt,
1762            BinaryOpKind::LtEq => ZipOp::Le,
1763            BinaryOpKind::GtEq => ZipOp::Ge,
1764            BinaryOpKind::Eq => ZipOp::Eq,
1765            BinaryOpKind::NotEq => ZipOp::Ne,
1766            BinaryOpKind::And => ZipOp::And,
1767            BinaryOpKind::Or => ZipOp::Or,
1768            _ => return None,
1769        })
1770    }
1771
1772    /// Whether the operator compares two numbers.
1773    pub fn compares(self) -> bool {
1774        matches!(self, ZipOp::Lt | ZipOp::Gt | ZipOp::Le | ZipOp::Ge | ZipOp::Eq | ZipOp::Ne)
1775    }
1776
1777    /// Whether the operator joins two booleans.
1778    pub fn connects(self) -> bool {
1779        matches!(self, ZipOp::And | ZipOp::Or)
1780    }
1781}
1782
1783/// The type of a [`ZipTerm`], or of an array element it reads: LOGOS's 64-bit `Int`, its `Real` (a double) and `Bool`.
1784#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1785pub enum Ty {
1786    Int,
1787    Real,
1788    Bool,
1789}
1790
1791/// A scalar a [`ZipTerm`] takes, as the VM holds it.
1792#[derive(Debug, Clone, Copy, PartialEq)]
1793pub enum Scalar {
1794    Int(i64),
1795    Real(f64),
1796    Bool(bool),
1797}
1798
1799/// Why [`ZipTerm::value`] has no value: an `Int` left the range asked for, a `Real` is not finite or left its bound,
1800/// a divisor is zero, an exponent negative, or an operator met an operand it does not take.
1801#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1802pub enum Stop {
1803    IntRange,
1804    RealRange,
1805    ZeroDivisor,
1806    NegativeExponent,
1807    Mistyped,
1808}
1809
1810/// The term a zip loop computes from the elements it reads at the counter's index: arithmetic, comparisons and
1811/// selects over array elements and constants. A guarded accumulation (`If P: Set s to s + E`) is the select
1812/// `P ? E : 0`; a call of one of the program's pure functions is its body, inlined.
1813#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1814pub enum ZipTerm {
1815    /// The element of operand `k` (the `k`-th distinct array the loop reads, in reading order).
1816    Read(usize),
1817    Const(i64),
1818    /// A `Real` constant, by its bits.
1819    Real(u64),
1820    Op(ZipOp, Box<ZipTerm>, Box<ZipTerm>),
1821    /// `cond ? then : else`, `cond` a boolean.
1822    Select(Box<ZipTerm>, Box<ZipTerm>, Box<ZipTerm>),
1823    /// `abs(x)`.
1824    Abs(Box<ZipTerm>),
1825}
1826
1827impl ZipTerm {
1828    /// The `Real` constant `v`.
1829    pub fn real(v: f64) -> ZipTerm {
1830        ZipTerm::Real(v.to_bits())
1831    }
1832
1833    /// The term's type when operand `k` holds a `reads[k]`; `None` when an operator sees an operand it does not
1834    /// take, a select's guard is not a boolean or its arms differ in type, or the term is a boolean select.
1835    pub fn ty(&self, reads: &[Ty]) -> Option<Ty> {
1836        match self {
1837            ZipTerm::Read(k) => reads.get(*k).copied(),
1838            ZipTerm::Const(_) => Some(Ty::Int),
1839            ZipTerm::Real(_) => Some(Ty::Real),
1840            ZipTerm::Abs(x) => abs_ty(x.ty(reads)?),
1841            ZipTerm::Op(op, l, r) => op_ty(*op, l.ty(reads)?, r.ty(reads)?),
1842            ZipTerm::Select(c, a, b) => select_ty(c.ty(reads)?, a.ty(reads)?, b.ty(reads)?),
1843        }
1844    }
1845
1846    /// The term's value at one element, every operand an `Int` (`row[k]` the element of operand `k`), booleans
1847    /// as 0 and 1; `None` when an intermediate value leaves 64 bits or it has none.
1848    pub fn eval(&self, row: &[i64]) -> Option<i64> {
1849        self.eval_within(row, i64::MIN as i128, i64::MAX as i128)
1850    }
1851
1852    /// [`Self::eval`] with every intermediate value held to `lo..=hi` (`None` when one leaves it).
1853    pub fn eval_within(&self, row: &[i64], lo: i128, hi: i128) -> Option<i64> {
1854        let row: Vec<Scalar> = row.iter().map(|v| Scalar::Int(*v)).collect();
1855        match self.value(&row, (lo, hi), f64::MAX).ok()? {
1856            Scalar::Int(v) => Some(v),
1857            Scalar::Bool(b) => Some(b as i64),
1858            Scalar::Real(_) => None,
1859        }
1860    }
1861
1862    /// The term's value at one element (`row[k]` the element of operand `k`) as the VM computes it, every `Int`
1863    /// held to `ints` and every `Real` finite and within `±reals`. Only the arm a select takes is evaluated, as
1864    /// the VM's `If` does (and a `mux` keeps).
1865    pub fn value(&self, row: &[Scalar], ints: (i128, i128), reals: f64) -> Result<Scalar, Stop> {
1866        match self {
1867            ZipTerm::Read(k) => bounded(*row.get(*k).ok_or(Stop::Mistyped)?, ints, reals),
1868            ZipTerm::Const(c) => bounded(Scalar::Int(*c), ints, reals),
1869            ZipTerm::Real(b) => bounded(Scalar::Real(f64::from_bits(*b)), ints, reals),
1870            ZipTerm::Abs(x) => abs_of(x.value(row, ints, reals)?, ints, reals),
1871            ZipTerm::Select(c, a, b) => match c.value(row, ints, reals)? {
1872                Scalar::Bool(true) => a.value(row, ints, reals),
1873                Scalar::Bool(false) => b.value(row, ints, reals),
1874                _ => Err(Stop::Mistyped),
1875            },
1876            ZipTerm::Op(op, l, r) => apply(*op, l.value(row, ints, reals)?, r.value(row, ints, reals)?, ints, reals),
1877        }
1878    }
1879}
1880
1881/// The type `op` gives operands of types `a` and `b`, by the VM's rules; `None` when it does not take them.
1882fn op_ty(op: ZipOp, a: Ty, b: Ty) -> Option<Ty> {
1883    let num = |t: Ty| t != Ty::Bool;
1884    if op.connects() {
1885        (a == Ty::Bool && b == Ty::Bool).then_some(Ty::Bool)
1886    } else if !(num(a) && num(b)) {
1887        None
1888    } else if op.compares() {
1889        Some(Ty::Bool)
1890    } else if a == Ty::Real || b == Ty::Real {
1891        Some(Ty::Real)
1892    } else {
1893        Some(Ty::Int)
1894    }
1895}
1896
1897/// A select's type: its arms', when its guard is a boolean and its arms are one numeric type.
1898fn select_ty(c: Ty, a: Ty, b: Ty) -> Option<Ty> {
1899    (c == Ty::Bool && a == b && a != Ty::Bool).then_some(a)
1900}
1901
1902fn abs_ty(x: Ty) -> Option<Ty> {
1903    (x != Ty::Bool).then_some(x)
1904}
1905
1906/// `v` when it is an `Int` within `ints`, a finite `Real` within `±reals`, or a boolean.
1907fn bounded(v: Scalar, ints: (i128, i128), reals: f64) -> Result<Scalar, Stop> {
1908    match v {
1909        Scalar::Int(i) if !(ints.0 <= i as i128 && i as i128 <= ints.1) => Err(Stop::IntRange),
1910        Scalar::Real(r) if !(r.is_finite() && r.abs() <= reals) => Err(Stop::RealRange),
1911        v => Ok(v),
1912    }
1913}
1914
1915fn abs_of(v: Scalar, ints: (i128, i128), reals: f64) -> Result<Scalar, Stop> {
1916    match v {
1917        Scalar::Int(i) => {
1918            let a = (i as i128).abs();
1919            if ints.0 <= a && a <= ints.1 {
1920                Ok(Scalar::Int(a as i64))
1921            } else {
1922                Err(Stop::IntRange)
1923            }
1924        }
1925        Scalar::Real(r) => bounded(Scalar::Real(r.abs()), ints, reals),
1926        Scalar::Bool(_) => Err(Stop::Mistyped),
1927    }
1928}
1929
1930/// `a op b` as the VM computes it: two `Int`s exactly (in 128 bits, then held to `ints`), `/` truncating toward
1931/// zero and refusing a zero divisor, `**` refusing a negative exponent; an `Int` beside a `Real` promoted to it.
1932fn apply(op: ZipOp, a: Scalar, b: Scalar, ints: (i128, i128), reals: f64) -> Result<Scalar, Stop> {
1933    let int = |v: i128| {
1934        if ints.0 <= v && v <= ints.1 {
1935            Ok(Scalar::Int(v as i64))
1936        } else {
1937            Err(Stop::IntRange)
1938        }
1939    };
1940    let cmp = |o: std::cmp::Ordering| {
1941        use std::cmp::Ordering::*;
1942        Scalar::Bool(match op {
1943            ZipOp::Lt => o == Less,
1944            ZipOp::Gt => o == Greater,
1945            ZipOp::Le => o != Greater,
1946            ZipOp::Ge => o != Less,
1947            ZipOp::Eq => o == Equal,
1948            _ => o != Equal,
1949        })
1950    };
1951    match (a, b) {
1952        (Scalar::Bool(x), Scalar::Bool(y)) => match op {
1953            ZipOp::And => Ok(Scalar::Bool(x && y)),
1954            ZipOp::Or => Ok(Scalar::Bool(x || y)),
1955            _ => Err(Stop::Mistyped),
1956        },
1957        (Scalar::Bool(_), _) | (_, Scalar::Bool(_)) => Err(Stop::Mistyped),
1958        _ if op.connects() => Err(Stop::Mistyped),
1959        (Scalar::Int(x), Scalar::Int(y)) => {
1960            let (x, y) = (x as i128, y as i128);
1961            match op {
1962                ZipOp::Add => int(x + y),
1963                ZipOp::Sub => int(x - y),
1964                ZipOp::Mul => int(x * y),
1965                ZipOp::Div if y == 0 => Err(Stop::ZeroDivisor),
1966                ZipOp::Div => int(x / y),
1967                ZipOp::Pow if y < 0 => Err(Stop::NegativeExponent),
1968                ZipOp::Pow => u32::try_from(y).ok().and_then(|e| x.checked_pow(e)).ok_or(Stop::IntRange).and_then(int),
1969                _ => Ok(cmp(x.cmp(&y))),
1970            }
1971        }
1972        (a, b) => {
1973            let f = |v: Scalar| match v {
1974                Scalar::Int(i) => i as f64,
1975                Scalar::Real(r) => r,
1976                Scalar::Bool(_) => unreachable!("booleans are refused above"),
1977            };
1978            let (x, y) = (f(a), f(b));
1979            let real = |v: f64| bounded(Scalar::Real(v), ints, reals);
1980            match op {
1981                ZipOp::Add => real(x + y),
1982                ZipOp::Sub => real(x - y),
1983                ZipOp::Mul => real(x * y),
1984                ZipOp::Div => real(x / y),
1985                ZipOp::Pow => real(x.powf(y)),
1986                _ => x.partial_cmp(&y).map(cmp).ok_or(Stop::RealRange),
1987            }
1988        }
1989    }
1990}
1991
1992/// A node of a [`TermDag`]: a [`ZipTerm`] node whose operands are nodes, by index.
1993#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1994pub enum DagNode {
1995    Read(usize),
1996    Const(i64),
1997    Real(u64),
1998    Op(ZipOp, usize, usize),
1999    Select(usize, usize, usize),
2000    Abs(usize),
2001}
2002
2003/// A term as a DAG: each distinct subterm once, every node after its operands — how a large term (a function
2004/// inlined at each call) is evaluated per element and printed with each shared subterm bound once.
2005#[derive(Debug, Clone, PartialEq, Eq)]
2006pub struct TermDag {
2007    pub nodes: Vec<DagNode>,
2008    /// How many times each node is an operand of another.
2009    pub uses: Vec<usize>,
2010    pub root: usize,
2011}
2012
2013impl TermDag {
2014    pub fn of(term: &ZipTerm) -> TermDag {
2015        fn intern(t: &ZipTerm, nodes: &mut Vec<DagNode>, index: &mut HashMap<DagNode, usize>) -> usize {
2016            let mut child = |c: &ZipTerm| intern(c, nodes, index);
2017            let node = match t {
2018                ZipTerm::Read(k) => DagNode::Read(*k),
2019                ZipTerm::Const(c) => DagNode::Const(*c),
2020                ZipTerm::Real(b) => DagNode::Real(*b),
2021                ZipTerm::Abs(x) => DagNode::Abs(child(x)),
2022                ZipTerm::Op(op, l, r) => {
2023                    let l = child(l);
2024                    DagNode::Op(*op, l, child(r))
2025                }
2026                ZipTerm::Select(c, a, b) => {
2027                    let (c, a) = (child(c), child(a));
2028                    DagNode::Select(c, a, child(b))
2029                }
2030            };
2031            *index.entry(node).or_insert_with(|| {
2032                nodes.push(node);
2033                nodes.len() - 1
2034            })
2035        }
2036        let (mut nodes, mut index) = (Vec::new(), HashMap::new());
2037        let root = intern(term, &mut nodes, &mut index);
2038        let mut uses = vec![0; nodes.len()];
2039        for n in &nodes {
2040            match *n {
2041                DagNode::Abs(x) => uses[x] += 1,
2042                DagNode::Op(_, l, r) => {
2043                    uses[l] += 1;
2044                    uses[r] += 1;
2045                }
2046                DagNode::Select(c, a, b) => {
2047                    uses[c] += 1;
2048                    uses[a] += 1;
2049                    uses[b] += 1;
2050                }
2051                _ => {}
2052            }
2053        }
2054        TermDag { nodes, uses, root }
2055    }
2056
2057    /// Every node's type when operand `k` holds a `reads[k]` ([`ZipTerm::ty`] node by node); `None` when the term
2058    /// is ill-typed.
2059    pub fn types(&self, reads: &[Ty]) -> Option<Vec<Ty>> {
2060        let mut tys: Vec<Ty> = Vec::with_capacity(self.nodes.len());
2061        for n in &self.nodes {
2062            let t = match *n {
2063                DagNode::Read(k) => reads.get(k).copied(),
2064                DagNode::Const(_) => Some(Ty::Int),
2065                DagNode::Real(_) => Some(Ty::Real),
2066                DagNode::Abs(x) => abs_ty(tys[x]),
2067                DagNode::Op(op, l, r) => op_ty(op, tys[l], tys[r]),
2068                DagNode::Select(c, a, b) => select_ty(tys[c], tys[a], tys[b]),
2069            }?;
2070            tys.push(t);
2071        }
2072        Some(tys)
2073    }
2074
2075    /// The term's value at one element, as [`ZipTerm::value`] computes it — each node at most once, and only the
2076    /// arm a select takes.
2077    pub fn value(&self, row: &[Scalar], ints: (i128, i128), reals: f64) -> Result<Scalar, Stop> {
2078        self.value_of(self.root, row, ints, reals)
2079    }
2080
2081    /// The value of node `node` at one element, as [`Self::value`] computes the root's.
2082    pub fn value_of(&self, node: usize, row: &[Scalar], ints: (i128, i128), reals: f64) -> Result<Scalar, Stop> {
2083        fn at(
2084            d: &TermDag,
2085            i: usize,
2086            row: &[Scalar],
2087            ints: (i128, i128),
2088            reals: f64,
2089            memo: &mut Vec<Option<Result<Scalar, Stop>>>,
2090        ) -> Result<Scalar, Stop> {
2091            if let Some(v) = memo[i] {
2092                return v;
2093            }
2094            let v = match d.nodes[i] {
2095                DagNode::Read(k) => row.get(k).copied().ok_or(Stop::Mistyped).and_then(|v| bounded(v, ints, reals)),
2096                DagNode::Const(c) => bounded(Scalar::Int(c), ints, reals),
2097                DagNode::Real(b) => bounded(Scalar::Real(f64::from_bits(b)), ints, reals),
2098                DagNode::Abs(x) => at(d, x, row, ints, reals, memo).and_then(|v| abs_of(v, ints, reals)),
2099                DagNode::Op(op, l, r) => at(d, l, row, ints, reals, memo)
2100                    .and_then(|a| Ok((a, at(d, r, row, ints, reals, memo)?)))
2101                    .and_then(|(a, b)| apply(op, a, b, ints, reals)),
2102                DagNode::Select(c, a, b) => match at(d, c, row, ints, reals, memo) {
2103                    Ok(Scalar::Bool(true)) => at(d, a, row, ints, reals, memo),
2104                    Ok(Scalar::Bool(false)) => at(d, b, row, ints, reals, memo),
2105                    Ok(_) => Err(Stop::Mistyped),
2106                    Err(e) => Err(e),
2107                },
2108            };
2109            memo[i] = Some(v);
2110            v
2111        }
2112        at(self, node, row, ints, reals, &mut vec![None; self.nodes.len()])
2113    }
2114}
2115
2116/// What a loop's term may draw on beyond the loop: the program's pure functions (inlined where they are called), the
2117/// builtin `abs`, and the element type of every array it declares (`a new Seq of Int`, `Seq of Real`, `Seq of Bool`).
2118pub struct Program<'a> {
2119    fns: HashMap<Symbol, PureFn<'a>>,
2120    abs: Option<Symbol>,
2121    /// Each array's element type; `None` for a name declared twice with different element types.
2122    elems: HashMap<Symbol, Option<Ty>>,
2123    prims: [(Option<Symbol>, Ty); 3],
2124}
2125
2126/// A function a term may call: typed parameters and result, a body of immutable `Let`s, `If`s and `Return`s.
2127struct PureFn<'a> {
2128    params: Vec<(Symbol, Ty)>,
2129    ret: Ty,
2130    body: &'a [Stmt<'a>],
2131}
2132
2133impl<'a> Program<'a> {
2134    pub fn of(stmts: &'a [Stmt<'a>], interner: &Interner) -> Program<'a> {
2135        let prims = [
2136            (interner.lookup("Int"), Ty::Int),
2137            (interner.lookup("Real"), Ty::Real),
2138            (interner.lookup("Bool"), Ty::Bool),
2139        ];
2140        let mut program = Program { fns: HashMap::new(), abs: interner.lookup("abs"), elems: HashMap::new(), prims };
2141        let seqs = [interner.lookup("Seq"), interner.lookup("List")];
2142        for s in stmts {
2143            if let Stmt::FunctionDef { name, generics, params, body, return_type: Some(ret), is_native: false, .. } = s {
2144                let typed: Option<Vec<(Symbol, Ty)>> =
2145                    params.iter().map(|(p, t)| program.ty_of(t).map(|ty| (*p, ty))).collect();
2146                if let (true, Some(params), Some(ret)) = (generics.is_empty(), typed, program.ty_of(ret)) {
2147                    program.fns.insert(*name, PureFn { params, ret, body });
2148                }
2149            }
2150        }
2151        fn declare<'a>(program: &mut Program<'a>, seqs: &[Option<Symbol>; 2], stmts: &'a [Stmt<'a>]) {
2152            for s in stmts {
2153                match s {
2154                    Stmt::Let { var, ty, value, .. } => {
2155                        let elem = match (ty, value) {
2156                            (Some(TypeExpr::Generic { base, params: [t] }), _) if seqs.contains(&Some(*base)) => {
2157                                program.ty_of(t)
2158                            }
2159                            (_, Expr::New { type_name, type_args, .. }) if seqs.contains(&Some(*type_name)) => {
2160                                match type_args.as_slice() {
2161                                    [t] => program.ty_of(t),
2162                                    _ => None,
2163                                }
2164                            }
2165                            _ => continue,
2166                        };
2167                        let entry = program.elems.entry(*var).or_insert(elem);
2168                        if *entry != elem {
2169                            *entry = None;
2170                        }
2171                    }
2172                    Stmt::While { body, .. } | Stmt::Repeat { body, .. } | Stmt::Splice { body } => {
2173                        declare(program, seqs, body)
2174                    }
2175                    Stmt::If { then_block, else_block, .. } => {
2176                        declare(program, seqs, then_block);
2177                        declare(program, seqs, else_block.unwrap_or(&[]));
2178                    }
2179                    _ => {}
2180                }
2181            }
2182        }
2183        declare(&mut program, &seqs, stmts);
2184        program
2185    }
2186
2187    /// The type a `TypeExpr` names, when it is `Int`, `Real` or `Bool`.
2188    fn ty_of(&self, t: &TypeExpr) -> Option<Ty> {
2189        match t {
2190            TypeExpr::Primitive(s) | TypeExpr::Named(s) => {
2191                self.prims.iter().find(|(p, _)| *p == Some(*s)).map(|(_, ty)| *ty)
2192            }
2193            _ => None,
2194        }
2195    }
2196
2197    /// The element type of the array `array`, when it is declared once with one.
2198    pub fn elem(&self, array: Symbol) -> Option<Ty> {
2199        self.elems.get(&array).copied().flatten()
2200    }
2201}
2202
2203/// Why an expression is not a term: it reads something a term cannot (or is ill-typed), an index is not affine, or
2204/// a read is not where the recognizer's layout puts it.
2205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2206enum TermReject {
2207    Unsupported,
2208    NonAffine,
2209    Layout,
2210}
2211
2212/// How many nodes a term may grow to before it is refused — inlining copies a function's body at every call.
2213const MAX_TERM_NODES: usize = 1 << 20;
2214
2215/// Builds [`ZipTerm`]s from a loop body's expressions: the body's immutable `Let`s read through, named constants
2216/// folded to their values, a call of one of the program's pure functions inlined (its parameters bound to the
2217/// call's argument terms, its `If`s and `Return`s a chain of selects), `abs` as itself. An array element becomes
2218/// the operand the recognizer's `read(array, index)` numbers it as, typed by the array's declared element type.
2219struct Terms<'c, 'a> {
2220    program: &'c Program<'a>,
2221    consts: &'c HashMap<Symbol, i64>,
2222    exprs: &'c HashMap<Symbol, &'a Expr<'a>>,
2223    /// Names a term may not read as constants: the loop counters and what the loop writes.
2224    hidden: Vec<Symbol>,
2225    /// Each operand's element type, by operand number (an operand the term has not read yet has none).
2226    types: Vec<Option<Ty>>,
2227    nodes: usize,
2228}
2229
2230type Read<'r, 'a> = dyn FnMut(Symbol, &'a Expr<'a>) -> Result<usize, TermReject> + 'r;
2231
2232impl<'c, 'a> Terms<'c, 'a> {
2233    fn new(
2234        program: &'c Program<'a>,
2235        consts: &'c HashMap<Symbol, i64>,
2236        exprs: &'c HashMap<Symbol, &'a Expr<'a>>,
2237        hidden: Vec<Symbol>,
2238    ) -> Self {
2239        Terms { program, consts, exprs, hidden, types: Vec::new(), nodes: 0 }
2240    }
2241
2242    fn term(&mut self, e: &'a Expr<'a>, read: &mut Read<'_, 'a>) -> Result<ZipTerm, TermReject> {
2243        self.within(e, None, read, &mut Vec::new())
2244    }
2245
2246    /// Every operand's element type, once each has been read.
2247    fn types(&self) -> Option<Vec<Ty>> {
2248        self.types.iter().copied().collect()
2249    }
2250
2251    /// The type of `t` over the operands read so far.
2252    fn ty(&self, t: &ZipTerm) -> Option<Ty> {
2253        t.ty(&self.types.iter().map(|t| t.unwrap_or(Ty::Bool)).collect::<Vec<_>>())
2254    }
2255
2256    /// `e` as a term. Inside an inlined function `env` binds its parameters and `Let`s, and nothing else is in scope.
2257    fn within(
2258        &mut self,
2259        e: &'a Expr<'a>,
2260        env: Option<&HashMap<Symbol, ZipTerm>>,
2261        read: &mut Read<'_, 'a>,
2262        calls: &mut Vec<Symbol>,
2263    ) -> Result<ZipTerm, TermReject> {
2264        self.nodes += 1;
2265        if self.nodes > MAX_TERM_NODES {
2266            return Err(TermReject::Unsupported);
2267        }
2268        let e = match env {
2269            Some(env) => match as_ident(e) {
2270                Some(s) => return env.get(&s).cloned().ok_or(TermReject::Unsupported),
2271                None => e,
2272            },
2273            None => through_lets(e, self.exprs),
2274        };
2275        match e {
2276            Expr::Literal(Literal::Number(n)) => Ok(ZipTerm::Const(*n)),
2277            Expr::Literal(Literal::Float(f)) => Ok(ZipTerm::real(*f)),
2278            Expr::Identifier(s) => self
2279                .consts
2280                .get(s)
2281                .filter(|_| !self.hidden.contains(s))
2282                .map(|c| ZipTerm::Const(*c))
2283                .ok_or(TermReject::Unsupported),
2284            Expr::Index { collection, index } if env.is_none() => {
2285                let array = as_ident(collection).ok_or(TermReject::Unsupported)?;
2286                let ty = self.program.elem(array).ok_or(TermReject::Unsupported)?;
2287                let k = read(array, index)?;
2288                if self.types.len() <= k {
2289                    self.types.resize(k + 1, None);
2290                }
2291                self.types[k] = Some(ty);
2292                Ok(ZipTerm::Read(k))
2293            }
2294            Expr::BinaryOp { op, left, right } => {
2295                let op = ZipOp::of(*op).ok_or(TermReject::Unsupported)?;
2296                let l = self.within(left, env, read, calls)?;
2297                let r = self.within(right, env, read, calls)?;
2298                Ok(ZipTerm::Op(op, Box::new(l), Box::new(r)))
2299            }
2300            Expr::Call { function, args } => self.call(*function, args, env, read, calls),
2301            _ => Err(TermReject::Unsupported),
2302        }
2303    }
2304
2305    /// A call: one of the program's pure functions inlined, or `abs`. A function that calls itself, however
2306    /// indirectly, is refused.
2307    fn call(
2308        &mut self,
2309        function: Symbol,
2310        args: &[&'a Expr<'a>],
2311        env: Option<&HashMap<Symbol, ZipTerm>>,
2312        read: &mut Read<'_, 'a>,
2313        calls: &mut Vec<Symbol>,
2314    ) -> Result<ZipTerm, TermReject> {
2315        let program = self.program;
2316        if let Some(f) = program.fns.get(&function) {
2317            if calls.contains(&function) || args.len() != f.params.len() {
2318                return Err(TermReject::Unsupported);
2319            }
2320            let mut bound = HashMap::new();
2321            for ((p, want), a) in f.params.iter().zip(args) {
2322                let t = self.within(a, env, read, calls)?;
2323                if self.ty(&t) != Some(*want) {
2324                    return Err(TermReject::Unsupported);
2325                }
2326                bound.insert(*p, t);
2327            }
2328            calls.push(function);
2329            let body = self.body(f.body.iter().collect(), bound, read, calls);
2330            calls.pop();
2331            let body = body?;
2332            if self.ty(&body) != Some(f.ret) {
2333                return Err(TermReject::Unsupported);
2334            }
2335            Ok(body)
2336        } else if Some(function) == program.abs && args.len() == 1 {
2337            Ok(ZipTerm::Abs(Box::new(self.within(args[0], env, read, calls)?)))
2338        } else {
2339            Err(TermReject::Unsupported)
2340        }
2341    }
2342
2343    /// A function body from `stmts[0]` on, as the term it returns: a `Let` binds, an `If` is the select of its two
2344    /// continuations (each arm followed by the statements after the `If`), a `Return` ends the path. A path that
2345    /// ends without a `Return`, or any other statement, is refused.
2346    fn body(
2347        &mut self,
2348        stmts: Vec<&'a Stmt<'a>>,
2349        mut env: HashMap<Symbol, ZipTerm>,
2350        read: &mut Read<'_, 'a>,
2351        calls: &mut Vec<Symbol>,
2352    ) -> Result<ZipTerm, TermReject> {
2353        for (k, s) in stmts.iter().enumerate() {
2354            match s {
2355                Stmt::Let { var, ty, value, mutable: false } => {
2356                    let t = self.within(value, Some(&env), read, calls)?;
2357                    if let Some(declared) = ty {
2358                        if self.program.ty_of(declared).is_none() || self.program.ty_of(declared) != self.ty(&t) {
2359                            return Err(TermReject::Unsupported);
2360                        }
2361                    }
2362                    env.insert(*var, t);
2363                }
2364                Stmt::Return { value: Some(v) } => return self.within(v, Some(&env), read, calls),
2365                Stmt::If { cond, then_block, else_block } => {
2366                    let c = self.within(cond, Some(&env), read, calls)?;
2367                    let rest = &stmts[k + 1..];
2368                    let path = |arm: &'a [Stmt<'a>]| arm.iter().chain(rest.iter().copied()).collect::<Vec<_>>();
2369                    let a = self.body(path(then_block), env.clone(), read, calls)?;
2370                    let b = self.body(path(else_block.unwrap_or(&[])), env.clone(), read, calls)?;
2371                    return Ok(ZipTerm::Select(Box::new(c), Box::new(a), Box::new(b)));
2372                }
2373                _ => return Err(TermReject::Unsupported),
2374            }
2375        }
2376        Err(TermReject::Unsupported)
2377    }
2378}
2379
2380/// The reads of a zip loop: every array at one unit-stride index of the counter, numbered in reading order.
2381struct Aligned<'c> {
2382    iv: Symbol,
2383    consts: &'c HashMap<Symbol, i64>,
2384    lets: &'c HashMap<Symbol, Affine>,
2385    operands: Vec<Symbol>,
2386    index: Option<Affine>,
2387}
2388
2389impl<'c> Aligned<'c> {
2390    fn read(&mut self, array: Symbol, index: &Expr) -> Result<usize, TermReject> {
2391        let at = eval_affine(index, self.consts, self.lets).ok_or(TermReject::NonAffine)?;
2392        if at.coeff.len() != 1 || at.coeff_of(self.iv) != 1 || self.index.as_ref().map_or(false, |i| *i != at) {
2393            return Err(TermReject::Layout);
2394        }
2395        self.index = Some(at);
2396        Ok(match self.operands.iter().position(|o| *o == array) {
2397            Some(k) => k,
2398            None => {
2399                self.operands.push(array);
2400                self.operands.len() - 1
2401            }
2402        })
2403    }
2404}
2405
2406/// A recognized zip sum `acc = Σ_{t<n} f(A₀[t], A₁[t], …)`: one counted loop accumulating an integer term
2407/// of several `Seq of Int` arrays, every one read at the same unit-stride index — a dot product, TPC-H Q6's
2408/// filtered revenue, any map-reduce over aligned streams.
2409#[derive(Debug, Clone, PartialEq, Eq)]
2410pub struct ZipSum {
2411    pub n: usize,
2412    /// The arrays read, in reading order; [`ZipTerm::Read`]`(k)` is the element of `operands[k]`.
2413    pub operands: Vec<Symbol>,
2414    pub term: ZipTerm,
2415    pub acc: Symbol,
2416}
2417
2418/// Why a program is not a recognizable zip sum, least to most advanced (what a loop's body computes ranks
2419/// before whether its counter is provable).
2420#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2421pub enum ZipReject {
2422    /// No `While counter is less than BOUND` loop.
2423    NoLoop,
2424    /// No `Set s to s + …`, plain or under an `If`, in the loop body.
2425    NoAccumulation,
2426    /// The accumulated term reads something other than array elements and constants (the counter, the
2427    /// accumulator, a variable the loop does not fix), uses an operator or calls a function a term does not have,
2428    /// or is ill-typed.
2429    UnsupportedTerm,
2430    /// A well-typed term, but not an `Int` over `Int` arrays — the integer sum the zip-sum app computes.
2431    NotInt,
2432    /// An index is not affine in the counter.
2433    NonAffineIndex,
2434    /// The arrays are not all read at one unit-stride index of the counter.
2435    NotAligned,
2436    /// The counter is not set to a constant right before the loop, or is not stepped by exactly one.
2437    NotCounted,
2438    /// The loop body does more than the zip sum.
2439    ExtraWork,
2440    /// The accumulator is not set to zero right before the loop.
2441    NotFromZero,
2442}
2443
2444impl From<TermReject> for ZipReject {
2445    fn from(r: TermReject) -> ZipReject {
2446        match r {
2447            TermReject::Unsupported => ZipReject::UnsupportedTerm,
2448            TermReject::NonAffine => ZipReject::NonAffineIndex,
2449            TermReject::Layout => ZipReject::NotAligned,
2450        }
2451    }
2452}
2453
2454/// Recognize a zip sum anywhere in a LOGOS statement list:
2455///
2456/// ```text
2457/// Let mutable s be 0.
2458/// Let mutable i be 0.
2459/// While i is less than n:
2460///     Let date be item (i + 1) of dates.
2461///     If date is greater than 0 and date is less than 9999:
2462///         Set s to s + item (i + 1) of prices * item (i + 1) of discts.
2463///     Set i to i + 1.
2464/// ```
2465///
2466/// The loop is counted (its counter set to a constant right before it, stepped by one), the accumulator
2467/// starts at zero right before it, every array is a declared `Seq of Int` read at the same unit-stride index, the
2468/// accumulation is `Set s to s + E` or an `If` whose arms are each exactly one such accumulation (a missing `Else`
2469/// adds 0), and the body holds nothing else but immutable names for elements and subterms. The first loop that is
2470/// one is returned; otherwise the refusal of the loop that came closest.
2471pub fn recognize_zip_sum(stmts: &[Stmt], interner: &Interner) -> Result<ZipSum, ZipReject> {
2472    recognize_zip_sum_where(stmts, interner, &|_| true)
2473}
2474
2475/// [`recognize_zip_sum`] for the first loop whose zip sum `wanted` accepts; a zip sum it turns away counts as
2476/// [`ZipReject::UnsupportedTerm`] (its term is not the one asked for).
2477pub fn recognize_zip_sum_where(
2478    stmts: &[Stmt],
2479    interner: &Interner,
2480    wanted: &dyn Fn(&ZipSum) -> bool,
2481) -> Result<ZipSum, ZipReject> {
2482    let mut consts: HashMap<Symbol, i64> = HashMap::new();
2483    collect_consts_rec(stmts, &mut consts);
2484    let program = Program::of(stmts, interner);
2485    search_loops(stmts, ZipReject::NoLoop, &|block, at, cond, body| {
2486        try_zip_at(block, at, cond, body, &consts, &program)
2487            .and_then(|z| if wanted(&z) { Ok(z) } else { Err(ZipReject::UnsupportedTerm) })
2488    })
2489}
2490
2491/// `Set acc to acc + E` (either operand order), `acc` not `iv`: `(acc, E)`.
2492fn accumulation_into<'a>(s: &'a Stmt<'a>, iv: Symbol) -> Option<(Symbol, &'a Expr<'a>)> {
2493    match s {
2494        Stmt::Set { target, value } if *target != iv => {
2495            let (l, r) = as_binop(value, BinaryOpKind::Add)?;
2496            if as_ident(l) == Some(*target) {
2497                Some((*target, r))
2498            } else if as_ident(r) == Some(*target) {
2499                Some((*target, l))
2500            } else {
2501                None
2502            }
2503        }
2504        _ => None,
2505    }
2506}
2507
2508/// A loop body's accumulation statement: plain, or an `If` whose arms each hold exactly one accumulation into
2509/// the same register — `(acc, guard, then-term, else-term)`.
2510fn accumulation_of<'a>(
2511    s: &'a Stmt<'a>,
2512    iv: Symbol,
2513) -> Option<(Symbol, Option<&'a Expr<'a>>, &'a Expr<'a>, Option<&'a Expr<'a>>)> {
2514    if let Some((acc, e)) = accumulation_into(s, iv) {
2515        return Some((acc, None, e, None));
2516    }
2517    let Stmt::If { cond, then_block, else_block } = s else { return None };
2518    let [then] = then_block else { return None };
2519    let (acc, e1) = accumulation_into(then, iv)?;
2520    let e2 = match else_block {
2521        None => None,
2522        Some([other]) => match accumulation_into(other, iv)? {
2523            (a2, e2) if a2 == acc => Some(e2),
2524            _ => return None,
2525        },
2526        Some(_) => return None,
2527    };
2528    Some((acc, Some(*cond), e1, e2))
2529}
2530
2531fn try_zip_at<'a>(
2532    stmts: &'a [Stmt<'a>],
2533    at: usize,
2534    cond: &'a Expr<'a>,
2535    body: &'a [Stmt<'a>],
2536    consts: &HashMap<Symbol, i64>,
2537    program: &Program<'a>,
2538) -> Result<ZipSum, ZipReject> {
2539    let bound = loop_iv_bound(cond, consts).ok_or(ZipReject::NoLoop)?;
2540    let iv = bound.0;
2541    let (lets, exprs) = body_lets(body, consts);
2542    let (pos, (acc, guard, then, otherwise)) = body
2543        .iter()
2544        .enumerate()
2545        .find_map(|(k, s)| accumulation_of(s, iv).map(|a| (k, a)))
2546        .ok_or(ZipReject::NoAccumulation)?;
2547    // Converted in source order — the guard before its arms — so the operands are numbered as they are read.
2548    let mut aligned = Aligned { iv, consts, lets: &lets, operands: Vec::new(), index: None };
2549    let mut terms = Terms::new(program, consts, &exprs, vec![iv, acc]);
2550    let mut read = |a, i| aligned.read(a, i);
2551    let term = match guard {
2552        None => terms.term(then, &mut read)?,
2553        Some(g) => {
2554            let g = terms.term(g, &mut read)?;
2555            let value = terms.term(then, &mut read)?;
2556            let e = match otherwise {
2557                Some(e) => terms.term(e, &mut read)?,
2558                None => ZipTerm::Const(0),
2559            };
2560            ZipTerm::Select(Box::new(g), Box::new(value), Box::new(e))
2561        }
2562    };
2563    let types = terms.types().ok_or(ZipReject::UnsupportedTerm)?;
2564    match term.ty(&types) {
2565        None | Some(Ty::Bool) => return Err(ZipReject::UnsupportedTerm),
2566        Some(Ty::Real) => return Err(ZipReject::NotInt),
2567        Some(Ty::Int) if types.iter().any(|t| *t != Ty::Int) => return Err(ZipReject::NotInt),
2568        Some(Ty::Int) => {}
2569    }
2570    let operands = aligned.operands;
2571    let loop_ = counted_at(stmts, at, bound, body, consts).ok_or(ZipReject::NotCounted)?;
2572    let mut kept = vec![iv, acc];
2573    kept.extend(&operands);
2574    if body
2575        .iter()
2576        .enumerate()
2577        .any(|(k, s)| !(k == pos || is_unit_step(s, iv) || is_body_let(s, &kept)))
2578    {
2579        return Err(ZipReject::ExtraWork);
2580    }
2581    if entry_values(stmts, at, consts).get(&acc).copied().flatten() != Some(0) {
2582        return Err(ZipReject::NotFromZero);
2583    }
2584    Ok(ZipSum {
2585        n: loop_.trips as usize,
2586        operands,
2587        term,
2588        acc,
2589    })
2590}
2591
2592/// A recognized zip map `out[t] = f(A₀[t], A₁[t], …)` for `t < n`: one counted loop storing a term of several
2593/// arrays into another, every one read at the index it stores at — Black-Scholes over its option columns, any
2594/// elementwise map over aligned streams. The term's type is the output's element type, `Int` or `Real`.
2595#[derive(Debug, Clone, PartialEq, Eq)]
2596pub struct ZipMap {
2597    pub n: usize,
2598    /// The arrays read, in reading order; [`ZipTerm::Read`]`(k)` is the element of `operands[k]`.
2599    pub operands: Vec<Symbol>,
2600    /// Each operand's element type.
2601    pub types: Vec<Ty>,
2602    pub term: ZipTerm,
2603    /// The term's type: the element type of `out`.
2604    pub ty: Ty,
2605    pub out: Symbol,
2606}
2607
2608/// Why a program is not a recognizable zip map, least to most advanced.
2609#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2610pub enum ZipMapReject {
2611    /// No `While counter is less than BOUND` loop.
2612    NoLoop,
2613    /// No `Set item _ of out to …` in the loop body.
2614    NoStore,
2615    /// The stored term reads something other than array elements and constants (the counter, the output, a
2616    /// variable the loop does not fix), reads no array, uses an operator or calls a function a term does not have,
2617    /// is ill-typed or a boolean, or is not of the output's element type.
2618    UnsupportedTerm,
2619    /// An index is not affine in the counter.
2620    NonAffineIndex,
2621    /// The arrays are not all read at one unit-stride index of the counter, the index the output is written at.
2622    NotAligned,
2623    /// The counter is not set to a constant right before the loop, or is not stepped by exactly one.
2624    NotCounted,
2625    /// The loop body does more than the zip map.
2626    ExtraWork,
2627}
2628
2629impl From<TermReject> for ZipMapReject {
2630    fn from(r: TermReject) -> ZipMapReject {
2631        match r {
2632            TermReject::Unsupported => ZipMapReject::UnsupportedTerm,
2633            TermReject::NonAffine => ZipMapReject::NonAffineIndex,
2634            TermReject::Layout => ZipMapReject::NotAligned,
2635        }
2636    }
2637}
2638
2639/// Recognize a zip map anywhere in a LOGOS statement list:
2640///
2641/// ```text
2642/// Let mutable i be 0.
2643/// While i is less than n:
2644///     Set item (i + 1) of out to blackScholes(item (i + 1) of prices, item (i + 1) of strikes, …).
2645///     Set i to i + 1.
2646/// ```
2647///
2648/// The loop is counted; it writes one array, `out`, at a unit-stride index of the counter, and every array the
2649/// stored term reads is a declared `Seq` read at that same index; the term calls only the program's pure functions
2650/// (each inlined) and `abs`; and the body holds nothing else but immutable names. The first loop that is one is
2651/// returned; otherwise the refusal of the loop that came closest.
2652pub fn recognize_zip_map(stmts: &[Stmt], interner: &Interner) -> Result<ZipMap, ZipMapReject> {
2653    recognize_zip_map_where(stmts, interner, &|_| true)
2654}
2655
2656/// [`recognize_zip_map`] for the first loop whose zip map `wanted` accepts; one it turns away counts as
2657/// [`ZipMapReject::UnsupportedTerm`] (its term is not the one asked for).
2658pub fn recognize_zip_map_where(
2659    stmts: &[Stmt],
2660    interner: &Interner,
2661    wanted: &dyn Fn(&ZipMap) -> bool,
2662) -> Result<ZipMap, ZipMapReject> {
2663    let mut consts: HashMap<Symbol, i64> = HashMap::new();
2664    collect_consts_rec(stmts, &mut consts);
2665    let program = Program::of(stmts, interner);
2666    search_loops(stmts, ZipMapReject::NoLoop, &|block, at, cond, body| {
2667        try_map_at(block, at, cond, body, &consts, &program)
2668            .and_then(|z| if wanted(&z) { Ok(z) } else { Err(ZipMapReject::UnsupportedTerm) })
2669    })
2670}
2671
2672fn try_map_at<'a>(
2673    stmts: &'a [Stmt<'a>],
2674    at: usize,
2675    cond: &'a Expr<'a>,
2676    body: &'a [Stmt<'a>],
2677    consts: &HashMap<Symbol, i64>,
2678    program: &Program<'a>,
2679) -> Result<ZipMap, ZipMapReject> {
2680    let bound = loop_iv_bound(cond, consts).ok_or(ZipMapReject::NoLoop)?;
2681    let iv = bound.0;
2682    let (lets, exprs) = body_lets(body, consts);
2683    let (pos, out, index, value) = body
2684        .iter()
2685        .enumerate()
2686        .find_map(|(k, s)| match s {
2687            Stmt::SetIndex { collection, index, value } => Some((k, as_ident(collection)?, *index, *value)),
2688            _ => None,
2689        })
2690        .ok_or(ZipMapReject::NoStore)?;
2691    let mut aligned = Aligned { iv, consts, lets: &lets, operands: Vec::new(), index: None };
2692    let mut terms = Terms::new(program, consts, &exprs, vec![iv, out]);
2693    let term = terms.term(value, &mut |a, i| aligned.read(a, i))?;
2694    let types = terms.types().ok_or(ZipMapReject::UnsupportedTerm)?;
2695    let ty = term.ty(&types).filter(|t| *t != Ty::Bool).ok_or(ZipMapReject::UnsupportedTerm)?;
2696    if aligned.operands.is_empty() || aligned.operands.contains(&out) || program.elem(out) != Some(ty) {
2697        return Err(ZipMapReject::UnsupportedTerm);
2698    }
2699    let stored = eval_affine(index, consts, &lets).ok_or(ZipMapReject::NonAffineIndex)?;
2700    if aligned.index.as_ref() != Some(&stored) {
2701        return Err(ZipMapReject::NotAligned);
2702    }
2703    let loop_ = counted_at(stmts, at, bound, body, consts).ok_or(ZipMapReject::NotCounted)?;
2704    let mut kept = vec![iv, out];
2705    kept.extend(&aligned.operands);
2706    if body
2707        .iter()
2708        .enumerate()
2709        .any(|(k, s)| !(k == pos || is_unit_step(s, iv) || is_body_let(s, &kept)))
2710    {
2711        return Err(ZipMapReject::ExtraWork);
2712    }
2713    Ok(ZipMap {
2714        n: loop_.trips as usize,
2715        operands: aligned.operands,
2716        types,
2717        term,
2718        ty,
2719        out,
2720    })
2721}
2722
2723/// Recognize an outer product anywhere in a LOGOS statement list:
2724///
2725/// ```text
2726/// Let mutable i be 0.
2727/// While i is less than m:
2728///     Let mutable j be 0.
2729///     While j is less than n:
2730///         Let idx be i * n + j + 1.
2731///         Set item idx of c to item (i + 1) of a * item (j + 1) of b.
2732///         Set j to j + 1.
2733///     Set i to i + 1.
2734/// ```
2735///
2736/// Both loops are counted; the store is `C[i·n + j]` (row-major against the inner trip count), `A` is read
2737/// at a unit stride of `i` alone and `B` of `j` alone; and the bodies hold nothing else but immutable index
2738/// or product names. `C[idx] := C[idx] + A·B` is refused by name: it is a rank-1 update.
2739pub fn recognize_outer(stmts: &[Stmt], interner: &Interner) -> Result<OuterSpec, OuterReject> {
2740    let mut consts: HashMap<Symbol, i64> = HashMap::new();
2741    collect_consts_rec(stmts, &mut consts);
2742    let program = Program::of(stmts, interner);
2743    search_loops(stmts, OuterReject::NoLoopNest, &|block, at, cond, body| {
2744        outer_nest_at(block, at, cond, body, &consts, &program, false)
2745    })
2746}
2747
2748/// The index of `e` when it reads `item _ of array`.
2749fn index_read_of<'a>(e: &'a Expr<'a>, array: Symbol) -> Option<&'a Expr<'a>> {
2750    as_index(e).filter(|(coll, _)| as_ident(coll) == Some(array)).map(|(_, i)| i)
2751}
2752
2753/// The counted nest `i < m, j < n` at `stmts[at]` writing `C[i·n + j]` from `A[i]·B[j]`: stored (an outer product)
2754/// or, when `accumulate`, added to `C[i·n + j]` (a rank-1 update).
2755fn outer_nest_at(
2756    stmts: &[Stmt],
2757    at: usize,
2758    cond_i: &Expr,
2759    body_i: &[Stmt],
2760    consts: &HashMap<Symbol, i64>,
2761    program: &Program,
2762    accumulate: bool,
2763) -> Result<OuterSpec, OuterReject> {
2764    let bound_i = loop_iv_bound(cond_i, consts).ok_or(OuterReject::NoLoopNest)?;
2765    let (at_j, cond_j, body_j) = body_i
2766        .iter()
2767        .enumerate()
2768        .find_map(|(k, s)| match s {
2769            Stmt::While { cond, body, .. } => Some((k, *cond, *body)),
2770            _ => None,
2771        })
2772        .ok_or(OuterReject::NotDoubleNested)?;
2773    let bound_j = loop_iv_bound(cond_j, consts).ok_or(OuterReject::NotDoubleNested)?;
2774    let (lets, exprs) = body_lets(body_i[..at_j].iter().chain(body_j), consts);
2775
2776    let (store_coll, store_idx, value) = body_j
2777        .iter()
2778        .find_map(|s| match s {
2779            Stmt::SetIndex {
2780                collection,
2781                index,
2782                value,
2783            } => Some((*collection, *index, *value)),
2784            _ => None,
2785        })
2786        .ok_or(OuterReject::NoStore)?;
2787    let c = as_ident(store_coll).ok_or(OuterReject::NoStore)?;
2788    let value = through_lets(value, &exprs);
2789    // `C[idx] + A·B` (either order): the read of C and the product beside it.
2790    let accumulated = as_binop(value, BinaryOpKind::Add).and_then(|(l, r)| {
2791        match (index_read_of(l, c), index_read_of(r, c)) {
2792            (Some(ri), _) if indexed_product(r, &exprs).is_some() => Some((ri, r)),
2793            (_, Some(ri)) if indexed_product(l, &exprs).is_some() => Some((ri, l)),
2794            _ => None,
2795        }
2796    });
2797    let value = match (accumulated, accumulate) {
2798        (Some(_), false) => return Err(OuterReject::Accumulates),
2799        (None, true) => {
2800            return Err(if indexed_product(value, &exprs).is_some() { OuterReject::Stores } else { OuterReject::NoProduct })
2801        }
2802        (Some((read_index, product)), true) => {
2803            let ri = eval_affine(read_index, consts, &lets).ok_or(OuterReject::NonAffineIndex)?;
2804            if Some(ri) != eval_affine(store_idx, consts, &lets) {
2805                return Err(OuterReject::NotOuterLayout);
2806            }
2807            product
2808        }
2809        (None, false) => value,
2810    };
2811    let (a, ai, b, bi) = indexed_product(value, &exprs).ok_or(OuterReject::NoProduct)?;
2812    if [a, b, c].iter().any(|x| program.elem(*x) != Some(Ty::Int)) {
2813        return Err(OuterReject::NotInt);
2814    }
2815    let ci = eval_affine(store_idx, consts, &lets).ok_or(OuterReject::NonAffineIndex)?;
2816    let ai = eval_affine(ai, consts, &lets).ok_or(OuterReject::NonAffineIndex)?;
2817    let bi = eval_affine(bi, consts, &lets).ok_or(OuterReject::NonAffineIndex)?;
2818    let li = counted_at(stmts, at, bound_i, body_i, consts).ok_or(OuterReject::NotCounted)?;
2819    let lj = counted_at(body_i, at_j, bound_j, body_j, consts).ok_or(OuterReject::NotCounted)?;
2820    let (iv_i, iv_j, n) = (li.iv, lj.iv, lj.trips);
2821    let only = |x: &Affine, pairs: &[(Symbol, i64)]| {
2822        x.coeff.len() == pairs.len() && pairs.iter().all(|(s, k)| x.coeff_of(*s) == *k)
2823    };
2824    if !(only(&ci, &[(iv_i, n), (iv_j, 1)]) && only(&ai, &[(iv_i, 1)]) && only(&bi, &[(iv_j, 1)])) {
2825        return Err(OuterReject::NotOuterLayout);
2826    }
2827    let kept = [iv_i, iv_j, a, b, c];
2828    let store = |s: &Stmt| matches!(s, Stmt::SetIndex { .. });
2829    let inner_ok = body_j
2830        .iter()
2831        .all(|s| is_unit_step(s, iv_j) || store(s) || is_body_let(s, &kept))
2832        && body_j.iter().filter(|s| store(s)).count() == 1;
2833    let j_init = |k: usize| {
2834        k < at_j
2835            && body_i[k..at_j]
2836                .iter()
2837                .all(|s| matches!(s, Stmt::Let { .. } | Stmt::Set { .. }))
2838            && matches!(&body_i[k], Stmt::Let { var, .. } | Stmt::Set { target: var, .. } if *var == iv_j)
2839    };
2840    let outer_ok = body_i.iter().enumerate().all(|(k, s)| {
2841        k == at_j || is_unit_step(s, iv_i) || j_init(k) || is_body_let(s, &kept)
2842    }) && body_i
2843        .iter()
2844        .filter(|s| matches!(s, Stmt::While { .. }))
2845        .count()
2846        == 1;
2847    if !(inner_ok && outer_ok) {
2848        return Err(OuterReject::ExtraWork);
2849    }
2850    Ok(OuterSpec {
2851        m: li.trips as usize,
2852        n: n as usize,
2853        a,
2854        b,
2855        c,
2856    })
2857}
2858
2859/// `term` with every select whose two arms apply one operator to a common operand rewritten to apply it once, to the
2860/// select of what differs — `c ? x − a : x − b` becomes `x − (c ? a : b)`, and the same for `+`, `×` and a common
2861/// right operand. Exact for every value: both forms apply the same operator to the same operands of the arm taken.
2862pub fn hoist_common_operand(term: ZipTerm) -> ZipTerm {
2863    match term {
2864        ZipTerm::Op(op, l, r) => ZipTerm::Op(op, Box::new(hoist_common_operand(*l)), Box::new(hoist_common_operand(*r))),
2865        ZipTerm::Abs(x) => ZipTerm::Abs(Box::new(hoist_common_operand(*x))),
2866        ZipTerm::Select(c, a, b) => {
2867            let (c, a, b) = (hoist_common_operand(*c), hoist_common_operand(*a), hoist_common_operand(*b));
2868            match (a, b) {
2869                (ZipTerm::Op(o1, l1, r1), ZipTerm::Op(o2, l2, r2))
2870                    if o1 == o2 && matches!(o1, ZipOp::Add | ZipOp::Sub | ZipOp::Mul) && (l1 == l2 || r1 == r2) =>
2871                {
2872                    let select = |x: ZipTerm, y: ZipTerm| hoist_common_operand(ZipTerm::Select(Box::new(c.clone()), Box::new(x), Box::new(y)));
2873                    if l1 == l2 {
2874                        ZipTerm::Op(o1, l1, Box::new(select(*r1, *r2)))
2875                    } else {
2876                        ZipTerm::Op(o1, Box::new(select(*l1, *l2)), r1)
2877                    }
2878                }
2879                (a, b) => ZipTerm::Select(Box::new(c), Box::new(a), Box::new(b)),
2880            }
2881        }
2882        leaf => leaf,
2883    }
2884}
2885
2886/// How a scatter row's term reads an array: at the row (`y[r]`), at the column (`mu[c]`), or at both, row-major
2887/// (`x[r·C + c]`).
2888#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2889pub enum Axis {
2890    Row,
2891    Col,
2892    RowCol,
2893}
2894
2895/// A recognized scatter-matrix accumulation `S = Σ_{r<R} d_r d_rᵀ` (`C × C`, row-major, from zero): each row's vector
2896/// `d_r[c] = term` computed into a scratch array by an elementwise loop over the row, then added to `S` as a rank-1
2897/// update of itself — the covariance step of Gaussian discriminant analysis.
2898#[derive(Debug, Clone, PartialEq, Eq)]
2899pub struct ScatterSpec {
2900    pub rows: usize,
2901    pub cols: usize,
2902    /// The scratch row and the accumulated matrix.
2903    pub sub: Symbol,
2904    pub sigma: Symbol,
2905    /// The arrays the row term reads, in reading order, each with its axis; [`ZipTerm::Read`]`(k)` is
2906    /// `operands[k]` at that axis.
2907    pub operands: Vec<(Symbol, Axis)>,
2908    /// The row term, a select's common operand hoisted out of its arms ([`hoist_common_operand`]).
2909    pub term: ZipTerm,
2910}
2911
2912/// Why a program is not a recognizable scatter accumulation, least to most advanced.
2913#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2914pub enum ScatterReject {
2915    /// No `While counter is less than BOUND` loop.
2916    NoLoop,
2917    /// A loop with no elementwise loop over the row inside it.
2918    NoRowLoop,
2919    /// The row loop does not write one array at its counter (`Set item (c + k) of sub to …`, or an `If` whose two
2920    /// arms each write it).
2921    NoRowStore,
2922    /// The row term reads something other than array elements and constants, uses an operator or calls a function a
2923    /// term does not have, or is ill-typed.
2924    UnsupportedTerm,
2925    /// A well-typed row term, but not an `Int` over `Int` arrays — the integer covariance the app computes.
2926    NotInt,
2927    /// An index is not affine in the counters.
2928    NonAffineIndex,
2929    /// A read that is not at the row, the column, or the row-major element of both.
2930    NotScatterLayout,
2931    /// The row's vector is not added to a `C × C` matrix as a rank-1 update of itself (`S[i·C + j] += sub[i]·sub[j]`).
2932    NoRank1Update,
2933    /// A counter not set to a constant right before its loop, or not stepped by exactly one.
2934    NotCounted,
2935    /// A loop body does more than the scatter accumulation.
2936    ExtraWork,
2937    /// The matrix is not filled with zeros before the rows are added.
2938    NotFromZero,
2939}
2940
2941/// Recognize a scatter accumulation anywhere in a LOGOS statement list:
2942///
2943/// ```text
2944/// Let mutable r be 0.
2945/// While r is less than rows:
2946///     Let label be item (r + 1) of y.
2947///     Let mutable c be 0.
2948///     While c is less than cols:
2949///         If label is 1:
2950///             Set item (c + 1) of sub to item (r * cols + c + 1) of x - item (c + 1) of mu1.
2951///         Otherwise:
2952///             Set item (c + 1) of sub to item (r * cols + c + 1) of x - item (c + 1) of mu0.
2953///         Set c to c + 1.
2954///     Let mutable i be 0.
2955///     While i is less than cols:
2956///         Let mutable j be 0.
2957///         While j is less than cols:
2958///             Let idx be i * cols + j + 1.
2959///             Set item idx of sigma to item idx of sigma + item (i + 1) of sub * item (j + 1) of sub.
2960///             Set j to j + 1.
2961///         Set i to i + 1.
2962///     Set r to r + 1.
2963/// ```
2964///
2965/// Every loop is counted; the row loop writes the scratch row at its counter from reads at the row, the column, or
2966/// the row-major element; the rank-1 update adds the scratch row's outer product with itself to a zero-filled
2967/// `C × C` matrix; and every body holds nothing else but immutable names.
2968pub fn recognize_scatter(stmts: &[Stmt], interner: &Interner) -> Result<ScatterSpec, ScatterReject> {
2969    let mut consts: HashMap<Symbol, i64> = HashMap::new();
2970    collect_consts_rec(stmts, &mut consts);
2971    let program = Program::of(stmts, interner);
2972    let spec = search_loops(stmts, ScatterReject::NoLoop, &|block, at, cond, body| {
2973        try_scatter_at(block, at, cond, body, &consts, &program)
2974    })?;
2975    if recognize_fill(stmts, spec.sigma) != Fill::Const(0) {
2976        return Err(ScatterReject::NotFromZero);
2977    }
2978    Ok(spec)
2979}
2980
2981/// The reads of one scatter row term: each array with its axis, against the row counter `r`, the column counter
2982/// `c` and the row length `cols`.
2983struct Rows<'c> {
2984    r: Symbol,
2985    c: Symbol,
2986    cols: i64,
2987    consts: &'c HashMap<Symbol, i64>,
2988    lets: &'c HashMap<Symbol, Affine>,
2989    operands: Vec<(Symbol, Axis)>,
2990}
2991
2992impl<'c> Rows<'c> {
2993    fn read(&mut self, array: Symbol, index: &Expr) -> Result<usize, TermReject> {
2994        let at = eval_affine(index, self.consts, self.lets).ok_or(TermReject::NonAffine)?;
2995        let (cr, cc) = (at.coeff_of(self.r), at.coeff_of(self.c));
2996        let axis = match (at.coeff.len(), cr, cc) {
2997            (1, 1, 0) => Axis::Row,
2998            (1, 0, 1) => Axis::Col,
2999            (2, r, 1) if r == self.cols => Axis::RowCol,
3000            _ => return Err(TermReject::Layout),
3001        };
3002        match self.operands.iter().position(|(o, _)| *o == array) {
3003            Some(k) if self.operands[k].1 == axis => Ok(k),
3004            Some(_) => Err(TermReject::Layout),
3005            None => {
3006                self.operands.push((array, axis));
3007                Ok(self.operands.len() - 1)
3008            }
3009        }
3010    }
3011}
3012
3013impl From<TermReject> for ScatterReject {
3014    fn from(r: TermReject) -> ScatterReject {
3015        match r {
3016            TermReject::Unsupported => ScatterReject::UnsupportedTerm,
3017            TermReject::NonAffine => ScatterReject::NonAffineIndex,
3018            TermReject::Layout => ScatterReject::NotScatterLayout,
3019        }
3020    }
3021}
3022
3023/// The row loop's write: `Set item IDX of sub to T` alone, or an `If P` whose two arms each write `sub` at the same
3024/// index — `(sub, IDX, guard, T, else-T)`.
3025fn row_store<'a>(
3026    s: &'a Stmt<'a>,
3027) -> Option<(Symbol, &'a Expr<'a>, Option<&'a Expr<'a>>, &'a Expr<'a>, Option<&'a Expr<'a>>)> {
3028    let store = |s: &'a Stmt<'a>| match s {
3029        Stmt::SetIndex { collection, index, value } => Some((as_ident(collection)?, *index, *value)),
3030        _ => None,
3031    };
3032    if let Some((sub, idx, value)) = store(s) {
3033        return Some((sub, idx, None, value, None));
3034    }
3035    let Stmt::If { cond, then_block, else_block: Some(else_block) } = s else { return None };
3036    let ([then], [other]) = (then_block, else_block) else { return None };
3037    let ((sub, idx, a), (sub2, _, b)) = (store(then)?, store(other)?);
3038    (sub == sub2).then_some((sub, idx, Some(*cond), a, Some(b)))
3039}
3040
3041fn try_scatter_at<'a>(
3042    stmts: &'a [Stmt<'a>],
3043    at: usize,
3044    cond: &'a Expr<'a>,
3045    body: &'a [Stmt<'a>],
3046    consts: &HashMap<Symbol, i64>,
3047    program: &Program<'a>,
3048) -> Result<ScatterSpec, ScatterReject> {
3049    let bound_r = loop_iv_bound(cond, consts).ok_or(ScatterReject::NoLoop)?;
3050    let loops: Vec<(usize, &Expr, &[Stmt])> = body
3051        .iter()
3052        .enumerate()
3053        .filter_map(|(k, s)| match s {
3054            Stmt::While { cond, body, .. } => Some((k, *cond, *body)),
3055            _ => None,
3056        })
3057        .collect();
3058    let (at_c, cond_c, body_c) = *loops.first().ok_or(ScatterReject::NoRowLoop)?;
3059    let bound_c = loop_iv_bound(cond_c, consts).ok_or(ScatterReject::NoRowLoop)?;
3060    let (pos, (sub, idx, guard, then, otherwise)) = body_c
3061        .iter()
3062        .enumerate()
3063        .find_map(|(k, s)| row_store(s).map(|w| (k, w)))
3064        .ok_or(ScatterReject::NoRowStore)?;
3065    let lr = counted_at(stmts, at, bound_r, body, consts).ok_or(ScatterReject::NotCounted)?;
3066    let lc = counted_at(body, at_c, bound_c, body_c, consts).ok_or(ScatterReject::NotCounted)?;
3067    let (lets, exprs) = body_lets(body[..at_c].iter().chain(body_c), consts);
3068    let at_sub = eval_affine(idx, consts, &lets).ok_or(ScatterReject::NonAffineIndex)?;
3069    if at_sub.coeff.len() != 1 || at_sub.coeff_of(lc.iv) != 1 {
3070        return Err(ScatterReject::NoRowStore);
3071    }
3072    let mut rows = Rows { r: lr.iv, c: lc.iv, cols: lc.trips, consts, lets: &lets, operands: Vec::new() };
3073    let mut terms = Terms::new(program, consts, &exprs, vec![lr.iv, lc.iv, sub]);
3074    let mut read = |a, i| rows.read(a, i);
3075    let term = match guard {
3076        None => terms.term(then, &mut read)?,
3077        Some(g) => {
3078            let g = terms.term(g, &mut read)?;
3079            let (a, b) = (terms.term(then, &mut read)?, terms.term(otherwise.expect("both arms"), &mut read)?);
3080            ZipTerm::Select(Box::new(g), Box::new(a), Box::new(b))
3081        }
3082    };
3083    let types = terms.types().ok_or(ScatterReject::UnsupportedTerm)?;
3084    match term.ty(&types) {
3085        None | Some(Ty::Bool) => return Err(ScatterReject::UnsupportedTerm),
3086        Some(Ty::Int) if types.iter().all(|t| *t == Ty::Int) => {}
3087        Some(_) => return Err(ScatterReject::NotInt),
3088    }
3089    let (at_i, cond_i, body_i) = *loops.get(1).ok_or(ScatterReject::NoRank1Update)?;
3090    let nest = outer_nest_at(body, at_i, cond_i, body_i, consts, program, true).map_err(|e| match e {
3091        OuterReject::NotCounted => ScatterReject::NotCounted,
3092        OuterReject::ExtraWork => ScatterReject::ExtraWork,
3093        _ => ScatterReject::NoRank1Update,
3094    })?;
3095    if (nest.a, nest.b) != (sub, sub) || (nest.m as i64, nest.n as i64) != (lc.trips, lc.trips) {
3096        return Err(ScatterReject::NoRank1Update);
3097    }
3098    let mut kept = vec![lr.iv, lc.iv, sub, nest.c];
3099    kept.extend(rows.operands.iter().map(|(o, _)| *o));
3100    let init_of = |block: &[Stmt], at_loop: usize, iv: Symbol, k: usize| {
3101        k < at_loop
3102            && block[k..at_loop].iter().all(|s| matches!(s, Stmt::Let { .. } | Stmt::Set { .. }))
3103            && matches!(&block[k], Stmt::Let { var, .. } | Stmt::Set { target: var, .. } if *var == iv)
3104    };
3105    let i_iv = loop_iv_bound(cond_i, consts).map(|(iv, _)| iv);
3106    let row_ok = body.iter().enumerate().all(|(k, s)| {
3107        k == at_c
3108            || k == at_i
3109            || is_unit_step(s, lr.iv)
3110            || init_of(body, at_c, lc.iv, k)
3111            || i_iv.map_or(false, |iv| init_of(body, at_i, iv, k))
3112            || is_body_let(s, &kept)
3113    }) && loops.len() == 2;
3114    let col_ok = body_c.iter().enumerate().all(|(k, s)| k == pos || is_unit_step(s, lc.iv) || is_body_let(s, &kept));
3115    if !(row_ok && col_ok) {
3116        return Err(ScatterReject::ExtraWork);
3117    }
3118    Ok(ScatterSpec {
3119        rows: lr.trips as usize,
3120        cols: lc.trips as usize,
3121        sub,
3122        sigma: nest.c,
3123        operands: rows.operands,
3124        term: hoist_common_operand(term),
3125    })
3126}
3127
3128/// Which of several equally near centroids a point joins.
3129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3130pub enum Ties {
3131    /// The first nearest (`If c is 0 or dist is less than bestDist`).
3132    First,
3133    /// The last nearest (`If c is 0 or dist is at most bestDist`).
3134    Last,
3135}
3136
3137/// A recognized k-means clustering (Lloyd's algorithm): `n` points of `f` integer features, row-major in `points`,
3138/// around `k` centroids, row-major in `centroids`, which start as the first `k` points. Each of `iters` epochs
3139/// assigns every point to its nearest centroid by the distance `Σ_d term(point[d], centroid[d])` (ties as `ties`
3140/// says), sums each centroid's points and counts them, then sets every centroid to the integer mean of its points —
3141/// 0 for a centroid with none.
3142#[derive(Debug, Clone, PartialEq, Eq)]
3143pub struct KmeansSpec {
3144    pub n: usize,
3145    pub k: usize,
3146    pub f: usize,
3147    pub iters: usize,
3148    pub points: Symbol,
3149    pub centroids: Symbol,
3150    /// The distance a feature adds: [`ZipTerm::Read`]`(0)` the point's, `Read(1)` the centroid's.
3151    pub distance: ZipTerm,
3152    pub ties: Ties,
3153}
3154
3155impl KmeansSpec {
3156    /// The final centroids (`k × f`, row-major) for the points `points` (`n × f`, row-major), as the program computes
3157    /// them; refused (with why) when a distance, a sum of distance terms in any order, or a feature sum over the
3158    /// points in any order leaves `ints`.
3159    pub fn run(&self, points: &[i64], ints: (i128, i128)) -> Result<Vec<i64>, Stop> {
3160        let (n, k, f) = (self.n, self.k, self.f);
3161        if points.len() < n * f || n < k {
3162            return Err(Stop::Mistyped);
3163        }
3164        let within = |v: i128| if ints.0 <= v && v <= ints.1 { Ok(v as i64) } else { Err(Stop::IntRange) };
3165        for d in 0..f {
3166            within((0..n).map(|p| (points[p * f + d] as i128).abs()).sum())?;
3167        }
3168        within(n as i128)?;
3169        let dag = TermDag::of(&self.distance);
3170        let mut cts: Vec<i64> = points[..k * f].to_vec();
3171        for _ in 0..self.iters {
3172            let (mut sums, mut counts) = (vec![0i64; k * f], vec![0i64; k]);
3173            for p in 0..n {
3174                let (mut best, mut best_dist) = (0usize, 0i64);
3175                for c in 0..k {
3176                    let (mut dist, mut magnitude) = (0i128, 0i128);
3177                    for d in 0..f {
3178                        let row = [Scalar::Int(points[p * f + d]), Scalar::Int(cts[c * f + d])];
3179                        let Scalar::Int(v) = dag.value(&row, ints, f64::MAX)? else { return Err(Stop::Mistyped) };
3180                        dist += v as i128;
3181                        magnitude += (v as i128).abs();
3182                        within(magnitude)?;
3183                    }
3184                    let dist = within(dist)?;
3185                    let nearer = match self.ties {
3186                        Ties::First => dist < best_dist,
3187                        Ties::Last => dist <= best_dist,
3188                    };
3189                    if c == 0 || nearer {
3190                        best = c;
3191                        best_dist = dist;
3192                    }
3193                }
3194                for d in 0..f {
3195                    sums[best * f + d] += points[p * f + d];
3196                }
3197                counts[best] += 1;
3198            }
3199            for c in 0..k {
3200                for d in 0..f {
3201                    cts[c * f + d] = if counts[c] == 0 { 0 } else { sums[c * f + d] / counts[c] };
3202                }
3203            }
3204        }
3205        Ok(cts)
3206    }
3207}
3208
3209/// Why a program is not a recognizable k-means clustering, least to most advanced.
3210#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
3211pub enum KmeansReject {
3212    /// No `While counter is less than BOUND` loop.
3213    NoLoop,
3214    /// No loop over the points whose body finds each point's nearest centroid: a loop over the centroids around a
3215    /// loop over the features.
3216    NoAssignment,
3217    /// The loop over the features does not add a term into one register.
3218    NoDistance,
3219    /// The distance term reads something other than the point's and the centroid's feature and constants, uses an
3220    /// operator or calls a function a term does not have, or is ill-typed.
3221    UnsupportedTerm,
3222    /// A distance, but not an `Int` over `Int` arrays — the integer clustering the app computes.
3223    NotInt,
3224    /// An index is not affine in the counters.
3225    NonAffineIndex,
3226    /// A read that is not the point's or the centroid's feature, row-major (`row · f + feature`).
3227    NotRowLayout,
3228    /// The loop over the centroids does not keep the nearest one (`If c is 0 or dist is at most bestDist:` setting
3229    /// `best` to `c` and `bestDist` to `dist`).
3230    NoArgmin,
3231    /// The point is not added to its nearest centroid's row of sums and counted once.
3232    NoAccumulation,
3233    /// No loop over the centroids setting each to its sum over its count, or to 0 when it has no points.
3234    NoUpdate,
3235    /// The sums and counts are not set to zero at the start of every epoch.
3236    NotZeroed,
3237    /// The centroids do not start as the first `k` points.
3238    NotFromFirstPoints,
3239    /// A counter not set to a constant right before its loop, or not stepped by exactly one.
3240    NotCounted,
3241    /// A loop body does more than the clustering.
3242    ExtraWork,
3243}
3244
3245impl From<TermReject> for KmeansReject {
3246    fn from(r: TermReject) -> KmeansReject {
3247        match r {
3248            TermReject::Unsupported => KmeansReject::UnsupportedTerm,
3249            TermReject::NonAffine => KmeansReject::NonAffineIndex,
3250            TermReject::Layout => KmeansReject::NotRowLayout,
3251        }
3252    }
3253}
3254
3255/// Recognize a k-means clustering anywhere in a LOGOS statement list — the epoch loop of Lloyd's algorithm:
3256///
3257/// ```text
3258/// While epoch is less than iters:
3259///     (zero `sums` (k · f) and `counts` (k), each with a loop setting every item to 0)
3260///     Let mutable p be 0.
3261///     While p is less than n:
3262///         Let mutable best be 0.
3263///         Let mutable bestDist be 0.
3264///         Let mutable c be 0.
3265///         While c is less than k:
3266///             Let mutable dist be 0.
3267///             Let mutable d be 0.
3268///             While d is less than f:
3269///                 Set dist to dist + (item (p * f + d + 1) of pts - item (c * f + d + 1) of cts) ** 2.
3270///                 Set d to d + 1.
3271///             If c is 0 or dist is at most bestDist:
3272///                 Set best to c.
3273///                 Set bestDist to dist.
3274///             Set c to c + 1.
3275///         (a loop over the features adding the point's row to row `best` of `sums`)
3276///         Set item (best + 1) of counts to item (best + 1) of counts + 1.
3277///         Set p to p + 1.
3278///     (a loop over the centroids, and in it over the features: `If cnt is 0:` the centroid's feature set to 0,
3279///      `Otherwise:` to its sum over `cnt`, `cnt` the centroid's count)
3280///     Set epoch to epoch + 1.
3281/// ```
3282///
3283/// where `cts` starts as the first `k · f` items of `pts` (a loop pushing `item (q + 1) of pts` onto it). Every loop
3284/// is counted and every body holds nothing else but immutable names and the counters' resets. `dist is less than
3285/// bestDist` keeps the first of equally near centroids, `at most` the last.
3286pub fn recognize_kmeans(stmts: &[Stmt], interner: &Interner) -> Result<KmeansSpec, KmeansReject> {
3287    let mut consts: HashMap<Symbol, i64> = HashMap::new();
3288    collect_consts_rec(stmts, &mut consts);
3289    let program = Program::of(stmts, interner);
3290    let spec = search_loops(stmts, KmeansReject::NoLoop, &|block, at, cond, body| {
3291        try_kmeans_at(block, at, cond, body, &consts, &program)
3292    })?;
3293    match copied_prefix(stmts, spec.centroids, &consts) {
3294        Some((src, len)) if src == spec.points && len == (spec.k * spec.f) as i64 && spec.n >= spec.k => Ok(spec),
3295        _ => Err(KmeansReject::NotFromFirstPoints),
3296    }
3297}
3298
3299/// The `While` at `block[at]`: its condition and body.
3300fn while_at<'a>(block: &'a [Stmt<'a>], at: usize) -> Option<(&'a Expr<'a>, &'a [Stmt<'a>])> {
3301    match &block[at] {
3302        Stmt::While { cond, body, .. } => Some((*cond, *body)),
3303        _ => None,
3304    }
3305}
3306
3307/// A counted loop at `block[at]` with its body: `Ok((counter, trips, body))`, `Err(NotCounted)` when its counter is not
3308/// counted, `None` when `block[at]` is not a `While` over a counter.
3309fn counted_while<'a>(
3310    block: &'a [Stmt<'a>],
3311    at: usize,
3312    consts: &HashMap<Symbol, i64>,
3313) -> Option<Result<(Symbol, i64, &'a [Stmt<'a>]), KmeansReject>> {
3314    let (cond, body) = while_at(block, at)?;
3315    let bound = loop_iv_bound(cond, consts)?;
3316    Some(counted_at(block, at, bound, body, consts).map(|c| (c.iv, c.trips, body)).ok_or(KmeansReject::NotCounted))
3317}
3318
3319/// The array `dst` is filled from, and how many items, when its first fill loop pushes `item (q + 1) of SRC` for its
3320/// counter `q` from 0 — a copy of `SRC`'s first items.
3321fn copied_prefix(stmts: &[Stmt], dst: Symbol, consts: &HashMap<Symbol, i64>) -> Option<(Symbol, i64)> {
3322    let at = stmts.iter().position(|s| {
3323        matches!(s, Stmt::While { body, .. }
3324            if body.iter().any(|b| matches!(b, Stmt::Push { collection, .. } if as_ident(collection) == Some(dst))))
3325    })?;
3326    let (iv, trips, body) = counted_while(stmts, at, consts)?.ok()?;
3327    let value = body.iter().find_map(|b| match b {
3328        Stmt::Push { value, collection } if as_ident(collection) == Some(dst) => Some(*value),
3329        _ => None,
3330    })?;
3331    let src = indexed(value, &[(iv, 1)], consts, &HashMap::new())?;
3332    (entry_values(stmts, at, consts).get(&iv).copied().flatten() == Some(0)).then_some((src, trips))
3333}
3334
3335/// `item IDX of A` with `IDX` affine exactly as `pairs` (each counter's coefficient) plus 1: `A`.
3336fn indexed(e: &Expr, pairs: &[(Symbol, i64)], consts: &HashMap<Symbol, i64>, lets: &HashMap<Symbol, Affine>) -> Option<Symbol> {
3337    let (coll, index) = as_index(e)?;
3338    let at = eval_affine(index, consts, lets)?;
3339    let shaped = at.cst == 1 && at.coeff.len() == pairs.len() && pairs.iter().all(|(s, k)| at.coeff_of(*s) == *k);
3340    shaped.then(|| as_ident(coll)).flatten()
3341}
3342
3343/// `Set target to target + E` (either order): `(target, E)`.
3344fn accumulation<'a>(s: &'a Stmt<'a>) -> Option<(Symbol, &'a Expr<'a>)> {
3345    let Stmt::Set { target, value } = s else { return None };
3346    let (l, r) = as_binop(value, BinaryOpKind::Add)?;
3347    match (as_ident(l) == Some(*target), as_ident(r) == Some(*target)) {
3348        (true, _) => Some((*target, r)),
3349        (_, true) => Some((*target, l)),
3350        _ => None,
3351    }
3352}
3353
3354/// `Set item IDX of A to item IDX of A + E` (either order) at `IDX` affine as `pairs` plus 1: `(A, E)`.
3355fn indexed_accumulation<'a>(
3356    s: &'a Stmt<'a>,
3357    pairs: &[(Symbol, i64)],
3358    consts: &HashMap<Symbol, i64>,
3359) -> Option<(Symbol, &'a Expr<'a>)> {
3360    let Stmt::SetIndex { collection, index, value } = s else { return None };
3361    let a = as_ident(collection)?;
3362    let at = eval_affine(index, consts, &HashMap::new())?;
3363    if !(at.cst == 1 && at.coeff.len() == pairs.len() && pairs.iter().all(|(v, k)| at.coeff_of(*v) == *k)) {
3364        return None;
3365    }
3366    let (l, r) = as_binop(value, BinaryOpKind::Add)?;
3367    let own = |x: &Expr| indexed(x, pairs, consts, &HashMap::new()) == Some(a);
3368    if own(l) {
3369        Some((a, r))
3370    } else if own(r) {
3371        Some((a, l))
3372    } else {
3373        None
3374    }
3375}
3376
3377/// A loop setting every item of one array to 0, its counter from 0: `(array, items)`.
3378fn zeroing(block: &[Stmt], at: usize, consts: &HashMap<Symbol, i64>) -> Option<(Symbol, i64)> {
3379    let (iv, trips, body) = counted_while(block, at, consts)?.ok()?;
3380    let store = |s: &Stmt| match s {
3381        Stmt::SetIndex { collection, index, value: Expr::Literal(Literal::Number(0)) } => {
3382            let at = eval_affine(index, consts, &HashMap::new())?;
3383            (at.cst == 1 && at.coeff.len() == 1 && at.coeff_of(iv) == 1).then(|| as_ident(collection)).flatten()
3384        }
3385        _ => None,
3386    };
3387    let array = match body {
3388        [a, b] if is_unit_step(b, iv) => store(a)?,
3389        [a, b] if is_unit_step(a, iv) => store(b)?,
3390        _ => return None,
3391    };
3392    (entry_values(block, at, consts).get(&iv).copied().flatten() == Some(0)).then_some((array, trips))
3393}
3394
3395/// Whether `block[k]` resets the counter of a later loop of `block` to a number, or names something immutably
3396/// without rebinding `kept`: what a body may hold beside the clustering.
3397fn is_scaffold(block: &[Stmt], k: usize, kept: &[Symbol]) -> bool {
3398    let resets = |iv: Symbol| {
3399        matches!(&block[k], Stmt::Let { var, mutable: true, value: Expr::Literal(Literal::Number(_)), .. }
3400            | Stmt::Set { target: var, value: Expr::Literal(Literal::Number(_)) } if *var == iv)
3401    };
3402    is_body_let(&block[k], kept)
3403        || block[k + 1..].iter().any(|later| match later {
3404            Stmt::While { cond, .. } => as_binop_any(cond).and_then(|(l, _)| as_ident(l)).map_or(false, resets),
3405            _ => false,
3406        })
3407}
3408
3409/// `Let mutable v be N.` for a number `N` (or exactly 0, when `zero`).
3410fn starts_at(s: &Stmt, v: Symbol, zero: bool) -> bool {
3411    matches!(s, Stmt::Let { var, mutable: true, value: Expr::Literal(Literal::Number(n)), .. } if *var == v && (!zero || *n == 0))
3412}
3413
3414/// The assignment step of an epoch: the loop over the points, in it the loop over the centroids keeping the nearest
3415/// by the distance its loop over the features sums, then the point added to that centroid's sums and counted.
3416struct Assignment {
3417    at: usize,
3418    n: i64,
3419    k: i64,
3420    f: i64,
3421    kept: Vec<Symbol>,
3422    points: Symbol,
3423    centroids: Symbol,
3424    sums: Symbol,
3425    counts: Symbol,
3426    distance: ZipTerm,
3427    ties: Ties,
3428}
3429
3430impl Assignment {
3431    /// The assignment at `body[at]`; `None` when `body[at]` is not a loop with a loop around a loop inside it.
3432    fn at<'a>(
3433        body: &'a [Stmt<'a>],
3434        at: usize,
3435        consts: &HashMap<Symbol, i64>,
3436        program: &Program<'a>,
3437    ) -> Option<Result<Assignment, KmeansReject>> {
3438        let (cond_p, body_p) = while_at(body, at)?;
3439        let (p, _) = loop_iv_bound(cond_p, consts)?;
3440        let at_c = body_p.iter().position(|s| matches!(s, Stmt::While { body, .. } if body.iter().any(|t| matches!(t, Stmt::While { .. }))))?;
3441        let (cond_c, body_c) = while_at(body_p, at_c)?;
3442        let (c, _) = loop_iv_bound(cond_c, consts)?;
3443        let at_d = body_c.iter().position(|s| matches!(s, Stmt::While { .. }))?;
3444        let (cond_d, body_d) = while_at(body_c, at_d)?;
3445        let (d, _) = loop_iv_bound(cond_d, consts)?;
3446        Some(Self::read(body, at, (p, body_p), (c, at_c, body_c), (d, at_d, body_d), consts, program))
3447    }
3448
3449    #[allow(clippy::too_many_arguments)]
3450    fn read<'a>(
3451        body: &'a [Stmt<'a>],
3452        at: usize,
3453        (p, body_p): (Symbol, &'a [Stmt<'a>]),
3454        (c, at_c, body_c): (Symbol, usize, &'a [Stmt<'a>]),
3455        (d, at_d, body_d): (Symbol, usize, &'a [Stmt<'a>]),
3456        consts: &HashMap<Symbol, i64>,
3457        program: &Program<'a>,
3458    ) -> Result<Assignment, KmeansReject> {
3459        // The distance: one register summing a term of the point's and the centroid's feature.
3460        let (pos_dist, (dist, term)) = body_d
3461            .iter()
3462            .enumerate()
3463            .find_map(|(k, s)| accumulation(s).filter(|(t, _)| *t != d).map(|a| (k, a)))
3464            .ok_or(KmeansReject::NoDistance)?;
3465        let (lets, exprs) = body_lets(body_p[..at_c].iter().chain(&body_c[..at_d]).chain(body_d), consts);
3466        let mut roles: [Option<Symbol>; 2] = [None, None];
3467        let mut width: Option<i64> = None;
3468        let mut read = |array: Symbol, index: &'a Expr<'a>| -> Result<usize, TermReject> {
3469            let at = eval_affine(index, consts, &lets).ok_or(TermReject::NonAffine)?;
3470            let role = [p, c].iter().position(|r| at.coeff_of(*r) != 0).ok_or(TermReject::Layout)?;
3471            let w = at.coeff_of([p, c][role]);
3472            if at.coeff.len() != 2 || at.coeff_of(d) != 1 || at.cst != 1 || width.map_or(false, |x| x != w) {
3473                return Err(TermReject::Layout);
3474            }
3475            width = Some(w);
3476            match roles[role] {
3477                Some(a) if a != array => Err(TermReject::Layout),
3478                _ => {
3479                    roles[role] = Some(array);
3480                    Ok(role)
3481                }
3482            }
3483        };
3484        let mut terms = Terms::new(program, consts, &exprs, vec![p, c, d, dist]);
3485        let distance = terms.term(term, &mut read)?;
3486        let ([Some(points), Some(centroids)], Some(f)) = (roles, width) else { return Err(KmeansReject::NotRowLayout) };
3487        let types = terms.types().ok_or(KmeansReject::NotRowLayout)?;
3488        match distance.ty(&types) {
3489            None | Some(Ty::Bool) => return Err(KmeansReject::UnsupportedTerm),
3490            Some(Ty::Int) if types == [Ty::Int, Ty::Int] => {}
3491            Some(_) => return Err(KmeansReject::NotInt),
3492        }
3493        if points == centroids {
3494            return Err(KmeansReject::NotRowLayout);
3495        }
3496        // The nearest centroid kept.
3497        let (pos_if, best, best_dist, ties) = body_c
3498            .iter()
3499            .enumerate()
3500            .find_map(|(k, s)| argmin_update(s, c, dist).map(|(b, bd, t)| (k, b, bd, t)))
3501            .ok_or(KmeansReject::NoArgmin)?;
3502        // The point's row added to row `best` of the sums, and the point counted.
3503        let row_sum = |k: usize| -> Option<(Symbol, i64)> {
3504            let (e, trips, body_e) = counted_while(body_p, k, consts)?.ok()?;
3505            let [a, b] = body_e else { return None };
3506            let add = if is_unit_step(b, e) { a } else if is_unit_step(a, e) { b } else { return None };
3507            let (sums, x) = indexed_accumulation(add, &[(best, f), (e, 1)], consts)?;
3508            (indexed(x, &[(p, f), (e, 1)], consts, &HashMap::new()) == Some(points)).then_some((sums, trips))
3509        };
3510        let (at_sum, (sums, sum_trips)) = (0..body_p.len())
3511            .filter(|k| *k != at_c)
3512            .find_map(|k| row_sum(k).map(|r| (k, r)))
3513            .ok_or(KmeansReject::NoAccumulation)?;
3514        let counted = |s: &Stmt| {
3515            indexed_accumulation(s, &[(best, 1)], consts)
3516                .filter(|(_, x)| matches!(x, Expr::Literal(Literal::Number(1))))
3517                .map(|(a, _)| a)
3518        };
3519        let (at_count, counts) = body_p
3520            .iter()
3521            .enumerate()
3522            .find_map(|(k, s)| counted(s).map(|a| (k, a)))
3523            .ok_or(KmeansReject::NoAccumulation)?;
3524        if sum_trips != f || sums == counts || [sums, counts].contains(&points) || [sums, counts].contains(&centroids) {
3525            return Err(KmeansReject::NoAccumulation);
3526        }
3527        // Every loop counted, and the features as wide as a row.
3528        let (p, n, _) = counted_while(body, at, consts).expect("a loop over a counter")?;
3529        let (c, k, _) = counted_while(body_p, at_c, consts).expect("a loop over a counter")?;
3530        let (d, f_trips, _) = counted_while(body_c, at_d, consts).expect("a loop over a counter")?;
3531        if f_trips != f {
3532            return Err(KmeansReject::NotRowLayout);
3533        }
3534        // Nothing else in the three bodies.
3535        let kept = vec![p, c, d, dist, best, best_dist, points, centroids, sums, counts];
3536        let point_ok = body_p.iter().enumerate().all(|(j, s)| {
3537            [at_c, at_sum, at_count].contains(&j)
3538                || is_unit_step(s, p)
3539                || starts_at(s, best, false)
3540                || starts_at(s, best_dist, false)
3541                || is_scaffold(body_p, j, &kept)
3542        }) && body_p.iter().any(|s| starts_at(s, best, false))
3543            && body_p.iter().any(|s| starts_at(s, best_dist, false));
3544        let centroid_ok = body_c.iter().enumerate().all(|(j, s)| {
3545            j == at_d || j == pos_if || is_unit_step(s, c) || starts_at(s, dist, true) || is_scaffold(body_c, j, &kept)
3546        }) && body_c[..at_d].iter().any(|s| starts_at(s, dist, true));
3547        let feature_ok =
3548            body_d.iter().enumerate().all(|(j, s)| j == pos_dist || is_unit_step(s, d) || is_body_let(s, &kept));
3549        if !(point_ok && centroid_ok && feature_ok) {
3550            return Err(KmeansReject::ExtraWork);
3551        }
3552        Ok(Assignment { at, n, k, f, kept, points, centroids, sums, counts, distance, ties })
3553    }
3554}
3555
3556/// `If c is 0 or dist is at most bestDist: Set best to c. Set bestDist to dist.` (the disjuncts in either order, the
3557/// sets in either order; `less than` for the first of equally near centroids): `(best, bestDist, ties)`.
3558fn argmin_update(s: &Stmt, c: Symbol, dist: Symbol) -> Option<(Symbol, Symbol, Ties)> {
3559    let Stmt::If { cond, then_block: [a, b], else_block: None } = s else { return None };
3560    let (l, r) = as_binop(cond, BinaryOpKind::Or)?;
3561    let first = |e: &Expr| {
3562        as_binop(e, BinaryOpKind::Eq)
3563            .map_or(false, |(x, z)| as_ident(x) == Some(c) && matches!(z, Expr::Literal(Literal::Number(0))))
3564    };
3565    let nearer = |e: &Expr| -> Option<(Symbol, Ties)> {
3566        let Expr::BinaryOp { op, left, right } = e else { return None };
3567        let ties = match op {
3568            BinaryOpKind::LtEq => Ties::Last,
3569            BinaryOpKind::Lt => Ties::First,
3570            _ => return None,
3571        };
3572        (as_ident(left) == Some(dist)).then_some((as_ident(right)?, ties))
3573    };
3574    let (best_dist, ties) = if first(l) { nearer(r)? } else if first(r) { nearer(l)? } else { return None };
3575    let set = |s: &Stmt| match s {
3576        Stmt::Set { target, value } => Some((*target, as_ident(value)?)),
3577        _ => None,
3578    };
3579    let (sa, sb) = (set(a)?, set(b)?);
3580    let best = [sa, sb].iter().find(|(_, v)| *v == c).map(|(t, _)| *t)?;
3581    ([sa, sb].contains(&(best_dist, dist)) && best != best_dist && best_dist != dist).then_some((best, best_dist, ties))
3582}
3583
3584/// The loop at `block[at]` over the centroids setting each to its mean — in a loop over the features, `If cnt is 0:`
3585/// the feature 0, `Otherwise:` `sums[j·f + e] / cnt`, `cnt` the centroid's count — as its trip count; `None` when it
3586/// is not a loop around a loop.
3587fn centroid_update(
3588    block: &[Stmt],
3589    at: usize,
3590    a: &Assignment,
3591    consts: &HashMap<Symbol, i64>,
3592) -> Option<Result<i64, KmeansReject>> {
3593    let (_, body_j) = while_at(block, at)?;
3594    let at_e = body_j.iter().position(|s| matches!(s, Stmt::While { .. }))?;
3595    Some((|| {
3596        let (j, trips, body_j) = counted_while(block, at, consts).expect("a loop")?;
3597        let (e, f_trips, body_e) = counted_while(body_j, at_e, consts).ok_or(KmeansReject::NoUpdate)??;
3598        let (lets, exprs) = body_lets(&body_j[..at_e], consts);
3599        let count = |x: &Expr| indexed(through_lets(x, &exprs), &[(j, 1)], consts, &lets) == Some(a.counts);
3600        let feature = |x: &Expr| indexed(x, &[(j, a.f), (e, 1)], consts, &lets);
3601        let set_feature = |s: &Stmt| -> Option<bool> {
3602            let Stmt::SetIndex { collection, index, value } = s else { return None };
3603            let at = eval_affine(index, consts, &lets)?;
3604            let ok = as_ident(collection) == Some(a.centroids)
3605                && at.cst == 1
3606                && at.coeff.len() == 2
3607                && at.coeff_of(j) == a.f
3608                && at.coeff_of(e) == 1;
3609            ok.then(|| match value {
3610                Expr::Literal(Literal::Number(0)) => true,
3611                v => as_binop(v, BinaryOpKind::Divide).map_or(false, |(n, d)| feature(n) == Some(a.sums) && count(d)),
3612            })
3613        };
3614        let rule = |s: &Stmt| {
3615            let Stmt::If { cond, then_block: [t], else_block: Some([o]) } = s else { return false };
3616            let empty =
3617                as_binop(cond, BinaryOpKind::Eq).map_or(false, |(x, z)| count(x) && matches!(z, Expr::Literal(Literal::Number(0))));
3618            let zero = matches!(t, Stmt::SetIndex { value: Expr::Literal(Literal::Number(0)), .. }) && set_feature(t) == Some(true);
3619            let mean = !matches!(o, Stmt::SetIndex { value: Expr::Literal(_), .. }) && set_feature(o) == Some(true);
3620            empty && zero && mean
3621        };
3622        let pos = body_e.iter().position(rule).ok_or(KmeansReject::NoUpdate)?;
3623        if f_trips != a.f {
3624            return Err(KmeansReject::NotRowLayout);
3625        }
3626        let mut kept = a.kept.clone();
3627        kept.extend([j, e]);
3628        let only = body_e.iter().enumerate().all(|(k, s)| k == pos || is_unit_step(s, e))
3629            && body_j.iter().enumerate().all(|(k, s)| k == at_e || is_unit_step(s, j) || is_scaffold(body_j, k, &kept));
3630        if only {
3631            Ok(trips)
3632        } else {
3633            Err(KmeansReject::ExtraWork)
3634        }
3635    })())
3636}
3637
3638fn try_kmeans_at<'a>(
3639    stmts: &'a [Stmt<'a>],
3640    at: usize,
3641    cond: &'a Expr<'a>,
3642    body: &'a [Stmt<'a>],
3643    consts: &HashMap<Symbol, i64>,
3644    program: &Program<'a>,
3645) -> Result<KmeansSpec, KmeansReject> {
3646    let bound = loop_iv_bound(cond, consts).ok_or(KmeansReject::NoLoop)?;
3647    let a = (0..body.len())
3648        .find_map(|k| Assignment::at(body, k, consts, program))
3649        .ok_or(KmeansReject::NoAssignment)??;
3650    let (at_u, trips) = (0..body.len())
3651        .filter(|k| *k != a.at)
3652        .find_map(|k| centroid_update(body, k, &a, consts).map(|r| r.map(|t| (k, t))))
3653        .ok_or(KmeansReject::NoUpdate)??;
3654    let zeroed: Vec<(usize, (Symbol, i64))> = (0..a.at).filter_map(|k| zeroing(body, k, consts).map(|z| (k, z))).collect();
3655    let has = |arr: Symbol, len: i64| zeroed.iter().any(|(_, z)| *z == (arr, len));
3656    if trips != a.k || !(has(a.sums, a.k * a.f) && has(a.counts, a.k)) {
3657        return Err(KmeansReject::NotZeroed);
3658    }
3659    let epoch = counted_at(stmts, at, bound, body, consts).ok_or(KmeansReject::NotCounted)?;
3660    let mut kept = a.kept.clone();
3661    kept.push(epoch.iv);
3662    let epoch_ok = body.iter().enumerate().all(|(k, s)| {
3663        k == a.at
3664            || k == at_u
3665            || zeroed.iter().any(|(z, _)| *z == k)
3666            || is_unit_step(s, epoch.iv)
3667            || is_scaffold(body, k, &kept)
3668    });
3669    if !epoch_ok {
3670        return Err(KmeansReject::ExtraWork);
3671    }
3672    if [a.points, a.centroids, a.sums, a.counts].iter().any(|x| program.elem(*x) != Some(Ty::Int)) {
3673        return Err(KmeansReject::NotInt);
3674    }
3675    Ok(KmeansSpec {
3676        n: a.n as usize,
3677        k: a.k as usize,
3678        f: a.f as usize,
3679        iters: epoch.trips as usize,
3680        points: a.points,
3681        centroids: a.centroids,
3682        distance: a.distance,
3683        ties: a.ties,
3684    })
3685}