Skip to content

Commit 7d7d85c

Browse files
committed
More accurate tracking of super assignments
1 parent 985b851 commit 7d7d85c

4 files changed

Lines changed: 281 additions & 47 deletions

File tree

crates/oak_index/src/builder.rs

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,20 +151,84 @@ impl SemanticIndexBuilder {
151151
});
152152

153153
self.current_use_def.ensure_symbol(symbol_id);
154-
self.current_use_def.record_binding(symbol_id, def_id);
154+
self.current_use_def.record_definition(symbol_id, def_id);
155155
}
156156

157157
// Super-assignment is lexically in the current scope but binds in an
158-
// ancestor. We record the definition here (for go-to-definition, rename)
159-
// but skip use-def tracking since the binding doesn't affect local flow.
158+
// ancestor. We record the definition in the current scope and append
159+
// it to the target scope's use-def map (without shadowing prior
160+
// definitions).
161+
//
162+
// R's `<<-` walks up the environment chain from the parent, targeting
163+
// the first scope where the symbol is already bound. If no binding is
164+
// found, it assigns in the global (file) scope.
160165
fn add_super_definition(&mut self, name: &str, kind: DefinitionKind, range: TextRange) {
161166
let symbol_id =
162167
self.symbol_tables[self.current_scope].intern(name, SymbolFlags::IS_SUPER_BOUND);
163168
self.definitions[self.current_scope].push(Definition {
164169
symbol: symbol_id,
170+
kind: kind.clone(),
171+
range,
172+
});
173+
174+
let target_scope = self.resolve_super_target(name);
175+
176+
let target_symbol = self.symbol_tables[target_scope].intern(name, SymbolFlags::IS_BOUND);
177+
let target_def_id = self.definitions[target_scope].push(Definition {
178+
symbol: target_symbol,
165179
kind,
166180
range,
167181
});
182+
183+
let builder = self.use_def_builder_mut(target_scope);
184+
builder.ensure_symbol(target_symbol);
185+
builder.append_definition(target_symbol, target_def_id);
186+
}
187+
188+
// Walk up from the parent scope looking for a scope where `name` already
189+
// has `IS_BOUND`. Returns that scope, or the file scope if no binding is
190+
// found (mirroring R's assignment to the global environment).
191+
fn resolve_super_target(&self, name: &str) -> ScopeId {
192+
let file_scope = ScopeId::from(0);
193+
let mut scope = match self.scopes[self.current_scope].parent {
194+
Some(parent) => parent,
195+
None => return file_scope,
196+
};
197+
198+
loop {
199+
if let Some(id) = self.symbol_tables[scope].id(name) {
200+
if self.symbol_tables[scope]
201+
.symbol(id)
202+
.flags()
203+
.contains(SymbolFlags::IS_BOUND)
204+
{
205+
return scope;
206+
}
207+
}
208+
scope = match self.scopes[scope].parent {
209+
Some(parent) => parent,
210+
None => return file_scope,
211+
};
212+
}
213+
}
214+
215+
fn use_def_builder_mut(&mut self, target: ScopeId) -> &mut UseDefMapBuilder {
216+
if target == self.current_scope {
217+
return &mut self.current_use_def;
218+
}
219+
220+
let mut steps = 0;
221+
let mut scope = self.current_scope;
222+
while scope != target {
223+
scope = match self.scopes[scope].parent {
224+
Some(parent) => parent,
225+
None => panic!("Target scope {target:?} is not an ancestor of current scope"),
226+
};
227+
steps += 1;
228+
}
229+
230+
let index = self.use_def_stack.len() - steps;
231+
&mut self.use_def_stack[index]
168232
}
169233

170234
fn add_use(&mut self, name: &str, range: TextRange) {
@@ -513,7 +577,7 @@ impl SemanticIndexBuilder {
513577
});
514578

515579
self.current_use_def.ensure_symbol(symbol_id);
516-
self.current_use_def.record_loop_binding(symbol_id, def_id);
580+
self.current_use_def.append_definition(symbol_id, def_id);
517581
loop_header.push((symbol_id, def_id));
518582
}
519583

