Skip to main content

openjd_expr/
value.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// Copyright by contributors to this project.
3// SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5//! Runtime values for expression evaluation.
6
7use crate::path_mapping::PathFormat;
8use crate::range_expr::RangeExpr;
9use crate::types::{ExprType, TypeCode};
10
11/// A float with optional original string representation for passthrough.
12/// 16 bytes: 8 for f64, 8 for `Option<Box<str>>` (NULL or heap pointer).
13///
14/// Fields are private. Construction goes through [`Float64::new`] or
15/// [`Float64::with_str`], which enforce the no-NaN / no-Inf / no-`-0.0`
16/// invariants that the `Hash` and `PartialEq` impls on `ExprValue` depend on.
17#[derive(Debug, Clone, serde::Serialize)]
18pub struct Float64 {
19    value: f64,
20    original: Option<Box<str>>,
21}
22
23impl std::hash::Hash for Float64 {
24    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
25        self.value.to_bits().hash(state);
26    }
27}
28
29/// Normalize -0.0 to 0.0 (matches Python's copysign normalization).
30fn normalize_zero(v: f64) -> f64 {
31    if v == 0.0 {
32        0.0
33    } else {
34        v
35    }
36}
37
38impl Float64 {
39    /// Create a new `Float64`, rejecting NaN and infinity, normalizing -0.0 to 0.0.
40    pub fn new(v: f64) -> Result<Self, crate::error::ExpressionError> {
41        let v = normalize_zero(v);
42        if v.is_nan() {
43            return Err(crate::error::ExpressionError::float_error(
44                "Float operation produced NaN",
45            ));
46        }
47        if v.is_infinite() {
48            return Err(crate::error::ExpressionError::float_error(
49                "Float operation produced infinity",
50            ));
51        }
52        Ok(Self {
53            value: v,
54            original: None,
55        })
56    }
57    /// Create a `Float64` preserving the original string representation for lossless display.
58    pub fn with_str(v: f64, s: String) -> Result<Self, crate::error::ExpressionError> {
59        let v = normalize_zero(v);
60        if v.is_nan() {
61            return Err(crate::error::ExpressionError::float_error(
62                "Float operation produced NaN",
63            ));
64        }
65        if v.is_infinite() {
66            return Err(crate::error::ExpressionError::float_error(
67                "Float operation produced infinity",
68            ));
69        }
70        Ok(Self {
71            value: v,
72            original: if v == 0.0 && s != "0.0" {
73                None
74            } else {
75                Some(s.into_boxed_str())
76            },
77        })
78    }
79    /// The underlying `f64` value.
80    pub fn value(&self) -> f64 {
81        self.value
82    }
83    /// Display string: the original literal if preserved, otherwise formatted.
84    pub fn to_display_string(&self) -> String {
85        if let Some(s) = &self.original {
86            s.to_string()
87        } else {
88            format_float(self.value)
89        }
90    }
91}
92
93impl std::fmt::Display for Float64 {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(f, "{}", self.to_display_string())
96    }
97}
98
99impl std::ops::Deref for Float64 {
100    type Target = f64;
101    fn deref(&self) -> &f64 {
102        &self.value
103    }
104}
105
106impl PartialEq<f64> for Float64 {
107    fn eq(&self, other: &f64) -> bool {
108        self.value == *other
109    }
110}
111
112impl PartialOrd<f64> for Float64 {
113    fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
114        self.value.partial_cmp(other)
115    }
116}
117
118/// A typed value during expression evaluation.
119///
120/// `#[non_exhaustive]` because future revisions or extensions may add
121/// new primitive types (e.g., `Duration`, `Url`, `Decimal`). Adding a
122/// variant must not be a breaking change for downstream crates that
123/// match on this enum. The `Path` variant has its own `#[non_exhaustive]`
124/// attribute, which serves a separate purpose (preventing direct
125/// struct-literal construction so that `ExprValue::new_path` can
126/// enforce the separator-normalization invariant).
127#[derive(Debug, Clone, serde::Serialize)]
128#[non_exhaustive]
129pub enum ExprValue {
130    Null,
131    Bool(bool),
132    Int(i64),
133    Float(Float64),
134    String(String),
135    /// A PATH value — a string path together with its format.
136    ///
137    /// `#[non_exhaustive]` prevents direct construction outside this crate;
138    /// downstream callers must use [`ExprValue::new_path`], which enforces
139    /// the separator-normalization invariant (`\` ↔ `/` per `PathFormat`,
140    /// and no normalization for URI paths). The fields remain visible for
141    /// pattern matching (using `..` is required from outside the crate).
142    #[non_exhaustive]
143    Path {
144        value: String,
145        format: PathFormat,
146    },
147    // Typed list variants (new)
148    ListBool(Vec<bool>),
149    ListInt(Vec<i64>),
150    ListFloat(Vec<Float64>),
151    ListString(Vec<String>, usize), // (elements, cached_memory_size)
152    ListPath(Vec<String>, PathFormat, usize), // (elements, format, cached_memory_size)
153    ListList(Vec<ExprValue>, ExprType, usize), // (elements, element_type_hint, cached_memory_size)
154    RangeExpr(RangeExpr),
155    Unresolved(ExprType),
156}
157
158impl std::hash::Hash for ExprValue {
159    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
160        // Must be consistent with PartialEq (which uses equals()):
161        // Int(1) == Float(1.0), String("x") == Path{value:"x",...}
162        // Empty lists of any type are equal, so they must hash identically.
163        match self {
164            Self::Null => 0u8.hash(state),
165            Self::Bool(b) => {
166                1u8.hash(state);
167                b.hash(state);
168            }
169            // Int hashes with integer tag + raw i64 bits.
170            Self::Int(i) => {
171                2u8.hash(state);
172                i.hash(state);
173            }
174            // Float hashes as Int when it's an exact integer in i64 range,
175            // otherwise uses float tag + f64 bits.
176            Self::Float(f) => {
177                let v = f.value;
178                if v.fract() == 0.0 && v >= i64::MIN as f64 && v <= i64::MAX as f64 {
179                    2u8.hash(state);
180                    (v as i64).hash(state);
181                } else {
182                    12u8.hash(state);
183                    v.to_bits().hash(state);
184                }
185            }
186            // String and Path hash the same way so they match
187            Self::String(s) => {
188                3u8.hash(state);
189                s.hash(state);
190            }
191            Self::Path { value, .. } => {
192                3u8.hash(state);
193                value.hash(state);
194            }
195            // All list types use discriminant 4 so empty lists hash equally.
196            // Elements are hashed via their ExprValue-equivalent hash to maintain
197            // consistency with cross-type equality (e.g. ListInt([1]) == ListFloat([1.0])).
198            Self::ListBool(v) => {
199                4u8.hash(state);
200                for b in v {
201                    1u8.hash(state);
202                    b.hash(state);
203                }
204            }
205            Self::ListInt(v) => {
206                4u8.hash(state);
207                for i in v {
208                    2u8.hash(state);
209                    i.hash(state);
210                }
211            }
212            Self::ListFloat(v) => {
213                4u8.hash(state);
214                for f in v {
215                    let fv = f.value;
216                    if fv.fract() == 0.0 && fv >= i64::MIN as f64 && fv <= i64::MAX as f64 {
217                        2u8.hash(state);
218                        (fv as i64).hash(state);
219                    } else {
220                        12u8.hash(state);
221                        fv.to_bits().hash(state);
222                    }
223                }
224            }
225            Self::ListString(v, _) => {
226                4u8.hash(state);
227                for s in v {
228                    3u8.hash(state);
229                    s.hash(state);
230                }
231            }
232            Self::ListPath(v, _, _) => {
233                4u8.hash(state);
234                for s in v {
235                    3u8.hash(state);
236                    s.hash(state);
237                }
238            }
239            Self::ListList(v, _, _) => {
240                4u8.hash(state);
241                for e in v {
242                    e.hash(state);
243                }
244            }
245            Self::RangeExpr(r) => {
246                10u8.hash(state);
247                r.hash(state);
248            }
249            Self::Unresolved(t) => {
250                11u8.hash(state);
251                t.hash(state);
252            }
253        }
254    }
255}
256
257impl Eq for ExprValue {}
258
259impl ExprValue {
260    /// Create a list, promoting elements as needed. Produces old List variant for compatibility.
261    fn make_list_string(v: Vec<String>) -> Self {
262        let heap =
263            v.len() * std::mem::size_of::<String>() + v.iter().map(|s| s.len()).sum::<usize>();
264        Self::ListString(v, heap)
265    }
266    fn make_list_path(v: Vec<String>, fmt: PathFormat) -> Self {
267        let heap =
268            v.len() * std::mem::size_of::<String>() + v.iter().map(|s| s.len()).sum::<usize>();
269        Self::ListPath(v, fmt, heap)
270    }
271    fn make_list_list(v: Vec<ExprValue>, elem_hint: ExprType) -> Self {
272        // Vec buffer holds ExprValues inline; only count their additional heap allocations
273        let heap = v.len() * std::mem::size_of::<ExprValue>()
274            + v.iter().map(|e| e.heap_size()).sum::<usize>();
275        let elem_type = v.first().map(|e| e.expr_type()).unwrap_or(elem_hint);
276        Self::ListList(v, elem_type, heap)
277    }
278
279    /// Estimate the heap allocation required to build a list from `elements`.
280    ///
281    /// Upper bound on the `heap_size()` of the resulting list — ignores the
282    /// type-promotion shortcuts in [`make_list`](Self::make_list) that can
283    /// shrink the final footprint (e.g. collapsing `ListInt` elements into
284    /// a single `ListFloat`). Treats the worst case of storing every
285    /// element through a `ListList`, which is what a heterogeneous input
286    /// ultimately materializes to.
287    ///
288    /// Used by [`make_list_checked`](Self::make_list_checked) to fail a
289    /// memory-bounded evaluator cleanly before the list allocation
290    /// happens, rather than after.
291    fn estimate_list_heap_size(elements: &[ExprValue]) -> usize {
292        let per_slot = std::mem::size_of::<ExprValue>();
293        elements
294            .iter()
295            .fold(elements.len().saturating_mul(per_slot), |acc, e| {
296                acc.saturating_add(e.heap_size())
297            })
298    }
299
300    /// Memory-checked variant of [`make_list`](Self::make_list).
301    ///
302    /// Pre-checks the evaluator's memory budget against an upper-bound
303    /// estimate of the list's heap footprint before any allocation occurs.
304    /// This is the defense-in-depth path: call sites that have an
305    /// [`EvalContext`](crate::function_library::EvalContext) available
306    /// should prefer this over [`make_list`](Self::make_list) so that a
307    /// memory-bounded evaluator fails cleanly on oversized intermediate
308    /// lists — even from code paths that did not charge ops proportionally
309    /// to the list size.
310    ///
311    /// Type promotion and nesting validation are otherwise identical to
312    /// [`make_list`](Self::make_list); this function forwards to it after
313    /// the memory check passes.
314    pub fn make_list_checked(
315        ctx: &mut dyn crate::function_library::EvalContext,
316        elements: Vec<ExprValue>,
317        hint_type: ExprType,
318    ) -> Result<Self, crate::error::ExpressionError> {
319        ctx.check_memory(Self::estimate_list_heap_size(&elements))?;
320        Self::make_list(elements, hint_type)
321    }
322
323    /// Construct a typed list from heterogeneous elements.
324    ///
325    /// Applies type promotion rules: int+float→float, path+string→string.
326    /// Uses `hint_type` for empty lists to determine the element type.
327    /// Returns an error if any element is a `ListList`, which would create 3+ nesting levels.
328    ///
329    /// When called from an evaluator or function implementation that has
330    /// an [`EvalContext`](crate::function_library::EvalContext), prefer
331    /// [`make_list_checked`](Self::make_list_checked) so that an oversized
332    /// intermediate list fails the evaluator's memory limit before the
333    /// allocation happens.
334    pub fn make_list(
335        mut elements: Vec<ExprValue>,
336        hint_type: ExprType,
337    ) -> Result<Self, crate::error::ExpressionError> {
338        // Reject 3+ nesting levels: if any element is itself a ListList with a
339        // non-nulltype element type, that's too deep. Empty lists (ListList with
340        // NULLTYPE) represent `list[nulltype]` — a flat empty list, not a nested one.
341        if elements
342            .iter()
343            .any(|e| matches!(e, Self::ListList(_, et, _) if *et != ExprType::NULLTYPE))
344        {
345            return Err(crate::error::ExpressionError::new(
346                "Lists may be nested at most 2 levels deep",
347            ));
348        }
349        // Convert empty ListList([], NULLTYPE) elements to match typed list siblings.
350        // e.g. in [[], [1]], the empty [] should become ListInt([]) not ListList([], NULLTYPE).
351        let has_empty_listlist = elements.iter().any(
352            |e| matches!(e, Self::ListList(v, et, _) if v.is_empty() && *et == ExprType::NULLTYPE),
353        );
354        if has_empty_listlist {
355            // Find the first typed list sibling to determine the target variant
356            let sibling_code = elements.iter().find_map(|e| match e {
357                Self::ListBool(v) if !v.is_empty() => Some(crate::types::TypeCode::Bool),
358                Self::ListInt(v) if !v.is_empty() => Some(crate::types::TypeCode::Int),
359                Self::ListFloat(_) => Some(crate::types::TypeCode::Float),
360                Self::ListString(v, _) if !v.is_empty() => Some(crate::types::TypeCode::String),
361                Self::ListPath(v, _, _) if !v.is_empty() => Some(crate::types::TypeCode::Path),
362                _ => None,
363            });
364            if let Some(code) = sibling_code {
365                for e in &mut elements {
366                    if matches!(e, Self::ListList(v, et, _) if v.is_empty() && *et == ExprType::NULLTYPE)
367                    {
368                        *e = match code {
369                            crate::types::TypeCode::Bool => Self::ListBool(Vec::new()),
370                            crate::types::TypeCode::Int => Self::ListInt(Vec::new()),
371                            crate::types::TypeCode::Float => Self::ListFloat(Vec::new()),
372                            crate::types::TypeCode::String => Self::ListString(Vec::new(), 0),
373                            crate::types::TypeCode::Path => {
374                                Self::make_list_path(Vec::new(), PathFormat::host())
375                            }
376                            _ => continue,
377                        };
378                    }
379                }
380            }
381        }
382        if elements.is_empty() {
383            // Empty lists are list[nulltype], compatible with any list type.
384            // When a concrete hint is provided, use the matching typed variant
385            // so that subsequent operations (e.g. append) preserve the type.
386            // Otherwise (Null or unknown hint), use ListList with NULLTYPE as the
387            // canonical empty list representation, compatible with any list type.
388            return Ok(match hint_type.code() {
389                crate::types::TypeCode::Bool => Self::ListBool(Vec::new()),
390                crate::types::TypeCode::Int => Self::ListInt(Vec::new()),
391                crate::types::TypeCode::Float => Self::ListFloat(Vec::new()),
392                crate::types::TypeCode::Path => {
393                    Self::make_list_path(Vec::new(), PathFormat::host())
394                }
395                crate::types::TypeCode::List => Self::make_list_list(Vec::new(), hint_type),
396                crate::types::TypeCode::String => Self::ListString(Vec::new(), 0),
397                crate::types::TypeCode::NullType => {
398                    Self::ListList(Vec::new(), ExprType::NULLTYPE, 0)
399                }
400                _ => Self::ListList(Vec::new(), ExprType::NULLTYPE, 0),
401            });
402        }
403        let has_int = elements.iter().any(|e| matches!(e, Self::Int(_)));
404        let has_float = elements.iter().any(|e| matches!(e, Self::Float(_)));
405        if has_int && has_float {
406            for e in &mut elements {
407                if let Self::Int(i) = e {
408                    *e = Self::Float(Float64::new(*i as f64).unwrap());
409                }
410            }
411            return Ok(Self::ListFloat(
412                elements
413                    .into_iter()
414                    .map(|e| match e {
415                        Self::Float(f) => f,
416                        _ => unreachable!("all elements promoted to Float above"),
417                    })
418                    .collect(),
419            ));
420        }
421        let has_list_int = elements
422            .iter()
423            .any(|e| e.is_list() && e.list_elem_type() == Some(ExprType::INT));
424        let has_list_float = elements
425            .iter()
426            .any(|e| e.is_list() && e.list_elem_type() == Some(ExprType::FLOAT));
427        if has_list_int && has_list_float {
428            for e in &mut elements {
429                if let Self::ListInt(ints) = e {
430                    *e = Self::ListFloat(
431                        ints.iter()
432                            .map(|i| Float64::new(*i as f64).unwrap())
433                            .collect(),
434                    );
435                }
436            }
437            return Ok(Self::make_list_list(elements, ExprType::NULLTYPE));
438        }
439        // Nested list path/string promotion: list[path] + list[string] → list[string]
440        let has_list_path = elements
441            .iter()
442            .any(|e| e.is_list() && e.list_elem_type() == Some(ExprType::PATH));
443        let has_list_string = elements
444            .iter()
445            .any(|e| e.is_list() && e.list_elem_type() == Some(ExprType::STRING));
446        if has_list_path && has_list_string {
447            for e in &mut elements {
448                if let Self::ListPath(paths, _, _) = e {
449                    *e = Self::make_list_string(std::mem::take(paths));
450                }
451            }
452            return Ok(Self::make_list_list(elements, ExprType::NULLTYPE));
453        }
454        // Path/string promotion: mix of path and string → string
455        let has_path = elements.iter().any(|e| matches!(e, Self::Path { .. }));
456        let has_string = elements.iter().any(|e| matches!(e, Self::String(_)));
457        if has_path && has_string {
458            return Ok(Self::make_list_string(
459                elements
460                    .into_iter()
461                    .map(|e| match e {
462                        Self::String(s) | Self::Path { value: s, .. } => s,
463                        _ => e.to_display_string(),
464                    })
465                    .collect(),
466            ));
467        }
468        Ok(match &elements[0] {
469            Self::Bool(_) => Self::ListBool(
470                elements
471                    .into_iter()
472                    .map(|e| match e {
473                        Self::Bool(b) => Ok(b),
474                        _ => Err(crate::error::ExpressionError::type_error(format!(
475                            "make_list expected bool element, got {}",
476                            e.type_name()
477                        ))),
478                    })
479                    .collect::<Result<_, _>>()?,
480            ),
481            Self::Int(_) => Self::ListInt(
482                elements
483                    .into_iter()
484                    .map(|e| match e {
485                        Self::Int(i) => Ok(i),
486                        _ => Err(crate::error::ExpressionError::type_error(format!(
487                            "make_list expected int element, got {}",
488                            e.type_name()
489                        ))),
490                    })
491                    .collect::<Result<_, _>>()?,
492            ),
493            Self::Float(_) => Self::ListFloat(
494                elements
495                    .into_iter()
496                    .map(|e| match e {
497                        Self::Float(f) => Ok(f),
498                        _ => Err(crate::error::ExpressionError::type_error(format!(
499                            "make_list expected float element, got {}",
500                            e.type_name()
501                        ))),
502                    })
503                    .collect::<Result<_, _>>()?,
504            ),
505            Self::String(_) => Self::make_list_string(
506                elements
507                    .into_iter()
508                    .map(|e| match e {
509                        Self::String(s) => Ok(s),
510                        _ => Err(crate::error::ExpressionError::type_error(format!(
511                            "make_list expected string element, got {}",
512                            e.type_name()
513                        ))),
514                    })
515                    .collect::<Result<_, _>>()?,
516            ),
517            Self::Path { format, .. } => {
518                let fmt = *format;
519                Self::make_list_path(
520                    elements
521                        .into_iter()
522                        .map(|e| match e {
523                            Self::Path { value, .. } => Ok(value),
524                            Self::String(value) => Ok(value),
525                            _ => Err(crate::error::ExpressionError::type_error(format!(
526                                "make_list expected path element, got {}",
527                                e.type_name()
528                            ))),
529                        })
530                        .collect::<Result<_, _>>()?,
531                    fmt,
532                )
533            }
534            _ if elements[0].is_list() => Self::make_list_list(elements, ExprType::NULLTYPE),
535            Self::RangeExpr(_) => Self::make_list_list(elements, ExprType::RANGE_EXPR),
536            _ => {
537                return Err(crate::error::ExpressionError::type_error(format!(
538                    "Cannot create list from {} elements",
539                    elements[0].type_name()
540                )))
541            }
542        })
543    }
544
545    /// Create an unresolved value with a type constraint (for validation-time type checking).
546    pub fn unresolved(constraint: ExprType) -> Self {
547        Self::Unresolved(constraint)
548    }
549    /// Returns `true` if this is an `Unresolved` value.
550    pub fn is_unresolved(&self) -> bool {
551        matches!(self, Self::Unresolved(_))
552    }
553
554    /// Create a PATH value with separators normalized to the given format.
555    ///
556    /// This is the only public constructor for `ExprValue::Path`; the variant
557    /// itself is `#[non_exhaustive]` so downstream crates cannot bypass the
558    /// separator-normalization invariant by constructing the struct directly.
559    ///
560    /// - `Posix`: no normalization — backslash is a valid filename character
561    /// - `Windows`: `/` → `\` (unless the value is a URI)
562    /// - `Uri`: no normalization
563    pub fn new_path(value: impl Into<String>, format: PathFormat) -> Self {
564        let value = value.into();
565        let normalized = normalize_path_separators(&value, format);
566        Self::Path {
567            value: normalized,
568            format,
569        }
570    }
571
572    /// Coerce a string value to the given type.
573    pub fn from_str_coerce(
574        s: &str,
575        target: &ExprType,
576        path_format: PathFormat,
577    ) -> Result<Self, String> {
578        match target.code() {
579            TypeCode::Int => s
580                .parse::<i64>()
581                .map(ExprValue::Int)
582                .map_err(|e| format!("Cannot convert '{s}' to int: {e}")),
583            TypeCode::Float => {
584                let v: f64 = s
585                    .parse()
586                    .map_err(|e| format!("Cannot convert '{s}' to float: {e}"))?;
587                if v.is_infinite() || v.is_nan() {
588                    return Err(format!("Cannot convert '{s}' to float"));
589                }
590                Ok(ExprValue::Float(
591                    Float64::with_str(v, s.to_string()).map_err(|e| e.to_string())?,
592                ))
593            }
594            TypeCode::Bool => match s.to_lowercase().as_str() {
595                "true" | "yes" | "on" | "1" => Ok(ExprValue::Bool(true)),
596                "false" | "no" | "off" | "0" => Ok(ExprValue::Bool(false)),
597                _ => Err(format!("Cannot convert '{s}' to bool")),
598            },
599            TypeCode::String => Ok(ExprValue::String(s.to_string())),
600            TypeCode::Path => Ok(ExprValue::new_path(s, path_format)),
601            TypeCode::RangeExpr => {
602                let r: crate::range_expr::RangeExpr =
603                    s.parse().map_err(|e: crate::error::ExpressionError| {
604                        format!("Cannot convert '{s}' to range_expr: {e}")
605                    })?;
606                Ok(ExprValue::RangeExpr(r))
607            }
608            TypeCode::NullType if s == "null" => Ok(ExprValue::Null),
609            _ => Err(format!("Cannot coerce string to {target}")),
610        }
611    }
612
613    /// Coerce a value to the given type.
614    ///
615    /// Coercion is non-destructive: only conversions that don't lose
616    /// information are attempted (`int → float`, `int → string`, etc).
617    ///
618    /// For union targets, the rules are:
619    ///
620    /// 1. **Match first** — if the value's type is already one of the
621    ///    union members, return it unchanged.
622    /// 2. **Per-member coercion** — otherwise try non-destructive
623    ///    coercion to each scalar member (skipping `nulltype`, `list[T]`,
624    ///    and nested unions). Return the first successful coercion.
625    /// 3. **Error** — if neither step yields a result.
626    ///
627    /// This mirrors the behavior of the pure-Python reference
628    /// implementation's `evaluate.try_coerce_nondestructive` loop and
629    /// satisfies RFC 0005 §"Implicit Type Coercion": `int | string`
630    /// accepts an `int` value as-is rather than rejecting it.
631    pub fn coerce(self, target: &ExprType, path_format: PathFormat) -> Result<Self, String> {
632        // Match-first: also accepts the case where the target is a union
633        // and the value's type is one of its members. Falls back to the
634        // existing strict-equality behavior for non-union targets.
635        if self.expr_type() == *target {
636            return Ok(self);
637        }
638        if target.code() == TypeCode::Union && target.match_type(&self.expr_type()).is_some() {
639            return Ok(self);
640        }
641        // For union targets that don't match by type-membership: try
642        // non-destructive coercion to each scalar member. Skip
643        // `nulltype` (only matches `null`, which would have matched
644        // above), `list[T]` (lists must satisfy by membership, not
645        // coercion — matching the reference), and nested unions.
646        if target.code() == TypeCode::Union {
647            for member in target.params() {
648                if matches!(
649                    member.code(),
650                    TypeCode::NullType | TypeCode::List | TypeCode::Union
651                ) {
652                    continue;
653                }
654                if let Ok(coerced) = self.clone().coerce(member, path_format) {
655                    return Ok(coerced);
656                }
657            }
658            return Err(format!("Cannot coerce {} to {target}", self.expr_type()));
659        }
660        match (&self, target.code()) {
661            (ExprValue::Int(i), TypeCode::Float) => {
662                Ok(ExprValue::Float(Float64::new(*i as f64).unwrap()))
663            }
664            (ExprValue::Float(f), TypeCode::Int) => {
665                let v = f.value();
666                if v.fract() == 0.0 && v.is_finite() {
667                    Ok(ExprValue::Int(v as i64))
668                } else {
669                    Err(format!(
670                        "Cannot coerce float to int: {} is not a whole number",
671                        f.to_display_string()
672                    ))
673                }
674            }
675            (ExprValue::Bool(b), TypeCode::String) => Ok(ExprValue::String(
676                if *b { "true" } else { "false" }.to_string(),
677            )),
678            (ExprValue::Int(i), TypeCode::String) => Ok(ExprValue::String(i.to_string())),
679            (ExprValue::Float(f), TypeCode::String) => Ok(ExprValue::String(f.to_display_string())),
680            (ExprValue::String(s), _) => ExprValue::from_str_coerce(s, target, path_format),
681            (ExprValue::Path { value, .. }, TypeCode::String) => {
682                Ok(ExprValue::String(value.clone()))
683            }
684            (ExprValue::RangeExpr(r), TypeCode::String) => Ok(ExprValue::String(r.to_string())),
685            (ExprValue::RangeExpr(r), TypeCode::List) => Ok(ExprValue::ListInt(r.to_vec())),
686            _ if target.code() == TypeCode::List && target.params().len() == 1 => {
687                let elem_type = &target.params()[0];
688                if let Some(elements) = self.list_elements() {
689                    let coerced: Result<Vec<_>, _> = elements
690                        .into_iter()
691                        .map(|e| e.coerce(elem_type, path_format))
692                        .collect();
693                    Ok(ExprValue::make_list(coerced?, elem_type.clone())
694                        .map_err(|e| e.to_string())?)
695                } else {
696                    Err(format!("Cannot coerce {} to {target}", self.expr_type()))
697                }
698            }
699            _ => Err(format!("Cannot coerce {} to {target}", self.expr_type())),
700        }
701    }
702
703    /// Python-style repr: `ExprValue(42)`, `ExprValue('hello')`, `ExprValue([1, 2], type='list[int]')`.
704    pub fn repr_python(&self) -> String {
705        match self {
706            Self::Null => "ExprValue(None)".to_string(),
707            Self::Bool(b) => format!("ExprValue({})", if *b { "True" } else { "False" }),
708            Self::Int(i) => format!("ExprValue({i})"),
709            Self::Float(f) => {
710                if f.original.is_some() {
711                    format!("ExprValue('{}', type='float')", f.to_display_string())
712                } else {
713                    format!("ExprValue({})", f.to_display_string())
714                }
715            }
716            Self::String(s) => format!("ExprValue('{s}')"),
717            Self::Path { value, format } => {
718                format!(
719                    "ExprValue('{value}', type='path', path_format=PathFormat.{})",
720                    match format {
721                        PathFormat::Posix => "POSIX",
722                        PathFormat::Windows => "WINDOWS",
723                        PathFormat::Uri => "URI",
724                    }
725                )
726            }
727            Self::RangeExpr(r) => format!("ExprValue('{}', type='range_expr')", r),
728            Self::Unresolved(t) => format!("ExprValue.unresolved(ExprType(\"{t}\"))"),
729            val if val.is_list() => {
730                let type_str = val.expr_type().to_string();
731                // Find path format if any
732                let pf = val.find_path_format();
733                let pf_str = pf
734                    .map(|f| {
735                        format!(
736                            ", path_format=PathFormat.{}",
737                            match f {
738                                PathFormat::Posix => "POSIX",
739                                PathFormat::Windows => "WINDOWS",
740                                PathFormat::Uri => "URI",
741                            }
742                        )
743                    })
744                    .unwrap_or_default();
745                format!(
746                    "ExprValue({}, type='{type_str}'{pf_str})",
747                    val.repr_python_list()
748                )
749            }
750            _ => format!("ExprValue('{}')", self.to_display_string()),
751        }
752    }
753
754    fn repr_python_list(&self) -> String {
755        let elements = self.list_elements().unwrap_or_default();
756        let items: Vec<String> = elements
757            .iter()
758            .map(|e| {
759                if e.is_list() {
760                    e.repr_python_list()
761                } else {
762                    match e {
763                        ExprValue::String(s) | ExprValue::Path { value: s, .. } => format!("'{s}'"),
764                        ExprValue::Bool(b) => if *b { "True" } else { "False" }.to_string(),
765                        ExprValue::Int(i) => i.to_string(),
766                        ExprValue::Float(f) => f.to_display_string(),
767                        _ => e.to_display_string(),
768                    }
769                }
770            })
771            .collect();
772        format!("[{}]", items.join(", "))
773    }
774
775    fn find_path_format(&self) -> Option<PathFormat> {
776        match self {
777            Self::ListPath(_, fmt, _) => Some(*fmt),
778            Self::ListList(v, _, _) => v.first().and_then(|e| e.find_path_format()),
779            _ => None,
780        }
781    }
782
783    /// Serialize to JSON transport format: `{"type": "int", "value": "42"}`.
784    /// Lists serialize value as nested JSON arrays of strings.
785    /// The caller adds the `"name"` field.
786    pub fn to_json_transport(&self) -> serde_json::Value {
787        let type_str = self.expr_type().to_string();
788        let value = self.transport_value();
789        serde_json::json!({"type": type_str, "value": value})
790    }
791
792    pub fn transport_value(&self) -> serde_json::Value {
793        match self {
794            val if val.is_list() => {
795                let elements = val.list_elements().unwrap_or_default();
796                serde_json::Value::Array(elements.iter().map(|e| e.transport_value()).collect())
797            }
798            _ => serde_json::Value::String(self.to_display_string()),
799        }
800    }
801
802    /// Deserialize from JSON transport format.
803    /// `json` must have `"type"` and `"value"` fields.
804    pub fn from_json_transport(
805        json: &serde_json::Value,
806        path_format: PathFormat,
807    ) -> Result<Self, String> {
808        let type_str = json
809            .get("type")
810            .and_then(|v| v.as_str())
811            .ok_or("Missing 'type' field")?;
812        let value = json.get("value").ok_or("Missing 'value' field")?;
813        let expr_type = ExprType::parse(type_str)?;
814        Self::from_transport_value(value, &expr_type, path_format)
815    }
816
817    pub fn from_transport_value(
818        value: &serde_json::Value,
819        target: &ExprType,
820        path_format: PathFormat,
821    ) -> Result<Self, String> {
822        Self::from_transport_value_inner(value, target, path_format, 0)
823    }
824
825    fn from_transport_value_inner(
826        value: &serde_json::Value,
827        target: &ExprType,
828        path_format: PathFormat,
829        depth: usize,
830    ) -> Result<Self, String> {
831        if depth > 10 {
832            return Err("Transport value nesting depth exceeded".to_string());
833        }
834        if target.code() == TypeCode::List {
835            let elem_type = target
836                .params()
837                .first()
838                .ok_or("List type missing element type")?;
839            let arr = value.as_array().ok_or("Expected array for list type")?;
840            let elements: Result<Vec<_>, _> = arr
841                .iter()
842                .map(|v| Self::from_transport_value_inner(v, elem_type, path_format, depth + 1))
843                .collect();
844            return ExprValue::make_list(elements?, elem_type.clone()).map_err(|e| e.to_string());
845        }
846        let s = value
847            .as_str()
848            .ok_or_else(|| format!("Expected string value for {target}"))?;
849        ExprValue::from_str_coerce(s, target, path_format)
850    }
851
852    /// Returns `true` if this value is a list variant.
853    pub fn is_list(&self) -> bool {
854        matches!(
855            self,
856            Self::ListBool(_)
857                | Self::ListInt(_)
858                | Self::ListFloat(_)
859                | Self::ListString(_, _)
860                | Self::ListPath(_, _, _)
861                | Self::ListList(_, _, _)
862        )
863    }
864
865    /// Number of elements if this is a list, `None` otherwise.
866    pub fn list_len(&self) -> Option<usize> {
867        match self {
868            Self::ListBool(v) => Some(v.len()),
869            Self::ListInt(v) => Some(v.len()),
870            Self::ListFloat(v) => Some(v.len()),
871            Self::ListString(v, _) => Some(v.len()),
872            Self::ListPath(v, _, _) => Some(v.len()),
873            Self::ListList(v, _, _) => Some(v.len()),
874            _ => None,
875        }
876    }
877
878    /// Collect all elements into a `Vec`. Prefer [`list_iter`](Self::list_iter) to avoid allocation.
879    pub fn list_elements(&self) -> Option<Vec<ExprValue>> {
880        match self {
881            Self::ListBool(v) => Some(v.iter().map(|b| ExprValue::Bool(*b)).collect()),
882            Self::ListInt(v) => Some(v.iter().map(|i| ExprValue::Int(*i)).collect()),
883            Self::ListFloat(v) => Some(v.iter().map(|f| ExprValue::Float(f.clone())).collect()),
884            Self::ListString(v, _) => {
885                Some(v.iter().map(|s| ExprValue::String(s.clone())).collect())
886            }
887            Self::ListPath(v, fmt, _) => Some(
888                v.iter()
889                    .map(|s| ExprValue::new_path(s.clone(), *fmt))
890                    .collect(),
891            ),
892            Self::ListList(v, _, _) => Some(v.clone()),
893            _ => None,
894        }
895    }
896
897    /// Iterate over list elements without allocating a Vec.
898    /// Returns None for non-list values.
899    pub fn list_iter(&self) -> Option<ListIter<'_>> {
900        match self {
901            Self::ListBool(v) => Some(ListIter::Bool(v.iter())),
902            Self::ListInt(v) => Some(ListIter::Int(v.iter())),
903            Self::ListFloat(v) => Some(ListIter::Float(v.iter())),
904            Self::ListString(v, _) => Some(ListIter::String(v.iter())),
905            Self::ListPath(v, fmt, _) => Some(ListIter::Path(v.iter(), *fmt)),
906            Self::ListList(v, _, _) => Some(ListIter::List(v.iter())),
907            _ => None,
908        }
909    }
910
911    /// Get a single element by index without allocating.
912    /// Supports negative indexing (Python-style).
913    pub fn list_get(&self, index: i64) -> Option<ExprValue> {
914        let len = self.list_len()? as i64;
915        let i = if index < 0 { len + index } else { index };
916        if i < 0 || i >= len {
917            return None;
918        }
919        let i = i as usize;
920        match self {
921            Self::ListBool(v) => Some(ExprValue::Bool(v[i])),
922            Self::ListInt(v) => Some(ExprValue::Int(v[i])),
923            Self::ListFloat(v) => Some(ExprValue::Float(v[i].clone())),
924            Self::ListString(v, _) => Some(ExprValue::String(v[i].clone())),
925            Self::ListPath(v, fmt, _) => Some(ExprValue::new_path(v[i].clone(), *fmt)),
926            Self::ListList(v, _, _) => Some(v[i].clone()),
927            _ => None,
928        }
929    }
930
931    /// Element type of a list, or `None` for non-list values.
932    ///
933    /// Returns the element type based on the list variant, even for empty
934    /// lists. For example, an empty `ListString` returns `STRING`, not
935    /// `NULLTYPE`. This ensures that operations on empty typed lists
936    /// (e.g. `sorted([])` where `[]` was originally `list[string]`)
937    /// preserve the element type through round-trips via `into_list` +
938    /// `make_list`.
939    pub fn list_elem_type(&self) -> Option<ExprType> {
940        match self {
941            Self::ListBool(_) => Some(ExprType::BOOL),
942            Self::ListInt(_) => Some(ExprType::INT),
943            Self::ListFloat(_) => Some(ExprType::FLOAT),
944            Self::ListString(_, _) => Some(ExprType::STRING),
945            Self::ListPath(_, _, _) => Some(ExprType::PATH),
946            Self::ListList(_, elem_type, _) => Some(elem_type.clone()),
947            _ => None,
948        }
949    }
950
951    /// Destructure into (elements, elem_type) for migration compatibility.
952    pub fn into_list(self) -> Option<(Vec<ExprValue>, ExprType)> {
953        let et = self.list_elem_type()?;
954        Some((self.list_elements()?, et))
955    }
956
957    /// The [`ExprType`] of this value.
958    pub fn expr_type(&self) -> ExprType {
959        match self {
960            Self::Null => ExprType::NULLTYPE,
961            Self::Bool(_) => ExprType::BOOL,
962            Self::Int(_) => ExprType::INT,
963            Self::Float(_) => ExprType::FLOAT,
964            Self::String(_) => ExprType::STRING,
965            Self::Path { .. } => ExprType::PATH,
966            Self::ListBool(_) => ExprType::list(ExprType::BOOL),
967            Self::ListInt(_) => ExprType::list(ExprType::INT),
968            Self::ListFloat(_) => ExprType::list(ExprType::FLOAT),
969            Self::ListString(_, _) => ExprType::list(ExprType::STRING),
970            Self::ListPath(_, _, _) => ExprType::list(ExprType::PATH),
971            Self::ListList(_, elem_type, _) => ExprType::list(elem_type.clone()),
972            Self::RangeExpr(_) => ExprType::RANGE_EXPR,
973            Self::Unresolved(t) => ExprType::unresolved(t.clone()),
974        }
975    }
976
977    /// Get a string representation for use in path manipulation and constraint checking.
978    /// Returns a `Cow` to avoid allocation when the value is already a string.
979    pub fn as_str_repr(&self) -> std::borrow::Cow<'_, str> {
980        match self {
981            Self::String(s) => std::borrow::Cow::Borrowed(s),
982            Self::Path { value, .. } => std::borrow::Cow::Borrowed(value),
983            _ => std::borrow::Cow::Owned(self.to_display_string()),
984        }
985    }
986
987    /// Short type name for error messages.
988    pub fn type_name(&self) -> &'static str {
989        match self {
990            Self::Null => "null",
991            Self::Bool(_) => "bool",
992            Self::Int(_) => "int",
993            Self::Float(_) => "float",
994            Self::String(_) => "string",
995            Self::Path { .. } => "path",
996            Self::RangeExpr(_) => "range_expr",
997            Self::Unresolved(_) => "unresolved",
998            _ if self.is_list() => "list",
999            _ => "unknown",
1000        }
1001    }
1002
1003    /// Human-readable string for format string interpolation and display.
1004    pub fn to_display_string(&self) -> String {
1005        match self {
1006            Self::Null => "null".to_string(),
1007            Self::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1008            Self::Int(i) => i.to_string(),
1009            Self::Float(fv) => fv.to_display_string(),
1010            Self::String(s) => s.clone(),
1011            Self::Path { value, .. } => value.clone(),
1012            Self::ListBool(v) => format!(
1013                "[{}]",
1014                v.iter()
1015                    .map(|b| if *b { "true" } else { "false" })
1016                    .collect::<Vec<_>>()
1017                    .join(", ")
1018            ),
1019            Self::ListInt(v) => format!(
1020                "[{}]",
1021                v.iter()
1022                    .map(|i| i.to_string())
1023                    .collect::<Vec<_>>()
1024                    .join(", ")
1025            ),
1026            Self::ListFloat(v) => format!(
1027                "[{}]",
1028                v.iter()
1029                    .map(|f| f.to_display_string())
1030                    .collect::<Vec<_>>()
1031                    .join(", ")
1032            ),
1033            Self::ListString(v, _) => format!(
1034                "[{}]",
1035                v.iter()
1036                    .map(|s| format!("\"{}\"", s))
1037                    .collect::<Vec<_>>()
1038                    .join(", ")
1039            ),
1040            Self::ListPath(v, _, _) => format!(
1041                "[{}]",
1042                v.iter()
1043                    .map(|s| format!("\"{}\"", s))
1044                    .collect::<Vec<_>>()
1045                    .join(", ")
1046            ),
1047            Self::ListList(v, _, _) => format!(
1048                "[{}]",
1049                v.iter()
1050                    .map(|e| e.to_display_string())
1051                    .collect::<Vec<_>>()
1052                    .join(", ")
1053            ),
1054            Self::RangeExpr(r) => r.to_string(),
1055            Self::Unresolved(t) => format!("<unresolved[{t}]>"),
1056        }
1057    }
1058
1059    /// Memory size: `size_of::<ExprValue>` (the enum itself) plus heap allocations.
1060    pub fn memory_size(&self) -> usize {
1061        std::mem::size_of::<ExprValue>() + self.heap_size()
1062    }
1063
1064    /// Heap-only allocation size (excludes the inline ExprValue struct).
1065    fn heap_size(&self) -> usize {
1066        use std::mem::size_of;
1067        match self {
1068            Self::Null | Self::Bool(_) | Self::Int(_) | Self::Unresolved(_) => 0,
1069            Self::Float(f) => f.original.as_ref().map_or(0, |s| s.len()),
1070            Self::String(s) | Self::Path { value: s, .. } => s.capacity(),
1071            Self::ListBool(v) => v.capacity(),
1072            Self::ListInt(v) => v.capacity() * size_of::<i64>(),
1073            Self::ListFloat(v) => v.capacity() * size_of::<Float64>(),
1074            Self::ListString(_, cached) | Self::ListPath(_, _, cached) => *cached,
1075            Self::ListList(_, _, cached) => *cached,
1076            Self::RangeExpr(r) => r.heap_size(),
1077        }
1078    }
1079
1080    /// Value equality with cross-type support (Int↔Float, String↔Path).
1081    pub fn equals(&self, other: &ExprValue) -> bool {
1082        match (self, other) {
1083            (Self::Null, Self::Null) => true,
1084            (Self::Bool(a), Self::Bool(b)) => a == b,
1085            (Self::Int(a), Self::Int(b)) => a == b,
1086            (Self::Float(a), Self::Float(b)) => a.value == b.value,
1087            (Self::Int(a), Self::Float(b)) => (*a as f64) == b.value,
1088            (Self::Float(a), Self::Int(b)) => a.value == (*b as f64),
1089            (Self::String(a), Self::String(b)) => a == b,
1090            (Self::Path { value: a, .. }, Self::Path { value: b, .. }) => a == b,
1091            (Self::String(a), Self::Path { value: b, .. })
1092            | (Self::Path { value: b, .. }, Self::String(a)) => a == b,
1093            _ if self.is_list() && other.is_list() => {
1094                let (a_iter, b_iter) = match (self.list_iter(), other.list_iter()) {
1095                    (Some(a), Some(b)) => (a, b),
1096                    _ => return false,
1097                };
1098                let (a_len, b_len) = (a_iter.len(), b_iter.len());
1099                if a_len != b_len {
1100                    return false;
1101                }
1102                a_iter.zip(b_iter).all(|(x, y)| x.equals(&y))
1103            }
1104            (Self::ListInt(elems), Self::RangeExpr(r))
1105            | (Self::RangeExpr(r), Self::ListInt(elems)) => {
1106                let rv: Vec<i64> = r.iter().collect();
1107                elems.len() == rv.len() && elems.iter().zip(rv.iter()).all(|(a, b)| a == b)
1108            }
1109            (Self::RangeExpr(a), Self::RangeExpr(b)) => a == b,
1110            (Self::Unresolved(a), Self::Unresolved(b)) => a == b,
1111            _ => false,
1112        }
1113    }
1114
1115    /// Ordering comparison. Returns `Err` for incomparable types.
1116    pub fn compare(
1117        &self,
1118        other: &ExprValue,
1119    ) -> Result<std::cmp::Ordering, crate::error::ExpressionError> {
1120        match (self, other) {
1121            (Self::Int(a), Self::Int(b)) => Ok(a.cmp(b)),
1122            (Self::Float(a), Self::Float(b)) => a
1123                .value
1124                .partial_cmp(&b.value)
1125                .ok_or_else(|| crate::error::ExpressionError::new("Cannot compare NaN")),
1126            (Self::Int(a), Self::Float(b)) => (*a as f64)
1127                .partial_cmp(&b.value)
1128                .ok_or_else(|| crate::error::ExpressionError::new("Cannot compare NaN")),
1129            (Self::Float(a), Self::Int(b)) => a
1130                .value
1131                .partial_cmp(&(*b as f64))
1132                .ok_or_else(|| crate::error::ExpressionError::new("Cannot compare NaN")),
1133            (Self::Bool(a), Self::Bool(b)) => Ok(a.cmp(b)),
1134            (Self::String(a), Self::String(b)) => Ok(a.cmp(b)),
1135            (Self::Path { value: a, .. }, Self::Path { value: b, .. }) => Ok(a.cmp(b)),
1136            (Self::String(a), Self::Path { value: b, .. })
1137            | (Self::Path { value: b, .. }, Self::String(a)) => Ok(a.cmp(b)),
1138            _ if self.is_list() && other.is_list() => {
1139                let (a_iter, b_iter) = match (self.list_iter(), other.list_iter()) {
1140                    (Some(a), Some(b)) => (a, b),
1141                    _ => {
1142                        return Err(crate::error::ExpressionError::new(format!(
1143                            "Cannot compare {} and {}",
1144                            self.expr_type(),
1145                            other.expr_type()
1146                        )))
1147                    }
1148                };
1149                let (a_len, b_len) = (a_iter.len(), b_iter.len());
1150                for (x, y) in a_iter.zip(b_iter) {
1151                    match x.compare(&y) {
1152                        Ok(std::cmp::Ordering::Equal) => continue,
1153                        other => return other,
1154                    }
1155                }
1156                Ok(a_len.cmp(&b_len))
1157            }
1158            _ => Err(crate::error::ExpressionError::new(format!(
1159                "Cannot compare {} and {}",
1160                self.expr_type(),
1161                other.expr_type()
1162            ))),
1163        }
1164    }
1165}
1166
1167impl PartialEq for ExprValue {
1168    fn eq(&self, other: &Self) -> bool {
1169        self.equals(other)
1170    }
1171}
1172
1173impl From<bool> for ExprValue {
1174    fn from(v: bool) -> Self {
1175        Self::Bool(v)
1176    }
1177}
1178impl From<i32> for ExprValue {
1179    fn from(v: i32) -> Self {
1180        Self::Int(v as i64)
1181    }
1182}
1183impl From<i64> for ExprValue {
1184    fn from(v: i64) -> Self {
1185        Self::Int(v)
1186    }
1187}
1188impl From<String> for ExprValue {
1189    fn from(v: String) -> Self {
1190        Self::String(v)
1191    }
1192}
1193impl From<&str> for ExprValue {
1194    fn from(v: &str) -> Self {
1195        Self::String(v.to_string())
1196    }
1197}
1198impl From<RangeExpr> for ExprValue {
1199    fn from(v: RangeExpr) -> Self {
1200        Self::RangeExpr(v)
1201    }
1202}
1203impl From<crate::types::ExprType> for ExprValue {
1204    fn from(t: crate::types::ExprType) -> Self {
1205        Self::Unresolved(t)
1206    }
1207}
1208
1209/// Zero-allocation iterator over list elements.
1210pub enum ListIter<'a> {
1211    Bool(std::slice::Iter<'a, bool>),
1212    Int(std::slice::Iter<'a, i64>),
1213    Float(std::slice::Iter<'a, Float64>),
1214    String(std::slice::Iter<'a, String>),
1215    Path(std::slice::Iter<'a, String>, PathFormat),
1216    List(std::slice::Iter<'a, ExprValue>),
1217}
1218
1219impl<'a> Iterator for ListIter<'a> {
1220    type Item = ExprValue;
1221    fn next(&mut self) -> Option<ExprValue> {
1222        match self {
1223            Self::Bool(it) => it.next().map(|b| ExprValue::Bool(*b)),
1224            Self::Int(it) => it.next().map(|i| ExprValue::Int(*i)),
1225            Self::Float(it) => it.next().map(|f| ExprValue::Float(f.clone())),
1226            Self::String(it) => it.next().map(|s| ExprValue::String(s.clone())),
1227            Self::Path(it, fmt) => it.next().map(|s| ExprValue::new_path(s.clone(), *fmt)),
1228            Self::List(it) => it.next().cloned(),
1229        }
1230    }
1231    fn size_hint(&self) -> (usize, Option<usize>) {
1232        match self {
1233            Self::Bool(it) => it.size_hint(),
1234            Self::Int(it) => it.size_hint(),
1235            Self::Float(it) => it.size_hint(),
1236            Self::String(it) => it.size_hint(),
1237            Self::Path(it, _) => it.size_hint(),
1238            Self::List(it) => it.size_hint(),
1239        }
1240    }
1241}
1242
1243impl<'a> ExactSizeIterator for ListIter<'a> {}
1244
1245pub fn format_float(f: f64) -> String {
1246    if f == 0.0 {
1247        return "0.0".to_string();
1248    }
1249    let abs = f.abs();
1250    if !(1e-4..1e16).contains(&abs) {
1251        format!("{:e}", f)
1252            .replace("e-0", "e-")
1253            .replace("e0", "e+0")
1254            .replace("e", "e+")
1255            .replace("e+-", "e-")
1256            .replace("e++", "e+")
1257    } else if f.fract() == 0.0 {
1258        format!("{}.0", f as i64)
1259    } else {
1260        f.to_string()
1261    }
1262}
1263
1264/// Normalize path separators to match `format`.
1265///
1266/// - `Posix`: no normalization — backslashes are valid filename characters
1267/// - `Windows`: `/` → `\` (unless the value is a URI)
1268/// - `Uri`: no normalization
1269#[must_use]
1270pub fn normalize_path_separators(value: &str, format: PathFormat) -> String {
1271    if crate::uri_path::is_uri(value) {
1272        return value.to_string();
1273    }
1274    match format {
1275        PathFormat::Windows => value.replace('/', "\\"),
1276        PathFormat::Posix | PathFormat::Uri => value.to_string(),
1277    }
1278}