pub(super) use std::collections::{HashMap, HashSet};
pub(super) use crate::ast::{for_each_child_block, Expr, InterpPart, Params, Script, Stmt};
pub(super) use crate::diagnostics::Diagnostic;
pub(super) use crate::modules::Program;
pub(super) use crate::token::Span;
mod scopes;
mod stmt;
#[cfg(test)]
mod tests;
pub fn check_program(program: &Program) -> Result<(), Diagnostic> {
for file in &program.files {
check(&file.path, &file.source, &file.script)?;
if !file.is_entry {
check_module_defs_only(&file.path, &file.source, &file.script)?;
}
}
Ok(())
}
fn check_module_defs_only(path: &str, source: &str, script: &Script) -> Result<(), Diagnostic> {
let lines = crate::diagnostics::split_source_lines(source);
let make = |span: Span, message: String| {
let source_line = crate::diagnostics::source_line(&lines, span.line);
Diagnostic::new(path, span.line, span.col, source_line, message)
};
for stmt in &script.stmts {
match stmt {
Stmt::FuncDef { .. }
| Stmt::ObjDef { .. }
| Stmt::ConstDecl { .. }
| Stmt::Import { .. } => {}
other => {
return Err(make(
other.span(),
"a module only defines things — this statement would have to run".to_string(),
)
.with_headline("very loose. much module.")
.with_hint("wrap it in a such …: function, or move it to your main script"));
}
}
}
Ok(())
}
#[derive(Default)]
pub struct SessionScope {
pub globals: Vec<String>,
pub consts: Vec<String>,
pub classes: Vec<ClassInfo>,
}
pub struct ClassInfo {
pub name: String,
pub parent: Option<String>,
pub methods: Vec<String>,
}
impl SessionScope {
pub fn empty() -> SessionScope {
SessionScope::default()
}
}
pub fn check(path: &str, source: &str, script: &Script) -> Result<(), Diagnostic> {
check_snippet(path, source, script, &SessionScope::empty())
}
pub fn check_snippet(
path: &str,
source: &str,
script: &Script,
session: &SessionScope,
) -> Result<(), Diagnostic> {
let lines = crate::diagnostics::split_source_lines(source);
let mut checker = Checker {
path: path.to_string(),
lines,
globals: session.globals.iter().cloned().collect(),
consts: session.consts.iter().cloned().collect(),
classes: session
.classes
.iter()
.map(|c| {
(
c.name.clone(),
ClassSig {
parent: c.parent.clone(),
methods: c.methods.iter().cloned().collect(),
span: Span { line: 0, col: 0 },
},
)
})
.collect(),
};
for stmt in &script.stmts {
match stmt {
Stmt::FuncDef { name, .. } => {
checker.globals.insert(name.clone());
}
Stmt::ObjDef {
name,
parent,
methods,
span,
} => {
checker.globals.insert(name.clone());
let method_names = methods
.iter()
.filter_map(|m| match m {
Stmt::FuncDef { name, .. } => Some(name.clone()),
_ => None,
})
.collect();
checker.classes.insert(
name.clone(),
ClassSig {
parent: parent.clone(),
methods: method_names,
span: *span,
},
);
}
Stmt::Decl { names, rest, .. } => {
for name in names {
checker.globals.insert(name.clone());
}
if let Some(rest) = rest {
checker.globals.insert(rest.clone());
}
}
Stmt::Import { module, .. } => {
checker.globals.insert(module.clone());
}
Stmt::ConstDecl { name, .. } => {
checker.globals.insert(name.clone());
checker.consts.insert(name.clone());
}
Stmt::Assign { .. }
| Stmt::Bark { .. }
| Stmt::If { .. }
| Stmt::For { .. }
| Stmt::While { .. }
| Stmt::Try { .. }
| Stmt::Return { .. }
| Stmt::Bonk { .. }
| Stmt::Amaze { .. }
| Stmt::Bork { .. }
| Stmt::Continue { .. }
| Stmt::ExprStmt { .. } => {}
}
}
checker.check_unique_toplevel(script)?;
checker.check_inheritance()?;
let mut ctx = Ctx {
locals: session.globals.iter().cloned().collect(),
in_function: false,
class: None,
loop_depth: 0,
};
checker.check_stmts(&script.stmts, &mut ctx)
}
struct Checker {
path: String,
lines: Vec<String>,
globals: HashSet<String>,
consts: HashSet<String>,
classes: HashMap<String, ClassSig>,
}
struct ClassSig {
parent: Option<String>,
methods: HashSet<String>,
span: Span,
}
struct Ctx {
locals: HashSet<String>,
in_function: bool,
class: Option<String>,
loop_depth: usize,
}
impl Checker {
fn name_clash(&self, span: Span, message: String) -> Diagnostic {
self.diag(span, message)
.with_headline("very twice. much name.")
.with_hint("pick a different name")
}
fn method_clash(&self, span: Span, message: String) -> Diagnostic {
self.diag(span, message)
.with_headline("very twice. much name.")
.with_hint("pick a different name for the method")
}
fn check_inheritance(&self) -> Result<(), Diagnostic> {
let mut names: Vec<&String> = self.classes.keys().collect();
names.sort();
for name in &names {
let sig = &self.classes[*name];
if let Some(parent) = &sig.parent {
if !self.classes.contains_key(parent) {
return Err(self
.diag(
sig.span,
format!("{name} inherits from {parent}, which is not a class here"),
)
.with_headline("very parent. much unknown.")
.with_hint(format!(
"define many {parent}: in this file, or fix the name"
)));
}
}
}
for name in &names {
let mut chain = vec![name.as_str()];
let mut cur = self.classes[*name].parent.as_deref();
let mut guard = 0;
while let Some(c) = cur {
chain.push(c);
if c == name.as_str() {
let sig = &self.classes[*name];
return Err(self
.diag(
sig.span,
format!("these classes inherit in a loop: {}", chain.join(" → ")),
)
.with_headline("very loop. much family.")
.with_hint("break the cycle — a class cannot be its own ancestor"));
}
guard += 1;
if guard > self.classes.len() {
break;
}
cur = self.classes.get(c).and_then(|s| s.parent.as_deref());
}
}
Ok(())
}
fn check_super(&self, method: &str, ctx: &Ctx, span: Span) -> Result<(), Diagnostic> {
let Some(class) = &ctx.class else {
return Err(self
.diag(span, "super only works inside a method")
.with_headline("very super. much lost.")
.with_hint("use super inside a method of a class with a parent"));
};
let sig = self
.classes
.get(class)
.expect("compiler bug: a method's class is always known");
let Some(parent) = &sig.parent else {
return Err(self
.diag(span, format!("{class} has no parent to call super on"))
.with_headline("very super. much orphan.")
.with_hint(format!("give it a parent — many {class} much Parent:")));
};
let mut cur = Some(parent.as_str());
let mut guard = 0;
while let Some(c) = cur {
let Some(csig) = self.classes.get(c) else {
break;
};
if csig.methods.contains(method) {
return Ok(());
}
guard += 1;
if guard > self.classes.len() {
break;
}
cur = csig.parent.as_deref();
}
Err(self
.diag(span, format!("no parent of {class} has a method {method}"))
.with_headline("very super. much unknown.")
.with_hint(format!("check the method name — super.{method}(…)")))
}
fn in_scope(&self, name: &str, ctx: &Ctx) -> bool {
crate::builtins::is_builtin(name)
|| ctx.locals.contains(name)
|| (ctx.in_function && self.globals.contains(name))
}
fn undeclared_assign(&self, name: &str, span: Span) -> Diagnostic {
self.diag(
span,
format!("cannot assign to {name} before it is declared"),
)
.with_headline("very undeclared. much assign.")
.with_hint(format!("declare it first — such {name} = …"))
}
fn diag(&self, span: Span, message: impl Into<String>) -> Diagnostic {
let source_line = crate::diagnostics::source_line(&self.lines, span.line);
Diagnostic::new(&self.path, span.line, span.col, source_line, message)
}
}