Skip to main content

kui_core/
calc.rs

1//! Size expressions: CSS's `min()`, `max()` and `clamp()` over lengths
2//! and percentages, resolved by layout against the parent's content box
3//! (the same box a `Percent` sizing takes its cut of).
4//!
5//! ```text
6//! size   := number ["px"] | number "%" | fn "(" size ("," size)* ")"
7//! fn     := "min" | "max" | "clamp"         -- clamp takes exactly three
8//! number := digits ["." digits] | "." digits  -- no sign, no exponent
9//! ```
10//!
11//! As CSS reads it: a unit straight after its number (`80 %` is refused),
12//! a function's `(` straight after its name, names and `px` in any case,
13//! and whitespace free around the commas and inside the parentheses.
14//! `"clamp(400px, 80%, 1000px)"` is 80% of the room, never under 400 nor
15//! over 1000, and, as CSS has it, the minimum wins when it is over the
16//! maximum. An expression with no percentage in it is a length
17//! (`"min(300px, 400)"` is `Fixed(300)`), a bare percentage is a
18//! `Percent`, and only what depends on the room becomes a [`Calc`].
19//!
20//! ```rust
21//! use kui_core::{NodeSpec, Sizing, calc};
22//!
23//! let w = calc::sizing("clamp(400px, 80%, 1000px)").unwrap();
24//! assert!(matches!(w, Sizing::Calc(_)));
25//! assert_eq!(calc::sizing("min(300px, 400)").unwrap(), Sizing::Fixed(300.0));
26//!
27//! let pane = NodeSpec::column().width(w).max_width(calc::bound("50%").unwrap());
28//! if let Sizing::Calc(c) = pane.layout.width {
29//!     assert_eq!(c.resolve(1500.0), 1000.0); // 80% of 1500 is capped
30//!     assert_eq!(c.resolve(200.0), 400.0);   // and floored
31//! }
32//! ```
33//!
34//! The same expression as data, for a binding that would rather not spell
35//! it ([`from_value`]): a number is px, `{ pct = N }` (or `{ percent: N }`
36//! in JS) a percentage, `{ px = N }` a length, and a function a one-key
37//! table of its arguments. Rust builds one with [`Expr`] and [`intern`].
38//!
39//! A [`Calc`] is a handle. `LayoutSpec` is `Copy` and copied per node per
40//! frame, so the tree it names lives in a process-wide table, one entry
41//! per distinct expression, which a frame that declares the same
42//! expression again finds rather than adds to. The table holds at most
43//! [`MAX_CALCS`] entries and never lets one go: a program that reaches the
44//! cap is spelling a new expression per frame (`format!("clamp({n}px, ..)")`
45//! fed a drag). Past it a new expression is refused with [`FULL`] in its
46//! error, which every binding reads as the prop left undeclared, and a
47//! [`crate::diag::SIZE_EXPRESSIONS_FULL`] warning says so. An expression
48//! already kept still resolves.
49
50use std::collections::HashMap;
51use std::fmt;
52use std::sync::atomic::{AtomicU64, Ordering};
53use std::sync::{Arc, Mutex, OnceLock, RwLock};
54
55/// How many distinct expressions the table keeps.
56pub const MAX_CALCS: usize = 1 << 16;
57
58/// What the error of an expression refused for want of room in the table
59/// starts with, under whatever a binding put before it: see [`is_full`].
60pub const FULL: &str = "too many distinct size expressions";
61
62/// Whether `err` is a refusal for want of room ([`FULL`]), not a bad
63/// spelling: a binding leaves the prop at its default on one — the
64/// expression was fine, the process has spelled too many — and fails on
65/// the other.
66pub fn is_full(err: &str) -> bool {
67    err.contains(FULL)
68}
69
70/// How many expressions the full table refused, over the process, and
71/// the last one's spelling: what the warning names.
72static REFUSED: AtomicU64 = AtomicU64::new(0);
73static LAST_REFUSED: Mutex<String> = Mutex::new(String::new());
74
75/// How many new expressions the table has refused since the process
76/// started, with the last one spelled.
77pub fn refused() -> (u64, String) {
78    let n = REFUSED.load(Ordering::Relaxed);
79    if n == 0 {
80        // Every drain of every core asks; the lock is for the rare answer.
81        return (0, String::new());
82    }
83    let last = LAST_REFUSED.lock().map(|s| s.clone()).unwrap_or_default();
84    (n, last)
85}
86
87/// How deep an expression nests: at most this many functions inside one
88/// another, whatever built it — the grammar, data, prefix code, C's
89/// builders or a Rust tree handed to [`intern`]. Every walk of a tree
90/// (evaluating, hashing, comparing, spelling, dropping) recurses, so a
91/// tree the table keeps is one those walks can finish.
92pub const MAX_DEPTH: u32 = 32;
93
94fn too_deep(what: &str) -> String {
95    format!("bad size{what}: nested past {MAX_DEPTH}")
96}
97
98/// A number an expression may hold: finite, and `-0` as `0`. The table
99/// finds an entry by its numbers' bits, and `NaN` is equal to nothing —
100/// not even itself — so `{ min = { 0/0, { pct = 50 } } }` declared each
101/// frame was a new entry each frame, towards the cap every view shares;
102/// and `-0` and `0` were two entries for one expression.
103/// Neither infinity means anything a room can be cut to either.
104fn finite(v: f32) -> Result<f32, String> {
105    if v.is_finite() {
106        // `-0.0 == 0.0`, so this is `0.0` for either.
107        Ok(if v == 0.0 { 0.0 } else { v })
108    } else {
109        Err(format!("bad size: {v} is not a finite number"))
110    }
111}
112
113/// Holds a tree to what the table keeps — nested no deeper than
114/// [`MAX_DEPTH`], its numbers [`finite`] and `-0` made `0` — at depth
115/// `depth`: what [`intern`] and [`norm`] make of a tree built by hand (C's
116/// builders, a Rust `Expr`), stopping at the first level past the cap
117/// rather than walking the rest.
118fn canon(e: &mut Expr, depth: u32) -> Result<(), String> {
119    if depth > MAX_DEPTH {
120        return Err(too_deep(""));
121    }
122    match e {
123        Expr::Px(v) | Expr::Pct(v) => {
124            *v = finite(*v)?;
125            Ok(())
126        }
127        Expr::Min(xs) | Expr::Max(xs) => xs.iter_mut().try_for_each(|x| canon(x, depth + 1)),
128        Expr::Clamp(a, b, c) => {
129            canon(a, depth + 1)?;
130            canon(b, depth + 1)?;
131            canon(c, depth + 1)
132        }
133    }
134}
135
136/// A parsed expression: lengths in logical px, percentages as fractions.
137#[derive(Clone, Debug, PartialEq)]
138pub enum Expr {
139    Px(f32),
140    /// A fraction of the room (`"50%"` is `Pct(0.5)`).
141    Pct(f32),
142    Min(Vec<Expr>),
143    Max(Vec<Expr>),
144    /// `clamp(min, target, max)`.
145    Clamp(Box<Expr>, Box<Expr>, Box<Expr>),
146}
147
148impl Expr {
149    /// The expression in logical px of `room` px, never below zero.
150    pub fn resolve(&self, room: f32) -> f32 {
151        self.eval(room).max(0.0)
152    }
153
154    fn eval(&self, room: f32) -> f32 {
155        match self {
156            Expr::Px(px) => *px,
157            Expr::Pct(f) => room * f,
158            Expr::Min(xs) => xs
159                .iter()
160                .map(|x| x.eval(room))
161                .fold(f32::INFINITY, f32::min),
162            Expr::Max(xs) => xs
163                .iter()
164                .map(|x| x.eval(room))
165                .fold(f32::NEG_INFINITY, f32::max),
166            // CSS's order: the minimum over the maximum.
167            Expr::Clamp(lo, target, hi) => target.eval(room).min(hi.eval(room)).max(lo.eval(room)),
168        }
169    }
170
171    /// Whether the value depends on the room: a percentage anywhere in it.
172    pub fn relative(&self) -> bool {
173        match self {
174            Expr::Px(_) => false,
175            Expr::Pct(_) => true,
176            Expr::Min(xs) | Expr::Max(xs) => xs.iter().any(Expr::relative),
177            Expr::Clamp(a, b, c) => a.relative() || b.relative() || c.relative(),
178        }
179    }
180}
181
182/// Structural equality and hashing by the numbers' bits: what the table
183/// finds an entry by, so data and prefix code find theirs without being
184/// spelled out first.
185impl Eq for Expr {}
186
187impl std::hash::Hash for Expr {
188    fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
189        match self {
190            Expr::Px(v) => (0u8, v.to_bits()).hash(h),
191            Expr::Pct(v) => (1u8, v.to_bits()).hash(h),
192            Expr::Min(xs) => (2u8, xs).hash(h),
193            Expr::Max(xs) => (3u8, xs).hash(h),
194            Expr::Clamp(a, b, c) => (4u8, a, b, c).hash(h),
195        }
196    }
197}
198
199impl fmt::Display for Expr {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        fn list(f: &mut fmt::Formatter<'_>, name: &str, xs: &[&Expr]) -> fmt::Result {
202            write!(f, "{name}(")?;
203            for (i, x) in xs.iter().enumerate() {
204                if i > 0 {
205                    write!(f, ", ")?;
206                }
207                write!(f, "{x}")?;
208            }
209            write!(f, ")")
210        }
211        match self {
212            Expr::Px(px) => write!(f, "{px}px"),
213            Expr::Pct(p) => write!(f, "{}%", p * 100.0),
214            Expr::Min(xs) => list(f, "min", &xs.iter().collect::<Vec<_>>()),
215            Expr::Max(xs) => list(f, "max", &xs.iter().collect::<Vec<_>>()),
216            Expr::Clamp(a, b, c) => list(f, "clamp", &[a, b, c]),
217        }
218    }
219}
220
221/// An expression that depends on the room, by its place in the table.
222/// `Copy`, so a `Sizing` holding one still is.
223#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
224pub struct Calc(u32);
225
226impl Calc {
227    /// The expression in logical px of `room` px — evaluated under the
228    /// table's read lock, since layout asks once per node that has one.
229    pub fn resolve(self, room: f32) -> f32 {
230        table()
231            .read()
232            .ok()
233            .and_then(|t| t.exprs.get(self.0 as usize).map(|e| e.resolve(room)))
234            .unwrap_or(0.0)
235    }
236
237    /// The tree the handle names.
238    pub fn expr(self) -> Option<Arc<Expr>> {
239        table().read().ok()?.exprs.get(self.0 as usize).cloned()
240    }
241
242    /// The handle's number: what a binding carries across (the C ABI's
243    /// `KUI_CALC` sizing holds it in its `value`).
244    pub fn id(self) -> u32 {
245        self.0
246    }
247
248    /// A handle by number, when the table has one there.
249    pub fn from_id(id: u32) -> Option<Calc> {
250        let t = table().read().ok()?;
251        ((id as usize) < t.exprs.len()).then_some(Calc(id))
252    }
253
254    /// The canonical spelling (`clamp(400px, 80%, 1000px)`), for a reader.
255    pub fn describe(self) -> String {
256        self.expr()
257            .map_or_else(|| "calc(?)".into(), |e| e.to_string())
258    }
259}
260
261#[derive(Default)]
262struct Table {
263    exprs: Vec<Arc<Expr>>,
264    by_expr: HashMap<Expr, u32>,
265    /// What a spelling came to, by the text a binding handed in — so the
266    /// view that declares `"clamp(400px, 80%, 1000px)"` every frame parses
267    /// it once. Bounded with the rest ([`MAX_CALCS`]); past it, a spelling
268    /// is parsed each time and not kept.
269    by_input: HashMap<String, Norm>,
270    /// The same for prefix code, by its slots' bytes: a Node view that
271    /// declares `{ clamp: [...] }` every frame builds the tree once.
272    by_code: HashMap<Box<[u8]>, Norm>,
273}
274
275/// An expression reduced to what it needs to be.
276#[derive(Clone, Copy, Debug, PartialEq)]
277enum Norm {
278    Px(f32),
279    Pct(f32),
280    Calc(Calc),
281}
282
283fn norm(mut e: Expr) -> Result<Norm, String> {
284    // Before `relative` and `resolve` walk it: a tree from C's builders
285    // or a Rust caller has had no cap on the way in.
286    canon(&mut e, 0)?;
287    Ok(match e {
288        Expr::Pct(f) => Norm::Pct(f),
289        e if !e.relative() => Norm::Px(e.resolve(0.0)),
290        e => Norm::Calc(intern_checked(e)?),
291    })
292}
293
294/// A spelling reduced, through the input cache.
295fn norm_str(s: &str) -> Result<Norm, String> {
296    if let Some(n) = table().read().ok().and_then(|t| t.by_input.get(s).copied()) {
297        return Ok(n);
298    }
299    let n = norm(parse(s)?)?;
300    if let Ok(mut t) = table().write()
301        && t.by_input.len() < MAX_CALCS
302    {
303        t.by_input.insert(s.to_string(), n);
304    }
305    Ok(n)
306}
307
308fn norm_sizing(n: Norm) -> crate::spec::Sizing {
309    use crate::spec::Sizing;
310    match n {
311        Norm::Px(px) => Sizing::Fixed(px),
312        Norm::Pct(f) => Sizing::Percent(f),
313        Norm::Calc(c) => Sizing::Calc(c),
314    }
315}
316
317fn norm_bound(n: Norm) -> Result<crate::spec::Bound, String> {
318    use crate::spec::Bound;
319    Ok(match n {
320        Norm::Px(px) => Bound::Px(px),
321        // A percentage clamp needs the room as much as a calc does.
322        Norm::Pct(f) => Bound::Calc(intern(Expr::Pct(f))?),
323        Norm::Calc(c) => Bound::Calc(c),
324    })
325}
326
327/// A size expression as data (see the module's doc): a number, a
328/// string, `{ pct }` / `{ percent }` / `{ px }`, or a one-key
329/// `{ min | max | clamp = [args] }`.
330pub fn from_value(v: &crate::value::Value) -> Result<Expr, String> {
331    value_at(v, 0)
332}
333
334fn value_at(v: &crate::value::Value, depth: u32) -> Result<Expr, String> {
335    use crate::value::Value;
336    if depth > MAX_DEPTH {
337        return Err(too_deep(""));
338    }
339    match v {
340        Value::Int(_) | Value::Float(_) => {
341            Ok(Expr::Px(finite(v.as_float().unwrap_or(0.0) as f32)?))
342        }
343        Value::Str(s) => parse(s),
344        Value::Map(m) => {
345            let mut it = m.iter();
346            let (Some((k, arg)), None) = (it.next(), it.next()) else {
347                return Err(
348                    "bad size: a table names one of pct, percent, px, min, max, clamp".into(),
349                );
350            };
351            let num = || {
352                arg.as_float()
353                    .map(|n| n as f32)
354                    .ok_or_else(|| format!("bad size: {k} takes a number"))
355                    .and_then(finite)
356            };
357            let args = || -> Result<Vec<Expr>, String> {
358                let Value::List(xs) = arg else {
359                    return Err(format!("bad size: {k} takes a list"));
360                };
361                if xs.is_empty() {
362                    return Err(format!("bad size: {k} takes at least one"));
363                }
364                xs.iter().map(|x| value_at(x, depth + 1)).collect()
365            };
366            match k.as_str() {
367                "pct" | "percent" => Ok(Expr::Pct(finite(num()? / 100.0)?)),
368                "px" => Ok(Expr::Px(num()?)),
369                "min" => Ok(Expr::Min(args()?)),
370                "max" => Ok(Expr::Max(args()?)),
371                "clamp" => match <[Expr; 3]>::try_from(args()?) {
372                    Ok([a, b, c]) => Ok(Expr::Clamp(Box::new(a), Box::new(b), Box::new(c))),
373                    Err(_) => Err("bad size: clamp takes three: clamp(MIN, TARGET, MAX)".into()),
374                },
375                _ => Err(format!(
376                    "bad size: no {k:?} (pct, percent, px, min, max, clamp)"
377                )),
378            }
379        }
380        _ => Err("bad size: a number, a string or a table".into()),
381    }
382}
383
384/// A size expression in prefix code, the form a transport that carries
385/// only numbers sends (the Node wire's `SIZE_MODE_TREE`, v19): `1 px`,
386/// `2 fraction`, `3 n args…` (min), `4 n args…` (max), `5 a b c`
387/// (clamp). The whole slice is one expression.
388pub fn from_code(code: &[f64]) -> Result<Expr, String> {
389    fn one(code: &[f64], at: &mut usize, depth: u32) -> Result<Expr, String> {
390        let mut next = || -> Result<f64, String> {
391            let v = code.get(*at).copied().ok_or("bad size code: truncated")?;
392            *at += 1;
393            Ok(v)
394        };
395        if depth > MAX_DEPTH {
396            return Err(too_deep(" code"));
397        }
398        Ok(match next()? as u32 {
399            1 => Expr::Px(finite(next()? as f32)?),
400            2 => Expr::Pct(finite(next()? as f32)?),
401            op @ (3 | 4) => {
402                let n = next()? as usize;
403                if n == 0 || n > code.len() {
404                    return Err("bad size code: an argument count out of range".into());
405                }
406                let args = (0..n)
407                    .map(|_| one(code, at, depth + 1))
408                    .collect::<Result<Vec<_>, _>>()?;
409                if op == 3 {
410                    Expr::Min(args)
411                } else {
412                    Expr::Max(args)
413                }
414            }
415            5 => {
416                let a = one(code, at, depth + 1)?;
417                let b = one(code, at, depth + 1)?;
418                let c = one(code, at, depth + 1)?;
419                Expr::Clamp(Box::new(a), Box::new(b), Box::new(c))
420            }
421            op => return Err(format!("bad size code: no op {op}")),
422        })
423    }
424    let mut at = 0;
425    let e = one(code, &mut at, 0)?;
426    if at != code.len() {
427        return Err("bad size code: slots left over".into());
428    }
429    Ok(e)
430}
431
432/// Prefix code reduced, through the code cache.
433fn norm_code(code: &[f64]) -> Result<Norm, String> {
434    // SAFETY: an `f64` slice is initialised bytes, and a `u8` view of it
435    // has no alignment to keep; the view lives only for the lookup.
436    let bytes =
437        unsafe { std::slice::from_raw_parts(code.as_ptr().cast::<u8>(), size_of_val(code)) };
438    if let Some(n) = table()
439        .read()
440        .ok()
441        .and_then(|t| t.by_code.get(bytes).copied())
442    {
443        return Ok(n);
444    }
445    let n = norm(from_code(code)?)?;
446    if let Ok(mut t) = table().write()
447        && t.by_code.len() < MAX_CALCS
448    {
449        t.by_code.insert(bytes.into(), n);
450    }
451    Ok(n)
452}
453
454/// A size expression in prefix code, as a sizing.
455pub fn sizing_code(code: &[f64]) -> Result<crate::spec::Sizing, String> {
456    norm_code(code).map(norm_sizing)
457}
458
459/// A size expression in prefix code, as a clamp.
460pub fn bound_code(code: &[f64]) -> Result<crate::spec::Bound, String> {
461    norm_bound(norm_code(code)?)
462}
463
464/// A size expression as data, as a sizing.
465pub fn sizing_value(v: &crate::value::Value) -> Result<crate::spec::Sizing, String> {
466    match v {
467        crate::value::Value::Str(s) => sizing(s),
468        v => Ok(norm_sizing(norm(from_value(v)?)?)),
469    }
470}
471
472/// A size expression as data, as a clamp.
473pub fn bound_value(v: &crate::value::Value) -> Result<crate::spec::Bound, String> {
474    match v {
475        crate::value::Value::Str(s) => bound(s),
476        v => norm_bound(norm(from_value(v)?)?),
477    }
478}
479
480/// An expression tree built by hand, as a sizing: C's builders and a
481/// Rust view that composes one.
482pub fn sizing_of(e: Expr) -> Result<crate::spec::Sizing, String> {
483    Ok(norm_sizing(norm(e)?))
484}
485
486fn table() -> &'static RwLock<Table> {
487    static TABLE: OnceLock<RwLock<Table>> = OnceLock::new();
488    TABLE.get_or_init(Default::default)
489}
490
491/// The handle for `expr`: the one an equal expression already has, or a
492/// new one.
493pub fn intern(mut expr: Expr) -> Result<Calc, String> {
494    canon(&mut expr, 0)?;
495    intern_checked(expr)
496}
497
498/// [`intern`] for a tree [`canon`] has passed.
499fn intern_checked(expr: Expr) -> Result<Calc, String> {
500    if let Some(&id) = table()
501        .read()
502        .map_err(|e| e.to_string())?
503        .by_expr
504        .get(&expr)
505    {
506        return Ok(Calc(id));
507    }
508    let mut t = table().write().map_err(|e| e.to_string())?;
509    if let Some(&id) = t.by_expr.get(&expr) {
510        return Ok(Calc(id));
511    }
512    if t.exprs.len() >= MAX_CALCS {
513        drop(t);
514        return Err(refuse(&expr));
515    }
516    let id = t.exprs.len() as u32;
517    t.exprs.push(Arc::new(expr.clone()));
518    t.by_expr.insert(expr, id);
519    Ok(Calc(id))
520}
521
522/// The table is full: `expr` is counted and named, and refused. Out of
523/// line and cold, so the path every lookup takes stays as it was.
524#[cold]
525#[inline(never)]
526fn refuse(expr: &Expr) -> String {
527    let spelled = expr.to_string();
528    REFUSED.fetch_add(1, Ordering::Relaxed);
529    if let Ok(mut last) = LAST_REFUSED.lock() {
530        last.clone_from(&spelled);
531    }
532    format!(
533        "{FULL} ({MAX_CALCS}): \"{spelled}\" is not kept — declare one per layout, not one per \
534         frame"
535    )
536}
537
538/// Parses a size expression; the public grammar, which a host validating
539/// its own settings reuses so what it accepts is what kui draws.
540pub fn parse(s: &str) -> Result<Expr, String> {
541    let mut p = Parser {
542        s: s.as_bytes(),
543        at: 0,
544    };
545    let e = p.expr(0)?;
546    p.skip_ws();
547    if p.at < p.s.len() {
548        return Err(p.error("the end"));
549    }
550    Ok(e)
551}
552
553/// What a spelling is as a sizing: a length, a percentage, or a
554/// [`Calc`] for anything else.
555pub fn sizing(s: &str) -> Result<crate::spec::Sizing, String> {
556    norm_str(s).map(norm_sizing)
557}
558
559/// What a spelling is as a clamp: a length, or a [`Calc`] for one that
560/// depends on the room.
561pub fn bound(s: &str) -> Result<crate::spec::Bound, String> {
562    norm_bound(norm_str(s)?)
563}
564
565struct Parser<'a> {
566    s: &'a [u8],
567    at: usize,
568}
569
570impl Parser<'_> {
571    fn skip_ws(&mut self) {
572        while self.s.get(self.at).is_some_and(|c| c.is_ascii_whitespace()) {
573            self.at += 1;
574        }
575    }
576
577    fn error(&self, wanted: &str) -> String {
578        let rest = String::from_utf8_lossy(&self.s[self.at.min(self.s.len())..]);
579        if rest.is_empty() {
580            format!("bad size: {wanted} expected at the end")
581        } else {
582            format!("bad size: {wanted} expected at {rest:?}")
583        }
584    }
585
586    fn eat(&mut self, word: &str) -> bool {
587        self.skip_ws();
588        if self.s[self.at..].starts_with(word.as_bytes()) {
589            self.at += word.len();
590            true
591        } else {
592            false
593        }
594    }
595
596    /// One argument, `depth` functions in: past [`MAX_DEPTH`] it is
597    /// refused before it is read, so the recursion is bounded by the cap
598    /// and not by the input.
599    fn expr(&mut self, depth: u32) -> Result<Expr, String> {
600        if depth > MAX_DEPTH {
601            return Err(too_deep(""));
602        }
603        self.skip_ws();
604        for name in ["clamp", "min", "max"] {
605            // As CSS reads a function: its name in any case, and the
606            // parenthesis straight after it — `min (1, 2)` is no call
607            //.
608            let rest = &self.s[self.at..];
609            if rest.len() > name.len()
610                && rest[..name.len()].eq_ignore_ascii_case(name.as_bytes())
611                && rest[name.len()] == b'('
612            {
613                self.at += name.len() + 1;
614                let mut args = vec![self.expr(depth + 1)?];
615                while self.eat(",") {
616                    args.push(self.expr(depth + 1)?);
617                }
618                if !self.eat(")") {
619                    return Err(self.error("\",\" or \")\""));
620                }
621                return match name {
622                    "clamp" => match <[Expr; 3]>::try_from(args) {
623                        Ok([a, b, c]) => Ok(Expr::Clamp(Box::new(a), Box::new(b), Box::new(c))),
624                        Err(_) => {
625                            Err("bad size: clamp takes three: clamp(MIN, TARGET, MAX)".into())
626                        }
627                    },
628                    "min" => Ok(Expr::Min(args)),
629                    _ => Ok(Expr::Max(args)),
630                };
631            }
632        }
633        let start = self.at;
634        while self
635            .s
636            .get(self.at)
637            .is_some_and(|c| c.is_ascii_digit() || *c == b'.')
638        {
639            self.at += 1;
640        }
641        let n: f32 = std::str::from_utf8(&self.s[start..self.at])
642            .ok()
643            .and_then(|t| t.parse().ok())
644            .ok_or_else(|| {
645                self.at = start;
646                self.error("a number, \"N%\", \"Npx\", min(…), max(…) or clamp(…)")
647            })?;
648        // Digits alone can still overflow an `f32` (forty of them do):
649        // an infinity is refused as `NaN` is from data.
650        let n = finite(n)?;
651        // The unit straight after the number, as CSS has it: `80 %` and
652        // `100 px` are a number and a stray word.
653        let rest = &self.s[self.at..];
654        if rest.first() == Some(&b'%') {
655            self.at += 1;
656            Ok(Expr::Pct(finite(n / 100.0)?))
657        } else {
658            if rest.len() >= 2 && rest[..2].eq_ignore_ascii_case(b"px") {
659                self.at += 2;
660            }
661            Ok(Expr::Px(n))
662        }
663    }
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use crate::spec::{Bound, Sizing};
670
671    fn px(s: &str, room: f32) -> f32 {
672        parse(s).unwrap().resolve(room)
673    }
674
675    #[test]
676    fn expressions_resolve_against_the_room() {
677        let c = "clamp(400px, 80%, 1000px)";
678        assert_eq!(px(c, 300.0), 400.0, "the minimum");
679        assert_eq!(px(c, 1000.0), 800.0, "the target");
680        assert_eq!(px(c, 2000.0), 1000.0, "the maximum");
681        assert_eq!(
682            px("clamp(500, 10%, 200)", 1000.0),
683            500.0,
684            "the minimum over the maximum"
685        );
686        assert_eq!(px("min(720px, 100%)", 500.0), 500.0);
687        assert_eq!(px("max(50%, 300)", 400.0), 300.0);
688        assert_eq!(px("min(clamp(1, 50%, 900), 30%)", 1000.0), 300.0, "nested");
689    }
690
691    #[test]
692    fn only_what_depends_on_the_room_is_a_calc() {
693        assert_eq!(sizing("720px").unwrap(), Sizing::Fixed(720.0));
694        assert_eq!(sizing("min(300px, 400)").unwrap(), Sizing::Fixed(300.0));
695        assert_eq!(sizing("50%").unwrap(), Sizing::Percent(0.5));
696        let Sizing::Calc(a) = sizing("clamp(400px,80%,1000px)").unwrap() else {
697            panic!("a calc");
698        };
699        let Sizing::Calc(b) = sizing(" clamp( 400 , 80% , 1000px ) ").unwrap() else {
700            panic!("a calc");
701        };
702        assert_eq!(a, b, "one entry per expression, however spelled");
703        assert_eq!(a.describe(), "clamp(400px, 80%, 1000px)");
704        assert_eq!(bound("300").unwrap(), Bound::Px(300.0));
705        assert!(matches!(bound("50%").unwrap(), Bound::Calc(_)));
706    }
707
708    #[test]
709    fn the_same_expression_as_data() {
710        use crate::value::Value;
711        let list = |xs: Vec<Value>| Value::List(xs);
712        let map = |k: &str, v: Value| Value::Map([(k.to_string(), v)].into_iter().collect());
713        let v = map(
714            "clamp",
715            list(vec![
716                Value::Int(400),
717                map("pct", Value::Int(80)),
718                Value::Str("1000px".into()),
719            ]),
720        );
721        let Sizing::Calc(a) = sizing_value(&v).unwrap() else {
722            panic!("a calc");
723        };
724        assert_eq!(
725            Some(a),
726            match sizing("clamp(400px, 80%, 1000px)").unwrap() {
727                Sizing::Calc(c) => Some(c),
728                _ => None,
729            },
730            "one entry, spelled or built"
731        );
732        assert_eq!(
733            sizing_value(&map("percent", Value::Int(50))).unwrap(),
734            Sizing::Percent(0.5)
735        );
736        assert_eq!(
737            sizing_value(&map("min", list(vec![Value::Int(300), Value::Int(400)]))).unwrap(),
738            Sizing::Fixed(300.0)
739        );
740        assert!(
741            sizing_value(&map("clamp", list(vec![Value::Int(1)])))
742                .unwrap_err()
743                .contains("three")
744        );
745        assert!(
746            sizing_value(&map("wide", Value::Int(1)))
747                .unwrap_err()
748                .contains("no \"wide\"")
749        );
750    }
751
752    #[test]
753    fn the_same_expression_in_prefix_code() {
754        let code = [5.0, 1.0, 400.0, 2.0, 0.8, 3.0, 2.0, 1.0, 1000.0, 2.0, 1.0];
755        assert_eq!(
756            from_code(&code).unwrap().to_string(),
757            "clamp(400px, 80%, min(1000px, 100%))"
758        );
759        assert!(from_code(&code[..4]).unwrap_err().contains("truncated"));
760        assert!(
761            from_code(&[1.0, 3.0, 9.0])
762                .unwrap_err()
763                .contains("left over")
764        );
765        assert!(from_code(&[9.0]).unwrap_err().contains("no op 9"));
766    }
767
768    #[test]
769    fn a_bad_one_says_where() {
770        assert_eq!(
771            parse("80%x").unwrap_err(),
772            "bad size: the end expected at \"x\""
773        );
774        // CSS's spacing: the unit and a function's
775        // parenthesis sit against what they belong to.
776        assert_eq!(
777            parse("80 %").unwrap_err(),
778            "bad size: the end expected at \"%\""
779        );
780        assert_eq!(
781            parse("100 px").unwrap_err(),
782            "bad size: the end expected at \"px\""
783        );
784        assert!(
785            parse("min (1, 2)")
786                .unwrap_err()
787                .contains("at \"min (1, 2)\"")
788        );
789        assert!(parse("1.2.3%").is_err());
790        assert!(parse("50px%").unwrap_err().contains("at \"%\""));
791        // And its case: names and units in any.
792        assert_eq!(
793            parse("MIN(10PX, 50%)").unwrap(),
794            parse("min(10px, 50%)").unwrap()
795        );
796        assert_eq!(
797            parse(" max( 1px ,2% ) ").unwrap(),
798            parse("max(1px, 2%)").unwrap()
799        );
800        assert!(parse("clamp(1, 2)").unwrap_err().contains("three"));
801        assert!(parse("wide").unwrap_err().contains("at \"wide\""));
802        assert!(parse("min(1, 2").unwrap_err().contains("at the end"));
803    }
804
805    /// `n` functions nested, spelled, as data and as prefix code.
806    fn nested(n: usize) -> (String, crate::value::Value, Vec<f64>, Expr) {
807        use crate::value::Value;
808        let spelled = format!("{}50%{}", "min(".repeat(n), ")".repeat(n));
809        let mut data = Value::Map(vec![("pct".into(), Value::Int(50))]);
810        let mut code = [3.0, 1.0].repeat(n);
811        code.extend([2.0, 0.5]);
812        let mut built = Expr::Pct(0.5);
813        for _ in 0..n {
814            data = Value::Map(vec![("min".into(), Value::List(vec![data]))]);
815            built = Expr::Min(vec![built]);
816        }
817        (spelled, data, code, built)
818    }
819
820    /// Nesting stops at [`MAX_DEPTH`] whatever builds the tree: the
821    /// parser and data recursed as deep as the input went, and `"min("`
822    /// a hundred thousand times overflowed the stack.
823    #[test]
824    fn nesting_stops_at_the_cap() {
825        let deep = parse(&"min(".repeat(100_000)).unwrap_err();
826        assert_eq!(deep, "bad size: nested past 32");
827        let (s, v, code, e) = nested(MAX_DEPTH as usize);
828        assert!(parse(&s).is_ok(), "32 is allowed");
829        assert!(from_value(&v).is_ok());
830        assert!(from_code(&code).is_ok());
831        assert!(intern(e).is_ok());
832        let (s, v, code, e) = nested(MAX_DEPTH as usize + 1);
833        assert_eq!(parse(&s).unwrap_err(), "bad size: nested past 32");
834        assert_eq!(from_value(&v).unwrap_err(), "bad size: nested past 32");
835        assert_eq!(
836            from_code(&code).unwrap_err(),
837            "bad size code: nested past 32"
838        );
839        assert_eq!(intern(e.clone()).unwrap_err(), "bad size: nested past 32");
840        assert!(sizing_of(e).is_err(), "a tree built by hand is held to it");
841        assert!(sizing(&s).is_err());
842    }
843
844    /// `NaN` equals nothing, so an expression holding one was a new entry
845    /// each time it was declared — a view declaring it every frame filled
846    /// the table — and `-0` was an entry apart from `0`.
847    /// Every way in refuses a number that is not finite and reads `-0`
848    /// as `0`.
849    #[test]
850    fn only_finite_numbers_and_one_zero() {
851        use crate::value::Value;
852        let map = |k: &str, v: Value| Value::Map(vec![(k.to_string(), v)]);
853        let with = |n: f64| {
854            map(
855                "min",
856                Value::List(vec![Value::Float(n), map("pct", Value::Int(50))]),
857            )
858        };
859        for n in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1e300] {
860            let e = sizing_value(&with(n)).unwrap_err();
861            assert!(e.contains("not a finite number"), "{n}: {e}");
862            assert!(sizing_value(&map("px", Value::Float(n))).is_err());
863            assert!(sizing_value(&map("pct", Value::Float(n))).is_err());
864            assert!(from_code(&[3.0, 2.0, 1.0, n, 2.0, 0.5]).is_err());
865            assert!(from_code(&[2.0, n]).is_err());
866        }
867        assert!(bound_value(&with(f64::NAN)).is_err());
868        assert!(intern(Expr::Min(vec![Expr::Px(f32::NAN), Expr::Pct(0.5)])).is_err());
869        assert!(sizing_of(Expr::Pct(f32::INFINITY)).is_err());
870        let digits = format!("min(1{}px, 50%)", "0".repeat(40));
871        assert!(parse(&digits).unwrap_err().contains("not a finite number"));
872        assert!(parse(&format!("1{}%", "0".repeat(40))).is_err());
873
874        let zero = sizing_value(&with(0.0)).unwrap();
875        assert_eq!(sizing_value(&with(-0.0)).unwrap(), zero, "-0 is 0");
876        assert_eq!(
877            sizing_of(Expr::Min(vec![Expr::Px(-0.0), Expr::Pct(0.5)])).unwrap(),
878            zero
879        );
880        assert_eq!(sizing_code(&[3.0, 2.0, 1.0, -0.0, 2.0, 0.5]).unwrap(), zero);
881        assert_eq!(zero.describe(), "min(0px, 50%)");
882    }
883}