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// === HashMap<K, V> with K (key-based indexing) ===
213
214impl<K: Eq + Hash, V: Clone> LogosIndex<K> for FxHashMap<K, V> {
215    type Output = V;
216
217    #[inline(always)]
218    fn logos_get(&self, key: K) -> V {
219        self.get(&key).cloned().expect("Key not found in map")
220    }
221}
222
223impl<K: Eq + Hash, V: Clone> LogosIndexMut<K> for FxHashMap<K, V> {
224    #[inline(always)]
225    fn logos_set(&mut self, key: K, value: V) {
226        self.insert(key, value);
227    }
228}
229
230// === &str convenience for HashMap<String, V> ===
231
232impl<V: Clone> LogosIndex<&str> for FxHashMap<String, V> {
233    type Output = V;
234
235    #[inline(always)]
236    fn logos_get(&self, key: &str) -> V {
237        self.get(key).cloned().expect("Key not found in map")
238    }
239}
240
241impl<V: Clone> LogosIndexMut<&str> for FxHashMap<String, V> {
242    #[inline(always)]
243    fn logos_set(&mut self, key: &str, value: V) {
244        self.insert(key.to_string(), value);
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn vec_1_based_indexing() {
254        let v = vec![10, 20, 30];
255        assert_eq!(LogosIndex::logos_get(&v, 1i64), 10);
256        assert_eq!(LogosIndex::logos_get(&v, 2i64), 20);
257        assert_eq!(LogosIndex::logos_get(&v, 3i64), 30);
258    }
259
260    #[test]
261    #[should_panic(expected = "1-based indexing")]
262    fn vec_zero_index_panics() {
263        let v = vec![10, 20, 30];
264        let _ = LogosIndex::logos_get(&v, 0i64);
265    }
266
267    #[test]
268    fn vec_set_1_based() {
269        let mut v = vec![10, 20, 30];
270        LogosIndexMut::logos_set(&mut v, 2i64, 99);
271        assert_eq!(v, vec![10, 99, 30]);
272    }
273
274    #[test]
275    fn hashmap_string_key() {
276        let mut m: FxHashMap<String, i64> = FxHashMap::default();
277        m.insert("iron".to_string(), 42);
278        assert_eq!(LogosIndex::logos_get(&m, "iron".to_string()), 42);
279    }
280
281    #[test]
282    fn hashmap_str_key() {
283        let mut m: FxHashMap<String, i64> = FxHashMap::default();
284        m.insert("iron".to_string(), 42);
285        assert_eq!(LogosIndex::logos_get(&m, "iron"), 42);
286    }
287
288    #[test]
289    fn hashmap_set_key() {
290        let mut m: FxHashMap<String, i64> = FxHashMap::default();
291        LogosIndexMut::logos_set(&mut m, "iron", 42i64);
292        assert_eq!(m.get("iron"), Some(&42));
293    }
294}