Design an optimal data structure and algorithm for contextual code completion with hierarchical lexical scopes.
x might be visible in scopes {0, 2, 5}{{{x}}{}} form scope treex knows visible scopes {0, 2, 3}// Global scope (id=0)
let global_var = 1;
{ // Scope 1
let outer = 2;
{ // Scope 2
let inner = 3;
// Code completion at this point:
// Query: prefix="in", visible_scopes={0, 1, 2}
//
// Candidates:
// - "inner" (scopes={2}) → MATCH (2 ∈ {0,1,2})
// - "input" (scopes={0}) → MATCH (0 ∈ {0,1,2})
// - "internal" (scopes={3}) → NO MATCH (3 ∉ {0,1,2})
}
}
Data Structure:
PathMapDictionary<HashSet<u32>>
// term → set of scope IDs where term is visible
Query Algorithm:
let visible_scopes: HashSet<u32> = get_visible_scopes(); // {0, 1, 2}
transducer.query_filtered(prefix, max_distance, |term_scopes| {
!term_scopes.is_disjoint(&visible_scopes) // Check intersection
})
Complexity:
Data Structure:
PathMapDictionary<u64> // Bit mask (up to 64 scopes)
// term → bitmask of scopes (bit i set if term in scope i)
Query Algorithm:
let visible_mask: u64 = scope_set_to_mask(&visible_scopes);
transducer.query_filtered(prefix, max_distance, |term_mask| {
(term_mask & visible_mask) != 0 // Bitwise intersection
})
Complexity:
Data Structure:
PathMapDictionary<Vec<u32>> // Sorted vector of scope IDs
Query Algorithm:
let results: Vec<_> = transducer
.query(prefix, max_distance)
.filter(|term| {
let term_scopes = dict.get_value(term).unwrap();
has_intersection(&term_scopes, &visible_scopes)
})
.collect();
Complexity:
Data Structure:
struct ScopeIndex {
automaton: PathMapDictionary<()>, // All terms
scope_to_terms: HashMap<u32, HashSet<String>>, // scope → terms
}
Query Algorithm:
// 1. Get all fuzzy matches
let all_matches: HashSet<_> = transducer.query(prefix, max_distance).collect();
// 2. Get terms visible in any query scope
let mut visible_terms = HashSet::new();
for scope_id in &visible_scopes {
if let Some(terms) = scope_index.get(scope_id) {
visible_terms.extend(terms);
}
}
// 3. Intersection
let results: Vec<_> = all_matches.intersection(&visible_terms).collect();
Complexity:
Data Structure:
enum ScopeData {
Mask(u64), // For scopes 0-63
Set(HashSet<u32>), // For overflow scopes
}
PathMapDictionary<ScopeData>
Query Algorithm:
transducer.query_filtered(prefix, max_distance, |scope_data| {
match scope_data {
ScopeData::Mask(mask) => (mask & visible_mask) != 0,
ScopeData::Set(set) => !set.is_disjoint(&visible_scopes),
}
})
Complexity:
Data Structure:
struct BloomScopeData {
bloom: BloomFilter, // Quick negative check
scopes: HashSet<u32>, // Precise check
}
Query Algorithm:
transducer.query_filtered(prefix, max_distance, |data| {
// Fast negative check
if !bloom_possibly_intersects(&data.bloom, &visible_scopes) {
return false;
}
// Precise check
!data.scopes.is_disjoint(&visible_scopes)
})
Complexity:
The core operation is checking if two sets intersect. Several optimizations:
fn has_intersection(a: &HashSet<u32>, b: &HashSet<u32>) -> bool {
!a.is_disjoint(b) // Iterates entire smaller set
}
Complexity: O(min(|a|, |b|))
fn has_intersection_sorted(a: &[u32], b: &[u32]) -> bool {
let mut i = 0;
let mut j = 0;
while i < a.len() && j < b.len() {
if a[i] == b[j] { return true; }
if a[i] < b[j] { i += 1; } else { j += 1; }
}
false
}
Complexity: O(|a| + |b|) worst case, O(1) if intersection found early
fn has_intersection_bitset(a: u64, b: u64) -> bool {
(a & b) != 0
}
Complexity: O(1)
fn has_intersection_hybrid(small: &[u32], large: &[u32]) -> bool {
// Use smaller set to search in larger
for &item in small {
if large.binary_search(&item).is_ok() {
return true;
}
}
false
}
Complexity: O(min * log(max))
Scenario 1: Shallow Hierarchy (2-3 levels)
Scenario 2: Deep Hierarchy (10+ levels)
Scenario 3: Wide Hierarchy (many siblings)
Scenario 4: Dense Graph (many scopes per term)
// For each approach:
fn bench_scope_completion_{approach}(c: &mut Criterion) {
// Setup: Create dictionary with scope data
let dict = create_scoped_dictionary(scenario);
let transducer = Transducer::new(dict, Algorithm::Standard);
// Queries: Various completion contexts
let queries = vec![
("pr", 2, vec![0, 1, 2]), // Shallow
("get", 1, vec![0, 5, 10]), // Medium
("x", 2, vec![0, 1, 2, 3, 4, 5]), // Wide
];
c.bench_function(&format!("{}_scenario_{}", approach, scenario), |b| {
b.iter(|| {
for (prefix, dist, scopes) in &queries {
let results: Vec<_> = query_with_scopes(
&transducer,
prefix,
*dist,
scopes
).collect();
black_box(results);
}
})
});
}
Based on theory:
| Approach | Memory | Query Time | Best For |
|---|---|---|---|
| 1. Scope Sets | Medium | Medium | General purpose |
| 2. Bit Masks | Low | Fastest | ≤64 scopes |
| 3. Post-filter | Medium | Slow | Few matches |
| 4. Inverted Index | High | Fast | Many scopes |
| 5. Hybrid | Low | Fastest | Most cases |
| 6. Bloom Filter | High | Fast | Large scope sets |
Predicted Winner: Approach 5 (Hybrid) or Approach 2 (Bit Masks for ≤64 scopes)
What's the typical number of scopes in real codebases?
What's the typical scope depth?
What's the distribution of scopes per term?
Is early termination significant?
Does cache locality matter for small sets?
Are there patterns we can exploit?
Based on comprehensive benchmarking:
Use Sorted Vector (Vec<u32>) for general-purpose scope filtering
Use Bitmask (u64) when scope count is guaranteed ≤64
Avoid Hybrid Approaches
Implementation Complete
src/transducer/helpers.rsbenches/hierarchical_scope_benchmarks.rsexamples/hierarchical_scope_completion.rsCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |