Skip to main content

tang_expr/int/
mod.rs

1//! Integer dialect: exact 32-bit integer graphs with loop-carried state.
2//!
3//! A separate graph from the f64 RISC [`ExprGraph`](crate::ExprGraph): the
4//! float ops are untouched, and nothing here is ever reassociated, contracted
5//! or simplified beyond folding constant-only nodes with the interpreter's own
6//! semantics. The contract is bit-exactness: [`IntGraph::eval`] (the CPU
7//! reference), the generated WGSL ([`IntGraph::to_wgsl_fn`]) and the generated
8//! Rust ([`IntGraph::to_rust`]) compute the same bits.
9//!
10//! - Types: `i32`, `u32`, `bool`; `i8`/`u8` exist only as storage
11//!   ([`Elem`]) and widen on load.
12//! - Arithmetic wraps (`+ − ×`, negation); shift amounts are taken mod 32;
13//!   `>>` is arithmetic on `i32`, logical on `u32`.
14//! - [`IntGraph::mul_wide`]: exact 32×32→64 as `(lo, hi)`; WGSL (no 64-bit
15//!   integers) gets 16-bit-limb helpers.
16//! - Loops: [`IntGraph::while_loop`] is structured SSA (`scf.while`): state
17//!   values, a condition and a next-state over [`INode::State`] nodes.
18//!   Results are [`INode::LoopOut`] nodes. Loops nest; every node belongs to
19//!   the innermost loop whose state it depends on, so invariant code is
20//!   hoisted by construction.
21//! - Buffers are read-only ([`IntGraph::load`]), except scratch buffers
22//!   ([`IntGraph::buffer_rw`]): small fixed-length arrays read and written
23//!   through ordered effects. An effect token ([`IntGraph::token`]) is
24//!   threaded from store to store ([`IntGraph::store`] returns the next
25//!   token); a scratch load ([`IntGraph::load_rw`]) depends on a token. The
26//!   tokens only order the effects through data dependencies: every backend
27//!   executes scratch loads and stores in the one shared schedule order
28//!   (never inlined, never moved), so effects are deterministic and
29//!   identical across backends whatever the graph. A builder that wants
30//!   "load sees the state as of its token" makes every load of a token feed
31//!   the store that consumes it. Everything else is pure. Results leave
32//!   through the caller's bindings of the outputs.
33
34mod emit;
35mod eval;
36mod sched;
37mod wire;
38
39pub use emit::WGSL_PRELUDE;
40pub use eval::Program;
41pub use wire::{Limits, WireError};
42
43use std::collections::HashMap;
44
45/// Value type of a node.
46#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
47pub enum IType {
48    /// Signed 32-bit, two's complement, wrapping.
49    I32,
50    /// Unsigned 32-bit, wrapping.
51    U32,
52    /// Boolean (comparison results, select conditions).
53    Bool,
54}
55
56/// Storage element type of a read-only buffer.
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
58pub enum Elem {
59    /// Sign-extended to `i32` on load.
60    I8,
61    /// Zero-extended to `u32` on load.
62    U8,
63    /// `i32`.
64    I32,
65    /// `u32`.
66    U32,
67}
68
69impl Elem {
70    /// The value type a load produces.
71    pub fn ty(self) -> IType {
72        match self {
73            Elem::I8 | Elem::I32 => IType::I32,
74            Elem::U8 | Elem::U32 => IType::U32,
75        }
76    }
77}
78
79/// Handle to a node of an [`IntGraph`].
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
81pub struct IId(pub(crate) u32);
82
83impl IId {
84    /// Raw index.
85    pub fn index(self) -> usize {
86        self.0 as usize
87    }
88}
89
90/// Binary integer ops (operands of one type, except shift amounts).
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
92pub enum BinOp {
93    /// Wrapping add.
94    Add,
95    /// Wrapping subtract.
96    Sub,
97    /// Wrapping multiply (low 32 bits).
98    Mul,
99    /// High 32 bits of the exact 64-bit product (signed for `i32`).
100    MulHi,
101    /// Bitwise / logical and.
102    And,
103    /// Bitwise / logical or.
104    Or,
105    /// Bitwise / logical xor.
106    Xor,
107    /// Left shift by `b mod 32`.
108    Shl,
109    /// Right shift by `b mod 32` (arithmetic for `i32`).
110    Shr,
111    /// Minimum.
112    Min,
113    /// Maximum.
114    Max,
115    /// Division, WGSL semantics: `x / 0 = x`, `i32::MIN / −1 = i32::MIN`;
116    /// signed division truncates.
117    Div,
118    /// Remainder, WGSL semantics: `x % 0 = 0`, `i32::MIN % −1 = 0`; the sign
119    /// follows the dividend.
120    Rem,
121}
122
123/// Comparisons (produce `bool`).
124#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
125pub enum CmpOp {
126    /// `==`
127    Eq,
128    /// `!=`
129    Ne,
130    /// `<`
131    Lt,
132    /// `<=`
133    Le,
134}
135
136/// Unary ops.
137#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
138pub enum UnOp {
139    /// Wrapping negation.
140    Neg,
141    /// Bitwise not (logical not on `bool`).
142    Not,
143    /// Count of leading zero bits (32 for 0).
144    Clz,
145}
146
147/// A node.
148#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
149pub enum INode {
150    /// Constant (bits; `bool` is 0/1).
151    Const(IType, u32),
152    /// Function input.
153    Param(u16, IType),
154    /// Binary op.
155    Bin(BinOp, IId, IId),
156    /// Unary op.
157    Un(UnOp, IId),
158    /// Comparison.
159    Cmp(CmpOp, IId, IId),
160    /// `if c { a } else { b }` (both sides evaluated).
161    Select(IId, IId, IId),
162    /// Reinterpret bits as another 32-bit type.
163    Bitcast(IType, IId),
164    /// `buf[index]` from a read-only buffer (index is `u32`).
165    Load(u16, IId),
166    /// State `k` of loop `l`, inside its body.
167    State(u32, u16),
168    /// Final state `k` of loop `l`, after it.
169    LoopOut(u32, u16),
170    /// The initial effect token (`u32`, value 0).
171    Tok,
172    /// `buf[index]` from a scratch buffer, as of effect token `tok`.
173    LoadRw(u16, IId, IId),
174    /// `buf[index] = value` on a scratch buffer after effect token `tok`;
175    /// yields the next token.
176    Store(u16, IId, IId, IId),
177}
178
179/// A structured while loop: `s = init; while cond(s) { s = next(s) }`.
180#[derive(Clone, Debug)]
181pub struct Loop {
182    /// Initial states (in the enclosing scope).
183    pub init: Vec<IId>,
184    /// Condition, checked before each iteration.
185    pub cond: IId,
186    /// Next states.
187    pub next: Vec<IId>,
188    /// Enclosing loop, if nested.
189    pub parent: Option<u32>,
190    /// Trip cap: when set, the loop also exits after this many iterations
191    /// (every backend counts the same way). Untrusted graphs always carry one.
192    pub cap: Option<u32>,
193}
194
195/// A buffer declaration.
196#[derive(Clone, Debug)]
197pub struct Buffer {
198    /// Name used by the lowerings.
199    pub name: String,
200    /// Element type.
201    pub elem: Elem,
202    /// Scratch buffer: `(length, base)`. Element `i < length` lives at
203    /// `name[base + i]` in WGSL (`base` is a host expression); out-of-range
204    /// loads read 0 and out-of-range stores are dropped.
205    pub rw: Option<(u32, String)>,
206}
207
208/// Integer expression graph with structural interning.
209#[derive(Clone, Default)]
210pub struct IntGraph {
211    nodes: Vec<INode>,
212    types: Vec<IType>,
213    /// Innermost loop each node depends on.
214    scope: Vec<Option<u32>>,
215    intern: HashMap<INode, IId>,
216    loops: Vec<Loop>,
217    bufs: Vec<Buffer>,
218    building: Vec<u32>,
219}
220
221impl IntGraph {
222    /// Empty graph.
223    pub fn new() -> Self {
224        Self::default()
225    }
226
227    /// Number of nodes.
228    pub fn len(&self) -> usize {
229        self.nodes.len()
230    }
231
232    /// Whether the graph has no nodes.
233    pub fn is_empty(&self) -> bool {
234        self.nodes.is_empty()
235    }
236
237    /// Node of `id`.
238    pub fn node(&self, id: IId) -> INode {
239        self.nodes[id.index()]
240    }
241
242    /// Type of `id`.
243    pub fn ty(&self, id: IId) -> IType {
244        self.types[id.index()]
245    }
246
247    /// Loop `l`.
248    pub fn loop_(&self, l: u32) -> &Loop {
249        &self.loops[l as usize]
250    }
251
252    /// Declared buffers.
253    pub fn buffers(&self) -> &[Buffer] {
254        &self.bufs
255    }
256
257    fn depth(&self, mut l: Option<u32>) -> usize {
258        let mut d = 0;
259        while let Some(x) = l {
260            d += 1;
261            l = self.loops[x as usize].parent;
262        }
263        d
264    }
265
266    /// Innermost of two scopes, which must lie on one chain of nested loops.
267    fn inner(&self, a: Option<u32>, b: Option<u32>) -> Option<u32> {
268        self.try_inner(a, b)
269            .expect("int graph: node mixes states of unrelated loops")
270    }
271
272    fn try_inner(&self, a: Option<u32>, b: Option<u32>) -> Result<Option<u32>, &'static str> {
273        let (da, db) = (self.depth(a), self.depth(b));
274        let (deep, shallow, mut d, ds) = if da >= db {
275            (a, b, da, db)
276        } else {
277            (b, a, db, da)
278        };
279        let mut x = deep;
280        while d > ds {
281            x = self.loops[x.unwrap() as usize].parent;
282            d -= 1;
283        }
284        if x != shallow {
285            return Err("node mixes states of unrelated loops");
286        }
287        Ok(deep)
288    }
289
290    fn insert(&mut self, n: INode) -> IId {
291        if let Some(&id) = self.intern.get(&n) {
292            return id;
293        }
294        let (ty, scope) = self.infer(n);
295        if let Some(folded) = self.fold(n, ty) {
296            return folded;
297        }
298        let id = IId(self.nodes.len() as u32);
299        self.nodes.push(n);
300        self.types.push(ty);
301        self.scope.push(scope);
302        self.intern.insert(n, id);
303        id
304    }
305
306    fn infer(&self, n: INode) -> (IType, Option<u32>) {
307        self.try_infer(n)
308            .unwrap_or_else(|e| panic!("int graph: {e}"))
309    }
310
311    fn try_infer(&self, n: INode) -> Result<(IType, Option<u32>), &'static str> {
312        let sc = |a: IId| self.scope[a.index()];
313        let ok = |c: bool, e: &'static str| if c { Ok(()) } else { Err(e) };
314        Ok(match n {
315            INode::Const(t, _) => (t, None),
316            INode::Param(_, t) => (t, None),
317            INode::Bin(op, a, b) => {
318                let (ta, tb) = (self.ty(a), self.ty(b));
319                match op {
320                    BinOp::Shl | BinOp::Shr => {
321                        ok(ta != IType::Bool && tb != IType::Bool, "shift of bool")?
322                    }
323                    BinOp::And | BinOp::Or | BinOp::Xor => {
324                        ok(ta == tb, "bitwise op type mismatch")?
325                    }
326                    _ => {
327                        ok(ta == tb, "arithmetic type mismatch")?;
328                        ok(ta != IType::Bool, "arithmetic on bool")?;
329                    }
330                }
331                (ta, self.try_inner(sc(a), sc(b))?)
332            }
333            INode::Un(op, a) => {
334                ok(
335                    op == UnOp::Not || self.ty(a) != IType::Bool,
336                    "integer op on bool",
337                )?;
338                (self.ty(a), sc(a))
339            }
340            INode::Cmp(_, a, b) => {
341                ok(self.ty(a) == self.ty(b), "compare type mismatch")?;
342                (IType::Bool, self.try_inner(sc(a), sc(b))?)
343            }
344            INode::Select(c, a, b) => {
345                ok(self.ty(c) == IType::Bool, "select condition must be bool")?;
346                ok(self.ty(a) == self.ty(b), "select type mismatch")?;
347                let s = self.try_inner(sc(a), sc(b))?;
348                (self.ty(a), self.try_inner(s, sc(c))?)
349            }
350            INode::Bitcast(t, a) => {
351                ok(
352                    t != IType::Bool && self.ty(a) != IType::Bool,
353                    "bitcast of bool",
354                )?;
355                (t, sc(a))
356            }
357            INode::Load(b, i) => {
358                ok(self.ty(i) == IType::U32, "load index must be u32")?;
359                let buf = self.bufs.get(b as usize).ok_or("buffer")?;
360                ok(buf.rw.is_none(), "plain load from a scratch buffer")?;
361                (buf.elem.ty(), sc(i))
362            }
363            INode::Tok => (IType::U32, None),
364            INode::LoadRw(b, i, t) => {
365                let buf = self.bufs.get(b as usize).ok_or("buffer")?;
366                ok(buf.rw.is_some(), "scratch load from a read-only buffer")?;
367                ok(
368                    self.ty(i) == IType::U32 && self.ty(t) == IType::U32,
369                    "scratch load index and token must be u32",
370                )?;
371                let s = self.try_inner(sc(i), sc(t))?;
372                ok(s == sc(t), "scratch load outside its token's scope")?;
373                (buf.elem.ty(), s)
374            }
375            INode::Store(b, i, v, t) => {
376                let buf = self.bufs.get(b as usize).ok_or("buffer")?;
377                ok(buf.rw.is_some(), "store to a read-only buffer")?;
378                ok(
379                    self.ty(i) == IType::U32 && self.ty(t) == IType::U32,
380                    "store index and token must be u32",
381                )?;
382                ok(self.ty(v) == buf.elem.ty(), "store value type")?;
383                let s = self.try_inner(self.try_inner(sc(i), sc(v))?, sc(t))?;
384                ok(s == sc(t), "store outside its token's scope")?;
385                (IType::U32, s)
386            }
387            INode::State(l, k) => (self.ty(self.loops[l as usize].init[k as usize]), Some(l)),
388            INode::LoopOut(l, k) => (
389                self.ty(self.loops[l as usize].init[k as usize]),
390                self.loops[l as usize].parent,
391            ),
392        })
393    }
394
395    /// Fold nodes whose operands are all constants (same semantics as eval).
396    fn fold(&mut self, n: INode, ty: IType) -> Option<IId> {
397        let c = |a: IId| match self.nodes[a.index()] {
398            INode::Const(_, v) => Some(v),
399            _ => None,
400        };
401        let v = match n {
402            INode::Bin(op, a, b) => eval::bin(op, self.ty(a), c(a)?, c(b)?),
403            INode::Un(op, a) => eval::un(op, self.ty(a), c(a)?),
404            INode::Cmp(op, a, b) => eval::cmp(op, self.ty(a), c(a)?, c(b)?),
405            INode::Select(s, a, b) => {
406                if c(s)? != 0 {
407                    return Some(a);
408                } else {
409                    return Some(b);
410                }
411            }
412            INode::Bitcast(_, a) => c(a)?,
413            _ => return None,
414        };
415        Some(self.insert(INode::Const(ty, v)))
416    }
417
418    // ---- atoms ----
419
420    /// `i32` constant.
421    pub fn i32(&mut self, v: i32) -> IId {
422        self.insert(INode::Const(IType::I32, v as u32))
423    }
424
425    /// `u32` constant.
426    pub fn u32(&mut self, v: u32) -> IId {
427        self.insert(INode::Const(IType::U32, v))
428    }
429
430    /// `bool` constant.
431    pub fn bool(&mut self, v: bool) -> IId {
432        self.insert(INode::Const(IType::Bool, v as u32))
433    }
434
435    /// Function input `i` of type `t`.
436    pub fn param(&mut self, i: u16, t: IType) -> IId {
437        self.insert(INode::Param(i, t))
438    }
439
440    /// Declare a read-only buffer; returns its index for [`load`](Self::load).
441    pub fn buffer(&mut self, name: &str, elem: Elem) -> u16 {
442        self.bufs.push(Buffer {
443            name: name.into(),
444            elem,
445            rw: None,
446        });
447        (self.bufs.len() - 1) as u16
448    }
449
450    /// Declare a scratch buffer of `len` 32-bit elements (`elem` is `I32` or
451    /// `U32`), at `name[base + i]` in WGSL.
452    pub fn buffer_rw(&mut self, name: &str, elem: Elem, len: u32, base: &str) -> u16 {
453        assert!(
454            matches!(elem, Elem::I32 | Elem::U32),
455            "scratch buffers hold 32-bit elements"
456        );
457        self.bufs.push(Buffer {
458            name: name.into(),
459            elem,
460            rw: Some((len, base.into())),
461        });
462        (self.bufs.len() - 1) as u16
463    }
464
465    /// The initial effect token.
466    pub fn token(&mut self) -> IId {
467        self.insert(INode::Tok)
468    }
469
470    /// `buf[index]` of a scratch buffer as of effect token `tok`.
471    pub fn load_rw(&mut self, buf: u16, index: IId, tok: IId) -> IId {
472        self.insert(INode::LoadRw(buf, index, tok))
473    }
474
475    /// `buf[index] = value` after `tok`; returns the next token.
476    pub fn store(&mut self, buf: u16, index: IId, value: IId, tok: IId) -> IId {
477        self.insert(INode::Store(buf, index, value, tok))
478    }
479
480    /// `buf[index]`, widened per [`Elem`].
481    pub fn load(&mut self, buf: u16, index: IId) -> IId {
482        self.insert(INode::Load(buf, index))
483    }
484
485    // ---- ops ----
486
487    /// Binary op.
488    pub fn bin(&mut self, op: BinOp, a: IId, b: IId) -> IId {
489        self.insert(INode::Bin(op, a, b))
490    }
491
492    /// Wrapping `a + b`.
493    pub fn add(&mut self, a: IId, b: IId) -> IId {
494        self.bin(BinOp::Add, a, b)
495    }
496
497    /// Wrapping `a − b`.
498    pub fn sub(&mut self, a: IId, b: IId) -> IId {
499        self.bin(BinOp::Sub, a, b)
500    }
501
502    /// Wrapping `a × b`.
503    pub fn mul(&mut self, a: IId, b: IId) -> IId {
504        self.bin(BinOp::Mul, a, b)
505    }
506
507    /// Exact 64-bit product as `(lo, hi)`.
508    pub fn mul_wide(&mut self, a: IId, b: IId) -> (IId, IId) {
509        (self.bin(BinOp::Mul, a, b), self.bin(BinOp::MulHi, a, b))
510    }
511
512    /// `a & b`.
513    pub fn and(&mut self, a: IId, b: IId) -> IId {
514        self.bin(BinOp::And, a, b)
515    }
516
517    /// `a | b`.
518    pub fn or(&mut self, a: IId, b: IId) -> IId {
519        self.bin(BinOp::Or, a, b)
520    }
521
522    /// `a ^ b`.
523    pub fn xor(&mut self, a: IId, b: IId) -> IId {
524        self.bin(BinOp::Xor, a, b)
525    }
526
527    /// `a << (b mod 32)`.
528    pub fn shl(&mut self, a: IId, b: IId) -> IId {
529        self.bin(BinOp::Shl, a, b)
530    }
531
532    /// `a >> (b mod 32)`, arithmetic for `i32`.
533    pub fn shr(&mut self, a: IId, b: IId) -> IId {
534        self.bin(BinOp::Shr, a, b)
535    }
536
537    /// `min(a, b)`.
538    pub fn min(&mut self, a: IId, b: IId) -> IId {
539        self.bin(BinOp::Min, a, b)
540    }
541
542    /// `max(a, b)`.
543    pub fn max(&mut self, a: IId, b: IId) -> IId {
544        self.bin(BinOp::Max, a, b)
545    }
546
547    /// `min(max(x, lo), hi)`.
548    pub fn clamp(&mut self, x: IId, lo: IId, hi: IId) -> IId {
549        let m = self.max(x, lo);
550        self.min(m, hi)
551    }
552
553    /// Wrapping `−a`.
554    pub fn neg(&mut self, a: IId) -> IId {
555        self.insert(INode::Un(UnOp::Neg, a))
556    }
557
558    /// `!a` (bitwise on integers).
559    pub fn not(&mut self, a: IId) -> IId {
560        self.insert(INode::Un(UnOp::Not, a))
561    }
562
563    /// Leading zero bits of `a` (`firstLeadingBit(a) = 31 − clz(a)` for `a > 0`).
564    pub fn clz(&mut self, a: IId) -> IId {
565        self.insert(INode::Un(UnOp::Clz, a))
566    }
567
568    /// Comparison.
569    pub fn cmp(&mut self, op: CmpOp, a: IId, b: IId) -> IId {
570        self.insert(INode::Cmp(op, a, b))
571    }
572
573    /// `a == b`.
574    pub fn eq(&mut self, a: IId, b: IId) -> IId {
575        self.cmp(CmpOp::Eq, a, b)
576    }
577
578    /// `a < b`.
579    pub fn lt(&mut self, a: IId, b: IId) -> IId {
580        self.cmp(CmpOp::Lt, a, b)
581    }
582
583    /// `a <= b`.
584    pub fn le(&mut self, a: IId, b: IId) -> IId {
585        self.cmp(CmpOp::Le, a, b)
586    }
587
588    /// `if c { a } else { b }`.
589    pub fn select(&mut self, c: IId, a: IId, b: IId) -> IId {
590        self.insert(INode::Select(c, a, b))
591    }
592
593    /// Reinterpret as `t`.
594    pub fn bitcast(&mut self, t: IType, a: IId) -> IId {
595        if self.ty(a) == t {
596            return a;
597        }
598        self.insert(INode::Bitcast(t, a))
599    }
600
601    // ---- loops ----
602
603    /// `s = init; while cond(s) { s = next(s) }`; returns the final states.
604    ///
605    /// `body` gets the state nodes and returns `(cond, next)`.
606    pub fn while_loop(
607        &mut self,
608        init: &[IId],
609        body: impl FnOnce(&mut Self, &[IId]) -> (IId, Vec<IId>),
610    ) -> Vec<IId> {
611        let l = self.loops.len() as u32;
612        let parent = self.building.last().copied();
613        for &x in init {
614            // inits live in the enclosing scope
615            self.inner(self.scope[x.index()], parent);
616        }
617        self.loops.push(Loop {
618            init: init.to_vec(),
619            cond: IId(0),
620            next: Vec::new(),
621            parent,
622            cap: None,
623        });
624        self.building.push(l);
625        let st: Vec<IId> = (0..init.len())
626            .map(|k| self.insert(INode::State(l, k as u16)))
627            .collect();
628        let (cond, next) = body(self, &st);
629        self.building.pop();
630        assert_eq!(self.ty(cond), IType::Bool, "loop condition must be bool");
631        assert_eq!(next.len(), init.len(), "loop next-state arity");
632        for (k, &n) in next.iter().enumerate() {
633            assert_eq!(self.ty(n), self.ty(init[k]), "loop state {k} changes type");
634        }
635        self.loops[l as usize].cond = cond;
636        self.loops[l as usize].next = next;
637        (0..init.len())
638            .map(|k| self.insert(INode::LoopOut(l, k as u16)))
639            .collect()
640    }
641
642    /// Cap the trip count of the loop that produced `out` (a loop result).
643    pub fn cap_loop(&mut self, out: IId, cap: u32) {
644        match self.node(out) {
645            INode::LoopOut(l, _) => self.loops[l as usize].cap = Some(cap),
646            _ => panic!("cap_loop: not a loop result"),
647        }
648    }
649
650    /// Unsigned / signed division (WGSL semantics, see [`BinOp::Div`]).
651    pub fn div(&mut self, a: IId, b: IId) -> IId {
652        self.bin(BinOp::Div, a, b)
653    }
654
655    /// Remainder (WGSL semantics, see [`BinOp::Rem`]).
656    pub fn rem(&mut self, a: IId, b: IId) -> IId {
657        self.bin(BinOp::Rem, a, b)
658    }
659
660    /// `for i in 0..n` (`n: u32`) over states; `body(g, i, s)` returns next states.
661    pub fn for_range(
662        &mut self,
663        n: IId,
664        init: &[IId],
665        body: impl FnOnce(&mut Self, IId, &[IId]) -> Vec<IId>,
666    ) -> Vec<IId> {
667        let zero = self.u32(0);
668        let mut s0 = vec![zero];
669        s0.extend_from_slice(init);
670        let out = self.while_loop(&s0, |g, s| {
671            let c = g.lt(s[0], n);
672            let one = g.u32(1);
673            let mut next = vec![g.add(s[0], one)];
674            next.extend(body(g, s[0], &s[1..]));
675            (c, next)
676        });
677        out[1..].to_vec()
678    }
679}
680
681#[cfg(test)]
682mod tests;