Skip to content

Commit 08d7ec6

Browse files
authored
Merge pull request #3127 from veryl-lang/fix/drop-dangling-symbol-refs
fix(analyzer): drop scope tree bindings together with their symbols
2 parents c5663de + af613ca commit 08d7ec6

7 files changed

Lines changed: 231 additions & 59 deletions

File tree

crates/analyzer/src/analyzer.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::analyzer_error::{AnalyzerError, ExceedLimitKind};
22
use crate::attribute_table;
33
use crate::comb_loop_detect;
44
use crate::conv::{Context, Conv};
5+
use crate::definition_table;
56
use crate::generic_inference_table;
67
use crate::handlers::*;
78
use crate::ir::{Ir, IrResult};
@@ -13,10 +14,12 @@ use crate::scope;
1314
use crate::symbol::{DocComment, ProjectPropertyValueProperty, Symbol, SymbolKind};
1415
use crate::symbol_table;
1516
use crate::type_dag;
17+
use crate::unsafe_table;
1618
use std::collections::BTreeMap;
1719
use veryl_metadata::{Build, Lint, Metadata, ProjectProperty};
1820
use veryl_parser::doc_comment_table;
19-
use veryl_parser::resource_table::{self, StrId};
21+
use veryl_parser::resource_table::{self, PathId, StrId};
22+
use veryl_parser::text_table;
2023
use veryl_parser::veryl_grammar_trait::*;
2124
use veryl_parser::veryl_token::Token;
2225
use veryl_parser::veryl_walker::{Handler, VerylWalker};
@@ -251,6 +254,28 @@ impl Analyzer {
251254
ret
252255
}
253256

257+
/// Removes what one file registered in the global tables, so it can be
258+
/// re-analyzed or a partial fragment restore rolled back.
259+
///
260+
/// `prj` scopes the drop to one project: the same source can be registered
261+
/// under several at once — two dependencies aliasing one path — and each
262+
/// registration must only remove what it added. `None` removes all of them,
263+
/// which is what a deleted file needs.
264+
///
265+
/// `text_table` / `attribute_table` / `unsafe_table` are keyed by position
266+
/// rather than by project, so they are dropped whole and re-registered
267+
/// identically by the next parse.
268+
pub fn drop_file(path: PathId, prj: Option<StrId>) {
269+
// Must precede `drop_tokens`: it matches reference tokens against
270+
// their token scope.
271+
symbol_table::drop(path, prj);
272+
scope::drop_tokens(path, prj);
273+
text_table::drop(path);
274+
attribute_table::drop(path);
275+
unsafe_table::drop(path);
276+
definition_table::drop(path, prj);
277+
}
278+
254279
pub fn clear(&self) {
255280
attribute_table::clear();
256281
crate::component_manifest_table::clear();

crates/analyzer/src/scope.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::HashMap;
2+
use crate::HashSet;
23
use crate::SVec;
34
use crate::namespace::{DefineContext, Namespace};
45
use crate::symbol::{SymbolId, SymbolKind};
@@ -52,6 +53,14 @@ pub fn scope_kind_of(kind: &SymbolKind) -> Option<ScopeKind> {
5253
}
5354
}
5455

