Getting Variables From Values

lua-users home
wiki

If we have a value, can we obtain its variable? For example, can a function return the names of the variables passed to it?

f(x,y) --> 'x', 'y'

Or can Lua support "pass-by-reference" (or maybe even "pass-by-name") calling semantics?

local a = 10; local b = 11
print(a,b) --> 10,11
swap(a,b)
print(a,b) --> 11,10

The answer is not normally. However, with some byte code inspection (using the lbci library [1]) and Lua debug library hackery, it may be possible...

Note first the bytecode in a typical function call:

$ echo -e 'local x = {2,3};\n local y = 4;\n f(x,y,z)' | luac -p -l -
main <stdin:0,0> (11 instructions, 44 bytes at 0x6d0ea8)
0+ params, 6 slots, 0 upvalues, 2 locals, 5 constants, 0 functions
        1       [1]     NEWTABLE        0 2 0
        2       [1]     LOADK           1 -1    ; 2
        3       [1]     LOADK           2 -2    ; 3
        4       [1]     SETLIST         0 2 1   ; 1
        5       [2]     LOADK           1 -3    ; 4
        6       [3]     GETGLOBAL       2 -4    ; f
        7       [3]     MOVE            3 0
        8       [3]     MOVE            4 1
        9       [3]     GETGLOBAL       5 -5    ; z
        10      [3]     CALL            2 4 1
        11      [3]     RETURN          0 1

The instructions prior to the CALL move the variables into place. Here's how we might make use of that:

-- D.Manura, 2009-10. Public domain.

require "bci"

-- Returns list of variables passed to function at given
-- stack level number `level`.  `level` defaults to 1, the calling
-- function, if omitted. If confused, returns nothing.
-- See code for format of variables.
-- WARNING: This code is experimental.  Not intended for production use.
local mt
local function getargvariables(level)
  mt = mt or {__tostring = function(t)
    local s = '{'
    for i=1,#t do
      s = s .. (i==1 and '' or ',') .. tostring(t[i])
    end
    s = s .. '}'
    return s
  end}

  -- Get function info.
  level = (level or 1) + 2
  local f = debug.getinfo(level,'f').func
  if not f then return end -- could be a tail call
  local currentline = debug.getinfo(level,'l').currentline
  local F = inspector.getheader(f)

  -- Get instruction pointer of call.
  -- Unfortunately, we only have the line number from which to infer the
  -- instruction pointer.  So, we can only do this unambiguously when the
  -- call is the only call on its line.  Perhaps bci or debug.getinfo
  -- could be patched to return the exact instruction pointer.
  local count = 0
  local currentip
  for i=1,F.instructions do
    local line, opcode, a, b, c = inspector.getinstruction(f,i)
    if line == currentline and opcode == "CALL" then
      currentip = i
      count = count + 1
    end
  end
  if count ~= 1 then return end -- ambiguous, return nothing

  -- Get CALL opcode data
  local _,_,idxfunc,nparamsp,_ = inspector.getinstruction(f,currentip)

  -- Get arguments.
  local names = {}
  for i=1,nparamsp-1 do
    local _,opcode,a,b,c = inspector.getinstruction(f,currentip-i)
    if opcode == 'MOVE' and a == idxfunc + nparamsp - i then -- local
      local varname,_,_ = inspector.getlocal(f,b+1)
      names[nparamsp - i] = setmetatable({'local', varname, b+1}, mt)
    elseif opcode == 'GETGLOBAL' and b < 0 then -- global
      local varname = inspector.getconstant(f,-b)
      names[nparamsp - i] = setmetatable({'global', varname}, mt)
    else
      return -- other possibilities not currently implemented
    end
  end
  return unpack(names, 1, nparamsp-1)
end

-- Set variable `var` (as returned by `getargvariables`) in context of
-- stack level number `level` to value `value`.
local function setvariable(level, var, value)
  level = (level or 1) + 1
  if var[1] == 'local' then
    local varname, idx = var[2], var[3]
    debug.setlocal(level, idx, value)
  elseif var[1] == 'global' then
    local varname = var[2]
    getfenv(2)[varname] = value
  else
    assert(false)
  end
end

-- TESTS

local function f(a,b)
  print(getargvariables()) --> {local,y,5} {local,z,6} {global,w}
end

do
  local y=1
  local z=2
  f(y,z,w)
end

local function swap(x,y)
  local xvar, yvar = getargvariables()
  assert(xvar and yvar)
  setvariable(2, xvar, y)
  setvariable(2, yvar, x)
end

a = 10
local b = 11
print(a,b) --> 10,11
swap(a,b)
print(a,b) --> 11,10

print 'DONE'

As is, the above is not intended for production use. The above could be generalized further. Please do so if you are so inclined.

--DavidManura

See Also


RecentChanges · preferences
edit · history
Last edited October 9, 2009 5:56 am GMT (diff)