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/// Resolve a 1-based LOGOS index (negative = end-relative: `-1` is the last
70/// element) to a 0-based offset, with the canonical loud errors. ONE
71/// definition — every engine's indexing goes through this rule.
72#[inline(always)]
73pub fn resolve_logos_index(index: i64, len: usize) -> usize {
74    if index >= 1 {
75        let idx = (index - 1) as usize;
76        if idx >= len {
77            panic!("Index {} is out of bounds for seq of length {}", index, len);
78        }
79        idx
80    } else if index <= -1 {
81        let back = index.unsigned_abs() as usize;
82        if back > len {
83            panic!("Index {} is out of bounds for seq of length {}", index, len);
84        }
85        len - back
86    } else {
87        panic!("Index 0 is invalid: LOGOS uses 1-based indexing (minimum is 1, and -1 reads from the end)");
88    }
89}
90
91// === Vec<T> with i64 (1-based indexing) ===
92
93impl<T: Clone> LogosIndex<i64> for Vec<T> {
94    type Output = T;
95
96    #[inline(always)]
97    fn logos_get(&self, index: i64) -> T {
98        let idx = resolve_logos_index(index, self.len());
99        unsafe { self.get_unchecked(idx).clone() }
100    }
101}
102
103impl<T: Clone> LogosIndexMut<i64> for Vec<T> {
104    #[inline(always)]
105    fn logos_set(&mut self, index: i64, value: T) {
106        let idx = resolve_logos_index(index, self.len());
107        unsafe { *self.get_unchecked_mut(idx) = value; }
108    }
109}
110
111// === [T] slice with i64 (1-based indexing, used by &mut [T] borrow params) ===
112
113impl<T: Clone> LogosIndex<i64> for [T] {
114    type Output = T;
115
116    #[inline(always)]
117    fn logos_get(&self, index: i64) -> T {
118        let idx = resolve_logos_index(index, self.len());
119        unsafe { self.get_unchecked(idx).clone() }
120    }
121}
122
123impl<T: Clone> LogosIndexMut<i64> for [T] {
124    #[inline(always)]
125    fn logos_set(&mut self, index: i64, value: T) {
126        let idx = resolve_logos_index(index, self.len());
127        unsafe { *self.get_unchecked_mut(idx) = value; }
128    }
129}
130
131// === &mut [T] with i64 (thin wrapper for UFCS compatibility) ===
132//
133// When the codegen emits `LogosIndex::logos_get(&arr, i)` where `arr: &mut [T]`,
134// the first argument is `&&mut [T]`. Rust doesn't auto-coerce this to `&[T]`
135// in UFCS, so we need an explicit impl that delegates to the `[T]` impl.
136
137impl<T: Clone> LogosIndex<i64> for &mut [T] {
138    type Output = T;
139
140    #[inline(always)]
141    fn logos_get(&self, index: i64) -> T {
142        <[T] as LogosIndex<i64>>::logos_get(self, index)
143    }
144}
145
146impl<T: Clone> LogosIndexMut<i64> for &mut [T] {
147    #[inline(always)]
148    fn logos_set(&mut self, index: i64, value: T) {
149        <[T] as LogosIndexMut<i64>>::logos_set(self, index, value)
150    }
151}
152
153// === String with i64 (1-based character indexing) ===
154
155impl LogosIndex<i64> for String {
156    type Output = String;
157
158    #[inline(always)]
159    fn logos_get(&self, index: i64) -> String {
160        // Positive indexes keep the count-free ASCII fast path; only an
161        // end-relative (or zero) index pays the char count.
162        let idx = if index >= 1 {
163            (index - 1) as usize
164        } else {
165            resolve_logos_index(index, self.chars().count())
166        };
167        match self.as_bytes().get(idx) {
168            Some(&b) if b.is_ascii() => {
169                // Fast path: ASCII byte
170                String::from(b as char)
171            }
172            _ => {
173                // Slow path: Unicode or out of bounds
174                self.chars().nth(idx)
175                    .map(|c| c.to_string())
176                    .unwrap_or_else(|| panic!("Index {} is out of bounds for text of length {}", index, self.chars().count()))
177            }
178        }
179    }
180}
181
182// === String with i64 (1-based character indexing, char return) ===
183
184/// Zero-allocation character access for string comparisons.
185///
186/// Unlike [`LogosIndex`] for `String` which returns a `String`,
187/// this trait returns a `char` — avoiding heap allocation entirely.
188/// Used by the codegen optimizer for string-index-vs-string-index comparisons.
189pub trait LogosGetChar {
190    fn logos_get_char(&self, index: i64) -> char;
191}
192
193impl LogosGetChar for String {
194    #[inline(always)]
195    fn logos_get_char(&self, index: i64) -> char {
196        let idx = if index >= 1 {
197            (index - 1) as usize
198        } else {
199            resolve_logos_index(index, self.chars().count())
200        };
201        match self.as_bytes().get(idx) {
202            Some(&b) if b.is_ascii() => b as char,
203            _ => {
204                self.chars().nth(idx)
205                    .unwrap_or_else(|| panic!(
206                        "Index {} is out of bounds for text of length {}",
207                        index, self.chars().count()
208                    ))
209            }
210        }
211    }
212}
213
214// === LogosSeq<T> with i64 (1-based indexing, reference semantics) ===
215
216impl<T: Clone> LogosIndex<i64> for crate::types::LogosSeq<T> {
217    type Output = T;
218
219    #[inline(always)]
220    fn logos_get(&self, index: i64) -> T {
221        let inner = self.borrow();
222        <Vec<T> as LogosIndex<i64>>::logos_get(&*inner, index)
223    }
224}
225
226impl<T: Clone> LogosIndexMut<i64> for crate::types::LogosSeq<T> {
227    #[inline(always)]
228    fn logos_set(&mut self, index: i64, value: T) {
229        let mut inner = self.borrow_mut();
230        <Vec<T> as LogosIndexMut<i64>>::logos_set(&mut *inner, index, value)
231    }
232}
233
234// === LogosMap<K, V> with K (key-based indexing, reference semantics) ===
235
236impl<K: Eq + Hash, V: Clone> LogosIndex<K> for crate::types::LogosMap<K, V> {
237    type Output = V;
238
239    #[inline(always)]
240    fn logos_get(&self, key: K) -> V {
241        let inner = self.borrow();
242        inner.get(&key).cloned().expect("Key not found in map")
243    }
244}
245
246impl<K: Eq + Hash, V: Clone> LogosIndexMut<K> for crate::types::LogosMap<K, V> {
247    #[inline(always)]
248    fn logos_set(&mut self, key: K, value: V) {
249        self.insert(key, value);
250    }
251}
252
253// === &str convenience for LogosMap<String, V> ===
254
255impl<V: Clone> LogosIndex<&str> for crate::types::LogosMap<String, V> {
256    type Output = V;
257
258    #[inline(always)]
259    fn logos_get(&self, key: &str) -> V {
260        let inner = self.borrow();
261        inner.get(key).cloned().expect("Key not found in map")
262    }
263}
264
265impl<V: Clone> LogosIndexMut<&str> for crate::types::LogosMap<String, V> {
266    #[inline(always)]
267    fn logos_set(&mut self, key: &str, value: V) {
268        self.insert(key.to_string(), value);
269    }
270}
271
272// === HashMap<K, V> with K (key-based indexing) ===
273
274impl<K: Eq + Hash, V: Clone> LogosIndex<K> for FxHashMap<K, V> {
275    type Output = V;
276
277    #[inline(always)]
278    fn logos_get(&self, key: K) -> V {
279        self.get(&key).cloned().expect("Key not found in map")
280    }
281}
282
283impl<K: Eq + Hash, V: Clone> LogosIndexMut<K> for FxHashMap<K, V> {
284    #[inline(always)]
285    fn logos_set(&mut self, key: K, value: V) {
286        self.insert(key, value);
287    }
288}
289
290// === &str convenience for HashMap<String, V> ===
291
292impl<V: Clone> LogosIndex<&str> for FxHashMap<String, V> {
293    type Output = V;
294
295    #[inline(always)]
296    fn logos_get(&self, key: &str) -> V {
297        self.get(key).cloned().expect("Key not found in map")
298    }
299}
300
301impl<V: Clone> LogosIndexMut<&str> for FxHashMap<String, V> {
302    #[inline(always)]
303    fn logos_set(&mut self, key: &str, value: V) {
304        self.insert(key.to_string(), value);
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn vec_1_based_indexing() {
314        let v = vec![10, 20, 30];
315        assert_eq!(LogosIndex::logos_get(&v, 1i64), 10);
316        assert_eq!(LogosIndex::logos_get(&v, 2i64), 20);
317        assert_eq!(LogosIndex::logos_get(&v, 3i64), 30);
318    }
319
320    #[test]
321    #[should_panic(expected = "1-based indexing")]
322    fn vec_zero_index_panics() {
323        let v = vec![10, 20, 30];
324        let _ = LogosIndex::logos_get(&v, 0i64);
325    }
326
327    #[test]
328    fn vec_set_1_based() {
329        let mut v = vec![10, 20, 30];
330        LogosIndexMut::logos_set(&mut v, 2i64, 99);
331        assert_eq!(v, vec![10, 99, 30]);
332    }
333
334    #[test]
335    fn hashmap_string_key() {
336        let mut m: FxHashMap<String, i64> = FxHashMap::default();
337        m.insert("iron".to_string(), 42);
338        assert_eq!(LogosIndex::logos_get(&m, "iron".to_string()), 42);
339    }
340
341    #[test]
342    fn hashmap_str_key() {
343        let mut m: FxHashMap<String, i64> = FxHashMap::default();
344        m.insert("iron".to_string(), 42);
345        assert_eq!(LogosIndex::logos_get(&m, "iron"), 42);
346    }
347
348    #[test]
349    fn hashmap_set_key() {
350        let mut m: FxHashMap<String, i64> = FxHashMap::default();
351        LogosIndexMut::logos_set(&mut m, "iron", 42i64);
352        assert_eq!(m.get("iron"), Some(&42));
353    }
354}