Skip to main content

sim_kernel/
table.rs

1//! The table contract: the pluggable [`Table`] and [`Dir`] backend protocol.
2//!
3//! The kernel defines the table/directory protocols and a backend registry;
4//! concrete table representations are libs loaded against it, with `AssocTable`
5//! provided as a baseline backend rather than kernel-fixed behavior.
6
7use std::{
8    collections::BTreeMap,
9    sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard},
10};
11
12use crate::{
13    catalog::CatalogBackend,
14    env::Cx,
15    error::{Error, Result},
16    expr::Expr,
17    id::{CORE_TABLE_CLASS_ID, Symbol},
18    object::{ClassRef, Object},
19    value::{RuntimeObject, Value},
20};
21
22/// Universal map surface. Keys are symbols; values are runtime values.
23pub trait Table: RuntimeObject {
24    /// Symbol identifying the backend representation.
25    fn backend_symbol(&self) -> Symbol;
26
27    /// Looks up `key`, returning nil when absent.
28    fn get(&self, cx: &mut Cx, key: Symbol) -> Result<Value>;
29
30    /// Inserts or replaces the value for `key`.
31    fn set(&self, cx: &mut Cx, key: Symbol, value: Value) -> Result<()>;
32
33    /// Whether `key` is present.
34    fn has(&self, cx: &mut Cx, key: Symbol) -> Result<bool>;
35
36    /// Removes `key`, returning its prior value or nil.
37    fn del(&self, cx: &mut Cx, key: Symbol) -> Result<Value>;
38
39    /// All keys, in backend order.
40    fn keys(&self, cx: &mut Cx) -> Result<Vec<Symbol>>;
41
42    /// All key/value pairs, in backend order.
43    fn entries(&self, cx: &mut Cx) -> Result<Vec<(Symbol, Value)>>;
44
45    /// Number of entries.
46    fn len(&self, cx: &mut Cx) -> Result<usize>;
47
48    /// Whether the table has no entries.
49    fn is_empty(&self, cx: &mut Cx) -> Result<bool> {
50        Ok(self.len(cx)? == 0)
51    }
52
53    /// Removes all entries.
54    fn clear(&self, cx: &mut Cx) -> Result<()>;
55
56    /// Projects the table to an [`Expr::Map`].
57    fn as_table_expr(&self, cx: &mut Cx) -> Result<Expr> {
58        let entries = self.entries(cx)?;
59        let mut pairs = Vec::with_capacity(entries.len());
60        for (key, value) in entries {
61            pairs.push((Expr::Symbol(key), value.object().as_expr(cx)?));
62        }
63        Ok(Expr::Map(pairs))
64    }
65
66    /// Order-insensitive equality against another table's entries.
67    fn table_eq(&self, cx: &mut Cx, other: &dyn Table) -> Result<bool> {
68        let mut left = self.entries(cx)?;
69        let mut right = other.entries(cx)?;
70        left.sort_by(|a, b| a.0.cmp(&b.0));
71        right.sort_by(|a, b| a.0.cmp(&b.0));
72        Ok(left == right)
73    }
74}
75
76/// Hierarchical table surface for backends that support nested subtables.
77pub trait Dir: Table {
78    /// Creates a nested subtable under `name`, returning it.
79    fn mkdir(&self, cx: &mut Cx, name: Symbol) -> Result<Value>;
80
81    /// Opens the subtable at `name`, or `Ok(None)` when absent.
82    fn opendir(&self, cx: &mut Cx, name: Symbol) -> Result<Option<Value>>;
83
84    /// Removes the subtable at `name`, returning it.
85    fn rmdir(&self, cx: &mut Cx, name: Symbol) -> Result<Value>;
86
87    /// Whether `name` resolves to a subtable.
88    fn is_dir(&self, cx: &mut Cx, name: Symbol) -> Result<bool>;
89}
90
91/// Baseline table backend backed by an association list under a lock.
92// sim-non-citizen(reason = "kernel table backing object; canonical form is native table entries", kind = "private", descriptor = "")
93// FUTURE CLEANUP: this is the kernel's built-in REFERENCE table backend (the
94// bootstrap implementation). Once a default-loaded distribution lib can supply
95// a `TableBackend` before any user code runs, move `AssocTable` out of the
96// kernel and keep only the `Table`/`TableBackend` contracts here.
97pub struct AssocTable {
98    entries: RwLock<Vec<(Symbol, Value)>>,
99}
100
101impl AssocTable {
102    /// Builds an empty table.
103    pub fn new() -> Self {
104        Self {
105            entries: RwLock::new(Vec::new()),
106        }
107    }
108
109    /// Builds a table seeded with the given entries.
110    pub fn with_entries(entries: Vec<(Symbol, Value)>) -> Self {
111        Self {
112            entries: RwLock::new(entries),
113        }
114    }
115
116    fn read_entries(&self) -> Result<RwLockReadGuard<'_, Vec<(Symbol, Value)>>> {
117        self.entries.read().map_err(|_| poisoned_table_error())
118    }
119
120    fn write_entries(&self) -> Result<RwLockWriteGuard<'_, Vec<(Symbol, Value)>>> {
121        self.entries.write().map_err(|_| poisoned_table_error())
122    }
123}
124
125impl Default for AssocTable {
126    fn default() -> Self {
127        Self::new()
128    }
129}
130
131impl Object for AssocTable {
132    fn display(&self, _cx: &mut Cx) -> Result<String> {
133        Ok(format!("table[{}]", self.read_entries()?.len()))
134    }
135
136    fn as_any(&self) -> &dyn std::any::Any {
137        self
138    }
139}
140
141impl crate::ObjectCompat for AssocTable {
142    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
143        let symbol = Symbol::qualified("core", "Table");
144        if let Some(value) = cx.registry().class_by_symbol(&symbol) {
145            return Ok(value.clone());
146        }
147        cx.factory().class_stub(CORE_TABLE_CLASS_ID, symbol)
148    }
149    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
150        self.as_table_expr(cx)
151    }
152    fn truth(&self, _cx: &mut Cx) -> Result<bool> {
153        Ok(!self.read_entries()?.is_empty())
154    }
155    fn as_table_impl(&self) -> Option<&dyn Table> {
156        Some(self)
157    }
158}
159
160impl Table for AssocTable {
161    fn backend_symbol(&self) -> Symbol {
162        Symbol::qualified("core", "Table")
163    }
164
165    fn get(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
166        let guard = self.read_entries()?;
167        match guard.iter().find(|(candidate, _)| *candidate == key) {
168            Some((_, value)) => Ok(value.clone()),
169            None => cx.factory().nil(),
170        }
171    }
172
173    fn set(&self, _cx: &mut Cx, key: Symbol, value: Value) -> Result<()> {
174        let mut guard = self.write_entries()?;
175        if let Some((_, slot)) = guard.iter_mut().find(|(candidate, _)| *candidate == key) {
176            *slot = value;
177        } else {
178            guard.push((key, value));
179        }
180        Ok(())
181    }
182
183    fn has(&self, _cx: &mut Cx, key: Symbol) -> Result<bool> {
184        Ok(self
185            .read_entries()?
186            .iter()
187            .any(|(candidate, _)| *candidate == key))
188    }
189
190    fn del(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
191        let mut guard = self.write_entries()?;
192        if let Some(index) = guard.iter().position(|(candidate, _)| *candidate == key) {
193            Ok(guard.remove(index).1)
194        } else {
195            cx.factory().nil()
196        }
197    }
198
199    fn keys(&self, _cx: &mut Cx) -> Result<Vec<Symbol>> {
200        Ok(self
201            .read_entries()?
202            .iter()
203            .map(|(key, _)| key.clone())
204            .collect())
205    }
206
207    fn entries(&self, _cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
208        Ok(self.read_entries()?.clone())
209    }
210
211    fn len(&self, _cx: &mut Cx) -> Result<usize> {
212        Ok(self.read_entries()?.len())
213    }
214
215    fn clear(&self, _cx: &mut Cx) -> Result<()> {
216        self.write_entries()?.clear();
217        Ok(())
218    }
219}
220
221fn poisoned_table_error() -> Error {
222    Error::Eval("assoc table lock poisoned".to_owned())
223}
224
225/// Factory protocol for constructing tables in a particular representation.
226pub trait TableBackend: Send + Sync {
227    /// Stable name the backend is registered and selected under.
228    fn name(&self) -> &str;
229
230    /// Builds a table from an initial set of entries.
231    fn new_table(&self, cx: &mut Cx, entries: Vec<(Symbol, Value)>) -> Result<Value>;
232}
233
234/// Registry of named table backends with one active default.
235pub struct TableRegistry {
236    backends: BTreeMap<String, Arc<dyn TableBackend>>,
237    active: String,
238}
239
240impl TableRegistry {
241    /// Builds a registry preloaded with the `assoc` and catalog backends.
242    pub fn new() -> Self {
243        let mut registry = Self {
244            backends: BTreeMap::new(),
245            active: "assoc".to_owned(),
246        };
247        registry.register(Arc::new(AssocBackend));
248        registry.register(Arc::new(CatalogBackend));
249        registry
250    }
251
252    /// Registers a backend under its own name, replacing any prior one.
253    pub fn register(&mut self, backend: Arc<dyn TableBackend>) {
254        self.backends.insert(backend.name().to_owned(), backend);
255    }
256
257    /// Selects the active backend by name, erroring if it is unknown.
258    pub fn set_active(&mut self, name: &str) -> Result<()> {
259        if self.backends.contains_key(name) {
260            self.active = name.to_owned();
261            Ok(())
262        } else {
263            Err(Error::Eval(format!("unknown table backend: {name}")))
264        }
265    }
266
267    /// Name of the currently active backend.
268    pub fn active(&self) -> &str {
269        &self.active
270    }
271
272    /// Builds a table using the active backend.
273    pub fn new_table(&self, cx: &mut Cx, entries: Vec<(Symbol, Value)>) -> Result<Value> {
274        self.backend()?.new_table(cx, entries)
275    }
276
277    fn backend(&self) -> Result<&Arc<dyn TableBackend>> {
278        self.backends
279            .get(&self.active)
280            .ok_or_else(|| Error::Eval("active table backend missing".to_owned()))
281    }
282}
283
284impl Default for TableRegistry {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290struct AssocBackend;
291
292impl TableBackend for AssocBackend {
293    fn name(&self) -> &str {
294        "assoc"
295    }
296
297    fn new_table(&self, cx: &mut Cx, entries: Vec<(Symbol, Value)>) -> Result<Value> {
298        cx.factory()
299            .opaque(Arc::new(AssocTable::with_entries(entries)))
300    }
301}
302
303#[cfg(test)]
304#[path = "table_tests.rs"]
305mod table_tests;