dwara_core/cel/jit.rs
1//! CEL JIT compilation via cranelift (PERF-09, #260).
2//!
3//! This module provides a cranelift-based JIT compiler for CEL
4//! expressions. When the `cel-jit` cargo feature is enabled, the
5//! [`CelProgram`] can optionally JIT-compile simple CEL expressions
6//! to native code for faster evaluation.
7//!
8//! # Supported expressions
9//!
10//! The JIT compiler supports a subset of CEL expressions:
11//! - Boolean literals (`true`, `false`)
12//! - Integer literals (`1`, `42`)
13//! - String literals (`"hello"`)
14//! - Identifier references (`request.path`)
15//! - Comparison operators (`==`, `!=`, `<`, `<=`, `>`, `>=`)
16//! - Logical operators (`&&`, `||`, `!`)
17//! - The `startsWith`, `endsWith`, `contains` string functions
18//!
19//! Complex expressions (comprehensions, maps, structs, custom
20//! functions) fall back to the tree-walking interpreter.
21//!
22//! # Architecture
23//!
24//! The JIT compiler:
25//! 1. Parses the CEL expression using `cel-parser` to get the AST.
26//! 2. Checks if the expression is supported by the JIT (simple enough).
27//! 3. Compiles the AST to cranelift IR.
28//! 4. Compiles the cranelift IR to native code.
29//! 5. Returns a function pointer that can be called to evaluate the
30//! expression.
31//!
32//! The JIT-compiled function takes a context pointer and returns a
33//! `CelJitResult` (a tagged union of bool, i64, f64, or string).
34//!
35//! # Current status
36//!
37//! The `cel-jit` feature gate, dependency wiring, and module structure
38//! are in place. The actual AST-to-cranelift-IR compilation is a future
39//! enhancement — the JIT currently returns `Unsupported` for all
40//! expressions, so evaluation falls back to the tree-walking
41//! interpreter. This scaffolding allows the feature to be compiled and
42//! tested without a full JIT implementation, and provides the
43//! integration point for future work.
44//!
45//! # Fallback
46//!
47//! When the `cel-jit` feature is OFF or the expression is too complex
48//! for the JIT, evaluation falls back to the tree-walking interpreter
49//! (`cel-interpreter`). This is transparent to the caller: the
50//! [`CelProgram::evaluate`] API is the same regardless of whether the
51//! JIT is used.
52
53use cel_interpreter::{Context, Value};
54
55/// The result of a JIT-compiled CEL evaluation. A tagged union of
56/// the CEL scalar types the JIT supports. The JIT-compiled function
57/// returns this by value.
58#[repr(C, u8)]
59#[derive(Debug, Clone)]
60pub enum CelJitResult {
61 /// A boolean value.
62 Bool(bool),
63 /// A 64-bit integer value.
64 Int(i64),
65 /// A 64-bit float value.
66 Float(f64),
67 /// The JIT does not support this expression; the caller should
68 /// fall back to the tree-walking interpreter.
69 Unsupported,
70 /// An error occurred during JIT evaluation.
71 Error,
72}
73
74/// Check if the `cel-jit` cargo feature is compiled in.
75pub fn cel_jit_available() -> bool {
76 cfg!(feature = "cel-jit")
77}
78
79/// A JIT-compiled CEL program. When the `cel-jit` feature is ON and
80/// the expression is simple enough, [`CelProgram::compile`] produces
81/// a `CelJitProgram` that can be evaluated without the tree-walking
82/// interpreter. When the feature is OFF or the expression is too
83/// complex, this is `None` and evaluation falls back to the
84/// interpreter.
85pub struct CelJitProgram {
86 _private: (),
87}
88
89impl CelJitProgram {
90 /// Try to JIT-compile a CEL expression. Returns `None` if the
91 /// expression is too complex for the JIT or if compilation fails.
92 ///
93 /// The current implementation always returns `None`: the
94 /// scaffolding (feature gate, dependency wiring, module structure)
95 /// is in place, but the actual AST-to-cranelift-IR compilation is
96 /// a future enhancement. All expressions fall back to the
97 /// tree-walking interpreter.
98 pub fn try_compile(_source: &str) -> Option<Self> {
99 None
100 }
101
102 /// Evaluate the JIT-compiled expression. Returns
103 /// [`CelJitResult::Unsupported`] if the JIT cannot handle the
104 /// expression (the caller should fall back to the interpreter).
105 pub fn evaluate(&self, _context: &Context) -> CelJitResult {
106 CelJitResult::Unsupported
107 }
108}
109
110/// Convert a `CelJitResult` to a `cel_interpreter::Value`. Returns
111/// `None` for `Unsupported` or `Error` (the caller should fall back
112/// to the interpreter).
113pub fn jit_result_to_value(result: CelJitResult) -> Option<Value> {
114 match result {
115 CelJitResult::Bool(b) => Some(Value::Bool(b)),
116 CelJitResult::Int(i) => Some(Value::Int(i)),
117 CelJitResult::Float(f) => Some(Value::Float(f)),
118 CelJitResult::Unsupported | CelJitResult::Error => None,
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn jit_result_bool() {
128 let r = CelJitResult::Bool(true);
129 let v = jit_result_to_value(r);
130 assert!(matches!(v, Some(Value::Bool(true))));
131 }
132
133 #[test]
134 fn jit_result_int() {
135 let r = CelJitResult::Int(42);
136 let v = jit_result_to_value(r);
137 assert!(matches!(v, Some(Value::Int(42))));
138 }
139
140 #[test]
141 fn jit_result_float() {
142 let r = CelJitResult::Float(2.71);
143 let v = jit_result_to_value(r);
144 assert!(matches!(v, Some(Value::Float(_))));
145 }
146
147 #[test]
148 fn jit_result_unsupported() {
149 let r = CelJitResult::Unsupported;
150 let v = jit_result_to_value(r);
151 assert!(v.is_none());
152 }
153
154 #[test]
155 fn jit_result_error() {
156 let r = CelJitResult::Error;
157 let v = jit_result_to_value(r);
158 assert!(v.is_none());
159 }
160
161 #[test]
162 fn cel_jit_feature_flag() {
163 // When the feature is on, cel_jit_available returns true.
164 // When off, it returns false.
165 assert_eq!(cel_jit_available(), cfg!(feature = "cel-jit"));
166 }
167
168 #[test]
169 fn try_compile_returns_none() {
170 // The JIT scaffolding returns None for all expressions (the
171 // actual AST-to-IR compilation is a future enhancement).
172 let prog = CelJitProgram::try_compile("1 + 2");
173 assert!(prog.is_none());
174 }
175}