Calls cannot be tail in the scope of a to-be-closed variable

A to-be-closed variable must be closed when a block ends, so even
a 'return foo()' cannot directly returns the results of 'foo'; the
function must close the scope before returning.
This commit is contained in:
Roberto Ierusalimschy
2018-12-04 15:01:42 -02:00
parent 6d04537ea6
commit 28d829c867
4 changed files with 21 additions and 16 deletions

View File

@@ -53,6 +53,7 @@ typedef struct BlockCnt {
lu_byte nactvar; /* # active locals outside the block */ lu_byte nactvar; /* # active locals outside the block */
lu_byte upval; /* true if some variable in the block is an upvalue */ lu_byte upval; /* true if some variable in the block is an upvalue */
lu_byte isloop; /* true if 'block' is a loop */ lu_byte isloop; /* true if 'block' is a loop */
lu_byte insidetbc; /* true if inside the scope of a to-be-closed var. */
} BlockCnt; } BlockCnt;
@@ -510,6 +511,7 @@ static void enterblock (FuncState *fs, BlockCnt *bl, lu_byte isloop) {
bl->firstlabel = fs->ls->dyd->label.n; bl->firstlabel = fs->ls->dyd->label.n;
bl->firstgoto = fs->ls->dyd->gt.n; bl->firstgoto = fs->ls->dyd->gt.n;
bl->upval = 0; bl->upval = 0;
bl->insidetbc = (fs->bl != NULL && fs->bl->insidetbc);
bl->previous = fs->bl; bl->previous = fs->bl;
fs->bl = bl; fs->bl = bl;
lua_assert(fs->freereg == fs->nactvar); lua_assert(fs->freereg == fs->nactvar);
@@ -1631,6 +1633,7 @@ static void tocloselocalstat (LexState *ls) {
checknext(ls, '='); checknext(ls, '=');
exp1(ls, 0); exp1(ls, 0);
markupval(fs, fs->nactvar); markupval(fs, fs->nactvar);
fs->bl->insidetbc = 1; /* in the scope of a to-be-closed variable */
adjustlocalvars(ls, 1); adjustlocalvars(ls, 1);
luaK_codeABC(fs, OP_TBC, fs->nactvar - 1, 0, 0); luaK_codeABC(fs, OP_TBC, fs->nactvar - 1, 0, 0);
} }
@@ -1701,7 +1704,7 @@ static void retstat (LexState *ls) {
nret = explist(ls, &e); /* optional return values */ nret = explist(ls, &e); /* optional return values */
if (hasmultret(e.k)) { if (hasmultret(e.k)) {
luaK_setmultret(fs, &e); luaK_setmultret(fs, &e);
if (e.k == VCALL && nret == 1) { /* tail call? */ if (e.k == VCALL && nret == 1 && !fs->bl->insidetbc) { /* tail call? */
SET_OPCODE(getinstruction(fs,&e), OP_TAILCALL); SET_OPCODE(getinstruction(fs,&e), OP_TAILCALL);
lua_assert(GETARG_A(getinstruction(fs,&e)) == fs->nactvar); lua_assert(GETARG_A(getinstruction(fs,&e)) == fs->nactvar);
} }

2
lvm.c
View File

@@ -1565,7 +1565,7 @@ void luaV_execute (lua_State *L, CallInfo *ci) {
if (nparams1) /* vararg function? */ if (nparams1) /* vararg function? */
delta = ci->u.l.nextraargs + nparams1; delta = ci->u.l.nextraargs + nparams1;
/* close upvalues from current call */ /* close upvalues from current call */
ProtectNT(luaF_close(L, base, LUA_OK)); luaF_close(L, base, -1); /* (no to-be-closed vars. here) */
updatestack(ci); updatestack(ci);
} }
if (!ttisfunction(s2v(ra))) { /* not a function? */ if (!ttisfunction(s2v(ra))) { /* not a function? */

View File

@@ -1538,9 +1538,6 @@ except that its value is @emph{closed} whenever the variable
goes out of scope, including normal block termination, goes out of scope, including normal block termination,
exiting its block by @Rw{break}/@Rw{goto}/@Rw{return}, exiting its block by @Rw{break}/@Rw{goto}/@Rw{return},
or exiting by an error. or exiting by an error.
If a block ends in a tail call @see{functioncall},
all variables of the caller function go out of scope
before the start of the callee function.
To \emph{close} a value has the following meaning here: To \emph{close} a value has the following meaning here:
If the value of the variable when it goes out of scope is a function, If the value of the variable when it goes out of scope is a function,
@@ -2038,8 +2035,8 @@ A call of the form @T{f'@rep{string}'}
is syntactic sugar for @T{f('@rep{string}')}; is syntactic sugar for @T{f('@rep{string}')};
that is, the argument list is a single literal string. that is, the argument list is a single literal string.
A call of the form @T{return @rep{functioncall}} is called A call of the form @T{return @rep{functioncall}} not in the
a @def{tail call}. scope of a to-be-closed variable is called a @def{tail call}.
Lua implements @def{proper tail calls} Lua implements @def{proper tail calls}
(or @emph{proper tail recursion}): (or @emph{proper tail recursion}):
in a tail call, in a tail call,
@@ -2049,13 +2046,15 @@ a program can execute.
However, a tail call erases any debug information about the However, a tail call erases any debug information about the
calling function. calling function.
Note that a tail call only happens with a particular syntax, Note that a tail call only happens with a particular syntax,
where the @Rw{return} has one single function call as argument; where the @Rw{return} has one single function call as argument,
this syntax makes the calling function return exactly and it is outside the scope of any to-be-closed variable.
the returns of the called function. This syntax makes the calling function return exactly
the returns of the called function,
without any intervening action.
So, none of the following examples are tail calls: So, none of the following examples are tail calls:
@verbatim{ @verbatim{
return (f(x)) -- results adjusted to 1 return (f(x)) -- results adjusted to 1
return 2 * f(x) return 2 * f(x) -- result multiplied by 2
return x, f(x) -- additional results return x, f(x) -- additional results
f(x); return -- results discarded f(x); return -- results discarded
return x or f(x) -- results adjusted to 1 return x or f(x) -- results adjusted to 1

View File

@@ -225,21 +225,24 @@ end
do do
-- to-be-closed variables must be closed in tail calls -- calls cannot be tail in the scope of to-be-closed variables
local X, Y local X, Y
local function foo () local function foo ()
local *toclose _ = function () Y = 10 end local *toclose _ = function () Y = 10 end
assert(X == 20 and Y == nil) assert(X == true and Y == nil) -- 'X' not closed yet
return 1,2,3 return 1,2,3
end end
local function bar () local function bar ()
local *toclose _ = function () X = 20 end local *toclose _ = function () X = false end
return foo() X = true
do
return foo() -- not a tail call!
end
end end
local a, b, c, d = bar() local a, b, c, d = bar()
assert(a == 1 and b == 2 and c == 3 and X == 20 and Y == 10 and d == nil) assert(a == 1 and b == 2 and c == 3 and X == false and Y == 10 and d == nil)
end end