Yielding inside of a generic for loop iterator function

I understand they’re re-writing and testing a new Lua interpreter and was wondering why you can’t yield inside of a generic for loop function.

3 Likes

I’m not sure what you mean, as I think you can do that.

Could you please post a piece of example code?

Hmmm, in regular Lua you can. However, I could see the advantage to not allowing yielding in loop iterator functions in an optimized interpreter. Pure functions, those without side effects (take immutable input and don’t change global state / upvalues), lend themselves to easy optimization. Even more so if they are deterministic (only dependent upon input and produce the same output every call for the same input). Doing so would allow for easy loop unrolling, merging, conditional skipping, and much better constant propagation / folding inside the loop.

But did they actually do this? IDK, just some thoughts.

To my knowledge, you could not yield in a generic for loop iterator function on Roblox’s current Lua interpreter.

With Roblox’s new Lua Interpreter:

With a wait() function:
image

2 Likes

(oops i should have tested)

for _ in coroutine.yield do
	
end

errors in vanilla lua also

I don’t know if it’s a bug though. Because as @IdiomicLanguage was saying, there are a lot of optimizations that can be done if it didn’t allow yielding in loop iterator functions. And it if was my prior knowledge before and with the new interpreter, then it probably was like this with the old Lua interpreter.

The most common use of generic for loops is with the ipairs, pairs, and next functions. I find that I use ipairs and pairs a lot in my scripts and I know my other co-worker does too.

I find a useful situation for the function I used to be creating an effect for an action of some sort.

Sorry I was unclear, in vanilla lua 5.1 you cannot “Yielding inside of a generic for loop iterator function”


ipairs, pairs, and next all don’t yield, but the code inside the for loop’s block may yield

I don’t know the internals about why scripts cannot “yield across metamethod/C-call boundary” but I guess it is a lua limitation, and in this case lua has opcodes for iterating generic for loops “TFORLOOP”, and since the interpreter is written in c, the c-call boundary exists in this case

http://luaforge.net/docman/83/98/ANoFrillsIntroToLua51VMInstructions.pdf page 44

I’ve dug into it a bit more. This is the coroutine.resume function’s source:

LUA_API int lua_yield (lua_State *L, int nresults) {
  luai_userstateyield(L, nresults);
  lua_lock(L);
  if (L->nCcalls > L->baseCcalls)
    luaG_runerror(L, "attempt to yield across metamethod/C-call boundary");
  L->base = L->top - nresults;  /* protect stack slots below */
  L->status = LUA_YIELD;
  lua_unlock(L);
  return -1;
}

Now generic for loops use the TFORLOOP opcode as seen in A No Frills Intro to Lua 5.1 VM Instructions:

Which looking at the main interpreter loop (luaV_execute) translates to this:

      case OP_TFORLOOP: {
        StkId cb = ra + 3;  /* call base */
        setobjs2s(L, cb+2, ra+2);
        setobjs2s(L, cb+1, ra+1);
        setobjs2s(L, cb, ra);
        L->top = cb+3;  /* func. + 2 args (state and index) */
        Protect(luaD_call(L, cb, GETARG_C(i)));
        L->top = L->ci->top;
        cb = RA(i) + 3;  /* previous call may change the stack */
        if (!ttisnil(cb)) {  /* continue loop? */
          setobjs2s(L, cb-1, cb);  /* save control variable */
          dojump(L, pc, GETARG_sBx(*pc));  /* jump back */
        }
        pc++;
        continue;
      }

Which we can see calls luaD_call which is defined here:

/*
** Call a function (C or Lua). The function to be called is at *func.
** The arguments are on the stack, right after the function.
** When returns, all the results are on the stack, starting at the original
** function position.
*/ 
void luaD_call (lua_State *L, StkId func, int nResults) {
  if (++L->nCcalls >= LUAI_MAXCCALLS) {
    if (L->nCcalls == LUAI_MAXCCALLS)
      luaG_runerror(L, "C stack overflow");
    else if (L->nCcalls >= (LUAI_MAXCCALLS + (LUAI_MAXCCALLS>>3)))
      luaD_throw(L, LUA_ERRERR);  /* error while handing stack error */
  }
  if (luaD_precall(L, func, nResults) == PCRLUA)  /* is a Lua function? */
    luaV_execute(L, 1);  /* call it */
  L->nCcalls--;
  luaC_checkGC(L);
}

So, TFORLOOP (the generic for opcode) calls the given function with luaD_call every iteration which increases the C call count. Yield later reads this C call count to determine if a C boundary is crossed. The baseCcalls is set to the nCcalls + 1 by resume. lua_call (the normal function to call a C / lua using the Lua stack from C) also uses luaD_call. Translating all this to human terms, yielding is only possible when C isn’t using the Lua stack for its own calls. However since TFORLOOP is C code it uses the C calling API which means that yielding is disabled.

TFORLOOP could be easily modified to edit the stack and call the generic for loop iterator like CALL does. However, TFORLOOP does additional work after the call (it copies the return values into the loop variables). Doing so means that another instruction to copy the variables would be needed after the TFORLOOP returns. TFORLOOP was introduced to optimize generic for loop execution and one of the ways it does that is a reduction in the number of calls required to execute the loop (and thus decrease interpretation overhead).

So no, default Lua isn’t doing anything near as fancy as what I described in my last post. Unless the new interpreter prevents modifying / using upvalues in generic for loop iterators, I doubt it is either. (I say interpreter but really it is the parser that sets the rules.)

Edit: I read @Acreol’s post after I posted. Sorry, I basically said the same thing in more detail. I believe you can mark multiple solutions?

2 Likes

Thanks for help guys! (please excuse my non-politically correct talk)

I don’t really understand what exactly all of that means. I’ve been programming on Roblox for like 5 years now, but I only learned C++ last year in college.