Skip to main content

aura_lang/resolve/
mod.rs

1//! Name resolution: which binding does each identifier occurrence refer to?
2//!
3//! The walk deliberately mirrors [`crate::analysis`], which in turn mirrors the
4//! runtime `Environment` — a test asserts the two agree on every binding, so the
5//! answers here cannot drift from what evaluation actually does. This is what
6//! makes scope-precise editor features safe: `x` in one scope and `x` in another
7//! are different bindings, and a rename must never conflate them.
8//!
9//! Consumers get exact byte spans for the declaration and for every use,
10//! including uses inside `#{...}` interpolation.
11
12use 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` or `enum` — a declared name usable as a field type or in `new`.
27    Type,
28}
29
30#[derive(Debug, Clone)]
31pub struct Binding {
32    pub name: String,
33    /// The span of the declared *name* token, not of the whole statement.
34    pub decl: Span,
35    pub kind: BindingKind,
36    /// Byte range of the scope this binding lives in; a rename's safety checks
37    /// need it to reason about capture.
38    pub scope: Span,
39    /// `pub def` / `pub type` / `pub enum` (D12) — part of the module's API, so
40    /// importers this file cannot see may depend on the name.
41    pub public: bool,
42}
43
44/// One identifier occurrence that is not a declaration.
45#[derive(Debug, Clone)]
46pub struct Use {
47    pub span: Span,
48    /// `None` for builtins (`env`, `range`, …) and undefined names.
49    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    /// The binding the identifier covering `offset` refers to — whether the
60    /// cursor sits on the declaration itself or on any use of it.
61    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    /// The declaration plus every use of `binding`, in source order.
73    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    /// Bindings of `name` whose scope overlaps `scope` — i.e. those an inner or
87    /// outer scope would capture if something were renamed to `name`.
88    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
96/// Resolve every name in `module`. `src` must be the exact text the module was
97/// parsed from: declaration spans are narrowed to the name token by re-lexing it.
98pub 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        // The alias is the identifier after `as`.
112        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
122/// A lexical scope: its byte range and the bindings declared directly in it.
123struct Scope<'a> {
124    range: Span,
125    /// Indices into `Resolution::bindings`, grouped by name in declaration order.
126    /// Keyed by name rather than a flat list so a lookup does not scan every
127    /// binding in the scope — the module scope of a large manifest holds thousands,
128    /// and each use would then cost a full pass.
129    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    /// Offset added to recorded use spans. Non-zero only while walking a `#{...}`
138    /// sub-expression, which is lexed standalone and so reports spans from 0.
139    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    /// Innermost binding of `name` visible at `at`. Within one scope a use
174    /// prefers a declaration at or before it, so a shadowing `=` later in the
175    /// same scope does not capture earlier uses.
176    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    // ---- narrowing statement spans down to the identifier token ----
206
207    /// Tokens whose span lies inside `range`.
208    ///
209    /// Tokens are sorted by start offset, so the sub-range is found by binary
210    /// search and then walked until it leaves `range`. Scanning the whole vector
211    /// instead — one full pass per declaration — made resolution quadratic in file
212    /// size, and an editor pays this on every keystroke: 119 ms on a 190 KB
213    /// manifest, against 6.2 ms with this and the name-keyed `Scope::decls`. The
214    /// indexing costs ~2 µs on a small file, which is the right trade.
215    /// See `benches/resolve.rs`.
216    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    /// The first `Ident(name)` token inside `range`.
227    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    /// Parameter name spans: the `Ident` tokens of the first parenthesised group
248    /// in `range`. Scanning the group rather than the whole statement keeps
249    /// `def f(f)` from pointing the parameter at the function's own name.
250    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        // Match positionally; fall back to by-name if the shapes disagree.
267        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    // ---- the walk (mirrors analysis::SemanticAnalyzer) ----
281
282    fn walk_stmt(&mut self, stmt: &Stmt<'a>) {
283        match stmt {
284            Stmt::Assign {
285                name, value, span, ..
286            } => {
287                self.walk_expr(value); // the value does not see its own name
288                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                    // A custom field type is a use of that schema/enum name.
305                    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    /// The span of a field's custom type name, e.g. `Tier` in `tier: Tier`.
350    /// Looked up after the field's own name so a schema whose field shares the
351    /// type's name still points at the type.
352    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                // `method` is a stdlib name, never a binding.
421                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            // `field` is a key in the receiver, not a binding.
430            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                // `new Name` uses the schema; `new mod.Name` uses the module alias.
463                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    /// Uses inside `#{...}`. The part is a subslice of `src`, so its absolute
473    /// offset is recoverable and the spans stay usable for renaming.
474    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        // Walk it in the *current* scopes — interpolation sees the enclosing
485        // bindings — with the bias that turns its spans absolute, so resolution
486        // (which is position-sensitive) sees the real offsets too.
487        let outer_bias = self.bias;
488        self.bias = base;
489        self.walk_expr(&expr);
490        self.bias = outer_bias;
491    }
492}
493
494/// Byte offset of `sub` within `outer`, if `sub` really is a subslice of it.
495fn 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    /// The text of each occurrence of the binding under the cursor at `offset`.
511    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        // The `x` inside the def is a parameter; the top-level `x` is unrelated.
523        // Conflating them is exactly the corruption a rename must never cause.
524        let src = "x = 1\ndef f(x)\n  y: x\nend\nz: x\n";
525        let outer = occurrences_at(src, 0); // on the top-level `x`
526        let inner = occurrences_at(src, 12); // on the parameter
527        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        // No offset appears in both sets.
530        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        // The outer binding is used only by `b: p` after the block.
541        assert_eq!(outer.len(), 2, "{outer:?}");
542        // The shadowing binding owns the use inside the block.
543        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        // `host` is a schema field and a property key, never a variable; `map` is
576        // a stdlib method. None of them may be returned as a binding.
577        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        // `new E` is a use of the schema.
585        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        // `b` lives in the def's scope, which is nested inside `a`'s module scope.
595        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    /// The doc claim that resolution matches the interpreter's scopes, checked
600    /// against the analyzer that mirrors the Environment: on input the analyzer
601    /// accepts, every non-builtin use must resolve to a binding. A scope bug here
602    /// (a missed `push`, a body walked in the wrong scope) breaks this.
603    #[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    /// Mirrors `analysis::BUILTINS` — names that resolve to no binding by design.
629    const BUILTIN_NAMES: &[&str] = &["env", "read_file", "fail", "range"];
630}