Vectrum
Sign In
769 lines · 32.6 KB · Rust
//! Evaluates an OpenUI Lang program into a tree of values for the renderer,
//! given the current `$state`. Programs are small, so the whole program is
//! evaluated on every render; each statement once, then reused.
//!
//! Evaluation never fails as a whole: a reference that isn't defined yet
//! (the answer is still streaming) is `Pending`, and anything wrong is an
//! `Error` value in its place, so the rest still renders.

use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;

use super::lang::{BinaryOp, Expr, Program, UnaryOp};

/// How deep references and nesting may go before it counts as a cycle.
const MAX_DEPTH: usize = 64;
/// How many expressions one evaluation may visit. Statements are cached, so
/// only `@Each` over large arrays gets near this.
const MAX_STEPS: usize = 200_000;

#[derive(Clone, Debug, PartialEq)]
pub enum Value {
    Null,
    Bool(bool),
    Number(f64),
    String(String),
    Array(Vec<Value>),
    Object(BTreeMap<String, Value>),
    Component { name: String, args: Vec<Value> },
    Action(Arc<Action>),
    /// A `$var` passed straight to a component, so inputs can bind to it,
    /// with its current value.
    State(String, Box<Value>),
    /// Not defined yet: the program is still streaming in.
    Pending,
    Error(String),
}

/// `Action([steps])`. Step arguments are evaluated when the action runs, so
/// `@Set($n, $n + 1)` sees the state at that moment; the loop variables of
/// the `@Each` it was built in come along.
#[derive(Debug, PartialEq)]
pub struct Action {
    pub steps: Vec<Step>,
    locals: Vec<(String, Value)>,
}

#[derive(Debug, PartialEq)]
pub enum Step {
    Set(String, Expr),
    Reset(Vec<String>),
    ToAssistant(Expr),
    OpenUrl(Expr),
    /// `@Run` and anything else this app can't do.
    Unsupported(String),
}

/// What running an action asks of the app, beyond changing state.
#[derive(Debug, PartialEq)]
pub enum Effect {
    ToAssistant(String),
    OpenUrl(String),
    Unsupported(String),
}

impl Value {
    /// The value itself, seeing through a `$var` binding.
    pub fn get(&self) -> &Value {
        match self {
            Value::State(_, value) => value.get(),
            value => value,
        }
    }

    pub fn as_str(&self) -> Option<&str> {
        match self.get() {
            Value::String(text) => Some(text),
            _ => None,
        }
    }

    pub fn as_number(&self) -> Option<f64> {
        match self.get() {
            Value::Number(number) => Some(*number),
            Value::String(text) => text.trim().parse().ok(),
            Value::Bool(value) => Some(if *value { 1. } else { 0. }),
            _ => None,
        }
    }

    pub fn as_array(&self) -> Option<&[Value]> {
        match self.get() {
            Value::Array(items) => Some(items),
            _ => None,
        }
    }

    /// Truthiness as in JS: empty strings, zero, null and false are false.
    pub fn truthy(&self) -> bool {
        match self.get() {
            Value::Null | Value::Pending | Value::Error(_) => false,
            Value::Bool(value) => *value,
            Value::Number(number) => *number != 0. && !number.is_nan(),
            Value::String(text) => !text.is_empty(),
            _ => true,
        }
    }

    /// The value as text for display and string concatenation. `null`
    /// shows as nothing, since models use it as "no value".
    pub fn text(&self) -> String {
        match self.get() {
            Value::Null | Value::Pending | Value::Component { .. } | Value::Action(_) => String::new(),
            Value::Bool(value) => value.to_string(),
            Value::Number(number) => format_number(*number),
            Value::String(text) => text.clone(),
            Value::Array(items) => items.iter().map(Value::text).collect::<Vec<_>>().join(", "),
            Value::Object(_) => self.to_json().to_string(),
            Value::Error(error) => error.clone(),
            Value::State(..) => unreachable!("`get` sees through state"),
        }
    }

