diff --git a/anselme/ast/Environment.lua b/anselme/ast/Environment.lua index 1b32d20..fbd55cc 100644 --- a/anselme/ast/Environment.lua +++ b/anselme/ast/Environment.lua @@ -85,6 +85,14 @@ local Environment = ast.abstract.Runtime { self._lookup_cache = {} self._lookup_cache_current = {} end, + -- precache variable + -- when cached, if a variable is defined in a parent scope after it has been cached here from a higher parent, it will not be used in this env + -- most of the time scopes are discarded after a pop so there's no possibility for this anyway, except for closures as they restore an old environment + -- in which case we may want to precache variables that appear in the function so future definitions don't affect the closure + precache = function(self, state, identifier) + self:_lookup(state, identifier) + self:_lookup_in_current(state, identifier:to_symbol()) + end, traverse = function(self, fn, ...) if self.parent then diff --git a/anselme/ast/Function.lua b/anselme/ast/Function.lua index e69d4d2..7f6fa3e 100644 --- a/anselme/ast/Function.lua +++ b/anselme/ast/Function.lua @@ -8,6 +8,15 @@ local operator_priority = require("anselme.common").operator_priority local resume_manager, calling_environment_manager +local function list_upvalues(v, l) + if ast.Identifier:is(v) then + table.insert(l, v) + elseif ast.Symbol:is(v) then + table.insert(l, v:to_identifier()) + end + v:traverse(list_upvalues, l) +end + local Function Function = Overloadable { type = "function", @@ -15,11 +24,13 @@ Function = Overloadable { parameters = nil, -- ParameterTuple expression = nil, -- function content scope = nil, -- Environment; captured scope for closure (evaluated functions); not set when not evaluated + upvalues = nil, -- list of identifiers; not set when not evaluated. Contain _at least_ all the upvalues explicitely defined in the function code. - init = function(self, parameters, expression, scope) + init = function(self, parameters, expression, scope, upvalues) self.parameters = parameters self.expression = expression self.scope = scope + self.upvalues = upvalues end, with_return_boundary = function(self, parameters, expression) return Function:new(parameters, ReturnBoundary:new(expression)) @@ -51,7 +62,19 @@ Function = Overloadable { local scope = state.scope:capture() -- capture current scope to build closure state.scope:pop() - return Function:new(self.parameters:eval(state), self.expression, scope) + -- get upvalues + local upvalues = {} + self.expression:traverse(list_upvalues, upvalues) + if scope:defined(state, ast.Identifier:new("_")) then + scope:get(state, ast.Identifier:new("_")):traverse(list_upvalues) + end + + -- cache upvalues so they aren't affected by future redefinition in a parent scope + for _, ident in ipairs(upvalues) do + scope:precache(state, ident) + end + + return Function:new(self.parameters:eval(state), self.expression, scope, upvalues) end, compatible_with_arguments = function(self, state, args)