Liking cljdoc? Tell your friends :D

Phase 6: Dictionary Layer Completeness - Status Report

Objective

Ensure all dictionary backends support MappedDictionary (value storage) and ValuedDictZipper (hierarchical navigation with values) to provide complete layer support across the library.

Summary

Phase 6 is 100% complete! 🎉

As of 2025-11-11, comprehensive verification and analysis revealed:

  • MappedDictionary: 9/9 complete (100%) ✅
  • ValuedDictZipper: 7/7 complete (100%) ✅
  • OptimizedDawg: DEPRECATED (11× slower than DynamicDawg)

All production-ready backends now support the complete feature set. OptimizedDawg was deprecated after benchmarking showed DynamicDawg provides superior performance (11× faster construction) with full feature support.

Current Status

Last Updated: 2025-11-11

MappedDictionary Support

Implemented (9/10): 🎉

  • ✅ PathMapDictionary
  • ✅ PathMapDictionaryChar
  • ✅ DynamicDawg
  • ✅ DynamicDawgChar
  • DoubleArrayTrie (verified 2025-11-11)
  • DoubleArrayTrieChar (verified 2025-11-11)
  • SuffixAutomaton (verified 2025-11-11)
  • SuffixAutomatonChar (verified 2025-11-11)
  • ✅ DawgDictionary (legacy, has MappedDictionary)

Deprecated/Skipped (2/10):

  • 🚫 OptimizedDawg (DEPRECATED in 0.7.0 - 11× slower than DynamicDawg, use DynamicDawg instead)
  • ~~ CompressedSuffixAutomaton (SKIP - deprecated)

ValuedDictZipper Support

Implemented (7/7): 🎉 COMPLETE!

  • ✅ PathMapZipper
  • DoubleArrayTrieZipper (verified 2025-11-11)
  • DoubleArrayTrieCharZipper (verified 2025-11-11)
  • DynamicDawgZipper (verified 2025-11-11)
  • DynamicDawgCharZipper (verified 2025-11-11)
  • SuffixAutomatonZipper (verified 2025-11-11)
  • SuffixAutomatonCharZipper (verified 2025-11-11)

Not Applicable:

  • OptimizedDawgZipper (OptimizedDawg doesn't have a zipper implementation)
  • DawgDictionaryZipper (legacy backend, no zipper)

Implementation Plan: DoubleArrayTrie (Step 1)

1. Add Generic Type Parameter

Current:

pub struct DoubleArrayTrie {
    shared: DATShared,
    // ...
}

Target:

pub struct DoubleArrayTrie<V: DictionaryValue = ()> {
    shared: DATShared<V>,
    // ...
}

Changes Required:

  • Add type parameter to DoubleArrayTrie
  • Add type parameter to DATShared
  • Add type parameter to DoubleArrayTrieBuilder
  • Add type parameter to DoubleArrayTrieNode
  • Update all trait implementations
  • Maintain backward compatibility with default V = ()

2. Add Value Storage to DATShared

Current Structure:

struct DATShared {
    base: Arc<Vec<i32>>,
    check: Arc<Vec<i32>>,
    is_final: Arc<Vec<bool>>,
    edges: Arc<Vec<Vec<u8>>>,
}

Target Structure:

struct DATShared<V: DictionaryValue> {
    base: Arc<Vec<i32>>,
    check: Arc<Vec<i32>>,
    is_final: Arc<Vec<bool>>,
    edges: Arc<Vec<Vec<u8>>>,
    values: Arc<Vec<Option<V>>>,  // NEW: indexed by state
}

Design Decision:

  • Use Vec<Option<V>> indexed by state number
  • Only final states can have values (Some(v))
  • Non-final states have None
  • Parallel to is_final array

3. Update DoubleArrayTrieBuilder

Add Fields:

pub struct DoubleArrayTrieBuilder<V: DictionaryValue = ()> {
    base: Vec<i32>,
    check: Vec<i32>,
    is_final: Vec<bool>,
    values: Vec<Option<V>>,  // NEW
    // ... existing fields
}

Add Methods:

impl<V: DictionaryValue> DoubleArrayTrieBuilder<V> {
    // Keep existing insert() for backward compatibility
    pub fn insert(&mut self, term: &str) -> bool {
        self.insert_with_value(term, None)
    }

    // NEW: Insert with optional value
    pub fn insert_with_value(&mut self, term: &str, value: Option<V>) -> bool {
        // Same logic as current insert()
        // But also store value at final state
        // ...
        if is_new_term {
            while state >= self.values.len() {
                self.values.push(None);
            }
            self.values[state] = value;
            true
        } else {
            false
        }
    }
}

Update build():

pub fn build(self) -> DoubleArrayTrie<V> {
    // ... existing edge computation ...

    DoubleArrayTrie {
        shared: DATShared {
            base: Arc::new(self.base),
            check: Arc::new(self.check),
            is_final: Arc::new(self.is_final),
            edges: Arc::new(edges),
            values: Arc::new(self.values),  // NEW
        },
        // ...
    }
}

4. Add from_terms_with_values() Method

impl<V: DictionaryValue> DoubleArrayTrie<V> {
    /// Create a DAT from an iterator of (term, value) pairs.
    pub fn from_terms_with_values<I, S>(terms: I) -> Self
    where
        I: IntoIterator<Item = (S, V)>,
        S: AsRef<str>,
    {
        let mut term_value_pairs: Vec<(String, V)> = terms
            .into_iter()
            .map(|(s, v)| (s.as_ref().to_string(), v))
            .collect();

        // Sort by term
        term_value_pairs.sort_by(|a, b| a.0.cmp(&b.0));

        // Remove duplicates (keep last value)
        term_value_pairs.dedup_by(|a, b| {
            if a.0 == b.0 {
                b.1 = a.1.clone();  // Keep most recent value
                true
            } else {
                false
            }
        });

        let mut builder = DoubleArrayTrieBuilder::new();
        for (term, value) in term_value_pairs {
            builder.insert_with_value(&term, Some(value));
        }
        builder.build()
    }
}

5. Implement MappedDictionary Trait

impl<V: DictionaryValue> MappedDictionary for DoubleArrayTrie<V> {
    type Value = V;

    fn get_value(&self, term: &str) -> Option<Self::Value> {
        // Navigate to final state
        let mut state = 1; // Root
        for &byte in term.as_bytes() {
            let base = self.shared.base[state];
            if base < 0 {
                return None;
            }
            let next = (base as usize) + (byte as usize);
            if next >= self.shared.check.len()
                || self.shared.check[next] != state as i32 {
                return None;
            }
            state = next;
        }

        // Check if final and return value
        if state < self.shared.is_final.len() && self.shared.is_final[state] {
            self.shared.values.get(state).and_then(|v| v.clone())
        } else {
            None
        }
    }

    fn contains_with_value<F>(&self, term: &str, predicate: F) -> bool
    where
        F: Fn(&Self::Value) -> bool,
    {
        match self.get_value(term) {
            Some(ref value) => predicate(value),
            None => false,
        }
    }
}

6. Implement MappedDictionaryNode Trait

impl<V: DictionaryValue> MappedDictionaryNode for DoubleArrayTrieNode<V> {
    type Value = V;

    fn value(&self) -> Option<Self::Value> {
        if self.state < self.shared.values.len() {
            self.shared.values[self.state].clone()
        } else {
            None
        }
    }
}

7. Update Serialization

Ensure values field is included in serialization:

#[cfg_attr(
    feature = "serialization",
    derive(serde::Serialize, serde::Deserialize)
)]
struct DATShared<V: DictionaryValue> {
    #[cfg_attr(
        feature = "serialization",
        serde(
            serialize_with = "serialize_arc_vec",
            deserialize_with = "deserialize_arc_vec"
        )
    )]
    values: Arc<Vec<Option<V>>>,
    // ... other fields
}

8. Create DoubleArrayTrieZipper

File: src/dictionary/double_array_trie_zipper.rs

use crate::dictionary::double_array_trie::{DoubleArrayTrie, DATShared};
use crate::dictionary::value::DictionaryValue;
use crate::dictionary::zipper::{DictZipper, ValuedDictZipper};
use std::sync::Arc;

/// Zipper for navigating DoubleArrayTrie structures.
#[derive(Clone, Debug)]
pub struct DoubleArrayTrieZipper<V: DictionaryValue = ()> {
    /// Current state index
    state: usize,

    /// Shared DAT data
    shared: Arc<DATShared<V>>,
}

impl<V: DictionaryValue> DoubleArrayTrieZipper<V> {
    /// Create a new zipper at the root of the dictionary.
    pub fn new_from_dict(dict: &DoubleArrayTrie<V>) -> Self {
        Self {
            state: 1, // Root is state 1
            shared: Arc::clone(&dict.shared),
        }
    }

    /// Get the current state index.
    pub fn state(&self) -> usize {
        self.state
    }
}

impl<V: DictionaryValue> DictZipper for DoubleArrayTrieZipper<V> {
    type Unit = u8;

    fn is_final(&self) -> bool {
        self.state < self.shared.is_final.len()
            && self.shared.is_final[self.state]
    }

    fn descend(&self, label: Self::Unit) -> Option<Self> {
        if self.state >= self.shared.base.len() {
            return None;
        }

        let base = self.shared.base[self.state];
        if base < 0 {
            return None;
        }

        let next = (base as usize) + (label as usize);
        if next >= self.shared.check.len()
            || self.shared.check[next] != self.state as i32 {
            return None;
        }

        Some(Self {
            state: next,
            shared: Arc::clone(&self.shared),
        })
    }

    fn children(&self) -> impl Iterator<Item = (Self::Unit, Self)> + '_ {
        // Use precomputed edge list for efficiency
        let edges = if self.state < self.shared.edges.len() {
            &self.shared.edges[self.state]
        } else {
            &[]
        };

        edges.iter().filter_map(move |&byte| {
            self.descend(byte).map(|child| (byte, child))
        })
    }
}

impl<V: DictionaryValue> ValuedDictZipper for DoubleArrayTrieZipper<V> {
    type Value = V;

    fn value(&self) -> Option<Self::Value> {
        if self.is_final() && self.state < self.shared.values.len() {
            self.shared.values[self.state].clone()
        } else {
            None
        }
    }
}

9. Update Module Exports

In src/dictionary/mod.rs:

pub mod double_array_trie_zipper;
pub use double_array_trie_zipper::DoubleArrayTrieZipper;

10. Add Tests

#[cfg(test)]
mod tests {
    use super::*;
    use crate::dictionary::{MappedDictionary, MutableMappedDictionary};

    #[test]
    fn test_double_array_trie_with_values() {
        let terms = vec![
            ("apple", 1),
            ("application", 2),
            ("apply", 3),
        ];

        let dict = DoubleArrayTrie::from_terms_with_values(terms);

        assert_eq!(dict.get_value("apple"), Some(1));
        assert_eq!(dict.get_value("application"), Some(2));
        assert_eq!(dict.get_value("apply"), Some(3));
        assert_eq!(dict.get_value("apricot"), None);
    }

    #[test]
    fn test_contains_with_value() {
        let dict = DoubleArrayTrie::from_terms_with_values(vec![
            ("test", 42),
            ("testing", 100),
        ]);

        assert!(dict.contains_with_value("test", |v| *v == 42));
        assert!(dict.contains_with_value("testing", |v| *v > 50));
        assert!(!dict.contains_with_value("test", |v| *v > 50));
    }

    #[test]
    fn test_zipper_with_values() {
        use crate::dictionary::zipper::{DictZipper, ValuedDictZipper};

        let dict = DoubleArrayTrie::from_terms_with_values(vec![
            ("cat", 1),
            ("catch", 2),
        ]);

        let zipper = DoubleArrayTrieZipper::new_from_dict(&dict);

        // Navigate to "cat"
        let z = zipper.descend(b'c')
            .and_then(|z| z.descend(b'a'))
            .and_then(|z| z.descend(b't'))
            .unwrap();

        assert!(z.is_final());
        assert_eq!(z.value(), Some(1));

        // Continue to "catch"
        let z = z.descend(b'c')
            .and_then(|z| z.descend(b'h'))
            .unwrap();

        assert!(z.is_final());
        assert_eq!(z.value(), Some(2));
    }

    #[test]
    fn test_backward_compatibility() {
        // Default type parameter should be ()
        let dict: DoubleArrayTrie = DoubleArrayTrie::from_terms(vec!["test", "testing"]);

        assert!(dict.contains("test"));
        assert_eq!(dict.len(), Some(2));
    }
}

Implementation Order

  1. Planning (current)
  2. ⏭️ DoubleArrayTrie refactoring (~2-3 hours)
    • Add generic type parameter
    • Add value storage
    • Implement MappedDictionary
  3. ⏭️ DoubleArrayTrieZipper (~1 hour)
    • Create zipper module
    • Implement DictZipper
    • Implement ValuedDictZipper
  4. ⏭️ Testing (~1 hour)
    • Unit tests
    • Integration tests
    • Backward compatibility tests
  5. ⏭️ Commit
  6. ⏭️ Repeat for DoubleArrayTrieChar (~2 hours - similar pattern)
  7. ⏭️ Repeat for remaining dictionaries (~1-2 hours each)

Estimated Timeline

  • DoubleArrayTrie + Zipper: 4-5 hours
  • DoubleArrayTrieChar + Zipper: 2-3 hours
  • OptimizedDawg + Zipper: 2-3 hours
  • SuffixAutomaton: 2 hours
  • DawgDictionary: 2 hours
  • DynamicDawg/Char Zippers: 2-3 hours
  • Factory updates: 1 hour
  • Documentation: 1 hour

Total estimate: 16-20 hours over 2-3 weeks

Success Criteria

  • [ ] All 6 dictionaries implement MappedDictionary
  • [ ] All 5 priority dictionaries have ValuedDictZipper implementations
  • [ ] All tests pass
  • [ ] Backward compatibility maintained (default V = ())
  • [ ] Serialization works with values
  • [ ] Documentation updated
  • [ ] Factory supports new features

Notes

  • Maintain backward compatibility with V = () default
  • Follow existing patterns from DynamicDawg and PathMapZipper
  • Update serialization to handle generic values
  • Add comprehensive tests for value operations
  • Consider performance implications of cloning values

What's Next: Advanced Research Initiatives

Phase 6 is complete! All production backends now support full feature sets (MappedDictionary + ValuedDictZipper).

Future Optimizations

With the dictionary layer complete, three research initiatives have been identified for advanced optimization:

  1. SIMD Edge Search (2-4 weeks) - Extract SIMD optimization from deprecated OptimizedDawg
  2. Hybrid Storage (4-6 weeks) - Arena/per-node hybrid for immutable dictionaries
  3. DAT/DAWG Hybrid (8-12 weeks) - Novel BASE+CHECK + suffix sharing algorithm

See: Research Initiatives for detailed plans and methodology.

These represent substantial multi-week research projects with phased approaches and early exit criteria based on empirical data. Each follows the scientific method with hypothesis formation, controlled experiments, and data-driven decisions.

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