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
13pub 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
26fn 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#[derive(Default)]
62pub struct SessionScope {
63 pub globals: Vec<String>,
64 pub consts: Vec<String>,
65 pub classes: Vec<ClassInfo>,
66}
67
68pub struct ClassInfo {
71 pub name: String,
72 pub parent: Option<String>,
73 pub methods: Vec<String>,
74}
75
76impl SessionScope {
77 pub fn empty() -> SessionScope {
79 SessionScope::default()
80 }
81}
82
83pub fn check(path: &str, source: &str, script: &Script) -> Result<(), Diagnostic> {
86 check_snippet(path, source, script, &SessionScope::empty())
87}
88
89pub 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 span: Span { line: 0, col: 0 },
119 },
120 )
121 })
122 .collect(),
123 };
124
125 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 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 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 globals: HashSet<String>,
206 consts: HashSet<String>,
208 classes: HashMap<String, ClassSig>,
211}
212
213struct ClassSig {
216 parent: Option<String>,
217 methods: HashSet<String>,
218 span: Span,
219}
220
221struct Ctx {
225 locals: HashSet<String>,
227 in_function: bool,
228 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 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 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 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 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}