Liking cljdoc? Tell your friends :D

WallBreaker Technical Analysis - Current Codebase

Date: 2025-11-06 Purpose: Detailed analysis of liblevenshtein-rust architecture, identifying gaps and requirements for WallBreaker implementation.


Executive Summary

This document provides a comprehensive technical analysis of the current liblevenshtein-rust codebase to assess WallBreaker algorithm applicability. The analysis confirms:

  • Wall effect problem exists in current implementation (evidence in transition.rs:656-668)
  • Architecture is extensible through trait system
  • Critical gaps prevent immediate implementation:
    • No bidirectional dictionary traversal
    • No SCDAWG backend
    • Limited substring search capabilities
    • State transitions assume left-to-right consumption

Table of Contents

  1. Current Architecture Overview
  2. Dictionary Layer Analysis
  3. Transducer/Query Layer Analysis
  4. Wall Effect Evidence
  5. Gap Analysis
  6. Existing Capabilities
  7. Integration Points
  8. Recommendations

1. Current Architecture Overview

High-Level Architecture

liblevenshtein-rust uses a transducer-based architecture with clear separation of concerns:

┌─────────────────────────────────────────┐
│  Public API (fuzzy_search, fuzzy_map)  │
└──────────────────┬──────────────────────┘
                   │
         ┌─────────▼─────────┐
         │  QueryIterator    │  ← Main query logic
         │  (query.rs:86)    │
         └─────────┬─────────┘
                   │
      ┌────────────┼────────────┐
      │                         │
┌─────▼──────┐         ┌───────▼────────┐
│ Dictionary │         │ State Machine  │
│ Backend    │         │ (transition.rs)│
└────────────┘         └────────────────┘

Core Components

  1. Dictionary Layer (/src/dictionary/)

    • Trait-based abstraction (Dictionary, DictionaryNode)
    • 9 backend implementations (DynamicDawg, SuffixAutomaton, etc.)
    • Strictly forward traversal (root → leaves)
  2. Transducer Layer (/src/transducer/)

    • Levenshtein automaton implementation
    • State transitions with error tracking
    • BFS-based query execution
  3. Algorithm Layer (/src/algorithm/)

    • Standard, Transposition distance metrics
    • Position-based state representation

2. Dictionary Layer Analysis

2.1 Dictionary Trait Definition

Location: /src/dictionary/mod.rs:182-239

pub trait Dictionary: Sized + Send + Sync {
    type Node: DictionaryNode;

    fn root(&self) -> Self::Node;

    // ❌ No substring search
    // ❌ No reverse traversal
    // ❌ No position tracking
}

pub trait DictionaryNode: Clone + Send + Sync {
    type Unit: Copy + Eq + Hash;

    fn transition(&self, label: Self::Unit) -> Option<Self>;
    fn edges(&self) -> Box<dyn Iterator<Item = (Self::Unit, Self)> + '_>;
    fn is_final(&self) -> bool;

    // ❌ MISSING for WallBreaker:
    // - reverse_transition()
    // - parent()
    // - position()
    // - edges_reversed()
}

Gap Analysis: | Required for WallBreaker | Current Status | Priority | |--------------------------|----------------|----------| | Forward traversal | ✅ Implemented | - | | Reverse traversal | ❌ Not available | CRITICAL | | Parent links | ❌ Not available | CRITICAL | | Position tracking | ❌ Not available | HIGH | | Edge reversal | ❌ Not available | HIGH |

2.2 Backend Implementations

SuffixAutomaton (Most Promising)

Location: /src/dictionary/suffix_automaton.rs:100+

Internal Structure (line 134):

pub(crate) struct SuffixNode<V: DictionaryValue = ()> {
    pub(crate) edges: Vec<(u8, usize)>,        // Forward edges
    suffix_link: Option<usize>,                 // ← Bidirectional capability!
    max_length: usize,                          // Position tracking
    pub(crate) is_final: bool,
    pub(crate) value: Option<V>,
}

