1use indexmap::IndexMap;
11
12use crate::error::{Diagnostic, Severity};
13use crate::lexer::token::StrPart;
14use crate::lexer::Lexer;
15use crate::parser::ast::*;
16use crate::parser::Parser;
17use crate::span::Span;
18
19const BUILTINS: &[&str] = &["env", "read_file", "fail", "range"];
20const EFFECTFUL: &[&str] = &["env", "read_file"];
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23enum DeclKind {
24 Var,
25 Param,
27 Import,
28 Func,
29 Type,
30}
31
32struct Decl {
33 span: Span,
34 used: bool,
35 kind: DeclKind,
36}
37
38pub struct SemanticAnalyzer<'a> {
39 scopes: Vec<IndexMap<&'a str, Decl>>,
40 diags: Vec<Diagnostic>,
41 is_root: bool,
42 hermetic: bool,
44}
45
46pub fn analyze<'a>(module: &Module<'a>, is_root: bool) -> Vec<Diagnostic> {
48 analyze_with(module, is_root, false)
49}
50
51pub fn analyze_with<'a>(module: &Module<'a>, is_root: bool, hermetic: bool) -> Vec<Diagnostic> {
58 let mut a = SemanticAnalyzer {
59 scopes: Vec::new(),
60 diags: Vec::new(),
61 is_root,
62 hermetic,
63 };
64 a.push_scope();
65 for imp in &module.imports {
66 a.declare(imp.alias, imp.span, DeclKind::Import, false);
67 }
68 for stmt in &module.stmts {
69 a.walk_stmt(stmt);
70 }
71 a.pop_scope();
72 a.diags
73}
74
75pub fn has_blocking(diags: &[Diagnostic], strict: bool) -> bool {
77 diags
78 .iter()
79 .any(|d| strict || d.severity == Severity::Error)
80}
81
82impl<'a> SemanticAnalyzer<'a> {
83 fn push_scope(&mut self) {
84 self.scopes.push(IndexMap::new());
85 }
86
87 fn pop_scope(&mut self) {
89 let scope = self.scopes.pop().expect("scope stack underflow");
90 for (name, decl) in scope {
91 if decl.used {
92 continue;
93 }
94 let (code, what) = match decl.kind {
95 DeclKind::Var => ("W0501", "variable"),
96 DeclKind::Import => ("W0502", "import"),
97 DeclKind::Func => ("W0503", "function"),
98 DeclKind::Type => ("W0503", "type"),
99 DeclKind::Param => continue,
100 };
101 self.diags.push(Diagnostic::warning(
102 code,
103 format!("unused {what} '{name}'"),
104 decl.span,
105 "never used",
106 ));
107 }
108 }
109
110 fn declare(&mut self, name: &'a str, span: Span, kind: DeclKind, shadow: bool) {
111 let in_current = self.scopes.last().is_some_and(|s| s.contains_key(name));
112 if in_current {
113 self.diags.push(Diagnostic::error(
114 "E0301",
115 format!("'{name}' is already defined in this scope"),
116 span,
117 "duplicate definition",
118 ));
119 return;
120 }
121 let outer = self.lookup_span(name);
122 match (shadow, outer) {
123 (false, Some(orig)) if kind == DeclKind::Var => {
124 let mut d = Diagnostic::error(
125 "E0302",
126 format!("'{name}' shadows an outer variable"),
127 span,
128 "add `shadow`",
129 );
130 d.secondary
131 .push((orig, "outer variable declared here".to_string()));
132 d.help = Some(format!(
133 "write `shadow {name} = ...` to make the shadowing explicit"
134 ));
135 self.diags.push(d);
136 }
137 (true, None) => {
138 self.diags.push(Diagnostic::warning(
139 "W0303",
140 format!("`shadow` on '{name}' shadows nothing"),
141 span,
142 "remove `shadow`",
143 ));
144 }
145 _ => {}
146 }
147 self.scopes.last_mut().unwrap().insert(
148 name,
149 Decl {
150 span,
151 used: false,
152 kind,
153 },
154 );
155 }
156
157 fn lookup_span(&self, name: &str) -> Option<Span> {
158 self.scopes
159 .iter()
160 .rev()
161 .skip(1)
162 .find_map(|s| s.get(name).map(|d| d.span))
163 }
164
165 fn mark_used(&mut self, name: &str, span: Span) {
167 for scope in self.scopes.iter_mut().rev() {
168 if let Some(decl) = scope.get_mut(name) {
169 decl.used = true;
170 return;
171 }
172 }
173 if !BUILTINS.contains(&name) {
174 self.diags.push(Diagnostic::error(
175 "E0504",
176 format!("use of undefined variable '{name}'"),
177 span,
178 "not found in any scope",
179 ));
180 }
181 }
182
183 fn walk_stmt(&mut self, stmt: &Stmt<'a>) {
184 match stmt {
185 Stmt::Assign {
186 name,
187 shadow,
188 value,
189 span,
190 } => {
191 self.walk_expr(value); self.declare(name, *span, DeclKind::Var, *shadow);
193 }
194 Stmt::Property { value, .. } => self.walk_expr(value),
195 Stmt::Assert { cond, message, .. } => {
196 self.walk_expr(cond);
197 if let Some(m) = message {
198 self.walk_expr(m);
199 }
200 }
201 Stmt::EnumDecl(en) => {
202 self.declare(en.name, en.span, DeclKind::Type, false);
203 if en.public {
205 self.mark_used(en.name, en.span);
206 }
207 }
208 Stmt::TypeDecl(schema) => {
209 for f in &schema.fields {
210 if let TypeName::Custom(name) = f.ty {
211 self.mark_used(name, schema.span);
212 }
213 if let Some(default) = &f.default {
215 self.walk_expr(default);
216 }
217 }
218 self.declare(schema.name, schema.span, DeclKind::Type, false);
219 if schema.public {
221 self.mark_used(schema.name, schema.span);
222 }
223 }
224 Stmt::FuncDecl {
225 name,
226 params,
227 body,
228 public,
229 span,
230 } => {
231 self.declare(name, *span, DeclKind::Func, false);
232 if *public {
234 self.mark_used(name, *span);
235 }
236 self.push_scope();
237 for p in params {
238 self.declare(p, *span, DeclKind::Param, false);
239 }
240 self.walk_stmt_body(body);
241 self.pop_scope();
242 }
243 Stmt::Block(block) => self.walk_block(block),
244 Stmt::Expr(e) => self.walk_expr(e),
245 }
246 }
247
248 fn walk_block(&mut self, block: &BlockDeclaration<'a>) {
249 self.walk_expr(&block.label);
250 self.push_scope();
251 for stmt in &block.body {
252 self.walk_stmt(stmt);
253 }
254 self.pop_scope();
255 }
256
257 fn walk_stmt_body(&mut self, body: &[Stmt<'a>]) {
259 for stmt in body {
260 self.walk_stmt(stmt);
261 }
262 }
263
264 fn walk_object_body(&mut self, body: &ObjectBody<'a>) {
265 for (_, value, _) in &body.props {
266 self.walk_expr(value);
267 }
268 }
269
270 fn walk_expr(&mut self, e: &Expr<'a>) {
271 match e {
272 Expr::Literal(LitValue::InterpStr(parts), span) => {
273 for part in parts {
274 if let StrPart::Interp(src) = part {
275 self.walk_interp(src, *span);
276 }
277 }
278 }
279 Expr::Literal(..) => {}
280 Expr::Variable(name, span) => self.mark_used(name, *span),
281 Expr::Unary { rhs, .. } => self.walk_expr(rhs),
282 Expr::Binary { lhs, rhs, .. } => {
283 self.walk_expr(lhs);
284 self.walk_expr(rhs);
285 }
286 Expr::Ternary {
287 cond,
288 then,
289 otherwise,
290 ..
291 } => {
292 self.walk_expr(cond);
293 self.walk_expr(then);
294 self.walk_expr(otherwise);
295 }
296 Expr::Cond {
297 arms, otherwise, ..
298 } => {
299 for (condition, value) in arms {
300 self.walk_expr(condition);
301 self.walk_expr(value);
302 }
303 self.walk_expr(otherwise);
304 }
305 Expr::Call { callee, args, span } => {
306 if let Expr::Variable(name, _) = callee.as_ref() {
308 if self.hermetic && EFFECTFUL.contains(name) {
313 let mut d = Diagnostic::error(
314 "E0505",
315 format!("{name}() is not allowed in hermetic mode"),
316 *span,
317 "no I/O is permitted here",
318 );
319 d.help = Some(format!(
320 "pass the value in from the host, or drop --hermetic to grant {name}() explicitly"
321 ));
322 self.diags.push(d);
323 }
324 if !self.is_root && EFFECTFUL.contains(name) {
325 self.diags.push(Diagnostic::warning(
326 "W0512",
327 format!("effectful call {name}() in imported module"),
328 *span,
329 "imports have no I/O capability by default",
330 ));
331 }
332 }
333 self.walk_expr(callee);
334 for a in args {
335 self.walk_expr(a);
336 }
337 }
338 Expr::MethodCall {
339 recv, args, lambda, ..
340 } => {
341 self.walk_expr(recv);
342 for a in args {
343 self.walk_expr(a);
344 }
345 if let Some(l) = lambda {
346 self.walk_expr(l);
347 }
348 }
349 Expr::FieldAccess { recv, .. } => self.walk_expr(recv),
350 Expr::Index { recv, key, .. } => {
351 self.walk_expr(recv);
352 self.walk_expr(key);
353 }
354 Expr::ObjectLiteral(body) => self.walk_object_body(body),
355 Expr::ListLiteral(items, _) => {
356 for i in items {
357 self.walk_expr(i);
358 }
359 }
360 Expr::Lambda { params, body, span } => {
361 self.push_scope();
362 for p in params {
363 self.declare(p, *span, DeclKind::Param, false);
364 }
365 match body {
366 LambdaBody::Expr(e) => self.walk_expr(e),
367 LambdaBody::Block(b) => self.walk_stmt_body(b),
368 }
369 self.pop_scope();
370 }
371 Expr::SchemaInstance {
372 schema,
373 schema_alias,
374 body,
375 span,
376 } => {
377 match schema_alias {
379 Some(alias) => self.mark_used(alias, *span),
380 None => self.mark_used(schema, *span),
381 }
382 self.walk_object_body(body);
383 }
384 }
385 }
386
387 fn walk_interp(&mut self, src: &'a str, span: Span) {
390 let parsed = Lexer::new(src, span.source)
391 .tokenize()
392 .and_then(|toks| Parser::new(toks).parse_expression());
393 match parsed {
394 Ok(expr) => self.walk_expr(&expr),
395 Err(d) => self.diags.push(Diagnostic::error(
396 "E0316",
397 format!("invalid interpolation: {}", d.message),
398 span,
399 "in #{...}",
400 )),
401 }
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 fn diags(src: &str) -> Vec<Diagnostic> {
410 diags_as(src, true)
411 }
412
413 fn diags_as(src: &str, is_root: bool) -> Vec<Diagnostic> {
414 let toks = Lexer::new(src, 0).tokenize().expect("lex ok");
415 let module = Parser::new(toks).parse_module().expect("parse ok");
416 analyze(&module, is_root)
417 }
418
419 fn codes(src: &str) -> Vec<&'static str> {
420 diags(src).into_iter().map(|d| d.code).collect()
421 }
422
423 #[test]
424 fn dead_code_detection() {
425 assert_eq!(codes("x = 1"), vec!["W0501"]);
426 assert_eq!(
427 codes("import \"a.aura\" as a\nx = 1\ny = x"),
428 vec!["W0502", "W0501"]
429 ); assert_eq!(codes("type T\n a: Int\nend"), vec!["W0503"]);
431 assert_eq!(codes("def f(x)\n a: x\nend"), vec!["W0503"]);
432 assert!(codes("x = 1\ny = x + 1\nassert y > 0").is_empty());
434 }
435
436 #[test]
437 fn undefined_variable_is_e0504() {
438 assert!(codes("x = nope + 1").contains(&"E0504"));
439 assert!(!codes("assert fail == fail").contains(&"E0504"));
441 }
442
443 #[test]
444 fn static_shadow_rules() {
445 assert!(codes("x = 1\ndomain \"d\"\n x = 2\nend").contains(&"E0302"));
446 assert!(codes("x = 1\nx = 2").contains(&"E0301"));
447 assert!(codes("shadow x = 1\nassert x == 1").contains(&"W0303"));
448 let src = "x = 1\ndomain \"d\"\n shadow x = 2\n assert x == 2\nend\nassert x == 1";
450 assert!(codes(src).is_empty());
451 }
452
453 #[test]
454 fn interpolation_uses_are_counted() {
455 assert!(codes("x = 1\ns = \"v#{x}\"\nassert s == \"v1\"").is_empty());
457 assert!(codes("s = \"v#{nope}\"\nassert s == \"\"").contains(&"E0504"));
459 }
460
461 #[test]
462 fn effectful_call_in_import_is_w0512() {
463 let src = "x = env(\"A\", \"b\")\nassert x == x";
464 assert!(!diags_as(src, true).iter().any(|d| d.code == "W0512"));
465 assert!(diags_as(src, false).iter().any(|d| d.code == "W0512"));
466 }
467
468 #[test]
469 fn strict_upgrades_warnings_to_blocking() {
470 let ds = diags("x = 1");
471 assert!(!has_blocking(&ds, false));
472 assert!(has_blocking(&ds, true));
473 }
474}