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/// - `legacy_names(since, [:first, …])`: definitions that share a builtin's
129///   name and were reached bare under the flat runtime. Over the same window
130///   an importer's bare call of one is B0012; outside it, and for every
131///   definition not listed, the importer's bare name is the builtin.
132///
133/// Like `use`, it is consumed by the resolver and evaluates to nothing; an
134/// expired ledger stays in the source, and powers the fix for a late caller.
135#[derive(Clone, Debug, PartialEq)]
136pub struct Legacy {
137    pub since: String,
138    pub kind: LegacyKind,
139    pub span: Span,
140}
141
142/// What a `legacy_names` declaration records.
143#[derive(Clone, Debug, PartialEq)]
144pub enum LegacyKind {
145    /// The prefix every definition once carried.
146    Prefix(String),
147    /// Definitions moved out of the bidama, in source order.
148    Moved(Vec<Moved>),
149    Flat(Vec<(String, Span)>),
150}
151
152/// One moved definition: `old` in the declaring bidama is `home::new` now.
153/// `home` is empty when the target was not written `home::new`.
154///
155/// A third string retires a word whose replacement takes more arguments:
156/// `["refuse", "kyohi::enforce", "[(fs) -> fs], {kind: :first}"]` says
157/// `refuse(x)` is written `kyohi::enforce(x, [(fs) -> fs], {kind: :first})`.
158/// A retired word never resolves; its callers' fix is read from this row.
159#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct Moved {
161    pub old: String,
162    pub home: String,
163    pub new: String,
164    pub append: Option<String>,
165}
166
167/// Is this form a `legacy_names` declaration? A version string, then a
168/// prefix string, a list of `[old, "home::new"]` string pairs (or
169/// `[old, "home::new", "args"]` retirements), or a list of flat-era
170/// `:name` keywords, or it is not
171/// one (and `legacy_names` is then an unbound call).
172#[must_use]
173pub fn legacy_target(form: &Spanned) -> Option<Legacy> {
174    let items = form.as_list()?;
175    let [head, since, what] = items else {
176        return None;
177    };
178    if head.as_symbol() != Some(LEGACY_NAMES) {
179        return None;
180    }
181    let SpannedForm::Atom(Atom::Str(since)) = &since.form else {
182        return None;
183    };
184    let str_of = |s: &Spanned| match &s.form {
185        SpannedForm::Atom(Atom::Str(x)) => Some(x.clone()),
186        _ => None,
187    };
188    let keyword_of = |s: &Spanned| match &s.form {
189        SpannedForm::Atom(Atom::Keyword(k)) => Some((k.clone(), s.span)),
190        _ => None,
191    };
192    let kind = if let Some(prefix) = str_of(what) {
193        LegacyKind::Prefix(prefix)
194    } else if let Some(flat) = list_items(what)
195        .filter(|items| !items.is_empty())
196        .and_then(|items| items.iter().map(keyword_of).collect::<Option<Vec<_>>>())
197    {
198        LegacyKind::Flat(flat)
199    } else {
200        let moves = list_items(what)?
201            .iter()
202            .map(|pair| {
203                let (old, target, append) = match list_items(pair)? {
204                    [old, target] => (old, target, None),
205                    [old, target, append] => (old, target, Some(str_of(append)?)),
206                    _ => return None,
207                };
208                let (old, target) = (str_of(old)?, str_of(target)?);
209                let (home, new) = target
210                    .split_once("::")
211                    .map_or((String::new(), target.clone()), |(h, n)| {
212                        (h.to_string(), n.to_string())
213                    });
214                Some(Moved {
215                    old,
216                    home,
217                    new,
218                    append,
219                })
220            })
221            .collect::<Option<Vec<_>>>()?;
222        LegacyKind::Moved(moves)
223    };
224    Some(Legacy {
225        since: since.clone(),
226        kind,
227        span: form.span,
228    })
229}
230
231/// The elements of a list literal `[a, b]`, which parses to `(list a b)`.
232fn list_items(form: &Spanned) -> Option<&[Spanned]> {
233    let items = form.as_list()?;
234    (items.first()?.as_symbol() == Some("list")).then(|| &items[1..])
235}
236
237/// The special forms whose SHAPE the walker knows — the heads a pass without
238/// an interpreter to ask (the reach walk) answers [`HeadKind::SpecialForm`]
239/// for, so their binders bind. Every other special form is walked as an
240/// ordinary application, which is what it is for scoping purposes.
241pub const SHAPED_FORMS: &[&str] = &[
242    "lambda",
243    "let",
244    "let*",
245    "letrec",
246    "try",
247    "cond",
248    "quote",
249    "quasiquote",
250    "provide",
251    "require",
252];
253
254/// A pass over the binder grammar. The walker calls these in source order.
255pub trait Scopes {
256    /// Which arbiter claims `name` as a head, if any. `None` walks the call
257    /// as an ordinary application.
258    fn head_kind(&self, name: &str) -> Option<HeadKind>;
259    /// A frame opens: a function body, a `let`, a `catch`, a test body.
260    fn open(&mut self);
261    /// `name` is bound in the innermost open frame.
262    fn bind(&mut self, name: &str, span: Span, kind: BinderKind);
263    /// The innermost frame closes.
264    fn close(&mut self);
265    /// A symbol in code position. `opaque`: inside a macro call's arguments,
266    /// which are syntax the macro may never evaluate.
267    fn reference(&mut self, node: &Spanned, name: &str, opaque: bool);
268    fn call(&mut self, node: &Spanned, _whole: &Spanned, name: &str, opaque: bool) {
269        self.reference(node, name, opaque);
270    }
271    fn place(&mut self, node: &Spanned, name: &str, opaque: bool) {
272        self.reference(node, name, opaque);
273    }
274    /// The definition a test's declaration targets: resolved as a place,
275    /// named and never called.
276    fn target(&mut self, node: &Spanned, name: &str, opaque: bool) {
277        self.place(node, name, opaque);
278    }
279    /// The head of a special form or a macro call — not a reference to a
280    /// binding, but still a name the program writes.
281    fn head(&mut self, _node: &Spanned, _name: &str, _kind: HeadKind) {}
282    /// A symbol inside a quasiquoted template, outside any `unquote`: data at
283    /// expansion, code once the expansion runs.
284    fn template_symbol(&mut self, _node: &Spanned, _name: &str) {}
285}
286
287/// Walk a whole program. Top-level definitions are the pass's to know (they
288/// are globals, see [`definitions_of`]); the walker opens frames only below
289/// them.
290pub fn walk_program(forms: &[Spanned], pass: &mut dyn Scopes) {
291    for form in forms {
292        walk_top(form, pass);
293    }
294}
295
296/// Walk one top-level form.
297pub fn walk_top(form: &Spanned, pass: &mut dyn Scopes) {
298    Walker { pass, depth: 0 }.walk(form, false);
299}
300
301/// The names `form` defines into the frame it is evaluated in, with the
302/// span of each name and whether it is a value or a macro.
303///
304/// A `define` binds in the environment it is EVALUATED in, and every part of
305/// a form is evaluated in the enclosing environment except the parts of a
306/// frame-opening form (`lambda`, a function `define`, `let`, a `catch`
307/// clause, a test body) and quoted data. So this descends into every
308/// sub-form but those: `x = if c … else y = 1 … end` binds `y` in the
309/// function's frame, which is where a later `fn(s) … y … end` reads it.
310#[must_use]
311pub fn definitions_of(form: &Spanned) -> Vec<(String, Span, HeadKind)> {
312    let mut out = Vec::new();
313    definition_nodes(form, &mut |node, kind| {
314        if let Some(n) = node.as_symbol() {
315            out.push((n.to_string(), node.span, kind));
316        }
317    });
318    out
319}
320
321/// [`definitions_of`], handing over the NAME NODE of each definition, so a
322/// pass that rewrites definitions can find the symbol it renames.
323pub fn definition_nodes<'a>(form: &'a Spanned, out: &mut dyn FnMut(&'a Spanned, HeadKind)) {
324    let Some(items) = form.as_list() else { return };
325    let head = items.first().and_then(Spanned::as_symbol);
326    match head {
327        Some("defmacro") => {
328            if let Some(n) = items.get(1).filter(|n| n.as_symbol().is_some()) {
329                out(n, HeadKind::Macro);
330            }
331        }
332        Some("define" | "define-typed") => {
333            if let Some(n) = defined_name_node(items) {
334                out(n, HeadKind::Value);
335            }
336            // `(define x e)`: `e` is evaluated right here, so its defines are
337            // this frame's too. A function define opens a frame; stop.
338            if items.get(1).is_some_and(|t| t.as_symbol().is_some()) {
339                for item in &items[2..] {
340                    definition_nodes(item, out);
341                }
342            }
343        }
344        Some("lambda" | "let" | "let*" | "letrec" | "deftest" | "quote" | "quasiquote") => {}
345        Some("try") => {
346            // The body shares the frame; a `catch` clause opens its own.
347            for item in &items[1..] {
348                let is_clause = item
349                    .as_list()
350                    .and_then(|c| c.first())
351                    .and_then(Spanned::as_symbol)
352                    .is_some_and(|h| h == "catch" || h == "finally");
353                if !is_clause {
354                    definition_nodes(item, out);
355                }
356            }
357        }
358        _ => {
359            for item in items {
360                definition_nodes(item, out);
361            }
362        }
363    }
364}
365
366#[must_use]
367pub fn nullary_definition(form: &Spanned) -> Option<&str> {
368    let items = form.as_list()?;
369    let head = items.first()?.as_symbol()?;
370    if head != "define" && head != "define-typed" {
371        return None;
372    }
373    match items.get(1)?.as_list()? {
374        [name] => name.as_symbol(),
375        _ => None,
376    }
377}
378
379/// The name node a `define`/`define-typed` form binds.
380#[must_use]
381pub fn defined_name_node(items: &[Spanned]) -> Option<&Spanned> {
382    let head = items.first()?.as_symbol()?;
383    if head != "define" && head != "define-typed" {
384        return None;
385    }
386    let target = items.get(1)?;
387    if target.as_symbol().is_some() {
388        return Some(target);
389    }
390    let name = target.as_list()?.first()?;
391    name.as_symbol().map(|_| name)
392}
393
394/// A lambda parameter list: symbols, skipping `&rest`/`&optional`/`.`
395/// markers; a bare symbol is a variadic parameter.
396#[must_use]
397pub fn params_of(list: &Spanned) -> Vec<(String, Span)> {
398    if let Some(n) = list.as_symbol() {
399        return vec![(n.to_string(), list.span)];
400    }
401    list.as_list()
402        .map(|ps| ps.iter().filter_map(param).collect())
403        .unwrap_or_default()
404}
405
406fn param(p: &Spanned) -> Option<(String, Span)> {
407    let n = p.as_symbol()?;
408    if n.starts_with('&') || n == "." {
409        return None;
410    }
411    Some((n.to_string(), p.span))
412}
413
414struct Walker<'p> {
415    pass: &'p mut dyn Scopes,
416    /// Open frames. Zero at the top level of a form.
417    depth: usize,
418}
419
420impl Walker<'_> {
421    fn open(&mut self) {
422        self.depth += 1;
423        self.pass.open();
424    }
425
426    fn close(&mut self) {
427        self.depth -= 1;
428        self.pass.close();
429    }
430
431    /// Bind, in the current frame, every `define` a body reaches without
432    /// crossing a frame boundary — so a body can read a local defined later in
433    /// it, as a closure over the frame can at runtime.
434    fn hoist(&mut self, body: &[Spanned]) {
435        for f in body {
436            let function = f.as_list().is_some_and(|items| {
437                items.first().and_then(Spanned::as_symbol) == Some("define")
438                    && items.get(1).is_some_and(|t| t.as_list().is_some())
439            });
440            let pass = &mut *self.pass;
441            definition_nodes(f, &mut |node, kind| {
442                let Some(name) = node.as_symbol() else { return };
443                let bk = match kind {
444                    HeadKind::Value if !function => BinderKind::Assignment,
445                    _ => BinderKind::Function,
446                };
447                pass.bind(name, node.span, bk);
448            });
449        }
450    }
451
452    /// Walk a body: a new frame, its parameters bound, its defines hoisted,
453    /// each form walked.
454    fn body(&mut self, params: &[(String, Span)], forms: &[Spanned], opaque: bool) {
455        self.open();
456        for (p, s) in params {
457            self.pass.bind(p, *s, BinderKind::Parameter);
458        }
459        self.hoist(forms);
460        for f in forms {
461            self.walk(f, opaque);
462        }
463        self.close();
464    }
465
466    fn walk(&mut self, form: &Spanned, opaque: bool) {
467        match &form.form {
468            SpannedForm::Atom(Atom::Symbol(n)) => self.pass.reference(form, n, opaque),
469            SpannedForm::Quote(_) => {}
470            SpannedForm::Quasiquote(inner) => self.walk_template(inner, opaque),
471            SpannedForm::Unquote(inner) | SpannedForm::UnquoteSplice(inner) => {
472                self.walk(inner, opaque);
473            }
474            SpannedForm::List(items) if !items.is_empty() => self.walk_list(form, items, opaque),
475            _ => {}
476        }
477    }
478
479    /// Inside a quasiquote only the unquoted parts are code.
480    fn walk_template(&mut self, form: &Spanned, opaque: bool) {
481        match &form.form {
482            SpannedForm::Atom(Atom::Symbol(n)) => self.pass.template_symbol(form, n),
483            SpannedForm::Unquote(inner) | SpannedForm::UnquoteSplice(inner) => {
484                self.walk(inner, opaque);
485            }
486            SpannedForm::List(items) => {
487                for i in items {
488                    self.walk_template(i, opaque);
489                }
490            }
491            SpannedForm::Quasiquote(inner) => self.walk_template(inner, opaque),
492            _ => {}
493        }
494    }
495
496    /// A test's declaration is not code: its head is the harness's, its
497    /// controls are data, and its target is a reference resolved as a place,
498    /// so a zero-parameter definition it names is named, never called.
499    fn walk_test_meta(&mut self, meta: &Spanned, opaque: bool) {
500        if let Some(head) = meta.as_list().and_then(|items| items.first()) {
501            self.pass
502                .head(head, crate::parse::LOWERED_TEST_META, HeadKind::SpecialForm);
503        }
504        let target = crate::test_form::test_meta(meta).and_then(|m| m.target);
505        if let Some(t) = target {
506            if let Some(name) = t.as_symbol() {
507                self.pass.target(t, name, opaque);
508            }
509        }
510    }
511
512    fn walk_all(&mut self, items: &[Spanned], opaque: bool) {
513        for i in items {
514            self.walk(i, opaque);
515        }
516    }
517
518    fn walk_list(&mut self, form: &Spanned, items: &[Spanned], opaque: bool) {
519        let Some(head) = items[0].as_symbol() else {
520            self.walk_all(items, opaque);
521            return;
522        };
523        // `deftest` is the test harness's declaration, not a special form: a
524        // body, run in a frame of its own.
525        if head == "deftest" && self.depth == 0 {
526            match crate::test_form::test_parts(form) {
527                Some(parts) => {
528                    if let Some(meta) = parts.meta {
529                        self.walk_test_meta(meta, opaque);
530                    }
531                    self.body(&[], std::slice::from_ref(parts.body), opaque);
532                }
533                None => {
534                    if let Some(body) = items.get(2) {
535                        self.body(&[], std::slice::from_ref(body), opaque);
536                    }
537                }
538            }
539            return;
540        }
541        // `use("x", [:a])` at the top level is a declaration the resolver
542        // consumed, not a call: nothing in it is a reference.
543        if (head == "use" || head == LEGACY_NAMES) && self.depth == 0 {
544            self.pass.head(&items[0], head, HeadKind::SpecialForm);
545            return;
546        }
547        // `define-typed` is erased before evaluation; its shape is `define`'s
548        // with each parameter written `(name Type)`.
549        if head == "define" || head == "define-typed" {
550            self.pass.head(&items[0], head, HeadKind::SpecialForm);
551            self.walk_define(head, items, opaque);
552            return;
553        }
554        // `defmacro` is registered by the expander before evaluation, not
555        // dispatched as a special form, so no arbiter claims it as a head.
556        // `(defmacro name (params) body…)`.
557        if head == "defmacro" && self.depth == 0 {
558            self.pass.head(&items[0], head, HeadKind::SpecialForm);
559            let params = items.get(2).map(params_of).unwrap_or_default();
560            self.body(&params, items.get(3..).unwrap_or(&[]), opaque);
561            return;
562        }
563        match self.pass.head_kind(head) {
564            Some(HeadKind::SpecialForm) => {
565                self.pass.head(&items[0], head, HeadKind::SpecialForm);
566                self.walk_special(head, items, opaque);
567            }
568            Some(HeadKind::Macro) => {
569                // Opaque: a macro's arguments are syntax.
570                self.pass.head(&items[0], head, HeadKind::Macro);
571                for i in &items[1..] {
572                    self.walk(i, true);
573                }
574            }
575            Some(HeadKind::Value) | None => {
576                self.pass.call(&items[0], form, head, opaque);
577                self.walk_all(&items[1..], opaque);
578            }
579        }
580    }
581
582    fn walk_define(&mut self, head: &str, items: &[Spanned], opaque: bool) {
583        let Some(target) = items.get(1) else { return };
584        if target.as_symbol().is_some() {
585            // `(define x e)`: `x` is already bound (hoisted, or a global).
586            self.walk_all(&items[2..], opaque);
587            return;
588        }
589        let Some(sig) = target.as_list() else {
590            self.walk_all(&items[1..], opaque);
591            return;
592        };
593        let params: Vec<(String, Span)> = sig
594            .iter()
595            .skip(1)
596            .filter_map(|p| {
597                if head == "define-typed" && p.as_list().is_some() {
598                    let n = p.as_list()?.first()?;
599                    Some((n.as_symbol()?.to_string(), n.span))
600                } else {
601                    param(p)
602                }
603            })
604            .collect();
605        // `(define-typed sig R body)`: the return type is not code.
606        let body_from = if head == "define-typed" { 3 } else { 2 };
607        self.body(&params, items.get(body_from..).unwrap_or(&[]), opaque);
608    }
609
610    fn walk_special(&mut self, head: &str, items: &[Spanned], opaque: bool) {
611        let args = &items[1..];
612        match head {
613            "quote" | "provide" | "require" => {}
614            "quasiquote" => {
615                for a in args {
616                    self.walk_template(a, opaque);
617                }
618            }
619            "lambda" => {
620                let params = args.first().map(params_of).unwrap_or_default();
621                self.body(&params, args.get(1..).unwrap_or(&[]), opaque);
622            }
623            "cond" => {
624                for clause in args {
625                    match clause.as_list() {
626                        Some(parts) => {
627                            let rest = match parts.first().and_then(Spanned::as_symbol) {
628                                Some("else") => &parts[1..],
629                                _ => parts,
630                            };
631                            for p in rest {
632                                if p.as_symbol() != Some("=>") {
633                                    self.walk(p, opaque);
634                                }
635                            }
636                        }
637                        None => self.walk(clause, opaque),
638                    }
639                }
640            }
641            "let" | "let*" | "letrec" => self.walk_let(head, args, opaque),
642            "set!" => match args.split_first() {
643                Some((target, rest)) if target.as_symbol().is_some() => {
644                    self.pass
645                        .place(target, target.as_symbol().expect("matched"), opaque);
646                    self.walk_all(rest, opaque);
647                }
648                _ => self.walk_all(args, opaque),
649            },
650            "try" => {
651                for a in args {
652                    let clause = a.as_list();
653                    match clause.and_then(|c| c.first()).and_then(Spanned::as_symbol) {
654                        Some("catch") => {
655                            let c = clause.expect("matched");
656                            let var = c.get(1).and_then(|v| {
657                                v.as_list()
658                                    .and_then(|l| l.first())
659                                    .and_then(|s| s.as_symbol().map(|n| (n.to_string(), s.span)))
660                                    .or_else(|| v.as_symbol().map(|n| (n.to_string(), v.span)))
661                            });
662                            self.open();
663                            if let Some((n, s)) = var {
664                                self.pass.bind(&n, s, BinderKind::Catch);
665                            }
666                            self.walk_all(c.get(2..).unwrap_or(&[]), opaque);
667                            self.close();
668                        }
669                        Some("finally") => {
670                            let c = clause.expect("matched");
671                            self.walk_all(&c[1..], opaque);
672                        }
673                        _ => self.walk(a, opaque),
674                    }
675                }
676            }
677            // if, when, unless, and, or, not, begin, set!, delay, eval,
678            // macroexpand, macroexpand-1: every part is an expression.
679            _ => self.walk_all(args, opaque),
680        }
681    }
682
683    fn walk_let(&mut self, head: &str, args: &[Spanned], opaque: bool) {
684        // Named let: `(let name ((a e) …) body…)`.
685        let (name, rest) = match args.first() {
686            Some(n) if n.as_symbol().is_some() && head == "let" => (Some(n), &args[1..]),
687            _ => (None, args),
688        };
689        let Some(bindings) = rest.first().and_then(Spanned::as_list) else {
690            self.walk_all(rest, opaque);
691            return;
692        };
693        let pairs: Vec<(String, Span, Option<&Spanned>)> = bindings
694            .iter()
695            .filter_map(|b| match b.as_list() {
696                Some(pair) => {
697                    let n = pair.first()?;
698                    Some((n.as_symbol()?.to_string(), n.span, pair.get(1)))
699                }
700                None => b.as_symbol().map(|n| (n.to_string(), b.span, None)),
701            })
702            .collect();
703        let body = rest.get(1..).unwrap_or(&[]);
704        if head == "let" {
705            // Inits see the OUTER scope: walk them before the frame opens.
706            for (_, _, init) in &pairs {
707                if let Some(e) = init {
708                    self.walk(e, opaque);
709                }
710            }
711        }
712        self.open();
713        match head {
714            "let" => {
715                if let Some(n) = name {
716                    self.pass.bind(
717                        n.as_symbol().expect("matched"),
718                        n.span,
719                        BinderKind::Function,
720                    );
721                }
722                for (n, s, _) in &pairs {
723                    self.pass.bind(n, *s, BinderKind::Let);
724                }
725            }
726            "let*" => {
727                for (n, s, init) in &pairs {
728                    if let Some(e) = init {
729                        self.walk(e, opaque);
730                    }
731                    self.pass.bind(n, *s, BinderKind::Let);
732                }
733            }
734            _ => {
735                for (n, s, _) in &pairs {
736                    self.pass.bind(n, *s, BinderKind::Let);
737                }
738                for (_, _, init) in &pairs {
739                    if let Some(e) = init {
740                        self.walk(e, opaque);
741                    }
742                }
743            }
744        }
745        self.hoist(body);
746        self.walk_all(body, opaque);
747        self.close();
748    }
749}