Skip to main content

logicaffeine_data/
indexing.rs

1//! Polymorphic indexing traits for Logos collections.
2//!
3//! Logos uses **1-based indexing** to match natural language conventions.
4//! These traits provide get/set operations that automatically convert
5//! 1-based indices to 0-based for underlying Rust collections.
6//!
7//! # Supported Collections
8//!
9//! - [`Vec<T>`]: Indexed by `i64` (1-based, converted to 0-based internally)
10//! - [`HashMap<K, V>`]: Indexed by key `K` (pass-through semantics)
11//! - [`HashMap<String, V>`]: Also supports `&str` keys for convenience
12//!
13//! # Panics
14//!
15//! Vector indexing operations panic if the index is out of bounds
16//! (less than 1 or greater than collection length). Map operations
17//! panic if the key is not found.
18
19use rustc_hash::FxHashMap;
20use std::hash::Hash;
21
22/// Immutable element access by index.
23///
24/// Provides 1-based indexing for Logos collections. Index `1` refers
25/// to the first element, index `2` to the second, and so on.
26///
27/// # Examples
28///
29/// ```
30/// use logicaffeine_data::LogosIndex;
31///
32/// let v = vec!["a", "b", "c"];
33/// assert_eq!(v.logos_get(1i64), "a");  // 1-based!
34/// assert_eq!(v.logos_get(3i64), "c");
35/// ```
36///
37/// # Panics
38///
39/// Panics if the index is less than 1 or greater than the collection length.
40pub trait LogosIndex<I> {
41    /// The type of element returned by indexing.
42    type Output;
43    /// Get the element at the given index.
44    fn logos_get(&self, index: I) -> Self::Output;
45}
46
47/// Mutable element access by index.
48///
49/// Provides 1-based mutable indexing for Logos collections.
50///
51/// # Examples
52///
53/// ```
54/// use logicaffeine_data::LogosIndexMut;
55///
56/// let mut v = vec![1, 2, 3];
57/// v.logos_set(2i64, 20);
58/// assert_eq!(v, vec![1, 20, 3]);
59/// ```
60///
61/// # Panics
62///
63/// Panics if the index is less than 1 or greater than the collection length.
64pub trait LogosIndexMut<I>: LogosIndex<I> {
65    /// Set the element at the given index.
66    fn logos_set(&mut self, index: I, value: Self::Output);
67}
68
69// === Vec<T> with i64 (1-based indexing) ===
70
71impl<T: Clone> LogosIndex<i64> for Vec<T> {
72    type Output = T;
73
74    #[inline(always)]
75    fn logos_get(&self, index: i64) -> T {
76        if index < 1 {
77            panic!("Index {} is invalid: LOGOS uses 1-based indexing (minimum is 1)", index);
78        }
79        let idx = (index - 1) as usize;
80        if idx >= self.len() {
81            panic!("Index {} is out of bounds for seq of length {}", index, self.len());
82        }
83        unsafe { self.get_unchecked(idx).clone() }
84    }
85}
86
87impl<T: Clone> LogosIndexMut<i64> for Vec<T> {
88    #[inline(always)]
89    fn logos_set(&mut self, index: i64, value: T) {
90        if index < 1 {
91            panic!("Index {} is invalid: LOGOS uses 1-based indexing (minimum is 1)", index);
92        }
93        let idx = (index - 1) as usize;
94        if idx >= self.len() {
95            panic!("Index {} is out of bounds for seq of length {}", index, self.len());
96        }
97        unsafe { *self.get_unchecked_mut(idx) = value; }
98    }
99}
100
101// === [T] slice with i64 (1-based indexing, used by &mut [T] borrow params) ===
102
103impl<T: Clone> LogosIndex<i64> for [T] {
104    type Output = T;
105
106    #[inline(always)]
107    fn logos_get(&self, index: i64) -> T {
108        if index < 1 {
109            panic!("Index {} is invalid: LOGOS uses 1-based indexing (minimum is 1)", index);
110        }
111        let idx = (index - 1) as usize;
112        if idx >= self.len() {
113            panic!("Index {} is out of bounds for seq of length {}", index, self.len());
114        }
115        unsafe { self.get_unchecked(idx).clone() }
116    }
117}
118
119impl<T: Clone> LogosIndexMut<i64> for [T] {
120    #[inline(always)]
121    fn logos_set(&mut self, index: i64, value: T) {
122        if index < 1 {
123            panic!("Index {} is invalid: LOGOS uses 1-based indexing (minimum is 1)", index);
124        }
125        let idx = (index - 1) as usize;
126        if idx >= self.len() {
127            panic!("Index {} is out of bounds for seq of length {}", index, self.len());
128        }
129        unsafe { *self.get_unchecked_mut(idx) = value; }
130    }
131}
132
133// === &mut [T] with i64 (thin wrapper for UFCS compatibility) ===
134//
135// When the codegen emits `LogosIndex::logos_get(&arr, i)` where `arr: &mut [T]`,
136// the first argument is `&&mut [T]`. Rust doesn't auto-coerce this to `&[T]`
137// in UFCS, so we need an explicit impl that delegates to the `[T]` impl.
138
139impl<T: Clone> LogosIndex<i64> for &mut [T] {
140    type Output = T;
141
142    #[inline(always)]
143    fn logos_get(&self, index: i64) -> T {
144        <[T] as LogosIndex<i64>>::logos_get(self, index)
145    }
146}
147
148impl<T: Clone> LogosIndexMut<i64> for &mut [T] {
149    #[inline(always)]
150    fn logos_set(&mut self, index: i64, value: T) {
151        <[T] as LogosIndexMut<i64>>::logos_set(self, index, value)
152    }
153}
154
155// === String with i64 (1-based character indexing) ===
156
157impl LogosIndex<i64> for String {
158    type Output = String;
159
160    #[inline(always)]
161    fn logos_get(&self, index: i64) -> String {
162        if index < 1 {
163            panic!("Index {} is invalid: LOGOS uses 1-based indexing (minimum is 1)", index);
164        }
165        let idx = (index - 1) as usize;
166        match self.as_bytes().get(idx) {
167            Some(&b) if b.is_ascii() => {
168                // Fast path: ASCII byte
169                String::from(b as char)
170            }
171            _ => {
172                // Slow path: Unicode or out of bounds
173                self.chars().nth(idx)
174                    .map(|c| c.to_string())
175                    .unwrap_or_else(|| panic!("Index {} is out of bounds for text of length {}", index, self.chars().count()))
176            }
177        }
178    }
179}
180
181// === String with i64 (1-based character indexing, char return) ===
182
183/// Zero-allocation character access for string comparisons.
184///
185/// Unlike [`LogosIndex`] for `String` which returns a `String`,
186/// this trait returns a `char` — avoiding heap allocation entirely.
187/// Used by the codegen optimizer for string-index-vs-string-index comparisons.
188pub trait LogosGetChar {
189    fn logos_get_char(&self, index: i64) -> char;
190}
191
192impl LogosGetChar for String {
193    #[inline(always)]
194    fn logos_get_char(&self, index: i64) -> char {
195        if index < 1 {
196            panic!("Index {} is invalid: LOGOS uses 1-based indexing (minimum is 1)", index);
197        }
198        let idx = (index - 1) as usize;
199        match self.as_bytes().get(idx) {
200            Some(&b) if b.is_ascii() => b as char,
201            _ => {
202                self.chars().nth(idx)
203                    .unwrap_or_else(|| panic!(
204                        "Index {} is out of bounds for text of length {}",
205                        index, self.chars().count()
206                    ))
207            }
208        }
209    }
210}
211
212// === LogosSeq<T> with i64 (1-based indexing, reference semantics) ===
213
214impl<T: Clone> LogosIndex<i64> for crate::types::LogosSeq<T> {
215    type Output = T;
216
217    #[inline(always)]
218    fn logos_get(&self, index: i64) -> T {
219        let inner = self.borrow();
220        <Vec<T> as LogosIndex<i64>>::logos_get(&*inner, index)
221    }
222}
223
224impl<T: Clone> LogosIndexMut<i64> for crate::types::LogosSeq<T> {
225    #[inline(always)]
226    fn logos_set(&mut self, index: i64, value: T) {
227        let mut inner = self.borrow_mut();
228        <Vec<T> as LogosIndexMut<i64>>::logos_set(&mut *inner, index, value)
229    }
230}
231
232// === LogosMap<K, V> with K (key-based indexing, reference semantics) ===
233
234impl<K: Eq + Hash, V: Clone> LogosIndex<K> for crate::types::LogosMap<K, V> {
235    type Output = V;
236
237    #[inline(always)]
238    fn logos_get(&self, key: K) -> V {
239        let inner = self.borrow();
240        inner.get(&key).cloned().expect("Key not found in map")
241    }
242}
243
244impl<K: Eq + Hash, V: Clone> LogosIndexMut<K> for crate::types::LogosMap<K, V> {
245    #[inline(always)]
246    fn logos_set(&mut self, key: K, value: V) {
247        self.insert(key, value);
248    }
249}
250
251// === &str convenience for LogosMap<String, V> ===
252
253impl<V: Clone> LogosIndex<&str> for crate::types::LogosMap<String, V> {
254    type Output = V;
255
256    #[inline(always)]
257    fn logos_get(&self, key: &str) -> V {
258        let inner = self.borrow();
259        inner.get(key).cloned().expect("Key not found in map")
260    }
261}
262
263impl<V: Clone> LogosIndexMut<&str> for crate::types::LogosMap<String, V> {
264    #[inline(always)]
265    fn logos_set(&mut self, key: &str, value: V) {
266        self.insert(key.to_string(), value);
267    }
268}
269
270// === HashMap<K, V> with K (key-based indexing) ===
271
272impl<K: Eq + Hash, V: Clone> LogosIndex<K> for FxHashMap<K, V> {
273    type Output = V;
274
275    #[inline(always)]
276    fn logos_get(&self, key: K) -> V {
277        self.get(&key).cloned().expect("Key not found in map")
278    }
279}
280
281impl<K: Eq + Hash, V: Clone> LogosIndexMut<K> for FxHashMap<K, V> {
282    #[inline(always)]
283    fn logos_set(&mut self, key: K, value: V) {
284        self.insert(key, value);
285    }
286}
287
288// === &str convenience for HashMap<String, V> ===
289
290impl<V: Clone> LogosIndex<&str> for FxHashMap<String, V> {
291    type Output = V;
292
293    #[inline(always)]
294    fn logos_get(&self, key: &str) -> V {
295        self.get(key).cloned().expect("Key not found in map")
296    }
297}
298
299impl<V: Clone> LogosIndexMut<&str> for FxHashMap<String, V> {
300    #[inline(always)]
301    fn logos_set(&mut self, key: &str, value: V) {
302        self.insert(key.to_string(), value);
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn vec_1_based_indexing() {
312        let v = vec![10, 20, 30];
313        assert_eq!(LogosIndex::logos_get(&v, 1i64), 10);
314        assert_eq!(LogosIndex::logos_get(&v, 2i64), 20);
315        assert_eq!(LogosIndex::logos_get(&v, 3i64), 30);
316    }
317
318    #[test]
319    #[should_panic(expected = "1-based indexing")]
320    fn vec_zero_index_panics() {
321        let v = vec![10, 20, 30];
322        let _ = LogosIndex::logos_get(&v, 0i64);
323    }
324
325    #[test]
326    fn vec_set_1_based() {
327        let mut v = vec![10, 20, 30];
328        LogosIndexMut::logos_set(&mut v, 2i64, 99);
329        assert_eq!(v, vec![10, 99, 30]);
330    }
331
332    #[test]
333    fn hashmap_string_key() {
334        let mut m: FxHashMap<String, i64> = FxHashMap::default();
335        m.insert("iron".to_string(), 42);
336        assert_eq!(LogosIndex::logos_get(&m, "iron".to_string()), 42);
337    }
338
339    #[test]
340    fn hashmap_str_key() {
341        let mut m: FxHashMap<String, i64> = FxHashMap::default();
342        m.insert("iron".to_string(), 42);
343        assert_eq!(LogosIndex::logos_get(&m, "iron"), 42);
344    }
345
346    #[test]
347    fn hashmap_set_key() {
348        let mut m: FxHashMap<String, i64> = FxHashMap::default();
349        LogosIndexMut::logos_set(&mut m, "iron", 42i64);
350        assert_eq!(m.get("iron"), Some(&42));
351    }
352}