diff --git a/src/ast.rs b/src/ast.rs index 51da78e6..735d3f8c 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1400,6 +1400,8 @@ pub struct FnCallExpr { pub capture: bool, /// List of function call arguments. pub args: StaticVec, + /// List of function call arguments that are constants. + pub constant_args: StaticVec<(Dynamic, Position)>, /// Namespace of the function, if any. Boxed because it occurs rarely. pub namespace: Option, /// Function name. @@ -1408,6 +1410,19 @@ pub struct FnCallExpr { pub name: Cow<'static, str>, } +impl FnCallExpr { + /// Are there no arguments to this function call? + #[inline(always)] + pub fn is_args_empty(&self) -> bool { + self.args.is_empty() && self.constant_args.is_empty() + } + /// Get the number of arguments to this function call. + #[inline(always)] + pub fn num_args(&self) -> usize { + self.args.len() + self.constant_args.len() + } +} + /// A type that wraps a [`FLOAT`] and implements [`Hash`]. #[cfg(not(feature = "no_float"))] #[derive(Clone, Copy, PartialEq, PartialOrd)] diff --git a/src/engine.rs b/src/engine.rs index 8652ef25..442e806f 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1440,16 +1440,21 @@ impl Engine { Expr::FnCall(x, _) if parent_chain_type == ChainType::Dot && x.namespace.is_none() => { let mut arg_positions: StaticVec<_> = Default::default(); - let arg_values = x + let mut arg_values = x .args .iter() + .inspect(|arg_expr| arg_positions.push(arg_expr.position())) .map(|arg_expr| { - arg_positions.push(arg_expr.position()); self.eval_expr(scope, mods, state, lib, this_ptr, arg_expr, level) .map(Dynamic::flatten) }) .collect::, _>>()?; + x.constant_args + .iter() + .inspect(|(_, pos)| arg_positions.push(*pos)) + .for_each(|(v, _)| arg_values.push(v.clone())); + idx_values.push((arg_values, arg_positions).into()); } Expr::FnCall(_, _) if parent_chain_type == ChainType::Dot => { @@ -1475,16 +1480,21 @@ impl Engine { { let mut arg_positions: StaticVec<_> = Default::default(); - let arg_values = x + let mut arg_values = x .args .iter() + .inspect(|arg_expr| arg_positions.push(arg_expr.position())) .map(|arg_expr| { - arg_positions.push(arg_expr.position()); self.eval_expr(scope, mods, state, lib, this_ptr, arg_expr, level) .map(Dynamic::flatten) }) .collect::, _>>()?; + x.constant_args + .iter() + .inspect(|(_, pos)| arg_positions.push(*pos)) + .for_each(|(v, _)| arg_values.push(v.clone())); + (arg_values, arg_positions).into() } Expr::FnCall(_, _) if parent_chain_type == ChainType::Dot => { @@ -1700,10 +1710,12 @@ impl Engine { capture, hash, args, + constant_args: c_args, .. } = x.as_ref(); self.make_function_call( - scope, mods, state, lib, this_ptr, name, args, *hash, *pos, *capture, level, + scope, mods, state, lib, this_ptr, name, args, c_args, *hash, *pos, *capture, + level, ) } @@ -1714,12 +1726,14 @@ impl Engine { namespace, hash, args, + constant_args: c_args, .. } = x.as_ref(); let namespace = namespace.as_ref(); let hash = hash.native_hash(); self.make_qualified_function_call( - scope, mods, state, lib, this_ptr, namespace, name, args, hash, *pos, level, + scope, mods, state, lib, this_ptr, namespace, name, args, c_args, hash, *pos, + level, ) } diff --git a/src/fn_call.rs b/src/fn_call.rs index 18cc509c..d02d75e6 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -1058,33 +1058,39 @@ impl Engine { this_ptr: &mut Option<&mut Dynamic>, fn_name: &str, args_expr: &[Expr], + constant_args: &[(Dynamic, Position)], mut hash: FnCallHash, pos: Position, capture_scope: bool, level: usize, ) -> RhaiResult { - let args_expr = args_expr.as_ref(); - // Handle call() - Redirect function call let redirected; - let mut args_expr = args_expr.as_ref(); + let mut args_expr = args_expr; + let mut constant_args = constant_args; + let mut total_args = args_expr.len() + constant_args.len(); let mut curry = StaticVec::new(); let mut name = fn_name; match name { // Handle call() - KEYWORD_FN_PTR_CALL if args_expr.len() >= 1 => { - let fn_ptr = - self.eval_expr(scope, mods, state, lib, this_ptr, &args_expr[0], level)?; + KEYWORD_FN_PTR_CALL if total_args >= 1 => { + let (arg, arg_pos) = args_expr.get(0).map_or_else( + || Ok(constant_args[0].clone()), + |arg| { + self.eval_expr(scope, mods, state, lib, this_ptr, arg, level) + .map(|v| (v, arg.position())) + }, + )?; - if !fn_ptr.is::() { + if !arg.is::() { return Err(self.make_type_mismatch_err::( - self.map_type_name(fn_ptr.type_name()), - args_expr[0].position(), + self.map_type_name(arg.type_name()), + arg_pos, )); } - let fn_ptr = fn_ptr.cast::(); + let fn_ptr = arg.cast::(); curry.extend(fn_ptr.curry().iter().cloned()); // Redirect function name @@ -1092,10 +1098,15 @@ impl Engine { name = &redirected; // Skip the first argument - args_expr = &args_expr.as_ref()[1..]; + if !args_expr.is_empty() { + args_expr = &args_expr[1..]; + } else { + constant_args = &constant_args[1..]; + } + total_args -= 1; // Recalculate hash - let args_len = args_expr.len() + curry.len(); + let args_len = total_args + curry.len(); hash = if !hash.is_native_only() { FnCallHash::from_script(calc_fn_hash(empty(), name, args_len)) } else { @@ -1103,66 +1114,95 @@ impl Engine { }; } // Handle Fn() - KEYWORD_FN_PTR if args_expr.len() == 1 => { + KEYWORD_FN_PTR if total_args == 1 => { + let (arg, arg_pos) = args_expr.get(0).map_or_else( + || Ok(constant_args[0].clone()), + |arg| { + self.eval_expr(scope, mods, state, lib, this_ptr, arg, level) + .map(|v| (v, arg.position())) + }, + )?; + // Fn - only in function call style - return self - .eval_expr(scope, mods, state, lib, this_ptr, &args_expr[0], level)? + return arg .take_immutable_string() - .map_err(|typ| { - self.make_type_mismatch_err::(typ, args_expr[0].position()) - }) + .map_err(|typ| self.make_type_mismatch_err::(typ, arg_pos)) .and_then(|s| FnPtr::try_from(s)) .map(Into::::into) - .map_err(|err| err.fill_position(args_expr[0].position())); + .map_err(|err| err.fill_position(arg_pos)); } // Handle curry() - KEYWORD_FN_PTR_CURRY if args_expr.len() > 1 => { - let fn_ptr = - self.eval_expr(scope, mods, state, lib, this_ptr, &args_expr[0], level)?; + KEYWORD_FN_PTR_CURRY if total_args > 1 => { + let (arg, arg_pos) = args_expr.get(0).map_or_else( + || Ok(constant_args[0].clone()), + |arg| { + self.eval_expr(scope, mods, state, lib, this_ptr, arg, level) + .map(|v| (v, arg.position())) + }, + )?; - if !fn_ptr.is::() { + if !arg.is::() { return Err(self.make_type_mismatch_err::( - self.map_type_name(fn_ptr.type_name()), - args_expr[0].position(), + self.map_type_name(arg.type_name()), + arg_pos, )); } - let (name, mut fn_curry) = fn_ptr.cast::().take_data(); + let (name, mut fn_curry) = arg.cast::().take_data(); // Append the new curried arguments to the existing list. - - args_expr.iter().skip(1).try_for_each(|expr| { - self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) - .map(|value| fn_curry.push(value)) - })?; + if !args_expr.is_empty() { + args_expr.iter().skip(1).try_for_each(|expr| { + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) + .map(|value| fn_curry.push(value)) + })?; + fn_curry.extend(constant_args.iter().map(|(v, _)| v.clone())); + } else { + fn_curry.extend(constant_args.iter().skip(1).map(|(v, _)| v.clone())); + } return Ok(FnPtr::new_unchecked(name, fn_curry).into()); } // Handle is_shared() #[cfg(not(feature = "no_closure"))] - crate::engine::KEYWORD_IS_SHARED if args_expr.len() == 1 => { - let value = - self.eval_expr(scope, mods, state, lib, this_ptr, &args_expr[0], level)?; - return Ok(value.is_shared().into()); + crate::engine::KEYWORD_IS_SHARED if total_args == 1 => { + let arg = args_expr.get(0).map_or_else( + || Ok(constant_args[0].0.clone()), + |arg| self.eval_expr(scope, mods, state, lib, this_ptr, arg, level), + )?; + return Ok(arg.is_shared().into()); } // Handle is_def_fn() #[cfg(not(feature = "no_function"))] - crate::engine::KEYWORD_IS_DEF_FN if args_expr.len() == 2 => { - let fn_name = self - .eval_expr(scope, mods, state, lib, this_ptr, &args_expr[0], level)? + crate::engine::KEYWORD_IS_DEF_FN if total_args == 2 => { + let (arg, arg_pos) = if !args_expr.is_empty() { + ( + self.eval_expr(scope, mods, state, lib, this_ptr, &args_expr[0], level)?, + args_expr[0].position(), + ) + } else { + constant_args[0].clone() + }; + + let fn_name = arg .take_immutable_string() - .map_err(|err| { - self.make_type_mismatch_err::(err, args_expr[0].position()) - })?; - let num_params = self - .eval_expr(scope, mods, state, lib, this_ptr, &args_expr[1], level)? + .map_err(|err| self.make_type_mismatch_err::(err, arg_pos))?; + + let (arg, arg_pos) = if args_expr.len() > 1 { + ( + self.eval_expr(scope, mods, state, lib, this_ptr, &args_expr[1], level)?, + args_expr[1].position(), + ) + } else { + constant_args[if args_expr.is_empty() { 1 } else { 0 }].clone() + }; + + let num_params = arg .as_int() - .map_err(|err| { - self.make_type_mismatch_err::(err, args_expr[0].position()) - })?; + .map_err(|err| self.make_type_mismatch_err::(err, arg_pos))?; return Ok(if num_params < 0 { Dynamic::FALSE @@ -1174,29 +1214,34 @@ impl Engine { } // Handle is_def_var() - KEYWORD_IS_DEF_VAR if args_expr.len() == 1 => { - let var_name = self - .eval_expr(scope, mods, state, lib, this_ptr, &args_expr[0], level)? + KEYWORD_IS_DEF_VAR if total_args == 1 => { + let (arg, arg_pos) = args_expr.get(0).map_or_else( + || Ok(constant_args[0].clone()), + |arg| { + self.eval_expr(scope, mods, state, lib, this_ptr, arg, level) + .map(|v| (v, arg.position())) + }, + )?; + let var_name = arg .take_immutable_string() - .map_err(|err| { - self.make_type_mismatch_err::(err, args_expr[0].position()) - })?; + .map_err(|err| self.make_type_mismatch_err::(err, arg_pos))?; return Ok(scope.contains(&var_name).into()); } // Handle eval() - KEYWORD_EVAL if args_expr.len() == 1 => { - let script_expr = &args_expr[0]; - let script_pos = script_expr.position(); - + KEYWORD_EVAL if total_args == 1 => { // eval - only in function call style let prev_len = scope.len(); - let script = self - .eval_expr(scope, mods, state, lib, this_ptr, script_expr, level)? - .take_immutable_string() - .map_err(|typ| { - self.make_type_mismatch_err::(typ, script_pos) - })?; + let (script, script_pos) = args_expr.get(0).map_or_else( + || Ok(constant_args[0].clone()), + |script_expr| { + self.eval_expr(scope, mods, state, lib, this_ptr, script_expr, level) + .map(|v| (v, script_expr.position())) + }, + )?; + let script = script.take_immutable_string().map_err(|typ| { + self.make_type_mismatch_err::(typ, script_pos) + })?; let result = self.eval_script_expr_in_place( scope, mods, @@ -1240,14 +1285,17 @@ impl Engine { None }; - if args_expr.is_empty() && curry.is_empty() { + if args_expr.is_empty() && constant_args.is_empty() && curry.is_empty() { // No arguments args = Default::default(); } else { // If the first argument is a variable, and there is no curried arguments, // convert to method-call style in order to leverage potential &mut first argument and // avoid cloning the value - if curry.is_empty() && args_expr[0].get_variable_access(false).is_some() { + if curry.is_empty() + && !args_expr.is_empty() + && args_expr[0].get_variable_access(false).is_some() + { // func(x, ...) -> x.func(...) arg_values = args_expr .iter() @@ -1256,6 +1304,7 @@ impl Engine { self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) .map(Dynamic::flatten) }) + .chain(constant_args.iter().map(|(v, _)| Ok(v.clone()))) .collect::>()?; let (mut target, pos) = @@ -1285,6 +1334,7 @@ impl Engine { self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) .map(Dynamic::flatten) }) + .chain(constant_args.iter().map(|(v, _)| Ok(v.clone()))) .collect::>()?; args = curry.iter_mut().chain(arg_values.iter_mut()).collect(); @@ -1310,25 +1360,24 @@ impl Engine { namespace: Option<&NamespaceRef>, fn_name: &str, args_expr: &[Expr], + constant_args: &[(Dynamic, Position)], hash: u64, pos: Position, level: usize, ) -> RhaiResult { - let args_expr = args_expr.as_ref(); - let namespace = namespace.unwrap(); let mut arg_values: StaticVec<_>; let mut first_arg_value = None; let mut args: StaticVec<_>; - if args_expr.is_empty() { + if args_expr.is_empty() && constant_args.is_empty() { // No arguments args = Default::default(); } else { // See if the first argument is a variable (not namespace-qualified). // If so, convert to method-call style in order to leverage potential // &mut first argument and avoid cloning the value - if args_expr[0].get_variable_access(true).is_some() { + if !args_expr.is_empty() && args_expr[0].get_variable_access(true).is_some() { // func(x, ...) -> x.func(...) arg_values = args_expr .iter() @@ -1342,6 +1391,7 @@ impl Engine { .map(Dynamic::flatten) } }) + .chain(constant_args.iter().map(|(v, _)| Ok(v.clone()))) .collect::>()?; // Get target reference to first argument @@ -1368,6 +1418,7 @@ impl Engine { self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) .map(Dynamic::flatten) }) + .chain(constant_args.iter().map(|(v, _)| Ok(v.clone()))) .collect::>()?; args = arg_values.iter_mut().collect(); diff --git a/src/optimize.rs b/src/optimize.rs index 383ca44f..61edaac9 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -740,11 +740,14 @@ fn optimize_expr(expr: &mut Expr, state: &mut State) { Expr::FnCall(x, pos) if x.namespace.is_none() // Non-qualified && state.optimization_level == OptimizationLevel::Simple // simple optimizations - && x.args.len() == 2 // binary call + && x.num_args() == 2 // binary call && x.args.iter().all(Expr::is_constant) // all arguments are constants //&& !is_valid_identifier(x.name.chars()) // cannot be scripted => { - let mut arg_values: StaticVec<_> = x.args.iter().map(|e| e.get_constant_value().unwrap()).collect(); + let mut arg_values: StaticVec<_> = x.args.iter().map(|e| e.get_constant_value().unwrap()) + .chain(x.constant_args.iter().map(|(v, _)| v).cloned()) + .collect(); + let arg_types: StaticVec<_> = arg_values.iter().map(Dynamic::type_id).collect(); // Search for overloaded operators (can override built-in). @@ -764,6 +767,15 @@ fn optimize_expr(expr: &mut Expr, state: &mut State) { } x.args.iter_mut().for_each(|a| optimize_expr(a, state)); + + // Move constant arguments to the right + while x.args.last().map(Expr::is_constant).unwrap_or(false) { + let arg = x.args.pop().unwrap(); + let arg_pos = arg.position(); + x.constant_args.insert(0, (arg.get_constant_value().unwrap(), arg_pos)); + } + + x.args.shrink_to_fit(); } // Eagerly call functions @@ -774,12 +786,14 @@ fn optimize_expr(expr: &mut Expr, state: &mut State) { => { // First search for script-defined functions (can override built-in) #[cfg(not(feature = "no_function"))] - let has_script_fn = state.lib.iter().any(|&m| m.get_script_fn(x.name.as_ref(), x.args.len()).is_some()); + let has_script_fn = state.lib.iter().any(|&m| m.get_script_fn(x.name.as_ref(), x.num_args()).is_some()); #[cfg(feature = "no_function")] let has_script_fn = false; if !has_script_fn { - let mut arg_values: StaticVec<_> = x.args.iter().map(|e| e.get_constant_value().unwrap()).collect(); + let mut arg_values: StaticVec<_> = x.args.iter().map(|e| e.get_constant_value().unwrap()) + .chain(x.constant_args.iter().map(|(v, _)| v).cloned()) + .collect(); // Save the typename of the first argument if it is `type_of()` // This is to avoid `call_args` being passed into the closure @@ -810,7 +824,18 @@ fn optimize_expr(expr: &mut Expr, state: &mut State) { } // id(args ..) -> optimize function call arguments - Expr::FnCall(x, _) => x.args.iter_mut().for_each(|a| optimize_expr(a, state)), + Expr::FnCall(x, _) => { + x.args.iter_mut().for_each(|a| optimize_expr(a, state)); + + // Move constant arguments to the right + while x.args.last().map(Expr::is_constant).unwrap_or(false) { + let arg = x.args.pop().unwrap(); + let arg_pos = arg.position(); + x.constant_args.insert(0, (arg.get_constant_value().unwrap(), arg_pos)); + } + + x.args.shrink_to_fit(); + } // constant-name Expr::Variable(x) if x.1.is_none() && state.find_constant(&x.2.name).is_some() => { diff --git a/src/parser.rs b/src/parser.rs index fa5aa424..b0f6f963 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1531,8 +1531,8 @@ fn make_dot_expr( Expr::FnCall(mut func, func_pos) => { // Recalculate hash func.hash = FnCallHash::from_script_and_native( - calc_fn_hash(empty(), &func.name, func.args.len()), - calc_fn_hash(empty(), &func.name, func.args.len() + 1), + calc_fn_hash(empty(), &func.name, func.num_args()), + calc_fn_hash(empty(), &func.name, func.num_args() + 1), ); let rhs = Expr::Dot( @@ -1563,7 +1563,7 @@ fn make_dot_expr( } // lhs.Fn() or lhs.eval() (_, Expr::FnCall(x, pos)) - if x.args.len() == 0 + if x.is_args_empty() && [crate::engine::KEYWORD_FN_PTR, crate::engine::KEYWORD_EVAL] .contains(&x.name.as_ref()) => { @@ -1587,8 +1587,8 @@ fn make_dot_expr( (lhs, Expr::FnCall(mut func, func_pos)) => { // Recalculate hash func.hash = FnCallHash::from_script_and_native( - calc_fn_hash(empty(), &func.name, func.args.len()), - calc_fn_hash(empty(), &func.name, func.args.len() + 1), + calc_fn_hash(empty(), &func.name, func.num_args()), + calc_fn_hash(empty(), &func.name, func.num_args() + 1), ); let rhs = Expr::FnCall(func, func_pos); Expr::Dot(Box::new(BinaryExpr { lhs, rhs }), op_pos) diff --git a/tests/operations.rs b/tests/operations.rs index bcc9019f..9ebf5fcf 100644 --- a/tests/operations.rs +++ b/tests/operations.rs @@ -72,7 +72,7 @@ fn test_max_operations_functions() -> Result<(), Box> { fn inc(x) { x + 1 } let x = 0; - while x < 28 { + while x < 31 { print(x); x = inc(x); }