Skip to main content

qcode/value/insn/
scan.rs

1//! `scan`: a total left-scan (prefix fold) over an array value.
2//!
3//! [`Scan`] threads an accumulator left-to-right across every lane of the array
4//! `src`, emitting the accumulator after each step: conceptually
5//!
6//! ```text
7//!   acc_0   = init
8//!   acc_i+1 = body(acc_i, src[i], captures…)
9//!   out[i]  = acc_i+1
10//! ```
11//!
12//! producing an array of the same length as `src`. It is the projectable
13//! representation of a loop whose per-element write depends on the previous
14//! iteration's result — `out[i] = f(out[i-1], i)` — which [`Map`](super::Map)
15//! cannot express because its body is element-local. The MT19937 seeding loop
16//! `mt[i] = 1812433253 * (mt[i-1] ^ (mt[i-1] >> 30)) + i` is the canonical case;
17//! its `src` is `enumerate(arr)`, so the body's element is the `(index, elem)`
18//! tuple and the `elem` half is simply unused.
19//!
20//! Like [`Map`](super::Map), `body` is a **function symbol** (not a value
21//! operand), so `Scan` stays an ordinary first-order SSA instruction. Its value
22//! operands are `init` and `src` plus any loop-invariant `captures` the body
23//! closes over. The body is **binary in (accumulator, element)**: its first
24//! parameter is the carried accumulator (typed as the result element), its second
25//! is the lane element of `src`.
26
27use crate::value::LocalValueId;
28
29use super::{
30    Callee,
31    mnemonic::{Args, MnemonicKind},
32};
33use smallvec::SmallVec;
34
35/// A total left-scan `out[i] = acc_i+1` where `acc_i+1 = body(acc_i, src[i],
36/// captures…)` and `acc_0 = init`. The result is `[U; N]` where `N` is `src`'s
37/// length and `U` is the body's return type (also the accumulator's type).
38#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
39pub struct Scan {
40    /// The pure per-element fold function, applied at each lane. A symbol, not an
41    /// operand — exactly like a direct call's target. Binary in `(accumulator,
42    /// element)`.
43    pub body: Callee,
44    /// The initial accumulator value (`acc_0`).
45    pub init: LocalValueId,
46    /// The array value scanned over.
47    pub src: LocalValueId,
48    /// Loop-invariant values the body closes over (the accumulator and element
49    /// are supplied per-lane by the scan itself). Empty for a closed body.
50    pub captures: Vec<LocalValueId>,
51}
52
53impl MnemonicKind for Scan {
54    fn opcode(&self) -> &'static str {
55        "scan"
56    }
57
58    fn args(&self) -> Args {
59        let mut args = SmallVec::with_capacity(2 + self.captures.len());
60        args.push(self.init);
61        args.push(self.src);
62        args.extend(self.captures.iter().copied());
63        args
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use crate::{
70        testing::TestContext,
71        value::{
72            FunctionBody, ValueId,
73            insn::{Callee, Mnemonic, mnemonic::MnemonicKind},
74        },
75    };
76
77    /// A `scan` renders as `scanl @body init src` (and a partial application
78    /// `scanl (@body c0) init src` when it captures loop invariants).
79    #[test]
80    fn scan_renders_as_scanl() {
81        let mut tc = TestContext::new();
82        let body = FunctionBody::make(&mut tc.ctx, "foo".into()).unwrap().id;
83        let host = FunctionBody::make(&mut tc.ctx, "host".into()).unwrap().id;
84        let entry = tc.ctx.get_or_make_block(0x2000, host);
85        {
86            let mut f = FunctionBody::from_id_mut(&mut tc.ctx, host);
87            f.set_root(entry).unwrap();
88            f.add_block(entry);
89        }
90        let i32_ty = tc.ctx.shared.types.get_or_make_int(4);
91        let array_ty = tc.ctx.shared.types.get_or_make_array(i32_ty, 8);
92        let (init, src, cap) = {
93            let mut b = tc.ctx.builder(entry);
94            (
95                b.push_param(4).id(),
96                b.push_param(32).id(),
97                b.push_param(4).id(),
98            )
99        };
100        if let ValueId::BlockParam(pid) = src {
101            tc.ctx.block_param_mut(pid).type_id = array_ty;
102        }
103
104        let plain = {
105            let mut b = tc.ctx.builder(entry);
106            b.push_scan(body, init, src, Vec::new()).id()
107        };
108        let ValueId::Instruction(plain_id) = plain else {
109            unreachable!()
110        };
111        let rendered = tc.ctx.get_insn(plain_id).as_statement().to_string();
112        assert!(
113            rendered.contains("scanl @foo"),
114            "scan renders as scanl, got: {rendered}"
115        );
116
117        let with_cap = {
118            let mut b = tc.ctx.builder(entry);
119            b.push_scan(body, init, src, vec![cap]).id()
120        };
121        let ValueId::Instruction(cap_id) = with_cap else {
122            unreachable!()
123        };
124        let rendered = tc.ctx.get_insn(cap_id).as_statement().to_string();
125        assert!(
126            rendered.contains("scanl (@foo "),
127            "a capturing scan renders as a partial application, got: {rendered}"
128        );
129    }
130
131    /// `push_scan` yields an array-typed value whose operands are `init`, `src`,
132    /// then captures — with `body` kept as a symbol, never an operand — and
133    /// `replace_value` rewrites the operands but never the body.
134    #[test]
135    fn scan_builds_with_array_result_and_symbol_body() {
136        let mut tc = TestContext::new();
137        let body = FunctionBody::make(&mut tc.ctx, "body".into()).unwrap().id;
138        let host = FunctionBody::make(&mut tc.ctx, "host".into()).unwrap().id;
139        let entry = tc.ctx.get_or_make_block(0x1000, host);
140        {
141            let mut f = FunctionBody::from_id_mut(&mut tc.ctx, host);
142            f.set_root(entry).unwrap();
143            f.add_block(entry);
144        }
145        let i32_ty = tc.ctx.shared.types.get_or_make_int(4);
146        let array_ty = tc.ctx.shared.types.get_or_make_array(i32_ty, 20);
147        let (init, src, cap) = {
148            let mut b = tc.ctx.builder(entry);
149            (
150                b.push_param(4).id(),
151                b.push_param(80).id(),
152                b.push_param(4).id(),
153            )
154        };
155        if let ValueId::BlockParam(pid) = src {
156            tc.ctx.block_param_mut(pid).type_id = array_ty;
157        }
158        let scan_val = {
159            let mut b = tc.ctx.builder(entry);
160            b.push_scan(body, init, src, vec![cap]).id()
161        };
162        let ValueId::Instruction(scan_id) = scan_val else {
163            panic!("push_scan should yield an instruction value");
164        };
165        let m = match tc.ctx.get_insn(scan_id).mnemonic().clone() {
166            Mnemonic::Scan(m) => m,
167            other => panic!("expected Scan, got {other:?}"),
168        };
169        assert_eq!(m.body, Callee::Real(body));
170        assert_eq!(
171            m.args().to_vec(),
172            vec![init.strip_func(), src.strip_func(), cap.strip_func()],
173            "init, src, then captures are the operands"
174        );
175        assert!(
176            !m.args().contains(&ValueId::Function(body).strip_func()),
177            "body is not an operand"
178        );
179        // Result type is the array type of `src` (same length, body return elem).
180        assert_eq!(tc.ctx.type_of(scan_val), array_ty);
181
182        let new_src = {
183            let mut b = tc.ctx.builder(entry);
184            b.push_param(80).id()
185        };
186        let mut rewritten = Mnemonic::Scan(m);
187        rewritten.replace_value(src.strip_func(), new_src.strip_func());
188        let Mnemonic::Scan(r) = rewritten else {
189            unreachable!()
190        };
191        assert_eq!(r.src, new_src.strip_func());
192        assert_eq!(
193            r.body,
194            Callee::Real(body),
195            "body symbol is untouched by replace_value"
196        );
197        assert_eq!(r.init, init.strip_func());
198        assert_eq!(r.captures, vec![cap.strip_func()]);
199    }
200}