56+
/// A symbol whose scope-tree bindings must go away with it.
57+
pub struct DroppedSymbol {
58+
/// Scope the symbol was inserted into (`Symbol::scope`).
59+
pub scope: ScopeId,
60+
pub name: StrId,
61+
pub id: SymbolId,
62+
}
63+
5564
/// An explicit `import pkg::name` binding local to a scope.
5665
#[derive(Debug, Clone)]
5766
pub struct ImportBinding {
@@ -254,6 +263,47 @@ impl ScopeArena {
254263
.push(symbol);
255264
}
256265

266+
/// Removes the tree's own copies of dropped `SymbolId`s. Without this a
267+
/// lookup dereferences an id the symbol table no longer holds.
268+
fn drop_symbols(&mut self, symbols: &[DroppedSymbol]) {
269+
if symbols.is_empty() {
270+
return;
271+
}
272+
273+
for symbol in symbols {
274+
let name = resource_table::canonical_str_id(symbol.name);
275+
276+
if let Some(scope) = self.scopes.get_mut(symbol.scope.0 as usize)
277+
&& let Some(ids) = scope.locals.get_mut(&name)
278+
{
279+
ids.retain(|x| *x != symbol.id);
280+
if ids.is_empty() {
281+
scope.locals.remove(&name);
282+
}
283+
}
284+
285+
// The inner scope a symbol opens is the child named after it. The
286+
// owner check matters for ifdef-exclusive declarations, which share
287+
// that child.
288+
if let Some(&owned) = self.intern.get(&(symbol.scope.0, name))
289+
&& let Some(scope) = self.scopes.get_mut(owned as usize)
290+
&& scope.owner == Some(symbol.id)
291+
{
292+
scope.owner = None;
293+
}
294+
}
295+
296+
// An import binds the id in the importing file's scope, so there is no
297+
// reverse route and this has to scan. `apply_import` re-adds them.
298+
let dropped: HashSet<SymbolId> = symbols.iter().map(|x| x.id).collect();
299+
for scope in &mut self.scopes {
300+
scope.imports.retain(|_, bindings| {
301+
bindings.retain(|x| !dropped.contains(&x.symbol));
302+
!bindings.is_empty()
303+
});
304+
}
305+
}
306+
257307
fn add_import(
258308
&mut self,
259309
scope: ScopeId,
@@ -587,6 +637,10 @@ pub fn add_local(scope: ScopeId, name: StrId, symbol: SymbolId) {
587637
SCOPE_ARENA.with(|f| f.borrow_mut().add_local(scope, name, symbol))
588638
}
589639

640+
pub fn drop_symbols(symbols: &[DroppedSymbol]) {
641+
SCOPE_ARENA.with(|f| f.borrow_mut().drop_symbols(symbols))
642+
}
643+
590644
pub fn set_kind_owner(scope: ScopeId, kind: ScopeKind, owner: SymbolId) {
591645
SCOPE_ARENA.with(|f| f.borrow_mut().set_kind_owner(scope, kind, owner))
592646
}
@@ -841,6 +895,25 @@ pub fn wildcards_get(scope: ScopeId) -> SVec<WildcardImport> {
841895
})
842896
}
843897

