336 lines
10 KiB
Lua
336 lines
10 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
|
|
local NONE = {}
|
|
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 callback = nil
|
|
local earliest = {}
|
|
--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 earliest.index) or (earliest.index > new_index))
|
|
then
|
|
if not new_index then return end -- (later:) what why?
|
|
earliest.index = new_index
|
|
earliest.final = new_final
|
|
earliest.pattern = pattern
|
|
callback = new_func
|
|
earliest.excess = self.range.reader:get(
|
|
self.range.first or 1,
|
|
earliest.index - 1
|
|
)
|
|
end
|
|
end
|
|
-- Pass into the func
|
|
if not callback then
|
|
if patterns[NONE] then
|
|
return patterns[NONE]()
|
|
end
|
|
return nil
|
|
end
|
|
assert(earliest.index) assert(earliest.final)
|
|
self.range:move(earliest.final+1) -- Move range to after the match
|
|
local fmatch = self.range.reader:match(earliest.pattern,earliest.final)
|
|
|
|
-- This seems to be broken:
|
|
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 callback(earliest,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.multiLineCommentClose = "%*/"
|
|
patterns.colonSyntax = ":"
|
|
|
|
-- Give me: {patterns that take (match,state)}, the state, and a handler for comments
|
|
-- I will give you a function that will return the result of consumePatterns(patterns)
|
|
-- With escapes handled.
|
|
local function makeConsumerEscaping(patterns,state,handler)
|
|
return function()
|
|
handler = handler or function() end
|
|
local escaped = true
|
|
local function escape() escaped = true end
|
|
while escaped do
|
|
escaped = false
|
|
consumer:consumePatterns(union({
|
|
[patterns.singleLineComment] = function(...)
|
|
handler(...)
|
|
consumer:consumePatterns({
|
|
[patterns.newLine] = escape
|
|
})
|
|
end,
|
|
[patterns.multiLineCommentOpen] = function(...)
|
|
handler(...)
|
|
consumer:consumePatterns({
|
|
[NONE] = error,
|
|
[patterns.multiLineCommentClose] = escape
|
|
})
|
|
end,
|
|
},patterns),state)
|
|
end
|
|
end
|
|
end
|
|
-- Give me {patterns that take (match,current)} and current (expression)
|
|
local function makeConsumerAppending(patterns,state)
|
|
local appendingPatterns = map(patterns,function(func)
|
|
return function(match,current)
|
|
current:insert(match.excess,func(match,current))
|
|
end
|
|
end)
|
|
makeConsumerEscaping(appendingPatterns,function(match,current)
|
|
current:insert(match.excess)
|
|
end)
|
|
end
|
|
local function makeConsumerExpressive()
|
|
local function consumeSingleLineString()
|
|
return
|
|
end
|
|
local function consumeNamed()
|
|
|
|
end
|
|
local function consumeMultiLineString()
|
|
|
|
end
|
|
local function consumeURI()
|
|
|
|
end
|
|
return makeConsumerAppending({
|
|
[patterns.singleLineString] = function(match,current)
|
|
return consumer:consumePatterns({
|
|
["[\n\r]"] = function()
|
|
error("Incomplete string literal")
|
|
end,
|
|
[patterns.singleLineString] = function(words,_)
|
|
return expression({"text",Expression({words})})
|
|
end
|
|
})
|
|
end,
|
|
[patterns.named] = function(words,_,current)
|
|
current:insert(words,consumePatternsEscaping({
|
|
[patterns.named] = function(words,_)
|
|
return expression({"the",Expression({words})})
|
|
end
|
|
}))
|
|
end,
|
|
[patterns.multiLineStringOpen] = function(match,current)
|
|
return consumer:consumePatterns({
|
|
[patterns.multiLineStringClose] = function(words,_)
|
|
return expression({"text",Expression({words})})
|
|
end
|
|
})
|
|
end,
|
|
[patterns.multiLineStringOpen] = function(words,_,current)
|
|
current:insert(words,consumePatternsEscaping({
|
|
[patterns.multiLineStringClose] = function(words,_)
|
|
return expression({"text",Expression({words})})
|
|
end
|
|
}))
|
|
end,
|
|
[patterns.uriOpen] = function(words,_,current)
|
|
current:insert()
|
|
current:insert(words,consumePatternsEscaping({
|
|
["[\n\r]"] = function()
|
|
current:error("Incomplete URI literal")
|
|
end,
|
|
[patterns.uriClose] = function(path)
|
|
return read(path)
|
|
end
|
|
}))
|
|
end,
|
|
[patterns.expressionOpen] = function(match,current)
|
|
return consumeExpression()
|
|
end,
|
|
[patterns.colonSyntax] = function(match,current)
|
|
return consumeExpression()
|
|
end,
|
|
[patterns.blockOpen] = function(match,current)
|
|
return consumeBlock()
|
|
end,
|
|
})
|
|
end
|
|
local function consumePatternsExpressive(patterns,current)
|
|
|
|
return consumePatternsEscaping
|
|
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 = consumePatternsEscaping(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 = consumePatternsEscaping(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 |