Skip to main content

rustial_engine/
expression.rs

1//! Typed expression engine for data-driven style evaluation.
2//!
3//! [`Expression<T>`] is the core type used by the style system to represent
4//! values that may depend on zoom level, feature properties, feature state,
5//! or combinations thereof.
6//!
7//! # Design
8//!
9//! Rather than interpreting JSON expression arrays at runtime (the MapLibre
10//! approach, motivated by JavaScript's lack of a type system), Rustial uses a
11//! **typed enum AST** that is:
12//!
13//! - **Compile-time checked** — invalid expressions cannot be constructed.
14//! - **Zero-cost for constants** — `Expression::Constant(v)` is a plain value.
15//! - **Branch-predictor friendly** — evaluation is a single `match` dispatch.
16//! - **Rust-ergonomic** — users build expressions with constructors, not JSON.
17//!
18//! JSON-based style documents (MapLibre/Mapbox `.json`) are parsed into
19//! `Expression<T>` by the `style-json` feature's deserialiser, making JSON
20//! an input format rather than the core representation.
21//!
22//! # Backward compatibility
23//!
24//! [`StyleValue<T>`] is a type alias for `Expression<T>`, preserving all
25//! existing API call-sites.
26
27use crate::geometry::PropertyValue;
28use crate::query::FeatureState;
29use std::collections::HashMap;
30use std::fmt;
31
32// ---------------------------------------------------------------------------
33// Evaluation contexts
34// ---------------------------------------------------------------------------
35
36/// Properties of a single feature being styled.
37///
38/// This is the same type as the `properties` map on [`Feature`](crate::Feature)
39/// and the [`FeatureState`] map — a `HashMap<String, PropertyValue>`.
40pub type FeatureProperties = HashMap<String, PropertyValue>;
41
42/// Full evaluation context for expressions.
43///
44/// Carries zoom level, optional feature properties (for data-driven styling),
45/// and optional feature state (for interactive hover/selection styling).
46#[derive(Debug, Clone, Copy)]
47pub struct ExprEvalContext<'a> {
48    /// Current map zoom level (0–22+).
49    pub zoom: f32,
50    /// Current camera pitch in degrees.
51    pub pitch: f32,
52    /// Per-feature properties (from GeoJSON / MVT).
53    ///
54    /// `None` when evaluating at the layer level (no specific feature).
55    pub properties: Option<&'a FeatureProperties>,
56    /// Per-feature mutable state (hover, selected, etc.).
57    ///
58    /// `None` when feature state is not available.
59    pub feature_state: Option<&'a FeatureState>,
60}
61
62impl<'a> ExprEvalContext<'a> {
63    /// Create a zoom-only context.
64    pub fn zoom_only(zoom: f32) -> Self {
65        Self {
66            zoom,
67            pitch: 0.0,
68            properties: None,
69            feature_state: None,
70        }
71    }
72
73    /// Create a context with feature properties for data-driven styling.
74    pub fn with_feature(zoom: f32, properties: &'a FeatureProperties) -> Self {
75        Self {
76            zoom,
77            pitch: 0.0,
78            properties: Some(properties),
79            feature_state: None,
80        }
81    }
82
83    /// Add feature state to this context.
84    pub fn and_state(mut self, state: &'a FeatureState) -> Self {
85        self.feature_state = Some(state);
86        self
87    }
88
89    /// Add pitch to this context.
90    pub fn and_pitch(mut self, pitch: f32) -> Self {
91        self.pitch = pitch;
92        self
93    }
94
95    /// Look up a feature property by key.
96    pub fn get_property(&self, key: &str) -> Option<&PropertyValue> {
97        self.properties.and_then(|p| p.get(key))
98    }
99
100    /// Look up a feature-state value by key.
101    pub fn get_state(&self, key: &str) -> Option<&PropertyValue> {
102        self.feature_state.and_then(|s| s.get(key))
103    }
104}
105
106// ---------------------------------------------------------------------------
107// Expression<T> — the core typed AST
108// ---------------------------------------------------------------------------
109
110/// A typed expression that evaluates to a value of type `T`.
111///
112/// This is the core representation for all style property values. The
113/// variants range from plain literals to data-driven expressions that
114/// depend on feature properties, zoom level, and feature state.
115///
116/// # Backward compatibility
117///
118/// `StyleValue<T>` is a type alias for this type, so all existing code
119/// that uses `StyleValue::Constant(...)`, `StyleValue::ZoomStops(...)`,
120/// or `StyleValue::FeatureState { .. }` continues to work unchanged.
121#[derive(Debug, Clone, PartialEq)]
122pub enum Expression<T> {
123    // =======================================================================
124    // Original StyleValue variants (unchanged)
125    // =======================================================================
126    /// Constant literal value.
127    Constant(T),
128
129    /// Zoom-keyed stops with linear interpolation.
130    ZoomStops(Vec<(f32, T)>),
131
132    /// Value driven by a per-feature state key.
133    FeatureState {
134        /// Feature-state key to look up (e.g. `"hover"`, `"selected"`).
135        key: String,
136        /// Default value when the key is absent.
137        fallback: T,
138    },
139
140    // =======================================================================
141    // New data-driven expression variants
142    // =======================================================================
143    /// Read a feature property and convert to `T`.
144    ///
145    /// Equivalent to MapLibre `["get", "property_name"]`.
146    /// Falls back to `fallback` when the property is missing or
147    /// cannot be converted to `T`.
148    GetProperty {
149        /// Property key to read from feature properties.
150        key: String,
151        /// Value to use when the property is absent or incompatible.
152        fallback: T,
153    },
154
155    /// Interpolate between stops based on a numeric input expression.
156    ///
157    /// Equivalent to MapLibre `["interpolate", ["linear"], input, z0, v0, z1, v1, ...]`.
158    Interpolate {
159        /// The numeric input value (typically `Expression::Zoom` or a property).
160        input: Box<NumericExpression>,
161        /// Ordered stop pairs `(input_value, output_value)`.
162        stops: Vec<(f32, T)>,
163    },
164
165    /// Step function: returns the stop value for the greatest stop ≤ input.
166    ///
167    /// Equivalent to MapLibre `["step", input, default, z0, v0, z1, v1, ...]`.
168    Step {
169        /// The numeric input.
170        input: Box<NumericExpression>,
171        /// Default value when input is below all stops.
172        default: T,
173        /// Ordered stops `(threshold, output_value)`.
174        stops: Vec<(f32, T)>,
175    },
176
177    /// Pattern match on a string input expression.
178    ///
179    /// Equivalent to MapLibre `["match", input, label1, val1, ..., fallback]`.
180    Match {
181        /// The string input to match against.
182        input: Box<StringExpression>,
183        /// Cases: `(label, output_value)`.
184        cases: Vec<(String, T)>,
185        /// Value when no case matches.
186        fallback: T,
187    },
188
189    /// Conditional branches evaluated in order.
190    ///
191    /// Equivalent to MapLibre `["case", cond1, val1, cond2, val2, ..., fallback]`.
192    Case {
193        /// Branches: `(condition, output_value)`.
194        branches: Vec<(BoolExpression, T)>,
195        /// Value when no condition is true.
196        fallback: T,
197    },
198
199    /// Return the first non-null result from a list of expressions.
200    ///
201    /// Equivalent to MapLibre `["coalesce", expr1, expr2, ...]`.
202    Coalesce(Vec<Expression<T>>),
203}
204
205// ---------------------------------------------------------------------------
206// Numeric sub-expression (untyped, evaluates to f64)
207// ---------------------------------------------------------------------------
208
209/// A numeric expression that evaluates to `f64`.
210///
211/// Used as the `input` for `Interpolate` and `Step` expressions, and as
212/// operands for arithmetic and comparison operations.
213#[derive(Debug, Clone, PartialEq)]
214pub enum NumericExpression {
215    /// A constant numeric literal.
216    Literal(f64),
217    /// The current map zoom level.
218    Zoom,
219    /// The current camera pitch in degrees.
220    Pitch,
221    /// Read a numeric feature property.
222    GetProperty {
223        /// Property key.
224        key: String,
225        /// Fallback when absent or non-numeric.
226        fallback: f64,
227    },
228    /// Read a numeric feature-state value.
229    GetState {
230        /// State key.
231        key: String,
232        /// Fallback when absent or non-numeric.
233        fallback: f64,
234    },
235    /// Addition: `a + b`.
236    Add(Box<NumericExpression>, Box<NumericExpression>),
237    /// Subtraction: `a - b`.
238    Sub(Box<NumericExpression>, Box<NumericExpression>),
239    /// Multiplication: `a * b`.
240    Mul(Box<NumericExpression>, Box<NumericExpression>),
241    /// Division: `a / b` (returns 0 on division by zero).
242    Div(Box<NumericExpression>, Box<NumericExpression>),
243    /// Remainder: `a % b`.
244    Mod(Box<NumericExpression>, Box<NumericExpression>),
245    /// Exponentiation: `a ^ b`.
246    Pow(Box<NumericExpression>, Box<NumericExpression>),
247    /// Absolute value.
248    Abs(Box<NumericExpression>),
249    /// Natural logarithm.
250    Ln(Box<NumericExpression>),
251    /// Square root.
252    Sqrt(Box<NumericExpression>),
253    /// Minimum of two values.
254    Min(Box<NumericExpression>, Box<NumericExpression>),
255    /// Maximum of two values.
256    Max(Box<NumericExpression>, Box<NumericExpression>),
257}
258
259// ---------------------------------------------------------------------------
260// String sub-expression
261// ---------------------------------------------------------------------------
262
263/// A string expression that evaluates to a `String`.
264///
265/// Used as the `input` for `Match` expressions.
266#[derive(Debug, Clone, PartialEq)]
267pub enum StringExpression {
268    /// A constant string literal.
269    Literal(String),
270    /// Read a string feature property.
271    GetProperty {
272        /// Property key.
273        key: String,
274        /// Fallback when absent or non-string.
275        fallback: String,
276    },
277    /// Read a string feature-state value.
278    GetState {
279        /// State key.
280        key: String,
281        /// Fallback when absent or non-string.
282        fallback: String,
283    },
284    /// Concatenate two strings.
285    Concat(Box<StringExpression>, Box<StringExpression>),
286    /// Uppercase.
287    Upcase(Box<StringExpression>),
288    /// Lowercase.
289    Downcase(Box<StringExpression>),
290}
291
292// ---------------------------------------------------------------------------
293// Boolean sub-expression
294// ---------------------------------------------------------------------------
295
296/// A boolean expression that evaluates to `bool`.
297///
298/// Used as the `condition` in `Case` branches.
299#[derive(Debug, Clone, PartialEq)]
300pub enum BoolExpression {
301    /// A constant boolean literal.
302    Literal(bool),
303    /// Read a boolean feature property.
304    GetProperty {
305        /// Property key.
306        key: String,
307        /// Fallback when absent or non-boolean.
308        fallback: bool,
309    },
310    /// Read a boolean feature-state value.
311    GetState {
312        /// State key.
313        key: String,
314        /// Fallback when absent or non-boolean.
315        fallback: bool,
316    },
317    /// Check whether a feature property key exists.
318    Has(String),
319    /// Logical NOT.
320    Not(Box<BoolExpression>),
321    /// Logical AND (all must be true).
322    All(Vec<BoolExpression>),
323    /// Logical OR (any must be true).
324    Any(Vec<BoolExpression>),
325    /// Numeric equality: `a == b`.
326    Eq(NumericExpression, NumericExpression),
327    /// Numeric inequality: `a != b`.
328    Neq(NumericExpression, NumericExpression),
329    /// Greater than: `a > b`.
330    Gt(NumericExpression, NumericExpression),
331    /// Greater or equal: `a >= b`.
332    Gte(NumericExpression, NumericExpression),
333    /// Less than: `a < b`.
334    Lt(NumericExpression, NumericExpression),
335    /// Less or equal: `a <= b`.
336    Lte(NumericExpression, NumericExpression),
337    /// String equality.
338    StrEq(StringExpression, StringExpression),
339}
340
341// ---------------------------------------------------------------------------
342// Evaluation — NumericExpression
343// ---------------------------------------------------------------------------
344
345impl NumericExpression {
346    /// Evaluate this numeric expression against a context.
347    pub fn eval(&self, ctx: &ExprEvalContext<'_>) -> f64 {
348        match self {
349            NumericExpression::Literal(v) => *v,
350            NumericExpression::Zoom => ctx.zoom as f64,
351            NumericExpression::Pitch => ctx.pitch as f64,
352            NumericExpression::GetProperty { key, fallback } => ctx
353                .get_property(key)
354                .and_then(PropertyValue::as_f64)
355                .unwrap_or(*fallback),
356            NumericExpression::GetState { key, fallback } => ctx
357                .get_state(key)
358                .and_then(PropertyValue::as_f64)
359                .unwrap_or(*fallback),
360            NumericExpression::Add(a, b) => a.eval(ctx) + b.eval(ctx),
361            NumericExpression::Sub(a, b) => a.eval(ctx) - b.eval(ctx),
362            NumericExpression::Mul(a, b) => a.eval(ctx) * b.eval(ctx),
363            NumericExpression::Div(a, b) => {
364                let denom = b.eval(ctx);
365                if denom.abs() < f64::EPSILON {
366                    0.0
367                } else {
368                    a.eval(ctx) / denom
369                }
370            }
371            NumericExpression::Mod(a, b) => {
372                let denom = b.eval(ctx);
373                if denom.abs() < f64::EPSILON {
374                    0.0
375                } else {
376                    a.eval(ctx) % denom
377                }
378            }
379            NumericExpression::Pow(a, b) => a.eval(ctx).powf(b.eval(ctx)),
380            NumericExpression::Abs(a) => a.eval(ctx).abs(),
381            NumericExpression::Ln(a) => a.eval(ctx).ln(),
382            NumericExpression::Sqrt(a) => a.eval(ctx).sqrt(),
383            NumericExpression::Min(a, b) => a.eval(ctx).min(b.eval(ctx)),
384            NumericExpression::Max(a, b) => a.eval(ctx).max(b.eval(ctx)),
385        }
386    }
387}
388
389// ---------------------------------------------------------------------------
390// Evaluation — StringExpression
391// ---------------------------------------------------------------------------
392
393impl StringExpression {
394    /// Evaluate this string expression against a context.
395    pub fn eval(&self, ctx: &ExprEvalContext<'_>) -> String {
396        match self {
397            StringExpression::Literal(v) => v.clone(),
398            StringExpression::GetProperty { key, fallback } => ctx
399                .get_property(key)
400                .and_then(PropertyValue::as_str)
401                .map(|s| s.to_owned())
402                .unwrap_or_else(|| fallback.clone()),
403            StringExpression::GetState { key, fallback } => ctx
404                .get_state(key)
405                .and_then(PropertyValue::as_str)
406                .map(|s| s.to_owned())
407                .unwrap_or_else(|| fallback.clone()),
408            StringExpression::Concat(a, b) => {
409                let mut s = a.eval(ctx);
410                s.push_str(&b.eval(ctx));
411                s
412            }
413            StringExpression::Upcase(a) => a.eval(ctx).to_uppercase(),
414            StringExpression::Downcase(a) => a.eval(ctx).to_lowercase(),
415        }
416    }
417}
418
419// ---------------------------------------------------------------------------
420// Evaluation — BoolExpression
421// ---------------------------------------------------------------------------
422
423impl BoolExpression {
424    /// Evaluate this boolean expression against a context.
425    pub fn eval(&self, ctx: &ExprEvalContext<'_>) -> bool {
426        match self {
427            BoolExpression::Literal(v) => *v,
428            BoolExpression::GetProperty { key, fallback } => ctx
429                .get_property(key)
430                .and_then(PropertyValue::as_bool)
431                .unwrap_or(*fallback),
432            BoolExpression::GetState { key, fallback } => ctx
433                .get_state(key)
434                .and_then(PropertyValue::as_bool)
435                .unwrap_or(*fallback),
436            BoolExpression::Has(key) => ctx
437                .properties
438                .map(|p| p.contains_key(key.as_str()))
439                .unwrap_or(false),
440            BoolExpression::Not(a) => !a.eval(ctx),
441            BoolExpression::All(exprs) => exprs.iter().all(|e| e.eval(ctx)),
442            BoolExpression::Any(exprs) => exprs.iter().any(|e| e.eval(ctx)),
443            BoolExpression::Eq(a, b) => (a.eval(ctx) - b.eval(ctx)).abs() < f64::EPSILON,
444            BoolExpression::Neq(a, b) => (a.eval(ctx) - b.eval(ctx)).abs() >= f64::EPSILON,
445            BoolExpression::Gt(a, b) => a.eval(ctx) > b.eval(ctx),
446            BoolExpression::Gte(a, b) => a.eval(ctx) >= b.eval(ctx),
447            BoolExpression::Lt(a, b) => a.eval(ctx) < b.eval(ctx),
448            BoolExpression::Lte(a, b) => a.eval(ctx) <= b.eval(ctx),
449            BoolExpression::StrEq(a, b) => a.eval(ctx) == b.eval(ctx),
450        }
451    }
452}
453
454// ---------------------------------------------------------------------------
455// Evaluation — Expression<T>
456// ---------------------------------------------------------------------------
457
458/// Helper: interpolate zoom stops using the `StyleInterpolatable` trait.
459fn eval_stops<T: super::style::StyleInterpolatable>(stops: &[(f32, T)], input: f32) -> T {
460    debug_assert!(!stops.is_empty(), "stop list must not be empty");
461    let (first_input, first_value) = &stops[0];
462    if input <= *first_input {
463        return first_value.clone();
464    }
465    for pair in stops.windows(2) {
466        let (i0, v0) = &pair[0];
467        let (i1, v1) = &pair[1];
468        if input <= *i1 {
469            let span = (*i1 - *i0).max(f32::EPSILON);
470            let t = (input - *i0) / span;
471            return T::interpolate(v0, v1, t);
472        }
473    }
474    stops.last().expect("non-empty stops").1.clone()
475}
476
477impl<T: super::style::StyleInterpolatable> Expression<T> {
478    /// Evaluate with no context (uses defaults).
479    pub fn evaluate(&self) -> T {
480        self.eval_full(&ExprEvalContext::zoom_only(0.0))
481    }
482
483    /// Evaluate with a zoom-only legacy context.
484    pub fn evaluate_with_context(&self, ctx: super::style::StyleEvalContext) -> T {
485        self.eval_full(&ExprEvalContext::zoom_only(ctx.zoom))
486    }
487
488    /// Evaluate with a full legacy context (zoom + feature state).
489    pub fn evaluate_with_full_context(&self, ctx: &super::style::StyleEvalContextFull<'_>) -> T {
490        let expr_ctx = ExprEvalContext {
491            zoom: ctx.zoom,
492            pitch: 0.0,
493            properties: None,
494            feature_state: Some(ctx.feature_state),
495        };
496        self.eval_full(&expr_ctx)
497    }
498
499    /// Evaluate with feature properties for data-driven styling.
500    pub fn evaluate_with_properties(&self, ctx: &ExprEvalContext<'_>) -> T {
501        self.eval_full(ctx)
502    }
503
504    /// Core evaluation entry point.
505    pub fn eval_full(&self, ctx: &ExprEvalContext<'_>) -> T {
506        match self {
507            // --- Original StyleValue variants (unchanged behavior) ---
508            Expression::Constant(value) => value.clone(),
509
510            Expression::ZoomStops(stops) => eval_stops(stops, ctx.zoom),
511
512            Expression::FeatureState { key, fallback } => ctx
513                .get_state(key)
514                .and_then(|prop| T::from_feature_state_property(prop))
515                .unwrap_or_else(|| fallback.clone()),
516
517            // --- New data-driven variants ---
518            Expression::GetProperty { key, fallback } => ctx
519                .get_property(key)
520                .and_then(|prop| T::from_feature_state_property(prop))
521                .unwrap_or_else(|| fallback.clone()),
522
523            Expression::Interpolate { input, stops } => {
524                let input_val = input.eval(ctx) as f32;
525                eval_stops(stops, input_val)
526            }
527
528            Expression::Step {
529                input,
530                default,
531                stops,
532            } => {
533                let input_val = input.eval(ctx) as f32;
534                if stops.is_empty() || input_val < stops[0].0 {
535                    return default.clone();
536                }
537                // Find the greatest stop ≤ input_val.
538                let mut result = default;
539                for (threshold, value) in stops {
540                    if input_val >= *threshold {
541                        result = value;
542                    } else {
543                        break;
544                    }
545                }
546                result.clone()
547            }
548
549            Expression::Match {
550                input,
551                cases,
552                fallback,
553            } => {
554                let input_val = input.eval(ctx);
555                for (label, value) in cases {
556                    if *label == input_val {
557                        return value.clone();
558                    }
559                }
560                fallback.clone()
561            }
562
563            Expression::Case { branches, fallback } => {
564                for (condition, value) in branches {
565                    if condition.eval(ctx) {
566                        return value.clone();
567                    }
568                }
569                fallback.clone()
570            }
571
572            Expression::Coalesce(exprs) => {
573                // Coalesce: for typed expressions, we evaluate each and return
574                // the first result. Since all expressions always produce a
575                // value (with fallbacks), we return the first one.
576                // In practice, Coalesce is most useful when combined with
577                // GetProperty where the fallback indicates "missing".
578                if let Some(first) = exprs.first() {
579                    first.eval_full(ctx)
580                } else {
581                    // Empty coalesce — this shouldn't happen, but return
582                    // a reasonable default.
583                    panic!("Expression::Coalesce requires at least one sub-expression");
584                }
585            }
586        }
587    }
588}
589
590// ---------------------------------------------------------------------------
591// Convenience constructors
592// ---------------------------------------------------------------------------
593
594impl<T> Expression<T> {
595    /// Create a feature-state-driven expression.
596    pub fn feature_state_key(key: impl Into<String>, fallback: T) -> Self {
597        Expression::FeatureState {
598            key: key.into(),
599            fallback,
600        }
601    }
602
603    /// Whether this expression depends on per-feature mutable state.
604    pub fn is_feature_state_driven(&self) -> bool {
605        match self {
606            Expression::FeatureState { .. } => true,
607            Expression::Case { branches, .. } => {
608                branches.iter().any(|(cond, _)| cond.uses_feature_state())
609            }
610            Expression::Coalesce(exprs) => exprs.iter().any(|e| e.is_feature_state_driven()),
611            _ => false,
612        }
613    }
614
615    /// Whether this expression depends on feature properties.
616    pub fn is_data_driven(&self) -> bool {
617        match self {
618            Expression::GetProperty { .. } => true,
619            Expression::Match { .. } => true,
620            Expression::Interpolate { .. } => true,
621            Expression::Step { .. } => true,
622            Expression::Case { .. } => true,
623            Expression::Coalesce(exprs) => exprs.iter().any(|e| e.is_data_driven()),
624            _ => false,
625        }
626    }
627}
628
629impl<T> From<T> for Expression<T> {
630    fn from(value: T) -> Self {
631        Expression::Constant(value)
632    }
633}
634
635impl BoolExpression {
636    /// Whether this boolean expression references feature state.
637    pub fn uses_feature_state(&self) -> bool {
638        match self {
639            BoolExpression::GetState { .. } => true,
640            BoolExpression::Not(a) => a.uses_feature_state(),
641            BoolExpression::All(exprs) => exprs.iter().any(|e| e.uses_feature_state()),
642            BoolExpression::Any(exprs) => exprs.iter().any(|e| e.uses_feature_state()),
643            _ => false,
644        }
645    }
646}
647
648// ---------------------------------------------------------------------------
649// Display
650// ---------------------------------------------------------------------------
651
652impl<T: fmt::Debug> fmt::Display for Expression<T> {
653    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
654        match self {
655            Expression::Constant(v) => write!(f, "{v:?}"),
656            Expression::ZoomStops(stops) => {
657                write!(f, "zoom_stops[")?;
658                for (i, (z, v)) in stops.iter().enumerate() {
659                    if i > 0 {
660                        write!(f, ", ")?;
661                    }
662                    write!(f, "{z}: {v:?}")?;
663                }
664                write!(f, "]")
665            }
666            Expression::FeatureState { key, fallback } => {
667                write!(f, "feature_state(\"{key}\", {fallback:?})")
668            }
669            Expression::GetProperty { key, fallback } => {
670                write!(f, "get(\"{key}\", {fallback:?})")
671            }
672            Expression::Interpolate { input, stops } => {
673                write!(f, "interpolate({input:?}, [")?;
674                for (i, (z, v)) in stops.iter().enumerate() {
675                    if i > 0 {
676                        write!(f, ", ")?;
677                    }
678                    write!(f, "{z}: {v:?}")?;
679                }
680                write!(f, "])")
681            }
682            Expression::Step {
683                input,
684                default,
685                stops,
686            } => {
687                write!(f, "step({input:?}, {default:?}, [")?;
688                for (i, (z, v)) in stops.iter().enumerate() {
689                    if i > 0 {
690                        write!(f, ", ")?;
691                    }
692                    write!(f, "{z}: {v:?}")?;
693                }
694                write!(f, "])")
695            }
696            Expression::Match {
697                input,
698                cases,
699                fallback,
700            } => {
701                write!(f, "match({input:?}, [")?;
702                for (i, (lbl, v)) in cases.iter().enumerate() {
703                    if i > 0 {
704                        write!(f, ", ")?;
705                    }
706                    write!(f, "\"{lbl}\": {v:?}")?;
707                }
708                write!(f, "], {fallback:?})")
709            }
710            Expression::Case { branches, fallback } => {
711                write!(f, "case([")?;
712                for (i, (cond, v)) in branches.iter().enumerate() {
713                    if i > 0 {
714                        write!(f, ", ")?;
715                    }
716                    write!(f, "{cond:?} => {v:?}")?;
717                }
718                write!(f, "], {fallback:?})")
719            }
720            Expression::Coalesce(exprs) => {
721                write!(f, "coalesce(")?;
722                for (i, e) in exprs.iter().enumerate() {
723                    if i > 0 {
724                        write!(f, ", ")?;
725                    }
726                    write!(f, "{e}")?;
727                }
728                write!(f, ")")
729            }
730        }
731    }
732}
733
734// ---------------------------------------------------------------------------
735// Builder helpers for common patterns
736// ---------------------------------------------------------------------------
737
738impl Expression<f32> {
739    /// Interpolate linearly on zoom: `["interpolate", ["linear"], ["zoom"], z0, v0, z1, v1, ...]`.
740    pub fn zoom_interpolate(stops: Vec<(f32, f32)>) -> Self {
741        Expression::Interpolate {
742            input: Box::new(NumericExpression::Zoom),
743            stops,
744        }
745    }
746
747    /// Step on zoom: `["step", ["zoom"], default, z0, v0, z1, v1, ...]`.
748    pub fn zoom_step(default: f32, stops: Vec<(f32, f32)>) -> Self {
749        Expression::Step {
750            input: Box::new(NumericExpression::Zoom),
751            default,
752            stops,
753        }
754    }
755
756    /// Read a numeric property with fallback.
757    pub fn property(key: impl Into<String>, fallback: f32) -> Self {
758        Expression::GetProperty {
759            key: key.into(),
760            fallback,
761        }
762    }
763
764    /// Interpolate linearly on a numeric feature property.
765    pub fn property_interpolate(
766        property: impl Into<String>,
767        fallback: f64,
768        stops: Vec<(f32, f32)>,
769    ) -> Self {
770        Expression::Interpolate {
771            input: Box::new(NumericExpression::GetProperty {
772                key: property.into(),
773                fallback,
774            }),
775            stops,
776        }
777    }
778}
779
780impl Expression<[f32; 4]> {
781    /// Interpolate colors linearly on zoom.
782    pub fn zoom_interpolate(stops: Vec<(f32, [f32; 4])>) -> Self {
783        Expression::Interpolate {
784            input: Box::new(NumericExpression::Zoom),
785            stops,
786        }
787    }
788
789    /// Step on zoom for colors.
790    pub fn zoom_step(default: [f32; 4], stops: Vec<(f32, [f32; 4])>) -> Self {
791        Expression::Step {
792            input: Box::new(NumericExpression::Zoom),
793            default,
794            stops,
795        }
796    }
797
798    /// Match a string property to a color.
799    pub fn property_match(
800        property: impl Into<String>,
801        cases: Vec<(String, [f32; 4])>,
802        fallback: [f32; 4],
803    ) -> Self {
804        Expression::Match {
805            input: Box::new(StringExpression::GetProperty {
806                key: property.into(),
807                fallback: String::new(),
808            }),
809            cases,
810            fallback,
811        }
812    }
813}
814
815impl Expression<bool> {
816    /// Read a boolean feature property.
817    pub fn property(key: impl Into<String>, fallback: bool) -> Self {
818        Expression::GetProperty {
819            key: key.into(),
820            fallback,
821        }
822    }
823}
824
825impl Expression<String> {
826    /// Read a string feature property.
827    pub fn property(key: impl Into<String>, fallback: impl Into<String>) -> Self {
828        Expression::GetProperty {
829            key: key.into(),
830            fallback: fallback.into(),
831        }
832    }
833}
834
835// ---------------------------------------------------------------------------
836// Tests
837// ---------------------------------------------------------------------------
838
839#[cfg(test)]
840mod tests {
841    use super::*;
842    use crate::geometry::PropertyValue;
843    use crate::style::{StyleEvalContext, StyleEvalContextFull};
844
845    // -- Backward compatibility: Constant --
846
847    #[test]
848    fn constant_evaluates_directly() {
849        let expr: Expression<f32> = Expression::Constant(42.0);
850        assert!((expr.evaluate() - 42.0).abs() < f32::EPSILON);
851    }
852
853    #[test]
854    fn constant_via_into() {
855        let expr: Expression<f32> = 42.0.into();
856        assert!((expr.evaluate() - 42.0).abs() < f32::EPSILON);
857    }
858
859    // -- Backward compatibility: ZoomStops --
860
861    #[test]
862    fn zoom_stops_interpolates() {
863        let expr = Expression::ZoomStops(vec![(0.0, 0.0_f32), (10.0, 100.0)]);
864        let ctx = ExprEvalContext::zoom_only(5.0);
865        let result = expr.eval_full(&ctx);
866        assert!((result - 50.0).abs() < 0.1);
867    }
868
869    #[test]
870    fn zoom_stops_clamps_below() {
871        let expr = Expression::ZoomStops(vec![(5.0, 10.0_f32), (10.0, 20.0)]);
872        let ctx = ExprEvalContext::zoom_only(0.0);
873        assert!((expr.eval_full(&ctx) - 10.0).abs() < f32::EPSILON);
874    }
875
876    #[test]
877    fn zoom_stops_clamps_above() {
878        let expr = Expression::ZoomStops(vec![(5.0, 10.0_f32), (10.0, 20.0)]);
879        let ctx = ExprEvalContext::zoom_only(99.0);
880        assert!((expr.eval_full(&ctx) - 20.0).abs() < f32::EPSILON);
881    }
882
883    // -- Backward compatibility: FeatureState --
884
885    #[test]
886    fn feature_state_returns_fallback_without_state() {
887        let expr = Expression::<f32>::feature_state_key("opacity", 0.5);
888        let ctx = ExprEvalContext::zoom_only(10.0);
889        assert!((expr.eval_full(&ctx) - 0.5).abs() < f32::EPSILON);
890    }
891
892    #[test]
893    fn feature_state_resolves_from_state_map() {
894        let mut state = HashMap::new();
895        state.insert("opacity".to_string(), PropertyValue::Number(0.8));
896        let ctx = ExprEvalContext::zoom_only(10.0).and_state(&state);
897        let expr = Expression::<f32>::feature_state_key("opacity", 0.5);
898        assert!((expr.eval_full(&ctx) - 0.8).abs() < f32::EPSILON);
899    }
900
901    // -- Backward compatibility: legacy context wrappers --
902
903    #[test]
904    fn legacy_evaluate_with_context() {
905        let expr = Expression::ZoomStops(vec![(0.0, 0.0_f32), (10.0, 100.0)]);
906        let result = expr.evaluate_with_context(StyleEvalContext::new(5.0));
907        assert!((result - 50.0).abs() < 0.1);
908    }
909
910    #[test]
911    fn legacy_evaluate_with_full_context() {
912        let mut state = HashMap::new();
913        state.insert("opacity".to_string(), PropertyValue::Number(0.8));
914        let ctx = StyleEvalContextFull::new(10.0, &state);
915        let expr = Expression::<f32>::feature_state_key("opacity", 0.5);
916        assert!((expr.evaluate_with_full_context(&ctx) - 0.8).abs() < f32::EPSILON);
917    }
918
919    // -- New: GetProperty --
920
921    #[test]
922    fn get_property_reads_feature_property() {
923        let mut props = HashMap::new();
924        props.insert("height".to_string(), PropertyValue::Number(50.0));
925        let ctx = ExprEvalContext::with_feature(10.0, &props);
926
927        let expr = Expression::<f32>::property("height", 0.0);
928        assert!((expr.eval_full(&ctx) - 50.0).abs() < f32::EPSILON);
929    }
930
931    #[test]
932    fn get_property_returns_fallback_when_missing() {
933        let props = HashMap::new();
934        let ctx = ExprEvalContext::with_feature(10.0, &props);
935        let expr = Expression::<f32>::property("height", 10.0);
936        assert!((expr.eval_full(&ctx) - 10.0).abs() < f32::EPSILON);
937    }
938
939    // -- New: Interpolate on property --
940
941    #[test]
942    fn interpolate_on_property() {
943        let mut props = HashMap::new();
944        props.insert("population".to_string(), PropertyValue::Number(500.0));
945        let ctx = ExprEvalContext::with_feature(10.0, &props);
946
947        let expr = Expression::<f32>::property_interpolate(
948            "population",
949            0.0,
950            vec![(0.0, 2.0), (1000.0, 20.0)],
951        );
952        let result = expr.eval_full(&ctx);
953        assert!((result - 11.0).abs() < 0.1);
954    }
955
956    // -- New: Interpolate on zoom (convenience) --
957
958    #[test]
959    fn zoom_interpolate_convenience() {
960        let expr = Expression::<f32>::zoom_interpolate(vec![(0.0, 1.0), (20.0, 10.0)]);
961        let ctx = ExprEvalContext::zoom_only(10.0);
962        assert!((expr.eval_full(&ctx) - 5.5).abs() < 0.1);
963    }
964
965    // -- New: Step --
966
967    #[test]
968    fn step_below_first_returns_default() {
969        let expr = Expression::Step {
970            input: Box::new(NumericExpression::Zoom),
971            default: 1.0_f32,
972            stops: vec![(5.0, 2.0), (10.0, 3.0)],
973        };
974        let ctx = ExprEvalContext::zoom_only(3.0);
975        assert!((expr.eval_full(&ctx) - 1.0).abs() < f32::EPSILON);
976    }
977
978    #[test]
979    fn step_between_stops() {
980        let expr = Expression::Step {
981            input: Box::new(NumericExpression::Zoom),
982            default: 1.0_f32,
983            stops: vec![(5.0, 2.0), (10.0, 3.0)],
984        };
985        let ctx = ExprEvalContext::zoom_only(7.0);
986        assert!((expr.eval_full(&ctx) - 2.0).abs() < f32::EPSILON);
987    }
988
989    #[test]
990    fn step_above_last() {
991        let expr = Expression::Step {
992            input: Box::new(NumericExpression::Zoom),
993            default: 1.0_f32,
994            stops: vec![(5.0, 2.0), (10.0, 3.0)],
995        };
996        let ctx = ExprEvalContext::zoom_only(15.0);
997        assert!((expr.eval_full(&ctx) - 3.0).abs() < f32::EPSILON);
998    }
999
1000    // -- New: Match --
1001
1002    #[test]
1003    fn match_on_string_property() {
1004        let mut props = HashMap::new();
1005        props.insert(
1006            "type".to_string(),
1007            PropertyValue::String("residential".to_string()),
1008        );
1009        let ctx = ExprEvalContext::with_feature(10.0, &props);
1010
1011        let expr: Expression<[f32; 4]> = Expression::property_match(
1012            "type",
1013            vec![
1014                ("residential".to_string(), [0.0, 0.0, 1.0, 1.0]),
1015                ("commercial".to_string(), [1.0, 0.0, 0.0, 1.0]),
1016            ],
1017            [0.5, 0.5, 0.5, 1.0],
1018        );
1019        let result = expr.eval_full(&ctx);
1020        assert_eq!(result, [0.0, 0.0, 1.0, 1.0]);
1021    }
1022
1023    #[test]
1024    fn match_returns_fallback_when_no_case() {
1025        let mut props = HashMap::new();
1026        props.insert(
1027            "type".to_string(),
1028            PropertyValue::String("industrial".to_string()),
1029        );
1030        let ctx = ExprEvalContext::with_feature(10.0, &props);
1031
1032        let expr: Expression<[f32; 4]> = Expression::property_match(
1033            "type",
1034            vec![("residential".to_string(), [0.0, 0.0, 1.0, 1.0])],
1035            [0.5, 0.5, 0.5, 1.0],
1036        );
1037        assert_eq!(expr.eval_full(&ctx), [0.5, 0.5, 0.5, 1.0]);
1038    }
1039
1040    // -- New: Case --
1041
1042    #[test]
1043    fn case_with_bool_conditions() {
1044        let mut props = HashMap::new();
1045        props.insert("height".to_string(), PropertyValue::Number(150.0));
1046        let ctx = ExprEvalContext::with_feature(10.0, &props);
1047
1048        let expr: Expression<[f32; 4]> = Expression::Case {
1049            branches: vec![
1050                (
1051                    BoolExpression::Gt(
1052                        NumericExpression::GetProperty {
1053                            key: "height".to_string(),
1054                            fallback: 0.0,
1055                        },
1056                        NumericExpression::Literal(100.0),
1057                    ),
1058                    [1.0, 0.0, 0.0, 1.0], // red if height > 100
1059                ),
1060                (
1061                    BoolExpression::Gt(
1062                        NumericExpression::GetProperty {
1063                            key: "height".to_string(),
1064                            fallback: 0.0,
1065                        },
1066                        NumericExpression::Literal(50.0),
1067                    ),
1068                    [1.0, 1.0, 0.0, 1.0], // yellow if height > 50
1069                ),
1070            ],
1071            fallback: [0.0, 1.0, 0.0, 1.0], // green otherwise
1072        };
1073        assert_eq!(expr.eval_full(&ctx), [1.0, 0.0, 0.0, 1.0]);
1074    }
1075
1076    #[test]
1077    fn case_fallback_when_no_branch_matches() {
1078        let props = HashMap::new();
1079        let ctx = ExprEvalContext::with_feature(10.0, &props);
1080
1081        let expr: Expression<f32> = Expression::Case {
1082            branches: vec![
1083                (BoolExpression::Literal(false), 10.0),
1084                (BoolExpression::Literal(false), 20.0),
1085            ],
1086            fallback: 99.0,
1087        };
1088        assert!((expr.eval_full(&ctx) - 99.0).abs() < f32::EPSILON);
1089    }
1090
1091    // -- New: Numeric sub-expressions --
1092
1093    #[test]
1094    fn numeric_arithmetic() {
1095        let ctx = ExprEvalContext::zoom_only(10.0);
1096
1097        let add = NumericExpression::Add(
1098            Box::new(NumericExpression::Literal(3.0)),
1099            Box::new(NumericExpression::Literal(4.0)),
1100        );
1101        assert!((add.eval(&ctx) - 7.0).abs() < f64::EPSILON);
1102
1103        let mul = NumericExpression::Mul(
1104            Box::new(NumericExpression::Zoom),
1105            Box::new(NumericExpression::Literal(2.0)),
1106        );
1107        assert!((mul.eval(&ctx) - 20.0).abs() < f64::EPSILON);
1108    }
1109
1110    #[test]
1111    fn numeric_division_by_zero() {
1112        let ctx = ExprEvalContext::zoom_only(10.0);
1113        let div = NumericExpression::Div(
1114            Box::new(NumericExpression::Literal(10.0)),
1115            Box::new(NumericExpression::Literal(0.0)),
1116        );
1117        assert!((div.eval(&ctx) - 0.0).abs() < f64::EPSILON);
1118    }
1119
1120    // -- New: BoolExpression --
1121
1122    #[test]
1123    fn bool_has_checks_property_existence() {
1124        let mut props = HashMap::new();
1125        props.insert(
1126            "name".to_string(),
1127            PropertyValue::String("test".to_string()),
1128        );
1129        let ctx = ExprEvalContext::with_feature(10.0, &props);
1130
1131        assert!(BoolExpression::Has("name".to_string()).eval(&ctx));
1132        assert!(!BoolExpression::Has("missing".to_string()).eval(&ctx));
1133    }
1134
1135    #[test]
1136    fn bool_all_and_any() {
1137        let ctx = ExprEvalContext::zoom_only(10.0);
1138
1139        assert!(BoolExpression::All(vec![
1140            BoolExpression::Literal(true),
1141            BoolExpression::Literal(true),
1142        ])
1143        .eval(&ctx));
1144
1145        assert!(!BoolExpression::All(vec![
1146            BoolExpression::Literal(true),
1147            BoolExpression::Literal(false),
1148        ])
1149        .eval(&ctx));
1150
1151        assert!(BoolExpression::Any(vec![
1152            BoolExpression::Literal(false),
1153            BoolExpression::Literal(true),
1154        ])
1155        .eval(&ctx));
1156    }
1157
1158    // -- New: StringExpression --
1159
1160    #[test]
1161    fn string_concat() {
1162        let ctx = ExprEvalContext::zoom_only(10.0);
1163        let concat = StringExpression::Concat(
1164            Box::new(StringExpression::Literal("hello ".to_string())),
1165            Box::new(StringExpression::Literal("world".to_string())),
1166        );
1167        assert_eq!(concat.eval(&ctx), "hello world");
1168    }
1169
1170    #[test]
1171    fn string_upcase_downcase() {
1172        let ctx = ExprEvalContext::zoom_only(10.0);
1173        let up = StringExpression::Upcase(Box::new(StringExpression::Literal("hello".to_string())));
1174        assert_eq!(up.eval(&ctx), "HELLO");
1175
1176        let down =
1177            StringExpression::Downcase(Box::new(StringExpression::Literal("HELLO".to_string())));
1178        assert_eq!(down.eval(&ctx), "hello");
1179    }
1180
1181    // -- Trait flags --
1182
1183    #[test]
1184    fn is_data_driven_flags() {
1185        let constant: Expression<f32> = Expression::Constant(1.0);
1186        assert!(!constant.is_data_driven());
1187
1188        let get: Expression<f32> = Expression::GetProperty {
1189            key: "height".into(),
1190            fallback: 0.0,
1191        };
1192        assert!(get.is_data_driven());
1193
1194        let interp = Expression::<f32>::zoom_interpolate(vec![(0.0, 1.0), (10.0, 5.0)]);
1195        assert!(interp.is_data_driven()); // has Interpolate variant
1196    }
1197
1198    #[test]
1199    fn is_feature_state_driven_flags() {
1200        let constant: Expression<f32> = Expression::Constant(1.0);
1201        assert!(!constant.is_feature_state_driven());
1202
1203        let driven: Expression<f32> = Expression::feature_state_key("opacity", 1.0);
1204        assert!(driven.is_feature_state_driven());
1205    }
1206
1207    // -- Combined: data-driven + zoom = composite expression --
1208
1209    #[test]
1210    fn composite_expression_zoom_and_property() {
1211        // Interpolate on zoom where the base value comes from a property.
1212        // This is equivalent to the MapLibre "composite" expression pattern.
1213        let mut props = HashMap::new();
1214        props.insert("rank".to_string(), PropertyValue::Number(5.0));
1215        let ctx = ExprEvalContext::with_feature(10.0, &props);
1216
1217        // Step on property: rank < 3 → small, rank >= 3 → large.
1218        // Then the result is further modulated by a zoom interpolation.
1219        let expr: Expression<f32> = Expression::Case {
1220            branches: vec![(
1221                BoolExpression::Gte(
1222                    NumericExpression::GetProperty {
1223                        key: "rank".to_string(),
1224                        fallback: 0.0,
1225                    },
1226                    NumericExpression::Literal(3.0),
1227                ),
1228                20.0, // large text
1229            )],
1230            fallback: 10.0, // small text
1231        };
1232        assert!((expr.eval_full(&ctx) - 20.0).abs() < f32::EPSILON);
1233    }
1234}