    pub fn to_json(&self) -> serde_json::Value {
        use serde_json::Value as Json;
        match self.get() {
            Value::Null | Value::Pending | Value::Component { .. } | Value::Action(_) | Value::Error(_) => Json::Null,
            Value::Bool(value) => Json::Bool(*value),
            Value::Number(number) => serde_json::Number::from_f64(*number)
                .map(|number| match number.as_f64() {
                    Some(float) if float.fract() == 0. && float.abs() < 9e15 => Json::from(float as i64),
                    _ => Json::Number(number),
                })
                .unwrap_or(Json::Null),
            Value::String(text) => Json::String(text.clone()),
            Value::Array(items) => Json::Array(items.iter().map(Value::to_json).collect()),
            Value::Object(fields) => Json::Object(fields.iter().map(|(key, value)| (key.clone(), value.to_json())).collect()),
            Value::State(..) => unreachable!("`get` sees through state"),
        }
    }

    /// Whether this or anything inside is still `Pending`.
    #[cfg(test)]
    pub fn is_pending(&self) -> bool {
        match self {
            Value::Pending => true,
            Value::State(_, value) => value.is_pending(),
            Value::Array(items) | Value::Component { args: items, .. } => items.iter().any(Value::is_pending),
            Value::Object(fields) => fields.values().any(Value::is_pending),
            _ => false,
        }
    }
}

/// Integers without a decimal point, other numbers as short as they
/// round-trip.
pub fn format_number(number: f64) -> String {
    if number.fract() == 0. && number.abs() < 1e15 {
        format!("{}", number as i64)
    } else {
        format!("{number}")
    }
}

/// `$state` values the user has changed. Variables not in here have their
/// declared default.
pub type State = BTreeMap<String, Value>;

/// Evaluates the program's root.
pub fn evaluate(program: &Program, state: &State) -> Value {
    let Some(root) = program.root() else {
        return Value::Pending;
    };
    Evaluator::new(program, state).reference(&root.name)
}

/// Runs an action's steps in order against `state`, returning what else it
/// asks the app to do.
pub fn run(program: &Program, state: &mut State, action: &Action) -> Vec<Effect> {
    let mut effects = Vec::new();
    for step in &action.steps {
        // Each step sees the state the previous ones left.
        let snapshot = state.clone();
        let evaluator = Evaluator::new(program, &snapshot);
        *evaluator.locals.borrow_mut() = action.locals.clone();
        match step {
            Step::Set(name, expr) => {
                let value = evaluator.eval(expr, 0).get().clone();
                state.insert(name.clone(), value);
            }
            Step::Reset(names) => {
                for name in names {
                    state.remove(name);
                }
            }
            Step::ToAssistant(expr) => effects.push(Effect::ToAssistant(evaluator.eval(expr, 0).text())),
            Step::OpenUrl(expr) => effects.push(Effect::OpenUrl(evaluator.eval(expr, 0).text())),
            Step::Unsupported(name) => effects.push(Effect::Unsupported(name.clone())),
        }
    }
    effects
}

/// The value of `$name` now: the user's, else the declared default.
pub fn state_value(program: &Program, state: &State, name: &str) -> Value {
    Evaluator::new(program, state).state(name, 0)
}

struct Evaluator<'a> {
    program: &'a Program,
    state: &'a State,
    statements: HashMap<&'a str, &'a Expr>,
    cache: RefCell<HashMap<String, Value>>,
    /// Statements being evaluated, to catch cycles.
    stack: RefCell<Vec<String>>,
    /// `@Each` loop variables in scope, innermost last.
    locals: RefCell<Vec<(String, Value)>>,
    steps: RefCell<usize>,
}

impl<'a> Evaluator<'a> {
    fn new(program: &'a Program, state: &'a State) -> Self {
        let statements = program
            .statements
            .iter()
            .filter(|statement| !statement.state)
            .map(|statement| (statement.name.as_str(), &statement.expr))
            .collect();
        Self {
            program,
            state,
            statements,
            cache: RefCell::default(),
            stack: RefCell::default(),
            locals: RefCell::default(),
            steps: RefCell::new(0),
        }
    }

    fn reference(&self, name: &str) -> Value {
        if let Some((_, value)) = self.locals.borrow().iter().rev().find(|(local, _)| local == name) {
            return value.clone();
        }
        if let Some(value) = self.cache.borrow().get(name) {
            return value.clone();
        }
        let Some(expr) = self.statements.get(name) else {
            return Value::Pending;
        };
        if self.stack.borrow().iter().any(|open| open == name) {
            return Value::Error(format!("`{name}` refers to itself"));
        }
        if self.stack.borrow().len() >= MAX_DEPTH {
            return Value::Error("Nested too deeply".into());
        }
        self.stack.borrow_mut().push(name.to_string());
        // Statements don't see loop variables; those are only for the
        // template written inline in `@Each`.
        let locals = std::mem::take(&mut *self.locals.borrow_mut());
        let value = self.eval(expr, 0);
        *self.locals.borrow_mut() = locals;
        self.stack.borrow_mut().pop();
        self.cache.borrow_mut().insert(name.to_string(), value.clone());
        value
    }

    fn state(&self, name: &str, depth: usize) -> Value {
        if let Some(value) = self.state.get(name) {
            return value.clone();
        }
        match self.program.state_default(name) {
            Some(expr) => self.eval(expr, depth + 1).get().clone(),
            None => Value::Null,
        }
    }

    fn eval(&self, expr: &Expr, depth: usize) -> Value {
        if depth > MAX_DEPTH {
            return Value::Error("Nested too deeply".into());
        }
        {
            let mut steps = self.steps.borrow_mut();
            *steps += 1;
            if *steps > MAX_STEPS {
                return Value::Error("Too much to evaluate".into());
            }
        }
        let depth = depth + 1;
        match expr {
            Expr::Null => Value::Null,
            Expr::Bool(value) => Value::Bool(*value),
            Expr::Number(number) => Value::Number(*number),
            Expr::String(text) => Value::String(text.clone()),
            Expr::Array(items) => Value::Array(items.iter().map(|item| self.eval(item, depth)).collect()),
            Expr::Object(fields) => {
                Value::Object(fields.iter().map(|(key, value)| (key.clone(), self.eval(value, depth).get().clone())).collect())
            }
            Expr::Ref(name) => self.reference(name),
            Expr::State(name) => self.state(name, depth),
            Expr::Call(name, args) => match name.as_str() {
                "Action" => self.action(args),
                _ => Value::Component {
                    name: name.clone(),
                    args: args
                        .iter()
                        .map(|arg| match arg {
                            // Inputs bind to a `$var` handed to them directly.
                            Expr::State(var) => Value::State(var.clone(), Box::new(self.state(var, depth))),
                            arg => self.eval(arg, depth),
                        })
                        .collect(),
                },
            },
            Expr::Builtin(name, args) => self.builtin(name, args, depth),
            Expr::Member(target, field) => member(self.eval(target, depth).get(), field),
            Expr::Unary(op, operand) => {
                let value = self.eval(operand, depth);
                match (op, value.get()) {
                    (_, Value::Pending) => Value::Pending,
                    (_, Value::Error(error)) => Value::Error(error.clone()),
                    (UnaryOp::Not, value) => Value::Bool(!value.truthy()),
                    (UnaryOp::Neg, value) => match value.as_number() {
                        Some(number) => Value::Number(-number),
                        None => Value::Error(format!("Can't negate {}", describe(value))),
                    },
                }
            }
            Expr::Binary(BinaryOp::And, left, right) => {
                let left = self.eval(left, depth);
                if left.get() == &Value::Pending || !left.truthy() { left } else { self.eval(right, depth) }
            }
            Expr::Binary(BinaryOp::Or, left, right) => {
                let left = self.eval(left, depth);
                if left.get() == &Value::Pending || left.truthy() { left } else { self.eval(right, depth) }
            }
            Expr::Binary(op, left, right) => binary(*op, self.eval(left, depth).get(), self.eval(right, depth).get()),
            Expr::Ternary(condition, then, otherwise) => {
                let condition = self.eval(condition, depth);
                match condition.get() {
                    Value::Pending => Value::Pending,
                    Value::Error(error) => Value::Error(error.clone()),
                    value if value.truthy() => self.eval(then, depth),
                    _ => self.eval(otherwise, depth),
                }
            }
        }
    }

