Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/compiler/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
44 changes: 36 additions & 8 deletions src/compiler/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}'"),
Expand Down Expand Up @@ -615,7 +624,9 @@ impl<'a> Lexer<'a> {
return Err(self.syntax_error_near("unfinished string", "<eof>"));
}
Some(b'\n') | Some(b'\r') => {
return Err(self.syntax_error_near("unfinished string", "<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
Expand Down Expand Up @@ -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", "<string>")
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)),
"<string>",
));
// 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", "<eof>"));
Expand Down Expand Up @@ -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 ']'
Expand Down Expand Up @@ -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 --
Expand Down
18 changes: 18 additions & 0 deletions src/compiler/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// Whether `...` may be used here (PUC-Rio: `fs->f->is_vararg`).
/// The main chunk is always vararg.
is_vararg: bool,
}

/// Parser state.
Expand Down Expand Up @@ -81,6 +84,7 @@ impl<'a> Parser<'a> {
local_count: 0,
local_names: Vec::new(),
upvalue_names: Vec::new(),
is_vararg: true,
}],
})
}
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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(&params)?;

Expand Down Expand Up @@ -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 ...");
Expand Down
81 changes: 61 additions & 20 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
Expand Down Expand Up @@ -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..."]"#);
}
}
6 changes: 4 additions & 2 deletions src/stdlib/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1450,7 +1450,9 @@ pub fn str_find(state: &mut LuaState) -> LuaResult<u32> {
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 {
Expand Down Expand Up @@ -1547,7 +1549,7 @@ pub fn str_match(state: &mut LuaState) -> LuaResult<u32> {

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);
Expand Down
27 changes: 24 additions & 3 deletions src/vm/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/vm/gc/collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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+)'))");
Expand Down
Loading