From a1591ae45b806e7cd0de6bade4c27b038344285c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 3 Mar 2020 15:20:20 +0800 Subject: [PATCH] Better API for Scope. --- README.md | 24 +++++++++-- src/api.rs | 3 +- src/builtin.rs | 5 --- src/engine.rs | 60 +++++++++----------------- src/lib.rs | 4 +- src/scope.rs | 103 +++++++++++++++++++++++++++++++++++++++++++++ tests/var_scope.rs | 35 +++++++++++++++ 7 files changed, 184 insertions(+), 50 deletions(-) create mode 100644 src/scope.rs diff --git a/README.md b/README.md index 6f9c78ae..290aa9fc 100644 --- a/README.md +++ b/README.md @@ -391,11 +391,11 @@ a.x.change(); // Only a COPY of 'a.x' is changed. 'a.x' is NOT changed. a.x == 500; ``` -# Maintaining state +# Initializing and maintaining state By default, Rhai treats each engine invocation as a fresh one, persisting only the functions that have been defined but no top-level state. This gives each one a fairly clean starting place. Sometimes, though, you want to continue using the same top-level state from one invocation to the next. -In this example, we thread the same state through multiple invocations: +In this example, we first create a state with a few initialized variables, then thread the same state through multiple invocations: ```rust extern crate rhai; @@ -403,13 +403,29 @@ use rhai::{Engine, Scope}; fn main() { let mut engine = Engine::new(); + + // First create the state let mut scope = Scope::new(); - if let Ok(_) = engine.eval_with_scope::<()>(&mut scope, "let x = 4 + 5") { } else { assert!(false); } + // Then push some initialized variables into the state + // NOTE: Remember the default numbers used by Rhai are i64 and f64. + // Better stick to them or it gets hard to work with other variables in the script. + scope.push("y".into(), 42_i64); + scope.push("z".into(), 999_i64); + // First invocation + engine.eval_with_scope::<()>(&mut scope, r" + let x = 4 + 5 - y + z; + y = 1; + ").expect("y and z not found?"); + + // Second invocation using the same state if let Ok(result) = engine.eval_with_scope::(&mut scope, "x") { - println!("result: {}", result); + println!("result: {}", result); // should print 966 } + + // Variable y is changed in the script + assert_eq!(scope.get_value::("y").unwrap(), 1); } ``` diff --git a/src/api.rs b/src/api.rs index f1c32155..916a0858 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,6 +1,7 @@ use crate::any::{Any, AnyExt, Dynamic}; -use crate::engine::{Engine, EvalAltResult, FnIntExt, FnSpec, Scope}; +use crate::engine::{Engine, EvalAltResult, FnIntExt, FnSpec}; use crate::parser::{lex, parse, ParseError, Position, AST}; +use crate::scope::Scope; use std::sync::Arc; impl Engine { diff --git a/src/builtin.rs b/src/builtin.rs index da23abe2..e8fb2f00 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -174,11 +174,6 @@ impl Engine { self.register_fn("+", concat); self.register_fn("==", unit_eq); - // self.register_fn("[]", idx); - // FIXME? Registering array lookups are a special case because we want to return boxes - // directly let ent = self.fns.entry("[]".to_string()).or_insert_with(Vec::new); - // (*ent).push(FnType::ExternalFn2(Box::new(idx))); - // Register conversion functions self.register_fn("to_float", |x: i8| x as f64); self.register_fn("to_float", |x: u8| x as f64); diff --git a/src/engine.rs b/src/engine.rs index 89544c16..750d548e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -8,6 +8,7 @@ use crate::any::{Any, AnyExt, Dynamic, Variant}; use crate::call::FunArgs; use crate::fn_register::RegisterFn; use crate::parser::{Expr, FnDef, ParseError, Position, Stmt}; +use crate::scope::Scope; pub type Array = Vec; pub type FnCallArgs<'a> = Vec<&'a mut Variant>; @@ -162,22 +163,6 @@ pub enum FnIntExt { pub type FnAny = dyn Fn(FnCallArgs, Position) -> Result; -/// A type containing information about current scope. -/// Useful for keeping state between `Engine` runs -/// -/// ```rust -/// use rhai::{Engine, Scope}; -/// -/// let mut engine = Engine::new(); -/// let mut my_scope = Scope::new(); -/// -/// assert!(engine.eval_with_scope::<()>(&mut my_scope, "let x = 5;").is_ok()); -/// assert_eq!(engine.eval_with_scope::(&mut my_scope, "x + 1").unwrap(), 6); -/// ``` -/// -/// Between runs, `Engine` only remembers functions when not using own `Scope`. -pub type Scope = Vec<(String, Dynamic)>; - impl Engine { pub fn call_fn<'a, I, A, T>(&self, ident: I, args: A) -> Result where @@ -185,7 +170,7 @@ impl Engine { A: FunArgs<'a>, T: Any + Clone, { - let pos = Position { line: 0, pos: 0 }; + let pos = Position::new(); self.call_fn_raw(ident.into(), args.into_vec(), None, pos) .and_then(|b| { @@ -438,19 +423,16 @@ impl Engine { } } - fn search_scope<'a, T>( - scope: &'a mut Scope, + fn search_scope( + scope: &Scope, id: &str, - map: impl FnOnce(&'a mut Variant) -> Result, + map: impl FnOnce(&Variant) -> Result, begin: Position, ) -> Result<(usize, T), EvalAltResult> { scope - .iter_mut() - .enumerate() - .rev() - .find(|&(_, &mut (ref name, _))| id == name) + .get(id) .ok_or_else(|| EvalAltResult::ErrorVariableNotFound(id.into(), begin)) - .and_then(move |(idx, &mut (_, ref mut val))| map(val.as_mut()).map(|val| (idx, val))) + .and_then(move |(idx, _, val)| map(val.as_ref()).map(|v| (idx, v))) } fn indexed_value( @@ -471,7 +453,7 @@ impl Engine { scope, id, |val| { - if let Some(arr) = (*val).downcast_mut() as Option<&mut Array> { + if let Some(arr) = (*val).downcast_ref() as Option<&Array> { is_array = true; if idx >= 0 { @@ -481,7 +463,7 @@ impl Engine { } else { Err(EvalAltResult::ErrorArrayBounds(arr.len(), idx, begin)) } - } else if let Some(s) = (*val).downcast_mut() as Option<&mut String> { + } else if let Some(s) = (*val).downcast_ref() as Option<&String> { is_array = false; if idx >= 0 { @@ -537,7 +519,7 @@ impl Engine { // In case the expression mutated `target`, we need to reassign it because // of the above `clone`. - scope[sc_idx].1 = target; + *scope.get_mut(id, sc_idx) = target; value } @@ -551,10 +533,10 @@ impl Engine { // of the above `clone`. if is_array { - scope[sc_idx].1.downcast_mut::().unwrap()[idx] = target; + scope.get_mut(id, sc_idx).downcast_mut::().unwrap()[idx] = target; } else { Self::str_replace_char( - scope[sc_idx].1.downcast_mut::().unwrap(), // Root is a string + scope.get_mut(id, sc_idx).downcast_mut::().unwrap(), // Root is a string idx, *target.downcast::().unwrap(), // Target should be a char ); @@ -617,7 +599,7 @@ impl Engine { // In case the expression mutated `target`, we need to reassign it because // of the above `clone`. - scope[sc_idx].1 = target; + *scope.get_mut(id, sc_idx) = target; value } @@ -630,10 +612,10 @@ impl Engine { // In case the expression mutated `target`, we need to reassign it because // of the above `clone`. if is_array { - scope[sc_idx].1.downcast_mut::().unwrap()[idx] = target; + scope.get_mut(id, sc_idx).downcast_mut::().unwrap()[idx] = target; } else { Self::str_replace_char( - scope[sc_idx].1.downcast_mut::().unwrap(), // Root is a string + scope.get_mut(id, sc_idx).downcast_mut::().unwrap(), // Root is a string idx, *target.downcast::().unwrap(), // Target should be a char ); @@ -871,11 +853,11 @@ impl Engine { let tid = Any::type_id(&*arr); if let Some(iter_fn) = self.type_iterators.get(&tid) { - scope.push((name.clone(), Box::new(()))); + scope.push(name.clone(), ()); let idx = scope.len() - 1; for a in iter_fn(&arr) { - scope[idx].1 = a; + *scope.get_mut(name, idx) = a; match self.eval_stmt(scope, body) { Err(EvalAltResult::LoopBreak) => break, @@ -883,7 +865,7 @@ impl Engine { _ => (), } } - scope.remove(idx); + scope.pop(); Ok(Box::new(())) } else { return Err(EvalAltResult::ErrorFor(expr.position())); @@ -901,10 +883,10 @@ impl Engine { Stmt::Let(name, init, _) => { if let Some(v) = init { - let i = self.eval_expr(scope, v)?; - scope.push((name.clone(), i)); + let val = self.eval_expr(scope, v)?; + scope.push_dynamic(name.clone(), val); } else { - scope.push((name.clone(), Box::new(()))); + scope.push(name.clone(), ()); } Ok(Box::new(())) } diff --git a/src/lib.rs b/src/lib.rs index a7ac4d8e..074abf23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,8 +46,10 @@ mod call; mod engine; mod fn_register; mod parser; +mod scope; pub use any::Dynamic; -pub use engine::{Array, Engine, EvalAltResult, Scope}; +pub use engine::{Array, Engine, EvalAltResult}; +pub use scope::Scope; pub use fn_register::{RegisterDynamicFn, RegisterFn}; pub use parser::{ParseError, ParseErrorType, AST}; diff --git a/src/scope.rs b/src/scope.rs new file mode 100644 index 00000000..16035f48 --- /dev/null +++ b/src/scope.rs @@ -0,0 +1,103 @@ +use crate::any::{Any, Dynamic}; + +/// A type containing information about current scope. +/// Useful for keeping state between `Engine` runs +/// +/// ```rust +/// use rhai::{Engine, Scope}; +/// +/// let mut engine = Engine::new(); +/// let mut my_scope = Scope::new(); +/// +/// assert!(engine.eval_with_scope::<()>(&mut my_scope, "let x = 5;").is_ok()); +/// assert_eq!(engine.eval_with_scope::(&mut my_scope, "x + 1").unwrap(), 6); +/// ``` +/// +/// Between runs, `Engine` only remembers functions when not using own `Scope`. + +pub struct Scope(Vec<(String, Dynamic)>); + +impl Scope { + /// Create a new Scope. + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Empty the Scope. + pub fn clear(&mut self) { + self.0.clear(); + } + + /// Get the number of variables inside the Scope. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Add (push) a new variable to the Scope. + pub fn push(&mut self, key: String, value: T) { + self.0.push((key, Box::new(value))); + } + + /// Add (push) a new variable to the Scope. + pub(crate) fn push_dynamic(&mut self, key: String, value: Dynamic) { + self.0.push((key, value)); + } + + /// Remove (pop) the last variable from the Scope. + pub fn pop(&mut self) -> Option<(String, Dynamic)> { + self.0.pop() + } + + /// Truncate (rewind) the Scope to a previous size. + pub fn rewind(&mut self, size: usize) { + self.0.truncate(size); + } + + /// Find a variable in the Scope, starting from the last. + pub fn get(&self, key: &str) -> Option<(usize, String, Dynamic)> { + self.0 + .iter() + .enumerate() + .rev() + .find(|(_, (n, _))| n == key) + .map(|(i, (n, v))| (i, n.clone(), v.clone())) + } + + /// Get the value of a variable in the Scope, starting from the last. + pub fn get_value(&self, key: &str) -> Option { + self.0 + .iter() + .enumerate() + .rev() + .find(|(_, (n, _))| n == key) + .map(|(_, (_, v))| v.downcast_ref() as Option<&T>) + .map(|v| v.unwrap().clone()) + } + + /// Get a mutable reference to a variable in the Scope. + pub(crate) fn get_mut(&mut self, key: &str, index: usize) -> &mut Dynamic { + let entry = self.0.get_mut(index).expect("invalid index in Scope"); + + if entry.0 != key { + panic!("incorrect key at Scope entry"); + } + + &mut entry.1 + } + + /// Get an iterator to variables in the Scope. + pub fn iter(&self) -> std::slice::Iter<(String, Dynamic)> { + self.0.iter() + } + + /// Get a mutable iterator to variables in the Scope. + pub(crate) fn iter_mut(&mut self) -> std::slice::IterMut<(String, Dynamic)> { + self.0.iter_mut() + } +} + +impl std::iter::Extend<(String, Dynamic)> for Scope { + fn extend>(&mut self, iter: T) { + self.0.extend(iter); + } +} diff --git a/tests/var_scope.rs b/tests/var_scope.rs index 6b378299..5522ba67 100644 --- a/tests/var_scope.rs +++ b/tests/var_scope.rs @@ -14,3 +14,38 @@ fn test_var_scope() -> Result<(), EvalAltResult> { Ok(()) } + +#[test] +fn test_scope_eval() -> Result<(), EvalAltResult> { + let mut engine = Engine::new(); + + // First create the state + let mut scope = Scope::new(); + + // Then push some initialized variables into the state + // NOTE: Remember the default numbers used by Rhai are i64 and f64. + // Better stick to them or it gets hard to work with other variables in the script. + scope.push("y".into(), 42_i64); + scope.push("z".into(), 999_i64); + + // First invocation + engine + .eval_with_scope::<()>( + &mut scope, + r" + let x = 4 + 5 - y + z; + y = 1; + ", + ) + .expect("y and z not found?"); + + // Second invocation using the same state + if let Ok(result) = engine.eval_with_scope::(&mut scope, "x") { + println!("result: {}", result); // should print 966 + } + + // Variable y is changed in the script + assert_eq!(scope.get_value::("y").unwrap(), 1); + + Ok(()) +}