mirror of
https://github.com/Reuh/anselme.git
synced 2025-10-27 16:49:31 +00:00
Woke up and felt like changing a couple things. It's actually been worked on for a while, little at a time... The goal was to make the language and implementation much simpler. Well I don't know if it really ended up being simpler but it sure is more robust. Main changes: * proper first class functions and closures supports! proper scoping rules! no more namespace shenanigans! * everything is an expression, no more statements! make the implementation both simpler and more complex, but it's much more consistent now! the syntax has massively changed as a result though. * much more organized and easy to modify codebase: one file for each AST node, no more random fields or behavior set by some random node exceptionally, everything should now follow the same API defined in ast.abstract.Node Every foundational feature should be implemented right now. The vast majority of things that were possible in v2 are possible now; some things aren't, but that's usually because v2 is a bit more sane. The main missing things before a proper release are tests and documentation. There's a few other things that might be implemented later, see the ideas.md file.
159 lines
5.9 KiB
Lua
159 lines
5.9 KiB
Lua
-- The current scope stack. One scope stack per State branch.
|
|
-- Only the scope currently on top of the stack will be used by the running script.
|
|
|
|
local class = require("class")
|
|
local ast = require("ast")
|
|
local to_anselme = require("common.to_anselme")
|
|
|
|
local LuaFunction, Environment, Node
|
|
|
|
local parameter_tuple = require("parser.expression.contextual.parameter_tuple")
|
|
local symbol = require("parser.expression.primary.symbol")
|
|
|
|
local ScopeStack = class {
|
|
state = nil,
|
|
|
|
stack = nil, -- stack of Environment
|
|
current = nil, -- Environment
|
|
|
|
initial_size = nil, -- number, size of the stack at creation (i.e. the "global" scopes only)
|
|
|
|
init = function(self, state, branch_from_state)
|
|
self.state = state
|
|
self.stack = {}
|
|
if branch_from_state then
|
|
-- load existing environments from branched from state instead of creating new ones
|
|
for _, env in ipairs(branch_from_state.scope.stack) do
|
|
self:push(env)
|
|
end
|
|
else
|
|
self:push_export() -- root scope is the global scope, stuff can be exported there
|
|
self:push() -- for non-exported variables
|
|
end
|
|
self.initial_size = #self.stack
|
|
end,
|
|
|
|
-- store all changed variables back into the main branch
|
|
merge = function(self)
|
|
local cache = {}
|
|
for _, env in ipairs(self.stack) do
|
|
env:merge(self.state, cache)
|
|
end
|
|
end,
|
|
|
|
-- helper to define stuff from lua easily in the current scope
|
|
-- for lua functions: define_lua("name", "(x, y, z=5)", function(x, y, z) ... end), where arguments and return values of the function are automatically converted between anselme and lua values
|
|
-- for other lua values: define_lua("name", value)
|
|
-- for anselme AST: define_lua("name", value)
|
|
-- name can be prefixed with symbol modifiers, for example ":name" for a constant variable
|
|
-- if `raw_mode` is true, no anselme-to/from-lua conversion will be performed in the function
|
|
-- the function will receive the state followed by AST nodes as arguments, and is expected to return an AST node
|
|
define_lua = function(self, name, value, func, raw_mode)
|
|
local source = require("parser.Source"):new()
|
|
local sym = symbol:parse(source, (":%s"):format(name))
|
|
if func then
|
|
local parameters = parameter_tuple:parse(source, value)
|
|
if not raw_mode then
|
|
local original_func = func
|
|
func = function(state, ...)
|
|
local lua_args = {}
|
|
for _, arg in ipairs{...} do
|
|
table.insert(lua_args, arg:to_lua(state))
|
|
end
|
|
return to_anselme(original_func(table.unpack(lua_args)))
|
|
end
|
|
end
|
|
self:define_overloadable(sym, LuaFunction:new(parameters, func):eval(self.state))
|
|
elseif Node:is(value) then
|
|
self:define(sym, value)
|
|
else
|
|
self:define(sym, to_anselme(value))
|
|
end
|
|
end,
|
|
|
|
-- methods that call the associated method from the current scope, see ast.Environment for details
|
|
define = function(self, symbol, exp) self.current:define(self.state, symbol, exp) end,
|
|
define_overloadable = function(self, symbol, exp) return self.current:define_overloadable(self.state, symbol, exp) end,
|
|
defined = function(self, identifier) return self.current:defined(self.state, identifier) end,
|
|
defined_in_current = function(self, symbol) return self.current:defined_in_current(self.state, symbol) end,
|
|
set = function(self, identifier, exp) self.current:set(self.state, identifier, exp) end,
|
|
get = function(self, identifier) return self.current:get(self.state, identifier) end,
|
|
depth = function(self) return self.current:depth() end,
|
|
|
|
-- push new scope
|
|
-- if environment is given, it will be used instead of creating a new children of the current environment
|
|
push = function(self, environment)
|
|
local env
|
|
if environment then
|
|
env = environment
|
|
else
|
|
env = Environment:new(self.state, self.current)
|
|
end
|
|
table.insert(self.stack, env)
|
|
self.current = env
|
|
end,
|
|
-- push a partial layer on the current scope
|
|
-- this is used to shadow or temporarly define specific variable in the current scope
|
|
-- a partial layer is considered to be part of the current scope when looking up and defining variables, and any
|
|
-- other variable will still be defined in the current scope
|
|
-- ... is a list of identifiers
|
|
-- (still use :pop to pop it though)
|
|
push_partial = function(self, ...)
|
|
local is_partial = {}
|
|
for _, id in ipairs{...} do is_partial[id.name] = true end
|
|
local env = Environment:new(self.state, self.current, is_partial)
|
|
self:push(env)
|
|
end,
|
|
-- push an export layer on the current scope
|
|
-- this is where any exported variable defined on this or children scope will end up being defined
|
|
-- note: non-exported variables will not be defined in such a layer, make sure to add a normal layer on top as needed
|
|
-- still need to be :pop'd
|
|
push_export = function(self)
|
|
local env = Environment:new(self.state, self.current, nil, true)
|
|
self:push(env)
|
|
end,
|
|
-- push the global scope on top of the stack
|
|
push_global = function(self)
|
|
self:push(self.stack[self.initial_size])
|
|
end,
|
|
-- pop current scope
|
|
pop = function(self)
|
|
table.remove(self.stack)
|
|
self.current = self.stack[#self.stack]
|
|
assert(self.current, "popped the root scope!")
|
|
end,
|
|
-- get the size of the stack
|
|
size = function(self)
|
|
return #self.stack
|
|
end,
|
|
-- pop all the scopes until only n are left (by default, only keep the global scopes)
|
|
reset = function(self, n)
|
|
n = n or self.initial_size
|
|
while self.stack[n+1] do
|
|
self:pop()
|
|
end
|
|
end,
|
|
|
|
-- return current environment, to use with :push later (mostly for closures)
|
|
-- reminder: scopes are mutable
|
|
capture = function(self)
|
|
return self.current
|
|
end,
|
|
|
|
-- return a table { [symbol] = value } of persistent variables defined on the root scope on this branch
|
|
list_persistent_global = function(self)
|
|
local env = self.stack[1]
|
|
return env:list_persistent(self.state)
|
|
end,
|
|
|
|
_debug_state = function(self, filter)
|
|
filter = filter or ""
|
|
local s = "current branch id: "..self.state.branch_id.."\n"
|
|
return s .. table.concat(self.current:_debug_state(self.state, filter), "\n")
|
|
end
|
|
}
|
|
|
|
package.loaded[...] = ScopeStack
|
|
LuaFunction, Environment, Node = ast.LuaFunction, ast.Environment, ast.abstract.Node
|
|
|
|
return ScopeStack
|