commit 7de1bb3ef06c24fd76dded68fb300cb9d29f9fa1 Author: Christian Lincoln Date: Mon Jan 12 13:47:14 2026 +0000 Initial commit. Note: this is broken right now. diff --git a/helper.lua b/helper.lua new file mode 100644 index 0000000..dc3a149 --- /dev/null +++ b/helper.lua @@ -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 diff --git a/interpret.lua b/interpret.lua new file mode 100644 index 0000000..7a1de85 --- /dev/null +++ b/interpret.lua @@ -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 diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..517b301 --- /dev/null +++ b/main.lua @@ -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() diff --git a/note b/note new file mode 100644 index 0000000..2d552ec --- /dev/null +++ b/note @@ -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. diff --git a/parse.lua b/parse.lua new file mode 100644 index 0000000..8849831 --- /dev/null +++ b/parse.lua @@ -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 "()"! +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 diff --git a/test.nel b/test.nel new file mode 100644 index 0000000..35cbe85 --- /dev/null +++ b/test.nel @@ -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!"); +}