diff --git a/src/compiler/codegen.rs b/src/compiler/codegen.rs index d9c288b..4c06bad 100644 --- a/src/compiler/codegen.rs +++ b/src/compiler/codegen.rs @@ -1407,7 +1407,8 @@ impl Compiler { if v2 == 0.0 { return false; } - (v1 / v2).floor().mul_add(-v2, v1) + // PUC-Rio uses explicit arithmetic; mul_add can differ on some FPUs. + v1 - (v1 / v2).floor() * v2 } OpCode::Pow => v1.powf(v2), OpCode::Unm => -v1, diff --git a/src/compiler/lexer.rs b/src/compiler/lexer.rs index 4475e82..1dbe6ed 100644 --- a/src/compiler/lexer.rs +++ b/src/compiler/lexer.rs @@ -254,6 +254,15 @@ impl<'a> Lexer<'a> { }) } + /// Text of a string token read so far (opening delimiter + contents), as + /// PUC-Rio's `txtToken` shows it in "near '...'" messages. + fn partial_string(delimiter: u8, buf: &[u8]) -> String { + let mut v = Vec::with_capacity(buf.len() + 1); + v.push(delimiter); + v.extend_from_slice(buf); + String::from_utf8_lossy(&v).into_owned() + } + fn syntax_error_near(&self, msg: &str, near: &str) -> LuaError { LuaError::Syntax(SyntaxError { message: format!("{msg} near '{near}'"), @@ -615,7 +624,9 @@ impl<'a> Lexer<'a> { return Err(self.syntax_error_near("unfinished string", "")); } Some(b'\n') | Some(b'\r') => { - return Err(self.syntax_error_near("unfinished string", "")); + // PUC-Rio quotes the string read so far (txtToken). + let near = Self::partial_string(delimiter, &buf); + return Err(self.syntax_error_near("unfinished string", &near)); } Some(c) if c == delimiter => { self.advance(); // consume closing delimiter @@ -684,17 +695,19 @@ impl<'a> Lexer<'a> { } } if val > 255 { + let near = Self::partial_string(delimiter, &buf); return Err( - self.syntax_error_near("escape sequence too large", "") + self.syntax_error_near("escape sequence too large", &near) ); } buf.push(val as u8); } Some(c) => { - return Err(self.syntax_error_near( - &format!("invalid escape sequence '\\{}'", char::from(c)), - "", - )); + // Lua 5.1 (llex.c read_string): any other escaped + // character stands for itself ("\A" is "A", + // "\?" is "?"); only Lua 5.2+ rejects them. + self.advance(); + buf.push(c); } None => { return Err(self.syntax_error_near("unfinished string", "")); @@ -763,6 +776,11 @@ impl<'a> Lexer<'a> { buf.push(b'\n'); // normalize all newlines to \n self.inc_line(); } + Some(b'[') if sep == 0 && self.peek_ahead(1) == Some(b'[') => { + // PUC-Rio 5.1 llex.c (LUA_COMPAT_LSTR == 1): "[[" inside + // a level-0 long string or comment is an error. + return Err(self.syntax_error_near("nesting of [[...]] is deprecated", "[")); + } Some(b']') => { if self.check_closing_long_bracket(sep) { // Consume closing delimiter: ']' '='*sep ']' @@ -1198,8 +1216,18 @@ mod tests { } #[test] - fn invalid_escape() { - assert!(lex_tokens(r#""\z""#).is_err()); + fn unknown_escape_is_the_char_itself() { + // Lua 5.1: "\z" is "z", "\A" is "A" (only Lua 5.2+ rejects them). + let tokens = lex_tokens(r#""\z" "C:\Apps""#).unwrap(); + assert_eq!(tokens[0], Token::Str(b"z".to_vec())); + assert_eq!(tokens[1], Token::Str(b"C:Apps".to_vec())); + } + + #[test] + fn nested_long_bracket_is_an_error() { + assert!(lex_tokens("[[a [[b]]").is_err()); + assert!(lex_tokens("--[[a [[b]] x").is_err()); + assert!(lex_tokens("[=[a [[b]=]").is_ok()); } // -- Reader-based lexer tests -- diff --git a/src/compiler/parser.rs b/src/compiler/parser.rs index e322ffb..13c50f8 100644 --- a/src/compiler/parser.rs +++ b/src/compiler/parser.rs @@ -28,6 +28,9 @@ struct FuncScope { /// Names of upvalues already resolved for this function. /// Prevents double-counting the same upvalue from multiple references. upvalue_names: Vec, + /// Whether `...` may be used here (PUC-Rio: `fs->f->is_vararg`). + /// The main chunk is always vararg. + is_vararg: bool, } /// Parser state. @@ -81,6 +84,7 @@ impl<'a> Parser<'a> { local_count: 0, local_names: Vec::new(), upvalue_names: Vec::new(), + is_vararg: true, }], }) } @@ -827,6 +831,13 @@ impl<'a> Parser<'a> { Ok(Expr::False(span)) } Token::Dots => { + // PUC-Rio lparser.c simpleexp: check_condition(ls, + // fs->f->is_vararg, "cannot use '...' outside a vararg function") + if !self.func_scopes.last().is_none_or(|f| f.is_vararg) { + return Err( + self.syntax_error_near("cannot use '...' outside a vararg function") + ); + } self.advance()?; Ok(Expr::VarArg(span)) } @@ -1002,6 +1013,7 @@ impl<'a> Parser<'a> { local_count: 0, local_names: Vec::new(), upvalue_names: Vec::new(), + is_vararg: has_varargs, }); self.register_locals_named(¶ms)?; @@ -1713,6 +1725,12 @@ mod tests { // -- Vararg -- + #[test] + fn vararg_outside_vararg_function() { + assert!(parse(b"local function f() return ... end", "t").is_err()); + assert!(parse(b"local function f(...) return ... end", "t").is_ok()); + assert!(parse(b"return ...", "t").is_ok()); + } #[test] fn vararg_expr() { let block = parse_ok("return ..."); diff --git a/src/error.rs b/src/error.rs index a5c03a5..cf3abd5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -177,30 +177,35 @@ const LUA_IDSIZE: usize = 60; /// - `"@filename"` -> `"filename"` (file, with `"..."` prefix if too long) /// - other -> `[string "first_line..."]` pub fn chunkid(source: &str) -> String { - if let Some(rest) = source.strip_prefix('=') { - // Literal name -- strip the '=' prefix. - if rest.len() < LUA_IDSIZE { - rest.to_string() + // Byte-exact port of `luaO_chunkid` (lobject.c, Lua 5.1.4) with + // `bufflen == LUA_IDSIZE`. + let src = source.as_bytes(); + let lossy = |b: &[u8]| String::from_utf8_lossy(b).into_owned(); + if let Some(rest) = src.strip_prefix(b"=") { + // strncpy(out, source+1, bufflen); out[bufflen-1] = '\0'; + let n = rest.len().min(LUA_IDSIZE - 1); + lossy(&rest[..n]) + } else if let Some(rest) = src.strip_prefix(b"@") { + // bufflen -= sizeof(" '...' "); + let bufflen = LUA_IDSIZE - " '...' ".len() - 1; + if rest.len() > bufflen { + format!("...{}", lossy(&rest[rest.len() - bufflen..])) } else { - rest[..LUA_IDSIZE - 1].to_string() - } - } else if let Some(rest) = source.strip_prefix('@') { - // File name. - if rest.len() < LUA_IDSIZE { - rest.to_string() - } else { - let skip = rest.len() - (LUA_IDSIZE - 4); - format!("...{}", &rest[skip..]) + lossy(rest) } } else { - // String source. - let first_line = source.split('\n').next().unwrap_or(source); - let max_len = LUA_IDSIZE - "[string \"...\"]".len(); - if first_line.len() <= max_len && !source.contains('\n') { - format!("[string \"{first_line}\"]") + // bufflen -= sizeof(" [string \"...\"] "); + let bufflen = LUA_IDSIZE - " [string \"...\"] ".len() - 1; + let mut len = src.len().min(bufflen); + if let Some(nl) = src.iter().position(|&c| c == b'\n') + && len > nl + { + len = nl; + } + if len < src.len() { + format!("[string \"{}...\"]", lossy(&src[..len])) } else { - let truncated = &first_line[..first_line.len().min(max_len)]; - format!("[string \"{truncated}...\"]") + format!("[string \"{}\"]", lossy(src)) } } } @@ -367,4 +372,40 @@ mod tests { let err = LuaError::Memory; let _: &dyn std::error::Error = &err; } + + #[test] + fn chunkid_literal() { + assert_eq!(chunkid("=name"), "name"); + // Truncate to LUA_IDSIZE-1 + let long = "a".repeat(100); + let cid = chunkid(&format!("={long}")); + assert_eq!(cid.len(), LUA_IDSIZE - 1); + } + + #[test] + fn chunkid_file_truncation() { + // Short filename: no truncation + assert_eq!(chunkid("@short.lua"), "short.lua"); + // Long filename: show tail with "..." prefix + let long = "/very/long/path/to/a/file/that/exceeds/the/buffer/limit.lua"; + let cid = chunkid(&format!("@{long}")); + assert!(cid.starts_with("...")); + } + + #[test] + fn chunkid_string_source() { + // Short string: no truncation + assert_eq!(chunkid("x + y"), r#"[string "x + y"]"#); + // Long string: truncate with "..." + let long_line = "a".repeat(100); + let cid = chunkid(&long_line); + assert!(cid.contains("...")); + } + + #[test] + fn chunkid_multiline_string() { + // Multiline strings should truncate at first newline + let cid = chunkid("line1\nline2"); + assert_eq!(cid, r#"[string "line1..."]"#); + } } diff --git a/src/stdlib/string.rs b/src/stdlib/string.rs index 0115b16..24da1bb 100644 --- a/src/stdlib/string.rs +++ b/src/stdlib/string.rs @@ -1450,7 +1450,9 @@ pub fn str_find(state: &mut LuaState) -> LuaResult { let plain_val = arg(state, 3); let init = state.opt_integer(3, 1).map(|i| posrelat(i, s.len()))?; - let init = (init.max(1) as usize).saturating_sub(1); // Convert to 0-based. + // Convert to 0-based and clamp to the subject length (PUC-Rio str_find_aux: + // `else if ((size_t)(init) > l1) init = (ptrdiff_t)l1;`). + let init = ((init.max(1) as usize).saturating_sub(1)).min(s.len()); let plain = plain_val.is_truthy(); if plain { @@ -1547,7 +1549,7 @@ pub fn str_match(state: &mut LuaState) -> LuaResult { let init_raw = state.opt_integer(3, 1)?; let init = posrelat(init_raw, s.len()); - let init = (init.max(1) as usize).saturating_sub(1); + let init = ((init.max(1) as usize).saturating_sub(1)).min(s.len()); // clamp like PUC-Rio let anchor = !pat.is_empty() && pat[0] == b'^'; let pat_start = usize::from(anchor); diff --git a/src/vm/execute.rs b/src/vm/execute.rs index 3f7b674..2626240 100644 --- a/src/vm/execute.rs +++ b/src/vm/execute.rs @@ -556,6 +556,7 @@ impl LuaState { } Closure::Rust(rust_cl) => { let func = rust_cl.func; + // All Rust functions in rilua are built-in library functions. // Ensure minimum stack for Rust functions. self.ensure_stack(self.top + LUA_MINSTACK); @@ -573,10 +574,30 @@ impl LuaState { if self.hook.hook_mask & MASK_CALL != 0 { self.callhook("call", -1)?; } - // Execute the Rust function. - let n_results = func(self)?; - + let my_ci = self.ci; + let n_results = match func(self) { + Ok(n) => n, + // PUC-Rio's library functions raise errors through + // `luaL_error`, which prefixes the position of the + // calling Lua function (`luaL_where(L, 1)`). Do the same + // for errors that originate in a built-in function + // (frame still current, no thrown error object, not a + // VM-style "attempt to ..." error). + Err(LuaError::Runtime(mut e)) + if self.ci == my_ci + && self.error_object.is_none() + && !e.message.starts_with("attempt to ") + && !e.message.starts_with("loop in ") => + { + let where_prefix = get_where(self, 1); + if !where_prefix.is_empty() && !e.message.starts_with(&where_prefix) { + e.message.insert_str(0, &where_prefix); + } + return Err(LuaError::Runtime(e)); + } + Err(e) => return Err(e), + }; // Move results into place. let first_result = self.top - n_results as usize; self.poscall(first_result); diff --git a/src/vm/gc/collector.rs b/src/vm/gc/collector.rs index 7234169..c853ea2 100644 --- a/src/vm/gc/collector.rs +++ b/src/vm/gc/collector.rs @@ -1462,7 +1462,11 @@ impl LuaState { let base = self.top; self.push(gc_fn); self.push(Val::Userdata(ud_ref)); + // PUC-Rio GCTM: stop debug hooks during GC tag methods. + let old_allow_hook = self.hook.allow_hook; + self.hook.allow_hook = false; let result = self.call_function(base, 0); + self.hook.allow_hook = old_allow_hook; if result.is_err() { self.ci = saved_ci; self.base = self.call_stack[saved_ci].base; diff --git a/tests/integration.rs b/tests/integration.rs index 7cb5866..9ed2721 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -896,6 +896,17 @@ fn string_find_not_found() { assert_eq!(stdout, "nil\n"); } +#[test] +fn string_init_clamped_to_length() { + // PUC-Rio: init beyond string length is clamped to len (avoids underflow). + let (stdout, _, code) = run_rilua("print(string.find('hi', 'x', -100))"); + assert_eq!(code, 0); + assert_eq!(stdout, "nil\n"); + let (stdout, _, code) = run_rilua("print(string.match('ab', 'a', -50))"); + assert_eq!(code, 0); + assert_eq!(stdout, "a\n"); +} + #[test] fn string_match_captures() { let (stdout, _, code) = run_rilua("print(string.match('2024-01-15', '(%d+)-(%d+)-(%d+)'))");