Skip to content

Commit 24381e7

Browse files
committed
Implement enclosing snapshots
1 parent 8ba3b79 commit 24381e7

4 files changed

Lines changed: 837 additions & 46 deletions

File tree

crates/oak_index/src/builder.rs

Lines changed: 204 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,22 @@ use biome_rowan::AstSeparatedList;
1616
use biome_rowan::SyntaxNodeCast;
1717
use biome_rowan::TextRange;
1818
use biome_rowan::WalkEvent;
19+
use rustc_hash::FxHashMap;
20+
use smallvec::SmallVec;
1921

2022
use crate::index_vec::Idx;
2123
use crate::index_vec::IndexVec;
2224
use crate::semantic_index::Definition;
2325
use crate::semantic_index::DefinitionId;
2426
use crate::semantic_index::DefinitionKind;
27+
use crate::semantic_index::EnclosingSnapshotId;
28+
use crate::semantic_index::EnclosingSnapshotKey;
2529
use crate::semantic_index::Scope;
2630
use crate::semantic_index::ScopeId;
2731
use crate::semantic_index::ScopeKind;
2832
use crate::semantic_index::SemanticIndex;
2933
use crate::semantic_index::SymbolFlags;
34+
use crate::semantic_index::SymbolId;
3035
use crate::semantic_index::SymbolTableBuilder;
3136
use crate::semantic_index::Use;
3237
use crate::semantic_index::UseId;
@@ -37,6 +42,7 @@ use crate::use_def_map::UseDefMapBuilder;
3742
pub 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

5867
impl 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+
578775
fn is_assignment(bin: &RBinaryExpression) -> bool {
579776
let Ok(op) = bin.operator() else {
580777
return false;

0 commit comments

Comments
 (0)