use indexmap::IndexMap;
use rustpython_parser::ast::{self, Ranged};
use super::{
builtins::try_builtin,
definitions::{
VariableCheckpoint, apply_function_scope, apply_lambda_scope, contains_yield_stmts,
writeback_nonlocal_cell,
},
helpers::{bytes_fromhex, dict_fromkeys},
method_dispatch::{CallArgs, dispatch_method},
params::{bind_params, execute_body},
};
use crate::{
error::{ControlFlow, EvalError, EvalResult, InterpreterError},
state::InterpreterState,
tools::Tools,
value::{ExceptionValue, FunctionDef, LambdaDef, Value},
};
pub(crate) async fn call_user_function(
state: &mut InterpreterState,
func_def: &FunctionDef,
args: &[Value],
kwargs: &IndexMap<String, Value>,
tools: &Tools,
) -> EvalResult {
state.enter_call().map_err(EvalError::Interpreter)?;
if args.is_empty()
&& kwargs.is_empty()
&& func_def.params.args.is_empty()
&& func_def.params.vararg.is_none()
&& func_def.params.kwonlyargs.is_empty()
&& func_def.params.kwarg.is_none()
&& func_def.nonlocal_names.is_empty()
&& func_def.assigned_names.is_empty()
&& func_def.global_names.is_empty()
&& !func_def.is_generator
{
let func_name = func_def.name.clone();
let body = state.function_bodies.get(&func_name).cloned();
if let Some(body_stmts) = body.as_ref() {
if let Some(direct) = try_eval_trivial_body(body_stmts) {
state.exit_call();
return Ok(direct);
}
}
state.body_source_stack.push(func_def.source.clone());
let outcome = if let Some(body_stmts) = body {
match execute_body(state, body_stmts.as_slice(), tools).await {
Ok(v) => Ok(v),
Err(EvalError::Signal(ControlFlow::Return(v))) => Ok(*v),
Err(e) => Err(e),
}
} else {
Ok(Value::None)
};
state.body_source_stack.pop();
state.exit_call();
return outcome;
}
state.frame_cell_owners.push(rustc_hash::FxHashMap::default());
let bind_outcome = bind_params(&func_def.params, args, kwargs, state, tools).await;
let local_scope = match bind_outcome {
Ok(s) => s,
Err(e) => {
state.frame_cell_owners.pop();
state.exit_call();
return Err(e);
}
};
let closure_touched = func_def.closure.iter().filter(|(name, value)| {
if func_def.is_module_level {
return false;
}
!state.variables.get(*name).is_some_and(|live| live == *value)
});
let touched: Vec<String> = func_def
.params
.args
.iter()
.map(|p| p.name.clone())
.chain(func_def.params.vararg.iter().cloned())
.chain(func_def.params.kwonlyargs.iter().map(|p| p.name.clone()))
.chain(func_def.params.kwarg.iter().cloned())
.chain(closure_touched.map(|(name, _)| name.clone()))
.chain(func_def.assigned_names.iter().cloned())
.filter(|n| !func_def.global_names.contains(n) && !func_def.nonlocal_names.contains(n))
.collect();
let checkpoint = VariableCheckpoint::capture(state, touched);
if let Err(e) = apply_function_scope(state, func_def, &local_scope) {
checkpoint.restore(state);
state.frame_cell_owners.pop();
state.exit_call();
return Err(e);
}
let func_name = func_def.name.clone();
let body = state.function_bodies.get(&func_name).cloned();
state.body_source_stack.push(func_def.source.clone());
let is_generator =
func_def.is_generator || body.as_ref().is_some_and(|stmts| contains_yield_stmts(stmts));
let exec_result = if let Some(body_stmts) = body {
if is_generator {
state.yield_stack.push(Vec::new());
let body_result = execute_body(state, body_stmts.as_slice(), tools).await;
let collected = state.yield_stack.pop().unwrap_or_default();
match body_result {
Ok(_) | Err(EvalError::Signal(ControlFlow::Return(_))) => {
let cursor_id = state.next_cursor_id;
state.next_cursor_id = state.next_cursor_id.wrapping_add(1);
state.lazy_cursors.insert(cursor_id, 0);
Ok(Value::Lazy { items: collected, cursor_id })
}
Err(e) => Err(e),
}
} else {
execute_body(state, body_stmts.as_slice(), tools).await
}
} else {
Ok(Value::None)
};
writeback_nonlocal_cell(state, func_def);
checkpoint.restore(state);
state.frame_cell_owners.pop();
state.exit_call();
state.body_source_stack.pop();
match exec_result {
Ok(val) => Ok(val),
Err(EvalError::Signal(ControlFlow::Return(val))) => Ok(*val),
Err(e) => Err(e),
}
}
fn try_eval_trivial_body(body: &[ast::Stmt]) -> Option<Value> {
if body.len() != 1 {
return None;
}
match &body[0] {
ast::Stmt::Pass(_) => Some(Value::None),
ast::Stmt::Return(node) => {
node.value.as_ref().map_or(Some(Value::None), |expr| match expr.as_ref() {
ast::Expr::Constant(c) => Some(crate::eval::literals::eval_constant(&c.value)),
_ => None,
})
}
_ => None,
}
}
pub(crate) async fn call_lambda(
state: &mut InterpreterState,
lambda_def: &LambdaDef,
args: &[Value],
kwargs: &IndexMap<String, Value>,
tools: &Tools,
) -> EvalResult {
state.enter_call().map_err(EvalError::Interpreter)?;
let bind_outcome = bind_params(&lambda_def.params, args, kwargs, state, tools).await;
let local_scope = match bind_outcome {
Ok(s) => s,
Err(e) => {
state.exit_call();
return Err(e);
}
};
let closure_touched = lambda_def.closure.iter().filter(|(name, value)| {
if lambda_def.is_module_level {
return false;
}
!state.variables.get(*name).is_some_and(|live| live == *value)
});
let touched: Vec<String> = lambda_def
.params
.args
.iter()
.map(|p| p.name.clone())
.chain(lambda_def.params.vararg.iter().cloned())
.chain(lambda_def.params.kwonlyargs.iter().map(|p| p.name.clone()))
.chain(lambda_def.params.kwarg.iter().cloned())
.chain(closure_touched.map(|(name, _)| name.clone()))
.chain(lambda_def.assigned_names.iter().cloned())
.collect();
let checkpoint = VariableCheckpoint::capture(state, touched);
if let Err(e) = apply_lambda_scope(state, lambda_def, &local_scope) {
checkpoint.restore(state);
state.exit_call();
return Err(e);
}
let body = state.lambda_bodies.get(&lambda_def.lambda_id).cloned();
state.body_source_stack.push(lambda_def.source.clone());
let result = if let Some(body_expr) = body {
let body_line =
crate::eval::line_of(&lambda_def.source, body_expr.range().start().to_usize());
crate::eval::eval_expr(state, &body_expr, tools)
.await
.map_err(|e| crate::eval::stamp_line(e, body_line))
} else {
Ok(Value::None)
};
state.body_source_stack.pop();
checkpoint.restore(state);
state.exit_call();
result
}
pub(crate) async fn call_value_as_function(
state: &mut InterpreterState,
func: &Value,
args: &[Value],
tools: &Tools,
) -> EvalResult {
match func {
Value::Function(func_def) => {
call_user_function(state, func_def, args, &IndexMap::new(), tools).await
}
Value::Lambda(lambda_def) => {
call_lambda(state, lambda_def, args, &IndexMap::new(), tools).await
}
Value::BoundMethod { receiver, method } => match receiver {
crate::value::BoundMethodReceiver::Snapshot(value) => {
let mut recv = (**value).clone();
Ok(dispatch_method(&mut recv, method, args)?.value)
}
crate::value::BoundMethodReceiver::Place { root, steps } => {
use crate::{
eval::place::{PlaceStep, apply_mem_delta, with_navigate_mut},
value::BoundMethodStep,
};
let pl_steps: Vec<PlaceStep> = steps
.iter()
.map(|s| match s {
BoundMethodStep::Index(v) => PlaceStep::Index(v.clone()),
BoundMethodStep::Attr(n) => PlaceStep::Attr(n.clone()),
})
.collect();
let outcome = {
let root_slot = state
.variables
.get_mut(root)
.ok_or_else(|| EvalError::from(InterpreterError::name_not_defined(root)))?;
with_navigate_mut(root_slot, &pl_steps, |target| {
dispatch_method(target, method, args)
})??
};
apply_mem_delta(state, outcome.mem_delta)?;
Ok(outcome.value)
}
},
Value::BuiltinTypeMethod { type_name, method } => {
if type_name == "dict" && method == "fromkeys" {
return dict_fromkeys(state, args, tools).await;
}
if (type_name == "bytes" || type_name == "bytearray") && method == "fromhex" {
return bytes_fromhex(args);
}
let Some((recv_arg, rest)) = args.split_first() else {
return Err(InterpreterError::TypeError(format!(
"unbound method {type_name}.{method}() needs a {type_name} as first argument"
))
.into());
};
let mut recv = recv_arg.clone();
Ok(dispatch_method(&mut recv, method, rest)?.value)
}
Value::ModuleFunction { module, name } => {
crate::eval::modules::call_function(state, module, name, args, &IndexMap::new(), tools)
.await
}
Value::BuiltinName(builtin_name) => {
let kwargs = IndexMap::new();
Box::pin(try_builtin(state, builtin_name, args, &kwargs, tools)).await?.ok_or_else(
|| InterpreterError::TypeError(format!("'{builtin_name}' is not callable")).into(),
)
}
Value::ToolName(tool_name) => crate::tools::resolver::resolve_and_dispatch(
state,
crate::tools::resolver::ToolCallDescriptor {
name: tool_name,
args,
kwargs: &IndexMap::new(),
},
tools,
)
.await?
.ok_or_else(|| {
InterpreterError::TypeError(format!("'{tool_name}' is not callable")).into()
}),
Value::UnboundClassMethod { class, method } => {
let Some(def) = crate::eval::classes::lookup_class_method(state, class, method) else {
return Err(InterpreterError::AttributeError(format!(
"type object '{class}' has no classmethod '{method}'"
))
.into());
};
let kwargs = IndexMap::new();
let call = CallArgs { positional: args, keyword: &kwargs };
let (returned, _self) = crate::eval::classes::call_method(
state,
&def,
Value::Class(class.clone()),
call,
tools,
)
.await?;
Ok(returned)
}
Value::ExceptionType(type_name) => {
let message = match args.len() {
0 => String::new(),
1 => format!("{}", args[0]),
_ => args.iter().map(|v| format!("{v}")).collect::<Vec<_>>().join(", "),
};
Ok(Value::Exception(
ExceptionValue::new(type_name.clone(), message).with_args(args.to_vec()),
))
}
Value::Partial(data) => {
let bound_args = &data.args;
let target = &data.func;
let mut combined: Vec<Value> = Vec::with_capacity(bound_args.len() + args.len());
combined.extend(bound_args.iter().cloned());
combined.extend_from_slice(args);
return Box::pin(call_value_as_function(state, target, &combined, tools)).await;
}
Value::Instance(inst) => {
let class_name = inst.class_name.clone();
if let Some((_, method)) =
crate::eval::classes::lookup_method_in_mro(state, &class_name, "__call__")
{
let kwargs = IndexMap::new();
let call = CallArgs { positional: args, keyword: &kwargs };
let (returned, _self) =
crate::eval::classes::call_method(state, &method, func.clone(), call, tools)
.await?;
return Ok(returned);
}
Err(InterpreterError::TypeError(format!("'{class_name}' object is not callable"))
.into())
}
Value::None => {
Err(InterpreterError::TypeError("'NoneType' object is not callable".into()).into())
}
_ => Err(InterpreterError::TypeError(format!(
"'{}' object is not callable",
func.type_name()
))
.into()),
}
}