898+
/// Every symbol the tree binds, for asserting a drop left no dangling id.
899+
#[cfg(test)]
900+
pub(crate) fn bound_symbols() -> Vec<SymbolId> {
901+
SCOPE_ARENA.with(|f| {
902+
f.borrow()
903+
.scopes
904+
.iter()
905+
.flat_map(|s| {
906+
s.locals
907+
.values()
908+
.flatten()
909+
.chain(s.imports.values().flatten().map(|x| &x.symbol))
910+
.chain(s.owner.iter())
911+
.copied()
912+
})
913+
.collect()
914+
})
915+
}
916+
844917
pub fn mixin_get(scope: ScopeId) -> SVec<Mixin> {
845918
SCOPE_ARENA.with(|f| {
846919
f.borrow()
@@ -895,6 +968,46 @@ mod tests {
895968
assert_eq!(owner_of(a), Some(SymbolId(42)));
896969
}
897970

971+
#[test]
972+
fn drop_symbols_removes_locals_and_owner() {
973+
clear();
974+
let prj = intern_child(ScopeId(0), name("prj"), ScopeKind::Project);
975+
let owned = intern_child(prj, name("Pkg"), ScopeKind::Package);
976+
set_kind_owner(owned, ScopeKind::Package, SymbolId(1));
977+
add_local(prj, name("Pkg"), SymbolId(1));
978+
add_local(prj, name("Mod"), SymbolId(2));
979+
980+
drop_symbols(&[DroppedSymbol {
981+
scope: prj,
982+
name: name("Pkg"),
983+
id: SymbolId(1),
984+
}]);
985+
986+
assert!(locals_get(prj, name("Pkg")).is_empty());
987+
assert_eq!(owner_of(owned), None);
988+
assert_eq!(locals_get(prj, name("Mod")).as_slice(), [SymbolId(2)]);
989+
}
990+
991+
#[test]
992+
fn drop_symbols_keeps_an_owner_claimed_by_another_symbol() {
993+
clear();
994+
let prj = intern_child(ScopeId(0), name("prj"), ScopeKind::Project);
995+
let owned = intern_child(prj, name("Pkg"), ScopeKind::Package);
996+
// Two ifdef-exclusive declarations share the scope they open.
997+
set_kind_owner(owned, ScopeKind::Package, SymbolId(2));
998+
add_local(prj, name("Pkg"), SymbolId(1));
999+
add_local(prj, name("Pkg"), SymbolId(2));
1000+
1001+
drop_symbols(&[DroppedSymbol {
1002+
scope: prj,
1003+
name: name("Pkg"),
1004+
id: SymbolId(1),
1005+
}]);
1006+
1007+
assert_eq!(locals_get(prj, name("Pkg")).as_slice(), [SymbolId(2)]);
1008+
assert_eq!(owner_of(owned), Some(SymbolId(2)));
1009+
}
1010+
8981011
#[test]
8991012
fn intern_namespace_builds_and_dedups_the_chain() {
9001013
clear();

crates/analyzer/src/symbol_table.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -889,7 +889,9 @@ impl SymbolTable {
889889
let mut max_depth = 0;
890890
let mut found = None;
891891
for id in candidates {
892-
let symbol = self.symbol_table.get(&id).unwrap();
892+
let Some(symbol) = self.symbol_table.get(&id) else {
893+
continue;
894+
};
893895
let matched = self.match_nested_generic_instance(context, symbol)
894896
|| (context.scope == symbol.scope
895897
&& !context
@@ -910,7 +912,9 @@ impl SymbolTable {
910912
continue;
911913
}
912914
for id in scope::locals_get(mixin.source, name) {
913-
let symbol = self.symbol_table.get(&id).unwrap();
915+
let Some(symbol) = self.symbol_table.get(&id) else {
916+
continue;
917+
};
914918
if !symbol
915919
.namespace
916920
.define_context
@@ -1682,6 +1686,20 @@ impl SymbolTable {
16821686
.map(|x| *x.0)
16831687
.collect();
16841688

1689+
// Read while the symbols are still in the table.
1690+
let dropped: Vec<_> = drop_list
1691+
.iter()
1692+
.filter_map(|id| {
1693+
self.symbol_table
1694+
.get(id)
1695+
.map(|symbol| scope::DroppedSymbol {
1696+
scope: symbol.scope,
1697+
name: symbol.token.text,
1698+
id: *id,
1699+
})
1700+
})
1701+
.collect();
1702+
16851703
for id in &drop_list {
16861704
if let Some(symbol) = self.symbol_table.get(id)
16871705
&& let Some(ids) = self.namespace_index.get_mut(&symbol.namespace.paths)
@@ -1699,6 +1717,8 @@ impl SymbolTable {
16991717
for tokens in self.reference_table.values_mut() {
17001718
tokens.retain(|x| !is_drop_token(x, file_path, prj));
17011719
}
1720+
1721+
scope::drop_symbols(&dropped);
17021722
}
17031723

17041724
pub fn add_reference(&mut self, target: SymbolId, token: &Token) {

crates/analyzer/src/tests.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18388,3 +18388,59 @@ fn partially_driven_output_port() {
1838818388
"{errors:?}"
1838918389
);
1839018390
}
18391+
18392+
/// The language server drops a file's state and re-analyzes it on every edit.
18393+
/// The scope tree mirrors each insertion, so the drop must take its bindings.
18394+
#[test]
18395+
fn reanalyze_after_drop() {
18396+
const CODE: &str = r#"package P {
18397+
const A: u32 = 1;
18398+
}
18399+
package Q::<W: u32> {
18400+
const B: u32 = W;
18401+
}
18402+
module M {
18403+
import P::A;
18404+
const C: u32 = A;
18405+
const D: u32 = P::A;
18406+
const E: u32 = Q::<2>::B;
18407+
}
18408+
"#;
18409+
18410+
symbol_table::clear();
18411+
attribute_table::clear();
18412+
doc_comment_table::clear();
18413+
18414+
let metadata = Metadata::create_default("prj").unwrap();
18415+
let path = std::path::PathBuf::from("reanalyze_after_drop.veryl");
18416+
18417+
let analyze = || {
18418+
let parser = Parser::parse(CODE, &path).unwrap();
18419+
let analyzer = Analyzer::new(&metadata);
18420+
let mut context = Context::default();
18421+
let mut ir = Ir::default();
18422+
let mut errors = analyzer.analyze_pass1("prj", &parser.veryl);
18423+
errors.append(&mut Analyzer::analyze_post_pass1());
18424+
errors.append(&mut analyzer.analyze_pass2(&parser.veryl, &mut context, Some(&mut ir)));
18425+
errors.append(&mut Analyzer::analyze_post_pass2(&ir));
18426+
errors
18427+
};
18428+
18429+
let errors = analyze();
18430+
assert!(errors.is_empty(), "{errors:?}");
18431+
18432+
Analyzer::drop_file(
18433+
veryl_parser::resource_table::insert_path(&path),
18434+
Some("prj".into()),
18435+
);
18436+
18437+
for id in crate::scope::bound_symbols() {
18438+
assert!(
18439+
symbol_table::get(id).is_some(),
18440+
"scope tree still binds dropped symbol {id:?}"
18441+
);
18442+
}
18443+
18444+
let errors = analyze();
18445+
assert!(errors.is_empty(), "{errors:?}");
18446+
}

crates/languageserver/src/incremental.rs

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,13 @@
99
1010
use std::collections::HashMap;
1111
use std::fs;
12-
use std::path::{Path, PathBuf};
12+
use std::path::PathBuf;
1313
use veryl_analyzer::fragment_cache::{self, Fragment, FragmentWatermark};
14-
use veryl_analyzer::{
15-
attribute_table, definition_table, scope, symbol_table, type_dag, unsafe_table,
16-
};
14+
use veryl_analyzer::{Analyzer, scope, type_dag};
1715
use veryl_cache::Store;
1816
use veryl_metadata::Metadata;
17+
use veryl_parser::resource_table;
1918
use veryl_parser::resource_table::StrId;
20-
use veryl_parser::{resource_table, text_table};
2119
use veryl_path::PathSet;
2220

2321
pub struct LsIncremental {
@@ -65,7 +63,7 @@ impl LsIncremental {
6563
// Clear any stale state from a previous analysis of this file
6664
// before re-registering it.
6765
let prj: StrId = path.prj.as_str().into();
68-
drop_file_state(prj, &path.src);
66+
Analyzer::drop_file(resource_table::insert_path(&path.src), Some(prj));
6967

7068
let is_root = path.prj == self.root_project;
7169
scope::set_project(prj, is_root);
@@ -77,7 +75,7 @@ impl LsIncremental {
7775
true
7876
}
7977
Err(_) => {
80-
drop_file_state(prj, &path.src);
78+
Analyzer::drop_file(resource_table::insert_path(&path.src), Some(prj));
8179
false
8280
}
8381
}
@@ -142,19 +140,6 @@ fn global_key(metadata: &Metadata) -> Option<String> {
142140
]))
143141
}
144142

145-
/// Removes everything a file may have registered in the global tables
146-
/// (same set as the server's `drop_tables`).
147-
fn drop_file_state(prj: StrId, src: &Path) {
148-
let path = resource_table::insert_path(src);
149-
let prj = Some(prj);
150-
symbol_table::drop(path, prj);
151-
scope::drop_tokens(path, prj);
152-
text_table::drop(path);
153-
attribute_table::drop(path);
154-
unsafe_table::drop(path);
155-
definition_table::drop(path, prj);
156-
}
157-
158143
/// Per-project stores, so a server handling files from several projects
159144
/// keeps one store each. Keyed by metadata path.
160145
#[derive(Default)]
@@ -175,7 +160,7 @@ impl LsIncrementalMap {
175160
mod tests {
176161
use super::*;
177162
use std::thread;
178-
use veryl_analyzer::Analyzer;
163+
use veryl_analyzer::symbol_table;
179164
use veryl_parser::Parser;
180165

181166
const FILE_A: &str = "package P { const W: u32 = 8; }\n";

0 commit comments

Comments
 (0)