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:
parent
404e7dd56e
commit
5dd971ff8f
179 changed files with 603 additions and 579 deletions
35
anselme/ast/Anchor.lua
Normal file
35
anselme/ast/Anchor.lua
Normal 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
|
||||
206
anselme/ast/ArgumentTuple.lua
Normal file
206
anselme/ast/ArgumentTuple.lua
Normal 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
|
||||
37
anselme/ast/Assignment.lua
Normal file
37
anselme/ast/Assignment.lua
Normal 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
81
anselme/ast/Block.lua
Normal 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
28
anselme/ast/Boolean.lua
Normal 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
59
anselme/ast/Branched.lua
Normal 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
86
anselme/ast/Call.lua
Normal 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
52
anselme/ast/Choice.lua
Normal 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
58
anselme/ast/Closure.lua
Normal 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
|
||||
63
anselme/ast/Definition.lua
Normal file
63
anselme/ast/Definition.lua
Normal 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
237
anselme/ast/Environment.lua
Normal 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
28
anselme/ast/Flush.lua
Normal 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
82
anselme/ast/Function.lua
Normal 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
|
||||
45
anselme/ast/FunctionParameter.lua
Normal file
45
anselme/ast/FunctionParameter.lua
Normal 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
|
||||
44
anselme/ast/Identifier.lua
Normal file
44
anselme/ast/Identifier.lua
Normal 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
81
anselme/ast/List.lua
Normal 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
|
||||
64
anselme/ast/LuaFunction.lua
Normal file
64
anselme/ast/LuaFunction.lua
Normal 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
20
anselme/ast/Nil.lua
Normal 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
25
anselme/ast/Number.lua
Normal 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
61
anselme/ast/Overload.lua
Normal 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
25
anselme/ast/Pair.lua
Normal 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,
|
||||
}
|
||||
67
anselme/ast/ParameterTuple.lua
Normal file
67
anselme/ast/ParameterTuple.lua
Normal 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
|
||||
91
anselme/ast/PartialScope.lua
Normal file
91
anselme/ast/PartialScope.lua
Normal 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
41
anselme/ast/Quote.lua
Normal 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
33
anselme/ast/Return.lua
Normal 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
|
||||
37
anselme/ast/ReturnBoundary.lua
Normal file
37
anselme/ast/ReturnBoundary.lua
Normal 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
34
anselme/ast/String.lua
Normal 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
|
||||
53
anselme/ast/StringInterpolation.lua
Normal file
53
anselme/ast/StringInterpolation.lua
Normal 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
133
anselme/ast/Struct.lua
Normal 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
84
anselme/ast/Symbol.lua
Normal 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
79
anselme/ast/Table.lua
Normal 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
36
anselme/ast/Text.lua
Normal 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
|
||||
}
|
||||
59
anselme/ast/TextInterpolation.lua
Normal file
59
anselme/ast/TextInterpolation.lua
Normal 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
|
||||
49
anselme/ast/Translatable.lua
Normal file
49
anselme/ast/Translatable.lua
Normal 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
66
anselme/ast/Tuple.lua
Normal 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
41
anselme/ast/Typed.lua
Normal 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
|
||||
8
anselme/ast/abstract/AutoCall.lua
Normal file
8
anselme/ast/abstract/AutoCall.lua
Normal 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
|
||||
}
|
||||
22
anselme/ast/abstract/Event.lua
Normal file
22
anselme/ast/abstract/Event.lua
Normal 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
|
||||
}
|
||||
339
anselme/ast/abstract/Node.lua
Normal file
339
anselme/ast/abstract/Node.lua
Normal 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
|
||||
31
anselme/ast/abstract/Overloadable.lua
Normal file
31
anselme/ast/abstract/Overloadable.lua
Normal 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,
|
||||
}
|
||||
12
anselme/ast/abstract/Runtime.lua
Normal file
12
anselme/ast/abstract/Runtime.lua
Normal 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
13
anselme/ast/init.lua
Normal 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
71
anselme/common/init.lua
Normal 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
|
||||
26
anselme/common/to_anselme.lua
Normal file
26
anselme/common/to_anselme.lua
Normal 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
93
anselme/init.lua
Normal 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
100
anselme/lib/ansicolors.lua
Normal 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
753
anselme/lib/binser.lua
Normal 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
169
anselme/lib/class.lua
Normal 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
39
anselme/parser/Source.lua
Normal 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
|
||||
67
anselme/parser/code_to_tree.lua
Normal file
67
anselme/parser/code_to_tree.lua
Normal 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
|
||||
57
anselme/parser/expression/comment.lua
Normal file
57
anselme/parser/expression/comment.lua
Normal 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
|
||||
40
anselme/parser/expression/contextual/function_parameter.lua
Normal file
40
anselme/parser/expression/contextual/function_parameter.lua
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
49
anselme/parser/expression/contextual/parameter_tuple.lua
Normal file
49
anselme/parser/expression/contextual/parameter_tuple.lua
Normal 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
|
||||
}
|
||||
25
anselme/parser/expression/primary/anchor.lua
Normal file
25
anselme/parser/expression/primary/anchor.lua
Normal 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
|
||||
}
|
||||
16
anselme/parser/expression/primary/block_identifier.lua
Normal file
16
anselme/parser/expression/primary/block_identifier.lua
Normal 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
|
||||
}
|
||||
204
anselme/parser/expression/primary/function_definition.lua
Normal file
204
anselme/parser/expression/primary/function_definition.lua
Normal 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
|
||||
}
|
||||
40
anselme/parser/expression/primary/identifier.lua
Normal file
40
anselme/parser/expression/primary/identifier.lua
Normal 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
|
||||
}
|
||||
43
anselme/parser/expression/primary/init.lua
Normal file
43
anselme/parser/expression/primary/init.lua
Normal 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
|
||||
}
|
||||
19
anselme/parser/expression/primary/number.lua
Normal file
19
anselme/parser/expression/primary/number.lua
Normal 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
|
||||
}
|
||||
31
anselme/parser/expression/primary/parenthesis.lua
Normal file
31
anselme/parser/expression/primary/parenthesis.lua
Normal 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
|
||||
}
|
||||
8
anselme/parser/expression/primary/prefix/else.lua
Normal file
8
anselme/parser/expression/primary/prefix/else.lua
Normal 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["~_"]
|
||||
}
|
||||
35
anselme/parser/expression/primary/prefix/function.lua
Normal file
35
anselme/parser/expression/primary/prefix/function.lua
Normal 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
|
||||
}
|
||||
9
anselme/parser/expression/primary/prefix/mutable.lua
Normal file
9
anselme/parser/expression/primary/prefix/mutable.lua
Normal 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["*_"]
|
||||
}
|
||||
9
anselme/parser/expression/primary/prefix/negation.lua
Normal file
9
anselme/parser/expression/primary/prefix/negation.lua
Normal 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["-_"]
|
||||
}
|
||||
9
anselme/parser/expression/primary/prefix/not.lua
Normal file
9
anselme/parser/expression/primary/prefix/not.lua
Normal 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["!_"]
|
||||
}
|
||||
34
anselme/parser/expression/primary/prefix/prefix.lua
Normal file
34
anselme/parser/expression/primary/prefix/prefix.lua
Normal 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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
15
anselme/parser/expression/primary/prefix/return.lua
Normal file
15
anselme/parser/expression/primary/prefix/return.lua
Normal 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
|
||||
}
|
||||
13
anselme/parser/expression/primary/prefix/semicolon.lua
Normal file
13
anselme/parser/expression/primary/prefix/semicolon.lua
Normal 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
|
||||
}
|
||||
16
anselme/parser/expression/primary/prefix/translatable.lua
Normal file
16
anselme/parser/expression/primary/prefix/translatable.lua
Normal 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
|
||||
}
|
||||
33
anselme/parser/expression/primary/primary.lua
Normal file
33
anselme/parser/expression/primary/primary.lua
Normal 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
|
||||
}
|
||||
76
anselme/parser/expression/primary/string.lua
Normal file
76
anselme/parser/expression/primary/string.lua
Normal 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
|
||||
}
|
||||
17
anselme/parser/expression/primary/struct.lua
Normal file
17
anselme/parser/expression/primary/struct.lua
Normal 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
|
||||
}
|
||||
40
anselme/parser/expression/primary/symbol.lua
Normal file
40
anselme/parser/expression/primary/symbol.lua
Normal 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
|
||||
}
|
||||
25
anselme/parser/expression/primary/text.lua
Normal file
25
anselme/parser/expression/primary/text.lua
Normal 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
|
||||
}
|
||||
41
anselme/parser/expression/primary/tuple.lua
Normal file
41
anselme/parser/expression/primary/tuple.lua
Normal 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,
|
||||
}
|
||||
9
anselme/parser/expression/secondary/infix/addition.lua
Normal file
9
anselme/parser/expression/secondary/infix/addition.lua
Normal 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["_+_"]
|
||||
}
|
||||
9
anselme/parser/expression/secondary/infix/and.lua
Normal file
9
anselme/parser/expression/secondary/infix/and.lua
Normal 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["_&_"]
|
||||
}
|
||||
23
anselme/parser/expression/secondary/infix/assignment.lua
Normal file
23
anselme/parser/expression/secondary/infix/assignment.lua
Normal 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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
27
anselme/parser/expression/secondary/infix/call.lua
Normal file
27
anselme/parser/expression/secondary/infix/call.lua
Normal 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
|
||||
}
|
||||
17
anselme/parser/expression/secondary/infix/choice.lua
Normal file
17
anselme/parser/expression/secondary/infix/choice.lua
Normal 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
|
||||
}
|
||||
22
anselme/parser/expression/secondary/infix/definition.lua
Normal file
22
anselme/parser/expression/secondary/infix/definition.lua
Normal 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
|
||||
}
|
||||
9
anselme/parser/expression/secondary/infix/different.lua
Normal file
9
anselme/parser/expression/secondary/infix/different.lua
Normal 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["_!=_"]
|
||||
}
|
||||
9
anselme/parser/expression/secondary/infix/division.lua
Normal file
9
anselme/parser/expression/secondary/infix/division.lua
Normal 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["_/_"]
|
||||
}
|
||||
9
anselme/parser/expression/secondary/infix/equal.lua
Normal file
9
anselme/parser/expression/secondary/infix/equal.lua
Normal 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["_==_"]
|
||||
}
|
||||
9
anselme/parser/expression/secondary/infix/exponent.lua
Normal file
9
anselme/parser/expression/secondary/infix/exponent.lua
Normal 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["_^_"]
|
||||
}
|
||||
9
anselme/parser/expression/secondary/infix/greater.lua
Normal file
9
anselme/parser/expression/secondary/infix/greater.lua
Normal 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["_>_"]
|
||||
}
|
||||
|
|
@ -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["_>=_"]
|
||||
}
|
||||
9
anselme/parser/expression/secondary/infix/if.lua
Normal file
9
anselme/parser/expression/secondary/infix/if.lua
Normal 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["_~_"]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
19
anselme/parser/expression/secondary/infix/index.lua
Normal file
19
anselme/parser/expression/secondary/infix/index.lua
Normal 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
|
||||
}
|
||||
34
anselme/parser/expression/secondary/infix/infix.lua
Normal file
34
anselme/parser/expression/secondary/infix/infix.lua
Normal 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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue