geekhunger
geekhunger

Reputation: 550

change/update value of a local variable (Lua upvalue)

I've written a script to hot-reload already requireed modules. It work s only partially however...

My approach to this task is quite simple. I changed Lua's require function so that it remembers modules that it loaded together with a timestamp and its file path. Then I use a shell script to observe the modification time of those files and re-require them if they changed. I simply dofile() and if no errors happen, I take the return value and (re-)assign it at package.loaded[<module>]. So far so good.

All of this works perfect when I use global variables, e.g. foo = require "foobar", but when I use local assignments, like local foo = require "foobar", my hotswapper failes (partially)!

It seems that the package gets swapped out like intended, however the local variable (from the assignment above) still holds an old reference or the old value that it got when require was called the first time.

My idea was to use Lua's debug.getlocal and debug.setlocal functions to find all local variables (upvalues in stack) and update their values/references.

BUT I get an error that the upvalue I want to change is "out of range"... Could somebody help me please? What should I do or how could I work around this?

The complete code is over at Gist, the important/relevant snippets however are...

  1. the function local_upvalues() at line 27, which collects all available upvalues
local function local_upvalues()
    local upvalues = {}
    local failures = 0
    local thread = 0
    while true do
        thread = thread + 1
        local index = 0
        while true do
            index = index + 1
            local success, name, value = pcall(debug.getlocal, thread, index)
            if success and name ~= nil then
                table.insert(upvalues, {
                    name = name,
                    value = value,
                    thread = thread,
                    index = index
                })
            else
                if index == 1 then failures = failures + 1 end
                break
            end
        end
        if failures > 1 then break end
    end
    return upvalues
end
  1. and the debug.setlocal() at line 89, which tries to update the upvalue that holds the absolete module reference
        -- update module references of local upvalues
        for count, upvalue in ipairs(local_upvalues()) do
            if upvalue.value == package.loaded[resource] then
                -- print(upvalue.name, "updated from", upvalue.value, "to", message)
                table.foreach(debug.getinfo(1), print)
                print(upvalue.name, upvalue.thread, upvalue.index)
                debug.setlocal(upvalue.thread, upvalue.index, message)
            end
        end
        package.loaded[resource] = message -- update the absolete module

Upvotes: 3

Views: 5635

Answers (2)

geekhunger
geekhunger

Reputation: 550

I accepted @Nifim answer. However that will only work for tables as far as I can tell. But require can also return any type of value. - Nevertheless, it's a nice solution that can work with some tweaking...

However, just for reference - I got my approach working as well! First, I removed the wrapping pcall() from debug.getlocal() because this introduced another stack level and thus returned wrong thread and index values that didn't work with debug.setlocal(). Finally, I moved the debug.setlocal call into the same function (=same scope) so that I check and re-assign in one step!

See my rereference(absolete, new) function code below.

local thread = 1
while debug.getinfo(thread) ~= nil do
    local index, name, value = 0, nil, nil
    repeat
        index = index + 1
        name, value = debug.getlocal(thread, index)
        if name ~= nil
        and name ~= "absolete"
        and name ~= "new"
        then
            if value == absolete then
                if debug.setlocal(thread, index, new) == name then
                    print(string.format(
                        "%s local upvalue '%s' has been re-referenced",
                        os.date("%d.%m.%Y %H:%M:%S"),
                        name
                    ))
                end
            end
        end
    until name == nil
    thread = thread + 1
end

Upvotes: 0

Nifim
Nifim

Reputation: 5031

You can use a metatable with __index. Rather then returning package.loaded[resource] or _require(resource) return:

_require(resource)
return setmetatable({}, --create a dummy table
  {
    __index = function(_, k)
      return package.loaded[resource][k] -- pass all index requests to the real resource.
    end
  })

And

package.loaded[resource] = message -- update the absolete module

print(string.format("%s %s hot-swap of module '%s'",
    os.date("%d.%m.%Y %H:%M:%S"),
    stateless and "stateless" or "stateful",
    hotswap.registry[resource].url
  ))
return setmetatable({}, 
  {
    __index = function(_, k)
      return package.loaded[resource][k]
    end
  })

Doing this you shouldn't need to look up the upvalues at all, as this will force any local require results to always reference the up-to-date resource.

There are likely cases where this will not work well, or otherwise break a module, but with some tweaking it can.

Upvotes: 0

Related Questions