Skip to main content

shape_vm/executor/
call_convention.rs

1//! Function and closure call convention, execution wrappers, and async resolution.
2
3use shape_value::{Upvalue, VMError, ValueWord};
4
5use super::{CallFrame, ExecutionResult, VirtualMachine, task_scheduler};
6
7impl VirtualMachine {
8    /// Execute a named function with arguments, returning its result.
9    ///
10    /// If the program has module-level bindings, the top-level code is executed
11    /// first (once) to initialize them before calling the target function.
12    pub fn execute_function_by_name(
13        &mut self,
14        name: &str,
15        args: Vec<ValueWord>,
16        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
17    ) -> Result<ValueWord, VMError> {
18        let func_id = self
19            .program
20            .functions
21            .iter()
22            .position(|f| f.name == name)
23            .ok_or_else(|| VMError::RuntimeError(format!("Function '{}' not found", name)))?;
24
25        // Run the top-level code first to initialize module bindings,
26        // but only if there are module bindings that need initialization.
27        if !self.program.module_binding_names.is_empty() && !self.module_init_done {
28            self.reset();
29            self.execute(None)?;
30            self.module_init_done = true;
31        }
32
33        // Now call the target function.
34        // Use reset_stack to keep module_bindings intact.
35        self.reset_stack();
36        self.ip = self.program.instructions.len();
37        self.call_function_with_nb_args(func_id as u16, &args)?;
38        self.execute(ctx)
39    }
40
41    /// Execute a function by its ID with positional arguments.
42    ///
43    /// Used by the remote execution system when the caller already knows the
44    /// function index (e.g., from a `RemoteCallRequest.function_id`).
45    pub fn execute_function_by_id(
46        &mut self,
47        func_id: u16,
48        args: Vec<ValueWord>,
49        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
50    ) -> Result<ValueWord, VMError> {
51        self.reset();
52        self.ip = self.program.instructions.len();
53        self.call_function_with_nb_args(func_id, &args)?;
54        self.execute(ctx)
55    }
56
57    /// Execute a closure with its captured upvalues and arguments.
58    ///
59    /// Used by the remote execution system to run closures that were
60    /// serialized with their captured values.
61    pub fn execute_closure(
62        &mut self,
63        function_id: u16,
64        upvalues: Vec<Upvalue>,
65        args: Vec<ValueWord>,
66        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
67    ) -> Result<ValueWord, VMError> {
68        self.reset();
69        self.ip = self.program.instructions.len();
70        self.call_closure_with_nb_args(function_id, upvalues, &args)?;
71        self.execute(ctx)
72    }
73
74    /// Fast function execution for hot loops (backtesting)
75    /// - Uses pre-computed function ID (no name lookup)
76    /// - Uses reset_minimal() for minimum overhead
77    /// - Uses execute_fast() which skips debugging overhead
78    /// - Assumes function doesn't create GC objects or use exceptions
79    pub fn execute_function_fast(
80        &mut self,
81        func_id: u16,
82        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
83    ) -> Result<ValueWord, VMError> {
84        // Minimal reset - only essential state, no GC overhead
85        self.reset_minimal();
86        self.ip = self.program.instructions.len();
87        self.call_function_with_nb_args(func_id, &[])?;
88        self.execute_fast(ctx)
89    }
90
91    /// Execute a function with named arguments
92    /// Maps named args to positional based on function's param_names
93    pub fn execute_function_with_named_args(
94        &mut self,
95        func_id: u16,
96        named_args: &[(String, ValueWord)],
97        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
98    ) -> Result<ValueWord, VMError> {
99        let function = self
100            .program
101            .functions
102            .get(func_id as usize)
103            .ok_or(VMError::InvalidCall)?;
104
105        // Map named args to positional based on param_names
106        let mut args = vec![ValueWord::none(); function.arity as usize];
107        for (name, value) in named_args {
108            if let Some(idx) = function.param_names.iter().position(|p| p == name) {
109                if idx < args.len() {
110                    args[idx] = value.clone();
111                }
112            }
113        }
114
115        self.reset_minimal();
116        self.ip = self.program.instructions.len();
117        self.call_function_with_nb_args(func_id, &args)?;
118        self.execute_fast(ctx)
119    }
120
121    /// Resume execution after a suspension.
122    ///
123    /// The resolved value is pushed onto the stack, and execution continues
124    /// from where it left off (the IP is already set to the resume point).
125    pub fn resume(
126        &mut self,
127        value: ValueWord,
128        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
129    ) -> Result<ExecutionResult, VMError> {
130        self.push_vw(value)?;
131        self.execute_with_suspend(ctx)
132    }
133
134    /// Execute with automatic async task resolution.
135    ///
136    /// Runs `execute_with_suspend` in a loop. Each time the VM suspends on a
137    /// `Future { id }`, the host resolves the task via the TaskScheduler
138    /// (synchronously executing the spawned callable inline) and resumes the
139    /// VM with the result. This continues until execution completes or an
140    /// unresolvable suspension is encountered.
141    pub fn execute_with_async(
142        &mut self,
143        mut ctx: Option<&mut shape_runtime::context::ExecutionContext>,
144    ) -> Result<ValueWord, VMError> {
145        loop {
146            match self.execute_with_suspend(ctx.as_deref_mut())? {
147                ExecutionResult::Completed(value) => return Ok(value),
148                ExecutionResult::Suspended { future_id, .. } => {
149                    // Try to resolve via the task scheduler
150                    let result = self.resolve_spawned_task(future_id)?;
151                    // Push the result so the resumed VM finds it on the stack
152                    self.push_vw(result)?;
153                    // Loop continues with execute_with_suspend
154                }
155            }
156        }
157    }
158
159    /// Resolve a spawned task by executing its callable synchronously.
160    ///
161    /// Looks up the callable in the TaskScheduler, then executes it:
162    /// - NanTag::Function -> calls via call_function_with_nb_args
163    /// - HeapValue::Closure -> calls via call_closure_with_nb_args
164    /// - Other values -> returns them directly (already-resolved value)
165    fn resolve_spawned_task(&mut self, task_id: u64) -> Result<ValueWord, VMError> {
166        // Check if already resolved (cached)
167        if let Some(task_scheduler::TaskStatus::Completed(val)) =
168            self.task_scheduler.get_result(task_id)
169        {
170            return Ok(val.clone());
171        }
172        if let Some(task_scheduler::TaskStatus::Cancelled) = self.task_scheduler.get_result(task_id)
173        {
174            return Err(VMError::RuntimeError(format!(
175                "Task {} was cancelled",
176                task_id
177            )));
178        }
179
180        // Take the callable
181        let callable_nb = self.task_scheduler.take_callable(task_id).ok_or_else(|| {
182            VMError::RuntimeError(format!("No callable registered for task {}", task_id))
183        })?;
184
185        // Execute based on callable type.
186        // We save/restore the instruction pointer and stack depth so the
187        // nested execution doesn't corrupt the outer (suspended) state.
188        use shape_value::NanTag;
189
190        let result_nb = match callable_nb.tag() {
191            NanTag::Function => {
192                let func_id = callable_nb.as_function().ok_or(VMError::InvalidCall)?;
193                let saved_ip = self.ip;
194                let saved_sp = self.sp;
195
196                self.ip = self.program.instructions.len();
197                self.call_function_with_nb_args(func_id, &[])?;
198                let res = self.execute_fast(None);
199
200                self.ip = saved_ip;
201                // Restore stack pointer (clear anything left above saved_sp)
202                for i in saved_sp..self.sp {
203                    self.stack[i] = ValueWord::none();
204                }
205                self.sp = saved_sp;
206
207                res?
208            }
209            NanTag::Heap => {
210                if let Some((function_id, upvalues)) = callable_nb.as_closure() {
211                    let upvalues = upvalues.to_vec();
212                    let saved_ip = self.ip;
213                    let saved_sp = self.sp;
214
215                    self.ip = self.program.instructions.len();
216                    self.call_closure_with_nb_args(function_id, upvalues, &[])?;
217                    let res = self.execute_fast(None);
218
219                    self.ip = saved_ip;
220                    for i in saved_sp..self.sp {
221                        self.stack[i] = ValueWord::none();
222                    }
223                    self.sp = saved_sp;
224
225                    res?
226                } else {
227                    // If someone spawned an already-resolved value, just return it
228                    callable_nb
229                }
230            }
231            // If someone spawned an already-resolved value, just return it
232            _ => callable_nb,
233        };
234
235        // Cache the result
236        self.task_scheduler.complete(task_id, result_nb.clone());
237
238        Ok(result_nb)
239    }
240
241    /// ValueWord-module function call: takes ValueWord args directly.
242    pub(crate) fn call_function_with_nb_args(
243        &mut self,
244        func_id: u16,
245        args: &[ValueWord],
246    ) -> Result<(), VMError> {
247        let function = self
248            .program
249            .functions
250            .get(func_id as usize)
251            .ok_or(VMError::InvalidCall)?;
252
253        if self.call_stack.len() >= self.config.max_call_depth {
254            return Err(VMError::StackOverflow);
255        }
256
257        let locals_count = function.locals_count as usize;
258        let param_count = function.arity as usize;
259        let entry_point = function.entry_point;
260
261        let bp = self.sp;
262        let needed = bp + locals_count;
263        if needed > self.stack.len() {
264            self.stack.resize_with(needed * 2 + 1, ValueWord::none);
265        }
266
267        for i in 0..param_count {
268            if i < locals_count {
269                self.stack[bp + i] = args.get(i).cloned().unwrap_or_else(ValueWord::none);
270            }
271        }
272
273        self.sp = needed;
274
275        let blob_hash = self.blob_hash_for_function(func_id);
276        let frame = CallFrame {
277            return_ip: self.ip,
278            base_pointer: bp,
279            locals_count,
280            function_id: Some(func_id),
281            upvalues: None,
282            blob_hash,
283        };
284        self.call_stack.push(frame);
285        self.ip = entry_point;
286        Ok(())
287    }
288
289    /// ValueWord-host closure call: takes ValueWord args directly.
290    pub(crate) fn call_closure_with_nb_args(
291        &mut self,
292        func_id: u16,
293        upvalues: Vec<Upvalue>,
294        args: &[ValueWord],
295    ) -> Result<(), VMError> {
296        let function = self
297            .program
298            .functions
299            .get(func_id as usize)
300            .ok_or(VMError::InvalidCall)?;
301
302        if self.call_stack.len() >= self.config.max_call_depth {
303            return Err(VMError::StackOverflow);
304        }
305
306        let locals_count = function.locals_count as usize;
307        let captures_count = function.captures_count as usize;
308        let arity = function.arity as usize;
309        let entry_point = function.entry_point;
310
311        let bp = self.sp;
312        let needed = bp + locals_count;
313        if needed > self.stack.len() {
314            self.stack.resize_with(needed * 2 + 1, ValueWord::none);
315        }
316
317        // Bind upvalue values as the first N locals
318        for (i, upvalue) in upvalues.iter().enumerate() {
319            if i < locals_count {
320                self.stack[bp + i] = upvalue.get();
321            }
322        }
323
324        // Bind the regular arguments after the upvalues
325        for (i, arg) in args.iter().enumerate() {
326            let local_idx = captures_count + i;
327            if local_idx < locals_count {
328                self.stack[bp + local_idx] = arg.clone();
329            }
330        }
331
332        // Fill remaining parameters with None
333        for i in (captures_count + args.len())..arity.min(locals_count) {
334            self.stack[bp + i] = ValueWord::none();
335        }
336
337        self.sp = needed;
338
339        let blob_hash = self.blob_hash_for_function(func_id);
340        self.call_stack.push(CallFrame {
341            return_ip: self.ip,
342            base_pointer: bp,
343            locals_count,
344            function_id: Some(func_id),
345            upvalues: Some(upvalues),
346            blob_hash,
347        });
348
349        self.ip = entry_point;
350        Ok(())
351    }
352
353    /// ValueWord-native call_value_immediate: dispatches on NanTag/HeapKind.
354    ///
355    /// Returns ValueWord directly.
356    pub(in crate::executor) fn call_value_immediate_nb(
357        &mut self,
358        callee: &ValueWord,
359        args: &[ValueWord],
360        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
361    ) -> Result<ValueWord, VMError> {
362        use shape_value::NanTag;
363        let target_depth = self.call_stack.len();
364
365        match callee.tag() {
366            NanTag::Function => {
367                let func_id = callee.as_function().ok_or(VMError::InvalidCall)?;
368                self.call_function_with_nb_args(func_id, args)?;
369            }
370            NanTag::ModuleFunction => {
371                let func_id = callee.as_module_function().ok_or(VMError::InvalidCall)?;
372                let module_fn = self.module_fn_table.get(func_id).cloned().ok_or_else(|| {
373                    VMError::RuntimeError(format!(
374                        "Module function ID {} not found in registry",
375                        func_id
376                    ))
377                })?;
378                let args_vec: Vec<ValueWord> = args.to_vec();
379                let result_nb = self.invoke_module_fn(&module_fn, &args_vec)?;
380                return Ok(result_nb);
381            }
382            NanTag::Heap => match callee.as_heap_ref() {
383                Some(shape_value::HeapValue::Closure {
384                    function_id,
385                    upvalues,
386                }) => {
387                    self.call_closure_with_nb_args(*function_id, upvalues.clone(), args)?;
388                }
389                Some(shape_value::HeapValue::HostClosure(callable)) => {
390                    let args_vec: Vec<ValueWord> = args.to_vec();
391                    let result_nb = callable.call(&args_vec).map_err(VMError::RuntimeError)?;
392                    return Ok(result_nb);
393                }
394                _ => return Err(VMError::InvalidCall),
395            },
396            _ => return Err(VMError::InvalidCall),
397        }
398
399        self.execute_until_call_depth(target_depth, ctx)?;
400        self.pop_vw()
401    }
402
403    /// Fast-path function call: reads `arg_count` arguments directly from the
404    /// value stack instead of collecting them into a temporary `Vec`.
405    ///
406    /// Precondition: the top `arg_count` values on the stack (below sp) are the
407    /// arguments in left-to-right order (arg0 deepest, argN-1 at top).
408    /// These args become the first locals of the new frame's register window.
409    pub(crate) fn call_function_from_stack(
410        &mut self,
411        func_id: u16,
412        arg_count: usize,
413    ) -> Result<(), VMError> {
414        let function = self
415            .program
416            .functions
417            .get(func_id as usize)
418            .ok_or(VMError::InvalidCall)?;
419
420        if self.call_stack.len() >= self.config.max_call_depth {
421            return Err(VMError::StackOverflow);
422        }
423
424        let locals_count = function.locals_count as usize;
425        let entry_point = function.entry_point;
426        let arity = function.arity as usize;
427
428        // The args are already on the stack at positions [sp - arg_count .. sp).
429        // They become the first locals in the register window.
430        // bp = sp - arg_count (args are already in place as the first locals)
431        let bp = self.sp.saturating_sub(arg_count);
432
433        // Ensure stack has room for all locals (some may be beyond the args)
434        let needed = bp + locals_count;
435        if needed > self.stack.len() {
436            self.stack.resize_with(needed * 2 + 1, ValueWord::none);
437        }
438
439        // Zero remaining local slots (including omitted args that the compiler
440        // may intentionally represent as null sentinels for default params).
441        let copy_count = arg_count.min(arity).min(locals_count);
442        for i in copy_count..locals_count {
443            self.stack[bp + i] = ValueWord::none();
444        }
445
446        // Advance sp past all locals
447        self.sp = needed;
448
449        let blob_hash = self.blob_hash_for_function(func_id);
450        self.call_stack.push(CallFrame {
451            return_ip: self.ip,
452            base_pointer: bp,
453            locals_count,
454            function_id: Some(func_id),
455            upvalues: None,
456            blob_hash,
457        });
458        self.ip = entry_point;
459        Ok(())
460    }
461}