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