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}