From 169185e4b48ff4e7e73c8c26d4c817ee13befd65 Mon Sep 17 00:00:00 2001 From: Laurence Tratt Date: Wed, 2 Sep 2026 14:20:36 +0100 Subject: [PATCH] Track recursion on `LClosure` not `Proto`. This more accurately reflects "this is a recursive function" (using `Proto` overapproximates what we want). The tricky part of this commit is that, implemented naively, it causes `cl` to live longer which causes more expensive deopt (etc). Fortunately we can move the tracking `if`s earlier in the interpreter, avoiding this problem. --- src/ldo.c | 78 +++++++++++++++++++++++++-------------------------- src/lfunc.c | 5 ++-- src/lobject.h | 17 +++++------ src/lvm.c | 22 +++++++++++---- 4 files changed, 66 insertions(+), 56 deletions(-) diff --git a/src/ldo.c b/src/ldo.c index 53799a9..d322035 100644 --- a/src/ldo.c +++ b/src/ldo.c @@ -646,7 +646,6 @@ l_sinline CallInfo *prepCallInfo (lua_State *L, StkId func, unsigned status, return ci; } - /* ** precall for C functions */ @@ -694,7 +693,30 @@ int luaD_pretailcall (lua_State *L, CallInfo *ci, StkId func, case LUA_VLCF: /* light C function */ return precallC(L, func, status, fvalue(s2v(func))); case LUA_VLCL: { /* Lua function */ - Proto *p = clLvalue(s2v(func))->p; + LClosure *cl = clLvalue(s2v(func)); + Proto *p = cl->p; +#ifdef USE_YK + if (yk_is_interpreting()) { + // If this is a recursive call and we don't yet have a yk_location, + // create one now. + LClosure *caller_cl = ci_func(ci); + if (cl->called && yk_location_is_null(p->yklocs[0])) { + p->yklocs[0] = yk_location_new(); +#if YKLUA_DEBUG_STRS + yk_location_set_debug_str(&p->yklocs[0], p->instdebugstrs[0]); +#endif + } else { + // Because this is a tail call the "current" function -- `caller_p` + // -- has implicitly returned. If the "current" function is the same + // as the "about to call" function, we don't do anything; in all + // other cases we mark the "current" function as uncalled. + if (!cl->called) + cl->called = true; + if (caller_cl != cl) + caller_cl->called = false; + } + } +#endif int fsize = p->maxstacksize; /* frame size */ int nfixparams = p->numparams; int i; @@ -707,27 +729,6 @@ int luaD_pretailcall (lua_State *L, CallInfo *ci, StkId func, setnilvalue(s2v(func + narg1)); /* complete missing arguments */ ci->top.p = func + 1 + fsize; /* top for new function */ lua_assert(ci->top.p <= L->stack_last.p); -#ifdef USE_YK - if (yk_is_interpreting()) { - // If this is a recursive call and we don't yet have a yk_location, - // create one now. - if (p->called && yk_location_is_null(p->yklocs[0])) { - p->yklocs[0] = yk_location_new(); -#if YKLUA_DEBUG_STRS - yk_location_set_debug_str(&p->yklocs[0], p->instdebugstrs[0]); -#endif - } else if (!p->called) { - p->called = true; - } - // Because this is a tail call the "current" function -- `caller_p` -- - // has implicitly returned. If the "current" function is the same as - // the "about to call" function, we don't do anything; in all other - // cases we mark the "current" function as uncalled. - Proto *caller_p = ci_func(ci)->p; - if (caller_p != p) - caller_p->called = false; - } -#endif ci->u.l.savedpc = p->code; /* starting point */ ci->callstatus |= CIST_TAIL; L->top.p = func + narg1; /* set top */ @@ -765,30 +766,29 @@ CallInfo *luaD_precall (lua_State *L, StkId func, int nresults) { return NULL; case LUA_VLCL: { /* Lua function */ CallInfo *ci; - Proto *p = clLvalue(s2v(func))->p; - int narg = cast_int(L->top.p - func) - 1; /* number of real arguments */ - int nfixparams = p->numparams; - int fsize = p->maxstacksize; /* frame size */ - checkstackp(L, fsize, func); - L->ci = ci = prepCallInfo(L, func, status, func + 1 + fsize); - ci->u.l.savedpc = p->code; /* starting point */ - for (; narg < nfixparams; narg++) - setnilvalue(s2v(L->top.p++)); /* complete missing arguments */ - lua_assert(ci->top.p <= L->stack_last.p); + LClosure *cl = clLvalue(s2v(func)); + Proto *p = cl->p; #ifdef USE_YK if (yk_is_interpreting()) { - // If this is a recursive call and we don't yet have a yk_location, - // create one now. - if (p->called && yk_location_is_null(p->yklocs[0])) { + if (cl->called && yk_location_is_null(p->yklocs[0])) { p->yklocs[0] = yk_location_new(); #if YKLUA_DEBUG_STRS yk_location_set_debug_str(&p->yklocs[0], p->instdebugstrs[0]); #endif - } else if (!p->called) { - p->called = true; } + else if (!cl->called) + cl->called = true; } #endif + int narg = cast_int(L->top.p - func) - 1; /* number of real arguments */ + int nfixparams = p->numparams; + int fsize = p->maxstacksize; /* frame size */ + checkstackp(L, fsize, func); + L->ci = ci = prepCallInfo(L, func, status, func + 1 + fsize); + ci->u.l.savedpc = p->code; /* starting point */ + for (; narg < nfixparams; narg++) + setnilvalue(s2v(L->top.p++)); /* complete missing arguments */ + lua_assert(ci->top.p <= L->stack_last.p); return ci; } default: { /* not a function */ @@ -1214,5 +1214,3 @@ TStatus luaD_protectedparser (lua_State *L, ZIO *z, const char *name, decnny(L); return status; } - - diff --git a/src/lfunc.c b/src/lfunc.c index 6a0948b..25fdcb5 100644 --- a/src/lfunc.c +++ b/src/lfunc.c @@ -42,6 +42,9 @@ LClosure *luaF_newLclosure (lua_State *L, int nupvals) { GCObject *o = luaC_newobj(L, LUA_VLCL, sizeLclosure(nupvals)); LClosure *c = gco2lcl(o); c->p = NULL; +#ifdef USE_YK + c->called = false; +#endif c->nupvalues = cast_byte(nupvals); while (nupvals--) c->upvals[nupvals] = NULL; return c; @@ -271,7 +274,6 @@ Proto *luaF_newproto (lua_State *L) { f->lastlinedefined = 0; f->source = NULL; #ifdef USE_YK - f->called = false; f->yklocs = NULL; #ifdef YKLUA_DEBUG_STRS f->instdebugstrs = NULL; @@ -338,4 +340,3 @@ const char *luaF_getlocalname (const Proto *f, int local_number, int pc) { } return NULL; /* not found */ } - diff --git a/src/lobject.h b/src/lobject.h index df0fd3e..5a21133 100644 --- a/src/lobject.h +++ b/src/lobject.h @@ -619,13 +619,6 @@ typedef struct Proto { TValue *k; /* constants used by the function */ Instruction *code; /* opcodes */ #ifdef USE_YK - /* Used to detect recursive function calls. When a function is - * called this is set to `true` and when we return it is set to `false`. This - * works because a recursive function call must detect the `true` case before - * the bit is flipped. In other words, `called` being `false` does not mean - * "this isn't a recursive call", but if it's `true` it definitely is a - * recursive call. */ - bool called; YkLocation *yklocs; /* One 'YkLocation' per instruction in 'code' */ #ifdef YKLUA_DEBUG_STRS char **instdebugstrs; /* One `char *` per instruction in `code` */ @@ -724,6 +717,15 @@ typedef struct CClosure { typedef struct LClosure { ClosureHeader; struct Proto *p; +#ifdef USE_YK + /* Used to detect recursive function calls. When a closure is + * called this is set to `true` and when we return it is set to `false`. This + * works because a recursive function call must detect the `true` case before + * the bit is flipped. In other words, `called` being `false` does not mean + * "this isn't a recursive call", but if it's `true` it definitely is a + * recursive call. */ + bool called; +#endif UpVal *upvals[1]; /* list of upvalues */ } LClosure; @@ -879,4 +881,3 @@ LUAI_FUNC void luaO_chunkid (char *out, const char *source, size_t srclen); #endif - diff --git a/src/lvm.c b/src/lvm.c index b221aeb..ed5ea39 100644 --- a/src/lvm.c +++ b/src/lvm.c @@ -1301,7 +1301,6 @@ Instruction load_inst(uint64_t pv, const Instruction *pc) { #define vmcase(l) case l: #define vmbreak break - void luaV_execute (lua_State *L, CallInfo *ci) { LClosure *cl; TValue *k; @@ -1854,6 +1853,10 @@ void luaV_execute (lua_State *L, CallInfo *ci) { vmbreak; } vmcase(OP_TAILCALL) { +#ifdef USE_YK + if (yk_is_interpreting()) + cl->called = false; +#endif StkId ra = RA(i); int b = GETARG_B(i); /* number of arguments + 1 (function) */ int n; /* number of results when calling a C function */ @@ -1880,6 +1883,10 @@ void luaV_execute (lua_State *L, CallInfo *ci) { } } vmcase(OP_RETURN) { +#ifdef USE_YK + if (yk_is_interpreting()) + cl->called = false; +#endif StkId ra = RA(i); int n = GETARG_B(i) - 1; /* number of results */ int nparams1 = GETARG_C(i); @@ -1902,6 +1909,10 @@ void luaV_execute (lua_State *L, CallInfo *ci) { goto ret; } vmcase(OP_RETURN0) { +#ifdef USE_YK + if (yk_is_interpreting()) + cl->called = false; +#endif if (l_unlikely(L->hookmask)) { StkId ra = RA(i); L->top.p = ra; @@ -1919,6 +1930,10 @@ void luaV_execute (lua_State *L, CallInfo *ci) { goto ret; } vmcase(OP_RETURN1) { +#ifdef USE_YK + if (yk_is_interpreting()) + cl->called = false; +#endif if (l_unlikely(L->hookmask)) { StkId ra = RA(i); L->top.p = ra + 1; @@ -1940,11 +1955,6 @@ void luaV_execute (lua_State *L, CallInfo *ci) { } } ret: /* return from a Lua function */ -#ifdef USE_YK - if (yk_is_interpreting()) { - cl->p->called = false; - } -#endif if (ci->callstatus & CIST_FRESH) return; /* end this frame */ else {