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

Move anselme code into its own directory

This commit is contained in:
Étienne Fildadut 2023-12-29 18:41:06 +01:00
parent 404e7dd56e
commit 5dd971ff8f
179 changed files with 603 additions and 579 deletions

35
anselme/ast/Anchor.lua Normal file
View file

@ -0,0 +1,35 @@
local ast = require("anselme.ast")
local resume_manager
local Anchor
Anchor = ast.abstract.Node {
type = "anchor",
name = nil,
init = function(self, name)
self.name = name
self._list_anchors_cache = { [name] = true }
end,
_hash = function(self)
return ("anchor<%q>"):format(self.name)
end,
_format = function(self, ...)
return "#"..self.name
end,
_eval = function(self, state)
if self:contains_resume_target(state) then
resume_manager:set_reached(state)
end
return Anchor:new(self.name)
end
}
package.loaded[...] = Anchor
resume_manager = require("anselme.state.resume_manager")
return Anchor

View file

@ -0,0 +1,206 @@
local ast = require("anselme.ast")
local Identifier, Number
local operator_priority = require("anselme.common").operator_priority
local ArgumentTuple
ArgumentTuple = ast.abstract.Node {
type = "argument tuple",
arguments = nil,
positional = nil, -- list of expr - can be sparse! but for each hole there should be an associated named arg
named = nil, -- { [string name] = arg1, [pos number] = string name, ... }
assignment = nil, -- expr; always the last argument if set
arity = 0, -- number of arguments, i.e. number of positional+named+assignment arguments
init = function(self, ...)
self.positional = { ... }
self.named = {}
self.arity = #self.positional
end,
add_positional = function(self, val) -- only for construction
assert(not (self.positional[self.arity+1]) or self.assignment)
self.arity = self.arity + 1
self.positional[self.arity] = val
end,
add_named = function(self, identifier, val) -- only for construction
local name = identifier.name
assert(not (self.named[name] or self.assignment))
self.arity = self.arity + 1
self.named[name] = val
self.named[self.arity] = name
end,
add_assignment = function(self, val) -- only for construction
assert(not self.assignment)
self.arity = self.arity + 1
self.assignment = val
self.format_priority = operator_priority["_=_"]
end,
_format = function(self, state, priority, ...)
local l = {}
for i=1, self.arity do
if self.positional[i] then
table.insert(l, self.positional[i]:format(state, operator_priority["_,_"], ...))
elseif self.named[i] then
local name = self.named[i]
table.insert(l, name.."="..self.named[name]:format_right(state, operator_priority["_=_"], ...))
else
break
end
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 i=1, self.arity do
if self.positional[i] then
fn(self.positional[i], ...)
elseif self.named[i] then
fn(self.named[self.named[i]], ...)
else
fn(self.assignment, ...)
end
end
end,
_eval = function(self, state)
local r = ArgumentTuple:new()
for i=1, self.arity do
if self.positional[i] then
r:add_positional(self.positional[i]:eval(state))
elseif self.named[i] then
r:add_named(Identifier:new(self.named[i]), self.named[self.named[i]]:eval(state))
else
r:add_assignment(self.assignment:eval(state))
end
end
return r
end,
-- recreate new argumenttuple with a first positional argument added
with_first_argument = function(self, first)
local r = ArgumentTuple:new()
r:add_positional(first)
for i=1, self.arity do
if self.positional[i] then
r:add_positional(self.positional[i])
elseif self.named[i] then
r:add_named(Identifier:new(self.named[i]), self.named[self.named[i]])
else
r:add_assignment(self.assignment)
end
end
return r
end,
-- recreate new argumenttuple with an assignment argument added
with_assignment = function(self, assignment)
local r = ArgumentTuple:new()
for i=1, self.arity do
if self.positional[i] then
r:add_positional(self.positional[i])
elseif self.named[i] then
r:add_named(Identifier:new(self.named[i]), self.named[self.named[i]])
else
r:add_assignment(self.assignment) -- welp it'll error below anyway
end
end
r:add_assignment(assignment)
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.positional[i] then
used_list[i] = true
arg = self.positional[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 (assume ok for default values)
if param.type_check and arg ~= param.default 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=1, self.arity do
if self.positional[i] then
if not used_list[i] then
return false, ("%sth positional argument is unused"):format(i)
end
elseif self.named[i] then
if not used_named[self.named[i]] then
return false, ("named argument %s is unused"):format(self.named[i])
end
else
break
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.positional[i] then
state.scope:define(arg.identifier:to_symbol(), self.positional[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

View file

@ -0,0 +1,37 @@
local ast = require("anselme.ast")
local Nil
local operator_priority = require("anselme.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

81
anselme/ast/Block.lua Normal file
View file

@ -0,0 +1,81 @@
local ast = require("anselme.ast")
local Nil, Return, AutoCall, ArgumentTuple, Flush
local resume_manager = require("anselme.state.resume_manager")
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:contains_resume_target(state) then
local anchor = resume_manager:get(state)
local resumed = false
for _, e in ipairs(self.expressions) do
if e:contains_anchor(anchor) 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
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
anselme/ast/Boolean.lua Normal file
View file

@ -0,0 +1,28 @@
local ast = require("anselme.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
anselme/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("anselme.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
anselme/ast/Call.lua Normal file
View file

@ -0,0 +1,86 @@
local ast = require("anselme.ast")
local Identifier
local regular_operators = require("anselme.common").regular_operators
local operator_priority = require("anselme.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.positional[1]:format(...)
local right = self.arguments.positional[2]:format_right(...)
return ("%s %s %s"):format(left, name:match("^_(.*)_$"), right)
elseif prefix[name] and arity == 1 then
local right = self.arguments.positional[1]:format_right(...)
return ("%s%s"):format(name:match("^(.*)_$"), right)
elseif suffix[name] and arity == 1 then
local left = self.arguments.positional[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
anselme/ast/Choice.lua Normal file
View file

@ -0,0 +1,52 @@
local ast = require("anselme.ast")
local ArgumentTuple
local operator_priority = require("anselme.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
anselme/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("anselme.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_dispatched = function(self, state, args)
state.scope:push(self.exported_scope)
local exp = self.func:call_dispatched(state, args)
state.scope:pop()
return exp
end,
}
package.loaded[...] = Closure
Definition = ast.Definition
return Closure

View file

@ -0,0 +1,63 @@
local ast = require("anselme.ast")
local Nil, Overloadable
local operator_priority = require("anselme.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)
if symbol.alias then
state.scope:define_alias(symbol, self.expression)
else
local val = self.expression:eval(state)
if Overloadable:issub(val) then
state.scope:define_overloadable(symbol, val)
else
state.scope:define(symbol, val)
end
end
return Nil:new()
end,
_prepare = function(self, state)
local symbol, val = self.symbol, self.expression
symbol:prepare(state)
val:prepare(state)
-- predefine exported variables
if symbol.exported then
self:eval(state)
end
end
}
package.loaded[...] = Definition
Nil, Overloadable = ast.Nil, ast.abstract.Overloadable
return Definition

237
anselme/ast/Environment.lua Normal file
View file

@ -0,0 +1,237 @@
local ast = require("anselme.ast")
local operator_priority = require("anselme.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)
if self.symbol.alias then
return self.branched:get(state):call(state, ArgumentTuple:new())
else
return self.branched:get(state)
end
end,
get_symbol = function(self)
return self.symbol
end,
set = function(self, state, value)
if self.symbol.constant then
error(("trying to change the value of constant %s"):format(self.symbol.string), 0)
end
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), 0) end
end
if self.symbol.alias then
local assign_args = ArgumentTuple:new()
assign_args:add_assignment(value)
self.branched:get(state):call(state, assign_args)
else
self.branched:set(state, value)
end
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", 0)
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), 0)
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,
define_alias = function(self, state, symbol, call)
assert(symbol.alias, "symbol is not an alias")
assert(call.type == "call", "alias expression must be a call")
local get = ast.Function:new(ast.ParameterTuple:new(), call):eval(state)
if symbol.constant then
self:define(state, symbol, get)
else
local set_param = ast.ParameterTuple:new()
set_param:insert_assignment(ast.FunctionParameter:new(ast.Identifier:new("value")))
local assign_expr = ast.Call:new(call.func, call.arguments:with_assignment(ast.Identifier:new("value")))
local set = ast.Function:new(set_param, assign_expr):eval(state)
self:define(state, symbol, ast.Overload:new(get, set))
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,
-- get the symbol that was used to define the variable in current or parent environment
get_symbol = function(self, state, identifier)
return self:_get_variable(state, identifier):get_symbol()
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 exported variables (evaluated) 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
r[vm.symbol] = vm:get(state)
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
anselme/ast/Flush.lua Normal file
View file

@ -0,0 +1,28 @@
local ast = require("anselme.ast")
local Nil
local event_manager = require("anselme.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

82
anselme/ast/Function.lua Normal file
View file

@ -0,0 +1,82 @@
-- note: functions only appear in non-evaluated nodes! once evaluated, they always become closures
local ast = require("anselme.ast")
local Overloadable = ast.abstract.Overloadable
local Closure, ReturnBoundary
local operator_priority = require("anselme.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, ...)
for sym, val in pairs(self.exports) do
fn(sym, ...)
fn(val, ...)
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_dispatched = function(self, state, args)
state.scope:push()
args:bind_parameter_tuple(state, self.parameters)
local exp = self.expression:eval(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

View file

@ -0,0 +1,45 @@
local ast = require("anselme.ast")
local operator_priority = require("anselme.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

View file

@ -0,0 +1,44 @@
local ast = require("anselme.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
anselme/ast/List.lua Normal file
View file

@ -0,0 +1,81 @@
local ast = require("anselme.ast")
local Branched, Tuple
local operator_priority = require("anselme.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", 0) 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+1 or index == 0 then error("list index out of bounds", 0) 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

View file

@ -0,0 +1,64 @@
local ast = require("anselme.ast")
local Overloadable = ast.abstract.Overloadable
local operator_priority = require("anselme.common").operator_priority
local unpack = table.unpack or unpack
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_dispatched = 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(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
anselme/ast/Nil.lua Normal file
View file

@ -0,0 +1,20 @@
local ast = require("anselme.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
anselme/ast/Number.lua Normal file
View file

@ -0,0 +1,25 @@
local ast = require("anselme.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

61
anselme/ast/Overload.lua Normal file
View file

@ -0,0 +1,61 @@
local ast = require("anselme.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,
dispatch = 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
return nil, ("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))
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, args
else
return nil, ("no function match %s, possible functions were:\n\t• %s"):format(args:format(state), table.concat(failure, "\n\t"))
end
end
}
return Overload

25
anselme/ast/Pair.lua Normal file
View file

@ -0,0 +1,25 @@
local ast = require("anselme.ast")
local operator_priority = require("anselme.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,
}

View file

@ -0,0 +1,67 @@
local ast = require("anselme.ast")
local operator_priority = require("anselme.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

View file

@ -0,0 +1,91 @@
-- create a partial layer to define temporary variables
local ast = require("anselme.ast")
local Identifier, Quote
local attached_block_identifier, attached_block_symbol
local unpack = table.unpack or unpack
local PartialScope
PartialScope = ast.abstract.Node {
type = "partial scope",
expression = nil,
definitions = nil, -- {[sym]=value,...} where values are already evaluated!
_identifiers = nil, -- {identifier,...} - just a cache so we don't rebuild it on every eval
init = function(self, expression)
self.expression = expression
self.definitions = {}
self._identifiers = {}
self.format_priority = self.expression.format_priority
end,
define = function(self, symbol, value) -- for construction only
assert(not self.definitions[symbol], ("%s already defined in partial layer"):format(symbol))
table.insert(self._identifiers, symbol:to_identifier())
self.definitions[symbol] = value
end,
_format = function(self, state, priority, indentation, ...)
if self.definitions[attached_block_symbol] then
local block = self.definitions[attached_block_symbol]
local exp = self.expression:format(state, priority, indentation, ...)
if exp:sub(-2) == " _" then exp = exp:sub(1, -3) end
return exp.."\n\t"..block:format(state, priority, indentation + 1, ...)
else
return self.expression:format(state, priority, indentation, ...)
end
end,
traverse = function(self, fn, ...)
fn(self.expression, ...)
for sym, val in pairs(self.definitions) do
fn(sym, ...)
fn(val, ...)
end
end,
_eval = function(self, state)
state.scope:push_partial(unpack(self._identifiers))
for sym, val in pairs(self.definitions) do state.scope:define(sym, val) end
local exp = self.expression:eval(state)
state.scope:pop()
return exp
end,
_prepare = function(self, state)
state.scope:push_partial(unpack(self._identifiers))
for sym, val in pairs(self.definitions) do state.scope:define(sym, val) end
self.expression:prepare(state)
state.scope:pop()
end,
-- class method: if the identifier is currently defined, wrap node in an PartialScope so the identifier is still defined in this node
-- used to e.g. preserve the defined _ block without the need to build a full closure
-- used e.g. for -> translation, as we want to preserve _ while still executing the translation in the Translatable scope and not restore a different scope from a closure
-- (operates on un-evaluated nodes!)
preserve = function(self, state, expression, ...)
local partial = PartialScope:new(expression)
for _, ident in ipairs{...} do
if state.scope:defined(ident) then
partial:define(state.scope:get_symbol(ident), state.scope:get(ident))
end
end
return partial
end,
-- class method: return a PartialScope that define the block identifier _ to a Quote of `block`
attach_block = function(self, expression, block)
local partial = ast.PartialScope:new(expression)
partial:define(attached_block_symbol, Quote:new(block))
return partial
end
}
package.loaded[...] = PartialScope
Identifier, Quote = ast.Identifier, ast.Quote
attached_block_identifier = Identifier:new("_")
attached_block_symbol = attached_block_identifier:to_symbol()
return PartialScope

41
anselme/ast/Quote.lua Normal file
View file

@ -0,0 +1,41 @@
-- 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.
-- kinda like a function, but no parameters, no closure and no new scope
-- 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("anselme.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,
dispatch = function(self, state, args)
if args.arity == 0 then
return self, args
else
return nil, "Quote! does not accept arguments"
end
end,
call_dispatched = function(self, state, args)
return self.expression:eval(state)
end
}
return Quote

33
anselme/ast/Return.lua Normal file
View file

@ -0,0 +1,33 @@
local ast = require("anselme.ast")
local operator_priority = require("anselme.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

View file

@ -0,0 +1,37 @@
-- used stop propagating Return when leaving functions
local ast = require("anselme.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
anselme/ast/String.lua Normal file
View file

@ -0,0 +1,34 @@
local ast = require("anselme.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("anselme.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

133
anselme/ast/Struct.lua Normal file
View file

@ -0,0 +1,133 @@
local ast = require("anselme.ast")
local Pair, Number, Nil
local operator_priority = require("anselme.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()
local tuple = self.tuple:eval(state)
for i, e in ipairs(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
table.sort(l)
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,
iter = function(self)
local t, h = self.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,
}
package.loaded[...] = Struct
Pair, Number, Nil = ast.Pair, ast.Number, ast.Nil
return Struct

84
anselme/ast/Symbol.lua Normal file
View file

@ -0,0 +1,84 @@
local ast = require("anselme.ast")
local Identifier, String
local operator_priority = require("anselme.common").operator_priority
local Symbol
Symbol = ast.abstract.Node {
type = "symbol",
string = nil,
constant = nil, -- bool
type_check = nil, -- exp
alias = nil, -- bool
exported = nil, -- bool
confined_to_branch = nil, -- bool
init = function(self, str, modifiers)
modifiers = modifiers or {}
self.string = str
self.constant = modifiers.constant
self.type_check = modifiers.type_check
self.alias = modifiers.alias
self.confined_to_branch = modifiers.confined_to_branch
self.exported = modifiers.exported
if self.type_check then
self.format_priority = operator_priority["_::_"]
end
end,
_eval = function(self, state)
return self:with {
type_check = self.type_check and self.type_check:eval(state)
}
end,
with = function(self, modifiers)
modifiers = modifiers or {}
for _, k in ipairs{"constant", "type_check", "alias", "exported", "confined_to_branch"} do
if modifiers[k] == nil then
modifiers[k] = self[k]
end
end
return Symbol:new(self.string, modifiers)
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.alias 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

79
anselme/ast/Table.lua Normal file
View file

@ -0,0 +1,79 @@
local ast = require("anselme.ast")
local Branched, Struct, Nil = ast.Branched, ast.Struct, ast.Nil
local operator_priority = require("anselme.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 s = self.branched:get(state)
return s:iter()
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
anselme/ast/Text.lua Normal file
View file

@ -0,0 +1,36 @@
local ast = require("anselme.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
}

View file

@ -0,0 +1,59 @@
local ast = require("anselme.ast")
local Text, String
local tag_manager = require("anselme.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

View file

@ -0,0 +1,49 @@
local ast = require("anselme.ast")
local TextInterpolation, String
local operator_priority = require("anselme.common").operator_priority
local translation_manager
local Translatable = ast.abstract.Node {
type = "translatable",
format_priority = operator_priority["%_"],
expression = nil,
init = function(self, expression)
self.expression = expression
self.context = ast.Struct:new()
self.context:set(String:new("source"), String:new(self.expression.source))
if TextInterpolation:is(self.expression) then
self.format_priority = expression.format_priority
end
end,
_format = function(self, ...)
if TextInterpolation:is(self.expression) then -- wrapped in translatable by default
return self.expression:format(...)
else
return "%"..self.expression:format_right(...)
end
end,
traverse = function(self, fn, ...)
fn(self.expression, ...)
end,
_eval = function(self, state)
return translation_manager:eval(state, self.context, self)
end,
list_translatable = function(self, t)
table.insert(t, self)
end
}
package.loaded[...] = Translatable
TextInterpolation, String = ast.TextInterpolation, ast.String
translation_manager = require("anselme.state.translation_manager")
return Translatable

66
anselme/ast/Tuple.lua Normal file
View file

@ -0,0 +1,66 @@
local ast = require("anselme.ast")
local operator_priority = require("anselme.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", 0) end
return self.list[index]
end
}
return Tuple

41
anselme/ast/Typed.lua Normal file
View file

@ -0,0 +1,41 @@
local ast = require("anselme.ast")
local operator_priority = require("anselme.common").operator_priority
local format_identifier
local Typed
Typed = ast.abstract.Runtime {
type = "typed",
expression = nil,
type_expression = nil,
init = function(self, expression, type)
self.expression = expression
self.type_expression = type
end,
_format = function(self, state, prio, ...)
-- try custom format
if state and state.scope:defined(format_identifier) then
local custom_format = format_identifier:eval(state)
local args = ast.ArgumentTuple:new(self)
local fn, d_args = custom_format:dispatch(state, args)
if fn then
return custom_format:call(state, d_args):format(state, prio, ...)
end
end
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.expression, ...)
fn(self.type_expression, ...)
end
}
package.loaded[...] = Typed
format_identifier = ast.Identifier:new("format")
return Typed

View file

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

View file

@ -0,0 +1,22 @@
-- for nodes that can be written to the event buffer
local ast = require("anselme.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
}

View file

@ -0,0 +1,339 @@
local class = require("anselme.lib.class")
local fmt = require("anselme.common").fmt
local binser = require("anselme.lib.binser")
local utf8 = utf8 or require("lua-utf8")
-- 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("anselme.common").uuid
local State, Runtime, Call, Identifier, ArgumentTuple
local resume_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,
list_translatable = function(self, t)
self:list_translatable(t)
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 == "?" and str_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
r:set_source(self.source)
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 and preserve evaluation order
-- i.e. when :prepare is called on a node, it should be in a similar scope stack context and order 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,
-- returns a reversed list { [anchor] = true, ... } of the anchors contained in this node and its children
_list_anchors = function(self)
if not self._list_anchors_cache then
self._list_anchors_cache = {}
self:traverse(function(v)
for name in pairs(v:_list_anchors()) do
self._list_anchors_cache[name] = true
end
end)
end
return self._list_anchors_cache
end,
_list_anchors_cache = nil,
-- returns true if the node or its children contains the anchor
contains_anchor = function(self, anchor)
return not not self:_list_anchors()[anchor.name]
end,
-- returns true if we are currently trying to resume to an anchor target contained in the current node
contains_resume_target = function(self, state)
return resume_manager:resuming(state) and self:contains_anchor(resume_manager:get(state))
end,
-- generate a list of translatable nodes that appear in this node
-- should only be called on non-runtime nodes
-- if a node is translatable, redefine this to add it to the table - note that it shouldn't call :traverse or :list_translatable on its children, as nested translations should not be needed
list_translatable = function(self, t)
t = t or {}
self:traverse(traverse.list_translatable, t)
return t
end,
-- generate anselme code that can be used as a base for a translation file
-- will include every translatable element found in this node and its children
-- TODO: generate more stable context than source position, and only add necessery context to the tag
generate_translation_template = function(self)
local l = self:list_translatable()
local r = {}
for _, tr in ipairs(l) do
table.insert(r, "(("..tr.source.."))")
table.insert(r, Call:new(Identifier:new("_#_"), ArgumentTuple:new(tr.context, Identifier:new("_"))):format())
table.insert(r, "\t"..Call:new(Identifier:new("_->_"), ArgumentTuple:new(tr, tr)):format())
table.insert(r, "")
end
return table.concat(r, "\n")
end,
-- call the node with the given arguments
-- return result AST
-- arg is a ArgumentTuple node (already evaluated)
-- do not redefine; instead redefine :dispatch and :call_dispatched
call = function(self, state, arg)
local dispatched, dispatched_arg = self:dispatch(state, arg)
if dispatched then
return dispatched:call_dispatched(state, dispatched_arg)
else
error(("can't call %s %s: %s"):format(self.type, self:format(state), dispatched_arg), 0)
end
end,
-- find a function that can be called with the given arguments
-- return function, arg if a function is found that can be called with arg. The returned arg may be different than the input arg.
-- return nil, message if no matching function found
dispatch = function(self, state, arg)
-- by default, look for custom call operator
if state.scope:defined(custom_call_identifier) then
local custom_call = custom_call_identifier:eval(state)
local dispatched, dispatched_arg = custom_call:dispatch(state, arg:with_first_argument(self))
if dispatched then
return dispatched, dispatched_arg
else
return nil, dispatched_arg
end
end
return nil, "not callable"
end,
-- call the node with the given arguments
-- this assumes that this node was correctly dispatched to (was returned by a previous call to :dispatch)
-- you can therefore assume that the arguments are valid and compatible with this node
call_dispatched = function(self, state, arg)
error(("%s is not callable"):format(self.type))
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.
-- assuming nothing was mutated in the node, the returned string should remain the same - so if make sure the function is deterministic, e.g. sort if you use pairs()
-- 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("anselme.ast")
Runtime, Call, Identifier, ArgumentTuple = ast.abstract.Runtime, ast.Call, ast.Identifier, ast.ArgumentTuple
custom_call_identifier = Identifier:new("_!")
State = require("anselme.state.State")
resume_manager = require("anselme.state.resume_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,31 @@
-- for nodes that can be put in an Overload
local ast = require("anselme.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,
-- return string
format_parameters = function(self, state)
return self:format(state)
end,
-- can be called either after a successful :dispatch or :compatible_with_arguments
call_dispatched = function(self, state, args)
error("not implemented for "..self.type)
end,
-- default for :dispatch
dispatch = function(self, state, args)
local s, err = self:compatible_with_arguments(state, args)
if s then return self, args
else return nil, err end
end,
}

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("anselme.ast")
return ast.abstract.Node {
type = "runtime",
init = false,
_evaluated = true,
_prepared = true
}

13
anselme/ast/init.lua Normal file
View file

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

71
anselme/common/init.lua Normal file
View file

@ -0,0 +1,71 @@
local escape_cache = {}
local ansicolors = require("anselme.lib.ansicolors")
local common = {
-- escape text to be used as an exact pattern
escape = function(str)
if not escape_cache[str] then
escape_cache[str] = str:gsub("[^%w]", "%%%1")
end
return escape_cache[str]
end,
--- transform an identifier into a clean version (trim each part)
trim = function(str)
return str:match("^%s*(.-)%s*$")
end,
fmt = function(str, ...)
return ansicolors(str):format(...)
end,
uuid = function()
return ("xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx") -- version 4
:gsub("N", math.random(0x8, 0xb)) -- variant 1
:gsub("x", function() return ("%x"):format(math.random(0x0, 0xf)) end) -- random hexadecimal digit
end,
-- list of operators and their priority that are handled through regular function calls & can be overloaded/etc. by the user
regular_operators = {
prefixes = {
{ "~", 3.5 }, -- just below _~_ so else-if (~ condition ~ expression) parses as (~ (condition ~ expression))
{ "!", 11 },
{ "-", 11 },
{ "*", 11 },
{ "%", 11 },
},
suffixes = {
{ ";", 1 },
{ "!", 12 }
},
infixes = {
{ ";", 1 },
{ "#", 2 }, { "->", 2 }, { "~>", 2 },
{ "~", 4 }, { "~?", 4 },
{ "|>", 5 }, { "&", 5 }, { "|", 5 },
{ "==", 7 }, { "!=", 7 }, { ">=", 7 }, { "<=", 7 }, { "<", 7 }, { ">", 7 },
{ "+", 8 }, { "-", 8 },
{ "//", 9 }, { "/", 9 }, { "*", 9 }, { "%", 9 },
{ "^", 10 },
{ "::", 11 },
{ ".", 14 },
{ ":", 5 }
}
},
-- list of all operators and their priority
operator_priority = {
[";_"] = 1,
["$_"] = 1,
["@_"] = 1,
["_,_"] = 2,
["_=_"] = 3,
["_!_"] = 12,
["_()"] = 13
-- generated at run-time for regular operators
}
}
local function store_priority(t, fmt)
for _, v in ipairs(t) do common.operator_priority[fmt:format(v[1])] = v[2] end
end
store_priority(common.regular_operators.infixes, "_%s_")
store_priority(common.regular_operators.prefixes, "%s_")
store_priority(common.regular_operators.suffixes, "_%s")
return common

View file

@ -0,0 +1,26 @@
local ast = require("anselme.ast")
local Number, Struct, String, Nil, Boolean
local function to_anselme(val)
if type(val) == "number" then
return Number:new(val)
elseif type(val) == "table" then
local s = Struct:new()
for k, v in pairs(val) do
s:set(to_anselme(k), to_anselme(v))
end
return s
elseif type(val) == "string" then
return String:new(val)
elseif type(val) == "nil" then
return Nil:new()
elseif type(val) == "boolean" then
return Boolean:new(val)
else
error("can't convert "..type(val).." to an Anselme value")
end
end
Number, Struct, String, Nil, Boolean = ast.Number, ast.Struct, ast.String, ast.Nil, ast.Boolean
return to_anselme

93
anselme/init.lua Normal file
View file

@ -0,0 +1,93 @@
--- The main module.
-- Naming conventions:
-- * Classes
-- * everything_else
-- * (note: "classes" that are not meat to be instancied and are just here to benefit from inheritance fall into everything_else, e.g. parsing classes)
--- Usage:
-- ```lua
-- local anselme = require("anselme")
--
-- -- create a new state
-- local state = anselme.new()
-- state:load_stdlib()
--
-- -- read an anselme script file
-- local f = assert(io.open("script.ans"))
-- local script = anselme.parse(f:read("*a"), "script.ans")
-- f:close()
--
-- -- load the script in a new branch
-- local run_state = state:branch()
-- run_state:run(script)
--
-- -- run the script
-- while run_state:active() do
-- local e, data = run_state:step()
-- if e == "text" then
-- for _, l in ipairs(data) do
-- print(l:format(run_state))
-- end
-- elseif e == "choice" then
-- for i, l in ipairs(data) do
-- print(("%s> %s"):format(i, l:format(run_state)))
-- end
-- local choice = tonumber(io.read("*l"))
-- data:choose(choice)
-- elseif e == "return" then
-- run_state:merge()
-- elseif e == "error" then
-- error(data)
-- end
-- end
-- ```
--
-- If `require("anselme")` fails with an error similar to `module 'anselme' not found`, you might need to redefine `package.path` before the require:
-- ```lua
-- package.path = "path/?/init.lua;path/?.lua;" .. package.path -- where path is the directory where anselme is located
-- require("anselme")
-- ```
-- Anselme expects that `require("anselme.module")` will try loading both `anselme/module/init.lua` and `anselme/module.lua`, which may not be the case without the above code as `package.path`'s default value is system dependent, i.e. not my problem.
local parser, State
local anselme = {
--- Global version string. Follow semver.
version = "2.0.0-alpha",
--- Table containing per-category version numbers. Incremented by one for any change that may break compatibility.
versions = {
--- Version number for languages and standard library changes.
language = 27,
--- Version number for save/AST format changes.
save = 4,
--- Version number for Lua API changes.
api = 8
},
--- Parse a `code` string and return the generated AST.
--
-- `source` is an optional string; it will be used as the code source name in error messages.
--
-- Usage:
-- ```lua
-- local ast = anselme.parse("1 + 2", "test")
-- ast:eval()
-- ```
parse = function(code, source)
return parser(code, source)
end,
--- Return a new [State](#state).
new = function()
return State:new()
end,
}
package.loaded[...] = anselme
parser = require("anselme.parser")
State = require("anselme.state.State")
require("anselme.ast.abstract.Node"):_i_hate_cycles()
return anselme

100
anselme/lib/ansicolors.lua Normal file
View file

@ -0,0 +1,100 @@
-- ansicolors.lua v1.0.2 (2012-08)
-- Copyright (c) 2009 Rob Hoelz <rob@hoelzro.net>
-- Copyright (c) 2011 Enrique García Cota <enrique.garcia.cota@gmail.com>
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.
-- support detection
local function isWindows()
return type(package) == 'table' and type(package.config) == 'string' and package.config:sub(1,1) == '\\'
end
local supported = not isWindows()
if isWindows() then supported = os.getenv("ANSICON") end
local keys = {
-- reset
reset = 0,
-- misc
bright = 1,
dim = 2,
underline = 4,
blink = 5,
reverse = 7,
hidden = 8,
-- foreground colors
black = 30,
red = 31,
green = 32,
yellow = 33,
blue = 34,
magenta = 35,
cyan = 36,
white = 37,
-- background colors
blackbg = 40,
redbg = 41,
greenbg = 42,
yellowbg = 43,
bluebg = 44,
magentabg = 45,
cyanbg = 46,
whitebg = 47
}
local escapeString = string.char(27) .. '[%dm'
local function escapeNumber(number)
return escapeString:format(number)
end
local function escapeKeys(str)
if not supported then return "" end
local buffer = {}
local number
for word in str:gmatch("%w+") do
number = keys[word]
assert(number, "Unknown key: " .. word)
table.insert(buffer, escapeNumber(number) )
end
return table.concat(buffer)
end
local function replaceCodes(str)
str = string.gsub(str,"(%%{(.-)})", function(_, str) return escapeKeys(str) end )
return str
end
-- public
local function ansicolors( str )
str = tostring(str or '')
return replaceCodes('%{reset}' .. str .. '%{reset}')
end
return setmetatable({noReset = replaceCodes}, {__call = function (_, str) return ansicolors (str) end})

753
anselme/lib/binser.lua Normal file
View file

@ -0,0 +1,753 @@
-- binser.lua
--[[
Copyright (c) 2016-2019 Calvin Rose
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
local assert = assert
local error = error
local select = select
local pairs = pairs
local getmetatable = getmetatable
local setmetatable = setmetatable
local type = type
local loadstring = loadstring or load
local concat = table.concat
local char = string.char
local byte = string.byte
local format = string.format
local sub = string.sub
local dump = string.dump
local floor = math.floor
local frexp = math.frexp
local unpack = unpack or table.unpack
local huge = math.huge
-- Lua 5.3 frexp polyfill
-- From https://github.com/excessive/cpml/blob/master/modules/utils.lua
if not frexp then
local log, abs, floor = math.log, math.abs, math.floor
local log2 = log(2)
frexp = function(x)
if x == 0 then return 0, 0 end
local e = floor(log(abs(x)) / log2 + 1)
return x / 2 ^ e, e
end
end
local function pack(...)
return {...}, select("#", ...)
end
local function not_array_index(x, len)
return type(x) ~= "number" or x < 1 or x > len or x ~= floor(x)
end
local function type_check(x, tp, name)
assert(type(x) == tp,
format("Expected parameter %q to be of type %q.", name, tp))
end
local bigIntSupport = false
local isInteger
if math.type then -- Detect Lua 5.3
local mtype = math.type
bigIntSupport = loadstring[[
local char = string.char
return function(n)
local nn = n < 0 and -(n + 1) or n
local b1 = nn // 0x100000000000000
local b2 = nn // 0x1000000000000 % 0x100
local b3 = nn // 0x10000000000 % 0x100
local b4 = nn // 0x100000000 % 0x100
local b5 = nn // 0x1000000 % 0x100
local b6 = nn // 0x10000 % 0x100
local b7 = nn // 0x100 % 0x100
local b8 = nn % 0x100
if n < 0 then
b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
end
return char(212, b1, b2, b3, b4, b5, b6, b7, b8)
end]]()
isInteger = function(x)
return mtype(x) == 'integer'
end
else
isInteger = function(x)
return floor(x) == x
end
end
-- Copyright (C) 2012-2015 Francois Perrad.
-- number serialization code modified from https://github.com/fperrad/lua-MessagePack
-- Encode a number as a big-endian ieee-754 double, big-endian signed 64 bit integer, or a small integer
local function number_to_str(n)
if isInteger(n) then -- int
if n <= 100 and n >= -27 then -- 1 byte, 7 bits of data
return char(n + 27)
elseif n <= 8191 and n >= -8192 then -- 2 bytes, 14 bits of data
n = n + 8192
return char(128 + (floor(n / 0x100) % 0x100), n % 0x100)
elseif bigIntSupport then
return bigIntSupport(n)
end
end
local sign = 0
if n < 0.0 then
sign = 0x80
n = -n
end
local m, e = frexp(n) -- mantissa, exponent
if m ~= m then
return char(203, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
elseif m == huge then
if sign == 0 then
return char(203, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
else
return char(203, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
end
elseif m == 0.0 and e == 0 then
return char(0xCB, sign, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
end
e = e + 0x3FE
if e < 1 then -- denormalized numbers
m = m * 2 ^ (52 + e)
e = 0
else
m = (m * 2 - 1) * 2 ^ 52
end
return char(203,
sign + floor(e / 0x10),
(e % 0x10) * 0x10 + floor(m / 0x1000000000000),
floor(m / 0x10000000000) % 0x100,
floor(m / 0x100000000) % 0x100,
floor(m / 0x1000000) % 0x100,
floor(m / 0x10000) % 0x100,
floor(m / 0x100) % 0x100,
m % 0x100)
end
-- Copyright (C) 2012-2015 Francois Perrad.
-- number deserialization code also modified from https://github.com/fperrad/lua-MessagePack
local function number_from_str(str, index)
local b = byte(str, index)
if not b then error("Expected more bytes of input.") end
if b < 128 then
return b - 27, index + 1
elseif b < 192 then
local b2 = byte(str, index + 1)
if not b2 then error("Expected more bytes of input.") end
return b2 + 0x100 * (b - 128) - 8192, index + 2
end
local b1, b2, b3, b4, b5, b6, b7, b8 = byte(str, index + 1, index + 8)
if (not b1) or (not b2) or (not b3) or (not b4) or
(not b5) or (not b6) or (not b7) or (not b8) then
error("Expected more bytes of input.")
end
if b == 212 then
local flip = b1 >= 128
if flip then -- negative
b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
end
local n = ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) *
0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
if flip then
return (-n) - 1, index + 9
else
return n, index + 9
end
end
if b ~= 203 then
error("Expected number")
end
local sign = b1 > 0x7F and -1 or 1
local e = (b1 % 0x80) * 0x10 + floor(b2 / 0x10)
local m = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
local n
if e == 0 then
if m == 0 then
n = sign * 0.0
else
n = sign * (m / 2 ^ 52) * 2 ^ -1022
end
elseif e == 0x7FF then
if m == 0 then
n = sign * huge
else
n = 0
end
else
n = sign * (1.0 + m / 2 ^ 52) * 2 ^ (e - 0x3FF)
end
return n, index + 9
end
local function newbinser()
-- unique table key for getting next value
local NEXT = {}
local CTORSTACK = {}
-- NIL = 202
-- FLOAT = 203
-- TRUE = 204
-- FALSE = 205
-- STRING = 206
-- TABLE = 207
-- REFERENCE = 208
-- CONSTRUCTOR = 209
-- FUNCTION = 210
-- RESOURCE = 211
-- INT64 = 212
-- TABLE WITH META = 213
local mts = {}
local ids = {}
local serializers = {}
local deserializers = {}
local resources = {}
local resources_by_name = {}
local types = {}
types["nil"] = function(x, visited, accum)
accum[#accum + 1] = "\202"
end
function types.number(x, visited, accum)
accum[#accum + 1] = number_to_str(x)
end
function types.boolean(x, visited, accum)
accum[#accum + 1] = x and "\204" or "\205"
end
function types.string(x, visited, accum)
local alen = #accum
if visited[x] then
accum[alen + 1] = "\208"
accum[alen + 2] = number_to_str(visited[x])
else
visited[x] = visited[NEXT]
visited[NEXT] = visited[NEXT] + 1
accum[alen + 1] = "\206"
accum[alen + 2] = number_to_str(#x)
accum[alen + 3] = x
end
end
local function check_custom_type(x, visited, accum)
local res = resources[x]
if res then
accum[#accum + 1] = "\211"
types[type(res)](res, visited, accum)
return true
end
local mt = getmetatable(x)
local id = mt and ids[mt]
if id then
local constructing = visited[CTORSTACK]
if constructing[x] then
error("Infinite loop in constructor.")
end
constructing[x] = true
accum[#accum + 1] = "\209"
types[type(id)](id, visited, accum)
local args, len = pack(serializers[id](x))
accum[#accum + 1] = number_to_str(len)
for i = 1, len do
local arg = args[i]
types[type(arg)](arg, visited, accum)
end
visited[x] = visited[NEXT]
visited[NEXT] = visited[NEXT] + 1
-- We finished constructing
constructing[x] = nil
return true
end
end
function types.userdata(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
error("Cannot serialize this userdata.")
end
end
function types.table(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
visited[x] = visited[NEXT]
visited[NEXT] = visited[NEXT] + 1
local xlen = #x
local mt = getmetatable(x)
if mt then
accum[#accum + 1] = "\213"
types.table(mt, visited, accum)
else
accum[#accum + 1] = "\207"
end
accum[#accum + 1] = number_to_str(xlen)
for i = 1, xlen do
local v = x[i]
types[type(v)](v, visited, accum)
end
local key_count = 0
for k in pairs(x) do
if not_array_index(k, xlen) then
key_count = key_count + 1
end
end
accum[#accum + 1] = number_to_str(key_count)
for k, v in pairs(x) do
if not_array_index(k, xlen) then
types[type(k)](k, visited, accum)
types[type(v)](v, visited, accum)
end
end
end
end
types["function"] = function(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
visited[x] = visited[NEXT]
visited[NEXT] = visited[NEXT] + 1
local str = dump(x)
accum[#accum + 1] = "\210"
accum[#accum + 1] = number_to_str(#str)
accum[#accum + 1] = str
end
end
types.cdata = function(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, #accum) then return end
error("Cannot serialize this cdata.")
end
end
types.thread = function() error("Cannot serialize threads.") end
local function deserialize_value(str, index, visited)
local t = byte(str, index)
if not t then return nil, index end
if t < 128 then
return t - 27, index + 1
elseif t < 192 then
local b2 = byte(str, index + 1)
if not b2 then error("Expected more bytes of input.") end
return b2 + 0x100 * (t - 128) - 8192, index + 2
elseif t == 202 then
return nil, index + 1
elseif t == 203 or t == 212 then
return number_from_str(str, index)
elseif t == 204 then
return true, index + 1
elseif t == 205 then
return false, index + 1
elseif t == 206 then
local length, dataindex = number_from_str(str, index + 1)
local nextindex = dataindex + length
if not (length >= 0) then error("Bad string length") end
if #str < nextindex - 1 then error("Expected more bytes of string") end
local substr = sub(str, dataindex, nextindex - 1)
visited[#visited + 1] = substr
return substr, nextindex
elseif t == 207 or t == 213 then
local mt, count, nextindex
local ret = {}
visited[#visited + 1] = ret
nextindex = index + 1
if t == 213 then
mt, nextindex = deserialize_value(str, nextindex, visited)
if type(mt) ~= "table" then error("Expected table metatable") end
end
count, nextindex = number_from_str(str, nextindex)
for i = 1, count do
local oldindex = nextindex
ret[i], nextindex = deserialize_value(str, nextindex, visited)
if nextindex == oldindex then error("Expected more bytes of input.") end
end
count, nextindex = number_from_str(str, nextindex)
for i = 1, count do
local k, v
local oldindex = nextindex
k, nextindex = deserialize_value(str, nextindex, visited)
if nextindex == oldindex then error("Expected more bytes of input.") end
oldindex = nextindex
v, nextindex = deserialize_value(str, nextindex, visited)
if nextindex == oldindex then error("Expected more bytes of input.") end
if k == nil then error("Can't have nil table keys") end
ret[k] = v
end
if mt then setmetatable(ret, mt) end
return ret, nextindex
elseif t == 208 then
local ref, nextindex = number_from_str(str, index + 1)
return visited[ref], nextindex
elseif t == 209 then
local count
local name, nextindex = deserialize_value(str, index + 1, visited)
count, nextindex = number_from_str(str, nextindex)
local args = {}
for i = 1, count do
local oldindex = nextindex
args[i], nextindex = deserialize_value(str, nextindex, visited)
if nextindex == oldindex then error("Expected more bytes of input.") end
end
if not name or not deserializers[name] then
error(("Cannot deserialize class '%s'"):format(tostring(name)))
end
local ret = deserializers[name](unpack(args))
visited[#visited + 1] = ret
return ret, nextindex
elseif t == 210 then
local length, dataindex = number_from_str(str, index + 1)
local nextindex = dataindex + length
if not (length >= 0) then error("Bad string length") end
if #str < nextindex - 1 then error("Expected more bytes of string") end
local ret = loadstring(sub(str, dataindex, nextindex - 1))
visited[#visited + 1] = ret
return ret, nextindex
elseif t == 211 then
local resname, nextindex = deserialize_value(str, index + 1, visited)
if resname == nil then error("Got nil resource name") end
local res = resources_by_name[resname]
if res == nil then
error(("No resources found for name '%s'"):format(tostring(resname)))
end
return res, nextindex
else
error("Could not deserialize type byte " .. t .. ".")
end
end
local function serialize(...)
local visited = {[NEXT] = 1, [CTORSTACK] = {}}
local accum = {}
for i = 1, select("#", ...) do
local x = select(i, ...)
types[type(x)](x, visited, accum)
end
return concat(accum)
end
local function make_file_writer(file)
return setmetatable({}, {
__newindex = function(_, _, v)
file:write(v)
end
})
end
local function serialize_to_file(path, mode, ...)
local file, err = io.open(path, mode)
assert(file, err)
local visited = {[NEXT] = 1, [CTORSTACK] = {}}
local accum = make_file_writer(file)
for i = 1, select("#", ...) do
local x = select(i, ...)
types[type(x)](x, visited, accum)
end
-- flush the writer
file:flush()
file:close()
end
local function writeFile(path, ...)
return serialize_to_file(path, "wb", ...)
end
local function appendFile(path, ...)
return serialize_to_file(path, "ab", ...)
end
local function deserialize(str, index)
assert(type(str) == "string", "Expected string to deserialize.")
local vals = {}
index = index or 1
local visited = {}
local len = 0
local val
while true do
local nextindex
val, nextindex = deserialize_value(str, index, visited)
if nextindex > index then
len = len + 1
vals[len] = val
index = nextindex
else
break
end
end
return vals, len
end
local function deserializeN(str, n, index)
assert(type(str) == "string", "Expected string to deserialize.")
n = n or 1
assert(type(n) == "number", "Expected a number for parameter n.")
assert(n > 0 and floor(n) == n, "N must be a poitive integer.")
local vals = {}
index = index or 1
local visited = {}
local len = 0
local val
while len < n do
local nextindex
val, nextindex = deserialize_value(str, index, visited)
if nextindex > index then
len = len + 1
vals[len] = val
index = nextindex
else
break
end
end
vals[len + 1] = index
return unpack(vals, 1, n + 1)
end
local function readFile(path)
local file, err = io.open(path, "rb")
assert(file, err)
local str = file:read("*all")
file:close()
return deserialize(str)
end
-- Resources
local function registerResource(resource, name)
type_check(name, "string", "name")
assert(not resources[resource],
"Resource already registered.")
assert(not resources_by_name[name],
format("Resource %q already exists.", name))
resources_by_name[name] = resource
resources[resource] = name
return resource
end
local function unregisterResource(name)
type_check(name, "string", "name")
assert(resources_by_name[name], format("Resource %q does not exist.", name))
local resource = resources_by_name[name]
resources_by_name[name] = nil
resources[resource] = nil
return resource
end
-- Templating
local function normalize_template(template)
local ret = {}
for i = 1, #template do
ret[i] = template[i]
end
local non_array_part = {}
-- The non-array part of the template (nested templates) have to be deterministic, so they are sorted.
-- This means that inherently non deterministicly sortable keys (tables, functions) should NOT be used
-- in templates. Looking for way around this.
for k in pairs(template) do
if not_array_index(k, #template) then
non_array_part[#non_array_part + 1] = k
end
end
table.sort(non_array_part)
for i = 1, #non_array_part do
local name = non_array_part[i]
ret[#ret + 1] = {name, normalize_template(template[name])}
end
return ret
end
local function templatepart_serialize(part, argaccum, x, len)
local extras = {}
local extracount = 0
for k, v in pairs(x) do
extras[k] = v
extracount = extracount + 1
end
for i = 1, #part do
local name
if type(part[i]) == "table" then
name = part[i][1]
len = templatepart_serialize(part[i][2], argaccum, x[name], len)
else
name = part[i]
len = len + 1
argaccum[len] = x[part[i]]
end
if extras[name] ~= nil then
extracount = extracount - 1
extras[name] = nil
end
end
if extracount > 0 then
argaccum[len + 1] = extras
else
argaccum[len + 1] = nil
end
return len + 1
end
local function templatepart_deserialize(ret, part, values, vindex)
for i = 1, #part do
local name = part[i]
if type(name) == "table" then
local newret = {}
ret[name[1]] = newret
vindex = templatepart_deserialize(newret, name[2], values, vindex)
else
ret[name] = values[vindex]
vindex = vindex + 1
end
end
local extras = values[vindex]
if extras then
for k, v in pairs(extras) do
ret[k] = v
end
end
return vindex + 1
end
local function template_serializer_and_deserializer(metatable, template)
return function(x)
local argaccum = {}
local len = templatepart_serialize(template, argaccum, x, 0)
return unpack(argaccum, 1, len)
end, function(...)
local ret = {}
local args = {...}
templatepart_deserialize(ret, template, args, 1)
return setmetatable(ret, metatable)
end
end
-- Used to serialize classes withh custom serializers and deserializers.
-- If no _serialize or _deserialize (or no _template) value is found in the
-- metatable, then the metatable is registered as a resources.
local function register(metatable, name, serialize, deserialize)
if type(metatable) == "table" then
name = name or metatable.name
serialize = serialize or metatable._serialize
deserialize = deserialize or metatable._deserialize
if (not serialize) or (not deserialize) then
if metatable._template then
-- Register as template
local t = normalize_template(metatable._template)
serialize, deserialize = template_serializer_and_deserializer(metatable, t)
else
-- Register the metatable as a resource. This is semantically
-- similar and more flexible (handles cycles).
registerResource(metatable, name)
return
end
end
elseif type(metatable) == "string" then
name = name or metatable
end
type_check(name, "string", "name")
type_check(serialize, "function", "serialize")
type_check(deserialize, "function", "deserialize")
assert((not ids[metatable]) and (not resources[metatable]),
"Metatable already registered.")
assert((not mts[name]) and (not resources_by_name[name]),
("Name %q already registered."):format(name))
mts[name] = metatable
ids[metatable] = name
serializers[name] = serialize
deserializers[name] = deserialize
return metatable
end
local function unregister(item)
local name, metatable
if type(item) == "string" then -- assume name
name, metatable = item, mts[item]
else -- assume metatable
name, metatable = ids[item], item
end
type_check(name, "string", "name")
mts[name] = nil
if (metatable) then
resources[metatable] = nil
ids[metatable] = nil
end
serializers[name] = nil
deserializers[name] = nil
resources_by_name[name] = nil;
return metatable
end
local function registerClass(class, name)
name = name or class.name
if class.__instanceDict then -- middleclass
register(class.__instanceDict, name)
else -- assume 30log or similar library
register(class, name)
end
return class
end
return {
VERSION = "0.0-8",
-- aliases
s = serialize,
d = deserialize,
dn = deserializeN,
r = readFile,
w = writeFile,
a = appendFile,
serialize = serialize,
deserialize = deserialize,
deserializeN = deserializeN,
readFile = readFile,
writeFile = writeFile,
appendFile = appendFile,
register = register,
unregister = unregister,
registerResource = registerResource,
unregisterResource = unregisterResource,
registerClass = registerClass,
newbinser = newbinser
}
end
return newbinser()

169
anselme/lib/class.lua Normal file
View file

@ -0,0 +1,169 @@
--- classtoi v2: finding a sweet spot between classtoi-light and classtoi-heavy
-- aka getlost v2
--
-- usage:
--
-- local class = require("anselme.lib.class")
-- local Vehicle = class {
-- type = "vehicle", -- class name, optional
--
-- stability_threshold = 3, -- class variable, also availabe in instances
-- wheel_count = nil, -- doesn't do anything, but i like to keep track of variables that will need to be defined later in a subclass or a constructor
--
-- init = false, -- abstract class, can't be instanciated
--
-- is_stable = function(self) -- method, available both in class and instances
-- return self.wheel_count > self.stability_threshold
-- end
-- }
--
-- local Car = Vehicle { -- subclassing by calling the parent class; multiple inheritance possible by either chaining calls or passing several tables as arguments
-- type = "car",
-- wheel_count = 4,
-- color = nil,
-- init = function(self, color) -- constructor
-- self.color = color
-- end
-- }
-- local car = Car:new("red") -- instancing
-- print(car:is_stable(), car.color) -- true, "red"
--
-- the default class returned by require("anselme.lib.class") contains a few other default methods that will be inherited by all subclasses
-- see line 99 and further for details & documentation
--
-- design philosophy:
-- do not add feature until we need it
-- what we want to be fast: instance creation, class & instance method call & property acces
-- do not care: class creation
--
-- and if you're wondering, no i'm not using either classtoi-heavy nor classtoi-light in any current project anymore.
--# helper functions #--
-- tostring that ignore __tostring methamethod
local function rawtostring(v)
local mt = getmetatable(v)
setmetatable(v, nil)
local str = tostring(v)
setmetatable(v, mt)
return str
end
-- deep table copy, preserve metatable
local function copy(t, cache)
if cache == nil then cache = {} end
if cache[t] then return cache[t] end
local r = {}
cache[t] = r
for k, v in pairs(t) do
r[k] = type(v) == "table" and copy(v, cache) or v
end
return setmetatable(r, getmetatable(t))
end
-- add val to set
local function add_to_set(set, val)
if not set[val] then
table.insert(set, val)
set[val] = true
end
end
--# class creation logic #--
local class_mt
local function new_class(...)
local class = {}
local include = {...}
for i=1, #include do
local parent = include[i]
parent = parent.__included ~= nil and parent:__included(class) or parent
for k, v in pairs(parent) do
class[k] = v
end
end
class.__index = class
setmetatable(class, class_mt)
return class.__created ~= nil and class:__created() or class
end
class_mt = {
__call = new_class,
__tostring = function(self)
local name = self.type and ("class %q"):format(self.type) or "class"
return rawtostring(self):gsub("^table", name)
end
}
class_mt.__index = class_mt
--# base class and its contents #--
-- feel free to redefine these as needed in your own classes; all of these are also optional and can be deleted.
return new_class {
--- instanciate. arguments are passed to the (eventual) constructor :init.
-- behavior undefined when called on an object.
-- set to false to make class non-instanciable (will give unhelpful error on instanciation attempt).
-- obj = class:new(...)
new = function(self, ...)
local obj = setmetatable({}, self)
return obj.init ~= nil and obj:init(...) or obj
end,
--- constructor. arguments are passed from :new. if :init returns a value, it will be returned by :new instead of the self object.
-- set to false to make class abstract (will give unhelpful error on instanciation attempt), redefine in subclass to make non-abstract again.
-- init = function(self, ...) content... end
init = nil,
--- check if the object is an instance of this class.
-- class:is(obj)
-- obj:is(class)
is = function(self, other) -- class:is(obj)
if getmetatable(self) == class_mt then
return getmetatable(other) == self
else
return other:is(self)
end
end,
--- check if the object is an instance of this class or of a class that inherited this class.
-- parentclass:issub(obj)
-- parentclass:issub(class)
-- obj:issub(parentclass)
issub = function(self, other)
if getmetatable(self) == class_mt then
return other.__parents and other.__parents[self] or self:is(other)
else
return other:issub(self)
end
end,
--- check if self is a class
-- class:isclass()
isclass = function(self)
return getmetatable(self) == class_mt
end,
--- called when included in a new class. if it returns a value, it will be used as the included table instead of the self table.
-- default function tracks parent classes and is needed for :issub to work, and returns a deep copy of the included table.
__included = function(self, into)
-- add to parents
if not into.__parents then
into.__parents = {}
end
local __parents = self.__parents
if __parents then
for i=1, #__parents do
add_to_set(into.__parents, __parents[i])
end
end
add_to_set(into.__parents, self)
-- create copied table
local copied = copy(self)
copied.__parents = nil -- prevent __parents being overwritten
return copied
end,
-- automatically created by __included and needed for :issub to work
-- list and set of classes that are parents of this class: { parent_a, [parent_a] = true, parent_b, [parent_b] = true, ... }
__parents = nil,
--- called on the class when it is created. if it returns a value, it will be returned as the new class instead of the self class.
__created = nil,
--- pretty printing. type is used as the name of the class.
type = "object",
__tostring = function(self)
return rawtostring(self):gsub("^table", self.type)
end
}

39
anselme/parser/Source.lua Normal file
View file

@ -0,0 +1,39 @@
local class = require("anselme.lib.class")
local utf8 = utf8 or require("lua-utf8")
local Source
Source = class {
name = "?",
line = -1,
position = -1,
init = function(self, name, line, position)
self.name = name
self.line = line
self.position = position
end,
increment = function(self, n, ...)
self.position = self.position + n
end,
count = function(self, capture, ...)
self:increment(utf8.len(capture))
return capture, ...
end,
consume = function(self, capture, ...)
self:increment(utf8.len(capture))
return ...
end,
clone = function(self)
return Source:new(self.name, self.line, self.position)
end,
set = function(self, other)
self.name, self.line, self.position = other.name, other.line, other.position
end,
__tostring = function(self)
return ("%s:%s:%s"):format(self.name, self.line, self.position)
end
}
return Source

View file

@ -0,0 +1,67 @@
--- transform raw code string into a nested tree of lines
local utf8 = utf8 or require("lua-utf8")
local Source = require("anselme.parser.Source")
local function indented_to_tree(indented)
local tree = {}
local current_parent = tree
local current_level = 0
local last_line_empty = nil
for _, l in ipairs(indented) do
-- indentation of empty line is determined using the next line
-- (consecutive empty lines are merged into one)
if l.content == "" then
last_line_empty = l
else
-- raise indentation
if l.level > current_level then
if #current_parent == 0 then -- can't add children to nil
error(("invalid indentation; at %s"):format(l.source))
end
current_parent = current_parent[#current_parent]
current_level = l.level
-- lower indentation
elseif l.level < current_level then
current_parent = tree
current_level = 0
while current_level < l.level do -- find correct level starting back from the root
current_parent = current_parent[#current_parent]
current_level = current_parent[1].level
end
if current_level ~= l.level then
error(("invalid indentation; at %s"):format(l.source))
end
end
-- add line
if last_line_empty then
last_line_empty.level = current_level
table.insert(current_parent, last_line_empty)
last_line_empty = nil
end
table.insert(current_parent, l)
end
end
return tree
end
local function code_to_indented(code, source_name)
local indented = {}
local i = 1
for line in (code.."\n"):gmatch("(.-)\n") do
local indent, rem = line:match("^(%s*)(.-)$")
local indent_len = utf8.len(indent)
table.insert(indented, { level = indent_len, content = rem, source = Source:new(source_name, i, 1+indent_len) })
i = i + 1
end
return indented
end
return function(code, source_name)
return indented_to_tree(code_to_indented(code, source_name or "?"))
end

View file

@ -0,0 +1,57 @@
local primary = require("anselme.parser.expression.primary.primary")
local comment
comment = primary {
match = function(self, str)
return str:match("^%(%(")
end,
parse = function(self, source, str, limit_pattern)
local rem = source:consume(str:match("^(%(%()(.*)$"))
local content_list = {}
while not rem:match("^%)%)") do
local content
content, rem = rem:match("^([^%(%)]*)(.-)$")
-- cut the text prematurely at limit_pattern if relevant
if limit_pattern and content:match(limit_pattern) then
local pos = content:match("()"..limit_pattern) -- limit_pattern can contain $, so can't directly extract with captures
content, rem = source:count(content:sub(1, pos-1)), ("))%s%s"):format(content:sub(pos), rem)
source:increment(-2)
else
source:count(content)
end
table.insert(content_list, content)
-- nested comment
if rem:match("^%(%(") then
local subcomment
subcomment, rem = comment:parse(source, rem, limit_pattern)
table.insert(content_list, "((")
table.insert(content_list, subcomment)
table.insert(content_list, "))")
-- no end token after the comment
elseif not rem:match("^%)%)") then
-- single ) or (, keep on commentin'
if rem:match("^[%)%(]") then
local s
s, rem = source:count(rem:match("^([%)%(])(.-)$"))
table.insert(content_list, s)
-- anything other than end-of-line
elseif rem:match("[^%s]") then
error(("unexpected %q at end of comment"):format(rem), 0)
-- consumed everything until end-of-line, close your eyes and imagine the text has been closed
else
rem = rem .. "))"
end
end
end
rem = source:consume(rem:match("^(%)%))(.*)$"))
return table.concat(content_list, ""), rem
end
}
return comment

View file

@ -0,0 +1,40 @@
local primary = require("anselme.parser.expression.primary.primary")
local identifier = require("anselme.parser.expression.primary.identifier")
local expression_to_ast = require("anselme.parser.expression.to_ast")
local ast = require("anselme.ast")
local FunctionParameter = ast.FunctionParameter
local operator_priority = require("anselme.common").operator_priority
local assignment_priority = operator_priority["_=_"]
local type_check_priority = operator_priority["_::_"]
return primary {
match = function(self, str)
return identifier:match(str)
end,
parse = function(self, source, str, limit_pattern, no_default_value)
local source_param = source:clone()
-- name
local ident, rem = identifier:parse(source, str)
-- type check
local type_check
if rem:match("^%s*::") then
local scheck = source:consume(rem:match("^(%s*::%s*)(.*)$"))
type_check, rem = expression_to_ast(source, scheck, limit_pattern, type_check_priority)
end
-- default value
local default
if not no_default_value then
if rem:match("^%s*=") then
local sdefault = source:consume(rem:match("^(%s*=%s*)(.*)$"))
default, rem = expression_to_ast(source, sdefault, limit_pattern, assignment_priority)
end
end
return FunctionParameter:new(ident, default, type_check):set_source(source_param), rem
end
}

View file

@ -0,0 +1,7 @@
local function_parameter = require("anselme.parser.expression.contextual.function_parameter")
return function_parameter {
parse = function(self, source, str, limit_pattern)
return function_parameter:parse(source, str, limit_pattern, true)
end
}

View file

@ -0,0 +1,49 @@
local primary = require("anselme.parser.expression.primary.primary")
local function_parameter = require("anselme.parser.expression.contextual.function_parameter")
local function_parameter_no_default = require("anselme.parser.expression.contextual.function_parameter_no_default")
local ast = require("anselme.ast")
local ParameterTuple = ast.ParameterTuple
return primary {
match = function(self, str)
return str:match("^%(")
end,
parse = function(self, source, str, limit_pattern)
local source_start = source:clone()
local parameters = ParameterTuple:new()
local rem = source:consume(str:match("^(%()(.*)$"))
-- i would LOVE to reuse the regular list parsing code for this, but unfortunately the list parsing code
-- itself depends on this and expect this to be available quite early, and it's ANNOYING
while not rem:match("^%s*%)") do
-- parameter
local func_param
func_param, rem = function_parameter:expect(source, rem, limit_pattern)
-- next! comma separator
if not rem:match("^%s*%)") then
if not rem:match("^%s*,") then
error(("unexpected %q at end of argument list"):format(rem), 0)
end
rem = source:consume(rem:match("^(%s*,)(.*)$"))
end
-- add
parameters:insert(func_param)
end
rem = rem:match("^%s*%)(.*)$")
-- assigment param
if rem:match("^%s*=") then
rem = source:consume(rem:match("^(%s*=%s*)(.*)$"))
local func_param
func_param, rem = function_parameter_no_default:expect(source, rem, limit_pattern)
parameters:insert_assignment(func_param)
end
return parameters:set_source(source_start), rem
end
}

View file

@ -0,0 +1,25 @@
local primary = require("anselme.parser.expression.primary.primary")
local identifier = require("anselme.parser.expression.primary.identifier")
local ast = require("anselme.ast")
local Anchor = ast.Anchor
return primary {
match = function(self, str)
if str:match("^#") then
return identifier:match(str:match("^#(.-)$"))
end
return false
end,
parse = function(self, source, str)
local start_source = source:clone()
local rem = source:consume(str:match("^(#)(.-)$"))
local ident
ident, rem = identifier:parse(source, rem)
return Anchor:new(ident.name):set_source(start_source), rem
end
}

View file

@ -0,0 +1,16 @@
local primary = require("anselme.parser.expression.primary.primary")
local ast = require("anselme.ast")
local Identifier, Call, ArgumentTuple = ast.Identifier, ast.Call, ast.ArgumentTuple
return primary {
match = function(self, str)
return str:match("^_")
end,
parse = function(self, source, str)
local source_start = source:clone()
local rem = source:consume(str:match("^(_)(.-)$"))
return Call:new(Identifier:new("_"), ArgumentTuple:new()):set_source(source_start), rem
end
}

View file

@ -0,0 +1,204 @@
local primary = require("anselme.parser.expression.primary.primary")
local function_parameter_no_default = require("anselme.parser.expression.contextual.function_parameter_no_default")
local parameter_tuple = require("anselme.parser.expression.contextual.parameter_tuple")
local identifier = require("anselme.parser.expression.primary.identifier")
local expression_to_ast = require("anselme.parser.expression.to_ast")
local escape = require("anselme.common").escape
local ast = require("anselme.ast")
local Symbol, Definition, Function, ParameterTuple = ast.Symbol, ast.Definition, ast.Function, ast.ParameterTuple
local regular_operators = require("anselme.common").regular_operators
local prefixes = regular_operators.prefixes
local suffixes = regular_operators.suffixes
local infixes = regular_operators.infixes
local operator_priority = require("anselme.common").operator_priority
-- same as function_parameter_no_default, but allow wrapping in (evenetual) parentheses
-- in order to solve some priotity issues (_._ has higher priority than _::_, leading to not being possible to overload it with type filtering without parentheses)
local function_parameter_maybe_parenthesis = function_parameter_no_default {
match = function(self, str)
if str:match("^%(") then
return function_parameter_no_default:match(str:match("^%((.*)$"))
else
return function_parameter_no_default:match(str)
end
end,
parse = function(self, source, str, limit_pattern)
if str:match("^%(") then
str = source:consume(str:match("^(%()(.*)$"))
local exp, rem = function_parameter_no_default:parse(source, str, limit_pattern)
if not rem:match("^%s*%)") then error(("unexpected %q at end of parenthesis"):format(rem), 0) end
rem = source:consume(rem:match("^(%s*%))(.-)$"))
return exp, rem
else
return function_parameter_no_default:parse(source, str, limit_pattern)
end
end
}
-- signature type 1: unary prefix
-- :$-parameter exp
-- returns symbol, parameter_tuple, rem if success
-- return nil otherwise
local function search_prefix_signature(modifiers, source, str, limit_pattern)
for _, pfx in ipairs(prefixes) do
local prefix = pfx[1]
local prefix_pattern = "%s*"..escape(prefix).."%s*"
if str:match("^"..prefix_pattern) then
-- operator name
local rem = source:consume(str:match("^("..prefix_pattern..")(.*)$"))
local symbol = Symbol:new(prefix.."_", modifiers):set_source(source:clone():increment(-1))
-- parameters
local parameter
parameter, rem = function_parameter_maybe_parenthesis:expect(source, rem, limit_pattern)
local parameters = ParameterTuple:new()
parameters:insert(parameter)
return symbol, parameters, rem
end
end
end
-- signature type 2: binary infix
-- should be checked before suffix signature
-- :$parameterA + parameterB exp
-- returns symbol, parameter_tuple, rem if success
-- return nil otherwise
local function search_infix_signature(modifiers, source, str, limit_pattern)
if function_parameter_maybe_parenthesis:match(str) then
local src = source:clone() -- operate on clone source since search success is not yet guaranteed
local parameter_a, rem = function_parameter_maybe_parenthesis:parse(src, str, limit_pattern)
local parameters = ParameterTuple:new()
parameters:insert(parameter_a)
for _, ifx in ipairs(infixes) do
local infix = ifx[1]
local infix_pattern = "%s*"..escape(infix).."%s*"
if rem:match("^"..infix_pattern) then
-- operator name
rem = src:consume(rem:match("^("..infix_pattern..")(.*)$"))
local symbol = Symbol:new("_"..infix.."_", modifiers):set_source(src:clone():increment(-1))
-- parameters
if function_parameter_maybe_parenthesis:match(rem) then
local parameter_b
parameter_b, rem = function_parameter_maybe_parenthesis:parse(src, rem, limit_pattern)
parameters:insert(parameter_b)
source:set(src)
return symbol, parameters, rem
else
return
end
end
end
end
end
-- signature type 3: unary suffix
-- :$parameter! exp
-- returns symbol, parameter_tuple, rem if success
-- return nil otherwise
local function search_suffix_signature(modifiers, source, str, limit_pattern)
if function_parameter_maybe_parenthesis:match(str) then
local src = source:clone() -- operate on clone source since search success is not yet guaranteed
local parameter_a, rem = function_parameter_maybe_parenthesis:parse(src, str, limit_pattern)
local parameters = ParameterTuple:new()
parameters:insert(parameter_a)
for _, sfx in ipairs(suffixes) do
local suffix = sfx[1]
local suffix_pattern = "%s*"..escape(suffix).."%s*"
if rem:match("^"..suffix_pattern) then
-- operator name
rem = src:count(rem:match("^("..suffix_pattern..")(.*)$"))
local symbol = Symbol:new("_"..suffix, modifiers):set_source(src:clone():increment(-1))
source:set(src)
return symbol, parameters, rem
end
end
end
end
-- signature type 4: regular function
-- :$identifier(parameter_tuple, ...) exp
-- returns symbol, parameter_tuple, rem if success
-- return nil otherwise
local function search_function_signature(modifiers, source, str, limit_pattern)
if identifier:match(str) then
local name_source = source:clone()
local name, rem = identifier:parse(source, str, limit_pattern)
-- name
local symbol = name:to_symbol(modifiers):set_source(name_source)
-- parse eventual parameters
local parameters
if parameter_tuple:match(rem) then
parameters, rem = parameter_tuple:parse(source, rem)
else
parameters = ParameterTuple:new()
end
return symbol, parameters, rem
end
end
return primary {
match = function(self, str)
return str:match("^%::?[&@]?%$")
end,
parse = function(self, source, str, limit_pattern)
local source_start = source:clone()
local mod_const, mod_exported, rem = source:consume(str:match("^(%:(:?)([&@]?)%$)(.-)$"))
-- get modifiers
local constant, exported, alias
if mod_const == ":" then constant = true end
if mod_exported == "@" then exported = true
elseif mod_exported == "&" then alias = true end
local modifiers = { constant = constant, exported = exported, alias = alias }
-- search for a valid signature
local symbol, parameters
local s, p, r = search_prefix_signature(modifiers, source, rem, limit_pattern)
if s then symbol, parameters, rem = s, p, r
else
s, p, r = search_infix_signature(modifiers, source, rem, limit_pattern)
if s then symbol, parameters, rem = s, p, r
else
s, p, r = search_suffix_signature(modifiers, source, rem, limit_pattern)
if s then symbol, parameters, rem = s, p, r
else
s, p, r = search_function_signature(modifiers, source, rem, limit_pattern)
if s then symbol, parameters, rem = s, p, r end
end
end
end
-- done
if symbol then
-- parse expression
local right
s, right, rem = pcall(expression_to_ast, source, rem, limit_pattern, operator_priority["$_"])
if not s then error(("invalid expression in function definition: %s"):format(right), 0) end
-- return function
local fn = Function:new(parameters, right):set_source(source_start)
return Definition:new(symbol, fn):set_source(source_start), rem
end
end
}

View file

@ -0,0 +1,40 @@
local primary = require("anselme.parser.expression.primary.primary")
local Identifier = require("anselme.ast.Identifier")
local disallowed_set = (".~`^+-=<>/[]*{}|\\_!?,;:()\"@&$#%"):gsub("[^%w]", "%%%1")
local identifier_pattern = "%s*[^0-9%s'"..disallowed_set.."][^"..disallowed_set.."]*"
local common = require("anselme.common")
local trim, escape = common.trim, common.escape
-- for operator identifiers
local regular_operators = require("anselme.common").regular_operators
local operators = {}
for _, prefix in ipairs(regular_operators.prefixes) do table.insert(operators, prefix[1].."_") end
for _, infix in ipairs(regular_operators.infixes) do table.insert(operators, "_"..infix[1].."_") end
for _, suffix in ipairs(regular_operators.suffixes) do table.insert(operators, "_"..suffix[1]) end
-- all valid identifier patterns
local identifier_patterns = { identifier_pattern }
for _, operator in ipairs(operators) do table.insert(identifier_patterns, "%s*"..escape(operator)) end
return primary {
match = function(self, str)
for _, pat in ipairs(identifier_patterns) do
if str:match("^"..pat) then return true end
end
return false
end,
parse = function(self, source, str)
for _, pat in ipairs(identifier_patterns) do
if str:match("^"..pat) then
local start_source = source:clone()
local name, rem = source:count(str:match("^("..pat..")(.-)$"))
name = trim(name)
return Identifier:new(name):set_source(start_source), rem
end
end
end
}

View file

@ -0,0 +1,43 @@
--- try to parse a primary expression
local function r(name)
return require("anselme.parser.expression.primary."..name), nil
end
local primaries = {
r("number"),
r("string"),
r("text"),
r("parenthesis"),
r("function_definition"),
r("symbol"),
r("identifier"),
r("anchor"),
r("block_identifier"),
r("tuple"),
r("struct"),
-- prefixes
-- 1
r("prefix.semicolon"),
r("prefix.function"),
r("prefix.return"),
-- 3.5
r("prefix.else"),
-- 11
r("prefix.negation"),
r("prefix.not"),
r("prefix.mutable"),
r("prefix.translatable"),
}
return {
-- returns exp, rem if expression found
-- returns nil if no expression found
search = function(self, source, str, limit_pattern)
for _, primary in ipairs(primaries) do
local exp, rem = primary:search(source, str, limit_pattern)
if exp then return exp, rem end
end
end
}

View file

@ -0,0 +1,19 @@
local primary = require("anselme.parser.expression.primary.primary")
local Number = require("anselme.ast.Number")
return primary {
match = function(self, str)
return str:match("^%d*%.%d+") or str:match("^%d+")
end,
parse = function(self, source, str)
local start_source = source:clone()
local d, r = str:match("^(%d*%.%d+)(.*)$")
if not d then
d, r = source:count(str:match("^(%d+)(.*)$"))
else
source:count(d)
end
return Number:new(tonumber(d)):set_source(start_source), r
end
}

View file

@ -0,0 +1,31 @@
-- either parentheses or nil ()
local primary = require("anselme.parser.expression.primary.primary")
local ast = require("anselme.ast")
local Nil = ast.Nil
local expression_to_ast = require("anselme.parser.expression.to_ast")
return primary {
match = function(self, str)
return str:match("^%(")
end,
parse = function(self, source, str)
local start_source = source:clone()
local rem = source:consume(str:match("^(%()(.*)$"))
local exp
if rem:match("^%s*%)") then
exp = Nil:new()
else
local s
s, exp, rem = pcall(expression_to_ast, source, rem, "%)")
if not s then error("invalid expression inside parentheses: "..exp, 0) end
if not rem:match("^%s*%)") then error(("unexpected %q at end of parenthesis"):format(rem), 0) end
end
rem = source:consume(rem:match("^(%s*%))(.*)$"))
return exp:set_source(start_source), rem
end
}

View file

@ -0,0 +1,8 @@
local prefix_quote_right = require("anselme.parser.expression.primary.prefix.prefix_quote_right")
local operator_priority = require("anselme.common").operator_priority
return prefix_quote_right {
operator = "~",
identifier = "~_",
priority = operator_priority["~_"]
}

View file

@ -0,0 +1,35 @@
local prefix = require("anselme.parser.expression.primary.prefix.prefix")
local parameter_tuple = require("anselme.parser.expression.contextual.parameter_tuple")
local escape = require("anselme.common").escape
local expression_to_ast = require("anselme.parser.expression.to_ast")
local ast = require("anselme.ast")
local Function, ParameterTuple = ast.Function, ast.ParameterTuple
local operator_priority = require("anselme.common").operator_priority
return prefix {
operator = "$",
priority = operator_priority["$_"],
parse = function(self, source, str, limit_pattern)
local source_start = source:clone()
local escaped = escape(self.operator)
local rem = source:consume(str:match("^("..escaped..")(.*)$"))
-- parse eventual parameters
local parameters
if parameter_tuple:match(rem) then
parameters, rem = parameter_tuple:parse(source, rem)
else
parameters = ParameterTuple:new()
end
-- parse expression
local s, right
s, right, rem = pcall(expression_to_ast, source, rem, limit_pattern, self.priority)
if not s then error(("invalid expression after unop %q: %s"):format(self.operator, right), 0) end
return Function:new(parameters, right):set_source(source_start), rem
end
}

View file

@ -0,0 +1,9 @@
local prefix = require("anselme.parser.expression.primary.prefix.prefix")
local operator_priority = require("anselme.common").operator_priority
return prefix {
operator = "*",
identifier = "*_",
priority = operator_priority["*_"]
}

View file

@ -0,0 +1,9 @@
local prefix = require("anselme.parser.expression.primary.prefix.prefix")
local operator_priority = require("anselme.common").operator_priority
return prefix {
operator = "-",
identifier = "-_",
priority = operator_priority["-_"]
}

View file

@ -0,0 +1,9 @@
local prefix = require("anselme.parser.expression.primary.prefix.prefix")
local operator_priority = require("anselme.common").operator_priority
return prefix {
operator = "!",
identifier = "!_",
priority = operator_priority["!_"]
}

View file

@ -0,0 +1,34 @@
-- unary prefix operators, for example: the - in -5
local primary = require("anselme.parser.expression.primary.primary")
local escape = require("anselme.common").escape
local expression_to_ast = require("anselme.parser.expression.to_ast")
local ast = require("anselme.ast")
local Call, Identifier, ArgumentTuple = ast.Call, ast.Identifier, ast.ArgumentTuple
return primary {
operator = nil,
identifier = nil,
priority = nil,
match = function(self, str)
local escaped = escape(self.operator)
return str:match("^"..escaped)
end,
parse = function(self, source, str, limit_pattern)
local source_start = source:clone()
local escaped = escape(self.operator)
local sright = source:consume(str:match("^("..escaped..")(.*)$"))
local s, right, rem = pcall(expression_to_ast, source, sright, limit_pattern, self.priority)
if not s then error(("invalid expression after prefix operator %q: %s"):format(self.operator, right), 0) end
return self:build_ast(right):set_source(source_start), rem
end,
build_ast = function(self, right)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(right))
end
}

View file

@ -0,0 +1,21 @@
local prefix = require("anselme.parser.expression.primary.prefix.prefix")
local escape = require("anselme.common").escape
local expression_to_ast = require("anselme.parser.expression.to_ast")
local ast = require("anselme.ast")
local Nil = ast.Nil
return prefix {
parse = function(self, source, str, limit_pattern)
local source_start = source:clone()
local escaped = escape(self.operator)
local sright = source:consume(str:match("^("..escaped..")(.*)$"))
local s, right, rem = pcall(expression_to_ast, source, sright, limit_pattern, self.priority)
if not s then
return self:build_ast(Nil:new()):set_source(source_start), sright
else
return self:build_ast(right):set_source(source_start), rem
end
end,
}

View file

@ -0,0 +1,11 @@
local prefix = require("anselme.parser.expression.primary.prefix.prefix")
local ast = require("anselme.ast")
local Call, Identifier, ArgumentTuple, Quote = ast.Call, ast.Identifier, ast.ArgumentTuple, ast.Quote
return prefix {
build_ast = function(self, right)
right = Quote:new(right)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(right))
end
}

View file

@ -0,0 +1,15 @@
local prefix_maybe_nil_right = require("anselme.parser.expression.primary.prefix.prefix_maybe_nil_right")
local ast = require("anselme.ast")
local Return = ast.Return
local operator_priority = require("anselme.common").operator_priority
return prefix_maybe_nil_right {
operator = "@",
priority = operator_priority["@_"],
build_ast = function(self, right)
return Return:new(right)
end
}

View file

@ -0,0 +1,13 @@
local prefix = require("anselme.parser.expression.primary.prefix.prefix")
local operator_priority = require("anselme.common").operator_priority
return prefix {
operator = ";",
identifier = ";_",
priority = operator_priority[";_"],
build_ast = function(self, right)
return right
end
}

View file

@ -0,0 +1,16 @@
local prefix = require("anselme.parser.expression.primary.prefix.prefix")
local ast = require("anselme.ast")
local Translatable = ast.Translatable
local operator_priority = require("anselme.common").operator_priority
return prefix {
operator = "%",
identifier = "%_",
priority = operator_priority["%_"],
build_ast = function(self, right)
return Translatable:new(right)
end
}

View file

@ -0,0 +1,33 @@
local class = require("anselme.lib.class")
return class {
new = false, -- static class
-- returns exp, rem if expression found
-- returns nil if no expression found
search = function(self, source, str, limit_pattern)
if not self:match(str) then
return nil
end
return self:parse(source, str, limit_pattern)
end,
-- return bool
-- (not needed if you redefined :search)
match = function(self, str)
return false
end,
-- return AST, rem
-- (not needed if you redefined :search)
parse = function(self, source, str, limit_pattern)
error("unimplemented")
end,
-- class helpers --
-- return AST, rem
expect = function(self, source, str, limit_pattern)
local exp, rem = self:search(source, str, limit_pattern)
if not exp then error(("expected %s but got %s"):format(self.type, str)) end
return exp, rem
end
}

View file

@ -0,0 +1,76 @@
-- note: this is reused in primary.text, hence all the configurable fields
local primary = require("anselme.parser.expression.primary.primary")
local ast = require("anselme.ast")
local String, StringInterpolation = ast.String, ast.StringInterpolation
local expression_to_ast = require("anselme.parser.expression.to_ast")
local escape = require("anselme.common").escape
local escape_code = {
["n"] = "\n",
["t"] = "\t",
-- everything else is identity by default
}
return primary {
type = "string", -- interpolation type - used for errors
start_pattern = "\"", -- pattern that start the string interpolation
stop_char = "\"", -- character that stops the string interpolation - must be a single character!
allow_implicit_stop = false, -- set to true to allow the string to be closed implicitely when reaching the end of the expression or limit_pattern
interpolation = StringInterpolation,
match = function(self, str)
return str:match("^"..self.start_pattern)
end,
parse = function(self, source, str, limit_pattern)
local interpolation = self.interpolation:new()
local stop_pattern = escape(self.stop_char)
local start_source = source:clone()
local rem = source:consume(str:match("^("..self.start_pattern..")(.-)$"))
while not rem:match("^"..stop_pattern) do
local text_source = source:clone()
local text
text, rem = rem:match("^([^%{%\\"..stop_pattern.."]*)(.-)$") -- get all text until something potentially happens
-- cut the text prematurely at limit_pattern if relevant
if self.allow_implicit_stop and limit_pattern and text:match(limit_pattern) then
local pos = text:match("()"..limit_pattern) -- limit_pattern can contain $, so can't directly extract with captures
text, rem = source:count(text:sub(1, pos-1)), ("%s%s%s"):format(self.stop_char, text:sub(pos), rem)
source:increment(-1)
else
source:count(text)
end
interpolation:insert(String:new(text):set_source(text_source))
if rem:match("^%{") then
local ok, exp
ok, exp, rem = pcall(expression_to_ast, source, source:consume(rem:match("^(%{)(.*)$")), "%}")
if not ok then error("invalid expression inside interpolation: "..exp, 0) end
if not rem:match("^%s*%}") then error(("unexpected %q at end of interpolation"):format(rem), 0) end
rem = source:consume(rem:match("^(%s*%})(.*)$"))
interpolation:insert(exp)
elseif rem:match("^\\") then
text, rem = source:consume(rem:match("^(\\(.))(.*)$"))
interpolation:insert(String:new(escape_code[text] or text))
elseif not rem:match("^"..stop_pattern) then
if not self.allow_implicit_stop or rem:match("[^%s]") then
error(("unexpected %q at end of "..self.type):format(rem), 0)
-- consumed everything until end-of-line, implicit stop allowed, close your eyes and imagine the text has been closed
else
rem = rem .. self.stop_char
end
end
end
rem = source:consume(rem:match("^("..stop_pattern..")(.*)$"))
return interpolation:set_source(start_source), rem
end
}

View file

@ -0,0 +1,17 @@
local primary = require("anselme.parser.expression.primary.primary")
local tuple = require("anselme.parser.expression.primary.tuple")
local ast = require("anselme.ast")
local Struct = ast.Struct
return primary {
match = function(self, str)
return str:match("^%{")
end,
parse = function(self, source, str)
local l, rem = tuple:parse_tuple(source, str, "{", '}')
return Struct:from_tuple(l), rem
end
}

View file

@ -0,0 +1,40 @@
local primary = require("anselme.parser.expression.primary.primary")
local type_check = require("anselme.parser.expression.secondary.infix.type_check")
local identifier = require("anselme.parser.expression.primary.identifier")
local ast = require("anselme.ast")
local Nil = ast.Nil
return primary {
match = function(self, str)
if str:match("^%::?[&@]?") then
return identifier:match(str:match("^%::?[&@]?(.-)$"))
end
return false
end,
parse = function(self, source, str)
local mod_const, mod_export, rem = source:consume(str:match("^(%:(:?)([&@]?))(.-)$"))
local constant, alias, type_check_exp, exported
-- get modifier
if mod_const == ":" then constant = true end
if mod_export == "&" then alias = true
elseif mod_export == "@" then exported = true end
-- name
local ident
ident, rem = identifier:parse(source, rem)
-- type check
local nil_val = Nil:new()
if type_check:match(rem, 0, nil_val) then
local exp
exp, rem = type_check:parse(source, rem, nil, 0, nil_val)
type_check_exp = exp.arguments.positional[2]
end
return ident:to_symbol{ constant = constant, alias = alias, exported = exported, type_check = type_check_exp }:set_source(source), rem
end
}

View file

@ -0,0 +1,25 @@
local string = require("anselme.parser.expression.primary.string")
local ast = require("anselme.ast")
local TextInterpolation, Translatable = ast.TextInterpolation, ast.Translatable
return string {
type = "text",
start_pattern = "|%s?",
stop_char = "|",
allow_implicit_stop = true,
interpolation = TextInterpolation,
parse = function(self, source, str, limit_pattern)
local start_source = source:clone()
local interpolation, rem = string.parse(self, source, str, limit_pattern)
-- restore | when chaining with a choice operator
if rem:match("^>") then
rem = "|" .. rem
source:increment(-1)
end
return Translatable:new(interpolation):set_source(start_source), rem
end
}

View file

@ -0,0 +1,41 @@
local primary = require("anselme.parser.expression.primary.primary")
local ast = require("anselme.ast")
local Tuple = ast.Tuple
local expression_to_ast = require("anselme.parser.expression.to_ast")
local escape = require("anselme.common").escape
return primary {
match = function(self, str)
return str:match("^%[")
end,
parse = function(self, source, str)
return self:parse_tuple(source, str, "[", "]")
end,
parse_tuple = function(self, source, str, start_char, end_char)
local start_source = source:clone()
local rem = source:consume(str:match("^("..escape(start_char)..")(.*)$"))
local end_match = escape(end_char)
local l
if not rem:match("^%s*"..end_match) then
local s
s, l, rem = pcall(expression_to_ast, source, rem, end_match)
if not s then error("invalid expression in list: "..l, 0) end
end
if not Tuple:is(l) or l.explicit then l = Tuple:new(l) end -- single or no element
if not rem:match("^%s*"..end_match) then
error(("unexpected %q at end of list"):format(rem), 0)
end
rem = source:consume(rem:match("^(%s*"..end_match..")(.*)$"))
l.explicit = true
return l:set_source(start_source), rem
end,
}

View file

@ -0,0 +1,9 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local operator_priority = require("anselme.common").operator_priority
return infix {
operator = "+",
identifier = "_+_",
priority = operator_priority["_+_"]
}

View file

@ -0,0 +1,9 @@
local infix_quote_right = require("anselme.parser.expression.secondary.infix.infix_quote_right")
local operator_priority = require("anselme.common").operator_priority
return infix_quote_right {
operator = "&",
identifier = "_&_",
priority = operator_priority["_&_"]
}

View file

@ -0,0 +1,23 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local escape = require("anselme.common").escape
local operator_priority = require("anselme.common").operator_priority
local ast = require("anselme.ast")
local Identifier, Assignment = ast.Identifier, ast.Assignment
return infix {
operator = "=",
identifier = "_=_",
priority = operator_priority["_=_"],
-- return bool
match = function(self, str, current_priority, primary)
local escaped = escape(self.operator)
return self.priority > current_priority and str:match("^"..escaped) and Identifier:is(primary)
end,
build_ast = function(self, left, right)
return Assignment:new(left, right)
end
}

View file

@ -0,0 +1,24 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local escape = require("anselme.common").escape
local operator_priority = require("anselme.common").operator_priority
local ast = require("anselme.ast")
local Call = ast.Call
return infix {
operator = "=",
identifier = "_=_",
priority = operator_priority["_=_"],
-- return bool
match = function(self, str, current_priority, primary)
local escaped = escape(self.operator)
return self.priority > current_priority and str:match("^"..escaped) and Call:is(primary)
end,
build_ast = function(self, left, right)
local args = left.arguments:with_assignment(right)
return Call:new(left.func, args)
end,
}

View file

@ -0,0 +1,39 @@
local ast = require("anselme.ast")
local Call, Identifier, ArgumentTuple = ast.Call, ast.Identifier, ast.ArgumentTuple
local assignment = require("anselme.parser.expression.secondary.infix.assignment")
local assignment_call = require("anselme.parser.expression.secondary.infix.assignment_call")
local infixes = require("anselme.common").regular_operators.infixes
local operator_priority = require("anselme.common").operator_priority
local generated = {}
for _, infix in ipairs(infixes) do
local compound_operator = infix[1].."="
local identifier = "_=_"
local infix_identifier = "_"..infix[1].."_"
-- avoid a lot of unecessary trouble with <= & friends. why would you ever want to use i <= 7 as i = i < 7 anyway.
if not operator_priority["_"..compound_operator.."_"] then
table.insert(generated, assignment {
operator = compound_operator,
identifier = identifier,
build_ast = function(self, left, right)
right = Call:new(Identifier:new(infix_identifier), ArgumentTuple:new(left, right))
return assignment.build_ast(self, left, right)
end
})
table.insert(generated, assignment_call {
operator = compound_operator,
identifier = identifier,
build_ast = function(self, left, right)
right = Call:new(Identifier:new(infix_identifier), ArgumentTuple:new(left, right))
return assignment_call.build_ast(self, left, right)
end
})
end
end
return generated

View file

@ -0,0 +1,27 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local escape = require("anselme.common").escape
local identifier = require("anselme.parser.expression.primary.identifier")
local operator_priority = require("anselme.common").operator_priority
local ast = require("anselme.ast")
local Call, ArgumentTuple = ast.Call, ast.ArgumentTuple
return infix {
operator = "!",
identifier = "_!_",
priority = operator_priority["_!_"],
match = function(self, str, current_priority, primary)
local escaped = escape(self.operator)
return self.priority > current_priority and str:match("^"..escaped) and identifier:match(str:match("^"..escaped.."%s*(.-)$"))
end,
build_ast = function(self, left, right)
if Call:is(right) then
return Call:new(right.func, right.arguments:with_first_argument(left))
else
return Call:new(right, ArgumentTuple:new(left))
end
end
}

View file

@ -0,0 +1,17 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local operator_priority = require("anselme.common").operator_priority
local ast = require("anselme.ast")
local Call, Identifier, ArgumentTuple, ParameterTuple, Function = ast.Call, ast.Identifier, ast.ArgumentTuple, ast.ParameterTuple, ast.Function
return infix {
operator = "|>",
identifier = "_|>_",
priority = operator_priority["_|>_"],
build_ast = function(self, left, right)
right = Function:new(ParameterTuple:new(), right)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left, right))
end
}

View file

@ -0,0 +1,22 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local escape = require("anselme.common").escape
local operator_priority = require("anselme.common").operator_priority
local ast = require("anselme.ast")
local Definition, Symbol = ast.Definition, ast.Symbol
return infix {
operator = "=",
identifier = "_=_",
priority = operator_priority["_=_"],
match = function(self, str, current_priority, primary)
local escaped = escape(self.operator)
return self.priority > current_priority and str:match("^"..escaped) and Symbol:is(primary)
end,
build_ast = function(self, left, right)
return Definition:new(left, right)
end
}

View file

@ -0,0 +1,9 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local operator_priority = require("anselme.common").operator_priority
return infix {
operator = "!=",
identifier = "_!=_",
priority = operator_priority["_!=_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local operator_priority = require("anselme.common").operator_priority
return infix {
operator = "/",
identifier = "_/_",
priority = operator_priority["_/_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local operator_priority = require("anselme.common").operator_priority
return infix {
operator = "==",
identifier = "_==_",
priority = operator_priority["_==_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local operator_priority = require("anselme.common").operator_priority
return infix {
operator = "^",
identifier = "_^_",
priority = operator_priority["_^_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local operator_priority = require("anselme.common").operator_priority
return infix {
operator = ">",
identifier = "_>_",
priority = operator_priority["_>_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local operator_priority = require("anselme.common").operator_priority
return infix {
operator = ">=",
identifier = "_>=_",
priority = operator_priority["_>=_"]
}

View file

@ -0,0 +1,9 @@
local infix_quote_right = require("anselme.parser.expression.secondary.infix.infix_quote_right")
local operator_priority = require("anselme.common").operator_priority
return infix_quote_right {
operator = "~",
identifier = "_~_",
priority = operator_priority["_~_"]
}

View file

@ -0,0 +1,23 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local identifier = require("anselme.parser.expression.primary.identifier")
local operator_priority = require("anselme.common").operator_priority
local ast = require("anselme.ast")
local Call, Identifier, ArgumentTuple = ast.Call, ast.Identifier, ast.ArgumentTuple
return infix {
operator = "*",
identifier = "_*_",
priority = operator_priority["_*_"]+.5, -- just above / so 1/2x gives 1/(2x)
match = function(self, str, current_priority, primary)
return self.priority > current_priority and identifier:match(str)
end,
parse = function(self, source, str, limit_pattern, current_priority, primary)
local start_source = source:clone()
local right, rem = identifier:parse(source, str, limit_pattern)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(primary, right)):set_source(start_source), rem
end
}

View file

@ -0,0 +1,19 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local operator_priority = require("anselme.common").operator_priority
local ast = require("anselme.ast")
local Call, Identifier, ArgumentTuple = ast.Call, ast.Identifier, ast.ArgumentTuple
return infix {
operator = ".",
identifier = "_._",
priority = operator_priority["_._"],
build_ast = function(self, left, right)
if Identifier:is(right) then
right = right:to_string()
end
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left, right))
end
}

View file

@ -0,0 +1,34 @@
local secondary = require("anselme.parser.expression.secondary.secondary")
local escape = require("anselme.common").escape
local expression_to_ast = require("anselme.parser.expression.to_ast")
local ast = require("anselme.ast")
local Call, Identifier, ArgumentTuple = ast.Call, ast.Identifier, ast.ArgumentTuple
return secondary {
operator = nil,
identifier = nil,
priority = nil,
-- return bool
match = function(self, str, current_priority, primary)
local escaped = escape(self.operator)
return self.priority > current_priority and str:match("^"..escaped)
end,
-- return AST, rem
parse = function(self, source, str, limit_pattern, current_priority, primary)
local start_source = source:clone()
local escaped = escape(self.operator)
local sright = source:consume(str:match("^("..escaped..")(.*)$"))
local s, right, rem = pcall(expression_to_ast, source, sright, limit_pattern, self.priority)
if not s then error(("invalid expression after binary operator %q: %s"):format(self.operator, right), 0) end
return self:build_ast(primary, right):set_source(start_source), rem
end,
build_ast = function(self, left, right)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left, right))
end
}

View file

@ -0,0 +1,32 @@
-- same as infix, but skip if no valid expression after the operator instead of erroring
-- useful for operators that are both valid as infix and as suffix
local infix = require("anselme.parser.expression.secondary.infix.infix")
local escape = require("anselme.common").escape
local expression_to_ast = require("anselme.parser.expression.to_ast")
return infix {
-- returns exp, rem if expression found
-- returns nil if no expression found
search = function(self, source, str, limit_pattern, current_priority, operating_on_primary)
if not self:match(str, current_priority, operating_on_primary) then
return nil
end
return self:maybe_parse(source, str, limit_pattern, current_priority, operating_on_primary)
end,
parse = function() error("no guaranteed parse for this operator") end,
-- return AST, rem
-- return nil
maybe_parse = function(self, source, str, limit_pattern, current_priority, primary)
local start_source = source:clone()
local escaped = escape(self.operator)
local sright = source:consume(str:match("^("..escaped..")(.*)$"))
local s, right, rem = pcall(expression_to_ast, source, sright, limit_pattern, self.priority)
if not s then return nil end
return self:build_ast(primary, right):set_source(start_source), rem
end,
}

View file

@ -0,0 +1,12 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local ast = require("anselme.ast")
local Call, Identifier, ArgumentTuple, Quote = ast.Call, ast.Identifier, ast.ArgumentTuple, ast.Quote
return infix {
build_ast = function(self, left, right)
left = Quote:new(left)
right = Quote:new(right)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left, right))
end
}

View file

@ -0,0 +1,11 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local ast = require("anselme.ast")
local Call, Identifier, ArgumentTuple, Quote = ast.Call, ast.Identifier, ast.ArgumentTuple, ast.Quote
return infix {
build_ast = function(self, left, right)
right = Quote:new(right)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left, right))
end
}

View file

@ -0,0 +1,9 @@
local infix = require("anselme.parser.expression.secondary.infix.infix")
local operator_priority = require("anselme.common").operator_priority
return infix {
operator = "//",
identifier = "_//_",
priority = operator_priority["_//_"]
}

Some files were not shown because too many files have changed in this diff Show more