1
0
Fork 0
mirror of https://github.com/Reuh/anselme.git synced 2025-10-28 09:09:31 +00:00

Anselme v2.0.0-alpha rewrite

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.
This commit is contained in:
Étienne Fildadut 2023-12-17 17:15:16 +01:00
parent 2ff494d108
commit fe351b5ca4
484 changed files with 7099 additions and 18084 deletions

207
ast/ArgumentTuple.lua Normal file
View file

@ -0,0 +1,207 @@
local ast = require("ast")
local Identifier, Number
local operator_priority = require("common").operator_priority
local ArgumentTuple
ArgumentTuple = ast.abstract.Node {
type = "argument tuple",
list = nil, -- list of expr
named = nil, -- { [string] = expr, ... }
assignment = nil, -- expr
arity = 0,
init = function(self, ...)
self.list = { ... }
self.named = {}
self.arity = #self.list
end,
insert_positional = function(self, position, val) -- only for construction
local l = {}
for k, v in pairs(self.list) do
if k >= position then l[k+1] = v
else l[k] = v end
end
l[position] = val
self.list = l
self.arity = self.arity + 1
end,
set_positional = function(self, position, val) -- only for construction
assert(not self.list[position])
self.list[position] = val
self.arity = self.arity + 1
end,
set_named = function(self, identifier, val) -- only for construction
local name = identifier.name
assert(not self.named[name])
self.named[name] = val
self.arity = self.arity + 1
end,
set_assignment = function(self, val) -- only for construction
assert(not self.assignment)
self.assignment = val
self.arity = self.arity + 1
self.format_priority = operator_priority["_=_"]
end,
_format = function(self, state, priority, ...)
local l = {}
for _, e in pairs(self.list) do
table.insert(l, e:format(state, operator_priority["_,_"], ...))
end
for n, e in pairs(self.named) do
table.insert(l, n.."="..e:format_right(state, operator_priority["_=_"], ...))
end
local s = ("(%s)"):format(table.concat(l, ", "))
if self.assignment then
s = s .. (" = %s"):format(self.assignment:format_right(state, operator_priority["_=_"], ...))
end
return s
end,
traverse = function(self, fn, ...)
for _, e in pairs(self.list) do
fn(e, ...)
end
for _, e in pairs(self.named) do
fn(e, ...)
end
if self.assignment then
fn(self.assignment, ...)
end
end,
-- need to redefine hash to include a table.sort as pairs() in :traverse is non-deterministic
-- as well as doesn't account for named arguments names
_hash = function(self)
local t = {}
for _, e in pairs(self.list) do
table.insert(t, e:hash())
end
for n, e in pairs(self.named) do
table.insert(t, ("%s=%s"):format(n, e:hash()))
end
if self.assignment then
table.insert(t, self.assignment:hash())
end
table.sort(t)
return ("%s<%s>"):format(self.type, table.concat(t, ";"))
end,
_eval = function(self, state)
local r = ArgumentTuple:new()
for i, e in pairs(self.list) do
r:set_positional(i, e:eval(state))
end
for n, e in pairs(self.named) do
r:set_named(Identifier:new(n), e:eval(state))
end
if self.assignment then
r:set_assignment(self.assignment:eval(state))
end
return r
end,
with_first_argument = function(self, first)
local r = ArgumentTuple:new()
r:set_positional(1, first)
for i, e in pairs(self.list) do
r:set_positional(i+1, e)
end
for n, e in pairs(self.named) do
r:set_named(Identifier:new(n), e)
end
if self.assignment then
r:set_assignment(self.assignment)
end
return r
end,
-- return specificity (>=0), secondary specificity (>=0)
-- return false, failure message
match_parameter_tuple = function(self, state, params)
-- basic arity checks
if self.arity > params.max_arity or self.arity < params.min_arity then
if params.min_arity == params.max_arity then
return false, ("expected %s arguments, received %s"):format(params.min_arity, self.arity)
else
return false, ("expected between %s and %s arguments, received %s"):format(params.min_arity, params.max_arity, self.arity)
end
end
if params.assignment and not self.assignment then
return false, "expected an assignment argument"
end
-- search for parameter -> argument match
local specificity = 0
local used_list = {}
local used_named = {}
local used_assignment = false
for i, param in ipairs(params.list) do
-- search in args
local arg
if self.list[i] then
used_list[i] = true
arg = self.list[i]
elseif self.named[param.identifier.name] then
used_named[param.identifier.name] = true
arg = self.named[param.identifier.name]
elseif i == params.max_arity and params.assignment and self.assignment then
used_assignment = true
arg = self.assignment
elseif param.default then
arg = param.default
end
-- not found
if not arg then return false, ("missing parameter %s"):format(param.identifier:format(state)) end
-- type check
if param.type_check then
local r = param.type_check:call(state, ArgumentTuple:new(arg))
if not r:truthy() then return false, ("type check failure for parameter %s in function %s"):format(param.identifier:format(state), params:format(state)) end
if Number:is(r) then
specificity = specificity + r.number
else
specificity = specificity + 1
end
end
end
-- check for unused arguments
for i in pairs(self.list) do
if not used_list[i] then
return false, ("%sth positional argument is unused"):format(i)
end
end
for n in pairs(self.named) do
if not used_named[n] then
return false, ("named argument %s is unused"):format(n)
end
end
if self.assignment and not used_assignment then
return false, "assignment argument is unused"
end
-- everything is A-ok
return specificity, params.eval_depth
end,
-- assume :match_parameter_tuple was already called and returned true
bind_parameter_tuple = function(self, state, params)
for i, arg in ipairs(params.list) do
if self.list[i] then
state.scope:define(arg.identifier:to_symbol(), self.list[i])
elseif self.named[arg.identifier.name] then
state.scope:define(arg.identifier:to_symbol(), self.named[arg.identifier.name])
elseif i == params.max_arity and params.assignment then
state.scope:define(arg.identifier:to_symbol(), self.assignment)
elseif arg.default then
state.scope:define(arg.identifier:to_symbol(), arg.default:eval(state))
else
error(("no argument matching parameter %q"):format(arg.identifier.name))
end
end
end
}
package.loaded[...] = ArgumentTuple
Identifier, Number = ast.Identifier, ast.Number
return ArgumentTuple

37
ast/Assignment.lua Normal file
View file

@ -0,0 +1,37 @@
local ast = require("ast")
local Nil
local operator_priority = require("common").operator_priority
local Assignment = ast.abstract.Node {
type = "assignment",
identifier = nil,
expression = nil,
format_priority = operator_priority["_=_"],
init = function(self, identifier, expression)
self.identifier = identifier
self.expression = expression
end,
_format = function(self, ...)
return self.identifier:format(...).." = "..self.expression:format_right(...)
end,
traverse = function(self, fn, ...)
fn(self.identifier, ...)
fn(self.expression, ...)
end,
_eval = function(self, state)
local val = self.expression:eval(state)
state.scope:set(self.identifier, val)
return Nil:new()
end,
}
package.loaded[...] = Assignment
Nil = ast.Nil
return Assignment

49
ast/AttachBlock.lua Normal file
View file

@ -0,0 +1,49 @@
local ast = require("ast")
local Identifier, Quote
local attached_block_identifier
local AttachBlock = ast.abstract.Node {
type = "attach block",
expression = nil,
block = nil,
init = function(self, expression, block)
self.expression = expression
self.block = block
self.format_priority = self.expression.format_priority
end,
_format = function(self, state, priority, indentation, ...)
return self.expression:format(state, priority, indentation, ...).."\n\t"..self.block:format(state, priority, indentation + 1, ...)
end,
traverse = function(self, fn, ...)
fn(self.expression, ...)
fn(self.block, ...)
end,
_eval = function(self, state)
state.scope:push_partial(attached_block_identifier)
state.scope:define(attached_block_identifier:to_symbol(), Quote:new(self.block)) -- _ is always wrapped in a Call when it appears
local exp = self.expression:eval(state)
state.scope:pop()
return exp
end,
_prepare = function(self, state)
state.scope:push_partial(attached_block_identifier)
state.scope:define(attached_block_identifier:to_symbol(), Quote:new(self.block))
self.expression:prepare(state)
state.scope:pop()
end
}
package.loaded[...] = AttachBlock
Identifier, Quote = ast.Identifier, ast.Quote
attached_block_identifier = Identifier:new("_")
return AttachBlock

80
ast/Block.lua Normal file
View file

