use luna_core::runtime::Value;
use luna_core::version::LuaVersion;
use luna_core::vm::Vm;
fn eval(src: &str) -> Vec<Value> {
let mut vm = Vm::new(LuaVersion::Lua55);
match vm.eval(src) {
Ok(v) => v,
Err(e) => panic!("runtime error in {src:?}: {}", vm.error_text(&e)),
}
}
fn eval1(src: &str) -> Value {
let mut v = eval(src);
assert_eq!(v.len(), 1, "expected 1 result from {src:?}");
let v = v.pop().unwrap();
assert!(
!matches!(v, Value::Str(_) | Value::Table(_) | Value::Closure(_)),
"eval1 must not return GC values; use a vm-scoped helper"
);
v
}
#[track_caller]
fn check_str(src: &str, expect: &[u8]) {
let mut vm = Vm::new(LuaVersion::Lua55);
let v = match vm.eval(src) {
Ok(v) => v,
Err(e) => panic!("runtime error in {src:?}: {}", vm.error_text(&e)),
};
assert_eq!(v.len(), 1, "expected 1 result from {src:?}");
match v[0] {
Value::Str(s) => assert_eq!(
s.as_bytes(),
expect,
"{src:?} → {:?}",
String::from_utf8_lossy(s.as_bytes())
),
v => panic!("{src:?} → {v:?}, expected a string"),
}
}
#[track_caller]
fn check_int(src: &str, expect: i64) {
let v = eval1(src);
assert!(
v.raw_eq(Value::Int(expect)) && matches!(v, Value::Int(_)),
"{src:?} → {v:?}, expected Int({expect})"
);
}
#[track_caller]
fn check_float(src: &str, expect: f64) {
let v = eval1(src);
match v {
Value::Float(f) => assert!(
f == expect || (f.is_nan() && expect.is_nan()),
"{src:?} → {f}, expected {expect}"
),
v => panic!("{src:?} → {v:?}, expected Float({expect})"),
}
}
#[track_caller]
fn check_bool(src: &str, expect: bool) {
let v = eval1(src);
assert!(
matches!(v, Value::Bool(b) if b == expect),
"{src:?} → {v:?}, expected Bool({expect})"
);
}
#[track_caller]
fn check_error(src: &str, contains: &str) {
let mut vm = Vm::new(LuaVersion::Lua55);
match vm.eval(src) {
Ok(v) => panic!("{src:?} unexpectedly returned {v:?}"),
Err(e) => {
let msg = vm.error_text(&e);
assert!(
msg.contains(contains),
"{src:?} error {msg:?} does not contain {contains:?}"
);
}
}
}
#[test]
fn arithmetic_semantics() {
check_int("return 1 + 2", 3);
check_int("return 7 * 6 - 2", 40);
check_float("return 7 / 2", 3.5);
if !cfg!(miri) {
check_float("return 2 ^ 10", 1024.0);
}
check_int("return 7 // 2", 3);
check_int("return -7 // 2", -4);
check_int("return 7 % 3", 1);
check_int("return -7 % 3", 2);
check_int("return 7 % -3", -2);
check_float("return 7.5 % 2", 1.5);
check_float("return -7.5 % 2", 0.5);
check_float("return 1 + 0.5", 1.5);
check_int("local a = 9223372036854775807 return a + 1", i64::MIN);
check_int("return -(3)", -3);
check_float("return -(3.5)", -3.5);
check_int("local x = 5 return -x", -5);
}
#[test]
fn bitwise_semantics() {
check_int("return 3 & 5", 1);
check_int("return 3 | 5", 7);
check_int("return 3 ~ 5", 6);
check_int("return ~0", -1);
check_int("return 1 << 4", 16);
check_int("return 256 >> 4", 16);
check_int("return 1 << 64", 0);
check_int("return 1 << 100", 0);
check_int("return -1 >> 1", i64::MAX);
check_int("return 1 << -2", 0);
check_int("return 16 >> -2", 64);
check_int("return 3.0 & 5", 1);
check_error("return 3.5 & 1", "no integer representation");
check_error("return {} & 1", "bitwise operation");
}
#[test]
fn division_by_zero() {
check_error("return 1 // 0", "attempt to divide by zero");
check_error("return 1 % 0", "'n%0'");
check_float("return 1 / 0", f64::INFINITY);
check_float("return 1.0 // 0", f64::INFINITY);
check_float("return 0 / 0", f64::NAN);
}
#[test]
fn concat_result_as_binop_operand() {
check_int("return (1 .. 2) << 1", 24); check_int("return (1 .. 2) + 1", 13);
check_int("return (\"7\" .. 3) << 1", 146);
check_bool("return \"a\" .. \"b\" > \"a\"", true);
check_bool("return not(2+1 > 3*1) and \"a\"..\"b\" > \"a\"", true);
}
#[test]
fn comparison_semantics() {
check_bool("return 1 < 2", true);
check_bool("return 2 < 1", false);
check_bool("return 1 <= 1", true);
check_bool("return 2 > 1", true);
check_bool("return 1 >= 2", false);
check_bool("return 1 == 1.0", true);
check_bool("return 1 ~= 1.0", false);
check_bool("return 'a' < 'b'", true);
check_bool("return 'abc' <= 'abc'", true);
check_bool("return 'abc' < 'abd'", true);
check_bool("return 9223372036854775807 < 9223372036854775808.0", true);
check_bool("return 9223372036854775807.0 <= 9223372036854775807", false);
check_bool("local nan = 0/0 return nan == nan", false);
check_bool("local nan = 0/0 return nan < nan", false);
check_bool("return 1 == '1'", false);
check_error("return 1 < 'x'", "attempt to compare number with string");
check_error("return {} < {}", "attempt to compare two table values");
}
#[test]
fn logic_and_truthiness() {
check_int("return 1 and 2", 2);
assert!(eval1("return nil and 2").is_nil()); check_int("return nil or 5", 5);
check_int("return false or 5", 5);
check_int("return 1 or 2", 1);
check_bool("return not nil", true);
check_bool("return not 0", false); check_int("return (nil and 1) or (false or 7)", 7);
check_int("local t = nil return false and t.x or 3", 3);
}
#[test]
fn locals_scoping_and_assignment() {
check_int("local a = 1 local b = a + 1 return a + b", 3);
check_int("local a = 1 do local a = 100 end return a", 1);
check_int("local a, b, c = 1, 2 return (c == nil) and (a + b) or 0", 3);
check_int("local a, b = 1, 2, 3 return a + b", 3);
check_int("local a = 1 a = a + 10 return a", 11);
check_int("local a, b = 1, 2 a, b = b, a return a * 10 + b", 21);
check_int("x = 5 return x + 1", 6);
check_bool("return rawunset == nil", true); check_int("local t = {} t.x = 1 t.x = t.x + 1 return t.x", 2);
}
#[test]
fn control_flow() {
check_int("if true then return 1 else return 2 end", 1);
check_int("if false then return 1 else return 2 end", 2);
check_int(
"if nil then return 1 elseif 0 then return 2 else return 3 end",
2,
);
check_int(
"local n = 0 local i = 1 while i <= 10 do n = n + i i = i + 1 end return n",
55,
);
check_int(
"local n = 0 while true do n = n + 1 if n >= 5 then break end end return n",
5,
);
check_int("local n = 0 repeat n = n + 1 until n >= 3 return n", 3);
check_int(
"local n = 0 repeat local done = n >= 2 n = n + 1 until done return n",
3,
);
}
#[test]
fn numeric_for_semantics() {
check_int("local s = 0 for i = 1, 10 do s = s + i end return s", 55);
check_int(
"local s = 0 for i = 10, 1, -1 do s = s + i end return s",
55,
);
check_int("local s = 0 for i = 1, 10, 2 do s = s + i end return s", 25);
check_int("local n = 0 for i = 1, 0 do n = n + 1 end return n", 0);
check_int("local n = 0 for i = 1, 1 do n = n + 1 end return n", 1);
check_int("local s = 0 for i = 1, 2.5 do s = s + i end return s", 3);
check_bool("for i = 1, 2.5 do return i == 1 end", true);
check_float(
"local s = 0.0 for x = 0.5, 2.0, 0.5 do s = s + x end return s",
5.0,
);
check_int(
"local n = 0 for i = 9223372036854775805, 9223372036854775807 do n = n + 1 end return n",
3,
);
check_error("for i = 1, 10, 0 do end", "'for' step is zero");
check_error(
"for i = {}, 10 do end",
"bad 'for' initial value (number expected, got table)",
);
check_int(
"local s = 0 for i = 1, 100 do if i > 3 then break end s = s + i end return s",
6,
);
}
#[test]
fn tables_and_indexing() {
check_int("local t = {1, 2, 3} return t[1] + t[2] + t[3]", 6);
check_int("local t = {x = 10, y = 20} return t.x + t.y", 30);
check_int("local t = {[2 + 2] = 7} return t[4]", 7);
check_int("local t = {1, 2; x = 3} return t[1] + t[2] + t.x", 6);
check_int("local t = {} t[1] = 5 t['k'] = 6 return t[1] + t.k", 11);
check_int("local t = {{1, 2}, {3, 4}} return t[2][1]", 3);
check_int("return #'hello'", 5);
check_int("local t = {1, 2, 3} return #t", 3);
check_int(
"local t = {} for i = 1, 100 do t[i] = i * 2 end return t[77]",
154,
);
check_bool("local t = {} return t[1] == nil", true);
check_bool("local t = {a = 1} return t.b == nil", true);
check_float("local t = {2.0, [3.0] = 9} return t[1.0] + t[3]", 11.0);
check_error(
"local x = nil return x.field",
"attempt to index a nil value",
);
check_error("local x = 5 x.field = 1", "attempt to index a number value");
check_error("local t = {} t[nil] = 1", "table index is nil");
check_error("local t = {} t[0/0] = 1", "table index is NaN");
}
#[test]
fn concat_semantics() {
check_str("return 'a' .. 'b' .. 'c'", b"abc");
check_str("return 1 .. 2", b"12");
check_str("return 1.5 .. 'x'", b"1.5x");
check_str("return 'pi=' .. 3.5 .. '!'", b"pi=3.5!");
check_error("return {} .. 'x'", "attempt to concatenate a table value");
}
#[test]
fn multiple_returns_fixed() {
let v = eval("return 1, 2, 3");
assert_eq!(v.len(), 3);
assert!(v[0].raw_eq(Value::Int(1)));
assert!(v[2].raw_eq(Value::Int(3)));
assert_eq!(eval("return").len(), 0);
assert_eq!(eval("local a = 1").len(), 0);
}
#[test]
fn globals_via_env() {
check_int("x = 1 y = 2 return x + y", 3);
check_int(
"counter = 0 for i = 1, 5 do counter = counter + 1 end return counter",
5,
);
let mut vm = Vm::new(LuaVersion::Lua55);
vm.eval("answer = 42").unwrap();
let v = vm.eval("return answer").unwrap();
assert!(v[0].raw_eq(Value::Int(42)));
}
#[test]
fn error_positions() {
let mut vm = Vm::new(LuaVersion::Lua55);
let Err(e) = vm.eval("local x = 1\nlocal y = nil\nreturn y.z") else {
panic!("expected runtime error")
};
let msg = vm.error_text(&e);
assert!(msg.starts_with("eval:3:"), "position missing: {msg}");
}
#[test]
fn functions_and_calls() {
check_int(
"local function add(a, b) return a + b end return add(2, 3)",
5,
);
check_int("local f = function(x) return x * 2 end return f(21)", 42);
check_int("function double(x) return x + x end return double(7)", 14);
check_int(
"local function fib(n) if n < 2 then return n end return fib(n-1) + fib(n-2) end \
return fib(15)",
610,
);
check_int(
"local t = {v = 10} function t.get() return 1 end function t:geti() return self.v end \
return t.get() + t:geti()",
11,
);
check_int(
"local M = {} M.sub = {} function M.sub:m(x) return x + (self.k or 0) end \
M.sub.k = 5 return M.sub:m(2)",
7,
);
check_int(
"local function f(a, b) return (a or 10) + (b or 20) end return f(1)",
21,
);
check_int("local function f(a) return a end return f(1, 2, 3)", 1);
check_error("local x = 5 x()", "attempt to call a number value");
}
#[test]
fn closures_and_upvalues() {
check_int(
"local function counter() local n = 0 return function() n = n + 1 return n end end \
local c = counter() c() c() return c()",
3,
);
check_int(
"local n = 0 local function inc() n = n + 1 end local function get() return n end \
inc() inc() return get()",
2,
);
check_int(
"local fs = {} for i = 1, 3 do fs[i] = function() return i end end \
return fs[1]() * 100 + fs[2]() * 10 + fs[3]()",
123,
);
check_int(
"local x = 7 local function outer() local function inner() return x end return inner() end \
return outer()",
7,
);
check_int(
"local x = 1 local function set(v) x = v end set(99) return x",
99,
);
check_int(
"g = 5 local function f() g = g + 1 return g end return f()",
6,
);
}
#[test]
fn varargs_55_semantics() {
check_int(
"local function f(...) local a, b = ... return a + b end return f(3, 4)",
7,
);
check_int(
"local function f(...) return select('#', ...) end return f(1, nil, 3)",
3,
);
check_int(
"local function f(...) return ... end return (f(1, 2, 3))",
1,
);
let v = eval("local function f(...) return ... end return f(1, 2, 3)");
assert_eq!(v.len(), 3);
check_int(
"local function f(...t) return t.n end return f(10, 20, 30)",
3,
);
check_int(
"local function f(...t) return t[2] end return f(10, 20, 30)",
20,
);
check_int("local function f(...t) return t.n end return f()", 0);
check_int(
"local function f(...t) local a = ... return a + t.n end return f(5, 6)",
7,
);
check_int(
"local function f(...) local a, b = (...), 100 return a + b end return f(7, 8)",
107,
);
check_int("local n = select('#', ...) return n", 0);
check_int(
"local function f(...t) t[1] = t[1] + 10 return (...) end return f(5)",
15,
);
check_int(
"local function f(...t) t.n = 3 return select('#', ...) end return f(1)",
3,
);
check_error(
"local function f(...t) t.n = -1 return ... end return f(1)",
"no proper 'n'",
);
check_error(
"local function f(...t) t.n = 1.0 return ... end return f(1)",
"no proper 'n'",
);
}
#[test]
fn multret_semantics() {
check_int(
"local function two() return 1, 2 end local a, b = two() return a * 10 + b",
12,
);
check_int(
"local function two() return 1, 2 end local a, b, c = two(), 9 \
return a * 100 + b * 10 + (c or 0)",
190,
);
check_int(
"local function two() return 1, 2 end local t = {two()} return #t",
2,
);
check_int(
"local function two() return 1, 2 end local t = {two(), two()} return #t",
3,
);
check_int(
"local function two() return 1, 2 end local function sum(a, b, c) return a + b + (c or 0) end \
return sum(two(), 10)",
11,
);
check_int(
"local function two() return 1, 2 end local function sum(a, b, c) return a + b + (c or 0) end \
return sum(10, two())",
13,
);
let v =
eval("local function two() return 1, 2 end local function f() return two() end return f()");
assert_eq!(v.len(), 2);
}
#[test]
fn tail_calls_do_not_grow_frames() {
const N: i64 = if cfg!(miri) { 2_000 } else { 1_000_000 };
check_int(
&format!(
"local function loop(n, acc) if n == 0 then return acc end return loop(n - 1, acc + 1) end return loop({N}, 0)"
),
N,
);
check_int(
"local t = {} function t:f(n) if n == 0 then return 42 end return self:f(n - 1) end \
return t:f(10000)",
42,
);
}
#[test]
fn generic_for_loops() {
check_int(
"local t = {10, 20, 30} local s = 0 for i, v in ipairs(t) do s = s + i + v end return s",
66,
);
check_int(
"local t = {a = 1, b = 2, c = 3} local s = 0 for k, v in pairs(t) do s = s + v end return s",
6,
);
check_int(
"local t = {x = 1} local n = 0 for k in pairs(t) do n = n + 1 end return n",
1,
);
check_int(
"local function range(n) local i = 0 return function() i = i + 1 if i <= n then return i end end end \
local s = 0 for v in range(5) do s = s + v end return s",
15,
);
check_int(
"local s = 0 for i, v in ipairs({5, 6, 7}) do if i == 2 then break end s = s + v end return s",
5,
);
check_error("for x in 5 do end", "attempt to call a number value");
}
#[test]
fn pcall_and_error() {
check_bool("local ok = pcall(function() return 1 end) return ok", true);
check_bool(
"local ok = pcall(function() error('boom') end) return ok",
false,
);
check_str(
"local _, e = pcall(function() error('boom') end) return e",
b"eval:1: boom",
);
check_int(
"local _, e = pcall(function() error({code = 42}) end) return e.code",
42,
);
check_str(
"local _, e = pcall(function() error('raw', 0) end) return e",
b"raw",
);
check_int(
"local ok, a, b = pcall(function() return 3, 4 end) return a + b",
7,
);
check_bool(
"local ok = pcall(function() local ok2 = pcall(error) return ok2 end) return ok",
true,
);
check_bool(
"local ok = pcall(function() local x = nil return x.y end) return ok",
false,
);
check_str(
"local _, e = pcall(function() assert(false, 'msg') end) return e",
b"eval:1: msg",
);
check_int("return assert(42)", 42);
}
#[test]
fn builtin_basics() {
check_str("return type(nil)", b"nil");
check_str("return type(1)", b"number");
check_str("return type('x')", b"string");
check_str("return type({})", b"table");
check_str("return type(print)", b"function");
check_str("return type(function() end)", b"function");
check_str("return tostring(12)", b"12");
check_str("return tostring(1.5)", b"1.5");
check_str("return tostring(nil)", b"nil");
check_str("return tostring(true)", b"true");
check_int("return select('#', 1, 2, 3)", 3);
check_int("return (select(2, 7, 8, 9))", 8);
check_int("return (select(-1, 7, 8, 9))", 9);
check_bool("return rawequal('a', 'a')", true);
check_bool("return rawequal({}, {})", false);
check_int("return rawlen({1, 2, 3})", 3);
check_int(
"local t = setmetatable({}, {}) return rawget(t, 'x') == nil and 1 or 0",
1,
);
check_str("return _VERSION", b"Lua 5.5");
check_int("_G.zz1 = 8 return zz1", 8);
}
#[test]
fn closures_survive_gc() {
let mut vm = Vm::new(LuaVersion::Lua55);
vm.eval(
"local n = 0
counter = function() n = n + 1 return n end",
)
.unwrap();
vm.collect_garbage();
let v = vm.eval("return counter() + counter()").unwrap();
assert!(v[0].raw_eq(Value::Int(3)));
vm.collect_garbage();
let v = vm.eval("return counter()").unwrap();
assert!(v[0].raw_eq(Value::Int(3)));
}
#[test]
fn mm_index_and_newindex() {
check_int(
"local Base = {greet = 4} local Mid = setmetatable({x = 2}, {__index = Base}) \
local obj = setmetatable({}, {__index = Mid}) return obj.greet + obj.x",
6,
);
check_int(
"local t = setmetatable({}, {__index = function(t, k) return #k end}) return t.abc",
3,
);
check_int(
"local t = setmetatable({v = 1}, {__index = function() return 99 end}) return t.v",
1,
);
check_int(
"local log = {} local t = setmetatable({}, {__newindex = function(t, k, v) log[k] = v * 2 end}) \
t.a = 21 return log.a",
42,
);
check_int(
"local store = {} local t = setmetatable({}, {__newindex = store}) t.k = 7 \
return store.k + (rawget(t, 'k') == nil and 1 or 0)",
8,
);
check_int(
"local n = 0 local t = setmetatable({k = 1}, {__newindex = function() n = 99 end}) \
t.k = 2 return t.k + n",
2,
);
check_error(
"local t = {} setmetatable(t, {__index = t}) return t.x",
"chain too long",
);
}
#[test]
fn mm_arithmetic_and_string_coercion() {
check_int(
"local function val(x) return type(x) == 'table' and x.v or x end \
local V = {} V.__add = function(a, b) return val(a) + val(b) end \
local x = setmetatable({v = 40}, V) return x + 2",
42,
);
check_int(
"local V = {__sub = function(a, b) return 7 end} \
local x = setmetatable({}, V) return 1 - x",
7,
);
check_int(
"local V = {__unm = function(x) return 5 end} return -setmetatable({}, V)",
5,
);
check_int("return '10' + 1", 11);
check_float("return '2.5' * 2", 5.0);
check_int("return '0x10' + 0", 16);
check_int("return '8' // '3'", 2);
check_int("return '12' & 4", 4);
check_error(
"return 'abc' + 1",
"attempt to perform arithmetic on a string value",
);
}
#[test]
fn mm_comparison() {
let v = "local V = {__eq = function(a, b) return a.id == b.id end, \
__lt = function(a, b) return a.id < b.id end, \
__le = function(a, b) return a.id <= b.id end} \
local a = setmetatable({id = 1}, V) local b = setmetatable({id = 1}, V) \
local c = setmetatable({id = 2}, V) ";
check_bool(&format!("{v} return a == b"), true);
check_bool(&format!("{v} return a ~= b"), false);
check_bool(&format!("{v} return a == c"), false);
check_bool(&format!("{v} return a < c"), true);
check_bool(&format!("{v} return c <= a"), false);
check_bool(&format!("{v} return a > c"), false);
check_bool(
"local t = setmetatable({}, {__eq = function() return true end}) return t == 1",
false,
);
}
#[test]
fn mm_call_concat_len_tostring() {
check_int(
"local t = setmetatable({base = 40}, {__call = function(self, x) return self.base + x end}) \
return t(2)",
42,
);
check_str(
"local t = setmetatable({}, {__concat = function(a, b) return 'C' end}) return t .. 'x'",
b"C",
);
check_str(
"local t = setmetatable({}, {__concat = function(a, b) return a .. '!' end}) return 'hi' .. t",
b"hi!",
);
check_int(
"local t = setmetatable({}, {__len = function() return 99 end}) return #t",
99,
);
check_str(
"local t = setmetatable({}, {__tostring = function() return 'OBJ' end}) return tostring(t)",
b"OBJ",
);
check_str(
"local t = setmetatable({}, {__metatable = 'locked'}) return getmetatable(t)",
b"locked",
);
check_error(
"local t = setmetatable({}, {__metatable = 'locked'}) setmetatable(t, {})",
"protected metatable",
);
}
#[test]
fn class_pattern_end_to_end() {
check_int(
"local Account = {} Account.__index = Account \
function Account.new(b) return setmetatable({balance = b}, Account) end \
function Account:deposit(v) self.balance = self.balance + v end \
function Account:get() return self.balance end \
local a = Account.new(100) a:deposit(20) a:deposit(3) return a:get()",
123,
);
}
#[test]
fn runtime_stack_overflow_is_caught() {
check_bool(
"local function f() return 1 + f() end local ok = pcall(f) return ok",
false,
);
check_str(
"local function f() return 1 + f() end local _, e = pcall(f) return e",
b"eval:1: stack overflow",
);
}
fn check_compile_error(src: &str, contains: &str) {
let mut vm = Vm::new(LuaVersion::Lua55);
match vm.eval(src) {
Ok(v) => panic!("{src:?} unexpectedly compiled and returned {v:?}"),
Err(e) => {
let m = vm.error_text(&e);
assert!(
m.contains(contains),
"{src:?} error {m:?} does not contain {contains:?}"
);
}
}
}
#[test]
fn goto_and_labels() {
check_int("do goto done end ::done:: return 1", 1);
check_int("local x = 1 goto skip x = 99 ::skip:: return x", 1);
check_int(
"local n = 0 ::top:: n = n + 1 if n < 5 then goto top end return n",
5,
);
check_int(
"local s = 0 for i = 1, 5 do if i % 2 == 0 then goto continue end \
local double = i * 2 s = s + double ::continue:: end return s",
18,
);
check_int("do do goto out end end ::out:: return 7", 7);
check_compile_error("goto nowhere", "no visible label 'nowhere'");
check_compile_error(
"goto later local x = 1 ::later:: return x",
"jumps into the scope",
);
check_compile_error("::dup:: ::dup::", "already defined");
check_compile_error("::l1:: do ::l1:: end", "label 'l1' already defined");
check_compile_error("goto l1 do ::l1:: end", "no visible label 'l1'");
check_compile_error(
"goto l1 local aa ::l1:: return aa",
"jumps into the scope of 'aa'",
);
check_int(
"local fs = {} local i = 1 ::top:: do local v = i fs[i] = function() return v end end \
i = i + 1 if i <= 2 then goto top end return fs[1]() * 10 + fs[2]()",
12,
);
}
#[test]
fn to_be_closed() {
check_str(
"local log = '' local function tracker(n) return setmetatable({}, \
{__close = function() log = log .. n end}) end \
do local a <close> = tracker('a') local b <close> = tracker('b') end \
return log",
b"ba",
);
check_str(
"local seen local t = setmetatable({}, {__close = function(_, e) seen = e end}) \
local ok, err = pcall(function() local x <close> = t error('boom', 0) end) \
return seen",
b"boom",
);
check_int(
"local n = 0 local mt = {__close = function() n = n + 1 end} \
for i = 1, 3 do local x <close> = setmetatable({}, mt) end return n",
3,
);
check_int(
"do local x <close> = nil local y <close> = false end return 1",
1,
);
check_error("local x <close> = 42", "non-closable value");
check_compile_error(
"local a <close>, b <close> = nil, nil",
"multiple to-be-closed",
);
check_compile_error(
"local x <close> = nil x = 1",
"attempt to assign to const variable 'x'",
);
check_int(
"local n local mt = {__close = function(...) n = select('#', ...) end} \
do local y <close> = setmetatable({}, mt) end return n",
1,
);
check_int(
"local n local mt = {__close = function(...) n = select('#', ...) end} \
pcall(function() local y <close> = setmetatable({}, mt) error('e', 0) end) return n",
2,
);
check_bool(
"local function c(f) return setmetatable({}, {__close = f}) end \
local ok, msg = pcall(function() \
local x <close> = c(function(_, m) assert(m:find('@y')) error('@x') end) \
local y <close> = c(function(_, m) assert(m == nil) error('@y') end) \
end) \
return msg:find('@x') ~= nil",
true,
);
check_bool(
"local function c(f) return setmetatable({}, {__close = f}) end \
local ok, msg = pcall(function() \
local x <close> = c(function(_, m) assert(m:find('@y')) error('@x') end) \
local g <close> = c(function() collectgarbage() end) \
local y <close> = c(function(_, m) assert(m == nil) error('@y') end) \
end) \
return msg:find('@x') ~= nil",
true,
);
check_int(
"local function multi() return 1, 2, 3 end \
local function bar() local _ <close> = setmetatable({}, {__close=function() end}) \
do return multi() end end \
local a, b, c = bar() return a + b + c",
6,
);
}
#[test]
fn const_attribs() {
check_int("local x <const> = 41 return x + 1", 42);
check_compile_error(
"local x <const> = 1 x = 2",
"attempt to assign to const variable 'x'",
);
check_compile_error(
"local <const> a, b = 1, 2 b = 3",
"attempt to assign to const variable 'b'",
);
check_compile_error(
"for i = 1, 3 do i = 5 end",
"attempt to assign to const variable 'i'",
);
check_compile_error(
"for k, v in pairs({}) do k = 1 end",
"attempt to assign to const variable 'k'",
);
check_int(
"for k, v in pairs({x = 1}) do v = 7 return v end return 0",
7,
);
check_compile_error(
"local z <const> = 1 function foo() return function() z = 2 end end",
"attempt to assign to const variable 'z'",
);
check_compile_error(
"local foo <const> = 10 function foo() end",
"attempt to assign to const variable 'foo'",
);
}
#[test]
fn global_declarations_55() {
check_int("global x = 5 return x + 1", 6);
check_compile_error("global x = 1 return y", "variable 'y' not declared");
check_compile_error("global x = 1 y = 2", "variable 'y' not declared");
check_int("global x = 1 global * y = 2 return x + y", 3);
check_int(
"global <const> * return type(print) == 'function' and 1 or 0",
1,
);
check_compile_error(
"global <const> * y = 2",
"attempt to assign to const variable 'y'",
);
check_int("global <const> * global n n = 41 return n + 1", 42);
check_compile_error(
"global z <const> = 1 z = 2",
"attempt to assign to const variable 'z'",
);
check_int("do global x x = 1 end y = 2 return y", 2);
check_int("global function gf() return 21 end return gf() * 2", 42);
check_int("global g local a = 3 g = a return g", 3);
check_int("global <const> * _ENV.bypass = 9 return _ENV.bypass", 9);
}
#[test]
fn math_library() {
check_int("return math.floor(2.7)", 2);
check_int("return math.floor(-2.7)", -3);
check_int("return math.ceil(2.1)", 3);
check_int("return math.abs(-5)", 5);
check_float("return math.abs(-5.5)", 5.5);
check_int("return math.max(3, 1, 4, 1, 5)", 5);
check_int("return math.min(3, 1, 4, 1, 5)", 1);
check_float("return math.sqrt(16)", 4.0);
check_float("return math.huge", f64::INFINITY);
check_int("return math.maxinteger", i64::MAX);
check_int("return math.mininteger", i64::MIN);
check_str("return math.type(1)", b"integer");
check_str("return math.type(1.0)", b"float");
check_bool("return math.type('x') == nil", true);
check_int("return math.tointeger(3.0)", 3);
check_bool("return math.tointeger(3.5) == nil", true);
check_int("return math.fmod(7, 3)", 1);
check_int("return math.fmod(-7, 3)", -1); check_error("return math.fmod(1, 0)", "zero");
check_bool("return math.ult(-1, 1)", false); check_float("return math.log(8, 2)", 3.0);
let v = eval("local ip, fp = math.modf(3.7) return ip");
assert!(matches!(v[0], Value::Float(f) if f == 3.0));
check_bool(
"math.randomseed(42) local a = math.random() math.randomseed(42) \
return a == math.random()",
true,
);
check_bool(
"math.randomseed(7) for i = 1, 100 do local r = math.random(3, 9) \
if r < 3 or r > 9 then return false end end return true",
true,
);
check_error("return math.random(5, 2)", "interval is empty");
check_error("return math.random(1, 2, 3)", "wrong number of arguments");
check_int(
"math.randomseed(1007) return math.random(0)",
0x7a7040a5a323c9d6u64 as i64,
);
check_bool("return math.deg(math.pi) == 180.0", true);
check_bool("return math.rad(180) == math.pi", true);
check_bool(
"local m, e = math.frexp(8.0) return m == 0.5 and e == 4",
true,
);
check_bool("return math.ldexp(0.5, 4) == 8.0", true);
check_float("return (-1.0) % 2.0", 1.0);
check_bool("local z <const> = -0.0 return 1/z < 0", true);
check_error("return math.huge << 1", "field 'huge'");
}
#[test]
fn table_library() {
check_int("local t = {1, 2, 3} table.insert(t, 4) return t[4] + #t", 8);
check_int(
"local t = {1, 3} table.insert(t, 2, 2) return t[1] * 100 + t[2] * 10 + t[3]",
123,
);
check_error("table.insert({}, 5, 1)", "position out of bounds");
check_error("table.insert({}, 2, 3, 4)", "wrong number of arguments");
check_error(
"local t = setmetatable({}, {__len = function() return 'abc' end}) table.insert(t, 1)",
"object length is not an integer",
);
check_error("table.create(0, 1 << 31)", "out of range");
check_error("table.create(0, (1 << 31) - 1)", "table overflow");
check_error(
"table.unpack({}, math.mininteger, math.maxinteger)",
"too many results",
);
check_int(
"local t = {1, 2, 3} local v = table.remove(t) return v * 10 + #t",
32,
);
check_int(
"local t = {1, 2, 3} local v = table.remove(t, 1) return v * 10 + t[1]",
12,
);
check_str("return table.concat({1, 'b', 2.5}, '-')", b"1-b-2.5");
check_str("return table.concat({}, 'x')", b"");
check_str("return table.concat({9, 8, 7}, '', 2, 3)", b"87");
check_error("table.concat({{}})", "invalid value");
check_int(
"local a, b, c = table.unpack({7, 8, 9}) return a * 100 + b * 10 + c",
789,
);
check_int("return (table.unpack({1, 2, 3}, 2))", 2);
check_int("return select('#', table.unpack({1, 2, 3}))", 3);
check_int("local p = table.pack(4, 5, 6) return p.n * 100 + p[3]", 306);
check_int(
"local t = {1, 2, 3, 4, 5} table.move(t, 1, 3, 3) \
return t[3] * 100 + t[4] * 10 + t[5]",
123,
);
check_int(
"local d = table.move({7, 8}, 1, 2, 1, {}) return d[1] * 10 + d[2]",
78,
);
check_int("return #table.create(16)", 0);
check_str(
"local t = {3, 1, 4, 1, 5, 9, 2, 6} table.sort(t) return table.concat(t, '')",
b"11234569",
);
check_str(
"local t = {3, 1, 4, 1, 5} table.sort(t, function(a, b) return a > b end) \
return table.concat(t, '')",
b"54311",
);
check_str(
"local t = {'pear', 'apple', 'fig'} table.sort(t) return table.concat(t, ',')",
b"apple,fig,pear",
);
check_bool(
"local t = {} for i = 1, 200 do t[i] = (i * 37) % 101 end table.sort(t) \
for i = 2, 200 do if t[i - 1] > t[i] then return false end end return true",
true,
);
check_error(
"local t = {} for i = 1, 64 do t[i] = i end \
table.sort(t, function() return true end)",
"invalid order function",
);
}
#[test]
fn base_additions() {
check_int("return tonumber('42')", 42);
check_float("return tonumber('2.5')", 2.5);
check_int("return tonumber('0x10')", 16);
check_bool("return tonumber('zz') == nil", true);
check_bool("return tonumber({}) == nil", true);
check_int("return tonumber('ff', 16)", 255);
check_int("return tonumber('111', 2)", 7);
check_int("return tonumber('-z', 36)", -35);
check_bool("return tonumber('12', 2) == nil", true);
check_error("return tonumber('1', 99)", "base out of range");
check_int("local f = load('return 1 + 1') return f()", 2);
check_int("local f = load('return ...', 'chunk') return f(9)", 9);
check_bool(
"local f, e = load('syntax ! error') return f == nil and type(e) == 'string'",
true,
);
check_int(
"local env = {x = 5} local f = load('return x', 'c', 't', env) return f()",
5,
);
check_bool("return collectgarbage('count') > 0", true);
check_int("return collectgarbage()", 0);
check_bool("return pairs({}) == next", true);
}
#[test]
fn string_core() {
check_int("return string.len('hello')", 5);
check_int("return ('hello'):len()", 5); check_str("return ('hello'):sub(2, 4)", b"ell");
check_str("return ('hello'):sub(-3)", b"llo");
check_str("return ('hello'):sub(2)", b"ello");
check_str("return ('hello'):sub(4, 2)", b"");
check_str("return ('hello'):sub(-100, 100)", b"hello");
check_str("return ('aBc'):upper()", b"ABC");
check_str("return ('aBc'):lower()", b"abc");
check_str("return ('ab'):rep(3)", b"ababab");
check_str("return ('ab'):rep(3, '-')", b"ab-ab-ab");
check_str("return ('ab'):rep(0)", b"");
check_str("return ('abc'):reverse()", b"cba");
check_int("return ('A'):byte()", 65);
check_int("return select('#', ('abc'):byte(1, 3))", 3);
check_str("return string.char(104, 105)", b"hi");
check_error("return string.char(300)", "value out of range");
check_int("return string.len(123)", 3);
}
#[test]
fn string_find_and_match() {
check_int("return (string.find('hello', 'll'))", 3);
check_int("return select(2, string.find('hello', 'll'))", 4);
check_bool("return string.find('hello', 'xyz') == nil", true);
check_int("return (string.find('hello', 'l+'))", 3);
check_int("return (string.find('a.b', '.', 1, true))", 2); check_int("return (string.find('hello', 'l', -2))", 4); check_str("return (string.match('key=val', '(%w+)=(%w+)'))", b"key");
check_str(
"return select(2, string.match('key=val', '(%w+)=(%w+)'))",
b"val",
);
check_str("return string.match('hello 42!', '%d+')", b"42");
check_bool("return string.match('abc', '%d') == nil", true);
check_int("return string.match('abc', '()b')", 2); check_str("return string.match(' trim ', '^%s*(.-)%s*$')", b"trim");
check_error("return string.match('x', '%')", "malformed pattern");
}
#[test]
fn string_gmatch() {
check_int(
"local n = 0 for w in ('one two three'):gmatch('%a+') do n = n + 1 end return n",
3,
);
check_str(
"local t = {} for k, v in ('a=1,b=2'):gmatch('(%w+)=(%w+)') do t[#t+1] = k .. v end \
return table.concat(t, ' ')",
b"a1 b2",
);
check_str(
"local it = ('x y'):gmatch('%a') local a = it() local b = it() return a .. b",
b"xy",
);
check_int(
"local n = 0 for _ in ('abc'):gmatch('x*') do n = n + 1 end return n",
4,
);
}
#[test]
fn string_gsub() {
check_str("return (('hello world'):gsub('o', '0'))", b"hell0 w0rld");
check_int("return select(2, ('hello'):gsub('l', 'L'))", 2);
check_str("return (('hello'):gsub('l', 'L', 1))", b"heLlo");
check_str("return (('abc'):gsub('(%a)', '%1%1'))", b"aabbcc");
check_str(
"return (('key=val'):gsub('(%w+)=(%w+)', '%2=%1'))",
b"val=key",
);
check_str("return (('ab'):gsub('b', '100%%'))", b"a100%");
check_str(
"return (('$name is $age'):gsub('%$(%w+)', {name = 'lua', age = 30}))",
b"lua is 30",
);
check_str(
"return (('1 2 3'):gsub('%d', function(d) return tonumber(d) * 2 end))",
b"2 4 6",
);
check_str(
"return (('keep drop'):gsub('%a+', function(w) if w == 'drop' then return 'X' end end))",
b"keep X",
);
check_str("return (('ab'):gsub('', '-'))", b"-a-b-");
check_error("return ('x'):gsub('x', '%9')", "invalid capture index");
check_str("return (('x'):gsub('x', {}))", b"x"); check_str("return (('a b cd'):gsub(' *', '-'))", b"-a-b-c-d-");
check_int("return select(2, ('a b cd'):gsub(' *', '-'))", 5);
check_str(
"local t = setmetatable({}, {__index = function(_, k) return k:upper() end}) \
return (('a bb'):gsub('%a+', t))",
b"A BB",
);
check_int("return select(2, ('aaa'):gsub('.', {}))", 3);
}
#[test]
fn string_gmatch_init_and_empty() {
check_int(
"local s = 0 for k in ('10 20 30'):gmatch('%d+', 3) do s = s + tonumber(k) end return s",
50,
);
check_int(
"local s = 0 for k in ('11 21 31'):gmatch('%d+', -2) do s = s + tonumber(k) end return s",
31,
);
check_str(
"local r = '' local i = 1 local sub = 'a b' \
for p, e in sub:gmatch('()%s*()') do r = r .. sub:sub(i, p - 1) .. '-' i = e end \
return r",
b"-a-b-",
);
}
#[test]
fn pattern_classes_balanced_frontier() {
check_str("return string.match('foo (bar) baz', '%b()')", b"(bar)");
check_str("return string.match('THE quick', '%f[%l]%a+')", b"quick");
check_str("return string.match('abc123', '%a+')", b"abc");
check_str("return string.match('abc123', '%A+')", b"123"); check_str("return string.match('a-b', '%p')", b"-");
check_str("return string.match('x\\ty', '%s')", b"\t");
check_str("return string.match('abcabc', '(a%w+)%1')", b"abc"); check_str("return string.match('[x]', '%[(%a)%]')", b"x");
}
#[test]
fn string_format() {
check_str("return string.format('%d', 42)", b"42");
check_str("return string.format('%d', -42)", b"-42");
check_str("return string.format('%5d', 42)", b" 42");
check_str("return string.format('%-5d|', 42)", b"42 |");
check_str("return string.format('%05d', 42)", b"00042");
check_str("return string.format('%+d %+d', 5, -5)", b"+5 -5");
check_str("return string.format('%x', 255)", b"ff");
check_str("return string.format('%X', 255)", b"FF");
check_str("return string.format('%#x', 255)", b"0xff");
check_str("return string.format('%o', 8)", b"10");
check_str("return string.format('%x', -1)", b"ffffffffffffffff");
check_str("return string.format('%c%c', 104, 105)", b"hi");
check_str("return string.format('%s=%s', 'a', 1)", b"a=1");
check_str("return string.format('%10s|', 'hi')", b" hi|");
check_str("return string.format('%-10s|', 'hi')", b"hi |");
check_str("return string.format('%.3s', 'hello')", b"hel");
check_str("return string.format('%f', 1.5)", b"1.500000");
check_str("return string.format('%.2f', 3.14159)", b"3.14");
check_str("return string.format('%.0f', 2.5)", b"2");
check_str("return string.format('%e', 1500.0)", b"1.500000e+03");
check_str("return string.format('%.2E', 0.0001)", b"1.00E-04");
check_str("return string.format('%g', 100000.0)", b"100000");
check_str("return string.format('%g', 1e+20)", b"1e+20");
check_str("return string.format('%g', 0.0001)", b"0.0001");
check_str("return string.format('%g', 0.00001)", b"1e-05");
check_str("return string.format('%.3g', 3.14159)", b"3.14");
check_str("return string.format('%g', 2.0)", b"2");
check_str("return string.format('%a', 1.0)", b"0x1p+0");
check_str("return string.format('%a', 0.5)", b"0x1p-1");
check_str("return string.format('%a', 3.0)", b"0x1.8p+1");
check_str("return string.format('%d%%', 99)", b"99%");
check_str(
"return string.format('%q', 'a\\nb\"c\\\\d')",
b"\"a\\\nb\\\"c\\\\d\"",
);
check_str(
"return string.format('%q', math.mininteger)",
b"0x8000000000000000",
);
check_str("return string.format('%q', 7)", b"7");
check_bool(
"return load('return ' .. string.format('%q', 'x\\0y'))() == 'x\\0y'",
true,
);
check_str("return string.format('%q', 0/0)", b"(0/0)");
check_str("return string.format('%q', 2.0)", b"2.0");
check_str(
"local t = setmetatable({}, {__tostring = function() return 'T' end}) \
return string.format('[%s]', t)",
b"[T]",
);
check_error(
"return string.format('%d', 1.5)",
"no integer representation",
);
check_error("return string.format('%d')", "no value");
check_error("return string.format('%k', 1)", "invalid conversion");
}
#[test]
fn utf8_library() {
check_str("return utf8.char(72, 105)", b"Hi");
check_str("return utf8.char(0x4F60, 0x597D)", "你好".as_bytes());
check_int("return utf8.len('héllo')", 5);
check_int("return utf8.len('你好')", 2);
check_int("return (utf8.codepoint('你好'))", 0x4F60);
check_int("return select(2, utf8.codepoint('你好', 1, -1))", 0x597D);
check_int("return (utf8.offset('你好', 2))", 4);
check_int("return select(2, utf8.offset('你好', 1))", 3);
check_int("return (utf8.offset('你好x', -1))", 7);
check_int(
"local n = 0 for p, c in utf8.codes('a你b') do n = n + 1 end return n",
3,
);
check_int(
"local last for p in utf8.codes('a你b') do last = p end return last",
5,
);
check_bool("return utf8.len('\\xFF') == nil", true);
check_int("return select(2, utf8.len('a\\xFFb'))", 2);
check_error("return utf8.codepoint('\\x80')", "invalid UTF-8 code");
check_bool("return utf8.charpattern ~= nil", true);
check_int("return (utf8.offset('a你b', 0, 3))", 2);
check_int("return select(2, utf8.offset('a你b', 0, 3))", 4);
check_error("return utf8.offset('abc', 1, 5)", "position out of bounds");
check_error("return utf8.offset('', 1, 2)", "position out of bounds");
check_error("return utf8.offset('\\x80', 1)", "continuation byte");
check_error("return utf8.len('abc', 0, 2)", "out of bounds");
check_error("return utf8.len('abc', 1, 4)", "out of bounds");
}
#[test]
fn debug_getinfo_name() {
check_str(
"local function F() return debug.getinfo(1, 'n').name end local r = F() return r",
b"F",
);
check_str(
"local t = {} function t.m() return debug.getinfo(1).name end local r = t.m() return r",
b"m",
);
check_str(
"function glob() return debug.getinfo(1).name end local r = glob() return r",
b"glob",
);
check_bool(
"local r = (function() return debug.getinfo(1, 'n').name end)() return r == nil",
true,
);
}
#[test]
fn debug_getinfo_c_frame_boundary() {
check_str(
"local function c(f) return setmetatable({}, {__close = f}) end \
local what \
local function foo() \
local x <close> = c(function() what = debug.getinfo(2).what end) \
error('e') \
end \
pcall(foo) \
return what",
b"C",
);
check_str(
"local seen \
local function f() seen = debug.getinfo(2).what end \
pcall(f) \
return seen",
b"C",
);
}
#[test]
fn debug_getinfo_source_lines_options() {
check_str(
"local function test (a) \n\
local x = a \n\
return x \n\
end \n\
local i = debug.getinfo(test, 'S') \n\
return i.what .. ',' .. i.linedefined .. ',' .. i.lastlinedefined \n\
.. ',' .. i.namewhat .. ',' .. tostring(i.name)",
b"Lua,1,4,,nil",
);
check_bool(
"local function test (a) \n\
local x = a \n\
return x \n\
end \n\
local act = debug.getinfo(test, 'L').activelines \n\
return act[2] and act[4] and not act[1] and not act[5] \n\
and debug.getinfo(print, 'L').activelines == nil",
true,
);
check_bool(
"return debug.getinfo(1000) == nil \
and not pcall(debug.getinfo, print, 'X') \
and not pcall(debug.getinfo, 1, '>')",
true,
);
check_bool(
"local f = load('return 1') \
local g = load('return ' .. ('p'):rep(400)) \
return debug.getinfo(f).short_src == '[string \"return 1\"]' \
and string.find(debug.getinfo(g).short_src, '^%[string [^\\n]*%.%.%.\"%]$') ~= nil",
true,
);
check_int(
"local f = load(string.dump(load('print(1)'), true)) \
local act = debug.getinfo(f, 'L').activelines \
return #act",
0,
);
}
#[test]
fn debug_line_hook() {
check_str(
"local out = {} \n\
local function h(ev, ln) out[#out + 1] = ev .. ':' .. ln end \n\
debug.sethook(h, 'l') \n\
local a = 1 \n\
local b = 2 \n\
debug.sethook() \n\
return table.concat(out, ',')",
b"line:4,line:5,line:6",
);
check_bool(
"local function h() end \
debug.sethook(h, 'l') \
local got = debug.gethook() \
debug.sethook() \
return got == h and debug.gethook() == nil",
true,
);
}
#[test]
fn debug_line_hook_does_not_recurse_into_itself() {
check_int(
"local n = 0 \n\
local function h(ev) \n\
assert(ev == 'line') \n\
n = n + 1 \n\
end \n\
debug.sethook(h, 'l') \n\
local a = 1 \n\
local b = 2 \n\
local c = 3 \n\
debug.sethook() \n\
return n",
4,
);
}
#[test]
fn debug_line_table_precision() {
let trace = |chunk: &str| -> String {
format!(
"local l = {{}} \
debug.sethook(function(e, n) l[#l + 1] = n end, 'l'); \
load({chunk})(); debug.sethook() \
return table.concat(l, ',')"
)
};
check_str(
&trace("'if\\nmath.sin(1)\\nthen\\n a=1\\nelse\\n a=2\\nend\\n'"),
b"2,4,7",
);
check_str(&trace("'for i=1,3 do\\n a=i\\nend\\n'"), b"1,2,1,2,1,2,1,3");
check_str(
&trace("'local function foo()\\nend\\nfoo()\\nA=1\\nA=2\\nA=3\\n'"),
b"2,3,2,4,5,6",
);
}
#[test]
fn debug_upvalue_order_and_id() {
for _ in 0..50 {
check_str(
"local a, b = 1, 2 \
local f = function (y) if y then a = 10 + b else return a end end \
local n1 = (debug.getupvalue(f, 1)) \
local n2 = (debug.getupvalue(f, 2)) \
return n1 .. ',' .. n2",
b"a,b",
);
check_int(
"local a, b = 0, 5 \
local f = function () return a + b end \
local nm = debug.setupvalue(f, 1, 7) \
return (nm == 'a') and f() or -1",
12,
);
check_bool(
"local a, b = 1, 2 \
local f = function () return a + b end \
local g = function () return b + a end \
return debug.upvalueid(f, 3) == nil \
and debug.upvalueid(f, 1) ~= debug.upvalueid(f, 2) \
and debug.upvalueid(f, 1) == debug.upvalueid(g, 2) \
and debug.upvalueid(string.gmatch('x', 'x'), 1) ~= nil \
and (not pcall(debug.upvaluejoin, f, 9, g, 1))",
true,
);
}
}
#[test]
fn type_error_varinfo() {
check_error(
"local x return x.y",
"attempt to index a nil value (local 'x')",
);
check_error("return undefined_glob.y", "(global 'undefined_glob')");
check_error("local t = {} return t.a.b", "(field 'a')");
check_error(
"local f local r = f()",
"attempt to call a nil value (local 'f')",
);
check_error(
"local r = nope()",
"attempt to call a nil value (global 'nope')",
);
check_error(
"local n return n + 1",
"attempt to perform arithmetic on a nil value (local 'n')",
);
check_error(
"local up local function g() return up.x end return g()",
"(upvalue 'up')",
);
}
#[test]
fn error_object_edges() {
check_str(
"local ok, msg = pcall(function() error() end) return msg",
b"<no error object>",
);
check_str(
"local ok, msg = pcall(function() error(nil) end) return msg",
b"<no error object>",
);
check_bool(
"local ok, msg = pcall(function() error('boom', 0) end) return msg == 'boom'",
true,
);
check_error(
"return tostring()",
"bad argument #1 to 'tostring' (value expected)",
);
check_error(
"return tonumber()",
"bad argument #1 to 'tonumber' (value expected)",
);
check_str("return tostring(1)", b"1");
}
#[test]
fn metatables_basic_types_and_len_arity() {
check_int(
"local t = setmetatable({}, {__len = function(a, b) return (a == b) and 7 or 0 end}) \
return #t",
7,
);
check_int(
"debug.setmetatable(10, {__index = function(a, b) return a + b end}) \
local r = (10)[3] debug.setmetatable(10, nil) return r",
13,
);
check_bool(
"debug.setmetatable(true, {__index = {hi = 42}}) \
local r = (true).hi debug.setmetatable(true, nil) return r == 42",
true,
);
check_bool(
"local mt = {} debug.setmetatable(1.5, mt) \
local ok = getmetatable(-2) == mt debug.setmetatable(1.5, nil) return ok",
true,
);
}
#[test]
fn table_move_and_sort_guards() {
check_str(
"local src = setmetatable({}, {__index = function(_, k) return ('%d'):format(k) end}) \
local dst = table.move(src, 1, 3, 1, {}) return dst[1] .. dst[2] .. dst[3]",
b"123",
);
check_error(
"table.move({}, 0, math.maxinteger, 1)",
"too many elements to move",
);
check_error(
"table.move({}, 1, 2, math.maxinteger)",
"destination wrap around",
);
check_error(
"table.sort(setmetatable({}, {__len = function() return math.maxinteger end}))",
"array too big",
);
check_error(
"table.sort({1,2,3,4}, function() return true end)",
"invalid order function",
);
check_int(
"local t = {3,1,2,5,4} table.sort(t) return t[1]*10000+t[2]*1000+t[3]*100+t[4]*10+t[5]",
12345,
);
}
#[test]
fn global_declarations_55_edges() {
check_int("global = 1; return global", 1);
check_int("local global = 41; return global + 1", 42);
check_compile_error("global none; X = 1", "variable 'X'");
check_compile_error(
"global none; local function f() XXX = 1 end",
"variable 'XXX'",
);
check_int("global *; Y = 7; return _ENV.Y", 7);
check_compile_error(
"global<const> foo; function foo() end",
"assign to const variable 'foo'",
);
check_int("global<const> a = 5; return _ENV.a", 5);
check_compile_error("global X<close>", "cannot be to-be-closed");
check_compile_error("global <close> *", "cannot be to-be-closed");
check_compile_error("global _ENV, a; a = 10", "variable 'a'");
check_int(
"local X = 10; do global X; X = 20 end; return X * 1000 + _ENV.X",
10020,
);
check_int(
"local a, b = 100, 200; do global a, b = a, b end; return _ENV.a + _ENV.b",
300,
);
check_error(
"_ENV.dup = 1; global dup = 2",
"global 'dup' already defined",
);
check_error(
"_ENV.fdup = 1; global function fdup() end",
"global 'fdup' already defined",
);
}
#[test]
fn goto_scope_over_declarations() {
check_compile_error("goto l1; local aa ::l1:: ::l2:: return 0", "scope of 'aa'");
check_compile_error("goto l2; global *; ::l1:: ::l2:: return 0", "scope of '*'");
check_compile_error(
"repeat if x then goto cont end local xuxu = 10 ::cont:: until xuxu < 1",
"scope of 'xuxu'",
);
}
#[test]
fn lexer_string_escape_near_tokens() {
check_compile_error(r#"return "\999""#, r#"near '\999"'"#);
check_compile_error(r#"return "abc\u{100000000}""#, r#"near 'abc\u{100000000'"#);
check_compile_error(r#"return "abc\u{11r""#, r#"near 'abc\u{11r'"#);
check_compile_error(r#"return "abc\u""#, r#"near 'abc\u"'"#);
check_compile_error("return 'alo", "unfinished string near <eof>");
}
#[test]
fn string_constants_are_interned_per_chunk() {
let long = "0123456789012345678901234567890123456789012345"; check_int(
&format!(
"local a <const> = {long:?} \
local function f() return {long:?} end \
return (string.format('%p', a) == string.format('%p', f())) and 1 or 0"
),
1,
);
check_compile_error("#x = 1", "unexpected symbol");
}
#[test]
fn comparison_and_for_error_wording() {
check_error(
"return print < print",
"attempt to compare two function values",
);
check_error("return {} < {}", "attempt to compare two table values");
check_error("return 1 < 'x'", "attempt to compare number with string");
check_error(
"for i = 1, 'x', 10 do end",
"bad 'for' limit (number expected, got string)",
);
check_error(
"for i = 1, 10, print do end",
"bad 'for' step (number expected, got function)",
);
}
#[test]
fn parser_near_token_and_goto_lines() {
check_compile_error("local a = {", "near <eof>");
check_compile_error("local a = (1", "near <eof>");
check_compile_error("local a = 1 +", "near <eof>");
check_compile_error("::A:: a = 1 ::A::", "already defined on line 1");
check_compile_error(
"goto A do ::A:: end",
"no visible label 'A' for <goto> at line 1",
);
}
#[test]
fn string_dump_round_trips() {
check_int(
"local s = string.dump(load('return 6*7')) return load(s)()",
42,
);
check_int("return load(string.dump(load('return 40+2'), true))()", 42);
check_str(
"local f = load([[return ('a'..1)..(2.5)..tostring(true)..tostring(nil)]]) \
return load(string.dump(f))()",
b"a12.5truenil",
);
check_int(
"gv = 9 local s = string.dump(load('return gv')) return load(s)()",
9,
);
check_int(
"local src = 'local function k() return 5 end return k()+k()' \
return load(string.dump(load(src)))()",
10,
);
check_error("string.dump(print)", "unable to dump given function");
}
#[test]
fn dbg_frame_inserts_tail_synthetic_under_51() {
let mut vm = Vm::new(LuaVersion::Lua51);
vm.eval(
"local function f (x) \
if x then \
assert(debug.getinfo(2).what == 'tail') \
assert(not pcall(getfenv, 3)) \
assert(debug.getinfo(3, 'f').func == g1) \
end \
end \
function g(x) return f(x) end \
function g1(x) g(x) end \
local function h(x) local f = g1; return f(x) end \
h(true)",
)
.expect("5.1 tail-call shape");
let mut vm5 = Vm::new(LuaVersion::Lua55);
vm5.eval(
"local function f (x) \
if x then \
assert(debug.getinfo(1, 't').istailcall == true) \
local tail = debug.getinfo(2) \
assert(tail.func == g1 and tail.istailcall == true) \
end \
end \
function g(x) return f(x) end \
function g1(x) g(x) end \
local function h(x) local f = g1; return f(x) end \
h(true)",
)
.expect("5.5 tail-call shape");
}
#[test]
fn tail_call_to_native_keeps_caller_frame() {
let mut vm = Vm::new(LuaVersion::Lua51);
let r = vm
.eval(
"local function foo (a) \
setfenv(0, a) \
coroutine.yield(getfenv()) \
return getfenv() \
end \
local f = coroutine.wrap(foo) \
local a = {} \
local r1 = f(a) \
local _, r2 = pcall(f) \
return r1 == _G, r2 == _G",
)
.expect("eval");
assert!(
matches!(r.first(), Some(Value::Bool(true))),
"r1 == _G slot: {:?}",
r.first()
);
assert!(
matches!(r.get(1), Some(Value::Bool(true))),
"r2 == _G slot (tail call to native must preserve caller frame): {:?}",
r.get(1)
);
}
#[test]
fn weak_table_dead_key_does_not_alias_reused_alloc() {
check_int(
"local rounds = 32 \
for _ = 1, rounds do \
local a = setmetatable({}, {__mode = 'kv'}) \
a[string.rep('a', 2^14)] = 1 \
a[string.rep('b', 2^14)] = {} \
a[{}] = 2 \
collectgarbage() \
local k = next(a) \
a[k] = nil \
collectgarbage() \
assert(next(a) == nil) \
end \
return rounds",
32,
);
}
#[test]
fn weak_kv_table_marks_surviving_string_keys() {
check_int(
"local rounds = 16 \
for _ = 1, rounds do \
local a = setmetatable({}, {__mode = 'kv'}) \
a[string.rep('a', 1024)] = 25 \
a[string.rep('b', 1024)] = {} \
a[{}] = 14 \
collectgarbage() \
local k, v = next(a) \
assert(type(k) == 'string' and k:sub(1,1) == 'a' and v == 25, \
'expected (\"a*\", 25), got ('..tostring(k)..', '..tostring(v)..')') \
assert(next(a, k) == nil) \
end \
return rounds",
16,
);
}
#[test]
fn coroutine_resume_refuses_too_many_results() {
check_str(
"local lim = 1000000 \
local out = {} \
for _, j in ipairs{lim - 10, lim - 5, lim - 1, lim, lim + 1} do \
local co = coroutine.create(function () \
local t = {} \
for i = 1, j do t[i] = i end \
return table.unpack(t) \
end) \
local r = coroutine.resume(co) \
out[#out + 1] = tostring(r) \
end \
return table.concat(out, ',')",
b"false,false,false,false,false",
);
}
#[test]
fn le_synthesis_via_lt_is_yieldable_53() {
let mut vm = Vm::new(LuaVersion::Lua53);
let r = vm
.eval(
"local mt = { __lt = function (a, b) \
coroutine.yield(nil, 'lt'); return a.x < b.x end } \
local a = setmetatable({x=10}, mt) \
local b = setmetatable({x=12}, mt) \
local co = coroutine.wrap(function () return a <= b end) \
local _, stat = co() \
local r = co() \
return stat, r",
)
.expect("eval");
assert!(
matches!(r.first(), Some(Value::Str(s)) if s.as_bytes() == b"lt"),
"stat slot: {:?}",
r.first()
);
assert!(
matches!(r.get(1), Some(Value::Bool(true))),
"result slot: {:?}",
r.get(1)
);
}
#[test]
fn load_seeds_globals_only_when_one_upvalue() {
check_bool(
"local a, b = 20, 30 \
local d = string.dump(function (set) \
if set == 'set' then a = 10+b; b = b+1 else return a end \
end) \
local x = assert(load(d)) \
return x() == nil",
true,
);
check_int(
"local x = assert(load('return 1 + #_G')) return x() >= 1 and 1 or 0",
1,
);
}
#[test]
fn string_dump_header_is_per_version() {
let cases = &[
(LuaVersion::Lua53, 0x53u8, 0x11usize),
(LuaVersion::Lua54, 0x54u8, 0x0fusize),
(LuaVersion::Lua55, 0x55u8, 0usize), ];
for &(version, ver_byte, luac_int_off) in cases {
let mut vm = Vm::new(version);
let bytes = match vm.eval("return string.dump(function () return 7 end)") {
Ok(vs) => match vs.into_iter().next() {
Some(Value::Str(s)) => s.as_bytes().to_vec(),
v => panic!("{version:?}: dump returned {v:?}, expected Str"),
},
Err(e) => panic!("{version:?}: eval failed: {e:?}"),
};
assert!(
bytes.starts_with(b"\x1bLua"),
"{version:?}: missing signature, got {:?}",
&bytes[..4]
);
assert_eq!(bytes[4], ver_byte, "{version:?}: version byte mismatch");
if luac_int_off > 0 {
let off = luac_int_off;
let int_bytes: [u8; 8] = bytes[off..off + 8].try_into().unwrap();
assert_eq!(
i64::from_le_bytes(int_bytes),
0x5678,
"{version:?}: LUAC_INT mismatch at offset {off}"
);
}
}
}
#[test]
fn coroutine_basics() {
check_int(
"local co = coroutine.create(function(a) local b = coroutine.yield(a+1) return b*10 end) \
local _, x = coroutine.resume(co, 4) \
local _, y = coroutine.resume(co, 7) \
return x*100 + y",
570, );
check_str(
"local co = coroutine.create(function() coroutine.yield() end) \
local a = coroutine.status(co) coroutine.resume(co) \
local b = coroutine.status(co) coroutine.resume(co) \
local c = coroutine.status(co) return a..','..b..','..c",
b"suspended,suspended,dead",
);
check_int(
"local function gen(t) for _,v in ipairs(t) do coroutine.yield(v) end end \
local data = {3, 4, 5} \
local sum = 0 \
for v in coroutine.wrap(function() gen(data) end) do sum = sum + v end \
return sum",
12,
);
check_bool(
"local co = coroutine.create(function() end) coroutine.resume(co) \
local ok = coroutine.resume(co) return ok",
false,
);
check_str(
"local co = coroutine.create(function() error('boom') end) \
local ok, msg = coroutine.resume(co) \
return tostring(ok)..':'..(msg:match('boom') or '?')",
b"false:boom",
);
check_error(
"coroutine.yield(1)",
"attempt to yield from outside a coroutine",
);
check_bool("local _, m = coroutine.running() return m", true);
check_int(
"local p = os.tmpname() \
local w = assert(io.open(p, 'w')) \
w:write('local x, z = coroutine.yield(10)\\n') \
w:write('local y = coroutine.yield(20)\\n') \
w:write('return x + y * z\\n') \
w:close() \
local co = coroutine.wrap(dofile) \
local a = co(p) \
local b = co(100, 101) \
local c = co(7) \
os.remove(p) \
return a * 1000000 + b * 1000 + c",
10_020_807,
);
check_str(
"local co = coroutine.create(error) \
local _, m1 = coroutine.resume(co, 100) \
local s2, m2 = coroutine.close(co) \
local s3, m3 = coroutine.close(co) \
return m1 .. ',' .. tostring(s2) .. ',' .. m2 .. ',' .. tostring(s3) .. ',' .. tostring(m3)",
b"100,false,100,true,nil",
);
check_str(
"local function c(f) return setmetatable({}, {__close = f}) end \
local trace = {} \
local co = coroutine.create(function () \
local a <close> = c(function (_, e) trace[#trace+1] = 'a:'..tostring(e) end) \
coroutine.yield() \
end) \
coroutine.resume(co) \
local ok = coroutine.close(co) \
return tostring(ok) .. ',' .. table.concat(trace, ',') .. ',' .. coroutine.status(co)",
b"true,a:nil,dead",
);
check_str(
"local function c(f) return setmetatable({}, {__close = f}) end \
local co = coroutine.create(function () \
local a <close> = c(function () error('boom') end) \
coroutine.yield() \
end) \
coroutine.resume(co) \
local ok, err = coroutine.close(co) \
return tostring(ok) .. ',' .. (err:match('boom') or '?')",
b"false,boom",
);
check_str(
"local function f() coroutine.yield(10); return 20 end \
local co = coroutine.create(function() return pcall(f) end) \
local _, y1 = coroutine.resume(co) \
local ok, st, v = coroutine.resume(co) \
return y1 .. ',' .. tostring(ok) .. ',' .. tostring(st) .. ',' .. v",
b"10,true,true,20",
);
check_str(
"local function f() coroutine.yield(); error('boom') end \
local co = coroutine.create(function() return pcall(f) end) \
coroutine.resume(co) \
local _, ok, msg = coroutine.resume(co) \
return tostring(ok) .. ',' .. (msg:match('boom') or '?')",
b"false,boom",
);
check_str(
"local co = coroutine.create(pcall) \
local _, y = coroutine.resume(co, function() return coroutine.yield(5) + 1 end) \
local ok, st, v = coroutine.resume(co, 100) \
return y .. ',' .. tostring(ok) .. ',' .. tostring(st) .. ',' .. v",
b"5,true,true,101",
);
check_str(
"local function f() coroutine.yield(); error('boom') end \
local co = coroutine.create(function() return xpcall(f, function(m) return 'H:'..m end) end) \
coroutine.resume(co) \
local _, ok, msg = coroutine.resume(co) \
return tostring(ok) .. ',' .. tostring(msg:match('^H:') ~= nil) .. ',' .. (msg:match('boom') or '?')",
b"false,true,boom",
);
}
#[test]
fn pcall_continuation_no_yield() {
check_str(
"local ok, a, b = pcall(function() return 1, 2 end) \
return tostring(ok) .. ',' .. a .. ',' .. b",
b"true,1,2",
);
check_str(
"local ok, msg = pcall(function() error('x') end) \
return tostring(ok) .. ',' .. (msg:match('x') or '?')",
b"false,x",
);
check_str(
"local ok, n = pcall(math.type, 1.0) return tostring(ok) .. ',' .. n",
b"true,float",
);
check_int(
"local function ce(f) local s = pcall(f); assert(not s) end \
ce(function() error('e') end) \
local a,b,c,d,e,f,g,h = 1,2,3,4,5,6,7,8 \
return a+b+c+d+e+f+g+h",
36,
);
check_str(
"local function c(fn) return setmetatable({}, {__close = fn}) end \
local seen \
local ok, msg = pcall(function() \
local x <close> = c(function(_, e) seen = e end) \
error('boom') \
end) \
return tostring(ok) .. ',' .. (msg:match('boom') or '?') .. ',' .. (seen:match('boom') or '?')",
b"false,boom,boom",
);
check_bool(
"local n = 0 \
local function rec() n = n + 1; return pcall(rec) end \
pcall(rec) \
return n > 100 and n < 2000",
true,
);
check_str(
"local ok, m = xpcall(function() error('boom') end, function(msg) return 'H:'..msg end) \
return tostring(ok) .. ',' .. tostring(m:match('^H:') ~= nil) .. ',' .. (m:match('boom') or '?')",
b"false,true,boom",
);
}
#[test]
fn yield_across_c_boundary() {
check_str(
"local r \
coroutine.wrap(function() \
string.gsub('a', 'a', function() r = coroutine.isyieldable() end) \
end)() \
return tostring(r)",
b"false",
);
check_str(
"local co = coroutine.wrap(function() \
local ok, msg = pcall(table.sort, {3, 1, 2}, coroutine.yield) \
return tostring(ok) .. ',' .. (msg:match('C%-call boundary') or '?') \
end) \
return co()",
b"false,C-call boundary",
);
check_str(
"local c = function(f) return setmetatable({}, {__close = f}) end \
local X = 'no' \
local co = coroutine.create(function() \
local v <close> = c(function() X = 'closed' end) \
string.gsub('a', 'a', function() \
coroutine.close() \
X = 'unreachable' \
end) \
end) \
local st, msg = coroutine.resume(co) \
return tostring(st) .. ',' .. tostring(msg) .. ',' .. X .. ',' .. coroutine.status(co)",
b"true,nil,closed,dead",
);
check_str(
"local c = function(f) return setmetatable({}, {__close = f}) end \
local co = coroutine.create(function() \
local v <close> = c(function() error('boom') end) \
string.gsub('a', 'a', function() \
assert(pcall(pcall, function() coroutine.close() end)) \
end) \
end) \
local st, msg = coroutine.resume(co) \
return tostring(st) .. ',' .. (msg:match('boom') or '?')",
b"false,boom",
);
check_str(
"local function iter(_, i) return coroutine.yield(i) end \
local co = coroutine.wrap(function() \
for i in iter, nil, 1 do end \
end) \
return tostring(co()) .. ',' .. tostring(co(7))",
b"1,7",
);
check_str(
"local coro = false \
for i = 1, 1000 do \
local previous = coro \
coro = coroutine.create(function() \
local cc <close> = setmetatable({}, {__close = function() \
if previous then assert(coroutine.close(previous)) end \
end}) \
coroutine.yield() \
end) \
assert(coroutine.resume(coro)) \
end \
local st, msg = coroutine.close(coro) \
return tostring(st) .. ',' .. (msg:match('C stack overflow') or '?')",
b"false,C stack overflow",
);
}
#[test]
fn named_vararg() {
check_str(
"local function f(...v) \
return v[1]..','..v[2]..','..tostring(v.n)..','..tostring(v[5])..','..tostring(v[1.0]) end \
return f(10, 20, 30)",
b"10,20,3,nil,10",
);
check_bool(
"local function f(...v) return v[1] end \
f(1, 2, 3) \
collectgarbage() \
local m = collectgarbage'count' \
f(4, 5, 6); f(7, 8, 9) \
return m == collectgarbage'count'",
true,
);
check_str(
"local function aux(...t) t[1] = t[1] + 100; return ... end \
return table.concat({aux(1, 2, 3)}, ',')",
b"101,2,3",
);
check_int(
"local function aux(..._ENV) global x; x = 10; return x end return aux()",
10,
);
check_int(
"local function f(...t) local g = function() return t.n end return g() end \
return f(5, 6, 7, 8)",
4,
);
}
#[test]
fn call_metamethod_chains() {
check_int(
"local function f() return 42 end \
local t = setmetatable({}, {__call = f}) \
t = setmetatable({}, {__call = t}) \
t = setmetatable({}, {__call = t}) \
return t()",
42,
);
check_error(
"local a = {} for i = 1, 16 do a = setmetatable({}, {__call = a}) end a()",
"'__call' chain too long",
);
check_error(
"local a = {} setmetatable(a, {__call = a}) a()",
"'__call' chain too long",
);
}
#[test]
fn load_reader_and_mode() {
check_int(
"local parts = {'return ', '1 + ', '2'} local i = 0 \
local f = load(function() i = i + 1 return parts[i] end) return f()",
3,
);
check_bool("return (load(function() error('boom') end)) == nil", true);
check_bool("return (load(function() return true end)) == nil", true);
check_str(
"local _, m = load('return 1', 'c', 'b') return m",
b"attempt to load a text chunk (mode is 'b')",
);
check_int(
"local d = string.dump(load('return 6*7')) return load(function() local s=d d=nil return s end, 'c', 'b')()",
42,
);
}
#[test]
fn length_border_on_sparse_power_of_two_keys() {
check_int(
"local t = {} for i = 62, 0, -1 do t[2^i] = true end \
local n = #t return (n == 2 or n == 4) and n or -1",
2,
);
check_str(
"collectgarbage('incremental') \
local a = collectgarbage('generational') \
local b = collectgarbage('incremental') return a..','..b",
b"incremental,generational",
);
check_int(
"local old = collectgarbage('param', 'pause', 100) \
return collectgarbage('param', 'pause')",
100,
);
check_int(
"collectgarbage('param', 'stepmul', 250) \
return collectgarbage('param', 'stepmul', 300)",
250,
);
}
#[test]
fn incremental_gc_step() {
check_bool(
"collectgarbage('incremental') collectgarbage() \
local a = {} for i = 1, 500 do a[i] = {{}} end \
local before = collectgarbage('count') \
a = nil \
repeat until collectgarbage('step', 1) \
return collectgarbage('count') < before",
true,
);
check_bool(
"collectgarbage('incremental') collectgarbage('param', 'stepsize', 0) \
return collectgarbage('step')",
true,
);
check_bool(
"collectgarbage('generational') \
local t = setmetatable({}, {__mode = 'v'}) \
t[1] = {10} \
collectgarbage('step') \
local r = t[1] == nil \
collectgarbage('incremental') return r",
true,
);
}
#[test]
fn stdlibs_registered_in_package_loaded() {
check_bool(
"for _, n in ipairs{'string','table','math','os','io','utf8','debug','coroutine'} do \
if package.loaded[n] ~= _G[n] then return false end \
end \
return true",
true,
);
}
#[test]
fn io_read_number_format() {
check_bool(
"local p = os.tmpname() \
local f = assert(io.open(p, 'w')) \
f:write(math.maxinteger, '\\n', '0xABCp-3\\n', '1234x') \
f:close() \
local g = assert(io.open(p, 'r')) \
local a, b, c = g:read('n'), g:read('n'), g:read('n') \
local d = g:read(1) \
g:close(); os.remove(p) \
return a == math.maxinteger and b == 0xABCp-3 and c == 1234 and d == 'x'",
true,
);
check_bool(
"return (not pcall(load, '', '', 'B')) and load('return 7', 'n', 't')() == 7",
true,
);
check_bool(
"local p = os.tmpname() \
local f = assert(io.open(p, 'w')); f:write('234e+13E'); f:close() \
local g = assert(io.open(p, 'r')) \
local n, rest = g:read('n'), g:read(1) \
g:close(); os.remove(p) \
return n == 234e13 and rest == 'E'",
true,
);
check_bool(
"local p = os.tmpname() \
local f = assert(io.open(p, 'w')); f:write('1234'); for _=1,1000 do f:write('0') end; f:close() \
local g = assert(io.open(p, 'r')) \
local n = g:read('n') \
local tail = g:read('a') \
local eof = g:read(0) \
g:close(); os.remove(p) \
return n == nil and tail:match('^00*$') ~= nil and eof == nil",
true,
);
}
#[test]
fn io_error_and_close_semantics() {
check_bool(
"local p = os.tmpname() \
local w = assert(io.open(p, 'w')); w:write('a\\nb\\n'); w:close() \
local it = io.lines(p) \
while it() do end \
local ok, err = pcall(it) \
os.remove(p) \
return not ok and string.find(err, 'file is already closed', 1, true) ~= nil",
true,
);
check_bool(
"local p = os.tmpname() \
local f = assert(io.open(p, 'w')); assert(f:close()) \
local ok, err = pcall(io.close, f) \
os.remove(p) \
return not ok and string.find(err, 'closed file', 1, true) ~= nil",
true,
);
check_bool(
"local p = os.tmpname() \
io.output(p); io.write('x'); assert(io.flush()) \
io.input(p); io.close(io.input()) \
local rok, rerr = pcall(io.read) \
io.close(io.output()) \
local wok, werr = pcall(io.write, 'y') \
os.remove(p) \
return not rok and string.find(rerr, 'input file is closed', 1, true) ~= nil \
and not wok and string.find(werr, 'output file is closed', 1, true) ~= nil",
true,
);
check_bool(
"local p = os.tmpname() \
local w = assert(io.open(p, 'w')); w:write('hello\\n'); w:close() \
local t = {}; for i = 1, 251 do t[i] = 1 end \
local ok, err = pcall(io.lines, p, table.unpack(t)) \
os.remove(p) \
return not ok and string.find(err, 'too many arguments', 1, true) ~= nil",
true,
);
}
#[test]
fn io_open_read_write_seek() {
check_bool(
"local p = os.tmpname() \
local f = assert(io.open(p, 'w')) \
f:write('a\\n', 'bb\\n', 'ccc'):close() \
local g = assert(io.open(p, 'r')) \
local l1 = g:read('l') \
local pos = g:seek() \
local rest = g:read('a') \
g:close() \
local h = assert(io.open(p, 'a')); h:write('Z'); local e = h:seek('end'); h:close() \
local ok_remove = os.remove(p) \
return l1 == 'a' and pos == 2 and rest == 'bb\\nccc' \
and e == 9 and ok_remove == true and io.open(p) == nil",
true,
);
check_bool(
"for _, m in ipairs{'rw', 'rb+', 'r+bk', '', '+', 'b'} do \
if pcall(io.open, 'x', m) then return false end \
end \
return true",
true,
);
}
#[test]
fn io_file_model_foundation() {
check_bool(
"return io.type(io.stdin) == 'file' \
and io.type(8) == nil and io.type({}) == nil \
and getmetatable(io.stdin).__name == 'FILE*' \
and io.input() == io.stdin and io.output() == io.stdout \
and (not io.close(io.stdin)) and (not io.stdout:close()) \
and tostring(io.stdout):sub(1, 5) == 'file '",
true,
);
check_bool(
"local ok, err = pcall(io.stdin.close) \
return not ok and string.find(err, 'got no value', 1, true) ~= nil",
true,
);
}
#[test]
fn io_std_streams_are_userdata() {
check_bool(
"return type(io.stdin) == 'userdata' \
and io.stdin == io.stdin and io.stdin ~= io.stdout \
and string.format('%p', io.stdin) ~= '(null)' \
and ({[io.stdin] = 7})[io.stdin] == 7 \
and not pcall(rawlen, io.stdin)",
true,
);
}
#[test]
fn gc_finalizers() {
check_bool(
"local finished = false \
local u = setmetatable({}, {__gc = function () finished = true end}) \
u = nil \
collectgarbage() \
return finished",
true,
);
check_bool(
"local res = true \
setmetatable({}, {__gc = function () res = collectgarbage() end}) \
collectgarbage() \
return res == nil",
true,
);
check_bool(
"local ran = false \
local mt = {} \
local u = setmetatable({}, mt) \
mt.__gc = function () ran = true end \
u = nil \
collectgarbage() \
return not ran",
true,
);
check_str(
"local n = '' \
setmetatable({}, {__gc = function () \
local t = debug.getinfo(1) \
n = t.namewhat .. ':' .. tostring(t.name) \
end}) \
collectgarbage() \
return n",
b"metamethod:__gc",
);
check_int(
"local count = 0 \
local alive \
local mt = {__gc = function (o) count = count + 1; alive = o end} \
alive = setmetatable({}, mt) \
alive = nil \
collectgarbage() \
setmetatable(alive, mt) \
alive = nil \
collectgarbage() \
return count",
2,
);
}
#[test]
fn warn_library_5_4_plus() {
let mut vm = Vm::new(LuaVersion::Lua55);
vm.eval(
"warn('silent before @on') \
warn('@on') \
warn('hello') \
warn('multi', '-', 'arg') \
warn('@unknown') -- ignored, no emit \
warn('@off') \
warn('silent after @off')",
)
.expect("warn calls succeed");
let log = vm.warn_log_take();
let lines: Vec<String> = log
.into_iter()
.map(|b| String::from_utf8_lossy(&b).into_owned())
.collect();
assert_eq!(lines, vec!["hello".to_string(), "multi-arg".to_string()]);
}
#[test]
fn warn_library_absent_on_5_3() {
let mut vm = Vm::new(LuaVersion::Lua53);
let result = vm.eval("warn('test')");
match result {
Err(e) => {
let msg = vm.error_text(&e);
assert!(
msg.contains("attempt to call") || msg.contains("nil value"),
"expected nil-call error, got: {msg}"
);
}
Ok(_) => panic!("warn should be absent on 5.3 (no error)"),
}
}
#[test]
fn os_execute_shell_probe_and_command() {
check_bool("return os.execute() == true", true);
let mut vm51 = Vm::new(LuaVersion::Lua51);
let v = vm51.eval("return os.execute()").expect("5.1 probe ok");
assert_eq!(v.len(), 1);
match v[0] {
Value::Int(1) => {}
v => panic!("5.1 os.execute() expected Int(1), got {v:?}"),
}
check_str(
"local ok, kind, code = os.execute('true') \
return tostring(ok)..':'..kind..':'..tostring(code)",
b"true:exit:0",
);
check_str(
"local ok, kind, code = os.execute('exit 7') \
return tostring(ok)..':'..kind..':'..tostring(code)",
b"false:exit:7",
);
}
#[cfg(unix)]
#[test]
fn io_popen_read_write_and_close_status() {
check_str(
"local f = io.popen('printf hello-popen') \
local out = f:read('a') \
local ok, kind, code = f:close() \
return out..':'..tostring(ok)..':'..kind..':'..tostring(code)",
b"hello-popen:true:exit:0",
);
check_str(
"local f = io.popen('cat > /dev/null', 'w') \
f:write('whatever') \
local ok, kind, code = f:close() \
return tostring(ok)..':'..kind..':'..tostring(code)",
b"true:exit:0",
);
check_str(
"local f = io.popen('exit 4') \
f:read('a') \
local ok, kind, code = f:close() \
return tostring(ok)..':'..kind..':'..tostring(code)",
b"false:exit:4",
);
}
#[test]
fn embedding_instr_budget_interrupts_infinite_loop() {
let mut vm = Vm::new(LuaVersion::Lua55);
vm.set_instr_budget(Some(5_000));
let result = vm.eval(
"local ok, err = pcall(function () \
while true do end \
end) \
return ok, err",
);
let v = result.expect("pcall itself must succeed");
assert_eq!(v.len(), 2);
assert!(
matches!(v[0], Value::Bool(false)),
"pcall should have caught the budget error, got {:?}",
v[0]
);
match v[1] {
Value::Str(s) => assert!(
s.as_bytes()
.windows(20)
.any(|w| w == b"instruction budget e"),
"error msg should mention the budget: {:?}",
String::from_utf8_lossy(s.as_bytes())
),
v => panic!("expected error string, got {v:?}"),
}
assert_eq!(vm.instr_budget_remaining(), None);
}
#[test]
fn embedding_instr_budget_unset_runs_normally() {
let mut vm = Vm::new(LuaVersion::Lua55);
let v = vm
.eval("local s = 0 for i=1,1000 do s = s + i end return s")
.unwrap();
assert_eq!(v.len(), 1);
assert!(matches!(v[0], Value::Int(500_500)), "got {:?}", v[0]);
}
#[test]
fn embedding_new_minimal_has_no_globals() {
let mut vm = Vm::new_minimal(LuaVersion::Lua55);
let result = vm.eval("print('hi')");
match result {
Err(e) => {
let msg = vm.error_text(&e);
assert!(
msg.contains("attempt to call") || msg.contains("nil"),
"expected nil-call error, got: {msg}"
);
}
Ok(_) => panic!("print should not exist on a new_minimal vm"),
}
}
#[test]
fn embedding_selective_open_base_enables_print() {
let mut vm = Vm::new_minimal(LuaVersion::Lua55);
vm.open_base();
let v = vm
.eval("return type(print), type(tostring), tostring(42)")
.unwrap();
assert_eq!(v.len(), 3);
assert!(matches!(v[0], Value::Str(_)));
if let Value::Str(s) = v[0] {
assert_eq!(s.as_bytes(), b"function");
}
if let Value::Str(s) = v[2] {
assert_eq!(s.as_bytes(), b"42");
}
let v = vm.eval("return math").unwrap();
assert_eq!(v.len(), 1);
assert!(matches!(v[0], Value::Nil));
}
#[test]
fn embedding_memory_cap_catches_runaway_alloc() {
let mut vm = Vm::new(LuaVersion::Lua55);
let baseline = vm.memory_used();
vm.set_memory_cap(Some(baseline + 64 * 1024)); let v = vm
.eval(
"local outer = {} \
local ok, err = pcall(function () \
for i = 1, 1000000 do outer[i] = string.rep('x', 100) end \
return outer \
end) \
return ok, err",
)
.expect("pcall succeeds even when the inner alloc trips the cap");
assert_eq!(v.len(), 2);
assert!(
matches!(v[0], Value::Bool(false)),
"pcall should catch the cap error, got {:?}",
v[0]
);
match v[1] {
Value::Str(s) => assert!(
s.as_bytes().windows(15).any(|w| w == b"memory cap exce"),
"msg should mention the cap: {:?}",
String::from_utf8_lossy(s.as_bytes())
),
v => panic!("expected error string, got {v:?}"),
}
}
#[test]
fn embedding_kevy_shape_short_script_per_request() {
let mut vm = Vm::new(LuaVersion::Lua55);
vm.set_instr_budget(Some(10_000));
let v = vm
.eval("local s = 0 for i=1,100 do s = s + i end return s")
.unwrap();
assert!(matches!(v[0], Value::Int(5050)));
assert!(vm.instr_budget_remaining().unwrap_or(0) > 0);
vm.set_instr_budget(Some(500));
let err = vm.eval("while true do end").expect_err("budget must trip");
let msg = vm.error_text(&err);
assert!(msg.contains("instruction budget"), "got: {msg}");
assert_eq!(vm.instr_budget_remaining(), None);
vm.set_instr_budget(Some(10_000));
let v = vm
.eval("local t = {1,2,3,4,5}; local s = 0 for _, x in ipairs(t) do s = s + x end return s")
.unwrap();
assert!(matches!(v[0], Value::Int(15)));
vm.set_global("counter", Value::Int(0)).unwrap();
for expected in 1..=5 {
vm.set_instr_budget(Some(10_000));
let v = vm.eval("counter = counter + 1; return counter").unwrap();
assert!(
matches!(v[0], Value::Int(n) if n == expected),
"iter {expected}: got {:?}",
v[0],
);
}
}
#[test]
fn embedding_memory_cap_unset_runs_normally() {
let mut vm = Vm::new(LuaVersion::Lua55);
let v = vm
.eval(
"local t = {} \
for i = 1, 10000 do t[i] = tostring(i) end \
return #t",
)
.unwrap();
assert_eq!(v.len(), 1);
assert!(matches!(v[0], Value::Int(10000)));
}
fn panic_string_native(
_vm: &mut Vm,
_fs: u32,
_nargs: u32,
) -> Result<u32, luna_core::vm::LuaError> {
panic!("boom from a native");
}
fn panic_static_str_native(
_vm: &mut Vm,
_fs: u32,
_nargs: u32,
) -> Result<u32, luna_core::vm::LuaError> {
panic!("static boom");
}
#[test]
fn embedding_native_panic_caught_as_lua_error() {
let mut vm = Vm::new(LuaVersion::Lua55);
let f1 = vm.native(panic_string_native);
vm.set_global("p1", f1).unwrap();
let f2 = vm.native(panic_static_str_native);
vm.set_global("p2", f2).unwrap();
let v = vm.eval("return pcall(p1)").expect("pcall returns normally");
assert!(matches!(v[0], Value::Bool(false)));
if let Value::Str(s) = v[1] {
let msg = String::from_utf8_lossy(s.as_bytes());
assert!(
msg.contains("native panic") && msg.contains("boom from a native"),
"string-payload panic should surface: {msg}"
);
} else {
panic!("expected error string, got {:?}", v[1]);
}
let v = vm.eval("return pcall(p2)").expect("pcall returns normally");
assert!(matches!(v[0], Value::Bool(false)));
if let Value::Str(s) = v[1] {
let msg = String::from_utf8_lossy(s.as_bytes());
assert!(
msg.contains("native panic") && msg.contains("static boom"),
"static-str-payload panic should surface: {msg}"
);
} else {
panic!("expected error string, got {:?}", v[1]);
}
}
#[test]
fn os_exit_is_callable_function() {
check_str("return type(os.exit)", b"function");
}
#[test]
fn warn_on_gc_error_5_4_plus() {
let mut vm = Vm::new(LuaVersion::Lua55);
vm.eval(
"warn('@on') \
setmetatable({}, {__gc = function () error('@bang@') end}) \
collectgarbage()",
)
.expect("collectgarbage swallows the __gc error under 5.4+");
let log = vm.warn_log_take();
assert_eq!(
log.len(),
1,
"exactly one warn emission expected, got {log:?}"
);
let line = String::from_utf8_lossy(&log[0]);
assert!(
line.contains("error in __gc metamethod") && line.contains("@bang@"),
"warn line should mention both the wrapper and the inner error: {line}"
);
}
#[test]
fn xpcall_msgh_recursion_and_no_error_object() {
check_str(
"local function err (n) \
if type(n) ~= 'number' then return n \
elseif n == 0 then return 'END' \
else error(n - 1) end \
end \
local _, msg = xpcall(error, err, 5) \
return msg",
b"END",
);
check_str(
"local function err (n) \
if type(n) ~= 'number' then return n \
elseif n == 0 then return 'END' \
else error(n - 1) end \
end \
local _, msg = xpcall(error, err, 300) \
return msg",
b"C stack overflow",
);
check_bool(
"local function loop(x,y,z) return 1 + loop(x,y,z) end \
local _, msg = xpcall(loop, function (m) \
local _, e = pcall(loop) \
return string.find(e, 'error handling') ~= nil \
end) \
return msg",
true,
);
check_str(
"local _, m = pcall(function() error(nil) end); return m",
b"<no error object>",
);
check_bool(
"local _, m = pcall(assert, nil, nil); return type(m) == 'string'",
true,
);
check_bool(
"local _, m = pcall(assert); return string.find(m, 'value expected') ~= nil",
true,
);
}
#[test]
fn io_buffered_writes_and_round_trip_time() {
check_bool(
"local f = assert(io.open(os.tmpname(), 'w')) \
local r = f:write('abcd') \
f:close() \
return r == f",
true,
);
check_bool(
"local p = os.tmpname() \
local fw = assert(io.open(p, 'w')); fw:write('x'); fw:close() \
local f = assert(io.open(p, 'r')) \
local a, b, c = f:write('xuxu') \
f:close(); os.remove(p) \
return not a and type(b) == 'string' and type(c) == 'number'",
true,
);
check_int(
"local t = 1234567890 \
return os.time(os.date('*t', t)) - t",
0,
);
check_str(
"local t1 = {year=2005, month=1, day=1, hour=1, min=0, sec=-3602} \
os.time(t1) \
return string.format('%d-%d-%d %d:%d:%d yday=%d', \
t1.year, t1.month, t1.day, t1.hour, t1.min, t1.sec, t1.yday)",
b"2004-12-31 23:59:58 yday=366",
);
}
#[test]
fn parse_time_local_var_limit() {
let s = std::iter::once("function foo ()\n local ".to_string())
.chain((1..=200).map(|j| format!("a{j}, ")))
.chain(std::iter::once("b\nend".to_string()))
.collect::<String>();
let mut vm = Vm::new(LuaVersion::Lua55);
let err = vm.load(s.as_bytes(), b"=t").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("too many local variables"), "msg = {msg:?}");
assert!(msg.contains("function at line 1"), "msg = {msg:?}");
let s = (0..50)
.map(|_| "do local a,b,c,d,e,f,g,h end ".to_string())
.collect::<String>();
let _ = vm.load(s.as_bytes(), b"=t").expect("scoped locals reset");
}
#[test]
fn debug_traceback_honours_level() {
check_int(
"local function deep(lvl, n) \
if lvl == 0 then return (debug.traceback('m', n)) end \
return (deep(lvl-1, n)) \
end \
local function checkdeep(total, start) \
local s = deep(total, start) \
local rest = string.match(s, '^m\\nstack traceback:\\n(.*)$') \
return select(2, string.gsub(rest, '\\n', '')) \
end \
return coroutine.wrap(checkdeep)(11, 5)",
8,
);
}
#[test]
fn stripped_chunk_debug_surface() {
check_str(
"local f = function () return 1 end \
f = load(string.dump(f, true)) \
return debug.getinfo(f).short_src",
b"?",
);
check_int(
"local prog = 'return debug.getinfo(1).currentline' \
local f = assert(load(string.dump(load(prog), true))) \
return f()",
-1,
);
check_str(
"local a = 12 \
local f = function () return a end \
f = load(string.dump(f, true)) \
local n = debug.getupvalue(f, 1) \
return n",
b"(no name)",
);
check_bool(
"local foo = function () local a = 1; return a end \
local s = load(string.dump(foo, true)) \
local line = true \
debug.sethook(function (e, l) line = l end, 'l') \
s() \
debug.sethook(nil) \
return line == nil",
true,
);
}
#[test]
fn ephemeron_weak_key_tables() {
check_int(
"local a = setmetatable({}, {__mode = 'k'}) \
local x = nil \
for i = 1, 50 do local n = {}; a[n] = {k = {x}}; x = n end \
collectgarbage() \
local n = x local i = 0 \
while n do n = a[n].k[1]; i = i + 1 end \
return i",
50,
);
check_bool(
"local a = setmetatable({}, {__mode = 'k'}) \
local x = nil \
for i = 1, 50 do local n = {}; a[n] = {k = {x}}; x = n end \
x = nil \
collectgarbage() \
return next(a) == nil",
true,
);
}
#[test]
fn weak_table_string_keys_survive() {
check_str(
"local a = setmetatable({}, {__mode = 'k'}) \
local s = 'weakkey-' .. tostring(98765) \
a[s] = 'kept' \
s = nil \
collectgarbage() \
return a['weakkey-98765']",
b"kept",
);
}
#[test]
fn weak_tables_clear_dead_entries() {
check_int(
"local kept = {} local w = setmetatable({}, {__mode = 'v'}) \
w.dead = {} w.live = kept \
collectgarbage() \
return (w.dead == nil and w.live == kept) and 1 or 0",
1,
);
check_int(
"local w = setmetatable({}, {__mode = 'k'}) \
w[{}] = 1 local k = {} w[k] = 2 \
collectgarbage() \
local n = 0 for _ in pairs(w) do n = n + 1 end \
return n",
1, );
check_int(
"local t = {} t[1] = {} collectgarbage() return t[1] ~= nil and 1 or 0",
1,
);
}
#[test]
fn repeat_until_with_captured_body_local_terminates() {
check_int(
"local a = {} local i = 1 \
repeat local x = i a[i] = function () return x end i = i + 1 \
until i > 10 \
return i",
11,
);
check_int(
"local a = {} local i = 1 \
repeat local x = i a[i] = function () i = x + 1; return x end \
until i > 10 or a[i]() ~= x \
return (i == 11 and a[1]() == 1 and a[3]() == 3 and i == 4) and 1 or 0",
1,
);
}
#[test]
fn pattern_backref_zero_is_invalid() {
let mut vm = Vm::new(LuaVersion::Lua55);
match vm.eval("return string.match('abc', '%0')") {
Ok(v) => panic!("expected error, got {v:?}"),
Err(e) => {
let msg = vm.error_text(&e);
assert!(
msg.contains("invalid capture index"),
"unexpected error: {msg}"
);
}
}
}
#[test]
fn string_format_modifiers() {
check_error(
"return string.format('%10s', '\\0')",
"string contains zeros",
);
check_str("return string.format('%+.2A', 12)", b"+0X1.80P+3");
check_str("return string.format('%.4A', -12)", b"-0X1.8000P+3");
check_str("return string.format('%+#014.0f', 100)", b"+000000000100.");
check_error("return string.format('%100.3d', 10)", "invalid conversion");
check_error("return string.format('%#i', 10)", "invalid conversion");
check_error("return string.format('%010c', 10)", "invalid conversion");
check_error("return string.format('%F', 10)", "invalid conversion");
check_error(
"return string.format('%'..string.rep('0',600)..'d', 10)",
"too long",
);
}
#[test]
fn pushglobalfuncname_qualifies_nested_native_arg_error() {
check_error("table.sort({1,2,3}, table.sort)", "'table.sort'");
check_error("string.gsub('s', 's', setmetatable)", "'setmetatable'");
check_error("table.sort({}, 7)", "'sort'");
}
#[test]
fn syntax_error_source_uses_chunkid() {
let mut vm = Vm::new(LuaVersion::Lua55);
let name_at = format!("@{}", "x".repeat(70));
let src = format!("return load('x', '{name_at}')");
let r = vm.eval(&src).expect("load itself succeeds");
let msg = r.into_iter().nth(1).expect("(nil, msg)");
if let crate::Value::Str(s) = msg {
let bytes = s.as_bytes();
assert!(
bytes.starts_with(b"..."),
"expected '...' truncation, got {:?}",
String::from_utf8_lossy(bytes)
);
let prefix = bytes.split(|b| *b == b':').next().unwrap();
assert!(prefix.len() <= 59, "prefix len {} > 59", prefix.len());
} else {
panic!("load did not return a string error");
}
}
#[test]
fn light_userdata_from_debug_upvalueid() {
check_error(
"local x = debug.upvalueid(function () return debug end, 1); \
debug.setuservalue(x, {})",
"light userdata",
);
check_str(
"local f = function () return debug end; \
local a, b = debug.upvalueid(f, 1), debug.upvalueid(f, 1); \
return tostring(a == b)",
b"true",
);
check_str(
"return type(debug.upvalueid(function () return debug end, 1))",
b"userdata",
);
}
#[test]
fn table_concat_at_max_index() {
check_str(
"return table.concat({[math.maxinteger] = 'alo'}, 'x', math.maxinteger, math.maxinteger)",
b"alo",
);
}
#[test]
fn error_message_fidelity() {
check_error(
"local a = setmetatable({}, {__add = 34}); local _ = a + 1",
"metamethod 'add'",
);
check_error("local a = {}; return a.bbbb(3)", "field 'bbbb'");
check_error(
"local x = setmetatable({}, {__name = 'My Type'}); local _ = x + 1",
"on a My Type value",
);
check_error(
"local x = setmetatable({}, {__name = 'My Type'}); local _ = x < x",
"two My Type values",
);
check_error("local a = {_ENV = {}}; local _ = a._ENV.x + 1", "field 'x'");
check_error("collectgarbage('nooption')", "invalid option");
check_error(
"local t = setmetatable({}, {__index = string}); t:rep(2)",
"calling 'rep' on bad self",
);
check_error("return string.sub('a', {})", "number expected, got table");
check_error("return string.sub('a', {})", "#2");
check_error(
"local f = assert(load(string.dump(function () return nil + 1 end, true))); f()",
"?:?:",
);
}
#[test]
fn getobjname_global_via_gettable() {
let mut src = String::new();
for i in 0..300 {
src.push_str(&format!("aaa = x{i}; "));
}
src.push_str("local _ = bbb + 1");
let mut vm = Vm::new(LuaVersion::Lua55);
match vm.eval(&src) {
Ok(v) => panic!("expected error, got {v:?}"),
Err(e) => assert!(
vm.error_text(&e).contains("global 'bbb'"),
"unexpected: {}",
vm.error_text(&e)
),
}
}
#[test]
fn table_lib_metamethods() {
check_str(
"local t = {}; local p = setmetatable({}, {__len = function () return #t end, \
__index = t, __newindex = t}); for i = 1, 10 do table.insert(p, 1, i) end; \
table.sort(p); return table.concat(p, ',')",
b"1,2,3,4,5,6,7,8,9,10",
);
check_int(
"local t = setmetatable({}, {__len = function () return math.maxinteger end}); \
table.insert(t, 20); return (next(t))",
i64::MIN,
);
check_int(
"local a = setmetatable({n = 10}, {__index = function (t, k) \
if k <= t.n then return k * 10 end end}); \
local c = 0; for _ in ipairs(a) do c = c + 1 end; return c",
10,
);
check_int(
"local function it(_, i) if i < 3 then return i + 1, (i + 1) * 10 end end; \
local a = setmetatable({}, {__pairs = function (x) return it, x, 0 end}); \
local c = 0; for _ in pairs(a) do c = c + 1 end; return c",
3,
);
}
#[test]
fn getinfo_names_c_boundary() {
check_str(
"local function f() local i = debug.getinfo(2); return i.namewhat .. '/' .. i.name end \
return select(2, pcall(f))",
b"global/pcall",
);
}
#[test]
fn debug_getlocal_and_for_state() {
check_str(
"local function basic(a, b) local c = a * b; local n, v = debug.getlocal(1, 3); \
return n .. '=' .. v end \
return basic(6, 7)",
b"c=42",
);
check_bool(
"local function gettoclose(lv) lv = lv + 1; local st = 0 \
for i = 1, 20 do local n, v = debug.getlocal(lv, i) \
if n == '(for state)' then st = st + 1; if st == 3 then return v end end end end \
local marker = setmetatable({}, {__close = function () end}) \
local function iter(_, c) if c < 1 then return c + 1 end end \
local got \
for _ in iter, nil, 0, marker do got = gettoclose(1); break end \
return got == marker",
true,
);
}
#[test]
fn close_handler_debug_parent_is_enclosing_function() {
check_str(
"local captured \
local function foo() \
local _ <close> = setmetatable({}, {__close = function () \
captured = debug.getinfo(2).name \
end}) \
return 1 \
end \
foo() \
return captured",
b"foo",
);
}
#[test]
fn xpcall_traceback_sees_close_handler_frame() {
check_int(
"local _, msg = xpcall(function () \
local _ <close> = setmetatable({}, {__close = function () error('boom') end}) \
end, debug.traceback) \
return string.find(msg, \"in metamethod 'close'\") and 1 or 0",
1,
);
}
#[test]
fn non_closable_value_at_tbc_names_variable() {
check_int(
"local ok, msg = pcall(function () local x <close> = {} end) \
return (not ok) and string.find(msg, \"variable 'x' got a non%-closable value\") and 1 or 0",
1,
);
}
#[test]
fn close_handler_removed_metamethod_errors() {
check_int(
"local ok, msg = pcall(function () \
local x <close> = setmetatable({}, {__close = print}) \
getmetatable(x).__close = nil \
end) \
return (not ok) and string.find(msg, \"metamethod 'close'\") and 1 or 0",
1,
);
}
#[test]
fn stack_overflow_recovery_runs_close_in_errorh() {
check_int(
"local function overflow (n) overflow(n + 1) end \
local function errorh (m) \
local x <close> = setmetatable({}, {__close = function (o) o[1] = 42 end}) \
return x \
end \
local _, obj = xpcall(overflow, errorh) \
return obj[1]",
42,
);
}
#[test]
fn return_hook_for_native_names_callee() {
check_str(
"local cap = '?' \
local function hook (event) \
if cap == '?' then cap = (debug.getinfo(2).name or '?') end \
end \
(function () debug.sethook(hook, 'r') end)() \
debug.sethook() \
return cap",
b"sethook",
);
}
#[test]
fn yield_inside_pairs_metamethod() {
check_int(
"local t = setmetatable({10, 20, 30}, {__pairs = function (t) \
local inc = coroutine.yield() \
return function (t, i) if i > 1 then return i - inc, t[i - inc] end end, t, #t + 1 \
end}) \
local sum = 0 \
local co = coroutine.wrap(function () for _, p in pairs(t) do sum = sum + p end end) \
co(); co(1) \
return sum",
60,
);
}
#[test]
fn numeric_for_coercion_and_bounds() {
check_int(
"local a = 0; for _ = '10', '1', '-2' do a = a + 1 end; return a",
5,
);
check_int(
"local c = 0; for _ = math.maxinteger, 10e100, -1 do c = c + 1 end; return c",
0,
);
check_int(
"local c = 0; for _ = math.mininteger, -10e100 do c = c + 1 end; return c",
0,
);
}
#[test]
fn multi_assign_snapshots_indexed_lhs() {
check_int(
"local a, i, j, b = {'a', 'b'}, 1, 2; b = a \
i, a[i], a, j, a[j], a[i+j] = j, i, i, b, j, i \
assert(i == 2 and b[1] == 1 and a == 1 and j == b and b[2] == 2 and b[3] == 1) \
return 1",
1,
);
check_int(
"local a, i = {}, 3 \
i, a[i] = i + 1, 20 \
assert(i == 4 and a[3] == 20 and a[4] == nil) \
return 1",
1,
);
}
#[test]
fn yieldable_close_at_block_exit() {
check_str(
"local trace = {} \
local function f2c(f) return setmetatable({}, {__close = f}) end \
local co = coroutine.wrap(function () \
do \
local z <close> = f2c(function (_, msg) \
trace[#trace + 1] = 'z1'; coroutine.yield('z'); trace[#trace + 1] = 'z2' \
end) \
end \
trace[#trace + 1] = 'after' \
end) \
assert(co() == 'z') \
co() \
return table.concat(trace, ',')",
b"z1,z2,after",
);
}
#[test]
fn yieldable_close_at_function_return() {
check_int(
"local function f2c(f) return setmetatable({}, {__close = f}) end \
local trace = {} \
local co = coroutine.wrap(function () \
local function foo (x) \
local _ <close> = f2c(function (_, msg) \
trace[#trace + 1] = 'y1'; coroutine.yield('y'); trace[#trace + 1] = 'y2' \
end) \
return x, 23 \
end \
local a, b = foo(1.5) \
assert(a == 1.5 and b == 23) \
trace[#trace + 1] = 'done' \
end) \
assert(co() == 'y') \
co() \
assert(trace[1] == 'y1' and trace[2] == 'y2' and trace[3] == 'done') \
return 1",
1,
);
}
#[test]
fn yieldable_close_during_error_unwind() {
check_str(
"local trace = {} \
local function f2c(f) return setmetatable({}, {__close = f}) end \
local co = coroutine.wrap(function () \
local function foo () \
local x <close> = f2c(function (_, msg) \
trace[#trace + 1] = 'x1'; coroutine.yield('x'); trace[#trace + 1] = 'x2' \
end) \
local y <close> = f2c(function (_, msg) \
trace[#trace + 1] = 'y1'; coroutine.yield('y'); trace[#trace + 1] = 'y2' \
end) \
error('boom') \
end \
local ok, msg = pcall(foo) \
assert(not ok and msg:find('boom')) \
trace[#trace + 1] = 'done' \
end) \
assert(co() == 'y') \
assert(co() == 'x') \
co() \
return table.concat(trace, ',')",
b"y1,y2,x1,x2,done",
);
}
#[test]
fn close_handler_debug_parent_on_error_unwind_is_c_boundary() {
check_str(
"local function f2c(f) return setmetatable({}, {__close = f}) end \
local got = '?' \
local function foo () \
local _ <close> = f2c(function (_, msg) \
got = debug.getinfo(2).name or 'nil' \
end) \
error('boom') \
end \
pcall(foo) \
return got",
b"pcall",
);
}
#[test]
fn goto_out_of_nested_for_closes_iterator_close_values() {
check_int(
"local numopen = 0 \
local function f2c(f) return setmetatable({}, {__close = f}) end \
local function open (n) \
numopen = numopen + 1 \
return function () n = n - 1; if n > 0 then return n end end, \
nil, nil, \
f2c(function () numopen = numopen - 1 end) \
end \
local s = 0 \
for i in open(3) do \
for j in open(3) do \
if i + j < 3 then goto endloop end \
s = s + i \
end \
end \
::endloop:: \
assert(numopen == 0, 'open iterators leaked: ' .. numopen) \
return s",
5,
);
}
#[test]
fn dump_inherits_parent_source_in_child_protos() {
check_int(
"local foo = load([[ \
local str <const> = 'MARKER' \
return { \
function () return str end, \
function () return str end, \
function () return str end \
} \
]]) \
local dump = string.dump(foo) \
local _, count = string.gsub(dump, 'MARKER', function () return 'X' end) \
return count",
2,
);
}
#[test]
fn return_stat_caps_at_254_results() {
check_str(
"local code = 'return 10' .. string.rep(',10', 254) \
local f, msg = load(code) \
return tostring(f) .. '|' .. (msg:find('too many returns') and 'ok' or msg)",
b"nil|ok",
);
}
fn api_count_args(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, luna_core::vm::LuaError> {
Ok(vm.nat_return(fs, &[Value::Int(nargs as i64)]))
}
fn api_echo(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, luna_core::vm::LuaError> {
let mut out = Vec::with_capacity(nargs as usize);
for i in 0..nargs {
out.push(vm.nat_arg(fs, nargs, i));
}
Ok(vm.nat_return(fs, &out))
}
#[test]
fn api_call_value_zero_args_zero_results() {
let mut vm = Vm::new(LuaVersion::Lua55);
let cl = vm.load(b"local x = 1 + 1", b"=chunk").expect("compile");
let r = vm.call_value(Value::Closure(cl), &[]).expect("call");
assert!(r.is_empty(), "no-return chunk produces zero values: {r:?}");
}
#[test]
fn api_call_value_multi_arg_multi_result() {
let mut vm = Vm::new(LuaVersion::Lua55);
let cl = vm
.load(b"local a, b = ...; return a + b, a * b, a - b", b"=chunk")
.expect("compile");
let r = vm
.call_value(Value::Closure(cl), &[Value::Int(3), Value::Int(4)])
.expect("call");
assert_eq!(r.len(), 3);
assert!(matches!(r[0], Value::Int(7)), "sum: {:?}", r[0]);
assert!(matches!(r[1], Value::Int(12)), "prod: {:?}", r[1]);
assert!(matches!(r[2], Value::Int(-1)), "diff: {:?}", r[2]);
}
#[test]
fn api_native_sees_correct_nargs() {
let mut vm = Vm::new(LuaVersion::Lua55);
let f = vm.native(api_count_args);
vm.set_global("count_args", f).unwrap();
let v = vm
.eval("return count_args(), count_args(1), count_args(1,2,3,4,5)")
.unwrap();
assert_eq!(v.len(), 3);
assert!(matches!(v[0], Value::Int(0)));
assert!(matches!(v[1], Value::Int(1)));
assert!(matches!(v[2], Value::Int(5)));
}
#[test]
fn api_native_multi_return_passthrough() {
let mut vm = Vm::new(LuaVersion::Lua55);
let f = vm.native(api_echo);
vm.set_global("echo", f).unwrap();
let v = vm.eval("return echo('a','b','c')").unwrap();
assert_eq!(v.len(), 3);
for (got, want) in v.iter().zip([b"a", b"b", b"c"].iter()) {
if let Value::Str(s) = got {
assert_eq!(s.as_bytes(), *want);
} else {
panic!("not a string: {got:?}");
}
}
}
#[test]
fn api_globals_round_trip_through_set_and_lua_read() {
let mut vm = Vm::new(LuaVersion::Lua55);
vm.set_global("from_host", Value::Int(42)).unwrap();
let v = vm.eval("return from_host").unwrap();
assert_eq!(v.len(), 1);
assert!(matches!(v[0], Value::Int(42)));
vm.eval("from_lua = 'set by lua'").unwrap();
let g = vm.globals();
let key = Value::Str(vm.heap.intern(b"from_lua"));
let got = g.get(key);
if let Value::Str(s) = got {
assert_eq!(s.as_bytes(), b"set by lua");
} else {
panic!("expected string, got {got:?}");
}
}
#[test]
fn api_lua_error_propagates_to_host_with_render() {
let mut vm = Vm::new(LuaVersion::Lua55);
let cl = vm.load(b"error('boom', 0)", b"=chunk").expect("compile");
let err = vm
.call_value(Value::Closure(cl), &[])
.expect_err("error chunk should fail");
let text = vm.error_text(&err);
assert_eq!(text, "boom");
}
#[test]
fn api_call_value_can_catch_internally() {
let mut vm = Vm::new(LuaVersion::Lua55);
let v = vm
.eval(
"local ok, msg = pcall(function () error('inner') end) \
return ok, msg",
)
.unwrap();
assert_eq!(v.len(), 2);
assert!(matches!(v[0], Value::Bool(false)));
if let Value::Str(s) = v[1] {
assert!(
s.as_bytes().ends_with(b"inner"),
"msg should end with 'inner': {:?}",
String::from_utf8_lossy(s.as_bytes())
);
} else {
panic!("expected error string, got {:?}", v[1]);
}
}
#[test]
fn api_load_returns_callable_chunk() {
let mut vm = Vm::new(LuaVersion::Lua55);
let cl = vm.load(b"local n = ...; return n * n", b"=chunk").unwrap();
let r1 = vm.call_value(Value::Closure(cl), &[Value::Int(5)]).unwrap();
assert!(matches!(r1[0], Value::Int(25)));
let r2 = vm.call_value(Value::Closure(cl), &[Value::Int(7)]).unwrap();
assert!(matches!(r2[0], Value::Int(49)));
}
#[test]
fn api_native_runs_lua_callback_through_call_value() {
fn api_with_callback(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, luna_core::vm::LuaError> {
assert!(nargs >= 1);
let cb = vm.nat_arg(fs, nargs, 0);
let r = vm.call_value(cb, &[Value::Int(10)])?;
let n = if let Value::Int(i) = r[0] {
Value::Int(i + 1)
} else {
Value::Nil
};
Ok(vm.nat_return(fs, &[n]))
}
let mut vm = Vm::new(LuaVersion::Lua55);
let f = vm.native(api_with_callback);
vm.set_global("with_cb", f).unwrap();
let v = vm
.eval("return with_cb(function (x) return x * 3 end)")
.unwrap();
assert_eq!(v.len(), 1);
assert!(matches!(v[0], Value::Int(31)), "got {:?}", v[0]);
}
#[test]
fn api_collect_garbage_returns_freed_count() {
let mut vm = Vm::new(LuaVersion::Lua55);
vm.eval("for i = 1, 100 do local t = {i, i} end").unwrap();
let _ = vm.collect_garbage(); let _ = vm.collect_garbage(); }
#[test]
fn close_handler_debug_parent_is_lua_on_normal_close() {
check_str(
"local function f2c(f) return setmetatable({}, {__close = f}) end \
local what = '?' \
local function foo () \
local _ <close> = f2c(function (_, msg) \
what = debug.getinfo(2).what or 'nil' \
end) \
return 1 \
end \
foo() \
return what",
b"Lua",
);
}