Hierarchical tree structure for managing lexical scopes and visibility relationships in contextual code completion.
The ContextTree manages hierarchical relationships between contexts (lexical scopes) in a parent-child tree structure. It implements the fundamental rule of lexical scoping: child contexts can see definitions from parent contexts, but not vice versa.
Lexical Scope Hierarchy Example:
Global (0)
│
├─── Module A (1)
│ │
│ ├─── Function foo (10)
│ │ └─── Block (100)
│ │
│ └─── Function bar (11)
│
└─── Module B (2)
└─── Class Baz (20)
└─── Method qux (200)
Visibility Rules:
- Context 100 (block) can see: [100, 10, 1, 0] (self → function → module → global)
- Context 11 (bar) can see: [11, 1, 0] (self → module → global)
- Context 200 (qux) can see: [200, 20, 2, 0] (self → class → module B → global)
| Use Case | Context Mapping | Visibility Pattern |
|---|---|---|
| LSP Server | File → Class → Method → Block | Nested scope inheritance |
| REPL | Session → Expression history | Sequential scope chain |
| Code Editor | Global → Module → Function → Block | Standard lexical scoping |
| Interactive Notebook | Notebook → Cell → Code block | Cell isolation + global visibility |
pub struct ContextTree {
/// Map from context ID to parent ID (None for root contexts)
nodes: HashMap<ContextId, Option<ContextId>>,
}
pub type ContextId = u32;
Design Rationale:
\mathcal{O}(1)$ parent lookup, efficient for sparse IDsNone indicates root context| Component | Size (bytes) | Notes |
|---|---|---|
| HashMap entry | ~24 | Key (u32) + value (Option) + hash/metadata |
| Per-context overhead | ~24 | Single HashMap entry |
| Tree with 100 contexts | ~2.4 KB | 100 × 24 bytes |
| Tree with 1000 contexts | ~24 KB | 1000 × 24 bytes |
Conclusion: Extremely lightweight. Even large applications (1000 contexts) use <25KB.
// Create root context (no parent)
pub fn create_root(&mut self, id: ContextId) -> ContextId;
// Create child context
pub fn create_child(&mut self, id: ContextId, parent_id: ContextId)
-> Result<ContextId>;
Complexity:
create_root(): $\mathcal{O}(1)$ - HashMap insertcreate_child(): $\mathcal{O}(1)$ - HashMap lookup + insertExample:
let mut tree = ContextTree::new();
// Create global scope (root)
let global = tree.create_root(0);
// Create module scope (child of global)
let module = tree.create_child(1, global)?;
// Create function scope (child of module)
let function = tree.create_child(10, module)?;
pub fn parent(&self, id: ContextId) -> Option<ContextId>;
Returns:
Some(parent_id) if context has a parentNone if context is root or doesn't existComplexity: $\mathcal{O}(1)$ - HashMap lookup
Example:
let mut tree = ContextTree::new();
let root = tree.create_root(0);
let child = tree.create_child(1, root)?;
assert_eq!(tree.parent(root), None); // Root has no parent
assert_eq!(tree.parent(child), Some(0)); // Child's parent is root
pub fn visible_contexts(&self, id: ContextId) -> Vec<ContextId>;
Returns: [self, parent, grandparent, ..., root] in that order.
Algorithm:
Complexity: $\mathcal{O}(\text{depth})$ where depth is distance from root (typically $\le 5$ for code)
Example:
let mut tree = ContextTree::new();
let global = tree.create_root(0);
let module = tree.create_child(1, global)?;
let function = tree.create_child(10, module)?;
let visible = tree.visible_contexts(function);
// Returns: [10, 1, 0] (function → module → global)
pub fn depth(&self, id: ContextId) -> Option<usize>;
Returns:
Some(depth) where root = 0, children = 1, etc.None if context doesn't existComplexity: $\mathcal{O}(\text{depth})$ - Walk parent chain
Example:
assert_eq!(tree.depth(global), Some(0)); // Root
assert_eq!(tree.depth(module), Some(1)); // 1 level deep
assert_eq!(tree.depth(function), Some(2)); // 2 levels deep
pub fn is_descendant(&self, child_id: ContextId, ancestor_id: ContextId) -> bool;
Returns: true if child_id is a descendant of ancestor_id.
Algorithm:
child_idancestor_id encountered, return trueComplexity: $\mathcal{O}(\text{depth})$
Example:
let mut tree = ContextTree::new();
let root = tree.create_root(0);
let child = tree.create_child(1, root)?;
let grandchild = tree.create_child(2, child)?;
assert!(tree.is_descendant(grandchild, root)); // true
assert!(tree.is_descendant(grandchild, child)); // true
assert!(!tree.is_descendant(root, child)); // false (reversed)
assert!(!tree.is_descendant(child, grandchild)); // false (reversed)
pub fn remove(&mut self, id: ContextId) -> bool;
Behavior: Removes context and all descendants (cascading delete).
Algorithm:
is_descendant() checkComplexity: $\mathcal{O}(n)$ where n is total number of contexts (must check all for descendants)
Example:
let mut tree = ContextTree::new();
let root = tree.create_root(0);
let child = tree.create_child(1, root)?;
let grandchild = tree.create_child(2, child)?;
// Remove child - grandchild also removed
tree.remove(child);
assert!(!tree.contains(child)); // Removed
assert!(!tree.contains(grandchild)); // Also removed (descendant)
assert!(tree.contains(root)); // Unaffected
function visible_contexts(context_id):
result = []
current = context_id
while current is not None:
if current exists in tree:
result.append(current)
current = parent(current)
else:
break // Context not found
return result
Tree Structure:
0 (global)
└── 1 (module)
└── 10 (function)
└── 100 (block)
Call: visible_contexts(100)
Step 1: current = 100, add to result → [100]
Step 2: parent(100) = 10, add to result → [100, 10]
Step 3: parent(10) = 1, add to result → [100, 10, 1]
Step 4: parent(1) = 0, add to result → [100, 10, 1, 0]
Step 5: parent(0) = None, stop
Result: [100, 10, 1, 0]
Time Complexity: $\mathcal{O}(d)$ where d = depth from root
\mathcal{O}(1)$ - querying root context\mathcal{O}(d)$ - deeply nested context\mathcal{O}(3-5)$ - practical code has 3-5 nesting levelsSpace Complexity: $\mathcal{O}(d)$ - result vector size
Real-World Depth:
| Language Construct | Typical Depth | Example |
|---|---|---|
| Global scope | 0 | Top-level variables |
| Module scope | 1 | Module-level functions |
| Class scope | 2 | Class members |
| Method scope | 3 | Method local variables |
| Block scope | 4-5 | Nested if/while/for blocks |
Conclusion: Even deeply nested code (depth 10) requires only ~10 HashMap lookups (~200ns total).
use liblevenshtein::contextual::ContextTree;
let mut tree = ContextTree::new();
// Create global → module → function hierarchy
let global = tree.create_root(0);
let module = tree.create_child(1, global).unwrap();
let function = tree.create_child(10, module).unwrap();
// Check visibility from function
let visible = tree.visible_contexts(function);
assert_eq!(visible, vec![10, 1, 0]);
// Function can see all three levels
assert!(visible.contains(&global));
assert!(visible.contains(&module));
assert!(visible.contains(&function));
let mut tree = ContextTree::new();
// Create tree with siblings:
// global (0)
// ├── module_a (1)
// └── module_b (2)
let global = tree.create_root(0);
let module_a = tree.create_child(1, global).unwrap();
let module_b = tree.create_child(2, global).unwrap();
// Module A can see global, but not Module B
let visible_a = tree.visible_contexts(module_a);
assert!(visible_a.contains(&global));
assert!(visible_a.contains(&module_a));
assert!(!visible_a.contains(&module_b)); // Sibling not visible
// Module B can see global, but not Module A
let visible_b = tree.visible_contexts(module_b);
assert!(visible_b.contains(&global));
assert!(visible_b.contains(&module_b));
assert!(!visible_b.contains(&module_a)); // Sibling not visible
let mut tree = ContextTree::new();
// Create separate root contexts (e.g., multiple files)
let file1 = tree.create_root(1);
let file2 = tree.create_root(2);
// Each root has its own hierarchy
let class1 = tree.create_child(10, file1).unwrap();
let class2 = tree.create_child(20, file2).unwrap();
// Class in file1 cannot see file2
let visible1 = tree.visible_contexts(class1);
assert_eq!(visible1, vec![10, 1]); // Only file1 hierarchy
// Class in file2 cannot see file1
let visible2 = tree.visible_contexts(class2);
assert_eq!(visible2, vec![20, 2]); // Only file2 hierarchy
let mut tree = ContextTree::new();
// Create deeply nested structure
let global = tree.create_root(0);
let module = tree.create_child(1, global).unwrap();
let class = tree.create_child(10, module).unwrap();
let method = tree.create_child(100, class).unwrap();
let block1 = tree.create_child(1000, method).unwrap();
let block2 = tree.create_child(10000, block1).unwrap();
// Deep block can see all ancestors
let visible = tree.visible_contexts(block2);
assert_eq!(visible, vec![10000, 1000, 100, 10, 1, 0]);
// Check depth
assert_eq!(tree.depth(block2), Some(5)); // 5 levels deep
let mut tree = ContextTree::new();
// Build tree:
// 0
// └── 1
// ├── 10
// │ └── 100
// └── 11
let root = tree.create_root(0);
let child = tree.create_child(1, root).unwrap();
let gc1 = tree.create_child(10, child).unwrap();
let ggc = tree.create_child(100, gc1).unwrap();
let gc2 = tree.create_child(11, child).unwrap();
// Remove child - cascades to all descendants
tree.remove(child);
assert!(!tree.contains(child)); // Removed
assert!(!tree.contains(gc1)); // Removed (descendant)
assert!(!tree.contains(ggc)); // Removed (descendant)
assert!(!tree.contains(gc2)); // Removed (descendant)
assert!(tree.contains(root)); // Unaffected (ancestor)
let mut tree = ContextTree::new();
// 0 (global)
// / \
// 1 2
// / \
// 10 20
let global = tree.create_root(0);
let mod1 = tree.create_child(1, global).unwrap();
let mod2 = tree.create_child(2, global).unwrap();
let func1 = tree.create_child(10, mod1).unwrap();
let func2 = tree.create_child(20, mod2).unwrap();
// Vertical relationships
assert!(tree.is_descendant(func1, global)); // true (grandchild)
assert!(tree.is_descendant(func1, mod1)); // true (child)
assert!(tree.is_descendant(mod1, global)); // true (child)
// Horizontal relationships (siblings)
assert!(!tree.is_descendant(mod1, mod2)); // false (siblings)
assert!(!tree.is_descendant(func1, func2)); // false (cousins)
// Reverse relationships
assert!(!tree.is_descendant(global, mod1)); // false (ancestor, not descendant)
let mut tree = ContextTree::new();
let root = tree.create_root(0);
// Check existence
assert!(tree.contains(root));
assert!(!tree.contains(999)); // Doesn't exist
// Check if root
assert!(tree.is_root(root));
assert!(!tree.is_root(999)); // Doesn't exist, so not root
// Get parent safely
match tree.parent(root) {
None => println!("Root context has no parent"),
Some(p) => println!("Parent is {}", p),
}
// Pre-allocate for known number of contexts
let mut tree = ContextTree::with_capacity(100);
// Avoids reallocation during insertions
for i in 0..100 {
tree.create_root(i);
}
assert_eq!(tree.len(), 100);
| Operation | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
create_root() | $\mathcal{O}(1)$ | $\mathcal{O}(1)$ | HashMap insert |
create_child() | $\mathcal{O}(1)$ | $\mathcal{O}(1)$ | HashMap lookup + insert |
parent() | $\mathcal{O}(1)$ | $\mathcal{O}(1)$ | HashMap lookup |
contains() | $\mathcal{O}(1)$ | $\mathcal{O}(1)$ | HashMap lookup |
is_root() | $\mathcal{O}(1)$ | $\mathcal{O}(1)$ | HashMap lookup + match |
visible_contexts() | $\mathcal{O}(d)$ | $\mathcal{O}(d)$ | d = depth from root |
depth() | $\mathcal{O}(d)$ | $\mathcal{O}(1)$ | d = depth from root |
is_descendant() | $\mathcal{O}(d)$ | $\mathcal{O}(1)$ | d = depth from root |
remove() | $\mathcal{O}(n)$ | $\mathcal{O}(n)$ | n = total contexts, must find descendants |
Test Environment: Intel Xeon E5-2699 v3 @ 2.30GHz, Rust 1.75, release build
| Operation | Time (ns) | Throughput |
|---|---|---|
create_root() | ~50 | 20M ops/sec |
create_child() | ~70 | 14M ops/sec |
parent() | ~15 | 67M ops/sec |
visible_contexts(depth=5) | ~150 | 6.7M ops/sec |
depth(depth=5) | ~120 | 8.3M ops/sec |
is_descendant(depth=5) | ~100 | 10M ops/sec |
Key Observations:
Scaling Test (measuring actual memory usage):
| Context Count | Memory (KB) | Per-Context Overhead |
|---|---|---|
| 10 | ~0.3 | ~30 bytes |
| 100 | ~2.5 | ~25 bytes |
| 1,000 | ~24 | ~24 bytes |
| 10,000 | ~240 | ~24 bytes |
Conclusion: ~24 bytes per context, linear scaling, minimal overhead.
Why HashMap over Vec?
Rejected Alternative: Vec<Option<ContextId>> indexed by context ID.
Reasons:
\mathcal{O}(1)$ operations with minimal wasteExample:
Context IDs: [0, 1, 100, 10000]
Vec approach:
- Allocation: 10001 entries (mostly None)
- Memory: 10001 × 8 bytes = 80KB
HashMap approach:
- Allocation: 4 entries
- Memory: 4 × 24 bytes = 96 bytes
Savings: 99.9%
Design Choice: Store only child → parent mapping, not parent → children.
Rationale:
Trade-off:
remove() (must scan all contexts)Conclusion: Optimizes for the common case (queries >> removals).
Why not store children explicitly?
Cascading removal requires finding all descendants. Two approaches:
Approach 1: Store bi-directional links (parent → children, child → parent)
\mathcal{O}(\text{descendants})$ direct lookupApproach 2: Store parent only, scan on removal (current implementation)
\mathcal{O}(\text{total}_\text{contexts})$ scanDecision: Approach 2 (current) because:
\mathcal{O}(n)$ scanContextTree is not thread-safe by itself (uses &mut self for mutations). However, the engine wraps it in Arc<RwLock<ContextTree>>:
context_tree: Arc<RwLock<ContextTree>>,
Locking Strategy:
visible_contexts, parent): Read lock (shared, concurrent)create_child, remove): Write lock (exclusive)Typical Access Pattern:
Included Tests (in src/contextual/context_tree.rs):
test_new() - Empty tree initializationtest_create_root() - Root context creationtest_create_child() - Child context creationtest_create_child_invalid_parent() - Error handling for invalid parenttest_visible_contexts() - Visibility computationtest_is_descendant() - Descendant relationshipstest_depth() - Depth calculationtest_remove() - Cascading removaltest_remove_nonexistent() - Removal error handlingtest_clear() - Clearing entire treetest_multiple_roots() - Multi-root supporttest_complex_hierarchy() - Complex tree scenariosTest Coverage: ~95% of code paths
Potential Properties (for future proptest integration):
// Property: Visibility is transitive
forall contexts A, B, C:
if A in visible(B) and B in visible(C),
then A in visible(C)
// Property: Depth increases monotonically
forall contexts child, parent:
if parent(child) == Some(parent),
then depth(child) == depth(parent) + 1
// Property: Removal is idempotent
forall context C:
remove(C); remove(C) == remove(C)
Can 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 |