    fn action(&self, args: &[Expr]) -> Value {
        let steps = match args.first() {
            Some(Expr::Array(items)) => items.iter().map(step).collect(),
            Some(single) => vec![step(single)],
            None => Vec::new(),
        };
        Value::Action(Arc::new(Action { steps, locals: self.locals.borrow().clone() }))
    }

    fn builtin(&self, name: &str, args: &[Expr], depth: usize) -> Value {
        // Action steps used on their own, as `Button("Go", @ToAssistant("go"))`.
        if matches!(name, "Set" | "Reset" | "ToAssistant" | "OpenUrl" | "Run") {
            return Value::Action(Arc::new(Action {
                steps: vec![step(&Expr::Builtin(name.to_string(), args.to_vec()))],
                locals: self.locals.borrow().clone(),
            }));
        }
        if name == "Each" {
            return self.each(args, depth);
        }
        let values: Vec<Value> = args.iter().map(|arg| self.eval(arg, depth).get().clone()).collect();
        if values.iter().any(|value| value == &Value::Pending) {
            return Value::Pending;
        }
        if let Some(error) = values.iter().find_map(|value| match value {
            Value::Error(error) => Some(error.clone()),
            _ => None,
        }) {
            return Value::Error(error);
        }
        let arg = |ix: usize| values.get(ix).unwrap_or(&Value::Null);
        let array = || arg(0).as_array().map(<[Value]>::to_vec);
        let numbers = || array().map(|items| items.iter().filter_map(Value::as_number).collect::<Vec<f64>>());
        let number = || arg(0).as_number();
        let need = |what: &str| Value::Error(format!("@{name} needs {what}"));
        match name {
            "Count" => match arg(0) {
                Value::Array(items) => Value::Number(items.len() as f64),
                Value::String(text) => Value::Number(text.chars().count() as f64),
                Value::Null => Value::Number(0.),
                _ => need("an array"),
            },
            "Sum" => numbers().map(|numbers| Value::Number(numbers.iter().sum())).unwrap_or_else(|| need("an array")),
            "Avg" => match numbers() {
                Some(numbers) if numbers.is_empty() => Value::Null,
                Some(numbers) => Value::Number(numbers.iter().sum::<f64>() / numbers.len() as f64),
                None => need("an array"),
            },
            "Min" => match numbers() {
                Some(numbers) => numbers.into_iter().reduce(f64::min).map(Value::Number).unwrap_or(Value::Null),
                None => need("an array"),
            },
            "Max" => match numbers() {
                Some(numbers) => numbers.into_iter().reduce(f64::max).map(Value::Number).unwrap_or(Value::Null),
                None => need("an array"),
            },
            "First" => array().map(|items| items.first().cloned().unwrap_or(Value::Null)).unwrap_or_else(|| need("an array")),
            "Last" => array().map(|items| items.last().cloned().unwrap_or(Value::Null)).unwrap_or_else(|| need("an array")),
            "Filter" => {
                let Some(items) = array() else { return need("an array") };
                let field = arg(1).text();
                let op = arg(2).text();
                let wanted = arg(3);
                let mut kept = Vec::new();
                for item in items {
                    let value = member(&item, &field);
                    let keep = match op.as_str() {
                        "contains" => value.text().to_lowercase().contains(&wanted.text().to_lowercase()),
                        "==" => equals(&value, wanted),
                        "!=" => !equals(&value, wanted),
                        ">" | "<" | ">=" | "<=" => {
                            let op = match op.as_str() {
                                ">" => BinaryOp::Gt,
                                "<" => BinaryOp::Lt,
                                ">=" => BinaryOp::Ge,
                                _ => BinaryOp::Le,
                            };
                            binary(op, &value, wanted).truthy()
                        }
                        _ => return Value::Error(format!("@Filter doesn't know the operator {op:?}")),
                    };
                    if keep {
                        kept.push(item);
                    }
                }
                Value::Array(kept)
            }
            "Sort" => {
                let Some(mut items) = array() else { return need("an array") };
                let field = arg(1).text();
                let descending = arg(2).text().eq_ignore_ascii_case("desc");
                items.sort_by(|a, b| {
                    let (a, b) = if field.is_empty() { (a.clone(), b.clone()) } else { (member(a, &field), member(b, &field)) };
                    let order = match (a.as_number(), b.as_number(), &a, &b) {
                        (_, _, Value::String(a), Value::String(b)) => a.cmp(b),
                        (Some(a), Some(b), _, _) => a.total_cmp(&b),
                        _ => a.text().cmp(&b.text()),
                    };
                    if descending { order.reverse() } else { order }
                });
                Value::Array(items)
            }
            "Round" => match number() {
                Some(value) => {
                    let factor = 10f64.powi(arg(1).as_number().unwrap_or(0.).clamp(0., 12.) as i32);
                    Value::Number((value * factor).round() / factor)
                }
                None => need("a number"),
            },
            "Abs" => number().map(|value| Value::Number(value.abs())).unwrap_or_else(|| need("a number")),
            "Floor" => number().map(|value| Value::Number(value.floor())).unwrap_or_else(|| need("a number")),
            "Ceil" => number().map(|value| Value::Number(value.ceil())).unwrap_or_else(|| need("a number")),
            _ => Value::Error(format!("Unknown function @{name}")),
        }
    }

