| @@ -0,0 +1,768 @@ |
| 1 | + //! Evaluates an OpenUI Lang program into a tree of values for the renderer, |
| 2 | + //! given the current `$state`. Programs are small, so the whole program is |
| 3 | + //! evaluated on every render; each statement once, then reused. |
| 4 | + //! |
| 5 | + //! Evaluation never fails as a whole: a reference that isn't defined yet |
| 6 | + //! (the answer is still streaming) is `Pending`, and anything wrong is an |
| 7 | + //! `Error` value in its place, so the rest still renders. |
| 8 | + |
| 9 | + use std::cell::RefCell; |
| 10 | + use std::collections::{BTreeMap, HashMap}; |
| 11 | + use std::sync::Arc; |
| 12 | + |
| 13 | + use super::lang::{BinaryOp, Expr, Program, UnaryOp}; |
| 14 | + |
| 15 | + /// How deep references and nesting may go before it counts as a cycle. |
| 16 | + const MAX_DEPTH: usize = 64; |
| 17 | + /// How many expressions one evaluation may visit. Statements are cached, so |
| 18 | + /// only `@Each` over large arrays gets near this. |
| 19 | + const MAX_STEPS: usize = 200_000; |
| 20 | + |
| 21 | + #[derive(Clone, Debug, PartialEq)] |
| 22 | + pub enum Value { |
| 23 | + Null, |
| 24 | + Bool(bool), |
| 25 | + Number(f64), |
| 26 | + String(String), |
| 27 | + Array(Vec<Value>), |
| 28 | + Object(BTreeMap<String, Value>), |
| 29 | + Component { name: String, args: Vec<Value> }, |
| 30 | + Action(Arc<Action>), |
| 31 | + /// A `$var` passed straight to a component, so inputs can bind to it, |
| 32 | + /// with its current value. |
| 33 | + State(String, Box<Value>), |
| 34 | + /// Not defined yet: the program is still streaming in. |
| 35 | + Pending, |
| 36 | + Error(String), |
| 37 | + } |
| 38 | + |
| 39 | + /// `Action([steps])`. Step arguments are evaluated when the action runs, so |
| 40 | + /// `@Set($n, $n + 1)` sees the state at that moment; the loop variables of |
| 41 | + /// the `@Each` it was built in come along. |
| 42 | + #[derive(Debug, PartialEq)] |
| 43 | + pub struct Action { |
| 44 | + pub steps: Vec<Step>, |
| 45 | + locals: Vec<(String, Value)>, |
| 46 | + } |
| 47 | + |
| 48 | + #[derive(Debug, PartialEq)] |
| 49 | + pub enum Step { |
| 50 | + Set(String, Expr), |
| 51 | + Reset(Vec<String>), |
| 52 | + ToAssistant(Expr), |
| 53 | + OpenUrl(Expr), |
| 54 | + /// `@Run` and anything else this app can't do. |
| 55 | + Unsupported(String), |
| 56 | + } |
| 57 | + |
| 58 | + /// What running an action asks of the app, beyond changing state. |
| 59 | + #[derive(Debug, PartialEq)] |
| 60 | + pub enum Effect { |
| 61 | + ToAssistant(String), |
| 62 | + OpenUrl(String), |
| 63 | + Unsupported(String), |
| 64 | + } |
| 65 | + |
| 66 | + impl Value { |
| 67 | + /// The value itself, seeing through a `$var` binding. |
| 68 | + pub fn get(&self) -> &Value { |
| 69 | + match self { |
| 70 | + Value::State(_, value) => value.get(), |
| 71 | + value => value, |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + pub fn as_str(&self) -> Option<&str> { |
| 76 | + match self.get() { |
| 77 | + Value::String(text) => Some(text), |
| 78 | + _ => None, |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + pub fn as_number(&self) -> Option<f64> { |
| 83 | + match self.get() { |
| 84 | + Value::Number(number) => Some(*number), |
| 85 | + Value::String(text) => text.trim().parse().ok(), |
| 86 | + Value::Bool(value) => Some(if *value { 1. } else { 0. }), |
| 87 | + _ => None, |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + pub fn as_array(&self) -> Option<&[Value]> { |
| 92 | + match self.get() { |
| 93 | + Value::Array(items) => Some(items), |
| 94 | + _ => None, |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + /// Truthiness as in JS: empty strings, zero, null and false are false. |
| 99 | + pub fn truthy(&self) -> bool { |
| 100 | + match self.get() { |
| 101 | + Value::Null | Value::Pending | Value::Error(_) => false, |
| 102 | + Value::Bool(value) => *value, |
| 103 | + Value::Number(number) => *number != 0. && !number.is_nan(), |
| 104 | + Value::String(text) => !text.is_empty(), |
| 105 | + _ => true, |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + /// The value as text for display and string concatenation. `null` |
| 110 | + /// shows as nothing, since models use it as "no value". |
| 111 | + pub fn text(&self) -> String { |
| 112 | + match self.get() { |
| 113 | + Value::Null | Value::Pending | Value::Component { .. } | Value::Action(_) => String::new(), |
| 114 | + Value::Bool(value) => value.to_string(), |
| 115 | + Value::Number(number) => format_number(*number), |
| 116 | + Value::String(text) => text.clone(), |
| 117 | + Value::Array(items) => items.iter().map(Value::text).collect::<Vec<_>>().join(", "), |
| 118 | + Value::Object(_) => self.to_json().to_string(), |
| 119 | + Value::Error(error) => error.clone(), |
| 120 | + Value::State(..) => unreachable!("`get` sees through state"), |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + pub fn to_json(&self) -> serde_json::Value { |
| 125 | + use serde_json::Value as Json; |
| 126 | + match self.get() { |
| 127 | + Value::Null | Value::Pending | Value::Component { .. } | Value::Action(_) | Value::Error(_) => Json::Null, |
| 128 | + Value::Bool(value) => Json::Bool(*value), |
| 129 | + Value::Number(number) => serde_json::Number::from_f64(*number) |
| 130 | + .map(|number| match number.as_f64() { |
| 131 | + Some(float) if float.fract() == 0. && float.abs() < 9e15 => Json::from(float as i64), |
| 132 | + _ => Json::Number(number), |
| 133 | + }) |
| 134 | + .unwrap_or(Json::Null), |
| 135 | + Value::String(text) => Json::String(text.clone()), |
| 136 | + Value::Array(items) => Json::Array(items.iter().map(Value::to_json).collect()), |
| 137 | + Value::Object(fields) => Json::Object(fields.iter().map(|(key, value)| (key.clone(), value.to_json())).collect()), |
| 138 | + Value::State(..) => unreachable!("`get` sees through state"), |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + /// Whether this or anything inside is still `Pending`. |
| 143 | + pub fn is_pending(&self) -> bool { |
| 144 | + match self { |
| 145 | + Value::Pending => true, |
| 146 | + Value::State(_, value) => value.is_pending(), |
| 147 | + Value::Array(items) | Value::Component { args: items, .. } => items.iter().any(Value::is_pending), |
| 148 | + Value::Object(fields) => fields.values().any(Value::is_pending), |
| 149 | + _ => false, |
| 150 | + } |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + /// Integers without a decimal point, other numbers as short as they |
| 155 | + /// round-trip. |
| 156 | + pub fn format_number(number: f64) -> String { |
| 157 | + if number.fract() == 0. && number.abs() < 1e15 { |
| 158 | + format!("{}", number as i64) |
| 159 | + } else { |
| 160 | + format!("{number}") |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + /// `$state` values the user has changed. Variables not in here have their |
| 165 | + /// declared default. |
| 166 | + pub type State = BTreeMap<String, Value>; |
| 167 | + |
| 168 | + /// Evaluates the program's root. |
| 169 | + pub fn evaluate(program: &Program, state: &State) -> Value { |
| 170 | + let Some(root) = program.root() else { |
| 171 | + return Value::Pending; |
| 172 | + }; |
| 173 | + Evaluator::new(program, state).reference(&root.name) |
| 174 | + } |
| 175 | + |
| 176 | + /// Runs an action's steps in order against `state`, returning what else it |
| 177 | + /// asks the app to do. |
| 178 | + pub fn run(program: &Program, state: &mut State, action: &Action) -> Vec<Effect> { |
| 179 | + let mut effects = Vec::new(); |
| 180 | + for step in &action.steps { |
| 181 | + // Each step sees the state the previous ones left. |
| 182 | + let snapshot = state.clone(); |
| 183 | + let evaluator = Evaluator::new(program, &snapshot); |
| 184 | + *evaluator.locals.borrow_mut() = action.locals.clone(); |
| 185 | + match step { |
| 186 | + Step::Set(name, expr) => { |
| 187 | + let value = evaluator.eval(expr, 0).get().clone(); |
| 188 | + state.insert(name.clone(), value); |
| 189 | + } |
| 190 | + Step::Reset(names) => { |
| 191 | + for name in names { |
| 192 | + state.remove(name); |
| 193 | + } |
| 194 | + } |
| 195 | + Step::ToAssistant(expr) => effects.push(Effect::ToAssistant(evaluator.eval(expr, 0).text())), |
| 196 | + Step::OpenUrl(expr) => effects.push(Effect::OpenUrl(evaluator.eval(expr, 0).text())), |
| 197 | + Step::Unsupported(name) => effects.push(Effect::Unsupported(name.clone())), |
| 198 | + } |
| 199 | + } |
| 200 | + effects |
| 201 | + } |
| 202 | + |
| 203 | + /// The value of `$name` now: the user's, else the declared default. |
| 204 | + pub fn state_value(program: &Program, state: &State, name: &str) -> Value { |
| 205 | + Evaluator::new(program, state).state(name, 0) |
| 206 | + } |
| 207 | + |
| 208 | + struct Evaluator<'a> { |
| 209 | + program: &'a Program, |
| 210 | + state: &'a State, |
| 211 | + statements: HashMap<&'a str, &'a Expr>, |
| 212 | + cache: RefCell<HashMap<String, Value>>, |
| 213 | + /// Statements being evaluated, to catch cycles. |
| 214 | + stack: RefCell<Vec<String>>, |
| 215 | + /// `@Each` loop variables in scope, innermost last. |
| 216 | + locals: RefCell<Vec<(String, Value)>>, |
| 217 | + steps: RefCell<usize>, |
| 218 | + } |
| 219 | + |
| 220 | + impl<'a> Evaluator<'a> { |
| 221 | + fn new(program: &'a Program, state: &'a State) -> Self { |
| 222 | + let statements = program |
| 223 | + .statements |
| 224 | + .iter() |
| 225 | + .filter(|statement| !statement.state) |
| 226 | + .map(|statement| (statement.name.as_str(), &statement.expr)) |
| 227 | + .collect(); |
| 228 | + Self { |
| 229 | + program, |
| 230 | + state, |
| 231 | + statements, |
| 232 | + cache: RefCell::default(), |
| 233 | + stack: RefCell::default(), |
| 234 | + locals: RefCell::default(), |
| 235 | + steps: RefCell::new(0), |
| 236 | + } |
| 237 | + } |
| 238 | + |
| 239 | + fn reference(&self, name: &str) -> Value { |
| 240 | + if let Some((_, value)) = self.locals.borrow().iter().rev().find(|(local, _)| local == name) { |
| 241 | + return value.clone(); |
| 242 | + } |
| 243 | + if let Some(value) = self.cache.borrow().get(name) { |
| 244 | + return value.clone(); |
| 245 | + } |
| 246 | + let Some(expr) = self.statements.get(name) else { |
| 247 | + return Value::Pending; |
| 248 | + }; |
| 249 | + if self.stack.borrow().iter().any(|open| open == name) { |
| 250 | + return Value::Error(format!("`{name}` refers to itself")); |
| 251 | + } |
| 252 | + if self.stack.borrow().len() >= MAX_DEPTH { |
| 253 | + return Value::Error("Nested too deeply".into()); |
| 254 | + } |
| 255 | + self.stack.borrow_mut().push(name.to_string()); |
| 256 | + // Statements don't see loop variables; those are only for the |
| 257 | + // template written inline in `@Each`. |
| 258 | + let locals = std::mem::take(&mut *self.locals.borrow_mut()); |
| 259 | + let value = self.eval(expr, 0); |
| 260 | + *self.locals.borrow_mut() = locals; |
| 261 | + self.stack.borrow_mut().pop(); |
| 262 | + self.cache.borrow_mut().insert(name.to_string(), value.clone()); |
| 263 | + value |
| 264 | + } |
| 265 | + |
| 266 | + fn state(&self, name: &str, depth: usize) -> Value { |
| 267 | + if let Some(value) = self.state.get(name) { |
| 268 | + return value.clone(); |
| 269 | + } |
| 270 | + match self.program.state_default(name) { |
| 271 | + Some(expr) => self.eval(expr, depth + 1).get().clone(), |
| 272 | + None => Value::Null, |
| 273 | + } |
| 274 | + } |
| 275 | + |
| 276 | + fn eval(&self, expr: &Expr, depth: usize) -> Value { |
| 277 | + if depth > MAX_DEPTH { |
| 278 | + return Value::Error("Nested too deeply".into()); |
| 279 | + } |
| 280 | + { |
| 281 | + let mut steps = self.steps.borrow_mut(); |
| 282 | + *steps += 1; |
| 283 | + if *steps > MAX_STEPS { |
| 284 | + return Value::Error("Too much to evaluate".into()); |
| 285 | + } |
| 286 | + } |
| 287 | + let depth = depth + 1; |
| 288 | + match expr { |
| 289 | + Expr::Null => Value::Null, |
| 290 | + Expr::Bool(value) => Value::Bool(*value), |
| 291 | + Expr::Number(number) => Value::Number(*number), |
| 292 | + Expr::String(text) => Value::String(text.clone()), |
| 293 | + Expr::Array(items) => Value::Array(items.iter().map(|item| self.eval(item, depth)).collect()), |
| 294 | + Expr::Object(fields) => { |
| 295 | + Value::Object(fields.iter().map(|(key, value)| (key.clone(), self.eval(value, depth).get().clone())).collect()) |
| 296 | + } |
| 297 | + Expr::Ref(name) => self.reference(name), |
| 298 | + Expr::State(name) => self.state(name, depth), |
| 299 | + Expr::Call(name, args) => match name.as_str() { |
| 300 | + "Action" => self.action(args), |
| 301 | + _ => Value::Component { |
| 302 | + name: name.clone(), |
| 303 | + args: args |
| 304 | + .iter() |
| 305 | + .map(|arg| match arg { |
| 306 | + // Inputs bind to a `$var` handed to them directly. |
| 307 | + Expr::State(var) => Value::State(var.clone(), Box::new(self.state(var, depth))), |
| 308 | + arg => self.eval(arg, depth), |
| 309 | + }) |
| 310 | + .collect(), |
| 311 | + }, |
| 312 | + }, |
| 313 | + Expr::Builtin(name, args) => self.builtin(name, args, depth), |
| 314 | + Expr::Member(target, field) => member(self.eval(target, depth).get(), field), |
| 315 | + Expr::Unary(op, operand) => { |
| 316 | + let value = self.eval(operand, depth); |
| 317 | + match (op, value.get()) { |
| 318 | + (_, Value::Pending) => Value::Pending, |
| 319 | + (_, Value::Error(error)) => Value::Error(error.clone()), |
| 320 | + (UnaryOp::Not, value) => Value::Bool(!value.truthy()), |
| 321 | + (UnaryOp::Neg, value) => match value.as_number() { |
| 322 | + Some(number) => Value::Number(-number), |
| 323 | + None => Value::Error(format!("Can't negate {}", describe(value))), |
| 324 | + }, |
| 325 | + } |
| 326 | + } |
| 327 | + Expr::Binary(BinaryOp::And, left, right) => { |
| 328 | + let left = self.eval(left, depth); |
| 329 | + if left.get() == &Value::Pending || !left.truthy() { left } else { self.eval(right, depth) } |
| 330 | + } |
| 331 | + Expr::Binary(BinaryOp::Or, left, right) => { |
| 332 | + let left = self.eval(left, depth); |
| 333 | + if left.get() == &Value::Pending || left.truthy() { left } else { self.eval(right, depth) } |
| 334 | + } |
| 335 | + Expr::Binary(op, left, right) => binary(*op, self.eval(left, depth).get(), self.eval(right, depth).get()), |
| 336 | + Expr::Ternary(condition, then, otherwise) => { |
| 337 | + let condition = self.eval(condition, depth); |
| 338 | + match condition.get() { |
| 339 | + Value::Pending => Value::Pending, |
| 340 | + Value::Error(error) => Value::Error(error.clone()), |
| 341 | + value if value.truthy() => self.eval(then, depth), |
| 342 | + _ => self.eval(otherwise, depth), |
| 343 | + } |
| 344 | + } |
| 345 | + } |
| 346 | + } |
| 347 | + |
| 348 | + fn action(&self, args: &[Expr]) -> Value { |
| 349 | + let steps = match args.first() { |
| 350 | + Some(Expr::Array(items)) => items.iter().map(step).collect(), |
| 351 | + Some(single) => vec![step(single)], |
| 352 | + None => Vec::new(), |
| 353 | + }; |
| 354 | + Value::Action(Arc::new(Action { steps, locals: self.locals.borrow().clone() })) |
| 355 | + } |
| 356 | + |
| 357 | + fn builtin(&self, name: &str, args: &[Expr], depth: usize) -> Value { |
| 358 | + // Action steps used on their own, as `Button("Go", @ToAssistant("go"))`. |
| 359 | + if matches!(name, "Set" | "Reset" | "ToAssistant" | "OpenUrl" | "Run") { |
| 360 | + return Value::Action(Arc::new(Action { |
| 361 | + steps: vec![step(&Expr::Builtin(name.to_string(), args.to_vec()))], |
| 362 | + locals: self.locals.borrow().clone(), |
| 363 | + })); |
| 364 | + } |
| 365 | + if name == "Each" { |
| 366 | + return self.each(args, depth); |
| 367 | + } |
| 368 | + let values: Vec<Value> = args.iter().map(|arg| self.eval(arg, depth).get().clone()).collect(); |
| 369 | + if values.iter().any(|value| value == &Value::Pending) { |
| 370 | + return Value::Pending; |
| 371 | + } |
| 372 | + if let Some(error) = values.iter().find_map(|value| match value { |
| 373 | + Value::Error(error) => Some(error.clone()), |
| 374 | + _ => None, |
| 375 | + }) { |
| 376 | + return Value::Error(error); |
| 377 | + } |
| 378 | + let arg = |ix: usize| values.get(ix).unwrap_or(&Value::Null); |
| 379 | + let array = || arg(0).as_array().map(<[Value]>::to_vec); |
| 380 | + let numbers = || array().map(|items| items.iter().filter_map(Value::as_number).collect::<Vec<f64>>()); |
| 381 | + let number = || arg(0).as_number(); |
| 382 | + let need = |what: &str| Value::Error(format!("@{name} needs {what}")); |
| 383 | + match name { |
| 384 | + "Count" => match arg(0) { |
| 385 | + Value::Array(items) => Value::Number(items.len() as f64), |
| 386 | + Value::String(text) => Value::Number(text.chars().count() as f64), |
| 387 | + Value::Null => Value::Number(0.), |
| 388 | + _ => need("an array"), |
| 389 | + }, |
| 390 | + "Sum" => numbers().map(|numbers| Value::Number(numbers.iter().sum())).unwrap_or_else(|| need("an array")), |
| 391 | + "Avg" => match numbers() { |
| 392 | + Some(numbers) if numbers.is_empty() => Value::Null, |
| 393 | + Some(numbers) => Value::Number(numbers.iter().sum::<f64>() / numbers.len() as f64), |
| 394 | + None => need("an array"), |
| 395 | + }, |
| 396 | + "Min" => match numbers() { |
| 397 | + Some(numbers) => numbers.into_iter().reduce(f64::min).map(Value::Number).unwrap_or(Value::Null), |
| 398 | + None => need("an array"), |
| 399 | + }, |
| 400 | + "Max" => match numbers() { |
| 401 | + Some(numbers) => numbers.into_iter().reduce(f64::max).map(Value::Number).unwrap_or(Value::Null), |
| 402 | + None => need("an array"), |
| 403 | + }, |
| 404 | + "First" => array().map(|items| items.first().cloned().unwrap_or(Value::Null)).unwrap_or_else(|| need("an array")), |
| 405 | + "Last" => array().map(|items| items.last().cloned().unwrap_or(Value::Null)).unwrap_or_else(|| need("an array")), |
| 406 | + "Filter" => { |
| 407 | + let Some(items) = array() else { return need("an array") }; |
| 408 | + let field = arg(1).text(); |
| 409 | + let op = arg(2).text(); |
| 410 | + let wanted = arg(3); |
| 411 | + let mut kept = Vec::new(); |
| 412 | + for item in items { |
| 413 | + let value = member(&item, &field); |
| 414 | + let keep = match op.as_str() { |
| 415 | + "contains" => value.text().to_lowercase().contains(&wanted.text().to_lowercase()), |
| 416 | + "==" => equals(&value, wanted), |
| 417 | + "!=" => !equals(&value, wanted), |
| 418 | + ">" | "<" | ">=" | "<=" => { |
| 419 | + let op = match op.as_str() { |
| 420 | + ">" => BinaryOp::Gt, |
| 421 | + "<" => BinaryOp::Lt, |
| 422 | + ">=" => BinaryOp::Ge, |
| 423 | + _ => BinaryOp::Le, |
| 424 | + }; |
| 425 | + binary(op, &value, wanted).truthy() |
| 426 | + } |
| 427 | + _ => return Value::Error(format!("@Filter doesn't know the operator {op:?}")), |
| 428 | + }; |
| 429 | + if keep { |
| 430 | + kept.push(item); |
| 431 | + } |
| 432 | + } |
| 433 | + Value::Array(kept) |
| 434 | + } |
| 435 | + "Sort" => { |
| 436 | + let Some(mut items) = array() else { return need("an array") }; |
| 437 | + let field = arg(1).text(); |
| 438 | + let descending = arg(2).text().eq_ignore_ascii_case("desc"); |
| 439 | + items.sort_by(|a, b| { |
| 440 | + let (a, b) = if field.is_empty() { (a.clone(), b.clone()) } else { (member(a, &field), member(b, &field)) }; |
| 441 | + let order = match (a.as_number(), b.as_number(), &a, &b) { |
| 442 | + (_, _, Value::String(a), Value::String(b)) => a.cmp(b), |
| 443 | + (Some(a), Some(b), _, _) => a.total_cmp(&b), |
| 444 | + _ => a.text().cmp(&b.text()), |
| 445 | + }; |
| 446 | + if descending { order.reverse() } else { order } |
| 447 | + }); |
| 448 | + Value::Array(items) |
| 449 | + } |
| 450 | + "Round" => match number() { |
| 451 | + Some(value) => { |
| 452 | + let factor = 10f64.powi(arg(1).as_number().unwrap_or(0.).clamp(0., 12.) as i32); |
| 453 | + Value::Number((value * factor).round() / factor) |
| 454 | + } |
| 455 | + None => need("a number"), |
| 456 | + }, |
| 457 | + "Abs" => number().map(|value| Value::Number(value.abs())).unwrap_or_else(|| need("a number")), |
| 458 | + "Floor" => number().map(|value| Value::Number(value.floor())).unwrap_or_else(|| need("a number")), |
| 459 | + "Ceil" => number().map(|value| Value::Number(value.ceil())).unwrap_or_else(|| need("a number")), |
| 460 | + _ => Value::Error(format!("Unknown function @{name}")), |
| 461 | + } |
| 462 | + } |
| 463 | + |
| 464 | + /// `@Each(array, "item", template)`: the template once per element, with |
| 465 | + /// the element bound to the name. |
| 466 | + fn each(&self, args: &[Expr], depth: usize) -> Value { |
| 467 | + let [array, name, template] = args else { |
| 468 | + return Value::Error("@Each needs an array, a name and a template".into()); |
| 469 | + }; |
| 470 | + let array = self.eval(array, depth); |
| 471 | + let items = match array.get() { |
| 472 | + Value::Pending => return Value::Pending, |
| 473 | + Value::Error(error) => return Value::Error(error.clone()), |
| 474 | + Value::Null => return Value::Array(Vec::new()), |
| 475 | + Value::Array(items) => items.clone(), |
| 476 | + other => vec![other.clone()], |
| 477 | + }; |
| 478 | + // The name is a string, but models also write it bare. |
| 479 | + let name = match name { |
| 480 | + Expr::String(name) | Expr::Ref(name) => name.clone(), |
| 481 | + _ => return Value::Error("@Each needs a name for the loop variable".into()), |
| 482 | + }; |
| 483 | + Value::Array( |
| 484 | + items |
| 485 | + .into_iter() |
| 486 | + .map(|item| { |
| 487 | + self.locals.borrow_mut().push((name.clone(), item)); |
| 488 | + let value = self.eval(template, depth); |
| 489 | + self.locals.borrow_mut().pop(); |
| 490 | + value |
| 491 | + }) |
| 492 | + .collect(), |
| 493 | + ) |
| 494 | + } |
| 495 | + } |
| 496 | + |
| 497 | + /// An action step from its expression; anything but a known step builtin |
| 498 | + /// is unsupported. |
| 499 | + fn step(expr: &Expr) -> Step { |
| 500 | + let Expr::Builtin(name, args) = expr else { |
| 501 | + return Step::Unsupported("a step that isn't a @function".into()); |
| 502 | + }; |
| 503 | + let state_names = || { |
| 504 | + args.iter() |
| 505 | + .filter_map(|arg| match arg { |
| 506 | + Expr::State(name) | Expr::String(name) => Some(name.trim_start_matches('$').to_string()), |
| 507 | + _ => None, |
| 508 | + }) |
| 509 | + .collect() |
| 510 | + }; |
| 511 | + match (name.as_str(), args.as_slice()) { |
| 512 | + ("Set", [Expr::State(var), value]) => Step::Set(var.clone(), value.clone()), |
| 513 | + ("Reset", _) => Step::Reset(state_names()), |
| 514 | + ("ToAssistant", [message, ..]) => Step::ToAssistant(message.clone()), |
| 515 | + ("OpenUrl", [url, ..]) => Step::OpenUrl(url.clone()), |
| 516 | + _ => Step::Unsupported(format!("@{name}")), |
| 517 | + } |
| 518 | + } |
| 519 | + |
| 520 | + /// `value.field`: a field of an object, the field of every element of an |
| 521 | + /// array, or an index into an array. |
| 522 | + fn member(value: &Value, field: &str) -> Value { |
| 523 | + match value.get() { |
| 524 | + Value::Pending => Value::Pending, |
| 525 | + Value::Error(error) => Value::Error(error.clone()), |
| 526 | + Value::Object(fields) => fields.get(field).cloned().unwrap_or(Value::Null), |
| 527 | + Value::Array(items) => match field.parse::<usize>() { |
| 528 | + Ok(index) => items.get(index).cloned().unwrap_or(Value::Null), |
| 529 | + Err(_) if field == "length" => Value::Number(items.len() as f64), |
| 530 | + Err(_) => Value::Array(items.iter().map(|item| member(item, field)).collect()), |
| 531 | + }, |
| 532 | + Value::String(text) if field == "length" => Value::Number(text.chars().count() as f64), |
| 533 | + _ => Value::Null, |
| 534 | + } |
| 535 | + } |
| 536 | + |
| 537 | + fn binary(op: BinaryOp, left: &Value, right: &Value) -> Value { |
| 538 | + match (left, right) { |
| 539 | + (Value::Pending, _) | (_, Value::Pending) => return Value::Pending, |
| 540 | + (Value::Error(error), _) | (_, Value::Error(error)) => return Value::Error(error.clone()), |
| 541 | + _ => {} |
| 542 | + } |
| 543 | + let numbers = || match (left.as_number(), right.as_number()) { |
| 544 | + (Some(a), Some(b)) if !matches!(left, Value::Null | Value::Array(_)) => Some((a, b)), |
| 545 | + _ => None, |
| 546 | + }; |
| 547 | + match op { |
| 548 | + BinaryOp::Add => match (left, right) { |
| 549 | + (Value::String(_), _) | (_, Value::String(_)) => Value::String(left.text() + &right.text()), |
| 550 | + (Value::Number(a), Value::Number(b)) => Value::Number(a + b), |
| 551 | + (Value::Array(a), Value::Array(b)) => Value::Array(a.iter().chain(b).cloned().collect()), |
| 552 | + _ => match numbers() { |
| 553 | + Some((a, b)) => Value::Number(a + b), |
| 554 | + None => Value::Error(format!("Can't add {} and {}", describe(left), describe(right))), |
| 555 | + }, |
| 556 | + }, |
| 557 | + BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem => { |
| 558 | + let Some((a, b)) = numbers() else { |
| 559 | + return Value::Error(format!("Can't do arithmetic on {} and {}", describe(left), describe(right))); |
| 560 | + }; |
| 561 | + Value::Number(match op { |
| 562 | + BinaryOp::Sub => a - b, |
| 563 | + BinaryOp::Mul => a * b, |
| 564 | + BinaryOp::Div => a / b, |
| 565 | + _ => a % b, |
| 566 | + }) |
| 567 | + } |
| 568 | + BinaryOp::Eq => Value::Bool(equals(left, right)), |
| 569 | + BinaryOp::Ne => Value::Bool(!equals(left, right)), |
| 570 | + BinaryOp::Gt | BinaryOp::Lt | BinaryOp::Ge | BinaryOp::Le => { |
| 571 | + let order = match (left, right) { |
| 572 | + (Value::String(a), Value::String(b)) => Some(a.cmp(b)), |
| 573 | + _ => numbers().and_then(|(a, b)| a.partial_cmp(&b)), |
| 574 | + }; |
| 575 | + Value::Bool(order.is_some_and(|order| match op { |
| 576 | + BinaryOp::Gt => order.is_gt(), |
| 577 | + BinaryOp::Lt => order.is_lt(), |
| 578 | + BinaryOp::Ge => order.is_ge(), |
| 579 | + _ => order.is_le(), |
| 580 | + })) |
| 581 | + } |
| 582 | + BinaryOp::And | BinaryOp::Or => unreachable!("short-circuited in `eval`"), |
| 583 | + } |
| 584 | + } |
| 585 | + |
| 586 | + /// Equality that lets a number equal its text, since select and input |
| 587 | + /// values arrive as strings. |
| 588 | + fn equals(left: &Value, right: &Value) -> bool { |
| 589 | + match (left.get(), right.get()) { |
| 590 | + (Value::Number(a), Value::String(b)) | (Value::String(b), Value::Number(a)) => b.trim().parse::<f64>() == Ok(*a), |
| 591 | + (a, b) => a == b, |
| 592 | + } |
| 593 | + } |
| 594 | + |
| 595 | + fn describe(value: &Value) -> &'static str { |
| 596 | + match value.get() { |
| 597 | + Value::Null => "null", |
| 598 | + Value::Bool(_) => "a boolean", |
| 599 | + Value::Number(_) => "a number", |
| 600 | + Value::String(_) => "a string", |
| 601 | + Value::Array(_) => "an array", |
| 602 | + Value::Object(_) => "an object", |
| 603 | + Value::Component { .. } => "a component", |
| 604 | + Value::Action(_) => "an action", |
| 605 | + Value::Pending => "a pending value", |
| 606 | + Value::Error(_) => "an error", |
| 607 | + Value::State(..) => "state", |
| 608 | + } |
| 609 | + } |
| 610 | + |
| 611 | + #[cfg(test)] |
| 612 | + mod tests { |
| 613 | + use super::super::lang::parse; |
| 614 | + use super::{Effect, State, Value, evaluate, run, state_value}; |
| 615 | + |
| 616 | + fn eval(source: &str) -> Value { |
| 617 | + let program = parse(source); |
| 618 | + assert!(program.errors.is_empty(), "{source}: {:?}", program.errors); |
| 619 | + evaluate(&program, &State::new()) |
| 620 | + } |
| 621 | + |
| 622 | + fn n(number: f64) -> Value { |
| 623 | + Value::Number(number) |
| 624 | + } |
| 625 | + |
| 626 | + fn s(text: &str) -> Value { |
| 627 | + Value::String(text.into()) |
| 628 | + } |
| 629 | + |
| 630 | + #[::core::prelude::v1::test] |
| 631 | + fn arithmetic_and_logic() { |
| 632 | + assert_eq!(eval("x = 1 + 2 * 3"), n(7.)); |
| 633 | + assert_eq!(eval("x = 10 % 4 - -1"), n(3.)); |
| 634 | + assert_eq!(eval(r#"x = "" + 7 + " days""#), s("7 days")); |
| 635 | + assert_eq!(eval(r#"x = 1 + 2 + "a""#), s("3a")); |
| 636 | + assert_eq!(eval("x = 2.5 * 2 == 5 && 3 > 2"), Value::Bool(true)); |
| 637 | + assert_eq!(eval(r#"x = null || "fallback""#), s("fallback")); |
| 638 | + assert_eq!(eval(r#"x = "7" == 7"#), Value::Bool(true)); |
| 639 | + assert_eq!(eval(r#"x = "b" > "a" ? "yes" : "no""#), s("yes")); |
| 640 | + assert!(matches!(eval(r#"x = {a: 1} - 1"#), Value::Error(_))); |
| 641 | + assert_eq!(super::format_number(0.1 + 0.2), "0.30000000000000004"); |
| 642 | + assert_eq!(eval("x = 1.5").text(), "1.5"); |
| 643 | + assert_eq!(eval("x = 3").text(), "3"); |
| 644 | + } |
| 645 | + |
| 646 | + #[::core::prelude::v1::test] |
| 647 | + fn forward_references_and_pluck() { |
| 648 | + let value = eval( |
| 649 | + r#"root = Table([Col("Title", data.rows.title), Col("Total", @Sum(data.rows.n))]) |
| 650 | + data = {rows: [{title: "a", n: 2}, {title: "b", n: 3}]}"#, |
| 651 | + ); |
| 652 | + let Value::Component { name, args } = value else { panic!("{value:?}") }; |
| 653 | + assert_eq!(name, "Table"); |
| 654 | + let Value::Array(columns) = &args[0] else { panic!() }; |
| 655 | + assert_eq!( |
| 656 | + columns[0], |
| 657 | + Value::Component { name: "Col".into(), args: vec![s("Title"), Value::Array(vec![s("a"), s("b")])] } |
| 658 | + ); |
| 659 | + assert_eq!(columns[1], Value::Component { name: "Col".into(), args: vec![s("Total"), n(5.)] }); |
| 660 | + assert_eq!(eval("x = [[1, 2], [3]].0.1"), n(2.)); |
| 661 | + assert_eq!(eval("x = [1, 2, 3].length"), n(3.)); |
| 662 | + } |
| 663 | + |
| 664 | + #[::core::prelude::v1::test] |
| 665 | + fn pending_and_cycles() { |
| 666 | + assert_eq!(eval("root = Stack([later])"), Value::Component { name: "Stack".into(), args: vec![Value::Array(vec![Value::Pending])] }); |
| 667 | + assert!(eval("root = Stack([later])").is_pending()); |
| 668 | + assert_eq!(eval("x = missing + 1"), Value::Pending); |
| 669 | + let value = eval("a = Stack([b])\nb = Stack([a])"); |
| 670 | + let text = format!("{value:?}"); |
| 671 | + assert!(text.contains("refers to itself"), "{text}"); |
| 672 | + let value = eval("a = a + 1"); |
| 673 | + assert!(matches!(value, Value::Error(_)), "{value:?}"); |
| 674 | + // A long chain hits the depth limit instead of the stack. |
| 675 | + let chain: String = (0..200).map(|ix| format!("s{ix} = s{}\n", ix + 1)).collect(); |
| 676 | + assert!(matches!(eval(&chain), Value::Error(_))); |
| 677 | + } |
| 678 | + |
| 679 | + #[::core::prelude::v1::test] |
| 680 | + fn builtins() { |
| 681 | + let data = r#"rows = [{name: "b", n: 3}, {name: "a", n: 1}, {name: "c", n: 2}]"#; |
| 682 | + let with = |expr: &str| eval(&format!("x = {expr}\n{data}")); |
| 683 | + assert_eq!(with("@Count(rows)"), n(3.)); |
| 684 | + assert_eq!(with("@Sum(rows.n)"), n(6.)); |
| 685 | + assert_eq!(with("@Avg(rows.n)"), n(2.)); |
| 686 | + assert_eq!(with("@Min(rows.n)"), n(1.)); |
| 687 | + assert_eq!(with("@Max(rows.n)"), n(3.)); |
| 688 | + assert_eq!(with("@First(rows).name"), s("b")); |
| 689 | + assert_eq!(with("@Last(rows.name)"), s("c")); |
| 690 | + assert_eq!(with(r#"@Sort(rows, "n").name"#), Value::Array(vec![s("a"), s("c"), s("b")])); |
| 691 | + assert_eq!(with(r#"@Sort(rows, "name", "desc").name"#), Value::Array(vec![s("c"), s("b"), s("a")])); |
| 692 | + assert_eq!(with(r#"@Filter(rows, "n", ">=", 2).name"#), Value::Array(vec![s("b"), s("c")])); |
| 693 | + assert_eq!(with(r#"@Filter(rows, "name", "contains", "A").n"#), Value::Array(vec![n(1.)])); |
| 694 | + assert_eq!(with(r#"@Filter(rows, "n", "==", "3").name"#), Value::Array(vec![s("b")])); |
| 695 | + assert_eq!(eval("x = @Round(3.14159, 2)"), n(3.14)); |
| 696 | + assert_eq!(eval("x = @Round(2.5)"), n(3.)); |
| 697 | + assert_eq!(eval("x = @Abs(-2) + @Floor(1.7) + @Ceil(1.2)"), n(5.)); |
| 698 | + assert!(matches!(eval("x = @Nope(1)"), Value::Error(_))); |
| 699 | + assert_eq!(eval("x = @Sum(later)"), Value::Pending); |
| 700 | + } |
| 701 | + |
| 702 | + #[::core::prelude::v1::test] |
| 703 | + fn each() { |
| 704 | + let value = eval(r#"root = @Each(["a", "b"], "item", Tag(item + "!"))"#); |
| 705 | + assert_eq!( |
| 706 | + value, |
| 707 | + Value::Array(vec![ |
| 708 | + Value::Component { name: "Tag".into(), args: vec![s("a!")] }, |
| 709 | + Value::Component { name: "Tag".into(), args: vec![s("b!")] }, |
| 710 | + ]) |
| 711 | + ); |
| 712 | + // Loop variables don't leak into other statements. |
| 713 | + let value = eval(r#"root = @Each([1], "item", other) |
| 714 | + other = item"#); |
| 715 | + assert_eq!(value, Value::Array(vec![Value::Pending])); |
| 716 | + } |
| 717 | + |
| 718 | + #[::core::prelude::v1::test] |
| 719 | + fn state_and_actions() { |
| 720 | + let program = parse( |
| 721 | + r#"root = Stack([TextContent("Count: " + $count), Input($name), plus, reset]) |
| 722 | + $count = 0 |
| 723 | + $name = "Ada" |
| 724 | + plus = Button("+", Action([@Set($count, $count + 1), @Set($count, $count + 1), @ToAssistant("now " + $count)])) |
| 725 | + reset = Button("Reset", Action([@Reset($count, $name), @OpenUrl("https://example.com"), @Run(query)]))"#, |
| 726 | + ); |
| 727 | + assert!(program.errors.is_empty(), "{:?}", program.errors); |
| 728 | + let mut state = State::new(); |
| 729 | + let Value::Component { args, .. } = evaluate(&program, &state) else { panic!() }; |
| 730 | + let Value::Array(children) = &args[0] else { panic!() }; |
| 731 | + assert_eq!(children[0], Value::Component { name: "TextContent".into(), args: vec![s("Count: 0")] }); |
| 732 | + assert_eq!(children[1], Value::Component { name: "Input".into(), args: vec![Value::State("name".into(), Box::new(s("Ada")))] }); |
| 733 | + let action = |child: &Value| match child { |
| 734 | + Value::Component { args, .. } => match &args[1] { |
| 735 | + Value::Action(action) => action.clone(), |
| 736 | + other => panic!("{other:?}"), |
| 737 | + }, |
| 738 | + other => panic!("{other:?}"), |
| 739 | + }; |
| 740 | + let effects = run(&program, &mut state, &action(&children[2])); |
| 741 | + assert_eq!(state_value(&program, &state, "count"), n(2.)); |
| 742 | + assert_eq!(effects, vec![Effect::ToAssistant("now 2".into())]); |
| 743 | + state.insert("name".into(), s("Grace")); |
| 744 | + let effects = run(&program, &mut state, &action(&children[3])); |
| 745 | + assert!(state.is_empty()); |
| 746 | + assert_eq!(state_value(&program, &state, "name"), s("Ada")); |
| 747 | + assert_eq!(effects, vec![Effect::OpenUrl("https://example.com".into()), Effect::Unsupported("@Run".into())]); |
| 748 | + } |
| 749 | + |
| 750 | + #[::core::prelude::v1::test] |
| 751 | + fn actions_keep_loop_variables() { |
| 752 | + let program = parse(r#"root = @Each(["x", "y"], "item", Button(item, @ToAssistant("picked " + item)))"#); |
| 753 | + let Value::Array(buttons) = evaluate(&program, &State::new()) else { panic!() }; |
| 754 | + let Value::Component { args, .. } = &buttons[1] else { panic!() }; |
| 755 | + let Value::Action(action) = &args[1] else { panic!() }; |
| 756 | + assert_eq!(run(&program, &mut State::new(), action), vec![Effect::ToAssistant("picked y".into())]); |
| 757 | + } |
| 758 | + |
| 759 | + #[::core::prelude::v1::test] |
| 760 | + fn errors_stay_local() { |
| 761 | + let value = eval(r#"root = Stack([TextContent("ok"), TextContent(1 - "x")])"#); |
| 762 | + let Value::Component { args, .. } = value else { panic!() }; |
| 763 | + let Value::Array(children) = &args[0] else { panic!() }; |
| 764 | + assert_eq!(children[0], Value::Component { name: "TextContent".into(), args: vec![s("ok")] }); |
| 765 | + assert!(matches!(&children[1], Value::Component { args, .. } if matches!(args[0], Value::Error(_)))); |
| 766 | + assert_eq!(Value::Object([("a".to_string(), n(1.))].into()).to_json(), serde_json::json!({"a": 1})); |
| 767 | + } |
| 768 | + } |