Skip to main content

lua_vm/
string.rs

1//! Bootstrap string table used only to construct the pre-allocated
2//! out-of-memory message during VM startup.
3//!
4//! [`LuaStringImpl`]/[`StringPool`] mirror C's `TString`/`stringtable`
5//! (`lstring.c`/`lstring.h`), but nothing outside this module ever calls
6//! [`new_lstr`] again after [`init`] runs once at startup: general Lua
7//! string values and their interning go through `lua_types::LuaString` and
8//! `GlobalState::interned_lt` (see `state.rs`). `GlobalState::strt` — the
9//! [`StringPool`] this module maintains — ends up holding exactly one entry,
10//! the memory-error message, for the life of the process.
11
12#[allow(unused_imports)]
13use crate::prelude::*;
14use std::cell::Cell;
15use std::collections::HashMap;
16use std::rc::Rc;
17
18use crate::state::LuaState;
19
20use lua_types::GcRef;
21
22/// Converts the local `LuaStringImpl` into the canonical `lua_types::LuaString`
23/// used everywhere else in the VM. Only called once, on the bootstrap OOM
24/// message in [`init`].
25fn impl_to_lt(s: &GcRef<LuaStringImpl>) -> GcRef<lua_types::LuaString> {
26    GcRef::new(lua_types::LuaString::from_bytes(s.as_bytes().to_vec()))
27}
28
29// ── Constants ─────────────────────────────────────────────────────────────────
30
31/// Pre-allocated OOM error message.  Must be created before the allocator
32/// can fail so that the GC can always hand back a valid error string.
33pub(crate) const MEMERR_MSG: &[u8] = b"not enough memory";
34
35const MIN_STR_TAB_SIZE: usize = 128;
36
37const STRCACHE_N: usize = 53;
38
39const STRCACHE_M: usize = 2;
40
41pub(crate) const MAX_SHORT_LEN: usize = 40;
42
43const MAX_SIZE: usize = if std::mem::size_of::<usize>() < std::mem::size_of::<i64>() {
44    usize::MAX
45} else {
46    i64::MAX as usize
47};
48
49/// Upper bound on the number of hash buckets; derived from `i32::MAX` / pointer size.
50const MAX_STR_TAB: usize = i32::MAX as usize / std::mem::size_of::<usize>();
51
52// ── LuaStringImpl ────────────────────────────────────────────────────────────
53
54/// Whether a Lua string is short (interned) or long (not interned).
55///
56/// Corresponds to the `LUA_VSHRSTR` / `LUA_VLNGSTR` tags from `lobject.h`;
57/// C distinguishes them via a `shrlen` sentinel value (0xFF) rather than a
58/// separate enum.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum StringKind {
61    Short,
62    Long,
63}
64
65/// A Lua string: an immutable, reference-counted byte sequence. Corresponds
66/// to C's `TString`.
67///
68/// Short strings (`<= MAX_SHORT_LEN = 40` bytes) are interned in the
69/// [`StringPool`] on `GlobalState`; two short strings with the same bytes
70/// are guaranteed to be the same `GcRef` (pointer equality via `Rc::ptr_eq`).
71/// In practice the only `LuaStringImpl` ever created is the bootstrap OOM
72/// message — see the module doc.
73///
74/// Long strings are heap-allocated independently and never interned. `hash`
75/// is set once at construction in [`create_str_obj`], not computed lazily.
76pub struct LuaStringImpl {
77    bytes: Rc<[u8]>,
78
79    // Replaced by the StringKind enum; length is implicit in bytes.len().
80    kind: StringKind,
81
82    // Using Cell<u32> so that `hash_long_str` can cache the hash through a
83    // shared `&LuaStringImpl` reference (interior mutability, single-threaded).
84    #[allow(dead_code)]
85    hash: Cell<u32>,
86
87    // Short strings: reserved-word token index (0 = not a keyword).
88    // Long strings:  0 = hash not yet computed; 1 = hash is valid.
89    extra: Cell<u8>,
90}
91
92impl LuaStringImpl {
93    /// Returns the string's bytes.
94    pub fn as_bytes(&self) -> &[u8] {
95        &self.bytes
96    }
97
98    /// Returns the byte length of the string.
99    pub fn len(&self) -> usize {
100        self.bytes.len()
101    }
102
103    /// Returns `true` if this is a long (non-interned) string.
104    pub fn is_long(&self) -> bool {
105        self.kind == StringKind::Long
106    }
107
108    /// Returns `true` if this is a short (interned) string.
109    pub fn is_short(&self) -> bool {
110        self.kind == StringKind::Short
111    }
112
113    /// Returns `true` if this short string is a Lua reserved word.
114    pub fn is_reserved_word(&self) -> bool {
115        self.kind == StringKind::Short && self.extra.get() > 0
116    }
117
118    /// GC color predicate. `LuaStringImpl` values are never registered with
119    /// the tracing collector (see the module doc), so this always returns
120    /// `false`.
121    pub fn is_white(&self) -> bool {
122        false
123    }
124
125    /// Flip GC color from white to the current non-white (resurrect a dead
126    /// object). No-op; see [`Self::is_white`].
127    pub fn flip_white(&self) {
128    }
129}
130
131impl PartialEq for LuaStringImpl {
132    /// Equality for Lua strings.
133    ///
134    /// For short strings (interned), pointer equality via `Rc::ptr_eq` is sufficient
135    /// and matches `eqshrstr` in C.  For long strings, we fall back to byte
136    /// comparison, matching `luaS_eqlngstr` in C.
137    fn eq(&self, other: &Self) -> bool {
138        if self.kind == StringKind::Short && other.kind == StringKind::Short {
139            Rc::ptr_eq(&self.bytes, &other.bytes)
140        } else {
141            self.bytes == other.bytes
142        }
143    }
144}
145
146impl Eq for LuaStringImpl {}
147
148// ── StringPool ───────────────────────────────────────────────────────────────
149//
150// Corresponds to C's `stringtable`, which used an open-addressing hash table
151// where each bucket was the head of an intrusive singly-linked list threaded
152// through `TString.u.hnext`. The `HashMap` here replaces both the bucket
153// array and the chain: it provides O(1) average-case lookup, automatic
154// rehashing, and eliminates the need for `tablerehash`.
155//
156// `nuse` is redundant with `map.len()`; kept for parity with the C
157// invariants that other code in this module checks (e.g. `growstrtab` tests
158// `nuse >= size`).
159
160/// Intern table for short Lua strings.  Lives on `GlobalState`.
161pub struct StringPool {
162    // Keyed by owned byte slice; lookup by `&[u8]` via Borrow<[u8]>.
163    map: HashMap<Box<[u8]>, GcRef<LuaStringImpl>>,
164
165    nuse: usize,
166
167    // In Rust, HashMap manages its own capacity; this tracks the last requested size.
168    size: usize,
169}
170
171impl StringPool {
172    /// Create an empty pool with `MIN_STR_TAB_SIZE` preallocated capacity.
173    pub fn new() -> Self {
174        StringPool {
175            map: HashMap::with_capacity(MIN_STR_TAB_SIZE),
176            nuse: 0,
177            size: MIN_STR_TAB_SIZE,
178        }
179    }
180}
181
182impl Default for StringPool {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188// ── LuaUserData ──────────────────────────────────────────────────────────────
189
190/// Corresponds to C's `Udata`: a GC-tracked object carrying a raw byte
191/// payload plus optional Lua user values and an optional metatable.
192///
193/// Never constructed: `metatable`/`uv` are still placeholder `()` types
194/// rather than `GcRef<LuaTable>`/`LuaValue`, and no call site builds a
195/// `LuaUserDataImpl`. The userdata type actually used throughout the VM is
196/// `lua_types::userdata::LuaUserData`.
197pub struct LuaUserDataImpl {
198    pub len: usize,
199    pub nuvalue: u16,
200    pub metatable: Option<()>,
201    pub uv: Vec<()>,
202    // The raw byte payload; C accessed the equivalent via udatamemoffset
203    // pointer arithmetic on a flexible array member.
204    pub data: Box<[u8]>,
205}
206
207// ── Public functions ───────────────────────────────────────────────────────────
208
209// lstring.h: LUAI_FUNC → pub(crate)
210/// Hash a byte string with a seed using Lua's FNV-style hash.
211///
212/// This is a pure function with no allocations.  The algorithm XORs shifts and
213/// additions over each byte in reverse order, seeded by `seed ^ len`. Mirrors
214/// C's `luaS_hash`.
215///
216/// C parenthesises `(h<<5)` and `(h>>2)` explicitly, so the outer additions
217/// are unambiguous despite C's `<<`/`>>` having lower precedence than `+`.
218/// In Rust `<<` and `>>` have higher precedence than `+`, so the same
219/// expression is computed without extra parentheses; `wrapping_add` is used
220/// to match C's unsigned wrap-around arithmetic.
221pub(crate) fn hash_bytes(bytes: &[u8], seed: u32) -> u32 {
222    let mut h: u32 = seed ^ (bytes.len() as u32);
223
224    let mut l = bytes.len();
225    while l > 0 {
226        l -= 1;
227        h ^= (h << 5).wrapping_add(h >> 2).wrapping_add(bytes[l] as u32);
228    }
229
230    h
231}
232
233/// Resize the string intern table to approximately `nsize` buckets.
234///
235/// C's `tablerehash` walked the intrusive `hnext` chain in each bucket and
236/// redistributed `TString *` pointers into new bucket slots; that entire
237/// mechanism is unneeded here since `HashMap` rehashes itself automatically.
238/// When growing, `HashMap::reserve` hints the desired capacity. When
239/// shrinking, `HashMap::shrink_to` is a hint rather than a guarantee (C
240/// freed exact memory), used as an approximation of the C logic that would
241/// rehash entries out of the shrinking tail. The C function's graceful
242/// degradation on allocation failure (keep the current size) is preserved:
243/// `HashMap` will simply retain its existing capacity if memory is tight.
244pub(crate) fn resize(state: &mut LuaState, nsize: usize) {
245    let strt = &mut state.global_mut().strt;
246    let osize = strt.size;
247
248    if nsize > osize {
249        let additional = nsize.saturating_sub(strt.map.len());
250        strt.map.reserve(additional);
251    } else if nsize < osize {
252        strt.map.shrink_to(nsize);
253    }
254
255    strt.size = nsize;
256}
257
258// lstring.h: LUAI_FUNC → pub(crate)
259/// Initialise the string intern table and the API string cache.
260///
261/// Must be called exactly once during VM startup, before any strings are created.
262/// Pre-creates the memory-error message, then fills every cache slot with
263/// that same string.
264///
265/// C fixes the message in the GC (`luaC_fix`, marking it non-collectable);
266/// there is no equivalent call here (`state.gc().fix_object` is a no-op —
267/// see its doc in state.rs). The message instead stays alive for the life
268/// of the process simply because `GlobalState::memerrmsg` holds a
269/// permanent strong reference to it.
270pub(crate) fn init(state: &mut LuaState) -> Result<(), LuaError> {
271    // StringPool::new() sets the initial capacity to MIN_STR_TAB_SIZE,
272    // replacing both the C allocation and the tablerehash clear pass.
273    state.global_mut().strt = StringPool::new();
274
275    let memerrmsg = new_lstr(state, MEMERR_MSG)?;
276
277    let memerrmsg_lt = impl_to_lt(&memerrmsg);
278    state.global_mut().memerrmsg = memerrmsg_lt.clone();
279
280    for i in 0..STRCACHE_N {
281        for j in 0..STRCACHE_M {
282            state.global_mut().strcache[i][j] = memerrmsg_lt.clone();
283        }
284    }
285
286    Ok(())
287}
288
289/// Create or retrieve a Lua string from `bytes`.
290///
291/// If `bytes.len() <= MAX_SHORT_LEN` (40), the string is interned: an existing
292/// identical short string is returned if found, otherwise a new one is created
293/// and inserted into the intern table.
294///
295/// If `bytes.len() > MAX_SHORT_LEN`, a new long string is allocated each time
296/// (long strings are never interned).
297pub(crate) fn new_lstr(
298    state: &mut LuaState,
299    bytes: &[u8],
300) -> Result<GcRef<LuaStringImpl>, LuaError> {
301    if bytes.len() <= MAX_SHORT_LEN {
302        intern_short_str(state, bytes)
303    } else {
304        // `sizeof(TString)` is a C-specific per-object overhead; here we
305        // just check that the byte count fits within MAX_SIZE.
306        if bytes.len() >= MAX_SIZE {
307            return Err(LuaError::Memory);
308        }
309
310        let seed = state.global().seed;
311        let h = hash_bytes(bytes, seed);
312        let ts = create_str_obj(state, bytes, StringKind::Long, h);
313        Ok(ts)
314    }
315}
316
317// ── Private helpers ───────────────────────────────────────────────────────────
318
319/// Allocate and initialise a new `LuaStringImpl` with the given bytes, kind, and hash.
320///
321/// In C, `createstrobj` allocated uninitialised memory via `luaC_newobj` and set
322/// the header fields; the caller then filled the content via `memcpy`. Here the
323/// string is constructed directly from the provided `bytes`, eliminating the
324/// two-step pattern. `Rc<[u8]>` stores the bytes without C's nul terminator;
325/// callers that need a nul-terminated `*const u8` for FFI must use a temporary
326/// `CString` or equivalent.
327fn create_str_obj(
328    state: &mut LuaState,
329    bytes: &[u8],
330    kind: StringKind,
331    hash: u32,
332) -> GcRef<LuaStringImpl> {
333    // Creates a bare Rc<...>, never registered with any GC tracking list;
334    // harmless in practice since every caller of this module keeps its
335    // result alive permanently anyway (see the module doc).
336    let _ = state; // state needed for GC registration in Phase D
337    GcRef::new(LuaStringImpl {
338        hash: Cell::new(hash),
339        extra: Cell::new(0),
340        bytes: Rc::from(bytes),
341        kind,
342    })
343}
344
345/// Grow the string intern table.
346///
347/// C first attempts a full GC collection (`luaC_fullgc`) when the table is
348/// at its absolute maximum size, in case that frees up some short strings;
349/// this does not, and goes straight to the OOM error. In practice `nuse`
350/// never approaches `i32::MAX` here, since this pool only ever holds the
351/// one bootstrap OOM message (see the module doc).
352fn grow_str_tab(state: &mut LuaState) -> Result<(), LuaError> {
353    let nuse = state.global().strt.nuse;
354    if nuse == i32::MAX as usize {
355        if state.global().strt.nuse == i32::MAX as usize {
356            return Err(LuaError::Memory);
357        }
358    }
359
360    let size = state.global().strt.size;
361    if size <= MAX_STR_TAB / 2 {
362        resize(state, size * 2);
363    }
364
365    Ok(())
366}
367
368/// Look up `bytes` in the intern table; create and insert a new short string if
369/// not found.
370///
371/// C's bucket lookup walks an intrusive `hnext` chain and, on a hit, checks
372/// `isdead`/`changewhite` to resurrect a dead-but-not-yet-swept entry.
373/// `HashMap::get` replaces the chain walk; the resurrection check has no
374/// equivalent because `LuaStringImpl` values are plain `Rc`-held and kept
375/// alive by reference count, not tracked by the collector (see the module
376/// doc), so there is no dead-but-not-collected state to resurrect from.
377fn intern_short_str(state: &mut LuaState, bytes: &[u8]) -> Result<GcRef<LuaStringImpl>, LuaError> {
378    let seed = state.global().seed;
379    let h = hash_bytes(bytes, seed);
380
381    // Clone the existing GcRef<LuaStringImpl> so the immutable borrow on
382    // `state` ends before any mutable access below.
383    let existing = state.global().strt.map.get(bytes).cloned();
384    if let Some(ts) = existing {
385        return Ok(ts);
386    }
387
388    let needs_grow = {
389        let strt = &state.global().strt;
390        strt.nuse >= strt.size
391    };
392    if needs_grow {
393        grow_str_tab(state)?;
394    }
395
396    let ts = create_str_obj(state, bytes, StringKind::Short, h);
397
398    state
399        .global_mut()
400        .strt
401        .map
402        .insert(bytes.to_vec().into_boxed_slice(), ts.clone());
403    state.global_mut().strt.nuse += 1;
404
405    Ok(ts)
406}
407
408use lua_types::LuaError;