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