Use actual outer scope for function-bang calls.

This commit is contained in:
Stephen Chung 2021-11-14 22:48:57 +08:00
parent dc918447b6
commit 615c3acad6
7 changed files with 67 additions and 19 deletions

View File

@ -4,6 +4,11 @@ Rhai Release Notes
Version 1.2.0 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 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. * `#[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. * 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. * `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 Enhancements
------------ ------------

View File

@ -1836,6 +1836,8 @@ impl FnCallHashes {
pub struct FnCallExpr { pub struct FnCallExpr {
/// Namespace of the function, if any. /// Namespace of the function, if any.
pub namespace: Option<NamespaceRef>, pub namespace: Option<NamespaceRef>,
/// Function name.
pub name: Identifier,
/// Pre-calculated hashes. /// Pre-calculated hashes.
pub hashes: FnCallHashes, pub hashes: FnCallHashes,
/// List of function call argument expressions. /// List of function call argument expressions.
@ -1851,8 +1853,6 @@ pub struct FnCallExpr {
/// an [`Expr::DynamicConstant`] involves an additional allocation. Keeping the constant /// an [`Expr::DynamicConstant`] involves an additional allocation. Keeping the constant
/// values in an inlined array avoids these extra allocations. /// values in an inlined array avoids these extra allocations.
pub constants: smallvec::SmallVec<[Dynamic; 2]>, pub constants: smallvec::SmallVec<[Dynamic; 2]>,
/// Function name.
pub name: Identifier,
/// Does this function call capture the parent scope? /// Does this function call capture the parent scope?
pub capture_parent_scope: bool, pub capture_parent_scope: bool,
} }

View File

@ -649,7 +649,7 @@ impl Engine {
is_ref_mut: bool, is_ref_mut: bool,
is_method_call: bool, is_method_call: bool,
pos: Position, pos: Position,
captured_scope: Option<Scope>, scope: Option<&mut Scope>,
_level: usize, _level: usize,
) -> Result<(Dynamic, bool), Box<EvalAltResult>> { ) -> Result<(Dynamic, bool), Box<EvalAltResult>> {
fn no_method_err(name: &str, pos: Position) -> Result<(Dynamic, bool), Box<EvalAltResult>> { fn no_method_err(name: &str, pos: Position) -> Result<(Dynamic, bool), Box<EvalAltResult>> {
@ -728,7 +728,14 @@ impl Engine {
return Ok((Dynamic::UNIT, false)); 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 { let result = if _is_method_call {
// Method call of script function - map first argument to `this` // Method call of script function - map first argument to `this`
@ -740,7 +747,7 @@ impl Engine {
let level = _level + 1; let level = _level + 1;
let result = self.call_script_fn( let result = self.call_script_fn(
&mut scope, scope,
mods, mods,
state, state,
lib, lib,
@ -772,9 +779,8 @@ impl Engine {
let level = _level + 1; let level = _level + 1;
let result = self.call_script_fn( let result =
&mut scope, mods, state, lib, &mut None, func, args, pos, level, self.call_script_fn(scope, mods, state, lib, &mut None, func, args, pos, level);
);
// Restore the original source // Restore the original source
mods.source = orig_source; mods.source = orig_source;
@ -787,6 +793,8 @@ impl Engine {
result? result?
}; };
scope.rewind(orig_scope_len);
return Ok((result, false)); return Ok((result, false));
} }
@ -1225,12 +1233,30 @@ impl Engine {
let mut arg_values = StaticVec::with_capacity(args_expr.len()); let mut arg_values = StaticVec::with_capacity(args_expr.len());
let mut args = StaticVec::with_capacity(args_expr.len() + curry.len()); let mut args = StaticVec::with_capacity(args_expr.len() + curry.len());
let mut is_ref_mut = false; 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() { if args_expr.is_empty() && curry.is_empty() {
// No arguments // No arguments
} else { } else {
@ -1287,7 +1313,7 @@ impl Engine {
} }
self.exec_fn_call( 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) .map(|(v, _)| v)
} }

View File

@ -1801,7 +1801,7 @@ fn make_dot_expr(
// lhs.func!(...) // lhs.func!(...)
(_, Expr::FnCall(x, pos)) if x.capture_parent_scope => { (_, Expr::FnCall(x, pos)) if x.capture_parent_scope => {
return Err(PERR::MalformedCapture( 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)) .into_err(pos))
} }

View File

@ -517,7 +517,7 @@ impl<'a> Scope<'a> {
/// Shadowed variables are omitted in the copy. /// Shadowed variables are omitted in the copy.
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn clone_visible(&self) -> Self { pub fn clone_visible(&self) -> Self {
let mut entries = Self::new(); let mut entries = Self::new();
self.names self.names

View File

@ -164,5 +164,22 @@ fn test_functions_bang() -> Result<(), Box<EvalAltResult>> {
165 165
); );
assert_eq!(
engine.eval::<INT>(
"
fn foo() {
hello = 0;
hello + bar
}
let hello = 42;
let bar = 123;
foo!()
",
)?,
123
);
Ok(()) Ok(())
} }

View File

@ -204,8 +204,7 @@ fn test_function_pointers() -> Result<(), Box<EvalAltResult>> {
} }
#[test] #[test]
#[cfg(not(feature = "no_closure"))] fn test_internal_fn_bang() -> Result<(), Box<EvalAltResult>> {
fn test_internal_fn_captures() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new(); let engine = Engine::new();
assert_eq!( assert_eq!(
@ -219,7 +218,7 @@ fn test_internal_fn_captures() -> Result<(), Box<EvalAltResult>> {
foo!(1) + x foo!(1) + x
" "
)?, )?,
83 84
); );
assert!(engine assert!(engine