This document describes the design for a hierarchical zipper system that enables contextual code completion with concurrent read-write operations. The system allows character-by-character insertion of terms while simultaneously querying for spelling corrections, with full support for hierarchical lexical scopes and undo/redo operations.
As a user types an identifier in their code editor:
let engine = ContextualCompletionEngine::new();
// User is typing in function scope (context 42)
// Parent contexts: global (0), module (1), class (10)
let context_hierarchy = vec![0, 1, 10, 42];
// User types 'p'
engine.insert_char(42, 'p')?;
let completions = engine.complete("p", 1, &context_hierarchy);
// Results: ["print", "parse", "push", "p" (draft)]
// User types 'r'
engine.insert_char(42, 'r')?;
let completions = engine.complete("pr", 1, &context_hierarchy);
// Results: ["print", "printf", "process", "pr" (draft)]
// User saves checkpoint for undo
let checkpoint = engine.checkpoint(42)?;
// User types 'i'
engine.insert_char(42, 'i')?;
let completions = engine.complete("pri", 1, &context_hierarchy);
// Results: ["print", "printf", "private", "pri" (draft)]
// User hits backspace
engine.rollback_char(42)?;
// Back to "pr"
// User hits Ctrl+Z (undo to checkpoint)
engine.restore(42, checkpoint)?;
// Back to "p"
// User accepts "print" completion
engine.finalize(42, vec![42])?; // Mark visible in context 42
// "print" is now a finalized term
┌─────────────────────────────────────────────────────────┐
│ Contextual Completion Engine (Layer 4) │
│ - Draft management (insert/rollback/checkpoint) │
│ - Hierarchical context tree │
│ - Query fusion (finalized + drafts) │
└────────────────────┬────────────────────────────────────┘
│
┌───────────┴───────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Finalized Terms │ │ Draft Buffers │
│ (PathMap/DAT) │ │ (per-context) │
└────────┬────────┘ └────────┬────────┘
│ │
└───────────┬───────────┘
▼
┌───────────────────────┐
│ Intersection Zipper │
│ (Layer 3) │
└─────┬───────────┬─────┘
│ │
┌───────▼─────┐ ┌──▼──────────────┐
│ Dictionary │ │ Automaton │
│ Zipper │ │ Zipper │
│ (Layer 1) │ │ (Layer 2) │
└─────────────┘ └─────────────────┘
/// Core trait for dictionary navigation
pub trait DictionaryZipper: Clone {
type Unit: CharUnit;
/// Check if current position marks end of term
fn is_final(&self) -> bool;
/// Navigate to child node with given label
/// Returns None if no such child exists
fn descend(&self, label: Self::Unit) -> Option<Self>;
/// Iterator over all children (label, child zipper pairs)
fn children(&self) -> impl Iterator<Item = (Self::Unit, Self)>;
/// Get current path from root (for debugging)
fn path(&self) -> Vec<Self::Unit>;
}
/// Extension for dictionaries with associated values
pub trait ValuedDictionaryZipper: DictionaryZipper {
type Value: DictionaryValue;
/// Get value at current position if final
fn value(&self) -> Option<Self::Value>;
}
/// Zipper for PathMapDictionary backend (lock-free, TrieRef-based)
pub struct PathMapZipper<V: DictionaryValue> {
/// Lock-free focus handle over a copy-on-write `TrieRef` snapshot of the PathMap
root: TrieRefLike,
/// Current path from root
path: Arc<[u8]>,
}
impl<V: DictionaryValue> DictionaryZipper for PathMapZipper<V> {
type Unit = u8;
fn is_final(&self) -> bool {
self.with_zipper(|z| z.is_val())
}
fn descend(&self, label: u8) -> Option<Self> {
let mut new_path = (*self.path).clone();
new_path.push(label);
// Check if path exists
let exists = {
let map = self.map.read().unwrap();
let mut zipper = map.read_zipper();
zipper.descend_to(&new_path);
zipper.path_exists()
};
if exists {
Some(PathMapZipper {
map: Arc::clone(&self.map),
path: Arc::new(new_path),
})
} else {
None
}
}
fn children(&self) -> impl Iterator<Item = (u8, Self)> {
let mask = self.with_zipper(|z| z.child_mask());
let map = Arc::clone(&self.map);
let path = Arc::clone(&self.path);
(0u8..=255)
.filter(move |&byte| mask.contains(byte))
.filter_map(move |byte| {
PathMapZipper {
map: Arc::clone(&map),
path: Arc::clone(&path)
}.descend(byte).map(|z| (byte, z))
})
}
fn path(&self) -> Vec<u8> {
(*self.path).clone()
}
// Helper for lock-per-operation pattern
fn with_zipper<F, R>(&self, f: F) -> R
where
F: FnOnce(ReadZipperUntracked<'_, 'static, V>) -> R,
{
let map = self.map.read().unwrap();
let mut zipper = map.read_zipper();
zipper.descend_to(&*self.path);
f(zipper)
}
}
impl<V: DictionaryValue> ValuedDictionaryZipper for PathMapZipper<V> {
type Value = V;
fn value(&self) -> Option<V> {
self.with_zipper(|z| z.val().cloned())
}
}
/// Zipper for DoubleArrayTrie backend (read-only)
#[derive(Clone)]
pub struct DoubleArrayTrieZipper {
/// Reference to trie structure
trie: Arc<DoubleArrayTrie>,
/// Current node index (Copy type for efficiency)
node_index: usize,
/// Accumulated path for debugging
path: Arc<Vec<u8>>,
}
impl DictionaryZipper for DoubleArrayTrieZipper {
type Unit = u8;
fn is_final(&self) -> bool {
self.trie.is_final(self.node_index)
}
fn descend(&self, label: u8) -> Option<Self> {
self.trie.transition(self.node_index, label).map(|next_index| {
let mut new_path = (*self.path).clone();
new_path.push(label);
DoubleArrayTrieZipper {
trie: Arc::clone(&self.trie),
node_index: next_index,
path: Arc::new(new_path),
}
})
}
fn children(&self) -> impl Iterator<Item = (u8, Self)> {
let trie = Arc::clone(&self.trie);
let node_index = self.node_index;
let path = Arc::clone(&self.path);
(0u8..=255)
.filter_map(move |byte| {
trie.transition(node_index, byte).map(|next_index| {
let mut new_path = (*path).clone();
new_path.push(byte);
let zipper = DoubleArrayTrieZipper {
trie: Arc::clone(&trie),
node_index: next_index,
path: Arc::new(new_path),
};
(byte, zipper)
})
})
}
fn path(&self) -> Vec<u8> {
(*self.path).clone()
}
}
/// Zipper for Levenshtein automaton state
#[derive(Clone)]
pub struct AutomatonZipper {
/// Current automaton state (positions)
state: State,
/// Query string (shared across all zippers in a query)
query: Arc<Vec<u8>>,
/// Maximum allowed edit distance
max_distance: usize,
/// Algorithm variant (Standard, Transposition, MergeAndSplit)
algorithm: Algorithm,
}
impl AutomatonZipper {
/// Create root automaton zipper for query
pub fn new(query: &[u8], max_distance: usize, algorithm: Algorithm) -> Self {
let mut state = State::new();
// Initialize with starting positions for each distance threshold
for distance in 0..=max_distance {
state.insert(Position {
term_index: 0,
num_errors: distance,
is_special: false,
});
}
AutomatonZipper {
state,
query: Arc::new(query.to_vec()),
max_distance,
algorithm,
}
}
/// Transition automaton state by consuming a character
pub fn transition(&self, label: u8, pool: &mut StatePool) -> Option<Self> {
transition_state_pooled(
&self.state,
pool,
label,
&self.query,
self.max_distance,
self.algorithm,
false, // substring_mode
).map(|next_state| AutomatonZipper {
state: next_state,
query: Arc::clone(&self.query),
max_distance: self.max_distance,
algorithm: self.algorithm,
})
}
/// Get minimum distance if state is accepting
pub fn min_distance(&self) -> Option<usize> {
self.state.min_distance()
}
/// Infer distance for a term of given length
pub fn infer_distance(&self, term_length: usize) -> Option<usize> {
self.state.infer_distance(term_length)
}
/// Check if state could lead to valid matches
pub fn is_viable(&self) -> bool {
!self.state.is_empty()
}
}
/// Zipper representing intersection of dictionary and automaton
pub struct IntersectionZipper<D: DictionaryZipper> {
/// Dictionary zipper at current position
dict: D,
/// Automaton zipper at current state
automaton: AutomatonZipper,
/// Parent chain for path reconstruction (lightweight)
parent: Option<Box<PathNode<D::Unit>>>,
}
impl<D: DictionaryZipper> IntersectionZipper<D> {
/// Create root intersection zipper
pub fn new(dict: D, automaton: AutomatonZipper) -> Self {
IntersectionZipper {
dict,
automaton,
parent: None,
}
}
/// Check if current position represents a match
pub fn is_match(&self) -> bool {
self.dict.is_final() && self.distance().is_some()
}
/// Get distance for current match (if it is a match)
pub fn distance(&self) -> Option<usize> {
if self.dict.is_final() {
// Reconstruct term length from parent chain
let term_length = self.depth();
self.automaton.infer_distance(term_length)
} else {
None
}
}
/// Get depth (term length) from parent chain
pub fn depth(&self) -> usize {
self.parent.as_ref().map(|p| p.depth()).unwrap_or(0) + 1
}
/// Reconstruct full term from parent chain
pub fn term(&self) -> String {
let labels = self.collect_labels();
String::from_utf8_lossy(&labels).into_owned()
}
/// Navigate to children (combined dictionary + automaton transitions)
pub fn children(&self, pool: &mut StatePool)
-> impl Iterator<Item = IntersectionZipper<D>> + '_
{
let parent_for_children = self.parent.clone();
self.dict.children()
.filter_map(move |(label, dict_child)| {
// Try to transition automaton with this label
self.automaton.transition(label, pool).map(|auto_child| {
// Create parent node for child
let new_parent = Some(Box::new(PathNode::new(
label,
parent_for_children.clone(),
)));
IntersectionZipper {
dict: dict_child,
automaton: auto_child,
parent: new_parent,
}
})
})
}
// Helper to collect labels from parent chain
fn collect_labels(&self) -> Vec<D::Unit> {
let mut labels = Vec::new();
let mut current = &self.parent;
while let Some(node) = current {
labels.push(node.label());
current = node.parent();
}
labels.reverse();
labels
}
}
See contextual-completion-api.md for complete API design.
See contextual-completion-roadmap.md for detailed implementation roadmap.
Each layer should have comprehensive unit tests:
Test complete workflows:
#[test]
fn test_contextual_completion_workflow() {
let engine = ContextualCompletionEngine::new();
// Setup hierarchy: global(0) -> module(1) -> function(42)
engine.create_context(0, None); // root
engine.create_context(1, Some(0));
engine.create_context(42, Some(1));
// Finalize some terms in parent contexts
engine.finalize_direct(0, "print", vec![0]); // global
engine.finalize_direct(1, "parse", vec![1]); // module
// User types in function context
engine.insert_char(42, 'p');
engine.insert_char(42, 'r');
// Query with hierarchy
let results = engine.complete("pr", 1, &[0, 1, 42]);
// Should see: parent terms + draft
assert!(results.iter().any(|c| c.term == "print")); // from global
assert!(results.iter().any(|c| c.term == "parse")); // from module
assert!(results.iter().any(|c| c.term == "pr" && c.is_draft));
// Finalize draft
engine.finalize(42, vec![42]);
// Query again - now "pr" is finalized
let results = engine.complete("pr", 1, &[0, 1, 42]);
assert!(results.iter().any(|c| c.term == "pr" && !c.is_draft));
}
Test thread-safety:
#[test]
fn test_concurrent_insertions_and_queries() {
let engine = Arc::new(ContextualCompletionEngine::new());
let mut handles = vec![];
// Spawn multiple threads inserting in different contexts
for context_id in 0..8 {
let engine = Arc::clone(&engine);
handles.push(std::thread::spawn(move || {
for ch in "hello".chars() {
engine.insert_char(context_id, ch).unwrap();
}
engine.finalize(context_id, vec![context_id]).unwrap();
}));
}
// Spawn threads querying concurrently
for _ in 0..8 {
let engine = Arc::clone(&engine);
handles.push(std::thread::spawn(move || {
for i in 1..=5 {
let query = "hello"[..i].to_string();
let _ = engine.complete(&query, 1, &[0, 1, 2, 3, 4, 5, 6, 7]);
}
}));
}
// Wait for all threads
for handle in handles {
handle.join().unwrap();
}
// Verify all terms were finalized
let results = engine.complete("hello", 0, &[0, 1, 2, 3, 4, 5, 6, 7]);
assert_eq!(results.len(), 8);
}
Performance regression tests:
#[bench]
fn bench_query_with_drafts(b: &mut Bencher) {
let engine = setup_large_dictionary(10000);
// Add 100 draft terms
for i in 0..100 {
for ch in format!("draft{}", i).chars() {
engine.insert_char(i, ch).unwrap();
}
}
b.iter(|| {
engine.complete("draf", 1, &(0..100).collect::<Vec<_>>())
});
}
Per-context draft tracking:
VecDeque<char>: ~32 bytes + characters (grows incrementally)Estimated overhead:
Fusion overhead:
\mathcal{O}(n \log n)$ where n = total results (typically < 1000)Expected impact:
Target: < 10% average overhead (NFR2)
PathMap lock strategy:
Expected contention:
src/dictionary/mod.rs:111-211src/transducer/intersection.rssrc/transducer/pool.rssrc/dictionary/pathmap.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 |