1use std::collections::HashMap;
13
14use crate::lexer::token::StrPart;
15use crate::lexer::{Lexer, Token, TokenKind};
16use crate::parser::ast::*;
17use crate::parser::Parser;
18use crate::span::Span;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum BindingKind {
22 Var,
23 Param,
24 Import,
25 Func,
26 Type,
28}
29
30#[derive(Debug, Clone)]
31pub struct Binding {
32 pub name: String,
33 pub decl: Span,
35 pub kind: BindingKind,
36 pub scope: Span,
39 pub public: bool,
42}
43
44#[derive(Debug, Clone)]
46pub struct Use {
47 pub span: Span,
48 pub binding: Option<usize>,
50}
51
52#[derive(Debug, Default)]
53pub struct Resolution {
54 pub bindings: Vec<Binding>,
55 pub uses: Vec<Use>,
56}
57
58impl Resolution {
59 pub fn binding_at(&self, offset: u32) -> Option<usize> {
62 let covers = |s: &Span| s.start <= offset && offset <= s.end;
63 if let Some(i) = self.bindings.iter().position(|b| covers(&b.decl)) {
64 return Some(i);
65 }
66 self.uses
67 .iter()
68 .find(|u| covers(&u.span))
69 .and_then(|u| u.binding)
70 }
71
72 pub fn occurrences(&self, binding: usize) -> Vec<Span> {
74 let mut out: Vec<Span> = self
75 .uses
76 .iter()
77 .filter(|u| u.binding == Some(binding))
78 .map(|u| u.span)
79 .collect();
80 out.push(self.bindings[binding].decl);
81 out.sort_by_key(|s| (s.start, s.end));
82 out.dedup_by_key(|s| (s.start, s.end));
83 out
84 }
85
86 pub fn conflicting(&self, name: &str, scope: Span) -> Vec<&Binding> {
89 self.bindings
90 .iter()
91 .filter(|b| b.name == name && b.scope.start <= scope.end && scope.start <= b.scope.end)
92 .collect()
93 }
94}
95
96pub fn resolve(src: &str, module: &Module<'_>) -> Resolution {
99 let toks = Lexer::new(src, module.span.source)
100 .tokenize()
101 .unwrap_or_default();
102 let mut r = Resolver {
103 src,
104 toks,
105 out: Resolution::default(),
106 scopes: Vec::new(),
107 bias: 0,
108 };
109 r.push(module.span);
110 for imp in &module.imports {
111 let decl = r.alias_span(imp).unwrap_or(imp.span);
113 r.declare(imp.alias, decl, BindingKind::Import);
114 }
115 for stmt in &module.stmts {
116 r.walk_stmt(stmt);
117 }
118 r.pop();
119 r.out
120}
121
122struct Scope<'a> {
124 range: Span,
125 decls: HashMap<&'a str, Vec<usize>>,
130}
131
132struct Resolver<'a> {
133 src: &'a str,
134 toks: Vec<Token<'a>>,
135 out: Resolution,
136 scopes: Vec<Scope<'a>>,
137 bias: u32,
140}
141
142impl<'a> Resolver<'a> {
143 fn push(&mut self, range: Span) {
144 self.scopes.push(Scope {
145 range,
146 decls: HashMap::new(),
147 });
148 }
149
150 fn pop(&mut self) {
151 self.scopes.pop();
152 }
153
154 fn declare(&mut self, name: &'a str, decl: Span, kind: BindingKind) {
155 self.declare_vis(name, decl, kind, false)
156 }
157
158 fn declare_vis(&mut self, name: &'a str, decl: Span, kind: BindingKind, public: bool) {
159 let scope = self.scopes.last().map(|s| s.range).unwrap_or(decl);
160 self.out.bindings.push(Binding {
161 name: name.to_string(),
162 decl,
163 kind,
164 scope,
165 public,
166 });
167 let idx = self.out.bindings.len() - 1;
168 if let Some(s) = self.scopes.last_mut() {
169 s.decls.entry(name).or_default().push(idx);
170 }
171 }
172
173 fn lookup(&self, name: &str, at: u32) -> Option<usize> {
177 for scope in self.scopes.iter().rev() {
178 let Some(candidates) = scope.decls.get(name) else {
179 continue;
180 };
181 let mut fallback = None;
182 for &i in candidates.iter().rev() {
183 if self.out.bindings[i].decl.start <= at {
184 return Some(i);
185 }
186 fallback = Some(i);
187 }
188 if let Some(i) = fallback {
189 return Some(i);
190 }
191 }
192 None
193 }
194
195 fn use_of(&mut self, name: &str, span: Span) {
196 let span = Span {
197 source: span.source,
198 start: span.start + self.bias,
199 end: span.end + self.bias,
200 };
201 let binding = self.lookup(name, span.start);
202 self.out.uses.push(Use { span, binding });
203 }
204
205 fn toks_in(&self, range: Span) -> impl Iterator<Item = (usize, &Token<'a>)> {
217 let first = self.toks.partition_point(|t| t.span.start < range.start);
218 self.toks[first..]
219 .iter()
220 .enumerate()
221 .take_while(move |(_, t)| t.span.start <= range.end)
222 .filter(move |(_, t)| t.span.end <= range.end)
223 .map(move |(i, t)| (first + i, t))
224 }
225
226 fn name_span(&self, range: Span, name: &str) -> Option<Span> {
228 self.toks_in(range)
229 .find(|(_, t)| matches!(t.kind, TokenKind::Ident(n) if n == name))
230 .map(|(_, t)| t.span)
231 }
232
233 fn alias_span(&self, imp: &Import<'_>) -> Option<Span> {
234 let mut seen_as = false;
235 for (_, t) in self.toks_in(imp.span) {
236 if seen_as {
237 if matches!(t.kind, TokenKind::Ident(n) if n == imp.alias) {
238 return Some(t.span);
239 }
240 } else if matches!(t.kind, TokenKind::As) {
241 seen_as = true;
242 }
243 }
244 None
245 }
246
247 fn param_spans(&self, range: Span, params: &[&str]) -> Vec<Option<Span>> {
251 let mut idents: Vec<(&str, Span)> = Vec::new();
252 let mut depth = 0usize;
253 for (_, t) in self.toks_in(range) {
254 match t.kind {
255 TokenKind::LParen => depth += 1,
256 TokenKind::RParen => {
257 depth -= 1;
258 if depth == 0 {
259 break;
260 }
261 }
262 TokenKind::Ident(n) if depth == 1 => idents.push((n, t.span)),
263 _ => {}
264 }
265 }
266 params
268 .iter()
269 .enumerate()
270 .map(|(i, p)| {
271 idents
272 .get(i)
273 .filter(|(n, _)| n == p)
274 .or_else(|| idents.iter().find(|(n, _)| n == p))
275 .map(|(_, s)| *s)
276 })
277 .collect()
278 }
279
280 fn walk_stmt(&mut self, stmt: &Stmt<'a>) {
283 match stmt {
284 Stmt::Assign {
285 name, value, span, ..
286 } => {
287 self.walk_expr(value); let decl = self.name_span(*span, name).unwrap_or(*span);
289 self.declare(name, decl, BindingKind::Var);
290 }
291 Stmt::Property { value, .. } => self.walk_expr(value),
292 Stmt::Assert { cond, message, .. } => {
293 self.walk_expr(cond);
294 if let Some(m) = message {
295 self.walk_expr(m);
296 }
297 }
298 Stmt::EnumDecl(en) => {
299 let decl = self.name_span(en.span, en.name).unwrap_or(en.span);
300 self.declare_vis(en.name, decl, BindingKind::Type, en.public);
301 }
302 Stmt::TypeDecl(schema) => {
303 for f in &schema.fields {
304 if let TypeName::Custom(name) = f.ty {
306 if let Some(s) = self.field_type_span(schema, f.name, name) {
307 self.use_of(name, s);
308 }
309 }
310 if let Some(default) = &f.default {
311 self.walk_expr(default);
312 }
313 }
314 let decl = self
315 .name_span(schema.span, schema.name)
316 .unwrap_or(schema.span);
317 self.declare_vis(schema.name, decl, BindingKind::Type, schema.public);
318 }
319 Stmt::FuncDecl {
320 name,
321 params,
322 body,
323 public,
324 span,
325 } => {
326 let decl = self.name_span(*span, name).unwrap_or(*span);
327 self.declare_vis(name, decl, BindingKind::Func, *public);
328 self.push(*span);
329 for (p, s) in params.iter().zip(self.param_spans(*span, params)) {
330 self.declare(p, s.unwrap_or(*span), BindingKind::Param);
331 }
332 for s in body {
333 self.walk_stmt(s);
334 }
335 self.pop();
336 }
337 Stmt::Block(block) => {
338 self.walk_expr(&block.label);
339 self.push(block.span);
340 for s in &block.body {
341 self.walk_stmt(s);
342 }
343 self.pop();
344 }
345 Stmt::Expr(e) => self.walk_expr(e),
346 }
347 }
348
349 fn field_type_span(
353 &self,
354 schema: &SchemaDeclaration<'a>,
355 field: &str,
356 ty: &str,
357 ) -> Option<Span> {
358 let mut after_field = false;
359 for (_, t) in self.toks_in(schema.span) {
360 match t.kind {
361 TokenKind::Ident(n) if n == field && !after_field => after_field = true,
362 TokenKind::Ident(n) if after_field && n == ty => return Some(t.span),
363 TokenKind::Newline if after_field => after_field = false,
364 _ => {}
365 }
366 }
367 None
368 }
369
370 fn walk_object_body(&mut self, body: &ObjectBody<'a>) {
371 for (_, value, _) in &body.props {
372 self.walk_expr(value);
373 }
374 }
375
376 fn walk_expr(&mut self, e: &Expr<'a>) {
377 match e {
378 Expr::Literal(LitValue::InterpStr(parts), span) => {
379 for part in parts {
380 if let StrPart::Interp(sub) = part {
381 self.walk_interp(sub, *span);
382 }
383 }
384 }
385 Expr::Literal(..) => {}
386 Expr::Variable(name, span) => self.use_of(name, *span),
387 Expr::Unary { rhs, .. } => self.walk_expr(rhs),
388 Expr::Binary { lhs, rhs, .. } => {
389 self.walk_expr(lhs);
390 self.walk_expr(rhs);
391 }
392 Expr::Ternary {
393 cond,
394 then,
395 otherwise,
396 ..
397 } => {
398 self.walk_expr(cond);
399 self.walk_expr(then);
400 self.walk_expr(otherwise);
401 }
402 Expr::Cond {
403 arms, otherwise, ..
404 } => {
405 for (c, v) in arms {
406 self.walk_expr(c);
407 self.walk_expr(v);
408 }
409 self.walk_expr(otherwise);
410 }
411 Expr::Call { callee, args, .. } => {
412 self.walk_expr(callee);
413 for a in args {
414 self.walk_expr(a);
415 }
416 }
417 Expr::MethodCall {
418 recv, args, lambda, ..
419 } => {
420 self.walk_expr(recv);
422 for a in args {
423 self.walk_expr(a);
424 }
425 if let Some(l) = lambda {
426 self.walk_expr(l);
427 }
428 }
429 Expr::FieldAccess { recv, .. } => self.walk_expr(recv),
431 Expr::Index { recv, key, .. } => {
432 self.walk_expr(recv);
433 self.walk_expr(key);
434 }
435 Expr::ObjectLiteral(body) => self.walk_object_body(body),
436 Expr::ListLiteral(items, _) => {
437 for i in items {
438 self.walk_expr(i);
439 }
440 }
441 Expr::Lambda { params, body, span } => {
442 self.push(*span);
443 for (p, s) in params.iter().zip(self.param_spans(*span, params)) {
444 self.declare(p, s.unwrap_or(*span), BindingKind::Param);
445 }
446 match body {
447 LambdaBody::Expr(e) => self.walk_expr(e),
448 LambdaBody::Block(stmts) => {
449 for s in stmts {
450 self.walk_stmt(s);
451 }
452 }
453 }
454 self.pop();
455 }
456 Expr::SchemaInstance {
457 schema,
458 schema_alias,
459 body,
460 span,
461 } => {
462 let target = schema_alias.unwrap_or(schema);
464 if let Some(s) = self.name_span(*span, target) {
465 self.use_of(target, s);
466 }
467 self.walk_object_body(body);
468 }
469 }
470 }
471
472 fn walk_interp(&mut self, sub: &'a str, outer: Span) {
475 let Some(base) = subslice_offset(self.src, sub) else {
476 return;
477 };
478 let Ok(expr) = Lexer::new(sub, outer.source)
479 .tokenize()
480 .and_then(|toks| Parser::new(toks).parse_expression())
481 else {
482 return;
483 };
484 let outer_bias = self.bias;
488 self.bias = base;
489 self.walk_expr(&expr);
490 self.bias = outer_bias;
491 }
492}
493
494fn subslice_offset(outer: &str, sub: &str) -> Option<u32> {
496 let (o, s) = (outer.as_ptr() as usize, sub.as_ptr() as usize);
497 (s >= o && s + sub.len() <= o + outer.len()).then(|| (s - o) as u32)
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 fn resolved(src: &str) -> Resolution {
505 let toks = Lexer::new(src, 0).tokenize().expect("lex ok");
506 let module = Parser::new(toks).parse_module().expect("parse ok");
507 resolve(src, &module)
508 }
509
510 fn occurrences_at(src: &str, offset: u32) -> Vec<(u32, &str)> {
512 let r = resolved(src);
513 let b = r.binding_at(offset).expect("a binding under the cursor");
514 r.occurrences(b)
515 .into_iter()
516 .map(|s| (s.start, &src[s.start as usize..s.end as usize]))
517 .collect()
518 }
519
520 #[test]
521 fn same_name_in_two_scopes_is_two_bindings() {
522 let src = "x = 1\ndef f(x)\n y: x\nend\nz: x\n";
525 let outer = occurrences_at(src, 0); let inner = occurrences_at(src, 12); assert_eq!(outer.len(), 2, "decl + the use in `z: x`: {outer:?}");
528 assert_eq!(inner.len(), 2, "param + the use in `y: x`: {inner:?}");
529 for (o, _) in &outer {
531 assert!(!inner.iter().any(|(i, _)| i == o), "{outer:?} vs {inner:?}");
532 }
533 }
534
535 #[test]
536 fn shadow_is_a_distinct_binding() {
537 let src = "p = 1\ndomain \"d\"\n shadow p = 2\n a: p\nend\nb: p\n";
538 let outer = occurrences_at(src, 0);
539 let inner = occurrences_at(src, src.find("shadow p").unwrap() as u32 + 7);
540 assert_eq!(outer.len(), 2, "{outer:?}");
542 assert_eq!(inner.len(), 2, "{inner:?}");
544 let a_use = src.find("a: p").unwrap() as u32 + 3;
545 assert!(inner.iter().any(|(o, _)| *o == a_use), "{inner:?}");
546 }
547
548 #[test]
549 fn uses_inside_interpolation_are_found_at_absolute_offsets() {
550 let src = "name = \"api\"\nid: \"#{name}-1\"\n";
551 let occ = occurrences_at(src, 0);
552 let inside = src.find("#{name}").unwrap() as u32 + 2;
553 assert!(
554 occ.iter().any(|(o, t)| *o == inside && *t == "name"),
555 "interpolated use must be an occurrence: {occ:?}"
556 );
557 }
558
559 #[test]
560 fn lambda_params_and_body_bindings() {
561 let src = "xs = [1]\nys: xs.map (v, i) ->\n d = v * 2\n out: d + i\nend\n";
562 let v = src.find("(v, i)").unwrap() as u32 + 1;
563 let occ = occurrences_at(src, v);
564 assert_eq!(
565 occ.len(),
566 2,
567 "param `v` and its use in `d = v * 2`: {occ:?}"
568 );
569 let d = src.find("d = ").unwrap() as u32;
570 assert_eq!(occurrences_at(src, d).len(), 2, "`d` decl + use");
571 }
572
573 #[test]
574 fn field_names_and_methods_are_not_bindings() {
575 let src = "type E\n host: String\nend\ne: new E\n host: \"h\"\nend\n";
578 let r = resolved(src);
579 assert!(
580 r.bindings.iter().all(|b| b.name != "host"),
581 "{:?}",
582 r.bindings
583 );
584 let e = src.find("type E").unwrap() as u32 + 5;
586 assert_eq!(occurrences_at(src, e).len(), 2, "type decl + `new E`");
587 }
588
589 #[test]
590 fn conflicting_reports_overlapping_scopes_only() {
591 let src = "a = 1\ndef f()\n b = 2\n c: b\nend\n";
592 let r = resolved(src);
593 let a = r.binding_at(0).unwrap();
594 assert_eq!(r.conflicting("b", r.bindings[a].scope).len(), 1);
596 assert!(r.conflicting("nope", r.bindings[a].scope).is_empty());
597 }
598
599 #[test]
604 fn every_use_resolves_on_input_the_analyzer_accepts() {
605 for src in [
606 include_str!("../../../../examples/showcase/showcase.aura"),
607 include_str!("../../../../examples/showcase/lib.aura"),
608 include_str!("../../../../examples/production_deploy.aura"),
609 ] {
610 let toks = Lexer::new(src, 0).tokenize().expect("lex ok");
611 let module = Parser::new(toks).parse_module().expect("parse ok");
612 let diags = crate::analysis::analyze(&module, true);
613 let undefined: Vec<_> = diags.iter().filter(|d| d.code == "E0504").collect();
614 assert!(undefined.is_empty(), "fixture must be clean: {undefined:?}");
615
616 let r = resolve(src, &module);
617 let unresolved: Vec<&str> = r
618 .uses
619 .iter()
620 .filter(|u| u.binding.is_none())
621 .map(|u| &src[u.span.start as usize..u.span.end as usize])
622 .filter(|n| !BUILTIN_NAMES.contains(n))
623 .collect();
624 assert!(unresolved.is_empty(), "unresolved uses: {unresolved:?}");
625 }
626 }
627
628 const BUILTIN_NAMES: &[&str] = &["env", "read_file", "fail", "range"];
630}