1
//! CSS `calc()` expression evaluator.
2
//!
3
//! This module implements a two-pass stack-machine evaluator for `calc()` expressions.
4
//! It resolves `CalcAstItem` slices (flat, parenthesised AST) into a single `f32` pixel value.
5
//!
6
//! **Resolution context**: Em/rem units are resolved using per-node font sizes that are
7
//! captured lazily during style translation and stored alongside the AST pointer passed
8
//! to taffy. Percentages use the `basis` value provided by taffy (container width/height).
9

            
10
use azul_css::props::{
11
    basic::{
12
        pixel::PT_TO_PX,
13
        PixelValue, SizeMetric,
14
    },
15
    layout::dimensions::{CalcAstItem, CalcAstItemVec},
16
};
17

            
18
/// CSS reference pixels per inch (96 px/in per CSS spec).
19
pub(super) const PX_PER_INCH: f32 = 96.0;
20
/// Centimetres per inch.
21
pub(super) const CM_PER_INCH: f32 = 2.54;
22
/// Millimetres per inch.
23
pub(super) const MM_PER_INCH: f32 = 25.4;
24

            
25
/// Font-size context captured at style-translation time and stored alongside the calc AST.
26
///
27
/// Taffy's `resolve_calc_value` callback only receives `(*const (), f32)` — no node id.
28
/// We therefore bundle the per-node font sizes into the heap-pinned data that the opaque
29
/// pointer references, so the evaluator can resolve `em` / `rem` correctly.
30
#[derive(Debug, Clone)]
31
#[repr(C)]
32
pub struct CalcResolveContext {
33
    /// The calc AST items (flat stack-machine representation).
34
    pub items: CalcAstItemVec,
35
    /// Element's computed `font-size` in px — used for `em` resolution.
36
    pub em_size: f32,
37
    /// Root element's computed `font-size` in px — used for `rem` resolution.
38
    pub rem_size: f32,
39
}
40
// Tiny enum (Num(f32) = 4B vs Op(CalcOp) = 1B); the "large" variant is a bare
41
// f32, so boxing it would add a pointer + heap allocation — strictly worse than
42
// the few bytes of size disparity. Accepted.
43
#[allow(variant_size_differences)]
44
/// Internal intermediate representation: a number or an operator (after value resolution).
45
#[derive(Clone, Debug)]
46
enum CalcFlatItem {
47
    Num(f32),
48
    Op(CalcOp),
49
}
50

            
51
/// Arithmetic operators.
52
#[derive(Clone, Copy, Debug, PartialEq)]
53
enum CalcOp {
54
    Add,
55
    Sub,
56
    Mul,
57
    Div,
58
}
59

            
60
/// Evaluate a `CalcResolveContext` using the given `basis` (the "100 %" reference value,
61
/// e.g. containing-block width for `width: calc(…)`).
62
#[inline(never)] // M12.7: keep out of calc_used_size — its loop/jump-table inlined into the huge fn forces a remill PC-dispatch loop that mis-delivers auto_w
63
291
#[must_use] pub fn evaluate_calc(ctx: &CalcResolveContext, basis: f32) -> f32 {
64
291
    evaluate_calc_ast(ctx.items.as_slice(), basis, ctx.em_size, ctx.rem_size)
65
291
}
66

            
67
/// Stack-machine evaluator for a flat `CalcAstItem` slice.
68
///
69
/// `basis`    — the "100 %" reference value (e.g. containing-block width).
70
/// `em_size`  — element's computed font-size (for `em`).
71
/// `rem_size` — root element's computed font-size (for `rem`).
72
///
73
/// Two-pass approach with correct operator precedence:
74
///   Pass 1: evaluate `*` and `/`
75
///   Pass 2: evaluate `+` and `-`
76
/// Parenthesised sub-expressions are resolved recursively.
77
#[inline(never)] // M12.7: keep out of calc_used_size — its loop/jump-table inlined into the huge fn forces a remill PC-dispatch loop that mis-delivers auto_w
78
898
fn evaluate_calc_ast(
79
898
    items: &[CalcAstItem],
80
898
    basis: f32,
81
898
    em_size: f32,
82
898
    rem_size: f32,
83
898
) -> f32 {
84
    // Convert into a working vec of resolved numbers and operators.
85
898
    let mut flat: Vec<CalcFlatItem> = Vec::with_capacity(items.len());
86
898
    let mut i = 0;
87
4424
    while i < items.len() {
88
3526
        match &items[i] {
89
1627
            CalcAstItem::Value(pv) => {
90
1627
                flat.push(CalcFlatItem::Num(resolve_pixel_value(
91
1627
                    pv, basis, em_size, rem_size,
92
1627
                )));
93
1627
            }
94
1133
            CalcAstItem::Add => flat.push(CalcFlatItem::Op(CalcOp::Add)),
95
76
            CalcAstItem::Sub => flat.push(CalcFlatItem::Op(CalcOp::Sub)),
96
78
            CalcAstItem::Mul => flat.push(CalcFlatItem::Op(CalcOp::Mul)),
97
49
            CalcAstItem::Div => flat.push(CalcFlatItem::Op(CalcOp::Div)),
98
            CalcAstItem::BraceOpen => {
99
                // Find matching BraceClose and recurse
100
561
                let start = i + 1;
101
561
                let mut depth = 1u32;
102
561
                let mut j = start;
103
99217
                while j < items.len() && depth > 0 {
104
98656
                    match &items[j] {
105
65291
                        CalcAstItem::BraceOpen => depth += 1,
106
32953
                        CalcAstItem::BraceClose => depth -= 1,
107
412
                        _ => {}
108
                    }
109
98656
                    if depth > 0 {
110
98354
                        j += 1;
111
98354
                    }
112
                }
113
                // items[start..j] is the inner sub-expression (excl. braces)
114
561
                let sub_val = evaluate_calc_ast(&items[start..j], basis, em_size, rem_size);
115
561
                flat.push(CalcFlatItem::Num(sub_val));
116
561
                i = j; // skip past the closing brace
117
            }
118
2
            CalcAstItem::BraceClose => { /* shouldn't happen at top level */ }
119
        }
120
3526
        i += 1;
121
    }
122

            
123
    // Pass 1: resolve * and /
124
898
    let mut pass2: Vec<CalcFlatItem> = Vec::with_capacity(flat.len());
125
898
    let mut k = 0;
126
4302
    while k < flat.len() {
127
3404
        if let CalcFlatItem::Op(op @ (CalcOp::Mul | CalcOp::Div)) = &flat[k] {
128
            // Apply to previous Num in pass2 and next Num in flat
129
120
            if let (Some(CalcFlatItem::Num(lhs)), Some(CalcFlatItem::Num(rhs))) =
130
127
                (pass2.last(), flat.get(k + 1))
131
            {
132
120
                let result = match op {
133
74
                    CalcOp::Mul => lhs * rhs,
134
                    CalcOp::Div => {
135
46
                        if *rhs == 0.0 {
136
13
                            0.0
137
                        } else {
138
33
                            lhs / rhs
139
                        }
140
                    }
141
                    _ => unreachable!(),
142
                };
143
120
                *pass2.last_mut().unwrap() = CalcFlatItem::Num(result);
144
120
                k += 2; // skip operator + rhs
145
120
                continue;
146
7
            }
147
3277
        }
148
3284
        pass2.push(flat[k].clone());
149
3284
        k += 1;
150
    }
151

            
152
    // Pass 2: resolve + and -
153
898
    let mut result = match pass2.first() {
154
869
        Some(CalcFlatItem::Num(v)) => *v,
155
29
        _ => return 0.0,
156
    };
157
869
    let mut m = 1;
158
2070
    while m < pass2.len() {
159
1195
        if let (CalcFlatItem::Op(op), Some(CalcFlatItem::Num(rhs))) =
160
1201
            (&pass2[m], pass2.get(m + 1))
161
        {
162
1195
            match op {
163
1120
                CalcOp::Add => result += rhs,
164
74
                CalcOp::Sub => result -= rhs,
165
1
                _ => {} // already handled in pass 1
166
            }
167
1195
            m += 2;
168
6
        } else {
169
6
            m += 1;
170
6
        }
171
    }
172

            
173
869
    result
174
898
}
175

            
176
/// Resolve a single `PixelValue` to `f32` pixels inside a `calc()` expression.
177
///
178
/// - `basis`    — the "100 %" reference (containing-block width or height)
179
/// - `em_size`  — element's computed font-size (for `em` units)
180
/// - `rem_size` — root element's computed font-size (for `rem` units)
181
///
182
/// NOTE: this variant has no viewport context, so viewport units (`vw`/`vh`/
183
/// `vmin`/`vmax`) fall back to their raw number (i.e. `50vw` → `50px`). Callers
184
/// that may see viewport units must use [`resolve_pixel_value_with_viewport`]
185
/// (or [`resolve_pixel_value_no_percent_with_viewport`]) instead.
186
220225
#[must_use] pub fn resolve_pixel_value(
187
220225
    pv: &PixelValue,
