Initial commit. Note: this is broken right now.

This commit is contained in:
2026-01-12 13:47:14 +00:00
commit 7de1bb3ef0
6 changed files with 817 additions and 0 deletions

195
helper.lua Normal file
View 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
View 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
View 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
View 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
View 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

8
test.nel Normal file
View File

@@ -0,0 +1,8 @@
do {
let (dog);
print (get (dog));
set (dog) to (text (woof!));
print (get (dog));
if (true) then (print "true after"));
if (false) then (print "false!");
}