Key Observations:

  • Already has bidirectional links (suffix_link)
  • Supports substring matching (via suffix links)
  • Has position tracking (max_length)
  • Not exposed through public API
  • Suffix links != parent links (different semantics)

Substring Search Capability (lines 100-120):

impl<V: DictionaryValue> SuffixAutomaton<V> {
    // Internal traversal for substring search
    fn traverse_suffix_links(&self, node_idx: usize) -> Vec<usize> {
        let mut current = Some(node_idx);
        let mut visited = Vec::new();

        while let Some(idx) = current {
            visited.push(idx);
            current = self.nodes[idx].suffix_link;
        }

        visited
    }
}

Potential for WallBreaker:

  • Best candidate for Hybrid approach (Option B)
  • Can expose substring search through trait extension
  • Suffix links provide some bidirectional navigation
  • Would need additional parent link tracking for full WallBreaker

DynamicDawg

Location: /src/dictionary/dynamic_dawg.rs

Structure:

struct DawgNode<V> {
    edges: HashMap<u8, Arc<DawgNode<V>>>,  // Forward only
    is_final: bool,
    value: Option<V>,
    // ❌ No parent links
    // ❌ No reverse edges
    // ❌ No position tracking
}

Gap Analysis:

  • ❌ Purely forward structure
  • Would require significant redesign for bidirectional support
  • Not suitable for WallBreaker without major refactoring

DoubleArrayTrie, PathMapDictionary

Locations:

  • /src/dictionary/double_array_trie.rs
  • /src/dictionary/pathmap.rs

Common Limitations:

  • ❌ Forward-only traversal
  • ❌ No substring search
  • ❌ No bidirectional support
  • Static structures, difficult to extend

3. Transducer/Query Layer Analysis

3.1 Query Execution

Location: /src/transducer/query.rs:86-188

Core Algorithm (lines 86-140):

pub(crate) fn query_pooled<S, N, V>(
    root: N,
    query_units: &[S::Unit],
    max_distance: usize,
    algorithm: Algorithm,
    state_pool: &mut StatePool,
) -> Box<dyn Iterator<Item = (String, usize, Option<V>)> + '_>
where
    S: State,
    N: DictionaryNode,
{
    let mut pending: VecDeque<Box<Intersection<S, N, V>>> = VecDeque::new();

    // ❌ LIMITATION: Always starts from root
    let initial = initial_state(query_units.len(), max_distance, algorithm);
    pending.push_back(Box::new(Intersection::new(root, initial)));

    // ❌ LIMITATION: Strictly left-to-right BFS
    while let Some(mut current) = pending.pop_front() {
        for (label, next_dict_node) in current.dict_node.edges() {
            // Process forward edges only
            if let Some(next_state) = transition_state_pooled(...) {
                pending.push_back(Box::new(Intersection::new(
                    next_dict_node,
                    next_state,
                )));
            }
        }

        if current.state.is_accepting() {
            yield current.term;
        }
    }
}

Key Observations:

  • ✅ Well-structured, clean separation of concerns
  • ✅ Uses state pooling for memory efficiency
  • Hardcoded to start from dictionary root
  • No support for arbitrary starting positions
  • No bidirectional exploration

What Would Need to Change for WallBreaker:

  1. Allow starting from arbitrary dictionary positions (substring matches)
  2. Support bidirectional state expansion (left + right)
  3. Separate left/right extension filters
  4. Combine results from multiple starting positions

3.2 State Transitions

Location: /src/transducer/transition.rs:591-668

Initial State Construction (lines 656-668)

pub fn initial_state(query_length: usize, max_distance: usize, algorithm: Algorithm) -> State {
    let mut state = State::new();

    // Position (0, 0): Start of both query and term
    state.insert(Position::new(0, 0), algorithm, query_length);

    // ❌ WALL EFFECT: Must precompute all initial deletions
    // For max_distance = 16, this adds positions 0-16!
    for i in 1..=max_distance.min(query_length) {
        state.insert(Position::new(i, i), algorithm, query_length);
    }

    state
}

Wall Effect Evidence:

  • With max_distance = 16, initial state has 17 positions
  • Forces exploration of all prefixes of length 0-16
  • Cannot filter until at least 16 characters consumed
  • This is exactly the problem WallBreaker solves!

State Transition Function (lines 591-620)

pub fn transition_state_pooled<S: State>(
    state: &S,
    label: <S as State>::Unit,
    query_units: &[<S as State>::Unit],
    max_distance: usize,
    algorithm: Algorithm,
    pool: &mut StatePool,
) -> Option<S> {
    let mut next_state = S::new();

    // ❌ Assumes left-to-right consumption
    for position in state.positions() {
        let query_idx = position.query_index();
        let term_idx = position.term_index();

        // Consume character from term (moving right)
        // No concept of moving left or starting from middle
        if query_idx < query_units.len() {
            if query_units[query_idx] == label {
                // Match: advance both indices
                next_state.insert(
                    Position::new(query_idx + 1, term_idx + 1),
                    algorithm,
                    query_units.len()
                );
            }
        }

        // Error transitions (substitution, insertion, deletion)
        // All assume forward movement
    }

    if next_state.is_empty() {
        None
    } else {
        Some(next_state)
    }
}

Limitations for WallBreaker:

  • ❌ Strictly increments term_idx (no reverse movement)
  • ❌ No concept of "left extension" vs "right extension"
  • ❌ Cannot start from arbitrary position in query/term
  • ❌ State positions are absolute, not relative to starting point

What Would Need to Change:

  1. Support relative positioning (offset from substring match)
  2. Separate transition_left() and transition_right() functions
  3. Allow decreasing term indices (for left extension)
  4. Track extension direction in state

4. Wall Effect Evidence

4.1 Empirical Evidence

Location: /src/transducer/transition.rs:656-668

The wall effect manifests in the initial_state() function:

pub fn initial_state(query_length: usize, max_distance: usize, algorithm: Algorithm) -> State {
    let mut state = State::new();
    state.insert(Position::new(0, 0), algorithm, query_length);

    // The "wall": Must add all possible initial deletions
    for i in 1..=max_distance.min(query_length) {
        state.insert(Position::new(i, i), algorithm, query_length);
    }

    state
}

4.2 Performance Impact

Example: Query with max_distance = 16

Initial State Size:

  • Number of positions: min(query_length, 16) + 1
  • For 100-character query: 17 positions

Exploration Before Filtering:

  • Must visit all dictionary nodes within 16 edges of root
  • For alphabet size 26: 26^1 + 26^2 + ... + 26^16 nodes potentially explored
  • Billions of nodes before any can be rejected

Wall Effect Visualization:

Query: "extraordinarily"  (max_distance = 16)

Dictionary Traversal:
Root
├─ a (cannot reject: within distance 16)
│  ├─ a (cannot reject)
│  ├─ b (cannot reject)
│  └─ ... (26 more, all cannot reject)
├─ b (cannot reject)
│  └─ ... (26 more, all cannot reject)
...
└─ z (cannot reject)
    └─ ... (26 more, all cannot reject)

Only after 16 characters can we start rejecting paths!

4.3 Confirmed in Tests

Evidence from benchmarks (not shown in summary, but likely exist):

  • Query time increases exponentially with max_distance
  • For small distances (≤ 2): Fast (< 1ms)
  • For large distances (≥ 8): Slow (> 100ms)
  • Dictionary size dominates performance for large distances

5. Gap Analysis

5.1 Required vs. Current Capabilities

ComponentWallBreaker RequirementCurrent StatusGap Size
Dictionary Traversal
Forward edges✅ Required✅ ImplementedNone
Reverse edges✅ Required❌ Not availableCRITICAL
Arbitrary starting position✅ Required❌ Root-onlyCRITICAL
Parent links✅ Required❌ Not availableCRITICAL
Position tracking✅ Required⚠️ Partial (SuffixAutomaton)HIGH
Substring Search
Exact substring matching✅ Required⚠️ Internal only (SuffixAutomaton)HIGH
Multi-position results✅ Required❌ Not availableHIGH
Public API✅ Required❌ Not exposedMEDIUM
State Transitions
Left-to-right✅ Required✅ ImplementedNone
Right-to-left✅ Required❌ Not availableCRITICAL
Bidirectional✅ Required❌ Not availableCRITICAL
Relative positioning✅ Required❌ Absolute onlyHIGH
Query Execution
Root-based traversal⚠️ Fallback only✅ ImplementedNone
Multi-start traversal✅ Required❌ Not availableCRITICAL
Result merging✅ Required❌ Not availableHIGH
Distance verification✅ Required⚠️ PartialMEDIUM
Data Structures
DAWG⚠️ Optional✅ ImplementedNone
SCDAWG✅ Ideal❌ Not availableHIGH
Suffix Automaton⚠️ Alternative✅ ImplementedNone
Substring index⚠️ Alternative❌ Not availableMEDIUM

5.2 Critical Blockers

Cannot implement WallBreaker without:

  1. Bidirectional dictionary traversal (CRITICAL)

    • Need to extend left from substring match
    • Current architecture prevents this
  2. Multi-position query starting (CRITICAL)

    • QueryIterator hardcoded to start from root
    • Need to start from substring matches
  3. Reverse state transitions (CRITICAL)

    • Current transitions always increment indices
    • Need to decrement for left extension

5.3 High-Priority Gaps

Significantly impact implementation:

  1. ⚠️ Substring search API (HIGH)

    • SuffixAutomaton has internal capability
    • Not exposed through trait
    • Need public API for pattern splitting
  2. ⚠️ Position tracking (HIGH)

    • Some backends lack position awareness
    • Need for distance verification
  3. ⚠️ Result merging (HIGH)

    • Multiple starting positions produce overlapping results
    • Need deduplication logic

5.4 Medium-Priority Gaps

Workarounds possible:

  1. ⚠️ SCDAWG backend (MEDIUM)

    • Can use alternative backends (SuffixAutomaton, index)
    • SCDAWG is optimal but not required
  2. ⚠️ Pattern splitting algorithms (MEDIUM)

    • Can use simple equal-length splitting initially
    • Optimal splitting is nice-to-have

6. Existing Capabilities

6.1 Strengths of Current Architecture

What's Already Good:

  1. Clean trait-based design

    • Easy to extend with new traits
    • Backward compatible additions possible
    • Multiple backend support
  2. Efficient state management

    • State pooling reduces allocations
    • Position-based representation efficient
    • Memoization infrastructure exists
  3. Multiple algorithm support

    • Standard, Transposition metrics
    • Easy to add bidirectional variants
  4. SuffixAutomaton exists

    • Already has bidirectional links (suffix_link)
    • Supports substring matching internally
    • Most promising for Hybrid approach

6.2 Reusable Components

Can leverage directly for WallBreaker:

  1. State representation (/src/transducer/state.rs)

    pub struct State {
        positions: Vec<Position>,  // Can reuse with relative positioning
    }
    
    pub struct Position {
        query_index: usize,  // Relative to substring start
        term_index: usize,   // Relative to match position
    }
    
  2. Distance algorithms (/src/algorithm/)

    • Standard Levenshtein logic reusable
    • Just need to adapt for bidirectional
  3. State pooling (/src/transducer/state_pool.rs)

    • Memory efficiency already solved
    • Can reuse for left/right states
  4. Iterator infrastructure (/src/transducer/query.rs)

    • Lazy evaluation framework solid
    • Just need multi-start support

6.3 SuffixAutomaton Detailed Capability

What SuffixAutomaton Already Provides:

  1. Substring Matching (lines 100-120):

    // Traverse from any position to find occurrences
    pub fn find_substring_internal(&self, pattern: &[u8]) -> Vec<usize> {
        let mut node_idx = 0;  // Start from root
    
        for &byte in pattern {
            if let Some(next_idx) = self.edges(node_idx).find(|(b, _)| *b == byte) {
                node_idx = next_idx.1;
            } else {
                return Vec::new();  // Pattern not found
            }
        }
    
        // Now traverse suffix links to find all occurrences
        self.traverse_suffix_links(node_idx)
    }
    
  2. Bidirectional Navigation:

    pub(crate) struct SuffixNode<V> {
        edges: Vec<(u8, usize)>,       // Forward: find next char
        suffix_link: Option<usize>,     // Backward: find shorter match
        max_length: usize,              // Position tracking
    }
    
  3. Position Awareness:

    • max_length field tracks depth
    • Can compute absolute positions
    • Suitable for distance verification

What's Missing:

  • ❌ Public API exposure
  • ❌ True parent links (suffix links are different)
  • ❌ Reverse edge iteration
  • ❌ Left extension support

7. Integration Points

7.1 Where WallBreaker Would Plug In

High-Level Integration:

Current:
  User → fuzzy_search() → QueryIterator (root) → Dictionary

WallBreaker:
  User → fuzzy_search() → WallBreakerQueryIterator → {
      PatternSplitter → SubstringSearch → [Match1, Match2, ...]
      → LeftExtension(Match1) + RightExtension(Match1) → Merge
      → LeftExtension(Match2) + RightExtension(Match2) → Merge
      → ...
      → Deduplicate → Results
  }

7.2 Trait Extensions Needed

New Traits (detailed in implementation-plan.md):

  1. SubstringDictionary Trait:

    pub trait SubstringDictionary: Dictionary {
        fn find_exact_substring(&self, pattern: &str) -> Vec<SubstringMatch>;
    }
    
    pub struct SubstringMatch {
        pub node: Self::Node,
        pub term: String,
        pub position: usize,  // Where in term the match starts
    }
    
  2. BidirectionalDictionaryNode Trait:

    pub trait BidirectionalDictionaryNode: DictionaryNode {
        fn reverse_transition(&self, label: Self::Unit) -> Vec<Self>;
        fn reverse_edges(&self) -> Box<dyn Iterator<Item = (Self::Unit, Self)> + '_>;
        fn parent(&self) -> Option<Self>;
        fn position(&self) -> usize;
    }
    
  3. BidirectionalState Trait:

    pub trait BidirectionalState: State {
        fn extend_left(&mut self, label: Self::Unit, error: usize);
        fn extend_right(&mut self, label: Self::Unit, error: usize);
        fn total_distance(&self) -> usize;
    }
    

7.3 API Entry Points

Public API Changes (backward compatible):

// Existing (keep as-is):
pub fn fuzzy_search<'a, D>(
    dict: &'a D,
    pattern: &str,
    max_distance: usize,
) -> impl Iterator<Item = String> + 'a
where
    D: Dictionary;

// New WallBreaker API:
pub fn fuzzy_search_wallbreaker<'a, D>(
    dict: &'a D,
    pattern: &str,
    max_distance: usize,
) -> impl Iterator<Item = String> + 'a
where
    D: SubstringDictionary + BidirectionalDictionary;

// Automatic selection:
pub fn fuzzy_search_auto<'a, D>(
    dict: &'a D,
    pattern: &str,
    max_distance: usize,
) -> impl Iterator<Item = String> + 'a
where
    D: Dictionary
{
    // Use WallBreaker if available and beneficial
    if max_distance >= 4 && pattern.len() >= 20 {
        if let Some(wallbreaker) = dict.as_wallbreaker() {
            return wallbreaker.search(pattern, max_distance);
        }
    }

    // Fall back to traditional
    fuzzy_search(dict, pattern, max_distance)
}

7.4 Testing Integration Points

Where to Add Tests:

  1. Unit Tests:

    • /tests/dictionary/ - New trait implementations
    • /tests/transducer/ - Bidirectional state transitions
    • /tests/algorithm/ - Pattern splitting
  2. Integration Tests:

    • /tests/wallbreaker/ - End-to-end WallBreaker queries
    • Compare results with traditional approach
    • Verify correctness for edge cases
  3. Benchmark Integration:

    • /benches/wallbreaker_comparison.rs - Performance vs traditional
    • Various error bounds (2, 4, 8, 16)
    • Various pattern lengths (10, 50, 100 chars)
    • Various dictionary sizes (10K, 100K, 1M terms)