188
220225
    basis: f32,
189
220225
    em_size: f32,
190
220225
    rem_size: f32,
191
220225
) -> f32 {
192
220225
    match pv.metric {
193
219557
        SizeMetric::Px => pv.number.get(),
194
53
        SizeMetric::Pt => pv.number.get() * PT_TO_PX,
195
53
        SizeMetric::In => pv.number.get() * PX_PER_INCH,
196
53
        SizeMetric::Cm => pv.number.get() * PX_PER_INCH / CM_PER_INCH,
197
53
        SizeMetric::Mm => pv.number.get() * PX_PER_INCH / MM_PER_INCH,
198
102
        SizeMetric::Em => pv.number.get() * em_size,
199
86
        SizeMetric::Rem => pv.number.get() * rem_size,
200
131
        SizeMetric::Percent => basis * (pv.number.get() / 100.0),
201
        SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => {
202
            // Viewport units: fallback — proper resolution requires viewport context
203
137
            pv.number.get()
204
        }
205
    }
206
220225
}
207

            
208
/// Like `resolve_pixel_value`, but with proper viewport unit resolution.
209
#[inline(never)] // M12.7: keep out of calc_used_size — its loop/jump-table inlined into the huge fn forces a remill PC-dispatch loop that mis-delivers auto_w
210
218104
#[must_use] pub fn resolve_pixel_value_with_viewport(
211
218104
    pv: &PixelValue,
212
218104
    basis: f32,
213
218104
    em_size: f32,
214
218104
    rem_size: f32,
215
218104
    viewport_width: f32,
216
218104
    viewport_height: f32,
217
218104
) -> f32 {
218
218104
    match pv.metric {
219
7
        SizeMetric::Vw => pv.number.get() / 100.0 * viewport_width,
220
3
        SizeMetric::Vh => pv.number.get() / 100.0 * viewport_height,
221
6
        SizeMetric::Vmin => pv.number.get() / 100.0 * viewport_width.min(viewport_height),
222
5
        SizeMetric::Vmax => pv.number.get() / 100.0 * viewport_width.max(viewport_height),
223
218083
        _ => resolve_pixel_value(pv, basis, em_size, rem_size),
224
    }
225
218104
}
226

            
227
/// Resolve a `PixelValue` to pixels, returning `None` for percentage and viewport units.
228
70275
#[must_use] pub fn resolve_pixel_value_no_percent(
229
70275
    pv: &PixelValue,
230
70275
    em_size: f32,
231
70275
    rem_size: f32,
232
70275
) -> Option<f32> {
233
70275
    match pv.metric {
234
67364
        SizeMetric::Px => Some(pv.number.get()),
235
9
        SizeMetric::Pt => Some(pv.number.get() * PT_TO_PX),
236
9
        SizeMetric::In => Some(pv.number.get() * PX_PER_INCH),
237
9
        SizeMetric::Cm => Some(pv.number.get() * PX_PER_INCH / CM_PER_INCH),
238
9
        SizeMetric::Mm => Some(pv.number.get() * PX_PER_INCH / MM_PER_INCH),
239
12
        SizeMetric::Em => Some(pv.number.get() * em_size),
240
10
        SizeMetric::Rem => Some(pv.number.get() * rem_size),
241
        SizeMetric::Percent
242
        | SizeMetric::Vw
243
        | SizeMetric::Vh
244
        | SizeMetric::Vmin
245
2853
        | SizeMetric::Vmax => None,
246
    }
247
70275
}
248

            
249
/// Like `resolve_pixel_value_no_percent`, but resolves viewport units using
250
/// the provided viewport dimensions. Returns `None` only for percentages.
251
19028
#[must_use] pub fn resolve_pixel_value_no_percent_with_viewport(
252
19028
    pv: &PixelValue,
253
19028
    em_size: f32,
254
19028
    rem_size: f32,
255
19028
    viewport_width: f32,
256
19028
    viewport_height: f32,
257
19028
) -> Option<f32> {
258
19028
    match pv.metric {
259
3
        SizeMetric::Vw => Some(pv.number.get() / 100.0 * viewport_width),
260
3
        SizeMetric::Vh => Some(pv.number.get() / 100.0 * viewport_height),
261
3
        SizeMetric::Vmin => Some(pv.number.get() / 100.0 * viewport_width.min(viewport_height)),
262
3
        SizeMetric::Vmax => Some(pv.number.get() / 100.0 * viewport_width.max(viewport_height)),
263
19016
        _ => resolve_pixel_value_no_percent(pv, em_size, rem_size),
264
    }
265
19028
}
266

            
267
#[cfg(test)]
268
#[allow(clippy::float_cmp, clippy::cast_precision_loss, clippy::unreadable_literal)]
269
mod autotest_generated {
270
    use azul_css::props::basic::FP_PRECISION_MULTIPLIER;
271

            
272
    use super::*;
273

            
274
    // ------------------------------------------------------------------
275
    // Fixtures
276
    // ------------------------------------------------------------------
277

            
278
    /// Every `SizeMetric` variant, so "for all metrics" invariants stay honest
279
    /// if a new unit is added to the enum (the array length breaks the build).
280
    const ALL_METRICS: [SizeMetric; 12] = [
281
        SizeMetric::Px,
282
        SizeMetric::Pt,
283
        SizeMetric::Em,
284
        SizeMetric::Rem,
285
        SizeMetric::In,
286
        SizeMetric::Cm,
287
        SizeMetric::Mm,
288
        SizeMetric::Percent,
289
        SizeMetric::Vw,
290
        SizeMetric::Vh,
291
        SizeMetric::Vmin,
292
        SizeMetric::Vmax,
293
    ];
294

            
295
    /// Metrics that resolve to an absolute length without any layout context.
296
    const ABSOLUTE_METRICS: [SizeMetric; 7] = [
297
        SizeMetric::Px,
298
        SizeMetric::Pt,
299
        SizeMetric::Em,
300
        SizeMetric::Rem,
301
        SizeMetric::In,
302
        SizeMetric::Cm,
303
        SizeMetric::Mm,
304
    ];
305

            
306
    const VIEWPORT_METRICS: [SizeMetric; 4] = [
307
        SizeMetric::Vw,
308
        SizeMetric::Vh,
309
        SizeMetric::Vmin,
310
        SizeMetric::Vmax,
311
    ];
312

            
313
    /// `FloatValue` is fixed-point (`isize` scaled by `FP_PRECISION_MULTIPLIER`),
314
    /// so any `f32` too large to fit saturates to this magnitude on `get()`.
315
    fn saturated_magnitude() -> f32 {
316
        (isize::MAX as f32) / FP_PRECISION_MULTIPLIER
317
    }
318

            
319
    fn assert_close(actual: f32, expected: f32) {
320
        let tolerance = 1e-3 * expected.abs().max(1.0);
321
        assert!(
322
            (actual - expected).abs() <= tolerance,
323
            "expected ~{expected}, got {actual}",
324
        );
325
    }
326

            
327
    fn val(metric: SizeMetric, n: f32) -> CalcAstItem {
328
        CalcAstItem::Value(PixelValue::from_metric(metric, n))
329
    }
330

            
331
    fn px(n: f32) -> CalcAstItem {
332
        val(SizeMetric::Px, n)
333
    }
334

            
335
    const EM: f32 = 16.0;
336
    const REM: f32 = 10.0;
337

            
338
    fn eval(items: &[CalcAstItem], basis: f32) -> f32 {
339
        evaluate_calc_ast(items, basis, EM, REM)
340
    }
341

            
342
    fn ctx(items: Vec<CalcAstItem>, em_size: f32, rem_size: f32) -> CalcResolveContext {
343
        CalcResolveContext {
344
            items: CalcAstItemVec::from(items),
345
            em_size,
346
            rem_size,
347
        }
348
    }
349

            
350
    // ==================================================================
351
    // PixelValue encoding invariants
352
    //
353
    // These underpin every resolver below: because `FloatValue` stores an
354
    // `isize` and `f32 as isize` saturates (NaN -> 0, +/-inf -> MIN/MAX),
355
    // a `PixelValue` can never *carry* a NaN or an infinity. Any non-finite
356
    // result therefore has to come from `basis` / `em_size` / `viewport_*`.
357
    // ==================================================================
358

            
359
    #[test]
360
    fn pixel_value_number_is_always_finite_even_for_nan_and_inf() {
361
        for metric in ALL_METRICS {
362
            for hostile in [
363
                f32::NAN,
364
                f32::INFINITY,
365
                f32::NEG_INFINITY,
366
                f32::MAX,
367
                f32::MIN,
368
                f32::MIN_POSITIVE,
369
                -0.0,
370
            ] {
371
                let n = PixelValue::from_metric(metric, hostile).number.get();
372
                assert!(
373
                    n.is_finite(),
374
                    "PixelValue::from_metric({metric:?}, {hostile}) produced non-finite {n}",
375
                );
376
            }
377
        }
378
    }
379

            
380
    #[test]
381
    fn pixel_value_nan_encodes_as_zero() {
382
        assert_eq!(PixelValue::px(f32::NAN).number.get(), 0.0);
383
    }
384

            
385
    #[test]
386
    fn pixel_value_infinities_saturate_to_fixed_point_bounds() {
387
        let sat = saturated_magnitude();
388
        assert_close(PixelValue::px(f32::INFINITY).number.get(), sat);
389
        assert_close(PixelValue::px(f32::NEG_INFINITY).number.get(), -sat);
390
        // f32::MAX * 1000.0 already overflows to inf before the cast, so it
391
        // lands on the same saturation point rather than on f32::MAX.
392
        assert_close(PixelValue::px(f32::MAX).number.get(), sat);
393
        assert_close(PixelValue::px(f32::MIN).number.get(), -sat);
394
    }
395

            
396
    // ==================================================================
397
    // resolve_pixel_value
398
    // ==================================================================
399

            
400
    #[test]
401
    fn resolve_pixel_value_zero_is_zero_for_every_metric() {
402
        for metric in ALL_METRICS {
403
            let pv = PixelValue::from_metric(metric, 0.0);
404
            let got = resolve_pixel_value(&pv, 500.0, EM, REM);
405
            assert_eq!(got, 0.0, "0 {metric:?} should resolve to 0px, got {got}");
406
        }
407
    }
408

            
409
    #[test]
410
    fn resolve_pixel_value_absolute_unit_conversions() {
411
        assert_eq!(resolve_pixel_value(&PixelValue::px(10.0), 0.0, EM, REM), 10.0);
412
        assert_close(
413
            resolve_pixel_value(&PixelValue::pt(12.0), 0.0, EM, REM),
414
            12.0 * PT_TO_PX,
415
        );
416
        // 1in == 96px, 2.54cm == 1in, 25.4mm == 1in.
417
        assert_close(resolve_pixel_value(&PixelValue::inch(1.0), 0.0, EM, REM), 96.0);
418
        assert_close(resolve_pixel_value(&PixelValue::cm(2.54), 0.0, EM, REM), 96.0);
419
        assert_close(resolve_pixel_value(&PixelValue::mm(25.4), 0.0, EM, REM), 96.0);
420
    }
421

            
422
    #[test]
423
    fn resolve_pixel_value_em_and_rem_use_their_own_font_size() {
424
        assert_eq!(resolve_pixel_value(&PixelValue::em(2.0), 0.0, 16.0, 10.0), 32.0);
425
        assert_eq!(resolve_pixel_value(&PixelValue::rem(2.0), 0.0, 16.0, 10.0), 20.0);
426
    }
427

            
428
    #[test]
429
    fn resolve_pixel_value_percent_uses_basis() {
430
        assert_eq!(resolve_pixel_value(&PixelValue::percent(50.0), 200.0, EM, REM), 100.0);
431
        assert_eq!(resolve_pixel_value(&PixelValue::percent(0.0), 200.0, EM, REM), 0.0);
432
        assert_eq!(resolve_pixel_value(&PixelValue::percent(100.0), 0.0, EM, REM), 0.0);
433
    }
434

            
435
    #[test]
436
    fn resolve_pixel_value_handles_negative_inputs_deterministically() {
437
        assert_eq!(resolve_pixel_value(&PixelValue::px(-10.0), 0.0, EM, REM), -10.0);
438
        assert_eq!(resolve_pixel_value(&PixelValue::em(-2.0), 0.0, 16.0, REM), -32.0);
439
        // Negative percentage of a positive basis, and positive percentage of a
440
        // negative basis, both yield a negative length (no clamping here).
441
        assert_eq!(resolve_pixel_value(&PixelValue::percent(-50.0), 200.0, EM, REM), -100.0);
442
        assert_eq!(resolve_pixel_value(&PixelValue::percent(50.0), -200.0, EM, REM), -100.0);
443
    }
444

            
445
    #[test]
446
    fn resolve_pixel_value_viewport_units_fall_back_to_raw_number() {
447
        // Documented fallback: with no viewport context, `50vw` degrades to `50px`.
448
        for metric in VIEWPORT_METRICS {
449
            let pv = PixelValue::from_metric(metric, 50.0);
450
            assert_eq!(
451
                resolve_pixel_value(&pv, 1000.0, EM, REM),
452
                50.0,
453
                "{metric:?} should fall back to its raw number",
454
            );
455
        }
456
    }
457

            
458
    #[test]
459
    fn resolve_pixel_value_is_finite_for_any_pixel_value_in_a_sane_context() {
460
        // The core safety property: because a PixelValue can never *carry* a NaN
461
        // or an infinity, no value — not even one built from f32::MAX — can turn
462
        // a realistic basis/em/rem into a non-finite length. (An absurd basis
463
        // *can* still overflow; that is pinned separately below.)
464
        for metric in ALL_METRICS {
465
            for n in [0.0, -0.0, 1.0, -1.0, f32::MAX, f32::MIN, f32::NAN, f32::INFINITY] {
466
                let pv = PixelValue::from_metric(metric, n);
467
                for basis in [0.0_f32, -1000.0, 1920.0, 1_000_000.0] {
468
                    let got = resolve_pixel_value(&pv, basis, EM, REM);
469
                    assert!(
470
                        got.is_finite(),
471
                        "{metric:?} {n} @ basis {basis} produced non-finite {got}",
472
                    );
473
                }
474
            }
475
        }
476
    }
477

            
478
    #[test]
479
    fn resolve_pixel_value_saturated_percent_of_huge_basis_overflows_to_inf_not_panic() {
480
        // 100% of f32::MAX stays finite; 200% of it overflows.
481
        let hundred = resolve_pixel_value(&PixelValue::percent(100.0), f32::MAX, EM, REM);
482
        assert_eq!(hundred, f32::MAX);
483

            
484
        let two_hundred = resolve_pixel_value(&PixelValue::percent(200.0), f32::MAX, EM, REM);
485
        assert!(
486
            two_hundred.is_infinite() && two_hundred.is_sign_positive(),
487
            "expected +inf on overflow, got {two_hundred}",
488
        );
489
    }
490

            
491
    #[test]
492
    fn resolve_pixel_value_nan_basis_only_pollutes_percentages() {
493
        // A NaN basis must not leak into units that never read it.
494
        assert_eq!(resolve_pixel_value(&PixelValue::px(10.0), f32::NAN, EM, REM), 10.0);
495
        assert_eq!(resolve_pixel_value(&PixelValue::em(1.0), f32::NAN, 16.0, REM), 16.0);
496
        assert!(resolve_pixel_value(&PixelValue::percent(50.0), f32::NAN, EM, REM).is_nan());
497
    }
498

            
499
    #[test]
500
    fn resolve_pixel_value_zero_times_infinite_context_is_nan_not_zero() {
501
        // IEEE-754 quirk worth pinning: `0em` against an infinite font-size (and
502
        // `0%` against an infinite basis) is 0 * inf == NaN, NOT 0. Callers that
503
        // treat "zero length" as always-safe do not get a zero here.
504
        assert!(resolve_pixel_value(&PixelValue::em(0.0), 0.0, f32::INFINITY, REM).is_nan());
505
        assert!(resolve_pixel_value(&PixelValue::percent(0.0), f32::INFINITY, EM, REM).is_nan());
506
        // ...whereas a non-zero multiplier gives a plain infinity.
507
        let inf = resolve_pixel_value(&PixelValue::em(1.0), 0.0, f32::INFINITY, REM);
508
        assert!(inf.is_infinite() && inf.is_sign_positive());
509
    }
510

            
511
    // ==================================================================
512
    // resolve_pixel_value_with_viewport
513
    // ==================================================================
514

            
515
    #[test]
516
    fn resolve_with_viewport_resolves_viewport_units() {
517
        let (vw, vh) = (1000.0_f32, 800.0_f32);
518
        let r = |m: SizeMetric, n: f32| {
519
            resolve_pixel_value_with_viewport(&PixelValue::from_metric(m, n), 0.0, EM, REM, vw, vh)
520
        };
521
        assert_eq!(r(SizeMetric::Vw, 50.0), 500.0);
522
        assert_eq!(r(SizeMetric::Vh, 50.0), 400.0);
523
        assert_eq!(r(SizeMetric::Vmin, 100.0), 800.0);
524
        assert_eq!(r(SizeMetric::Vmax, 100.0), 1000.0);
525
    }
526

            
527
    #[test]
528
    fn resolve_with_viewport_delegates_non_viewport_metrics_verbatim() {
529
        for metric in ALL_METRICS {
530
            if VIEWPORT_METRICS.contains(&metric) {
531
                continue;
532
            }
533
            let pv = PixelValue::from_metric(metric, 33.75);
534
            assert_eq!(
535
                resolve_pixel_value_with_viewport(&pv, 500.0, EM, REM, 1920.0, 1080.0),
536
                resolve_pixel_value(&pv, 500.0, EM, REM),
537
                "{metric:?} must not be affected by viewport context",
538
            );
539
        }
540
    }
541

            
542
    #[test]
543
    fn resolve_with_viewport_zero_and_negative_viewport() {
544
        for metric in VIEWPORT_METRICS {
545
            let pv = PixelValue::from_metric(metric, 50.0);
546
            assert_eq!(
547
                resolve_pixel_value_with_viewport(&pv, 0.0, EM, REM, 0.0, 0.0),
548
                0.0,
549
                "{metric:?} against a 0x0 viewport should be 0px",
550
            );
551
        }
552
        // A negative viewport is nonsense but must stay deterministic, not panic.
553
        let vw50 = PixelValue::from_metric(SizeMetric::Vw, 50.0);
554
        assert_eq!(
555
            resolve_pixel_value_with_viewport(&vw50, 0.0, EM, REM, -100.0, -100.0),
556
            -50.0,
557
        );
558
        // min/max still order correctly with negatives: min(-100, -50) == -100.
559
        let vmin100 = PixelValue::from_metric(SizeMetric::Vmin, 100.0);
560
        let vmax100 = PixelValue::from_metric(SizeMetric::Vmax, 100.0);
561
        assert_eq!(
562
            resolve_pixel_value_with_viewport(&vmin100, 0.0, EM, REM, -100.0, -50.0),
563
            -100.0,
564
        );
565
        assert_eq!(
566
            resolve_pixel_value_with_viewport(&vmax100, 0.0, EM, REM, -100.0, -50.0),
567
            -50.0,
568
        );
569
    }
570

            
571
    #[test]
572
    fn resolve_with_viewport_nan_viewport_is_swallowed_by_vmin_vmax_but_not_vw_vh() {
573
        // f32::min / f32::max return the *other* operand when one side is NaN,
574
        // so a half-NaN viewport silently produces a finite vmin/vmax...
575
        let vmin = PixelValue::from_metric(SizeMetric::Vmin, 100.0);
576
        let vmax = PixelValue::from_metric(SizeMetric::Vmax, 100.0);
577
        assert_eq!(
578
            resolve_pixel_value_with_viewport(&vmin, 0.0, EM, REM, f32::NAN, 800.0),
579
            800.0,
580
        );
581
        assert_eq!(
582
            resolve_pixel_value_with_viewport(&vmax, 0.0, EM, REM, f32::NAN, 800.0),
583
            800.0,
584
        );
585
        // ...while vw/vh propagate the NaN. Both are defined; neither panics.
586
        let vw = PixelValue::from_metric(SizeMetric::Vw, 50.0);
587
        assert!(resolve_pixel_value_with_viewport(&vw, 0.0, EM, REM, f32::NAN, 800.0).is_nan());
588

            
589
        // Fully-NaN viewport: vmin/vmax have no finite operand to fall back on.
590
        assert!(
591
            resolve_pixel_value_with_viewport(&vmin, 0.0, EM, REM, f32::NAN, f32::NAN).is_nan()
592
        );
593
    }
594

            
595
    #[test]
596
    fn resolve_with_viewport_infinite_viewport_does_not_panic() {
597
        let vw = PixelValue::from_metric(SizeMetric::Vw, 50.0);
598
        let got = resolve_pixel_value_with_viewport(&vw, 0.0, EM, REM, f32::INFINITY, 0.0);
599
        assert!(got.is_infinite() && got.is_sign_positive());
600

            
601
        // 0vw of an infinite viewport is 0 * inf == NaN, same quirk as `0em`.
602
        let vw0 = PixelValue::from_metric(SizeMetric::Vw, 0.0);
603
        assert!(
604
            resolve_pixel_value_with_viewport(&vw0, 0.0, EM, REM, f32::INFINITY, 0.0).is_nan()
605
        );
606
    }
607

            
608
    // ==================================================================
609
    // resolve_pixel_value_no_percent (+ viewport variant)
610
    // ==================================================================
611

            
612
    #[test]
613
    fn no_percent_returns_none_for_percent_and_viewport_units() {
614
        for metric in [
615
            SizeMetric::Percent,
616
            SizeMetric::Vw,
617
            SizeMetric::Vh,
618
            SizeMetric::Vmin,
619
            SizeMetric::Vmax,
620
        ] {
621
            let pv = PixelValue::from_metric(metric, 50.0);
622
            assert_eq!(
623
                resolve_pixel_value_no_percent(&pv, EM, REM),
624
                None,
625
                "{metric:?} needs layout context, so it must be None",
626
            );
627
        }
628
    }
629

            
630
    #[test]
631
    fn no_percent_agrees_with_resolve_pixel_value_on_absolute_metrics() {
632
        // Invariant: where `no_percent` returns Some, the value must be exactly
633
        // what `resolve_pixel_value` computes — for *any* basis, since none of
634
        // these units read it.
635
        for metric in ABSOLUTE_METRICS {
636
            for n in [0.0, 1.0, -7.5, 1234.5, f32::MAX, f32::MIN, f32::NAN] {
637
                let pv = PixelValue::from_metric(metric, n);
638
                let some = resolve_pixel_value_no_percent(&pv, EM, REM);
639
                assert_eq!(
640
                    some,
641
                    Some(resolve_pixel_value(&pv, 999.0, EM, REM)),
642
                    "{metric:?} {n} disagrees between the two resolvers",
643
                );
644
                assert!(some.unwrap().is_finite());
645
            }
646
        }
647
    }
648

            
649
    #[test]
650
    fn no_percent_with_viewport_returns_none_only_for_percent() {
651
        for metric in ALL_METRICS {
652
            let pv = PixelValue::from_metric(metric, 50.0);
653
            let got = resolve_pixel_value_no_percent_with_viewport(&pv, EM, REM, 1000.0, 800.0);
654
            if metric == SizeMetric::Percent {
655
                assert_eq!(got, None, "% must stay None without a basis");
656
            } else {
657
                assert!(got.is_some(), "{metric:?} should resolve with a viewport");
658
                assert!(got.unwrap().is_finite());
659
            }
660
        }
661
    }
662

            
663
    #[test]
664
    fn no_percent_with_viewport_agrees_with_resolve_with_viewport() {
665
        for metric in ALL_METRICS {
666
            if metric == SizeMetric::Percent {
667
                continue;
668
            }
669
            let pv = PixelValue::from_metric(metric, 25.0);
670
            assert_eq!(
671
                resolve_pixel_value_no_percent_with_viewport(&pv, EM, REM, 1920.0, 1080.0),
672
                Some(resolve_pixel_value_with_viewport(&pv, 0.0, EM, REM, 1920.0, 1080.0)),
673
                "{metric:?} disagrees between the viewport resolvers",
674
            );
675
        }
676
    }
677

            
678
    #[test]
679
    fn no_percent_with_viewport_zero_viewport_yields_some_zero_not_none() {
680
        for metric in VIEWPORT_METRICS {
681
            let pv = PixelValue::from_metric(metric, 100.0);
682
            assert_eq!(
683
                resolve_pixel_value_no_percent_with_viewport(&pv, EM, REM, 0.0, 0.0),
684
                Some(0.0),
685
                "{metric:?} against a 0x0 viewport is a defined 0px, not None",
686
            );
687
        }
688
    }
689

            
690
    // ==================================================================
691
    // evaluate_calc_ast — structure & precedence
692
    // ==================================================================
693

            
694
    #[test]
695
    fn eval_empty_ast_is_zero() {
696
        assert_eq!(eval(&[], 500.0), 0.0);
697
    }
698

            
699
    #[test]
700
    fn eval_single_value() {
701
        assert_eq!(eval(&[px(42.0)], 500.0), 42.0);
702
        assert_eq!(eval(&[val(SizeMetric::Percent, 10.0)], 500.0), 50.0);
703
    }
704

            
705
    #[test]
706
    fn eval_basic_addition_and_subtraction() {
707
        // calc(100% - 20px) with a 500px containing block
708
        let items = [val(SizeMetric::Percent, 100.0), CalcAstItem::Sub, px(20.0)];
709
        assert_eq!(eval(&items, 500.0), 480.0);
710
    }
711

            
712
    #[test]
713
    fn eval_add_and_sub_are_left_associative() {
714
        let items = [px(10.0), CalcAstItem::Sub, px(3.0), CalcAstItem::Sub, px(2.0)];
715
        assert_eq!(eval(&items, 0.0), 5.0, "must be (10-3)-2, not 10-(3-2)");
716
    }
717

            
718
    #[test]
719
    fn eval_div_is_left_associative() {
720
        let items = [px(10.0), CalcAstItem::Div, px(4.0), CalcAstItem::Div, px(2.0)];
721
        assert_eq!(eval(&items, 0.0), 1.25, "must be (10/4)/2, not 10/(4/2)");
722
    }
723

            
724
    #[test]
725
    fn eval_mul_binds_tighter_than_add() {
726
        // 1 + 2 * 3 == 7, not 9
727
        let items = [px(1.0), CalcAstItem::Add, px(2.0), CalcAstItem::Mul, px(3.0)];
728
        assert_eq!(eval(&items, 0.0), 7.0);
729

            
730
        // 2 * 3 + 4 == 10 (operator on the left of the sum)
731
        let items = [px(2.0), CalcAstItem::Mul, px(3.0), CalcAstItem::Add, px(4.0)];
732
        assert_eq!(eval(&items, 0.0), 10.0);
733
    }
734

            
735
    #[test]
736
    fn eval_div_binds_tighter_than_sub() {
737
        // 10 - 6 / 2 == 7, not 2
738
        let items = [px(10.0), CalcAstItem::Sub, px(6.0), CalcAstItem::Div, px(2.0)];
739
        assert_eq!(eval(&items, 0.0), 7.0);
740
    }
741

            
742
    #[test]
743
    fn eval_braces_override_precedence() {
744
        // (1 + 2) * 3 == 9
745
        let items = [
746
            CalcAstItem::BraceOpen,
747
            px(1.0),
748
            CalcAstItem::Add,
749
            px(2.0),
750
            CalcAstItem::BraceClose,
751
            CalcAstItem::Mul,
752
            px(3.0),
753
        ];
754
        assert_eq!(eval(&items, 0.0), 9.0);
755
    }
756

            
757
    #[test]
758
    fn eval_nested_braces() {
759
        // ((2 + 3) * (4 - 2)) == 10
760
        let items = [
761
            CalcAstItem::BraceOpen,
762
            CalcAstItem::BraceOpen,
763
            px(2.0),
764
            CalcAstItem::Add,
765
            px(3.0),
766
            CalcAstItem::BraceClose,
767
            CalcAstItem::Mul,
768
            CalcAstItem::BraceOpen,
769
            px(4.0),
770
            CalcAstItem::Sub,
771
            px(2.0),
772
            CalcAstItem::BraceClose,
773
            CalcAstItem::BraceClose,
774
        ];
775
        assert_eq!(eval(&items, 0.0), 10.0);
776
    }
777

            
778
    #[test]
779
    fn eval_empty_braces_are_zero() {
780
        let items = [CalcAstItem::BraceOpen, CalcAstItem::BraceClose];
781
        assert_eq!(eval(&items, 500.0), 0.0);
782

            
783
        // ...and an empty brace still participates as a term: 10 + () == 10
784
        let items = [
785
            px(10.0),
786
            CalcAstItem::Add,
787
            CalcAstItem::BraceOpen,
788
            CalcAstItem::BraceClose,
789
        ];
790
        assert_eq!(eval(&items, 500.0), 10.0);
791
    }
792

            
793
    #[test]
794
    fn eval_percent_inside_braces_still_sees_the_basis() {
795
        // (100% - 20px) / 4  with basis 500 -> 120
796
        let items = [
797
            CalcAstItem::BraceOpen,
798
            val(SizeMetric::Percent, 100.0),
799
            CalcAstItem::Sub,
800
            px(20.0),
801
            CalcAstItem::BraceClose,
802
            CalcAstItem::Div,
803
            px(4.0),
804
        ];
805
        assert_eq!(eval(&items, 500.0), 120.0);
806
    }
807

            
808
    #[test]
809
    fn eval_em_and_rem_come_from_the_evaluator_args_not_the_basis() {
810
        // 2em + 1rem with em=10, rem=20 -> 40
811
        let items = [val(SizeMetric::Em, 2.0), CalcAstItem::Add, val(SizeMetric::Rem, 1.0)];
812
        assert_eq!(evaluate_calc_ast(&items, 0.0, 10.0, 20.0), 40.0);
813
    }
814

            
815
    // ==================================================================
816
    // evaluate_calc_ast — malformed / adversarial ASTs
817
    //
818
    // The AST is only ever produced by the parser, but it also crosses an FFI
819
    // boundary (`CalcAstItemVec` is `repr(C)`), so a hand-built or corrupted
820
    // item list must degrade, never panic or hang.
821
    // ==================================================================
822

            
823
    #[test]
824
    fn eval_unmatched_brace_open_does_not_panic_or_hang() {
825
        // A lone `(` — the scan for the matching `)` runs off the end.
826
        assert_eq!(eval(&[CalcAstItem::BraceOpen], 500.0), 0.0);
827
        // `(5px` — the unterminated body is still evaluated.
828
        assert_eq!(eval(&[CalcAstItem::BraceOpen, px(5.0)], 500.0), 5.0);
829
        // `1px + (2px` -> 3
830
        let items = [
831
            px(1.0),
832
            CalcAstItem::Add,
833
            CalcAstItem::BraceOpen,
834
            px(2.0),
835
        ];
836
        assert_eq!(eval(&items, 500.0), 3.0);
837
    }
838

            
839
    #[test]
840
    fn eval_stray_brace_close_is_ignored() {
841
        // `5px ) + 3px` -> the stray `)` is skipped at top level.
842
        let items = [
843
            px(5.0),
844
            CalcAstItem::BraceClose,
845
            CalcAstItem::Add,
846
            px(3.0),
847
        ];
848
        assert_eq!(eval(&items, 0.0), 8.0);
849

            
850
        // A leading `)` is likewise ignored.
851
        assert_eq!(eval(&[CalcAstItem::BraceClose, px(7.0)], 0.0), 7.0);
852
    }
853

            
854
    #[test]
855
    fn eval_leading_operator_collapses_to_zero() {
856
        // Nothing to apply the operator to -> the whole expression is 0.
857
        assert_eq!(eval(&[CalcAstItem::Mul, px(5.0)], 0.0), 0.0);
858
        assert_eq!(eval(&[CalcAstItem::Add, px(5.0)], 0.0), 0.0);
859
        assert_eq!(eval(&[CalcAstItem::Div, px(5.0)], 0.0), 0.0);
860
    }
861

            
862
    #[test]
863
    fn eval_trailing_operator_keeps_the_left_hand_side() {
864
        for op in [
865
            CalcAstItem::Add,
866
            CalcAstItem::Sub,
867
            CalcAstItem::Mul,
868
            CalcAstItem::Div,
869
        ] {
870
            assert_eq!(eval(&[px(10.0), op], 0.0), 10.0, "trailing {op:?} dropped the lhs");
871
        }
872
    }
873

            
874
    #[test]
875
    fn eval_operator_only_ast_is_zero() {
876
        let items = [
877
            CalcAstItem::Add,
878
            CalcAstItem::Sub,
879
            CalcAstItem::Mul,
880
            CalcAstItem::Div,
881
        ];
882
        assert_eq!(eval(&items, 500.0), 0.0);
883
    }
884

            
885
    #[test]
886
    fn eval_adjacent_values_without_an_operator_keep_only_the_first() {
887
        // `10px 20px` is not valid calc(); the evaluator silently drops the tail
888
        // rather than panicking. Pinning the current behaviour.
889
        assert_eq!(eval(&[px(10.0), px(20.0)], 0.0), 10.0);
890
    }
891

            
892
    #[test]
893
    fn eval_adjacent_operators_do_not_panic() {
894
        // `1px + * 2px` — the orphaned `*` survives pass 1 and is skipped in pass 2.
895
        let items = [
896
            px(1.0),
897
            CalcAstItem::Add,
898
            CalcAstItem::Mul,
899
            px(2.0),
900
        ];
901
        assert_eq!(eval(&items, 0.0), 1.0);
902
    }
903

            
904
    #[test]
905
    fn eval_deeply_nested_braces_do_not_overflow_the_stack() {
906
        // Brace handling recurses once per nesting level. 256 levels is far past
907
        // anything a real stylesheet produces and must still return cleanly.
908
        const DEPTH: usize = 256;
909
        let mut items: Vec<CalcAstItem> = vec![CalcAstItem::BraceOpen; DEPTH];
910
        items.push(px(1.0));
911
        items.resize(DEPTH * 2 + 1, CalcAstItem::BraceClose);
912
        assert_eq!(eval(&items, 0.0), 1.0);
913
    }
914

            
915
    #[test]
916
    fn eval_unbalanced_deep_braces_terminate() {
917
        // 256 `(` with no `)` at all: every level recurses on a strictly shorter
918
        // slice, so this must terminate rather than loop forever.
919
        let items = vec![CalcAstItem::BraceOpen; 256];
920
        assert_eq!(eval(&items, 0.0), 0.0);
921
    }
922

            
923
    #[test]
924
    fn eval_long_flat_ast_terminates_with_the_right_sum() {
925
        // 1000 terms of `1px + 1px + ...` — guards against a quadratic blow-up
926
        // or a non-advancing index in the two passes.
927
        const TERMS: usize = 1000;
928
        let mut items = Vec::with_capacity(TERMS * 2 - 1);
929
        for i in 0..TERMS {
930
            if i > 0 {
931
                items.push(CalcAstItem::Add);
932
            }
933
            items.push(px(1.0));
934
        }
935
        assert_eq!(eval(&items, 0.0), TERMS as f32);
936
    }
937

            
938
    // ==================================================================
939
    // evaluate_calc_ast — numeric edge cases
940
    // ==================================================================
941

            
942
    #[test]
943
    fn eval_division_by_zero_is_guarded_to_zero_not_infinity() {
944
        let items = [px(10.0), CalcAstItem::Div, px(0.0)];
945
        let got = eval(&items, 0.0);
946
        assert_eq!(got, 0.0);
947
        assert!(got.is_finite(), "10px / 0px must not be an infinity");
948
    }
949

            
950
    #[test]
951
    fn eval_division_by_a_zero_valued_percent_is_also_guarded() {
952
        // The divisor is only known after the basis is applied: 100% of a 0 basis.
953
        let items = [px(10.0), CalcAstItem::Div, val(SizeMetric::Percent, 100.0)];
954
        assert_eq!(eval(&items, 0.0), 0.0);
955

            
956
        // Negative zero compares equal to 0.0, so it takes the same guard and
957
        // yields +0.0 rather than -inf.
958
        let got = eval(&items, -0.0);
959
        assert_eq!(got, 0.0);
960
        assert!(got.is_finite(), "10px / (100% of -0) must not be -inf");
961
    }
962

            
963
    #[test]
964
    fn eval_division_by_nan_is_not_caught_by_the_zero_guard() {
965
        // The guard is `rhs == 0.0`, which NaN fails — so the NaN propagates.
966
        // Defined, not a panic, but worth pinning: NaN divisors are NOT sanitised.
967
        let items = [px(10.0), CalcAstItem::Div, val(SizeMetric::Percent, 100.0)];
968
        assert!(eval(&items, f32::NAN).is_nan());
969
    }
970

            
971
    #[test]
972
    fn eval_divisor_below_fixed_point_precision_truncates_into_the_zero_guard() {
973
        // `FloatValue` keeps 3 decimal places, so 0.0001 encodes to *exactly* 0 —
974
        // a divisor that is non-zero in the stylesheet but zero by the time the
975
        // evaluator sees it. `calc(10px / 0.0001)` is therefore 0px, not 100000px.
976
        assert_eq!(PixelValue::px(0.0001).number.get(), 0.0);
977
        let items = [px(10.0), CalcAstItem::Div, px(0.0001)];
978
        assert_eq!(eval(&items, 0.0), 0.0);
979

            
980
        // 0.001 is the smallest divisor that survives the encoding.
981
        assert_close(PixelValue::px(0.001).number.get(), 0.001);
982
        let items = [px(10.0), CalcAstItem::Div, px(0.001)];
983
        let got = eval(&items, 0.0);
984
        assert!(got.is_finite(), "10px / 0.001px should be finite, got {got}");
985
        assert_close(got, 10_000.0);
986
    }
987

            
988
    #[test]
989
    fn eval_multiplication_overflow_saturates_to_infinity_without_panicking() {
990
        // Chain enough saturated values that the product overflows f32 on any
991
        // isize width (64-bit *and* 32-bit/wasm).
992
        let mut items = vec![px(f32::MAX)];
993
        for _ in 0..7 {
994
            items.push(CalcAstItem::Mul);
995
            items.push(px(f32::MAX));
996
        }
997
        let got = eval(&items, 0.0);
998
        assert!(
999
            got.is_infinite() && got.is_sign_positive(),
            "expected +inf on overflow, got {got}",
        );
    }
    #[test]
    fn eval_inf_minus_inf_is_nan_not_a_panic() {
        // An infinite basis makes both percentages infinite; inf - inf == NaN.
        let items = [
            val(SizeMetric::Percent, 100.0),
            CalcAstItem::Sub,
            val(SizeMetric::Percent, 100.0),
        ];
        assert!(eval(&items, f32::INFINITY).is_nan());
    }
    #[test]
    fn eval_nan_basis_does_not_leak_into_an_absolute_expression() {
        let items = [px(10.0), CalcAstItem::Add, px(5.0)];
        assert_eq!(eval(&items, f32::NAN), 15.0);
        assert_eq!(eval(&items, f32::INFINITY), 15.0);
    }
    #[test]
    fn eval_negative_results_are_not_clamped() {
        // calc(20px - 100%) with a 500px basis is legitimately negative here;
        // clamping is the caller's job, not the evaluator's.
        let items = [px(20.0), CalcAstItem::Sub, val(SizeMetric::Percent, 100.0)];
        assert_eq!(eval(&items, 500.0), -480.0);
    }
    #[test]
    fn eval_viewport_units_inside_calc_use_the_no_viewport_fallback() {
        // evaluate_calc_ast routes through `resolve_pixel_value`, which has no
        // viewport context — so `50vw` inside calc() degrades to `50px`.
        let items = [val(SizeMetric::Vw, 50.0), CalcAstItem::Add, px(0.0)];
        assert_eq!(eval(&items, 1000.0), 50.0);
    }
    // ==================================================================
    // evaluate_calc (public wrapper)
    // ==================================================================
    #[test]
    fn evaluate_calc_empty_context_is_zero() {
        let c = ctx(Vec::new(), 16.0, 10.0);
        assert_eq!(evaluate_calc(&c, 500.0), 0.0);
        assert_eq!(evaluate_calc(&c, f32::NAN), 0.0);
    }
    #[test]
    fn evaluate_calc_matches_the_ast_evaluator_it_wraps() {
        let items = vec![
            val(SizeMetric::Percent, 100.0),
            CalcAstItem::Sub,
            CalcAstItem::BraceOpen,
            val(SizeMetric::Em, 2.0),
            CalcAstItem::Add,
            val(SizeMetric::Rem, 1.0),
            CalcAstItem::BraceClose,
        ];
        let c = ctx(items.clone(), 12.0, 20.0);
        // 100% of 500 - (2*12 + 1*20) == 500 - 44 == 456
        assert_eq!(evaluate_calc(&c, 500.0), 456.0);
        assert_eq!(evaluate_calc(&c, 500.0), evaluate_calc_ast(&items, 500.0, 12.0, 20.0));
    }
    #[test]
    fn evaluate_calc_forwards_its_own_em_and_rem_sizes_without_swapping_them() {
        // `2em + 1rem` is asymmetric on purpose: swapping em/rem must change the
        // answer, which a transposed-argument bug in the wrapper would not.
        let items = vec![val(SizeMetric::Em, 2.0), CalcAstItem::Add, val(SizeMetric::Rem, 1.0)];
        assert_eq!(evaluate_calc(&ctx(items.clone(), 10.0, 20.0), 0.0), 40.0);
        assert_eq!(evaluate_calc(&ctx(items, 20.0, 10.0), 0.0), 50.0);
    }
    #[test]
    fn evaluate_calc_with_hostile_font_sizes_does_not_panic() {
        let items = vec![val(SizeMetric::Em, 2.0), CalcAstItem::Add, px(1.0)];
        assert!(evaluate_calc(&ctx(items.clone(), f32::NAN, 10.0), 0.0).is_nan());
        let inf = evaluate_calc(&ctx(items.clone(), f32::INFINITY, 10.0), 0.0);
        assert!(inf.is_infinite() && inf.is_sign_positive());
        // A zero font-size is legal CSS and must give a plain 1px, not NaN.
        assert_eq!(evaluate_calc(&ctx(items, 0.0, 10.0), 0.0), 1.0);
    }
    #[test]
    fn evaluate_calc_is_pure_and_repeatable() {
        // The context is borrowed, not consumed: evaluating twice with different
        // bases must not mutate or corrupt the AST.
        let c = ctx(
            vec![val(SizeMetric::Percent, 50.0), CalcAstItem::Add, px(10.0)],
            16.0,
            10.0,
        );
        assert_eq!(evaluate_calc(&c, 100.0), 60.0);
        assert_eq!(evaluate_calc(&c, 200.0), 110.0);
        assert_eq!(evaluate_calc(&c, 100.0), 60.0);
    }
}