@@ -16,17 +16,22 @@ use biome_rowan::AstSeparatedList;
1616use biome_rowan:: SyntaxNodeCast ;
1717use biome_rowan:: TextRange ;
1818use biome_rowan:: WalkEvent ;
19+ use rustc_hash:: FxHashMap ;
20+ use smallvec:: SmallVec ;
1921
2022use crate :: index_vec:: Idx ;
2123use crate :: index_vec:: IndexVec ;
2224use crate :: semantic_index:: Definition ;
2325use crate :: semantic_index:: DefinitionId ;
2426use crate :: semantic_index:: DefinitionKind ;
27+ use crate :: semantic_index:: EnclosingSnapshotId ;
28+ use crate :: semantic_index:: EnclosingSnapshotKey ;
2529use crate :: semantic_index:: Scope ;
2630use crate :: semantic_index:: ScopeId ;
2731use crate :: semantic_index:: ScopeKind ;
2832use crate :: semantic_index:: SemanticIndex ;
2933use crate :: semantic_index:: SymbolFlags ;
34+ use crate :: semantic_index:: SymbolId ;
3035use crate :: semantic_index:: SymbolTableBuilder ;
3136use crate :: semantic_index:: Use ;
3237use crate :: semantic_index:: UseId ;
@@ -37,6 +42,7 @@ use crate::use_def_map::UseDefMapBuilder;
3742pub fn build ( root : & RRoot ) -> SemanticIndex {
3843 let range = root. syntax ( ) . text_trimmed_range ( ) ;
3944 let mut builder = SemanticIndexBuilder :: new ( range) ;
45+ builder. pre_scan_scope ( root. syntax ( ) ) ;
4046 builder. collect_expression_list ( & root. expressions ( ) ) ;
4147 builder. finish ( )
4248}
@@ -53,6 +59,9 @@ struct SemanticIndexBuilder {
5359 current_use_def : UseDefMapBuilder ,
5460 use_def_stack : Vec < UseDefMapBuilder > ,
5561 current_scope : ScopeId ,
62+ current_pre_scan : PreScanScope ,
63+ pre_scan_stack : Vec < PreScanScope > ,
64+ enclosing_snapshots : FxHashMap < EnclosingSnapshotKey , ( ScopeId , EnclosingSnapshotId ) > ,
5665}
5766
5867impl SemanticIndexBuilder {
@@ -87,6 +96,9 @@ impl SemanticIndexBuilder {
8796 current_use_def : UseDefMapBuilder :: new ( ) ,
8897 use_def_stack : Vec :: new ( ) ,
8998 current_scope : file,
99+ current_pre_scan : PreScanScope :: new ( ) ,
100+ pre_scan_stack : Vec :: new ( ) ,
101+ enclosing_snapshots : FxHashMap :: default ( ) ,
90102 }
91103 }
92104
@@ -114,6 +126,9 @@ impl SemanticIndexBuilder {
114126 let parent_use_def = std:: mem:: replace ( & mut self . current_use_def , UseDefMapBuilder :: new ( ) ) ;
115127 self . use_def_stack . push ( parent_use_def) ;
116128
129+ let parent_pre_scan = std:: mem:: replace ( & mut self . current_pre_scan , PreScanScope :: new ( ) ) ;
130+ self . pre_scan_stack . push ( parent_pre_scan) ;
131+
117132 id
118133 }
119134
@@ -130,8 +145,14 @@ impl SemanticIndexBuilder {
130145 Some ( builder) => builder,
131146 None => panic ! ( "`pop_scope()` called with empty use-def stack" ) ,
132147 } ;
133- let finalized = std:: mem:: replace ( & mut self . current_use_def , parent_use_def) . finish ( ) ;
148+ let builder = std:: mem:: replace ( & mut self . current_use_def , parent_use_def) ;
149+ let finalized = builder. finish ( & self . uses [ id] ) ;
134150 self . use_def_maps [ id] = finalized;
151+
152+ self . current_pre_scan = match self . pre_scan_stack . pop ( ) {
153+ Some ( pre_scan) => pre_scan,
154+ None => panic ! ( "`pop_scope()` called with empty pre-scan stack" ) ,
155+ } ;
135156 }
136157
137158 fn add_definition (
@@ -238,6 +259,71 @@ impl SemanticIndexBuilder {
238259
239260 self . current_use_def . ensure_symbol ( symbol_id) ;
240261 self . current_use_def . record_use ( symbol_id, use_id) ;
262+
263+ // Associate free variables with the enclosing snapshot where the
264+ // variable is defined
265+ if self . current_use_def . is_may_be_unbound ( symbol_id) {
266+ self . register_enclosing_snapshot ( name, symbol_id) ;
267+ }
268+ }
269+
270+ fn register_enclosing_snapshot ( & mut self , name : & str , nested_symbol_id : SymbolId ) {
271+ // We're looking for a parent definition for this scope's free variable
272+ // so start from parent
273+ let Some ( mut current_scope) = self . scopes [ self . current_scope ] . parent else {
274+ return ;
275+ } ;
276+ let Some ( mut stack_idx) = self . pre_scan_stack . len ( ) . checked_sub ( 1 ) else {
277+ return ;
278+ } ;
279+
280+ loop {
281+ let found_by_flag = self . symbol_tables [ current_scope]
282+ . id ( name)
283+ . is_some_and ( |sym_id| {
284+ self . symbol_tables [ current_scope]
285+ . symbol ( sym_id)
286+ . flags ( )
287+ . contains ( SymbolFlags :: IS_BOUND )
288+ } ) ;
289+
290+ let found_by_prescan = self . pre_scan_stack [ stack_idx] . has_name ( name) ;
291+
292+ if found_by_flag || found_by_prescan {
293+ // Intern with empty flags: we just need a stable `SymbolId` for
294+ // the lookup key. If found via `found_by_flag`, the symbol
295+ // already exists with `IS_BOUND`. If found via pre-scan only,
296+ // the later `add_definition` call during the full walk will set
297+ // `IS_BOUND`.
298+ let enclosing_symbol_id =
299+ self . symbol_tables [ current_scope] . intern ( name, SymbolFlags :: empty ( ) ) ;
300+
301+ let key = EnclosingSnapshotKey {
302+ nested_scope : self . current_scope ,
303+ nested_symbol : nested_symbol_id,
304+ } ;
305+ if self . enclosing_snapshots . contains_key ( & key) {
306+ return ;
307+ }
308+
309+ self . use_def_stack [ stack_idx] . ensure_symbol ( enclosing_symbol_id) ;
310+ let snapshot_id =
311+ self . use_def_stack [ stack_idx] . register_enclosing_snapshot ( enclosing_symbol_id) ;
312+ self . enclosing_snapshots
313+ . insert ( key, ( current_scope, snapshot_id) ) ;
314+
315+ return ;
316+ }
317+
318+ let Some ( parent) = self . scopes [ current_scope] . parent else {
319+ return ;
320+ } ;
321+ let Some ( next_idx) = stack_idx. checked_sub ( 1 ) else {
322+ return ;
323+ } ;
324+ current_scope = parent;
325+ stack_idx = next_idx;
326+ }
241327 }
242328
243329 // --- Recursive descent ---
@@ -352,7 +438,11 @@ impl SemanticIndexBuilder {
352438 if let Ok ( body) = stmt. body ( ) {
353439 let first_use = self . uses [ self . current_scope ] . next_id ( ) ;
354440 self . collect_expression ( & body) ;
355- self . current_use_def . finish_loop_defs ( & pre_loop, first_use) ;
441+ self . current_use_def . finish_loop_defs (
442+ & pre_loop,
443+ first_use,
444+ & self . uses [ self . current_scope ] ,
445+ ) ;
356446 }
357447
358448 self . current_use_def . merge ( pre_loop) ;
@@ -396,20 +486,28 @@ impl SemanticIndexBuilder {
396486 if let Ok ( body) = stmt. body ( ) {
397487 let first_use = self . uses [ self . current_scope ] . next_id ( ) ;
398488 self . collect_expression ( & body) ;
399- self . current_use_def . finish_loop_defs ( & pre_loop, first_use) ;
489+ self . current_use_def . finish_loop_defs (
490+ & pre_loop,
491+ first_use,
492+ & self . uses [ self . current_scope ] ,
493+ ) ;
400494 }
401495
402496 // Body may not execute
403497 self . current_use_def . merge ( pre_loop) ;
404498 } ,
405499
406500 AnyRExpression :: RRepeatStatement ( stmt) => {
407- // Body always executes at least once, no snapshot needed
501+ // Body always executes at least once, so no merge with pre-loop state.
408502 if let Ok ( body) = stmt. body ( ) {
409503 let pre_loop = self . current_use_def . snapshot ( ) ;
410504 let first_use = self . uses [ self . current_scope ] . next_id ( ) ;
411505 self . collect_expression ( & body) ;
412- self . current_use_def . finish_loop_defs ( & pre_loop, first_use) ;
506+ self . current_use_def . finish_loop_defs (
507+ & pre_loop,
508+ first_use,
509+ & self . uses [ self . current_scope ] ,
510+ ) ;
413511 }
414512 } ,
415513
@@ -459,12 +557,60 @@ impl SemanticIndexBuilder {
459557 self . collect_parameters ( & params) ;
460558 }
461559 if let Ok ( body) = fun. body ( ) {
560+ self . pre_scan_scope ( body. syntax ( ) ) ;
462561 self . collect_expression ( & body) ;
463562 }
464563
465564 self . pop_scope ( scope) ;
466565 }
467566
567+ /// Pre-scan a scope to collect all definition names (skipping nested
568+ /// function bodies). Runs before the full walk so that enclosing
569+ /// snapshot registration can find where free variables are bound,
570+ /// even when the walk in the parent scope hasn't reached the
571+ /// definition yet. Must stay in sync with the full walk's definition
572+ /// handling: any construct that calls `add_definition` should have a
573+ /// corresponding entry here.
574+ fn pre_scan_scope ( & mut self , node : & RSyntaxNode ) {
575+ let mut preorder = node. preorder ( ) ;
576+ while let Some ( event) = preorder. next ( ) {
577+ let WalkEvent :: Enter ( node) = event else {
578+ continue ;
579+ } ;
580+ let Some ( expr) = AnyRExpression :: cast ( node) else {
581+ continue ;
582+ } ;
583+ match & expr {
584+ // NSE scopes (e.g. `local({...})`) will also need to
585+ // be skipped here once recognized, since their
586+ // definitions belong to a child scope.
587+ AnyRExpression :: RFunctionDefinition ( _) => {
588+ preorder. skip_subtree ( ) ;
589+ } ,
590+ AnyRExpression :: RBinaryExpression ( bin) if is_assignment ( bin) => {
591+ if !is_super_assignment ( bin) {
592+ let right = is_right_assignment ( bin) ;
593+ let target = if right { bin. right ( ) } else { bin. left ( ) } ;
594+ if let Ok ( target) = target {
595+ if let Some ( ( name, range) ) = assignment_target_name ( & target) {
596+ self . current_pre_scan . add ( name, range) ;
597+ }
598+ }
599+ }
600+ } ,
601+ AnyRExpression :: RForStatement ( stmt) => {
602+ if let Ok ( variable) = stmt. variable ( ) {
603+ self . current_pre_scan . add (
604+ identifier_text ( & variable) ,
605+ variable. syntax ( ) . text_trimmed_range ( ) ,
606+ ) ;
607+ }
608+ } ,
609+ _ => { } ,
610+ }
611+ }
612+ }
613+
468614 fn collect_parameters ( & mut self , params : & RParameters ) {
469615 for param in params. items ( ) . iter ( ) {
470616 let Ok ( param) = param else { continue } ;
@@ -561,8 +707,10 @@ impl SemanticIndexBuilder {
561707 fn finish ( mut self ) -> SemanticIndex {
562708 self . scopes [ ScopeId :: from ( 0 ) ] . descendants . end = self . scopes . next_id ( ) ;
563709
564- let file_use_def_map = self . current_use_def . finish ( ) ;
565- self . use_def_maps [ ScopeId :: from ( 0 ) ] = file_use_def_map;
710+ let file_scope = ScopeId :: from ( 0 ) ;
711+ let file_builder = std:: mem:: replace ( & mut self . current_use_def , UseDefMapBuilder :: new ( ) ) ;
712+ let file_use_def_map = file_builder. finish ( & self . uses [ file_scope] ) ;
713+ self . use_def_maps [ file_scope] = file_use_def_map;
566714
567715 let symbol_tables = self . symbol_tables . into_iter ( ) . map ( |b| b. build ( ) ) . collect ( ) ;
568716 SemanticIndex :: new (
@@ -571,10 +719,59 @@ impl SemanticIndexBuilder {
571719 self . definitions ,
572720 self . uses ,
573721 self . use_def_maps ,
722+ self . enclosing_snapshots ,
574723 )
575724 }
576725}
577726
727+ /// All definitions in a scope, collected before the full walk. Skips nested
728+ /// function bodies (those belong to child scopes). Two consumers:
729+ ///
730+ /// - Enclosing snapshots: `has_name()` checks whether a symbol will be
731+ /// defined in an ancestor scope (even when the ancestor's walk hasn't reached
732+ /// that definition yet), so that `register_enclosing_snapshot()` can find the
733+ /// right ancestor for free variables.
734+ /// - NSE resolution: With NSE, each function call potentially pushes a scope
735+ /// (which can be lazy or eager). We need to resolve the called function's
736+ /// semantic during the walk. Inside lazy scopes (e.g. function bodies),
737+ /// `by_name` provides the complete set of parent definitions so that the
738+ /// function can be resolved against all the parent scope's definitions (if NSE
739+ /// semantics don't match across definitions, we pick one and lint). Intra-scope
740+ /// resolution is linear and uses the current `symbol_states` directly instead.
741+ struct PreScanScope {
742+ _defs : Vec < PreScanDef > ,
743+ by_name : FxHashMap < String , SmallVec < [ usize ; 2 ] > > ,
744+ }
745+
746+ /// A single definition site found during the pre-scan. Fields are not
747+ /// read yet but will be used for NSE lookup.
748+ struct PreScanDef {
749+ _name : String ,
750+ _range : TextRange ,
751+ }
752+
753+ impl PreScanScope {
754+ fn new ( ) -> Self {
755+ Self {
756+ _defs : Vec :: new ( ) ,
757+ by_name : FxHashMap :: default ( ) ,
758+ }
759+ }
760+
761+ fn add ( & mut self , name : String , range : TextRange ) {
762+ let idx = self . _defs . len ( ) ;
763+ self . by_name . entry ( name. clone ( ) ) . or_default ( ) . push ( idx) ;
764+ self . _defs . push ( PreScanDef {
765+ _name : name,
766+ _range : range,
767+ } ) ;
768+ }
769+
770+ fn has_name ( & self , name : & str ) -> bool {
771+ self . by_name . contains_key ( name)
772+ }
773+ }
774+
578775fn is_assignment ( bin : & RBinaryExpression ) -> bool {
579776 let Ok ( op) = bin. operator ( ) else {
580777 return false ;
0 commit comments