1use alloc::vec::Vec;
12
13use hashbrown::HashTable;
14
15use crate::code::Text;
16use crate::value::fnv1a;
17use crate::Ctx;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub struct Sym(u32);
24
25macro_rules! fixed_syms {
28 ($($name:ident = $text:literal,)*) => {
29 #[allow(non_camel_case_types, clippy::upper_case_acronyms)]
30 #[repr(u32)]
31 enum Fixed { $($name,)* _Count }
32
33 impl Sym {
34 $(pub const $name: Sym = Sym(Fixed::$name as u32);)*
35 }
36
37 const N_FIXED: u32 = Fixed::_Count as u32;
39
40 static FIXED: [&str; N_FIXED as usize] = [$($text,)*];
42 };
43}
44
45fixed_syms! {
49 MOD_BOOL = "Bool",
50 MOD_ERR = "Err",
51 MOD_FUN = "Fun",
52 MOD_INT = "Int",
53 MOD_LIST = "List",
54 MOD_MAP = "Map",
55 MOD_MOD = "Mod",
56 MOD_STR = "Str",
57 MOD_SYM = "Sym",
58 MOD_UNIT = "Unit",
59 ADD = "add",
60 AND = "and",
61 APPEND = "append",
62 BAD_CODEPOINT = "bad_codepoint",
63 BAD_PROJECTION = "bad_projection",
64 BAD_SYMBOL = "bad_symbol",
65 CANNOT_PARSE_INT = "cannot_parse_int",
66 CAT = "cat",
67 CATCH = "catch",
68 DIV_BY_ZERO = "div_by_zero",
69 DIVREM = "divrem",
70 EQ = "eq",
71 EXPORTS = "exports",
72 FALSE = "false",
73 FOR = "for",
74 FOREIGN_OBJECT = "foreign_object",
75 FROM = "from",
76 GE = "ge",
77 GET = "get",
78 GETS = "gets",
79 GT = "gt",
80 LE = "le",
81 LIST_SIZE_MISMATCH = "list_size_mismatch",
82 LT = "lt",
83 MAP_SIZE_MISMATCH = "map_size_mismatch",
84 MERGE = "merge",
85 MISSING_KEY = "missing_key",
86 MISSING_PROPERTY = "missing_property",
87 MODULE_ARITY = "module_arity",
88 MUL = "mul",
89 NAME = "name",
90 NEGATIVE_EXPONENT = "negative_exponent",
91 NIL = "nil",
92 NO_MATCH = "no_match",
93 NO_MODULE = "no_module",
94 NOT = "not",
95 NOT_A_BOOL = "not_a_bool",
96 NOT_A_LIST = "not_a_list",
97 NOT_A_MAP = "not_a_map",
98 NOT_A_MODULE = "not_a_module",
99 NOT_A_STR = "not_a_str",
100 NOT_A_SYM = "not_a_sym",
101 NOT_AN_INT = "not_an_int",
102 NOT_AN_OBJECT = "not_an_object",
103 NOT_APPLICABLE = "not_applicable",
104 NOT_INT = "not_int",
105 NOT_STRING = "not_string",
106 NOT_UNIT = "not_unit",
107 OR = "or",
108 PACK = "pack",
109 PARSE = "parse",
110 POW = "pow",
111 PROJECTION_OUT_OF_RANGE = "projection_out_of_range",
112 RAISE = "raise",
113 SIZE = "size",
114 SPLIT = "split",
115 STR = "str",
116 SUB = "sub",
117 TRUE = "true",
118 UNPACK = "unpack",
119 WITH = "with",
120 WITHOUT = "without",
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub(crate) enum Projection {
128 Name,
130 Index(u32),
132 TooLarge,
134}
135
136impl Projection {
137 fn of(text: &str) -> Projection {
138 if text.is_empty() || !text.bytes().all(|b| b.is_ascii_digit()) {
139 return Projection::Name;
140 }
141 text.parse().map_or(Projection::TooLarge, Projection::Index)
142 }
143}
144
145#[derive(Default)]
147pub(crate) struct Symbols {
148 entries: Vec<Entry>,
150 index: HashTable<u32>,
155}
156
157struct Entry {
158 text: Text,
159 hash: u64,
160 proj: Projection,
161}
162
163impl Symbols {
164 fn find(&self, s: &str, hash: u64) -> Option<Sym> {
165 self.index
166 .find(hash, |&id| self.str_of(id) == s)
167 .map(|&id| Sym(id))
168 }
169
170 fn str_of(&self, id: u32) -> &str {
171 match FIXED.get(id as usize) {
172 Some(s) => s,
173 None => &self.entries[(id - N_FIXED) as usize].text,
174 }
175 }
176
177 fn entry(&self, sym: Sym) -> &Entry {
178 &self.entries[(sym.0 - N_FIXED) as usize]
179 }
180}
181
182pub(crate) fn fixed(s: &str) -> Option<Sym> {
184 FIXED.binary_search(&s).ok().map(|i| Sym(i as u32))
185}
186
187impl Ctx {
188 pub fn intern(&self, s: &str) -> Sym {
190 let hash = fnv1a(s.as_bytes());
191 let mut syms = self.syms.borrow_mut();
192 if let Some(sym) = syms.find(s, hash) {
193 return sym;
194 }
195 let Symbols { entries, index } = &mut *syms;
196 let sym = match fixed(s) {
197 Some(sym) => sym,
198 None => {
199 let id = u32::try_from(entries.len())
200 .ok()
201 .and_then(|i| i.checked_add(N_FIXED))
202 .expect("fewer than 2^32 symbols");
203 entries.push(Entry {
204 text: Text::from(s),
205 hash,
206 proj: Projection::of(s),
207 });
208 Sym(id)
209 }
210 };
211 index.insert_unique(hash, sym.0, |&id| match FIXED.get(id as usize) {
212 Some(s) => fnv1a(s.as_bytes()),
213 None => entries[(id - N_FIXED) as usize].hash,
214 });
215 sym
216 }
217
218 pub fn sym(&self, s: &str) -> Option<Sym> {
221 self.syms
222 .borrow()
223 .find(s, fnv1a(s.as_bytes()))
224 .or_else(|| fixed(s))
225 }
226
227 pub fn text(&self, sym: Sym) -> Text {
229 match FIXED.get(sym.0 as usize) {
230 Some(s) => Text::from_static(s),
231 None => self.syms.borrow().entry(sym).text.clone(),
232 }
233 }
234
235 pub(crate) fn with_str<R>(&self, sym: Sym, f: impl FnOnce(&str) -> R) -> R {
238 match FIXED.get(sym.0 as usize) {
239 Some(s) => f(s),
240 None => f(&self.syms.borrow().entry(sym).text),
241 }
242 }
243
244 pub(crate) fn projection(&self, sym: Sym) -> Projection {
246 if sym.0 < N_FIXED {
247 return Projection::Name;
249 }
250 self.syms.borrow().entry(sym).proj
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn fixed_are_sorted_names() {
260 assert!(
261 FIXED.windows(2).all(|w| w[0] < w[1]),
262 "sorted, no duplicates"
263 );
264 assert!(FIXED.iter().all(|s| Projection::of(s) == Projection::Name));
265 assert_eq!(FIXED[Sym::DIV_BY_ZERO.0 as usize], "div_by_zero");
266 assert_eq!(FIXED[Sym::MOD_STR.0 as usize], "Str");
267 assert_eq!(FIXED[Sym::STR.0 as usize], "str");
268 }
269
270 #[test]
271 fn a_fixed_symbol_is_found_not_interned() {
272 let ctx = Ctx::new();
273 assert_eq!(ctx.intern("no_match"), Sym::NO_MATCH);
274 assert_eq!(ctx.sym("Int"), Some(Sym::MOD_INT));
275 assert_eq!(ctx.text(Sym::ADD), "add");
276 assert!(ctx.syms.borrow().entries.is_empty());
277 }
278
279 #[test]
280 fn interning_twice_gives_one_symbol() {
281 let ctx = Ctx::new();
282 let a = ctx.intern("apple");
283 let b = ctx.intern("banana");
284 assert_ne!(a, b);
285 assert_eq!(ctx.intern("apple"), a);
286 assert_eq!(ctx.text(a), "apple");
287 assert_eq!(ctx.with_str(b, str::len), 6);
288 assert_eq!(ctx.syms.borrow().entries.len(), 2);
289 }
290
291 #[test]
292 fn looking_up_interns_nothing() {
293 let ctx = Ctx::new();
294 assert_eq!(ctx.sym("pear"), None);
295 assert!(ctx.syms.borrow().entries.is_empty());
296 let p = ctx.intern("pear");
297 assert_eq!(ctx.sym("pear"), Some(p));
298 }
299
300 #[test]
301 fn many_symbols_stay_distinct() {
302 let ctx = Ctx::new();
303 let syms: Vec<Sym> = (0..1000)
304 .map(|i| ctx.intern(&alloc::format!("s{i}")))
305 .collect();
306 for (i, &s) in syms.iter().enumerate() {
307 assert_eq!(ctx.intern(&alloc::format!("s{i}")), s);
308 assert_eq!(ctx.text(s), alloc::format!("s{i}").as_str());
309 }
310 }
311
312 #[test]
313 fn a_projection_is_computed_once_interned() {
314 let ctx = Ctx::new();
315 let p = |s| ctx.projection(ctx.intern(s));
316 assert_eq!(p("0"), Projection::Index(0));
317 assert_eq!(p("12"), Projection::Index(12));
318 assert_eq!(p("4294967295"), Projection::Index(u32::MAX));
319 assert_eq!(p("4294967296"), Projection::TooLarge);
320 assert_eq!(p("1a"), Projection::Name);
321 assert_eq!(p("get"), Projection::Name);
322 assert_eq!(p(""), Projection::Name);
323 }
324}