Skip to main content

doge_compiler/check/
mod.rs

1pub(super) use std::collections::{HashMap, HashSet};
2
3pub(super) use crate::ast::{for_each_child_block, Expr, InterpPart, Params, Script, Stmt};
4pub(super) use crate::diagnostics::Diagnostic;
5pub(super) use crate::modules::Program;
6pub(super) use crate::token::Span;
7
8mod scopes;
9mod stmt;
10#[cfg(test)]
11mod tests;
12
13/// Check every file in a program. Each file is checked in its own scope (a
14/// module's functions see only that module's names), and a non-entry module gets
15/// the extra "defines things only" rule on top.
16pub fn check_program(program: &Program) -> Result<(), Diagnostic> {
17    for file in &program.files {
18        check(&file.path, &file.source, &file.script)?;
19        if !file.is_entry {
20            check_module_defs_only(&file.path, &file.source, &file.script)?;
21        }
22    }
23    Ok(())
24}
25
26/// A module file may only *define* things — functions, objects, constants, and
27/// imports. A loose statement would have to run at import time, which doge modules
28/// never do.
29fn check_module_defs_only(path: &str, source: &str, script: &Script) -> Result<(), Diagnostic> {
30    let lines = crate::diagnostics::split_source_lines(source);
31    let make = |span: Span, message: String| {
32        let source_line = crate::diagnostics::source_line(&lines, span.line);
33        Diagnostic::new(path, span.line, span.col, source_line, message)
34    };
35
36    for stmt in &script.stmts {
37        match stmt {
38            Stmt::FuncDef { .. }
39            | Stmt::ObjDef { .. }
40            | Stmt::ConstDecl { .. }
41            | Stmt::Import { .. } => {}
42            other => {
43                return Err(make(
44                    other.span(),
45                    "a module only defines things — this statement would have to run".to_string(),
46                )
47                .with_headline("very loose. much module.")
48                .with_hint("wrap it in a such …: function, or move it to your main script"));
49            }
50        }
51    }
52    Ok(())
53}
54
55/// The accumulated top-level scope of a REPL session: every name in scope, the
56/// subset that are constants, and the object definitions (for inheritance and
57/// `super` checks). A snippet is checked as if these were already declared, so it
58/// can reference — and redefine — anything earlier snippets introduced. Built by
59/// the interpreter from its live session state, so the checker and the runtime
60/// always agree on what exists.
61#[derive(Default)]
62pub struct SessionScope {
63    pub globals: Vec<String>,
64    pub consts: Vec<String>,
65    pub classes: Vec<ClassInfo>,
66}
67
68/// One class already defined in a REPL session: its name, the parent it inherits
69/// from (if any), and its method names.
70pub struct ClassInfo {
71    pub name: String,
72    pub parent: Option<String>,
73    pub methods: Vec<String>,
74}
75
76impl SessionScope {
77    /// An empty session — no prior declarations. Used by the batch [`check`].
78    pub fn empty() -> SessionScope {
79        SessionScope::default()
80    }
81}
82
83/// Run every semantic check over `script`. `path`/`source` are only used to
84/// render diagnostics against the original text.
85pub fn check(path: &str, source: &str, script: &Script) -> Result<(), Diagnostic> {
86    check_snippet(path, source, script, &SessionScope::empty())
87}
88
89/// Check a REPL snippet against a session's accumulated scope. Identical to
90/// [`check`] but with `session`'s names pre-declared: top-level references to
91/// earlier snippets resolve, and a snippet may redefine a name an earlier one
92/// introduced (only the current snippet is checked for duplicate definitions).
93/// Constant reassignment and undeclared-name use stay errors.
94pub fn check_snippet(
95    path: &str,
96    source: &str,
97    script: &Script,
98    session: &SessionScope,
99) -> Result<(), Diagnostic> {
100    let lines = crate::diagnostics::split_source_lines(source);
101    let mut checker = Checker {
102        path: path.to_string(),
103        lines,
104        globals: session.globals.iter().cloned().collect(),
105        consts: session.consts.iter().cloned().collect(),
106        classes: session
107            .classes
108            .iter()
109            .map(|c| {
110                (
111                    c.name.clone(),
112                    ClassSig {
113                        parent: c.parent.clone(),
114                        methods: c.methods.iter().cloned().collect(),
115                        // A prior snippet's class carries no span in this source;
116                        // it was validated when first entered, so an inheritance
117                        // error can only originate from a class in this snippet.
118                        span: Span { line: 0, col: 0 },
119                    },
120                )
121            })
122            .collect(),
123    };
124
125    // Pre-pass: every top-level name, and the top-level constants.
126    for stmt in &script.stmts {
127        match stmt {
128            Stmt::FuncDef { name, .. } => {
129                checker.globals.insert(name.clone());
130            }
131            Stmt::ObjDef {
132                name,
133                parent,
134                methods,
135                span,
136            } => {
137                checker.globals.insert(name.clone());
138                let method_names = methods
139                    .iter()
140                    .filter_map(|m| match m {
141                        Stmt::FuncDef { name, .. } => Some(name.clone()),
142                        _ => None,
143                    })
144                    .collect();
145                checker.classes.insert(
146                    name.clone(),
147                    ClassSig {
148                        parent: parent.clone(),
149                        methods: method_names,
150                        span: *span,
151                    },
152                );
153            }
154            Stmt::Decl { names, rest, .. } => {
155                for name in names {
156                    checker.globals.insert(name.clone());
157                }
158                if let Some(rest) = rest {
159                    checker.globals.insert(rest.clone());
160                }
161            }
162            Stmt::Import { module, .. } => {
163                checker.globals.insert(module.clone());
164            }
165            Stmt::ConstDecl { name, .. } => {
166                checker.globals.insert(name.clone());
167                checker.consts.insert(name.clone());
168            }
169            // Introduce no top-level name; listed explicitly so a new statement
170            // that should be a global cannot be silently skipped here.
171            Stmt::Assign { .. }
172            | Stmt::Bark { .. }
173            | Stmt::If { .. }
174            | Stmt::For { .. }
175            | Stmt::While { .. }
176            | Stmt::Try { .. }
177            | Stmt::Return { .. }
178            | Stmt::Bonk { .. }
179            | Stmt::Amaze { .. }
180            | Stmt::Bork { .. }
181            | Stmt::Continue { .. }
182            | Stmt::ExprStmt { .. } => {}
183        }
184    }
185
186    checker.check_unique_toplevel(script)?;
187    checker.check_inheritance()?;
188
189    let mut ctx = Ctx {
190        // Prior-session names are in scope at the top level from the first line;
191        // this snippet's own names are added by `check_stmt` as it reaches them,
192        // preserving declare-before-use *within* the snippet.
193        locals: session.globals.iter().cloned().collect(),
194        in_function: false,
195        class: None,
196        loop_depth: 0,
197    };
198    checker.check_stmts(&script.stmts, &mut ctx)
199}
200
201struct Checker {
202    path: String,
203    lines: Vec<String>,
204    /// All top-level names — visible from inside any function body.
205    globals: HashSet<String>,
206    /// Names bound with `so … =` — reassigning any of them is an error.
207    consts: HashSet<String>,
208    /// Object definitions in this file, by name — their parent and method names,
209    /// for validating the inheritance graph and `super` calls.
210    classes: HashMap<String, ClassSig>,
211}
212
213/// One class's inheritance-relevant signature: the parent it names (if any) and
214/// the method names it defines directly.
215struct ClassSig {
216    parent: Option<String>,
217    methods: HashSet<String>,
218    span: Span,
219}
220
221/// The scope state threaded through a single function (or the top level).
222/// Cloneable so a control-flow branch could be checked in isolation if needed;
223/// today branches share the parent scope (names leak, Python-style).
224struct Ctx {
225    /// Names declared so far in this scope (params, then local declarations).
226    locals: HashSet<String>,
227    in_function: bool,
228    /// The class whose method body is being checked, if any — set only for a
229    /// direct method body, so `super` is rejected in a plain function or a closure.
230    class: Option<String>,
231    loop_depth: usize,
232}
233
234impl Checker {
235    fn name_clash(&self, span: Span, message: String) -> Diagnostic {
236        self.diag(span, message)
237            .with_headline("very twice. much name.")
238            .with_hint("pick a different name")
239    }
240
241    fn method_clash(&self, span: Span, message: String) -> Diagnostic {
242        self.diag(span, message)
243            .with_headline("very twice. much name.")
244            .with_hint("pick a different name for the method")
245    }
246
247    /// Validate the inheritance graph: every named parent is a class defined in
248    /// this file, and no class is its own ancestor. A parent in another file is not
249    /// supported, so it reads here as an unknown parent.
250    fn check_inheritance(&self) -> Result<(), Diagnostic> {
251        let mut names: Vec<&String> = self.classes.keys().collect();
252        names.sort();
253        for name in &names {
254            let sig = &self.classes[*name];
255            if let Some(parent) = &sig.parent {
256                if !self.classes.contains_key(parent) {
257                    return Err(self
258                        .diag(
259                            sig.span,
260                            format!("{name} inherits from {parent}, which is not a class here"),
261                        )
262                        .with_headline("very parent. much unknown.")
263                        .with_hint(format!(
264                            "define many {parent}: in this file, or fix the name"
265                        )));
266                }
267            }
268        }
269        // Cycle detection: from each class, walk up the chain; returning to the
270        // start is a loop. The guard bounds a cycle that does not include the start
271        // (a later iteration reports it anchored at one of its own members).
272        for name in &names {
273            let mut chain = vec![name.as_str()];
274            let mut cur = self.classes[*name].parent.as_deref();
275            let mut guard = 0;
276            while let Some(c) = cur {
277                chain.push(c);
278                if c == name.as_str() {
279                    let sig = &self.classes[*name];
280                    return Err(self
281                        .diag(
282                            sig.span,
283                            format!("these classes inherit in a loop: {}", chain.join(" → ")),
284                        )
285                        .with_headline("very loop. much family.")
286                        .with_hint("break the cycle — a class cannot be its own ancestor"));
287                }
288                guard += 1;
289                if guard > self.classes.len() {
290                    break;
291                }
292                cur = self.classes.get(c).and_then(|s| s.parent.as_deref());
293            }
294        }
295        Ok(())
296    }
297
298    /// Validate a `super.method(…)` call: it must be inside a method whose class
299    /// has a parent, and some ancestor must define `method`.
300    fn check_super(&self, method: &str, ctx: &Ctx, span: Span) -> Result<(), Diagnostic> {
301        let Some(class) = &ctx.class else {
302            return Err(self
303                .diag(span, "super only works inside a method")
304                .with_headline("very super. much lost.")
305                .with_hint("use super inside a method of a class with a parent"));
306        };
307        let sig = self
308            .classes
309            .get(class)
310            .expect("compiler bug: a method's class is always known");
311        let Some(parent) = &sig.parent else {
312            return Err(self
313                .diag(span, format!("{class} has no parent to call super on"))
314                .with_headline("very super. much orphan.")
315                .with_hint(format!("give it a parent — many {class} much Parent:")));
316        };
317        let mut cur = Some(parent.as_str());
318        let mut guard = 0;
319        while let Some(c) = cur {
320            let Some(csig) = self.classes.get(c) else {
321                break;
322            };
323            if csig.methods.contains(method) {
324                return Ok(());
325            }
326            guard += 1;
327            if guard > self.classes.len() {
328                break;
329            }
330            cur = csig.parent.as_deref();
331        }
332        Err(self
333            .diag(span, format!("no parent of {class} has a method {method}"))
334            .with_headline("very super. much unknown.")
335            .with_hint(format!("check the method name — super.{method}(…)")))
336    }
337
338    /// Is `name` usable at this point? Locals (declared so far) and builtins are
339    /// always fine; top-level names are additionally visible inside functions.
340    fn in_scope(&self, name: &str, ctx: &Ctx) -> bool {
341        crate::builtins::is_builtin(name)
342            || ctx.locals.contains(name)
343            || (ctx.in_function && self.globals.contains(name))
344    }
345
346    fn undeclared_assign(&self, name: &str, span: Span) -> Diagnostic {
347        self.diag(
348            span,
349            format!("cannot assign to {name} before it is declared"),
350        )
351        .with_headline("very undeclared. much assign.")
352        .with_hint(format!("declare it first — such {name} = …"))
353    }
354
355    fn diag(&self, span: Span, message: impl Into<String>) -> Diagnostic {
356        let source_line = crate::diagnostics::source_line(&self.lines, span.line);
357        Diagnostic::new(&self.path, span.line, span.col, source_line, message)
358    }
359}