-- [ʞ] sirsem.lua
-- ~ lexu hale <lexi@hale.su>
-- ? utility library with functionality common to
-- cortav.lua and its extensions
-- from Ranuir "software utility"
-- > local ss = require 'sirsem.lua'
local ss
do -- pull ourselves up by our own bootstraps
local package = _G.package -- prevent namespace from being broken by env shenanigans
local function namespace(name, tbl)
local pkg = tbl or {}
if package then
package.loaded[name] = pkg
end
return pkg
end
ss = namespace 'sirsem'
ss.namespace = namespace
end
function ss.map(fn, lst)
local new = {}
for k,v in pairs(lst) do
table.insert(new, fn(v,k))
end
return new
end
function ss.reduce(fn, acc, lst)
for i,v in ipairs(lst) do
acc = fn(acc, v, i)
end
return acc
end
function ss.filter(list, fn)
local new = {}
for i, v in ipairs(list) do
if fn(v,i) then table.insert(new, v) end
end
return new
end
function ss.kmap(fn, list)
local new = {}
for k, v in pairs(list) do
local nk,nv = fn(k,v)
new[nk or k] = nv or v
end
return new
end
function ss.kfilter(list, fn)
local new = {}
for k, v in pairs(list) do
if fn(k,v) then new[k] = v end
end
return new
end
function ss.find(tbl, pred, ...)
pred = pred or function(a,b)
return a == b
end
for k,v in pairs(tbl) do
if pred(v,k,...) then return k,v end
end
return nil
end
function ss.clone(tbl) -- performs a shallow copy
if tbl.clone then return tbl:clone() end
local new = {}
for k,v in pairs(tbl) do
new[k] = v
end
return new
end
function ss.copy(tbl) -- performs a deep copy
local new = {}
for k,v in pairs(tbl) do
if type(v) == 'table' then
if v.clone then
new[k] = v:clone() -- this may or may not do a deep copy!
else
new[k] = ss.copy(v)
end
else
new[k] = v
end
end
return new
end
function ss.delegate(tbl,tpl) -- returns a table that looks up keys it lacks from
-- tbl (lightweight alternative to shallow copies)
tpl = tpl or {}
return setmetatable({}, {__index=tbl})
end
ss.str = {}
function ss.str.begins(str, pfx)
return string.sub(str, 1, #pfx) == pfx
end
ss.str.enc = {
utf8 = {
char = utf8.char;
codepoint = utf8.codepoint;
};
c6b = {};
ascii = {};
}
function ss.str.enc.utf8.each(str, ascode)
local pos = {
code = 1;
byte = 1;
}
return function()
if pos.byte > #str then return nil end
local thischar = utf8.codepoint(str, pos.byte)
local lastpos = {
code = pos.code;
byte = pos.byte;
next = pos;
}
if not ascode then
thischar = utf8.char(thischar)
pos.byte = pos.byte + #thischar
else
pos.byte = pos.byte + #utf8.char(thischar)
end
pos.code = pos.code + 1
return thischar, lastpos
end
end
ss.math = {}
function ss.math.lerp(t, a, b)
return (1-t)*a + (t*b)
end
function ss.dump(o, state, path, depth)
state = state or {tbls = {}}
depth = depth or 0
local pfx = string.rep(' ', depth)
if type(o) == "table" then
local str = ''
for k,p in pairs(o) do
local done = false
local exp
if type(p) == 'table' then
if state.tbls[p] then
exp = '<' .. state.tbls[p] ..'>'
done = true
else
state.tbls[p] = path and string.format('%s.%s', path, k) or k
end
end
if not done then
local function dodump() return ss.dump(
p, state,
path and string.format("%s.%s", path, k) or k,
depth + 1
) end
-- boy this is ugly
if type(p) ~= 'table' or
getmetatable(p) == nil or
getmetatable(p).__tostring == nil then
exp = dodump()
end
if type(p) == 'table' then
exp = string.format('{\n%s%s}', exp, pfx)
local meta = getmetatable(p)
if meta then
if meta.__tostring then
exp = tostring(p)
end
if meta.__name then
exp = meta.__name .. ' ' .. exp
end
end
end
end
str = str .. pfx .. string.format("%s = %s\n", k, exp)
end
return str
elseif type(o) == "string" then
return string.format('“%s”', o)
else
return tostring(o)
end
end
function ss.hexdump(s)
local hexlines, charlines = {},{}
for i=1,#s do
local line = math.floor((i-1)/16) + 1
hexlines[line] = (hexlines[line] or '') .. string.format("%02x ",string.byte(s, i))
charlines[line] = (charlines[line] or '') .. ' ' .. string.gsub(string.sub(s, i, i), '[^%g ]', '\x1b[;35m·\x1b[36;1m') .. ' '
end
local str = ''
for i=1,#hexlines do
str = str .. '\x1b[1;36m' .. charlines[i] .. '\x1b[m\n' .. hexlines[i] .. '\n'
end
return str
end
function ss.declare(c)
local cls = setmetatable({
__name = c.ident;
}, {
__name = 'class';
__tostring = function() return c.ident or '(class)' end;
})
cls.__call = c.call
cls.__index = function(self, k)
if c.default and c.default[k] then
return c.default[k]
end
if k == 'clone' then
return function(self)
local new = cls.mk()
for k,v in pairs(self) do
new[k] = v
end
if c.clonesetup then
c.clonesetup(new, self)
end
return new
end
elseif k == 'to' then
return function(self, to, ...)
if to == 'string' then return tostring(self)
elseif to == 'number' then return tonumber(self)
elseif to == 'int' then return math.floor(tonumber(self))
elseif c.cast and c.cast[to] then
return c.cast[to](self, ...)
elseif type(to) == 'table' and getmetatable(to) and getmetatable(to).cvt and getmetatable(to).cvt[cls] then
else error((c.ident or 'class') .. ' is not convertible to ' .. (type(to) == 'string' and to or tostring(to))) end
end
end
if c.fns then return c.fns[k] end
end
if c.cast then
if c.cast.string then
cls.__tostring = c.cast.string
end
if c.cast.number then
cls.__tonumber = c.cast.number
end
end
cls.mk = function(...)
local val = setmetatable(c.mk and c.mk(...) or {}, cls)
if c.init then
for k,v in pairs(c.init) do
val[k] = v
end
end
if c.construct then
c.construct(val, ...)
end
return val
end
getmetatable(cls).__call = function(_, ...) return cls.mk(...) end
cls.is = function(o) return getmetatable(o) == cls end
return cls
end
-- tidy exceptions
ss.exn = ss.declare {
ident = 'exn';
mk = function(kind, ...)
return {
vars = {...};
kind = kind;
}
end;
cast = {
string = function(me)
return me.kind.report(table.unpack(me.vars))
end;
};
fns = {
throw = function(me) error(me) end;
}
}
ss.exnkind = ss.declare {
ident = 'exn-kind';
mk = function(desc, report)
return {
desc = desc;
report = report or function(msg,...)
return string.format(msg,...)
end;
}
end;
call = function(me, ...)
return ss.exn(me, ...)
end;
}
ss.str.exn = ss.exnkind 'failure while string munging'
function ss.str.delimit(encoding, start, stop, s)
local depth = 0
encoding = encoding or ss.str.enc.utf8
if not ss.str.begins(s, start) then return nil end
for c,p in encoding.each(s) do
if c == (encoding.escape or '\\') then
p.next.byte = p.next.byte + #encoding.char(encoding.codepoint(s, p.next.byte))
p.next.code = p.next.code + 1
elseif c == start then
depth = depth + 1
elseif c == stop then
depth = depth - 1
if depth == 0 then
return s:sub(1+#start, p.byte - #stop), p.byte -- FIXME
elseif depth < 0 then
ss.str.exn('out of place token “%s”', stop):throw()
end
end
end
ss.str.exn('token “%s” expected before end of line', stop):throw()
end
ss.version = ss.declare {
name = 'version';
mk = function(tbl) return tbl end;
fns = {
pre = function(self,other) end;
post = function(self,other) end;
string = function(self) return tostring(self) end;
};
cast = {
string = function(vers)
if not(next(vers)) then return '0.0' end
local str = ''
for _,v in pairs(vers) do
if type(v) == 'string' then
if str ~= '' then str = str .. '-' end
str = str .. v
else
if str ~= '' then str = str .. '.' end
str = str .. tostring(v)
end
end
return str
end
};
}
function ss.classinstance(o)
local g = getmetatable(o)
if not o then return nil end
local mm = getmetatable(g)
if not o then return nil end
if mm.__name == 'class' then
return g
else
return nil
end
end
function ss.walk(o, key, ...)
if o[key] then
if select('#', ...) == 0 then
return o[key]
else
return ss.walk(o[key], ...)
end
end
return nil
end
function ss.coalesce(x, ...)
if x ~= nil then
return x
elseif select('#', ...) == 0 then
return nil
else
return ss.coalesce(...)
end
end