Skip to main content

rustpython_vm/protocol/
callable.rs

1use crate::{
2    builtins::{PyBoundMethod, PyFunction},
3    function::{FuncArgs, IntoFuncArgs},
4    types::{GenericMethod, VectorCallFunc},
5    {PyObject, PyObjectRef, PyResult, VirtualMachine},
6};
7
8impl PyObject {
9    #[inline]
10    pub fn to_callable(&self) -> Option<PyCallable<'_>> {
11        PyCallable::new(self)
12    }
13
14    #[inline]
15    pub fn is_callable(&self) -> bool {
16        self.to_callable().is_some()
17    }
18
19    /// PyObject_Call*Arg* series
20    #[inline]
21    pub fn call(&self, args: impl IntoFuncArgs, vm: &VirtualMachine) -> PyResult {
22        let args = args.into_args(vm);
23        self.call_with_args(args, vm)
24    }
25
26    /// PyObject_Call
27    pub fn call_with_args(&self, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
28        let Some(callable) = self.to_callable() else {
29            return Err(
30                vm.new_type_error(format!("'{}' object is not callable", self.class().name()))
31            );
32        };
33        vm_trace!("Invoke: {:?} {:?}", callable, args);
34        callable.invoke(args, vm)
35    }
36
37    /// Vectorcall: call with owned positional args + optional kwnames.
38    /// Falls back to FuncArgs-based call if no vectorcall slot.
39    #[inline]
40    pub fn vectorcall(
41        &self,
42        args: Vec<PyObjectRef>,
43        nargs: usize,
44        kwnames: Option<&[PyObjectRef]>,
45        vm: &VirtualMachine,
46    ) -> PyResult {
47        let Some(callable) = self.to_callable() else {
48            return Err(
49                vm.new_type_error(format!("'{}' object is not callable", self.class().name()))
50            );
51        };
52        callable.invoke_vectorcall(args, nargs, kwnames, vm)
53    }
54}
55
56#[derive(Debug)]
57pub struct PyCallable<'a> {
58    pub obj: &'a PyObject,
59    pub call: GenericMethod,
60    pub vectorcall: Option<VectorCallFunc>,
61}
62
63impl<'a> PyCallable<'a> {
64    pub fn new(obj: &'a PyObject) -> Option<Self> {
65        let slots = &obj.class().slots;
66        let call = slots.call.load()?;
67        let vectorcall = slots.vectorcall.load();
68        Some(PyCallable {
69            obj,
70            call,
71            vectorcall,
72        })
73    }
74
75    pub fn invoke(&self, args: impl IntoFuncArgs, vm: &VirtualMachine) -> PyResult {
76        let args = args.into_args(vm);
77        if !vm.use_tracing.get() {
78            return (self.call)(self.obj, args, vm);
79        }
80        // Python functions get 'call'/'return' events from with_frame().
81        // Bound methods delegate to the inner callable, which fires its own events.
82        // All other callables (built-in functions, etc.) get 'c_call'/'c_return'/'c_exception'.
83        let is_python_callable = self.obj.downcast_ref::<PyFunction>().is_some()
84            || self.obj.downcast_ref::<PyBoundMethod>().is_some();
85        if is_python_callable {
86            (self.call)(self.obj, args, vm)
87        } else {
88            let callable = self.obj.to_owned();
89            vm.trace_event(TraceEvent::CCall, Some(callable.clone()))?;
90            let result = (self.call)(self.obj, args, vm);
91            if result.is_ok() {
92                vm.trace_event(TraceEvent::CReturn, Some(callable))?;
93            } else {
94                let _ = vm.trace_event(TraceEvent::CException, Some(callable));
95            }
96            result
97        }
98    }
99
100    /// Vectorcall dispatch: use vectorcall slot if available, else fall back to FuncArgs.
101    #[inline]
102    pub fn invoke_vectorcall(
103        &self,
104        args: Vec<PyObjectRef>,
105        nargs: usize,
106        kwnames: Option<&[PyObjectRef]>,
107        vm: &VirtualMachine,
108    ) -> PyResult {
109        if let Some(vc) = self.vectorcall {
110            if !vm.use_tracing.get() {
111                return vc(self.obj, args, nargs, kwnames, vm);
112            }
113            let is_python_callable = self.obj.downcast_ref::<PyFunction>().is_some()
114                || self.obj.downcast_ref::<PyBoundMethod>().is_some();
115            if is_python_callable {
116                vc(self.obj, args, nargs, kwnames, vm)
117            } else {
118                let callable = self.obj.to_owned();
119                vm.trace_event(TraceEvent::CCall, Some(callable.clone()))?;
120                let result = vc(self.obj, args, nargs, kwnames, vm);
121                if result.is_ok() {
122                    vm.trace_event(TraceEvent::CReturn, Some(callable))?;
123                } else {
124                    let _ = vm.trace_event(TraceEvent::CException, Some(callable));
125                }
126                result
127            }
128        } else {
129            // Fallback: convert owned Vec to FuncArgs (move, no clone)
130            let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
131            self.invoke(func_args, vm)
132        }
133    }
134}
135
136/// Trace events for sys.settrace and sys.setprofile.
137pub(crate) enum TraceEvent {
138    Call,
139    Return,
140    Exception,
141    Line,
142    Opcode,
143    CCall,
144    CReturn,
145    CException,
146}
147
148impl TraceEvent {
149    /// Whether sys.settrace receives this event.
150    fn is_trace_event(&self) -> bool {
151        matches!(
152            self,
153            Self::Call | Self::Return | Self::Exception | Self::Line | Self::Opcode
154        )
155    }
156
157    /// Whether sys.setprofile receives this event.
158    /// In legacy_tracing.c, profile callbacks are only registered for
159    /// PY_RETURN, PY_UNWIND, C_CALL, C_RETURN, C_RAISE.
160    fn is_profile_event(&self) -> bool {
161        matches!(
162            self,
163            Self::Call | Self::Return | Self::CCall | Self::CReturn | Self::CException
164        )
165    }
166
167    /// Whether this event is dispatched only when f_trace_opcodes is set.
168    pub(crate) fn is_opcode_event(&self) -> bool {
169        matches!(self, Self::Opcode)
170    }
171}
172
173impl core::fmt::Display for TraceEvent {
174    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
175        use TraceEvent::*;
176        match self {
177            Call => write!(f, "call"),
178            Return => write!(f, "return"),
179            Exception => write!(f, "exception"),
180            Line => write!(f, "line"),
181            Opcode => write!(f, "opcode"),
182            CCall => write!(f, "c_call"),
183            CReturn => write!(f, "c_return"),
184            CException => write!(f, "c_exception"),
185        }
186    }
187}
188
189impl VirtualMachine {
190    /// Call registered trace function.
191    ///
192    /// Returns the trace function's return value:
193    /// - `Some(obj)` if the trace function returned a non-None value
194    /// - `None` if it returned Python None or no trace function was active
195    ///
196    /// In CPython's trace protocol:
197    /// - For 'call' events: the return value determines the per-frame `f_trace`
198    /// - For 'line'/'return' events: the return value can update `f_trace`
199    #[inline]
200    pub(crate) fn trace_event(
201        &self,
202        event: TraceEvent,
203        arg: Option<PyObjectRef>,
204    ) -> PyResult<Option<PyObjectRef>> {
205        if self.use_tracing.get() {
206            self._trace_event_inner(event, arg)
207        } else {
208            Ok(None)
209        }
210    }
211    fn _trace_event_inner(
212        &self,
213        event: TraceEvent,
214        arg: Option<PyObjectRef>,
215    ) -> PyResult<Option<PyObjectRef>> {
216        let trace_func = self.trace_func.borrow().to_owned();
217        let profile_func = self.profile_func.borrow().to_owned();
218        if self.is_none(&trace_func) && self.is_none(&profile_func) {
219            return Ok(None);
220        }
221
222        let is_trace_event = event.is_trace_event();
223        let is_profile_event = event.is_profile_event();
224        let is_opcode_event = event.is_opcode_event();
225
226        let Some(frame_ref) = self.current_frame() else {
227            return Ok(None);
228        };
229
230        // Opcode events are only dispatched when f_trace_opcodes is set.
231        if is_opcode_event && !*frame_ref.trace_opcodes.lock() {
232            return Ok(None);
233        }
234
235        let frame: PyObjectRef = frame_ref.into();
236        let event = self.ctx.new_str(event.to_string()).into();
237        let args = vec![frame, event, arg.unwrap_or_else(|| self.ctx.none())];
238
239        let mut trace_result = None;
240
241        // temporarily disable tracing, during the call to the
242        // tracing function itself.
243        if is_trace_event && !self.is_none(&trace_func) {
244            self.use_tracing.set(false);
245            let res = trace_func.call(args.clone(), self);
246            self.use_tracing.set(true);
247            match res {
248                Ok(result) => {
249                    if !self.is_none(&result) {
250                        trace_result = Some(result);
251                    }
252                }
253                Err(e) => {
254                    // trace_trampoline behavior: clear per-frame f_trace
255                    // and propagate the error.
256                    if let Some(frame_ref) = self.current_frame() {
257                        *frame_ref.trace.lock() = self.ctx.none();
258                    }
259                    return Err(e);
260                }
261            }
262        }
263
264        if is_profile_event && !self.is_none(&profile_func) {
265            self.use_tracing.set(false);
266            let res = profile_func.call(args, self);
267            self.use_tracing.set(true);
268            if res.is_err() {
269                *self.profile_func.borrow_mut() = self.ctx.none();
270            }
271        }
272        Ok(trace_result)
273    }
274}