`Call` orchestrator. Only user `Func` callees build a fresh `fn_slots` and run the body inline; every other callee kind short-circuits in `try_dispatch_non_func_callable`. */
(&mut self, operand: u16, chunk: &SSAChunk, slots: &mut [Val])
| 182 | |
| 183 | /* `Call` orchestrator. Only user `Func` callees build a fresh `fn_slots` and run the body inline; every other callee kind short-circuits in `try_dispatch_non_func_callable`. */ |
| 184 | pub(crate) fn exec_call(&mut self, operand: u16, chunk: &SSAChunk, slots: &mut [Val]) -> Result<(), VmErr> { |
| 185 | let (positional, kw_flat, _num_pos, num_kw) = self.parse_call_args(operand)?; |
| 186 | |
| 187 | if self.depth >= self.max_calls { return Err(cold_depth()); } |
| 188 | // Charge each call so wide recursion is op-budget bounded. |
| 189 | self.charge_step()?; |
| 190 | |
| 191 | let callee = self.pop()?; |
| 192 | if !callee.is_heap() { return Err(cold_type("object is not callable")); } |
| 193 | |
| 194 | if self.try_dispatch_non_func_callable(callee, &positional, &kw_flat, num_kw, chunk, slots)? { |
| 195 | return Ok(()); |
| 196 | } |
| 197 | |
| 198 | // Snapshot defaults/captures once, both are tiny (<10), and cloning beats the 3+ heap re-reads later phases would do. Back-prop still uses `get_mut` since it writes. |
| 199 | let (fi, defaults, captures) = match self.heap.get(callee) { |
| 200 | HeapObj::Func(i, d, c) => (*i, d.clone(), c.clone()), |
| 201 | _ => return Err(cold_type("object is not callable")), |
| 202 | }; |
| 203 | |
| 204 | // Pure-call memoisation. Disabled under impure outer frames (stale-view risk) or kwargs (cache key only spans positionals). |
| 205 | let outer_impure = self.observed_impure.last().copied().unwrap_or(false); |
| 206 | if num_kw == 0 && !outer_impure |
| 207 | && let Some(cached) = self.templates.lookup(fi, &positional, &self.heap) { |
| 208 | self.push(cached); |
| 209 | return Ok(()); |
| 210 | } |
| 211 | |
| 212 | self.depth += 1; |
| 213 | let (_params, body, _, _) = self.functions[fi]; |
| 214 | let mut fn_slots = self.slot_templates[fi].clone(); |
| 215 | |
| 216 | self.bind_function_args(fi, &defaults, &captures, &positional, &kw_flat, &mut fn_slots)?; |
| 217 | |
| 218 | if self.needs_caller_slots[fi] { |
| 219 | self.apply_caller_slot_propagation(fi, &captures, chunk, slots, &mut fn_slots); |
| 220 | } |
| 221 | |
| 222 | self.bind_self_reference(fi, callee, &mut fn_slots); |
| 223 | |
| 224 | // Generator/coroutine: return a suspended Coroutine instead of running. Both flags are O(1). |
| 225 | let is_async_fn = self.is_async.get(fi).copied().unwrap_or(false); |
| 226 | if is_async_fn || body.is_generator { |
| 227 | let coro = self.heap.alloc(HeapObj::Coroutine(0, fn_slots, Vec::new(), BodyRef::Fn(fi), Vec::new(), Vec::new(), Vec::new()))?; |
| 228 | self.push(coro); |
| 229 | self.depth -= 1; |
| 230 | return Ok(()); |
| 231 | } |
| 232 | |
| 233 | // Snapshot caller-visible depths so we can split the helper's stack/iter/exception contributions out if it suspends mid-body via a yielding builtin. |
| 234 | let stack_base = self.stack.len(); |
| 235 | let iter_base = self.iter_stack.len(); |
| 236 | let exc_base = self.exception_stack.len(); |
| 237 | let yields_before = self.yields.len(); |
| 238 | let (callee_impure, exec_result) = self.run_body_with_frame(fi, body, chunk, &mut fn_slots, slots); |
| 239 | self.depth -= 1; |
| 240 | |
| 241 | self.back_propagate_nonlocals(fi, body, callee, chunk, slots, &fn_slots); |
no test coverage detected