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}