2

I learned how to get the function name inside a function by using debug.getinfo(1, "n").name.

Using this feature, I found out the strange behavior in Lua.

Here's my code:

function myFunc()
  local name = debug.getinfo(1, "n").name
  return name
end

function foo()
  return myFunc()
end

function boo()
  local name = myFunc()
  return name
end

print(foo())
print(boo())

Result:

nil
myFunc

As you can see, the function foo() and boo() calls the same function myFunc() but they return different results.

If I replace debug.getinfo(1, "n").name with other string, they return the same results as expected but I don't understand the unexpected behavior caused by using the debug.getinfo().

Is it possible to correct myFunc() function so calling both foo() and boo() functions return the same result?

Expected result:

myFunc
myFunc
Zack Lee
  • 2,784
  • 6
  • 35
  • 77

3 Answers3

2

In Lua, any return statement of the form return <expression_yielding_a_function>(...) is a "tail call". Tail calls essentially don't exist in the call stack, so they take up no additional space or resources. The function you call effectively gets erased from the debug information.

Is it possible to correct myFunc() function so calling both foo() and boo() functions return the same result?

Um... yes, but before I tell you how, allow me to try to convince you not to do this.

As previously mentioned, tail calls are part of the Lua language. The removal of tail calls from the stack is not an "optimization" any more than it is an "optimization" for a for loop to exit when you use break. It is a part of Lua's grammar, and Lua programmers have just as much a right to expect a tail call to be a tail call as they have the right to expect break to exit loops.

Lua, as a language, specifically states that this:

local function recursive(...)
  --some terminating condition

  return recursive(modified_args)
end

will never, ever, run out of stack space. It will be just as stack space efficient as performing a loop. This is a part of the Lua language, just as much a part of it as the behavior of for and while.

If a user wants to call your function via a tail call, that is their right as the user of a language that makes tail calls a thing. Denying users of a language the right to use the features of that language is rude.

So don't do it.

Furthermore, your code suggests that you are attempting to rely on functions having names. That you're doing something significant and meaningful with those names.

Well, Lua is not Python; Lua functions do not have to have names, period. As such, you should not write code that meaningfully relies upon the name of a function. For debugging or logging purposes, fine. But you should not break user expectations just for debugging and logging. So if the user made a tail call, just accept that's what the user wanted and that your debugging/logging will suffer slightly.

OK, so, do we agree that you shouldn't do this? That Lua users have the right to tail calls, and you don't have the right to deny them? That Lua functions are not named and you shouldn't write code that requires them to maintain a name? OK?


What follows is terrible code that you should never use! (in Lua 5.3):

function bypass_tail_call(Func)
    local function tail_call_bypass(...)
        local rets = table.pack(Func(...))
        return table.unpack(rets, rets.n)
    end
    return tail_call_bypass
end

Then, simply replace your real function with the return of the bypass:

function myFunc()
  local name = debug.getinfo(1, "n").name
  return name
end

myFunc = bypass_tail_call(myFunc)

Note that the bypass function has to build an array to hold the return values, then unpack them into the final return statement. This obviously requires additional memory allocations that don't have to happen in regular code.

So there's another reason not to do this.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
1

This is a result of tail call optimisation, which Lua does.

In this case, Lua translates the function call into a "goto" statement, and does not use any extra stack frame to perform the tail call.

You can add traceback statement to check it:

    function myFunc()
      local name = debug.getinfo(1, "n").name
      print(debug.traceback("Stack trace"))
      return name
    end

Tail call optimisation happens in Lua when you return with a function call:

-- Optimized
function good1()
    return test()
end

-- Optimized
function good2()
    return test(foo(), bar(5 + baz()))
end

-- Not optimised
function bad1()
    return test() + 1
end

-- Not optimised
function bad2()
    return test()[2] + foo()
end

You can refer to the following links for more information: - Programming in Lua - 6.3: Proper Tail Calls - What is tail call optimisation? - Stack Overflow

DarkWiiPlayer
  • 6,871
  • 3
  • 23
  • 38
binhgreat
  • 982
  • 8
  • 13
  • So is it not possible to just correct `myFunc()` so ` boo()` and `foo()` can have the same result? – Zack Lee Jul 09 '19 at 10:39
  • 2
    @ZackLee No. Well, technically speaking they already do. The functions in the `debug` library are special; they give you insight into things you normally shouldn't care about. Your program should never base its actual behavior on anything in the debug library, precisely because it brings things to light that Lua normally hides from you for good reason. – DarkWiiPlayer Jul 09 '19 at 15:00
1

You can run your code through luac -l -p

...

function <stdin:6,8> (4 instructions at 0x555f561592a0)
0 params, 2 slots, 1 upvalue, 0 locals, 1 constant, 0 functions
  1 [7] GETTABUP    0 0 -1  ; _ENV "myFunc"
  2 [7] TAILCALL    0 1 0
  3 [7] RETURN      0 0
  4 [8] RETURN      0 1

function <stdin:10,13> (4 instructions at 0x555f561593b0)
0 params, 2 slots, 1 upvalue, 1 local, 1 constant, 0 functions
  1 [11]    GETTABUP    0 0 -1  ; _ENV "myFunc"
  2 [11]    CALL        0 1 2
  3 [12]    RETURN      0 2
  4 [13]    RETURN      0 1

Those are the two function that are of interest to us: foo and boo

As you can see, when boo calls myFunc, it's just a normal CALL, so nothing interesting there.

foo, however, does something called a tail call. That is, the return value of foo is the return value of myFunc.

What makes this kind of call special is that there is no need for the program to jump back into foo; once foo calls myFunc it can just hand over the keys and say "You know what to do"; myFunc then returns its results directly to where foo was called. This has two advantages:

  • The stack frame of foo can be cleaned up before myFunc is called
  • once myFunc returns, it doesn't need two jumps to return to the main thread; only one

Both of those are insignificant in examples like yours, but once you have a chain of lots and lots of tail calls, it becomes significant.

The downside of this is that, once the stack of foo gets cleaned up, Lua also forgets all the debugging information associated with it; it only remembers that myFunc was called as a tail call, but not from where.


An interesting side note, is that boo is almost also a tail call. If Lua didn't have multiple return values, it'd be exactly identical to foo, and a smarter compiler like LuaJIT might compile it to a tail call. PUC Lua won't though, since it needs a literal return some_function() to recognize the tail call.

The difference is that boo only returns the first value returned by myFunc, and while in your example, there will only ever be one, the interpreter can't make that assumption (LuaJIT might make that assumption during JIT compilation, but that's beyond my understanding)


Also note that, technically, the word tail call just describes a function A directly returning the return value of another function B.

It often gets used interchangeably with tail call optimization, which is what the compiler does when it re-uses the stack frame and turns the function call into a jump.

Strictly speaking, C (for example) has tail calls, but it has no tail call optimization, meaning something like

int recursive(n) { return recursive(n+1); }

is valid C code, but will eventually cause a stack overflow, while in Lua

local function recursive(n) return recursive(n+1) end

will just run forever. Both are tail calls, but only the second gets optimized.


EDIT: As always with C, some compilers may, on their own, implement tail call optimization, so don't go around telling everyone that "C never ever does it"; it's just not a requried part of the language, while in Lua it's actually defined in the language specification, so it's not Lua until it has TCO.

DarkWiiPlayer
  • 6,871
  • 3
  • 23
  • 38