use std::cell::RefCell;
use std::fmt::Write as _;
use std::process::{Command, Stdio};
use luna_core::runtime::Value;
use luna_core::version::LuaVersion;
use luna_core::vm::Vm;
thread_local! {
static CAPTURE: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
fn capture_print(vm: &mut Vm, fs: u32, nargs: u32) -> Result<u32, luna_core::vm::LuaError> {
let tostring_key = Value::Str(vm.heap.intern(b"tostring"));
let tostring_fn = vm.globals().get(tostring_key);
let mut line: Vec<u8> = Vec::new();
for i in 0..nargs {
if i > 0 {
line.push(b'\t');
}
let v = vm.nat_arg(fs, nargs, i);
let ret = vm.call_value(tostring_fn, &[v])?;
if let Some(Value::Str(s)) = ret.into_iter().next() {
line.extend(s.as_bytes());
}
}
line.push(b'\n');
CAPTURE.with(|c| c.borrow_mut().extend_from_slice(&line));
Ok(0)
}
fn drain_capture() -> Vec<u8> {
CAPTURE.with(|c| std::mem::take(&mut *c.borrow_mut()))
}
fn run_on_luna(version: LuaVersion, src: &str) -> Vec<u8> {
drain_capture(); let mut vm = Vm::new(version);
let f = vm.native(capture_print);
vm.set_global("print", f).unwrap();
let cl = vm
.load(src.as_bytes(), b"=(command line)")
.expect("luna load");
vm.call_value(Value::Closure(cl), &[]).expect("luna run");
drain_capture()
}
fn reference_bin_for(version: LuaVersion) -> Option<&'static str> {
let candidates = match version {
LuaVersion::Lua51 => &["lua-5.1"][..],
LuaVersion::Lua52 => &["lua-5.2"][..],
LuaVersion::Lua53 => &["lua-5.3"][..],
LuaVersion::Lua54 | LuaVersion::MacroLua => &["lua-5.4"][..],
LuaVersion::Lua55 => &["lua-5.5"][..],
};
candidates
.iter()
.find(|&&c| {
Command::new(c)
.arg("-v")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok()
})
.copied()
.map(|v| v as _)
}
fn run_on_puc(bin: &str, src: &str) -> Vec<u8> {
let mut child = Command::new(bin)
.arg("-e")
.arg(src)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn PUC");
let _ = child.stdin.take();
let out = child.wait_with_output().expect("PUC wait");
if !out.status.success() {
panic!(
"PUC {} -e failed: {}",
bin,
String::from_utf8_lossy(&out.stderr)
);
}
out.stdout
}
struct Program {
name: &'static str,
src: &'static str,
min_version: LuaVersion,
}
const PROGRAMS: &[Program] = &[
Program {
name: "fib_recursive",
src: r#"
local function f(n)
if n < 2 then return n end
return f(n-1) + f(n-2)
end
print(f(15))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "factorial_iter",
src: r#"
local function fact(n)
local p = 1
for i = 1, n do p = p * i end
return p
end
print(fact(10))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "string_concat_loop",
src: r#"
local parts = {}
for i = 1, 50 do parts[i] = tostring(i) end
print(table.concat(parts, ","))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "table_index_sort",
src: r#"
local t = {5, 3, 1, 4, 2, 9, 7, 8, 6, 10}
table.sort(t)
print(table.concat(t, " "))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "string_pattern_capture",
src: r#"
local s = "key1=42, key2=99, key3=137"
for k, v in s:gmatch("(%w+)=(%d+)") do
print(k, v)
end
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "coroutine_generator",
src: r#"
local function gen(n)
return coroutine.wrap(function()
for i = 1, n do coroutine.yield(i * i) end
end)
end
local out = {}
for v in gen(5) do out[#out+1] = tostring(v) end
print(table.concat(out, ","))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "pcall_error_chain",
src: r#"
local function inner() error("inner-boom") end
local function middle() inner() end
local ok, err = pcall(middle)
print(ok, type(err))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "linked_list_traverse",
src: r#"
-- build a 100-node linked list, sum the values
local head = nil
for i = 100, 1, -1 do head = {val = i, next = head} end
local sum = 0
local n = head
while n do sum = sum + n.val; n = n.next end
print(sum)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "metatable_inheritance",
src: r#"
local Animal = {sound = "?"}
Animal.__index = Animal
function Animal.speak(a) return a.name .. " says " .. a.sound end
local function new(name, sound)
return setmetatable({name = name, sound = sound}, Animal)
end
local cat = new("Cat", "meow")
local dog = new("Dog", "woof")
print(cat:speak())
print(dog:speak())
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "prime_sieve_int",
src: r#"
local N = 100
local is = {}
for i = 2, N do is[i] = true end
for i = 2, N do
if is[i] then
for j = i*i, N, i do is[j] = false end
end
end
local primes = {}
for i = 2, N do if is[i] then primes[#primes+1] = tostring(i) end end
print(table.concat(primes, ","))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "pcall_returns_multiple",
src: r#"
local ok, a, b, c = pcall(function() return 1, 2, 3 end)
print(ok, a, b, c)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "select_varargs",
src: r#"
local function f(...) return select('#', ...), select(2, ...) end
print(f('a', 'b', 'c', 'd'))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "nested_coroutine_pcall",
src: r#"
local function inner()
coroutine.yield(10)
coroutine.yield(20)
end
local co = coroutine.create(function()
local ok, err = pcall(inner)
print("pcall returned:", ok)
coroutine.yield(99)
end)
local _, a = coroutine.resume(co)
local _, b = coroutine.resume(co)
local _, c = coroutine.resume(co)
print(a, b, c)
"#,
min_version: LuaVersion::Lua52,
},
Program {
name: "pattern_anchors_captures",
src: r#"
-- anchors + multi-capture
print(string.match("abc123xyz", "^(%a+)(%d+)(%a+)$"))
-- alternation via char class
for w in string.gmatch("apple,banana;cherry", "[^,;]+") do print(w) end
-- pattern with %b balanced match
print(string.match("(foo(bar)baz)", "%b()"))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "table_insert_remove_mid",
src: r#"
local t = {1, 2, 3, 4, 5}
table.insert(t, 3, 99)
print(table.concat(t, ","))
local removed = table.remove(t, 4)
print(removed, table.concat(t, ","))
table.insert(t, 100)
print(table.concat(t, ","))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "string_byte_char_format",
src: r#"
print(string.byte("A"), string.byte("z"))
print(string.char(65, 66, 67))
print(string.format("%05d %.3f %s", 7, 3.14159, "hi"))
print(string.format("%x %X %o", 255, 255, 8))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "string_reverse_rep_sub",
src: r#"
print(string.reverse("hello"))
print(string.rep("ab", 4))
print(string.rep("x", 3, "-"))
print(string.sub("abcdefgh", 2, 5))
print(string.sub("abcdefgh", -3))
print(string.upper("Mixed Case 42"))
print(string.lower("Mixed Case 42"))
"#,
min_version: LuaVersion::Lua52,
},
Program {
name: "math_floor_modulo_bounds",
src: r#"
print(math.floor(3.9), math.floor(-3.1), math.ceil(3.1), math.ceil(-3.9))
print(math.max(1, 5, 3, 7, 2))
print(math.min(1, 5, 3, 7, 2))
print(math.huge > 1e300)
print(math.huge == math.huge)
print(0/0 ~= 0/0) -- NaN never equals itself
print(7 % 3, -7 % 3, 7 % -3, -7 % -3) -- Lua modulo semantics
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "tostring_tonumber_edges",
src: r#"
print(tostring(nil), tostring(true), tostring(false))
print(tonumber("42"), tonumber("3.14"))
print(tonumber("0x1f"), tonumber("not a number"))
print(tonumber("100", 2), tonumber("ff", 16), tonumber("777", 8))
print(type(tonumber("42")))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "error_object_propagation",
src: r#"
-- error with non-string value (table)
local ok, err = pcall(function() error({code = 42, msg = "boom"}) end)
print(ok, type(err), err.code, err.msg)
-- error with nil
local ok2, err2 = pcall(function() error(nil) end)
print(ok2, type(err2))
-- assert with message
local ok3, err3 = pcall(function() assert(false, "assertion-message") end)
print(ok3, err3)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "pcall_stack_overflow",
src: r#"
local function f(n) return 1 + f(n + 1) end
local ok, err = pcall(f, 0)
print(ok)
print(string.find(tostring(err), "stack overflow") ~= nil)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "ipairs_stops_at_nil",
src: r#"
-- ipairs is the integer-key iterator that stops at the first nil
local t = {10, 20, nil, 40, 50}
local count, sum = 0, 0
for i, v in ipairs(t) do count = count + 1; sum = sum + v end
print(count, sum) -- expects 2, 30 (stops at nil)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "pairs_iteration_order_irrelevant",
src: r#"
-- pairs iteration order is unspecified; sum the keys and values to
-- get a deterministic comparison cross-engine
local t = {x = 1, y = 2, z = 3, [4] = 4, [10] = 10}
local ksum, vsum = 0, 0
for k, v in pairs(t) do
if type(k) == "number" then ksum = ksum + k end
if type(v) == "number" then vsum = vsum + v end
end
print(ksum, vsum)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "string_len_byte_position",
src: r#"
local s = "Hello, world!"
print(#s, string.len(s))
print(s:sub(1, 5), s:sub(-6, -1))
local b, e = string.find(s, "world")
print(b, e)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "table_sort_with_comparator",
src: r#"
local t = {"banana", "apple", "cherry", "date"}
table.sort(t)
print(table.concat(t, "/"))
table.sort(t, function(a, b) return #a < #b end)
print(table.concat(t, "/"))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "gsub_with_table_replacement",
src: r#"
local t = {name = "Alice", age = "30"}
print((string.gsub("Hello $name, age $age", "%$(%w+)", t)))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "gsub_with_function_replacement",
src: r#"
print((string.gsub("hello world", "(%w+)", function(w) return w:upper() end)))
print((string.gsub("abc123def456", "%d+", function(n) return "[" .. n .. "]" end)))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "gsub_with_count_limit",
src: r#"
local s, n = string.gsub("a-b-c-d-e", "-", "/", 2)
print(s, n)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "integer_division_53plus",
src: r#"
print(10 // 3, -10 // 3, 10 // -3, -10 // -3)
print(10.0 // 3, 10 // 3.0)
print(math.floor(10 / 3))
"#,
min_version: LuaVersion::Lua53,
},
Program {
name: "bitwise_53plus",
src: r#"
print(0xff & 0x0f, 0xff | 0x100, 0xff ~ 0x0f, ~0)
print(1 << 8, 256 >> 4)
print(string.format("%x", 0xABCD ~ 0xFFFF))
"#,
min_version: LuaVersion::Lua53,
},
Program {
name: "string_format_pct_q",
src: r#"
-- %q produces a Lua-readable quoted string. Output format is dialect-
-- stable for printable ASCII without embedded controls.
print(string.format("%q", "hello"))
print(string.format("%q", [[plain ascii]]))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "long_string_literal",
src: r#"
local s = [[
line1
line2
line3]]
print(#s)
local s2 = [==[
nested [[ test ]]
]==]
print(#s2)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "goto_forward_label",
src: r#"
-- 5.2+: goto/label
local i = 0
::start::
i = i + 1
if i < 5 then goto start end
print(i)
"#,
min_version: LuaVersion::Lua52,
},
Program {
name: "const_attribute_54plus",
src: r#"
local x <const> = 42
print(x, x + 1)
local s <const> = "hello"
print(s, #s)
"#,
min_version: LuaVersion::Lua54,
},
Program {
name: "string_pack_unpack_53plus",
src: r#"
-- string.pack / string.unpack added in 5.3
local s = string.pack(">i4", 12345)
print(#s)
local n, pos = string.unpack(">i4", s)
print(n, pos)
print(string.packsize(">i4i2"))
"#,
min_version: LuaVersion::Lua53,
},
Program {
name: "math_type_53plus",
src: r#"
-- math.type added in 5.3
print(math.type(1), math.type(1.0), math.type("1"))
print(math.tointeger(3.0), math.tointeger(3.5))
"#,
min_version: LuaVersion::Lua53,
},
Program {
name: "string_to_number_coercion",
src: r#"
print(tonumber("42"))
print(tonumber("3.14"))
print(tonumber(" 42 "))
print(tonumber("0xff"))
print(tonumber("1e3"))
print(tonumber("not a number"))
print(tonumber(""))
print(tonumber(nil))
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "coroutine_close_54plus",
src: r#"
-- coroutine.close added in 5.4
local co = coroutine.create(function() coroutine.yield(1); coroutine.yield(2) end)
local _, a = coroutine.resume(co)
print(a, coroutine.status(co))
local closed = coroutine.close(co)
print(closed, coroutine.status(co))
"#,
min_version: LuaVersion::Lua54,
},
Program {
name: "tostring_special_floats",
src: r#"
-- tostring on Inf, -Inf, NaN. PUC outputs are dialect-stable
-- ("inf", "-inf", "nan" since 5.3; "1.#INF" / "-1.#INF" / "-1.#IND"
-- on Windows 5.1/5.2 — luna's reference is unix-PUC behavior).
local inf = 1/0
print(inf == math.huge)
print(-inf == -math.huge)
local nan = 0/0
print(nan == nan)
"#,
min_version: LuaVersion::Lua53,
},
Program {
name: "table_iteration_complete",
src: r#"
-- mixed string/integer keys via next + pairs
local t = {alpha = 1, beta = 2, gamma = 3, [1] = "a", [2] = "b"}
local strk_count, intk_count = 0, 0
for k in pairs(t) do
if type(k) == "string" then strk_count = strk_count + 1
else intk_count = intk_count + 1 end
end
print(strk_count, intk_count)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "err_arith_on_nil",
src: r#"
local function bad() local x; return x + 1 end
local ok, err = pcall(bad)
print(ok)
print(string.find(tostring(err), "arithmetic") ~= nil)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "err_index_nil_field",
src: r#"
-- accessing a field on nil
local function bad() local x; return x.field end
local ok, err = pcall(bad)
print(ok)
print(string.find(tostring(err), "nil") ~= nil)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "err_call_non_callable",
src: r#"
-- calling a non-callable value
local function bad() local x = 42; return x() end
local ok, err = pcall(bad)
print(ok)
print(string.find(tostring(err), "call") ~= nil)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "err_concat_with_nil",
src: r#"
local function bad() return "hello" .. nil end
local ok, err = pcall(bad)
print(ok)
print(string.find(tostring(err), "concatenate") ~= nil)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "err_compare_incompatible",
src: r#"
-- comparing incompatible types
local function bad() return "abc" < 42 end
local ok, err = pcall(bad)
print(ok)
print(string.find(tostring(err), "compare") ~= nil)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "err_index_nil_with_string_key",
src: r#"
local function bad()
local t
return t["key"]
end
local ok, err = pcall(bad)
print(ok)
print(string.find(tostring(err), "nil") ~= nil)
"#,
min_version: LuaVersion::Lua51,
},
Program {
name: "err_divide_by_zero_int_53plus",
src: r#"
-- 5.3+ integer division by zero raises; float / 0 returns Inf
local function bad() local x = 1; return x // 0 end
local ok, err = pcall(bad)
print(ok)
print(string.find(tostring(err), "zero") ~= nil)
-- float division by zero is NOT an error
print(1.0 / 0.0) -- "inf"
print(-1.0 / 0.0) -- "-inf"
"#,
min_version: LuaVersion::Lua53,
},
Program {
name: "assert_with_table_message",
src: r#"
local function bad() assert(false, {code = 7, msg = "fail"}) end
local ok, err = pcall(bad)
print(ok, type(err), err.code, err.msg)
"#,
min_version: LuaVersion::Lua53,
},
Program {
name: "error_with_level_arg",
src: r#"
-- error(msg, 2) reports the CALLER's line, not the error() call's
local function inner() error("err-from-inner", 2) end
local function outer() inner() end
local ok, err = pcall(outer)
print(ok)
-- both engines should report a line in the same file context
print(string.find(tostring(err), "err-from-inner") ~= nil)
"#,
min_version: LuaVersion::Lua51,
},
];
fn run_diff_for(version: LuaVersion, label: &str, prog: &Program) -> Result<(), String> {
if prog.min_version > version {
return Ok(()); }
let bin = match reference_bin_for(version) {
Some(b) => b,
None => return Ok(()), };
let luna_out = run_on_luna(version, prog.src);
let puc_out = run_on_puc(bin, prog.src);
if luna_out != puc_out {
let luna_str = String::from_utf8_lossy(&luna_out);
let puc_str = String::from_utf8_lossy(&puc_out);
return Err(format!(
"e2e divergence — [{}] program={}\n luna stdout: {:?}\n PUC stdout: {:?}",
label, prog.name, luna_str, puc_str
));
}
Ok(())
}
fn e2e_diff_for_dialect(version: LuaVersion, label: &str) {
let mut failures: Vec<String> = Vec::new();
for prog in PROGRAMS {
if let Err(e) = run_diff_for(version, label, prog) {
failures.push(e);
}
}
if !failures.is_empty() {
let mut s = String::new();
for f in &failures {
writeln!(&mut s, "{}", f).unwrap();
}
panic!(
"e2e dialect {}: {} divergences\n{}",
label,
failures.len(),
s
);
}
}
#[test]
fn e2e_5_1() {
e2e_diff_for_dialect(LuaVersion::Lua51, "5.1");
}
#[test]
fn e2e_5_2() {
e2e_diff_for_dialect(LuaVersion::Lua52, "5.2");
}
#[test]
fn e2e_5_3() {
e2e_diff_for_dialect(LuaVersion::Lua53, "5.3");
}
#[test]
fn e2e_5_4() {
e2e_diff_for_dialect(LuaVersion::Lua54, "5.4");
}
#[test]
fn e2e_5_5() {
e2e_diff_for_dialect(LuaVersion::Lua55, "5.5");
}