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