@ -0,0 +1,80 @@
local ast = require("ast")
local Nil, Return, AutoCall, ArgumentTuple, Flush
local Block = ast.abstract.Node {
type = "block",
expressions = {},
init = function(self)
self.expressions = {}
end,
add = function(self, expression) -- only for construction
table.insert(self.expressions, expression)
end,
_format = function(self, state, prio, ...)
local l = {}
for _, e in ipairs(self.expressions) do
if Flush:is(e) then
table.insert(l, (e:format(state, 0, ...):gsub("\n$", "")))
else
table.insert(l, e:format(state, 0, ...))
end
end
return table.concat(l, "\n")
end,
traverse = function(self, fn, ...)
for _, e in ipairs(self.expressions) do
fn(e, ...)
end
end,
_eval = function(self, state)
local r
state.scope:push()
if self:resuming(state) then
local resuming = self:get_resume_data(state)
local resumed = false
for _, e in ipairs(self.expressions) do
if e == resuming then resumed = true end
if resumed then
r = e:eval(state)
if AutoCall:issub(r) then
r = r:call(state, ArgumentTuple:new())
end
if Return:is(r) then
break -- pass on to parent block until we reach a function boundary
end
end
end
else
for _, e in ipairs(self.expressions) do
self:set_resume_data(state, e)
r = e:eval(state)
if AutoCall:issub(r) then
r = r:call(state, ArgumentTuple:new())
end
if Return:is(r) then
break -- pass on to parent block until we reach a function boundary
end
end
end
state.scope:pop()
return r or Nil:new()
end,
_prepare = function(self, state)
state.scope:push()
for _, e in ipairs(self.expressions) do
e:prepare(state)
end
state.scope:pop()
end
}
package.loaded[...] = Block
Nil, Return, AutoCall, ArgumentTuple, Flush = ast.Nil, ast.Return, ast.abstract.AutoCall, ast.ArgumentTuple, ast.Flush
return Block

28
ast/Boolean.lua Normal file
View file

@ -0,0 +1,28 @@
local ast = require("ast")
return ast.abstract.Node {
type = "boolean",
_evaluated = true, -- no evaluation needed
value = nil,
init = function(self, val)
self.value = val
end,
_hash = function(self)
return ("boolean<%q>"):format(self.value)
end,
_format = function(self)
return tostring(self.value)
end,
to_lua = function(self, state)
return self.value
end,
truthy = function(self)
return self.value
end
}

59
ast/Branched.lua Normal file
View file

@ -0,0 +1,59 @@
-- branched: associate to each branch a different value
-- used to handle mutability. probably the only mutable node you'll ever need! it's literally perfect!
-- note: all values here are expected to be already evaluated
local ast = require("ast")
local Branched = ast.abstract.Runtime {
type = "branched",
mutable = true,
value = nil, -- { [branch name] = value, ... }
init = function(self, state, value)
self.value = {}
self:set(state, value)
end,
in_branch = function(self, state)
return not not self.value[state.branch_id]
end,
get = function(self, state)
return self.value[state.branch_id] or self.value[state.source_branch_id]
end,
set = function(self, state, value)
self.value[state.branch_id] = value
end,
_merge = function(self, state, cache)
local val = self.value[state.branch_id]
if val then
self.value[state.source_branch_id] = val
self.value[state.branch_id] = nil
val:merge(state, cache)
end
end,
_format = function(self, state, ...)
if state then
return self:get(state):format(state, ...)
else
local t = {}
for b, v in pairs(self.value) do
table.insert(t, ("%s→%s"):format(b, v))
end
return "<"..table.concat(t, ", ")..">"
end
end,
traverse = function(self, fn, ...)
for _, v in pairs(self.value) do
fn(v, ...)
end
end,
_eval = function(self, state)
return self:get(state)
end
}
return Branched

86
ast/Call.lua Normal file
View file

@ -0,0 +1,86 @@
local ast = require("ast")
local Identifier
local regular_operators = require("common").regular_operators
local operator_priority = require("common").operator_priority
local function reverse(t, fmt)
for _, v in ipairs(t) do t[fmt:format(v[1])] = v[2] end
return t
end
local infix = reverse(regular_operators.infixes, "_%s_")
local prefix = reverse(regular_operators.prefixes, "%s_")
local suffix = reverse(regular_operators.suffixes, "_%s")
local Call
Call = ast.abstract.Node {
type = "call",
func = nil,
arguments = nil, -- ArgumentTuple
format_priority = infix["_!"], -- often overwritten in :init
init = function(self, func, arguments)
self.func = func
self.arguments = arguments
-- get priority: operators
if Identifier:is(self.func) then
local name, arity = self.func.name, self.arguments.arity
if infix[name] and arity == 2 then
self.format_priority = infix[name]
elseif prefix[name] and arity == 1 then
self.format_priority = prefix[name]
elseif suffix[name] and arity == 1 then
self.format_priority = suffix[name]
end
end
if self.arguments.assignment then
self.format_priority = operator_priority["_=_"]
end
end,
_format = function(self, ...)
if self.arguments.arity == 0 then
if Identifier:is(self.func) and self.func.name == "_" then
return "_" -- the _ identifier is automatically re-wrapped in a Call when it appears
end
local func = self.func:format(...)
return func.."!"
else
if Identifier:is(self.func) then
local name, arity = self.func.name, self.arguments.arity
if infix[name] and arity == 2 then
local left = self.arguments.list[1]:format(...)
local right = self.arguments.list[2]:format_right(...)
return ("%s %s %s"):format(left, name:match("^_(.*)_$"), right)
elseif prefix[name] and arity == 1 then
local right = self.arguments.list[1]:format_right(...)
return ("%s%s"):format(name:match("^(.*)_$"), right)
elseif suffix[name] and arity == 1 then
local left = self.arguments.list[1]:format(...)
return ("%s%s"):format(left, name:match("^_(.*)$"))
end
end
return self.func:format(...)..self.arguments:format(...) -- no need for format_right, we already handle the assignment priority here
end
end,
traverse = function(self, fn, ...)
fn(self.func, ...)
fn(self.arguments, ...)
end,
_eval = function(self, state)
local func = self.func:eval(state)
local arguments = self.arguments:eval(state)
return func:call(state, arguments)
end
}
package.loaded[...] = Call
Identifier = ast.Identifier
return Call

52
ast/Choice.lua Normal file
View file

@ -0,0 +1,52 @@
local ast = require("ast")
local ArgumentTuple
local operator_priority = require("common").operator_priority
local Choice
Choice = ast.abstract.Runtime {
type = "choice",
text = nil,
func = nil,
format_priority = operator_priority["_|>_"],
init = function(self, text, func)
self.text = text
self.func = func
end,
traverse = function(self, fn, ...)
fn(self.text, ...)
fn(self.func, ...)
end,
_format = function(self, ...)
return ("%s |> %s"):format(self.text:format(...), self.func:format_right(...))
end,
build_event_data = function(self, state, event_buffer)
local l = {
_selected = nil,
choose = function(self, choice)
self._selected = choice
end
}
for _, c in event_buffer:iter(state) do
table.insert(l, c.text)
end
return l
end,
post_flush_callback = function(self, state, event_buffer, data)
local choice = data._selected
assert(choice, "no choice made")
assert(choice > 0 and choice <= event_buffer:len(state), "choice out of bounds")
event_buffer:get(state, choice).func:call(state, ArgumentTuple:new())
end
}
package.loaded[...] = Choice
ArgumentTuple = ast.ArgumentTuple
return Choice

58
ast/Closure.lua Normal file
View file

@ -0,0 +1,58 @@
-- note: functions only appear in non-evaluated nodes! once evaluated, they always become closures
local ast = require("ast")
local Overloadable, Runtime = ast.abstract.Overloadable, ast.abstract.Runtime
local Definition
local Closure
Closure = Runtime(Overloadable) {
type = "closure",
func = nil, -- Function
scope = nil, -- Environment
exported_scope = nil, -- Environment
init = function(self, func, state)
self.func = func
self.scope = state.scope:capture()
-- layer a new export layer on top of captured/current scope
state.scope:push_export()
self.exported_scope = state.scope:capture()
-- pre-define exports
for sym, exp in pairs(self.func.exports) do
Definition:new(sym, exp):eval(state)
end
state.scope:pop()
end,
_format = function(self, ...)
return self.func:format(...)
end,
traverse = function(self, fn, ...)
fn(self.func, ...)
fn(self.scope, ...)
fn(self.exported_scope, ...)
end,
compatible_with_arguments = function(self, state, args)
return args:match_parameter_tuple(state, self.func.parameters)
end,
format_parameters = function(self, state)
return self.func.parameters:format(state)
end,
call_compatible = function(self, state, args)
state.scope:push(self.exported_scope)
local exp = self.func:call_compatible(state, args)
state.scope:pop()
return exp
end,
}
package.loaded[...] = Closure
Definition = ast.Definition
return Closure

60
ast/Definition.lua Normal file
View file

@ -0,0 +1,60 @@
local ast = require("ast")
local Nil, Overloadable
local operator_priority = require("common").operator_priority
local Definition = ast.abstract.Node {
type = "definition",
symbol = nil,
expression = nil,
format_priority = operator_priority["_=_"],
init = function(self, symbol, expression)
self.symbol = symbol
self.expression = expression
end,
_format = function(self, ...)
return self.symbol:format(...).." = "..self.expression:format_right(...)
end,
traverse = function(self, fn, ...)
fn(self.symbol, ...)
fn(self.expression, ...)
end,
_eval = function(self, state)
if self.symbol.exported and state.scope:defined_in_current(self.symbol) then
return Nil:new() -- export vars: can reuse existing defining
end
local symbol = self.symbol:eval(state)
local val = self.expression:eval(state)
if Overloadable:issub(val) then
state.scope:define_overloadable(symbol, val)
else
state.scope:define(symbol, val)
end
return Nil:new()
end,
_prepare = function(self, state)
local symbol, val = self.symbol, self.expression
symbol:prepare(state)
val:prepare(state)
if Overloadable:issub(val) then
state.scope:define_overloadable(symbol, val)
else
state.scope:define(symbol, val)
end
end
}
package.loaded[...] = Definition
Nil, Overloadable = ast.Nil, ast.abstract.Overloadable
return Definition

214
ast/Environment.lua Normal file
View file

@ -0,0 +1,214 @@
local ast = require("ast")
local operator_priority = require("common").operator_priority
local Branched, ArgumentTuple, Overload, Overloadable, Table
local VariableMetadata = ast.abstract.Runtime {
type = "variable metadata",
symbol = nil,
branched = nil,
format_priority = operator_priority["_=_"],
init = function(self, state, symbol, value)
self.symbol = symbol
self.branched = Branched:new(state, value)
end,
get = function(self, state)
return self.branched:get(state)
end,
set = function(self, state, value)
assert(not self.symbol.constant, ("trying to change the value of constant %s"):format(self.symbol.string))
if self.symbol.type_check then
local r = self.symbol.type_check:call(state, ArgumentTuple:new(value))
if not r:truthy() then error(("type check failure for %s; %s does not satisfy %s"):format(self.symbol.string, value, self.symbol.type_check)) end
end
self.branched:set(state, value)
end,
_format = function(self, ...)
return ("%s=%s"):format(self.symbol:format(...), self.branched:format(...))
end,
traverse = function(self, fn, ...)
fn(self.symbol, ...)
fn(self.branched, ...)
end,
_merge = function(self, state, cache)
if not self.symbol.confined_to_branch then
self.branched:merge(state, cache)
end
end
}
local Environment = ast.abstract.Runtime {
type = "environment",
parent = nil, -- environment or nil
variables = nil, -- Table of { {identifier} = variable metadata, ... }
partial = nil, -- { [name string] = true, ... }
export = nil, -- bool
init = function(self, state, parent, partial_names, is_export)
self.variables = Table:new(state)
self.parent = parent
self.partial = partial_names
self.export = is_export
end,
traverse = function(self, fn, ...)
if self.parent then
fn(self.parent, ...)
end
fn(self.variables, ...)
end,
_format = function(self, state)
return "<environment>"
end,
-- define new variable in the environment
define = function(self, state, symbol, exp)
local name = symbol.string
if self:defined_in_current(state, symbol) then
error(name.." is already defined in the current scope")
end
if (self.partial and not self.partial[name])
or (self.export ~= symbol.exported) then
return self.parent:define(state, symbol, exp)
end
self.variables:set(state, symbol:to_identifier(), VariableMetadata:new(state, symbol, exp))
end,
-- define or redefine new overloadable variable in current environment, inheriting existing overload variants from (parent) scopes
define_overloadable = function(self, state, symbol, exp)
assert(Overloadable:issub(exp), "trying to add an non-overloadable value to an overload")
local identifier = symbol:to_identifier()
-- add overload variants already defined in current or parent scope
if self:defined(state, identifier) then
local val = self:get(state, identifier)
if Overload:is(val) then
exp = Overload:new(exp)
for _, v in ipairs(val.list) do
exp:insert(v)
end
elseif Overloadable:issub(val) then
exp = Overload:new(exp, val)
elseif self:defined_in_current(state, symbol) then
error(("can't add an overload variant to non-overloadable variable %s defined in the same scope"):format(identifier))
end
end
-- update/define in current scope
if self:defined_in_current(state, symbol) then
self:set(state, identifier, exp)
else
self:define(state, symbol, exp)
end
end,
-- returns bool if variable defined in current or parent environment
defined = function(self, state, identifier)
if self.variables:has(state, identifier) then
return true
elseif self.parent then
return self.parent:defined(state, identifier)
end
return false
end,
-- returns bool if variable defined in current environment layer
-- (note: by current layer, we mean the closest one where the variable is able to exist - if it is exported, the closest export layer, etc.)
defined_in_current = function(self, state, symbol)
local name = symbol.string
if self.variables:has(state, symbol:to_identifier()) then
return true
elseif (self.partial and not self.partial[name])
or (self.export ~= symbol.exported) then
return self.parent:defined_in_current(state, symbol)
end
return false
end,
-- return bool if variable is defined in the current environment only - won't search in parent event for exported & partial names
defined_in_current_strict = function(self, state, identifier)
return self.variables:has(state, identifier)
end,
-- get variable in current or parent scope, with metadata
_get_variable = function(self, state, identifier)
if self:defined(state, identifier) then
if self.variables:has(state, identifier) then
return self.variables:get(state, identifier)
elseif self.parent then
return self.parent:_get_variable(state, identifier)
end
else
error(("identifier %q is undefined in branch %s"):format(identifier.name, state.branch_id), 0)
end
end,
-- get variable value in current or parent environment
get = function(self, state, identifier)
return self:_get_variable(state, identifier):get(state)
end,
-- set variable value in current or parent environment
set = function(self, state, identifier, val)
return self:_get_variable(state, identifier):set(state, val)
end,
-- returns a list {[symbol]=val,...} of all persistent variables in the current strict layer
list_persistent = function(self, state)
assert(self.export, "not an export scope layer")
local r = {}
for _, vm in self.variables:iter(state) do
if vm.symbol.persistent then
r[vm.symbol] = vm:get(state)
end
end
return r
end,
-- returns a list {[symbol]=val,...} of all exported variables in the current strict layer
list_exported = function(self, state)
assert(self.export, "not an export scope layer")
local r = {}
for _, vm in self.variables:iter(state) do
if vm.symbol.exported then
r[vm.symbol] = vm:get(state)
end
end
return r
end,
-- return the depth of the environmenet, i.e. the number of parents
depth = function(self)
local d = 0
local e = self
while e.parent do
e = e.parent
d = d + 1
end
return d
end,
_debug_state = function(self, state, filter, t, level)
level = level or 0
t = t or {}
local indentation = string.rep(" ", level)
table.insert(t, ("%s> %s %s scope"):format(indentation, self.export and "exported" or "", self.partial and "partial" or ""))
for name, var in self.variables:iter(state) do
if name.name:match(filter) then
table.insert(t, ("%s| %s = %s"):format(indentation, name, var:get(state)))
end
end
if self.parent then
self.parent:_debug_state(state, filter, t, level+1)
end
return t
end,
}
package.loaded[...] = Environment
Branched, ArgumentTuple, Overload, Overloadable, Table = ast.Branched, ast.ArgumentTuple, ast.Overload, ast.abstract.Overloadable, ast.Table
return Environment

28
ast/Flush.lua Normal file
View file

@ -0,0 +1,28 @@
local ast = require("ast")
local Nil
local event_manager = require("state.event_manager")
local Flush = ast.abstract.Node {
type = "flush",
init = function(self) end,
_hash = function(self)
return "flush"
end,
_format = function(self)
return "\n"
end,
_eval = function(self, state)
event_manager:flush(state)
return Nil:new()
end
}
package.loaded[...] = Flush
Nil = ast.Nil
return Flush

78
ast/Function.lua Normal file
View file

