lua_vm/object.rs
1//! Generic functions over Lua objects.
2//!
3//! Ported from `reference/lua-5.4.7/src/lobject.c` (602 lines, ~20 functions).
4
5// TODO(port): resolve import paths — all `crate::*` paths below are speculative;
6// Phase B will reconcile against the actual module tree.
7use crate::state::LuaState;
8#[allow(unused_imports)] use crate::prelude::*;
9use lua_types::{LuaValue, GcRef, LuaString, StackIdx};
10use lua_types::error::LuaError;
11use lua_types::arith::ArithOp;
12use lua_types::value::F2Imod;
13
14// ──────────────────────────────────────────────────────────────────────────
15// Module-level constants
16// ──────────────────────────────────────────────────────────────────────────
17
18/// Maximum number of significant hex digits to read (avoids overflow even for
19/// single-precision floats).
20/// C: `#define MAXSIGDIG 30`
21const MAX_SIG_DIG: usize = 30;
22
23/// Maximum length of a numeral string accepted for conversion to a number.
24/// C: `#define L_MAXLENNUM 200`
25const L_MAX_LEN_NUM: usize = 200;
26
27/// Maximum size of a number-to-string conversion buffer.
28/// Accommodates both `%.14g` float formatting and `%lld` integer formatting.
29/// C: `#define MAXNUMBER2STR 44`
30pub const MAX_NUMBER_2_STR: usize = 44;
31
32/// Buffer size (bytes) for UTF-8 encoding; encoded backwards into this buffer.
33/// C: `#define UTF8BUFFSZ 8`
34pub const UTF8_BUF_SZ: usize = 8;
35
36/// Maximum length of a chunk source identifier in error messages.
37/// C: `LUA_IDSIZE` (typically 60 in luaconf.h).
38// TODO(port): verify against luaconf.h; defaulting to 60 here.
39pub const LUA_ID_SIZE: usize = 60;
40
41/// Internal buffer size for `push_vfstring`.
42/// C: `#define BUFVFS (LUA_IDSIZE + MAXNUMBER2STR + 95)`
43const BUF_VFS: usize = LUA_ID_SIZE + MAX_NUMBER_2_STR + 95;
44
45/// Truncation marker for long chunk source strings.
46/// C: `#define RETS "..."`
47const RETS: &[u8] = b"...";
48
49/// Prefix for [string "..."] chunk identifiers.
50/// C: `#define PRE "[string \""`
51const PRE: &[u8] = b"[string \"";
52
53/// Suffix for [string "..."] chunk identifiers.
54/// C: `#define POS "\"]"`
55const POS: &[u8] = b"\"]";
56
57// ──────────────────────────────────────────────────────────────────────────
58// ceil_log2
59// ──────────────────────────────────────────────────────────────────────────
60
61/// Computes `ceil(log2(x))`; returns the minimum `k` such that `2^k >= x`.
62///
63/// C: `int luaO_ceillog2 (unsigned int x)`
64pub fn ceil_log2(x: u32) -> i32 {
65 // C: static const lu_byte log_2[256] = { /* log_2[i] = ceil(log2(i - 1)) */ ... }
66 static LOG_2: [u8; 256] = [
67 0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
68 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
69 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
70 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
71 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
72 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
73 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
74 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
75 ];
76 // C: int l = 0; x--; while (x >= 256) { l += 8; x >>= 8; } return l + log_2[x];
77 let mut l: i32 = 0;
78 let mut x = x.wrapping_sub(1);
79 while x >= 256 {
80 l += 8;
81 x >>= 8;
82 }
83 l + LOG_2[x as usize] as i32
84}
85
86// ──────────────────────────────────────────────────────────────────────────
87// Integer arithmetic dispatcher
88// ──────────────────────────────────────────────────────────────────────────
89
90/// Performs integer arithmetic for opcode `op` on operands `v1`, `v2`.
91/// Returns `Result` because floor-mod and floor-div can raise on zero divisor.
92///
93/// C: `static lua_Integer intarith (lua_State *L, int op, lua_Integer v1, lua_Integer v2)`
94fn int_arith(state: &mut LuaState, op: ArithOp, v1: i64, v2: i64) -> Result<i64, LuaError> {
95 match op {
96 // C: case LUA_OPADD: return intop(+, v1, v2);
97 ArithOp::Add => Ok((v1 as u64).wrapping_add(v2 as u64) as i64),
98 // C: case LUA_OPSUB: return intop(-, v1, v2);
99 ArithOp::Sub => Ok((v1 as u64).wrapping_sub(v2 as u64) as i64),
100 // C: case LUA_OPMUL: return intop(*, v1, v2);
101 ArithOp::Mul => Ok((v1 as u64).wrapping_mul(v2 as u64) as i64),
102 // C: case LUA_OPMOD: return luaV_mod(L, v1, v2);
103 // TODO(port): confirm function path for integer floor-mod in lvm.rs
104 ArithOp::Mod => crate::vm::int_floor_mod(state, v1, v2),
105 // C: case LUA_OPIDIV: return luaV_idiv(L, v1, v2);
106 // TODO(port): confirm function path for integer floor-div in lvm.rs
107 ArithOp::Idiv => crate::vm::int_floor_div(state, v1, v2),
108 // C: case LUA_OPBAND: return intop(&, v1, v2);
109 ArithOp::Band => Ok(v1 & v2),
110 // C: case LUA_OPBOR: return intop(|, v1, v2);
111 ArithOp::Bor => Ok(v1 | v2),
112 // C: case LUA_OPBXOR: return intop(^, v1, v2);
113 ArithOp::Bxor => Ok(v1 ^ v2),
114 // C: case LUA_OPSHL: return luaV_shiftl(v1, v2);
115 // TODO(port): confirm function path for shift-left in lvm.rs
116 ArithOp::Shl => Ok(crate::vm::shiftl(v1, v2)),
117 // C: case LUA_OPSHR: return luaV_shiftr(v1, v2); [which is shiftl(v1, -v2)]
118 ArithOp::Shr => Ok(crate::vm::shiftl(v1, -v2)),
119 // C: case LUA_OPUNM: return intop(-, 0, v1);
120 ArithOp::Unm => Ok((0u64).wrapping_sub(v1 as u64) as i64),
121 // C: case LUA_OPBNOT: return intop(^, ~l_castS2U(0), v1);
122 // l_castS2U(0) → 0u64, ~0u64 = 0xFFFFFFFFFFFFFFFF = !0u64
123 ArithOp::Bnot => Ok((!0u64 ^ v1 as u64) as i64),
124 // C: default: lua_assert(0); return 0;
125 _ => {
126 debug_assert!(false, "int_arith called with non-integer op");
127 Ok(0)
128 }
129 }
130}
131
132// ──────────────────────────────────────────────────────────────────────────
133// Float arithmetic dispatcher
134// ──────────────────────────────────────────────────────────────────────────
135
136/// Performs float arithmetic for opcode `op` on operands `v1`, `v2`.
137/// Returns `Result` because float floor-mod can raise on zero divisor.
138///
139/// C: `static lua_Number numarith (lua_State *L, int op, lua_Number v1, lua_Number v2)`
140fn float_arith(state: &mut LuaState, op: ArithOp, v1: f64, v2: f64) -> Result<f64, LuaError> {
141 match op {
142 // C: case LUA_OPADD: return luai_numadd(L, v1, v2);
143 ArithOp::Add => Ok(v1 + v2),
144 // C: case LUA_OPSUB: return luai_numsub(L, v1, v2);
145 ArithOp::Sub => Ok(v1 - v2),
146 // C: case LUA_OPMUL: return luai_nummul(L, v1, v2);
147 ArithOp::Mul => Ok(v1 * v2),
148 // C: case LUA_OPDIV: return luai_numdiv(L, v1, v2);
149 ArithOp::Div => Ok(v1 / v2),
150 // C: case LUA_OPPOW: return luai_numpow(L, v1, v2);
151 ArithOp::Pow => Ok(if v2 == 2.0 { v1 * v1 } else { v1.powf(v2) }),
152 // C: case LUA_OPIDIV: return luai_numidiv(L, v1, v2);
153 ArithOp::Idiv => Ok((v1 / v2).floor()),
154 // C: case LUA_OPUNM: return luai_numunm(L, v1);
155 ArithOp::Unm => Ok(-v1),
156 // C: case LUA_OPMOD: return luaV_modf(L, v1, v2);
157 // TODO(port): confirm function path for float floor-mod in lvm.rs
158 ArithOp::Mod => crate::vm::float_floor_mod(state, v1, v2),
159 // C: default: lua_assert(0); return 0;
160 _ => {
161 debug_assert!(false, "float_arith called with non-float op");
162 Ok(0.0)
163 }
164 }
165}
166
167// ──────────────────────────────────────────────────────────────────────────
168// Raw arithmetic (no metamethods)
169// ──────────────────────────────────────────────────────────────────────────
170
171/// Attempts raw (no-metamethod) arithmetic on two Lua values.
172/// Writes the result to `res` and returns `true` on success, `false` if the
173/// operation cannot be performed with the given types (caller should invoke
174/// a metamethod instead).
175///
176/// C: `int luaO_rawarith (lua_State *L, int op, const TValue *p1, const TValue *p2, TValue *res)`
177pub fn raw_arith(
178 state: &mut LuaState,
179 op: ArithOp,
180 p1: &LuaValue,
181 p2: &LuaValue,
182 res: &mut LuaValue,
183) -> Result<bool, LuaError> {
184 match op {
185 // C: case LUA_OPBAND: case LUA_OPBOR: case LUA_OPBXOR:
186 // case LUA_OPSHL: case LUA_OPSHR: case LUA_OPBNOT: — integer-only ops
187 ArithOp::Band | ArithOp::Bor | ArithOp::Bxor
188 | ArithOp::Shl | ArithOp::Shr | ArithOp::Bnot => {
189 // C: if (tointegerns(p1, &i1) && tointegerns(p2, &i2)) {
190 // setivalue(res, intarith(L, op, i1, i2)); return 1; }
191 // else return 0;
192 if let (Some(i1), Some(i2)) = (
193 p1.to_integer_no_strconv(),
194 p2.to_integer_no_strconv(),
195 ) {
196 *res = LuaValue::Int(int_arith(state, op, i1, i2)?);
197 Ok(true)
198 } else {
199 Ok(false)
200 }
201 }
202
203 // C: case LUA_OPDIV: case LUA_OPPOW: — float-only ops
204 ArithOp::Div | ArithOp::Pow => {
205 // C: if (tonumberns(p1, n1) && tonumberns(p2, n2)) {
206 // setfltvalue(res, numarith(L, op, n1, n2)); return 1; }
207 // else return 0;
208 if let (Some(n1), Some(n2)) = (
209 p1.to_number_no_strconv(),
210 p2.to_number_no_strconv(),
211 ) {
212 *res = LuaValue::Float(float_arith(state, op, n1, n2)?);
213 Ok(true)
214 } else {
215 Ok(false)
216 }
217 }
218
219 // C: default: — prefer integer if both operands are integers; else try float.
220 _ => {
221 // C: if (ttisinteger(p1) && ttisinteger(p2)) {
222 // setivalue(res, intarith(L, op, ivalue(p1), ivalue(p2))); return 1; }
223 if let (LuaValue::Int(i1), LuaValue::Int(i2)) = (p1, p2) {
224 *res = LuaValue::Int(int_arith(state, op, *i1, *i2)?);
225 return Ok(true);
226 }
227 // C: else if (tonumberns(p1, n1) && tonumberns(p2, n2)) { ... }
228 if let (Some(n1), Some(n2)) = (
229 p1.to_number_no_strconv(),
230 p2.to_number_no_strconv(),
231 ) {
232 *res = LuaValue::Float(float_arith(state, op, n1, n2)?);
233 Ok(true)
234 } else {
235 // C: else return 0;
236 Ok(false)
237 }
238 }
239 }
240}
241
242// ──────────────────────────────────────────────────────────────────────────
243// Arithmetic (with metamethod fallback)
244// ──────────────────────────────────────────────────────────────────────────
245
246/// Performs arithmetic for opcode `op`, writing the result to the stack slot
247/// `res`. Falls back to a binary tag-method if raw arithmetic is not possible.
248///
249/// C: `void luaO_arith (lua_State *L, int op, const TValue *p1, const TValue *p2, StkId res)`
250pub fn arith(
251 state: &mut LuaState,
252 op: ArithOp,
253 p1: &LuaValue,
254 p2: &LuaValue,
255 res: StackIdx,
256) -> Result<(), LuaError> {
257 // C: if (!luaO_rawarith(L, op, p1, p2, s2v(res))) {
258 // luaT_trybinTM(L, p1, p2, res, cast(TMS, (op - LUA_OPADD) + TM_ADD)); }
259 //
260 // PORT NOTE: raw_arith writes to a local `temp` first; we then set the stack
261 // slot. This avoids holding a &mut borrow into the stack across try_bin_tm,
262 // which would violate the StackIdx rule (PORTING.md §2 #5).
263 let mut temp = LuaValue::Nil;
264 if raw_arith(state, op, p1, p2, &mut temp)? {
265 state.set_at(res, temp);
266 } else {
267 let _ = (p1, p2);
268 return Err(LuaError::runtime(format_args!(
269 "arithmetic metamethod dispatch not yet implemented for opcode {:?}", op
270 )));
271 }
272 Ok(())
273}
274
275// ──────────────────────────────────────────────────────────────────────────
276// hex_value
277// ──────────────────────────────────────────────────────────────────────────
278
279/// Converts a hexadecimal digit byte to its numeric value (0–15).
280/// Caller must ensure `c` is a valid hex digit.
281///
282/// C: `int luaO_hexavalue (int c)`
283pub fn hex_value(c: u8) -> u8 {
284 // C: if (lisdigit(c)) return c - '0'; else return (ltolower(c) - 'a') + 10;
285 if c.is_ascii_digit() {
286 c - b'0'
287 } else {
288 c.to_ascii_lowercase() - b'a' + 10
289 }
290}
291
292// ──────────────────────────────────────────────────────────────────────────
293// Sign helper
294// ──────────────────────────────────────────────────────────────────────────
295
296/// Checks for and consumes a leading sign byte (`+` or `-`) in `s` starting
297/// at `*idx`. Returns `true` if a minus sign was consumed.
298///
299/// C: `static int isneg (const char **s)`
300fn is_neg(s: &[u8], idx: &mut usize) -> bool {
301 // C: if (**s == '-') { (*s)++; return 1; }
302 // else if (**s == '+') (*s)++;
303 // return 0;
304 if *idx < s.len() && s[*idx] == b'-' {
305 *idx += 1;
306 return true;
307 }
308 if *idx < s.len() && s[*idx] == b'+' {
309 *idx += 1;
310 }
311 false
312}
313
314// ──────────────────────────────────────────────────────────────────────────
315// Hexadecimal float parser
316// ──────────────────────────────────────────────────────────────────────────
317
318/// Converts a hexadecimal float literal (C99 `0x…p…` form) in `s` to `f64`.
319/// Returns `Some((value, end_index))` on success, `None` on failure.
320///
321/// C: `static lua_Number lua_strx2number (const char *s, char **endptr)`
322/// (conditionally compiled when the platform doesn't provide it)
323fn str_x2number(s: &[u8]) -> Option<(f64, usize)> {
324 let mut idx = 0;
325 // C: while (lisspace(cast_uchar(*s))) s++; — skip leading spaces
326 while idx < s.len() && s[idx].is_ascii_whitespace() {
327 idx += 1;
328 }
329 // C: neg = isneg(&s);
330 let neg = is_neg(s, &mut idx);
331 // C: if (!(*s == '0' && (*(s + 1) == 'x' || *(s + 1) == 'X'))) return 0.0;
332 if idx + 1 >= s.len() || s[idx] != b'0' || (s[idx + 1] != b'x' && s[idx + 1] != b'X') {
333 return None;
334 }
335 // C: for (s += 2; ; s++) { ... } — skip '0x' and read mantissa digits
336 idx += 2;
337 let mut r: f64 = 0.0;
338 let mut sigdig: usize = 0;
339 let mut nosigdig: usize = 0;
340 let mut e: i32 = 0;
341 let mut hasdot = false;
342
343 // PORT NOTE: `lua_getlocaledecpoint()` returns the locale decimal separator.
344 // Rust has no locale; we always treat '.' as the separator here.
345 let dot = b'.';
346
347 loop {
348 if idx >= s.len() {
349 break;
350 }
351 let ch = s[idx];
352 if ch == dot {
353 // C: if (hasdot) break; else hasdot = 1;
354 if hasdot {
355 break;
356 }
357 hasdot = true;
358 } else if ch.is_ascii_hexdigit() {
359 // C: if (sigdig == 0 && *s == '0') nosigdig++;
360 // else if (++sigdig <= MAXSIGDIG) r = (r * 16.0) + luaO_hexavalue(*s);
361 // else e++;
362 // if (hasdot) e--;
363 if sigdig == 0 && ch == b'0' {
364 nosigdig += 1;
365 } else if {
366 sigdig += 1;
367 sigdig <= MAX_SIG_DIG
368 } {
369 r = r * 16.0 + hex_value(ch) as f64;
370 } else {
371 e += 1;
372 }
373 if hasdot {
374 e -= 1;
375 }
376 } else {
377 break;
378 }
379 idx += 1;
380 }
381
382 // C: if (nosigdig + sigdig == 0) return 0.0; — no digits at all
383 if nosigdig + sigdig == 0 {
384 return None;
385 }
386 // `idx` is now the valid end so far
387 let valid_end = idx;
388 // C: e *= 4; — each hex digit is 4 bits
389 e *= 4;
390
391 // C: if (*s == 'p' || *s == 'P') { ... read exponent ... }
392 if idx < s.len() && (s[idx] == b'p' || s[idx] == b'P') {
393 idx += 1; // skip 'p'/'P'
394 let neg1 = is_neg(s, &mut idx);
395 // C: if (!lisdigit(cast_uchar(*s))) return 0.0;
396 if idx >= s.len() || !s[idx].is_ascii_digit() {
397 return None;
398 }
399 let mut exp1: i32 = 0;
400 // C: while (lisdigit(cast_uchar(*s))) exp1 = exp1 * 10 + *(s++) - '0';
401 while idx < s.len() && s[idx].is_ascii_digit() {
402 exp1 = exp1 * 10 + (s[idx] - b'0') as i32;
403 idx += 1;
404 }
405 if neg1 {
406 exp1 = -exp1;
407 }
408 e += exp1;
409 // update valid end: the exponent consumed up to here
410 // (valid_end is updated to idx below)
411 }
412 // C: if (neg) r = -r;
413 // C: return l_mathop(ldexp)(r, e);
414 let result = if neg { -r } else { r };
415 Some((result * (2.0f64).powi(e), idx))
416}
417
418// ──────────────────────────────────────────────────────────────────────────
419// String-to-float helpers
420// ──────────────────────────────────────────────────────────────────────────
421
422/// Inner conversion: tries to parse the bytes `s` as a float using the given
423/// `mode` (`b'x'` for hex, anything else for decimal).
424/// Returns `Some((value, end_index))` or `None`.
425///
426/// C: `static const char *l_str2dloc (const char *s, lua_Number *result, int mode)`
427fn str2dloc(s: &[u8], mode: u8) -> Option<(f64, usize)> {
428 // C: *result = (mode == 'x') ? lua_strx2number(s, &endptr) : lua_str2number(s, &endptr);
429 let (result, end) = if mode == b'x' {
430 str_x2number(s)?
431 } else {
432 // C: lua_str2number(s, &endptr) — essentially strtod.
433 // PORT NOTE: from_utf8 used here because numeric string literals are
434 // guaranteed to be ASCII (a strict subset of UTF-8).
435 // TODO(port): replace with a bytes-native float parser in Phase B
436 // (e.g., the `fast-float` crate) to satisfy the from_utf8 ban fully.
437 let text = core::str::from_utf8(s).ok()?;
438 let trimmed = text.trim();
439 // Reject "inf", "infinity", "nan" — Lua does not accept these.
440 let lower = trimmed.to_ascii_lowercase();
441 if lower.starts_with("inf") || lower.starts_with("nan") {
442 return None;
443 }
444 let f: f64 = trimmed.parse().ok()?;
445 (f, s.len()) // strtod parses as many chars as possible; we consumed all
446 };
447 // C: if (endptr == s) return NULL; — nothing recognized
448 if end == 0 {
449 return None;
450 }
451 // C: while (lisspace(cast_uchar(*endptr))) endptr++;
452 let mut end2 = end;
453 while end2 < s.len() && s[end2].is_ascii_whitespace() {
454 end2 += 1;
455 }
456 // C: return (*endptr == '\0') ? endptr : NULL; — OK iff no trailing chars
457 if end2 == s.len() {
458 Some((result, end2))
459 } else {
460 None
461 }
462}
463
464/// Converts bytes `s` to a Lua float value.
465/// Returns `Some((value, end_index))` on success, `None` on failure.
466///
467/// C: `static const char *l_str2d (const char *s, lua_Number *result)`
468fn str2d(s: &[u8]) -> Option<(f64, usize)> {
469 // C: const char *pmode = strpbrk(s, ".xXnN");
470 // int mode = pmode ? ltolower(cast_uchar(*pmode)) : 0;
471 let pmode = s.iter().position(|&b| {
472 b == b'.' || b == b'x' || b == b'X' || b == b'n' || b == b'N'
473 });
474 let mode = pmode.map(|i| s[i].to_ascii_lowercase()).unwrap_or(0);
475
476 // C: if (mode == 'n') return NULL; — reject 'inf' and 'nan'
477 if mode == b'n' {
478 return None;
479 }
480
481 // C: endptr = l_str2dloc(s, result, mode);
482 if let Some(result) = str2dloc(s, mode) {
483 return Some(result);
484 }
485
486 // C: if (endptr == NULL) { ... try replacing '.' with locale decimal point ... }
487 // PORT NOTE: Lua retries by replacing '.' with the locale decimal separator.
488 // Rust has no locale support; we skip this retry path and always use '.'.
489 // TODO(port): add locale retry if locale-aware float parsing is needed.
490
491 None
492}
493
494// ──────────────────────────────────────────────────────────────────────────
495// String-to-integer helper
496// ──────────────────────────────────────────────────────────────────────────
497
498/// Converts bytes `s` to a Lua integer value (decimal or `0x` hex).
499/// Returns `Some(value)` on success (the entire byte slice was consumed),
500/// `None` on failure or overflow.
501///
502/// C: `static const char *l_str2int (const char *s, lua_Integer *result)`
503fn str2int(s: &[u8]) -> Option<i64> {
504 let mut idx = 0;
505 // C: while (lisspace(cast_uchar(*s))) s++;
506 while idx < s.len() && s[idx].is_ascii_whitespace() {
507 idx += 1;
508 }
509 // C: neg = isneg(&s);
510 let neg = is_neg(s, &mut idx);
511
512 let mut a: u64 = 0;
513 let mut empty = true;
514
515 if idx + 1 < s.len() && s[idx] == b'0' && (s[idx + 1] == b'x' || s[idx + 1] == b'X') {
516 // C: s += 2; for (; lisxdigit(cast_uchar(*s)); s++) { a = a * 16 + ...; empty = 0; }
517 idx += 2;
518 while idx < s.len() && s[idx].is_ascii_hexdigit() {
519 a = a.wrapping_mul(16).wrapping_add(hex_value(s[idx]) as u64);
520 empty = false;
521 idx += 1;
522 }
523 } else {
524 // C: decimal loop with overflow check:
525 // MAXBY10 = cast(lua_Unsigned, LUA_MAXINTEGER / 10)
526 // MAXLASTD = cast_int(LUA_MAXINTEGER % 10)
527 // if (a >= MAXBY10 && (a > MAXBY10 || d > MAXLASTD + neg)) return NULL;
528 const MAX_BY10: u64 = (i64::MAX / 10) as u64;
529 const MAX_LAST_D: u64 = (i64::MAX % 10) as u64;
530 while idx < s.len() && s[idx].is_ascii_digit() {
531 let d = (s[idx] - b'0') as u64;
532 if a >= MAX_BY10 && (a > MAX_BY10 || d > MAX_LAST_D + if neg { 1 } else { 0 }) {
533 return None; // overflow
534 }
535 a = a.wrapping_mul(10).wrapping_add(d);
536 empty = false;
537 idx += 1;
538 }
539 }
540
541 // C: while (lisspace(cast_uchar(*s))) s++;
542 while idx < s.len() && s[idx].is_ascii_whitespace() {
543 idx += 1;
544 }
545 // C: if (empty || *s != '\0') return NULL;
546 if empty || idx != s.len() {
547 return None;
548 }
549 // C: *result = l_castU2S((neg) ? 0u - a : a);
550 let result = if neg { (0u64).wrapping_sub(a) as i64 } else { a as i64 };
551 Some(result)
552}
553
554// ──────────────────────────────────────────────────────────────────────────
555// str2num — main public string-to-number conversion
556// ──────────────────────────────────────────────────────────────────────────
557
558/// Tries to convert the byte string `s` to a Lua number (integer first, then
559/// float). Writes the result to `o` and returns `consumed_bytes + 1` on
560/// success (matching the C convention of including the null terminator in the
561/// count), or `0` on failure.
562///
563/// C: `size_t luaO_str2num (const char *s, TValue *o)`
564pub fn str2num(s: &[u8], o: &mut LuaValue) -> usize {
565 // C: if ((e = l_str2int(s, &i)) != NULL) { setivalue(o, i); }
566 if let Some(i) = str2int(s) {
567 *o = LuaValue::Int(i);
568 return s.len() + 1; // entire string consumed; +1 for C null-terminator convention
569 }
570 // C: else if ((e = l_str2d(s, &n)) != NULL) { setfltvalue(o, n); }
571 if let Some((n, end)) = str2d(s) {
572 *o = LuaValue::Float(n);
573 return end + 1;
574 }
575 // C: else return 0;
576 0
577}
578
579// ──────────────────────────────────────────────────────────────────────────
580// UTF-8 encoder
581// ──────────────────────────────────────────────────────────────────────────
582
583/// Encodes Unicode codepoint `x` as UTF-8 into `buff` (filled backwards from
584/// index `UTF8_BUF_SZ - 1`). Returns the number of bytes written.
585/// The valid bytes occupy `buff[UTF8_BUF_SZ - n .. UTF8_BUF_SZ]`.
586///
587/// C: `int luaO_utf8esc (char *buff, unsigned long x)`
588pub fn utf8_esc(buff: &mut [u8; UTF8_BUF_SZ], x: u32) -> usize {
589 // C: lua_assert(x <= 0x7FFFFFFFu);
590 debug_assert!(x <= 0x7FFF_FFFF, "codepoint out of range");
591 let mut n: usize = 1;
592 if x < 0x80 {
593 // C: buff[UTF8BUFFSZ - 1] = cast_char(x);
594 buff[UTF8_BUF_SZ - 1] = x as u8;
595 } else {
596 // C: unsigned int mfb = 0x3f; — max-fits-in-first-byte mask
597 let mut mfb: u32 = 0x3f;
598 let mut x = x;
599 loop {
600 // C: buff[UTF8BUFFSZ - (n++)] = cast_char(0x80 | (x & 0x3f));
601 buff[UTF8_BUF_SZ - n] = 0x80 | (x & 0x3f) as u8;
602 n += 1;
603 x >>= 6;
604 mfb >>= 1;
605 // C: while (x > mfb);
606 if x <= mfb {
607 break;
608 }
609 }
610 // C: buff[UTF8BUFFSZ - n] = cast_char((~mfb << 1) | x);
611 buff[UTF8_BUF_SZ - n] = ((!mfb << 1) | x) as u8;
612 }
613 n
614}
615
616// ──────────────────────────────────────────────────────────────────────────
617// Number → string conversion
618// ──────────────────────────────────────────────────────────────────────────
619
620/// Formats `f` as C's `printf("%.14g", f)` would, returning the bytes.
621///
622/// PORT NOTE: Rust has no built-in `%g` format. This replicates the C99
623/// `%g` algorithm with precision 14: pick scientific or fixed-point based
624/// on the value's exponent, strip trailing zeros, normalize the exponent
625/// to `e[+-]NN` with at least two digits (matching C's output).
626fn fmt_g14(f: f64) -> Vec<u8> {
627 if f.is_nan() {
628 return b"nan".to_vec();
629 }
630 if f.is_infinite() {
631 return if f > 0.0 { b"inf".to_vec() } else { b"-inf".to_vec() };
632 }
633 if f == 0.0 {
634 return if f.is_sign_negative() { b"-0".to_vec() } else { b"0".to_vec() };
635 }
636
637 let precision: i32 = 14;
638 let abs = f.abs();
639 let exp = abs.log10().floor() as i32;
640
641 let s = if exp < -4 || exp >= precision {
642 let mantissa_decimals = (precision - 1) as usize;
643 let raw = format!("{:.*e}", mantissa_decimals, f);
644 let e_idx = raw.find('e').expect("Rust scientific format always contains 'e'");
645 let mantissa = strip_fixed_trailing_zeros(&raw[..e_idx]);
646 let exp_num: i32 = raw[e_idx + 1..].parse().expect("Rust formats integer exponents");
647 let sign = if exp_num < 0 { '-' } else { '+' };
648 let abs_exp = exp_num.abs();
649 if abs_exp < 10 {
650 format!("{}e{}0{}", mantissa, sign, abs_exp)
651 } else {
652 format!("{}e{}{}", mantissa, sign, abs_exp)
653 }
654 } else {
655 let decimals = (precision - 1 - exp).max(0) as usize;
656 let raw = format!("{:.*}", decimals, f);
657 strip_fixed_trailing_zeros(&raw)
658 };
659
660 s.into_bytes()
661}
662
663fn strip_fixed_trailing_zeros(s: &str) -> String {
664 if !s.contains('.') {
665 return s.to_string();
666 }
667 let mut out = s.to_string();
668 while out.ends_with('0') {
669 out.pop();
670 }
671 if out.ends_with('.') {
672 out.pop();
673 }
674 out
675}
676
677/// Formats the numeric `LuaValue` `val` (must be Int or Float) into a byte
678/// buffer and returns it.
679///
680/// C: `static int tostringbuff (TValue *obj, char *buff)`
681fn number_to_str_buf(val: &LuaValue) -> Vec<u8> {
682 // C: lua_assert(ttisnumber(obj));
683 debug_assert!(
684 matches!(val, LuaValue::Int(_) | LuaValue::Float(_)),
685 "number_to_str_buf: value is not a number"
686 );
687
688 match val {
689 LuaValue::Int(i) => {
690 // C: len = lua_integer2str(buff, MAXNUMBER2STR, ivalue(obj));
691 // lua_integer2str → l_sprintf with LUA_INTEGER_FMT ("%lld")
692 // PORT NOTE: using Rust's default i64 Display formatting, which
693 // matches C's `%lld` for all values in [i64::MIN, i64::MAX].
694 let s = format!("{}", i);
695 s.into_bytes()
696 }
697 LuaValue::Float(f) => {
698 let mut bytes = fmt_g14(*f);
699
700 let looks_like_int = bytes.iter().all(|&b| b == b'-' || b.is_ascii_digit());
701 if looks_like_int {
702 bytes.push(b'.');
703 bytes.push(b'0');
704 }
705 bytes
706 }
707 // Unreachable — guarded by debug_assert above.
708 _ => Vec::new(),
709 }
710}
711
712/// Converts a numeric `LuaValue` to an interned `LuaString`, returning a
713/// `GcRef<LuaString>` handle. Callers are responsible for updating the
714/// `LuaValue` (or stack slot) with `LuaValue::Str(s)`.
715///
716/// C: `void luaO_tostring (lua_State *L, TValue *obj)` which modifies `obj`
717/// in place; in Rust we return the string because holding `&mut LuaValue`
718/// across a `state.intern_str` call would borrow `state` twice.
719pub fn num_to_string(state: &mut LuaState, val: &LuaValue) -> Result<GcRef<LuaString>, LuaError> {
720 // C: char buff[MAXNUMBER2STR];
721 // int len = tostringbuff(obj, buff);
722 // setsvalue(L, obj, luaS_newlstr(L, buff, len));
723 let bytes = number_to_str_buf(val);
724 // TODO(port): state.intern_str path needs to be confirmed in lua-vm
725 state.intern_str(&bytes)
726}
727
728// ──────────────────────────────────────────────────────────────────────────
729// push_vfstring infrastructure
730// ──────────────────────────────────────────────────────────────────────────
731
732/// Typed format argument for `push_vfstring`.
733///
734/// PORT NOTE: replaces the C `va_list` variadic interface. C callers of
735/// `luaO_pushfstring(L, fmt, ...)` must be updated to pass structured
736/// `FmtArg` slices. The format-string scanning logic is preserved in
737/// `push_vfstring`; only the argument-list type changes.
738pub enum FmtArg<'a> {
739 /// `%s` — a byte string (replaces `const char *` from va_list).
740 Str(&'a [u8]),
741 /// `%c` — a single byte character.
742 Char(u8),
743 /// `%d` — a 32-bit integer.
744 Int(i32),
745 /// `%I` — a Lua integer (i64).
746 LuaInt(i64),
747 /// `%f` — a Lua float (f64).
748 Float(f64),
749 /// `%U` — a Unicode codepoint (u32), encoded as UTF-8.
750 Utf8Codepoint(u32),
751 // TODO(port): %p (pointer) omitted — raw pointer in safe Rust is not allowed
752 // outside explicit unsafe-budget crates. Callers that need pointer formatting must handle
753 // it separately and pass the pre-formatted bytes as FmtArg::Str.
754}
755
756/// Internal accumulator for `push_vfstring`.
757///
758/// C: `typedef struct BuffFS { lua_State *L; int pushed; int blen; char space[BUFVFS]; } BuffFS;`
759///
760/// PORT NOTE: `space` is a `Vec<u8>` rather than a fixed-size array; the
761/// BUF_VFS threshold is still respected for flushing behaviour.
762struct BufFs {
763 /// Whether at least one partial result has been pushed onto the stack.
764 pushed: bool,
765 /// Accumulated bytes not yet pushed to the stack.
766 space: Vec<u8>,
767}
768
769impl BufFs {
770 fn new() -> Self {
771 BufFs {
772 pushed: false,
773 space: Vec::with_capacity(BUF_VFS),
774 }
775 }
776}
777
778/// Pushes the byte string `str_bytes` to the Lua stack and concatenates with
779/// any prior partial result.
780///
781/// C: `static void pushstr (BuffFS *buff, const char *str, size_t lstr)`
782fn pushstr(buf: &mut BufFs, state: &mut LuaState, str_bytes: &[u8]) -> Result<(), LuaError> {
783 // C: setsvalue2s(L, L->top.p, luaS_newlstr(L, str, lstr));
784 // L->top.p++;
785 // if (!buff->pushed) buff->pushed = 1;
786 // else luaV_concat(L, 2);
787 let s = state.intern_str(str_bytes)?;
788 state.push(LuaValue::Str(s));
789 if !buf.pushed {
790 buf.pushed = true;
791 } else {
792 // C: luaV_concat(L, 2);
793 // TODO(port): confirm path to string concatenation helper in lvm.rs
794 crate::vm::concat(state, 2)?;
795 }
796 Ok(())
797}
798
799/// Flushes the internal buffer to the Lua stack.
800///
801/// C: `static void clearbuff (BuffFS *buff)`
802fn clearbuff(buf: &mut BufFs, state: &mut LuaState) -> Result<(), LuaError> {
803 // C: pushstr(buff, buff->space, buff->blen); buff->blen = 0;
804 let bytes: Vec<u8> = buf.space.drain(..).collect();
805 pushstr(buf, state, &bytes)
806}
807
808/// Adds `str_bytes` to the internal buffer, flushing first if it won't fit.
809///
810/// C: `static void addstr2buff (BuffFS *buff, const char *str, size_t slen)`
811fn addstr2buff(buf: &mut BufFs, state: &mut LuaState, str_bytes: &[u8]) -> Result<(), LuaError> {
812 // C: if (slen <= BUFVFS) { ... memcpy ... addsize(buff, slen); }
813 // else { clearbuff; pushstr directly; }
814 if str_bytes.len() <= BUF_VFS {
815 // C: if (sz > BUFVFS - buff->blen) clearbuff(buff);
816 if str_bytes.len() > BUF_VFS - buf.space.len() {
817 clearbuff(buf, state)?;
818 }
819 buf.space.extend_from_slice(str_bytes);
820 } else {
821 clearbuff(buf, state)?;
822 pushstr(buf, state, str_bytes)?;
823 }
824 Ok(())
825}
826
827/// Formats the numeric value `num` and appends it to the buffer.
828///
829/// C: `static void addnum2buff (BuffFS *buff, TValue *num)`
830fn addnum2buff(buf: &mut BufFs, state: &mut LuaState, num: &LuaValue) -> Result<(), LuaError> {
831 // C: char *numbuff = getbuff(buff, MAXNUMBER2STR);
832 // int len = tostringbuff(num, numbuff);
833 // addsize(buff, len);
834 let bytes = number_to_str_buf(num);
835 addstr2buff(buf, state, &bytes)
836}
837
838// ──────────────────────────────────────────────────────────────────────────
839// push_vfstring / push_fstring
840// ──────────────────────────────────────────────────────────────────────────
841
842/// Builds a formatted Lua string from a format byte string and structured
843/// arguments, pushes it onto the stack, and returns the top-of-stack value.
844///
845/// Supported format specifiers (same subset as C's `luaO_pushvfstring`):
846/// `%s`, `%c`, `%d`, `%I`, `%f`, `%U`, `%%`.
847/// `%p` is **not** supported; see [`FmtArg`] documentation.
848///
849/// C: `const char *luaO_pushvfstring (lua_State *L, const char *fmt, va_list argp)`
850///
851/// PORT NOTE: `va_list` replaced by `&[FmtArg]`. Call sites that previously
852/// passed variadic arguments must be updated to build a `&[FmtArg]` slice.
853pub fn push_vfstring<'a>(
854 state: &mut LuaState,
855 fmt: &[u8],
856 args: &[FmtArg<'a>],
857) -> Result<GcRef<LuaString>, LuaError> {
858 let mut buf = BufFs::new();
859 let mut arg_idx = 0usize;
860 let mut pos = 0usize;
861
862 // C: while ((e = strchr(fmt, '%')) != NULL) { ... }
863 while let Some(rel) = fmt[pos..].iter().position(|&b| b == b'%') {
864 let e = pos + rel;
865 // C: addstr2buff(&buff, fmt, e - fmt);
866 addstr2buff(&mut buf, state, &fmt[pos..e])?;
867
868 // C: switch (*(e + 1)) { ... }
869 let spec = if e + 1 < fmt.len() { fmt[e + 1] } else { 0 };
870 match spec {
871 b's' => {
872 // C: const char *s = va_arg(argp, char *); if (!s) s = "(null)";
873 // addstr2buff(&buff, s, strlen(s));
874 let s = match args.get(arg_idx) {
875 Some(FmtArg::Str(b)) => *b,
876 None => b"(null)",
877 _ => b"(null)",
878 };
879 arg_idx += 1;
880 addstr2buff(&mut buf, state, s)?;
881 }
882 b'c' => {
883 // C: char c = cast_uchar(va_arg(argp, int));
884 // addstr2buff(&buff, &c, sizeof(char));
885 let c = match args.get(arg_idx) {
886 Some(FmtArg::Char(b)) => *b,
887 _ => b'?',
888 };
889 arg_idx += 1;
890 addstr2buff(&mut buf, state, &[c])?;
891 }
892 b'd' => {
893 // C: TValue num; setivalue(&num, va_arg(argp, int)); addnum2buff(&buff, &num);
894 let n = match args.get(arg_idx) {
895 Some(FmtArg::Int(i)) => *i as i64,
896 _ => 0,
897 };
898 arg_idx += 1;
899 addnum2buff(&mut buf, state, &LuaValue::Int(n))?;
900 }
901 b'I' => {
902 // C: TValue num; setivalue(&num, cast(lua_Integer, va_arg(argp, l_uacInt)));
903 // addnum2buff(&buff, &num);
904 let n = match args.get(arg_idx) {
905 Some(FmtArg::LuaInt(i)) => *i,
906 _ => 0,
907 };
908 arg_idx += 1;
909 addnum2buff(&mut buf, state, &LuaValue::Int(n))?;
910 }
911 b'f' => {
912 // C: TValue num; setfltvalue(&num, cast_num(va_arg(argp, l_uacNumber)));
913 // addnum2buff(&buff, &num);
914 let f = match args.get(arg_idx) {
915 Some(FmtArg::Float(f)) => *f,
916 _ => 0.0,
917 };
918 arg_idx += 1;
919 addnum2buff(&mut buf, state, &LuaValue::Float(f))?;
920 }
921 b'p' => {
922 // C: void *p = va_arg(argp, void *); int len = lua_pointer2str(bf, sz, p);
923 // TODO(port): %p pointer formatting not implemented in safe Rust;
924 // callers that need it should pre-format the pointer and pass FmtArg::Str.
925 arg_idx += 1; // consume the argument slot
926 addstr2buff(&mut buf, state, b"<ptr>")?;
927 }
928 b'U' => {
929 // C: char bf[UTF8BUFFSZ]; int len = luaO_utf8esc(bf, va_arg(argp, long));
930 // addstr2buff(&buff, bf + UTF8BUFFSZ - len, len);
931 let cp = match args.get(arg_idx) {
932 Some(FmtArg::Utf8Codepoint(u)) => *u,
933 _ => b'?' as u32,
934 };
935 arg_idx += 1;
936 let mut bf = [0u8; UTF8_BUF_SZ];
937 let n = utf8_esc(&mut bf, cp);
938 addstr2buff(&mut buf, state, &bf[UTF8_BUF_SZ - n..])?;
939 }
940 b'%' => {
941 // C: addstr2buff(&buff, "%", 1);
942 addstr2buff(&mut buf, state, b"%")?;
943 }
944 other => {
945 // C: luaG_runerror(L, "invalid option '%%%c' to 'lua_pushfstring'", *(e + 1));
946 return Err(LuaError::runtime(format_args!(
947 "invalid option '%%{}' to 'lua_pushfstring'",
948 other as char
949 )));
950 }
951 }
952 // C: fmt = e + 2; — skip '%' and the specifier
953 pos = e + 2;
954 }
955
956 // C: addstr2buff(&buff, fmt, strlen(fmt)); — rest of format string
957 addstr2buff(&mut buf, state, &fmt[pos..])?;
958 // C: clearbuff(&buff);
959 clearbuff(&mut buf, state)?;
960 // C: lua_assert(buff.pushed == 1);
961 debug_assert!(buf.pushed, "push_vfstring: no string was pushed");
962
963 // C: return getstr(tsvalue(s2v(L->top.p - 1)));
964 // Return the interned string at the top of the stack.
965 // PORT NOTE: in C this returns a `const char *` into the TString; in Rust
966 // we return the GcRef<LuaString> directly.
967 // TODO(port): state.peek_string_at_top() path needs to be confirmed.
968 Ok(state.peek_string_at_top())
969}
970
971/// Variadic entry point; delegates to `push_vfstring`.
972///
973/// C: `const char *luaO_pushfstring (lua_State *L, const char *fmt, ...)`
974///
975/// PORT NOTE: callers that previously used `luaO_pushfstring` for error
976/// messages should collapse the call into `LuaError::runtime(format_args!(...))`;
977/// see PORTING.md §4.2 and error_sites.tsv.
978pub fn push_fstring<'a>(
979 state: &mut LuaState,
980 fmt: &[u8],
981 args: &[FmtArg<'a>],
982) -> Result<GcRef<LuaString>, LuaError> {
983 // C: va_start(argp, fmt); msg = luaO_pushvfstring(L, fmt, argp); va_end(argp);
984 push_vfstring(state, fmt, args)
985}
986
987// ──────────────────────────────────────────────────────────────────────────
988// chunk_id — human-readable chunk identifier
989// ──────────────────────────────────────────────────────────────────────────
990
991/// Fills `out` with a human-readable identifier derived from `source` and
992/// returns the number of bytes written (not including any null terminator).
993///
994/// Rules (matching C):
995/// - `=...` → literal text (everything after `=`), truncated to `LUA_ID_SIZE - 1`.
996/// - `@...` → file name (everything after `@`), prefixed with `...` if too long.
997/// - anything else → `[string "..."]`, with the first line truncated.
998///
999/// C: `void luaO_chunkid (char *out, const char *source, size_t srclen)`
1000pub fn chunk_id(out: &mut [u8], source: &[u8]) -> usize {
1001 let bufflen = LUA_ID_SIZE;
1002 let mut written = 0usize;
1003
1004 let mut write_bytes = |out: &mut [u8], written: &mut usize, bytes: &[u8]| {
1005 let avail = out.len().saturating_sub(*written);
1006 let n = bytes.len().min(avail);
1007 out[*written..*written + n].copy_from_slice(&bytes[..n]);
1008 *written += n;
1009 };
1010
1011 let first = source.first().copied();
1012 let srclen = source.len();
1013
1014 match first {
1015 Some(b'=') => {
1016 let body = &source[1..];
1017 if srclen <= bufflen {
1018 write_bytes(out, &mut written, body);
1019 } else {
1020 write_bytes(out, &mut written, &body[..bufflen - 1]);
1021 if written < out.len() {
1022 out[written] = 0;
1023 }
1024 }
1025 }
1026 Some(b'@') => {
1027 let body = &source[1..];
1028 if srclen <= bufflen {
1029 write_bytes(out, &mut written, body);
1030 } else {
1031 write_bytes(out, &mut written, RETS);
1032 let tail_len = bufflen - RETS.len() - 1;
1033 let tail_start = body.len() - tail_len;
1034 write_bytes(out, &mut written, &body[tail_start..tail_start + tail_len]);
1035 }
1036 }
1037 _ => {
1038 let nl_pos = source.iter().position(|&b| b == b'\n');
1039 write_bytes(out, &mut written, PRE);
1040 let reserved = PRE.len() + RETS.len() + POS.len() + 1;
1041 let inner_limit = bufflen.saturating_sub(reserved);
1042
1043 if srclen < inner_limit && nl_pos.is_none() {
1044 write_bytes(out, &mut written, source);
1045 } else {
1046 let take = nl_pos.unwrap_or(srclen).min(inner_limit);
1047 write_bytes(out, &mut written, &source[..take]);
1048 write_bytes(out, &mut written, RETS);
1049 }
1050 write_bytes(out, &mut written, POS);
1051 }
1052 }
1053
1054 written
1055}
1056
1057// ──────────────────────────────────────────────────────────────────────────
1058// PORT STATUS
1059// source: src/lobject.c (602 lines, ~20 functions)
1060// target_crate: lua-vm
1061// confidence: medium
1062// todos: 15
1063// port_notes: 12
1064// unsafe_blocks: 0
1065// notes: All import paths are speculative (crate::state, lua_types::*);
1066// Phase B must reconcile. va_list replaced by FmtArg enum —
1067// call sites of push_fstring/push_vfstring need updating.
1068// Float formatting (%.14g) is approximated with {:.14e}; needs
1069// proper %g in Phase B. Locale decimal-point handling is
1070// stubbed (always '.'). str2dloc uses from_utf8 for ASCII
1071// number strings (flagged TODO). int_floor_mod, int_floor_div,
1072// shiftl, float_floor_mod, concat are assumed to exist in
1073// crate::vm; Phase B must confirm or create them.
1074// ──────────────────────────────────────────────────────────────────────────