Created
July 23, 2015 14:58
-
-
Save SquidDev/cd62fec68259a0af91c6 to your computer and use it in GitHub Desktop.
Parsel Operators
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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