@ -0,0 +1,78 @@
-- note: functions only appear in non-evaluated nodes! once evaluated, they always become closures
local ast = require("ast")
local Overloadable = ast.abstract.Overloadable
local Closure, ReturnBoundary
local operator_priority = require("common").operator_priority
local Function
Function = Overloadable {
type = "function",
parameters = nil, -- ParameterTuple
expression = nil,
format_priority = operator_priority["$_"],
exports = nil, -- { [sym] = exp, ... }, exctracted from expression during :prepare
init = function(self, parameters, expression, exports)
self.parameters = parameters
self.expression = ReturnBoundary:new(expression)
self.exports = exports or {}
end,
_format = function(self, ...)
if self.parameters.assignment then
return "$"..self.parameters:format(...).."; "..self.expression:format_right(...)
else
return "$"..self.parameters:format(...).." "..self.expression:format_right(...)
end
end,
traverse = function(self, fn, ...)
fn(self.parameters, ...)
fn(self.expression, ...)
end,
compatible_with_arguments = function(self, state, args)
return args:match_parameter_tuple(state, self.parameters)
end,
format_parameters = function(self, state)
return self.parameters:format(state)
end,
call_compatible = function(self, state, args)
state.scope:push()
args:bind_parameter_tuple(state, self.parameters)
local exp = self.expression:eval_resumable(state)
state.scope:pop()
-- reminder: don't do any additionnal processing here as that won't be executed when resuming self.expression
-- instead wrap it in some additional node, like our friend ReturnBoundary
return exp
end,
_eval = function(self, state)
return Closure:new(Function:new(self.parameters:eval(state), self.expression, self.exports), state)
end,
_prepare = function(self, state)
state.scope:push_export() -- recreate scope context that will be created by closure
state.scope:push()
self.parameters:prepare(state)
self.expression:prepare(state)
state.scope:pop()
self.exports = state.scope:capture():list_exported(state)
state.scope:pop()
end,
}
package.loaded[...] = Function
Closure, ReturnBoundary = ast.Closure, ast.ReturnBoundary
return Function

45
ast/FunctionParameter.lua Normal file
View file

@ -0,0 +1,45 @@
local ast = require("ast")
local operator_priority = require("common").operator_priority
local FunctionParameter
FunctionParameter = ast.abstract.Node {
type = "function parameter",
identifier = nil,
default = nil, -- can be nil
type_check = nil, -- can be nil
init = function(self, identifier, default, type_check)
self.identifier = identifier
self.default = default
self.type_check = type_check
if default then
self.format_priority = operator_priority["_=_"]
elseif type_check then -- type_check has higher prio than assignment in any case
self.format_priority = operator_priority["_::_"]
end
end,
_format = function(self, state, prio, ...)
local s = self.identifier:format(state, prio, ...)
if self.type_check then
s = s .. "::" .. self.type_check:format_right(state, operator_priority["_::_"], ...)
end
if self.default then
s = s .. "=" .. self.default:format_right(state, operator_priority["_=_"], ...)
end
return s
end,
traverse = function(self, fn, ...)
fn(self.identifier, ...)
if self.default then fn(self.default, ...) end
if self.type_check then fn(self.type_check, ...) end
end,
_eval = function(self, state)
return FunctionParameter:new(self.identifier, self.default, self.type_check and self.type_check:eval(state))
end
}
return FunctionParameter

44
ast/Identifier.lua Normal file
View file

@ -0,0 +1,44 @@
local ast = require("ast")
local Symbol, String
local Identifier
Identifier = ast.abstract.Node {
type = "identifier",
name = nil,
init = function(self, name)
self.name = name
end,
_hash = function(self)
return ("identifier<%q>"):format(self.name)
end,
_format = function(self)
return self.name
end,
_eval = function(self, state)
return state.scope:get(self)
end,
to_string = function(self)
return String:new(self.name)
end,
to_symbol = function(self, modifiers)
return Symbol:new(self.name, modifiers)
end,
_prepare = function(self, state)
if state.scope:defined(self) then
state.scope:get(self):prepare(state)
end
end
}
package.loaded[...] = Identifier
Symbol, String = ast.Symbol, ast.String
return Identifier

81
ast/List.lua Normal file
View file

@ -0,0 +1,81 @@
local ast = require("ast")
local Branched, Tuple
local operator_priority = require("common").operator_priority
local List
List = ast.abstract.Runtime {
type = "list",
format_priority = operator_priority["*_"],
-- note: yeah technically this isn't mutable, only .branched is
-- note: this a Branched of Tuple, and we *will* forcefully mutate the tuples, so make sure to not disseminate any reference to them outside the List
-- unless you want rumors about mutable tuples to spread
branched = nil,
init = function(self, state, from_tuple)
from_tuple = from_tuple or Tuple:new()
self.branched = Branched:new(state, from_tuple:copy())
end,
_format = function(self, ...)
return "*"..self.branched:format_right(...)
end,
traverse = function(self, fn, ...)
fn(self.branched, ...)
end,
-- List is always created from an evaluated Tuple, so no need to _eval here
-- create copy of the list in branch if not here
-- do this before any mutation
-- return the tuple for the current branch
_prepare_branch = function(self, state)
if not self.branched:in_branch(state) then
self.branched:set(state, self.branched:get(state):copy())
end
return self.branched:get(state)
end,
len = function(self, state)
return #self.branched:get(state).list
end,
iter = function(self, state)
return ipairs(self.branched:get(state).list)
end,
get = function(self, state, index)
local list = self.branched:get(state)
if index < 0 then index = #list.list + 1 + index end
if index > #list.list or index == 0 then error("list index out of bounds") end
return list.list[index]
end,
set = function(self, state, index, val)
local list = self:_prepare_branch(state)
if index < 0 then index = #list.list + 1 + index end
if index > #list.list or index == 0 then error("list index out of bounds") end
list.list[index] = val
end,
insert = function(self, state, val)
local l = self:_prepare_branch(state)
table.insert(l.list, val)
end,
remove = function(self, state)
local l = self:_prepare_branch(state)
table.remove(l.list)
end,
to_tuple = function(self, state)
return self.branched:get(state):copy()
end,
to_lua = function(self, state)
return self.branched:get(state):to_lua(state)
end,
}
package.loaded[...] = List
Branched, Tuple = ast.Branched, ast.Tuple
return List

63
ast/LuaFunction.lua Normal file
View file

@ -0,0 +1,63 @@
local ast = require("ast")
local Overloadable = ast.abstract.Overloadable
local operator_priority = require("common").operator_priority
local LuaFunction
LuaFunction = ast.abstract.Runtime(Overloadable) {
type = "lua function",
parameters = nil, -- ParameterTuple
func = nil, -- lua function
format_priority = operator_priority["$_"],
init = function(self, parameters, func)
self.parameters = parameters
self.func = func
end,
traverse = function(self, fn, ...)
fn(self.parameters, ...)
end,
_format = function(self, ...)
if self.parameters.assignment then
return "$"..self.parameters:format(...).."; <lua function>"
else
return "$"..self.parameters:format(...).." <lua function>"
end
end,
compatible_with_arguments = function(self, state, args)
return args:match_parameter_tuple(state, self.parameters)
end,
format_parameters = function(self, state)
return self.parameters:format(state)
end,
call_compatible = function(self, state, args)
local lua_args = { state }
state.scope:push()
args:bind_parameter_tuple(state, self.parameters)
for _, param in ipairs(self.parameters.list) do
table.insert(lua_args, state.scope:get(param.identifier))
end
state.scope:pop()
local r = self.func(table.unpack(lua_args))
assert(r, "lua function returned no value")
return r
end,
_eval = function(self, state)
return LuaFunction:new(self.parameters:eval(state), self.func)
end,
to_lua = function(self, state)
return self.func
end,
}
return LuaFunction

20
ast/Nil.lua Normal file
View file

@ -0,0 +1,20 @@
local ast = require("ast")
return ast.abstract.Node {
type = "nil",
_evaluated = true, -- no evaluation needed
init = function(self) end,
_hash = function(self)
return "nil"
end,
_format = function(self)
return "()"
end,
to_lua = function(self, state) return nil end,
truthy = function(self) return false end
}

25
ast/Number.lua Normal file
View file

@ -0,0 +1,25 @@
local ast = require("ast")
local Number
Number = ast.abstract.Node {
type = "number",
_evaluated = true, -- no evaluation needed
number = nil,
init = function(self, number)
self.number = number
end,
_hash = function(self)
return ("number<%s>"):format(self.number)
end,
_format = function(self)
return tostring(self.number)
end,
to_lua = function(self, state) return self.number end,
}
return Number

62
ast/Overload.lua Normal file
View file

