[Date Prev][Date Next][Thread Prev][Thread Next]
[Date Index]
[Thread Index]
- Subject: The "Environment Stack" (+ lexically scoped string metatables)
- From: "Soni \"They/Them\" L." <fakedme@...>
- Date: Wed, 29 Nov 2017 10:48:52 -0200
Hey Lua team!
I had this idea some years ago, I call it the "environment stack". It's
perfectly compatible with existing Lua syntax, requiring only changes to
the code generator, VM and libraries, and eliminates one of the pain
points of Lua: metatable handling. (I've been told you plan on changing
metatable handling, so I thought I'd pitch in)
Current Lua uses an actual register/upvalue for _ENV, and uses
register/upvalue lookup to find _ENV. Thus, code of the form:
print(getmetatable(""))
First transforms into:
_ENV.print(_ENV.getmetatable(""))
Which then transforms into:
<register or upvalue for _ENV>.print(<register or upvalue for
_ENV>.getmetatable(""))
I am not proposing we change the first transformation. Instead, I am
proposing we change the second transformation.
What the "environment stack" does is, it provides a stack of environments.
With the 6 opcodes: GETTABST, SETTABST, PUSHST, POPST, GETST, SETST, we
can build an "environment stack". From the given Lua code:
do
local _ENV = {print=print}
print()
end
We emit the following opcodes, with added comments detailing code
generator behaviour, as well as explaining how the opcodes operate:
NEWTABLE 0 0 1 ; creates table at reg 0
GETTABST 1 0 -1 ; gets key "print" from upvalue 0, puts it at reg 1
SETTABLE 0 -1 1 ; sets key "print" at reg 0 to value at reg 1
; (frees reg 1)
PUSHST 0 0 ; pushes table at reg 0 onto "environment stack" slot 0
; (frees reg 0)
GETTABST 0 0 -1 ; gets key "print" from "environment stack" slot 0, puts
it at reg 0
CALL 0 1 1 ; calls function at reg 0
; (frees reg 0)
POPST 0 ; pops "environment stack" slot 0 onto reg 0 -- corresponds to
"end" in above example
RETURN 0 1
This can then be easily extended to metatables:
_ENV --> slot 0
_MT_STRING --> slot 1
_MT_NUMBER --> slot 2
and so on
On a simple block of code like the above, this is indeed a simple
push/pop mechanism. However, if we add in some closures:
local f
local g
do
local _ENV = {}
function f() return _ENV end
function g(newenv) _ENV=newenv end
end
g(1)
print(f()) -- prints 1
Up until "local _ENV = {}" it's pretty simple: make a table. push it.
However, when you create the closure, the CLOSURE opcode captures the
current values/slots on the stack(s). This is similar to how upvalues
work, really. As with upvalues, you can set the environment (with _ENV =
whatever) in the closure and it'll change the environment for everything
using the same environment.
A small edge case: you can currently accept _ENV as an argument:
local function f(_ENV)
print()
end
f({print=function() print "hello world" end})
This is a rare edge case, but easily handled: just emit a PUSHST as the
first instruction in a function.
As for repeating it:
local function f(_ENV, _ENV, _ENV) end
Same idea applies, just PUSHST every one (from left to right, please!).
As for metatables and things: every time you use an opcode, it has
access to the current environment stack. It can easily look up which
metatable it needs to use. On the other hand, getmetatable and
setmetatable now strictly only apply to tables and full userdata - all
other metatables are accessed through the reserved names _MT_*.
To sum it up, here are some cases and their handling:
function f() local _ENV = {} return _ENV end -- handled by above rules -
"return _ENV" generates a GETST for _ENV, then "return" itself pops all
relevant PUSHSTs, and then does RETURN.
do local _ENV = {} return _ENV end -- same as above
do local _ENV = {} end -- simple PUSHST followed by POPST
function f(_ENV) end -- has to generate additional PUSHSTs for each _ENV
in the argument list.
do ::test:: local _ENV = {} goto test end -- "goto test" needs to POPST.
perhaps similar to `do ::test:: local f; local function g() return f
end; goto test; end`.
-- (most cases can be compared to closures and upvalues)
And here are some benefits:
- Modules can use things from caller environment using debug library.
(I've seen modules do this with coroutines before. See below.)
- SIGNIFICANTLY simplifies virtualization modules. Previously I've used
coroutines for this: I installed global metatables for all types (using
debug.setmetatable), then used the currently running coroutine to figure
out which environment made the call (with
`metatables[coroutine.running()][currentType]`), then passed it on to
the correct function. This feature would completely eliminate all that code!
- Modules would be able to locally customize string, number, etc
metatables, without affecting the main program. Modules will finally be
able to use `"%s %s" % {"hello", "world!"}`!
- Among other things.
I'm not providing an implementation because I know you don't accept
patches. However, if you do this, please don't make the Lua 5.1 mistake
of implementing this as a call-stack-sensitive standard library function
- if we've learned anything from Lua 5.1 it's that it's unsafe and
dangerous, and makes sandboxing rather difficult.
--
Disclaimer: these emails may be made public at any given time, with or without reason. If you don't agree with this, DO NOT REPLY.