diff --git a/src/ast/script_fn.rs b/src/ast/script_fn.rs index a8dd6d39..a9d924e7 100644 --- a/src/ast/script_fn.rs +++ b/src/ast/script_fn.rs @@ -15,11 +15,11 @@ pub struct ScriptFnDef { pub body: StmtBlock, /// Encapsulated running environment, if any. pub lib: Option>, - /// Encapsulated [`GlobalRuntimeState`][crate::GlobalRuntimeState]. + /// Encapsulated stack of imported modules, if any. /// /// Not available under `no_module`. #[cfg(not(feature = "no_module"))] - pub global: crate::engine::GlobalRuntimeState, + pub global: Option)]>>, /// Function name. pub name: Identifier, /// Function access mode. diff --git a/src/engine.rs b/src/engine.rs index 75f44be0..9ae4d5bb 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -771,10 +771,10 @@ pub struct FnResolutionCacheEntry { /// Exported under the `internals` feature only. pub type FnResolutionCache = BTreeMap>>; -/// _(internals)_ A type that holds all the current states of the [`Engine`]. +/// _(internals)_ A type that holds all volatile evaluation states data. /// Exported under the `internals` feature only. -#[derive(Debug, Clone)] -pub struct EvalState { +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Hash)] +pub struct EvalStateData { /// Normally, access to variables are parsed with a relative offset into the [`Scope`] to avoid a lookup. /// In some situation, e.g. after running an `eval` statement, or after a custom syntax statement, /// subsequent offsets may become mis-aligned. @@ -783,6 +783,24 @@ pub struct EvalState { /// Level of the current scope. The global (root) level is zero, a new block (or function call) /// is one level higher, and so on. pub scope_level: usize, +} + +impl EvalStateData { + /// Create a new [`EvalStateData`]. + pub const fn new() -> Self { + Self { + always_search_scope: false, + scope_level: 0, + } + } +} + +/// _(internals)_ A type that holds all the current states of the [`Engine`]. +/// Exported under the `internals` feature only. +#[derive(Debug, Clone)] +pub struct EvalState { + /// Volatile states data. + pub data: EvalStateData, /// Stack of function resolution caches. fn_resolution_caches: StaticVec, } @@ -793,8 +811,7 @@ impl EvalState { #[must_use] pub const fn new() -> Self { Self { - always_search_scope: false, - scope_level: 0, + data: EvalStateData::new(), fn_resolution_caches: StaticVec::new_const(), } } @@ -802,7 +819,7 @@ impl EvalState { #[inline(always)] #[must_use] pub const fn is_global(&self) -> bool { - self.scope_level == 0 + self.data.scope_level == 0 } /// Get the number of function resolution cache(s) in the stack. #[inline(always)] @@ -1142,7 +1159,7 @@ impl Engine { let root = &namespace[0].name; // Qualified - check if the root module is directly indexed - let index = if state.always_search_scope { + let index = if state.data.always_search_scope { None } else { namespace.index() @@ -1267,7 +1284,7 @@ impl Engine { Err(ERR::ErrorUnboundThis(*pos).into()) } } - _ if state.always_search_scope => (0, expr.position()), + _ if state.data.always_search_scope => (0, expr.position()), Expr::Variable(Some(i), pos, _) => (i.get() as usize, *pos), Expr::Variable(None, pos, v) => (v.0.map(NonZeroUsize::get).unwrap_or(0), *pos), _ => unreachable!("Expr::Variable expected, but gets {:?}", expr), @@ -2454,13 +2471,13 @@ impl Engine { return Ok(Dynamic::UNIT); } - let orig_always_search_scope = state.always_search_scope; + let orig_always_search_scope = state.data.always_search_scope; let orig_scope_len = scope.len(); let orig_mods_len = global.num_imported_modules(); let orig_fn_resolution_caches_len = state.fn_resolution_caches_len(); if rewind_scope { - state.scope_level += 1; + state.data.scope_level += 1; } let result = statements.iter().try_fold(Dynamic::UNIT, |_, stmt| { @@ -2500,14 +2517,14 @@ impl Engine { if rewind_scope { scope.rewind(orig_scope_len); - state.scope_level -= 1; + state.data.scope_level -= 1; } if restore_orig_state { global.truncate_modules(orig_mods_len); // The impact of new local variables goes away at the end of a block // because any new variables introduced will go out of scope - state.always_search_scope = orig_always_search_scope; + state.data.always_search_scope = orig_always_search_scope; } result diff --git a/src/func/call.rs b/src/func/call.rs index 24dabc9d..b6881dd9 100644 --- a/src/func/call.rs +++ b/src/func/call.rs @@ -695,48 +695,6 @@ impl Engine { }) } - /// Evaluate a text script in place - used primarily for 'eval'. - fn eval_script_expr_in_place( - &self, - scope: &mut Scope, - global: &mut GlobalRuntimeState, - lib: &[&Module], - script: impl AsRef, - _pos: Position, - level: usize, - ) -> RhaiResult { - #[cfg(not(feature = "unchecked"))] - self.inc_operations(&mut global.num_operations, _pos)?; - - let script = script.as_ref().trim(); - if script.is_empty() { - return Ok(Dynamic::UNIT); - } - - // Compile the script text - // No optimizations because we only run it once - let ast = self.compile_with_scope_and_optimization_level( - &Scope::new(), - &[script], - #[cfg(not(feature = "no_optimize"))] - crate::OptimizationLevel::None, - )?; - - // If new functions are defined within the eval string, it is an error - #[cfg(not(feature = "no_function"))] - if !ast.shared_lib().is_empty() { - return Err(crate::PERR::WrongFnDefinition.into()); - } - - let statements = ast.statements(); - if statements.is_empty() { - return Ok(Dynamic::UNIT); - } - - // Evaluate the AST - self.eval_global_statements(scope, global, &mut EvalState::new(), statements, lib, level) - } - /// Call a dot method. #[cfg(not(feature = "no_object"))] pub(crate) fn make_method_call( @@ -1081,13 +1039,14 @@ impl Engine { let script = &value .into_immutable_string() .map_err(|typ| self.make_type_mismatch_err::(typ, pos))?; + let level = level + 1; let result = - self.eval_script_expr_in_place(scope, global, lib, script, pos, level + 1); + self.eval_script_expr_in_place(scope, global, state, lib, script, pos, level); // IMPORTANT! If the eval defines new variables in the current scope, // all variable offsets from this point on will be mis-aligned. if scope.len() != orig_scope_len { - state.always_search_scope = true; + state.data.always_search_scope = true; } return result.map_err(|err| { diff --git a/src/func/native.rs b/src/func/native.rs index da7e104d..f3d45d1d 100644 --- a/src/func/native.rs +++ b/src/func/native.rs @@ -186,7 +186,8 @@ impl<'a> NativeCallContext<'a> { pub const fn source(&self) -> Option<&str> { self.source } - /// Get an iterator over the current set of modules imported via `import` statements. + /// Get an iterator over the current set of modules imported via `import` statements + /// in reverse order. /// /// Not available under `no_module`. #[cfg(not(feature = "no_module"))] @@ -194,7 +195,7 @@ impl<'a> NativeCallContext<'a> { pub fn iter_imports(&self) -> impl Iterator { self.global.iter().flat_map(|&m| m.iter_modules()) } - /// Get an iterator over the current set of modules imported via `import` statements. + /// Get an iterator over the current set of modules imported via `import` statements in reverse order. #[cfg(not(feature = "no_module"))] #[allow(dead_code)] #[inline] @@ -214,10 +215,11 @@ impl<'a> NativeCallContext<'a> { pub const fn global_runtime_state(&self) -> Option<&GlobalRuntimeState> { self.global } - /// Get an iterator over the namespaces containing definitions of all script-defined functions. + /// Get an iterator over the namespaces containing definitions of all script-defined functions + /// in reverse order. #[inline] pub fn iter_namespaces(&self) -> impl Iterator { - self.lib.iter().cloned() + self.lib.iter().rev().cloned() } /// _(internals)_ The current set of namespaces containing definitions of all script-defined functions. /// Exported under the `internals` feature only. diff --git a/src/func/script.rs b/src/func/script.rs index 285e3045..02c08ad9 100644 --- a/src/func/script.rs +++ b/src/func/script.rs @@ -3,7 +3,7 @@ use super::call::FnCallArgs; use crate::ast::ScriptFnDef; -use crate::engine::{EvalState, GlobalRuntimeState}; +use crate::engine::{EvalState, EvalStateData, GlobalRuntimeState}; use crate::r#unsafe::unsafe_cast_var_name_to_lifetime; use crate::{Dynamic, Engine, Module, Position, RhaiError, RhaiResult, Scope, StaticVec, ERR}; use std::mem; @@ -91,6 +91,8 @@ impl Engine { // Merge in encapsulated environment, if any let mut lib_merged = StaticVec::with_capacity(lib.len() + 1); let orig_fn_resolution_caches_len = state.fn_resolution_caches_len(); + let orig_states_data = state.data; + state.data = EvalStateData::new(); let lib = if let Some(ref fn_lib) = fn_def.lib { if fn_lib.is_empty() { @@ -106,15 +108,14 @@ impl Engine { }; #[cfg(not(feature = "no_module"))] - if fn_def.global.num_imported_modules() > 0 { - fn_def - .global - .iter_modules_raw() - .for_each(|(n, m)| global.push_module(n.clone(), m.clone())); + if let Some(ref modules) = fn_def.global { + modules + .iter() + .cloned() + .for_each(|(n, m)| global.push_module(n, m)); } // Evaluate the function - let body = &fn_def.body; let result = self .eval_stmt_block( scope, @@ -122,7 +123,7 @@ impl Engine { state, lib, this_ptr, - body, + &fn_def.body, true, rewind_scope, level, @@ -149,15 +150,17 @@ impl Engine { _ => make_error(fn_def.name.to_string(), fn_def, global, err, pos), }); - // Remove all local variables + // Remove all local variables and imported modules if rewind_scope { scope.rewind(orig_scope_len); } else if !args.is_empty() { // Remove arguments only, leaving new variables in the scope scope.remove_range(orig_scope_len, args.len()) } - global.truncate_modules(orig_mods_len); + + // Restore state + state.data = orig_states_data; state.rewind_fn_resolution_caches(orig_fn_resolution_caches_len); result @@ -193,4 +196,47 @@ impl Engine { result } + + /// Evaluate a text script in place - used primarily for 'eval'. + pub(crate) fn eval_script_expr_in_place( + &self, + scope: &mut Scope, + global: &mut GlobalRuntimeState, + state: &mut EvalState, + lib: &[&Module], + script: impl AsRef, + _pos: Position, + level: usize, + ) -> RhaiResult { + #[cfg(not(feature = "unchecked"))] + self.inc_operations(&mut global.num_operations, _pos)?; + + let script = script.as_ref().trim(); + if script.is_empty() { + return Ok(Dynamic::UNIT); + } + + // Compile the script text + // No optimizations because we only run it once + let ast = self.compile_with_scope_and_optimization_level( + &Scope::new(), + &[script], + #[cfg(not(feature = "no_optimize"))] + crate::OptimizationLevel::None, + )?; + + // If new functions are defined within the eval string, it is an error + #[cfg(not(feature = "no_function"))] + if !ast.shared_lib().is_empty() { + return Err(crate::PERR::WrongFnDefinition.into()); + } + + let statements = ast.statements(); + if statements.is_empty() { + return Ok(Dynamic::UNIT); + } + + // Evaluate the AST + self.eval_global_statements(scope, global, state, statements, lib, level) + } } diff --git a/src/lib.rs b/src/lib.rs index d9e61697..757c3135 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -241,7 +241,9 @@ pub use ast::{ pub use ast::FloatWrapper; #[cfg(feature = "internals")] -pub use engine::{EvalState, FnResolutionCache, FnResolutionCacheEntry, GlobalRuntimeState}; +pub use engine::{ + EvalState, EvalStateData, FnResolutionCache, FnResolutionCacheEntry, GlobalRuntimeState, +}; #[cfg(feature = "internals")] pub use module::Namespace; diff --git a/src/module/mod.rs b/src/module/mod.rs index c3b332ee..a182574e 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1580,15 +1580,17 @@ impl Module { }); // Extra modules left in the scope become sub-modules - let mut func_global = crate::engine::GlobalRuntimeState::new(); + let mut func_global = None; - global - .into_iter() - .skip(orig_mods_len) - .for_each(|(alias, m)| { - func_global.push_module(alias.clone(), m.clone()); - module.set_sub_module(alias, m); - }); + global.into_iter().skip(orig_mods_len).for_each(|kv| { + if func_global.is_none() { + func_global = Some(StaticVec::new()); + } + func_global.as_mut().expect("`Some`").push(kv.clone()); + module.set_sub_module(kv.0, kv.1); + }); + + let func_global = func_global.map(|v| v.into_boxed_slice()); // Non-private functions defined become module functions #[cfg(not(feature = "no_function"))] diff --git a/src/optimizer.rs b/src/optimizer.rs index 77390f99..aca3df1c 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -1155,7 +1155,7 @@ pub fn optimize_into_ast( params: fn_def.params.clone(), lib: None, #[cfg(not(feature = "no_module"))] - global: GlobalRuntimeState::new(), + global: None, #[cfg(not(feature = "no_function"))] #[cfg(feature = "metadata")] comments: None, diff --git a/src/parser.rs b/src/parser.rs index 5226cca2..46d1a742 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3129,7 +3129,7 @@ fn parse_fn( body, lib: None, #[cfg(not(feature = "no_module"))] - global: crate::engine::GlobalRuntimeState::new(), + global: None, #[cfg(not(feature = "no_function"))] #[cfg(feature = "metadata")] comments: if comments.is_empty() { @@ -3280,7 +3280,7 @@ fn parse_anon_fn( body: body.into(), lib: None, #[cfg(not(feature = "no_module"))] - global: crate::engine::GlobalRuntimeState::new(), + global: None, #[cfg(not(feature = "no_function"))] #[cfg(feature = "metadata")] comments: None,