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_bitwise_string_coercion() {
eq(LuaVersion::V53, r#"return "3" & 5"#, "1");
eq(LuaVersion::V53, r#"return "0xff" | 0"#, "255");
eq(LuaVersion::V53, r#"return ~"5""#, "-6");
eq(LuaVersion::V53, r#"return "8" >> "1""#, "4");
eq(LuaVersion::V53, r#"return "8" << 1"#, "16");
eq(LuaVersion::V53, r#"return 5 & "3""#, "1");
eq(LuaVersion::V53, r#"return " 0x10 " & 255"#, "16");
eq(LuaVersion::V53, r#"return "3.0" & 1"#, "1");
eq(LuaVersion::V53, r#"return "0xffffffffffffffff" | 0"#, "-1");
eq(LuaVersion::V53, r#"return "0xfffffffffffffffe" & "-1""#, "-2");
eq(LuaVersion::V53, r#"return " \n -45 \t " >> " -2 ""#, "-180");
err_contains(LuaVersion::V53, r#"return "3.5" & 1"#, "no integer representation");
err_contains(LuaVersion::V53, r#"return "0xffffffffffffffff.0" | 0"#, "no integer representation");
err_contains(LuaVersion::V53, r#"return "abc" & 1"#, "perform bitwise operation on a string value");
}
#[test]
fn v54_v55_bitwise_no_string_coercion() {
for v in [LuaVersion::V54, LuaVersion::V55] {
err_contains(v, r#"return "3" & 5"#, "perform bitwise operation on a string value");
err_contains(v, r#"return ~"5""#, "perform bitwise operation on a string value");
err_contains(v, r#"return "8" >> "1""#, "perform bitwise operation on a string value");
}
}
#[test]
fn v53_arith_string_error_wording() {
err_contains(
LuaVersion::V53,
r#"return "abc" + 1"#,
"attempt to perform arithmetic on a string value",
);
err_contains(
LuaVersion::V53,
r#"return "abc" * 2"#,
"attempt to perform arithmetic on a string value",
);
err_contains(
LuaVersion::V53,
r#"return -"x""#,
"attempt to perform arithmetic on a string value",
);
err_contains(LuaVersion::V53, r#"local x="a"; return x+1"#, "(local 'x')");
err_contains(LuaVersion::V53, r#"aaa="z"; return aaa+1"#, "(global 'aaa')");
err_contains(
LuaVersion::V53,
r#"aaa="2"; b=nil; return aaa*b"#,
"attempt to perform arithmetic on a nil value (global 'b')",
);
eq(LuaVersion::V53, r#"return math.type("1"+"2")"#, "float");
eq(LuaVersion::V53, r#"return math.type("1.0"+"2")"#, "float");
eq(LuaVersion::V53, r#"return "3" + 2"#, "5.0");
eq(
LuaVersion::V53,
r#"local t=setmetatable({},{__add=function() return 42 end}); return t+"5""#,
"42",
);
eq(
LuaVersion::V53,
r#"local t=setmetatable({},{__add=function() return 42 end}); return "5"+t"#,
"42",
);
for v in [LuaVersion::V54, LuaVersion::V55] {
eq(
v,
r#"local t=setmetatable({},{__add=function() return 42 end}); return "5"+t"#,
"42",
);
}
}
#[test]
fn v54_v55_arith_string_wording_unchanged() {
for v in [LuaVersion::V54, LuaVersion::V55] {
err_contains(v, r#"return "abc" + 1"#, "attempt to add a 'string' with a 'number'");
err_contains(v, r#"aaa="2"; b=nil; return aaa*b"#, "attempt to mul a 'string' with a 'nil'");
}
}
#[test]
fn v53_for_loop_error_wording() {
err_contains(LuaVersion::V53, "for i=1,'a' do end", "'for' limit must be a number");
err_contains(LuaVersion::V53, "for i='a',10 do end", "'for' initial value must be a number");
err_contains(LuaVersion::V53, "for i=1,10,'a' do end", "'for' step must be a number");
}
#[test]
fn v54_v55_for_loop_error_wording_unchanged() {
for v in [LuaVersion::V54, LuaVersion::V55] {
err_contains(v, "for i=1,'a' do end", "bad 'for' limit (number expected, got string)");
err_contains(v, "for i='a',10 do end", "bad 'for' initial value (number expected, got string)");
err_contains(v, "for i=1,10,'a' do end", "bad 'for' step (number expected, got string)");
}
}
#[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 v53_compat_math_present_and_correct() {
for v in [LuaVersion::V53, LuaVersion::V54] {
for name in ["atan2", "cosh", "sinh", "tanh", "pow", "log10", "frexp", "ldexp"] {
eq(v, &format!("return type(math.{name})"), "function");
}
}
for v in [LuaVersion::V53, LuaVersion::V54] {
eq(v, "return math.cosh(1)", "1.5430806348152");
eq(v, "return math.sinh(1)", "1.1752011936438");
eq(v, "return math.tanh(1)", "0.76159415595576");
eq(v, "return math.pow(2, 0.5)", "1.4142135623731");
eq(v, "return math.pow(2, 3)", "8.0");
eq(v, "return math.type(math.pow(2, 3))", "float");
eq(v, "return math.log10(1000)", "3.0");
eq(v, "return math.ldexp(0.5, 3)", "4.0");
eq(v, "return math.ldexp(1.0, -1)", "0.5");
eq(v, "return math.ldexp(1.0, -1074)", "4.9406564584125e-324");
eq(v, "local m, e = math.frexp(8.0); return m", "0.5");
eq(v, "local m, e = math.frexp(8.0); return e", "4");
eq(v, "local m, e = math.frexp(8.0); return math.type(m)", "float");
eq(v, "local m, e = math.frexp(8.0); return math.type(e)", "integer");
eq(v, "local m, e = math.frexp(0.0); return tostring(m) .. ',' .. tostring(e)", "0.0,0");
eq(v, "return math.atan2(1, 1) == math.atan(1, 1)", "true");
eq(v, "return math.atan2(1, 0) == math.atan(1, 0)", "true");
err_contains(v, "return math.cosh('x')", "bad argument #1 to 'cosh'");
err_contains(v, "return math.pow(2)", "bad argument #2 to 'pow'");
}
}
#[test]
fn v55_compat_math_partition() {
for name in ["atan2", "cosh", "sinh", "tanh", "pow", "log10"] {
eq(LuaVersion::V55, &format!("return type(math.{name})"), "nil");
}
for name in ["frexp", "ldexp"] {
eq(LuaVersion::V55, &format!("return type(math.{name})"), "function");
}
eq(LuaVersion::V55, "return math.ldexp(0.5, 3)", "4.0");
eq(LuaVersion::V55, "local m, e = math.frexp(8.0); return tostring(m) .. ',' .. tostring(e)", "0.5,4");
}
#[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}");
}
}