Files
nel/nellie/parser.lua
2026-03-08 18:59:45 +00:00

277 lines
8.4 KiB
Lua

require("nellie.interpreter")
-- 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
--[[
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:consumePatterns(patterns,state)
-- t: {string = function...}
assert_type("table")(patterns,"Arg 't' #1: %s")
if not next(patterns) then
error("The match table cannot be empty!",2)
end
for index,func in pairs(patterns) 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 -- TODO: make this not be a bunch of locals. New class?
for pattern,new_func in pairs(patterns) 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,state)
end
-- Interpret some text
function Parse(content,uri,chain)
-- LOCALS
-- So that all functions have access to eachother
local consume,
consumeExpression,
consumeBlock
local consumer = Consumer(content)
local function expression(...)
return Expression(...):locate(
string.format(
"%i:%i",
consumer.row,
consumer.col
),uri)
end
local baseExpression = expression()
local patterns = {}
patterns.contentClose = "$"
-- The pattern for words inbetween expressions
patterns.expressionWord = "(.*)"
-- The patterns to open and close expressions,
-- the words scattered inbetween them will be matches
patterns.expressionOpen = "%("
patterns.expressionClose = "%)"
patterns.subExpressionOpen = "%:"
patterns.subExpressionClose = "%,"
patterns.blockOpen = "%{"
patterns.blockClose = "%}"
patterns.blockDelimiter = "[;]+"
patterns.newLine = "[\n\r]+"
patterns.singleLineString = "\""
patterns.named = "'"
patterns.multiLineStringOpen = "%[%["
patterns.multiLineStringClose = "%]%]"
patterns.uriOpen = "<"
patterns.uriClose = ">"
patterns.singleLineComment = "//"
patterns.multiLineCommentOpen = "/%*"
patterns.multiLineCommentOpen = "%*/"
patterns.colonSyntax = ":"
-- TODO: I don't like that this structure does: open(stuff;close) instead of open(stuff)close
-- TODO: current:insert() is repeated in EVERY meal. I wonder if this should be fixed -__-
local expressionMeals = {
[patterns.singleLineString] = function(words,_,current)
current:insert(words,consumer:consumePatterns({
[patterns.singleLineString] = function(words,_)
return expression({"text",Expression({words})})
end
}))
end,
[patterns.named] = function(words,_,current)
current:insert(words,consumer:consumePatterns({
[patterns.named] = function(words,_)
return expression({"the",Expression({words})})
end
}))
end,
[patterns.multiLineStringOpen] = function(words,_,current)
current:insert(words,consumer:consumePatterns({
["[\n\r]"] = function()
error("Incomplete string literal")
end,
[patterns.multiLineStringClose] = function(words,_)
return expression({"text",Expression({words})})
end
}))
end,
[patterns.uriOpen] = function(words,_,current)
current:insert(words,consumer:consumePatterns({
["[\n\r]"] = function()
current:error("Incomplete URI literal")
end,
[patterns.uriClose] = function(path)
return read(path)
end
}))
end,
[patterns.expressionOpen] = function(words,_,current)
current:insert(words,consumeExpression())
end,
[patterns.singleLineComment] = function(words,_,current)
--current:insert(words) -- Consume what was left
consumer:consumePatterns({
[patterns.newLine] = function() end
})
end,
[patterns.multiLineCommentOpen] = function(words,_,current)
current:insert(words)
consumer:consumePatterns({
[patterns.multiLineCommentClose] = function() end
})
end,
[patterns.colonSyntax] = function(words,_,current)
current:insert(words,consumeExpression())
end,
[patterns.blockOpen] = function(words,_,current)
current:insert(words,consumeBlock())
end,
}
-- Consume a {} block of code
function consumeBlock(closing_pattern)
closing_pattern = closing_pattern or patterns.blockClose
local expressions = {}
local current = expression()
local loop = true
while loop do
local remaining = consumer:remaining()
local expr = consumer:consumePatterns(union(expressionMeals,{
[patterns.blockDelimiter] = function(words,_)
current:insert(words)
if current:empty() then
--error("Extravenous semicolon.")
else
table.insert(expressions,current)
end
current = expression()
end,
[closing_pattern] = function(words,_)
current:insert(words)
loop = false
end,
}),current)
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:consumePatterns(union(expressionMeals,{
[patterns.expressionClose] = function(words,_,_current)
current:insert(words)
loop = false
end
}),current)
-- 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 consumeBlock(patterns.contentClose)
-- TODO: check for later expressions please?
end