Skip to main content

kui_core/
calc.rs

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