Skip to main content

doge_interp/
lib.rs

1//! A tree-walking interpreter for Doge — the second execution engine beside the
2//! Rust-transpiling compiler. It evaluates a checked AST directly against
3//! `doge-runtime`, so `doge repl` (and the interpreter path in general) skips the
4//! rustc build entirely. Every value operation calls the same `doge-runtime`
5//! function the generated Rust would, so an interpreted program behaves
6//! identically to a compiled one — a guarantee the examples parity suite enforces.
7
8use std::cell::RefCell;
9use std::collections::HashMap;
10use std::rc::Rc;
11use std::sync::Arc;
12
13use doge_compiler as dc;
14use doge_runtime::{DogeError, DogeResult, ErrorKind, Value};
15
16mod analyze;
17mod call;
18mod exec;
19mod expr;
20mod natives;
21mod pack;
22#[cfg(test)]
23mod tests;
24
25pub use dc::{ClassInfo, ReplParse, SessionScope};
26
27/// A shared, mutable binding cell — the interpreter's variables and the compiled
28/// runtime's are the same `Rc<RefCell<Value>>`, so closures capture by sharing.
29type Cell = doge_runtime::Cell;
30
31/// A lexical scope: names bound in it to their shared cells. A function call gets
32/// a fresh one seeded with its captures and parameters; a file's top level shares
33/// one persistent scope (the REPL session's globals live here).
34type Scope = Rc<RefCell<HashMap<String, Cell>>>;
35
36fn scope() -> Scope {
37    Rc::new(RefCell::new(HashMap::new()))
38}
39
40fn cell(value: Value) -> Cell {
41    Rc::new(RefCell::new(value))
42}
43
44/// A resolved `so` import: a stdlib module, or a user module by file id.
45#[derive(Clone, Copy)]
46enum ModuleRef {
47    Stdlib(&'static dc::Module),
48    User(u32),
49}
50
51/// One source file's top-level scope: its persistent globals and its resolved
52/// imports (local name → module).
53struct FileScope {
54    globals: Scope,
55    imports: HashMap<String, ModuleRef>,
56}
57
58/// A user-defined function, method, or closure, keyed by a program-wide `fn_id`.
59/// The `capture_names` name each captured cell a closure value carries, in the
60/// same order, so a call can rebuild the closure's captured scope.
61struct Template {
62    name: String,
63    file_id: u32,
64    params: dc::Params,
65    body: Rc<[dc::Stmt]>,
66    capture_names: Vec<String>,
67    /// The class a method belongs to (for `super`); `None` for a plain function.
68    method_class: Option<u32>,
69}
70
71/// How a builtin/stdlib native is invoked: the exact runtime function it wires to
72/// plus how many arguments it takes.
73struct Native {
74    name: String,
75    runtime_fn: &'static str,
76    arity: Arity,
77}
78
79/// A native's accepted argument count: a fixed number, `range`'s one-or-two, or
80/// `gib`'s zero-or-one.
81#[derive(Clone, Copy)]
82enum Arity {
83    Exact(usize),
84    OneOrTwo,
85    ZeroOrOne,
86    /// A required count with one optional trailing argument (`min..=max`); an
87    /// omitted trailing argument is padded with `none` before dispatch.
88    Range(usize, usize),
89}
90
91/// Anything callable through a `fn_id`: a user definition, a runtime native, or a
92/// class constructor (a class name used as a value calls this to build an
93/// instance, carrying the class's `class_id`).
94enum Callable {
95    User(Template),
96    Native(Native),
97    Ctor(u32),
98}
99
100/// One class, keyed by a program-wide `class_id`: its name, defining file, parent
101/// (for inheritance and `super`), own methods (method name → `fn_id`), and the
102/// `fn_id` of its constructor callable (for materializing the class as a value).
103struct ClassData {
104    name: String,
105    file_id: u32,
106    parent: Option<u32>,
107    methods: HashMap<String, usize>,
108    ctor_fn_id: usize,
109}
110
111/// Non-local control flow bubbling out of statement execution.
112enum Flow {
113    Normal,
114    Return(Value),
115    Break,
116    Continue,
117}
118
119/// An interpreter session. Holds the program-wide callable and class tables, one
120/// scope per file, and the mutable run state (recursion depth, current location).
121/// A `doge repl` session reuses one `Interp` across snippets, so bindings persist.
122pub struct Interp {
123    callables: Vec<Rc<Callable>>,
124    classes: Vec<Rc<ClassData>>,
125    file_scopes: Vec<FileScope>,
126    /// Per file: top-level function name → `fn_id`, for hoisting function values.
127    file_funcs: Vec<HashMap<String, usize>>,
128    /// Per file: class name → `class_id`, for resolving constructors.
129    file_class_ids: Vec<HashMap<String, u32>>,
130    /// Builtin name → `fn_id`; stable for the session so function values keep working.
131    builtin_ids: HashMap<String, usize>,
132    /// (module, member) → `fn_id` for stdlib module functions.
133    module_fn_ids: HashMap<(String, String), usize>,
134    /// One path per file id, for rendering error locations.
135    file_paths: Vec<Rc<str>>,
136    depth: usize,
137    cur_fid: u32,
138    cur_line: u32,
139    /// The class whose method body is currently running, for `super` resolution.
140    current_method_class: Option<u32>,
141    /// A module constant initializer that failed during integration; surfaced
142    /// when the entry runs, matching the compiled program's initialization order.
143    pending_module_error: Option<DogeError>,
144    /// Names declared with `so … =` in the entry scope, so the REPL's checker keeps
145    /// rejecting reassignment to them across snippets.
146    session_consts: Vec<String>,
147    /// The loaded program, shared so a `pack.zoom` pup can rebuild a fresh
148    /// interpreter over the same files on its own thread. `None` in a REPL session
149    /// (which has no whole-program handle), where `pack.zoom` is not yet available.
150    program: Option<Arc<dc::Program>>,
151}
152
153/// Run a whole loaded program to completion: initialize every module, then
154/// execute the entry file's top-level statements. Used to run a `.doge` file
155/// through the interpreter (the parity path beside `doge build`). Takes the
156/// program by `Arc` so a `pack.zoom` pup can rebuild an interpreter over it.
157pub fn run_program(program: Arc<dc::Program>) -> DogeResult<()> {
158    let mut interp = Interp::new();
159    interp.run(program)
160}
161
162impl Default for Interp {
163    fn default() -> Self {
164        Interp::new()
165    }
166}
167
168impl Interp {
169    /// Integrate a loaded program and run its entry file to completion.
170    pub fn run(&mut self, program: Arc<dc::Program>) -> DogeResult<()> {
171        self.program = Some(program.clone());
172        self.integrate_program(&program);
173        self.run_entry(&program)
174    }
175
176    /// Integrate a loaded program *without* running its entry body — the setup the
177    /// test runner needs before it drives individual `test`-prefixed functions with
178    /// [`call_entry_function`]. A module constant initializer that failed during
179    /// integration surfaces here, just as it would when the entry runs.
180    pub fn prepare(&mut self, program: Arc<dc::Program>) -> DogeResult<()> {
181        self.program = Some(program.clone());
182        self.integrate_program(&program);
183        if let Some(err) = self.pending_module_error.take() {
184            return Err(err);
185        }
186        // The entry file's own constants aren't in `init_order`, so initialize them
187        // here — tests read them exactly as module constants are read.
188        let entry_consts = program.files[0].script.stmts.clone();
189        self.init_module(&entry_consts, 0)
190    }
191
192    /// The file id and line the interpreter last executed — the site of an uncaught
193    /// error, for the caller to render a doge-flavored location.
194    pub fn error_site(&self) -> (usize, u32) {
195        (self.cur_fid as usize, self.cur_line)
196    }
197
198    /// A fresh session with only the runtime natives (builtins + stdlib) registered.
199    pub fn new() -> Interp {
200        let mut interp = Interp {
201            callables: Vec::new(),
202            classes: Vec::new(),
203            file_scopes: vec![FileScope {
204                globals: scope(),
205                imports: HashMap::new(),
206            }],
207            file_funcs: vec![HashMap::new()],
208            file_class_ids: vec![HashMap::new()],
209            builtin_ids: HashMap::new(),
210            module_fn_ids: HashMap::new(),
211            file_paths: vec![Rc::from("<repl>")],
212            depth: 0,
213            cur_fid: 0,
214            cur_line: 0,
215            current_method_class: None,
216            pending_module_error: None,
217            session_consts: Vec::new(),
218            program: None,
219        };
220        interp.register_natives();
221        interp
222    }
223
224    // ----- file-scope helpers -----
225
226    fn globals(&self, fid: u32) -> Scope {
227        self.file_scopes[fid as usize].globals.clone()
228    }
229
230    fn import_ref(&self, fid: u32, name: &str) -> Option<ModuleRef> {
231        self.file_scopes[fid as usize].imports.get(name).copied()
232    }
233
234    /// Look up a name in a call frame, then the file's globals.
235    fn lookup(&self, frame: &Scope, fid: u32, name: &str) -> Option<Cell> {
236        if let Some(c) = frame.borrow().get(name) {
237            return Some(c.clone());
238        }
239        self.file_scopes[fid as usize]
240            .globals
241            .borrow()
242            .get(name)
243            .cloned()
244    }
245
246    /// The class defined as `name` in file `fid`, if any.
247    fn class_id_in(&self, fid: u32, name: &str) -> Option<u32> {
248        self.file_class_ids[fid as usize].get(name).copied()
249    }
250
251    /// Track the current source location so an uncaught or caught error reports
252    /// the file and line it was raised at, exactly as the compiled program's
253    /// `cur_file`/`cur_line` do.
254    fn mark(&mut self, fid: u32, span: dc::Span) {
255        self.cur_fid = fid;
256        self.cur_line = span.line;
257    }
258
259    /// The path of the file whose code is currently executing, for error values.
260    fn cur_path(&self) -> Rc<str> {
261        self.file_paths
262            .get(self.cur_fid as usize)
263            .cloned()
264            .unwrap_or_else(|| Rc::from("<repl>"))
265    }
266
267    // ----- program integration -----
268
269    /// Fold a loaded program's files into the session: create each file's scope
270    /// and imports, analyze every definition into the callable/class tables, hoist
271    /// top-level functions and classes, and initialize module constants.
272    fn integrate_program(&mut self, program: &dc::Program) {
273        // File 0 already has a scope (the session's entry/globals); add the rest.
274        for file in &program.files {
275            let fid = file.file_id as usize;
276            while self.file_scopes.len() <= fid {
277                self.file_scopes.push(FileScope {
278                    globals: scope(),
279                    imports: HashMap::new(),
280                });
281                self.file_funcs.push(HashMap::new());
282                self.file_class_ids.push(HashMap::new());
283                self.file_paths.push(Rc::from("<repl>"));
284            }
285            self.file_paths[fid] = Rc::from(file.path.as_str());
286            self.resolve_imports(file);
287        }
288        for file in &program.files {
289            self.analyze_file(&file.script.stmts, file.file_id);
290            self.hoist_file(&file.script.stmts, file.file_id);
291        }
292        // Module constants first, in dependency order, so a module that reads
293        // another's constant finds it ready — then the entry runs inline later.
294        for &fid in &program.init_order {
295            let stmts = program.files[fid as usize].script.stmts.clone();
296            if let Err(err) = self.init_module(&stmts, fid) {
297                // A module's constant initializer failing is a program error; it
298                // surfaces when the entry runs, matching the compiled init order.
299                self.pending_module_error = Some(err);
300                break;
301            }
302        }
303    }
304
305    /// Resolve one file's `so` imports into its scope's import map.
306    fn resolve_imports(&mut self, file: &dc::ProgramFile) {
307        let fid = file.file_id as usize;
308        for (name, module) in &file.stdlib_imports {
309            self.file_scopes[fid]
310                .imports
311                .insert(name.clone(), ModuleRef::Stdlib(module));
312        }
313        for (name, target) in &file.user_imports {
314            self.file_scopes[fid]
315                .imports
316                .insert(name.clone(), ModuleRef::User(*target));
317        }
318    }
319
320    /// Bind a file's top-level functions to function values and pre-bind its
321    /// hoisted variable names to `none`, mirroring the compiler's `Env` fields.
322    fn hoist_file(&mut self, stmts: &[dc::Stmt], fid: u32) {
323        let globals = self.globals(fid);
324        for name in dc::hoisted_names(stmts) {
325            globals
326                .borrow_mut()
327                .entry(name)
328                .or_insert_with(|| cell(Value::None));
329        }
330        self.hoist_functions(stmts, &globals, fid);
331    }
332
333    /// Run one file's constant initializers in its own scope (a module during
334    /// integration, or the entry file when [`prepare`](Self::prepare) sets up tests).
335    fn init_module(&mut self, stmts: &[dc::Stmt], fid: u32) -> DogeResult<()> {
336        let globals = self.globals(fid);
337        for stmt in stmts {
338            if matches!(stmt, dc::Stmt::ConstDecl { .. }) {
339                self.exec_stmt(stmt, &globals, fid)?;
340            }
341        }
342        Ok(())
343    }
344
345    /// Execute the entry file's top-level statements (skipping definitions and
346    /// imports, which are already integrated), returning any uncaught error.
347    fn run_entry(&mut self, program: &dc::Program) -> DogeResult<()> {
348        if let Some(err) = self.pending_module_error.take() {
349            return Err(err);
350        }
351        let entry = &program.files[0];
352        let globals = self.globals(0);
353        for stmt in &entry.script.stmts {
354            if matches!(
355                stmt,
356                dc::Stmt::FuncDef { .. } | dc::Stmt::ObjDef { .. } | dc::Stmt::Import { .. }
357            ) {
358                continue;
359            }
360            match self.exec_stmt(stmt, &globals, 0)? {
361                Flow::Normal => {}
362                // `return`/`bork`/`continue` at the top level are rejected by the
363                // checker, so reaching here would be a checked-away impossibility.
364                _ => break,
365            }
366        }
367        Ok(())
368    }
369
370    // ----- REPL session -----
371
372    /// The session's accumulated top-level scope, for seeding the checker of the
373    /// next snippet. Built from live interpreter state so the checker and runtime
374    /// never disagree about what is in scope.
375    pub fn session_scope(&self) -> SessionScope {
376        let mut globals: Vec<String> = self.file_scopes[0]
377            .globals
378            .borrow()
379            .keys()
380            .cloned()
381            .collect();
382        globals.extend(self.file_scopes[0].imports.keys().cloned());
383        globals.extend(self.file_class_ids[0].keys().cloned());
384        let classes = self.file_class_ids[0]
385            .iter()
386            .map(|(name, id)| {
387                let data = &self.classes[*id as usize];
388                ClassInfo {
389                    name: name.clone(),
390                    parent: data.parent.map(|p| self.classes[p as usize].name.clone()),
391                    methods: data.methods.keys().cloned().collect(),
392                }
393            })
394            .collect();
395        SessionScope {
396            globals,
397            consts: self.session_consts.clone(),
398            classes,
399        }
400    }
401
402    /// Evaluate one checked REPL snippet in the session: integrate its definitions,
403    /// run its statements, and return the value of a trailing bare expression for
404    /// the prompt to echo (`None` when the last statement is not a bare expression).
405    pub fn eval_snippet(&mut self, path: &str, script: &dc::Script) -> DogeResult<Option<Value>> {
406        self.file_paths[0] = Rc::from(path);
407        for stmt in &script.stmts {
408            if let dc::Stmt::Import { module, .. } = stmt {
409                self.register_repl_import(module)?;
410            }
411        }
412        self.analyze_file(&script.stmts, 0);
413        self.hoist_file(&script.stmts, 0);
414
415        let globals = self.globals(0);
416        let count = script.stmts.len();
417        for (i, stmt) in script.stmts.iter().enumerate() {
418            match stmt {
419                dc::Stmt::FuncDef { .. } | dc::Stmt::ObjDef { .. } | dc::Stmt::Import { .. } => {}
420                dc::Stmt::ExprStmt { expr } if i + 1 == count => {
421                    self.mark(0, expr.span());
422                    return Ok(Some(self.eval(expr, &globals, 0)?));
423                }
424                dc::Stmt::ConstDecl { name, .. } => {
425                    self.exec_stmt(stmt, &globals, 0)?;
426                    if !self.session_consts.iter().any(|c| c == name) {
427                        self.session_consts.push(name.clone());
428                    }
429                }
430                _ => {
431                    self.exec_stmt(stmt, &globals, 0)?;
432                }
433            }
434        }
435        Ok(None)
436    }
437
438    /// Register a `so` import encountered in a REPL snippet. Stdlib modules bind
439    /// into the entry scope; user modules are only available when running a file.
440    fn register_repl_import(&mut self, module: &str) -> DogeResult<()> {
441        if self.file_scopes[0].imports.contains_key(module) {
442            return Ok(());
443        }
444        match dc::stdlib_module(module) {
445            Some(m) => {
446                self.file_scopes[0]
447                    .imports
448                    .insert(module.to_string(), ModuleRef::Stdlib(m));
449                Ok(())
450            }
451            None => Err(DogeError::new(
452                ErrorKind::ValueError,
453                format!(
454                    "user modules aren't available in the repl yet — run the file instead (doge bark <script>.doge) to use {module}"
455                ),
456            )),
457        }
458    }
459}