use lua_rs_runtime::{Lua, LuaVersion};
fn run(version: LuaVersion, code: &str) -> Result<String, String> {
let lua = Lua::new_versioned(version);
let wrapper = format!(
"local f, e = load([==[\n{code}\n]==])\n\
if not f then return 'E\\0' .. e end\n\
local ok, r = pcall(f)\n\
if not ok then return 'E\\0' .. tostring(r) end\n\
return 'V\\0' .. tostring(r)"
);
let out: String = lua
.load(&wrapper)
.eval()
.unwrap_or_else(|e| panic!("harness failure for `{code}`: {e:?}"));
if let Some(v) = out.strip_prefix("V\0") {
Ok(v.to_string())
} else if let Some(e) = out.strip_prefix("E\0") {
Err(e.to_string())
} else {
panic!("harness: unexpected output `{out}` for `{code}`")
}
}
fn eq(version: LuaVersion, code: &str, expected: &str) {
match run(version, code) {
Ok(got) => assert_eq!(got, expected, "code: {code}"),
Err(e) => panic!("code `{code}` errored (`{e}`), expected `{expected}`"),
}
}
fn err_contains(version: LuaVersion, code: &str, needle: &str) {
match run(version, code) {
Ok(got) => panic!("code `{code}` returned `{got}`, expected error containing `{needle}`"),
Err(e) => assert!(e.contains(needle), "code `{code}` error `{e}` lacked `{needle}`"),
}
}
#[test]
fn v55_global_enforcement() {
eq(LuaVersion::V55, "y = 3; return y", "3");
eq(LuaVersion::V55, "global a; a = 5; return a", "5");
err_contains(LuaVersion::V55, "global a; a = 1; zz = 2", "variable 'zz' not declared");
err_contains(
LuaVersion::V55,
"global f; local function g() return nope end return g()",
"variable 'nope' not declared",
);
}
#[test]
fn v55_global_block_scoped() {
eq(LuaVersion::V55, "do global Y; Y = 1 end; return Y", "1");
eq(LuaVersion::V55, "if true then global Z; Z = 1 end; w = 2; return w", "2");
}
#[test]
fn v55_global_initializer_stored() {
eq(LuaVersion::V55, "do global x = 7 end; return x", "7");
eq(LuaVersion::V55, "do global a, b = 10, 20 end; return a + b", "30");
}
#[test]
fn v55_const_global_rejects_assignment() {
err_contains(
LuaVersion::V55,
"global x <const> = 1; x = 2",
"attempt to assign to const variable 'x'",
);
}
#[test]
fn v55_global_is_a_valid_identifier() {
eq(LuaVersion::V55, "local global = 5; return global", "5");
eq(LuaVersion::V55, "global = 7; return global", "7");
}
#[test]
fn v55_for_control_var_readonly() {
err_contains(LuaVersion::V55, "for i = 1, 3 do i = 10 end", "attempt to assign to const variable 'i'");
err_contains(
LuaVersion::V55,
"for k, v in pairs({1, 2}) do k = 10 end",
"attempt to assign to const variable 'k'",
);
eq(LuaVersion::V55, "local s = 0; for i = 1, 3 do s = s + i end; return s", "6");
eq(LuaVersion::V55, "for k, v in pairs({7}) do v = 9 end; return 'ok'", "ok");
}
#[test]
fn v55_float_tostring_round_trips() {
eq(LuaVersion::V55, "return 1/3", "0.33333333333333331");
eq(LuaVersion::V55, "return 3.14", "3.14");
eq(LuaVersion::V55, "return 0.1 + 0.2", "0.30000000000000004");
eq(LuaVersion::V55, "return 2^53", "9007199254740992.0");
eq(LuaVersion::V55, "return 1e16", "1e+16");
eq(LuaVersion::V55, "return 1.0", "1.0");
}
#[test]
fn v55_table_create_present() {
eq(LuaVersion::V55, "return type(table.create)", "function");
}
#[test]
fn v53_bit32_surface() {
eq(LuaVersion::V53, "return bit32.band(6, 3)", "2");
eq(LuaVersion::V53, "return bit32.btest(6, 3)", "true");
eq(LuaVersion::V53, "return bit32.extract(0xF0, 4, 4)", "15");
eq(LuaVersion::V53, "return bit32.replace(0, 5, 0, 4)", "5");
eq(LuaVersion::V53, "return bit32.arshift(-8, 1)", "4294967292");
eq(LuaVersion::V53, "return bit32.lrotate(1, 1)", "2");
eq(LuaVersion::V53, "return bit32.rrotate(1, 1)", "2147483648");
}
#[test]
fn v53_string_coercion_is_float() {
eq(LuaVersion::V53, "return math.type('0x10' + 0)", "float");
eq(LuaVersion::V54, "return math.type('0x10' + 0)", "integer");
}
#[test]
fn v53_removed_builtins_absent() {
eq(LuaVersion::V53, "return type(warn)", "nil");
eq(LuaVersion::V53, "return type(coroutine.close)", "nil");
eq(LuaVersion::V53, "return type(bit32)", "table");
eq(LuaVersion::V53, "return type(table.create)", "nil");
eq(LuaVersion::V53, "return type(math.type)", "function");
}
#[test]
fn v53_rejects_attribute_syntax() {
err_contains(LuaVersion::V53, "local x <const> = 1; return x", "unexpected symbol");
}
#[test]
fn v54_unchanged() {
eq(LuaVersion::V54, "return 1/3", "0.33333333333333"); eq(LuaVersion::V54, "return 2^53", "9.007199254741e+15");
eq(LuaVersion::V54, "return 3.14", "3.14");
eq(LuaVersion::V54, "return type(warn)", "function");
eq(LuaVersion::V54, "return type(coroutine.close)", "function");
eq(LuaVersion::V54, "return type(bit32)", "nil");
eq(LuaVersion::V54, "local x <const> = 42; return x", "42");
err_contains(LuaVersion::V54, "local x <const> = 1; x = 2", "attempt to assign to const variable 'x'");
eq(LuaVersion::V54, "local global = 8; return global", "8");
eq(LuaVersion::V54, "for i = 1, 1 do i = 10 end; return 'ok'", "ok");
}
#[test]
fn issue76_math_fail_returns_nil() {
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
eq(v, "return math.type('x')", "nil");
eq(v, "return math.type(true)", "nil");
eq(v, "return math.tointeger(3.5)", "nil");
eq(v, "return math.tointeger(2^63)", "nil");
eq(v, "return math.tointeger('7')", "7");
eq(v, "return math.type(1)", "integer");
eq(v, "return math.type(1.0)", "float");
eq(v, "return math.type('x') == nil", "true");
eq(
v,
"if math.tointeger(3.5) then return 'truthy' else return 'falsey' end",
"falsey",
);
}
}
#[test]
fn issue77_string_find_no_spurious_capture() {
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
eq(v, "return select('#', string.find('hello','l+'))", "2");
eq(
v,
"local a,b,c = string.find('hello','l+'); \
return tostring(a)..','..tostring(b)..','..tostring(c)",
"3,4,nil",
);
eq(v, "return select('#', string.find('hello','^h+'))", "2");
eq(v, "return select('#', string.find('hello','(l+)'))", "3");
eq(
v,
"local a,b,c = string.find('hello','(l+)'); \
return tostring(a)..','..tostring(b)..','..tostring(c)",
"3,4,ll",
);
eq(v, "return string.match('hello','l+')", "ll");
eq(v, "return select('#', string.find('hello','ll'))", "2");
eq(v, "return ({string.gsub('hello','l+','L')})[2]", "1");
eq(
v,
"return (string.gsub('hello','l+',function(w) return '['..w..']' end))",
"he[ll]o",
);
eq(
v,
"local t={}; for w in string.gmatch('a,b,c','%a+') do t[#t+1]=w end; \
return table.concat(t,'|')",
"a|b|c",
);
}
}
#[test]
fn issue78_le_derived_from_lt() {
let only_lt =
"local m = {__lt = function() return false end}; \
local a = setmetatable({}, m); local b = setmetatable({}, m); return a <= b";
eq(LuaVersion::V53, only_lt, "true");
eq(LuaVersion::V54, only_lt, "true");
err_contains(LuaVersion::V55, only_lt, "attempt to compare two table values");
eq(
LuaVersion::V54,
"local m = {__lt = function() return false end}; \
local a = setmetatable({}, m); local b = setmetatable({}, m); return a >= b",
"true",
);
let with_le =
"local m = {__le = function() return true end, __lt = function() return false end}; \
local a = setmetatable({}, m); return a <= a";
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
eq(v, with_le, "true");
}
}
#[test]
fn v_argerror_to_fnname() {
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
err_contains(v, "return string.char(256)", "to 'char'");
err_contains(v, "return string.char(256)", "value out of range");
err_contains(v, "return utf8.char(0x80000000)", "to 'char'");
err_contains(v, "return utf8.char(0x80000000)", "value out of range");
}
}
#[test]
fn v_argerror_no_value() {
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
err_contains(v, "return string.sub()", "got no value");
err_contains(v, "return string.rep('x')", "got no value");
}
}
#[test]
fn v_length_concat_location_prefix() {
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
err_contains(v, "return #nil", "attempt to get length of a nil value");
err_contains(v, "return ({})..({})", "attempt to concatenate a table value");
let e = run(v, "return #nil").unwrap_err();
let at = e.find("attempt").expect("message body present");
assert!(e[..at].contains(':'), "v{v:?} #nil missing location prefix: {e}");
let e = run(v, "return ({})..({})").unwrap_err();
let at = e.find("attempt").expect("message body present");
assert!(e[..at].contains(':'), "v{v:?} concat missing location prefix: {e}");
}
}
#[test]
fn v54_v55_string_arith_coercion_failure() {
for v in [LuaVersion::V54, LuaVersion::V55] {
err_contains(v, "return ({}) - 'y'", "attempt to sub a 'table' with a 'string'");
err_contains(v, "return -'x'", "attempt to unm a 'string' with a 'string'");
let e = run(v, "return ({}) - 'y'").unwrap_err();
let at = e.find("attempt").expect("message body present");
assert!(e[..at].contains(':'), "v{v:?} string-arith missing prefix: {e}");
}
}
#[test]
fn v_table_concat_invalid_value_type_name() {
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
err_contains(v, "return table.concat({ {} })",
"invalid value (table) at index 1 in table for 'concat'");
let e = run(v, "return table.concat({ {} })").unwrap_err();
assert!(!e.contains("116, 97"), "v{v:?} concat leaked byte-array: {e}");
}
}