solverforge_core/constraints/
named_expr.rs

1//! Named expressions for constraint stream integration.
2//!
3//! Provides `NamedExpression` which bundles an `Expression` tree with an auto-generated
4//! function name for use in constraint streams. This bridges the expression builder API
5//! with the constraint stream API.
6//!
7//! # Example
8//!
9//! ```
10//! use solverforge_core::wasm::{Expr, FieldAccessExt};
11//! use solverforge_core::constraints::{NamedExpression, StreamComponent};
12//!
13//! // Build an expression with auto-generated name
14//! let has_room = NamedExpression::new(
15//!     Expr::is_not_null(Expr::param(0).get("Lesson", "room"))
16//! );
17//!
18//! // Use directly in stream components
19//! let filter = StreamComponent::filter(has_room.into());
20//! ```
21
22use std::sync::atomic::{AtomicU64, Ordering};
23
24use crate::constraints::WasmFunction;
25use crate::wasm::Expression;
26
27/// Counter for generating unique function names
28static EXPR_COUNTER: AtomicU64 = AtomicU64::new(0);
29
30/// An expression bundled with a function name for use in constraint streams.
31///
32/// `NamedExpression` automatically generates unique function names for expressions,
33/// making it easy to use expressions in constraint stream components.
34///
35/// The generated function name follows the pattern `expr_{counter}` or uses a
36/// provided custom name.
37#[derive(Debug, Clone, PartialEq)]
38pub struct NamedExpression {
39    name: String,
40    expression: Expression,
41}
42
43impl NamedExpression {
44    /// Creates a new named expression with an auto-generated unique name.
45    ///
46    /// # Example
47    ///
48    /// ```
49    /// use solverforge_core::wasm::Expr;
50    /// use solverforge_core::constraints::NamedExpression;
51    ///
52    /// let expr = NamedExpression::new(Expr::is_not_null(Expr::param(0)));
53    /// assert!(expr.name().starts_with("expr_"));
54    /// ```
55    pub fn new(expression: Expression) -> Self {
56        let counter = EXPR_COUNTER.fetch_add(1, Ordering::SeqCst);
57        Self {
58            name: format!("expr_{}", counter),
59            expression,
60        }
61    }
62
63    /// Creates a named expression with a specific name.
64    ///
65    /// Use this when you want to give your expression a meaningful name
66    /// for debugging or readability.
67    ///
68    /// # Example
69    ///
70    /// ```
71    /// use solverforge_core::wasm::{Expr, FieldAccessExt};
72    /// use solverforge_core::constraints::NamedExpression;
73    ///
74    /// let has_room = NamedExpression::with_name(
75    ///     "lesson_has_room",
76    ///     Expr::is_not_null(Expr::param(0).get("Lesson", "room"))
77    /// );
78    /// assert_eq!(has_room.name(), "lesson_has_room");
79    /// ```
80    pub fn with_name(name: impl Into<String>, expression: Expression) -> Self {
81        Self {
82            name: name.into(),
83            expression,
84        }
85    }
86
87    /// Returns the function name.
88    pub fn name(&self) -> &str {
89        &self.name
90    }
91
92    /// Returns the expression.
93    pub fn expression(&self) -> &Expression {
94        &self.expression
95    }
96
97    /// Consumes self and returns the expression.
98    pub fn into_expression(self) -> Expression {
99        self.expression
100    }
101
102    /// Returns a tuple of (name, expression) for registration with WasmModuleBuilder.
103    pub fn into_parts(self) -> (String, Expression) {
104        (self.name, self.expression)
105    }
106}
107
108impl From<NamedExpression> for WasmFunction {
109    fn from(named: NamedExpression) -> WasmFunction {
110        WasmFunction::with_expression(named.name, named.expression)
111    }
112}
113
114impl From<&NamedExpression> for WasmFunction {
115    fn from(named: &NamedExpression) -> WasmFunction {
116        WasmFunction::with_expression(&named.name, named.expression.clone())
117    }
118}
119
120/// Extension trait for converting expressions to named expressions.
121pub trait IntoNamedExpression {
122    /// Converts to a NamedExpression with an auto-generated name.
123    fn named(self) -> NamedExpression;
124
125    /// Converts to a NamedExpression with the given name.
126    fn named_as(self, name: impl Into<String>) -> NamedExpression;
127}
128
129impl IntoNamedExpression for Expression {
130    fn named(self) -> NamedExpression {
131        NamedExpression::new(self)
132    }
133
134    fn named_as(self, name: impl Into<String>) -> NamedExpression {
135        NamedExpression::with_name(name, self)
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::wasm::{Expr, FieldAccessExt};
143
144    #[test]
145    fn test_named_expression_new() {
146        let expr = Expr::is_not_null(Expr::param(0));
147        let named = NamedExpression::new(expr.clone());
148
149        assert!(named.name().starts_with("expr_"));
150        assert_eq!(named.expression(), &expr);
151    }
152
153    #[test]
154    fn test_named_expression_with_name() {
155        let expr = Expr::is_not_null(Expr::param(0));
156        let named = NamedExpression::with_name("my_predicate", expr.clone());
157
158        assert_eq!(named.name(), "my_predicate");
159        assert_eq!(named.expression(), &expr);
160    }
161
162    #[test]
163    fn test_unique_names() {
164        let expr1 = NamedExpression::new(Expr::bool(true));
165        let expr2 = NamedExpression::new(Expr::bool(false));
166
167        assert_ne!(expr1.name(), expr2.name());
168    }
169
170    #[test]
171    fn test_into_wasm_function() {
172        let named = NamedExpression::with_name("test_fn", Expr::bool(true));
173        let wasm_fn: WasmFunction = named.into();
174
175        assert_eq!(wasm_fn.name(), "test_fn");
176    }
177
178    #[test]
179    fn test_into_parts() {
180        let expr = Expr::int(42);
181        let named = NamedExpression::with_name("answer", expr.clone());
182        let (name, expression) = named.into_parts();
183
184        assert_eq!(name, "answer");
185        assert_eq!(expression, expr);
186    }
187
188    #[test]
189    fn test_extension_trait() {
190        let expr = Expr::is_not_null(Expr::param(0).get("Lesson", "room"));
191        let named = expr.clone().named();
192
193        assert!(named.name().starts_with("expr_"));
194        assert_eq!(named.expression(), &expr);
195    }
196
197    #[test]
198    fn test_extension_trait_named_as() {
199        let expr = Expr::is_not_null(Expr::param(0).get("Lesson", "room"));
200        let named = expr.clone().named_as("has_room");
201
202        assert_eq!(named.name(), "has_room");
203        assert_eq!(named.expression(), &expr);
204    }
205
206    #[test]
207    fn test_complex_expression() {
208        // Build: lesson.room != null && lesson.timeslot != null
209        let has_room = Expr::is_not_null(Expr::param(0).get("Lesson", "room"));
210        let has_timeslot = Expr::is_not_null(Expr::param(0).get("Lesson", "timeslot"));
211        let both_assigned = Expr::and(has_room, has_timeslot);
212
213        let named = both_assigned.named_as("lesson_fully_assigned");
214
215        assert_eq!(named.name(), "lesson_fully_assigned");
216        match named.expression() {
217            Expression::And { .. } => {}
218            _ => panic!("Expected And expression"),
219        }
220    }
221}