469 lines
13 KiB
Lua
469 lines
13 KiB
Lua
require("helper")
|
|
|
|
-- CLASS DEFINITIONS
|
|
-- A stream of text (kind of)
|
|
-- with some helping methods
|
|
Reader = class("Reader")
|
|
-- Construct a reader from
|
|
function Reader:new(text)
|
|
assert_type("string")(text,"Arg 'text' #1: %s")
|
|
self.text = text
|
|
end
|
|
-- Get the substring of the readers text in a bounds
|
|
function Reader:get(first,final)
|
|
assert_type("number")(first,"Arg 'first' #1: %s")
|
|
assert_type("number","nil")(final,"Arg 'final' #2: %s")
|
|
return self.text:sub(first,final)
|
|
end
|
|
-- Match on the reader's text from an index
|
|
function Reader:find(pattern,init)
|
|
assert_type("string")(pattern,"Arg 'pattern' #1: %s")
|
|
assert_type("number","nil")(init,"Arg 'init' #2: %s")
|
|
return self.text:find(pattern,init)
|
|
end
|
|
function Reader:match(pattern,init)
|
|
assert_type("string")(pattern,"Arg 'pattern' #1: %s")
|
|
assert_type("number")(init,"Arg 'init' #2: %s")
|
|
return self.text:match(pattern,init)
|
|
end
|
|
|
|
-- A range of text being considered
|
|
-- like a cursor
|
|
Range = class("Range")
|
|
-- Construct a range of text from a reader and bounds
|
|
function Range:new(reader,first,final)
|
|
assert_meta(Reader)(reader)
|
|
assert_type("number","nil")(first,"Arg 'first' #1: %s")
|
|
assert_type("number","nil")(final,"Arg 'final' #2: %s")
|
|
self.reader = reader
|
|
self.first = first
|
|
self.final = final
|
|
end
|
|
function Range:text()
|
|
return self.reader:get(self.first or 1,self.final)
|
|
end
|
|
function Range:find(pattern)
|
|
return self.reader:find(pattern,self.first)
|
|
end
|
|
function Range:move(first,final)
|
|
assert_type("number","nil")(first,"Arg 'first' #1: %s")
|
|
assert_type("number","nil")(final,"Arg 'final' #2: %s")
|
|
self.first = first
|
|
self.final = final
|
|
-- note that this function returns itself
|
|
return self
|
|
end
|
|
|
|
Expression = class("Expression")
|
|
-- Construct an expression from items (or not)
|
|
function Expression:new(items)
|
|
items = items or {}
|
|
assert_type("table")(items)
|
|
self.items = {}
|
|
for item in iterate(items) do
|
|
self:insert(item)
|
|
end
|
|
end
|
|
-- Return the iterator through the expression
|
|
function Expression:iterate()
|
|
return iterate(self.items)
|
|
end
|
|
-- Is the expression empty?
|
|
function Expression:empty()
|
|
return #self.items == 0
|
|
end
|
|
--[[
|
|
Insert a word or sub expression into this one
|
|
Note: The expression has a .items = {} list.
|
|
]]
|
|
function Expression:insert(...)
|
|
-- closures for nvim highlighting
|
|
local function insert_word(word)
|
|
-- Whitespace might confuse bindings, remove them
|
|
local cleaned = word:match("^%s*(.-)%s*$")
|
|
if cleaned ~= "" then
|
|
table.insert(self.items,cleaned)
|
|
end
|
|
end
|
|
local function insert_expression(expr)
|
|
assert_meta(expr,Expression)
|
|
table.insert(self.items,expr)
|
|
end
|
|
for item in iterate({...}) do
|
|
case_of({
|
|
[Expression] = function(expr)
|
|
insert_expression(expr)
|
|
end,
|
|
string = function(word)
|
|
insert_word(word)
|
|
end
|
|
})(type_of(item),item)
|
|
end
|
|
end
|
|
--[[
|
|
Converts an expression to meaningful text format.
|
|
This should be reparseable.
|
|
]]
|
|
function Expression:string()
|
|
local parts = {}
|
|
for index,item in pairs(self.items) do
|
|
parts[index] = tostring(item)
|
|
end
|
|
return string.format("(%s)",table.concat(parts," "))
|
|
end
|
|
--[[
|
|
Similarly to abstract, returns this expression
|
|
with subexpression:reduce(depth-1)
|
|
Useful for more informative debugging
|
|
]]
|
|
function Expression:reduce(depth)
|
|
local parts = {}
|
|
for index,item in pairs(self.items) do
|
|
parts[index] = case_of({
|
|
[Expression] = function(item)
|
|
if depth == 0 then
|
|
return Expression({"..."})
|
|
else
|
|
return item:reduce(depth - 1)
|
|
end
|
|
end,
|
|
string = id
|
|
})(type_of(item),item)
|
|
end
|
|
end
|
|
--[[
|
|
Returns this expression with all sub expressions
|
|
simplified to text by :text() or '...'
|
|
]]
|
|
function Expression:abstract(infer)
|
|
local parts = {}
|
|
local count = 0
|
|
for index,item in pairs(self.items) do
|
|
parts[index] = case_of({
|
|
[Expression] = function(item)
|
|
local text
|
|
if infer then
|
|
text = item:text()
|
|
if not text then
|
|
count = count + 1
|
|
text = tostring(count)
|
|
end
|
|
else
|
|
text = "..."
|
|
end
|
|
return Expression({text})
|
|
end,
|
|
string = id
|
|
})(type_of(item),item)
|
|
end
|
|
return Expression(parts)
|
|
end
|
|
--[[
|
|
Return the single 'words' part of this expression
|
|
if it is alone. Otherwise return nil.
|
|
]]
|
|
function Expression:text()
|
|
-- Get the text
|
|
local text = self.items[#self.items]
|
|
-- Ensure that the subexpression is valid abstract
|
|
if type(text) ~= "string" then return end
|
|
return text
|
|
end
|
|
--[[
|
|
Give this expression a meaningful debug location
|
|
referring to some source code.
|
|
]]
|
|
function Expression:locate(str,uri)
|
|
self.location = str
|
|
self.source = uri
|
|
return self
|
|
end
|
|
--[[
|
|
Return the meaningful debug location as a string msg.
|
|
]]
|
|
function Expression:link(msg)
|
|
return string.format(
|
|
"in '%s' @%s (%s)",
|
|
msg,
|
|
tostring(self:abstract()),
|
|
tostring(self.location or "?"),
|
|
tostring(self.source or "?.nel")
|
|
)
|
|
end
|
|
|
|
--[[
|
|
A class which parses text using the 'consumer-meal'
|
|
model.
|
|
]]
|
|
Consumer = class("Consumer")
|
|
function Consumer:new(content)
|
|
self.range = Range(Reader(content))
|
|
self.col = 1
|
|
self.row = 1
|
|
end
|
|
function Consumer:remaining()
|
|
return self.range:text()
|
|
end
|
|
function Consumer:mark()
|
|
|
|
end
|
|
-- From a table of actions of patterns,
|
|
-- consume the earliest pattern (index)
|
|
-- and call the value
|
|
function Consumer:consume(t)
|
|
-- t: {string = function...}
|
|
assert_type("table")(t,"Arg 't' #1: %s")
|
|
if not next(t) then
|
|
error("The match table cannot be empty!",2)
|
|
end
|
|
for index,func in pairs(t) do
|
|
assert_type("string")(index,"Bad 't' index: %s")
|
|
assert_type("function")(func,"Bad 't' item: %s")
|
|
end
|
|
local origin = self.range.first or 1
|
|
-- Get the next earliest match
|
|
local findex,ffinal,fpattern,ffunc,fexcess
|
|
for pattern,new_func in pairs(t) do
|
|
local new_index,new_final = self.range:find(pattern)
|
|
if new_index and
|
|
((not findex) or (findex > new_index))
|
|
then
|
|
if not new_index then return end
|
|
findex = new_index
|
|
ffinal = new_final
|
|
fpattern = pattern
|
|
ffunc = new_func
|
|
fexcess = self.range.reader:get(
|
|
self.range.first or 1,
|
|
new_final - 1
|
|
)
|
|
end
|
|
end
|
|
-- Pass into the func
|
|
if not ffunc then
|
|
return nil
|
|
end
|
|
assert(findex) assert(ffinal)
|
|
self.range:move(ffinal+1) -- Move range to after the match
|
|
local fmatch = self.range.reader:match(fpattern,findex)
|
|
-- Pass back consumed result to
|
|
local sum = self.range.reader:get(origin+1,self.range.first)
|
|
:gsub("\r\n","\n")
|
|
:gsub("\r","\n")
|
|
for char in sum:gmatch(".") do
|
|
if char == "\n" then
|
|
self.row = self.row + 1
|
|
self.col = 1
|
|
else
|
|
self.col = self.col + 1
|
|
end
|
|
end
|
|
return ffunc(fexcess,fmatch)
|
|
end
|
|
|
|
Binding = class("Binding")
|
|
-- Construct from a neli string to infer a binding
|
|
-- note that you must wrap this definition in "(<def>)"!
|
|
function Binding:new(expression,callback)
|
|
assert_meta(Expression)(expression,"Arg 'expression' #1: %s")
|
|
if type_of(callback) == "function" then
|
|
self.callback = callback
|
|
else
|
|
self.value = callback
|
|
end
|
|
self.template = expression:abstract(true)
|
|
end
|
|
function Binding:string()
|
|
return string.format("Binding %s: %s",self.template,tostring(self.value or self.callback))
|
|
end
|
|
function Binding:call(s,e,c)
|
|
for item in iterate(e) do
|
|
assert_meta(Expression)(item)
|
|
end
|
|
assert_meta(Scope)(s,"Arg 'scope' #1: %s")
|
|
assert_meta(Chain)(c,"Arg 'chain' #3: %s")
|
|
return self.value or self.callback(s,e,c)
|
|
end
|
|
-- Expecting an abstract expression!
|
|
function Binding:check(expression,candid)
|
|
assert_meta(Expression)(expression,"Arg 'expression' #1: %s")
|
|
candid = candid or expression:abstract()
|
|
assert_meta(Expression)(candid,"Arg 'abstract' #2: %s")
|
|
local data = {}
|
|
local index
|
|
local loop = true
|
|
if #candid.items ~= #self.template.items then
|
|
return false
|
|
end
|
|
while loop do
|
|
local old = index
|
|
index,template_item = next(self.template.items,old)
|
|
index,candid_item = next(candid.items,old)
|
|
local result = case_of({
|
|
[Expression] = function()
|
|
-- item is not an expression, cannot bind
|
|
if type_of(candid_item) ~= Expression then
|
|
loop = false
|
|
else
|
|
local text = template_item:text()
|
|
if not text then
|
|
error(
|
|
string.format(
|
|
[[There was a subexpression without
|
|
just text in this expression: %s]],
|
|
tostring(candid)
|
|
)
|
|
)
|
|
end
|
|
data[text] = expression.items[index]
|
|
assert_meta(Expression)(expression.items[index])
|
|
end
|
|
end,
|
|
string = function()
|
|
-- words don't match, cannot bind
|
|
if candid_item ~= template_item then
|
|
loop = false
|
|
end
|
|
end,
|
|
["nil"] = function()
|
|
-- base case where both expressions terminate together
|
|
if not candid_item then
|
|
return data
|
|
end
|
|
end
|
|
})(type_of(template_item))
|
|
if result then return result end
|
|
end
|
|
end
|
|
|
|
Chain = class("Chain")
|
|
function Chain:new(data)
|
|
assert_type("string")(data,"Arg 'data' #1: %s")
|
|
self.data = data
|
|
end
|
|
function Chain:from(previous)
|
|
assert_meta(Chain)(previous,"Arg 'previous' #1: %s")
|
|
previous.next = self
|
|
self.previous = previous
|
|
return self
|
|
end
|
|
function Chain:string()
|
|
return string.format("Chain:%s",table.concat(self:flatten(),", "))
|
|
end
|
|
function Chain:flatten(tab)
|
|
tab = tab or {}
|
|
if self.next then
|
|
self.next:flatten(tab)
|
|
end
|
|
table.insert(tab,self.data)
|
|
return tab
|
|
end
|
|
function Chain:pcall(func)
|
|
local success,result = xpcall(func,function(msg)
|
|
-- If this is a 'nel:' error, strip the line responsible
|
|
-- Otherwise just return the whole traceback since something went really bad
|
|
local match = msg:match("nel:%s(.*)")
|
|
if match then
|
|
return match
|
|
else
|
|
return "internal: "..debug.traceback(msg,2)
|
|
end
|
|
end,self)
|
|
if not success then
|
|
log(string.format(
|
|
"nel: %s\nNEL traceback:\n\t%s",
|
|
result,
|
|
table.concat(self:flatten(),"\n\t")
|
|
))
|
|
end
|
|
return successs,result
|
|
end
|
|
function Chain:call(func)
|
|
local success,result = self:pcall(func)
|
|
if not success then error("Chain: Halt!") end
|
|
return result
|
|
end
|
|
|
|
-- The scope of a neli expression under evaluation
|
|
-- without exact faith to traditional language scopes
|
|
Scope = class("Scope")
|
|
function Scope:new(bindings)
|
|
self.bindings = {}
|
|
for binding in iterate(bindings or {}) do
|
|
self:insert(binding)
|
|
end
|
|
end
|
|
function Scope:string()
|
|
return string.format("Scope: {\n\t%s\n}",table.concat(map(self.bindings,tostring),"\n\t"))
|
|
end
|
|
function Scope:insert(binding)
|
|
assert_meta(Binding)(binding,"Arg 'binding' #1: %s")
|
|
for competitor in iterate(self.bindings) do
|
|
if competitor:check(binding.template) then
|
|
error(string.format("nel: Conflicting sibling bindings in scope for '%s'.",binding.template))
|
|
end
|
|
end
|
|
table.insert(self.bindings,binding)
|
|
end
|
|
function Scope:run(expression,chain,original)
|
|
original = original or self
|
|
assert_meta(Expression)(expression,"Arg 'expression' #1: %s")
|
|
assert_meta(Chain)(chain,"Arg 'chain' #2: %s")
|
|
assert_meta(Scope,original,"Arg 'scope' #3: %s")
|
|
-- Could be multiple bindings so make a table
|
|
local binds = {}
|
|
local abstract = expression:abstract()
|
|
for binding in iterate(self.bindings) do
|
|
local bind = binding:check(expression,abstract)
|
|
-- Insert into table
|
|
if bind then binds[binding] = bind end
|
|
end
|
|
local binding,bind = next(binds)
|
|
-- Check for multiple
|
|
if not bind then
|
|
if self.parent then
|
|
return self.parent:run(expression,chain,original)
|
|
else
|
|
local extra = ""
|
|
if #expression.items == 1 then
|
|
local item = expression.items[1]
|
|
if type_of(item) == Expression then
|
|
extra = extra .. "\nNote: This expression is formed (just bracketed) of another single expression. Maybe too many brackets were used here? Are you using curly braces?"
|
|
else
|
|
extra = extra .. "\n\tNote: This is a 'word only' expression, it has no subexpressions. It is most likely that an expected object definition was not bound to the scope or there is a typo."
|
|
end
|
|
end
|
|
error(
|
|
string.format(
|
|
"nel: No binding for '%s' in scope.%s",
|
|
tostring(expression),
|
|
extra
|
|
),3
|
|
)
|
|
end
|
|
-- Should never happen!
|
|
elseif next(binds,binding) then
|
|
local candidates = {}
|
|
for binding,bind in pairs(binds) do
|
|
table.insert(candidates,tostring(binding.template))
|
|
end
|
|
error(
|
|
string.format(
|
|
"nel: Ambiguous bindings in scope for '%s': %s",
|
|
tostring(expression:abstract()),
|
|
table.concat(candidates)
|
|
)
|
|
,2)
|
|
else
|
|
for index,item in pairs(binds) do
|
|
print("index:",index,"item:",item)
|
|
end
|
|
return binding:call(
|
|
original,
|
|
bind,
|
|
chain
|
|
)
|
|
end
|
|
end
|
|
return _G
|