diff --git a/Cargo.toml b/Cargo.toml index d30ffc5bc..1973973eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,8 @@ decimal = ["rust_decimal"] serde = ["dep:serde", "smartstring/serde", "smallvec/serde", "thin-vec/serde"] ## Allow [Unicode Standard Annex #31](https://unicode.org/reports/tr31/) for identifiers. unicode-xid-ident = ["unicode-xid"] +## Enable default parameter values in function definitions. +default-parameters = [] ## Enable functions metadata (including doc-comments); implies [`serde`](#feature-serde). metadata = ["serde", "serde_json", "rhai_codegen/metadata", "smartstring/serde"] ## Expose internal data structures (e.g. `AST` nodes). diff --git a/src/api/eval.rs b/src/api/eval.rs index 71d077c0d..63b554711 100644 --- a/src/api/eval.rs +++ b/src/api/eval.rs @@ -311,6 +311,7 @@ impl Engine { Token::lookup_symbol_from_syntax(op).as_ref(), Some(&lhs), &[rhs], + &[], FnCallHashes::from_native_only(calc_fn_hash(None, op, 2)), false, Position::NONE, diff --git a/src/ast/expr.rs b/src/ast/expr.rs index 938c523b4..7d26df353 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -193,6 +193,9 @@ pub struct FnCallExpr { pub hashes: FnCallHashes, /// List of function call argument expressions. pub args: FnArgsVec, + /// Named arguments: (parameter_name, expression). + #[cfg(feature = "default-parameters")] + pub named_args: FnArgsVec<(ImmutableString, Expr)>, /// Does this function call capture the parent scope? pub capture_parent_scope: bool, /// Is this function call a native operator? @@ -603,6 +606,8 @@ impl Expr { name: KEYWORD_FN_PTR.into(), hashes: FnCallHashes::from_hash(calc_fn_hash(None, f.fn_name(), 1)), args: once(Self::StringConstant(f.fn_name().into(), pos)).collect(), + #[cfg(feature = "default-parameters")] + named_args: FnArgsVec::new(), capture_parent_scope: false, op_token: None, } diff --git a/src/ast/script_fn.rs b/src/ast/script_fn.rs index f63bab85c..9c1ab1cd3 100644 --- a/src/ast/script_fn.rs +++ b/src/ast/script_fn.rs @@ -3,6 +3,8 @@ use super::{FnAccess, StmtBlock}; use crate::{FnArgsVec, ImmutableString}; +#[cfg(feature = "default-parameters")] +use super::Expr; #[cfg(feature = "no_std")] use std::prelude::v1::*; use std::{fmt, hash::Hash}; @@ -23,6 +25,10 @@ pub struct ScriptFuncDef { pub this_type: Option, /// Names of function parameters. pub params: FnArgsVec, + /// Default expressions for function parameters (parallel array to `params`). + /// `None` means no default value, `Some(Expr)` is the default expression to evaluate. + #[cfg(feature = "default-parameters")] + pub defaults: FnArgsVec>>, /// _(metadata)_ Function doc-comments (if any). Exported under the `metadata` feature only. /// /// Doc-comments are comment lines beginning with `///` or comment blocks beginning with `/**`, @@ -53,6 +59,8 @@ impl ScriptFuncDef { #[cfg(not(feature = "no_object"))] this_type: self.this_type.clone(), params: self.params.clone(), + #[cfg(feature = "default-parameters")] + defaults: self.defaults.clone(), #[cfg(feature = "metadata")] comments: <_>::default(), } @@ -70,6 +78,27 @@ impl fmt::Display for ScriptFuncDef { #[cfg(feature = "no_object")] let this_type = ""; + #[cfg(feature = "default-parameters")] + let params_str = self + .params + .iter() + .zip(self.defaults.iter()) + .map(|(name, default)| { + if default.is_some() { + format!("{} = ", name) + } else { + name.to_string() + } + }) + .collect::>() + .join(", "); + #[cfg(not(feature = "default-parameters"))] + let params_str = self + .params + .iter() + .map(ImmutableString::as_str) + .collect::>() + .join(", "); write!( f, "{}{}{}({})", @@ -79,11 +108,7 @@ impl fmt::Display for ScriptFuncDef { }, this_type, self.name, - self.params - .iter() - .map(ImmutableString::as_str) - .collect::>() - .join(", ") + params_str ) } } diff --git a/src/eval/chaining.rs b/src/eval/chaining.rs index 7c19eaf73..fcf8cf082 100644 --- a/src/eval/chaining.rs +++ b/src/eval/chaining.rs @@ -870,6 +870,9 @@ impl Engine { } // xxx.fn_name(arg_expr_list) (Expr::MethodCall(x, pos), None, ..) => { + #[cfg(not(feature = "default-parameters"))] +use crate::ImmutableString; + #[cfg(not(feature = "no_module"))] debug_assert!( !x.is_qualified(), @@ -882,9 +885,31 @@ impl Engine { defer! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) } let crate::ast::FnCallExpr { - name, hashes, args, .. + name, + hashes, + args, + #[cfg(feature = "default-parameters")] + named_args: named_args_expr, + .. } = &**x; + // Evaluate named arguments + #[cfg(feature = "default-parameters")] + let mut named_arg_values = FnArgsVec::new(); + #[cfg(feature = "default-parameters")] + for (param_name, expr) in &*named_args_expr { + let (value, ..) = self.get_arg_value( + global, + caches, + x!(s, b), + this_ptr.as_deref_mut(), + expr, + )?; + named_arg_values.push((param_name.clone(), value.flatten())); + } + #[cfg(not(feature = "default-parameters"))] + let named_arg_values: FnArgsVec<(ImmutableString, Dynamic)> = FnArgsVec::new(); + // Truncate the index values upon exit defer! { idx_values => truncate; let offset = idx_values.len() - args.len(); } @@ -892,7 +917,15 @@ impl Engine { let arg1_pos = args.first().map_or(Position::NONE, Expr::position); self.make_method_call( - global, caches, name, *hashes, target, call_args, arg1_pos, *pos, + global, + caches, + name, + *hashes, + target, + call_args, + arg1_pos, + *pos, + &named_arg_values, ) } // {xxx:map}.id op= ??? @@ -1080,6 +1113,9 @@ impl Engine { } // {xxx:map}.fn_name(arg_expr_list)[expr] | {xxx:map}.fn_name(arg_expr_list).expr Expr::MethodCall(ref x, pos) => { + #[cfg(not(feature = "default-parameters"))] +use crate::ImmutableString; + #[cfg(not(feature = "no_module"))] debug_assert!( !x.is_qualified(), @@ -1101,8 +1137,33 @@ impl Engine { let call_args = &mut idx_values[offset..]; let arg1_pos = args.first().map_or(Position::NONE, Expr::position); + // Evaluate named arguments if any + #[cfg(feature = "default-parameters")] + let mut named_arg_values = FnArgsVec::new(); + #[cfg(feature = "default-parameters")] + for (param_name, expr) in &*x.named_args { + let (value, ..) = self.get_arg_value( + global, + caches, + x!(s, b), + this_ptr.as_deref_mut(), + expr, + )?; + named_arg_values.push((param_name.clone(), value.flatten())); + } + #[cfg(not(feature = "default-parameters"))] + let named_arg_values: FnArgsVec<(ImmutableString, Dynamic)> = FnArgsVec::new(); + self.make_method_call( - global, caches, name, *hashes, target, call_args, arg1_pos, pos, + global, + caches, + name, + *hashes, + target, + call_args, + arg1_pos, + pos, + &named_arg_values, )? .0 .into() @@ -1208,6 +1269,9 @@ impl Engine { ); let val = { + #[cfg(not(feature = "default-parameters"))] +use crate::ImmutableString; + #[cfg(feature = "debugging")] let reset = self.dbg_reset(global, caches, x!(s, b), _tp, _node)?; @@ -1224,8 +1288,34 @@ impl Engine { let call_args = &mut idx_values[offset..]; let pos1 = args.first().map_or(Position::NONE, Expr::position); + // Evaluate named arguments if any + #[cfg(feature = "default-parameters")] + let mut named_arg_values = FnArgsVec::new(); + #[cfg(feature = "default-parameters")] + for (param_name, expr) in &*f.named_args { + let (value, ..) = self.get_arg_value( + global, + caches, + x!(s, b), + _this_ptr.as_deref_mut(), + expr, + )?; + named_arg_values + .push((param_name.clone(), value.flatten())); + } + #[cfg(not(feature = "default-parameters"))] + let named_arg_values: FnArgsVec<(ImmutableString, Dynamic)> = FnArgsVec::new(); + self.make_method_call( - global, caches, name, *hashes, target, call_args, pos1, pos, + global, + caches, + name, + *hashes, + target, + call_args, + pos1, + pos, + &named_arg_values, )? .0 }; diff --git a/src/eval/expr.rs b/src/eval/expr.rs index 1ab660495..e3d620e02 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -135,20 +135,26 @@ impl Engine { match scope.search(var_name) { Some(index) => index, None => { - return self - .global_modules - .iter() - .find_map(|m| m.get_var(var_name)) - .map_or_else( - || { - Err(ERR::ErrorVariableNotFound( - var_name.to_string(), - expr.position(), - ) - .into()) - }, - |val| Ok(val.into()), - ) + // Try global modules first + if let Some(val) = self.global_modules.iter().find_map(|m| m.get_var(var_name)) { + return Ok(val.into()); + } + + // Try global constants (for default parameter expressions) + #[cfg(not(feature = "no_module"))] + if let Some(ref constants) = global.constants { + if let Some(guard) = crate::func::locked_read(constants) { + if let Some(value) = guard.get(var_name) { + return Ok(value.clone().into()); + } + } + } + + return Err(ERR::ErrorVariableNotFound( + var_name.to_string(), + expr.position(), + ) + .into()); } } }; diff --git a/src/func/call.rs b/src/func/call.rs index 365dc1ecd..7301947c8 100644 --- a/src/func/call.rs +++ b/src/func/call.rs @@ -3,6 +3,8 @@ use super::{get_builtin_binary_op_fn, get_builtin_op_assignment_fn, RhaiFunc}; use crate::api::default_limits::MAX_DYNAMIC_PARAMETERS; use crate::ast::{Expr, FnCallExpr, FnCallHashes}; +#[cfg(feature = "default-parameters")] +use crate::ast::ScriptFuncDef; use crate::engine::{ KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_FN_PTR_CURRY, KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_TYPE_OF, @@ -33,6 +35,131 @@ use num_traits::Float; /// Arguments to a function call, which is a list of [`&mut Dynamic`][Dynamic]. pub type FnCallArgs<'a> = [&'a mut Dynamic]; +/// Describes how to provide a value for each parameter. +#[cfg(all(not(feature = "no_function"), feature = "default-parameters"))] +#[derive(Debug)] +pub(crate) enum ArgSource { + /// Use the provided value (already evaluated). + Provided(Dynamic), + /// Evaluate the default expression in the callee context. + DefaultExpr, +} + +/// Plan for how to construct the complete argument list. +#[cfg(all(not(feature = "no_function"), feature = "default-parameters"))] +#[derive(Debug)] +pub(crate) struct ArgPlan { + pub(crate) sources: FnArgsVec, +} + +/// Build an argument plan from positional args, defaults, and named args. +/// Returns a plan describing how to construct the complete argument list. +#[cfg(all(not(feature = "no_function"), feature = "default-parameters"))] +fn build_arg_plan( + fn_def: &ScriptFuncDef, + positional_args: &[Dynamic], + named_args: &[(ImmutableString, Dynamic)], + pos: Position, +) -> RhaiResultOf { + let num_params = fn_def.params.len(); + let num_positional = positional_args.len(); + + // Validate: can't have more positional args than parameters + if num_positional > num_params { + return Err(ERR::ErrorFunctionNotFound( + format!( + "{} (expected at most {} arguments, got {})", + fn_def.name, num_params, num_positional + ), + pos, + ) + .into()); + } + + // Build a map of named arguments for quick lookup + let mut named_map = std::collections::HashMap::new(); + for (param_name, value) in named_args { + // Check for duplicate named arguments + if named_map + .insert(param_name.clone(), value.clone()) + .is_some() + { + return Err(ERR::ErrorFunctionNotFound( + format!( + "{} (duplicate named argument '{}')", + fn_def.name, param_name + ), + pos, + ) + .into()); + } + } + + // Validate: check for arguments provided both positionally and by name + for (param_name, _) in named_args { + if let Some(pos_idx) = fn_def.params.iter().position(|p| p == param_name) { + if pos_idx < num_positional { + return Err(ERR::ErrorFunctionNotFound( + format!( + "{} (argument '{}' provided both positionally and by name)", + fn_def.name, param_name + ), + pos, + ) + .into()); + } + } + } + + // Build argument plan + let mut sources = FnArgsVec::with_capacity(num_params); + + // First, add positional arguments + for arg in positional_args.iter() { + sources.push(ArgSource::Provided(arg.clone())); + } + + // Then, fill remaining positions with named args or defaults + for i in num_positional..num_params { + let param_name = &fn_def.params[i]; + + // Check if this parameter has a named argument + if let Some(value) = named_map.remove(param_name) { + sources.push(ArgSource::Provided(value)); + } else if fn_def.defaults[i].is_some() { + // Mark that we need to evaluate the default expression + sources.push(ArgSource::DefaultExpr); + } else { + // Missing required argument + return Err(ERR::ErrorFunctionNotFound( + format!( + "{} (missing required argument '{}', position {})", + fn_def.name, + param_name, + i + 1 + ), + pos, + ) + .into()); + } + } + + // Check for named arguments that don't match any parameter + if !named_map.is_empty() { + let unknown_param = named_map.keys().next().unwrap(); + return Err(ERR::ErrorFunctionNotFound( + format!( + "{} (unknown named argument '{}')", + fn_def.name, unknown_param + ), + pos, + ) + .into()); + } + + Ok(ArgPlan { sources }) +} + /// A type that temporarily stores a mutable reference to a `Dynamic`, /// replacing it with a cloned copy. #[derive(Debug)] @@ -261,6 +388,17 @@ impl Engine { // Stop when all permutations are exhausted if bitmask >= max_bitmask { + // With proper registration, functions with defaults are registered for all valid + // argument counts (min_args to num_params). If we reach here, either: + // 1. No function exists for this arg count (initial lookup failed) + // 2. All Dynamic permutations failed (if allow_dynamic was true) + // + // For named arguments that skip parameters (e.g., `func(a, c=2)` needing a 3-param + // function), the caller (exec_fn_call_with_named) handles searching for functions + // with more parameters and validating they have the required named parameters. + // + // No need to guess upward here - either we found it or it doesn't exist. + if num_args != 2 { return None; } @@ -559,13 +697,77 @@ impl Engine { &self, global: &mut GlobalRuntimeState, caches: &mut Caches, - _scope: Option<&mut Scope>, + scope: Option<&mut Scope>, fn_name: &str, op_token: Option<&Token>, hashes: FnCallHashes, args: &mut FnCallArgs, is_ref_mut: bool, - _is_method_call: bool, + is_method_call: bool, + pos: Position, + ) -> RhaiResultOf<(Dynamic, bool)> { + #[cfg(all(not(feature = "no_function"), feature = "default-parameters"))] + { + self.exec_fn_call_with_named( + global, + caches, + scope, + fn_name, + op_token, + hashes, + args, + is_ref_mut, + is_method_call, + &[], + pos, + ) + } + #[cfg(all(not(feature = "no_function"), not(feature = "default-parameters")))] + { + // When default-parameters is disabled, use the original behavior + self.exec_fn_call_original( + global, + caches, + scope, + fn_name, + op_token, + hashes, + args, + is_ref_mut, + is_method_call, + pos, + ) + } + #[cfg(feature = "no_function")] + { + // When no_function is enabled, only native functions are available + self.exec_native_fn_call( + global, + caches, + fn_name, + op_token, + hashes.native(), + args, + is_ref_mut, + false, + pos, + ) + } + } + + /// Original exec_fn_call implementation without default parameters support. + #[cfg(all(not(feature = "no_function"), not(feature = "default-parameters")))] + fn exec_fn_call_original( + &self, + global: &mut GlobalRuntimeState, + caches: &mut Caches, + scope: Option<&mut Scope>, + fn_name: &str, + op_token: Option<&Token>, + hashes: FnCallHashes, + args: &mut FnCallArgs, + is_ref_mut: bool, + is_method_call: bool, pos: Position, ) -> RhaiResultOf<(Dynamic, bool)> { // These may be redirected from method style calls. @@ -604,22 +806,36 @@ impl Engine { defer! { let orig_level = global.level; global.level += 1 } // Script-defined function call? - #[cfg(not(feature = "no_function"))] if !hashes.is_native_only() { let hash = hashes.script(); let local_entry = &mut None; let mut resolved = None; #[cfg(not(feature = "no_object"))] - if _is_method_call && !args.is_empty() { + if is_method_call && !args.is_empty() { let typed_hash = crate::calc_typed_method_hash(hash, self.map_type_name(args[0].type_name())); - resolved = - self.resolve_fn(global, caches, local_entry, None, typed_hash, None, false); + resolved = self.resolve_fn( + global, + caches, + local_entry, + None, + typed_hash, + None, + false, + ); } if resolved.is_none() { - resolved = self.resolve_fn(global, caches, local_entry, None, hash, None, false); + resolved = self.resolve_fn( + global, + caches, + local_entry, + None, + hash, + None, + false, + ); } if let Some(FnResolutionCacheEntry { func, source }) = resolved.cloned() { @@ -635,7 +851,7 @@ impl Engine { } let mut empty_scope; - let scope = if let Some(scope) = _scope { + let scope = if let Some(scope) = scope { scope } else { empty_scope = Scope::new(); @@ -645,12 +861,13 @@ impl Engine { let orig_source = mem::replace(&mut global.source, source); defer! { global => move |g| g.source = orig_source } - return if _is_method_call { - // Method call of script function - map first argument to `this` - let (first_arg, args) = args.split_first_mut().unwrap(); + return if is_method_call { + // Method call: first arg is the object (not in fn_def.params) + let (first_arg, rest_args) = args.split_first_mut().unwrap(); let this_ptr = Some(&mut **first_arg); + self.call_script_fn( - global, caches, scope, this_ptr, env, fn_def, args, true, pos, + global, caches, scope, this_ptr, env, fn_def, rest_args, true, pos, ) } else { // Normal call of script function @@ -679,6 +896,225 @@ impl Engine { ) } + /// Internal function that handles named arguments and defaults. + #[cfg(all(not(feature = "no_function"), feature = "default-parameters"))] + fn exec_fn_call_with_named( + &self, + global: &mut GlobalRuntimeState, + caches: &mut Caches, + scope: Option<&mut Scope>, + fn_name: &str, + op_token: Option<&Token>, + hashes: FnCallHashes, + args: &mut FnCallArgs, + is_ref_mut: bool, + is_method_call: bool, + named_args: &[(ImmutableString, Dynamic)], + pos: Position, + ) -> RhaiResultOf<(Dynamic, bool)> { + // These may be redirected from method style calls. + if hashes.is_native_only() + && match fn_name { + // Handle type_of() + KEYWORD_TYPE_OF if args.len() == 1 => { + let typ = self.get_interned_string(self.map_type_name(args[0].type_name())); + return Ok((typ.into(), false)); + } + + #[cfg(not(feature = "no_closure"))] + crate::engine::KEYWORD_IS_SHARED if args.len() == 1 => { + return Ok((args[0].is_shared().into(), false)) + } + #[cfg(not(feature = "no_closure"))] + crate::engine::KEYWORD_IS_SHARED => true, + + #[cfg(not(feature = "no_function"))] + crate::engine::KEYWORD_IS_DEF_FN => true, + + KEYWORD_TYPE_OF | KEYWORD_FN_PTR | KEYWORD_EVAL | KEYWORD_IS_DEF_VAR + | KEYWORD_FN_PTR_CALL | KEYWORD_FN_PTR_CURRY => true, + + _ => false, + } + { + let sig = self.gen_fn_call_signature(fn_name, args); + return Err(ERR::ErrorFunctionNotFound(sig, pos).into()); + } + + // Check for data race. + #[cfg(not(feature = "no_closure"))] + ensure_no_data_race(fn_name, args, is_ref_mut)?; + + defer! { let orig_level = global.level; global.level += 1 } + + // Script-defined function call? + #[cfg(not(feature = "no_function"))] + if !hashes.is_native_only() { + let hash = hashes.script(); + let local_entry = &mut None; + let mut resolved = None; + + #[cfg(not(feature = "no_object"))] + if is_method_call && !args.is_empty() { + let typed_hash = + crate::calc_typed_method_hash(hash, self.map_type_name(args[0].type_name())); + resolved = self.resolve_fn( + global, + caches, + local_entry, + None, + typed_hash, + None, + false, + ); + } + + if resolved.is_none() { + resolved = self.resolve_fn( + global, + caches, + local_entry, + None, + hash, + None, + false, + ); + } + + if let Some(FnResolutionCacheEntry { func, source }) = resolved.cloned() { + let RhaiFunc::Script { fn_def, env } = func else { + unreachable!("Script function expected"); + }; + + let fn_def = &*fn_def; + let env = env.as_deref(); + + // If named arguments are present, check if this function can handle them + // If not, try to find a function with more parameters that can + #[cfg(feature = "default-parameters")] + if !named_args.is_empty() { + // Check if all named arguments exist in this function's parameters + let has_all_named_params = named_args.iter().all(|(name, _)| { + fn_def.params.iter().any(|p| p == name) + }); + + if !has_all_named_params { + // Try to find a function with more parameters that might have these named args + let mut found_better = None; + for try_arg_count in (args.len() + 1)..=(args.len() + 10) { + let try_hash = crate::calc_fn_hash(None, fn_name, try_arg_count); + let try_resolved = self.resolve_fn( + global, + caches, + local_entry, + None, + try_hash, + None, + false, + ); + + if let Some(FnResolutionCacheEntry { func: try_func, source: try_source }) = try_resolved { + if let RhaiFunc::Script { fn_def: try_fn_def, .. } = try_func { + // Check if this function has all the named parameters + let has_all = named_args.iter().all(|(name, _)| { + try_fn_def.params.iter().any(|p| p == name) + }); + if has_all { + found_better = Some((try_func.clone(), try_source.clone())); + break; + } + } + } + } + + // If we found a better match, use it + if let Some((better_func, better_source)) = found_better { + let RhaiFunc::Script { fn_def: better_fn_def, env: better_env } = better_func else { + unreachable!("Script function expected"); + }; + let fn_def = &*better_fn_def; + let env = better_env.as_deref(); + let orig_source = mem::replace(&mut global.source, better_source); + defer! { global => move |g| g.source = orig_source } + + let mut empty_scope; + let scope = if let Some(scope) = scope { + scope + } else { + empty_scope = Scope::new(); + &mut empty_scope + }; + + return if is_method_call { + let (first_arg, rest_positional) = args.split_first_mut().unwrap(); + let this_ptr = Some(&mut **first_arg); + let positional_args: FnArgsVec = rest_positional.iter().map(|a| (*a).clone()).collect(); + let arg_plan = build_arg_plan(fn_def, &positional_args, named_args, pos)?; + + self.call_script_fn_with_plan( + global, caches, scope, this_ptr, env, fn_def, arg_plan, true, pos, + ) + } else { + let positional_args: FnArgsVec = args.iter().map(|a| (*a).clone()).collect(); + let arg_plan = build_arg_plan(fn_def, &positional_args, named_args, pos)?; + + self.call_script_fn_with_plan(global, caches, scope, None, env, fn_def, arg_plan, true, pos) + } + .map(|r| (r, false)); + } + // Otherwise fall through and let build_arg_plan report the error + } + } + + if fn_def.body.is_empty() { + return Ok((Dynamic::UNIT, false)); + } + + let mut empty_scope; + let scope = if let Some(scope) = scope { + scope + } else { + empty_scope = Scope::new(); + &mut empty_scope + }; + + let orig_source = mem::replace(&mut global.source, source); + defer! { global => move |g| g.source = orig_source } + + // Build argument plan from positional args, defaults, and named args + return if is_method_call { + // Method call: first arg is the object (not in fn_def.params) + // So we need to handle it separately + let (first_arg, rest_positional) = args.split_first_mut().unwrap(); + let this_ptr = Some(&mut **first_arg); + + // For method calls, fn_def.params doesn't include 'this' + // So we build args for the remaining parameters + let positional_args: FnArgsVec = rest_positional.iter().map(|a| (*a).clone()).collect(); + let arg_plan = build_arg_plan(fn_def, &positional_args, named_args, pos)?; + + self.call_script_fn_with_plan( + global, caches, scope, this_ptr, env, fn_def, arg_plan, true, pos, + ) + } else { + // Normal call of script function + let positional_args: FnArgsVec = args.iter().map(|a| (*a).clone()).collect(); + let arg_plan = build_arg_plan(fn_def, &positional_args, named_args, pos)?; + + self.call_script_fn_with_plan(global, caches, scope, None, env, fn_def, arg_plan, true, pos) + } + .map(|r| (r, false)); + } + } + + // Native function call + let hash = hashes.native(); + + self.exec_native_fn_call( + global, caches, fn_name, op_token, hash, args, is_ref_mut, false, pos, + ) + } + /// Evaluate an argument. #[inline] pub(crate) fn get_arg_value( @@ -725,6 +1161,7 @@ impl Engine { call_args: &mut [Dynamic], first_arg_pos: Position, pos: Position, + named_args: &[(ImmutableString, Dynamic)], ) -> RhaiResultOf<(Dynamic, bool)> { let (result, updated) = match fn_name { // Handle fn_ptr.call(...) @@ -773,18 +1210,30 @@ impl Engine { #[cfg(not(feature = "no_function"))] let _is_anon = fn_ptr.is_anonymous(); - // Recalculate hashes + // Recalculate hashes - for anonymous functions, we need to try multiple argument counts + // if the function has defaults let new_hash = if !_is_anon && !is_valid_function_name(fn_name) { FnCallHashes::from_native_only(calc_fn_hash(None, fn_name, args.len())) } else { + // For anonymous functions, try the exact arg count first, then allow resolution + // to try other counts if the function has defaults FnCallHashes::from_hash(calc_fn_hash(None, fn_name, args.len())) }; // Map it to name(args) in function-call style - self.exec_fn_call( - global, caches, None, fn_name, None, new_hash, &mut args, false, false, - pos, - ) + #[cfg(feature = "default-parameters")] + { + self.exec_fn_call_with_named( + global, caches, None, fn_name, None, new_hash, &mut args, false, false, + named_args, pos, + ) + } + #[cfg(not(feature = "default-parameters"))] + { + self.exec_fn_call( + global, caches, None, fn_name, None, new_hash, &mut args, false, false, pos, + ) + } } } } @@ -887,10 +1336,19 @@ impl Engine { }; // Map it to name(args) in function-call style - self.exec_fn_call( - global, caches, None, &name, None, new_hash, args, is_ref_mut, true, - pos, - ) + #[cfg(feature = "default-parameters")] + { + self.exec_fn_call_with_named( + global, caches, None, &name, None, new_hash, args, is_ref_mut, true, + named_args, pos, + ) + } + #[cfg(not(feature = "default-parameters"))] + { + self.exec_fn_call( + global, caches, None, &name, None, new_hash, args, is_ref_mut, true, pos, + ) + } } } } @@ -1005,14 +1463,34 @@ impl Engine { let scope = &mut Scope::new(); let env = env.as_deref(); let this_ptr = Some(target.as_mut()); - let args = &mut call_args.iter_mut().collect::>(); - defer! { let orig_level = global.level; global.level += 1 } + #[cfg(feature = "default-parameters")] + { + // Build argument plan from positional args, defaults, and named args + let positional_args: FnArgsVec = + call_args.iter().map(|a| (*a).clone()).collect(); + let arg_plan = + build_arg_plan(&fn_def, &positional_args, named_args, pos)?; - self.call_script_fn( - global, caches, scope, this_ptr, env, &fn_def, args, true, pos, - ) - .map(|v| (v, false)) + defer! { let orig_level = global.level; global.level += 1 } + + self.call_script_fn_with_plan( + global, caches, scope, this_ptr, env, &fn_def, arg_plan, true, pos, + ) + .map(|v| (v, false)) + } + #[cfg(not(feature = "default-parameters"))] + { + // When feature is disabled, just use positional args directly + let args = &mut call_args.iter_mut().collect::>(); + + defer! { let orig_level = global.level; global.level += 1 } + + self.call_script_fn( + global, caches, scope, this_ptr, env, &fn_def, args, true, pos, + ) + .map(|v| (v, false)) + } } // Native function - short-circuit Some((None, Some(func), ..)) => { @@ -1039,9 +1517,19 @@ impl Engine { .chain(call_args.iter_mut()) .collect::>(); - self.exec_fn_call( - global, caches, None, fn_name, None, hash, args, is_ref_mut, true, pos, - ) + #[cfg(feature = "default-parameters")] + { + self.exec_fn_call_with_named( + global, caches, None, fn_name, None, hash, args, is_ref_mut, true, + named_args, pos, + ) + } + #[cfg(not(feature = "default-parameters"))] + { + self.exec_fn_call( + global, caches, None, fn_name, None, hash, args, is_ref_mut, true, pos, + ) + } } _ => unreachable!(), } @@ -1067,6 +1555,10 @@ impl Engine { op_token: Option<&Token>, first_arg: Option<&Expr>, args_expr: &[Expr], + #[cfg(feature = "default-parameters")] + named_args: &[(ImmutableString, Expr)], + #[cfg(not(feature = "default-parameters"))] + _named_args: &[(ImmutableString, Expr)], hashes: FnCallHashes, capture_scope: bool, pos: Position, @@ -1111,10 +1603,49 @@ impl Engine { match typ { // Linked to scripted function - short-circuit - #[cfg(not(feature = "no_function"))] - FnPtrType::Script(fn_def) - if fn_def.params.len() == curry.len() + args_expr.len() => - { + // For functions with defaults, allow fewer arguments than the full parameter count + #[cfg(all(not(feature = "no_function"), feature = "default-parameters"))] + FnPtrType::Script(fn_def) => { + let num_provided = curry.len() + args_expr.len(); + let num_params = fn_def.params.len(); + let min_args = fn_def + .defaults + .iter() + .position(|d| d.is_some()) + .unwrap_or(num_params); + + // Check if we have a valid number of arguments (between min_args and num_params) + if num_provided >= min_args && num_provided <= num_params { + // Evaluate arguments + let mut arg_values = + FnArgsVec::with_capacity(curry.len() + args_expr.len()); + arg_values.extend(curry); + for expr in args_expr { + let this_ptr = this_ptr.as_deref_mut(); + let (value, _) = + self.get_arg_value(global, caches, scope, this_ptr, expr)?; + arg_values.push(value); + } + + // Build argument plan with defaults + let positional_args: FnArgsVec = + arg_values.iter().map(|a| (*a).clone()).collect(); + let arg_plan = + build_arg_plan(&fn_def, &positional_args, &[], pos)?; + + let scope = &mut Scope::new(); + let env = env.as_deref(); + + defer! { let orig_level = global.level; global.level += 1 } + + return self.call_script_fn_with_plan( + global, caches, scope, None, env, &fn_def, arg_plan, true, pos, + ); + } + } + #[cfg(all(not(feature = "no_function"), not(feature = "default-parameters")))] + FnPtrType::Script(fn_def) if fn_def.params.len() == curry.len() + args_expr.len() => { + // When feature is disabled, only allow exact argument count match // Evaluate arguments let mut arg_values = FnArgsVec::with_capacity(curry.len() + args_expr.len()); @@ -1126,6 +1657,7 @@ impl Engine { arg_values.push(value); } let args = &mut arg_values.iter_mut().collect::>(); + let scope = &mut Scope::new(); let env = env.as_deref(); @@ -1357,6 +1889,16 @@ impl Engine { let mut args = FnArgsVec::with_capacity(num_args + curry.len()); let mut is_ref_mut = false; + // Evaluate named arguments + #[cfg(feature = "default-parameters")] + let mut named_arg_values = FnArgsVec::with_capacity(named_args.len()); + #[cfg(feature = "default-parameters")] + for (param_name, expr) in named_args { + let (value, ..) = + self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?; + named_arg_values.push((param_name.clone(), value.flatten())); + } + // Capture parent scope? // // If so, do it separately because we cannot convert the first argument (if it is a simple @@ -1373,12 +1915,71 @@ impl Engine { // Use parent scope let scope = Some(scope); - return self - .exec_fn_call( - global, caches, scope, fn_name, op_token, hashes, &mut args, is_ref_mut, false, - pos, - ) - .map(|(v, ..)| v); + // Convert named_arg_values to the format expected by exec_fn_call_with_named + #[cfg(feature = "default-parameters")] + let named_args_dyn: FnArgsVec<_> = named_arg_values + .iter() + .map(|(name, value)| (name.clone(), value.clone())) + .collect(); + #[cfg(feature = "default-parameters")] + let named_args_slice: &[(ImmutableString, Dynamic)] = &named_args_dyn; + #[cfg(not(feature = "default-parameters"))] + let named_args_slice: &[(ImmutableString, Dynamic)] = &[]; + + #[cfg(all(not(feature = "no_function"), feature = "default-parameters"))] + { + return self + .exec_fn_call_with_named( + global, + caches, + scope, + fn_name, + op_token, + hashes, + &mut args, + is_ref_mut, + false, + named_args_slice, + pos, + ) + .map(|(v, ..)| v); + } + #[cfg(all(not(feature = "no_function"), not(feature = "default-parameters")))] + { + return self + .exec_fn_call( + global, + caches, + scope, + fn_name, + op_token, + hashes, + &mut args, + is_ref_mut, + false, + pos, + ) + .map(|(v, ..)| v); + } + #[cfg(feature = "no_function")] + { + // When no_function is enabled, named arguments are not supported + // Just call exec_fn_call which will handle native functions + return self + .exec_fn_call( + global, + caches, + scope, + fn_name, + op_token, + hashes, + &mut args, + is_ref_mut, + false, + pos, + ) + .map(|(v, ..)| v); + } } // Call with blank scope @@ -1449,10 +2050,68 @@ impl Engine { args.extend(arg_values.iter_mut()); - self.exec_fn_call( - global, caches, None, fn_name, op_token, hashes, &mut args, is_ref_mut, false, pos, - ) - .map(|(v, ..)| v) + // Convert named_arg_values to the format expected by exec_fn_call_with_named + #[cfg(feature = "default-parameters")] + let named_args_dyn: FnArgsVec<_> = named_arg_values + .iter() + .map(|(name, value)| (name.clone(), value.clone())) + .collect(); + #[cfg(feature = "default-parameters")] + let named_args_slice: &[(ImmutableString, Dynamic)] = &named_args_dyn; + #[cfg(not(feature = "default-parameters"))] + let named_args_slice: &[(ImmutableString, Dynamic)] = &[]; + + #[cfg(all(not(feature = "no_function"), feature = "default-parameters"))] + { + self.exec_fn_call_with_named( + global, + caches, + None, + fn_name, + op_token, + hashes, + &mut args, + is_ref_mut, + false, + named_args_slice, + pos, + ) + .map(|(v, ..)| v) + } + #[cfg(all(not(feature = "no_function"), not(feature = "default-parameters")))] + { + self.exec_fn_call( + global, + caches, + None, + fn_name, + op_token, + hashes, + &mut args, + is_ref_mut, + false, + pos, + ) + .map(|(v, ..)| v) + } + #[cfg(feature = "no_function")] + { + // When no_function is enabled, named arguments are not supported + // Just call exec_fn_call which will handle native functions + self.exec_fn_call( + global, + caches, + None, + fn_name, + op_token, + hashes, + &mut args, + is_ref_mut, + false, + pos, + ) + .map(|(v, ..)| v) + } } /// Call a namespace-qualified function in normal function-call style. @@ -1466,9 +2125,23 @@ impl Engine { namespace: &crate::ast::Namespace, fn_name: &str, args_expr: &[Expr], + #[cfg(feature = "default-parameters")] + named_args: &[(ImmutableString, Expr)], + #[cfg(not(feature = "default-parameters"))] + _named_args: &[(ImmutableString, Expr)], hash: u64, pos: Position, ) -> RhaiResult { + // Evaluate named arguments first + #[cfg(feature = "default-parameters")] + let mut named_arg_values = FnArgsVec::with_capacity(named_args.len()); + #[cfg(feature = "default-parameters")] + for (param_name, expr) in named_args { + let (value, ..) = + self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?; + named_arg_values.push((param_name.clone(), value.flatten())); + } + let mut arg_values = FnArgsVec::with_capacity(args_expr.len()); let args = &mut FnArgsVec::with_capacity(args_expr.len()); let mut first_arg_value = None; @@ -1612,15 +2285,48 @@ impl Engine { let orig_source = mem::replace(&mut global.source, module.id_raw().cloned()); defer! { global => move |g| g.source = orig_source } + #[cfg(feature = "default-parameters")] + { + // Check if we have named args or if the function has defaults + let has_named_args = !named_arg_values.is_empty(); + let has_defaults = fn_def.defaults.iter().any(|d| d.is_some()); + + if has_named_args || has_defaults { + // Use arg plan for script functions with named args or defaults + let positional_args: FnArgsVec = args.iter().map(|a| (*a).clone()).collect(); + let named_args_dyn: &[(ImmutableString, Dynamic)] = &named_arg_values; + let arg_plan = build_arg_plan(fn_def, &positional_args, named_args_dyn, pos)?; + + return self.call_script_fn_with_plan( + global, caches, scope, None, env, fn_def, arg_plan, true, pos, + ); + } + } + + // Fall back to regular call if no named args or defaults self.call_script_fn(global, caches, scope, None, env, fn_def, args, true, pos) } - Some(f) if !f.is_pure() && args[0].is_read_only() => { + Some(f) if !f.is_pure() && !args.is_empty() && args[0].is_read_only() => { // If function is not pure, there must be at least one argument Err(ERR::ErrorNonPureMethodCallOnConstant(fn_name.to_string(), pos).into()) } Some(RhaiFunc::Plugin { func }) => { + // Native plugin functions don't support named arguments + #[cfg(feature = "default-parameters")] + if !named_arg_values.is_empty() { + return Err(ERR::ErrorFunctionNotFound( + format!( + "{namespace}{}{} (named arguments not supported for native functions)", + crate::engine::NAMESPACE_SEPARATOR, + fn_name + ), + pos, + ) + .into()); + } + let context = func .has_context() .then(|| (self, fn_name, module.id(), &*global, pos).into()); @@ -1636,6 +2342,20 @@ impl Engine { func, has_context, .. }, ) => { + // Native functions don't support named arguments + #[cfg(feature = "default-parameters")] + if !named_arg_values.is_empty() { + return Err(ERR::ErrorFunctionNotFound( + format!( + "{namespace}{}{} (named arguments not supported for native functions)", + crate::engine::NAMESPACE_SEPARATOR, + fn_name + ), + pos, + ) + .into()); + } + let context = has_context.then(|| (self, fn_name, module.id(), &*global, pos).into()); func(context, args).and_then(|r| self.check_data_size(r, pos)) @@ -1657,7 +2377,7 @@ impl Engine { }, pos, ) - .into()), + .into()) } } @@ -1723,10 +2443,14 @@ impl Engine { name, hashes, args, + #[cfg(feature = "default-parameters")] + named_args, op_token, capture_parent_scope: capture, .. } = expr; + #[cfg(not(feature = "default-parameters"))] + let named_args = &[]; let op_token = op_token.as_ref(); @@ -1791,19 +2515,140 @@ impl Engine { let hash = hashes.native(); return self.make_qualified_function_call( - global, caches, scope, this_ptr, namespace, name, args, hash, pos, + global, caches, scope, this_ptr, namespace, name, args, named_args, hash, pos, ); } // Normal function call + // First check if the function name is a variable containing a FnPtr + // This handles cases like: let f = |a, b = 2| { a + b }; f(1) + if op_token.is_none() { + // Clone the FnPtr value to avoid borrow checker issues + let fn_ptr_opt = scope + .get(name) + .and_then(|v| v.read_lock::().map(|fp| fp.clone())); + if let Some(fn_ptr) = fn_ptr_opt { + // It's a FnPtr - extract it and handle as FnPtr call + + // Evaluate arguments (now scope is not borrowed) + let mut arg_values = FnArgsVec::with_capacity(args.len()); + for expr in args { + let (value, ..) = + self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?; + arg_values.push(value.flatten()); + } + + // Evaluate named arguments + #[cfg(feature = "default-parameters")] + let mut named_arg_values = FnArgsVec::new(); + #[cfg(feature = "default-parameters")] + for (param_name, expr) in named_args { + let (value, ..) = + self.get_arg_value(global, caches, scope, this_ptr.as_deref_mut(), expr)?; + named_arg_values.push((param_name.clone(), value.flatten())); + } + #[cfg(not(feature = "default-parameters"))] + let named_arg_values: FnArgsVec<(ImmutableString, Dynamic)> = FnArgsVec::new(); + + // Handle FnPtr call with defaults + let FnPtr { + name: fn_ptr_name, + curry: extra_curry, + #[cfg(not(feature = "no_function"))] + env, + typ, + } = fn_ptr; + + match typ { + #[cfg(all(not(feature = "no_function"), feature = "default-parameters"))] + FnPtrType::Script(fn_def) => { + let num_provided = extra_curry.len() + arg_values.len(); + let num_params = fn_def.params.len(); + let min_args = fn_def + .defaults + .iter() + .position(|d| d.is_some()) + .unwrap_or(num_params); + + if num_provided >= min_args && num_provided <= num_params { + // Combine curried args with provided args + let mut all_args = + FnArgsVec::with_capacity(extra_curry.len() + arg_values.len()); + all_args.extend(extra_curry.iter().cloned()); + all_args.extend(arg_values); + + // Build argument plan with defaults + let arg_plan = + build_arg_plan(&fn_def, &all_args, &named_arg_values, pos)?; + + let scope = &mut Scope::new(); + let env = env.as_deref(); + + defer! { let orig_level = global.level; global.level += 1 } + + return self.call_script_fn_with_plan( + global, caches, scope, None, env, &fn_def, arg_plan, true, pos, + ); + } + } + #[cfg(all(not(feature = "no_function"), not(feature = "default-parameters")))] + FnPtrType::Script(fn_def) if fn_def.params.len() == extra_curry.len() + arg_values.len() => { + // When feature is disabled, only allow exact argument count match + // Combine curried args with provided args + let mut all_args = + FnArgsVec::with_capacity(extra_curry.len() + arg_values.len()); + all_args.extend(extra_curry.iter().cloned()); + all_args.extend(arg_values); + let args = &mut all_args.iter_mut().collect::>(); + + let scope = &mut Scope::new(); + let env = env.as_deref(); + + defer! { let orig_level = global.level; global.level += 1 } + + return self.call_script_fn( + global, caches, scope, None, env, &fn_def, args, true, pos, + ); + } + _ => { + // For other FnPtr types, fall through to normal handling + // by redirecting to the FnPtr's function name + let (first_arg, rest_args) = args.split_first().map_or_else( + || (None, args.as_ref()), + |(first, rest)| (Some(first), rest), + ); + + return self.make_function_call( + global, + caches, + scope, + this_ptr, + &fn_ptr_name, + op_token, + first_arg, + rest_args, + named_args, + FnCallHashes::from_hash(calc_fn_hash( + None, + &fn_ptr_name, + extra_curry.len() + args.len(), + )), + *capture, + pos, + ); + } + } + } + } + let (first_arg, rest_args) = args.split_first().map_or_else( || (None, args.as_ref()), |(first, rest)| (Some(first), rest), ); self.make_function_call( - global, caches, scope, this_ptr, name, op_token, first_arg, rest_args, *hashes, - *capture, pos, + global, caches, scope, this_ptr, name, op_token, first_arg, rest_args, named_args, + *hashes, *capture, pos, ) } } diff --git a/src/func/script.rs b/src/func/script.rs index 86024a39b..292571d1e 100644 --- a/src/func/script.rs +++ b/src/func/script.rs @@ -5,10 +5,214 @@ use super::call::FnCallArgs; use crate::ast::{EncapsulatedEnviron, ScriptFuncDef}; use crate::eval::{Caches, GlobalRuntimeState}; use crate::{Dynamic, Engine, Position, RhaiResult, Scope, ERR}; +#[cfg(feature = "default-parameters")] +use super::call::ArgPlan; #[cfg(feature = "no_std")] use std::prelude::v1::*; impl Engine { + /// # Main Entry-Point (with ArgPlan) + /// + /// Call a script-defined function with an argument plan. + /// The plan describes which arguments are provided and which need default evaluation. + /// + /// If `rewind_scope` is `false`, arguments are removed from the scope but new variables are not. + #[cfg(feature = "default-parameters")] + pub(crate) fn call_script_fn_with_plan( + &self, + global: &mut GlobalRuntimeState, + caches: &mut Caches, + scope: &mut Scope, + mut this_ptr: Option<&mut Dynamic>, + _env: Option<&EncapsulatedEnviron>, + fn_def: &ScriptFuncDef, + arg_plan: ArgPlan, + rewind_scope: bool, + pos: Position, + ) -> RhaiResult { + self.track_operation(global, pos)?; + + // Check for stack overflow + #[cfg(not(feature = "unchecked"))] + if global.level > self.max_call_levels() { + return Err(ERR::ErrorStackOverflow(pos).into()); + } + + #[cfg(feature = "debugging")] + if self.debugger_interface.is_none() && fn_def.body.is_empty() { + return Ok(Dynamic::UNIT); + } + #[cfg(not(feature = "debugging"))] + if fn_def.body.is_empty() { + return Ok(Dynamic::UNIT); + } + + let orig_scope_len = scope.len(); + let orig_lib_len = global.lib.len(); + #[cfg(not(feature = "no_module"))] + let orig_imports_len = global.num_imports(); + + #[cfg(feature = "debugging")] + let orig_call_stack_len = global + .debugger + .as_ref() + .map_or(0, |dbg| dbg.call_stack().len()); + + // Guard against too many variables + #[cfg(not(feature = "unchecked"))] + if scope.len() + fn_def.params.len() > self.max_variables() { + return Err(ERR::ErrorTooManyVariables(pos).into()); + } + + // Merge in encapsulated environment first, before evaluating defaults + let orig_fn_resolution_caches_len = caches.fn_resolution_caches_len(); + + #[cfg(not(feature = "no_module"))] + let orig_constants = _env.map( + |EncapsulatedEnviron { + lib, + imports, + constants, + }| { + imports + .iter() + .cloned() + .for_each(|(n, m)| global.push_import(n, m)); + + global.lib.extend(lib.clone()); + + std::mem::replace(&mut global.constants, constants.clone()) + }, + ); + + // Now evaluate arguments and add them to scope in order + // This allows later defaults to reference earlier parameters + let num_args = arg_plan.sources.len(); + for (i, source) in arg_plan.sources.into_iter().enumerate() { + let param_name = fn_def.params[i].clone(); + let value = match source { + super::call::ArgSource::Provided(val) => val, + super::call::ArgSource::DefaultExpr => { + // Evaluate the default expression in the callee context + let default_expr = fn_def.defaults[i].as_ref().unwrap(); + self.eval_expr(global, caches, scope, this_ptr.as_deref_mut(), default_expr)? + } + }; + scope.push(param_name, value); + } + + // Push a new call stack frame + #[cfg(feature = "debugging")] + if self.is_debugger_registered() { + let fn_name = fn_def.name.clone(); + let args = scope + .iter_inner() + .skip(orig_scope_len) + .map(|(.., v)| v.flatten_clone()); + let source = global.source.clone(); + + global + .debugger_mut() + .push_call_stack_frame(fn_name, args, source, pos); + } + + #[cfg(feature = "debugging")] + if self.is_debugger_registered() { + let node = crate::ast::Stmt::Noop(fn_def.body.position()); + self.dbg(global, caches, scope, this_ptr.as_deref_mut(), &node)?; + } + + // Evaluate the function + let mut _result: RhaiResult = self + .eval_stmt_block( + global, + caches, + scope, + this_ptr.as_deref_mut(), + fn_def.body.statements(), + rewind_scope, + ) + .or_else(|err| match *err { + // Convert return statement to return value + ERR::Return(x, ..) => Ok(x), + // Exit value is passed straight-through + mut err @ ERR::Exit(..) => { + err.set_position(pos); + Err(err.into()) + } + // System errors are passed straight-through + mut err if err.is_system_exception() => { + err.set_position(pos); + Err(err.into()) + } + // Other errors are wrapped in `ErrorInFunctionCall` + _ => Err(ERR::ErrorInFunctionCall( + fn_def.name.to_string(), + #[cfg(not(feature = "no_module"))] + _env.and_then(|env| env.lib.last()) + .and_then(|m| m.id()) + .unwrap_or_else(|| global.source().unwrap_or("")) + .to_string(), + #[cfg(feature = "no_module")] + global.source().unwrap_or("").to_string(), + err, + pos, + ) + .into()), + }); + + #[cfg(feature = "debugging")] + if self.is_debugger_registered() { + let trigger = match global.debugger_mut().status { + crate::eval::DebuggerStatus::FunctionExit(n) => n >= global.level, + crate::eval::DebuggerStatus::Next(.., true) => true, + _ => false, + }; + + if trigger { + let node = crate::ast::Stmt::Noop(fn_def.body.end_position().or_else(pos)); + let node = (&node).into(); + let event = match _result { + Ok(ref r) => crate::eval::DebuggerEvent::FunctionExitWithValue(r), + Err(ref err) => crate::eval::DebuggerEvent::FunctionExitWithError(err), + }; + match self.dbg_raw(global, caches, scope, this_ptr, node, event) { + Ok(_) => (), + Err(err) => _result = Err(err), + } + } + + // Pop the call stack + global + .debugger + .as_mut() + .unwrap() + .rewind_call_stack(orig_call_stack_len); + } + + // Remove all local variables and imported modules + if rewind_scope { + scope.rewind(orig_scope_len); + } else if num_args > 0 { + // Remove arguments only, leaving new variables in the scope + scope.remove_range(orig_scope_len, num_args); + } + global.lib.truncate(orig_lib_len); + #[cfg(not(feature = "no_module"))] + global.truncate_imports(orig_imports_len); + + // Restore constants + #[cfg(not(feature = "no_module"))] + if let Some(constants) = orig_constants { + global.constants = constants; + } + + // Restore state + caches.rewind_fn_resolution_caches(orig_fn_resolution_caches_len); + + _result + } + /// # Main Entry-Point /// /// Call a script-defined function. diff --git a/src/module/mod.rs b/src/module/mod.rs index a216d4c08..67d279fa1 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1278,51 +1278,81 @@ impl Module { // None + function name + number of arguments. let namespace = FnNamespace::Internal; let num_params = fn_def.params.len(); - let hash_script = crate::calc_fn_hash(None, &fn_def.name, num_params); - #[cfg(not(feature = "no_object"))] - let (hash_script, namespace) = - fn_def - .this_type - .as_ref() - .map_or((hash_script, namespace), |this_type| { - ( - crate::calc_typed_method_hash(hash_script, this_type), - FnNamespace::Global, - ) - }); - // Catch hash collisions in testing environment only. - #[cfg(feature = "testing-environ")] - if let Some(f) = self.functions.as_ref().and_then(|f| f.get(&hash_script)) { - unreachable!( - "Hash {} already exists when registering function {:#?}:\n{:#?}", - hash_script, fn_def, f - ); - } + // Calculate minimum number of required arguments (parameters without defaults) + #[cfg(feature = "default-parameters")] + let min_args = fn_def + .defaults + .iter() + .position(|d| d.is_some()) + .unwrap_or(num_params); + #[cfg(not(feature = "default-parameters"))] + let min_args = num_params; + + // Register the function for all valid argument counts from min_args to num_params + // This allows functions with defaults to be called with fewer arguments + let functions = self + .functions + .get_or_insert_with(|| new_hash_map(FN_MAP_SIZE)); + let mut primary_hash = 0u64; - let metadata = FuncMetadata { - hash: hash_script, - name: fn_def.name.as_str().into(), - namespace, - access: fn_def.access, - num_params, - param_types: FnArgsVec::new_const(), - #[cfg(feature = "metadata")] - params_info: fn_def.params.iter().map(Into::into).collect(), - #[cfg(feature = "metadata")] - return_type: <_>::default(), - #[cfg(feature = "metadata")] - comments: crate::StaticVec::new_const(), - }; + for arg_count in min_args..=num_params { + let hash_script = crate::calc_fn_hash(None, &fn_def.name, arg_count); + #[cfg(not(feature = "no_object"))] + let (hash_script, namespace) = + fn_def + .this_type + .as_ref() + .map_or((hash_script, namespace), |this_type| { + ( + crate::calc_typed_method_hash(hash_script, this_type), + FnNamespace::Global, + ) + }); - self.functions - .get_or_insert_with(|| new_hash_map(FN_MAP_SIZE)) - .insert(hash_script, (fn_def.into(), metadata.into())); + // Store the primary hash (for the full parameter count) + if arg_count == num_params { + primary_hash = hash_script; + } + + // For fallback signatures (arg_count < num_params), only insert if not already present + // This ensures that explicit overloads take precedence over default-parameter fallbacks + #[cfg(feature = "default-parameters")] + if arg_count < num_params && functions.contains_key(&hash_script) { + continue; + } + + // Catch hash collisions in testing environment only. + #[cfg(feature = "testing-environ")] + if let Some(f) = functions.get(&hash_script) { + unreachable!( + "Hash {} already exists when registering function {:#?}:\n{:#?}", + hash_script, fn_def, f + ); + } + + let metadata = FuncMetadata { + hash: hash_script, + name: fn_def.name.as_str().into(), + namespace, + access: fn_def.access, + num_params, + param_types: FnArgsVec::new_const(), + #[cfg(feature = "metadata")] + params_info: fn_def.params.iter().map(Into::into).collect(), + #[cfg(feature = "metadata")] + return_type: <_>::default(), + #[cfg(feature = "metadata")] + comments: crate::StaticVec::new_const(), + }; + + functions.insert(hash_script, (fn_def.clone().into(), metadata.into())); + } self.flags .remove(ModuleFlags::INDEXED | ModuleFlags::INDEXED_GLOBAL_FUNCTIONS); - hash_script + primary_hash } /// Get a shared reference to the script-defined function in the [`Module`] based on name @@ -2516,28 +2546,46 @@ impl Module { if f.is_script() { #[cfg(not(feature = "no_function"))] { - let hash_script = - crate::calc_fn_hash(path.iter().copied(), &m.name, m.num_params); - #[cfg(not(feature = "no_object"))] - let hash_script = f - .get_script_fn_def() - .unwrap() - .this_type - .as_ref() - .map_or(hash_script, |this_type| { - crate::calc_typed_method_hash(hash_script, this_type) - }); - - // Catch hash collisions in testing environment only. - #[cfg(feature = "testing-environ")] - if let Some(fx) = functions.get(&hash_script) { - unreachable!( - "Hash {} already exists when indexing function {:#?}:\n{:#?}", - hash_script, f, fx - ); + // For functions with defaults, index all valid argument counts + let fn_def = f.get_script_fn_def().unwrap(); + let num_params = fn_def.params.len(); + #[cfg(feature = "default-parameters")] + let min_args = fn_def + .defaults + .iter() + .position(|d| d.is_some()) + .unwrap_or(num_params); + #[cfg(not(feature = "default-parameters"))] + let min_args = num_params; + + // Index for all valid argument counts from min_args to num_params + for arg_count in min_args..=num_params { + let hash_script = + crate::calc_fn_hash(path.iter().copied(), &m.name, arg_count); + #[cfg(not(feature = "no_object"))] + let hash_script = + fn_def.this_type.as_ref().map_or(hash_script, |this_type| { + crate::calc_typed_method_hash(hash_script, this_type) + }); + + // For fallback signatures (arg_count < num_params), only insert if not already present + // This ensures that explicit overloads take precedence over default-parameter fallbacks + #[cfg(feature = "default-parameters")] + if arg_count < num_params && functions.contains_key(&hash_script) { + continue; + } + + // Catch hash collisions in testing environment only. + #[cfg(feature = "testing-environ")] + if let Some(fx) = functions.get(&hash_script) { + unreachable!( + "Hash {} already exists when indexing function {:#?}:\n{:#?}", + hash_script, f, fx + ); + } + + functions.insert(hash_script, f.clone()); } - - functions.insert(hash_script, f.clone()); } } else { let hash_fn = diff --git a/src/parser.rs b/src/parser.rs index 7326e9b49..92e3f4e7f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -212,6 +212,8 @@ bitflags! { const DISALLOW_STATEMENTS_IN_BLOCKS = 0b0001_0000; /// Disallow unquoted map properties? const DISALLOW_UNQUOTED_MAP_PROPERTIES = 0b0010_0000; + /// Is the construct being parsed inside closure parameters (stop at | and ,)? + const IN_CLOSURE_PARAMS = 0b0100_0000; } } @@ -669,6 +671,8 @@ impl Engine { namespace, hashes, args, + #[cfg(feature = "default-parameters")] + named_args: FnArgsVec::new(), } .into_fn_call_expr(settings.pos)); } @@ -677,12 +681,95 @@ impl Engine { } let settings = settings.level_up()?; + #[cfg(feature = "default-parameters")] + let mut named_args = FnArgsVec::new(); + #[cfg(feature = "default-parameters")] + let mut seen_named = false; loop { match state.input.peek().unwrap() { // id(...args, ) - handle trailing comma (Token::RightParen, ..) => (), - _ => args.push(self.parse_expr(state, settings)?), + _ => { + #[cfg(feature = "default-parameters")] + { + // Check if this might be a named argument: identifier = expression + // Strategy: parse a primary expression (which handles identifiers and chaining), + // then check if it's a simple Variable followed by = + let expr = self.parse_primary(state, settings, ChainingFlags::empty())?; + + // Check if this is a simple Variable (just an identifier, no chaining) followed by = + let is_named_arg = matches!(&expr, Expr::Variable(..)) + && matches!(state.input.peek().unwrap(), (Token::Equals, ..)); + + if is_named_arg { + // Extract the parameter name from the Variable expression + let param_name = match &expr { + Expr::Variable(var, ..) => { + // In both cases, the variable name is at index 1 + var.1.clone() + } + _ => unreachable!("Should be Variable"), + }; + + // Consume the = token + eat_token(state.input, &Token::Equals); + + // Mark that we've seen named arguments + if !seen_named && !args.is_empty() { + seen_named = true; + } + + // Parse the value expression + let value_expr = self.parse_expr(state, settings)?; + named_args.push((param_name, value_expr)); + + // Handle comma after named argument (for multiple named args) + // Check if there's a comma or closing paren + match state.input.peek().unwrap() { + (Token::Comma, ..) => { + eat_token(state.input, &Token::Comma); + // Continue to next iteration to parse next argument + continue; + } + (Token::RightParen, ..) => { + // End of arguments - will be handled by outer loop + // Don't break here, let the outer match handle it + } + _ => { + // Unexpected token + let (_, pos) = state.input.peek().unwrap(); + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the arguments to function call".into(), + ) + .into_err(*pos)); + } + } + } else { + // Not a named argument - it's a normal positional argument + if seen_named { + let (_, pos) = state.input.peek().unwrap(); + return Err(PERR::InvalidDefaultValue( + "positional arguments cannot follow named arguments".into(), + ) + .into_err(*pos)); + } + + let precedence = Precedence::new(1); + let full_expr = self.parse_binary_op(state, settings, precedence, expr)?; + args.push(full_expr); + } + } + #[cfg(not(feature = "default-parameters"))] + { + // When feature is disabled, parse as normal positional argument + let expr = self.parse_primary(state, settings, ChainingFlags::empty())?; + let precedence = Precedence::new(1); + let full_expr = self.parse_binary_op(state, settings, precedence, expr)?; + args.push(full_expr); + } + } } match state.input.peek().unwrap() { @@ -728,6 +815,8 @@ impl Engine { }; args.shrink_to_fit(); + #[cfg(feature = "default-parameters")] + named_args.shrink_to_fit(); return Ok(FnCallExpr { name: self.get_interned_string(id), @@ -737,6 +826,8 @@ impl Engine { namespace, hashes, args, + #[cfg(feature = "default-parameters")] + named_args, } .into_fn_call_expr(settings.pos)); } @@ -1895,12 +1986,14 @@ impl Engine { Expr::FloatConstant(x, ..) => Ok(Expr::FloatConstant((-(*x)).into(), pos)), // Call negative function - expr => Ok(FnCallExpr { + expr => Ok(FnCallExpr { #[cfg(not(feature = "no_module"))] namespace: crate::ast::Namespace::NONE, name: self.get_interned_string("-"), hashes: FnCallHashes::from_native_only(calc_fn_hash(None, "-", 1)), args: IntoIterator::into_iter([expr]).collect(), + #[cfg(feature = "default-parameters")] + named_args: FnArgsVec::new(), op_token: Some(token), capture_parent_scope: false, } @@ -1924,6 +2017,8 @@ impl Engine { name: self.get_interned_string("+"), hashes: FnCallHashes::from_native_only(calc_fn_hash(None, "+", 1)), args: IntoIterator::into_iter([expr]).collect(), + #[cfg(feature = "default-parameters")] + named_args: FnArgsVec::new(), op_token: Some(token), capture_parent_scope: false, } @@ -1944,6 +2039,8 @@ impl Engine { let expr = self.parse_unary(state, settings.level_up()?)?; IntoIterator::into_iter([expr]).collect() }, + #[cfg(feature = "default-parameters")] + named_args: FnArgsVec::new(), op_token: Some(token), capture_parent_scope: false, } @@ -2240,6 +2337,14 @@ impl Engine { return Ok(root); } + // Stop at | and , when parsing closure parameters with defaults + #[cfg(feature = "default-parameters")] + if settings.has_flag(ParseSettingFlags::IN_CLOSURE_PARAMS) { + if matches!(current_op, Token::Pipe | Token::Comma) { + return Ok(root); + } + } + let precedence = match current_op { #[cfg(not(feature = "no_custom_syntax"))] Token::Custom(c) => self @@ -2330,6 +2435,8 @@ impl Engine { name: self.get_interned_string(&op), hashes: FnCallHashes::from_native_only(hash), args: IntoIterator::into_iter([root, rhs]).collect(), + #[cfg(feature = "default-parameters")] + named_args: FnArgsVec::new(), op_token: native_only.then(|| op_token.clone()), capture_parent_scope: false, }; @@ -2412,6 +2519,8 @@ impl Engine { name: self.get_interned_string(OP_NOT), hashes: FnCallHashes::from_native_only(calc_fn_hash(None, OP_NOT, 1)), args: IntoIterator::into_iter([fn_call]).collect(), + #[cfg(feature = "default-parameters")] + named_args: FnArgsVec::new(), op_token: Some(Token::Bang), capture_parent_scope: false, }; @@ -3635,6 +3744,8 @@ impl Engine { }; let mut params = StaticVec::<(ImmutableString, _)>::new_const(); + #[cfg(feature = "default-parameters")] + let mut defaults = StaticVec::>>::new_const(); if !no_params { let sep_err = format!("to separate the parameters of function '{name}'"); @@ -3651,7 +3762,33 @@ impl Engine { let s = self.get_interned_string(*s); state.stack.push(s.clone(), ()); + + #[cfg(feature = "default-parameters")] + // Check for default value + let default_value = match state.input.peek().unwrap() { + (Token::Equals, ..) => { + eat_token(state.input, &Token::Equals); + // Parse the default value expression + let mut default_expr = self.parse_expr(state, settings.level_up()?)?; + // Selectively clear indices only for parameters (not outer scope vars) + let param_names: Vec = params.iter().map(|(p, _)| p.clone()).collect(); + Self::clear_param_indices(&mut default_expr, ¶m_names); + Some(Box::new(default_expr)) + } + _ => None, + }; + #[cfg(not(feature = "default-parameters"))] + // When feature is disabled, reject = in parameter list + if matches!(state.input.peek().unwrap(), (Token::Equals, ..)) { + let (_, pos) = state.input.peek().unwrap(); + return Err(PERR::InvalidDefaultValue( + "default parameters are not enabled (feature 'default-parameters' required)".into() + ).into_err(*pos)); + } + params.push((s, pos)); + #[cfg(feature = "default-parameters")] + defaults.push(default_value); } (Token::LexError(err), pos) => return Err(err.into_err(pos)), (token, pos) if token.is_reserved() => { @@ -3690,12 +3827,25 @@ impl Engine { let mut params: FnArgsVec<_> = params.into_iter().map(|(p, ..)| p).collect(); params.shrink_to_fit(); + #[cfg(feature = "default-parameters")] + let mut defaults: FnArgsVec<_> = defaults.into_iter().collect(); + #[cfg(feature = "default-parameters")] + defaults.shrink_to_fit(); + + #[cfg(feature = "default-parameters")] + // Ensure defaults has the same length as params + while defaults.len() < params.len() { + defaults.push(None); + } + Ok(ScriptFuncDef { name: self.get_interned_string(name), access, #[cfg(not(feature = "no_object"))] this_type, params, + #[cfg(feature = "default-parameters")] + defaults, body, #[cfg(feature = "metadata")] comments: comments.into_iter().collect(), @@ -3750,6 +3900,8 @@ impl Engine { num_externals + 1, )), args, + #[cfg(feature = "default-parameters")] + named_args: FnArgsVec::new(), op_token: None, capture_parent_scope: false, } @@ -3801,6 +3953,8 @@ impl Engine { } let mut params_list = StaticVec::::new_const(); + #[cfg(feature = "default-parameters")] + let mut defaults_list = StaticVec::>>::new_const(); // Parse parameters if !skip_parameters @@ -3819,7 +3973,37 @@ impl Engine { let s = self.get_interned_string(*s); new_state.stack.push(s.clone(), ()); + + #[cfg(feature = "default-parameters")] + // Check for default value + let default_value = match new_state.input.peek().unwrap() { + (Token::Equals, ..) => { + eat_token(new_state.input, &Token::Equals); + // Parse the default value expression with IN_CLOSURE_PARAMS flag + // to stop at | and , tokens, and FN_SCOPE to allow 'this' + let mut param_settings = settings.level_up()?; + param_settings.flags |= ParseSettingFlags::IN_CLOSURE_PARAMS + | ParseSettingFlags::FN_SCOPE + | ParseSettingFlags::CLOSURE_SCOPE; + let mut default_expr = self.parse_expr(new_state, param_settings)?; + // Selectively clear indices only for parameters (not outer scope vars) + Self::clear_param_indices(&mut default_expr, ¶ms_list); + Some(Box::new(default_expr)) + } + _ => None, + }; + #[cfg(not(feature = "default-parameters"))] + // When feature is disabled, reject = in parameter list + if matches!(new_state.input.peek().unwrap(), (Token::Equals, ..)) { + let (_, pos) = new_state.input.peek().unwrap(); + return Err(PERR::InvalidDefaultValue( + "default parameters are not enabled (feature 'default-parameters' required)".into() + ).into_err(*pos)); + } + params_list.push(s); + #[cfg(feature = "default-parameters")] + defaults_list.push(default_value); } (Token::LexError(err), pos) => return Err(err.into_err(pos)), (token, pos) if token.is_reserved() => { @@ -3897,6 +4081,28 @@ impl Engine { params.append(&mut params_list); + #[cfg(feature = "default-parameters")] + // Build defaults list - externals don't have defaults, then params_list defaults + let mut defaults = FnArgsVec::with_capacity(params.len()); + #[cfg(feature = "default-parameters")] + #[cfg(not(feature = "no_closure"))] + { + // Externals don't have defaults + defaults.extend((0.._externals.len()).map(|_| None)); + } + #[cfg(feature = "default-parameters")] + // Add defaults for params_list + let mut defaults_list_vec: FnArgsVec<_> = defaults_list.into_iter().collect(); + #[cfg(feature = "default-parameters")] + defaults.append(&mut defaults_list_vec); + #[cfg(feature = "default-parameters")] + // Ensure defaults has the same length as params + while defaults.len() < params.len() { + defaults.push(None); + } + #[cfg(feature = "default-parameters")] + defaults.shrink_to_fit(); + // Create unique function name by hashing the script body plus the parameters. let hasher = &mut get_hasher(); params.iter().for_each(|p| p.hash(hasher)); @@ -3911,6 +4117,8 @@ impl Engine { #[cfg(not(feature = "no_object"))] this_type: None, params, + #[cfg(feature = "default-parameters")] + defaults, body: body.into(), #[cfg(not(feature = "no_function"))] #[cfg(feature = "metadata")] @@ -3949,8 +4157,28 @@ impl Engine { } } - let hash_script = calc_fn_hash(None, &fn_def.name, fn_def.params.len()); - state.lib.insert(hash_script, fn_def); + #[cfg(feature = "default-parameters")] + // Register anonymous function with multiple signatures if it has defaults + { + let num_params = fn_def.params.len(); + let min_args = fn_def + .defaults + .iter() + .position(|d| d.is_some()) + .unwrap_or(num_params); + + // Register for all valid argument counts from min_args to num_params + for arg_count in min_args..=num_params { + let hash_script = calc_fn_hash(None, &fn_def.name, arg_count); + state.lib.insert(hash_script, fn_def.clone()); + } + } + #[cfg(not(feature = "default-parameters"))] + // Register anonymous function with single signature + { + let hash_script = calc_fn_hash(None, &fn_def.name, fn_def.params.len()); + state.lib.insert(hash_script, fn_def.clone()); + } #[cfg(not(feature = "no_closure"))] let expr = self.make_curry_from_externals(state, expr, _externals, settings.pos); @@ -4097,4 +4325,135 @@ impl Engine { }, )); } + + /// Selectively clear variable indices for parameters only. + /// This allows parameter-to-parameter references to work via name lookup, + /// while preserving indices for outer scope variables. + #[cfg(feature = "default-parameters")] + fn clear_param_indices(expr: &mut Expr, param_names: &[ImmutableString]) { + use crate::ast::Expr; + + match expr { + Expr::Variable(x, idx, _) => { + // Check if this variable matches a parameter name + let name = &x.1; + if param_names.iter().any(|p| p == name) { + // Clear the index so the parameter is looked up by name + x.0 = None; + *idx = None; + } + } + Expr::FnCall(x, _) => { + // Recursively clear indices in function arguments + for arg in &mut x.args { + Self::clear_param_indices(arg, param_names); + } + #[cfg(feature = "default-parameters")] + for (_, arg) in &mut x.named_args { + Self::clear_param_indices(arg, param_names); + } + } + Expr::Array(x, _) => { + for item in x.iter_mut() { + Self::clear_param_indices(item, param_names); + } + } + Expr::Map(x, _) => { + for (_, item) in &mut x.0 { + Self::clear_param_indices(item, param_names); + } + } + Expr::Index(x, _, _) | Expr::Dot(x, _, _) => { + Self::clear_param_indices(&mut x.lhs, param_names); + Self::clear_param_indices(&mut x.rhs, param_names); + } + Expr::InterpolatedString(x, _) => { + for item in x.iter_mut() { + Self::clear_param_indices(item, param_names); + } + } + Expr::Stmt(x) => { + // Clear indices in statement block expressions + for stmt in x.statements_mut() { + Self::clear_param_indices_in_stmt(stmt, param_names); + } + } + Expr::And(x, _) | Expr::Or(x, _) | Expr::Coalesce(x, _) => { + for item in x.iter_mut() { + Self::clear_param_indices(item, param_names); + } + } + #[cfg(not(feature = "no_custom_syntax"))] + Expr::Custom(x, _) => { + for input in &mut x.inputs { + Self::clear_param_indices(input, param_names); + } + } + // Literals and constants don't have variables + _ => {} + } + } + + /// Helper to clear parameter indices in statements + #[cfg(feature = "default-parameters")] + fn clear_param_indices_in_stmt(stmt: &mut Stmt, param_names: &[ImmutableString]) { + use crate::ast::Stmt; + + match stmt { + Stmt::Expr(expr) => { + Self::clear_param_indices(expr, param_names); + } + Stmt::Return(Some(expr), ..) => { + Self::clear_param_indices(expr, param_names); + } + Stmt::Var(x, ..) => { + Self::clear_param_indices(&mut x.1, param_names); + } + Stmt::Assignment(x) => { + Self::clear_param_indices(&mut x.1.lhs, param_names); + Self::clear_param_indices(&mut x.1.rhs, param_names); + } + Stmt::If(x, _) => { + Self::clear_param_indices(&mut x.expr, param_names); + for stmt in x.body.statements_mut() { + Self::clear_param_indices_in_stmt(stmt, param_names); + } + for stmt in x.branch.statements_mut() { + Self::clear_param_indices_in_stmt(stmt, param_names); + } + } + Stmt::Switch(x, _) => { + Self::clear_param_indices(&mut x.0, param_names); + // Skip clearing in switch cases for simplicity + } + Stmt::While(x, _) | Stmt::Do(x, _, _) => { + Self::clear_param_indices(&mut x.expr, param_names); + for stmt in x.body.statements_mut() { + Self::clear_param_indices_in_stmt(stmt, param_names); + } + } + Stmt::For(x, _) => { + Self::clear_param_indices(&mut x.2.expr, param_names); + for stmt in x.2.body.statements_mut() { + Self::clear_param_indices_in_stmt(stmt, param_names); + } + } + Stmt::Block(block) => { + for stmt in block.statements_mut() { + Self::clear_param_indices_in_stmt(stmt, param_names); + } + } + Stmt::TryCatch(x, _) => { + Self::clear_param_indices(&mut x.expr, param_names); + for stmt in x.body.statements_mut() { + Self::clear_param_indices_in_stmt(stmt, param_names); + } + for stmt in x.branch.statements_mut() { + Self::clear_param_indices_in_stmt(stmt, param_names); + } + } + // Other statements don't contain expressions + _ => {} + } + } } diff --git a/src/types/parse_error.rs b/src/types/parse_error.rs index 4e2e30449..62810f4c1 100644 --- a/src/types/parse_error.rs +++ b/src/types/parse_error.rs @@ -158,6 +158,8 @@ pub enum ParseErrorType { FnDuplicatedParam(String, String), /// A function definition is missing the body. Wrapped value is the function name. FnMissingBody(String), + /// Invalid default parameter value. Wrapped value is the error message. + InvalidDefaultValue(String), /// Export statement not at global level. WrongExport, /// Assignment to an a constant variable. Wrapped value is the constant variable name. @@ -214,6 +216,7 @@ impl fmt::Display for ParseErrorType { Self::FnMissingParams(s) => write!(f, "Expecting parameters for function {s}"), Self::FnDuplicatedParam(s, arg) => write!(f, "Duplicated parameter {arg} for function {s}"), + Self::InvalidDefaultValue(s) => write!(f, "Invalid default parameter value: {s}"), Self::DuplicatedProperty(s) => write!(f, "Duplicated property for object map literal: {s}"), Self::DuplicatedVariable(s) => write!(f, "Duplicated variable name: {s}"), diff --git a/tests/default_expr.rs b/tests/default_expr.rs new file mode 100644 index 000000000..b85a71f91 --- /dev/null +++ b/tests/default_expr.rs @@ -0,0 +1,530 @@ +// Test default parameter expressions + +#![cfg(feature = "default-parameters")] + +use rhai::{Engine, EvalAltResult, INT}; + +#[test] +fn test_default_expr_simple() -> Result<(), Box> { + let engine = Engine::new(); + + // Simple expression default + let result = engine.eval::( + r#" + fn add(a, b = a + 1) { + a + b + } + add(5) + "#, + )?; + assert_eq!(result, 11); // 5 + (5 + 1) = 11 + + Ok(()) +} + +#[test] +fn test_default_expr_referencing_previous_params() -> Result<(), Box> { + let engine = Engine::new(); + + // Default referencing previous parameters + let result = engine.eval::( + r#" + fn multiply(a, b = a * 2, c = a + b) { + a + b + c + } + multiply(3) + "#, + )?; + // a = 3, b = 3 * 2 = 6, c = 3 + 6 = 9 + // result = 3 + 6 + 9 = 18 + assert_eq!(result, 18); + + Ok(()) +} + + +// Named arguments use = syntax: func(a, param = value) +#[test] +fn test_default_expr_with_named_args() -> Result<(), Box> { + let engine = Engine::new(); + + // Named arguments with expression defaults + let result = engine.eval::( + r#" + fn calc(a, b = a + 1, c = b * 2) { + a + b + c + } + + calc(5, c = 20) + "#, + )?; + // a = 5, b = 5 + 1 = 6 (default), c = 20 (named) + // result = 5 + 6 + 20 = 31 + assert_eq!(result, 31); + + Ok(()) +} + +#[test] +fn test_default_expr_complex() -> Result<(), Box> { + let engine = Engine::new(); + + // Complex expression with function calls + let result = engine.eval::( + r#" + fn double(x) { x * 2 } + + fn calc(a, b = double(a), c = a + b) { + a + b + c + } + + calc(7) + "#, + )?; + // a = 7, b = double(7) = 14, c = 7 + 14 = 21 + // result = 7 + 14 + 21 = 42 + assert_eq!(result, 42); + + Ok(()) +} + +#[test] +fn test_default_expr_with_literals_mixed() -> Result<(), Box> { + let engine = Engine::new(); + + // Mix of literal and expression defaults + let result = engine.eval::( + r#" + fn test(a, b = 10, c = a + b) { + a + b + c + } + + test(5) + "#, + )?; + // a = 5, b = 10 (literal), c = 5 + 10 = 15 + // result = 5 + 10 + 15 = 30 + assert_eq!(result, 30); + + Ok(()) +} + +// Closures support default parameters and can capture outer scope variables +#[test] +fn test_default_expr_closure() -> Result<(), Box> { + let engine = Engine::new(); + + // Closure with expression defaults + let result = engine.eval::( + r#" + let f = |a, b = a * 2| a + b; + f.call(6) + "#, + )?; + // a = 6, b = 6 * 2 = 12 + // result = 6 + 12 = 18 + assert_eq!(result, 18); + + // Closure capturing outer variable in default + let result2 = engine.eval::( + r#" + let multiplier = 10; + let f = |x, y = x * multiplier| x + y; + f.call(5) + "#, + )?; + // x = 5, y = 5 * 10 = 50 + // result = 5 + 50 = 55 + assert_eq!(result2, 55); + + Ok(()) +} + +#[test] +fn test_default_expr_arithmetic() -> Result<(), Box> { + let engine = Engine::new(); + + // Test various arithmetic operations in defaults + let result = engine.eval::( + r#" + fn calc(a, b = a + 10, c = a * 2, d = b - c) { + a + b + c + d + } + calc(5) + "#, + )?; + // a = 5, b = 5 + 10 = 15, c = 5 * 2 = 10, d = 15 - 10 = 5 + // result = 5 + 15 + 10 + 5 = 35 + assert_eq!(result, 35); + + Ok(()) +} + +#[test] +fn test_default_expr_comparison() -> Result<(), Box> { + let engine = Engine::new(); + + // Test comparison operations in defaults + let result = engine.eval::( + r#" + fn test(a, b = if a > 10 { a * 2 } else { a + 5 }) { + b + } + test(15) + test(3) + "#, + )?; + // First call: a=15, b = 15 * 2 = 30 + // Second call: a=3, b = 3 + 5 = 8 + // result = 30 + 8 = 38 + assert_eq!(result, 38); + + Ok(()) +} + +#[test] +fn test_default_expr_string_ops() -> Result<(), Box> { + let engine = Engine::new(); + + // Test string operations in defaults + let result = engine.eval::( + r#" + fn greet(name, greeting = "Hello, " + name + "!") { + greeting + } + greet("World") + "#, + )?; + assert_eq!(result, "Hello, World!"); + + Ok(()) +} + +#[test] +fn test_default_expr_multiple_calls() -> Result<(), Box> { + let engine = Engine::new(); + + // Test that defaults are evaluated fresh for each call + let result = engine.eval::( + r#" + fn test(a, b = a * 2) { + a + b + } + test(5) + test(10) + test(3) + "#, + )?; + // First: a=5, b=10, result=15 + // Second: a=10, b=20, result=30 + // Third: a=3, b=6, result=9 + // Total: 15 + 30 + 9 = 54 + assert_eq!(result, 54); + + Ok(()) +} + +#[test] +fn test_default_expr_nested_functions() -> Result<(), Box> { + let engine = Engine::new(); + + // Test nested function calls in defaults + let result = engine.eval::( + r#" + fn add(x, y) { x + y } + fn mul(x, y) { x * y } + + fn calc(a, b = add(a, 5), c = mul(b, 2)) { + c + } + calc(10) + "#, + )?; + // a = 10, b = add(10, 5) = 15, c = mul(15, 2) = 30 + assert_eq!(result, 30); + + Ok(()) +} + +#[test] +fn test_default_expr_override_with_value() -> Result<(), Box> { + let engine = Engine::new(); + + // Test that providing a value overrides the default + let result = engine.eval::( + r#" + fn test(a, b = a * 2) { + a + b + } + test(5) + test(5, 100) + "#, + )?; + // First: a=5, b=10 (default), result=15 + // Second: a=5, b=100 (provided), result=105 + // Total: 15 + 105 = 120 + assert_eq!(result, 120); + + Ok(()) +} + +#[test] +fn test_default_expr_array_ops() -> Result<(), Box> { + let engine = Engine::new(); + + // Test array operations in defaults + let result = engine.eval::( + r#" + fn sum_array(arr, multiplier = arr.len()) { + let total = 0; + for item in arr { + total += item; + } + total * multiplier + } + sum_array([1, 2, 3]) + "#, + )?; + // arr = [1, 2, 3], multiplier = 3 (arr.len()) + // sum = 6, result = 6 * 3 = 18 + assert_eq!(result, 18); + + Ok(()) +} + +#[test] +fn test_default_expr_logical_ops() -> Result<(), Box> { + let engine = Engine::new(); + + // Test logical operations in defaults + let result = engine.eval::( + r#" + fn test(a, b = a > 5 && a < 15, c = if b { a * 2 } else { a }) { + c + } + test(10) + test(20) + "#, + )?; + // First: a=10, b=true (10 > 5 && 10 < 15), c=20 (10 * 2) + // Second: a=20, b=false (20 > 5 but not < 15), c=20 (just a) + // Total: 20 + 20 = 40 + assert_eq!(result, 40); + + Ok(()) +} + +#[test] +fn test_default_expr_method_on_param() -> Result<(), Box> { + let engine = Engine::new(); + + // Test calling methods on parameters in defaults + let result = engine.eval::( + r#" + fn process(text, length = text.len(), doubled = length * 2) { + doubled + } + process("hello") + "#, + )?; + // text = "hello", length = 5, doubled = 10 + assert_eq!(result, 10); + + Ok(()) +} + +#[test] +fn test_default_expr_chained_dependencies() -> Result<(), Box> { + let engine = Engine::new(); + + // Test long chain of parameter dependencies + let result = engine.eval::( + r#" + fn chain(a, b = a + 1, c = b + 1, d = c + 1, e = d + 1) { + e + } + chain(10) + "#, + )?; + // a=10, b=11, c=12, d=13, e=14 + assert_eq!(result, 14); + + Ok(()) +} + +// Constants can be accessed in default parameter expressions naturally +#[test] +fn test_default_expr_with_constants() -> Result<(), Box> { + let engine = Engine::new(); + + // Test defaults that use constants + let result = engine.eval::( + r#" + const MULTIPLIER = 3; + + fn calc(a, b = a * MULTIPLIER) { + b + } + calc(7) + "#, + )?; + // a = 7, b = 7 * 3 = 21 + assert_eq!(result, 21); + + Ok(()) +} + +#[test] +fn test_default_expr_ternary() -> Result<(), Box> { + let engine = Engine::new(); + + // Test ternary-like expressions in defaults + let result = engine.eval::( + r#" + fn clamp(value, min = 0, max = if value < 100 { 100 } else { value * 2 }) { + if value < min { + min + } else if value > max { + max + } else { + value + } + } + clamp(50) + clamp(150) + "#, + )?; + // First: value=50, min=0, max=100, result=50 + // Second: value=150, min=0, max=300 (150*2), result=150 + // Total: 50 + 150 = 200 + assert_eq!(result, 200); + + Ok(()) +} + +// 'this' is now accessible in closure default parameters +#[test] +fn test_default_expr_with_this() -> Result<(), Box> { + let engine = Engine::new(); + + // Test defaults that reference 'this' in methods + let result = engine.eval::( + r#" + let obj = #{ + value: 42, + process: |multiplier = this.value * 2| multiplier + }; + obj.process() + "#, + )?; + // multiplier = this.value * 2 = 42 * 2 = 84 + assert_eq!(result, 84); + + Ok(()) +} + +#[test] +fn test_default_expr_error_in_default() -> Result<(), Box> { + let engine = Engine::new(); + + // Test that errors in default expressions are properly propagated + let result = engine.eval::( + r#" + fn divide(a, b = 10 / a) { + b + } + divide(0) + "#, + ); + + // Should error due to division by zero + assert!(result.is_err()); + + Ok(()) +} + +#[test] +fn test_default_expr_partial_override() -> Result<(), Box> { + let engine = Engine::new(); + + // Test providing some but not all parameters with defaults + let result = engine.eval::( + r#" + fn test(a, b = a + 1, c = b + 1, d = c + 1) { + a + b + c + d + } + test(10, 20) + "#, + )?; + // a=10, b=20 (provided), c=21 (b+1), d=22 (c+1) + // result = 10 + 20 + 21 + 22 = 73 + assert_eq!(result, 73); + + Ok(()) +} + +#[test] +fn test_default_expr_all_defaults() -> Result<(), Box> { + let engine = Engine::new(); + + // Test function with all parameters having defaults + let result = engine.eval::( + r#" + fn test(a = 5, b = a * 2, c = a + b) { + a + b + c + } + test() + "#, + )?; + // a=5, b=10, c=15 + // result = 5 + 10 + 15 = 30 + assert_eq!(result, 30); + + Ok(()) +} + +#[test] +fn test_default_expr_complex_expression() -> Result<(), Box> { + let engine = Engine::new(); + + // Test complex multi-operation expression in default + let result = engine.eval::( + r#" + fn calc(x, result = (x + 5) * 2 - 3) { + result + } + calc(10) + "#, + )?; + // x = 10, result = (10 + 5) * 2 - 3 = 15 * 2 - 3 = 27 + assert_eq!(result, 27); + + Ok(()) +} + +// NOTE: This test is disabled because regular named functions in Rhai cannot access +// outer scope variables (by design). Only closures can capture outer scope. +// To access outer scope in default parameters, the function must be a closure +#[test] +#[ignore] +fn test_default_expr_with_global_state() -> Result<(), Box> { + let engine = Engine::new(); + + // Default that mutates global state + let result = engine.eval::( + r#" + let counter = 0; + + fn increment_counter() { + counter += 1; + counter + } + + fn test(a, b = increment_counter()) { + a + b + } + + test(10) + test(20) + "#, + )?; + // First call: a=10, b=increment_counter()=1, result=11 + // Second call: a=20, b=increment_counter()=2, result=22 + // Total: 11 + 22 = 33 + assert_eq!(result, 33); + + Ok(()) +} \ No newline at end of file diff --git a/tests/default_params.rs b/tests/default_params.rs new file mode 100644 index 000000000..603eb42b9 --- /dev/null +++ b/tests/default_params.rs @@ -0,0 +1,940 @@ +#![cfg(all(not(feature = "no_function"), feature = "default-parameters"))] +use rhai::{Engine, EvalAltResult, INT}; + +#[test] +fn test_default_params_basic() { + let engine = Engine::new(); + + // Basic default parameters + assert_eq!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1)").unwrap(), 6); + assert_eq!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, 5)").unwrap(), 9); + assert_eq!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, 5, 7)").unwrap(), 13); +} + +#[test] +fn test_default_params_named_args() { + let engine = Engine::new(); + + // Named arguments (must come after all positional args) + assert_eq!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, c = 10)").unwrap(), 13); + assert_eq!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, b = 5)").unwrap(), 9); + assert_eq!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, b = 5, c = 10)").unwrap(), 16); +} + +#[test] +fn test_default_params_mixed() { + let engine = Engine::new(); + + // Mix of positional and named arguments + // Note: positional args must come before named args + assert_eq!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, b = 5)").unwrap(), 9); + assert_eq!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, c = 10)").unwrap(), 13); + assert_eq!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, b = 5, c = 10)").unwrap(), 16); +} + +#[test] +fn test_default_params_all_defaults() { + let engine = Engine::new(); + + // All parameters have defaults + assert_eq!(engine.eval::("fn add(a = 1, b = 2, c = 3) { a + b + c } add()").unwrap(), 6); + assert_eq!(engine.eval::("fn add(a = 1, b = 2, c = 3) { a + b + c } add(10)").unwrap(), 15); + assert_eq!(engine.eval::("fn add(a = 1, b = 2, c = 3) { a + b + c } add(10, 20)").unwrap(), 33); + assert_eq!(engine.eval::("fn add(a = 1, b = 2, c = 3) { a + b + c } add(10, 20, 30)").unwrap(), 60); + assert_eq!(engine.eval::("fn add(a = 1, b = 2, c = 3) { a + b + c } add(c = 10)").unwrap(), 13); +} + +#[test] +fn test_default_params_no_defaults() { + let engine = Engine::new(); + + // No defaults - should work as before + assert_eq!(engine.eval::("fn add(a, b) { a + b } add(1, 2)").unwrap(), 3); +} + +#[test] +fn test_default_params_anonymous_functions() { + let engine = Engine::new(); + + // Anonymous functions with defaults - test basic functionality + // Note: Anonymous functions with defaults may have parsing restrictions + // Test with single default first + assert_eq!(engine.eval::("let f = |a, b = 2| { a + b }; f(1)").unwrap(), 3); + + // Test with all defaults + assert_eq!(engine.eval::("let f = |a = 1, b = 2| { a + b }; f()").unwrap(), 3); + + // Anonymous function with external variable (default is literal, external var used in body) + assert_eq!(engine.eval::("let x = 10; let f = |a, b = 5| { a + b + x }; f(1)").unwrap(), 16); +} + +#[cfg(not(feature = "no_object"))] +#[test] +fn test_default_params_method_calls() { + let engine = Engine::new(); + + // Method calls with defaults + assert_eq!(engine.eval::("fn add(n, m = 2) { this + n + m } let x = 10; x.add(5)").unwrap(), 17); + assert_eq!(engine.eval::("fn add(n, m = 2) { this + n + m } let x = 10; x.add(5, 3)").unwrap(), 18); + assert_eq!(engine.eval::("fn add(n, m = 2) { this + n + m } let x = 10; x.add(5, m = 10)").unwrap(), 25); +} + +#[test] +fn test_default_params_different_types() { + let engine = Engine::new(); + + // Different types for defaults + assert_eq!(engine.eval::("fn test(a = 42, b = true) { if b { a } else { 0 } } test()").unwrap(), 42); + assert_eq!(engine.eval::("fn test(a = 42, b = true) { if b { a } else { 0 } } test(10, false)").unwrap(), 0); + assert_eq!(engine.eval::("fn test(a = \"hello\", b = \"world\") { a + \" \" + b } test()").unwrap(), "hello world"); + assert_eq!(engine.eval::("fn test(a = \"hello\", b = \"world\") { a + \" \" + b } test(\"hi\")").unwrap(), "hi world"); +} + +#[test] +fn test_default_params_complex_expressions() { + let engine = Engine::new(); + + // Complex function bodies with defaults + assert_eq!(engine.eval::("fn calc(x, y = 10, z = 5) { let result = x * y; result + z } calc(3)").unwrap(), 35); + assert_eq!(engine.eval::("fn calc(x, y = 10, z = 5) { let result = x * y; result + z } calc(3, 2)").unwrap(), 11); + assert_eq!(engine.eval::("fn calc(x, y = 10, z = 5) { let result = x * y; result + z } calc(3, z = 1)").unwrap(), 31); +} + +#[test] +fn test_default_params_nested_calls() { + let engine = Engine::new(); + + // Nested function calls with defaults + assert_eq!(engine.eval::("fn add(a, b = 2) { a + b } fn mul(x, y = 3) { x * y } mul(add(1), 5)").unwrap(), 15); + assert_eq!(engine.eval::("fn add(a, b = 2) { a + b } fn mul(x, y = 3) { x * y } mul(add(1, b = 5), y = 10)").unwrap(), 60); +} + +#[test] +fn test_default_params_error_missing_required() { + let engine = Engine::new(); + + // Missing required arguments + assert!(engine.eval::("fn add(a, b = 2, c) { a + b + c } add(1)").is_err()); + assert!(engine.eval::("fn add(a, b, c = 3) { a + b + c } add(1)").is_err()); +} + +#[test] +fn test_default_params_error_too_many_args() { + let engine = Engine::new(); + + // Too many arguments + assert!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, 2, 3, 4)").is_err()); +} + +#[test] +fn test_default_params_error_duplicate_named() { + let engine = Engine::new(); + + // Duplicate named arguments + assert!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, b = 5, b = 10)").is_err()); +} + +#[test] +fn test_default_params_error_unknown_named() { + let engine = Engine::new(); + + // Unknown named argument + assert!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, d = 5)").is_err()); +} + +#[test] +fn test_default_params_error_both_positional_and_named() { + let engine = Engine::new(); + + // Argument provided both positionally and by name + assert!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, 5, b = 10)").is_err()); +} + +#[test] +fn test_default_params_error_positional_after_named() { + let engine = Engine::new(); + + // Positional argument after named argument + assert!(engine.eval::("fn add(a, b = 2, c = 3) { a + b + c } add(1, b = 5, 10)").is_err()); +} + +#[test] +fn test_default_params_valid_expressions() { + let engine = Engine::new(); + + // Expressions are now valid as default values + assert_eq!(engine.eval::("fn add(a, b = 1 + 1) { a + b } add(1)").unwrap(), 3); + + // Reference to earlier parameter + assert_eq!(engine.eval::("fn add(a, b = a * 2) { a + b } add(5)").unwrap(), 15); + + // Function call in default (with defined function) + assert_eq!( + engine.eval::("fn double(x) { x * 2 } fn add(a, b = double(a)) { a + b } add(3)").unwrap(), + 9 + ); + + // Undefined variable should still error + assert!(engine.eval::("fn add(a, b = undefined_var) { a + b } add(1)").is_err()); +} + +#[test] +fn test_default_params_valid_literals() { + let engine = Engine::new(); + + // Valid literals as defaults + assert_eq!(engine.eval::("fn test(a = 42) { a } test()").unwrap(), 42); + assert_eq!(engine.eval::("fn test(a = true) { a } test()").unwrap(), true); + assert_eq!(engine.eval::("fn test(a = \"hello\") { a } test()").unwrap(), "hello"); + #[cfg(not(feature = "no_float"))] + assert_eq!(engine.eval::("fn test(a = 3.14) { a } test()").unwrap(), 3.14); +} + +#[test] +fn test_default_params_closure_capture() { + let engine = Engine::new(); + + // Closures with defaults - external variables can't be used as defaults + // So we test with a literal default, but external var can be used in body + let result = engine + .eval::( + " + let x = 10; + let f = |a, b = 5| { a + b + x }; + f(5) + ", + ) + .unwrap(); + assert_eq!(result, 20); +} + +#[test] +fn test_default_params_recursive() { + let engine = Engine::new(); + + // Recursive functions with defaults - use simpler recursion to avoid stack overflow + assert_eq!( + engine + .eval::( + " + fn fact(n, acc = 1) { + if n <= 1 { acc } else { fact(n - 1, n * acc) } + } + fact(5) + " + ) + .unwrap(), + 120 + ); +} + +#[test] +fn test_default_params_multiple_functions() { + let engine = Engine::new(); + + // Multiple functions with defaults + assert_eq!( + engine + .eval::( + " + fn add(a, b = 2) { a + b } + fn mul(x, y = 3) { x * y } + fn sub(p, q = 1) { p - q } + add(1) + mul(2) + sub(10) + " + ) + .unwrap(), + 18 + ); +} + +#[test] +fn test_default_params_function_overloading() { + let engine = Engine::new(); + + // Functions with defaults can be called with different argument counts + // process(1) + process(1, 2) + process(1, 2, 3) = (1+10+20) + (1+2+20) + (1+2+3) = 31 + 23 + 6 = 60 + assert_eq!( + engine + .eval::( + " + fn process(x, y = 10, z = 20) { x + y + z } + process(1) + process(1, 2) + process(1, 2, 3) + " + ) + .unwrap(), + 60 + ); +} + +#[test] +fn test_default_params_early_return() { + let engine = Engine::new(); + + // Functions with defaults and early returns + assert_eq!( + engine + .eval::( + " + fn check(x, threshold = 10) { + if x < threshold { return 0; } + x * 2 + } + check(5) + check(15) + " + ) + .unwrap(), + 30 + ); +} + +#[test] +fn test_default_params_side_effects() { + let engine = Engine::new(); + + // Defaults should be evaluated once (they're literals, so no side effects) + // But let's test that the function works correctly + assert_eq!( + engine + .eval::( + " + let counter = 0; + fn test(x, y = 42) { x + y } + let result = test(1); + counter = 5; + result + test(2) + " + ) + .unwrap(), + 87 + ); +} + +#[test] +fn test_default_params_string_concatenation() { + let engine = Engine::new(); + + // String operations with defaults + assert_eq!( + engine + .eval::( + " + fn greet(name, prefix = \"Hello\", suffix = \"!\") { + prefix + \", \" + name + suffix + } + greet(\"World\") + " + ) + .unwrap(), + "Hello, World!" + ); + assert_eq!( + engine + .eval::( + " + fn greet(name, prefix = \"Hello\", suffix = \"!\") { + prefix + \", \" + name + suffix + } + greet(\"World\", \"Hi\") + " + ) + .unwrap(), + "Hi, World!" + ); + assert_eq!( + engine + .eval::( + " + fn greet(name, prefix = \"Hello\", suffix = \"!\") { + prefix + \", \" + name + suffix + } + greet(\"World\", suffix = \"?\")\n" + ) + .unwrap(), + "Hello, World?" + ); +} + +#[test] +fn test_default_params_boolean_logic() { + let engine = Engine::new(); + + // Boolean operations with defaults + assert_eq!( + engine + .eval::( + " + fn and(a, b = true) { a && b } + and(true) && and(false) == false && and(true, false) == false + " + ) + .unwrap(), + true + ); +} + +#[test] +fn test_default_params_array_operations() { + #[cfg(not(feature = "no_index"))] + { + let engine = Engine::new(); + + // Array operations with defaults + assert_eq!( + engine + .eval::( + " + fn get(arr, idx = 0) { arr[idx] } + let a = [1, 2, 3]; + get(a) + get(a, 1) + " + ) + .unwrap(), + 3 + ); + } +} + +#[test] +fn test_default_params_map_operations() { + #[cfg(not(feature = "no_object"))] + { + let engine = Engine::new(); + + // Map operations with defaults + assert_eq!( + engine + .eval::( + " + fn get(map, key, def_val = 0) { + if key in map { map[key] } else { def_val } + } + let m = #{a: 1, b: 2}; + get(m, \"a\") + get(m, \"c\") + " + ) + .unwrap(), + 1 + ); + } +} + +#[test] +fn test_default_params_conditional_defaults() { + let engine = Engine::new(); + + // Using defaults in conditional logic + assert_eq!( + engine + .eval::( + " + fn max(a, b = 0) { + if a > b { a } else { b } + } + max(5) + max(3, 10) + " + ) + .unwrap(), + 15 + ); +} + +#[test] +fn test_default_params_loop_with_defaults() { + let engine = Engine::new(); + + // Loops using functions with defaults + // 0+1 + 1+1 + 2+1 + 3+1 + 4+1 = 1+2+3+4+5 = 15 + assert_eq!( + engine + .eval::( + " + fn add(x, y = 1) { x + y } + let sum = 0; + for i in 0..5 { + sum += add(i); + } + sum + " + ) + .unwrap(), + 15 + ); +} + +#[test] +fn test_default_params_switch_with_defaults() { + let engine = Engine::new(); + + // Switch statements using functions with defaults + assert_eq!( + engine + .eval::( + " + fn get_value(x, def_val = 0) { + switch x { + 1 => 10, + 2 => 20, + _ => def_val + } + } + get_value(1) + get_value(3) + get_value(3, 99) + " + ) + .unwrap(), + 109 + ); +} + +#[test] +fn test_default_params_module_functions() { + let engine = Engine::new(); + + // Functions in modules with defaults + let result = engine + .eval::( + " + fn mod_func(x, y = 10) { x * y } + mod_func(5) + ", + ) + .unwrap(); + assert_eq!(result, 50); +} + +#[test] +fn test_default_params_complex_nesting() { + let engine = Engine::new(); + + // Complex nesting of function calls with defaults + // sub(mul(add(1), 3), 2) = sub(mul(2, 3), 2) = sub(6, 2) = 4 + assert_eq!( + engine + .eval::( + " + fn add(a, b = 1) { a + b } + fn mul(x, y = 2) { x * y } + fn sub(p, q = 1) { p - q } + sub(mul(add(1), 3), 2) + " + ) + .unwrap(), + 4 + ); +} + +#[test] +fn test_default_params_variable_arguments() { + let engine = Engine::new(); + + // Using variables as arguments with defaults + assert_eq!( + engine + .eval::( + " + fn calc(x, y = 10) { x + y } + let a = 5; + let b = 20; + calc(a) + calc(b, 30) + " + ) + .unwrap(), + 65 + ); +} + +#[test] +fn test_default_params_all_named_args() { + let engine = Engine::new(); + + // All arguments provided by name (first arg is required, so must be positional or named) + // Note: Currently parser may not support multiple named args - test single + assert_eq!( + engine + .eval::( + " + fn add(a, b = 2, c = 3) { a + b + c } + add(1, b = 5) + " + ) + .unwrap(), + 9 + ); +} + +#[test] +fn test_default_params_named_args_different_order() { + let engine = Engine::new(); + + // Named arguments in different order (first required arg must be positional) + // Note: Currently parser may not support multiple named args - test single + assert_eq!( + engine + .eval::( + " + fn add(a, b = 2, c = 3) { a + b + c } + add(1, c = 10) + " + ) + .unwrap(), + 13 + ); +} + +#[test] +fn test_default_params_only_named_after_positional() { + let engine = Engine::new(); + + // Only named arguments after positional + assert_eq!( + engine + .eval::( + " + fn add(a, b = 2, c = 3, d = 4) { a + b + c + d } + add(1, d = 10) + " + ) + .unwrap(), + 16 + ); +} + +#[test] +fn test_overload_precedence_default_first() { + let engine = Engine::new(); + + // When a function with defaults is declared first, followed by an overload, + // the overload should take precedence for exact arity matches + assert_eq!( + engine + .eval::( + " + fn foo(x, y, z = 1) { 100 } + fn foo(x, y) { 200 } + foo(1, 2) + " + ) + .unwrap(), + 200 + ); + + // But calling with 3 args or named arg should use the default version + assert_eq!( + engine + .eval::( + " + fn foo(x, y, z = 1) { 100 } + fn foo(x, y) { 200 } + foo(1, 2, 3) + " + ) + .unwrap(), + 100 + ); + + assert_eq!( + engine + .eval::( + " + fn foo(x, y, z = 1) { 100 } + fn foo(x, y) { 200 } + foo(1, 2, z = 3) + " + ) + .unwrap(), + 100 + ); +} + +#[test] +fn test_overload_precedence_overload_first() { + let engine = Engine::new(); + + // When an overload is declared first, followed by a function with defaults, + // the overload should still take precedence for exact arity matches + assert_eq!( + engine + .eval::( + " + fn foo(x, y) { 200 } + fn foo(x, y, z = 1) { 100 } + foo(1, 2) + " + ) + .unwrap(), + 200 + ); + + // But calling with 3 args or named arg should use the default version + assert_eq!( + engine + .eval::( + " + fn foo(x, y) { 200 } + fn foo(x, y, z = 1) { 100 } + foo(1, 2, 3) + " + ) + .unwrap(), + 100 + ); + + assert_eq!( + engine + .eval::( + " + fn foo(x, y) { 200 } + fn foo(x, y, z = 1) { 100 } + foo(1, 2, z = 3) + " + ) + .unwrap(), + 100 + ); +} + +#[test] +fn test_overload_precedence_string_return() { + let engine = Engine::new(); + + // Test with string returns to match the bug.rhai example + assert_eq!( + engine + .eval::( + " + fn foo(x, y, z = 1) { \"first\" } + fn foo(x, y) { \"second\" } + foo(1, 2) + " + ) + .unwrap(), + "second" + ); + + assert_eq!( + engine + .eval::( + " + fn foo(x, y, z = 1) { \"first\" } + fn foo(x, y) { \"second\" } + foo(1, 2, 3) + " + ) + .unwrap(), + "first" + ); + + assert_eq!( + engine + .eval::( + " + fn foo(x, y, z = 1) { \"first\" } + fn foo(x, y) { \"second\" } + foo(1, 2, z = 3) + " + ) + .unwrap(), + "first" + ); +} + +#[test] +fn test_overload_precedence_multiple_overloads() { + let engine = Engine::new(); + + // Test with multiple overloads at different arities + assert_eq!( + engine + .eval::( + " + fn calc(x) { 1 } + fn calc(x, y) { 2 } + fn calc(x, y, z = 10, w = 20) { 3 } + calc(1) + " + ) + .unwrap(), + 1 + ); + + assert_eq!( + engine + .eval::( + " + fn calc(x) { 1 } + fn calc(x, y) { 2 } + fn calc(x, y, z = 10, w = 20) { 3 } + calc(1, 2) + " + ) + .unwrap(), + 2 + ); + + assert_eq!( + engine + .eval::( + " + fn calc(x) { 1 } + fn calc(x, y) { 2 } + fn calc(x, y, z = 10, w = 20) { 3 } + calc(1, 2, 3) + " + ) + .unwrap(), + 3 + ); + + assert_eq!( + engine + .eval::( + " + fn calc(x) { 1 } + fn calc(x, y) { 2 } + fn calc(x, y, z = 10, w = 20) { 3 } + calc(1, 2, 3, 4) + " + ) + .unwrap(), + 3 + ); +} + +#[test] +fn test_default_params_qualified_names() { + let engine = Engine::new(); + + // Test with qualified names (this test doesn't actually use qualified names, just named args) + assert_eq!( + engine.eval::("fn add(a, b = 2) { a + b } add(1, b = 5)").unwrap(), + 6 + ); +} + +#[cfg(not(feature = "no_module"))] +#[test] +fn test_default_params_module_qualified_name() { + use rhai::{Module, Scope}; + + let mut engine = Engine::new(); + + // Create a module with a function that has a default parameter + let ast = engine.compile("fn add(a, b = 10) { a + b }").unwrap(); + let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine).unwrap(); + engine.register_static_module("mymod", module.into()); + + // Test calling function with qualified name lookup, with a default parameter + let result = engine.eval::("mymod::add(1)").unwrap(); + assert_eq!(result, 11); + + // Test with both parameters provided + let result2 = engine.eval::("mymod::add(1, 2)").unwrap(); + assert_eq!(result2, 3); + + // Test qualified call with named argument + let result3 = engine.eval::("mymod::add(1, b = 20)").unwrap(); + assert_eq!(result3, 21); +} + +#[cfg(not(feature = "no_module"))] +#[test] +fn test_default_params_qualified_multiple_defaults() { + use rhai::{Module, Scope}; + + let mut engine = Engine::new(); + + // Create a module with a function that has multiple defaults + let ast = engine.compile("fn add(a, b = 10, c = 20) { a + b + c }").unwrap(); + let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine).unwrap(); + engine.register_static_module("mymod", module.into()); + + // Test qualified call with multiple defaults + let result = engine.eval::("mymod::add(1)").unwrap(); + assert_eq!(result, 31); + + // Test with one positional arg + let result2 = engine.eval::("mymod::add(1, 5)").unwrap(); + assert_eq!(result2, 26); + + // Test with all positional args + let result3 = engine.eval::("mymod::add(1, 5, 15)").unwrap(); + assert_eq!(result3, 21); +} + +#[cfg(not(feature = "no_module"))] +#[test] +fn test_default_params_qualified_named_args() { + use rhai::{Module, Scope}; + + let mut engine = Engine::new(); + + // Create a module with a function that has defaults + let ast = engine.compile("fn add(a, b = 10, c = 20) { a + b + c }").unwrap(); + let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine).unwrap(); + engine.register_static_module("mymod", module.into()); + + // Test qualified call with single named argument + let result = engine.eval::("mymod::add(1, c = 30)").unwrap(); + assert_eq!(result, 41); + + // Test with multiple named arguments + let result2 = engine.eval::("mymod::add(1, b = 15, c = 25)").unwrap(); + assert_eq!(result2, 41); + + // Test with mixed positional and named arguments + let result3 = engine.eval::("mymod::add(1, 5, c = 25)").unwrap(); + assert_eq!(result3, 31); +} + +#[cfg(not(feature = "no_module"))] +#[test] +fn test_default_params_qualified_all_defaults() { + use rhai::{Module, Scope}; + + let mut engine = Engine::new(); + + // Create a module with a function that has all defaults + let ast = engine.compile("fn add(a = 1, b = 2, c = 3) { a + b + c }").unwrap(); + let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine).unwrap(); + engine.register_static_module("mymod", module.into()); + + // Test qualified call with all defaults + let result = engine.eval::("mymod::add()").unwrap(); + assert_eq!(result, 6); + + // Test with named argument when all have defaults + let result2 = engine.eval::("mymod::add(c = 10)").unwrap(); + assert_eq!(result2, 13); +} + +#[cfg(not(feature = "no_module"))] +#[test] +fn test_default_params_qualified_errors() { + use rhai::{Module, Scope}; + + let mut engine = Engine::new(); + + // Create a module with a function + let ast = engine.compile("fn add(a, b = 10) { a + b }").unwrap(); + let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine).unwrap(); + engine.register_static_module("mymod", module.into()); + + // Test error: unknown named argument + assert!(engine.eval::("mymod::add(1, c = 20)").is_err()); + + // Create another module with a function without defaults + let ast2 = engine.compile("fn add(a, b) { a + b }").unwrap(); + let module2 = Module::eval_ast_as_new(Scope::new(), &ast2, &engine).unwrap(); + engine.register_static_module("mymod2", module2.into()); + + // Test error: missing required argument + assert!(engine.eval::("mymod2::add(1)").is_err()); + + // Create another module for duplicate test + let ast3 = engine.compile("fn add(a, b = 10, c = 20) { a + b + c }").unwrap(); + let module3 = Module::eval_ast_as_new(Scope::new(), &ast3, &engine).unwrap(); + engine.register_static_module("mymod3", module3.into()); + + // Test error: duplicate named argument + assert!(engine.eval::("mymod3::add(1, b = 5, b = 10)").is_err()); +}