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:
117/// `legacy_names(since, prefix)`.
118///
119/// The bidama's definitions were once all named `prefix_x`. From version
120/// `since`, every definition `x` is also reachable as `prefix_x` — a BRIDGE
121/// for callers still on the old spelling, open while the bidama's version
122/// is at least `since` and below the next minor — and a definition that
123/// must keep its prefix (the stripped name is a reserved word or a builtin
124/// the bidama uses bare) is also reachable by its stripped name, for good.
125/// Like `use`, it is consumed by the resolver and evaluates to nothing; an
126/// expired ledger stays in the source, and powers the fix for a late caller.
127#[derive(Clone, Debug, PartialEq)]
128pub struct Legacy {
129    pub since: String,
130    pub prefix: String,
131    pub span: Span,
132}
133
134/// Is this form a `legacy_names(since, prefix)` declaration? Two string
135/// literals, or it is not one (and `legacy_names` is then an unbound call).
136#[must_use]
137pub fn legacy_target(form: &Spanned) -> Option<Legacy> {
138    let items = form.as_list()?;
139    let [head, since, prefix] = items else {
140        return None;
141    };
142    if head.as_symbol() != Some(LEGACY_NAMES) {
143        return None;
144    }
145    let (SpannedForm::Atom(Atom::Str(since)), SpannedForm::Atom(Atom::Str(prefix))) =
146        (&since.form, &prefix.form)
147    else {
148        return None;
149    };
150    Some(Legacy {
151        since: since.clone(),
152        prefix: prefix.clone(),
153        span: form.span,
154    })
155}
156
157/// The special forms whose SHAPE the walker knows — the heads a pass without
158/// an interpreter to ask (the reach walk) answers [`HeadKind::SpecialForm`]
159/// for, so their binders bind. Every other special form is walked as an
160/// ordinary application, which is what it is for scoping purposes.
161pub const SHAPED_FORMS: &[&str] = &[
162    "lambda",
163    "let",
164    "let*",
165    "letrec",
166    "try",
167    "cond",
168    "quote",
169    "quasiquote",
170    "provide",
171    "require",
172];
173
174/// A pass over the binder grammar. The walker calls these in source order.
175pub trait Scopes {
176    /// Which arbiter claims `name` as a head, if any. `None` walks the call
177    /// as an ordinary application.
178    fn head_kind(&self, name: &str) -> Option<HeadKind>;
179    /// A frame opens: a function body, a `let`, a `catch`, a test body.
180    fn open(&mut self);
181    /// `name` is bound in the innermost open frame.
182    fn bind(&mut self, name: &str, span: Span, kind: BinderKind);
183    /// The innermost frame closes.
184    fn close(&mut self);
185    /// A symbol in code position. `opaque`: inside a macro call's arguments,
186    /// which are syntax the macro may never evaluate.
187    fn reference(&mut self, node: &Spanned, name: &str, opaque: bool);
188    /// The head of a special form or a macro call — not a reference to a
189    /// binding, but still a name the program writes.
190    fn head(&mut self, _node: &Spanned, _name: &str, _kind: HeadKind) {}
191    /// A symbol inside a quasiquoted template, outside any `unquote`: data at
192    /// expansion, code once the expansion runs.
193    fn template_symbol(&mut self, _node: &Spanned, _name: &str) {}
194}
195
196/// Walk a whole program. Top-level definitions are the pass's to know (they
197/// are globals, see [`definitions_of`]); the walker opens frames only below
198/// them.
199pub fn walk_program(forms: &[Spanned], pass: &mut dyn Scopes) {
200    for form in forms {
201        walk_top(form, pass);
202    }
203}
204
205/// Walk one top-level form.
206pub fn walk_top(form: &Spanned, pass: &mut dyn Scopes) {
207    Walker { pass, depth: 0 }.walk(form, false);
208}
209
210/// The names `form` defines into the frame it is evaluated in, with the
211/// span of each name and whether it is a value or a macro.
212///
213/// A `define` binds in the environment it is EVALUATED in, and every part of
214/// a form is evaluated in the enclosing environment except the parts of a
215/// frame-opening form (`lambda`, a function `define`, `let`, a `catch`
216/// clause, a test body) and quoted data. So this descends into every
217/// sub-form but those: `x = if c … else y = 1 … end` binds `y` in the
218/// function's frame, which is where a later `fn(s) … y … end` reads it.
219#[must_use]
220pub fn definitions_of(form: &Spanned) -> Vec<(String, Span, HeadKind)> {
221    let mut out = Vec::new();
222    definition_nodes(form, &mut |node, kind| {
223        if let Some(n) = node.as_symbol() {
224            out.push((n.to_string(), node.span, kind));
225        }
226    });
227    out
228}
229
230/// [`definitions_of`], handing over the NAME NODE of each definition, so a
231/// pass that rewrites definitions can find the symbol it renames.
232pub fn definition_nodes<'a>(form: &'a Spanned, out: &mut dyn FnMut(&'a Spanned, HeadKind)) {
233    let Some(items) = form.as_list() else { return };
234    let head = items.first().and_then(Spanned::as_symbol);
235    match head {
236        Some("defmacro") => {
237            if let Some(n) = items.get(1).filter(|n| n.as_symbol().is_some()) {
238                out(n, HeadKind::Macro);
239            }
240        }
241        Some("define" | "define-typed") => {
242            if let Some(n) = defined_name_node(items) {
243                out(n, HeadKind::Value);
244            }
245            // `(define x e)`: `e` is evaluated right here, so its defines are
246            // this frame's too. A function define opens a frame; stop.
247            if items.get(1).is_some_and(|t| t.as_symbol().is_some()) {
248                for item in &items[2..] {
249                    definition_nodes(item, out);
250                }
251            }
252        }
253        Some("lambda" | "let" | "let*" | "letrec" | "deftest" | "quote" | "quasiquote") => {}
254        Some("try") => {
255            // The body shares the frame; a `catch` clause opens its own.
256            for item in &items[1..] {
257                let is_clause = item
258                    .as_list()
259                    .and_then(|c| c.first())
260                    .and_then(Spanned::as_symbol)
261                    .is_some_and(|h| h == "catch" || h == "finally");
262                if !is_clause {
263                    definition_nodes(item, out);
264                }
265            }
266        }
267        _ => {
268            for item in items {
269                definition_nodes(item, out);
270            }
271        }
272    }
273}
274
275/// The name node a `define`/`define-typed` form binds.
276#[must_use]
277pub fn defined_name_node(items: &[Spanned]) -> Option<&Spanned> {
278    let head = items.first()?.as_symbol()?;
279    if head != "define" && head != "define-typed" {
280        return None;
281    }
282    let target = items.get(1)?;
283    if target.as_symbol().is_some() {
284        return Some(target);
285    }
286    let name = target.as_list()?.first()?;
287    name.as_symbol().map(|_| name)
288}
289
290/// A lambda parameter list: symbols, skipping `&rest`/`&optional`/`.`
291/// markers; a bare symbol is a variadic parameter.
292#[must_use]
293pub fn params_of(list: &Spanned) -> Vec<(String, Span)> {
294    if let Some(n) = list.as_symbol() {
295        return vec![(n.to_string(), list.span)];
296    }
297    list.as_list()
298        .map(|ps| ps.iter().filter_map(param).collect())
299        .unwrap_or_default()
300}
301
302fn param(p: &Spanned) -> Option<(String, Span)> {
303    let n = p.as_symbol()?;
304    if n.starts_with('&') || n == "." {
305        return None;
306    }
307    Some((n.to_string(), p.span))
308}
309
310struct Walker<'p> {
311    pass: &'p mut dyn Scopes,
312    /// Open frames. Zero at the top level of a form.
313    depth: usize,
314}
315
316impl Walker<'_> {
317    fn open(&mut self) {
318        self.depth += 1;
319        self.pass.open();
320    }
321
322    fn close(&mut self) {
323        self.depth -= 1;
324        self.pass.close();
325    }
326
327    /// Bind, in the current frame, every `define` a body reaches without
328    /// crossing a frame boundary — so a body can read a local defined later in
329    /// it, as a closure over the frame can at runtime.
330    fn hoist(&mut self, body: &[Spanned]) {
331        for f in body {
332            let function = f.as_list().is_some_and(|items| {
333                items.first().and_then(Spanned::as_symbol) == Some("define")
334                    && items.get(1).is_some_and(|t| t.as_list().is_some())
335            });
336            let pass = &mut *self.pass;
337            definition_nodes(f, &mut |node, kind| {
338                let Some(name) = node.as_symbol() else { return };
339                let bk = match kind {
340                    HeadKind::Value if !function => BinderKind::Assignment,
341                    _ => BinderKind::Function,
342                };
343                pass.bind(name, node.span, bk);
344            });
345        }
346    }
347
348    /// Walk a body: a new frame, its parameters bound, its defines hoisted,
349    /// each form walked.
350    fn body(&mut self, params: &[(String, Span)], forms: &[Spanned], opaque: bool) {
351        self.open();
352        for (p, s) in params {
353            self.pass.bind(p, *s, BinderKind::Parameter);
354        }
355        self.hoist(forms);
356        for f in forms {
357            self.walk(f, opaque);
358        }
359        self.close();
360    }
361
362    fn walk(&mut self, form: &Spanned, opaque: bool) {
363        match &form.form {
364            SpannedForm::Atom(Atom::Symbol(n)) => self.pass.reference(form, n, opaque),
365            SpannedForm::Quote(_) => {}
366            SpannedForm::Quasiquote(inner) => self.walk_template(inner, opaque),
367            SpannedForm::Unquote(inner) | SpannedForm::UnquoteSplice(inner) => {
368                self.walk(inner, opaque);
369            }
370            SpannedForm::List(items) if !items.is_empty() => self.walk_list(items, opaque),
371            _ => {}
372        }
373    }
374
375    /// Inside a quasiquote only the unquoted parts are code.
376    fn walk_template(&mut self, form: &Spanned, opaque: bool) {
377        match &form.form {
378            SpannedForm::Atom(Atom::Symbol(n)) => self.pass.template_symbol(form, n),
379            SpannedForm::Unquote(inner) | SpannedForm::UnquoteSplice(inner) => {
380                self.walk(inner, opaque);
381            }
382            SpannedForm::List(items) => {
383                for i in items {
384                    self.walk_template(i, opaque);
385                }
386            }
387            SpannedForm::Quasiquote(inner) => self.walk_template(inner, opaque),
388            _ => {}
389        }
390    }
391
392    fn walk_all(&mut self, items: &[Spanned], opaque: bool) {
393        for i in items {
394            self.walk(i, opaque);
395        }
396    }
397
398    fn walk_list(&mut self, items: &[Spanned], opaque: bool) {
399        let Some(head) = items[0].as_symbol() else {
400            self.walk_all(items, opaque);
401            return;
402        };
403        // `deftest` is the test harness's declaration, not a special form: a
404        // body, run in a frame of its own.
405        if head == "deftest" && self.depth == 0 {
406            if let Some(body) = items.get(2) {
407                self.body(&[], std::slice::from_ref(body), opaque);
408            }
409            return;
410        }
411        // `use("x", [:a])` at the top level is a declaration the resolver
412        // consumed, not a call: nothing in it is a reference.
413        if (head == "use" || head == LEGACY_NAMES) && self.depth == 0 {
414            self.pass.head(&items[0], head, HeadKind::SpecialForm);
415            return;
416        }
417        // `define-typed` is erased before evaluation; its shape is `define`'s
418        // with each parameter written `(name Type)`.
419        if head == "define" || head == "define-typed" {
420            self.pass.head(&items[0], head, HeadKind::SpecialForm);
421            self.walk_define(head, items, opaque);
422            return;
423        }
424        // `defmacro` is registered by the expander before evaluation, not
425        // dispatched as a special form, so no arbiter claims it as a head.
426        // `(defmacro name (params) body…)`.
427        if head == "defmacro" && self.depth == 0 {
428            self.pass.head(&items[0], head, HeadKind::SpecialForm);
429            let params = items.get(2).map(params_of).unwrap_or_default();
430            self.body(&params, items.get(3..).unwrap_or(&[]), opaque);
431            return;
432        }
433        match self.pass.head_kind(head) {
434            Some(HeadKind::SpecialForm) => {
435                self.pass.head(&items[0], head, HeadKind::SpecialForm);
436                self.walk_special(head, items, opaque);
437            }
438            Some(HeadKind::Macro) => {
439                // Opaque: a macro's arguments are syntax.
440                self.pass.head(&items[0], head, HeadKind::Macro);
441                for i in &items[1..] {
442                    self.walk(i, true);
443                }
444            }
445            Some(HeadKind::Value) | None => {
446                self.walk(&items[0], opaque);
447                self.walk_all(&items[1..], opaque);
448            }
449        }
450    }
451
452    fn walk_define(&mut self, head: &str, items: &[Spanned], opaque: bool) {
453        let Some(target) = items.get(1) else { return };
454        if target.as_symbol().is_some() {
455            // `(define x e)`: `x` is already bound (hoisted, or a global).
456            self.walk_all(&items[2..], opaque);
457            return;
458        }
459        let Some(sig) = target.as_list() else {
460            self.walk_all(&items[1..], opaque);
461            return;
462        };
463        let params: Vec<(String, Span)> = sig
464            .iter()
465            .skip(1)
466            .filter_map(|p| {
467                if head == "define-typed" {
468                    let pair = p.as_list()?;
469                    let n = pair.first()?;
470                    Some((n.as_symbol()?.to_string(), n.span))
471                } else {
472                    param(p)
473                }
474            })
475            .collect();
476        // `(define-typed sig R body)`: the return type is not code.
477        let body_from = if head == "define-typed" { 3 } else { 2 };
478        self.body(&params, items.get(body_from..).unwrap_or(&[]), opaque);
479    }
480
481    fn walk_special(&mut self, head: &str, items: &[Spanned], opaque: bool) {
482        let args = &items[1..];
483        match head {
484            "quote" | "provide" | "require" => {}
485            "quasiquote" => {
486                for a in args {
487                    self.walk_template(a, opaque);
488                }
489            }
490            "lambda" => {
491                let params = args.first().map(params_of).unwrap_or_default();
492                self.body(&params, args.get(1..).unwrap_or(&[]), opaque);
493            }
494            "cond" => {
495                for clause in args {
496                    match clause.as_list() {
497                        Some(parts) => {
498                            let rest = match parts.first().and_then(Spanned::as_symbol) {
499                                Some("else") => &parts[1..],
500                                _ => parts,
501                            };
502                            for p in rest {
503                                if p.as_symbol() != Some("=>") {
504                                    self.walk(p, opaque);
505                                }
506                            }
507                        }
508                        None => self.walk(clause, opaque),
509                    }
510                }
511            }
512            "let" | "let*" | "letrec" => self.walk_let(head, args, opaque),
513            "try" => {
514                for a in args {
515                    let clause = a.as_list();
516                    match clause.and_then(|c| c.first()).and_then(Spanned::as_symbol) {
517                        Some("catch") => {
518                            let c = clause.expect("matched");
519                            let var = c.get(1).and_then(|v| {
520                                v.as_list()
521                                    .and_then(|l| l.first())
522                                    .and_then(|s| s.as_symbol().map(|n| (n.to_string(), s.span)))
523                                    .or_else(|| v.as_symbol().map(|n| (n.to_string(), v.span)))
524                            });
525                            self.open();
526                            if let Some((n, s)) = var {
527                                self.pass.bind(&n, s, BinderKind::Catch);
528                            }
529                            self.walk_all(c.get(2..).unwrap_or(&[]), opaque);
530                            self.close();
531                        }
532                        Some("finally") => {
533                            let c = clause.expect("matched");
534                            self.walk_all(&c[1..], opaque);
535                        }
536                        _ => self.walk(a, opaque),
537                    }
538                }
539            }
540            // if, when, unless, and, or, not, begin, set!, delay, eval,
541            // macroexpand, macroexpand-1: every part is an expression.
542            _ => self.walk_all(args, opaque),
543        }
544    }
545
546    fn walk_let(&mut self, head: &str, args: &[Spanned], opaque: bool) {
547        // Named let: `(let name ((a e) …) body…)`.
548        let (name, rest) = match args.first() {
549            Some(n) if n.as_symbol().is_some() && head == "let" => (Some(n), &args[1..]),
550            _ => (None, args),
551        };
552        let Some(bindings) = rest.first().and_then(Spanned::as_list) else {
553            self.walk_all(rest, opaque);
554            return;
555        };
556        let pairs: Vec<(String, Span, Option<&Spanned>)> = bindings
557            .iter()
558            .filter_map(|b| match b.as_list() {
559                Some(pair) => {
560                    let n = pair.first()?;
561                    Some((n.as_symbol()?.to_string(), n.span, pair.get(1)))
562                }
563                None => b.as_symbol().map(|n| (n.to_string(), b.span, None)),
564            })
565            .collect();
566        let body = rest.get(1..).unwrap_or(&[]);
567        if head == "let" {
568            // Inits see the OUTER scope: walk them before the frame opens.
569            for (_, _, init) in &pairs {
570                if let Some(e) = init {
571                    self.walk(e, opaque);
572                }
573            }
574        }
575        self.open();
576        match head {
577            "let" => {
578                if let Some(n) = name {
579                    self.pass.bind(
580                        n.as_symbol().expect("matched"),
581                        n.span,
582                        BinderKind::Function,
583                    );
584                }
585                for (n, s, _) in &pairs {
586                    self.pass.bind(n, *s, BinderKind::Let);
587                }
588            }
589            "let*" => {
590                for (n, s, init) in &pairs {
591                    if let Some(e) = init {
592                        self.walk(e, opaque);
593                    }
594                    self.pass.bind(n, *s, BinderKind::Let);
595                }
596            }
597            _ => {
598                for (n, s, _) in &pairs {
599                    self.pass.bind(n, *s, BinderKind::Let);
600                }
601                for (_, _, init) in &pairs {
602                    if let Some(e) = init {
603                        self.walk(e, opaque);
604                    }
605                }
606            }
607        }
608        self.hoist(body);
609        self.walk_all(body, opaque);
610        self.close();
611    }
612}