cel_core/ast.rs
1//! Unified AST representation for CEL expressions.
2//!
3//! This module provides the `Ast` type which wraps a parsed (and optionally type-checked)
4//! CEL expression. It follows the cel-go architecture pattern where a single `Ast` type
5//! can represent both parsed and checked expressions.
6//!
7//! # Example
8//!
9//! ```
10//! use cel_core::{Env, Ast};
11//!
12//! let env = Env::with_standard_library()
13//! .with_variable("x", cel_core::CelType::Int);
14//!
15//! // Compile returns a checked Ast
16//! let ast = env.compile("x + 1").unwrap();
17//! assert!(ast.is_checked());
18//!
19//! // Convert to CEL string
20//! let cel_string = ast.to_cel_string();
21//! ```
22//!
23//! # Proto Conversion
24//!
25//! For proto wire format conversion (interop with cel-go, cel-cpp), use the
26//! `cel-core-proto` crate:
27//!
28//! ```ignore
29//! use cel_core_proto::AstToProto;
30//!
31//! let parsed_expr = ast.to_parsed_expr();
32//! let checked_expr = ast.to_checked_expr()?;
33//! ```
34
35use std::sync::Arc;
36
37use crate::checker::CheckResult;
38use crate::types::{CelType, SpannedExpr};
39
40/// Error type for Ast operations.
41#[derive(Debug, Clone)]
42pub enum AstError {
43 /// The AST has not been type-checked, but a checked operation was requested.
44 NotChecked,
45 /// Error during conversion or other operations.
46 Other(String),
47}
48
49impl std::fmt::Display for AstError {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 AstError::NotChecked => write!(f, "AST has not been type-checked"),
53 AstError::Other(msg) => write!(f, "{}", msg),
54 }
55 }
56}
57
58impl std::error::Error for AstError {}
59
60/// Unified AST representation matching cel-go's Ast type.
61///
62/// An `Ast` can be either checked (has type info) or unchecked (parsed only).
63/// This follows the cel-go pattern where a single type represents both states.
64///
65/// # Creating an Ast
66///
67/// - Use `Env::compile()` to get a checked `Ast`
68/// - Use `Env::parse_only()` to get an unchecked `Ast`
69///
70/// # Proto Conversion
71///
72/// For proto wire format conversion, use the `cel-core-proto` crate directly.
73#[derive(Debug, Clone)]
74pub struct Ast {
75 /// The expression tree.
76 expr: SpannedExpr,
77 /// The original source text.
78 source: Arc<str>,
79 /// Type checking results (None if unchecked).
80 type_info: Option<CheckResult>,
81}
82
83impl Ast {
84 /// Create an unchecked AST (after parsing, before type checking).
85 pub fn new_unchecked(expr: SpannedExpr, source: impl Into<Arc<str>>) -> Self {
86 Self {
87 expr,
88 source: source.into(),
89 type_info: None,
90 }
91 }
92
93 /// Create a checked AST (after type checking).
94 pub fn new_checked(
95 expr: SpannedExpr,
96 source: impl Into<Arc<str>>,
97 check_result: CheckResult,
98 ) -> Self {
99 Self {
100 expr,
101 source: source.into(),
102 type_info: Some(check_result),
103 }
104 }
105
106 /// Returns true if this AST has been type-checked.
107 pub fn is_checked(&self) -> bool {
108 self.type_info.is_some()
109 }
110
111 /// Get the expression tree.
112 pub fn expr(&self) -> &SpannedExpr {
113 &self.expr
114 }
115
116 /// Get the original source text.
117 pub fn source(&self) -> &str {
118 &self.source
119 }
120
121 /// Get type checking results (if checked).
122 pub fn type_info(&self) -> Option<&CheckResult> {
123 self.type_info.as_ref()
124 }
125
126 /// Get the result type of the expression (if checked).
127 ///
128 /// Returns the type of the root expression from the type map.
129 pub fn result_type(&self) -> Option<&CelType> {
130 self.type_info
131 .as_ref()
132 .and_then(|info| info.type_map.get(&self.expr.id))
133 }
134
135 // ==================== String Conversion ====================
136
137 /// Convert the AST back to CEL source text.
138 ///
139 /// The output is a valid CEL expression that is semantically equivalent
140 /// to the original. Formatting may differ (whitespace, parenthesization).
141 ///
142 /// # Example
143 ///
144 /// ```
145 /// use cel_core::Env;
146 ///
147 /// let env = Env::with_standard_library();
148 /// let ast = env.compile("1 + 2 * 3").unwrap();
149 /// assert_eq!(ast.to_cel_string(), "1 + 2 * 3");
150 /// ```
151 pub fn to_cel_string(&self) -> String {
152 crate::unparser::ast_to_string(&self.expr)
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::Env;
160
161 #[test]
162 fn test_ast_is_checked() {
163 let env = Env::with_standard_library().with_variable("x", CelType::Int);
164
165 let ast = env.compile("x + 1").unwrap();
166 assert!(ast.is_checked());
167
168 let unchecked = env.parse_only("x + 1").unwrap();
169 assert!(!unchecked.is_checked());
170 }
171
172 #[test]
173 fn test_ast_result_type() {
174 let env = Env::with_standard_library().with_variable("x", CelType::Int);
175
176 let ast = env.compile("x + 1").unwrap();
177 assert_eq!(ast.result_type(), Some(&CelType::Int));
178
179 let unchecked = env.parse_only("x + 1").unwrap();
180 assert!(unchecked.result_type().is_none());
181 }
182
183 #[test]
184 fn test_ast_to_cel_string() {
185 let env = Env::with_standard_library().with_variable("x", CelType::Int);
186
187 let ast = env.compile("x + 1").unwrap();
188 assert_eq!(ast.to_cel_string(), "x + 1");
189
190 // Works for unchecked too
191 let unchecked = env.parse_only("x * 2 + 3").unwrap();
192 assert_eq!(unchecked.to_cel_string(), "x * 2 + 3");
193 }
194
195 #[test]
196 fn test_ast_to_cel_string_complex() {
197 let env = Env::with_standard_library();
198
199 let ast = env.compile("1 + 2 * 3").unwrap();
200 assert_eq!(ast.to_cel_string(), "1 + 2 * 3");
201
202 let ast = env.compile("(1 + 2) * 3").unwrap();
203 assert_eq!(ast.to_cel_string(), "(1 + 2) * 3");
204 }
205}