The way it's done in the reference implementation, and in many other implementations of similar languages: At compile time,
- Figure out at compile time which name refers to which scope.
- For each scope (except global), enumerate the variables and assign an index to each.
- Instead of mentioning the variable names in the bytecode, use the indices from the previous step.
Then during execution,
- Store the values of the locals in an array (per activation record). Read and write opcodes carry the necessary index.
- Implement globals like a Lua table (and get
nil
for missing variables for free).
This is much simpler, easier, and portable than mucking around with dynamically created types. It may even be faster, but certainly won't be much slower.
Closure support is mostly orthogonal. So-called upvalues refer to (a slot in) the locals array while it's still alive, and adopt the closed-over value when the locals array dies. You could do something similar with your original design. For correctness, you must take care to only create one upvalue per closed-over variable and to flatten closures. As an example, g
in the below code must also close over x
so that it still exists by the time the h
closure is created:
function f()
local x = 1
function g()
function h()
return x
end
return h
end
return g
end
print(f()()())