    /// `@Each(array, "item", template)`: the template once per element, with
    /// the element bound to the name.
    fn each(&self, args: &[Expr], depth: usize) -> Value {
        let [array, name, template] = args else {
            return Value::Error("@Each needs an array, a name and a template".into());
        };
        let array = self.eval(array, depth);
        let items = match array.get() {
            Value::Pending => return Value::Pending,
            Value::Error(error) => return Value::Error(error.clone()),
            Value::Null => return Value::Array(Vec::new()),
            Value::Array(items) => items.clone(),
            other => vec![other.clone()],
        };
        // The name is a string, but models also write it bare.
        let name = match name {
            Expr::String(name) | Expr::Ref(name) => name.clone(),
            _ => return Value::Error("@Each needs a name for the loop variable".into()),
        };
        Value::Array(
            items
                .into_iter()
                .map(|item| {
                    self.locals.borrow_mut().push((name.clone(), item));
                    let value = self.eval(template, depth);
                    self.locals.borrow_mut().pop();
                    value
                })
                .collect(),
        )
    }
}

/// An action step from its expression; anything but a known step builtin
/// is unsupported.
fn step(expr: &Expr) -> Step {
    let Expr::Builtin(name, args) = expr else {
        return Step::Unsupported("a step that isn't a @function".into());
    };
    let state_names = || {
        args.iter()
            .filter_map(|arg| match arg {
                Expr::State(name) | Expr::String(name) => Some(name.trim_start_matches('$').to_string()),
                _ => None,
            })
            .collect()
    };
    match (name.as_str(), args.as_slice()) {
        ("Set", [Expr::State(var), value]) => Step::Set(var.clone(), value.clone()),
        ("Reset", _) => Step::Reset(state_names()),
        ("ToAssistant", [message, ..]) => Step::ToAssistant(message.clone()),
        ("OpenUrl", [url, ..]) => Step::OpenUrl(url.clone()),
        _ => Step::Unsupported(format!("@{name}")),
    }
}

/// `value.field`: a field of an object, the field of every element of an
/// array, or an index into an array.
fn member(value: &Value, field: &str) -> Value {
    match value.get() {
        Value::Pending => Value::Pending,
        Value::Error(error) => Value::Error(error.clone()),
        Value::Object(fields) => fields.get(field).cloned().unwrap_or(Value::Null),
        Value::Array(items) => match field.parse::<usize>() {
            Ok(index) => items.get(index).cloned().unwrap_or(Value::Null),
            Err(_) if field == "length" => Value::Number(items.len() as f64),
            Err(_) => Value::Array(items.iter().map(|item| member(item, field)).collect()),
        },
        Value::String(text) if field == "length" => Value::Number(text.chars().count() as f64),
        _ => Value::Null,
    }
}

