Initial commit. Note: this is broken right now.
This commit is contained in:
195
helper.lua
Normal file
195
helper.lua
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
local old_print = print
|
||||||
|
function print(...)
|
||||||
|
local info = debug.getinfo(2)
|
||||||
|
local line = info.currentline
|
||||||
|
local name = info.source
|
||||||
|
old_print(string.format("%i%s> ",line,name),...)
|
||||||
|
end
|
||||||
|
function log(...)
|
||||||
|
old_print(...)
|
||||||
|
end
|
||||||
|
-- HELPER FUNCTIONS
|
||||||
|
-- Insert every item in t1 into t0
|
||||||
|
function just(...)
|
||||||
|
local items = {...}
|
||||||
|
return function()
|
||||||
|
return table.unpack(items)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
id = function(...) return ... end
|
||||||
|
|
||||||
|
function union(t0,t1)
|
||||||
|
for index,item in pairs(t0) do
|
||||||
|
t0[index] = t1
|
||||||
|
end
|
||||||
|
return t0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Iterate (ipairs without the index)
|
||||||
|
function iterate(...)
|
||||||
|
local iterator,tab,i = pairs(...)
|
||||||
|
return function(...)
|
||||||
|
local index,item = iterator(tab,i)
|
||||||
|
i = index
|
||||||
|
return item
|
||||||
|
end,tab,i
|
||||||
|
end
|
||||||
|
|
||||||
|
function indices(t)
|
||||||
|
if type(t) ~= "table" then
|
||||||
|
error(
|
||||||
|
string.format(
|
||||||
|
"Argument 1 '%s' must be a table",
|
||||||
|
tostring(t)
|
||||||
|
)
|
||||||
|
,2)
|
||||||
|
end
|
||||||
|
local indices = {}
|
||||||
|
for index,_ in pairs(t) do
|
||||||
|
table.insert(indices,index)
|
||||||
|
end
|
||||||
|
return indices
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ensures a table has a metatable
|
||||||
|
function ensure_metatable(t)
|
||||||
|
local meta = getmetatable(t)
|
||||||
|
if not meta then meta = {} setmetatable(t,meta) end
|
||||||
|
return meta
|
||||||
|
end
|
||||||
|
|
||||||
|
function strings(t,item)
|
||||||
|
ensure_metatable(t).__tostring = item
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Makes a table callable
|
||||||
|
function calls(t,fun)
|
||||||
|
ensure_metatable(t).__call = fun
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Makes a table record access
|
||||||
|
function indexes(t,fun)
|
||||||
|
ensure_metatable(t).__index = fun
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Makes a table record setting
|
||||||
|
function modifies(t,fun)
|
||||||
|
ensure_metatable(t).__newindex = fun
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Shallow table to string
|
||||||
|
function table_tostring(tab,sep)
|
||||||
|
local items = {}
|
||||||
|
for item in iterate(tab) do
|
||||||
|
table.insert(items,tostring(item))
|
||||||
|
end
|
||||||
|
return table.concat(items,sep)
|
||||||
|
end
|
||||||
|
|
||||||
|
function type_of(item)
|
||||||
|
local type_name = type(item)
|
||||||
|
if type_name == "table" then
|
||||||
|
local meta = getmetatable(item)
|
||||||
|
if not meta then
|
||||||
|
return "table"
|
||||||
|
else
|
||||||
|
return meta
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return type_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- DEBUGGING FUNCTIONS
|
||||||
|
-- Runs a function
|
||||||
|
function assert_meta(...)
|
||||||
|
local metas = {...}
|
||||||
|
return function(x,raise)
|
||||||
|
local meta = getmetatable(x)
|
||||||
|
for tab in iterate(metas) do
|
||||||
|
if meta == tab then return end
|
||||||
|
end
|
||||||
|
local s_metas = {}
|
||||||
|
for meta in iterate(metas) do
|
||||||
|
table.insert(s_metas,tostring(meta))
|
||||||
|
end
|
||||||
|
local level = 2
|
||||||
|
local msg = string.format(
|
||||||
|
"Expected metatables '%s' but got '%s'",
|
||||||
|
table.concat(s_metas,","),
|
||||||
|
type(x)
|
||||||
|
)
|
||||||
|
if raise then
|
||||||
|
level = 3
|
||||||
|
msg = string.format(raise,msg)
|
||||||
|
end
|
||||||
|
error(msg,level)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function assert_type(...)
|
||||||
|
local types = {...}
|
||||||
|
return function(x,raise)
|
||||||
|
local t = type(x)
|
||||||
|
for name in iterate(types) do
|
||||||
|
if t == name then return end
|
||||||
|
end
|
||||||
|
local level = 2
|
||||||
|
local msg = string.format(
|
||||||
|
"Expected type '%s' but got '%s'",
|
||||||
|
table_tostring(types," or "),
|
||||||
|
type(x))
|
||||||
|
-- For argument errors
|
||||||
|
if raise then
|
||||||
|
level = 3
|
||||||
|
msg = string.format(raise,msg)
|
||||||
|
end
|
||||||
|
error(msg,level)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- HELPER FUNCTIONS
|
||||||
|
function case_of(t)
|
||||||
|
return function(item,...)
|
||||||
|
return (t[item] or error(
|
||||||
|
string.format(
|
||||||
|
"Couldn't match '%s' in case.",
|
||||||
|
tostring(item)
|
||||||
|
),2
|
||||||
|
))(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Creates a (p)OOP class
|
||||||
|
function class(name)
|
||||||
|
local meta = {}
|
||||||
|
union(meta,{restrict = {}})
|
||||||
|
meta.__index = meta
|
||||||
|
strings(meta,just(name))
|
||||||
|
calls(meta,function(meta,...)
|
||||||
|
local new = {}
|
||||||
|
setmetatable(new,meta)
|
||||||
|
if meta.new then new:new(...) end
|
||||||
|
return new
|
||||||
|
end)
|
||||||
|
meta.__tostring = function(this)
|
||||||
|
if meta.string then return this:string() end
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
return meta
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Read a file's full contents
|
||||||
|
function read(path)
|
||||||
|
local file = io.open(path)
|
||||||
|
if not file then
|
||||||
|
error(
|
||||||
|
string.match("Could not open file '%s'.",path),3
|
||||||
|
)
|
||||||
|
end
|
||||||
|
local content = file:read("a")
|
||||||
|
return content
|
||||||
|
end
|
||||||
|
|
||||||
|
return _G
|
||||||
151
interpret.lua
Normal file
151
interpret.lua
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
require("parse")
|
||||||
|
|
||||||
|
-- TODO: This should really be a separate API and module
|
||||||
|
-- please break it out
|
||||||
|
-- Interpret some text
|
||||||
|
function interpret(content)
|
||||||
|
-- LOCALS
|
||||||
|
-- The pattern for words inbetween expressions
|
||||||
|
local expressionWordPattern = "(.*)"
|
||||||
|
-- The patterns to open and close expressions,
|
||||||
|
-- the words scattered inbetween them will be matches
|
||||||
|
local expressionOpenPattern = "%("
|
||||||
|
local expressionClosePattern = "%)"
|
||||||
|
local subExpressionOpenPattern = "%:"
|
||||||
|
local subExpressionClosePattern = "%,"
|
||||||
|
local blockOpenPattern = "%{"
|
||||||
|
local blockClosePattern = "%}"
|
||||||
|
local blockDelimiterPattern = "[;]+"
|
||||||
|
-- So that all functions have access to eachother
|
||||||
|
local consume,
|
||||||
|
consumeExpression,
|
||||||
|
processWords,
|
||||||
|
processClosure,
|
||||||
|
processOpening,
|
||||||
|
consumeBlock,
|
||||||
|
consumeText,
|
||||||
|
consumeNumber
|
||||||
|
|
||||||
|
local baseExpression = Expression()
|
||||||
|
local consumer = Consumer(content)
|
||||||
|
|
||||||
|
-- FUNCTIONS
|
||||||
|
-- Consume an expression, with its sub expressions
|
||||||
|
-- given that an opening has just been consumed
|
||||||
|
local singleLineStringPattern = "\""
|
||||||
|
local function singleLineStringMeal(current,match)
|
||||||
|
return function(words,_)
|
||||||
|
current:insert(words,consumer:consume({
|
||||||
|
[match] = function(words,_)
|
||||||
|
return Expression({"text",Expression({words})})
|
||||||
|
end
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local multiLineStringOpenPattern = "%[%["
|
||||||
|
local multiLineStringClosePattern = "%]%]"
|
||||||
|
local function multiLineStringMeal(current,match)
|
||||||
|
return function(words,_)
|
||||||
|
current:insert(words,consumer:consume({
|
||||||
|
["[\n\r]"] = function()
|
||||||
|
error("Incomplete string literal")
|
||||||
|
end,
|
||||||
|
[match] = function(words,_)
|
||||||
|
return Expression({"text",Expression({words})})
|
||||||
|
end
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local uriOpenPattern = "<"
|
||||||
|
local uriClosePattern = ">"
|
||||||
|
local function URIMeal(current,match)
|
||||||
|
return function(words,_)
|
||||||
|
current:insert(words,consumer:consume({
|
||||||
|
["[\n\r]"] = function()
|
||||||
|
error("Incomplete URI literal")
|
||||||
|
end,
|
||||||
|
[match] = function(path)
|
||||||
|
return read(path)
|
||||||
|
end
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function expressionMeal(current)
|
||||||
|
return function(words,_)
|
||||||
|
current:insert(words,consumeExpression())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function consumeBlock()
|
||||||
|
local expressions = {}
|
||||||
|
local current = Expression()
|
||||||
|
local loop = true
|
||||||
|
while loop do
|
||||||
|
local expr = consumer:consume({
|
||||||
|
[uriOpenPattern] = URIMeal(current,uriClosePattern),
|
||||||
|
[expressionOpenPattern] = expressionMeal(current),
|
||||||
|
[multiLineStringOpenPattern] =
|
||||||
|
multiLineStringMeal(
|
||||||
|
current,multiLineStringClosePattern),
|
||||||
|
[singleLineStringPattern] =
|
||||||
|
singleLineStringMeal(
|
||||||
|
current,singleLineStringPattern),
|
||||||
|
[blockDelimiterPattern] = function(words,_)
|
||||||
|
if current:empty() then
|
||||||
|
error("Extravenous semicolon.")
|
||||||
|
end
|
||||||
|
table.insert(expressions,current)
|
||||||
|
current = Expression()
|
||||||
|
end,
|
||||||
|
[blockClosePattern] = function(words,_)
|
||||||
|
current:insert(words)
|
||||||
|
loop = false
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
if #current.items ~= 0 then
|
||||||
|
table.insert(expressions,current)
|
||||||
|
end
|
||||||
|
return Expression(expressions)
|
||||||
|
end
|
||||||
|
function consumeExpression()
|
||||||
|
local current = Expression()
|
||||||
|
-- Loop, adding new expressions and words,
|
||||||
|
-- until closing that is
|
||||||
|
local loop = true
|
||||||
|
while loop do
|
||||||
|
local remaining = consumer:remaining()
|
||||||
|
local expr = consumer:consume({
|
||||||
|
[uriOpenPattern] = URIMeal(current,uriClosePattern),
|
||||||
|
[expressionOpenPattern] = expressionMeal(remaining),
|
||||||
|
[multiLineStringOpenPattern] =
|
||||||
|
multiLineStringMeal(
|
||||||
|
current,multiLineStringClosePattern),
|
||||||
|
[singleLineStringPattern] =
|
||||||
|
singleLineStringMeal(
|
||||||
|
current,singleLineStringPattern),
|
||||||
|
[expressionOpenPattern] = expressionMeal(current),
|
||||||
|
[expressionClosePattern] = function(words,_)
|
||||||
|
current:insert(words)
|
||||||
|
loop = false
|
||||||
|
end,
|
||||||
|
[blockOpenPattern] = function(words,_)
|
||||||
|
current:insert(
|
||||||
|
words,
|
||||||
|
consumeBlock()
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
["$"] = function(words,last)
|
||||||
|
current:insert(remaining)
|
||||||
|
loop = false
|
||||||
|
end
|
||||||
|
})
|
||||||
|
-- This single line below made me procrastinate life for a total of 4 hours on instagram reels; lock in.
|
||||||
|
-- if not expr then print("brk") break end -- Since now closing
|
||||||
|
end
|
||||||
|
return current
|
||||||
|
end
|
||||||
|
return consumeExpression()
|
||||||
|
-- TODO: check for later expressions please?
|
||||||
|
end
|
||||||
|
|
||||||
|
return _G
|
||||||
166
main.lua
Normal file
166
main.lua
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
require("interpret")
|
||||||
|
|
||||||
|
local nelli_scope = Scope()
|
||||||
|
local function B(abstract,callback)
|
||||||
|
nelli_scope:insert(
|
||||||
|
Binding(
|
||||||
|
interpret(abstract),
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
B("print (text)",function(s,e)
|
||||||
|
log(s:evaluate(e.text))
|
||||||
|
end)
|
||||||
|
B("text (literal)",function(s,e)
|
||||||
|
return e.literal:text()
|
||||||
|
end)
|
||||||
|
B("do (expressions)",function(s,e)
|
||||||
|
scope = Scope()
|
||||||
|
scope.parent = s
|
||||||
|
local res
|
||||||
|
print(e.expressions)
|
||||||
|
for item in iterate(e.expressions.items) do
|
||||||
|
if type_of(item) ~= Expression then
|
||||||
|
error(
|
||||||
|
string.format(
|
||||||
|
"Unexpected words '%s' in 'do' expression.",
|
||||||
|
tostring(item)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
res = scope:evaluate(item)
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end)
|
||||||
|
B("if (predicate) then (expression)",function(s,e)
|
||||||
|
if s:evaluate(e.predicate) then
|
||||||
|
return s:evaluate(e.expression)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
B("true",function(s,e) return true end)
|
||||||
|
B("false",function(s,e) return false end)
|
||||||
|
B("(constant) = (expression)",function(s,e)
|
||||||
|
local value = s:evaluate(e.expression)
|
||||||
|
s:insert(
|
||||||
|
Binding(e.constant:text(),function(s,e)
|
||||||
|
return value
|
||||||
|
end)
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
B("format (string) with (terms)",function(s,e)
|
||||||
|
local items = {}
|
||||||
|
for expression in iterate(e.terms.items) do
|
||||||
|
table.insert(items,s:evaluate(expression))
|
||||||
|
end
|
||||||
|
return string.format(
|
||||||
|
s:evaluate(e.string),
|
||||||
|
table.unpack(items)
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
B("while (predicate) (expression)",function(s,e)
|
||||||
|
while s:evaluate(e.predicate) do
|
||||||
|
s:evaluate(e.expression)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
B("repeat (expression) while (predicate)",function(s,e)
|
||||||
|
while true do
|
||||||
|
s:evaluate(e.expression)
|
||||||
|
if not s:evaluate(e.predicate) then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
B("repeat (expression) until (predicate)",function(s,e)
|
||||||
|
while true do
|
||||||
|
s:evaluate(e.expression)
|
||||||
|
if s:evaluate(e.predicate) then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
B("new table",function(s,e)
|
||||||
|
return {}
|
||||||
|
end)
|
||||||
|
B("(index) of (table)",function(s,e)
|
||||||
|
return s:evaluate(e.table)[s:evaluate(e.index)]
|
||||||
|
end)
|
||||||
|
B("set (index) of (table) to (item)",function(s,e)
|
||||||
|
s:evaluate(e.table)[s:evaluate(e.index)]
|
||||||
|
end)
|
||||||
|
B("macro (macro) expands to (expression)",function(s,e)
|
||||||
|
s:insert(Binding(e.abstract,function(_s,_e)
|
||||||
|
for identifier,expression in pairs(_e) do
|
||||||
|
|
||||||
|
end
|
||||||
|
_s:evaluate(e.expression)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
B("define (abstract) as (expression)",function(s,e)
|
||||||
|
s:insert(Binding(e.abstract,function(_s,_e)
|
||||||
|
return s:evaluate(e.expression)
|
||||||
|
end))
|
||||||
|
end)
|
||||||
|
local List = class("List")
|
||||||
|
function List:new(...) do
|
||||||
|
self.items = {...}
|
||||||
|
end
|
||||||
|
B("new list",function(s,e)
|
||||||
|
return List()
|
||||||
|
end)
|
||||||
|
B("insert (item) into (list)",function(s,e)
|
||||||
|
|
||||||
|
end)
|
||||||
|
B("insert (item) into (list) at (index)",function(s,e)
|
||||||
|
|
||||||
|
end)
|
||||||
|
B("remove (needle) from (haystack)",function(s,e)
|
||||||
|
|
||||||
|
end)
|
||||||
|
B("remove from (haystack) at (index)",function(s,e)
|
||||||
|
s:evaluate(e.haystack)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local Variable = class()
|
||||||
|
function Variable:new()
|
||||||
|
end
|
||||||
|
function Variable:set(item)
|
||||||
|
self.data = item
|
||||||
|
end
|
||||||
|
function Variable:get()
|
||||||
|
return self.data
|
||||||
|
end
|
||||||
|
B("let (variable)",function(s,e)
|
||||||
|
local variable = Variable()
|
||||||
|
s:insert(
|
||||||
|
Binding(e.variable,function(s,e)
|
||||||
|
return variable
|
||||||
|
end)
|
||||||
|
)
|
||||||
|
return variable
|
||||||
|
end)
|
||||||
|
B("set (variable) to (expression)",function(s,e)
|
||||||
|
local var = s:evaluate(e.variable)
|
||||||
|
local value = s:evaluate(e.expression)
|
||||||
|
assert_meta(Variable)(var)
|
||||||
|
var:set(value)
|
||||||
|
end)
|
||||||
|
B("get (variable)",function(s,e)
|
||||||
|
local var = s:evaluate(e.variable)
|
||||||
|
assert_meta(Variable)(var)
|
||||||
|
return var:get(value)
|
||||||
|
end)
|
||||||
|
for item in iterate(nelli_scope.bindings) do
|
||||||
|
print(item.template)
|
||||||
|
end
|
||||||
|
|
||||||
|
local args = {...}
|
||||||
|
local function main()
|
||||||
|
-- Take arguments as neli files to read and interpret
|
||||||
|
for _,parameter in pairs(args) do
|
||||||
|
local root = interpret(read(parameter))
|
||||||
|
nelli_scope:evaluate(root)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
main()
|
||||||
13
note
Normal file
13
note
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
I am thinking for the syntax in nel.
|
||||||
|
How can we have multiple arguments for something?
|
||||||
|
It feels rather stupid allowing this really.
|
||||||
|
It's not very clear, a list is a list in any case.
|
||||||
|
So maybe we really should just have another
|
||||||
|
substatement, and forget about powering up
|
||||||
|
the abstracts, for now? ...?
|
||||||
|
|
||||||
|
Well then... in that case.
|
||||||
|
We only need to make sure scoping works.
|
||||||
|
Which means, scopes inherit from eachother,
|
||||||
|
and applying congruent bindings causes an error.
|
||||||
|
I think.
|
||||||
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