1use std::cell::RefCell;
13use std::collections::{HashMap, HashSet};
14
15use rhai::{Dynamic, Engine as RhaiEngine, Module, Scope, AST};
16use rux_reactive::{Value, Warning};
17
18thread_local! {
19 static READS: RefCell<Option<HashSet<String>>> = const { RefCell::new(None) };
25}
26
27pub struct Builder {
31 engine: RhaiEngine,
32 host: Module,
33}
34
35impl Default for Builder {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl Builder {
42 pub fn new() -> Self {
43 let mut engine = RhaiEngine::new();
44 engine.register_fn("signal", |x: Dynamic| -> Dynamic {
47 match x.as_int() {
48 Ok(i) => Dynamic::from(i as f64),
49 Err(_) => x,
50 }
51 });
52 #[allow(deprecated)]
56 engine.on_var(|name, _index, _context| {
57 READS.with(|r| {
58 if let Some(set) = r.borrow_mut().as_mut() {
59 set.insert(name.to_string());
60 }
61 });
62 Ok(None)
63 });
64 Self {
65 engine,
66 host: Module::new(),
67 }
68 }
69
70 pub fn host_number(
72 &mut self,
73 name: &str,
74 f: impl Fn() -> f64 + Send + Sync + 'static,
75 ) -> &mut Self {
76 self.host.set_native_fn(name, move || -> Result<f64, Box<rhai::EvalAltResult>> {
77 Ok(f())
78 });
79 self
80 }
81
82 pub fn build(mut self, script: &str) -> Result<Engine, String> {
84 self.engine
85 .register_static_module("host", self.host.into());
86
87 let ast = self.engine.compile(script).map_err(|e| e.to_string())?;
88 let mut scope = Scope::new();
89 self.engine
90 .run_ast_with_scope(&mut scope, &ast)
91 .map_err(|e| e.to_string())?;
92 let funcs = ast.clone_functions_only();
93
94 let signals = scope.iter().map(|(name, _, _)| name.to_string()).collect();
98
99 Ok(Engine {
100 engine: self.engine,
101 scope,
102 funcs,
103 signals,
104 })
105 }
106}
107
108pub struct Engine {
110 engine: RhaiEngine,
111 scope: Scope<'static>,
112 funcs: AST,
113 signals: HashSet<String>,
115}
116
117thread_local! {
120 static WARNINGS: RefCell<Vec<Warning>> = const { RefCell::new(Vec::new()) };
124}
125
126fn warn(message: String) {
127 WARNINGS.with(|w| {
128 let mut w = w.borrow_mut();
129 if !w.iter().any(|existing: &Warning| existing.message == message) {
132 if ECHO.with(|e| e.get()) {
133 eprintln!("rux: {message}");
134 }
135 w.push(Warning::new(message));
140 }
141 });
142}
143
144thread_local! {
145 static ECHO: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
153}
154
155pub fn set_stderr_echo(on: bool) {
163 ECHO.with(|e| e.set(on));
164}
165
166pub fn take_warnings() -> Vec<Warning> {
168 WARNINGS.with(|w| std::mem::take(&mut *w.borrow_mut()))
169}
170
171fn trim_expr(src: &str) -> String {
174 let flat: String = src.split_whitespace().collect::<Vec<_>>().join(" ");
175 if flat.chars().count() > 60 {
176 format!("{}…", flat.chars().take(60).collect::<String>())
177 } else {
178 flat
179 }
180}
181
182fn strip_rhai_position(message: &str) -> String {
195 let trimmed = message.trim_end();
196 let Some(open) = trimmed.rfind(" (line ") else {
199 return trimmed.to_string();
200 };
201 let Some(inner) = trimmed[open + 1..].strip_prefix('(') else {
202 return trimmed.to_string();
203 };
204 let Some(inner) = inner.strip_suffix(')') else {
205 return trimmed.to_string();
206 };
207 let Some(rest) = inner.strip_prefix("line ") else {
208 return trimmed.to_string();
209 };
210 let Some((line, position)) = rest.split_once(", position ") else {
211 return trimmed.to_string();
212 };
213 let numeric = |s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit());
214 if numeric(line) && numeric(position) {
215 trimmed[..open].trim_end().to_string()
216 } else {
217 trimmed.to_string()
218 }
219}
220
221impl Engine {
222 fn eval(&mut self, src: &str, locals: &[(String, Value)]) -> Option<Dynamic> {
225 let ast = match self.engine.compile(src) {
226 Ok(ast) => ast,
227 Err(e) => {
228 warn(format!(
232 "expression `{}` failed to compile: {}",
233 trim_expr(src),
234 strip_rhai_position(&e.to_string())
235 ));
236 return None;
237 }
238 };
239 let merged = self.funcs.merge(&ast);
240
241 let base = self.scope.len();
242 for (name, value) in locals {
243 self.scope.push(name.clone(), to_dynamic(value));
244 }
245 let result = self.engine.eval_ast_with_scope::<Dynamic>(&mut self.scope, &merged);
246 self.scope.rewind(base); match result {
248 Ok(value) => Some(value),
249 Err(e) => {
250 warn(format!(
251 "expression `{}` failed: {}",
252 trim_expr(src),
253 strip_rhai_position(&e.to_string())
254 ));
255 None
256 }
257 }
258 }
259
260 pub fn eval_value(&mut self, src: &str, locals: &[(String, Value)]) -> Option<Value> {
262 self.eval(src, locals).map(|d| from_dynamic(&d))
263 }
264
265 pub fn eval_display(&mut self, src: &str, locals: &[(String, Value)]) -> String {
267 self.eval_value(src, locals)
268 .map(|v| v.to_display())
269 .unwrap_or_default()
270 }
271
272 pub fn eval_bool(&mut self, src: &str, locals: &[(String, Value)]) -> bool {
274 self.eval_value(src, locals)
275 .map(|v| v.is_truthy())
276 .unwrap_or(false)
277 }
278
279 pub fn run_handler(&mut self, src: &str) -> bool {
282 self.eval(src, &[]).is_some()
283 }
284
285 pub fn eval_value_tracked(
290 &mut self,
291 src: &str,
292 locals: &[(String, Value)],
293 ) -> (Option<Value>, HashSet<String>) {
294 READS.with(|r| *r.borrow_mut() = Some(HashSet::new()));
295 let value = self.eval_value(src, locals);
296 let mut reads = READS.with(|r| r.borrow_mut().take()).unwrap_or_default();
297 reads.retain(|n| self.signals.contains(n));
298 (value, reads)
299 }
300
301 pub fn eval_display_tracked(
304 &mut self,
305 src: &str,
306 locals: &[(String, Value)],
307 ) -> (String, HashSet<String>) {
308 let (value, deps) = self.eval_value_tracked(src, locals);
309 (value.map(|v| v.to_display()).unwrap_or_default(), deps)
310 }
311
312 pub fn eval_bool_tracked(
315 &mut self,
316 src: &str,
317 locals: &[(String, Value)],
318 ) -> (bool, HashSet<String>) {
319 let (value, deps) = self.eval_value_tracked(src, locals);
320 (value.map(|v| v.is_truthy()).unwrap_or(false), deps)
321 }
322
323 pub fn run_handler_tracked(&mut self, src: &str) -> HashSet<String> {
328 let names: Vec<String> = self.signals.iter().cloned().collect();
329 let before: HashMap<String, Option<Value>> =
330 names.iter().map(|n| (n.clone(), self.read_signal(n))).collect();
331 if !self.run_handler(src) {
332 return HashSet::new();
333 }
334 names
335 .into_iter()
336 .filter(|n| self.read_signal(n) != before[n])
337 .collect()
338 }
339
340 fn read_signal(&self, name: &str) -> Option<Value> {
342 self.scope.get_value::<Dynamic>(name).map(|d| from_dynamic(&d))
343 }
344
345 pub fn get_string(&mut self, name: &str) -> String {
347 self.eval_value(name, &[]).map(|v| v.to_display()).unwrap_or_default()
348 }
349
350 pub fn set_string(&mut self, name: &str, value: &str) {
352 self.scope.set_or_push(name, value.to_string());
353 }
354}
355
356fn to_dynamic(v: &Value) -> Dynamic {
357 match v {
358 Value::Number(n) => Dynamic::from(*n),
359 Value::Text(s) => Dynamic::from(s.clone()),
360 Value::Bool(b) => Dynamic::from(*b),
361 Value::List(items) => {
362 let arr: rhai::Array = items.iter().map(to_dynamic).collect();
363 Dynamic::from(arr)
364 }
365 Value::Map(entries) => {
366 let map: rhai::Map =
367 entries.iter().map(|(k, v)| (k.as_str().into(), to_dynamic(v))).collect();
368 Dynamic::from(map)
369 }
370 }
371}
372
373fn from_dynamic(d: &Dynamic) -> Value {
374 if let Ok(i) = d.as_int() {
375 return Value::Number(i as f64);
376 }
377 if let Ok(f) = d.as_float() {
378 return Value::Number(f);
379 }
380 if let Ok(b) = d.as_bool() {
381 return Value::Bool(b);
382 }
383 if let Some(s) = d.clone().try_cast::<String>() {
384 return Value::Text(s);
385 }
386 if let Some(arr) = d.clone().try_cast::<rhai::Array>() {
387 return Value::List(arr.iter().map(from_dynamic).collect());
388 }
389 if let Some(map) = d.clone().try_cast::<rhai::Map>() {
390 return Value::Map(
391 map.iter().map(|(k, v)| (k.to_string(), from_dynamic(v))).collect(),
392 );
393 }
394 Value::Text(d.to_string())
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
404 fn a_failing_expression_is_reported() {
405 let mut e = engine();
406 let _ = take_warnings(); assert_eq!(e.eval_display("nope(1)", &[]), "", "still degrades to empty");
409 let warnings = take_warnings();
410 assert_eq!(warnings.len(), 1, "{warnings:?}");
411 assert!(warnings[0].message.contains("nope(1)"), "names the expression: {warnings:?}");
412
413 assert!(take_warnings().is_empty(), "draining empties the sink");
414 }
415
416 #[test]
424 fn a_failing_expression_does_not_quote_a_line_that_is_not_in_the_file() {
425 let mut e = engine();
426 let _ = take_warnings();
427
428 let _ = e.eval_display("names", &[]); let warnings = take_warnings();
430 assert_eq!(warnings.len(), 1, "{warnings:?}");
431 let message = &warnings[0].message;
432
433 assert!(message.contains("Variable not found"), "keeps the cause: {message}");
434 assert!(message.contains("names"), "keeps the expression: {message}");
435 assert!(
436 !message.contains("line 1"),
437 "must not report a line that is not a line of the file: {message}"
438 );
439 assert!(!message.contains("position"), "nor a position: {message}");
440 }
441
442 #[test]
445 fn stripping_the_position_leaves_other_parentheses_alone() {
446 assert_eq!(
447 strip_rhai_position("Variable not found: names (line 1, position 1)"),
448 "Variable not found: names"
449 );
450 assert_eq!(strip_rhai_position("something (line 4)"), "something (line 4)");
452 assert_eq!(
453 strip_rhai_position("call to fn(a, b) failed"),
454 "call to fn(a, b) failed"
455 );
456 assert_eq!(strip_rhai_position("plain message"), "plain message");
457 assert_eq!(
459 strip_rhai_position("x (line one, position two)"),
460 "x (line one, position two)"
461 );
462 }
463
464 #[test]
467 fn repeated_failures_are_reported_once() {
468 let mut e = engine();
469 let _ = take_warnings();
470 for _ in 0..5 {
471 let _ = e.eval_display("nope(1)", &[]);
472 }
473 assert_eq!(take_warnings().len(), 1);
474 }
475
476 #[test]
478 fn a_good_expression_is_silent() {
479 let mut e = engine();
480 let _ = take_warnings();
481 assert_eq!(e.eval_display("double(4)", &[]), "8");
482 assert!(take_warnings().is_empty());
483 }
484
485 fn engine() -> Engine {
486 let mut b = Builder::new();
487 b.host_number("full", || 100.0);
488 b.build(
489 "let level = signal(82); \
490 let items = signal([1, 2, 3]); \
491 fn double(x) { x * 2 }",
492 )
493 .expect("build engine")
494 }
495
496 #[test]
497 fn reads_and_evaluates_state() {
498 let mut e = engine();
499 assert_eq!(e.eval_display("level", &[]), "82");
500 assert_eq!(e.eval_display("level - 2", &[]), "80");
501 assert!(e.eval_bool("level > 50", &[]));
502 assert!(!e.eval_bool("level < 20", &[]));
503 }
504
505 #[test]
506 fn runs_inline_handlers_and_pure_fns() {
507 let mut e = engine();
508 e.run_handler("level = level - 5"); assert_eq!(e.eval_display("level", &[]), "77");
510 e.run_handler("level = level + 3");
511 assert_eq!(e.eval_display("level", &[]), "80");
512 assert_eq!(e.eval_display("double(level)", &[]), "160");
514 }
515
516 #[test]
519 fn evaluates_backtick_string_interpolation() {
520 let mut e = engine(); assert_eq!(
523 e.eval_display("`background: ${c}`", &[("c".into(), Value::Text("teal".into()))]),
524 "background: teal"
525 );
526 assert_eq!(e.eval_display("`level is ${level}`", &[]), "level is 82.0");
531 let (_, deps) = e.eval_value_tracked("`level: ${level}`", &[]);
533 assert!(deps.contains("level"));
534 }
535
536 #[test]
537 fn calls_host_functions() {
538 let mut e = engine();
539 e.run_handler("level = host::full()");
540 assert_eq!(e.eval_display("level", &[]), "100");
541 }
542
543 #[test]
544 fn lists_and_locals() {
545 let mut e = engine();
546 let items = e.eval_value("items", &[]).unwrap();
547 assert_eq!(items.as_list().unwrap().len(), 3);
548 assert_eq!(e.eval_display("x + 1", &[("x".into(), Value::Number(4.0))]), "5");
550 }
551
552 fn deps(e: &mut Engine, src: &str, locals: &[(String, Value)]) -> Vec<String> {
553 let (_, set) = e.eval_value_tracked(src, locals);
554 let mut v: Vec<String> = set.into_iter().collect();
555 v.sort();
556 v
557 }
558
559 #[test]
561 fn tracks_binding_dependencies() {
562 let mut e = engine();
563 assert_eq!(deps(&mut e, "level", &[]), ["level"]);
564 assert_eq!(deps(&mut e, "level > 20", &[]), ["level"]);
565 assert_eq!(deps(&mut e, "double(level)", &[]), ["level"]);
568 assert_eq!(deps(&mut e, "x + 1", &[("x".into(), Value::Number(4.0))]), Vec::<String>::new());
570 assert_eq!(deps(&mut e, "x + level", &[("x".into(), Value::Number(4.0))]), ["level"]);
571 assert_eq!(deps(&mut e, "level + items[0]", &[]), ["items", "level"]);
573 }
574
575 #[test]
578 fn tracks_handler_writes() {
579 let mut e = engine();
580 let changed = |e: &mut Engine, src: &str| {
581 let mut v: Vec<String> = e.run_handler_tracked(src).into_iter().collect();
582 v.sort();
583 v
584 };
585 assert_eq!(changed(&mut e, "level = level - 5"), ["level"]);
586 assert_eq!(e.eval_display("level", &[]), "77");
587 assert_eq!(changed(&mut e, "level = level"), Vec::<String>::new());
589 assert_eq!(changed(&mut e, "items = [9]"), ["items"]);
591 assert_eq!(changed(&mut e, "level"), Vec::<String>::new()); }
593}
594