avmnif_rs/
atom.rs

1//! AtomVM Atom Table Interface
2//!
3//! This module provides safe Rust bindings to the AtomVM atom table,
4//! allowing Ports and NIFs to interact with the VM's atom storage system.
5//!
6//! # Design Philosophy
7//!
8//! This module uses dependency injection throughout - no global state.
9//! All operations take an `impl AtomTableOps` parameter, making the code
10//! generic and testable with any atom table implementation.
11//!
12//! # Examples
13//!
14//! ```rust,ignore
15//! use avmnif_rs::atom::{AtomTableOps, AtomTable};
16//!
17//! // In production - use real AtomVM table
18//! let atom_table = AtomTable::new(context);
19//! let hello_atom = atom_table.ensure_atom_str("hello")?;
20//!
21//! // In testing - use mock table
22//! let atom_table = MockAtomTable::new();
23//! let hello_atom = atom_table.ensure_atom_str("hello")?;
24//!
25//! // Both work the same way!
26//! ```
27
28extern crate alloc;
29
30use core::fmt;
31use core::str;
32use alloc::vec::Vec;
33
34// ── Core Types and Errors ───────────────────────────────────────────────────
35
36/// Index into the atom table
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
38pub struct AtomIndex(pub u32);
39
40impl AtomIndex {
41    pub const INVALID: AtomIndex = AtomIndex(0);
42    
43    pub fn new(index: u32) -> Self {
44        AtomIndex(index)
45    }
46    
47    pub fn get(self) -> u32 {
48        self.0
49    }
50    
51    pub fn is_valid(self) -> bool {
52        self.0 != 0
53    }
54}
55
56/// Copy options for atom insertion
57#[repr(u32)]
58pub enum AtomCopyOpt {
59    /// Reference existing data (caller must ensure lifetime)
60    Reference = 0,
61    /// Copy atom data into table-owned memory
62    Copy = 1,
63    /// Check if atom already exists, don't create
64    AlreadyExisting = 2,
65}
66
67/// Options for bulk atom operations
68#[repr(u32)]
69pub enum EnsureAtomsOpt {
70    /// Standard encoding (length byte + data)
71    Standard = 0,
72    /// Long encoding (variable-length encoding)
73    LongEncoding = 1,
74}
75
76/// Errors that can occur during atom operations
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum AtomError {
79    /// Atom not found in table
80    NotFound,
81    /// Memory allocation failed
82    AllocationFailed,
83    /// Invalid atom length (too long or encoding error)
84    InvalidLength,
85    /// Invalid atom data (bad UTF-8, null bytes, etc.)
86    InvalidAtomData,
87    /// Null pointer returned from C API
88    NullPointer,
89    /// Invalid atom index
90    InvalidIndex,
91}
92
93impl fmt::Display for AtomError {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match self {
96            AtomError::NotFound => write!(f, "atom not found in table"),
97            AtomError::AllocationFailed => write!(f, "memory allocation failed"),
98            AtomError::InvalidLength => write!(f, "invalid atom length"),
99            AtomError::InvalidAtomData => write!(f, "invalid atom data or encoding"),
100            AtomError::NullPointer => write!(f, "unexpected null pointer from atom table"),
101            AtomError::InvalidIndex => write!(f, "invalid atom index"),
102        }
103    }
104}
105
106/// Reference to atom data stored in the table
107#[derive(Debug)]
108pub struct AtomRef<'a> {
109    data: &'a [u8],
110    index: AtomIndex,
111}
112
113impl<'a> AtomRef<'a> {
114    pub fn new(data: &'a [u8], index: AtomIndex) -> Self {
115        Self { data, index }
116    }
117
118    /// Get the atom's index
119    pub fn index(&self) -> AtomIndex {
120        self.index
121    }
122
123    /// Get the atom's data as bytes
124    pub fn as_bytes(&self) -> &[u8] {
125        self.data
126    }
127
128    /// Get the atom's data as a string (if valid UTF-8)
129    pub fn as_str(&self) -> Result<&str, str::Utf8Error> {
130        str::from_utf8(self.data)
131    }
132
133    /// Get the atom's length in bytes
134    pub fn len(&self) -> usize {
135        self.data.len()
136    }
137
138    /// Check if the atom is empty
139    pub fn is_empty(&self) -> bool {
140        self.data.is_empty()
141    }
142}
143
144impl<'a> AsRef<[u8]> for AtomRef<'a> {
145    fn as_ref(&self) -> &[u8] {
146        self.data
147    }
148}
149
150impl<'a> PartialEq<[u8]> for AtomRef<'a> {
151    fn eq(&self, other: &[u8]) -> bool {
152        self.data == other
153    }
154}
155
156impl<'a> PartialEq<&[u8]> for AtomRef<'a> {
157    fn eq(&self, other: &&[u8]) -> bool {
158        self.data == *other
159    }
160}
161
162impl<'a> PartialEq<str> for AtomRef<'a> {
163    fn eq(&self, other: &str) -> bool {
164        self.data == other.as_bytes()
165    }
166}
167
168// ── Generic Atom Table Operations Trait ────────────────────────────────────
169
170/// Trait for atom table operations - the foundation of our generic design
171/// 
172/// Any implementation (real AtomVM, mock, in-memory, etc.) can provide
173/// these operations, making the entire system generic and testable.
174pub trait AtomTableOps {
175    /// Get the number of atoms currently in the table
176    fn count(&self) -> usize;
177
178    /// Get atom data by index
179    fn get_atom_string(&self, index: AtomIndex) -> Result<AtomRef<'_>, AtomError>;
180
181    /// Ensure an atom exists in the table, creating it if necessary
182    fn ensure_atom(&self, atom_data: &[u8]) -> Result<AtomIndex, AtomError>;
183
184    /// Ensure an atom exists, but only if it already exists
185    fn find_atom(&self, atom_data: &[u8]) -> Result<AtomIndex, AtomError>;
186
187    /// Ensure an atom exists using a string slice
188    fn ensure_atom_str(&self, atom_str: &str) -> Result<AtomIndex, AtomError> {
189        self.ensure_atom(atom_str.as_bytes())
190    }
191
192    /// Find an atom using a string slice
193    fn find_atom_str(&self, atom_str: &str) -> Result<AtomIndex, AtomError> {
194        self.find_atom(atom_str.as_bytes())
195    }
196
197    /// Check if an atom equals the given byte string
198    fn atom_equals(&self, atom_index: AtomIndex, data: &[u8]) -> bool;
199
200    /// Check if an atom equals the given string
201    fn atom_equals_str(&self, atom_index: AtomIndex, s: &str) -> bool {
202        self.atom_equals(atom_index, s.as_bytes())
203    }
204
205    /// Compare two atoms lexicographically
206    fn compare_atoms(&self, atom1: AtomIndex, atom2: AtomIndex) -> i32;
207
208    /// Bulk insert/lookup atoms from encoded atom data
209    fn ensure_atoms_bulk(
210        &self,
211        atoms_data: &[u8],
212        count: usize,
213        encoding: EnsureAtomsOpt,
214    ) -> Result<Vec<AtomIndex>, AtomError>;
215}
216
217// ── AtomVM Implementation ───────────────────────────────────────────────────
218
219use core::ffi::c_void;
220use core::slice;
221
222/// Opaque handle to the AtomVM atom table
223#[repr(transparent)]
224pub struct AtomTable(*mut c_void);
225
226/// Result of atom table operations
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228enum AtomTableResult {
229    Ok,
230    NotFound,
231    AllocationFailed,
232    InvalidLength,
233}
234
235// FFI declarations - Note: These expect raw u32 values, not AtomIndex structs
236extern "C" {
237    fn atom_table_get_atom_string(
238        table: *mut c_void,
239        index: u32,  // Raw u32, not AtomIndex
240        out_size: *mut usize,
241    ) -> *const u8;
242
243    fn atom_table_ensure_atom(
244        table: *mut c_void,
245        atom_data: *const u8,
246        atom_len: usize,
247        opts: u32,
248        result: *mut u32,  // Raw u32, not AtomIndex
249    ) -> u32;
250
251    fn atom_table_ensure_atoms(
252        table: *mut c_void,
253        atoms: *const c_void,
254        count: usize,
255        translate_table: *mut u32,  // Raw u32, not AtomIndex
256        opt: u32,
257    ) -> u32;
258
259    fn atom_table_count(table: *mut c_void) -> usize;
260
261    fn atom_table_is_equal_to_atom_string(
262        table: *mut c_void,
263        atom_index: u32,  // Raw u32, not AtomIndex
264        string_data: *const u8,
265        string_len: usize,
266    ) -> bool;
267
268    fn atom_table_cmp_using_atom_index(
269        table: *mut c_void,
270        atom1: u32,  // Raw u32, not AtomIndex
271        atom2: u32,  // Raw u32, not AtomIndex
272    ) -> i32;
273
274    fn atomvm_get_global_atom_table() -> *mut c_void;
275}
276
277// Helper to convert C result to Rust enum
278fn result_from_c(result: u32) -> AtomTableResult {
279    match result {
280        0 => AtomTableResult::Ok,
281        1 => AtomTableResult::NotFound,
282        2 => AtomTableResult::AllocationFailed,
283        3 => AtomTableResult::InvalidLength,
284        _ => AtomTableResult::AllocationFailed,
285    }
286}
287
288impl AtomTable {
289    /// Create an AtomTable from a raw pointer
290    /// 
291    /// # Safety
292    /// The pointer must be valid and point to a real AtomVM atom table
293    pub unsafe fn from_raw(ptr: *mut c_void) -> Self {
294        AtomTable(ptr)
295    }
296
297    /// Create an AtomTable from the global AtomVM instance
298    /// 
299    /// This should only be used in production with a running AtomVM.
300    /// For testing, use MockAtomTable instead.
301    pub fn from_global() -> Self {
302        let ptr = unsafe { atomvm_get_global_atom_table() };
303        AtomTable(ptr)
304    }
305
306    /// Get the raw pointer to the atom table
307    /// 
308    /// # Safety
309    /// The returned pointer should only be used with AtomVM C functions
310    pub unsafe fn as_raw(&self) -> *mut c_void {
311        self.0
312    }
313}
314
315impl AtomTableOps for AtomTable {
316    fn count(&self) -> usize {
317        unsafe { atom_table_count(self.0) }
318    }
319
320    fn get_atom_string(&self, index: AtomIndex) -> Result<AtomRef<'_>, AtomError> {
321        let mut size: usize = 0;
322        let ptr = unsafe { atom_table_get_atom_string(self.0, index.0, &mut size) };
323        
324        if ptr.is_null() {
325            return Err(AtomError::InvalidIndex);
326        }
327
328        let data = unsafe { slice::from_raw_parts(ptr, size) };
329        Ok(AtomRef::new(data, index))
330    }
331
332    fn ensure_atom(&self, atom_data: &[u8]) -> Result<AtomIndex, AtomError> {
333        let mut result: u32 = 0;  // Raw u32 for FFI
334        let status = unsafe {
335            atom_table_ensure_atom(
336                self.0,
337                atom_data.as_ptr(),
338                atom_data.len(),
339                AtomCopyOpt::Copy as u32,
340                &mut result,
341            )
342        };
343
344        match result_from_c(status) {
345            AtomTableResult::Ok => Ok(AtomIndex(result)),
346            AtomTableResult::NotFound => Err(AtomError::NotFound),
347            AtomTableResult::AllocationFailed => Err(AtomError::AllocationFailed),
348            AtomTableResult::InvalidLength => Err(AtomError::InvalidLength),
349        }
350    }
351
352    fn find_atom(&self, atom_data: &[u8]) -> Result<AtomIndex, AtomError> {
353        let mut result: u32 = 0;  // Raw u32 for FFI
354        let status = unsafe {
355            atom_table_ensure_atom(
356                self.0,
357                atom_data.as_ptr(),
358                atom_data.len(),
359                AtomCopyOpt::AlreadyExisting as u32,
360                &mut result,
361            )
362        };
363
364        match result_from_c(status) {
365            AtomTableResult::Ok => Ok(AtomIndex(result)),
366            AtomTableResult::NotFound => Err(AtomError::NotFound),
367            AtomTableResult::AllocationFailed => Err(AtomError::AllocationFailed),
368            AtomTableResult::InvalidLength => Err(AtomError::InvalidLength),
369        }
370    }
371
372    fn atom_equals(&self, atom_index: AtomIndex, data: &[u8]) -> bool {
373        unsafe {
374            atom_table_is_equal_to_atom_string(
375                self.0,
376                atom_index.0,  // Extract raw u32
377                data.as_ptr(),
378                data.len(),
379            )
380        }
381    }
382
383    fn compare_atoms(&self, atom1: AtomIndex, atom2: AtomIndex) -> i32 {
384        unsafe { atom_table_cmp_using_atom_index(self.0, atom1.0, atom2.0) }
385    }
386
387    fn ensure_atoms_bulk(
388        &self,
389        atoms_data: &[u8],
390        count: usize,
391        encoding: EnsureAtomsOpt,
392    ) -> Result<Vec<AtomIndex>, AtomError> {
393        let mut translate_table: Vec<u32> = Vec::with_capacity(count);  // Raw u32 for FFI
394        translate_table.resize(count, 0u32);
395        
396        let status = unsafe {
397            atom_table_ensure_atoms(
398                self.0,
399                atoms_data.as_ptr() as *const c_void,
400                count,
401                translate_table.as_mut_ptr(),
402                encoding as u32,
403            )
404        };
405
406        match result_from_c(status) {
407            AtomTableResult::Ok => {
408                // Convert Vec<u32> to Vec<AtomIndex>
409                let result: Vec<AtomIndex> = translate_table.into_iter().map(AtomIndex).collect();
410                Ok(result)
411            }
412            AtomTableResult::NotFound => Err(AtomError::NotFound),
413            AtomTableResult::AllocationFailed => Err(AtomError::AllocationFailed),
414            AtomTableResult::InvalidLength => Err(AtomError::InvalidLength),
415        }
416    }
417}
418
419// Safety: AtomTable operations are thread-safe due to internal locking
420unsafe impl Send for AtomTable {}
421unsafe impl Sync for AtomTable {}
422
423// ── Common Atom Utilities ───────────────────────────────────────────────────
424
425/// Utilities for working with common atoms
426/// 
427/// These functions work with any atom table implementation.
428pub mod atoms {
429    use super::*;
430
431    /// Ensure common atoms exist in a table
432    /// 
433    /// This is useful for initializing any atom table (real or mock)
434    /// with the standard atoms that AtomVM typically provides.
435    pub fn ensure_common_atoms<T: AtomTableOps>(table: &T) -> Result<(), AtomError> {
436        let common_atoms = [
437            "ok", "error", "true", "false", "undefined", "badarg", "nil",
438            "atom", "binary", "bitstring", "boolean", "float", "function",
439            "integer", "list", "map", "pid", "port", "reference", "tuple"
440        ];
441        
442        for atom_name in &common_atoms {
443            table.ensure_atom_str(atom_name)?;
444        }
445        
446        Ok(())
447    }
448
449    /// Get an "ok" atom from any table
450    pub fn ok<T: AtomTableOps>(table: &T) -> Result<AtomIndex, AtomError> {
451        table.ensure_atom_str("ok")
452    }
453
454    /// Get an "error" atom from any table
455    pub fn error<T: AtomTableOps>(table: &T) -> Result<AtomIndex, AtomError> {
456        table.ensure_atom_str("error")
457    }
458
459    /// Get a "true" atom from any table
460    pub fn true_atom<T: AtomTableOps>(table: &T) -> Result<AtomIndex, AtomError> {
461        table.ensure_atom_str("true")
462    }
463
464    /// Get a "false" atom from any table
465    pub fn false_atom<T: AtomTableOps>(table: &T) -> Result<AtomIndex, AtomError> {
466        table.ensure_atom_str("false")
467    }
468
469    /// Get a "nil" atom from any table
470    pub fn nil<T: AtomTableOps>(table: &T) -> Result<AtomIndex, AtomError> {
471        table.ensure_atom_str("nil")
472    }
473
474    /// Get an "undefined" atom from any table
475    pub fn undefined<T: AtomTableOps>(table: &T) -> Result<AtomIndex, AtomError> {
476        table.ensure_atom_str("undefined")
477    }
478
479    /// Get a "badarg" atom from any table
480    pub fn badarg<T: AtomTableOps>(table: &T) -> Result<AtomIndex, AtomError> {
481        table.ensure_atom_str("badarg")
482    }
483}