Vectrum
Sign In

Parse and evaluate OpenUI Lang

A lexer and parser for the OpenUI Lang v0.5 subset in the spec (statements,
state, component and builtin calls, member pluck, ternaries and C
precedence), and an evaluator that turns a program and its state into a
value tree. Undefined references are pending rather than errors, so a
streaming block renders what it has; cycles, bad lines and bad arithmetic
stay local errors. Actions keep their step expressions and run them
against the state at click time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JjohannSocommitted 1 parent 76d9f81commit96a8712

Showing 4 changed files with 1406 additions and 0 deletions.

+768−0src/inline_ui/eval.rsaddedView File
@@ -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+ }
+628−0src/inline_ui/lang.rsaddedView File
@@ -0,0 +1,628 @@
1+ //! OpenUI Lang's syntax: a lexer and a parser for the subset of v0.5 the app
2+ //! renders. A program is one statement per line, `name = Expression`, with
3+ //! `$name = default` declaring state. Lines inside brackets may wrap, since
4+ //! models like to spread long arrays over several.
5+ //!
6+ //! Spec: <https://www.openui.com/docs/openui-lang/specification-v05>.
7+
8+ use std::fmt;
9+
10+ /// More statements than this is a program no answer should hold.
11+ pub const MAX_STATEMENTS: usize = 500;
12+
13+ #[derive(Clone, Debug, PartialEq)]
14+ pub enum Expr {
15+ Null,
16+ Bool(bool),
17+ Number(f64),
18+ String(String),
19+ Array(Vec<Expr>),
20+ Object(Vec<(String, Expr)>),
21+ /// Another statement, or a loop variable inside `@Each`.
22+ Ref(String),
23+ /// `$name`.
24+ State(String),
25+ /// `Type(args...)`: a component, or `Action([...])`.
26+ Call(String, Vec<Expr>),
27+ /// `@Name(args...)`.
28+ Builtin(String, Vec<Expr>),
29+ /// `a.b`, which plucks `b` from every element when `a` is an array.
30+ Member(Box<Expr>, String),
31+ Unary(UnaryOp, Box<Expr>),
32+ Binary(BinaryOp, Box<Expr>, Box<Expr>),
33+ Ternary(Box<Expr>, Box<Expr>, Box<Expr>),
34+ }
35+
36+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
37+ pub enum UnaryOp {
38+ Not,
39+ Neg,
40+ }
41+
42+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
43+ pub enum BinaryOp {
44+ Add,
45+ Sub,
46+ Mul,
47+ Div,
48+ Rem,
49+ Eq,
50+ Ne,
51+ Gt,
52+ Lt,
53+ Ge,
54+ Le,
55+ And,
56+ Or,
57+ }
58+
59+ #[derive(Clone, Debug, PartialEq)]
60+ pub struct Statement {
61+ pub name: String,
62+ /// Declared as `$name = default`.
63+ pub state: bool,
64+ pub expr: Expr,
65+ /// 1-based, for error messages.
66+ pub line: usize,
67+ }
68+
69+ #[derive(Clone, Debug, Default, PartialEq)]
70+ pub struct Program {
71+ pub statements: Vec<Statement>,
72+ /// Statements that didn't parse, as "Line 3: ..." messages. They're left
73+ /// out of `statements`, so the rest still renders.
74+ pub errors: Vec<String>,
75+ }
76+
77+ impl Program {
78+ /// The statement the UI starts from: `root` if there is one, else the
79+ /// first that isn't a state declaration.
80+ pub fn root(&self) -> Option<&Statement> {
81+ self.statements
82+ .iter()
83+ .find(|statement| !statement.state && statement.name == "root")
84+ .or_else(|| self.statements.iter().find(|statement| !statement.state))
85+ }
86+
87+ pub fn state_default(&self, name: &str) -> Option<&Expr> {
88+ self.statements.iter().rev().find(|statement| statement.state && statement.name == name).map(|statement| &statement.expr)
89+ }
90+ }
91+
92+ #[derive(Clone, Debug, PartialEq)]
93+ enum Token {
94+ Ident(String),
95+ State(String),
96+ Builtin(String),
97+ String(String),
98+ Number(f64),
99+ Punct(&'static str),
100+ /// A line break outside any brackets, which ends a statement.
101+ Newline,
102+ }
103+
104+ impl fmt::Display for Token {
105+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106+ match self {
107+ Token::Ident(name) => write!(f, "`{name}`"),
108+ Token::State(name) => write!(f, "`${name}`"),
109+ Token::Builtin(name) => write!(f, "`@{name}`"),
110+ Token::String(_) => write!(f, "a string"),
111+ Token::Number(number) => write!(f, "`{number}`"),
112+ Token::Punct(punct) => write!(f, "`{punct}`"),
113+ Token::Newline => write!(f, "the end of the line"),
114+ }
115+ }
116+ }
117+
118+ /// Punctuation, longest first so `==` wins over `=`.
119+ const PUNCTS: [&str; 24] = [
120+ "==", "!=", ">=", "<=", "&&", "||", "(", ")", "[", "]", "{", "}", ",", ":", ".", "?", "+", "-", "*", "/", "%", ">", "<",
121+ "!",
122+ ];
123+
124+ /// Splits source into tokens with their line numbers. A lexing error ends
125+ /// the token stream there, with the error at that position.
126+ fn lex(source: &str) -> (Vec<(Token, usize)>, Option<(String, usize)>) {
127+ let mut tokens = Vec::new();
128+ let mut chars = source.char_indices().peekable();
129+ let mut line = 1;
130+ let mut depth = 0usize;
131+ while let Some(&(at, c)) = chars.peek() {
132+ match c {
133+ '\n' => {
134+ chars.next();
135+ if depth == 0 && !matches!(tokens.last(), Some((Token::Newline, _)) | None) {
136+ tokens.push((Token::Newline, line));
137+ }
138+ line += 1;
139+ }
140+ c if c.is_whitespace() => {
141+ chars.next();
142+ }
143+ '/' if source[at..].starts_with("//") => {
144+ while chars.next_if(|&(_, c)| c != '\n').is_some() {}
145+ }
146+ '"' | '\'' => {
147+ chars.next();
148+ let quote = c;
149+ let mut text = String::new();
150+ loop {
151+ match chars.next() {
152+ None => return (tokens, Some(("unterminated string".into(), line))),
153+ Some((_, '\n')) if quote == '\'' => return (tokens, Some(("unterminated string".into(), line))),
154+ Some((_, '\n')) => {
155+ line += 1;
156+ text.push('\n');
157+ }
158+ Some((_, c)) if c == quote => break,
159+ Some((_, '\\')) => match chars.next() {
160+ Some((_, 'n')) => text.push('\n'),
161+ Some((_, 't')) => text.push('\t'),
162+ Some((_, 'r')) => text.push('\r'),
163+ Some((_, 'u')) => {
164+ let hex: String = (0..4).filter_map(|_| chars.next_if(|(_, c)| c.is_ascii_hexdigit())).map(|(_, c)| c).collect();
165+ match u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
166+ Some(c) => text.push(c),
167+ None => return (tokens, Some(("bad \\u escape".into(), line))),
168+ }
169+ }
170+ Some((_, c)) => text.push(c),
171+ None => return (tokens, Some(("unterminated string".into(), line))),
172+ },
173+ Some((_, c)) => text.push(c),
174+ }
175+ }
176+ tokens.push((Token::String(text), line));
177+ }
178+ c if c.is_ascii_digit() => {
179+ let mut end = at;
180+ // After `.` a number is an index, so `rows.0.1` is two of them.
181+ let mut seen_dot = matches!(tokens.last(), Some((Token::Punct("."), _)));
182+ while let Some(&(ix, c)) = chars.peek() {
183+ let digit_follows = source[ix + 1..].starts_with(|c: char| c.is_ascii_digit());
184+ if c.is_ascii_digit() || c == '_' || (c == '.' && !seen_dot && digit_follows) {
185+ seen_dot |= c == '.';
186+ end = ix + c.len_utf8();
187+ chars.next();
188+ } else {
189+ break;
190+ }
191+ }
192+ let text = source[at..end].replace('_', "");
193+ match text.parse::<f64>() {
194+ Ok(number) => tokens.push((Token::Number(number), line)),
195+ Err(_) => return (tokens, Some((format!("bad number {text}"), line))),
196+ }
197+ }
198+ '$' | '@' => {
199+ chars.next();
200+ let name = ident(&mut chars);
201+ if name.is_empty() {
202+ return (tokens, Some((format!("expected a name after `{c}`"), line)));
203+ }
204+ tokens.push((if c == '$' { Token::State(name) } else { Token::Builtin(name) }, line));
205+ }
206+ c if c.is_alphabetic() || c == '_' => {
207+ let name = ident(&mut chars);
208+ tokens.push((Token::Ident(name), line));
209+ }
210+ _ => {
211+ let Some(punct) = PUNCTS.iter().find(|punct| source[at..].starts_with(**punct)) else {
212+ // `=` alone only separates a statement's name from its value.
213+ if c == '=' {
214+ chars.next();
215+ tokens.push((Token::Punct("="), line));
216+ continue;
217+ }
218+ return (tokens, Some((format!("unexpected `{c}`"), line)));
219+ };
220+ for _ in 0..punct.len() {
221+ chars.next();
222+ }
223+ match *punct {
224+ "(" | "[" | "{" => depth += 1,
225+ ")" | "]" | "}" => depth = depth.saturating_sub(1),
226+ _ => {}
227+ }
228+ tokens.push((Token::Punct(punct), line));
229+ }
230+ }
231+ }
232+ (tokens, None)
233+ }
234+
235+ fn ident(chars: &mut std::iter::Peekable<std::str::CharIndices>) -> String {
236+ let mut name = String::new();
237+ while let Some((_, c)) = chars.next_if(|&(_, c)| c.is_alphanumeric() || c == '_') {
238+ name.push(c);
239+ }
240+ name
241+ }
242+
243+ /// Parses a program. Statements that fail are skipped and reported in
244+ /// `errors`; the rest are kept, so one bad line doesn't blank the UI.
245+ pub fn parse(source: &str) -> Program {
246+ let (tokens, lex_error) = lex(source);
247+ let mut parser = Parser { tokens, pos: 0 };
248+ let mut program = Program::default();
249+ // A lexing error cuts the stream short, so the statement it hit is
250+ // incomplete. Drop it rather than report a misleading parse error.
251+ let lex_error_line = lex_error.as_ref().map(|(_, line)| *line);
252+ while parser.skip_newlines() {
253+ let start_line = parser.line();
254+ match parser.statement() {
255+ Ok(statement) => {
256+ if program.statements.len() >= MAX_STATEMENTS {
257+ program.errors.push(format!("More than {MAX_STATEMENTS} statements"));
258+ break;
259+ }
260+ program.statements.push(statement);
261+ }
262+ Err((message, line)) => {
263+ if parser.at_end() && lex_error_line.is_some() {
264+ break;
265+ }
266+ program.errors.push(format!("Line {line}: {message}"));
267+ parser.skip_statement(start_line);
268+ }
269+ }
270+ }
271+ if let Some((message, line)) = lex_error {
272+ program.errors.push(format!("Line {line}: {message}"));
273+ }
274+ program
275+ }
276+
277+ type ParseResult<T> = Result<T, (String, usize)>;
278+
279+ struct Parser {
280+ tokens: Vec<(Token, usize)>,
281+ pos: usize,
282+ }
283+
284+ impl Parser {
285+ fn peek(&self) -> Option<&Token> {
286+ self.tokens.get(self.pos).map(|(token, _)| token)
287+ }
288+
289+ fn line(&self) -> usize {
290+ self.tokens.get(self.pos).or(self.tokens.last()).map(|(_, line)| *line).unwrap_or(1)
291+ }
292+
293+ fn at_end(&self) -> bool {
294+ self.pos >= self.tokens.len()
295+ }
296+
297+ fn next(&mut self) -> Option<Token> {
298+ let token = self.tokens.get(self.pos).map(|(token, _)| token.clone());
299+ self.pos += 1;
300+ token
301+ }
302+
303+ fn error<T>(&self, message: impl Into<String>) -> ParseResult<T> {
304+ Err((message.into(), self.line()))
305+ }
306+
307+ fn unexpected<T>(&self, wanted: &str) -> ParseResult<T> {
308+ match self.peek() {
309+ Some(token) => self.error(format!("expected {wanted}, found {token}")),
310+ None => self.error(format!("expected {wanted}, found the end")),
311+ }
312+ }
313+
314+ fn eat(&mut self, punct: &str) -> bool {
315+ if matches!(self.peek(), Some(Token::Punct(p)) if *p == punct) {
316+ self.pos += 1;
317+ true
318+ } else {
319+ false
320+ }
321+ }
322+
323+ fn expect(&mut self, punct: &str) -> ParseResult<()> {
324+ if self.eat(punct) { Ok(()) } else { self.unexpected(&format!("`{punct}`")) }
325+ }
326+
327+ /// Skips blank lines; returns whether a statement follows.
328+ fn skip_newlines(&mut self) -> bool {
329+ while matches!(self.peek(), Some(Token::Newline)) {
330+ self.pos += 1;
331+ }
332+ !self.at_end()
333+ }
334+
335+ /// After an error, moves on to the next line after the statement's start.
336+ fn skip_statement(&mut self, start_line: usize) {
337+ while let Some((token, line)) = self.tokens.get(self.pos) {
338+ if *token == Token::Newline && *line >= start_line {
339+ break;
340+ }
341+ self.pos += 1;
342+ }
343+ }
344+
345+ fn statement(&mut self) -> ParseResult<Statement> {
346+ let line = self.line();
347+ let (name, state) = match self.next() {
348+ Some(Token::Ident(name)) => (name, false),
349+ Some(Token::State(name)) => (name, true),
350+ _ => {
351+ self.pos -= 1;
352+ return self.unexpected("a statement like `name = ...`");
353+ }
354+ };
355+ self.expect("=")?;
356+ let expr = self.expr()?;
357+ match self.peek() {
358+ None | Some(Token::Newline) => Ok(Statement { name, state, expr, line }),
359+ Some(_) => self.unexpected("the end of the line"),
360+ }
361+ }
362+
363+ fn expr(&mut self) -> ParseResult<Expr> {
364+ let condition = self.binary(0)?;
365+ if self.eat("?") {
366+ let then = self.expr()?;
367+ self.expect(":")?;
368+ let otherwise = self.expr()?;
369+ return Ok(Expr::Ternary(Box::new(condition), Box::new(then), Box::new(otherwise)));
370+ }
371+ Ok(condition)
372+ }
373+
374+ /// Binary operators by precedence climbing, loosest first, as in C and JS.
375+ fn binary(&mut self, level: usize) -> ParseResult<Expr> {
376+ const LEVELS: [&[(&str, BinaryOp)]; 6] = [
377+ &[("||", BinaryOp::Or)],
378+ &[("&&", BinaryOp::And)],
379+ &[("==", BinaryOp::Eq), ("!=", BinaryOp::Ne)],
380+ &[(">=", BinaryOp::Ge), ("<=", BinaryOp::Le), (">", BinaryOp::Gt), ("<", BinaryOp::Lt)],
381+ &[("+", BinaryOp::Add), ("-", BinaryOp::Sub)],
382+ &[("*", BinaryOp::Mul), ("/", BinaryOp::Div), ("%", BinaryOp::Rem)],
383+ ];
384+ let Some(ops) = LEVELS.get(level) else {
385+ return self.unary();
386+ };
387+ let mut left = self.binary(level + 1)?;
388+ 'outer: loop {
389+ for (punct, op) in *ops {
390+ if self.eat(punct) {
391+ let right = self.binary(level + 1)?;
392+ left = Expr::Binary(*op, Box::new(left), Box::new(right));
393+ continue 'outer;
394+ }
395+ }
396+ return Ok(left);
397+ }
398+ }
399+
400+ fn unary(&mut self) -> ParseResult<Expr> {
401+ if self.eat("!") {
402+ return Ok(Expr::Unary(UnaryOp::Not, Box::new(self.unary()?)));
403+ }
404+ if self.eat("-") {
405+ return Ok(match self.unary()? {
406+ Expr::Number(number) => Expr::Number(-number),
407+ expr => Expr::Unary(UnaryOp::Neg, Box::new(expr)),
408+ });
409+ }
410+ self.postfix()
411+ }
412+
413+ fn postfix(&mut self) -> ParseResult<Expr> {
414+ let mut expr = self.primary()?;
415+ while self.eat(".") {
416+ let field = match self.next() {
417+ Some(Token::Ident(field)) => field,
418+ Some(Token::Number(index)) if index.fract() == 0. && index >= 0. => format!("{index}"),
419+ _ => {
420+ self.pos -= 1;
421+ return self.unexpected("a field name after `.`");
422+ }
423+ };
424+ expr = Expr::Member(Box::new(expr), field);
425+ }
426+ Ok(expr)
427+ }
428+
429+ fn primary(&mut self) -> ParseResult<Expr> {
430+ let Some(token) = self.next() else {
431+ return self.error("expected a value, found the end");
432+ };
433+ Ok(match token {
434+ Token::Number(number) => Expr::Number(number),
435+ Token::String(text) => Expr::String(text),
436+ Token::State(name) => Expr::State(name),
437+ Token::Ident(name) => match name.as_str() {
438+ "true" => Expr::Bool(true),
439+ "false" => Expr::Bool(false),
440+ "null" => Expr::Null,
441+ _ if self.eat("(") => Expr::Call(name, self.list(")")?),
442+ _ => Expr::Ref(name),
443+ },
444+ Token::Builtin(name) => {
445+ self.expect("(")?;
446+ Expr::Builtin(name, self.list(")")?)
447+ }
448+ Token::Punct("[") => Expr::Array(self.list("]")?),
449+ Token::Punct("{") => Expr::Object(self.object()?),
450+ Token::Punct("(") => {
451+ let expr = self.expr()?;
452+ self.expect(")")?;
453+ expr
454+ }
455+ token => {
456+ self.pos -= 1;
457+ return self.error(format!("expected a value, found {token}"));
458+ }
459+ })
460+ }
461+
462+ /// Comma-separated expressions up to `close`, allowing a trailing comma.
463+ fn list(&mut self, close: &str) -> ParseResult<Vec<Expr>> {
464+ let mut items = Vec::new();
465+ loop {
466+ if self.eat(close) {
467+ return Ok(items);
468+ }
469+ items.push(self.expr()?);
470+ if !self.eat(",") {
471+ self.expect(close)?;
472+ return Ok(items);
473+ }
474+ }
475+ }
476+
477+ fn object(&mut self) -> ParseResult<Vec<(String, Expr)>> {
478+ let mut fields = Vec::new();
479+ loop {
480+ if self.eat("}") {
481+ return Ok(fields);
482+ }
483+ let key = match self.next() {
484+ Some(Token::Ident(key) | Token::String(key)) => key,
485+ _ => {
486+ self.pos -= 1;
487+ return self.unexpected("a key");
488+ }
489+ };
490+ self.expect(":")?;
491+ fields.push((key, self.expr()?));
492+ if !self.eat(",") {
493+ self.expect("}")?;
494+ return Ok(fields);
495+ }
496+ }
497+ }
498+ }
499+
500+ #[cfg(test)]
501+ mod tests {
502+ use super::{BinaryOp, Expr, parse};
503+
504+ fn single(source: &str) -> Expr {
505+ let program = parse(source);
506+ assert!(program.errors.is_empty(), "{source}: {:?}", program.errors);
507+ program.statements.into_iter().next().map(|statement| statement.expr).unwrap_or(Expr::Null)
508+ }
509+
510+ fn call(name: &str, args: Vec<Expr>) -> Expr {
511+ Expr::Call(name.into(), args)
512+ }
513+
514+ fn string(text: &str) -> Expr {
515+ Expr::String(text.into())
516+ }
517+
518+ #[::core::prelude::v1::test]
519+ fn syntax_page_example() {
520+ let program = parse(
521+ r#"root = Stack([title, form])
522+ title = TextContent("Sign up", "large-heavy")
523+ form = Form("signup", [nameField, emailField], actions)
524+ nameField = FormControl("Name", Input("name", "Your name", "text", ["required"]))
525+ emailField = FormControl("Email", Input("email", "you@example.com", "email", ["required", "email"]))
526+ actions = Buttons([submitBtn, cancelBtn], "row")
527+ submitBtn = Button("Submit", "submit:signup", "primary")
528+ cancelBtn = Button("Cancel", "action:cancel", "secondary")"#,
529+ );
530+ assert!(program.errors.is_empty(), "{:?}", program.errors);
531+ assert_eq!(program.statements.len(), 8);
532+ assert_eq!(program.root().map(|root| root.name.as_str()), Some("root"));
533+ assert_eq!(
534+ program.statements[1].expr,
535+ call("TextContent", vec![string("Sign up"), string("large-heavy")])
536+ );
537+ }
538+
539+ #[::core::prelude::v1::test]
540+ fn spec_examples() {
541+ for source in [
542+ r#"header = CardHeader("Title")"#,
543+ r#"$days = "7""#,
544+ r#"data = Query("tool", {}, {rows: []})"#,
545+ r#"kpi = TextContent("" + data.total)"#,
546+ r#"tbl = Table([Col("Title", data.rows.title)]) // plucks title from each row"#,
547+ r#"submitBtn = Button("Create", Action([@Run(mutation), @Run(query), @Reset($title)]))"#,
548+ r#"x = $show ? form : null"#,
549+ r#"result = Mutation("tool_name", {title: $title})"#,
550+ r#"data = Query("tool_name", {arg: value}, {rows: []}, 30)"#,
551+ r#"s = Select("days", $days, ["7", "30"])"#,
552+ "n = @Count(data.rows)",
553+ ] {
554+ single(source);
555+ }
556+ let program = parse(
557+ r#"root = Card([header, form])
558+ header = CardHeader("Deploy", "Pick a target")
559+ form = Form("deploy", [FormControl("Environment", Select("env", ["staging", "production"]))], Buttons([Button("Deploy", "submit:deploy", "primary")]))"#,
560+ );
561+ assert!(program.errors.is_empty(), "{:?}", program.errors);
562+ assert_eq!(program.statements.len(), 3);
563+ }
564+
565+ #[::core::prelude::v1::test]
566+ fn precedence() {
567+ use BinaryOp::*;
568+ let n = Expr::Number;
569+ let bin = |op, a, b| Expr::Binary(op, Box::new(a), Box::new(b));
570+ assert_eq!(single("x = 1 + 2 * 3"), bin(Add, n(1.), bin(Mul, n(2.), n(3.))));
571+ assert_eq!(single("x = (1 + 2) * 3"), bin(Mul, bin(Add, n(1.), n(2.)), n(3.)));
572+ assert_eq!(single("x = 1 - 2 - 3"), bin(Sub, bin(Sub, n(1.), n(2.)), n(3.)));
573+ assert_eq!(
574+ single("x = 1 < 2 == true && !false || null"),
575+ bin(
576+ Or,
577+ bin(And, bin(Eq, bin(Lt, n(1.), n(2.)), Expr::Bool(true)), Expr::Unary(super::UnaryOp::Not, Box::new(Expr::Bool(false)))),
578+ Expr::Null
579+ )
580+ );
581+ assert_eq!(
582+ single("x = a ? b : c ? d : e"),
583+ Expr::Ternary(
584+ Box::new(Expr::Ref("a".into())),
585+ Box::new(Expr::Ref("b".into())),
586+ Box::new(Expr::Ternary(Box::new(Expr::Ref("c".into())), Box::new(Expr::Ref("d".into())), Box::new(Expr::Ref("e".into()))))
587+ )
588+ );
589+ assert_eq!(single("x = -5"), n(-5.));
590+ assert_eq!(single("x = 10 % 3 * 2"), bin(Mul, bin(Rem, n(10.), n(3.)), n(2.)));
591+ }
592+
593+ #[::core::prelude::v1::test]
594+ fn strings_comments_and_wrapping() {
595+ assert_eq!(single(r#"x = "say \"hi\"\nnow""#), string("say \"hi\"\nnow"));
596+ assert_eq!(single("x = 'single'"), string("single"));
597+ assert_eq!(single(r#"x = "http://example.com" // a comment"#), string("http://example.com"));
598+ let program = parse("// heading\n\nroot = Stack([\n a,\n b,\n])\n\na = Tag(\"x\")\nb = Tag(\"y\")\n");
599+ assert!(program.errors.is_empty(), "{:?}", program.errors);
600+ assert_eq!(program.statements.len(), 3);
601+ assert_eq!(program.statements[1].line, 8);
602+ }
603+
604+ #[::core::prelude::v1::test]
605+ fn bad_lines_are_skipped() {
606+ let program = parse("root = Stack([a])\na = Tag(\"x\" \"y\")\nb = Tag(\"z\")\nc = = 1");
607+ assert_eq!(program.statements.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), ["root", "b"]);
608+ assert_eq!(program.errors.len(), 2);
609+ assert!(program.errors[0].starts_with("Line 2:"), "{:?}", program.errors);
610+ }
611+
612+ #[::core::prelude::v1::test]
613+ fn partial_last_line() {
614+ let program = parse("root = Stack([title])\ntitle = TextContent(\"Sig");
615+ assert_eq!(program.statements.len(), 1);
616+ let program = parse("root = Stack([title])\ntitle = TextContent(\"Sign up\", ");
617+ assert_eq!(program.statements.len(), 1);
618+ assert_eq!(program.errors.len(), 1);
619+ }
620+
621+ #[::core::prelude::v1::test]
622+ fn statement_cap() {
623+ let source: String = (0..600).map(|ix| format!("s{ix} = {ix}\n")).collect();
624+ let program = parse(&source);
625+ assert_eq!(program.statements.len(), super::MAX_STATEMENTS);
626+ assert!(!program.errors.is_empty());
627+ }
628+ }
+9−0src/inline_ui/mod.rsaddedView File
@@ -0,0 +1,9 @@
1+ //! OpenUI Lang, the small language agents write ` ```openui ` blocks in:
2+ //! its parser and evaluator, kept free of GPUI so they can be tested on
3+ //! their own. `ui::inline` renders what they produce.
4+
5+ // Nothing renders these yet.
6+ #![allow(dead_code)]
7+
8+ pub mod eval;
9+ pub mod lang;
+1−0src/main.rsView File
@@ -9,6 +9,7 @@ mod clock;
99 mod device;
1010 mod git;
1111 mod http;
12+ mod inline_ui;
1213 mod models;
1314 mod orchestrator;
1415 mod projects;