@ -0,0 +1,62 @@
local ast = require("ast")
local Overload
Overload = ast.abstract.Node {
type = "overload",
_evaluated = true,
list = nil,
init = function(self, ...)
self.list = { ... }
end,
insert = function(self, val) -- only for construction
table.insert(self.list, val)
end,
_format = function(self, ...)
local s = "overload<"
for i, e in ipairs(self.list) do
s = s .. e:format(...)
if i < #self.list then s = s .. ", " end
end
return s..">"
end,
traverse = function(self, fn, ...)
for _, e in ipairs(self.list) do
fn(e, ...)
end
end,
call = function(self, state, args)
local failure = {} -- list of failure messages (kept until we find the first success)
local success, success_specificity, success_secondary_specificity = nil, -1, -1
-- some might think that iterating a list for every function call is a terrible idea, but that list has a fixed number of elements, so big O notation says suck it up
for _, fn in ipairs(self.list) do
local specificity, secondary_specificity = fn:compatible_with_arguments(state, args)
if specificity then
if specificity > success_specificity then
success, success_specificity, success_secondary_specificity = fn, specificity, secondary_specificity
elseif specificity == success_specificity then
if secondary_specificity > success_secondary_specificity then
success, success_specificity, success_secondary_specificity = fn, specificity, secondary_specificity
elseif secondary_specificity == success_secondary_specificity then
error(("more than one function match %s, matching functions were at least (specificity %s.%s):\n\t• %s\n\t• %s"):format(args:format(state), specificity, secondary_specificity, fn:format_parameters(state), success:format_parameters(state)), 0)
end
end
-- no need to add error message for less specific function since we already should have at least one success
elseif not success then
table.insert(failure, fn:format_parameters(state) .. ": " .. secondary_specificity)
end
end
if success then
return success:call_compatible(state, args)
else
-- error
error(("no function match %s, possible functions were:\n\t• %s"):format(args:format(state), table.concat(failure, "\n\t")), 0)
end
end
}
return Overload

25
ast/Pair.lua Normal file
View file

@ -0,0 +1,25 @@
local ast = require("ast")
local operator_priority = require("common").operator_priority
return ast.abstract.Runtime {
type = "pair",
name = nil,
value = nil,
format_priority = operator_priority["_:_"],
init = function(self, name, value)
self.name = name
self.value = value
end,
traverse = function(self, fn, ...)
fn(self.name, ...)
fn(self.value, ...)
end,
_format = function(self, ...)
return ("%s:%s"):format(self.name:format(...), self.value:format(...))
end,
}

67
ast/ParameterTuple.lua Normal file
View file

