This document explains the two complementary approaches to Levenshtein automata implemented in this library, using intuitive "lazy" and "eager" terminology. The lazy engine is the production default; the eager (universal) engine is a parameter-free alternative and a testing oracle. A third, generalized implementation makes the edit operations configurable at run time.
A Levenshtein automaton $A(W, k)$ for a query word $W$ and error bound $k$
accepts exactly the strings within edit distance $k$ of $W$. Its states are
sets of positions $\langle i, e\rangle$ ($i$ characters of $W$ consumed, $e$ edits used),
kept minimal by subsumption (a partial order that drops dominated positions).
Also known as: Parameterized Levenshtein Automata
Key Characteristic: States are constructed lazily (on-demand) during dictionary traversal — there is no precompiled automaton.
\mathcal{O}(\lvert W\rvert)$ distinct states arise for fixed $k$.src/transducer/.\mathcal{O}(\log n)$ effective dictionary complexity (DAWG pruning).StatePool + the query-specific states.Analogy: like a JIT (just-in-time) compiler — compiles only what is needed, when it is needed.
Also known as: Universal Levenshtein Automata
Key Characteristic: the entire automaton structure is constructed eagerly (upfront) before any queries.
max_distance ($k$).src/transducer/universal/.\mathcal{O}(n)$ for a linear dictionary scan (currently).\mathcal{O}(n^2)$ states for distance $n$.Analogy: like an AOT (ahead-of-time) compiler — prepares everything upfront, reuses across inputs.
| Aspect | Lazy Automata | Eager Automata |
|---|---|---|
| Academic Name | Parameterized | Universal |
| Common Name | Lazy | Eager |
| Construction Timing | Query time (per word) | Upfront (once per distance) |
| State Construction | On-demand | Precomputed |
| Reusability | Per query word only | Any word with same distance |
| State Space Size | Minimal (reachable only) | Complete (all possible) |
| Position Type | Concrete (term_index) | Abstract (I/M + offset) |
| Dictionary Integration | ✅ Fully integrated | ❌ Standalone primitive |
| Performance (dict query) | 2-10× faster | Slower (linear scan) |
| Performance (primitive) | N/A | 339-490ns |
| Memory Footprint | StatePool + states | $\mathcal{O}(n^2)$ automaton |
| Best Use Case | Production queries | Oracle testing, primitives |
✅ Production dictionary queries (primary use case)
\mathcal{O}(\log n)$ complexity from DAWG integration✅ Large dictionaries (>1K terms)
✅ Batch processing
✅ Low to medium distances (d=1-3)
✅ Single word-pair distance checks (no dictionary)
✅ Oracle testing (differential testing)
✅ Research and prototyping
✅ Parameter-free reuse (future)
✅ Very high distances (d>5, future)
Single Query:
Batch Throughput: 3.9-4.1 Kelem/s (consistent)
Dictionary Scaling: $\mathcal{O}(\log n)$
Growth Rate: Sub-linear (DAWG pruning)
Primitive Operation (accepts):
Dictionary Query (linear scan):
Growth Rate: Linear $\mathcal{O}(n)$
Distance Scaling: Predictable
Core Components:
Position: (term_index, num_errors, is_special)
State: SmallVec<[Position; 8]>
\le 8$ positionsStatePool: Preallocated buffer for state reuse
AutomatonZipper: Manages traversal state
Key Optimizations:
Core Components:
UniversalPosition: I(offset, errors) or M(offset, errors)
CharacteristicVector: $\beta(x, w)$
w$ match character $x$State: SmallVec<[UniversalPosition; 8]>
DiagonalCrossing: Detects I→M transitions
Key Properties:
\mathcal{O}(n^2)$ state spaceThe eager automaton serves as an oracle (an independent reference implementation, trusted to give the correct answer) for testing the lazy automaton via differential / property-based testing:
// Differential testing
proptest! {
#[test]
fn prop_lazy_matches_eager_oracle(
query: String,
dict_word: String,
distance: u8
) {
// Eager (oracle - reference implementation)
let eager = EagerAutomaton::new(distance);
let eager_accepts = eager.accepts(&dict_word, &query);
// Lazy (implementation under test)
let dict = DynamicDawg::from_terms(vec![dict_word.clone()]);
let lazy = LazyTransducer::new(dict, Algorithm::Standard);
let lazy_accepts = lazy.query(&query, distance as usize)
.any(|r| r == dict_word.as_str());
// Must agree!
assert_eq!(eager_accepts, lazy_accepts);
}
}
Benefits:
Restricted Substitutions (available)
SubstitutionSet / SubstitutionPolicyUnrestricted is a ZST)Additional Optimizations
Dictionary Integration (planned)
\mathcal{O}(n)$ linear scanRestricted Substitutions
Parallel Dictionary Scanning
Lazy Automata:
Eager Automata:
Terminology:
Lazy and Eager automata are complementary, not competitive:
Both approaches have unique strengths for different use cases. The lazy/eager terminology makes the fundamental trade-off clear: when does construction happen, and what are the consequences?
For production use, choose lazy automata. For testing lazy automata, use eager automata as oracle. This dual approach ensures correctness while maintaining optimal performance.
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 |