fn binary(op: BinaryOp, left: &Value, right: &Value) -> Value {
    match (left, right) {
        (Value::Pending, _) | (_, Value::Pending) => return Value::Pending,
        (Value::Error(error), _) | (_, Value::Error(error)) => return Value::Error(error.clone()),
        _ => {}
    }
    let numbers = || match (left.as_number(), right.as_number()) {
        (Some(a), Some(b)) if !matches!(left, Value::Null | Value::Array(_)) => Some((a, b)),
        _ => None,
    };
    match op {
        BinaryOp::Add => match (left, right) {
            (Value::String(_), _) | (_, Value::String(_)) => Value::String(left.text() + &right.text()),
            (Value::Number(a), Value::Number(b)) => Value::Number(a + b),
            (Value::Array(a), Value::Array(b)) => Value::Array(a.iter().chain(b).cloned().collect()),
            _ => match numbers() {
                Some((a, b)) => Value::Number(a + b),
                None => Value::Error(format!("Can't add {} and {}", describe(left), describe(right))),
            },
        },
        BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem => {
            let Some((a, b)) = numbers() else {
                return Value::Error(format!("Can't do arithmetic on {} and {}", describe(left), describe(right)));
            };
            Value::Number(match op {
                BinaryOp::Sub => a - b,
                BinaryOp::Mul => a * b,
                BinaryOp::Div => a / b,
                _ => a % b,
            })
        }
        BinaryOp::Eq => Value::Bool(equals(left, right)),
        BinaryOp::Ne => Value::Bool(!equals(left, right)),
        BinaryOp::Gt | BinaryOp::Lt | BinaryOp::Ge | BinaryOp::Le => {
            let order = match (left, right) {
                (Value::String(a), Value::String(b)) => Some(a.cmp(b)),
                _ => numbers().and_then(|(a, b)| a.partial_cmp(&b)),
            };
            Value::Bool(order.is_some_and(|order| match op {
                BinaryOp::Gt => order.is_gt(),
                BinaryOp::Lt => order.is_lt(),
                BinaryOp::Ge => order.is_ge(),
                _ => order.is_le(),
            }))
        }
        BinaryOp::And | BinaryOp::Or => unreachable!("short-circuited in `eval`"),
    }
}

/// Equality that lets a number equal its text, since select and input
/// values arrive as strings.
fn equals(left: &Value, right: &Value) -> bool {
    match (left.get(), right.get()) {
        (Value::Number(a), Value::String(b)) | (Value::String(b), Value::Number(a)) => b.trim().parse::<f64>() == Ok(*a),
        (a, b) => a == b,
    }
}

fn describe(value: &Value) -> &'static str {
    match value.get() {
        Value::Null => "null",
        Value::Bool(_) => "a boolean",
        Value::Number(_) => "a number",
        Value::String(_) => "a string",
        Value::Array(_) => "an array",
        Value::Object(_) => "an object",
        Value::Component { .. } => "a component",
        Value::Action(_) => "an action",
        Value::Pending => "a pending value",
        Value::Error(_) => "an error",
        Value::State(..) => "state",
    }
}

#[cfg(test)]
mod tests {
    use super::super::lang::parse;
    use super::{Effect, State, Value, evaluate, run, state_value};

    fn eval(source: &str) -> Value {
        let program = parse(source);
        assert!(program.errors.is_empty(), "{source}: {:?}", program.errors);
        evaluate(&program, &State::new())
    }

    fn n(number: f64) -> Value {
        Value::Number(number)
    }

    fn s(text: &str) -> Value {
        Value::String(text.into())
    }