crates/oak_index/src/use_def_map.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ impl Bindings {
121121

122122
/// Replace all live definitions with a single new one, marking the
123123
/// symbol as definitely bound.
124-
fn record_binding(&mut self, def_id: DefinitionId) {
124+
fn record_definition(&mut self, def_id: DefinitionId) {
125125
self.definitions.clear();
126126
self.definitions.push(def_id);
127127
self.may_be_unbound = false;
@@ -229,15 +229,15 @@ impl UseDefMapBuilder {
229229

230230
/// Record a new binding for `symbol_id`. Replaces (shadows) all previous
231231
/// live definitions for that symbol.
232-
pub(crate) fn record_binding(&mut self, symbol_id: SymbolId, def_id: DefinitionId) {
233-
self.symbol_states[symbol_id].record_binding(def_id);
232+
pub(crate) fn record_definition(&mut self, symbol_id: SymbolId, def_id: DefinitionId) {
233+
self.symbol_states[symbol_id].record_definition(def_id);
234234
}
235235

236-
/// Record a loop-carried binding. Adds the definition to the symbol's
237-
/// live set without clearing existing definitions or changing
238-
/// `may_be_unbound`. This represents a value that might arrive from a
239-
/// previous loop iteration.
240-
pub(crate) fn record_loop_binding(&mut self, symbol_id: SymbolId, def_id: DefinitionId) {
236+
/// Add a definition to the symbol's live set without clearing existing
237+
/// definitions or changing `may_be_unbound`. Unlike `record_binding`
238+
/// which shadows all prior definitions, this appends to the live set.
239+
/// Used for loop-header placeholders and super-assignments.
240+
pub(crate) fn append_definition(&mut self, symbol_id: SymbolId, def_id: DefinitionId) {
241241
self.symbol_states[symbol_id].add_definition(def_id);
242242
}
243243

crates/oak_index/tests/builder.rs

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -549,20 +549,28 @@ fn test_repeat_loop() {
549549

550550
#[test]
551551
fn test_super_assignment_at_file_scope() {
552-
// At file scope, `<<-` records in file scope with IS_SUPER_BOUND
552+
// At file scope, `<<-` targets the file scope itself (no parent to
553+
// walk to), so the symbol gets both IS_SUPER_BOUND and IS_BOUND.
553554
let index = index("x <<- 1");
554555
let file = ScopeId::from(0);
555556

556557
let x = index.symbols(file).get("x").unwrap();
557-
assert_eq!(x.flags(), SymbolFlags::IS_SUPER_BOUND);
558+
assert_eq!(
559+
x.flags(),
560+
SymbolFlags::IS_SUPER_BOUND.union(SymbolFlags::IS_BOUND)
561+
);
558562

559-
assert_eq!(index.definitions(file).len(), 1);
560-
let DefinitionKind::SuperAssignment(node) =
561-
index.definitions(file)[DefinitionId::from(0)].kind()
562-
else {
563-
panic!("expected SuperAssignment");
564-
};
565-
assert_eq!(node.kind(), RSyntaxKind::R_BINARY_EXPRESSION);
563+
// Two definitions: one from the current-scope recording, one from the
564+
// target-scope recording (same scope in this case).
565+
assert_eq!(index.definitions(file).len(), 2);
566+
assert!(matches!(
567+
index.definitions(file)[DefinitionId::from(0)].kind(),
568+
DefinitionKind::SuperAssignment(_)
569+
));
570+
assert!(matches!(
571+
index.definitions(file)[DefinitionId::from(1)].kind(),
572+
DefinitionKind::SuperAssignment(_)
573+
));
566574
assert_eq!(index.uses(file).len(), 0);
567575
}
568576

@@ -572,9 +580,12 @@ fn test_super_assignment_right_at_file_scope() {
572580
let file = ScopeId::from(0);
573581

574582
let x = index.symbols(file).get("x").unwrap();
575-
assert_eq!(x.flags(), SymbolFlags::IS_SUPER_BOUND);
583+
assert_eq!(
584+
x.flags(),
585+
SymbolFlags::IS_SUPER_BOUND.union(SymbolFlags::IS_BOUND)
586+
);
576587

577-
assert_eq!(index.definitions(file).len(), 1);
588+
assert_eq!(index.definitions(file).len(), 2);
578589
assert!(matches!(
579590
index.definitions(file)[DefinitionId::from(0)].kind(),
580591
DefinitionKind::SuperAssignment(_)
@@ -585,16 +596,23 @@ fn test_super_assignment_right_at_file_scope() {
585596
#[test]
586597
fn test_super_assignment_recorded_in_current_scope() {
587598
// `<<-` records the definition in the function scope where it lexically
588-
// appears, not in an ancestor scope.
599+
// appears AND an extra definition in the parent scope.
589600
let index = index("f <- function() { x <<- 1 }");
590601
let file = ScopeId::from(0);
591602
let fun = ScopeId::from(1);
592603

593-
// File scope only has `f`
594-
assert!(index.symbols(file).get("x").is_none());
595-
assert_eq!(index.definitions(file).len(), 1);
604+
// File scope has `x` with IS_BOUND (extra definition from `<<-`)
605+
// and `f` with IS_BOUND. The `x <<-` definition is added during
606+
// function body processing, before `f <-`.
607+
let x_file = index.symbols(file).get("x").unwrap();
608+
assert_eq!(x_file.flags(), SymbolFlags::IS_BOUND);
609+
assert_eq!(index.definitions(file).len(), 2);
596610
assert!(matches!(
597611
index.definitions(file)[DefinitionId::from(0)].kind(),
612+
DefinitionKind::SuperAssignment(_)
613+
));
614+
assert!(matches!(
615+
index.definitions(file)[DefinitionId::from(1)].kind(),
598616
DefinitionKind::Assignment(_)
599617
));
600618

@@ -614,7 +632,8 @@ fn test_super_assignment_right_recorded_in_current_scope() {
614632
let file = ScopeId::from(0);
615633
let fun = ScopeId::from(1);
616634

617-
assert!(index.symbols(file).get("x").is_none());
635+
let x_file = index.symbols(file).get("x").unwrap();
636+
assert_eq!(x_file.flags(), SymbolFlags::IS_BOUND);
618637

619638
let x = index.symbols(fun).get("x").unwrap();
620639
assert_eq!(x.flags(), SymbolFlags::IS_SUPER_BOUND);
@@ -623,12 +642,13 @@ fn test_super_assignment_right_recorded_in_current_scope() {
623642
#[test]
624643
fn test_super_assignment_does_not_pollute_ancestor() {
625644
// `x <- 1` is in file scope, `x <<- 2` is in the function. The `<<-`
626-
// does NOT add a definition to the file scope.
645+
// adds an extra definition to the file scope in addition to the
646+
// existing `x <- 1` assignment.
627647
let index = index("x <- 1\nf <- function() { x <<- 2 }");
628648
let file = ScopeId::from(0);
629649
let fun = ScopeId::from(1);
630650

631-
// File scope: `x` has IS_BOUND from the `<-`, `f` has IS_BOUND
651+
// File scope: `x` has IS_BOUND (from both `<-` and `<<-`), `f` has IS_BOUND
632652
let x_file = index.symbols(file).get("x").unwrap();
633653
assert_eq!(x_file.flags(), SymbolFlags::IS_BOUND);
634654

@@ -637,11 +657,15 @@ fn test_super_assignment_does_not_pollute_ancestor() {
637657
.iter()
638658
.filter(|(_, d)| index.symbols(file).symbol(d.symbol()).name() == "x")
639659
.collect();
640-
assert_eq!(x_file_defs.len(), 1);
660+
assert_eq!(x_file_defs.len(), 2);
641661
assert!(matches!(
642662
x_file_defs[0].1.kind(),
643663
DefinitionKind::Assignment(_)
644664
));
665+
assert!(matches!(
666+
x_file_defs[1].1.kind(),
667+
DefinitionKind::SuperAssignment(_)
668+
));
645669

646670
// Function scope: `x` has IS_SUPER_BOUND from the `<<-`
647671
let x_fun = index.symbols(fun).get("x").unwrap();
@@ -656,7 +680,8 @@ fn test_super_assignment_does_not_pollute_ancestor() {
656680
#[test]
657681
fn test_super_assignment_nested_recorded_in_inner_scope() {
658682
// `x` is bound in both file and outer function. `<<-` in the inner
659-
// function records the definition in the inner scope, not in any ancestor.
683+
// function targets the outer function scope (immediate parent), adding
684+
// an extra definition there.
660685
let index = index("x <- 0\nf <- function() { x <- 1; g <- function() { x <<- 2 } }");
661686
let file = ScopeId::from(0);
662687
let outer = ScopeId::from(1);
@@ -666,7 +691,8 @@ fn test_super_assignment_nested_recorded_in_inner_scope() {
666691
let x_file = index.symbols(file).get("x").unwrap();
667692
assert_eq!(x_file.flags(), SymbolFlags::IS_BOUND);
668693

669-
// Outer function: `x` has IS_BOUND (from `<-`), no super-assignment here
694+
// Outer function: `x` has IS_BOUND (from both `x <- 1` and the `<<-`
695+
// extra definition from the inner function)
670696
let x_outer = index.symbols(outer).get("x").unwrap();
671697
assert_eq!(x_outer.flags(), SymbolFlags::IS_BOUND);
672698

@@ -675,11 +701,15 @@ fn test_super_assignment_nested_recorded_in_inner_scope() {
675701
.iter()
676702
.filter(|(_, d)| index.symbols(outer).symbol(d.symbol()).name() == "x")
677703
.collect();
678-
assert_eq!(x_outer_defs.len(), 1);
704+
assert_eq!(x_outer_defs.len(), 2);
679705
assert!(matches!(
680706
x_outer_defs[0].1.kind(),
681707
DefinitionKind::Assignment(_)
682708
));
709+
assert!(matches!(
710+
x_outer_defs[1].1.kind(),
711+
DefinitionKind::SuperAssignment(_)
712+
));
683713

684714
// Inner function: `x` has IS_SUPER_BOUND (from `<<-`)
685715
let x_inner = index.symbols(inner).get("x").unwrap();
@@ -693,8 +723,9 @@ fn test_super_assignment_nested_recorded_in_inner_scope() {
693723

694724
#[test]
695725
fn test_super_assignment_coexists_with_use_in_ancestors() {
696-
// Outer function uses `x` but doesn't bind it. `<<-` in the inner
697-
// function records in the inner scope. Ancestors are unaffected.
726+
// `<<-` in inner function walks up from outer, finds `x` bound in file
727+
// scope (from `x <- 1`), so it targets the file scope -- not the outer
728+
// function where `x` is only used.
698729
let index = index("x <- 1\nf <- function() { print(x); g <- function() { x <<- 2 } }");
699730
let file = ScopeId::from(0);
700731
let outer = ScopeId::from(1);
@@ -703,7 +734,8 @@ fn test_super_assignment_coexists_with_use_in_ancestors() {
703734
let x_file = index.symbols(file).get("x").unwrap();
704735
assert_eq!(x_file.flags(), SymbolFlags::IS_BOUND);
705736

706-
// Outer function has `x` as `IS_USED` only (from `print(x)`)
737+
// Outer function has `x` as IS_USED only (from `print(x)`). The `<<-`
738+
// skips it because `x` is not bound here.
707739
let x_outer = index.symbols(outer).get("x").unwrap();
708740
assert_eq!(x_outer.flags(), SymbolFlags::IS_USED);
709741

@@ -714,22 +746,24 @@ fn test_super_assignment_coexists_with_use_in_ancestors() {
714746

715747
#[test]
716748
fn test_super_assignment_not_visible_to_resolve_symbol() {
717-
// `<<-` does not create a binding visible to `resolve_symbol` (which
718-
// checks IS_BOUND, not IS_SUPER_BOUND). Cross-scope effects of `<<-`
719-
// are a runtime concern, not statically modelled.
749+
// `<<-` creates an extra definition in the parent scope with IS_BOUND,
750+
// which is visible to `resolve_symbol`.
720751
let index = index("f <- function() { x <<- 1 }");
721752
let file = ScopeId::from(0);
722753
let fun = ScopeId::from(1);
723754

724-
// File scope has no `x`
725-
assert!(index.symbols(file).get("x").is_none());
755+
// File scope has `x` with IS_BOUND (extra definition from `<<-`)
756+
let x_file = index.symbols(file).get("x").unwrap();
757+
assert_eq!(x_file.flags(), SymbolFlags::IS_BOUND);
726758

727759
// Function scope has `x` with IS_SUPER_BOUND
728760
let x = index.symbols(fun).get("x").unwrap();
729761
assert_eq!(x.flags(), SymbolFlags::IS_SUPER_BOUND);
730762

731-
// resolve_symbol does not find `x` because no scope has IS_BOUND for it
732-
assert!(index.resolve_symbol("x", fun).is_none());
763+
// `resolve_symbol` finds `x` in the file scope via the extra definition
764+
let resolved = index.resolve_symbol("x", fun);
765+
assert!(resolved.is_some());
766+
assert_eq!(resolved.unwrap().0, file);
733767
}
734768

735769
#[test]
@@ -1122,7 +1156,8 @@ fn test_string_super_assignment() {
11221156
let file = ScopeId::from(0);
11231157
let fun = ScopeId::from(1);
11241158

1125-
assert!(index.symbols(file).get("x").is_none());
1159+
let x_file = index.symbols(file).get("x").unwrap();
1160+
assert_eq!(x_file.flags(), SymbolFlags::IS_BOUND);
11261161

11271162
let x = index.symbols(fun).get("x").unwrap();
11281163
assert_eq!(x.flags(), SymbolFlags::IS_SUPER_BOUND);

0 commit comments

Comments
 (0)