Skip to main content

blue_lang_syntax/
scope.rs

1//! The binder grammar: which forms open a scope, which names they bind, and
2//! which symbols are references. **One walker, and every pass that needs
3//! scopes drives it** — the check stage's name table
4//! (`blue_lang_check::names`) and the capability frame's reach walk
5//! (`blue_lang_waku::check_reach_program`). Two hand-written walkers used to
6//! answer this question, and they disagreed: waku bound a `define` only at the
7//! level of a `begin`, so a name assigned inside an `if` branch and read after
8//! it was reported as an escape, and it knew no `let*`, `letrec` or `catch`.
9//!
10//! The walker knows the SHAPES of tatara-lisp's binding forms, as blue lowers
11//! them: `lambda` and a function `define` introduce parameters; `let`, `let*`
12//! and `letrec` their bindings (a named `let` its name); `try`'s `catch` clause
13//! its variable; a `define` inside a body binds in that body's frame,
14//! including through `if`, `cond` and `begin`, which open no frame of their
15//! own. `quote` is data; inside a quasiquote only the `unquote`d parts are
16//! code, and the rest is handed to [`Scopes::template_symbol`].
17//!
18//! What a pass DOES with a binding or a reference is the [`Scopes`]
19//! implementation's business — the checker tracks reads for its unused
20//! warning, the reach walk tests each free name against a frame — and which
21//! head is a special form or a macro is the implementation's oracle
22//! ([`Scopes::head_kind`]), since only the pass knows which interpreter it
23//! answers to.
24
25use tatara_lisp::{Atom, Span, Spanned, SpannedForm};
26
27/// Which of the evaluator's three arbiters claims a name in head position.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum HeadKind {
30    SpecialForm,
31    Macro,
32    Value,
33}
34
35/// What introduced a local binding.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum BinderKind {
38    Parameter,
39    /// `x = e` inside a body.
40    Assignment,
41    /// `def f(…)` inside a body, or a named `let`.
42    Function,
43    Let,
44    Catch,
45}
46
47/// One `use` form, as the file that wrote it declared it.
48///
49/// `use("moji")` makes `moji::split` reachable; `use("retsu", [:first,
50/// :size])` also makes `first` and `size` reachable bare. The form stays in
51/// the program (after the package's spliced forms) so the check stage can
52/// point at it, and evaluates to nothing (`blue_lang_runtime::uses::ResolvedProgram::sexps`).
53#[derive(Clone, Debug, PartialEq)]
54pub struct Import {
55    pub package: String,
56    /// The names listed for bare use, each with its `:name` span. Empty for
57    /// `use("moji")`.
58    pub names: Vec<(String, Span)>,
59    /// Whether a list was written at all: `use("x", [])` lists nothing.
60    pub listed: bool,
61    /// The whole `use(...)` form.
62    pub span: Span,
63    /// The span of the package-name string.
64    pub package_span: Span,
65}
66
67/// Is this form a `use` declaration? If so, what it declares.
68///
69/// `use("name")` or `use("name", [:a, :b])`, the call form only. `use "kazu"`
70/// without parentheses parses as two unrelated top-level atoms (blue has no
71/// paren-less call syntax), which would silently do nothing — so it is not
72/// treated as an import, and the bare symbol `use` then fails as an unbound
73/// name rather than being quietly ignored. A list that holds anything but
74/// `:name` symbols is not a declaration either, for the same reason.
75#[must_use]
76pub fn use_target(form: &Spanned) -> Option<Import> {
77    let items = form.as_list()?;
78    let (head, arg, list) = match items {
79        [head, arg] => (head, arg, None),
80        [head, arg, list] => (head, arg, Some(list)),
81        _ => return None,
82    };
83    let (SpannedForm::Atom(Atom::Symbol(s)), SpannedForm::Atom(Atom::Str(name))) =
84        (&head.form, &arg.form)
85    else {
86        return None;
87    };
88    if s != "use" {
89        return None;
90    }
91    let mut names = Vec::new();
92    if let Some(list) = list {
93        let parts = list.as_list()?;
94        if parts.first().and_then(Spanned::as_symbol) != Some("list") {
95            return None;
96        }
97        for p in &parts[1..] {
98            match &p.form {
99                SpannedForm::Atom(Atom::Keyword(k)) => names.push((k.clone(), p.span)),
100                _ => return None,
101            }
102        }
103    }
104    Some(Import {
105        package: name.clone(),
106        names,
107        listed: list.is_some(),
108        span: form.span,
109        package_span: arg.span,
110    })
111}
112
113/// The head of the rename-ledger declaration: `legacy_names("0.1.1", "lc")`.
114pub const LEGACY_NAMES: &str = "legacy_names";
115
116/// A bidama's rename ledger, declared at its top level. Two forms:
117///
118/// - `legacy_names(since, prefix)`: the bidama's definitions were once all
119///   named `prefix_x`. From version `since`, every definition `x` is also
120///   reachable as `prefix_x` — a BRIDGE for callers still on the old
121///   spelling, open while the bidama's version is at least `since` and below
122///   the next minor — and a definition that must keep its prefix (the
123///   stripped name is a reserved word or a builtin the bidama uses bare) is
124///   also reachable by its stripped name, for good.
125/// - `legacy_names(since, [["old", "home::new"], …])`: definitions the
126///   bidama MOVED to another bidama. `old` stays a bridge to `home::new`
127///   over the same window, so a twin is moved, not copied.
128///
129/// Like `use`, it is consumed by the resolver and evaluates to nothing; an
130/// expired ledger stays in the source, and powers the fix for a late caller.
131#[derive(Clone, Debug, PartialEq)]
132pub struct Legacy {
133    pub since: String,
134    pub kind: LegacyKind,
135    pub span: Span,
136}
137
138/// What a `legacy_names` declaration records.
139#[derive(Clone, Debug, PartialEq)]
140pub enum LegacyKind {
141    /// The prefix every definition once carried.
142    Prefix(String),
143    /// Definitions moved out of the bidama, in source order.
144    Moved(Vec<Moved>),
145}
146
147/// One moved definition: `old` in the declaring bidama is `home::new` now.
148/// `home` is empty when the target was not written `home::new`.
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct Moved {
151    pub old: String,
152    pub home: String,
153    pub new: String,
154}
155
156/// Is this form a `legacy_names` declaration? A version string, then a
157/// prefix string or a list of `[old, "home::new"]` string pairs, or it is not
158/// one (and `legacy_names` is then an unbound call).
159#[must_use]
160pub fn legacy_target(form: &Spanned) -> Option<Legacy> {
161    let items = form.as_list()?;
162    let [head, since, what] = items else {
163        return None;
164    };
165    if head.as_symbol() != Some(LEGACY_NAMES) {
166        return None;
167    }
168    let SpannedForm::Atom(Atom::Str(since)) = &since.form else {
169        return None;
170    };
171    let str_of = |s: &Spanned| match &s.form {
172        SpannedForm::Atom(Atom::Str(x)) => Some(x.clone()),
173        _ => None,
174    };
175    let kind = if let Some(prefix) = str_of(what) {
176        LegacyKind::Prefix(prefix)
177    } else {
178        let moves = list_items(what)?
179            .iter()
180            .map(|pair| {
181                let [old, target] = list_items(pair)? else {
182                    return None;
183                };
184                let (old, target) = (str_of(old)?, str_of(target)?);
185                let (home, new) = target
186                    .split_once("::")
187                    .map_or((String::new(), target.clone()), |(h, n)| {
188                        (h.to_string(), n.to_string())
189                    });
190                Some(Moved { old, home, new })
191            })
192            .collect::<Option<Vec<_>>>()?;
193        LegacyKind::Moved(moves)
194    };
195    Some(Legacy {
196        since: since.clone(),
197        kind,
198        span: form.span,
199    })
200}
201
202/// The elements of a list literal `[a, b]`, which parses to `(list a b)`.
203fn list_items(form: &Spanned) -> Option<&[Spanned]> {
204    let items = form.as_list()?;
205    (items.first()?.as_symbol() == Some("list")).then(|| &items[1..])
206}
207
208/// The special forms whose SHAPE the walker knows — the heads a pass without
209/// an interpreter to ask (the reach walk) answers [`HeadKind::SpecialForm`]
210/// for, so their binders bind. Every other special form is walked as an
211/// ordinary application, which is what it is for scoping purposes.
212pub const SHAPED_FORMS: &[&str] = &[
213    "lambda",
214    "let",
215    "let*",
216    "letrec",
217    "try",
218    "cond",
219    "quote",
220    "quasiquote",
221    "provide",
222    "require",
223];
224
225/// A pass over the binder grammar. The walker calls these in source order.
226pub trait Scopes {
227    /// Which arbiter claims `name` as a head, if any. `None` walks the call
228    /// as an ordinary application.
229    fn head_kind(&self, name: &str) -> Option<HeadKind>;
230    /// A frame opens: a function body, a `let`, a `catch`, a test body.
231    fn open(&mut self);
232    /// `name` is bound in the innermost open frame.
233    fn bind(&mut self, name: &str, span: Span, kind: BinderKind);
234    /// The innermost frame closes.
235    fn close(&mut self);
236    /// A symbol in code position. `opaque`: inside a macro call's arguments,
237    /// which are syntax the macro may never evaluate.
238    fn reference(&mut self, node: &Spanned, name: &str, opaque: bool);
239    /// The head of a special form or a macro call — not a reference to a
240    /// binding, but still a name the program writes.
241    fn head(&mut self, _node: &Spanned, _name: &str, _kind: HeadKind) {}
242    /// A symbol inside a quasiquoted template, outside any `unquote`: data at
243    /// expansion, code once the expansion runs.
244    fn template_symbol(&mut self, _node: &Spanned, _name: &str) {}
245}
246
247/// Walk a whole program. Top-level definitions are the pass's to know (they
248/// are globals, see [`definitions_of`]); the walker opens frames only below
249/// them.
250pub fn walk_program(forms: &[Spanned], pass: &mut dyn Scopes) {
251    for form in forms {
252        walk_top(form, pass);
253    }
254}
255
256/// Walk one top-level form.
257pub fn walk_top(form: &Spanned, pass: &mut dyn Scopes) {
258    Walker { pass, depth: 0 }.walk(form, false);
259}
260
261/// The names `form` defines into the frame it is evaluated in, with the
262/// span of each name and whether it is a value or a macro.
263///
264/// A `define` binds in the environment it is EVALUATED in, and every part of
265/// a form is evaluated in the enclosing environment except the parts of a
266/// frame-opening form (`lambda`, a function `define`, `let`, a `catch`
267/// clause, a test body) and quoted data. So this descends into every
268/// sub-form but those: `x = if c … else y = 1 … end` binds `y` in the
269/// function's frame, which is where a later `fn(s) … y … end` reads it.
270#[must_use]
271pub fn definitions_of(form: &Spanned) -> Vec<(String, Span, HeadKind)> {
272    let mut out = Vec::new();
273    definition_nodes(form, &mut |node, kind| {
274        if let Some(n) = node.as_symbol() {
275            out.push((n.to_string(), node.span, kind));
276        }
277    });
278    out
279}
280
281/// [`definitions_of`], handing over the NAME NODE of each definition, so a
282/// pass that rewrites definitions can find the symbol it renames.
283pub fn definition_nodes<'a>(form: &'a Spanned, out: &mut dyn FnMut(&'a Spanned, HeadKind)) {
284    let Some(items) = form.as_list() else { return };
285    let head = items.first().and_then(Spanned::as_symbol);
286    match head {
287        Some("defmacro") => {
288            if let Some(n) = items.get(1).filter(|n| n.as_symbol().is_some()) {
289                out(n, HeadKind::Macro);
290            }
291        }
292        Some("define" | "define-typed") => {
293            if let Some(n) = defined_name_node(items) {
294                out(n, HeadKind::Value);
295            }
296            // `(define x e)`: `e` is evaluated right here, so its defines are
297            // this frame's too. A function define opens a frame; stop.
298            if items.get(1).is_some_and(|t| t.as_symbol().is_some()) {
299                for item in &items[2..] {
300                    definition_nodes(item, out);
301                }
302            }
303        }
304        Some("lambda" | "let" | "let*" | "letrec" | "deftest" | "quote" | "quasiquote") => {}
305        Some("try") => {
306            // The body shares the frame; a `catch` clause opens its own.
307            for item in &items[1..] {
308                let is_clause = item
309                    .as_list()
310                    .and_then(|c| c.first())
311                    .and_then(Spanned::as_symbol)
312                    .is_some_and(|h| h == "catch" || h == "finally");
313                if !is_clause {
314                    definition_nodes(item, out);
315                }
316            }
317        }
318        _ => {
319            for item in items {
320                definition_nodes(item, out);
321            }
322        }
323    }
324}
325
326/// The name node a `define`/`define-typed` form binds.
327#[must_use]
328pub fn defined_name_node(items: &[Spanned]) -> Option<&Spanned> {
329    let head = items.first()?.as_symbol()?;
330    if head != "define" && head != "define-typed" {
331        return None;
332    }
333    let target = items.get(1)?;
334    if target.as_symbol().is_some() {
335        return Some(target);
336    }
337    let name = target.as_list()?.first()?;
338    name.as_symbol().map(|_| name)
339}
340
341/// A lambda parameter list: symbols, skipping `&rest`/`&optional`/`.`
342/// markers; a bare symbol is a variadic parameter.
343#[must_use]
344pub fn params_of(list: &Spanned) -> Vec<(String, Span)> {
345    if let Some(n) = list.as_symbol() {
346        return vec![(n.to_string(), list.span)];
347    }
348    list.as_list()
349        .map(|ps| ps.iter().filter_map(param).collect())
350        .unwrap_or_default()
351}
352
353fn param(p: &Spanned) -> Option<(String, Span)> {
354    let n = p.as_symbol()?;
355    if n.starts_with('&') || n == "." {
356        return None;
357    }
358    Some((n.to_string(), p.span))
359}
360
361struct Walker<'p> {
362    pass: &'p mut dyn Scopes,
363    /// Open frames. Zero at the top level of a form.
364    depth: usize,
365}
366
367impl Walker<'_> {
368    fn open(&mut self) {
369        self.depth += 1;
370        self.pass.open();
371    }
372
373    fn close(&mut self) {
374        self.depth -= 1;
375        self.pass.close();
376    }
377
378    /// Bind, in the current frame, every `define` a body reaches without
379    /// crossing a frame boundary — so a body can read a local defined later in
380    /// it, as a closure over the frame can at runtime.
381    fn hoist(&mut self, body: &[Spanned]) {
382        for f in body {
383            let function = f.as_list().is_some_and(|items| {
384                items.first().and_then(Spanned::as_symbol) == Some("define")
385                    && items.get(1).is_some_and(|t| t.as_list().is_some())
386            });
387            let pass = &mut *self.pass;
388            definition_nodes(f, &mut |node, kind| {
389                let Some(name) = node.as_symbol() else { return };
390                let bk = match kind {
391                    HeadKind::Value if !function => BinderKind::Assignment,
392                    _ => BinderKind::Function,
393                };
394                pass.bind(name, node.span, bk);
395            });
396        }
397    }
398
399    /// Walk a body: a new frame, its parameters bound, its defines hoisted,
400    /// each form walked.
401    fn body(&mut self, params: &[(String, Span)], forms: &[Spanned], opaque: bool) {
402        self.open();
403        for (p, s) in params {
404            self.pass.bind(p, *s, BinderKind::Parameter);
405        }
406        self.hoist(forms);
407        for f in forms {
408            self.walk(f, opaque);
409        }
410        self.close();
411    }
412
413    fn walk(&mut self, form: &Spanned, opaque: bool) {
414        match &form.form {
415            SpannedForm::Atom(Atom::Symbol(n)) => self.pass.reference(form, n, opaque),
416            SpannedForm::Quote(_) => {}
417            SpannedForm::Quasiquote(inner) => self.walk_template(inner, opaque),
418            SpannedForm::Unquote(inner) | SpannedForm::UnquoteSplice(inner) => {
419                self.walk(inner, opaque);
420            }
421            SpannedForm::List(items) if !items.is_empty() => self.walk_list(items, opaque),
422            _ => {}
423        }
424    }
425
426    /// Inside a quasiquote only the unquoted parts are code.
427    fn walk_template(&mut self, form: &Spanned, opaque: bool) {
428        match &form.form {
429            SpannedForm::Atom(Atom::Symbol(n)) => self.pass.template_symbol(form, n),
430            SpannedForm::Unquote(inner) | SpannedForm::UnquoteSplice(inner) => {
431                self.walk(inner, opaque);
432            }
433            SpannedForm::List(items) => {
434                for i in items {
435                    self.walk_template(i, opaque);
436                }
437            }
438            SpannedForm::Quasiquote(inner) => self.walk_template(inner, opaque),
439            _ => {}
440        }
441    }
442
443    fn walk_all(&mut self, items: &[Spanned], opaque: bool) {
444        for i in items {
445            self.walk(i, opaque);
446        }
447    }
448
449    fn walk_list(&mut self, items: &[Spanned], opaque: bool) {
450        let Some(head) = items[0].as_symbol() else {
451            self.walk_all(items, opaque);
452            return;
453        };
454        // `deftest` is the test harness's declaration, not a special form: a
455        // body, run in a frame of its own.
456        if head == "deftest" && self.depth == 0 {
457            if let Some(body) = items.get(2) {
458                self.body(&[], std::slice::from_ref(body), opaque);
459            }
460            return;
461        }
462        // `use("x", [:a])` at the top level is a declaration the resolver
463        // consumed, not a call: nothing in it is a reference.
464        if (head == "use" || head == LEGACY_NAMES) && self.depth == 0 {
465            self.pass.head(&items[0], head, HeadKind::SpecialForm);
466            return;
467        }
468        // `define-typed` is erased before evaluation; its shape is `define`'s
469        // with each parameter written `(name Type)`.
470        if head == "define" || head == "define-typed" {
471            self.pass.head(&items[0], head, HeadKind::SpecialForm);
472            self.walk_define(head, items, opaque);
473            return;
474        }
475        // `defmacro` is registered by the expander before evaluation, not
476        // dispatched as a special form, so no arbiter claims it as a head.
477        // `(defmacro name (params) body…)`.
478        if head == "defmacro" && self.depth == 0 {
479            self.pass.head(&items[0], head, HeadKind::SpecialForm);
480            let params = items.get(2).map(params_of).unwrap_or_default();
481            self.body(&params, items.get(3..).unwrap_or(&[]), opaque);
482            return;
483        }
484        match self.pass.head_kind(head) {
485            Some(HeadKind::SpecialForm) => {
486                self.pass.head(&items[0], head, HeadKind::SpecialForm);
487                self.walk_special(head, items, opaque);
488            }
489            Some(HeadKind::Macro) => {
490                // Opaque: a macro's arguments are syntax.
491                self.pass.head(&items[0], head, HeadKind::Macro);
492                for i in &items[1..] {
493                    self.walk(i, true);
494                }
495            }
496            Some(HeadKind::Value) | None => {
497                self.walk(&items[0], opaque);
498                self.walk_all(&items[1..], opaque);
499            }
500        }
501    }
502
503    fn walk_define(&mut self, head: &str, items: &[Spanned], opaque: bool) {
504        let Some(target) = items.get(1) else { return };
505        if target.as_symbol().is_some() {
506            // `(define x e)`: `x` is already bound (hoisted, or a global).
507            self.walk_all(&items[2..], opaque);
508            return;
509        }
510        let Some(sig) = target.as_list() else {
511            self.walk_all(&items[1..], opaque);
512            return;
513        };
514        let params: Vec<(String, Span)> = sig
515            .iter()
516            .skip(1)
517            .filter_map(|p| {
518                if head == "define-typed" {
519                    let pair = p.as_list()?;
520                    let n = pair.first()?;
521                    Some((n.as_symbol()?.to_string(), n.span))
522                } else {
523                    param(p)
524                }
525            })
526            .collect();
527        // `(define-typed sig R body)`: the return type is not code.
528        let body_from = if head == "define-typed" { 3 } else { 2 };
529        self.body(&params, items.get(body_from..).unwrap_or(&[]), opaque);
530    }
531
532    fn walk_special(&mut self, head: &str, items: &[Spanned], opaque: bool) {
533        let args = &items[1..];
534        match head {
535            "quote" | "provide" | "require" => {}
536            "quasiquote" => {
537                for a in args {
538                    self.walk_template(a, opaque);
539                }
540            }
541            "lambda" => {
542                let params = args.first().map(params_of).unwrap_or_default();
543                self.body(&params, args.get(1..).unwrap_or(&[]), opaque);
544            }
545            "cond" => {
546                for clause in args {
547                    match clause.as_list() {
548                        Some(parts) => {
549                            let rest = match parts.first().and_then(Spanned::as_symbol) {
550                                Some("else") => &parts[1..],
551                                _ => parts,
552                            };
553                            for p in rest {
554                                if p.as_symbol() != Some("=>") {
555                                    self.walk(p, opaque);
556                                }
557                            }
558                        }
559                        None => self.walk(clause, opaque),
560                    }
561                }
562            }
563            "let" | "let*" | "letrec" => self.walk_let(head, args, opaque),
564            "try" => {
565                for a in args {
566                    let clause = a.as_list();
567                    match clause.and_then(|c| c.first()).and_then(Spanned::as_symbol) {
568                        Some("catch") => {
569                            let c = clause.expect("matched");
570                            let var = c.get(1).and_then(|v| {
571                                v.as_list()
572                                    .and_then(|l| l.first())
573                                    .and_then(|s| s.as_symbol().map(|n| (n.to_string(), s.span)))
574                                    .or_else(|| v.as_symbol().map(|n| (n.to_string(), v.span)))
575                            });
576                            self.open();
577                            if let Some((n, s)) = var {
578                                self.pass.bind(&n, s, BinderKind::Catch);
579                            }
580                            self.walk_all(c.get(2..).unwrap_or(&[]), opaque);
581                            self.close();
582                        }
583                        Some("finally") => {
584                            let c = clause.expect("matched");
585                            self.walk_all(&c[1..], opaque);
586                        }
587                        _ => self.walk(a, opaque),
588                    }
589                }
590            }
591            // if, when, unless, and, or, not, begin, set!, delay, eval,
592            // macroexpand, macroexpand-1: every part is an expression.
593            _ => self.walk_all(args, opaque),
594        }
595    }
596
597    fn walk_let(&mut self, head: &str, args: &[Spanned], opaque: bool) {
598        // Named let: `(let name ((a e) …) body…)`.
599        let (name, rest) = match args.first() {
600            Some(n) if n.as_symbol().is_some() && head == "let" => (Some(n), &args[1..]),
601            _ => (None, args),
602        };
603        let Some(bindings) = rest.first().and_then(Spanned::as_list) else {
604            self.walk_all(rest, opaque);
605            return;
606        };
607        let pairs: Vec<(String, Span, Option<&Spanned>)> = bindings
608            .iter()
609            .filter_map(|b| match b.as_list() {
610                Some(pair) => {
611                    let n = pair.first()?;
612                    Some((n.as_symbol()?.to_string(), n.span, pair.get(1)))
613                }
614                None => b.as_symbol().map(|n| (n.to_string(), b.span, None)),
615            })
616            .collect();
617        let body = rest.get(1..).unwrap_or(&[]);
618        if head == "let" {
619            // Inits see the OUTER scope: walk them before the frame opens.
620            for (_, _, init) in &pairs {
621                if let Some(e) = init {
622                    self.walk(e, opaque);
623                }
624            }
625        }
626        self.open();
627        match head {
628            "let" => {
629                if let Some(n) = name {
630                    self.pass.bind(
631                        n.as_symbol().expect("matched"),
632                        n.span,
633                        BinderKind::Function,
634                    );
635                }
636                for (n, s, _) in &pairs {
637                    self.pass.bind(n, *s, BinderKind::Let);
638                }
639            }
640            "let*" => {
641                for (n, s, init) in &pairs {
642                    if let Some(e) = init {
643                        self.walk(e, opaque);
644                    }
645                    self.pass.bind(n, *s, BinderKind::Let);
646                }
647            }
648            _ => {
649                for (n, s, _) in &pairs {
650                    self.pass.bind(n, *s, BinderKind::Let);
651                }
652                for (_, _, init) in &pairs {
653                    if let Some(e) = init {
654                        self.walk(e, opaque);
655                    }
656                }
657            }
658        }
659        self.hoist(body);
660        self.walk_all(body, opaque);
661        self.close();
662    }
663}