Skip to main content

pmcp_workbook_runtime/
formula.rs

1//! The owned, serde/schemars-clean formula AST (CMP-01) the hand-rolled parser
2//! (workbook-compiler) builds and the DAG/`sheet_ir` executor consume.
3//!
4//! umya-quarantine invariant: no `umya`/`quick-xml`/`zip`/`pmcp-code-mode` type
5//! appears in any public signature here. Every node is owned
6//! `String`/`f64`/`bool`/`Box<Expr>`/owned-enum.
7//!
8//! Design (D-02):
9//! - A function call is a GENERIC [`Expr::Call`] carrying `name: String` +
10//!   `args: Vec<Expr>` — NOT per-function variants. The parser checks `name`
11//!   against the dialect `WHITELIST` at build time; the semantics layer
12//!   dispatches on the string. Widening the whitelist never churns this enum.
13//! - A range ([`Expr::Range`]) reuses [`crate::range_ref::RangeRef`], which
14//!   stores the sheet ONCE (`sheet` + `start` + `end`) — never duplicated.
15//! - [`Expr::ErrorLit`] wraps the SHARED [`crate::excel_error::ExcelError`]
16//!   (imported, NOT redefined) to represent a literal `#REF!`/`#N/A` parsed from
17//!   formula text.
18//!
19//! Derive note: every type derives `PartialEq` but NOT `Eq`, because
20//! [`Expr::Number`] carries an `f64` (which is not `Eq`).
21
22use serde::{Deserialize, Serialize};
23
24use crate::excel_error::ExcelError;
25use crate::range_ref::RangeRef;
26
27/// An owned Excel formula expression node.
28///
29/// Built by the workbook-compiler parser from `CellRecord.formula`; walked by
30/// the DAG reconstructor (refs/ranges/names → dependency edges) and the
31/// `sheet_ir` executor (leaf arithmetic → the pure-Rust scalar evaluator).
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
33pub enum Expr {
34    /// A single-cell reference, `$`-anchors already stripped (e.g. `"1_Inputs!E6"`
35    /// or a bare `"E6"`). Dependency identity does not depend on absolute vs
36    /// relative (D-07), so the parser normalizes to a plain A1/sheet!A1 string.
37    Ref(String),
38    /// A range reference (e.g. `B2:B10`), reusing the structured [`RangeRef`]
39    /// (sheet stored ONCE). Expanded to per-member-cell edges by the DAG (D-06).
40    Range(RangeRef),
41    /// A numeric literal.
42    Number(f64),
43    /// A string literal (the `""`-doubling already un-escaped by the lexer).
44    Str(String),
45    /// A boolean literal (`TRUE`/`FALSE`).
46    Bool(bool),
47    /// A binary operation (`left op right`).
48    BinaryOp {
49        /// The left operand.
50        left: Box<Expr>,
51        /// The operator.
52        op: BinOp,
53        /// The right operand.
54        right: Box<Expr>,
55    },
56    /// A unary operation (`op operand`).
57    UnaryOp {
58        /// The operator.
59        op: UnOp,
60        /// The operand.
61        operand: Box<Expr>,
62    },
63    /// A generic function call (D-02): `name` is checked against the dialect
64    /// `WHITELIST` at parse time; the semantics layer dispatches on it.
65    Call {
66        /// The function name (e.g. `"CEILING"`), case as authored.
67        name: String,
68        /// The positional arguments.
69        args: Vec<Expr>,
70    },
71    /// A defined-name reference (resolved against the manifest defined-names in
72    /// the DAG layer, D-07).
73    Name(String),
74    /// A literal Excel error parsed from the formula text (e.g. `#REF!`),
75    /// wrapping the SHARED [`ExcelError`] (imported from [`crate::excel_error`]).
76    ErrorLit(ExcelError),
77}
78
79/// The Excel binary operators (precedence handled by the parser, not encoded
80/// here).
81#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
82pub enum BinOp {
83    /// `+`
84    Add,
85    /// `-`
86    Sub,
87    /// `*`
88    Mul,
89    /// `/`
90    Div,
91    /// `^` (exponentiation; right-associative — handled by `f64::powf` in the
92    /// semantics layer, NOT lowered to a kernel which has no `Pow`).
93    Pow,
94    /// `&` (text concatenation).
95    Concat,
96    /// `=`
97    Eq,
98    /// `<>`
99    Ne,
100    /// `<`
101    Lt,
102    /// `>`
103    Gt,
104    /// `<=`
105    Le,
106    /// `>=`
107    Ge,
108}
109
110/// The Excel unary operators.
111#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
112pub enum UnOp {
113    /// Unary minus (negation).
114    Neg,
115    /// Unary plus (no-op, retained for fidelity).
116    Pos,
117    /// Postfix `%` (percent; `x%` == `x / 100`, applied in the semantics layer).
118    Percent,
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn number_serializes_to_its_value() {
127        let v = serde_json::to_value(Expr::Number(1.0)).expect("serialize");
128        assert_eq!(v["Number"], 1.0);
129    }
130
131    #[test]
132    fn call_carries_name_and_args() {
133        let expr = Expr::Call {
134            name: "CEILING".to_string(),
135            args: vec![Expr::Number(700.0), Expr::Number(50.0)],
136        };
137        let v = serde_json::to_value(&expr).expect("serialize Call");
138        assert_eq!(v["Call"]["name"], "CEILING");
139        assert_eq!(v["Call"]["args"][0]["Number"], 700.0);
140        assert_eq!(v["Call"]["args"][1]["Number"], 50.0);
141    }
142
143    #[test]
144    fn range_stores_sheet_once_via_rangeref() {
145        let expr = Expr::Range(RangeRef {
146            sheet: "5_Quantities".to_string(),
147            start: "B2".to_string(),
148            end: "B10".to_string(),
149        });
150        let v = serde_json::to_value(&expr).expect("serialize Range");
151        assert_eq!(v["Range"]["sheet"], "5_Quantities");
152        assert_eq!(v["Range"]["start"], "B2");
153        assert_eq!(v["Range"]["end"], "B10");
154    }
155
156    #[test]
157    fn binary_op_round_trips_shape() {
158        let expr = Expr::BinaryOp {
159            left: Box::new(Expr::Ref("A1".to_string())),
160            op: BinOp::Mul,
161            right: Box::new(Expr::Number(1.05)),
162        };
163        let v = serde_json::to_value(&expr).expect("serialize BinaryOp");
164        assert_eq!(v["BinaryOp"]["left"]["Ref"], "A1");
165        assert_eq!(v["BinaryOp"]["op"], "Mul");
166        assert_eq!(v["BinaryOp"]["right"]["Number"], 1.05);
167    }
168
169    #[test]
170    fn error_lit_wraps_shared_excel_error() {
171        let expr = Expr::ErrorLit(ExcelError::Ref);
172        let v = serde_json::to_value(&expr).expect("serialize ErrorLit");
173        assert_eq!(v["ErrorLit"], "Ref");
174    }
175
176    #[test]
177    fn unary_op_serializes() {
178        let expr = Expr::UnaryOp {
179            op: UnOp::Neg,
180            operand: Box::new(Expr::Number(3.0)),
181        };
182        let v = serde_json::to_value(&expr).expect("serialize UnaryOp");
183        assert_eq!(v["UnaryOp"]["op"], "Neg");
184        assert_eq!(v["UnaryOp"]["operand"]["Number"], 3.0);
185    }
186}