@ -0,0 +1,67 @@
local ast = require("ast")
local operator_priority = require("common").operator_priority
local ParameterTuple
ParameterTuple = ast.abstract.Node {
type = "parameter tuple",
assignment = false,
list = nil,
min_arity = 0,
max_arity = 0,
eval_depth = 0, -- scope deth where this parametertuple was evaluated, used as secondary specificity
init = function(self, ...)
self.list = {...}
end,
insert = function(self, val) -- only for construction
assert(not self.assignment, "can't add new parameters after assignment parameter was added")
table.insert(self.list, val)
self.max_arity = self.max_arity + 1
if not val.default then
self.min_arity = self.min_arity + 1
end
end,
insert_assignment = function(self, val) -- only for construction
self:insert(val)
self.assignment = true
self.format_priority = operator_priority["_=_"]
end,
_format = function(self, state, prio, ...)
local l = {}
for i, e in ipairs(self.list) do
if i < self.max_arity or not self.assignment then
table.insert(l, e:format(state, operator_priority["_,_"], ...))
end
end
local s = ("(%s)"):format(table.concat(l, ", "))
if self.assignment then
s = s .. (" = %s"):format(self.list[#self.list]:format_right(state, operator_priority["_=_"], ...))
end
return s
end,
traverse = function(self, fn, ...)
for _, e in ipairs(self.list) do
fn(e, ...)
end
end,
_eval = function(self, state)
local r = ParameterTuple:new()
for i, param in ipairs(self.list) do
if i < self.max_arity or not self.assignment then
r:insert(param:eval(state))
else
r:insert_assignment(param:eval(state))
end
end
r.eval_depth = state.scope:depth()
return r
end
}
return ParameterTuple

34
ast/Quote.lua Normal file
View file

@ -0,0 +1,34 @@
-- prevent an expression from being immediately evaluated, and instead only evaluate it when the node is explicitely called
-- it can be used to evaluate the expression on demand, as if the quote call AST was simply replaced by the unevaluated associated expression AST (like a macro)
-- keep in mind that this thus bypass any scoping rule, closure, etc.
--
-- used for infix operators where the evaluation of the right term depends of the left one (lazy boolean operators, conditionals, etc.)
local ast = require("ast")
local Quote
Quote = ast.abstract.Node {
type = "quote",
expression = nil,
init = function(self, expression)
self.expression = expression
self.format_priority = expression.format_priority
end,
_format = function(self, ...)
return self.expression:format(...) -- Quote is generated transparently by operators
end,
traverse = function(self, fn, ...)
fn(self.expression, ...)
end,
call = function(self, state, args)
assert(args.arity == 0, "Quote! does not accept arguments")
return self.expression:eval(state)
end
}
return Quote

60
ast/Resumable.lua Normal file
View file

@ -0,0 +1,60 @@
local ast = require("ast")
local Table
local resumable_manager
local Resumable
Resumable = ast.abstract.Runtime {
type = "resumable",
resuming = false,
expression = nil,
scope = nil,
data = nil,
init = function(self, state, expression, scope, data)
self.expression = expression
self.scope = scope
self.data = data or Table:new(state)
end,
_format = function(self)
return "<resumable>"
end,
traverse = function(self, fn, ...)
fn(self.expression, ...)
fn(self.data, ...)
fn(self.scope, ...)
end,
-- returns a copy with the data copied
capture = function(self, state)
return Resumable:new(state, self.expression, self.scope, self.data:copy(state))
end,
-- resume from this resumable
call = function(self, state, args)
assert(args.arity == 0, "Resumable! does not accept arguments")
state.scope:push(self.scope)
local resuming = self:capture(state)
resuming.resuming = true
resumable_manager:push(state, resuming)
local r = self.expression:eval(state)
resumable_manager:pop(state)
state.scope:pop()
return r
end,
}
package.loaded[...] = Resumable
Table = ast.Table
resumable_manager = require("state.resumable_manager")
return Resumable

View file

@ -0,0 +1,44 @@
-- intended to be wrapped in a Function, so that when resuming from the function, will keep resuming to where the function was called from
-- used in Choices to resume back from where the event was flushed
-- note: when resuming, the return value will be discarded, instead returning what the parent function will return
local ast = require("ast")
local ArgumentTuple
local resumable_manager
local ResumeParentFunction = ast.abstract.Node {
type = "resume parent function",
expression = nil,
init = function(self, expression)
self.expression = expression
self.format_priority = expression.format_priority
end,
_format = function(self, ...)
return self.expression:format(...)
end,
traverse = function(self, fn, ...)
fn(self.expression, ...)
end,
_eval = function(self, state)
if resumable_manager:resuming(state, self) then
self.expression:eval(state)
return resumable_manager:get_data(state, self):call(state, ArgumentTuple:new())
else
resumable_manager:set_data(state, self, resumable_manager:capture(state, 1))
return self.expression:eval(state)
end
end
}
package.loaded[...] = ResumeParentFunction
ArgumentTuple = ast.ArgumentTuple
resumable_manager = require("state.resumable_manager")
return ResumeParentFunction

33
ast/Return.lua Normal file
View file

@ -0,0 +1,33 @@
local ast = require("ast")
local operator_priority = require("common").operator_priority
local Return
Return = ast.abstract.Node {
type = "return",
expression = nil,
format_priority = operator_priority["@_"],
init = function(self, expression)
self.expression = expression
end,
_format = function(self, ...)
return ("@%s"):format(self.expression:format_right(...))
end,
traverse = function(self, fn, ...)
fn(self.expression, ...)
end,
_eval = function(self, state)
return Return:new(self.expression:eval(state))
end,
to_lua = function(self, state)
return self.expression:to_lua(state)
end
}
return Return

37
ast/ReturnBoundary.lua Normal file
View file

@ -0,0 +1,37 @@
-- used stop propagating Return when leaving functions
local ast = require("ast")
local Return
local ReturnBoundary = ast.abstract.Node {
type = "return boundary",
expression = nil,
init = function(self, expression)
self.expression = expression
self.format_priority = self.expression.format_priority
end,
_format = function(self, ...)
return self.expression:format(...)
end,
traverse = function(self, fn, ...)
fn(self.expression, ...)
end,
_eval = function(self, state)
local exp = self.expression:eval(state)
if Return:is(exp) then
return exp.expression
else
return exp
end
end
}
package.loaded[...] = ReturnBoundary
Return = ast.Return
return ReturnBoundary

34
ast/String.lua Normal file
View file

@ -0,0 +1,34 @@
local ast = require("ast")
local Identifier
local String = ast.abstract.Node {
type = "string",
_evaluated = true, -- no evaluation needed
string = nil,
init = function(self, str)
self.string = str
end,
_hash = function(self)
return ("string<%q>"):format(self.string)
end,
_format = function(self)
return ("%q"):format(self.string)
end,
to_lua = function(self, state)
return self.string
end,
to_identifier = function(self)
return Identifier:new(self.string)
end
}
package.loaded[...] = String
Identifier = ast.Identifier
return String

View file

@ -0,0 +1,53 @@
local ast = require("ast")
local String
local StringInterpolation = ast.abstract.Node {
type = "string interpolation",
list = nil,
init = function(self, ...)
self.list = {...}
end,
insert = function(self, val) -- only for construction
table.insert(self.list, val)
end,
traverse = function(self, fn, ...)
for _, e in ipairs(self.list) do
fn(e, ...)
end
end,
_format = function(self, ...)
local l = {}
for _, e in ipairs(self.list) do
if String:is(e) then
local t = e.string:gsub("\\", "\\\\"):gsub("\n", "\\n"):gsub("\t", "\\t"):gsub("\"", "\\\"")
table.insert(l, t)
else
table.insert(l, ("{%s}"):format(e:format(...)))
end
end
return ("\"%s\""):format(table.concat(l))
end,
_eval = function(self, state)
local t = {}
for _, e in ipairs(self.list) do
local r = e:eval(state)
if String:is(r) then
r = r.string
else
r = r:format(state)
end
table.insert(t, r)
end
return String:new(table.concat(t))
end
}
package.loaded[...] = StringInterpolation
String = ast.String
return StringInterpolation

121
ast/Struct.lua Normal file
View file

@ -0,0 +1,121 @@
local ast = require("ast")
local Pair, Number, Nil
local operator_priority = require("common").operator_priority
local Struct
local TupleToStruct = ast.abstract.Node {
type = "tuple to struct",
tuple = nil,
init = function(self, tuple)
self.tuple = tuple
end,
traverse = function(self, fn, ...)
fn(self.tuple, ...)
end,
_format = function(self, ...)
return self.tuple:format(...):gsub("^%[", "{"):gsub("%]$", "}")
end,
_eval = function(self, state)
local t = Struct:new()
for i, e in ipairs(self.tuple.list) do
if Pair:is(e) then
t:set(e.name, e.value)
else
t:set(Number:new(i), e)
end
end
return t
end
}
Struct = ast.abstract.Runtime {
type = "struct",
table = nil,
init = function(self)
self.table = {}
end,
set = function(self, key, value) -- only for construction
self.table[key:hash()] = { key, value }
end,
include = function(self, other) -- only for construction
for _, e in pairs(other.table) do
self:set(e[1], e[2])
end
end,
copy = function(self)
local s = Struct:new()
for _, e in pairs(self.table) do
s:set(e[1], e[2])
end
return s
end,
-- build from (non-evaluated) tuple
-- results needs to be evaluated
from_tuple = function(self, tuple)
return TupleToStruct:new(tuple)
end,
_format = function(self, state, prio, ...)
local l = {}
for _, e in pairs(self.table) do
-- _:_ has higher priority than _,_
table.insert(l, e[1]:format(state, operator_priority["_:_"], ...)..":"..e[2]:format_right(state, operator_priority["_:_"], ...))
end
return ("{%s}"):format(table.concat(l, ", "))
end,
traverse = function(self, fn, ...)
for _, e in pairs(self.table) do
fn(e[1], ...)
fn(e[2], ...)
end
end,
-- need to redefine hash to include a table.sort as pairs() in :traverse is non-deterministic
_hash = function(self)
local t = {}
for _, e in pairs(self.table) do
table.insert(t, ("%s;%s"):format(e[1]:hash(), e[2]:hash()))
end
table.sort(t)
return ("%s<%s>"):format(self.type, table.concat(t, ";"))
end,
-- regarding eval: Struct is built from TupleToStruct function call which already eval, so every Struct should be fully evaluated
to_lua = function(self, state)
local l = {}
for _, e in ipairs(self.table) do
l[e[1]:to_lua(state)] = e[2]:to_lua(state)
end
return l
end,
get = function(self, key)
local hash = key:hash()
if self.table[hash] then
return self.table[hash][2]
else
return Nil:new()
end
end,
has = function(self, key)
local hash = key:hash()
return not not self.table[hash]
end
}
package.loaded[...] = Struct
Pair, Number, Nil = ast.Pair, ast.Number, ast.Nil
return Struct

78
ast/Symbol.lua Normal file
View file

@ -0,0 +1,78 @@
local ast = require("ast")
local Identifier, String
local operator_priority = require("common").operator_priority
local Symbol
Symbol = ast.abstract.Node {
type = "symbol",
string = nil,
constant = nil, -- bool
type_check = nil, -- exp
exported = nil, -- bool
persistent = nil, -- bool, imply exported
confined_to_branch = nil, -- bool
init = function(self, str, modifiers)
modifiers = modifiers or {}
self.string = str
self.constant = modifiers.constant
self.persistent = modifiers.persistent
self.type_check = modifiers.type_check
self.confined_to_branch = modifiers.confined_to_branch
self.exported = modifiers.exported or modifiers.persistent
if self.type_check then
self.format_priority = operator_priority["_::_"]
end
end,
_eval = function(self, state)
return Symbol:new(self.string, {
constant = self.constant,
persistent = self.persistent,
type_check = self.type_check and self.type_check:eval(state),
confined_to_branch = self.confined_to_branch,
exported = self.exported
})
end,
_hash = function(self)
return ("symbol<%q>"):format(self.string)
end,
_format = function(self, state, prio, ...)
local s = ":"
if self.constant then
s = s .. ":"
end
if self.persistent then
s = s .. "&"
end
if self.exported then
s = s .. "@"
end
s = s .. self.string
if self.type_check then
s = s .. "::" .. self.type_check:format_right(state, operator_priority["_::_"], ...)
end
return s
end,
to_lua = function(self, state)
return self.string
end,
to_identifier = function(self)
return Identifier:new(self.string)
end,
to_string = function(self)
return String:new(self.string)
end
}
package.loaded[...] = Symbol
Identifier, String = ast.Identifier, ast.String
return Symbol

85
ast/Table.lua Normal file
View file

@ -0,0 +1,85 @@
local ast = require("ast")
local Branched, Struct, Nil = ast.Branched, ast.Struct, ast.Nil
local operator_priority = require("common").operator_priority
local Table
Table = ast.abstract.Runtime {
type = "table",
format_priority = operator_priority["*_"],
-- note: technically this isn't mutable, only .branched is
-- note: this a Branched of Struct, and we *will* forcefully mutate the tuples, so make sure to not disseminate any reference to them outside the Table
-- unless you want rumors about mutable structs to spread
branched = nil,
init = function(self, state, from_struct)
from_struct = from_struct or Struct:new()
self.branched = Branched:new(state, from_struct:copy())
end,
_format = function(self, ...)
return "*"..self.branched:format_right(...)
end,
traverse = function(self, fn, ...)
fn(self.branched, ...)
end,
-- Table is always created from an evaluated Struct, so no need to _eval here
-- create copy of the table in branch if not here
-- do this before any mutation
-- return the struct for the current branch
_prepare_branch = function(self, state)
if not self.branched:in_branch(state) then
self.branched:set(state, self.branched:get(state):copy())
end
return self.branched:get(state)
end,
get = function(self, state, key)
local s = self.branched:get(state)
return s:get(key)
end,
set = function(self, state, key, val)
local s = self:_prepare_branch(state)
local hash = key:hash()
if Nil:is(val) then
s.table[hash] = nil
else
s.table[hash] = { key, val }
end
end,
has = function(self, state, key)
local s = self.branched:get(state)
return s:has(key)
end,
iter = function(self, state)
local t, h = self.branched:get(state).table, nil
return function()
local e
h, e = next(t, h)
if h == nil then return nil
else return e[1], e[2]
end
end
end,
to_struct = function(self, state)
return self.branched:get(state):copy()
end,
to_lua = function(self, state)
return self.branched:get(state):to_lua(state)
end,
copy = function(self, state)
return Table:new(state, self:to_struct(state))
end
}
package.loaded[...] = Table
Branched, Struct, Nil = ast.Branched, ast.Struct, ast.Nil
return Table

36
ast/Text.lua Normal file
View file

@ -0,0 +1,36 @@
local ast = require("ast")
local AutoCall, Event, Runtime = ast.abstract.AutoCall, ast.abstract.Event, ast.abstract.Runtime
return Runtime(AutoCall, Event) {
type = "text",
list = nil, -- { { String, tag Table }, ... }
init = function(self)
self.list = {}
end,
insert = function(self, str, tags) -- only for construction
table.insert(self.list, { str, tags })
end,
traverse = function(self, fn, ...)
for _, e in ipairs(self.list) do
fn(e[1], ...)
fn(e[2], ...)
end
end,
_format = function(self, ...)
local t = {}
for _, e in ipairs(self.list) do
table.insert(t, ("%s%s"):format(e[2]:format(...), e[1]:format(...)))
end
return ("| %s |"):format(table.concat(t, " "))
end,
-- Text comes from TextInterpolation which already evals the contents
to_event_data = function(self)
return self
end
}

59
ast/TextInterpolation.lua Normal file
View file

@ -0,0 +1,59 @@
local ast = require("ast")
local Text, String
local tag_manager = require("state.tag_manager")
local TextInterpolation = ast.abstract.Node {
type = "text interpolation",
list = nil,
init = function(self, ...)
self.list = {...}
end,
insert = function(self, val) -- only for construction
table.insert(self.list, val)
end,
traverse = function(self, fn, ...)
for _, e in ipairs(self.list) do
fn(e, ...)
end
end,
_format = function(self, ...)
local l = {}
for _, e in ipairs(self.list) do
if String:is(e) then
local t = e.string:gsub("\\", "\\\\"):gsub("\n", "\\n"):gsub("\t", "\\t"):gsub("\"", "\\\"")
table.insert(l, t)
else
table.insert(l, ("{%s}"):format(e:format(...)))
end
end
return ("| %s|"):format(table.concat(l))
end,
_eval = function(self, state)
local t = Text:new()
local tags = tag_manager:get(state)
for _, e in ipairs(self.list) do
local r = e:eval(state)
if String:is(r) then
t:insert(r, tags)
elseif Text:is(r) then
for _, v in ipairs(r.list) do
t:insert(v[1], v[2])
end
else
t:insert(String:new(r:format(state)), tags)
end
end
return t
end,
}
package.loaded[...] = TextInterpolation
Text, String = ast.Text, ast.String
return TextInterpolation

66
ast/Tuple.lua Normal file
View file

@ -0,0 +1,66 @@
local ast = require("ast")
local operator_priority = require("common").operator_priority
local Tuple
Tuple = ast.abstract.Node {
type = "tuple",
explicit = true, -- false for implicitely created tuples, e.g. 1,2,3 without the brackets []
list = nil,
init = function(self, ...)
self.list = { ... }
end,
insert = function(self, val) -- only for construction
table.insert(self.list, val)
end,
_format = function(self, state, prio, ...)
local l = {}
for _, e in ipairs(self.list) do
table.insert(l, e:format(state, operator_priority["_,_"], ...))
end
return ("[%s]"):format(table.concat(l, ", "))
end,
traverse = function(self, fn, ...)
for _, e in ipairs(self.list) do
fn(e, ...)
end
end,
_eval = function(self, state)
local t = Tuple:new()
for _, e in ipairs(self.list) do
t:insert(e:eval(state))
end
if not self.explicit then
t.explicit = false
end
return t
end,
copy = function(self)
local t = Tuple:new()
for _, e in ipairs(self.list) do
t:insert(e)
end
return t
end,
to_lua = function(self, state)
local l = {}
for _, e in ipairs(self.list) do
table.insert(l, e:to_lua(state))
end
return l
end,
get = function(self, index)
if index < 0 then index = #self.list + 1 + index end
if index > #self.list or index == 0 then error("tuple index out of bounds") end
return self.list[index]
end
}
return Tuple

24
ast/Typed.lua Normal file
View file

@ -0,0 +1,24 @@
local ast = require("ast")
local operator_priority = require("common").operator_priority
return ast.abstract.Runtime {
type = "typed",
expression = nil,
type_expression = nil,
init = function(self, type, expression)
self.type_expression = type
self.expression = expression
end,
_format = function(self, state, prio, ...)
return ("type(%s, %s)"):format(self.type_expression:format(state, operator_priority["_,_"], ...), self.expression:format_right(state, operator_priority["_,_"], ...))
end,
traverse = function(self, fn, ...)
fn(self.type_expression, ...)
fn(self.expression, ...)
end
}

View file

@ -0,0 +1,8 @@
-- called automatically when returned by one of the expression in a block
local ast = require("ast")
return ast.abstract.Node {
type = "auto call",
init = false
}

22
ast/abstract/Event.lua Normal file
View file

@ -0,0 +1,22 @@
-- for nodes that can be written to the event buffer
local ast = require("ast")
return ast.abstract.Node {
type = "event",
init = false,
-- returns value that will be yielded by the whole event buffer data on flush
-- by default a list of what is returned by :to_event_data for each event of the buffer
build_event_data = function(self, state, event_buffer)
local l = {}
for _, event in event_buffer:iter(state) do
table.insert(l, event:to_event_data(state))
end
return l
end,
to_event_data = function(self, state) error("unimplemented") end,
-- post_flush_callback(self, state, event_buffer, event_data)
post_flush_callback = false
}

282
ast/abstract/Node.lua Normal file
View file

@ -0,0 +1,282 @@
local class = require("class")
local fmt = require("common").fmt
local binser = require("lib.binser")
-- NODES SHOULD BE IMMUTABLE AFTER CREATION IF POSSIBLE!
-- i don't think i actually rely on this behavior for anything but it makes me feel better about life in general
-- (well, unless node.mutable == true, in which case go ahead and break my little heart)
-- UPDATE: i actually assumed nodes to be immutable by default in a lot of places now, thank you past me, it did indeed make me feel better about life in general
-- reminder: when requiring AST nodes somewhere, try to do it at the end of the file. and if you need to require something in this file, do it in the :_i_hate_cycles method.
-- i've had enough headaches with cyclics references and nodes required several times...
local uuid = require("common").uuid
local State, Runtime
local resumable_manager
local custom_call_identifier
local context_max_length = 50
local function cutoff_text(str)
if str:match("\n") or utf8.len(str) > context_max_length then
local cut_pos = math.min((str:match("()\n") or math.huge)-1, (utf8.offset(str, context_max_length, 1) or math.huge)-1)
str = str:sub(1, cut_pos) .. ""
end
return str
end
local function format_error(state, node, message)
local ctx = cutoff_text(node:format(state)) -- get some context code around error
return fmt("%{red}%s%{reset}\n\t↳ from %{underline}%s%{reset} in %s: %{dim}%s", message, node.source, node.type, ctx)
end
-- traverse helpers
local traverse
traverse = {
set_source = function(self, source)
self:set_source(source)
end,
prepare = function(self, state)
self:prepare(state)
end,
merge = function(self, state, cache)
self:merge(state, cache)
end,
hash = function(self, t)
table.insert(t, self:hash())
end
}
local Node
Node = class {
type = "node",
source = "?",
mutable = false,
-- abstract class
-- must be redefined
init = false,
-- set the source of this node and its children (unless a source is already set)
-- to be preferably used during construction only
set_source = function(self, source)
local str_source = tostring(source)
if self.source == "?" then
self.source = str_source
self:traverse(traverse.set_source, str_source)
end
return self
end,
-- call function callback with args ... on the children Nodes of this Node
-- by default, assumes no children Nodes
-- you will want to redefine this for nodes with children nodes
-- (note: when calling, remember that cycles are common place in the AST, so stay safe use a cache)
traverse = function(self, callback, ...) end,
-- returns new AST
-- whatever this function returned is assumed to be already evaluated
-- the actual evaluation is done in _eval
eval = function(self, state)
if self._evaluated then return self end
local s, r = pcall(self._eval, self, state)
if s then
r._evaluated = true
return r
else
error(format_error(state, self, r), 0)
end
end,
_evaluated = false, -- if true, node is assumed to be already evaluated and :eval will be the identity function
-- evaluate this node and return the result
-- by default assume the node can't be evaluated further and return itself; redefine for everything else, probably
-- THIS SHOULD NOT MUTATE THE CURRENT NODE; create and return a new Node instead! (even if node is mutable)
_eval = function(self, state)
return self
end,
-- prepare the AST after parsing and before evaluation
-- this behave like a cached :traverse through the AST, except this keeps track of the scope stack
-- i.e. when :prepare is called on a node, it should be in a similar scope stack context as will be when it will be evaluated
-- used to predefine exported variables and other compile-time variable handling
-- note: the state here is a temporary state only used during the prepare step
-- the actual preparation is done in _prepare
-- (this can mutate the node as needed and is automatically called after each parse)
prepare = function(self, state)
assert(not Runtime:issub(self), ("can't prepare a %s node that should only exist at runtime"):format(self.type))
state = state or State:new()
if self._prepared then return end
local s, r = pcall(self._prepare, self, state)
if s then
self._prepared = true
else
error(format_error(state, self, r), 0)
end
end,
_prepared = false, -- indicate that the node was prepared and :prepare should nop
-- prepare this node. can mutate the node (considered to be part of construction).
_prepare = function(self, state)
self:traverse(traverse.prepare, state)
end,
-- same as eval, but make the evaluated expression as a resume boundary
-- i.e. if a checkpoint is defined somewhere in this eval, it will start back from this node eval when resuming
eval_resumable = function(self, state)
return resumable_manager:eval(state, self)
end,
-- set the current resume data for this node
-- (relevant inside :eval)
set_resume_data = function(self, state, data)
resumable_manager:set_data(state, self, data)
end,
-- get the current resume data for this node
get_resume_data = function(self, state)
return resumable_manager:get_data(state, self)
end,
-- returns true if the current node is in a resuming state
-- (relevant inside :eval)
resuming = function(self, state)
return resumable_manager:resuming(state, self)
end,
-- return result AST
-- arg is a ArgumentTuple node (already evaluated)
-- redefine if relevant
call = function(self, state, arg)
if state.scope:defined(custom_call_identifier) then
local custom_call = custom_call_identifier:eval(state)
return custom_call:call(state, arg:with_first_argument(self))
else
error("trying to call a "..self.type..": "..self:format(state))
end
end,
-- merge any changes back into the main branch
-- cache is a table indicating nodes when the merge has already been triggered { [node] = true, ... }
-- (just give an empty table on the initial call)
-- redefine :_merge if needed, not this
merge = function(self, state, cache)
if not cache[self] then
cache[self] = true
self:_merge(state, cache)
self:traverse(traverse.merge, state, cache)
end
end,
_merge = function(self, state, cache) end,
-- return string that uniquely represent this node
-- the actual hash is computed in :_hash, don't redefine :hash directly
-- note: if the node is mutable, this will return a UUID instead of calling :_hash
hash = function(self)
if not self._hash_cache then
if self.mutable then
self._hash_cache = uuid()
else
self._hash_cache = self:_hash()
end
end
return self._hash_cache
end,
_hash_cache = nil, -- cached hash
-- return string that uniquely represent this node
-- by default, build a "node type<children node hash;...>" representation using :traverse
-- you may want to redefine this for base types and other nodes with discriminating info that's not in children nodes.
-- also beware if :traverse uses pairs() or any other non-deterministic function, it'd be nice if this was properly bijective...
-- (no need to redefine for mutable nodes, since an uuid is used instead)
_hash = function(self)
local t = {}
self:traverse(traverse.hash, t)
return ("%s<%s>"):format(self.type, table.concat(t, ";"))
end,
-- return a pretty string representation of the node.
-- for non-runtime nodes (what was generated by a parse without any evaluation), this should return valid Anselme code that is functionnally equivalent to the parsed code. note that it currently does not preserve comment.
-- redefine _format, not this - note that _format is a mandary method for all nodes.
-- state is optional and should only be relevant for runtime nodes; if specified, only show what is relevant for the current branch.
-- indentation_level and parent_priority are optional value that respectively keep track in nester :format calls of the indentation level (number) and parent operator priority (number); if the node has a strictly lower priority than the parent node, parentheses will be added
-- also remember that execution is done left-to-right, so in case of priority equality, all is fine if the term appear left of the operator, but parentheses will need to be added if the term is right of the operator - so make sure to call :format_right for such cases
-- (:format is not cached as even immutable nodes may contain mutable children)
format = function(self, state, parent_priority, indentation_level)
indentation_level = indentation_level or 0
parent_priority = parent_priority or 0
local s = self:_format(state, self.format_priority, indentation_level)
if self.format_priority < parent_priority then
s = ("(%s)"):format(s)
end
local indentation = ("\t"):rep(indentation_level)
s = s:gsub("\n", "\n"..indentation)
return s
end,
-- same as :format, but should be called only for nodes right of the current operator
format_right = function(self, state, parent_priority, indentation_level)
indentation_level = indentation_level or 0
parent_priority = parent_priority or 0
local s = self:_format(state, self.format_priority, indentation_level)
if self.format_priority <= parent_priority then
s = ("(%s)"):format(s)
end
local indentation = (" "):rep(indentation_level)
s = indentation..s:gsub("\n", "\n"..indentation)
return s
end,
-- redefine this to provide a custom :format. returns a string.
_format = function(self, state, self_priority, identation)
error("format not implemented for "..self.type)
end,
-- priority of the node that will be used in :format to add eventually needed parentheses.
-- should not be modified after object construction!
format_priority = math.huge, -- by default, assumes primary node, i.e. never wrap in parentheses
-- return Lua value
-- this should probably be only called on a Node that is already evaluated
-- redefine if you want, probably only for nodes that are already evaluated
to_lua = function(self, state)
error("cannot convert "..self.type.." to a Lua value")
end,
-- returns truthiness of node
-- redefine for false stuff
truthy = function(self)
return true
end,
-- register the node for serialization on creation
__created = function(self)
if self.init then -- only call on non-abstract node
binser.register(self, self.type)
end
end,
__tostring = function(self) return self:format() end,
-- Node is required by every other AST node, some of which exist in cyclic require loops.
-- Delaying the requires in each node after it is defined is enough to fix it, but not for abstract Nodes, since because we are subclassing each node from
-- them, we need them to be available BEFORE the Node is defined. But Node require several other modules, which themselves require some other AST...
-- The worst thing with this kind of require loop combined with our existing cycle band-aids is that Lua won't error, it will just execute the first node to subclass from Node twice. Which is annoying since now we have several, technically distinct classes representing the same node frolicking around.
-- Thus, any require here that may require other Nodes shall be done here. This method is called in anselme.lua after everything else is required.
_i_hate_cycles = function(self)
local ast = require("ast")
custom_call_identifier = ast.Identifier:new("_!")
Runtime = ast.abstract.Runtime
State = require("state.State")
resumable_manager = require("state.resumable_manager")
end,
_debug_traverse = function(self, level)
level = level or 0
local t = {}
self:traverse(function(v) table.insert(t, v:_debug_ast(level+1)) end)
return ("%s%s:\n%s"):format((" "):rep(level), self.type, table.concat(t, "\n"))
end,
}
return Node

View file

@ -0,0 +1,27 @@
local ast = require("ast")
return ast.abstract.Node {
type = "overloadable",
init = false,
-- return specificity (number>=0), secondary specificity (number >=0)
-- return false, failure message (string)
compatible_with_arguments = function(self, state, args)
error("not implemented for "..self.type)
end,
-- same as :call, but assumes :compatible_with_arguments was checked before the call
call_compatible = function(self, state, args)
error("not implemented for "..self.type)
end,
-- return string
format_parameters = function(self, state)
return self:format(state)
end,
-- default for :call
call = function(self, state, args)
assert(self:compatible_with_arguments(state, args))
return self:call_compatible(state, args)
end
}

12
ast/abstract/Runtime.lua Normal file
View file

@ -0,0 +1,12 @@
-- indicate a Runtime node: it should not exist in the AST generated by the parser but only as a result of an evaluation or call
-- is assumed to be already evaluated and prepared (will actually error on prepare)
local ast = require("ast")
return ast.abstract.Node {
type = "runtime",
init = false,
_evaluated = true,
_prepared = true
}

13
ast/init.lua Normal file
View file

@ -0,0 +1,13 @@
return setmetatable({
abstract = setmetatable({}, {
__index = function(self, key)
self[key] = require("ast.abstract."..key)
return self[key]
end
})
}, {
__index = function(self, key)
self[key] = require("ast."..key)
return self[key]
end
})