Initial commit. Note: this is broken right now.
This commit is contained in:
284
parse.lua
Normal file
284
parse.lua
Normal file
@@ -0,0 +1,284 @@
|
||||
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
|
||||
function Expression:empty()
|
||||
return #self.items == 0
|
||||
end
|
||||
-- insert a word or sub expression into this one
|
||||
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({
|
||||
table = function(expr)
|
||||
insert_expression(expr)
|
||||
end,
|
||||
string = function(word)
|
||||
insert_word(word)
|
||||
end
|
||||
})(type(item),item)
|
||||
end
|
||||
end
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
Consumer = class("Consumer")
|
||||
function Consumer:new(content)
|
||||
self.range = Range(Reader(content))
|
||||
end
|
||||
function Consumer:remaining()
|
||||
return self.range:text()
|
||||
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
|
||||
-- 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
|
||||
-- Pass back consumed result to
|
||||
return ffunc(fexcess,self.range.reader:match(fpattern,findex))
|
||||
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")
|
||||
assert_type("function")(callback,"Arg 'callback' #2: %s")
|
||||
self.template = expression:abstract(true)
|
||||
self.callback = callback
|
||||
end
|
||||
function Binding:call(scope,binds)
|
||||
return self.callback(scope,binds)
|
||||
end
|
||||
function Binding:check(expression)
|
||||
assert_meta(Expression)(expression,"Arg 'expression' #1: %s")
|
||||
local candid = expression:abstract()
|
||||
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
|
||||
data[template_item:text()] = 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
|
||||
|
||||
-- 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:insert(binding)
|
||||
assert_meta(binding,Binding)
|
||||
table.insert(self.bindings,binding)
|
||||
end
|
||||
function Scope:evaluate(expression)
|
||||
assert_meta(Expression)(expression,"Arg 'expression' #1: %s")
|
||||
local parent = self.parent
|
||||
-- Could be multiple bindings so make a table
|
||||
local binds = {}
|
||||
for binding in iterate(self.bindings) do
|
||||
local bind = binding:check(expression)
|
||||
-- Insert into table
|
||||
if bind then binds[binding] = bind end
|
||||
end
|
||||
local binding,bind = next(binds)
|
||||
-- Check for multiple
|
||||
if not bind then
|
||||
if parent then
|
||||
parent:evaluate(expression)
|
||||
else
|
||||
error(
|
||||
string.format(
|
||||
"No binding for '%s' in transitive scope.",
|
||||
tostring(expression)
|
||||
),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(
|
||||
"Ambiguous bindings in scope for '%s': %s",
|
||||
tostring(expression:abstract()),
|
||||
table.concat(candidates)
|
||||
)
|
||||
,2)
|
||||
else
|
||||
return binding:call(self,bind)
|
||||
end
|
||||
end
|
||||
return _G
|
||||
Reference in New Issue
Block a user