4

I saw here how to insert local variables in a table using the debug.getlocal function in Lua (5.1).

function locals()
   local variables = {}
   local idx = 1
   while true do
     local ln, lv = debug.getlocal(2, idx)
     if ln ~= nil then
       variables[ln] = lv
     else
       break
     end
     idx = 1 + idx
   end
   return variables
 end

However, when I try to return the created table and access it's entries, it doesn't work.

function test1()
    local v = 'I am a local!'
    return locals()
end

print(test1().v) -- nil

After some trail and error, I noticed that binding the table to a variable before returning, or simply adding parentheses fixes the behavior:

function test2()
    local v = 'I am a local!'
    return (locals())
end

print(test2().v) -- 'I am a local!'

This is very confusing me. Why are these two programs in any way different? What am I not understanding? Does the fact that locals() is in a tail call position make any difference?

Community
  • 1
  • 1
yawn
  • 422
  • 1
  • 5
  • 21

1 Answers1

4

I guess what confuses you is the proper tail call feature of lua.

To understand this, we modify your locals function, making it accept one argument as the level stack used in call to debug.getlocal. (I'm using Lua 5.3.3)

-- Get local variables with stack level 'level'.
function locals(level)
    local variables = {}
    local idx = 1
    while true do
        local ln, lv = debug.getlocal(level, idx)
        if ln ~= nil then
            variables[ln] = lv
        else
            break
        end
        idx = 1 + idx
    end
    return variables
end

Then we modify your test functions, adding the same argument, and add a test3 function for reference.

function test1(level)
    local v = 'I am a local!'
    return locals(level)
end

function test2(level)
    local v = 'I am a local!'
    return (locals(level))
end

function test3(level)
    local v = 'I am a local!'
    local a = locals(level)
    return a
end

Finally we add some code to run the tests.

local function printTable(t)
    -- print(t)
    for k, v in pairs(t) do
        print(string.format("key = %s, val = %s.", k, v))
    end
end

for level = 1, 3 do
    print("==== Stack level: " .. tostring(level))
    for num = 1, 3 do
        print(string.format("What test%d returns: ", num))
        printTable(_G[(string.format("test%d", num))](level))
        print("")
    end
end

The code above runs the test functions with different stack level and print the key-value pairs returned. My result is as follows:

==== Stack level: 1
What test1 returns: 
key = variables, val = table: 0x7fa14bc081e0.
key = idx, val = 3.
key = level, val = 1.

What test2 returns: 
key = variables, val = table: 0x7fa14bc08220.
key = idx, val = 3.
key = level, val = 1.

What test3 returns: 
key = variables, val = table: 0x7fa14bc088b0.
key = idx, val = 3.
key = level, val = 1.

==== Stack level: 2
What test1 returns: 
key = (for step), val = 1.
key = (for limit), val = 3.
key = (for index), val = 1.
key = level, val = 2.
key = printTable, val = function: 0x7fa14bc08360.
key = (*temporary), val = function: 0x7fa14bc08360.
key = num, val = 1.

What test2 returns: 
key = level, val = 2.
key = v, val = I am a local!.

What test3 returns: 
key = level, val = 2.
key = v, val = I am a local!.

==== Stack level: 3
What test1 returns: 
key = (*temporary), val = function: 0x109f5a070.

What test2 returns: 
key = (for step), val = 1.
key = (for limit), val = 3.
key = (for index), val = 2.
key = level, val = 3.
key = printTable, val = function: 0x7fa14bc08360.
key = (*temporary), val = function: 0x7fa14bc08360.
key = num, val = 2.

What test3 returns: 
key = (for step), val = 1.
key = (for limit), val = 3.
key = (for index), val = 3.
key = level, val = 3.
key = printTable, val = function: 0x7fa14bc08360.
key = (*temporary), val = function: 0x7fa14bc08360.
key = num, val = 3.

When level is 1, locals works well to give its own local variables. But when level is 2, test1 returns variables of the outer scope, whereas test2 and test3 give the result you expect. For stack level 3 test2 and test3 return something like test1 at stack level 2. So it seems test1 skips a stack level, and the only explanation I could think of is the proper tail call.

According to PIL (the link I provide at the beginning), a proper tail call will never cause the stack to overflow, which I take as doing the call in some inline way. If I'm right with that, this explains the skipping behaviour of test1's return statement, because that's a proper tail call, and the only one in the 3 test functions.

  • I love your methodology! Thank you so much. – yawn Aug 07 '16 at 17:50
  • 1
    I'm not sure how to post code in comments, but it indeed seems to about be tail calls, as wrapping locals(2) with an identity function works, which, assuming it works like in Scheme, should be enough to disable the tail call optimization. I guess that since parentheses do some form of computation in Lua (return only the first of multiple values), then they must have been enough to disable the TCO – yawn Aug 07 '16 at 17:50