Skip to main content

ffcine_core/anim/
expr.rs

1//! The expression language (`docs/compositing.md#expressions`): ffmpeg's function names and
2//! semantics over `f64`, plus easing, `lerp`, `smoothstep`, and stateless `random` and `noise`.
3//!
4//! A source string is tokenised, parsed by precedence climbing into an AST, resolved against a
5//! [`Scope`] (names to variable slots), constant-folded and flattened to stack ops. Evaluating
6//! the ops is a pure function of the [`Frame`]: no state, no loops, so it always terminates.
7
8#![allow(clippy::many_single_char_names)] // maths: x, y, z, a, b
9
10use std::f64::consts::{E, PI};
11
12use super::{Ease, Frame, Scope};
13
14/// Longest accepted source, bytes.
15pub const MAX_LEN: usize = 4096;
16/// Deepest accepted nesting (parentheses, calls, unary operators, the right side of `^`, and
17/// each operator of a chain like `a + b + c`, which nests to the left).
18pub const MAX_DEPTH: usize = 64;
19/// Most arguments a call takes (`min`, `max`).
20pub const MAX_ARGS: usize = 255;
21
22/// The golden ratio, ffmpeg's `PHI`.
23const PHI: f64 = 1.618_033_988_749_895;
24
25/// A compile error: what is wrong and where (1-based column, in characters).
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ExprError {
28    pub col: usize,
29    pub msg: String,
30}
31
32impl ExprError {
33    fn new(col: usize, msg: impl Into<String>) -> Self {
34        Self {
35            col,
36            msg: msg.into(),
37        }
38    }
39}
40
41type Res<T> = std::result::Result<T, ExprError>;
42
43#[derive(Debug, Clone, PartialEq)]
44enum Tok {
45    Num(f64),
46    Name(String),
47    /// Operators and punctuation: `+ - * / % ^ ! < <= > >= == != && || ? : ( ) ,`.
48    Sym(&'static str),
49    End,
50}
51
52const SYMS: [&str; 21] = [
53    "<=", ">=", "==", "!=", "&&", "||", "+", "-", "*", "/", "%", "^", "!", "<", ">", "?", ":", "(",
54    ")", ",", "=",
55];
56
57fn lex(src: &str) -> Res<Vec<(Tok, usize)>> {
58    let chars: Vec<char> = src.chars().collect();
59    let mut out = Vec::new();
60    let mut i = 0;
61    while i < chars.len() {
62        let c = chars[i];
63        let col = i + 1;
64        if c.is_whitespace() {
65            i += 1;
66        } else if c.is_ascii_digit()
67            || (c == '.' && chars.get(i + 1).is_some_and(char::is_ascii_digit))
68        {
69            let start = i;
70            while i < chars.len() && chars[i].is_ascii_digit() {
71                i += 1;
72            }
73            if i < chars.len() && chars[i] == '.' {
74                i += 1;
75                while i < chars.len() && chars[i].is_ascii_digit() {
76                    i += 1;
77                }
78            }
79            if i < chars.len() && matches!(chars[i], 'e' | 'E') {
80                let mut j = i + 1;
81                if j < chars.len() && matches!(chars[j], '+' | '-') {
82                    j += 1;
83                }
84                if j < chars.len() && chars[j].is_ascii_digit() {
85                    i = j;
86                    while i < chars.len() && chars[i].is_ascii_digit() {
87                        i += 1;
88                    }
89                } else {
90                    return Err(ExprError::new(j + 1, "expected exponent digits"));
91                }
92            }
93            let s: String = chars[start..i].iter().collect();
94            let v = s
95                .parse()
96                .map_err(|_| ExprError::new(col, format!("bad number `{s}`")))?;
97            out.push((Tok::Num(v), col));
98        } else if c.is_alphabetic() || c == '_' {
99            let start = i;
100            while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
101                i += 1;
102            }
103            out.push((Tok::Name(chars[start..i].iter().collect()), col));
104        } else {
105            let two: String = chars[i..chars.len().min(i + 2)].iter().collect();
106            let sym = SYMS
107                .iter()
108                .find(|s| two.starts_with(**s))
109                .ok_or_else(|| ExprError::new(col, format!("unexpected `{c}`")))?;
110            if *sym == "=" {
111                return Err(ExprError::new(col, "unexpected `=` (comparison is `==`)"));
112            }
113            i += sym.len();
114            out.push((Tok::Sym(sym), col));
115        }
116    }
117    out.push((Tok::End, chars.len() + 1));
118    Ok(out)
119}
120
121/// The parsed, unresolved tree.
122#[derive(Debug, Clone, PartialEq)]
123pub(crate) enum Node {
124    Num(f64),
125    Var(String, usize),
126    /// An operator (`neg`, `not`, `+`, ...) or a call, with the column of its name.
127    Call(String, usize, Vec<Node>),
128}
129
130impl Node {
131    /// Every variable name in the tree (for ordering `vars`).
132    pub(crate) fn names(&self, out: &mut Vec<String>) {
133        match self {
134            Self::Num(_) => {}
135            Self::Var(n, _) => out.push(n.clone()),
136            Self::Call(_, _, a) => {
137                for a in a {
138                    a.names(out);
139                }
140            }
141        }
142    }
143}
144
145struct Parser {
146    toks: Vec<(Tok, usize)>,
147    i: usize,
148    depth: usize,
149}
150
151/// Binary operators by binding power (higher binds tighter).
152fn binary(t: &Tok) -> Option<(u8, &'static str)> {
153    let Tok::Sym(s) = t else { return None };
154    Some(match *s {
155        "||" => (1, "||"),
156        "&&" => (2, "&&"),
157        "<" | "<=" | ">" | ">=" | "==" | "!=" => (3, s),
158        "+" | "-" => (4, s),
159        "*" | "/" | "%" => (5, s),
160        _ => return None,
161    })
162}
163
164impl Parser {
165    fn peek(&self) -> &Tok {
166        &self.toks[self.i].0
167    }
168
169    fn col(&self) -> usize {
170        self.toks[self.i].1
171    }
172
173    fn next(&mut self) -> (Tok, usize) {
174        let t = self.toks[self.i].clone();
175        if self.i + 1 < self.toks.len() {
176            self.i += 1;
177        }
178        t
179    }
180
181    fn expect(&mut self, s: &'static str) -> Res<()> {
182        if *self.peek() == Tok::Sym(s) {
183            self.next();
184            Ok(())
185        } else {
186            Err(ExprError::new(self.col(), format!("expected \"{s}\"")))
187        }
188    }
189
190    fn enter(&mut self) -> Res<()> {
191        self.depth += 1;
192        if self.depth > MAX_DEPTH {
193            return Err(ExprError::new(
194                self.col(),
195                format!("nested deeper than {MAX_DEPTH} levels"),
196            ));
197        }
198        Ok(())
199    }
200
201    fn expr(&mut self) -> Res<Node> {
202        self.enter()?;
203        let c = self.binary(1)?;
204        let out = if *self.peek() == Tok::Sym("?") {
205            let col = self.col();
206            self.next();
207            let a = self.expr()?;
208            self.expect(":")?;
209            let b = self.expr()?;
210            Node::Call("?:".into(), col, vec![c, a, b])
211        } else {
212            c
213        };
214        self.depth -= 1;
215        Ok(out)
216    }
217
218    fn binary(&mut self, min: u8) -> Res<Node> {
219        let mut lhs = self.unary()?;
220        // Each operator of a chain nests the tree one level deeper on the left.
221        let mut chain = 0;
222        let out = loop {
223            let Some((p, op)) = binary(self.peek()) else {
224                break Ok(lhs);
225            };
226            if p < min {
227                break Ok(lhs);
228            }
229            if let Err(e) = self.enter() {
230                break Err(e);
231            }
232            chain += 1;
233            let col = self.col();
234            self.next();
235            let rhs = match self.binary(p + 1) {
236                Ok(r) => r,
237                Err(e) => break Err(e),
238            };
239            if p == 3 && binary(self.peek()).is_some_and(|(q, _)| q == 3) {
240                break Err(ExprError::new(
241                    self.col(),
242                    "comparisons don't chain: use `&&`",
243                ));
244            }
245            lhs = Node::Call(op.into(), col, vec![lhs, rhs]);
246        };
247        self.depth -= chain;
248        out
249    }
250
251    fn unary(&mut self) -> Res<Node> {
252        let col = self.col();
253        let op = match self.peek() {
254            Tok::Sym("-") => "neg",
255            Tok::Sym("+") => "pos",
256            Tok::Sym("!") => "!",
257            _ => return self.pow(),
258        };
259        self.next();
260        self.enter()?;
261        let a = self.unary()?;
262        self.depth -= 1;
263        Ok(if op == "pos" {
264            a
265        } else {
266            Node::Call(op.into(), col, vec![a])
267        })
268    }
269
270    fn pow(&mut self) -> Res<Node> {
271        let base = self.atom()?;
272        if *self.peek() != Tok::Sym("^") {
273            return Ok(base);
274        }
275        let col = self.col();
276        self.next();
277        self.enter()?;
278        let e = self.unary()?;
279        self.depth -= 1;
280        Ok(Node::Call("^".into(), col, vec![base, e]))
281    }
282
283    fn atom(&mut self) -> Res<Node> {
284        let (t, col) = self.next();
285        match t {
286            Tok::Num(v) => Ok(Node::Num(v)),
287            Tok::Name(n) => {
288                if *self.peek() != Tok::Sym("(") {
289                    return Ok(Node::Var(n, col));
290                }
291                self.next();
292                self.enter()?;
293                let mut args = Vec::new();
294                if *self.peek() != Tok::Sym(")") {
295                    args.push(self.expr()?);
296                    while *self.peek() == Tok::Sym(",") {
297                        if args.len() == MAX_ARGS {
298                            return Err(ExprError::new(
299                                col,
300                                format!("{n}() takes at most {MAX_ARGS} arguments"),
301                            ));
302                        }
303                        self.next();
304                        args.push(self.expr()?);
305                    }
306                }
307                self.expect(")")?;
308                self.depth -= 1;
309                Ok(Node::Call(n, col, args))
310            }
311            Tok::Sym("(") => {
312                self.enter()?;
313                let e = self.expr()?;
314                self.expect(")")?;
315                self.depth -= 1;
316                Ok(e)
317            }
318            Tok::End => Err(ExprError::new(col, "unexpected end")),
319            Tok::Sym(s) => Err(ExprError::new(col, format!("unexpected \"{s}\""))),
320        }
321    }
322}
323
324/// Parses `src` into its tree (no name resolution).
325pub(crate) fn parse(src: &str) -> Res<Node> {
326    if src.len() > MAX_LEN {
327        return Err(ExprError::new(1, format!("longer than {MAX_LEN} bytes")));
328    }
329    let mut p = Parser {
330        toks: lex(src)?,
331        i: 0,
332        depth: 0,
333    };
334    let e = p.expr()?;
335    match p.peek() {
336        Tok::End => Ok(e),
337        _ => Err(ExprError::new(p.col(), "expected an operator or the end")),
338    }
339}
340
341/// An operation on the stack: its arguments are the top `arity` values.
342#[derive(Debug, Clone, Copy, PartialEq)]
343enum F {
344    Neg,
345    Not,
346    Add,
347    Sub,
348    Mul,
349    Div,
350    Mod,
351    Pow,
352    Lt,
353    Le,
354    Gt,
355    Ge,
356    Eq,
357    Ne,
358    And,
359    Or,
360    /// `c, a, b`: `a` if `c` is non-zero, else `b`.
361    Select,
362    Abs,
363    Floor,
364    Ceil,
365    Round,
366    Trunc,
367    Sqrt,
368    Exp,
369    Log,
370    Sin,
371    Cos,
372    Tan,
373    Asin,
374    Acos,
375    Atan,
376    Atan2,
377    Hypot,
378    Sign,
379    Squish,
380    Gauss,
381    Between,
382    Clip,
383    Min,
384    Max,
385    Lerp,
386    Smoothstep,
387    Ease(Ease),
388    Bezier,
389    /// `seed, n`.
390    Random,
391    Noise,
392}
393
394fn b(c: bool) -> f64 {
395    if c { 1.0 } else { 0.0 }
396}
397
398impl F {
399    fn apply(self, a: &[f64]) -> f64 {
400        let x = a.first().copied().unwrap_or(0.0);
401        let y = a.get(1).copied().unwrap_or(0.0);
402        let z = a.get(2).copied().unwrap_or(0.0);
403        match self {
404            Self::Neg => -x,
405            Self::Not => b(x == 0.0),
406            Self::Add => x + y,
407            Self::Sub => x - y,
408            Self::Mul => x * y,
409            Self::Div => x / y,
410            // ffmpeg's `mod`: `x - floor(x / y) * y`.
411            Self::Mod => x - (x / y).floor() * y,
412            Self::Pow => x.powf(y),
413            Self::Lt => b(x < y),
414            Self::Le => b(x <= y),
415            Self::Gt => b(x > y),
416            Self::Ge => b(x >= y),
417            #[allow(clippy::float_cmp)]
418            Self::Eq => b(x == y),
419            #[allow(clippy::float_cmp)]
420            Self::Ne => b(x != y),
421            Self::And => b(x != 0.0 && y != 0.0),
422            Self::Or => b(x != 0.0 || y != 0.0),
423            Self::Select => {
424                if x == 0.0 {
425                    z
426                } else {
427                    y
428                }
429            }
430            Self::Abs => x.abs(),
431            Self::Floor => x.floor(),
432            Self::Ceil => x.ceil(),
433            Self::Round => x.round(),
434            Self::Trunc => x.trunc(),
435            Self::Sqrt => x.sqrt(),
436            Self::Exp => x.exp(),
437            Self::Log => x.ln(),
438            Self::Sin => x.sin(),
439            Self::Cos => x.cos(),
440            Self::Tan => x.tan(),
441            Self::Asin => x.asin(),
442            Self::Acos => x.acos(),
443            Self::Atan => x.atan(),
444            Self::Atan2 => x.atan2(y),
445            Self::Hypot => x.hypot(y),
446            Self::Sign => {
447                if x > 0.0 {
448                    1.0
449                } else if x < 0.0 {
450                    -1.0
451                } else {
452                    0.0
453                }
454            }
455            Self::Squish => 1.0 / (1.0 + (4.0 * x).exp()),
456            Self::Gauss => (-x * x / 2.0).exp() / (2.0 * PI).sqrt(),
457            Self::Between => b(x >= y && x <= z),
458            Self::Clip => {
459                if x.is_nan() || y.is_nan() || z.is_nan() || y > z {
460                    f64::NAN
461                } else {
462                    x.clamp(y, z)
463                }
464            }
465            Self::Min => a.iter().copied().fold(f64::INFINITY, f64::min),
466            Self::Max => a.iter().copied().fold(f64::NEG_INFINITY, f64::max),
467            Self::Lerp => x + (y - x) * z,
468            Self::Smoothstep => {
469                let u = ((z - x) / (y - x)).clamp(0.0, 1.0);
470                u * u * (3.0 - 2.0 * u)
471            }
472            Self::Ease(e) => e.at(x),
473            Self::Bezier => Ease::Bezier([x, y, z, a[3]]).at(a[4]),
474            Self::Random => random(x, y),
475            Self::Noise => noise(x, y),
476        }
477    }
478}
479
480fn splitmix64(mut z: u64) -> u64 {
481    z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
482    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
483    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
484    z ^ (z >> 31)
485}
486
487/// Uniform in [0, 1) from `seed` and an integer `i`.
488#[allow(
489    clippy::cast_possible_truncation,
490    clippy::cast_sign_loss,
491    clippy::cast_precision_loss
492)]
493fn hash01(seed: f64, i: f64) -> f64 {
494    // `+ 0.0` folds -0 into 0, so both seeds give the same stream.
495    let s = splitmix64((seed + 0.0).to_bits());
496    let h = splitmix64(s ^ (i as i64 as u64));
497    (h >> 11) as f64 / (1u64 << 53) as f64
498}
499
500/// `random(seed)` on frame `n`: uniform in [0, 1), the same on every render.
501pub fn random(seed: f64, n: f64) -> f64 {
502    hash01(seed, n.round())
503}
504
505/// `noise(seed, x)`: value noise in [-1, 1], smooth in `x` (lattice values at integers, eased
506/// with the quintic fade between them).
507pub fn noise(seed: f64, x: f64) -> f64 {
508    let i = x.floor();
509    let u = x - i;
510    let f = u * u * u * (u * (u * 6.0 - 15.0) + 10.0);
511    let (a, b) = (
512        hash01(seed, i) * 2.0 - 1.0,
513        hash01(seed, i + 1.0) * 2.0 - 1.0,
514    );
515    a + (b - a) * f
516}
517
518/// A function's op and its accepted argument counts.
519fn function(name: &str) -> Option<(F, usize, usize)> {
520    let many = MAX_ARGS;
521    Some(match name {
522        "lt" => (F::Lt, 2, 2),
523        "lte" => (F::Le, 2, 2),
524        "gt" => (F::Gt, 2, 2),
525        "gte" => (F::Ge, 2, 2),
526        "eq" => (F::Eq, 2, 2),
527        "not" => (F::Not, 1, 1),
528        "between" => (F::Between, 3, 3),
529        "clip" => (F::Clip, 3, 3),
530        "min" => (F::Min, 2, many),
531        "max" => (F::Max, 2, many),
532        "abs" => (F::Abs, 1, 1),
533        "floor" => (F::Floor, 1, 1),
534        "ceil" => (F::Ceil, 1, 1),
535        "round" => (F::Round, 1, 1),
536        "trunc" => (F::Trunc, 1, 1),
537        "mod" => (F::Mod, 2, 2),
538        "sqrt" => (F::Sqrt, 1, 1),
539        "pow" => (F::Pow, 2, 2),
540        "exp" => (F::Exp, 1, 1),
541        "log" => (F::Log, 1, 1),
542        "sin" => (F::Sin, 1, 1),
543        "cos" => (F::Cos, 1, 1),
544        "tan" => (F::Tan, 1, 1),
545        "asin" => (F::Asin, 1, 1),
546        "acos" => (F::Acos, 1, 1),
547        "atan" => (F::Atan, 1, 1),
548        "atan2" => (F::Atan2, 2, 2),
549        "hypot" => (F::Hypot, 2, 2),
550        "sign" | "sgn" => (F::Sign, 1, 1),
551        "squish" => (F::Squish, 1, 1),
552        "gauss" => (F::Gauss, 1, 1),
553        "lerp" => (F::Lerp, 3, 3),
554        "smoothstep" => (F::Smoothstep, 3, 3),
555        "ease" => (F::Ease(Ease::Ease), 1, 1),
556        "ease_in" => (F::Ease(Ease::In), 1, 1),
557        "ease_out" => (F::Ease(Ease::Out), 1, 1),
558        "ease_in_out" => (F::Ease(Ease::InOut), 1, 1),
559        "back" => (F::Ease(Ease::Back), 1, 1),
560        "bezier" => (F::Bezier, 5, 5),
561        "noise" => (F::Noise, 2, 2),
562        _ => return None,
563    })
564}
565
566/// Names an expression can't use for anything else (`vars` may not shadow them).
567pub fn is_builtin_name(name: &str) -> bool {
568    matches!(name, "PI" | "E" | "PHI" | "if" | "ifnot" | "random")
569        || function(name).is_some()
570        || super::Builtin::from_name(name).is_some()
571}
572
573/// The resolved, folded tree.
574#[derive(Debug, Clone, PartialEq)]
575enum Ir {
576    Num(f64),
577    Slot(u16),
578    Op(F, Vec<Ir>),
579}
580
581#[allow(clippy::too_many_lines)]
582fn resolve(n: &Node, scope: &Scope) -> Res<Ir> {
583    let ir = match n {
584        Node::Num(v) => Ir::Num(*v),
585        Node::Var(name, col) => match name.as_str() {
586            "PI" => Ir::Num(PI),
587            "E" => Ir::Num(E),
588            "PHI" => Ir::Num(PHI),
589            _ => Ir::Slot(
590                scope
591                    .slot(name)
592                    .ok_or_else(|| ExprError::new(*col, scope.unknown(name)))?,
593            ),
594        },
595        Node::Call(name, col, args) => {
596            let a = args
597                .iter()
598                .map(|a| resolve(a, scope))
599                .collect::<Res<Vec<_>>>()?;
600            let count = |lo: usize, hi: usize| {
601                if a.len() < lo || a.len() > hi {
602                    let want = match (lo, hi) {
603                        (l, h) if l == h => format!("{l}"),
604                        (l, MAX_ARGS) => format!("{l} or more"),
605                        (l, h) => format!("{l} or {h}"),
606                    };
607                    Err(ExprError::new(
608                        *col,
609                        format!("{name}() takes {want} arguments, not {}", a.len()),
610                    ))
611                } else {
612                    Ok(())
613                }
614            };
615            let op = match name.as_str() {
616                "neg" => F::Neg,
617                "!" => F::Not,
618                "+" => F::Add,
619                "-" => F::Sub,
620                "*" => F::Mul,
621                "/" => F::Div,
622                "%" => F::Mod,
623                "^" => F::Pow,
624                "<" => F::Lt,
625                "<=" => F::Le,
626                ">" => F::Gt,
627                ">=" => F::Ge,
628                "==" => F::Eq,
629                "!=" => F::Ne,
630                "&&" => F::And,
631                "||" => F::Or,
632                "?:" => F::Select,
633                "if" | "ifnot" => {
634                    count(2, 3)?;
635                    let mut a = a.into_iter();
636                    let (c, x) = (a.next(), a.next());
637                    let y = a.next().unwrap_or(Ir::Num(0.0));
638                    let (c, x) = (c.unwrap_or(Ir::Num(0.0)), x.unwrap_or(Ir::Num(0.0)));
639                    let args = if name == "if" {
640                        vec![c, x, y]
641                    } else {
642                        vec![c, y, x]
643                    };
644                    return Ok(fold(F::Select, args));
645                }
646                "random" => {
647                    count(1, 1)?;
648                    let n = scope.slot("n").ok_or_else(|| {
649                        ExprError::new(
650                            *col,
651                            "random() needs the frame index `n`, which isn't known here",
652                        )
653                    })?;
654                    let mut a = a;
655                    a.push(Ir::Slot(n));
656                    return Ok(Ir::Op(F::Random, a));
657                }
658                _ => {
659                    let (f, lo, hi) = function(name).ok_or_else(|| {
660                        ExprError::new(*col, format!("unknown function `{name}`"))
661                    })?;
662                    count(lo, hi)?;
663                    f
664                }
665            };
666            fold(op, a)
667        }
668    };
669    Ok(ir)
670}
671
672fn fold(f: F, args: Vec<Ir>) -> Ir {
673    let consts: Option<Vec<f64>> = args
674        .iter()
675        .map(|a| match a {
676            Ir::Num(v) => Some(*v),
677            _ => None,
678        })
679        .collect();
680    match consts {
681        Some(c) => Ir::Num(f.apply(&c)),
682        None => Ir::Op(f, args),
683    }
684}
685
686#[derive(Debug, Clone, Copy, PartialEq)]
687enum Op {
688    Push(f64),
689    Load(u16),
690    Apply(F, u8),
691}
692
693/// A compiled expression: evaluates in about a hundred nanoseconds.
694#[derive(Debug, Clone, PartialEq)]
695pub struct Compiled {
696    src: String,
697    ops: Vec<Op>,
698    depth: usize,
699}
700
701fn emit(ir: &Ir, ops: &mut Vec<Op>, sp: &mut usize, max: &mut usize) {
702    match ir {
703        Ir::Num(v) => {
704            ops.push(Op::Push(*v));
705            *sp += 1;
706        }
707        Ir::Slot(s) => {
708            ops.push(Op::Load(*s));
709            *sp += 1;
710        }
711        Ir::Op(f, a) => {
712            for x in a {
713                emit(x, ops, sp, max);
714            }
715            // The parser caps calls at `MAX_ARGS` (255) arguments.
716            let n = u8::try_from(a.len()).expect("at most MAX_ARGS arguments");
717            ops.push(Op::Apply(*f, n));
718            *sp = *sp + 1 - a.len();
719        }
720    }
721    *max = (*max).max(*sp);
722}
723
724impl Compiled {
725    /// Compiles `src` against `scope`. An expression that folds to a non-finite constant
726    /// (`1/0`, `log(-1)`) is an error.
727    pub fn new(src: &str, scope: &Scope) -> Res<Self> {
728        let ir = resolve(&parse(src)?, scope)?;
729        if let Ir::Num(v) = ir
730            && !v.is_finite()
731        {
732            return Err(ExprError::new(1, format!("always {v}: not finite")));
733        }
734        let (mut ops, mut sp, mut depth) = (Vec::new(), 0, 0);
735        emit(&ir, &mut ops, &mut sp, &mut depth);
736        Ok(Self {
737            src: src.to_owned(),
738            ops,
739            depth,
740        })
741    }
742
743    /// A constant (no source text).
744    pub fn constant(v: f64) -> Self {
745        Self {
746            src: String::new(),
747            ops: vec![Op::Push(v)],
748            depth: 1,
749        }
750    }
751
752    /// The value if it folded to a constant.
753    pub fn as_const(&self) -> Option<f64> {
754        match self.ops[..] {
755            [Op::Push(v)] => Some(v),
756            _ => None,
757        }
758    }
759
760    pub fn source(&self) -> &str {
761        &self.src
762    }
763
764    /// The value on `frame` (may be non-finite: [`super::Track::eval`] checks).
765    pub fn eval(&self, frame: &Frame) -> f64 {
766        if let [Op::Push(v)] = self.ops[..] {
767            return v;
768        }
769        let mut st: Vec<f64> = Vec::with_capacity(self.depth);
770        for op in &self.ops {
771            match *op {
772                Op::Push(v) => st.push(v),
773                Op::Load(s) => st.push(frame.get(s)),
774                Op::Apply(f, n) => {
775                    let at = st.len() - usize::from(n);
776                    let v = f.apply(&st[at..]);
777                    st.truncate(at);
778                    st.push(v);
779                }
780            }
781        }
782        st.pop().unwrap_or(f64::NAN)
783    }
784}