Skip to main content

polydat_core/iteration/comprehension/ir/
program.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Immutable IR Program wrapper — spec §9.1.
5//!
6//! `Program` is the public surface of the compiled IR. It's
7//! `#[non_exhaustive]` and accessible by value but cannot be
8//! mutated after construction: the optimizer (§10) is the only
9//! path from AST to IR, and the resulting program is frozen.
10
11use serde::{Deserialize, Serialize};
12
13use super::op::Op;
14
15/// An immutable IR program — a finite, ordered sequence of
16/// [`Op`]s ending in [`Op::Dispense`].
17///
18/// `Program` is the load-bearing immutable public API per
19/// spec §9.1. The optimizer is the only constructor; consumers
20/// read via `ops` and [`stack_depth`](Program::stack_depth).
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22#[non_exhaustive]
23pub struct Program {
24    ops: Vec<Op>,
25}
26
27impl Program {
28    /// Construct a Program. The compiler (`super::compile`) is
29    /// the canonical caller; external code shouldn't call this
30    /// directly except in tests.
31    pub fn new(ops: Vec<Op>) -> Self {
32        Self { ops }
33    }
34
35    /// The opcode sequence. Borrowed; the program owns the
36    /// vec.
37    pub fn ops(&self) -> &[Op] {
38        &self.ops
39    }
40
41    /// Number of opcodes.
42    pub fn len(&self) -> usize {
43        self.ops.len()
44    }
45
46    /// Whether the program has no opcode.
47    pub fn is_empty(&self) -> bool {
48        self.ops.is_empty()
49    }
50
51    /// Maximum stack depth this program reaches during
52    /// interpretation. Used by the bounds checker (§9.3's
53    /// `O(depth(C))` operator-stack term).
54    pub fn stack_depth(&self) -> usize {
55        let mut depth: i64 = 0;
56        let mut max_depth: i64 = 0;
57        for op in &self.ops {
58            let (pop, push) = op.stack_effect();
59            depth -= pop as i64;
60            depth += push as i64;
61            if depth > max_depth {
62                max_depth = depth;
63            }
64        }
65        max_depth as usize
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::iteration::comprehension::source::Source;
73    use crate::iteration::comprehension::strategy::ZipMode;
74
75    fn push_clause(name: &str) -> Op {
76        Op::PushClause {
77            name: name.into(),
78            source: Source::Literal { values: vec![] },
79        }
80    }
81
82    #[test]
83    fn stack_depth_simple() {
84        // PUSH, PUSH, CARTESIAN(2), DISPENSE — max depth 2.
85        let p = Program::new(vec![
86            push_clause("a"),
87            push_clause("b"),
88            Op::Cartesian { n: 2 },
89            Op::Dispense,
90        ]);
91        assert_eq!(p.stack_depth(), 2);
92    }
93
94    #[test]
95    fn stack_depth_nested() {
96        // PUSH, PUSH, PUSH, CARTESIAN(3), DISPENSE — max depth 3.
97        let p = Program::new(vec![
98            push_clause("a"),
99            push_clause("b"),
100            push_clause("c"),
101            Op::Cartesian { n: 3 },
102            Op::Dispense,
103        ]);
104        assert_eq!(p.stack_depth(), 3);
105    }
106
107    #[test]
108    fn stack_depth_zip_then_cartesian() {
109        // PUSH a, PUSH b, ZIP(2), PUSH c, CARTESIAN(2)
110        let p = Program::new(vec![
111            push_clause("a"),
112            push_clause("b"),
113            Op::Zip {
114                n: 2,
115                mode: ZipMode::Strict,
116            },
117            push_clause("c"),
118            Op::Cartesian { n: 2 },
119            Op::Dispense,
120        ]);
121        assert_eq!(p.stack_depth(), 2);
122    }
123
124    #[test]
125    fn round_trip_serde() {
126        let p = Program::new(vec![push_clause("a"), Op::Dispense]);
127        let json = serde_json::to_string(&p).unwrap();
128        let back: Program = serde_json::from_str(&json).unwrap();
129        assert_eq!(p, back);
130    }
131}