diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c981e62..ae7ec6b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ Rhai Release Notes Version 1.2.0 ============= +Breaking changes +---------------- + +* As originally intended, function calls with a bang (`!`) now operates directly on the caller's scope, allowing variables inside the scope to be mutated. + Bug fixes --------- @@ -15,6 +20,7 @@ New features * `#[cfg(...)]` attributes can now be put directly on plugin functions or function defined in a plugin module. * A custom syntax parser can now return a symbol starting with `$$` to inform the implementation function which syntax variant was actually parsed. * `AST::iter_literal_variables` extracts all top-level literal constant/variable definitions from a script without running it. +* `Scope::clone_visible` is added that copies only the last instance of each variable, omitting all shadowed variables. Enhancements ------------ diff --git a/src/ast.rs b/src/ast.rs index 4a6a81d9..4413fe4d 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1836,6 +1836,8 @@ impl FnCallHashes { pub struct FnCallExpr { /// Namespace of the function, if any. pub namespace: Option, + /// Function name. + pub name: Identifier, /// Pre-calculated hashes. pub hashes: FnCallHashes, /// List of function call argument expressions. @@ -1851,8 +1853,6 @@ pub struct FnCallExpr { /// an [`Expr::DynamicConstant`] involves an additional allocation. Keeping the constant /// values in an inlined array avoids these extra allocations. pub constants: smallvec::SmallVec<[Dynamic; 2]>, - /// Function name. - pub name: Identifier, /// Does this function call capture the parent scope? pub capture_parent_scope: bool, } diff --git a/src/func/call.rs b/src/func/call.rs index ec49af73..10bcb234 100644 --- a/src/func/call.rs +++ b/src/func/call.rs @@ -649,7 +649,7 @@ impl Engine { is_ref_mut: bool, is_method_call: bool, pos: Position, - captured_scope: Option, + scope: Option<&mut Scope>, _level: usize, ) -> Result<(Dynamic, bool), Box> { fn no_method_err(name: &str, pos: Position) -> Result<(Dynamic, bool), Box> { @@ -728,7 +728,14 @@ impl Engine { return Ok((Dynamic::UNIT, false)); } - let mut scope = captured_scope.unwrap_or_else(|| Scope::new()); + let mut empty_scope; + let scope = if let Some(scope) = scope { + scope + } else { + empty_scope = Scope::new(); + &mut empty_scope + }; + let orig_scope_len = scope.len(); let result = if _is_method_call { // Method call of script function - map first argument to `this` @@ -740,7 +747,7 @@ impl Engine { let level = _level + 1; let result = self.call_script_fn( - &mut scope, + scope, mods, state, lib, @@ -772,9 +779,8 @@ impl Engine { let level = _level + 1; - let result = self.call_script_fn( - &mut scope, mods, state, lib, &mut None, func, args, pos, level, - ); + let result = + self.call_script_fn(scope, mods, state, lib, &mut None, func, args, pos, level); // Restore the original source mods.source = orig_source; @@ -787,6 +793,8 @@ impl Engine { result? }; + scope.rewind(orig_scope_len); + return Ok((result, false)); } @@ -1225,12 +1233,30 @@ impl Engine { let mut arg_values = StaticVec::with_capacity(args_expr.len()); let mut args = StaticVec::with_capacity(args_expr.len() + curry.len()); let mut is_ref_mut = false; - let capture = if capture_scope && !scope.is_empty() { - Some(scope.clone_visible()) - } else { - None - }; + // Capture parent scope? + if capture_scope && !scope.is_empty() { + // func(..., ...) + for index in 0..args_expr.len() { + let (value, _) = self.get_arg_value( + scope, mods, state, lib, this_ptr, level, args_expr, constants, index, + )?; + arg_values.push(value.flatten()); + } + args.extend(curry.iter_mut()); + args.extend(arg_values.iter_mut()); + + // Use parent scope + let scope = Some(scope); + + return self + .exec_fn_call( + mods, state, lib, name, hashes, &mut args, is_ref_mut, false, pos, scope, level, + ) + .map(|(v, _)| v); + } + + // Call with blank scope if args_expr.is_empty() && curry.is_empty() { // No arguments } else { @@ -1287,7 +1313,7 @@ impl Engine { } self.exec_fn_call( - mods, state, lib, name, hashes, &mut args, is_ref_mut, false, pos, capture, level, + mods, state, lib, name, hashes, &mut args, is_ref_mut, false, pos, None, level, ) .map(|(v, _)| v) } diff --git a/src/parser.rs b/src/parser.rs index 6cfa647b..ddf540c8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1801,7 +1801,7 @@ fn make_dot_expr( // lhs.func!(...) (_, Expr::FnCall(x, pos)) if x.capture_parent_scope => { return Err(PERR::MalformedCapture( - "method-call style does not support capturing".into(), + "method-call style does not support running within the caller's scope".into(), ) .into_err(pos)) } diff --git a/src/types/scope.rs b/src/types/scope.rs index e5fdd8fe..00c540cf 100644 --- a/src/types/scope.rs +++ b/src/types/scope.rs @@ -517,7 +517,7 @@ impl<'a> Scope<'a> { /// Shadowed variables are omitted in the copy. #[inline] #[must_use] - pub(crate) fn clone_visible(&self) -> Self { + pub fn clone_visible(&self) -> Self { let mut entries = Self::new(); self.names diff --git a/tests/functions.rs b/tests/functions.rs index 3132907d..ba586cee 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -164,5 +164,22 @@ fn test_functions_bang() -> Result<(), Box> { 165 ); + assert_eq!( + engine.eval::( + " + fn foo() { + hello = 0; + hello + bar + } + + let hello = 42; + let bar = 123; + + foo!() + ", + )?, + 123 + ); + Ok(()) } diff --git a/tests/internal_fn.rs b/tests/internal_fn.rs index 18ee31fd..f4dc2ae8 100644 --- a/tests/internal_fn.rs +++ b/tests/internal_fn.rs @@ -204,8 +204,7 @@ fn test_function_pointers() -> Result<(), Box> { } #[test] -#[cfg(not(feature = "no_closure"))] -fn test_internal_fn_captures() -> Result<(), Box> { +fn test_internal_fn_bang() -> Result<(), Box> { let engine = Engine::new(); assert_eq!( @@ -219,7 +218,7 @@ fn test_internal_fn_captures() -> Result<(), Box> { foo!(1) + x " )?, - 83 + 84 ); assert!(engine