mardi 23 juillet 2019

Introspecting _ENV from coroutines

This question is motivated by exercise 25.1 (p. 264) of Programming in Lua (4th ed.). That exercise reads as follows:

Exercise 25.1: Adapt getvarvalue (Listing 25.1) to work with different coroutines (like the functions from the debug library).

The function getvarvalue that the exercise refers to is copied verbatim below.

-- Listing 25.1 (p. 256) of *Programming in Lua* (4th ed.)

function original_getvarvalue (name, level, isenv)
  local value
  local found = false

  level = (level or 1) + 1

  -- try local variables
  for i = 1, math.huge do
    local n, v = debug.getlocal(level, i)
    if not n then break end
    if n == name then
      value = v
      found = true
    end
  end
  if found then return "local", value end

  -- try non-local variables
  local func = debug.getinfo(level, "f").func
  for i = 1, math.huge do
    local n, v = debug.getupvalue(func, i)
    if not n then break end
    if n == name then return "upvalue", v end
  end

  if isenv then return "noenv" end   -- avoid loop

  -- not found; get value from the environment
  local _, env = getvarvalue("_ENV", level, true)
  if env then
    return "global", env[name]
  else        -- no _ENV available
    return "noenv"
  end

end

Below is my enhanced version of this function, which implements the additional functionality specified in the exercise. This version accepts an optional thread parameter, expected to be a coroutine. The only differences between this enhanced version and the original are:

  1. the handling of the additional optional parameter;
  2. the special setting of the level depending on whether the thread parameter is the same as the running coroutine or not; and
  3. the passing of the thread argument in the calls to debug.getlocal and debug.getinfo, and in the recursive call.

(I have marked these differences in the source code through numbered comments.)

function enhanced_getvarvalue (thread, name, level, isenv)
  -- 1
  if type(thread) ~= "thread" then
    -- (thread, name,  level, isenv)
    -- (name,   level, isenv)
    isenv = level
    level = name
    name = thread
    thread = coroutine.running()
  end

  local value
  local found = false

  -- 2
  level = level or 1
  if thread == coroutine.running() then
    level = level + 1
  end

  -- try local variables
  for i = 1, math.huge do
    local n, v = debug.getlocal(thread, level, i) -- 3
    if not n then break end
    if n == name then
      value = v
      found = true
    end
  end
  if found then return "local", value end

  -- try non-local variables
  local func = debug.getinfo(thread, level, "f").func  -- 3
  for i = 1, math.huge do
    local n, v = debug.getupvalue(func, i)
    if not n then break end
    if n == name then return "upvalue", v end
  end

  if isenv then return "noenv" end   -- avoid loop

  -- not found; get value from the environment
  local _, env = enhanced_getvarvalue(thread, "_ENV", level, true)  -- 3
  if env then
    return "global", env[name]
  else
    return "noenv"
  end

end

This function works reasonably well, but I have found one strange situation1 where it fails. The function make_nasty below generates a coroutine for which getvarvalue_enhanced fails to find an _ENV variable; i.e. it returns "noenv". (The function that serves as the basis for nasty is the closure outer_closure, which in turn invokes the closure inner_closure. It is inner_closure that then yields.)

function make_nasty ()
  local function inner_closure () coroutine.yield() end
  local function outer_closure ()
    inner_closure()
  end

  local thread = coroutine.create(outer_closure)
  coroutine.resume(thread)
  return thread
end

nasty = make_nasty()
print(enhanced_getvarvalue(nasty, "_ENV", 2))
-- noenv

In contrast, the almost identical function make_nice produces a coroutine for which getvarvalue_enhanced succeeds in finding an _ENV variable.

function make_nice ()
  local function inner_closure () coroutine.yield() end
  local function outer_closure ()
    local _ = one_very_much_non_existent_global_variable  -- only difference!
    inner_closure()
  end

  local thread = coroutine.create(outer_closure)
  coroutine.resume(thread)
  return thread
end

nice = make_nice()
print(enhanced_getvarvalue(nice, "_ENV", 2))
-- upvalue  table: 0x558a2633c930

The only difference between make_nasty and make_nice is that, in the latter, the closure outer_closure references a non-existent global variable (and does nothing with it).

Q: How can I modify getvarvalue_enhanced so that it is able to locate _ENV for nasty, the way it does for nice?


1 I am sure that someone with a better understanding of what is going on in this example will be able to come up with a less convoluted way to elicit the same behavior. The example I present here is the most minimal form I can come up with for the situation I found by accident.





Aucun commentaire:

Enregistrer un commentaire