Reputation: 303178
This question is similar to Modify Lua Chunk Environment, but with a twist.
I want to load strings as reusable Lua functions, where they all share the same sandbox for evaluation, but where each invocation I can pass in different values for the variables.
For example, say I want to evaluate the equation round(10*w/h) + a * b
, with round
, w
, and h
all values in a common sandbox environment, but where a
and b
are values that I want to modify each time I evaluate the equation.
This question shows how to dynamically change the environment for a compiled function. However, it does so setting the ENTIRE environment for the function, without the fallback sandbox that I have.
What's an efficient way to achieve my goals? Note that I almost only care about time needed to evaluate the function, not the setup time.
The way I have things currently, users write CSS expressions like:
box {
left: @x;
width: viewwidth - @w;
}
...where @x
and @w
are attributes of the box element (the 'local' variables) and viewwidth
is a sheet-level variable set up elsewhere (my sandbox). Each of the property values—the portion after the :
—is parsed out as a string to be compiled into a Lua function. They use normal Lua syntax, except where I currently swap the @
with _el.
in order to dereference the element table.
For answers to this question it is acceptable to keep this same syntax and require a differentiation between the local and sheet variables, BUT it is also acceptable to have a solution the gets rid of the @
symbols and treats all variables the same.
Upvotes: 3
Views: 1212
Reputation: 303178
I came up with 6 techniques for accomplishing the goal. The bottom of this post benchmarks their performance for evaluation speed. The fastest techniques require differentiating the local variables from sandbox variables in code:
local SANDBOX = {
w=1920,
h=1080,
round=function(n) return math.floor(n+0.5) end
}
local CODE = "round(10*w/h) + @a * @b"
function compile(code, sandbox)
local munged = 'local _el=... return '..code:gsub('@', '_el.')
return load(munged, nil, nil, sandbox)
end
local f = compile(CODE, SANDBOX)
print(f{a=1, b=2}) --> 20
print(f{a=3, b=4}) --> 30
If you don't want to differentiate the changing variables from those in the sandbox, or don't want to use a fragile sub()
like the above, the next fastest is to mutate the __index
of your locals and then pass it as the environment. You can wrap this in a helper function to make it easier:
local CODE = "round(10*w/h) + a * b"
function compile(code, sandbox)
local meta = {__index=sandbox}
return {meta=meta, f=load('_ENV=... return '..code)}
end
function eval(block, locals)
return block.f(setmetatable(locals, block.meta))
end
local f = compile(CODE, SANDBOX)
print(eval(f, {a=1, b=2})) --> 20
print(eval(f, {a=3, b=4})) --> 30
Here is the benchmark results of all the techniques below. Note that the fastest one can be even faster because unlike all other techniques the compile function returns a function that can be invoked directly, instead of wrapped in an evaluation helper script:
scope as a table, 2 : 0.9s (0.22µs per eval)
scope as a table : 1.1s (0.27µs per eval)
Use __ENV, change scope meta : 1.3s (0.32µs per eval)
blank env, change meta of scope : 1.6s (0.41µs per eval)
copy values over environment : 2.8s (0.70µs per eval)
setfenv, change scope meta : 3.0s (0.74µs per eval)
local SANDBOX = {
w = 1920,
h = 1080,
round = function(n) return math.floor(n+0.5) end
}
local TESTS = {
{env={a=1, b=2}, expected=18+2},
{env={a=4, b=3}, expected=18+12},
{env={a=9, b=7}, expected=18+63},
{env={a=4, b=5}, expected=18+20},
}
-- https://leafo.net/guides/setfenv-in-lua52-and-above.html
local function setfenv(fn, env)
local i = 1
while true do
local name = debug.getupvalue(fn, i)
if name == "_ENV" then
debug.upvaluejoin(fn, i, (function() return env end), 1)
break
elseif not name then
break
end
i = i + 1
end
return fn
end
local techniques = {
["copy values over environment"]={
code="round(10*w/h) + a*b",
setup=function(code, fallback)
local env = setmetatable({},{__index=fallback})
return {env=env,func=load("return "..code,nil,nil,env)}
end,
call=function(block, kvs)
for k,v in pairs(block.env) do block.env[k]=nil end
for k,v in pairs(kvs) do block.env[k]=v end
return block.func()
end
},
["blank env, change meta of scope"]={
code="round(10*w/h) + a*b",
setup=function(code, fallback)
local kvsmeta = {__index=fallback}
local envmeta = {}
local env = setmetatable({},envmeta)
return {envmeta=envmeta,meta=meta,kvsmeta=kvsmeta,func=load("return "..code,nil,nil,env)}
end,
call=function(block, kvs)
block.envmeta.__index=kvs
setmetatable(kvs, block.kvsmeta)
return block.func()
end
},
["setfenv, change scope meta"]={
code="round(10*w/h) + a*b",
setup=function(code, fallback)
return {meta={__index=fallback}, func=load("return "..code)}
end,
call=function(block, kvs)
setmetatable(kvs,block.meta)
setfenv(block.func, kvs)
return block.func()
end
},
["Use __ENV, change scope meta"]={
code="round(10*w/h) + a*b",
setup=function(code, fallback)
local meta = {__index=fallback}
return {meta=meta, func=load("_ENV=... return "..code)}
end,
call=function(block, kvs)
return block.func(setmetatable(kvs, block.meta))
end
},
["scope as a table"]={
-- NOTE: requires different code than all other techniques!
code="round(10*w/h) + _el.a * _el.b",
setup=function(code, fallback)
local env = setmetatable({},{__index=fallback})
return {env=env,func=load("return "..code,nil,nil,env)}
end,
call=function(block, kvs)
block.env._el=kvs
return block.func()
end
},
["scope as a table, 2"]={
-- NOTE: requires different code than all other techniques!
code="round(10*w/h) + _el.a * _el.b",
setup=function(code, fallback)
return load("local _el=... return "..code,nil,nil,fallback)
end,
call=function(func, kvs)
return func(kvs)
end
},
}
function validate()
for name,technique in pairs(techniques) do
local f = technique.setup(technique.code, SANDBOX)
for i,test in ipairs(TESTS) do
local actual = technique.call(f, test.env)
if actual~=test.expected then
local err = ("%s test #%d expected %d but got %s\n"):format(name, i, test.expected, tostring(actual))
io.stderr:write(err)
error(-1)
end
end
end
end
local N = 1e6
function benchmark(setup, call)
for name,technique in pairs(techniques) do
local f = technique.setup(technique.code, SANDBOX)
local start = os.clock()
for i=1,N do
for i,test in ipairs(TESTS) do
technique.call(f, test.env)
end
end
local elapsed = os.clock() - start
print(("%-33s: %.1fs (%.2fµs per eval)"):format(name, elapsed, 1e6*elapsed/#TESTS/N))
end
end
validate()
benchmark()
Upvotes: 1