Skip to content

Instantly share code, notes, and snippets.

@SquidDev
Created July 23, 2015 14:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SquidDev/cd62fec68259a0af91c6 to your computer and use it in GitHub Desktop.
Save SquidDev/cd62fec68259a0af91c6 to your computer and use it in GitHub Desktop.
Parsel Operators
local parsel = setmetatable({}, {__index = _G})
setfenv(assert(loadfile("lib/parsel.lua") or loadfile("../lib/parsel.lua"), "Cannot find parsel"), parsel)()
local textutils = assert(loadfile("lib/pprint.lua") or loadfile("../pprint.lua"), "Cannot find pprint")()
local a = parsel.symbol "a"
local abtest = (a/"b"/"test"):many()
local braces = abtest:between("{", "}")
local p = (abtest .. braces) % function(res) return { outer = res[1], inner = res[2]} end
local ok, val, cs = p(...)
print(ok,textutils.serialize(val),cs)
-- TABLE UTIL
local function concat(a, b)
local concatted = {}
for i,v in ipairs(a) do
concatted[i] = v
end
for i,v in ipairs(b) do
table.insert(concatted, v)
end
return concatted
end
local function tail(t)
local tl = {}
for i=2,#t do
tl[i - 1] = t[i]
end
return tl
end
local function replicate(n, x)
local t = {}
for i=1, n do
table.insert(t, x)
end
return t
end
-- ERROR UTIL
local function stackError(msg, level)
if type(level) ~= "number" or level == 0 then
level = 1
end
local errors = {}
local lastMsg
while lastMsg ~= msg do
local ok
ok, lastMsg = pcall(error, msg, level)
level = level + 1
if lastMsg ~= msg then
if #errors == 0 then
table.insert(errors, lastMsg)
else
table.insert(errors, " at: " .. lastMsg:sub(1, -3 -#msg))
end
end
end
if #errors >= 16 then
errors[16] = "..."
while table.remove(errors, 17) do end
end
error(table.concat(errors, "\n"), 0)
end
local function stackAssert(cond, msg, level)
if type(level) ~= "number" or level == 0 then
level = 1
end
if cond then
return cond
else
return stackError(msg or "Assertion failed!", level + 1)
end
end
-- PARSER TYPE
Parser = {}
local ParserMeta = {
__index = Parser,
__call = function(self, ...) return self.runParser(...) end,
__add = function(a, b) return new(a):bind(b) end,
__sub = function(a, b) return new(a):discardBind(new(b)) end,
__concat = function(a, b)
a, b = new(a), new(b)
return a:bind(function(aRes)
return b:bind(function(bRes)
return from({aRes, bRes})
end)
end)
end,
__div = function(a, b) return new(a):otherwise(new(b)) end,
__mod = function(a, b) return a:fmap(b) end
}
function new(f)
local t = type(f)
if t == "table" and getmetatable(f) == ParserMeta then
return f
elseif t == "string" then
return symbol(f)
elseif t ~= "table" and t ~= "function" then
stackError("Cannot create parser from " .. t)
end
local _, sou = pcall(error, "", 3)
local function f1(s)
local ok, a, cs = f(stackAssert(s, "Nil string at " .. sou))
stackAssert(cs, "Nil suffix at " .. sou)
return ok, a, cs
end
return setmetatable({runParser=f1}, ParserMeta)
end
function Parser:bind(f)
return new(function(s)
local ok, a, cs = self.runParser(s)
if ok then
return f(a).runParser(cs)
else
return ok, a, cs
end
end)
end
function Parser:discardBind(p)
return self:bind(function() return p end)
end
function Parser:fmap(f)
return self:bind(function(a)
return from(f(a))
end)
end
function Parser:expect(str)
return new(function(s)
local ok, a, cs = self.runParser(s)
if not ok then
return ok, {str}, cs
else
return ok, a, cs
end
end)
end
function Parser:try()
return new(function(s)
local ok, a, cs = self.runParser(s)
if not ok then
return ok, a, s
else
return ok, a, cs
end
end)
end
function Parser:lookahead()
return new(function(s)
local ok, a, cs = self.runParser(s)
if not ok then
return ok, a, cs
else
return ok, a, s
end
end)
end
function Parser:apply(s)
stackAssert(s, "Nil apply string")
return space:discardBind(self).runParser(s)
end
function Parser:otherwise(b)
b = new(b)
return new(function(s)
local ok, a, cs = self.runParser(s)
-- return if ok, or error if input was consumed
if (not ok) and cs == s then
local firstError = a
ok, a, cs = b.runParser(s)
if not ok and cs == s then
return ok, concat(firstError, a), cs
else
return ok, a, cs
end
else
return ok, a, cs
end
end)
end
function Parser:many()
return self:many1():otherwise(from({}))
end
function Parser:many1()
return self:bind(function(a)
return self:many():bind(function(as)
return from(concat({a}, as))
end)
end)
end
function Parser:sepBy(sep)
return self:sepBy1(sep):otherwise(from({}))
end
function Parser:sepBy1(sep)
sep = new(sep)
return self:bind(function(a)
return sep:discardBind(self):many():bind(function(as)
return from(concat({a}, as))
end)
end)
end
function Parser:chainl(op, x)
return self:chainl1(op):otherwise(from(x))
end
function Parser:chainl1(op)
op = new(op)
local function rest(a)
return op:bind(function(f)
return self:bind(function(b)
return rest(f(a, b))
end)
end):otherwise(from(a))
end
return self:bind(rest)
end
function Parser:chainr(op, x)
return self:chainr1(op):otherwise(from(x))
end
function Parser:chainr1(op)
op = new(op)
local function rest(a)
return op:bind(function(f)
return self:bind(rest):bind(function(b)
return from(f(a, b))
end)
end):otherwise(from(a))
end
return self:bind(rest)
end
function Parser:token()
return self:bind(function(a)
return space:discardBind(from(a))
end)
end
function Parser:count(n)
return sequence(replicate(n, self))
end
function Parser:between(start, stop)
start, stop = new(start), new(stop)
return start:discardBind(self:bind(function(a)
return stop:discardBind(from(a))
end))
end
-- CONSTRUCTORS
function fail(str)
return zero:expect(str)
end
function from(a)
return new(function(s) return true, a, s end)
end
function satisfy(f)
return item:bind(function(c)
if f(c) then
return from(c)
else
return zero
end
end):try()
end
function char(c)
return satisfy(function(c1) return c == c1 end):expect(c)
end
function string(s)
if s == "" then
return from("")
else
return char(s:sub(1,1)):discardBind(string(s:sub(2)):discardBind(from(s))):try():expect(s)
end
end
function symbol(s)
return string(s):token()
end
function choice(list)
local p = list[1]
if not p then
return fail("No choices")
end
for i=2, #list do
p = p:otherwise(new(list[i]))
end
return p
end
function sequence(list)
if #list == 0 then
return from({})
else
return new(list[1]):bind(function(a)
return sequence(tail(list)):bind(function(as)
return from(concat({a}, as))
end)
end)
end
end
-- SIMPLE PARSERS
zero = new(function(s) return false, {"Error"}, s end)
item = new(function(s)
if s == "" then
return false, {"Unexpected EOF"}, s
else
return true, s:sub(1,1), s:sub(2)
end
end)
space = satisfy(function(c) return c:find"%s" ~= nil end):many()
local M = { }
local insert = table.insert
M.DEFAULT_CFG = {
hide_hash = false; -- Print the non-array part of tables?
metalua_tag = true; -- Use Metalua's backtick syntax sugar?
keywords = { }; -- Set of keywords which must not use Lua's field shortcuts {["foo"]=...} -> {foo=...}
blacklist = { }; -- Set of fields to not display
max_depth = -1; -- Max depth to traverse
with_name = false; -- If the field 'name' exists, print that instead.
}
local function validId(x, cfg)
if type(x) ~= "string" then return false end
if not x:match "^[a-zA-Z_][a-zA-Z0-9_]*$" then return false end
if cfg.keywords and cfg.keywords[x] then return false end
return true
end
local function serializeImplicit(object, data)
if data.cfg.max_depth > -1 and data.indent > data.cfg.max_depth then
insert(data, "...")
return
end
local t = type(object)
if t == "string" then
insert(data, (string.format("%q", object):gsub("\\\n", "\\n")))
elseif t == "table" and not data.visited[object] then
if data.cfg.with_name and type(object.name) == "string" and object.name ~= "" then
insert(data, '[' .. object.name .. ']')
return
end
data.visited[object] = true
data.indent = data.indent + 1
local hasTag = data.cfg.metalua_tag and validId(object.tag, data.cfg)
if hasTag then
insert(data, '`')
insert(data, object.tag)
end
local shouldNewLine = false
local objLen = #object
local builder = 0
for k,v in pairs(object) do
if (not data.cfg.hide_hash and not data.cfg.blacklist[k]) or (type(k) == "number" and k > 0 and k <= objLen and math.fmod(k, 1) == 0) then
if type(k) == "table" or type(v) == "table" then
shouldNewLine = true
break
else
builder = builder + #tostring(v) + #tostring(k)
if builder > 80 then
shouldNewLine = true
end
end
end
end
local first = false
insert(data, "{")
if not data.cfg.hide_hash then
for k, v in pairs(object) do
if hasTag and k=='tag' then -- pass the 'tag' field
elseif type(k) == "number" and k <= objLen and k > 0 and math.fmod(k,1)==0 then
-- pass array-part keys (consecutive ints less than `#adt`)
elseif not data.cfg.blacklist[k] then
if first then
-- 1st hash-part pair ever found
insert(data, ", ")
else
first = true
end
if shouldNewLine then
insert(data, "\n" .. ("\t"):rep(data.indent))
end
-- Print the key
if validId(k, data.cfg) then
insert(data, k)
else
serializeImplicit(k, data)
end
insert(data, " = ")
serializeImplicit(v, data)
end
end
end
for k,v in ipairs(object) do
if first then
-- 1st hash-part pair ever found
insert(data, ", ")
else
first = true
end
if shouldNewLine then
insert(data, "\n" .. ("\t"):rep(data.indent))
end
serializeImplicit(v, data)
end
data.indent = data.indent - 1
if shouldNewLine then
insert(data, "\n" .. ("\t"):rep(data.indent))
end
insert(data, "}")
-- Allow repeated but not recursive entries
data.visited[object] = false
else
insert(data, tostring(object))
end
end
function M.tostring(object, cfg)
if cfg == nil then
cfg = M.DEFAULT_CFG
else
for k,v in pairs(M.DEFAULT_CFG) do
if cfg[k] == nil then cfg[k] = v end
end
end
local d = {
indent = 0,
cfg = cfg,
visited = {},
}
serializeImplicit(object, d)
return table.concat(d)
end
function M.print(...)
local args = {...}
for i,v in pairs(args) do args[i] = M.tostring(v) end
return print(unpack(args))
end
function M.sprintf(fmt, ...)
local args = {...}
for i,v in pairs(args) do args[i] = M.tostring(v) end
return string.format(fmt, unpack(args))
end
function M.printf(...) print(M.sprintf(...)) end
M.serialize = M.tostring
return M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment