logicaffeine_language/
registry.rs

1//! Symbol registry for consistent variable naming in FOL output.
2//!
3//! When translating to first-order logic, variables need consistent single-letter
4//! names (x, y, z, etc.). The [`SymbolRegistry`] maps full words to their
5//! assigned variable names and tracks usage counts to generate unique symbols.
6//!
7//! For example, "farmer" might map to "f₁" if "f" is already used.
8
9use std::collections::HashMap;
10use logicaffeine_base::{Interner, Symbol};
11
12/// Registry for mapping words to FOL variable names.
13pub struct SymbolRegistry {
14    mapping: HashMap<String, String>,
15    counters: HashMap<char, usize>,
16}
17
18impl SymbolRegistry {
19    pub fn new() -> Self {
20        SymbolRegistry {
21            mapping: HashMap::new(),
22            counters: HashMap::new(),
23        }
24    }
25
26    pub fn get_symbol_full(&self, sym: Symbol, interner: &Interner) -> String {
27        let word = interner.resolve(sym);
28        let mut chars = word.chars();
29        match chars.next() {
30            Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
31            None => String::new(),
32        }
33    }
34
35    pub fn get_symbol(&mut self, sym: Symbol, interner: &Interner) -> String {
36        let word = interner.resolve(sym);
37        let normalized = word.to_lowercase();
38
39        if let Some(sym) = self.mapping.get(&normalized) {
40            return sym.clone();
41        }
42
43        // For hyphenated compounds (non-intersective adjectives), return full form
44        // "fake-gun" → "Fake-Gun" (not "F")
45        if word.contains('-') {
46            let compound: String = word
47                .split('-')
48                .map(|part| {
49                    let mut chars = part.chars();
50                    match chars.next() {
51                        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
52                        None => String::new(),
53                    }
54                })
55                .collect::<Vec<_>>()
56                .join("-");
57            self.mapping.insert(normalized, compound.clone());
58            return compound;
59        }
60
61        // Preserve specific relational terms (bridging markers)
62        const PRESERVED_TERMS: &[&str] = &["PartOf"];
63        if PRESERVED_TERMS.iter().any(|t| t.eq_ignore_ascii_case(word)) {
64            self.mapping.insert(normalized, word.to_string());
65            return word.to_string();
66        }
67
68        let first = normalized
69            .chars()
70            .next()
71            .unwrap()
72            .to_uppercase()
73            .next()
74            .unwrap();
75
76        let counter = self.counters.entry(first).or_insert(0);
77        *counter += 1;
78
79        let symbol = if *counter == 1 {
80            first.to_string()
81        } else {
82            format!("{}{}", first, counter)
83        };
84
85        self.mapping.insert(normalized, symbol.clone());
86        symbol
87    }
88}
89
90impl Default for SymbolRegistry {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn first_word_gets_single_letter() {
102        let mut interner = Interner::new();
103        let mut reg = SymbolRegistry::new();
104        let dog = interner.intern("dog");
105        assert_eq!(reg.get_symbol(dog, &interner), "D");
106    }
107
108    #[test]
109    fn second_word_same_letter_gets_numbered() {
110        let mut interner = Interner::new();
111        let mut reg = SymbolRegistry::new();
112        let dog = interner.intern("dog");
113        let dangerous = interner.intern("dangerous");
114        reg.get_symbol(dog, &interner);
115        assert_eq!(reg.get_symbol(dangerous, &interner), "D2");
116    }
117
118    #[test]
119    fn same_word_returns_same_symbol() {
120        let mut interner = Interner::new();
121        let mut reg = SymbolRegistry::new();
122        let cat = interner.intern("cat");
123        let first = reg.get_symbol(cat, &interner);
124        let second = reg.get_symbol(cat, &interner);
125        assert_eq!(first, second);
126    }
127
128    #[test]
129    fn case_insensitive() {
130        let mut interner = Interner::new();
131        let mut reg = SymbolRegistry::new();
132        let dog = interner.intern("dog");
133        let dog_upper = interner.intern("DOG");
134        let lower = reg.get_symbol(dog, &interner);
135        let upper = reg.get_symbol(dog_upper, &interner);
136        assert_eq!(lower, upper);
137    }
138}