Skip to main content

bock_air/
scope.rs

1//! Scope analysis pass — S-AIR layer.
2//!
3//! Builds a [`ScopeTree`] from an AST [`Module`].  Every block, function,
4//! loop, match arm, and handling block introduces a new [`Scope`].
5//! Variable lifetimes are bounded by the scope in which they are declared.
6//!
7//! # Scope-introducing constructs
8//! - Module root (implicit)
9//! - Function body (params + body share one scope)
10//! - Lambda body (params + body share one scope)
11//! - Block expressions
12//! - For loop body (loop variable in scope)
13//! - While / loop bodies
14//! - Match arm bodies (pattern bindings in scope)
15//! - If-let then block (binding in scope)
16//! - Handling blocks
17
18use std::collections::HashMap;
19
20use bock_ast::{
21    Block, Expr, FnDecl, ForLoop, GuardStmt, HandlingBlock, ImplBlock, Item, LetStmt, LoopStmt,
22    MatchArm, Module, NodeId, Param, Pattern, RecordPatternField, Stmt, WhileLoop,
23};
24
25// ─── Public types ─────────────────────────────────────────────────────────────
26
27/// Unique identifier for a [`Scope`] in the [`ScopeTree`].
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub struct ScopeId(pub u32);
30
31/// A single name binding within a [`Scope`].
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Binding {
34    /// The name as written in source.
35    pub name: String,
36    /// The NodeId of the declaration (pattern node, param node, etc.).
37    pub node_id: NodeId,
38    /// Whether this binding was declared with `mut`.
39    pub mutable: bool,
40}
41
42/// A single lexical scope in the scope tree.
43#[derive(Debug)]
44pub struct Scope {
45    /// This scope's identity.
46    pub id: ScopeId,
47    /// Parent scope (`None` only for the module root scope).
48    pub parent: Option<ScopeId>,
49    /// Bindings introduced directly in this scope.
50    pub bindings: Vec<Binding>,
51}
52
53impl Scope {
54    fn new(id: ScopeId, parent: Option<ScopeId>) -> Self {
55        Self {
56            id,
57            parent,
58            bindings: Vec::new(),
59        }
60    }
61
62    fn add_binding(&mut self, name: String, node_id: NodeId, mutable: bool) {
63        self.bindings.push(Binding {
64            name,
65            node_id,
66            mutable,
67        });
68    }
69}
70
71/// The complete scope tree for a module.
72///
73/// Scopes are stored in insertion order.  The root scope always has
74/// [`ScopeId`]`(0)`.
75pub struct ScopeTree {
76    scopes: HashMap<ScopeId, Scope>,
77    next_id: u32,
78}
79
80impl Default for ScopeTree {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86impl ScopeTree {
87    /// Creates a new scope tree with an empty root scope.
88    #[must_use]
89    pub fn new() -> Self {
90        let root = Scope::new(ScopeId(0), None);
91        let mut scopes = HashMap::new();
92        scopes.insert(ScopeId(0), root);
93        Self { scopes, next_id: 1 }
94    }
95
96    /// Returns the root [`ScopeId`].
97    #[must_use]
98    pub fn root(&self) -> ScopeId {
99        ScopeId(0)
100    }
101
102    /// Returns the scope for `id`, if it exists.
103    #[must_use]
104    pub fn get(&self, id: ScopeId) -> Option<&Scope> {
105        self.scopes.get(&id)
106    }
107
108    /// Returns the total number of scopes in the tree.
109    #[must_use]
110    pub fn scope_count(&self) -> usize {
111        self.scopes.len()
112    }
113
114    /// Looks up `name` starting from `from` and walking up the parent chain.
115    ///
116    /// Returns the first matching [`Binding`], or `None` if not found.
117    #[must_use]
118    pub fn lookup(&self, name: &str, from: ScopeId) -> Option<&Binding> {
119        let mut current = Some(from);
120        while let Some(id) = current {
121            if let Some(scope) = self.scopes.get(&id) {
122                // Search bindings in reverse so later declarations shadow earlier ones.
123                if let Some(b) = scope.bindings.iter().rev().find(|b| b.name == name) {
124                    return Some(b);
125                }
126                current = scope.parent;
127            } else {
128                break;
129            }
130        }
131        None
132    }
133
134    /// Allocates a new child scope under `parent` and returns its [`ScopeId`].
135    fn alloc(&mut self, parent: ScopeId) -> ScopeId {
136        let id = ScopeId(self.next_id);
137        self.next_id += 1;
138        self.scopes.insert(id, Scope::new(id, Some(parent)));
139        id
140    }
141
142    /// Adds a binding to the scope identified by `scope_id`.
143    fn bind(&mut self, scope_id: ScopeId, name: String, node_id: NodeId, mutable: bool) {
144        if let Some(scope) = self.scopes.get_mut(&scope_id) {
145            scope.add_binding(name, node_id, mutable);
146        }
147    }
148}
149
150// ─── Public entry point ───────────────────────────────────────────────────────
151
152/// Build a [`ScopeTree`] from a parsed [`Module`].
153///
154/// Walks every declaration and expression in the module, allocating a new
155/// scope for each scope-introducing construct and recording all name bindings.
156#[must_use]
157pub fn build_scope_tree(module: &Module) -> ScopeTree {
158    let mut tree = ScopeTree::new();
159    let root = tree.root();
160    let mut builder = ScopeBuilder { tree: &mut tree };
161    builder.visit_module(module, root);
162    tree
163}
164
165// ─── Builder (private) ────────────────────────────────────────────────────────
166
167struct ScopeBuilder<'t> {
168    tree: &'t mut ScopeTree,
169}
170
171impl<'t> ScopeBuilder<'t> {
172    fn visit_module(&mut self, module: &Module, scope: ScopeId) {
173        // Collect top-level declaration names into the root scope.
174        for item in &module.items {
175            self.collect_item_name(item, scope);
176        }
177        // Then recurse into each item's body.
178        for item in &module.items {
179            self.visit_item(item, scope);
180        }
181    }
182
183    fn collect_item_name(&mut self, item: &Item, scope: ScopeId) {
184        match item {
185            Item::Fn(d) => self.tree.bind(scope, d.name.name.clone(), d.id, false),
186            Item::Record(d) => self.tree.bind(scope, d.name.name.clone(), d.id, false),
187            Item::Enum(d) => self.tree.bind(scope, d.name.name.clone(), d.id, false),
188            Item::Class(d) => self.tree.bind(scope, d.name.name.clone(), d.id, false),
189            Item::Trait(d) => self.tree.bind(scope, d.name.name.clone(), d.id, false),
190            Item::Effect(d) => self.tree.bind(scope, d.name.name.clone(), d.id, false),
191            Item::TypeAlias(d) => self.tree.bind(scope, d.name.name.clone(), d.id, false),
192            Item::Const(d) => self.tree.bind(scope, d.name.name.clone(), d.id, false),
193            Item::Impl(_)
194            | Item::ModuleHandle(_)
195            | Item::PropertyTest(_)
196            | Item::Error { .. }
197            | Item::PlatformTrait(_) => {}
198        }
199    }
200
201    fn visit_item(&mut self, item: &Item, parent: ScopeId) {
202        match item {
203            Item::Fn(d) => self.visit_fn(d, parent),
204            Item::Class(d) => {
205                for method in &d.methods {
206                    self.visit_fn(method, parent);
207                }
208            }
209            Item::Trait(d) => {
210                for method in &d.methods {
211                    self.visit_fn(method, parent);
212                }
213            }
214            Item::Effect(d) => {
215                for op in &d.operations {
216                    self.visit_fn(op, parent);
217                }
218            }
219            Item::Impl(d) => self.visit_impl(d, parent),
220            Item::Const(d) => self.visit_expr(&d.value, parent),
221            Item::Record(_)
222            | Item::Enum(_)
223            | Item::TypeAlias(_)
224            | Item::ModuleHandle(_)
225            | Item::PropertyTest(_)
226            | Item::PlatformTrait(_)
227            | Item::Error { .. } => {}
228        }
229    }
230
231    fn visit_impl(&mut self, impl_block: &ImplBlock, parent: ScopeId) {
232        for method in &impl_block.methods {
233            self.visit_fn(method, parent);
234        }
235    }
236
237    /// Function body creates one scope for params + body.
238    fn visit_fn(&mut self, decl: &FnDecl, parent: ScopeId) {
239        let fn_scope = self.tree.alloc(parent);
240        for param in &decl.params {
241            self.bind_param(param, fn_scope);
242        }
243        if let Some(ref body) = decl.body {
244            self.visit_block(body, fn_scope);
245        }
246    }
247
248    fn bind_param(&mut self, param: &Param, scope: ScopeId) {
249        self.bind_pattern(&param.pattern, scope);
250        if let Some(default) = &param.default {
251            self.visit_expr(default, scope);
252        }
253    }
254
255    fn visit_block(&mut self, block: &Block, parent: ScopeId) {
256        let block_scope = self.tree.alloc(parent);
257        for stmt in &block.stmts {
258            self.visit_stmt(stmt, block_scope);
259        }
260        if let Some(tail) = &block.tail {
261            self.visit_expr(tail, block_scope);
262        }
263    }
264
265    fn visit_stmt(&mut self, stmt: &Stmt, scope: ScopeId) {
266        match stmt {
267            Stmt::Let(let_stmt) => self.visit_let(let_stmt, scope),
268            Stmt::Expr(expr) => self.visit_expr(expr, scope),
269            Stmt::For(for_loop) => self.visit_for(for_loop, scope),
270            Stmt::While(while_loop) => self.visit_while(while_loop, scope),
271            Stmt::Loop(loop_stmt) => self.visit_loop(loop_stmt, scope),
272            Stmt::Guard(guard) => self.visit_guard(guard, scope),
273            Stmt::Handling(handling) => self.visit_handling(handling, scope),
274            Stmt::Empty => {}
275        }
276    }
277
278    fn visit_let(&mut self, let_stmt: &LetStmt, scope: ScopeId) {
279        // RHS is evaluated in the enclosing scope.
280        self.visit_expr(&let_stmt.value, scope);
281        // Pattern bindings are visible from here to scope end.
282        self.bind_pattern(&let_stmt.pattern, scope);
283    }
284
285    fn visit_for(&mut self, for_loop: &ForLoop, parent: ScopeId) {
286        // Iterable evaluated in parent scope.
287        self.visit_expr(&for_loop.iterable, parent);
288        // Loop variable and body share a new scope.
289        let loop_scope = self.tree.alloc(parent);
290        self.bind_pattern(&for_loop.pattern, loop_scope);
291        self.visit_block(&for_loop.body, loop_scope);
292    }
293
294    fn visit_while(&mut self, while_loop: &WhileLoop, parent: ScopeId) {
295        self.visit_expr(&while_loop.condition, parent);
296        self.visit_block(&while_loop.body, parent);
297    }
298
299    fn visit_loop(&mut self, loop_stmt: &LoopStmt, parent: ScopeId) {
300        self.visit_block(&loop_stmt.body, parent);
301    }
302
303    fn visit_guard(&mut self, guard: &GuardStmt, parent: ScopeId) {
304        if let Some(pat) = &guard.let_pattern {
305            self.bind_pattern(pat, parent);
306        }
307        self.visit_expr(&guard.condition, parent);
308        self.visit_block(&guard.else_block, parent);
309    }
310
311    fn visit_handling(&mut self, handling: &HandlingBlock, parent: ScopeId) {
312        // Handling block creates its own scope for the body.
313        let handling_scope = self.tree.alloc(parent);
314        // Handler expressions are evaluated in the parent scope.
315        for pair in &handling.handlers {
316            self.visit_expr(&pair.handler, parent);
317        }
318        self.visit_block(&handling.body, handling_scope);
319    }
320
321    fn visit_expr(&mut self, expr: &Expr, scope: ScopeId) {
322        match expr {
323            Expr::Literal { .. }
324            | Expr::Identifier { .. }
325            | Expr::Continue { .. }
326            | Expr::Unreachable { .. }
327            | Expr::Placeholder { .. } => {}
328
329            Expr::Binary { left, right, .. }
330            | Expr::Compose { left, right, .. }
331            | Expr::Pipe { left, right, .. }
332            | Expr::Range {
333                lo: left,
334                hi: right,
335                ..
336            } => {
337                self.visit_expr(left, scope);
338                self.visit_expr(right, scope);
339            }
340
341            Expr::Unary { operand, .. }
342            | Expr::Try { expr: operand, .. }
343            | Expr::Await { expr: operand, .. } => {
344                self.visit_expr(operand, scope);
345            }
346
347            Expr::Assign { target, value, .. } => {
348                self.visit_expr(target, scope);
349                self.visit_expr(value, scope);
350            }
351
352            Expr::Call { callee, args, .. } => {
353                self.visit_expr(callee, scope);
354                for arg in args {
355                    self.visit_expr(&arg.value, scope);
356                }
357            }
358
359            Expr::MethodCall { receiver, args, .. } => {
360                self.visit_expr(receiver, scope);
361                for arg in args {
362                    self.visit_expr(&arg.value, scope);
363                }
364            }
365
366            Expr::FieldAccess { object, .. } => {
367                self.visit_expr(object, scope);
368            }
369
370            Expr::Index { object, index, .. } => {
371                self.visit_expr(object, scope);
372                self.visit_expr(index, scope);
373            }
374
375            Expr::Lambda { params, body, .. } => {
376                let lambda_scope = self.tree.alloc(scope);
377                for param in params {
378                    self.bind_param(param, lambda_scope);
379                }
380                self.visit_expr(body, lambda_scope);
381            }
382
383            Expr::If {
384                let_pattern,
385                condition,
386                then_block,
387                else_block,
388                ..
389            } => {
390                self.visit_expr(condition, scope);
391                if let Some(pat) = let_pattern {
392                    // if-let: pattern bindings scoped to then block.
393                    let then_scope = self.tree.alloc(scope);
394                    self.bind_pattern(pat, then_scope);
395                    self.visit_block(then_block, then_scope);
396                } else {
397                    self.visit_block(then_block, scope);
398                }
399                if let Some(else_expr) = else_block {
400                    self.visit_expr(else_expr, scope);
401                }
402            }
403
404            Expr::Match {
405                scrutinee, arms, ..
406            } => {
407                self.visit_expr(scrutinee, scope);
408                for arm in arms {
409                    self.visit_match_arm(arm, scope);
410                }
411            }
412
413            Expr::Loop { body, .. } => {
414                self.visit_block(body, scope);
415            }
416
417            Expr::Block { block, .. } => {
418                self.visit_block(block, scope);
419            }
420
421            Expr::RecordConstruct { fields, spread, .. } => {
422                for field in fields {
423                    if let Some(val) = &field.value {
424                        self.visit_expr(val, scope);
425                    }
426                }
427                if let Some(s) = spread {
428                    self.visit_expr(&s.expr, scope);
429                }
430            }
431
432            Expr::ListLiteral { elems, .. }
433            | Expr::SetLiteral { elems, .. }
434            | Expr::TupleLiteral { elems, .. } => {
435                for elem in elems {
436                    self.visit_expr(elem, scope);
437                }
438            }
439
440            Expr::MapLiteral { entries, .. } => {
441                for (k, v) in entries {
442                    self.visit_expr(k, scope);
443                    self.visit_expr(v, scope);
444                }
445            }
446
447            Expr::Return { value, .. } | Expr::Break { value, .. } => {
448                if let Some(val) = value {
449                    self.visit_expr(val, scope);
450                }
451            }
452
453            Expr::Interpolation { parts, .. } => {
454                for part in parts {
455                    if let bock_ast::InterpolationPart::Expr(e) = part {
456                        self.visit_expr(e, scope);
457                    }
458                }
459            }
460
461            Expr::Is { expr, .. } => {
462                self.visit_expr(expr, scope);
463            }
464        }
465    }
466
467    fn visit_match_arm(&mut self, arm: &MatchArm, parent: ScopeId) {
468        // Each arm body gets its own scope for pattern bindings.
469        let arm_scope = self.tree.alloc(parent);
470        self.bind_pattern(&arm.pattern, arm_scope);
471        if let Some(guard) = &arm.guard {
472            self.visit_expr(guard, arm_scope);
473        }
474        self.visit_expr(&arm.body, arm_scope);
475    }
476
477    /// Recursively binds all names introduced by a pattern into `scope`.
478    fn bind_pattern(&mut self, pat: &Pattern, scope: ScopeId) {
479        match pat {
480            Pattern::Bind { id, name, .. } => {
481                self.tree.bind(scope, name.name.clone(), *id, false);
482            }
483            Pattern::MutBind { id, name, .. } => {
484                self.tree.bind(scope, name.name.clone(), *id, true);
485            }
486            Pattern::Wildcard { .. } | Pattern::Literal { .. } | Pattern::Rest { .. } => {}
487
488            Pattern::Constructor { fields, .. } => {
489                for field in fields {
490                    self.bind_pattern(field, scope);
491                }
492            }
493            Pattern::Record { fields, .. } => {
494                for field in fields {
495                    self.bind_record_pattern_field(field, scope);
496                }
497            }
498            Pattern::Tuple { elems, .. } => {
499                for elem in elems {
500                    self.bind_pattern(elem, scope);
501                }
502            }
503            Pattern::List { elems, rest, .. } => {
504                for elem in elems {
505                    self.bind_pattern(elem, scope);
506                }
507                if let Some(r) = rest {
508                    self.bind_pattern(r, scope);
509                }
510            }
511            Pattern::Or { alternatives, .. } => {
512                // All alternatives must bind the same names; bind from the first.
513                if let Some(first) = alternatives.first() {
514                    self.bind_pattern(first, scope);
515                }
516            }
517            Pattern::Range { lo, hi, .. } => {
518                self.bind_pattern(lo, scope);
519                self.bind_pattern(hi, scope);
520            }
521        }
522    }
523
524    fn bind_record_pattern_field(&mut self, field: &RecordPatternField, scope: ScopeId) {
525        if let Some(pat) = &field.pattern {
526            self.bind_pattern(pat, scope);
527        } else {
528            // Shorthand `{ name }` — bind field name as an immutable variable.
529            // Use a synthetic NodeId 0 as placeholder (no span available here).
530            self.tree.bind(scope, field.name.name.clone(), 0, false);
531        }
532    }
533}
534
535// ─── Tests ────────────────────────────────────────────────────────────────────
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540    use bock_ast::{Block, FnDecl, Ident, Item, LetStmt, Literal, Module, Pattern, Stmt};
541    use bock_errors::{FileId, Span};
542
543    fn span() -> Span {
544        Span {
545            file: FileId(0),
546            start: 0,
547            end: 0,
548        }
549    }
550
551    fn ident(name: &str) -> Ident {
552        Ident {
553            name: name.to_string(),
554            span: span(),
555        }
556    }
557
558    fn make_module(items: Vec<Item>) -> Module {
559        Module {
560            id: 0,
561            span: span(),
562            doc: vec![],
563            path: None,
564            imports: vec![],
565            items,
566        }
567    }
568
569    fn empty_block(id: NodeId) -> Block {
570        Block {
571            id,
572            span: span(),
573            stmts: vec![],
574            tail: None,
575        }
576    }
577
578    fn simple_fn(id: NodeId, name: &str, body: Block) -> Item {
579        Item::Fn(FnDecl {
580            id,
581            span: span(),
582            annotations: vec![],
583            visibility: bock_ast::Visibility::Private,
584            is_async: false,
585            name: ident(name),
586            generic_params: vec![],
587            params: vec![],
588            return_type: None,
589            effect_clause: vec![],
590            where_clause: vec![],
591            body: Some(body),
592        })
593    }
594
595    #[test]
596    fn root_scope_exists() {
597        let module = make_module(vec![]);
598        let tree = build_scope_tree(&module);
599        assert!(tree.get(tree.root()).is_some());
600    }
601
602    #[test]
603    fn top_level_fn_bound_in_root() {
604        let module = make_module(vec![simple_fn(1, "foo", empty_block(2))]);
605        let tree = build_scope_tree(&module);
606        let found = tree.lookup("foo", tree.root());
607        assert!(found.is_some());
608        assert_eq!(found.unwrap().name, "foo");
609    }
610
611    #[test]
612    fn fn_creates_child_scope() {
613        let module = make_module(vec![simple_fn(1, "bar", empty_block(2))]);
614        let tree = build_scope_tree(&module);
615        // Root + fn scope + block scope = at least 3.
616        assert!(tree.scope_count() >= 3);
617    }
618
619    #[test]
620    fn let_binding_visible_in_scope() {
621        // Build: fn foo() { let x = (); }
622        let let_stmt = Stmt::Let(LetStmt {
623            id: 10,
624            span: span(),
625            pattern: Pattern::Bind {
626                id: 11,
627                span: span(),
628                name: ident("x"),
629            },
630            ty: None,
631            value: Expr::Literal {
632                id: 12,
633                span: span(),
634                lit: Literal::Unit,
635            },
636        });
637        let body = Block {
638            id: 2,
639            span: span(),
640            stmts: vec![let_stmt],
641            tail: None,
642        };
643        let module = make_module(vec![simple_fn(1, "foo", body)]);
644        let tree = build_scope_tree(&module);
645
646        // x should be findable from the innermost scope.
647        // We don't know the exact ScopeId, but we can verify the root scope
648        // can look up "foo" and that some scope has "x".
649        let x_in_root = tree.lookup("x", tree.root());
650        // x is NOT in root scope; it's inside foo's body scope.
651        assert!(x_in_root.is_none());
652
653        // Verify "foo" is in root.
654        assert!(tree.lookup("foo", tree.root()).is_some());
655    }
656
657    #[test]
658    fn shadowing_in_nested_scopes() {
659        // fn outer() { let x = (); fn inner() { let x = (); } }
660        // Both declare x; inner x shadows outer x.
661        let inner_let = Stmt::Let(LetStmt {
662            id: 20,
663            span: span(),
664            pattern: Pattern::Bind {
665                id: 21,
666                span: span(),
667                name: ident("x"),
668            },
669            ty: None,
670            value: Expr::Literal {
671                id: 22,
672                span: span(),
673                lit: Literal::Unit,
674            },
675        });
676        let inner_body = Block {
677            id: 30,
678            span: span(),
679            stmts: vec![inner_let],
680            tail: None,
681        };
682        let outer_let = Stmt::Let(LetStmt {
683            id: 40,
684            span: span(),
685            pattern: Pattern::Bind {
686                id: 41,
687                span: span(),
688                name: ident("x"),
689            },
690            ty: None,
691            value: Expr::Literal {
692                id: 42,
693                span: span(),
694                lit: Literal::Unit,
695            },
696        });
697        let inner_fn_item = Item::Fn(FnDecl {
698            id: 50,
699            span: span(),
700            annotations: vec![],
701            visibility: bock_ast::Visibility::Private,
702            is_async: false,
703            name: ident("inner"),
704            generic_params: vec![],
705            params: vec![],
706            return_type: None,
707            effect_clause: vec![],
708            where_clause: vec![],
709            body: Some(inner_body),
710        });
711        // outer body: [let x = (); inner fn decl as stmt]
712        let outer_body = Block {
713            id: 60,
714            span: span(),
715            stmts: vec![outer_let],
716            tail: None,
717        };
718        let module = make_module(vec![simple_fn(1, "outer", outer_body), inner_fn_item]);
719        let tree = build_scope_tree(&module);
720        // Both "outer" and "inner" visible in root.
721        assert!(tree.lookup("outer", tree.root()).is_some());
722        assert!(tree.lookup("inner", tree.root()).is_some());
723    }
724
725    #[test]
726    fn match_arm_creates_scope() {
727        // fn test() { match () { x => () } }
728        let arm = MatchArm {
729            id: 5,
730            span: span(),
731            pattern: Pattern::Bind {
732                id: 6,
733                span: span(),
734                name: ident("x"),
735            },
736            guard: None,
737            body: Expr::Literal {
738                id: 7,
739                span: span(),
740                lit: Literal::Unit,
741            },
742        };
743        let match_expr = Expr::Match {
744            id: 4,
745            span: span(),
746            scrutinee: Box::new(Expr::Literal {
747                id: 3,
748                span: span(),
749                lit: Literal::Unit,
750            }),
751            arms: vec![arm],
752        };
753        let body = Block {
754            id: 2,
755            span: span(),
756            stmts: vec![],
757            tail: Some(Box::new(match_expr)),
758        };
759        let module = make_module(vec![simple_fn(1, "test", body)]);
760        let tree = build_scope_tree(&module);
761        // root + fn scope + block + match arm scope = at least 4
762        assert!(tree.scope_count() >= 4);
763    }
764
765    #[test]
766    fn mut_binding_flagged() {
767        // fn test() { let mut y = (); }
768        let let_stmt = Stmt::Let(LetStmt {
769            id: 10,
770            span: span(),
771            pattern: Pattern::MutBind {
772                id: 11,
773                span: span(),
774                name: ident("y"),
775            },
776            ty: None,
777            value: Expr::Literal {
778                id: 12,
779                span: span(),
780                lit: Literal::Unit,
781            },
782        });
783        let body = Block {
784            id: 2,
785            span: span(),
786            stmts: vec![let_stmt],
787            tail: None,
788        };
789        let module = make_module(vec![simple_fn(1, "test", body)]);
790        let tree = build_scope_tree(&module);
791
792        // Walk all scopes looking for the "y" binding.
793        let found = (0..tree.next_id)
794            .filter_map(|i| tree.get(ScopeId(i)))
795            .flat_map(|s| s.bindings.iter())
796            .find(|b| b.name == "y");
797
798        assert!(found.is_some());
799        assert!(found.unwrap().mutable);
800    }
801
802    #[test]
803    fn lookup_walks_parent_chain() {
804        // Verify that lookup from a child scope finds a binding in the root scope.
805        let module = make_module(vec![simple_fn(1, "greet", empty_block(2))]);
806        let tree = build_scope_tree(&module);
807        // ScopeId(1) is the fn scope, child of root.
808        let found = tree.lookup("greet", ScopeId(1));
809        assert!(found.is_some());
810    }
811}