Skip to main content

doge_interp/
call.rs

1//! Calls in every form Doge has: direct function calls, calls through a function
2//! value, method dispatch, constructors, module-member calls, and `super`. Each
3//! resolves its target, evaluates arguments, binds them to the callee's header
4//! (defaults, keyword arguments, and the `many` variadic), and runs the body.
5
6use doge_compiler as dc;
7use doge_runtime::{
8    builtin_method, callee_function, enter_call, exit_call, function_arity_error, object_class_id,
9    DogeError, DogeResult, ErrorKind, Value,
10};
11
12use crate::natives::call_native;
13use crate::{cell, scope, Callable, Flow, Interp, Scope, Template};
14
15/// A call's evaluated arguments: positional values, then `(name, value)` keyword
16/// pairs in source order.
17type EvaluatedArgs = (Vec<Value>, Vec<(String, Value)>);
18
19impl Interp {
20    /// Evaluate a call expression, resolving the callee the same way the compiler
21    /// does: a known function/constructor/module member statically, anything else
22    /// through the value it evaluates to.
23    pub(crate) fn eval_call(
24        &mut self,
25        callee: &dc::Expr,
26        args: &[dc::Expr],
27        kwargs: &[(String, dc::Expr)],
28        frame: &Scope,
29        fid: u32,
30    ) -> DogeResult<Value> {
31        match callee {
32            dc::Expr::Ident { name, .. } => {
33                // A bound name (local, or a top-level function/variable held as a
34                // value) is called through its value.
35                if let Some(cell) = self.lookup(frame, fid, name) {
36                    let value = cell.borrow().clone();
37                    let (args, kwargs) = self.eval_args(args, kwargs, frame, fid)?;
38                    return self.call_value(value, args, kwargs);
39                }
40                if let Some(id) = self.builtin_ids.get(name).copied() {
41                    let args = self.eval_positional(args, frame, fid)?;
42                    return self.call_id(id, Vec::new(), args, Vec::new(), name);
43                }
44                if let Some(class_id) = self.class_id_in(fid, name) {
45                    let (args, kwargs) = self.eval_args(args, kwargs, frame, fid)?;
46                    return self.construct(class_id, args, kwargs, name);
47                }
48                Err(DogeError::type_error(format!(
49                    "cannot call {name} — it is not a function"
50                )))
51            }
52            // `nerd.sqrt(...)` / `utils.square(...)` — a member call on a module.
53            dc::Expr::Attr { obj, name, .. }
54                if matches!(obj.as_ref(), dc::Expr::Ident { name: base, .. }
55                    if self.lookup(frame, fid, base).is_none()
56                        && self.import_ref(fid, base).is_some()) =>
57            {
58                let dc::Expr::Ident { name: base, .. } = obj.as_ref() else {
59                    unreachable!("guarded to an Ident base")
60                };
61                let module = self
62                    .import_ref(fid, base)
63                    .expect("guarded: base is an import");
64                self.call_module_member(module, base, name, args, kwargs, frame, fid)
65            }
66            // `kabosu.speak(...)` — a method call, dispatched on the receiver.
67            dc::Expr::Attr { obj, name, .. } => {
68                let recv = self.eval(obj, frame, fid)?;
69                let args = self.eval_positional(args, frame, fid)?;
70                self.call_method(recv, name, args)
71            }
72            // Any other callee expression is called through its value.
73            other => {
74                let value = self.eval(other, frame, fid)?;
75                let (args, kwargs) = self.eval_args(args, kwargs, frame, fid)?;
76                self.call_value(value, args, kwargs)
77            }
78        }
79    }
80
81    /// Call a top-level function of the entry file by name with no arguments,
82    /// returning its value or the (catchable) error it raised. The test runner
83    /// drives each discovered `test`-prefixed function through this, after
84    /// [`Interp::prepare`](crate::Interp::prepare) has integrated the program.
85    pub fn call_entry_function(&mut self, name: &str) -> DogeResult<Value> {
86        let value = self
87            .lookup(&self.globals(0), 0, name)
88            .map(|cell| cell.borrow().clone())
89            .ok_or_else(|| {
90                DogeError::new(ErrorKind::ValueError, format!("no function named {name}"))
91            })?;
92        self.call_value(value, Vec::new(), Vec::new())
93    }
94
95    /// Evaluate positional arguments only.
96    fn eval_positional(
97        &mut self,
98        args: &[dc::Expr],
99        frame: &Scope,
100        fid: u32,
101    ) -> DogeResult<Vec<Value>> {
102        let mut out = Vec::with_capacity(args.len());
103        for arg in args {
104            out.push(self.eval(arg, frame, fid)?);
105        }
106        Ok(out)
107    }
108
109    /// Evaluate positional then keyword arguments, left to right.
110    fn eval_args(
111        &mut self,
112        args: &[dc::Expr],
113        kwargs: &[(String, dc::Expr)],
114        frame: &Scope,
115        fid: u32,
116    ) -> DogeResult<EvaluatedArgs> {
117        let positional = self.eval_positional(args, frame, fid)?;
118        let mut keyword = Vec::with_capacity(kwargs.len());
119        for (name, value) in kwargs {
120            keyword.push((name.clone(), self.eval(value, frame, fid)?));
121        }
122        Ok((positional, keyword))
123    }
124
125    /// Call a function value: unwrap it, then dispatch on its `fn_id`. Arity
126    /// diagnostics name the function by its own definition name, not the call site.
127    pub(crate) fn call_value(
128        &mut self,
129        value: Value,
130        args: Vec<Value>,
131        kwargs: Vec<(String, Value)>,
132    ) -> DogeResult<Value> {
133        // A bound method routes back through method dispatch, exactly as a direct
134        // `a.speak(...)` would. Indirect calls carry no keyword arguments (the
135        // checker rejects them), so the defensive guard here never fires in a
136        // checked program.
137        if let Value::BoundMethod(m) = &value {
138            if !kwargs.is_empty() {
139                return Err(DogeError::type_error(
140                    "cannot call a bound method with keyword arguments",
141                ));
142            }
143            let recv = m.receiver.clone();
144            let method = m.method.clone();
145            return self.call_method(recv, &method, args);
146        }
147        let func = callee_function(&value)?;
148        self.call_id(
149            func.fn_id as usize,
150            func.captures.clone(),
151            args,
152            kwargs,
153            &func.name,
154        )
155    }
156
157    /// Dispatch a call to the callable at `fn_id`: a native, or a user function
158    /// with its captured cells.
159    fn call_id(
160        &mut self,
161        id: usize,
162        captures: Vec<crate::Cell>,
163        args: Vec<Value>,
164        kwargs: Vec<(String, Value)>,
165        label: &str,
166    ) -> DogeResult<Value> {
167        let callable = self.callables[id].clone();
168        match callable.as_ref() {
169            // `pack.zoom` is the one native that needs interpreter state: it spawns
170            // a fresh interpreter over the same program on a new thread, so it is
171            // dispatched here rather than through the stateless `call_native`.
172            Callable::Native(native) if native.runtime_fn == dc::PACK_ZOOM_RUNTIME_FN => {
173                self.interp_zoom(args)
174            }
175            Callable::Native(native) => call_native(native, args),
176            Callable::User(template) => {
177                self.call_user(template, &captures, args, kwargs, None, label)
178            }
179            // A class value called: build an instance through its constructor.
180            Callable::Ctor(class_id) => self.construct(*class_id, args, kwargs, label),
181        }
182    }
183
184    /// Run a user function/method/closure body: guard recursion, build its frame
185    /// (captures, `self`, parameters, hoisted locals), execute, and return the
186    /// value it `return`s (or `none` if it falls off the end).
187    fn call_user(
188        &mut self,
189        template: &Template,
190        captures: &[crate::Cell],
191        args: Vec<Value>,
192        kwargs: Vec<(String, Value)>,
193        self_value: Option<Value>,
194        label: &str,
195    ) -> DogeResult<Value> {
196        enter_call(&mut self.depth)?;
197        let saved_class = self.current_method_class;
198        self.current_method_class = template.method_class;
199        let result = self.user_body(template, captures, args, kwargs, self_value, label);
200        self.current_method_class = saved_class;
201        exit_call(&mut self.depth);
202        result
203    }
204
205    fn user_body(
206        &mut self,
207        template: &Template,
208        captures: &[crate::Cell],
209        args: Vec<Value>,
210        kwargs: Vec<(String, Value)>,
211        self_value: Option<Value>,
212        label: &str,
213    ) -> DogeResult<Value> {
214        let bound = self.bind_args(&template.params, args, kwargs, label, template.file_id)?;
215
216        let frame = scope();
217        {
218            let mut f = frame.borrow_mut();
219            for (name, captured) in template.capture_names.iter().zip(captures) {
220                f.insert(name.clone(), captured.clone());
221            }
222            if let Some(self_value) = self_value {
223                f.insert("self".to_string(), cell(self_value));
224            }
225            for (name, value) in template.params.binding_names().iter().zip(bound) {
226                f.insert(name.clone(), cell(value));
227            }
228        }
229        // Hoist the body's remaining local names to `none`, like the compiler's
230        // hoisted `Env` fields; nested-function values bind when their statement runs.
231        for name in dc::hoisted_names(&template.body) {
232            frame
233                .borrow_mut()
234                .entry(name)
235                .or_insert_with(|| cell(Value::None));
236        }
237
238        match self.exec_stmts(&template.body, &frame, template.file_id)? {
239            Flow::Return(value) => Ok(value),
240            // Falling off the end (or a checked-away stray bork/continue) yields none.
241            _ => Ok(Value::None),
242        }
243    }
244
245    /// Map a call's positional and keyword arguments onto a header's slots — filling
246    /// from positionals, then keywords, then defaults, and collecting any surplus
247    /// into the `many` variadic — returning the values in binding order. The arity
248    /// and keyword diagnostics match the compiler's wording.
249    fn bind_args(
250        &mut self,
251        params: &dc::Params,
252        args: Vec<Value>,
253        kwargs: Vec<(String, Value)>,
254        label: &str,
255        fid: u32,
256    ) -> DogeResult<Vec<Value>> {
257        let n = params.params.len();
258        let has_vararg = params.has_vararg();
259        let required = params.required();
260        let max = params.max_positional();
261        let total = args.len() + kwargs.len();
262
263        if !has_vararg && args.len() > n {
264            return Err(function_arity_error(label, required, max, total));
265        }
266
267        let mut slot: Vec<Option<Value>> = vec![None; n];
268        let mut extras: Vec<Value> = Vec::new();
269        for (i, arg) in args.into_iter().enumerate() {
270            if i < n {
271                slot[i] = Some(arg);
272            } else {
273                extras.push(arg);
274            }
275        }
276        for (name, value) in kwargs {
277            match params.params.iter().position(|p| p.name == name) {
278                Some(idx) if slot[idx].is_none() => slot[idx] = Some(value),
279                Some(_) => {
280                    return Err(DogeError::type_error(format!(
281                        "{label} got parameter {name} twice"
282                    )))
283                }
284                None => {
285                    return Err(DogeError::type_error(format!(
286                        "{label} has no parameter {name}"
287                    )))
288                }
289            }
290        }
291
292        let mut out = Vec::with_capacity(n + has_vararg as usize);
293        for (i, filled) in slot.into_iter().enumerate() {
294            match filled {
295                Some(value) => out.push(value),
296                None => match &params.params[i].default {
297                    Some(default) => out.push(self.eval(default, &scope(), fid)?),
298                    None => return Err(function_arity_error(label, required, max, total)),
299                },
300            }
301        }
302        if has_vararg {
303            out.push(Value::list(extras));
304        }
305        Ok(out)
306    }
307
308    /// Dispatch a method call: an object routes through its class's method table
309    /// (walking its ancestry); any other receiver forwards to the runtime's
310    /// collection methods, exactly like the compiled dispatcher.
311    fn call_method(&mut self, recv: Value, name: &str, args: Vec<Value>) -> DogeResult<Value> {
312        if !matches!(recv, Value::Object(_)) {
313            return builtin_method(&recv, name, args);
314        }
315        let class_id = object_class_id(&recv)?;
316        match self.resolve_method(class_id, name) {
317            Some((fn_id, def_class)) => {
318                let label = format!("{}.{name}", self.classes[def_class as usize].name);
319                self.invoke_method(fn_id, recv, args, &label)
320            }
321            None => Err(doge_runtime::no_such_method(&recv, name)),
322        }
323    }
324
325    /// Invoke a resolved method template with `recv` bound as `self`.
326    fn invoke_method(
327        &mut self,
328        fn_id: usize,
329        recv: Value,
330        args: Vec<Value>,
331        label: &str,
332    ) -> DogeResult<Value> {
333        let callable = self.callables[fn_id].clone();
334        let Callable::User(template) = callable.as_ref() else {
335            unreachable!("interp bug: a method id points at a native");
336        };
337        self.call_user(template, &[], args, Vec::new(), Some(recv), label)
338    }
339
340    /// `super.method(args)`: resolve `method` in the enclosing class's parent chain
341    /// and call it with the current `self`.
342    pub(crate) fn eval_super_call(
343        &mut self,
344        method: &str,
345        args: &[dc::Expr],
346        frame: &Scope,
347        fid: u32,
348    ) -> DogeResult<Value> {
349        let class_id = self
350            .current_method_class
351            .expect("checker guarantees super is inside a method");
352        let parent = self.classes[class_id as usize]
353            .parent
354            .expect("checker guarantees the class has a parent");
355        let (fn_id, def_class) = self
356            .resolve_method(parent, method)
357            .expect("checker guarantees a parent defines the method");
358        let self_value = self
359            .lookup(frame, fid, "self")
360            .expect("a method frame always binds self")
361            .borrow()
362            .clone();
363        let args = self.eval_positional(args, frame, fid)?;
364        let label = format!("{}.{method}", self.classes[def_class as usize].name);
365        self.invoke_method(fn_id, self_value, args, &label)
366    }
367
368    /// The method named `name` callable on `class_id`: the nearest definition up
369    /// its ancestry, returning its `fn_id` and the class that defines it.
370    pub(crate) fn resolve_method(&self, class_id: u32, name: &str) -> Option<(usize, u32)> {
371        let mut current = Some(class_id);
372        let mut guard = 0;
373        while let Some(cid) = current {
374            let class = &self.classes[cid as usize];
375            if let Some(fn_id) = class.methods.get(name) {
376                return Some((*fn_id, cid));
377            }
378            guard += 1;
379            if guard > self.classes.len() {
380                break;
381            }
382            current = class.parent;
383        }
384        None
385    }
386
387    /// Construct an instance of `class_id`: build the object, then run its
388    /// effective `init` (its own or the nearest inherited one) with `self` bound.
389    fn construct(
390        &mut self,
391        class_id: u32,
392        args: Vec<Value>,
393        kwargs: Vec<(String, Value)>,
394        label: &str,
395    ) -> DogeResult<Value> {
396        let class = self.classes[class_id as usize].clone();
397        let object = Value::object(class_id, &class.name);
398        match self.resolve_method(class_id, "init") {
399            Some((fn_id, _)) => {
400                self.invoke_method_kw(fn_id, object.clone(), args, kwargs, label)?;
401            }
402            None => {
403                // No init anywhere in the chain: construction takes no arguments.
404                self.bind_args(&dc::Params::default(), args, kwargs, label, class.file_id)?;
405            }
406        }
407        Ok(object)
408    }
409
410    /// Invoke a method template that accepts keyword arguments (a constructor's
411    /// `init`), binding `recv` as `self`.
412    fn invoke_method_kw(
413        &mut self,
414        fn_id: usize,
415        recv: Value,
416        args: Vec<Value>,
417        kwargs: Vec<(String, Value)>,
418        label: &str,
419    ) -> DogeResult<Value> {
420        let callable = self.callables[fn_id].clone();
421        let Callable::User(template) = callable.as_ref() else {
422            unreachable!("interp bug: a method id points at a native");
423        };
424        self.call_user(template, &[], args, kwargs, Some(recv), label)
425    }
426
427    /// A call to a module member: a stdlib function, or a user module's function or
428    /// constructor.
429    #[allow(clippy::too_many_arguments)]
430    fn call_module_member(
431        &mut self,
432        module: crate::ModuleRef,
433        base: &str,
434        member: &str,
435        args: &[dc::Expr],
436        kwargs: &[(String, dc::Expr)],
437        frame: &Scope,
438        fid: u32,
439    ) -> DogeResult<Value> {
440        match module {
441            crate::ModuleRef::Stdlib(m) => {
442                let args = self.eval_positional(args, frame, fid)?;
443                match self
444                    .module_fn_ids
445                    .get(&(m.name.to_string(), member.to_string()))
446                {
447                    Some(id) => self.call_id(*id, Vec::new(), args, Vec::new(), member),
448                    None => Err(DogeError::attr_error(format!(
449                        "{base} has no member {member}"
450                    ))),
451                }
452            }
453            crate::ModuleRef::User(mfid) => {
454                let (args, kwargs) = self.eval_args(args, kwargs, frame, fid)?;
455                if let Some(class_id) = self.class_id_in(mfid, member) {
456                    let label = format!("{base}.{member}");
457                    return self.construct(class_id, args, kwargs, &label);
458                }
459                let value = self
460                    .lookup(&self.globals(mfid), mfid, member)
461                    .map(|c| c.borrow().clone())
462                    .ok_or_else(|| {
463                        DogeError::attr_error(format!("{base} has no member {member}"))
464                    })?;
465                self.call_value(value, args, kwargs)
466            }
467        }
468    }
469}