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 eval_int(src: &str) -> i64 {
let mut v = eval(src);
assert_eq!(v.len(), 1, "expected 1 value from {src:?}");
match v.pop().unwrap() {
Value::Int(i) => i,
other => panic!("expected Int from {src:?}, got {other:?}"),
}
}
fn eval_bool(src: &str) -> bool {
let mut v = eval(src);
assert_eq!(v.len(), 1, "expected 1 value from {src:?}");
match v.pop().unwrap() {
Value::Bool(b) => b,
other => panic!("expected Bool from {src:?}, got {other:?}"),
}
}
#[test]
fn getfield_matches_gettable_for_present_str_key() {
let v = eval_int(
r#"
local t = { foo = 11, bar = 22, baz = 33 }
local k = "foo"
if t.foo == t[k] and t.bar == t["bar"] and t.baz == t[(function() return "baz" end)()] then
return 1
else
return 0
end
"#,
);
assert_eq!(
v, 1,
"Op::GetField and Op::GetTable diverged on present str key"
);
}
#[test]
fn getfield_matches_gettable_for_absent_str_key_returns_nil() {
let v = eval_int(
r#"
local t = { foo = 1 }
local k = "missing"
if t.missing == nil and t[k] == nil and t.missing == t[k] then
return 1
else
return 0
end
"#,
);
assert_eq!(v, 1);
}
#[test]
fn getfield_matches_gettable_for_present_with_nil_slot() {
let v = eval_int(
r#"
local t = { x = 1 }
t.x = nil
local k = "x"
if t.x == nil and t[k] == nil then return 1 else return 0 end
"#,
);
assert_eq!(v, 1);
}
#[test]
fn setfield_round_trips_via_getfield_and_gettable() {
let v = eval_int(
r#"
local t = { a = 0, b = 0, c = 0 }
t.a = 10 -- Op::SetField (fast path)
local k = "b"
t[k] = 20 -- Op::SetTable (slow path)
t.c = t.a + t[k] -- mixed read
local kc = "c"
return t.a + t["b"] + t[kc] + t.c
"#,
);
assert_eq!(
v,
10 + 20 + 30 + 30,
"SetField/SetTable cross-path observation broken"
);
}
#[test]
fn setfield_overwrites_existing_no_metatable() {
let v = eval_int(
r#"
local t = { tokens = 1000, last = 0, rate = 100 }
for i = 1, 100 do
t.tokens = t.tokens - 1
t.last = i
end
return t.tokens + t.last + t.rate
"#,
);
assert_eq!(v, 900 + 100 + 100);
}
#[test]
fn setfield_inserts_new_key_when_no_metatable() {
let v = eval_int(
r#"
local t = {}
t.fresh = 7
local k = "fresh"
return t.fresh + t[k]
"#,
);
assert_eq!(v, 14);
}
#[test]
fn getfield_with_metatable_routes_through_index_metamethod() {
let v = eval_int(
r#"
local fallback = { y = 99 }
local t = setmetatable({}, { __index = fallback })
return t.y
"#,
);
assert_eq!(v, 99);
}
#[test]
fn setfield_with_metatable_routes_through_newindex_chain() {
let v = eval_int(
r#"
local seen_key = nil
local seen_val = nil
local t = setmetatable({}, {
__newindex = function(_, k, v)
seen_key = k
seen_val = v
end,
})
t.absent = 42
if seen_key == "absent" and seen_val == 42 then return 1 else return 0 end
"#,
);
assert_eq!(v, 1, "SetField fast-path swallowed __newindex");
}
#[test]
fn setfield_present_key_with_newindex_metatable_still_in_place() {
let v = eval_int(
r#"
local fires = 0
local t = { x = 1 }
setmetatable(t, {
__newindex = function() fires = fires + 1 end,
})
t.x = 99
return fires
"#,
);
assert_eq!(v, 0);
}
#[test]
fn long_string_key_via_getfield_matches_gettable() {
let v = eval_int(
r#"
local longkey = "abcdefghijklmnopqrstuvwxyz0123456789_aaaaaaaaaaaaaaaaaaaa"
local t = {}
t[longkey] = 7
local longkey2 = "abcdefghijklmnopqrstuvwxyz0123456789_aaaaaaaaaaaaaaaaaaaa"
-- t[longkey2] forces a fresh string interning at compile-time
-- but the StringTable may dedupe both into one allocation; the
-- semantic test below holds either way.
if t[longkey] == t[longkey2] then return 1 else return 0 end
"#,
);
assert_eq!(v, 1, "long-string keys diverged across allocations");
}
#[test]
fn token_bucket_shape_terminates_with_expected_state() {
let v = eval_int(
r#"
local bucket = { tokens = 1000, last = 0, rate = 100 }
local now = 1
local refilled = 0
for i = 1, 1000 do
local elapsed = now - bucket.last
local refill = elapsed * bucket.rate
if refill > 0 then
bucket.tokens = math.min(1000, bucket.tokens + refill)
bucket.last = now
refilled = refilled + 1
end
if bucket.tokens >= 1 then
bucket.tokens = bucket.tokens - 1
end
now = now + 1
end
return bucket.tokens + bucket.last + bucket.rate + refilled
"#,
);
assert_eq!(v, 3099);
}
#[test]
fn fastpath_survives_rehash() {
let v = eval_int(
r#"
local t = {}
local names = { "a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r" }
for i, n in ipairs(names) do t[n] = i end
local sum = 0
for _, n in ipairs(names) do sum = sum + t[n] end
return sum + t.a + t.r
"#,
);
assert_eq!(v, 190);
}
#[test]
fn rawequal_holds_between_getfield_and_gettable_results() {
let r = eval_bool(
r#"
local t = { ref = {} }
local k = "ref"
return rawequal(t.ref, t[k])
"#,
);
assert!(r);
}