Skip to main content

doge_interp/
lib.rs

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