Skip to main content

qcode/value/insn/
map.rs

1//! `map`: a total element-wise map over an array value.
2//!
3//! [`Map`] applies the pure **unary** function `body` to every lane of the array
4//! `src`, producing an array of the same length: conceptually `out[i] =
5//! body(src[i], captures…)`. The body takes the element only — index-aware bodies
6//! map over [`enumerate`](super::Intrinsic)`(arr)`, whose element is the `(index,
7//! elem)` tuple. The result element type is the body's return type, which need
8//! not equal the input element type. It is the projectable representation of a
9//! loop that rewrites a buffer element-wise (see `ARGPROMOTE_ARRAY_MAP.md`).
10//!
11//! `body` is a **function symbol** (like [`Call::target`](super::Call)), not a
12//! value operand, so `Map` stays an ordinary first-order SSA instruction: its
13//! value operands are `src` plus any loop-invariant `captures` the body closes
14//! over. The projection rewrite
15//! `Range(Map(body, src), k·osz, osz) → body(Range(src, k·isz, isz), captures…)`
16//! recovers one element as an expression without materializing the whole array.
17
18use crate::value::LocalValueId;
19
20use super::{
21    Callee,
22    mnemonic::{Args, MnemonicKind},
23};
24use smallvec::SmallVec;
25
26/// A total element-wise map `out[i] = body(src[i], captures…)`. The result is
27/// `[U; N]` where `N` is `src`'s length and `U` is the body's return type.
28#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
29pub struct Map {
30    /// The pure per-element function, applied at each lane. A symbol, not an
31    /// operand — exactly like a direct call's target. Unary in the element.
32    pub body: Callee,
33    /// The array value mapped over.
34    pub src: LocalValueId,
35    /// Loop-invariant values the body closes over (the element is supplied
36    /// per-lane by the map itself). Empty for a closed body.
37    pub captures: Vec<LocalValueId>,
38}
39
40impl MnemonicKind for Map {
41    fn opcode(&self) -> &'static str {
42        "map"
43    }
44
45    fn args(&self) -> Args {
46        let mut args = SmallVec::with_capacity(1 + self.captures.len());
47        args.push(self.src);
48        args.extend(self.captures.iter().copied());
49        args
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use crate::{
56        testing::TestContext,
57        value::{
58            FunctionBody, ValueId,
59            insn::{Callee, Mnemonic, mnemonic::MnemonicKind},
60        },
61    };
62
63    /// A `map` renders as Haskell `fmap`: `@body <$> src` (and a partial
64    /// application `(@body c0) <$> src` when it captures loop invariants).
65    #[test]
66    fn map_renders_as_fmap() {
67        let mut tc = TestContext::new();
68        let body = FunctionBody::make(&mut tc.ctx, "foo".into()).unwrap().id;
69        let host = FunctionBody::make(&mut tc.ctx, "host".into()).unwrap().id;
70        let entry = tc.ctx.get_or_make_block(0x2000, host);
71        {
72            let mut f = FunctionBody::from_id_mut(&mut tc.ctx, host);
73            f.set_root(entry).unwrap();
74            f.add_block(entry);
75        }
76        let i8 = tc.ctx.shared.types.get_or_make_int(1);
77        let array_ty = tc.ctx.shared.types.get_or_make_array(i8, 8);
78        let (src, cap) = {
79            let mut b = tc.ctx.builder(entry);
80            (b.push_param(8).id(), b.push_param(4).id())
81        };
82        if let ValueId::BlockParam(pid) = src {
83            tc.ctx.block_param_mut(pid).type_id = array_ty;
84        }
85
86        let plain = {
87            let mut b = tc.ctx.builder(entry);
88            b.push_map(body, src, Vec::new()).id()
89        };
90        let ValueId::Instruction(plain_id) = plain else {
91            unreachable!()
92        };
93        let rendered = tc.ctx.get_insn(plain_id).as_statement().to_string();
94        assert!(
95            rendered.contains("foo <$>"),
96            "map renders as fmap, got: {rendered}"
97        );
98
99        let with_cap = {
100            let mut b = tc.ctx.builder(entry);
101            b.push_map(body, src, vec![cap]).id()
102        };
103        let ValueId::Instruction(cap_id) = with_cap else {
104            unreachable!()
105        };
106        let rendered = tc.ctx.get_insn(cap_id).as_statement().to_string();
107        assert!(
108            rendered.contains("(foo ") && rendered.contains(") <$>"),
109            "a capturing map renders as a partial application, got: {rendered}"
110        );
111    }
112
113    #[test]
114    fn map_builds_with_array_result_and_symbol_body() {
115        let mut tc = TestContext::new();
116
117        // A pure per-element body function (its content is irrelevant here).
118        let body = FunctionBody::make(&mut tc.ctx, "body".into()).unwrap().id;
119
120        // A host function holding an `[i8;20]`-typed value to map over.
121        let host = FunctionBody::make(&mut tc.ctx, "host".into()).unwrap().id;
122        let entry = tc.ctx.get_or_make_block(0x1000, host);
123        {
124            let mut f = FunctionBody::from_id_mut(&mut tc.ctx, host);
125            f.set_root(entry).unwrap();
126            f.add_block(entry);
127        }
128        let i8 = tc.ctx.shared.types.get_or_make_int(1);
129        let array_ty = tc.ctx.shared.types.get_or_make_array(i8, 20);
130
131        let (src, cap) = {
132            let mut b = tc.ctx.builder(entry);
133            (b.push_param(20).id(), b.push_param(4).id())
134        };
135        // Type the source as the array (params default to int of their width)
136        // *before* building the map, so the map's result type picks it up.
137        if let ValueId::BlockParam(pid) = src {
138            tc.ctx.block_param_mut(pid).type_id = array_ty;
139        }
140        let map_val = {
141            let mut b = tc.ctx.builder(entry);
142            b.push_map(body, src, vec![cap]).id()
143        };
144        let ValueId::Instruction(map_id) = map_val else {
145            panic!("push_map should yield an instruction value");
146        };
147
148        let m = match tc.ctx.get_insn(map_id).mnemonic().clone() {
149            Mnemonic::Map(m) => m,
150            other => panic!("expected Map, got {other:?}"),
151        };
152
153        // `body` is a symbol; `src` + captures are the value operands.
154        assert_eq!(m.body, Callee::Real(body));
155        assert_eq!(
156            m.args().to_vec(),
157            vec![src.strip_func(), cap.strip_func()],
158            "src then captures are the operands"
159        );
160        assert!(
161            !m.args().contains(&ValueId::Function(body).strip_func()),
162            "body is not an operand"
163        );
164
165        // Result type is the array type of `src`.
166        assert_eq!(tc.ctx.type_of(map_val), array_ty);
167
168        // replace_value rewrites operands but never the body symbol.
169        let new_src = {
170            let mut b = tc.ctx.builder(entry);
171            b.push_param(20).id()
172        };
173        let mut rewritten = Mnemonic::Map(m);
174        rewritten.replace_value(src.strip_func(), new_src.strip_func());
175        let Mnemonic::Map(r) = rewritten else {
176            unreachable!()
177        };
178        assert_eq!(r.src, new_src.strip_func());
179        assert_eq!(
180            r.body,
181            Callee::Real(body),
182            "body symbol is untouched by replace_value"
183        );
184        assert_eq!(r.captures, vec![cap.strip_func()]);
185    }
186}