Date: 2025-11-12 Status: Historical initial design; the current implementation contract is the generalized-automaton repair
This page preserves the original phased design context. It is not the current source of truth for acceptance, weighted costs, performance, or public APIs. The shipped engine uses an exact scaled sparse alignment graph; its older universal-position types remain only as a compatibility surface.
This document describes the design and implementation plan for GeneralizedAutomaton, a new automaton type that supports runtime-configurable operations via OperationSet. This enables phonetic corrections, custom edit distance metrics, and other advanced string matching features while preserving the performance of the existing UniversalAutomaton.
The existing UniversalAutomaton uses compile-time specialization via PositionVariant:
pub struct UniversalAutomaton<V: PositionVariant, P: SubstitutionPolicy> {
max_distance: u8,
policy: P, // Unused
_phantom: PhantomData<V>,
}
Problems:
PositionVariant and OperationSet create semantic conflict if mixedSemantic Clarity:
// Clear: Fixed operations, compile-time optimized
let fast = UniversalAutomaton::<Standard>::new(2);
// Clear: Custom operations, runtime flexibility
let custom = GeneralizedAutomaton::new(2, phonetic_english_basic());
Performance Preservation:
UniversalAutomaton - Zero overhead (unchanged)GeneralizedAutomaton - Pays for runtime flexibility only when neededAcademic Alignment:
OperationSet parameter\langle 1,1,w\rangle$ operations initially\langle m,n,w\rangle$ where $m>1$ or $n>1$)// Existing: Compile-time operations via PositionVariant
pub struct UniversalAutomaton<V: PositionVariant, P: SubstitutionPolicy = Unrestricted> {
max_distance: u8,
policy: P,
_phantom: PhantomData<V>,
}
// NEW: Runtime operations via OperationSet
pub struct GeneralizedAutomaton<P: SubstitutionPolicy = Unrestricted> {
max_distance: u8,
policy: P,
operation_set: OperationSet,
}
Key Difference: No PositionVariant generic parameter - operations come from operation_set field.
src/transducer/
├── universal/ # Existing - UNCHANGED
│ ├── mod.rs
│ ├── automaton.rs # UniversalAutomaton<V, P>
│ ├── position.rs # UniversalPosition<V>
│ ├── state.rs # UniversalState<V>
│ ├── subsumption.rs
│ ├── bit_vector.rs
│ └── diagonal.rs
│
├── generalized/ # NEW
│ ├── mod.rs
│ ├── automaton.rs # GeneralizedAutomaton<P>
│ ├── position.rs # GeneralizedPosition (no variant)
│ ├── state.rs # GeneralizedState
│ └── bit_vector.rs # CharacteristicVector (same as universal)
│
├── operation_set.rs # Already exists
├── operation_type.rs # Already exists
└── phonetic.rs # Already exists
Goal: Create basic GeneralizedAutomaton structure and position types.
Tasks:
src/transducer/generalized/ directoryuniversal/position.rs → generalized/position.rsPositionVariant generic parameter from GeneralizedPositionDeliverable: GeneralizedPosition that compiles and passes basic tests.
Goal: Implement state management and transition logic with OperationSet.
Tasks:
universal/state.rs → generalized/state.rsPositionVariant from GeneralizedStatetransition() to accept OperationSet parametersuccessors() to iterate over operations:
pub fn successors(
&self,
bit_vector: &CharacteristicVector,
max_distance: u8,
operation_set: &OperationSet, // NEW
) -> Vec<Self> {
let mut successors = Vec::new();
// Filter to single-character operations only (Phase 1)
for op in operation_set.operations().iter()
.filter(|op| op.consume_x() == 1 && op.consume_y() == 1)
{
// Generate successor based on operation
match (op.consume_x(), op.consume_y()) {
(1, 1) if op.weight() == 0.0 => {
// Match operation
if bit_vector.is_match(...) {
successors.push(...);
}
}
(1, 1) => {
// Substitution
if op.can_apply(...) {
successors.push(...);
}
}
(0, 1) => {
// Insertion
successors.push(...);
}
(1, 0) => {
// Deletion
successors.push(...);
}
_ => {} // Skip multi-char ops in Phase 1
}
}
successors
}
Deliverable: GeneralizedState with working transitions.
Goal: Implement GeneralizedAutomaton with accepts() method.
Tasks:
src/transducer/generalized/automaton.rsGeneralizedAutomaton structure:
pub struct GeneralizedAutomaton<P: SubstitutionPolicy = Unrestricted> {
max_distance: u8,
policy: P,
operation_set: OperationSet,
}
impl<P: SubstitutionPolicy> GeneralizedAutomaton<P> {
pub fn new(max_distance: u8, operation_set: OperationSet) -> Self {
Self {
max_distance,
policy: P::default(),
operation_set,
}
}
pub fn accepts(&self, word: &str, query: &str) -> bool {
// Similar to UniversalAutomaton::accepts()
// but passes operation_set to state.transition()
}
}
universal/bit_vector.rsgeneralized/mod.rsDeliverable: Working GeneralizedAutomaton that accepts strings.
Goal: Comprehensive tests and documentation.
Tasks:
Deliverable: Fully tested and documented GeneralizedAutomaton.
use liblevenshtein::transducer::GeneralizedAutomaton;
use liblevenshtein::transducer::OperationSet;
// With standard operations
let ops = OperationSet::standard();
let automaton = GeneralizedAutomaton::new(2, ops);
// With phonetic operations
let ops = phonetic_english_basic();
let automaton = GeneralizedAutomaton::new(2, ops);
// With custom operations
let ops = OperationSetBuilder::new()
.with_match()
.with_substitution()
.with_insertion()
.with_deletion()
.build();
let automaton = GeneralizedAutomaton::new(2, ops);
// Check if query matches word within distance
if automaton.accepts("phone", "fone") {
println!("Match!");
}
// Note: Phase 1 only supports accepts()
// Future: distance(), matches(), etc.
| Aspect | UniversalAutomaton | GeneralizedAutomaton |
|---|---|---|
| Operations | Compile-time (PositionVariant) | Runtime (OperationSet) |
| Performance | Zero-cost abstraction | Small runtime overhead |
| Flexibility | 3 fixed variants | Unlimited custom ops |
| Multi-char ops | Not supported | Phase 2 (future) |
| Weighted ops | Not supported | Phase 3 (future) |
| API complexity | Simple (variant only) | Medium (need OperationSet) |
Position Tests (generalized/position.rs):
State Tests (generalized/state.rs):
Automaton Tests (generalized/automaton.rs):
, etc.)
#[test]
fn test_standard_operations() {
let ops = OperationSet::standard();
let automaton = GeneralizedAutomaton::new(2, ops);
assert!(automaton.accepts("test", "test")); // exact match
assert!(automaton.accepts("test", "tst")); // deletion
assert!(automaton.accepts("test", "teest")); // insertion
assert!(automaton.accepts("test", "tast")); // substitution
}
#[test]
fn test_phonetic_operations() {
let ops = phonetic_english_basic();
let automaton = GeneralizedAutomaton::new(1, ops);
// Phonetic substitutions
assert!(automaton.accepts("phone", "fone")); // ph→f
assert!(automaton.accepts("knight", "nite")); // kn→n, gh→(silent)
assert!(automaton.accepts("write", "rite")); // wr→r
}
#[test]
fn test_custom_operations() {
let ops = OperationSetBuilder::new()
.with_match()
.with_deletion()
.build();
let automaton = GeneralizedAutomaton::new(1, ops);
// Only match and deletion allowed
assert!(automaton.accepts("test", "test")); // match
assert!(automaton.accepts("test", "tst")); // deletion
assert!(!automaton.accepts("test", "teest")); // insertion not allowed
}
Compared to UniversalAutomaton:
For Typical Use Cases:
\le 10$ ops): Minimal overhead>20$ ops): May see more overhead(consume_x, consume_y) signatureGoal: Support $\langle m,n,w\rangle$ where $m>1$ or $n>1$ (transposition, merge, split, phonetic digraphs).
Required Changes:
CharacteristicVector to match patternssuccessors()Estimated: 2-3 weeks
Goal: Support fractional operation costs (e.g., 0.15 for phonetic substitutions).
Required Changes:
errors: u8 → errors: f64 in positionsEstimated: 1-2 weeks
Goal: Extract common code between Universal and Generalized to eliminate duplication.
Approach: Create OperationProvider trait with compile-time and runtime implementations.
Estimated: 2-3 weeks
No changes required - UniversalAutomaton remains unchanged:
use liblevenshtein::transducer::universal::{UniversalAutomaton, Standard};
let automaton = UniversalAutomaton::<Standard>::new(2);
assert!(automaton.accepts("test", "tset"));
use liblevenshtein::transducer::GeneralizedAutomaton;
use liblevenshtein::transducer::phonetic::phonetic_english_basic;
let ops = phonetic_english_basic();
let automaton = GeneralizedAutomaton::new(2, ops);
// "fone" matches "phone" via ph→f phonetic rule
assert!(automaton.accepts("phone", "fone"));
Use UniversalAutomaton when:
Use GeneralizedAutomaton when:
Subsumption Relation: Does the Standard subsumption relation work for weighted operations? Need mathematical verification.
Diagonal Crossing: The Universal implementation has diagonal crossing disabled (bug). Should Generalized include it from the start?
Error Representation: Should we support fractional errors in Phase 1, or defer to Phase 3?
API Surface: Should we expose GeneralizedPosition and GeneralizedState publicly, or keep them internal?
| File | Lines | Description |
|---|---|---|
generalized/mod.rs | ~50 | Module exports |
generalized/automaton.rs | ~300 | GeneralizedAutomaton impl |
generalized/position.rs | ~400 | Position logic without variant |
generalized/state.rs | ~500 | State transitions with OperationSet |
generalized/bit_vector.rs | ~200 | Characteristic vectors (copy from universal) |
| Total | ~1450 |
// generalized/mod.rs
pub use self::automaton::GeneralizedAutomaton;
pub use self::position::GeneralizedPosition;
pub use self::state::GeneralizedState;
mod automaton;
mod position;
mod state;
mod bit_vector;
// Reuse from universal
use super::universal::{subsumption, diagonal};
Last Updated: 2025-11-12 Next Review: After Phase 1 completion
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 |