    #[::core::prelude::v1::test]
    fn arithmetic_and_logic() {
        assert_eq!(eval("x = 1 + 2 * 3"), n(7.));
        assert_eq!(eval("x = 10 % 4 - -1"), n(3.));
        assert_eq!(eval(r#"x = "" + 7 + " days""#), s("7 days"));
        assert_eq!(eval(r#"x = 1 + 2 + "a""#), s("3a"));
        assert_eq!(eval("x = 2.5 * 2 == 5 && 3 > 2"), Value::Bool(true));
        assert_eq!(eval(r#"x = null || "fallback""#), s("fallback"));
        assert_eq!(eval(r#"x = "7" == 7"#), Value::Bool(true));
        assert_eq!(eval(r#"x = "b" > "a" ? "yes" : "no""#), s("yes"));
        assert!(matches!(eval(r#"x = {a: 1} - 1"#), Value::Error(_)));
        assert_eq!(super::format_number(0.1 + 0.2), "0.30000000000000004");
        assert_eq!(eval("x = 1.5").text(), "1.5");
        assert_eq!(eval("x = 3").text(), "3");
    }

    #[::core::prelude::v1::test]
    fn forward_references_and_pluck() {
        let value = eval(
            r#"root = Table([Col("Title", data.rows.title), Col("Total", @Sum(data.rows.n))])
data = {rows: [{title: "a", n: 2}, {title: "b", n: 3}]}"#,
        );
        let Value::Component { name, args } = value else { panic!("{value:?}") };
        assert_eq!(name, "Table");
        let Value::Array(columns) = &args[0] else { panic!() };
        assert_eq!(
            columns[0],
            Value::Component { name: "Col".into(), args: vec![s("Title"), Value::Array(vec![s("a"), s("b")])] }
        );
        assert_eq!(columns[1], Value::Component { name: "Col".into(), args: vec![s("Total"), n(5.)] });
        assert_eq!(eval("x = [[1, 2], [3]].0.1"), n(2.));
        assert_eq!(eval("x = [1, 2, 3].length"), n(3.));
    }

    #[::core::prelude::v1::test]
    fn pending_and_cycles() {
        assert_eq!(eval("root = Stack([later])"), Value::Component { name: "Stack".into(), args: vec![Value::Array(vec![Value::Pending])] });
        assert!(eval("root = Stack([later])").is_pending());
        assert_eq!(eval("x = missing + 1"), Value::Pending);
        let value = eval("a = Stack([b])\nb = Stack([a])");
        let text = format!("{value:?}");
        assert!(text.contains("refers to itself"), "{text}");
        let value = eval("a = a + 1");
        assert!(matches!(value, Value::Error(_)), "{value:?}");
        // A long chain hits the depth limit instead of the stack.
        let chain: String = (0..200).map(|ix| format!("s{ix} = s{}\n", ix + 1)).collect();
        assert!(matches!(eval(&chain), Value::Error(_)));
    }

    #[::core::prelude::v1::test]
    fn builtins() {
        let data = r#"rows = [{name: "b", n: 3}, {name: "a", n: 1}, {name: "c", n: 2}]"#;
        let with = |expr: &str| eval(&format!("x = {expr}\n{data}"));
        assert_eq!(with("@Count(rows)"), n(3.));
        assert_eq!(with("@Sum(rows.n)"), n(6.));
        assert_eq!(with("@Avg(rows.n)"), n(2.));
        assert_eq!(with("@Min(rows.n)"), n(1.));
        assert_eq!(with("@Max(rows.n)"), n(3.));
        assert_eq!(with("@First(rows).name"), s("b"));
        assert_eq!(with("@Last(rows.name)"), s("c"));
        assert_eq!(with(r#"@Sort(rows, "n").name"#), Value::Array(vec![s("a"), s("c"), s("b")]));
        assert_eq!(with(r#"@Sort(rows, "name", "desc").name"#), Value::Array(vec![s("c"), s("b"), s("a")]));
        assert_eq!(with(r#"@Filter(rows, "n", ">=", 2).name"#), Value::Array(vec![s("b"), s("c")]));
        assert_eq!(with(r#"@Filter(rows, "name", "contains", "A").n"#), Value::Array(vec![n(1.)]));
        assert_eq!(with(r#"@Filter(rows, "n", "==", "3").name"#), Value::Array(vec![s("b")]));
        assert_eq!(eval("x = @Round(3.14159, 2)"), n(3.14));
        assert_eq!(eval("x = @Round(2.5)"), n(3.));
        assert_eq!(eval("x = @Abs(-2) + @Floor(1.7) + @Ceil(1.2)"), n(5.));
        assert!(matches!(eval("x = @Nope(1)"), Value::Error(_)));
        assert_eq!(eval("x = @Sum(later)"), Value::Pending);
    }

    #[::core::prelude::v1::test]
    fn each() {
        let value = eval(r#"root = @Each(["a", "b"], "item", Tag(item + "!"))"#);
        assert_eq!(
            value,
            Value::Array(vec![
                Value::Component { name: "Tag".into(), args: vec![s("a!")] },
                Value::Component { name: "Tag".into(), args: vec![s("b!")] },
            ])
        );
        // Loop variables don't leak into other statements.
        let value = eval(r#"root = @Each([1], "item", other)
other = item"#);
        assert_eq!(value, Value::Array(vec![Value::Pending]));
    }

    #[::core::prelude::v1::test]
    fn state_and_actions() {
        let program = parse(
            r#"root = Stack([TextContent("Count: " + $count), Input($name), plus, reset])
$count = 0
$name = "Ada"
plus = Button("+", Action([@Set($count, $count + 1), @Set($count, $count + 1), @ToAssistant("now " + $count)]))
reset = Button("Reset", Action([@Reset($count, $name), @OpenUrl("https://example.com"), @Run(query)]))"#,
        );
        assert!(program.errors.is_empty(), "{:?}", program.errors);
        let mut state = State::new();
        let Value::Component { args, .. } = evaluate(&program, &state) else { panic!() };
        let Value::Array(children) = &args[0] else { panic!() };
        assert_eq!(children[0], Value::Component { name: "TextContent".into(), args: vec![s("Count: 0")] });
        assert_eq!(children[1], Value::Component { name: "Input".into(), args: vec![Value::State("name".into(), Box::new(s("Ada")))] });
        let action = |child: &Value| match child {
            Value::Component { args, .. } => match &args[1] {
                Value::Action(action) => action.clone(),
                other => panic!("{other:?}"),
            },
            other => panic!("{other:?}"),
        };
        let effects = run(&program, &mut state, &action(&children[2]));
        assert_eq!(state_value(&program, &state, "count"), n(2.));
        assert_eq!(effects, vec![Effect::ToAssistant("now 2".into())]);
        state.insert("name".into(), s("Grace"));
        let effects = run(&program, &mut state, &action(&children[3]));
        assert!(state.is_empty());
        assert_eq!(state_value(&program, &state, "name"), s("Ada"));
        assert_eq!(effects, vec![Effect::OpenUrl("https://example.com".into()), Effect::Unsupported("@Run".into())]);
    }

    #[::core::prelude::v1::test]
    fn actions_keep_loop_variables() {
        let program = parse(r#"root = @Each(["x", "y"], "item", Button(item, @ToAssistant("picked " + item)))"#);
        let Value::Array(buttons) = evaluate(&program, &State::new()) else { panic!() };
        let Value::Component { args, .. } = &buttons[1] else { panic!() };
        let Value::Action(action) = &args[1] else { panic!() };
        assert_eq!(run(&program, &mut State::new(), action), vec![Effect::ToAssistant("picked y".into())]);
    }

    #[::core::prelude::v1::test]
    fn errors_stay_local() {
        let value = eval(r#"root = Stack([TextContent("ok"), TextContent(1 - "x")])"#);
        let Value::Component { args, .. } = value else { panic!() };
        let Value::Array(children) = &args[0] else { panic!() };
        assert_eq!(children[0], Value::Component { name: "TextContent".into(), args: vec![s("ok")] });
        assert!(matches!(&children[1], Value::Component { args, .. } if matches!(args[0], Value::Error(_))));
        assert_eq!(Value::Object([("a".to_string(), n(1.))].into()).to_json(), serde_json::json!({"a": 1}));
    }
}