@@ -123,11 +123,18 @@ int main(void)
123123 IdleReclaimCtx ctx ;
124124 PatchedPC loop_patch , ret_patch , itern_patch ;
125125 pthread_t reclaimer ;
126+ int cache_base , jfuncf_idx , loop_idx , ret_idx , itern_idx , itern_input_idx ;
127+ int probe_idx , generic_idx , generic_input_idx ;
126128
127129 assert (g != NULL );
128130 lua_gc (L , LUA_GCSTOP , 0 );
129131 publish_ptr (L , "__idle_reclaim_vmstate" , & G2TG (g )-> vmstate );
130132 publish_ptr (L , "__idle_reclaim_hits" , & native_hits );
133+ /* The native-entry probe intentionally uses FFI/cdata metadata and therefore
134+ ** runs only while the metadata SMR writer can make progress. The separate
135+ ** closed-window numeric and generic traces below use scalar/upvalue state;
136+ ** an unrelated retained metadata lookup would correctly wait for the test's
137+ ** deliberately frozen exclusive writer and obscure the JIT-entry proof. */
131138 ljt_lua_dostring (L ,
132139 "jit.flush()\n"
133140 "jit.on()\n"
@@ -151,11 +158,20 @@ int main(void)
151158 " return x\n"
152159 "end\n"
153160 "for _ = 1, 20 do assert(__idle_reclaim_probe(400) == 80200) end\n"
154- "generic_input = {11, 22, 33, 44, 55}\n"
155- "function __idle_reclaim_generic(t)\n"
161+ "function __idle_reclaim_closed_probe(n)\n"
162+ " local x = 0\n"
163+ " for i = 1, n do x = x + i end\n"
164+ " return x\n"
165+ "end\n"
166+ "for _ = 1, 20 do assert(__idle_reclaim_closed_probe(400) == 80200) end\n"
167+ "local function idle_generic_iter(limit, i)\n"
168+ " i = i + 1\n"
169+ " if i <= limit then return i, i * 11 end\n"
170+ "end\n"
171+ "generic_input = 5\n"
172+ "function __idle_reclaim_generic(limit)\n"
156173 " local n, x = 0, 0\n"
157- " for _, v in ipairs(t) do\n"
158- " hits[0] = hits[0] + bit.rshift(bit.bnot(vmstate[0]), 31)\n"
174+ " for _, v in idle_generic_iter, limit, 0 do\n"
159175 " n, x = n + 1, x + v\n"
160176 " end\n"
161177 " return n, x\n"
@@ -175,9 +191,11 @@ int main(void)
175191 "function __idle_shadow_ret(x) return x + 7 end\n"
176192 "jit.off(__idle_shadow_ret, true)\n"
177193 "idle_shadow_input = {11, 22, 33, 44, 55}\n"
178- "idle_real_next = next\n"
194+ "local idle_real_next = next\n"
195+ "local next = idle_real_next\n"
179196 "function idle_next_wrapper(t, k) return idle_real_next(t, k) end\n"
180197 "jit.off(idle_next_wrapper, true)\n"
198+ "function __idle_shadow_set_next(f) next = f end\n"
181199 "function __idle_shadow_itern(t)\n"
182200 " local n, x = 0, 0\n"
183201 " for _, v in next, t do n, x = n + 1, x + v end\n"
@@ -221,6 +239,32 @@ int main(void)
221239 }
222240 lj_gc2_test_idle_reclaim_leave (g );
223241
242+ /* Retained table lookup now takes a short SMR reader while copying a global
243+ ** value. Preload every value needed below before the test hook freezes the
244+ ** exclusive reclaimer; otherwise lua_getglobal() would correctly wait for
245+ ** the deliberately paused writer and the fixture would deadlock itself
246+ ** before exercising closed-gate bytecode recovery. Keep the cached values on
247+ ** the Lua stack as ordinary roots and duplicate them for each call. */
248+ cache_base = lua_gettop (L );
249+ lua_getglobal (L , "__idle_reclaim_jfuncf" );
250+ jfuncf_idx = lua_gettop (L );
251+ lua_getglobal (L , "__idle_shadow_loop" );
252+ loop_idx = lua_gettop (L );
253+ lua_getglobal (L , "__idle_shadow_ret" );
254+ ret_idx = lua_gettop (L );
255+ lua_getglobal (L , "__idle_shadow_itern" );
256+ itern_idx = lua_gettop (L );
257+ lua_getglobal (L , "idle_shadow_input" );
258+ itern_input_idx = lua_gettop (L );
259+ lua_getglobal (L , "__idle_reclaim_closed_probe" );
260+ probe_idx = lua_gettop (L );
261+ assert (lua_isfunction (L , jfuncf_idx ));
262+ assert (lua_isfunction (L , loop_idx ));
263+ assert (lua_isfunction (L , ret_idx ));
264+ assert (lua_isfunction (L , itern_idx ));
265+ assert (lua_istable (L , itern_input_idx ));
266+ assert (lua_isfunction (L , probe_idx ));
267+
224268 ctx .g = g ;
225269 ctx .epoch = lj_gc2_retire_epoch (g ) + 1u ;
226270 ctx .reclaimed = ~(uint32_t )0 ;
@@ -235,20 +279,23 @@ int main(void)
235279 ** immediately before its trace/mcode retired-slot release pass. A real
236280 ** BC_JLOOP entry attempt must remain interpreted while the owned gate is
237281 ** closed. Prototype-owned startins recovery must let the interpreter finish
238- ** every iteration before the paused SMR writer is released. */
282+ ** every iteration before the paused SMR writer is released. The scalar
283+ ** closed probe's displaced counter is the authoritative native-entry veto;
284+ ** the FFI/vmstate probe independently proves native execution before and
285+ ** after this artificial pause. */
239286 assert (gc2_smr_reclaiming_acq (g ) != 0 );
240287 assert (gc2_jit_phase_gate_acq (g ) == 0 );
241288 assert (!lj_gc2_jit_entry_open (g ));
242289 gc2_jit_sweep_displaced_rel (g , 0 );
243- lua_getglobal (L , "__idle_reclaim_jfuncf" );
290+ lua_pushvalue (L , jfuncf_idx );
244291 lua_pushinteger (L , 37 );
245292 ljt_lua_pcall (L , 1 , 1 , "closed IDLE JFUNCF entry" );
246293 assert (lua_tointeger (L , -1 ) == 112 );
247294 lua_pop (L , 1 );
248295 assert (gc2_jit_sweep_displaced_acq (g ) != 0 );
249296
250297 gc2_jit_sweep_displaced_rel (g , 0 );
251- lua_getglobal (L , "__idle_shadow_loop" );
298+ lua_pushvalue (L , loop_idx );
252299 lua_pushinteger (L , 1000 );
253300 ljt_lua_pcall (L , 1 , 2 , "closed IDLE LOOP shadow" );
254301 assert (lua_tointeger (L , -2 ) == 1000 );
@@ -257,7 +304,7 @@ int main(void)
257304 assert (gc2_jit_sweep_displaced_acq (g ) != 0 );
258305
259306 gc2_jit_sweep_displaced_rel (g , 0 );
260- lua_getglobal (L , "__idle_shadow_ret" );
307+ lua_pushvalue (L , ret_idx );
261308 lua_pushinteger (L , 35 );
262309 ljt_lua_pcall (L , 1 , 1 , "closed IDLE RET shadow" );
263310 assert (lua_tointeger (L , -1 ) == 42 );
@@ -266,8 +313,8 @@ int main(void)
266313
267314 gc2_jit_sweep_displaced_rel (g , 0 );
268315 lj_trace_test_force_startins_retry (1 );
269- lua_getglobal (L , "__idle_shadow_itern" );
270- lua_getglobal (L , "idle_shadow_input" );
316+ lua_pushvalue (L , itern_idx );
317+ lua_pushvalue (L , itern_input_idx );
271318 ljt_lua_pcall (L , 1 , 2 , "closed IDLE ITERN shadow" );
272319 assert (lua_tointeger (L , -2 ) == 5 );
273320 assert (lua_tointeger (L , -1 ) == 165 );
@@ -278,7 +325,7 @@ int main(void)
278325
279326 gc2_jit_sweep_displaced_rel (g , 0 );
280327 lj_trace_test_force_startins_retry (1 );
281- lua_getglobal (L , "__idle_reclaim_probe" );
328+ lua_pushvalue (L , probe_idx );
282329 lua_pushinteger (L , 20000 );
283330 ljt_lua_pcall (L , 1 , 1 , "closed IDLE reclaim entry" );
284331 assert (lua_tonumber (L , -1 ) == 200010000.0 );
@@ -293,15 +340,17 @@ int main(void)
293340 assert (la_load32_acq (& ctx .done ) != 0 );
294341 assert (gc2_smr_reclaiming_acq (g ) == 0 );
295342 assert (gc2_jit_phase_gate_acq (g ) != 0 );
343+ lua_settop (L , cache_base );
296344
297345 /* A failed ISNEXT must take a trace-body lease before deciding whether the
298346 ** observed JLOOP still names the exact live ITERN generation and before
299347 ** publishing JLOOP -> ITERC. Do that lease-dependent despecialization only
300348 ** after the deliberately paused exclusive SMR writer has left. During the
301349 ** pause the real builtin above still exercises closed-gate JLOOP recovery
302350 ** through the immutable sidecar without waiting for body admission. */
351+ lua_getglobal (L , "__idle_shadow_set_next" );
303352 lua_getglobal (L , "idle_next_wrapper" );
304- lua_setglobal (L , "next " );
353+ ljt_lua_pcall (L , 1 , 0 , "set ITERN invalidation iterator " );
305354 lua_getglobal (L , "__idle_shadow_itern" );
306355 lua_getglobal (L , "idle_shadow_input" );
307356 ljt_lua_pcall (L , 1 , 2 , "reopened IDLE ITERN invalidation" );
@@ -310,8 +359,9 @@ int main(void)
310359 lua_pop (L , 2 );
311360 assert (bc_op ((BCIns )la_load32_acq ((const uint32_t * )itern_patch .pc )) ==
312361 BC_ITERC );
313- lua_getglobal (L , "idle_real_next" );
314- lua_setglobal (L , "next" );
362+ lua_getglobal (L , "__idle_shadow_set_next" );
363+ lua_getglobal (L , "next" );
364+ ljt_lua_pcall (L , 1 , 0 , "restore ITERN iterator" );
315365 restore_patch (& loop_patch );
316366 restore_patch (& ret_patch );
317367 restore_patch (& itern_patch );
@@ -323,12 +373,19 @@ int main(void)
323373 ctx .done = 0 ;
324374 la_store32_rel (& native_hits , 0 );
325375 gc2_jit_sweep_displaced_rel (g , 0 );
376+ cache_base = lua_gettop (L );
377+ lua_getglobal (L , "__idle_reclaim_generic" );
378+ generic_idx = lua_gettop (L );
379+ lua_getglobal (L , "generic_input" );
380+ generic_input_idx = lua_gettop (L );
381+ assert (lua_isfunction (L , generic_idx ));
382+ assert (lua_isnumber (L , generic_input_idx ));
326383 lj_gc2_test_idle_reclaim_pause_after_jit_quiescence ();
327384 assert (pthread_create (& reclaimer , NULL , idle_reclaim_main , & ctx ) == 0 );
328385 wait_for_idle_reclaim_pause (& ctx );
329386 lj_trace_test_force_startins_retry (1 );
330- lua_getglobal (L , "__idle_reclaim_generic" );
331- lua_getglobal (L , "generic_input" );
387+ lua_pushvalue (L , generic_idx );
388+ lua_pushvalue (L , generic_input_idx );
332389 ljt_lua_pcall (L , 1 , 2 , "closed IDLE generic-loop entry" );
333390 assert (lua_tointeger (L , -2 ) == 5 );
334391 assert (lua_tointeger (L , -1 ) == 165 );
@@ -344,6 +401,7 @@ int main(void)
344401 assert (la_load32_acq (& ctx .done ) != 0 );
345402 assert (gc2_smr_reclaiming_acq (g ) == 0 );
346403 assert (gc2_jit_phase_gate_acq (g ) != 0 );
404+ lua_settop (L , cache_base );
347405
348406 lua_getglobal (L , "__idle_reclaim_probe" );
349407 lua_pushinteger (L , 20000 );
0 commit comments