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;