parlex_calc/symtab.rs
1//! A minimal, flat symbol table built on [`indexmap::IndexMap`].
2//!
3//! This table assigns each unique string name a stable integer index.
4//! You can use it to *intern* symbols (insert if missing and get index)
5//! and later access their values by index.
6//!
7//! ## Example
8//! ```rust
9//! # use parlex_calc::SymTab;
10//! let mut st = SymTab::new();
11//! let i = st.intern("foo"); // inserts "foo" at index 0
12//! assert_eq!(st.get(i).unwrap(), 0);
13//! st.set(i, 42).unwrap();
14//! assert_eq!(st.get(i).unwrap(), 42);
15//! assert_eq!(st.intern("foo"), i); // same index, not reinserted
16//! ```
17
18use indexmap::{IndexMap, map::Entry};
19use smartstring::alias::String;
20use thiserror::Error;
21
22/// Errors that can occur when operating on a [`SymTab`].
23#[derive(Debug, Error)]
24pub enum SymTabError {
25 /// Attempted to access an invalid index (out of bounds).
26 #[error("invalid symbol index {index} (table length {len})")]
27 InvalidIndex {
28 /// The index that was requested.
29 index: usize,
30 /// The number of entries currently in the table.
31 len: usize,
32 },
33}
34
35/// A simple symbol table that maps string names to integer values.
36///
37/// Each inserted name receives a stable index corresponding to its insertion order.
38/// Re-inserting the same name returns the existing index.
39#[derive(Debug)]
40pub struct SymTab {
41 tab: IndexMap<String, i64>,
42}
43
44impl SymTab {
45 /// Creates a new, empty symbol table.
46 pub fn new() -> Self {
47 Self {
48 tab: IndexMap::new(),
49 }
50 }
51
52 /// Returns the number of entries currently stored in the symbol table.
53 ///
54 /// Each entry corresponds to a unique symbol (e.g., variable or identifier)
55 /// that has been interned.
56 ///
57 /// # Example
58 /// ```rust
59 /// # use parlex_calc::SymTab;
60 /// let mut symtab = SymTab::new();
61 /// assert_eq!(symtab.len(), 0);
62 /// symtab.intern("foo");
63 /// assert_eq!(symtab.len(), 1);
64 /// symtab.intern("baz");
65 /// assert_eq!(symtab.len(), 2);
66 /// symtab.intern("foo");
67 /// assert_eq!(symtab.len(), 2);
68 /// ```
69 pub fn len(&self) -> usize {
70 self.tab.len()
71 }
72
73 /// Inserts the given name if it doesn't exist and returns its index.
74 ///
75 /// If the name already exists, this returns the existing index
76 /// without modifying the stored value.
77 ///
78 /// # Examples
79 /// ```
80 /// # use parlex_calc::SymTab;
81 /// let mut st = SymTab::new();
82 /// let i = st.intern("a");
83 /// assert_eq!(i, 0);
84 /// assert_eq!(st.intern("a"), 0); // existing index
85 /// ```
86 pub fn intern(&mut self, name: impl AsRef<str>) -> usize {
87 match self.tab.entry(String::from(name.as_ref())) {
88 Entry::Occupied(o) => o.index(),
89 Entry::Vacant(v) => {
90 let o = v.insert_entry(0);
91 o.index()
92 }
93 }
94 }
95
96 /// Updates the value at the given index.
97 ///
98 /// Returns [`Err`] if the index is out of bounds.
99 ///
100 /// # Examples
101 /// ```
102 /// # use parlex_calc::SymTab;
103 /// let mut st = SymTab::new();
104 /// let i = st.intern("x");
105 /// st.set(i, 123).unwrap();
106 /// assert_eq!(st.get(i).unwrap(), 123);
107 /// ```
108 pub fn set(&mut self, index: usize, new_value: i64) -> Result<(), SymTabError> {
109 let n = self.tab.len();
110 let (_, value) = self
111 .tab
112 .get_index_mut(index)
113 .ok_or(SymTabError::InvalidIndex { index, len: n })?;
114 *value = new_value;
115 Ok(())
116 }
117
118 /// Returns the value stored at the given index.
119 ///
120 /// Returns [`Err`] if the index is out of bounds.
121 pub fn get(&self, index: usize) -> Result<i64, SymTabError> {
122 let (_, value) = self.tab.get_index(index).ok_or(SymTabError::InvalidIndex {
123 index,
124 len: self.tab.len(),
125 })?;
126 Ok(*value)
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn new_table_is_empty() {
136 let st = SymTab::new();
137 assert_eq!(st.len(), 0);
138 }
139
140 #[test]
141 fn intern_assigns_sequential_indices() {
142 let mut st = SymTab::new();
143 let a = st.intern("a");
144 let b = st.intern("b");
145 let c = st.intern("c");
146 assert_eq!((a, b, c), (0, 1, 2));
147 assert_eq!(st.len(), 3);
148 }
149
150 #[test]
151 fn re_intern_returns_same_index_and_preserves_value() {
152 let mut st = SymTab::new();
153 let i = st.intern("x");
154 st.set(i, 42).unwrap();
155
156 let j = st.intern("x"); // should NOT overwrite the stored value
157 assert_eq!(i, j);
158 assert_eq!(st.get(i).unwrap(), 42);
159 }
160
161 #[test]
162 fn set_and_get_roundtrip() {
163 let mut st = SymTab::new();
164 let i = st.intern("n");
165 st.set(i, -7).unwrap();
166 assert_eq!(st.get(i).unwrap(), -7);
167
168 st.set(i, 123).unwrap();
169 assert_eq!(st.get(i).unwrap(), 123);
170 }
171
172 #[test]
173 fn get_invalid_index_errors() {
174 let mut st = SymTab::new();
175 let _ = st.intern("only_one");
176 match st.get(5) {
177 Err(SymTabError::InvalidIndex { index, len }) => {
178 assert_eq!(index, 5);
179 assert_eq!(len, 1);
180 }
181 other => panic!("expected InvalidIndex, got {:?}", other),
182 }
183 }
184
185 #[test]
186 fn set_invalid_index_errors() {
187 let st_len;
188 let mut st = SymTab::new();
189 let _ = st.intern("z");
190 st_len = st.len();
191 let err = st.set(999, 1).unwrap_err();
192 match err {
193 SymTabError::InvalidIndex { index, len } => {
194 assert_eq!(index, 999);
195 assert_eq!(len, st_len);
196 }
197 }
198 }
199
200 #[test]
201 fn intern_same_name_multiple_times_does_not_change_len() {
202 let mut st = SymTab::new();
203 let first = st.intern("dup");
204 let second = st.intern("dup");
205 let third = st.intern("dup");
206 assert_eq!(first, second);
207 assert_eq!(second, third);
208 assert_eq!(st.len(), 1);
209 }
210
211 #[test]
212 fn many_symbols_have_distinct_indices() {
213 let mut st = SymTab::new();
214 let mut seen = std::collections::BTreeSet::new();
215 for n in 0..100 {
216 let name = format!("v{n}");
217 let idx = st.intern(name);
218 assert!(seen.insert(idx), "duplicate index {}", idx);
219 }
220 assert_eq!(st.len(), 100);
221 assert_eq!(seen.len(), 100);
222 }
223}