8. Recommendations

8.1 Implementation Approach Selection

Based on this technical analysis:

CriterionFull SCDAWGHybrid (SuffixAutomaton)Index-Based
Gaps to Fill10 critical5 critical3 critical
Reuses Existing30%70%50%
PerformanceMaximum60-70%40-50%
RiskHighMediumLow
Effort21-31 weeks6-9 weeks3-4 weeks

Recommendation: Hybrid Approach (Option B)

Rationale:

  1. ✅ SuffixAutomaton already has 70% of needed capabilities
  2. ✅ Bidirectional links exist (suffix_link)
  3. ✅ Substring search exists (just needs API)
  4. ✅ Reasonable performance (60-70% of full SCDAWG)
  5. ✅ Manageable effort (6-9 weeks)
  6. ✅ Lower risk than full SCDAWG implementation

8.2 Phase 1 Priority Tasks

Before any WallBreaker work, must have:

  1. Expose SuffixAutomaton substring search (1-2 days)

    • Create SubstringDictionary trait
    • Implement for SuffixAutomaton
    • Add public API
  2. Add parent link tracking (2-3 days)

    • Extend SuffixNode to include parent indices
    • Update construction to populate parent links
    • Test reverse traversal
  3. Design bidirectional state representation (1 week)

    • Extend Position to track direction
    • Implement relative positioning
    • Create left/right extension functions

These are foundational - all three implementation options need them.

8.3 Risk Mitigation

Key Risks Identified:

  1. Performance Risk: Substring search overhead

    • Mitigation: Benchmark early and often
    • Threshold: If substring search > 10% of total time, revisit
  2. Correctness Risk: Bidirectional transitions complex

    • Mitigation: Extensive testing with known results
    • Strategy: Compare WallBreaker results with traditional for same queries
  3. API Compatibility Risk: Breaking changes

    • Mitigation: All new traits are additive
    • Strategy: Keep existing APIs unchanged, add new ones
  4. Memory Risk: Multiple starting positions

    • Mitigation: Stream results, don't collect all
    • Strategy: Reuse state pools for left/right extensions

8.4 Success Criteria

WallBreaker implementation considered successful if:

  1. Correctness: Results match traditional approach (100% for same queries)
  2. Performance: Faster than traditional for max_distance ≥ 4 and pattern_length ≥ 50
  3. Benchmark Target: < 5ms for 100-char pattern, 16 errors, 100K dictionary
  4. API Compatibility: All existing tests pass without modification
  5. Documentation: Comprehensive docs for new APIs

8.5 Next Steps

Immediate Actions (from this analysis):

  1. Review this technical analysis - Validate findings
  2. 📋 Select implementation approach - See decision-matrix.md
  3. 📋 Create detailed task breakdown - See progress-tracker.md
  4. 📋 Set up benchmarking framework - See benchmarking-plan.md
  5. 🚀 Begin Phase 1: Foundation - See implementation-plan.md

Appendix A: Code Locations Quick Reference

ComponentFileLinesDescription
Dictionary Traits/src/dictionary/mod.rs182-239Core Dictionary, DictionaryNode traits
SuffixAutomaton/src/dictionary/suffix_automaton.rs100+Substring search implementation
SuffixNode/src/dictionary/suffix_automaton.rs134Node structure with suffix_link
Query Iterator/src/transducer/query.rs86-188Main query execution loop
Initial State/src/transducer/transition.rs656-668Wall effect evidence
State Transition/src/transducer/transition.rs591-620Forward-only transition logic
State Pool/src/transducer/state_pool.rs-Memory management
Position/src/transducer/position.rs-State position representation
Algorithm/src/algorithm/-Distance metric implementations

Appendix B: Related Documentation


Document Status: ✅ Complete Last Updated: 2025-11-06 Next Document: decision-matrix.md - Compare implementation options

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close