metrale_grammar/support/
compact_2d_array.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// Compact2DArray — port of `cpp/support/compact_2d_array.h`.
4//
5// A Compressed-Sparse-Row (CSR) 2D array: a sequence of variable-length
6// rows packed end-to-end in one backing buffer. Inserted rows are
7// immutable. Metrale Engine's grammar AST already uses this pattern inline; this
8// is the reusable, standalone version.
9
10use serde::{Deserialize, Serialize};
11
12/// A CSR-style 2D array of `T`.
13///
14/// `data` holds every row's elements contiguously; `indptr` records the
15/// start offset of each row, with a trailing entry equal to
16/// `data.len()`. With `n` rows, `indptr` has `n + 1` entries — so
17/// `indptr` always starts as `[0]`.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct Compact2DArray<T> {
20    /// All row elements packed end-to-end.
21    data: Vec<T>,
22    /// Row start offsets; `indptr[i]..indptr[i + 1]` is row `i`.
23    indptr: Vec<i32>,
24}
25
26impl<T> Default for Compact2DArray<T> {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl<T> Compact2DArray<T> {
33    /// Create an empty array (`indptr` initialized to `[0]`).
34    pub fn new() -> Self {
35        Self {
36            data: Vec::new(),
37            indptr: vec![0],
38        }
39    }
40
41    /// Number of rows.
42    pub fn len(&self) -> i32 {
43        self.indptr.len() as i32 - 1
44    }
45
46    /// Whether there are no rows.
47    pub fn is_empty(&self) -> bool {
48        self.len() == 0
49    }
50
51    /// Borrow row `i` as a slice.
52    ///
53    /// # Panics
54    /// Panics if `i` is out of `0..len()`.
55    pub fn row(&self, i: i32) -> &[T] {
56        assert!(
57            i >= 0 && i < self.len(),
58            "Compact2DArray index {i} is out of bound"
59        );
60        let start = self.indptr[i as usize] as usize;
61        let end = self.indptr[i as usize + 1] as usize;
62        &self.data[start..end]
63    }
64
65    /// Borrow the last row.
66    ///
67    /// # Panics
68    /// Panics if the array has no rows.
69    pub fn back(&self) -> &[T] {
70        assert!(!self.is_empty(), "Compact2DArray is empty");
71        self.row(self.len() - 1)
72    }
73
74    /// Iterate over rows as slices.
75    pub fn iter(&self) -> impl Iterator<Item = &[T]> {
76        (0..self.len()).map(move |i| self.row(i))
77    }
78
79    /// The flat backing data buffer.
80    pub fn data(&self) -> &[T] {
81        &self.data
82    }
83
84    /// The row-offset (`indptr`) array.
85    pub fn indptr(&self) -> &[i32] {
86        &self.indptr
87    }
88
89    /// Append `new_data` as a fresh row; returns its row index.
90    pub fn push_row(&mut self, new_data: &[T]) -> i32
91    where
92        T: Clone,
93    {
94        self.data.extend_from_slice(new_data);
95        self.indptr.push(self.data.len() as i32);
96        self.indptr.len() as i32 - 2
97    }
98
99    /// Append `new_data` as a fresh row by value (no clone); returns
100    /// the row index.
101    pub fn push_row_owned(&mut self, new_data: Vec<T>) -> i32 {
102        self.data.extend(new_data);
103        self.indptr.push(self.data.len() as i32);
104        self.indptr.len() as i32 - 2
105    }
106
107    /// Append a row consisting of `first` followed by `rest`; returns
108    /// the row index. Mirrors C++ `PushBackNonContiguous`, used by the
109    /// grammar AST.
110    pub fn push_row_non_contiguous(&mut self, first: T, rest: &[T]) -> i32
111    where
112        T: Clone,
113    {
114        self.data.push(first);
115        self.data.extend_from_slice(rest);
116        self.indptr.push(self.data.len() as i32);
117        self.indptr.len() as i32 - 2
118    }
119
120    /// Append one element to the most recently inserted row.
121    ///
122    /// # Panics
123    /// Panics if there is no row to append to.
124    pub fn push_in_latest_row(&mut self, new_data: T) {
125        assert!(
126            self.indptr.len() > 1,
127            "Cannot push back in an empty Compact2DArray"
128        );
129        self.data.push(new_data);
130        *self.indptr.last_mut().unwrap() += 1;
131    }
132
133    /// Remove the last `cnt` rows.
134    ///
135    /// # Panics
136    /// Panics if `cnt` exceeds the number of rows.
137    pub fn pop_rows(&mut self, cnt: i32) {
138        assert!(
139            cnt >= 0 && cnt <= self.len(),
140            "Cannot pop {cnt} rows from a Compact2DArray with {} rows",
141            self.len()
142        );
143        let new_indptr_len = self.indptr.len() - cnt as usize;
144        self.indptr.truncate(new_indptr_len);
145        let new_data_len = *self.indptr.last().unwrap() as usize;
146        self.data.truncate(new_data_len);
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn empty_array() {
156        let arr: Compact2DArray<i32> = Compact2DArray::new();
157        assert_eq!(arr.len(), 0);
158        assert!(arr.is_empty());
159        assert_eq!(arr.indptr(), &[0]);
160    }
161
162    #[test]
163    fn push_and_read_rows() {
164        let mut arr = Compact2DArray::new();
165        assert_eq!(arr.push_row(&[1, 2, 3]), 0);
166        assert_eq!(arr.push_row(&[4]), 1);
167        assert_eq!(arr.push_row(&[]), 2);
168        assert_eq!(arr.push_row(&[5, 6]), 3);
169        assert_eq!(arr.len(), 4);
170        assert_eq!(arr.row(0), &[1, 2, 3]);
171        assert_eq!(arr.row(1), &[4]);
172        assert_eq!(arr.row(2), &[] as &[i32]);
173        assert_eq!(arr.row(3), &[5, 6]);
174        assert_eq!(arr.data(), &[1, 2, 3, 4, 5, 6]);
175        assert_eq!(arr.indptr(), &[0, 3, 4, 4, 6]);
176    }
177
178    #[test]
179    fn push_row_owned_matches_push_row() {
180        let mut arr = Compact2DArray::new();
181        arr.push_row_owned(vec![7, 8]);
182        arr.push_row_owned(vec![9]);
183        assert_eq!(arr.row(0), &[7, 8]);
184        assert_eq!(arr.row(1), &[9]);
185    }
186
187    #[test]
188    fn back_returns_last_row() {
189        let mut arr = Compact2DArray::new();
190        arr.push_row(&[1]);
191        arr.push_row(&[2, 3]);
192        assert_eq!(arr.back(), &[2, 3]);
193    }
194
195    #[test]
196    fn push_non_contiguous() {
197        let mut arr = Compact2DArray::new();
198        assert_eq!(arr.push_row_non_contiguous(10, &[20, 30]), 0);
199        assert_eq!(arr.row(0), &[10, 20, 30]);
200        assert_eq!(arr.push_row_non_contiguous(40, &[]), 1);
201        assert_eq!(arr.row(1), &[40]);
202    }
203
204    #[test]
205    fn push_in_latest_row() {
206        let mut arr = Compact2DArray::new();
207        arr.push_row(&[1, 2]);
208        arr.push_in_latest_row(3);
209        arr.push_in_latest_row(4);
210        assert_eq!(arr.row(0), &[1, 2, 3, 4]);
211        assert_eq!(arr.len(), 1);
212    }
213
214    #[test]
215    fn pop_rows() {
216        let mut arr = Compact2DArray::new();
217        arr.push_row(&[1, 2]);
218        arr.push_row(&[3]);
219        arr.push_row(&[4, 5, 6]);
220        arr.pop_rows(2);
221        assert_eq!(arr.len(), 1);
222        assert_eq!(arr.row(0), &[1, 2]);
223        assert_eq!(arr.data(), &[1, 2]);
224        arr.pop_rows(1);
225        assert!(arr.is_empty());
226    }
227
228    #[test]
229    fn iter_yields_all_rows() {
230        let mut arr = Compact2DArray::new();
231        arr.push_row(&[1]);
232        arr.push_row(&[2, 3]);
233        let rows: Vec<Vec<i32>> = arr.iter().map(|r| r.to_vec()).collect();
234        assert_eq!(rows, vec![vec![1], vec![2, 3]]);
235    }
236
237    #[test]
238    #[should_panic(expected = "out of bound")]
239    fn row_out_of_bounds_panics() {
240        let mut arr = Compact2DArray::new();
241        arr.push_row(&[1]);
242        arr.row(1);
243    }
244
245    #[test]
246    #[should_panic(expected = "Cannot push back in an empty")]
247    fn push_in_latest_row_empty_panics() {
248        let mut arr: Compact2DArray<i32> = Compact2DArray::new();
249        arr.push_in_latest_row(1);
250    }
251
252    #[test]
253    fn serde_round_trip() {
254        let mut arr = Compact2DArray::new();
255        arr.push_row(&[1, 2, 3]);
256        arr.push_row(&[4, 5]);
257        let json = serde_json::to_string(&arr).unwrap();
258        let back: Compact2DArray<i32> = serde_json::from_str(&json).unwrap();
259        assert_eq!(arr, back);
260    }
261}