1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
//! Tests for method-lookup inline caches w.r.t. `globals_version`.
//!
//! Both `StringMethodCacheEntry` and `MethodLookupCacheEntry` embed a
//! direct `ObjectPtr` into a global library table (the `string` lib for
//! the former, whatever `__index` resolved to for the latter). If a
//! builtin global is rebound from Lua, or `with_restricted_env` swaps
//! the environment, the original library can stay alive (still rooted
//! by the IC and by `saved_builtins`), so without a `globals_version`
//! check the cache silently bypasses the new binding. These tests pin
//! the invalidation behavior.
use dellingr::{ArgCount, RetCount, State};
fn run_number(code: &str) -> f64 {
let mut state = State::new();
state.load_string(code).unwrap();
state
.call(ArgCount::Fixed(0), RetCount::Fixed(1))
.unwrap_or_else(|e| panic!("Error running: {code}\n{e}"));
state.to_number(-1).unwrap()
}
#[test]
fn string_method_cache_invalidates_on_string_rebind() {
let val = run_number(
r#"
local function f() return ("a"):upper() == "A" and 1 or 0 end
local first = f()
string = { upper = function(_self) return "REBOUND" end }
local second = f()
local result_after = ("a"):upper() == "REBOUND" and 1 or 0
return first * 100 + second * 10 + result_after
"#,
);
// first: cache cold, hits real string lib -> 1
// second: cache previously warm, but globals_version bumped -> slow
// path resolves through new `string` -> "REBOUND" so == "A"
// is false -> 0
// result_after: direct slow-path call with new string lib -> 1
assert_eq!(val, 101.0);
}
#[test]
fn string_method_cache_survives_gc_after_string_rebind() {
let mut state = State::new();
state
.load_string(
r#"
function warm() return ("a"):upper() end
function rebind_and_drop()
string = { upper = function(_self) return "NEW" end }
end
function probe() return ("a"):upper() end
"#,
)
.unwrap();
state.call(ArgCount::Fixed(0), RetCount::Fixed(0)).unwrap();
// Warm the string-method IC at the call site inside `warm`.
state.get_global("warm").unwrap();
state.call(ArgCount::Fixed(0), RetCount::Fixed(1)).unwrap();
state.pop(1).unwrap();
// Rebind `string` in Lua and drop the local reference.
state.get_global("rebind_and_drop").unwrap();
state.call(ArgCount::Fixed(0), RetCount::Fixed(0)).unwrap();
// Force GC so the original `string` lib (no longer rooted via the
// builtins slot) is collected. With the IC fix the cache rejects
// the stale entry on globals_version mismatch and the slow path
// resolves through the new `string` table; without the fix this
// would either return stale data or panic on a stale ObjectPtr.
state.gc_collect();
state.get_global("probe").unwrap();
state.call(ArgCount::Fixed(0), RetCount::Fixed(1)).unwrap();
let result = state.to_string(-1).unwrap();
assert_eq!(result, "NEW");
}
#[test]
fn string_method_cache_blocked_by_with_restricted_env() {
let mut state = State::new();
state
.load_string(
r#"
function call_upper() return ("hi"):upper() end
"#,
)
.unwrap();
state.call(ArgCount::Fixed(0), RetCount::Fixed(0)).unwrap();
// Warm the IC outside the sandbox.
state.get_global("call_upper").unwrap();
state.call(ArgCount::Fixed(0), RetCount::Fixed(1)).unwrap();
let warm = state.to_string(-1).unwrap();
assert_eq!(warm, "HI");
state.pop(1).unwrap();
// Inside the sandbox, whitelist the entry-point function but NOT
// `string`. With the fix, the IC's globals_version mismatch forces
// the slow path, which sees Nil for the `string` builtin and
// errors. Without the fix, the cached ObjectPtr (still alive in
// `saved_builtins`) silently bypasses the restriction.
let restricted_result = state.with_restricted_env(&["call_upper"], |state| {
state.get_global("call_upper").unwrap();
state.call(ArgCount::Fixed(0), RetCount::Fixed(1))
});
assert!(
restricted_result.is_err(),
"with_restricted_env without `string` whitelist must reject `s:upper()`, \
got: {restricted_result:?}"
);
// After the sandbox returns, the IC works again on a fresh call.
state.get_global("call_upper").unwrap();
state.call(ArgCount::Fixed(0), RetCount::Fixed(1)).unwrap();
let after = state.to_string(-1).unwrap();
assert_eq!(after, "HI");
}