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