Skip to main content

oxihuman_core/
symbol_table.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! A symbol table that maps string names to integer symbol IDs and back.
6
7use std::collections::HashMap;
8
9/// An opaque symbol identifier.
10#[allow(dead_code)]
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct SymbolId(pub u32);
13
14/// A bidirectional symbol table.
15#[allow(dead_code)]
16pub struct SymbolTable {
17    name_to_id: HashMap<String, SymbolId>,
18    id_to_name: Vec<String>,
19    scope: String,
20}
21
22#[allow(dead_code)]
23impl SymbolTable {
24    pub fn new(scope: &str) -> Self {
25        Self {
26            name_to_id: HashMap::new(),
27            id_to_name: Vec::new(),
28            scope: scope.to_string(),
29        }
30    }
31
32    /// Interns a name, returning a new or existing [`SymbolId`].
33    pub fn intern(&mut self, name: &str) -> SymbolId {
34        if let Some(&id) = self.name_to_id.get(name) {
35            return id;
36        }
37        let id = SymbolId(self.id_to_name.len() as u32);
38        self.name_to_id.insert(name.to_string(), id);
39        self.id_to_name.push(name.to_string());
40        id
41    }
42
43    /// Looks up a name by id.
44    pub fn lookup(&self, id: SymbolId) -> Option<&str> {
45        self.id_to_name.get(id.0 as usize).map(|s| s.as_str())
46    }
47
48    /// Looks up an id by name.
49    pub fn find(&self, name: &str) -> Option<SymbolId> {
50        self.name_to_id.get(name).copied()
51    }
52
53    pub fn contains(&self, name: &str) -> bool {
54        self.name_to_id.contains_key(name)
55    }
56
57    pub fn len(&self) -> usize {
58        self.id_to_name.len()
59    }
60
61    pub fn is_empty(&self) -> bool {
62        self.id_to_name.is_empty()
63    }
64
65    pub fn scope(&self) -> &str {
66        &self.scope
67    }
68
69    pub fn all_names(&self) -> &[String] {
70        &self.id_to_name
71    }
72
73    pub fn clear(&mut self) {
74        self.name_to_id.clear();
75        self.id_to_name.clear();
76    }
77
78    /// Returns all ids that match a prefix.
79    pub fn ids_with_prefix(&self, prefix: &str) -> Vec<SymbolId> {
80        self.name_to_id
81            .iter()
82            .filter(|(k, _)| k.starts_with(prefix))
83            .map(|(_, &v)| v)
84            .collect()
85    }
86}
87
88impl Default for SymbolTable {
89    fn default() -> Self {
90        Self::new("global")
91    }
92}
93
94pub fn new_symbol_table(scope: &str) -> SymbolTable {
95    SymbolTable::new(scope)
96}
97
98pub fn sym_intern(table: &mut SymbolTable, name: &str) -> SymbolId {
99    table.intern(name)
100}
101
102pub fn sym_lookup(table: &SymbolTable, id: SymbolId) -> Option<&str> {
103    table.lookup(id)
104}
105
106pub fn sym_find(table: &SymbolTable, name: &str) -> Option<SymbolId> {
107    table.find(name)
108}
109
110pub fn sym_len(table: &SymbolTable) -> usize {
111    table.len()
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn new_is_empty() {
120        let t = new_symbol_table("test");
121        assert!(t.is_empty());
122    }
123
124    #[test]
125    fn intern_returns_same_id() {
126        let mut t = new_symbol_table("test");
127        let id1 = sym_intern(&mut t, "foo");
128        let id2 = sym_intern(&mut t, "foo");
129        assert_eq!(id1, id2);
130    }
131
132    #[test]
133    fn different_names_get_different_ids() {
134        let mut t = new_symbol_table("test");
135        let id1 = sym_intern(&mut t, "a");
136        let id2 = sym_intern(&mut t, "b");
137        assert_ne!(id1, id2);
138    }
139
140    #[test]
141    fn lookup_roundtrip() {
142        let mut t = new_symbol_table("test");
143        let id = sym_intern(&mut t, "hello");
144        assert_eq!(sym_lookup(&t, id), Some("hello"));
145    }
146
147    #[test]
148    fn find_existing() {
149        let mut t = new_symbol_table("test");
150        sym_intern(&mut t, "x");
151        assert!(sym_find(&t, "x").is_some());
152    }
153
154    #[test]
155    fn find_missing_returns_none() {
156        let t = new_symbol_table("test");
157        assert!(sym_find(&t, "ghost").is_none());
158    }
159
160    #[test]
161    fn len_grows() {
162        let mut t = new_symbol_table("test");
163        sym_intern(&mut t, "a");
164        sym_intern(&mut t, "b");
165        sym_intern(&mut t, "a"); // duplicate
166        assert_eq!(sym_len(&t), 2);
167    }
168
169    #[test]
170    fn clear_empties() {
171        let mut t = new_symbol_table("test");
172        sym_intern(&mut t, "x");
173        t.clear();
174        assert!(t.is_empty());
175    }
176
177    #[test]
178    fn ids_with_prefix() {
179        let mut t = new_symbol_table("test");
180        sym_intern(&mut t, "mesh_body");
181        sym_intern(&mut t, "mesh_hair");
182        sym_intern(&mut t, "texture_skin");
183        let ids = t.ids_with_prefix("mesh_");
184        assert_eq!(ids.len(), 2);
185    }
186
187    #[test]
188    fn scope_is_stored() {
189        let t = new_symbol_table("physics");
190        assert_eq!(t.scope(), "physics");
191    }
192}