diff --git a/CHANGELOG.md b/CHANGELOG.md index e47dc084..833c3b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,12 @@ Bug fixes --------- * Compound assignments now work properly with indexers. +* Cloning a `Scope` no longer turns all constants to mutable. Script-breaking changes ----------------------- -* _Strict Variables Mode_ no longer returns an error when an undeclared variable matches a constant in the provided external `Scope`. +* _Strict Variables Mode_ no longer returns an error when an undeclared variable matches a variable/constant in the provided external `Scope`. Enhancements ------------ @@ -22,6 +23,9 @@ Enhancements * `Engine::parse_json` now natively handles nested JSON inputs (using a token remap filter) without needing to replace `{` with `#{`. * `to_json` is added to object maps to cheaply convert it to JSON format (`()` is mapped to `null`, all other data types must be supported by JSON) * A global function `format_map_as_json` is provided which is the same as `to_json` for object maps. +* `FileModuleResolver` now accepts a custom `Scope` to provide constants for optimization. +* A new low-level method `Engine::call_fn_raw_raw` is added to add speed to repeated function calls. +* A new low-level method `Engine::eval_statements_raw` is added to evaluate a sequence of statements. Version 1.6.1 diff --git a/src/api/call_fn.rs b/src/api/call_fn.rs index 6ca6f59d..9d327c80 100644 --- a/src/api/call_fn.rs +++ b/src/api/call_fn.rs @@ -4,9 +4,10 @@ use crate::eval::{Caches, GlobalRuntimeState}; use crate::types::dynamic::Variant; use crate::{ - Dynamic, Engine, FuncArgs, Position, RhaiResult, RhaiResultOf, Scope, StaticVec, AST, ERR, + reify, Dynamic, Engine, FuncArgs, Position, RhaiResult, RhaiResultOf, Scope, StaticVec, AST, + ERR, }; -use std::any::type_name; +use std::any::{type_name, TypeId}; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -66,6 +67,15 @@ impl Engine { let result = self.call_fn_raw(scope, ast, true, true, name, None, arg_values)?; + // Bail out early if the return type needs no cast + if TypeId::of::() == TypeId::of::() { + return Ok(reify!(result => T)); + } + if TypeId::of::() == TypeId::of::<()>() { + return Ok(reify!(() => T)); + } + + // Cast return type let typ = self.map_type_name(result.type_name()); result.try_cast().ok_or_else(|| { @@ -73,8 +83,9 @@ impl Engine { ERR::ErrorMismatchOutputType(t, typ.into(), Position::NONE).into() }) } - /// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments and the - /// following options: + /// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments. + /// + /// The following options are available: /// /// * whether to evaluate the [`AST`] to load necessary modules before calling the function /// * whether to rewind the [`Scope`] after the function call @@ -138,7 +149,7 @@ impl Engine { /// # Ok(()) /// # } /// ``` - #[inline] + #[inline(always)] pub fn call_fn_raw( &self, scope: &mut Scope, @@ -149,9 +160,85 @@ impl Engine { this_ptr: Option<&mut Dynamic>, arg_values: impl AsMut<[Dynamic]>, ) -> RhaiResult { - let caches = &mut Caches::new(); - let global = &mut GlobalRuntimeState::new(self); + self.call_fn_internal( + scope, + &mut GlobalRuntimeState::new(self), + &mut Caches::new(), + ast, + eval_ast, + rewind_scope, + name, + this_ptr, + arg_values, + ) + } + /// _(internals)_ Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments. + /// Exported under the `internals` feature only. + /// + /// The following options are available: + /// + /// * whether to evaluate the [`AST`] to load necessary modules before calling the function + /// * whether to rewind the [`Scope`] after the function call + /// * a value for binding to the `this` pointer (if any) + /// + /// Not available under `no_function`. + /// + /// # WARNING - Low Level API + /// + /// This function is very low level. + /// + /// A [`GlobalRuntimeState`] and [`Caches`] need to be passed into the function, which can be + /// created via [`GlobalRuntimeState::new`] and [`Caches::new`]. + /// This makes repeatedly calling particular functions more efficient as the functions resolution cache + /// is kept intact. + /// + /// # Arguments + /// + /// All the arguments are _consumed_, meaning that they're replaced by `()`. + /// This is to avoid unnecessarily cloning the arguments. + /// + /// Do not use the arguments after this call. If they are needed afterwards, clone them _before_ + /// calling this function. + #[cfg(feature = "internals")] + #[inline(always)] + pub fn call_fn_raw_raw( + &self, + scope: &mut Scope, + global: &mut GlobalRuntimeState, + caches: &mut Caches, + ast: &AST, + eval_ast: bool, + rewind_scope: bool, + name: impl AsRef, + this_ptr: Option<&mut Dynamic>, + arg_values: impl AsMut<[Dynamic]>, + ) -> RhaiResult { + self.call_fn_internal( + scope, + global, + caches, + ast, + eval_ast, + rewind_scope, + name, + this_ptr, + arg_values, + ) + } + /// Call a script function defined in an [`AST`] with multiple [`Dynamic`] arguments. + fn call_fn_internal( + &self, + scope: &mut Scope, + global: &mut GlobalRuntimeState, + caches: &mut Caches, + ast: &AST, + eval_ast: bool, + rewind_scope: bool, + name: impl AsRef, + this_ptr: Option<&mut Dynamic>, + arg_values: impl AsMut<[Dynamic]>, + ) -> RhaiResult { let statements = ast.statements(); let orig_scope_len = scope.len(); diff --git a/src/api/eval.rs b/src/api/eval.rs index 094fd117..4b23958a 100644 --- a/src/api/eval.rs +++ b/src/api/eval.rs @@ -6,9 +6,9 @@ use crate::types::dynamic::Variant; use crate::{ Dynamic, Engine, Module, OptimizationLevel, Position, RhaiResult, RhaiResultOf, Scope, AST, ERR, }; -use std::any::type_name; #[cfg(feature = "no_std")] use std::prelude::v1::*; +use std::{any::type_name, mem}; impl Engine { /// Evaluate a string. @@ -211,9 +211,10 @@ impl Engine { global.source = ast.source_raw().clone(); #[cfg(not(feature = "no_module"))] - { - global.embedded_module_resolver = ast.resolver().cloned(); - } + let orig_embedded_module_resolver = mem::replace( + &mut global.embedded_module_resolver, + ast.resolver().cloned(), + ); let statements = ast.statements(); @@ -230,6 +231,36 @@ impl Engine { } else { &lib[..] }; - self.eval_global_statements(scope, global, &mut caches, statements, lib, level) + + let result = + self.eval_global_statements(scope, global, &mut caches, statements, lib, level); + + #[cfg(not(feature = "no_module"))] + { + global.embedded_module_resolver = orig_embedded_module_resolver; + } + + result + } + /// _(internals)_ Evaluate a list of statements with no `this` pointer. + /// Exported under the `internals` feature only. + /// + /// This is commonly used to evaluate a list of statements in an [`AST`] or a script function body. + /// + /// # WARNING - Low Level API + /// + /// This function is very low level. + #[cfg(feature = "internals")] + #[inline(always)] + pub fn eval_statements_raw( + &self, + scope: &mut Scope, + global: &mut GlobalRuntimeState, + caches: &mut Caches, + statements: &[crate::ast::Stmt], + lib: &[&Module], + level: usize, + ) -> RhaiResult { + self.eval_global_statements(scope, global, caches, statements, lib, level) } } diff --git a/src/bin/rhai-dbg.rs b/src/bin/rhai-dbg.rs index 7656c31a..ffe0aae1 100644 --- a/src/bin/rhai-dbg.rs +++ b/src/bin/rhai-dbg.rs @@ -154,43 +154,6 @@ fn print_debug_help() { println!(); } -/// Display the current scope. -fn print_scope(scope: &Scope, dedup: bool) { - let flattened_clone; - let scope = if dedup { - flattened_clone = scope.clone_visible(); - &flattened_clone - } else { - scope - }; - - for (i, (name, constant, value)) in scope.iter_raw().enumerate() { - #[cfg(not(feature = "no_closure"))] - let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; - #[cfg(feature = "no_closure")] - let value_is_shared = ""; - - if dedup { - println!( - "{}{}{} = {:?}", - if constant { "const " } else { "" }, - name, - value_is_shared, - *value.read_lock::().unwrap(), - ); - } else { - println!( - "[{}] {}{}{} = {:?}", - i + 1, - if constant { "const " } else { "" }, - name, - value_is_shared, - *value.read_lock::().unwrap(), - ); - } - } -} - // Load script to debug. fn load_script(engine: &Engine) -> (rhai::AST, String) { if let Some(filename) = env::args().skip(1).next() { @@ -365,7 +328,7 @@ fn debug_callback( [] | ["step" | "s"] => break Ok(DebuggerCommand::StepInto), ["over" | "o"] => break Ok(DebuggerCommand::StepOver), ["next" | "n"] => break Ok(DebuggerCommand::Next), - ["scope"] => print_scope(context.scope(), false), + ["scope"] => println!("{}", context.scope()), ["print" | "p", "this"] => { if let Some(value) = context.this_ptr() { println!("=> {:?}", value); @@ -381,7 +344,7 @@ fn debug_callback( } } ["print" | "p"] => { - print_scope(context.scope(), true); + println!("{}", context.scope().clone_visible()); if let Some(value) = context.this_ptr() { println!("this = {:?}", value); } diff --git a/src/bin/rhai-repl.rs b/src/bin/rhai-repl.rs index c0dfa093..0cf59289 100644 --- a/src/bin/rhai-repl.rs +++ b/src/bin/rhai-repl.rs @@ -108,27 +108,6 @@ fn print_keys() { println!(); } -/// Display the scope. -fn print_scope(scope: &Scope) { - for (i, (name, constant, value)) in scope.iter_raw().enumerate() { - #[cfg(not(feature = "no_closure"))] - let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; - #[cfg(feature = "no_closure")] - let value_is_shared = ""; - - println!( - "[{}] {}{}{} = {:?}", - i + 1, - if constant { "const " } else { "" }, - name, - value_is_shared, - *value.read_lock::().unwrap(), - ) - } - - println!(); -} - // Load script files specified in the command line. #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_std"))] @@ -458,7 +437,7 @@ fn main() { continue; } "scope" => { - print_scope(&scope); + println!("{}", scope); continue; } #[cfg(not(feature = "no_optimize"))] diff --git a/src/module/resolvers/file.rs b/src/module/resolvers/file.rs index 561dfaae..5b6f232a 100644 --- a/src/module/resolvers/file.rs +++ b/src/module/resolvers/file.rs @@ -50,6 +50,7 @@ pub struct FileModuleResolver { base_path: Option, extension: Identifier, cache_enabled: bool, + scope: Scope<'static>, #[cfg(not(feature = "sync"))] cache: std::cell::RefCell>>, @@ -57,6 +58,13 @@ pub struct FileModuleResolver { cache: std::sync::RwLock>>, } +impl Default for FileModuleResolver { + #[inline(always)] + fn default() -> Self { + Self::new() + } +} + impl FileModuleResolver { /// Create a new [`FileModuleResolver`] with the current directory as base path. /// @@ -126,6 +134,7 @@ impl FileModuleResolver { extension: extension.into(), cache_enabled: true, cache: BTreeMap::new().into(), + scope: Scope::new(), } } @@ -155,6 +164,7 @@ impl FileModuleResolver { extension: extension.into(), cache_enabled: true, cache: BTreeMap::new().into(), + scope: Scope::new(), } } @@ -185,6 +195,32 @@ impl FileModuleResolver { self } + /// Get a reference to the file module resolver's [scope][Scope]. + /// + /// The [scope][Scope] is used for compiling module scripts. + #[must_use] + #[inline(always)] + pub const fn scope(&self) -> &Scope { + &self.scope + } + + /// Set the file module resolver's [scope][Scope]. + /// + /// The [scope][Scope] is used for compiling module scripts. + #[inline(always)] + pub fn set_scope(&mut self, scope: Scope<'static>) { + self.scope = scope; + } + + /// Get a mutable reference to the file module resolver's [scope][Scope]. + /// + /// The [scope][Scope] is used for compiling module scripts. + #[must_use] + #[inline(always)] + pub fn scope_mut(&mut self) -> &mut Scope<'static> { + &mut self.scope + } + /// Enable/disable the cache. #[inline(always)] pub fn enable_cache(&mut self, enable: bool) -> &mut Self { @@ -281,10 +317,8 @@ impl FileModuleResolver { } } - let scope = Scope::new(); - let mut ast = engine - .compile_file(file_path.clone()) + .compile_file_with_scope(&self.scope, file_path.clone()) .map_err(|err| match *err { ERR::ErrorSystem(.., err) if err.is::() => { Box::new(ERR::ErrorModuleNotFound(path.to_string(), pos)) @@ -294,7 +328,9 @@ impl FileModuleResolver { ast.set_source(path); - let m: Shared = if let Some(global) = global { + let scope = Scope::new(); + + let m: Shared<_> = if let Some(global) = global { Module::eval_ast_as_new_raw(engine, scope, global, &ast) } else { Module::eval_ast_as_new(scope, &ast, engine) diff --git a/src/module/resolvers/stat.rs b/src/module/resolvers/stat.rs index be9110a7..3e2e9659 100644 --- a/src/module/resolvers/stat.rs +++ b/src/module/resolvers/stat.rs @@ -22,7 +22,7 @@ use std::{collections::btree_map::IntoIter, collections::BTreeMap, ops::AddAssig /// /// engine.set_module_resolver(resolver); /// ``` -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct StaticModuleResolver(BTreeMap>); impl StaticModuleResolver { diff --git a/src/parser.rs b/src/parser.rs index e33ece2b..72664a54 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1301,10 +1301,7 @@ impl Engine { if settings.options.strict_var && !settings.is_closure_scope && index.is_none() - && !matches!( - state.scope.get_index(name), - Some((_, AccessMode::ReadOnly)) - ) + && !state.scope.contains(name) { // If the parent scope is not inside another capturing closure // then we can conclude that the captured variable doesn't exist. @@ -1450,7 +1447,7 @@ impl Engine { if settings.options.strict_var && index.is_none() - && !matches!(state.scope.get_index(&s), Some((_, AccessMode::ReadOnly))) + && !state.scope.contains(&s) { return Err( PERR::VariableUndefined(s.to_string()).into_err(settings.pos) diff --git a/src/types/scope.rs b/src/types/scope.rs index bae3f2a8..6a169bec 100644 --- a/src/types/scope.rs +++ b/src/types/scope.rs @@ -6,6 +6,7 @@ use smallvec::SmallVec; #[cfg(feature = "no_std")] use std::prelude::v1::*; use std::{ + fmt, iter::{Extend, FromIterator}, marker::PhantomData, }; @@ -59,7 +60,7 @@ const SCOPE_ENTRIES_INLINED: usize = 8; // direct indexing, by-passing the name altogether. // // [`Dynamic`] is reasonably small so packing it tightly improves cache performance. -#[derive(Debug, Clone, Hash, Default)] +#[derive(Debug, Hash, Default)] pub struct Scope<'a> { /// Current value of the entry. values: SmallVec<[Dynamic; SCOPE_ENTRIES_INLINED]>, @@ -71,6 +72,51 @@ pub struct Scope<'a> { phantom: PhantomData<&'a ()>, } +impl fmt::Display for Scope<'_> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, (name, constant, value)) in self.iter_raw().enumerate() { + #[cfg(not(feature = "no_closure"))] + let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; + #[cfg(feature = "no_closure")] + let value_is_shared = ""; + + write!( + f, + "[{}] {}{}{} = {:?}\n", + i + 1, + if constant { "const " } else { "" }, + name, + value_is_shared, + *value.read_lock::().unwrap(), + )?; + } + + Ok(()) + } +} + +impl Clone for Scope<'_> { + #[inline] + fn clone(&self) -> Self { + Self { + values: self + .values + .iter() + .map(|v| { + // Also copy the value's access mode (otherwise will turn to read-write) + let mut v2 = v.clone(); + v2.set_access_mode(v.access_mode()); + v2 + }) + .collect(), + names: self.names.clone(), + aliases: self.aliases.clone(), + phantom: self.phantom.clone(), + } + } +} + impl IntoIterator for Scope<'_> { type Item = (String, Dynamic, Vec); type IntoIter = Box>; @@ -551,24 +597,24 @@ impl Scope<'_> { #[must_use] pub fn clone_visible(&self) -> Self { let len = self.len(); + let mut scope = Self::new(); - self.names - .iter() - .rev() - .enumerate() - .fold(Self::new(), |mut entries, (index, name)| { - if entries.names.is_empty() || !entries.names.contains(name) { - let orig_value = &self.values[len - 1 - index]; - let alias = &self.aliases[len - 1 - index]; - let mut value = orig_value.clone(); - value.set_access_mode(orig_value.access_mode()); + self.names.iter().rev().enumerate().for_each(|(i, name)| { + if scope.names.contains(name) { + return; + } - entries.names.push(name.clone()); - entries.values.push(value); - entries.aliases.push(alias.clone()); - } - entries - }) + let v1 = &self.values[len - 1 - i]; + let alias = &self.aliases[len - 1 - i]; + let mut v2 = v1.clone(); + v2.set_access_mode(v1.access_mode()); + + scope.names.push(name.clone()); + scope.values.push(v2); + scope.aliases.push(alias.clone()); + }); + + scope } /// Get an iterator to entries in the [`Scope`]. #[inline] diff --git a/tests/options.rs b/tests/options.rs index aa9b250d..5725a76f 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -57,7 +57,7 @@ fn test_options_strict_var() -> Result<(), Box> { let mut engine = Engine::new(); let mut scope = Scope::new(); - scope.push_constant("x", 42 as INT); + scope.push("x", 42 as INT); scope.push_constant("y", 0 as INT); engine.compile("let x = if y { z } else { w };")?; @@ -114,8 +114,8 @@ fn test_options_strict_var() -> Result<(), Box> { } #[cfg(not(feature = "no_optimize"))] assert_eq!( - engine.eval_with_scope::(&mut scope, "fn foo(z) { x * y + z } foo(1)")?, - 1 + engine.eval_with_scope::(&mut scope, "fn foo(z) { y + z } foo(x)")?, + 42 ); } diff --git a/tests/var_scope.rs b/tests/var_scope.rs index 2f50c457..014ec42b 100644 --- a/tests/var_scope.rs +++ b/tests/var_scope.rs @@ -79,6 +79,17 @@ fn test_var_scope() -> Result<(), Box> { ); } + scope.clear(); + + scope.push("x", 42 as INT); + scope.push_constant("x", 42 as INT); + + let scope2 = scope.clone(); + let scope3 = scope.clone_visible(); + + assert_eq!(scope2.is_constant("x"), Some(true)); + assert_eq!(scope3.is_constant("x"), Some(true)); + Ok(()) }