nomsu/nomsu.lua
Bruce Hill 568a44ef19 Reworking some stuff so that functions only allow expressions to be
return values with either an explicit "return" statement or if they're
the only line in the function, and the line is an expression.
2018-01-07 18:45:27 -08:00

1511 lines
50 KiB
Lua

local re = require('re')
local lpeg = require('lpeg')
local utils = require('utils')
local repr, stringify, min, max, equivalent, set, is_list, sum
repr, stringify, min, max, equivalent, set, is_list, sum = utils.repr, utils.stringify, utils.min, utils.max, utils.equivalent, utils.set, utils.is_list, utils.sum
local colors = setmetatable({ }, {
__index = function()
return ""
end
})
local colored = setmetatable({ }, {
__index = function(_, color)
return (function(msg)
return colors[color] .. (msg or '') .. colors.reset
end)
end
})
local insert, remove, concat
do
local _obj_0 = table
insert, remove, concat = _obj_0.insert, _obj_0.remove, _obj_0.concat
end
if _VERSION == "Lua 5.1" then
local xp = xpcall
local xpcall
xpcall = function(f, errhandler, ...)
local args = {
n = select("#", ...),
...
}
return xp(function(...)
return f(unpack(args, 1, args.n))
end), errhandler
end
end
lpeg.setmaxstack(10000)
local P, R, V, S, Cg, C, Cp, B, Cmt
P, R, V, S, Cg, C, Cp, B, Cmt = lpeg.P, lpeg.R, lpeg.V, lpeg.S, lpeg.Cg, lpeg.C, lpeg.Cp, lpeg.B, lpeg.Cmt
local STRING_ESCAPES = {
n = "\n",
t = "\t",
b = "\b",
a = "\a",
v = "\v",
f = "\f",
r = "\r"
}
local DIGIT, HEX = R('09'), R('09', 'af', 'AF')
local ESCAPE_CHAR = (P("\\") * S("xX") * C(HEX * HEX)) / function(self)
return string.char(tonumber(self, 16))
end
ESCAPE_CHAR = ESCAPE_CHAR + ((P("\\") * C(DIGIT * (DIGIT ^ -2))) / function(self)
return string.char(tonumber(self))
end)
ESCAPE_CHAR = ESCAPE_CHAR + ((P("\\") * C(S("ntbavfr"))) / STRING_ESCAPES)
local OPERATOR_CHAR = S("'~`!@$^&*-+=|<>?/")
local UTF8_CHAR = (R("\194\223") * R("\128\191") + R("\224\239") * R("\128\191") * R("\128\191") + R("\240\244") * R("\128\191") * R("\128\191") * R("\128\191"))
local IDENT_CHAR = R("az", "AZ", "09") + P("_") + UTF8_CHAR
local parse
do
local ctx = { }
local indent_patt = P(function(self, start)
local spaces = self:match("[ \t]*", start)
if #spaces > ctx.indent_stack[#ctx.indent_stack] then
insert(ctx.indent_stack, #spaces)
return start + #spaces
end
end)
local dedent_patt = P(function(self, start)
local spaces = self:match("[ \t]*", start)
if #spaces < ctx.indent_stack[#ctx.indent_stack] then
remove(ctx.indent_stack)
return start
end
end)
local nodent_patt = P(function(self, start)
local spaces = self:match("[ \t]*", start)
if #spaces == ctx.indent_stack[#ctx.indent_stack] then
return start + #spaces
end
end)
local gt_nodent_patt = P(function(self, start)
local spaces = self:match("[ \t]*", start)
if #spaces >= ctx.indent_stack[#ctx.indent_stack] + 4 then
return start + ctx.indent_stack[#ctx.indent_stack] + 4
end
end)
local defs = {
nl = P("\n"),
ws = S(" \t"),
tonumber = tonumber,
operator = OPERATOR_CHAR,
print = function(src, pos, msg)
return print(msg, pos, repr(src:sub(math.max(0, pos - 16), math.max(0, pos - 1)) .. "|" .. src:sub(pos, pos + 16))) or true
end,
utf8_char = (R("\194\223") * R("\128\191") + R("\224\239") * R("\128\191") * R("\128\191") + R("\240\244") * R("\128\191") * R("\128\191") * R("\128\191")),
indented = indent_patt,
nodented = nodent_patt,
dedented = dedent_patt,
gt_nodented = gt_nodent_patt,
escape_char = ESCAPE_CHAR,
error = function(src, pos, err_msg)
if ctx.source_code:sub(pos, pos) == "\n" then
pos = pos + #ctx.source_code:match("[ \t\n]*", pos)
end
local line_no = 1
while (ctx.line_starts[line_no + 1] or math.huge) < pos do
line_no = line_no + 1
end
local prev_line = line_no > 1 and ctx.source_code:match("[^\n]*", ctx.line_starts[line_no - 1]) or ""
local err_line = ctx.source_code:match("[^\n]*", ctx.line_starts[line_no])
local next_line = line_no < #ctx.line_starts and ctx.source_code:match("[^\n]*", ctx.line_starts[line_no + 1]) or ""
local pointer = ("-"):rep(pos - ctx.line_starts[line_no]) .. "^"
err_msg = (err_msg or "Parse error") .. " in " .. tostring(ctx.filename) .. " on line " .. tostring(line_no) .. ":\n"
err_msg = err_msg .. "\n" .. tostring(prev_line) .. "\n" .. tostring(err_line) .. "\n" .. tostring(pointer) .. "\n" .. tostring(next_line) .. "\n"
return error(err_msg)
end,
FunctionCall = function(start, value, stop)
local stub = concat((function()
local _accum_0 = { }
local _len_0 = 1
for _index_0 = 1, #value do
local t = value[_index_0]
_accum_0[_len_0] = (t.type == "Word" and t.value or "%")
_len_0 = _len_0 + 1
end
return _accum_0
end)(), " ")
local line_no = 1
while (ctx.line_starts[line_no + 1] or math.huge) < start do
line_no = line_no + 1
end
local src = ctx.source_code:sub(start, stop - 1)
return {
type = "FunctionCall",
src = src,
line_no = tostring(ctx.filename) .. ":" .. tostring(line_no),
value = value,
stub = stub
}
end
}
setmetatable(defs, {
__index = function(self, key)
local fn
fn = function(start, value, stop)
return {
start = start,
stop = stop,
value = value,
src = ctx.source_code:sub(start, stop - 1),
type = key
}
end
self[key] = fn
return fn
end
})
local peg_tidier = re.compile([[ file <- {~ %nl* (def/comment) (%nl+ (def/comment))* %nl* ~}
def <- anon_def / captured_def
anon_def <- ({ident} (" "*) ":"
{((%nl " "+ [^%nl]*)+) / ([^%nl]*)}) -> "%1 <- %2"
captured_def <- ({ident} (" "*) "(" {ident} ")" (" "*) ":"
{((%nl " "+ [^%nl]*)+) / ([^%nl]*)}) -> "%1 <- ({} %3 {}) -> %2"
ident <- [a-zA-Z_][a-zA-Z0-9_]*
comment <- "--" [^%nl]*
]])
local nomsu = peg_tidier:match(io.open("nomsu.peg"):read("*a"))
nomsu = re.compile(nomsu, defs)
parse = function(source_code, filename)
local old_ctx = ctx
ctx = {
source_code = source_code,
filename = filename,
indent_stack = {
0
}
}
ctx.line_starts = re.compile("lines <- {| line ('\n' line)* |} line <- {} [^\n]*"):match(source_code)
local tree = nomsu:match(source_code)
ctx = old_ctx
return tree
end
end
local NomsuCompiler
do
local _class_0
local _base_0 = {
writeln = function(self, ...)
self:write(...)
return self:write("\n")
end,
errorln = function(self, ...)
self:write_err(...)
return self:write_err("\n")
end,
def = function(self, signature, thunk, src, is_macro)
if is_macro == nil then
is_macro = false
end
if type(signature) == 'string' then
signature = self:get_stubs({
signature
})
elseif type(signature) == 'table' and type(signature[1]) == 'string' then
signature = self:get_stubs(signature)
end
self:assert(type(thunk) == 'function', "Bad thunk: " .. tostring(repr(thunk)))
local canonical_args = nil
local canonical_escaped_args = nil
local aliases = { }
self.__class.def_number = self.__class.def_number + 1
local def = {
thunk = thunk,
src = src,
is_macro = is_macro,
aliases = { },
def_number = self.__class.def_number,
defs = self.defs
}
local where_defs_go = (getmetatable(self.defs) or { }).__newindex or self.defs
for _index_0 = 1, #signature do
local _des_0 = signature[_index_0]
local stub, arg_names, escaped_args
stub, arg_names, escaped_args = _des_0[1], _des_0[2], _des_0[3]
self:assert(stub, "NO STUB FOUND: " .. tostring(repr(signature)))
if self.debug then
self:writeln(tostring(colored.bright("DEFINING RULE:")) .. " " .. tostring(colored.underscore(colored.magenta(repr(stub)))) .. " " .. tostring(colored.bright("WITH ARGS")) .. " " .. tostring(colored.dim(repr(arg_names))))
end
for i = 1, #arg_names - 1 do
for j = i + 1, #arg_names do
if arg_names[i] == arg_names[j] then
self:error("Duplicate argument in function " .. tostring(stub) .. ": '" .. tostring(arg_names[i]) .. "'")
end
end
end
if canonical_args then
self:assert(equivalent(set(arg_names), canonical_args), "Mismatched args")
else
canonical_args = set(arg_names)
end
if canonical_escaped_args then
self:assert(equivalent(escaped_args, canonical_escaped_args), "Mismatched escaped args")
else
canonical_escaped_args = escaped_args
def.escaped_args = escaped_args
end
insert(def.aliases, stub)
local stub_def = setmetatable({
stub = stub,
arg_names = arg_names,
escaped_args = escaped_args
}, {
__index = def
})
rawset(where_defs_go, stub, stub_def)
end
end,
defmacro = function(self, signature, thunk, src)
return self:def(signature, thunk, src, true)
end,
scoped = function(self, thunk)
local old_defs = self.defs
local new_defs = {
["#vars"] = setmetatable({ }, {
__index = self.defs["#vars"]
}),
["#loaded_files"] = setmetatable({ }, {
__index = self.defs["#loaded_files"]
})
}
self.defs = setmetatable(new_defs, {
__index = old_defs
})
local ok, ret1, ret2 = pcall(thunk, self)
self.defs = old_defs
if not ok then
self:error(ret1)
end
return ret1, ret2
end,
serialize_defs = function(self, scope, after)
if scope == nil then
scope = nil
end
if after == nil then
after = nil
end
after = after or (self.core_defs or 0)
scope = scope or self.defs
local defs_by_num = { }
for stub, def in pairs(scope) do
if def and stub:sub(1, 1) ~= "#" then
defs_by_num[def.def_number] = def
end
end
local keys
do
local _accum_0 = { }
local _len_0 = 1
for k, v in pairs(defs_by_num) do
_accum_0[_len_0] = k
_len_0 = _len_0 + 1
end
keys = _accum_0
end
table.sort(keys)
local buff = { }
local k_i = 1
local _using = nil
local _using_do = { }
for k_i, i in ipairs(keys) do
local _continue_0 = false
repeat
if i <= after then
_continue_0 = true
break
end
local def = defs_by_num[i]
if def.defs == scope then
if def.src then
insert(buff, def.src)
end
_continue_0 = true
break
end
if _using == def.defs then
if def.src then
insert(_using_do, def.src)
end
else
_using = def.defs
_using_do = {
def.src
}
end
if k_i == #keys or defs_by_num[keys[k_i + 1]].defs ~= _using then
insert(buff, "using:\n " .. tostring(self:indent(self:serialize_defs(_using))) .. "\n..do:\n " .. tostring(self:indent(concat(_using_do, "\n"))))
end
_continue_0 = true
until true
if not _continue_0 then
break
end
end
for k, v in pairs(scope["#vars"] or { }) do
insert(buff, "<%" .. tostring(k) .. "> = " .. tostring(self:value_to_nomsu(v)))
end
return concat(buff, "\n")
end,
call = function(self, stub, line_no, ...)
local def = self.defs[stub]
if def and def.is_macro and self.callstack[#self.callstack] ~= "#macro" then
self:error("Attempt to call macro at runtime: " .. tostring(stub) .. "\nThis can be caused by using a macro in a function that is defined before the macro.")
end
insert(self.callstack, {
stub,
line_no
})
if not (def) then
self:error("Attempt to call undefined function: " .. tostring(stub))
end
if not (def.is_macro) then
self:assert_permission(stub)
end
local thunk, arg_names
thunk, arg_names = def.thunk, def.arg_names
local args
do
local _tbl_0 = { }
for i, name in ipairs(arg_names) do
_tbl_0[name] = select(i, ...)
end
args = _tbl_0
end
if self.debug then
self:write(tostring(colored.bright("CALLING")) .. " " .. tostring(colored.magenta(colored.underscore(stub))) .. " ")
self:writeln(tostring(colored.bright("WITH ARGS:")) .. " " .. tostring(colored.dim(repr(args))))
end
local old_defs
old_defs, self.defs = self.defs, def.defs
local rets = {
thunk(self, args)
}
self.defs = old_defs
remove(self.callstack)
return unpack(rets)
end,
run_macro = function(self, tree)
local args
do
local _accum_0 = { }
local _len_0 = 1
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local arg = _list_0[_index_0]
if arg.type ~= "Word" then
_accum_0[_len_0] = arg
_len_0 = _len_0 + 1
end
end
args = _accum_0
end
if self.debug then
self:write(tostring(colored.bright("RUNNING MACRO")) .. " " .. tostring(colored.underscore(colored.magenta(tree.stub))) .. " ")
self:writeln(tostring(colored.bright("WITH ARGS:")) .. " " .. tostring(colored.dim(repr(args))))
end
insert(self.callstack, "#macro")
local expr, statement = self:call(tree.stub, tree.line_no, unpack(args))
remove(self.callstack)
return expr, statement
end,
dedent = function(self, code)
if not (code:find("\n")) then
return code
end
local spaces, indent_spaces = math.huge, math.huge
for line in code:gmatch("\n([^\n]*)") do
local _continue_0 = false
repeat
if line:match("^%s*#.*") then
_continue_0 = true
break
else
do
local s = line:match("^(%s*)%.%..*")
if s then
spaces = math.min(spaces, #s)
else
do
s = line:match("^(%s*)%S.*")
if s then
indent_spaces = math.min(indent_spaces, #s)
end
end
end
end
end
_continue_0 = true
until true
if not _continue_0 then
break
end
end
if spaces ~= math.huge and spaces < indent_spaces then
return (code:gsub("\n" .. (" "):rep(spaces), "\n"))
else
return (code:gsub("\n" .. (" "):rep(indent_spaces), "\n "))
end
end,
indent = function(self, code)
return (code:gsub("\n", "\n "))
end,
assert_permission = function(self, stub)
local fn_def = self.defs[stub]
if not (fn_def) then
self:error("Undefined function: " .. tostring(fn_name))
end
local whiteset = fn_def.whiteset
if whiteset == nil then
return true
end
local _list_0 = self.callstack
for _index_0 = 1, #_list_0 do
local caller = _list_0[_index_0]
if caller ~= "#macro" and whiteset[caller[1]] then
return true
end
end
return self:error("You do not have the authority to call: " .. tostring(stub))
end,
check_permission = function(self, fn_def)
if getmetatable(fn_def) ~= functiondef_mt then
local fn_name = fn_def
fn_def = self.defs[fn_name]
if fn_def == nil then
self:error("Undefined function: " .. tostring(fn_name))
end
end
local whiteset = fn_def.whiteset
if whiteset == nil then
return true
end
local _list_0 = self.callstack
for _index_0 = 1, #_list_0 do
local caller = _list_0[_index_0]
if caller ~= "#macro" and whiteset[caller[1]] then
return true
end
end
return false
end,
parse = function(self, str, filename)
if self.debug then
self:writeln(tostring(colored.bright("PARSING:")) .. "\n" .. tostring(colored.yellow(str)))
end
str = str:gsub("\r", "")
local tree = parse(str, filename)
self:assert(tree, "In file " .. tostring(colored.blue(filename)) .. " failed to parse:\n" .. tostring(colored.onyellow(colored.black(str))))
if self.debug then
self:writeln("PARSE TREE:")
self:print_tree(tree, " ")
end
return tree
end,
run = function(self, src, filename, vars, max_operations, output_file)
if vars == nil then
vars = { }
end
if max_operations == nil then
max_operations = nil
end
if output_file == nil then
output_file = nil
end
if src == "" then
return nil, "", vars
end
if max_operations then
local timeout
timeout = function()
debug.sethook()
return self:error("Execution quota exceeded. Your code took too long.")
end
debug.sethook(timeout, "", max_operations)
end
local tree = self:parse(src, filename)
self:assert(tree, "Tree failed to compile: " .. tostring(src))
self:assert(tree.type == "File", "Attempt to run non-file: " .. tostring(tree.type))
local buffer = { }
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local statement = _list_0[_index_0]
if self.debug then
self:writeln(tostring(colored.bright("RUNNING NOMSU:")) .. "\n" .. tostring(colored.bright(colored.yellow(statement.src))))
self:writeln(colored.bright("PARSED TO TREE:"))
self:print_tree(statement)
end
local ok, expr, statements = pcall(self.tree_to_lua, self, statement, filename)
if not ok then
self:errorln(tostring(colored.red("Error occurred in statement:")) .. "\n" .. tostring(colored.bright(colored.yellow(statement.src))))
error(expr)
end
local code_for_statement = ([[return (function(nomsu, vars)
%s
end);]]):format(statements or ("return " .. expr .. ";"))
if output_file then
if statements and #statements > 0 then
output_file:write("lua> \"..\"\n " .. tostring(self:indent(statements:gsub("\\", "\\\\"))) .. "\n")
end
if expr and #expr > 0 then
output_file:write("=lua \"..\"\n " .. tostring(self:indent(expr:gsub("\\", "\\\\"))) .. "\n")
end
end
if self.debug then
self:writeln(tostring(colored.bright("RUNNING LUA:")) .. "\n" .. tostring(colored.blue(colored.bright(code_for_statement))))
end
local lua_thunk, err = load(code_for_statement)
if not lua_thunk then
local n = 1
local fn
fn = function()
n = n + 1
return ("\n%-3d|"):format(n)
end
local code = "1 |" .. code_for_statement:gsub("\n", fn)
error("Failed to compile generated code:\n" .. tostring(colored.bright(colored.blue(colored.onblack(code)))) .. "\n\n" .. tostring(err) .. "\n\nProduced by statement:\n" .. tostring(colored.bright(colored.yellow(statement.src))))
end
local run_statement = lua_thunk()
local ret
ok, ret = pcall(run_statement, self, vars)
if not ok then
self:errorln(tostring(colored.red("Error occurred in statement:")) .. "\n" .. tostring(colored.yellow(statement.src)))
self:errorln(debug.traceback())
error(ret)
end
if statements then
insert(buffer, statements)
end
if expr then
insert(buffer, tostring(expr) .. ";")
end
end
if max_operations then
debug.sethook()
end
local lua_code = ([[return (function(nomsu, vars)
%s
end);]]):format(concat(buffer, "\n"))
return nil, lua_code, vars
end,
tree_to_value = function(self, tree, vars, filename)
local code = "return (function(nomsu, vars)\nreturn " .. tostring(self:tree_to_lua(tree, filename)) .. ";\nend);"
if self.debug then
self:writeln(tostring(colored.bright("RUNNING LUA TO GET VALUE:")) .. "\n" .. tostring(colored.blue(colored.bright(code))))
end
local lua_thunk, err = load(code)
if not lua_thunk then
self:error("Failed to compile generated code:\n" .. tostring(colored.bright(colored.blue(colored.onblack(code)))) .. "\n\n" .. tostring(colored.red(err)))
end
return (lua_thunk())(self, vars or { })
end,
tree_to_nomsu = function(self, tree, force_inline)
if force_inline == nil then
force_inline = false
end
self:assert(tree, "No tree provided.")
if not tree.type then
self:errorln(debug.traceback())
self:error("Invalid tree: " .. tostring(repr(tree)))
end
local _exp_0 = tree.type
if "File" == _exp_0 then
return concat((function()
local _accum_0 = { }
local _len_0 = 1
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local v = _list_0[_index_0]
_accum_0[_len_0] = self:tree_to_nomsu(v, force_inline)
_len_0 = _len_0 + 1
end
return _accum_0
end)(), "\n"), false
elseif "Nomsu" == _exp_0 then
local inside, inline = self:tree_to_nomsu(tree.value, force_inline)
return "\\" .. tostring(inside), inline
elseif "Thunk" == _exp_0 then
if force_inline then
return "{" .. tostring(concat((function()
local _accum_0 = { }
local _len_0 = 1
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local v = _list_0[_index_0]
_accum_0[_len_0] = self:tree_to_nomsu(v, true)
_len_0 = _len_0 + 1
end
return _accum_0
end)(), "; ")), true
else
return ":" .. self:indent("\n" .. concat((function()
local _accum_0 = { }
local _len_0 = 1
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local v = _list_0[_index_0]
_accum_0[_len_0] = self:tree_to_nomsu(v)
_len_0 = _len_0 + 1
end
return _accum_0
end)(), "\n")), false
end
elseif "FunctionCall" == _exp_0 then
local buff = ""
local sep = ""
local inline = true
local line_len = 0
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local arg = _list_0[_index_0]
local nomsu, arg_inline = self:tree_to_nomsu(arg, force_inline)
if sep == " " and line_len + #nomsu > 80 then
sep = "\n.."
end
if not (sep == " " and not arg_inline and nomsu:sub(1, 1) == ":") then
buff = buff .. sep
end
if arg_inline then
sep = " "
line_len = line_len + (1 + #nomsu)
else
line_len = 0
inline = false
sep = "\n.."
end
if arg.type == 'FunctionCall' then
if arg_inline then
buff = buff .. "(" .. tostring(nomsu) .. ")"
else
buff = buff .. "(..)\n " .. tostring(self:indent(nomsu))
end
else
buff = buff .. nomsu
end
end
return buff, inline
elseif "String" == _exp_0 then
local buff = "\""
local longbuff = "\"..\"\n |"
local inline = true
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local bit = _list_0[_index_0]
if type(bit) == "string" then
bit = bit:gsub("\\", "\\\\")
buff = buff .. bit:gsub("\n", "\\n"):gsub("\"", "\\\"")
longbuff = longbuff .. bit:gsub("\n", "\n |")
else
local inside, bit_inline = self:tree_to_nomsu(bit, force_inline)
inline = inline and bit_inline
buff = buff .. "\\(" .. tostring(inside) .. ")"
longbuff = longbuff .. "\\(" .. tostring(inside) .. ")"
end
end
buff = buff .. "\""
if force_inline or (inline and #buff <= 90) then
return buff, true
else
return longbuff, false
end
elseif "List" == _exp_0 then
local buff = "["
local longbuff = "[..]\n "
local longsep = ""
local longline = 0
local inline = true
for i, bit in ipairs(tree.value) do
local nomsu, bit_inline = self:tree_to_nomsu(bit, force_inline)
inline = inline and bit_inline
if inline then
if i > 1 then
buff = buff .. ", "
end
buff = buff .. nomsu
end
longbuff = longbuff .. (longsep .. nomsu)
longline = longline + #nomsu
if bit_inline and longline <= 90 then
longsep = ", "
else
longsep = "\n "
end
end
buff = buff .. "]"
if force_inline or (inline and #buff <= 90) then
return buff, true
else
return longbuff, false
end
elseif "Dict" == _exp_0 then
return error("Sorry, not yet implemented.")
elseif "Number" == _exp_0 then
return repr(tree.value), true
elseif "Var" == _exp_0 then
return "%" .. tostring(tree.value), true
elseif "Word" == _exp_0 then
return tree.value, true
else
return self:error("Unknown/unimplemented thingy: " .. tostring(tree.type))
end
end,
value_to_nomsu = function(self, value)
local _exp_0 = type(value)
if "nil" == _exp_0 then
return "(nil)"
elseif "bool" == _exp_0 then
return value and "(yes)" or "(no)"
elseif "number" == _exp_0 then
return repr(value)
elseif "table" == _exp_0 then
if is_list(value) then
return "[" .. tostring(concat((function()
local _accum_0 = { }
local _len_0 = 1
for _index_0 = 1, #value do
local v = value[_index_0]
_accum_0[_len_0] = self:value_to_nomsu(v)
_len_0 = _len_0 + 1
end
return _accum_0
end)(), ", ")) .. "]"
else
return "(d{" .. tostring(concat((function()
local _accum_0 = { }
local _len_0 = 1
for k, v in pairs(value) do
_accum_0[_len_0] = tostring(self:value_to_nomsu(k)) .. "=" .. tostring(self:value_to_nomsu(v))
_len_0 = _len_0 + 1
end
return _accum_0
end)(), "; ")) .. "})"
end
elseif "string" == _exp_0 then
if value == "\n" then
return "'\\n'"
elseif not value:find([["]]) and not value:find("\n") and not value:find("\\") then
return "\"" .. value .. "\""
else
return '".."\n ' .. (self:indent(value))
end
else
return error("Unsupported value_to_nomsu type: " .. tostring(type(value)))
end
end,
tree_to_lua = function(self, tree, filename)
self:assert(tree, "No tree provided.")
if not tree.type then
self:errorln(debug.traceback())
self:error("Invalid tree: " .. tostring(repr(tree)))
end
local _exp_0 = tree.type
if "File" == _exp_0 then
local lua_bits = { }
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local line = _list_0[_index_0]
local expr, statement = self:tree_to_lua(line, filename)
if statement then
insert(lua_bits, statement)
end
if expr then
insert(lua_bits, tostring(expr) .. ";")
end
end
return nil, concat(lua_bits, "\n")
elseif "Nomsu" == _exp_0 then
return "nomsu:parse(" .. tostring(repr(tree.value.src)) .. ", " .. tostring(repr(tree.line_no)) .. ").value[1]", nil
elseif "Thunk" == _exp_0 then
local lua_bits = { }
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local arg = _list_0[_index_0]
local expr, statement = self:tree_to_lua(arg, filename)
if #tree.value == 1 and expr and not statement then
return ([[(function(nomsu, vars)
return %s;
end)]]):format(expr)
end
if statement then
insert(lua_bits, statement)
end
if expr then
insert(lua_bits, tostring(expr) .. ";")
end
end
return ([[(function(nomsu, vars)
%s
end)]]):format(concat(lua_bits, "\n"))
elseif "FunctionCall" == _exp_0 then
insert(self.compilestack, tree)
local def = self.defs[tree.stub]
if def and def.is_macro then
local expr, statement = self:run_macro(tree)
remove(self.compilestack)
return expr, statement
elseif not def and self.__class.math_patt:match(tree.stub) then
local bits = { }
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local tok = _list_0[_index_0]
if tok.type == "Word" then
insert(bits, tok.value)
else
local expr, statement = self:tree_to_lua(tok, filename)
self:assert(statement == nil, "non-expression value inside math expression")
insert(bits, expr)
end
end
return "(" .. tostring(concat(bits, " ")) .. ")"
end
local args = {
repr(tree.stub),
repr(tree.line_no)
}
local arg_names, escaped_args
if def then
arg_names, escaped_args = def.arg_names, def.escaped_args
else
arg_names, escaped_args = (function()
local _accum_0 = { }
local _len_0 = 1
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local w = _list_0[_index_0]
if w.type == "Word" then
_accum_0[_len_0] = w.value
_len_0 = _len_0 + 1
end
end
return _accum_0
end)(), { }
end
local arg_num = 1
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local _continue_0 = false
repeat
local arg = _list_0[_index_0]
if arg.type == 'Word' then
_continue_0 = true
break
end
if escaped_args[arg_names[arg_num]] then
insert(args, "nomsu:parse(" .. tostring(repr(arg.src)) .. ", " .. tostring(repr(tree.line_no)) .. ").value[1]")
else
local expr, statement = self:tree_to_lua(arg, filename)
if statement then
self:error("Cannot use [[" .. tostring(arg.src) .. "]] as a function argument, since it's not an expression.")
end
insert(args, expr)
end
arg_num = arg_num + 1
_continue_0 = true
until true
if not _continue_0 then
break
end
end
remove(self.compilestack)
return self.__class:comma_separated_items("nomsu:call(", args, ")"), nil
elseif "String" == _exp_0 then
local concat_parts = { }
local string_buffer = ""
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local _continue_0 = false
repeat
local bit = _list_0[_index_0]
if type(bit) == "string" then
string_buffer = string_buffer .. bit
_continue_0 = true
break
end
if string_buffer ~= "" then
insert(concat_parts, repr(string_buffer))
string_buffer = ""
end
local expr, statement = self:tree_to_lua(bit, filename)
if self.debug then
self:writeln((colored.bright("INTERP:")))
self:print_tree(bit)
self:writeln(tostring(colored.bright("EXPR:")) .. " " .. tostring(expr) .. ", " .. tostring(colored.bright("STATEMENT:")) .. " " .. tostring(statement))
end
if statement then
self:error("Cannot use [[" .. tostring(bit.src) .. "]] as a string interpolation value, since it's not an expression.")
end
insert(concat_parts, "nomsu:stringify(" .. tostring(expr) .. ")")
_continue_0 = true
until true
if not _continue_0 then
break
end
end
if string_buffer ~= "" then
insert(concat_parts, repr(string_buffer))
end
if #concat_parts == 0 then
return "''", nil
elseif #concat_parts == 1 then
return concat_parts[1], nil
else
return "(" .. tostring(concat(concat_parts, "..")) .. ")", nil
end
elseif "List" == _exp_0 then
local items = { }
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local item = _list_0[_index_0]
local expr, statement = self:tree_to_lua(item, filename)
if statement then
self:error("Cannot use [[" .. tostring(item.src) .. "]] as a list item, since it's not an expression.")
end
insert(items, expr)
end
return self.__class:comma_separated_items("{", items, "}"), nil
elseif "Dict" == _exp_0 then
local items = { }
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local entry = _list_0[_index_0]
local key_expr, key_statement
if entry.dict_key.type == "Word" then
key_expr, key_statement = repr(entry.dict_key.value), nil
else
key_expr, key_statement = self:tree_to_lua(entry.dict_key, filename)
end
if key_statement then
self:error("Cannot use [[" .. tostring(entry.dict_key.src) .. "]] as a dict key, since it's not an expression.")
end
local value_expr, value_statement = self:tree_to_lua(entry.dict_value, filename)
if value_statement then
self:error("Cannot use [[" .. tostring(entry.dict_value.src) .. "]] as a dict value, since it's not an expression.")
end
local key_str = key_expr:match([=[["']([a-zA-Z_][a-zA-Z0-9_]*)['"]]=])
if key_str then
insert(items, tostring(key_str) .. "=" .. tostring(value_expr))
else
insert(items, "[" .. tostring(key_expr) .. "]=" .. tostring(value_expr))
end
end
return self.__class:comma_separated_items("{", items, "}"), nil
elseif "Number" == _exp_0 then
return repr(tree.value), nil
elseif "Var" == _exp_0 then
if tree.value:match("^[a-zA-Z_][a-zA-Z0-9_]*$") then
return "vars." .. tostring(tree.value), nil
else
return "vars[" .. tostring(repr(tree.value)) .. "]", nil
end
else
return self:error("Unknown/unimplemented thingy: " .. tostring(tree.type))
end
end,
walk_tree = function(self, tree, depth)
if depth == nil then
depth = 0
end
coroutine.yield(tree, depth)
if type(tree) ~= 'table' or not tree.type then
return
end
local _exp_0 = tree.type
if "List" == _exp_0 or "File" == _exp_0 or "Thunk" == _exp_0 or "FunctionCall" == _exp_0 or "String" == _exp_0 then
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local v = _list_0[_index_0]
self:walk_tree(v, depth + 1)
end
elseif "Dict" == _exp_0 then
local _list_0 = tree.value
for _index_0 = 1, #_list_0 do
local e = _list_0[_index_0]
self:walk_tree(e.dict_key, depth + 1)
self:walk_tree(e.dict_value, depth + 1)
end
else
self:walk_tree(tree.value, depth + 1)
end
return nil
end,
print_tree = function(self, tree)
self:write(colors.bright .. colors.green)
for node, depth in coroutine.wrap(function()
return self:walk_tree(tree)
end) do
if type(node) ~= 'table' or not node.type then
self:writeln((" "):rep(depth) .. repr(node))
else
self:writeln(tostring((" "):rep(depth)) .. tostring(node.type) .. ":")
end
end
return self:write(colors.reset)
end,
tree_to_str = function(self, tree)
local bits = { }
for node, depth in coroutine.wrap(function()
return self:walk_tree(tree)
end) do
if type(node) ~= 'table' or not node.type then
insert(bits, ((" "):rep(depth) .. repr(node)))
else
insert(bits, (tostring((" "):rep(depth)) .. tostring(node.type) .. ":"))
end
end
return concat(bits, "\n")
end,
replaced_vars = function(self, tree, vars)
if type(tree) ~= 'table' then
return tree
end
local _exp_0 = tree.type
if "Var" == _exp_0 then
if vars[tree.value] ~= nil then
tree = vars[tree.value]
end
elseif "File" == _exp_0 or "Nomsu" == _exp_0 or "Thunk" == _exp_0 or "List" == _exp_0 or "FunctionCall" == _exp_0 or "String" == _exp_0 then
local new_value = self:replaced_vars(tree.value, vars)
if new_value ~= tree.value then
do
local _tbl_0 = { }
for k, v in pairs(tree) do
_tbl_0[k] = v
end
tree = _tbl_0
end
tree.value = new_value
end
elseif "Dict" == _exp_0 then
local dirty = false
local replacements = { }
for i, e in ipairs(tree.value) do
local new_key = self:replaced_vars(e.dict_key, vars)
local new_value = self:replaced_vars(e.dict_value, vars)
dirty = dirty or (new_key ~= e.dict_key or new_value ~= e.dict_value)
replacements[i] = {
dict_key = new_key,
dict_value = new_value
}
end
if dirty then
do
local _tbl_0 = { }
for k, v in pairs(tree) do
_tbl_0[k] = v
end
tree = _tbl_0
end
tree.value = replacements
end
elseif nil == _exp_0 then
local new_values = { }
local any_different = false
for k, v in pairs(tree) do
new_values[k] = self:replaced_vars(v, vars)
any_different = any_different or (new_values[k] ~= tree[k])
end
if any_different then
tree = new_values
end
end
return tree
end,
get_stub = function(self, x)
if not x then
self:error("Nothing to get stub from")
end
if type(x) == 'string' then
local spec = concat(self.__class.stub_patt:match(x), " ")
local stub = spec:gsub("%%%S+", "%%"):gsub("\\", "")
local arg_names
do
local _accum_0 = { }
local _len_0 = 1
for arg in spec:gmatch("%%(%S*)") do
_accum_0[_len_0] = arg
_len_0 = _len_0 + 1
end
arg_names = _accum_0
end
local escaped_args
do
local _tbl_0 = { }
for arg in spec:gmatch("\\%%(%S*)") do
_tbl_0[arg] = true
end
escaped_args = _tbl_0
end
return stub, arg_names, escaped_args
end
if type(x) ~= 'table' then
self:error("Invalid type for getting stub: " .. tostring(type(x)) .. " for:\n" .. tostring(repr(x)))
end
local _exp_0 = x.type
if "String" == _exp_0 then
return self:get_stub(x.value)
elseif "FunctionCall" == _exp_0 then
return self:get_stub(x.src)
else
return self:error("Unsupported get stub type: " .. tostring(x.type) .. " for " .. tostring(repr(x)))
end
end,
get_stubs = function(self, x)
if type(x) ~= 'table' then
return {
{
self:get_stub(x)
}
}
end
local _exp_0 = x.type
if nil == _exp_0 then
local _accum_0 = { }
local _len_0 = 1
for _index_0 = 1, #x do
local i = x[_index_0]
_accum_0[_len_0] = {
self:get_stub(i)
}
_len_0 = _len_0 + 1
end
return _accum_0
elseif "List" == _exp_0 then
local _accum_0 = { }
local _len_0 = 1
local _list_0 = x.value
for _index_0 = 1, #_list_0 do
local i = _list_0[_index_0]
_accum_0[_len_0] = {
self:get_stub(i)
}
_len_0 = _len_0 + 1
end
return _accum_0
end
return {
{
self:get_stub(x)
}
}
end,
var_to_lua_identifier = function(self, var)
if type(var) == 'table' and var.type == "Var" then
var = var.value
end
return (var:gsub("%W", function(verboten)
if verboten == "_" then
return "__"
else
return ("_%x"):format(verboten:byte())
end
end))
end,
assert = function(self, condition, msg)
if msg == nil then
msg = ''
end
if not condition then
return self:error(msg)
end
end,
error = function(self, msg)
local error_msg = colored.red("ERROR!")
if msg then
error_msg = error_msg .. ("\n" .. (colored.bright(colored.yellow(colored.onred(msg)))))
end
error_msg = error_msg .. "\nCallstack:"
local maxlen = max((function()
local _accum_0 = { }
local _len_0 = 1
local _list_0 = self.callstack
for _index_0 = 1, #_list_0 do
local c = _list_0[_index_0]
if c ~= "#macro" then
_accum_0[_len_0] = #c[2]
_len_0 = _len_0 + 1
end
end
return _accum_0
end)())
for i = #self.callstack, 1, -1 do
if self.callstack[i] ~= "#macro" then
local line_no = self.callstack[i][2]
if line_no then
local nums
do
local _accum_0 = { }
local _len_0 = 1
for n in line_no:gmatch(":([0-9]+)") do
_accum_0[_len_0] = tonumber(n)
_len_0 = _len_0 + 1
end
nums = _accum_0
end
line_no = line_no:gsub(":.*$", ":" .. tostring(sum(nums) - #nums + 1))
end
error_msg = error_msg .. "\n " .. tostring(("%-" .. tostring(maxlen) .. "s"):format(line_no)) .. "| " .. tostring(self.callstack[i][1])
end
end
error_msg = error_msg .. "\n <top level>"
self.callstack = { }
return error(error_msg, 3)
end,
typecheck = function(self, vars, varname, desired_type)
local x = vars[varname]
local x_type = type(x)
if x_type == desired_type then
return x
end
if x_type == 'table' then
x_type = x.type or x_type
if x_type == desired_type then
return x
end
end
return self:error("Invalid type for %" .. tostring(varname) .. ". Expected " .. tostring(desired_type) .. ", but got " .. tostring(x_type) .. ":\n" .. tostring(repr(x)))
end,
source_code = function(self, level)
if level == nil then
level = 0
end
return self:dedent(self.compilestack[#self.compilestack - level].src)
end,
initialize_core = function(self)
local nomsu_string_as_lua
nomsu_string_as_lua = function(self, code)
local concat_parts = { }
local _list_0 = code.value
for _index_0 = 1, #_list_0 do
local bit = _list_0[_index_0]
if type(bit) == "string" then
insert(concat_parts, bit)
else
local expr, statement = self:tree_to_lua(bit, filename)
if statement then
self:error("Cannot use [[" .. tostring(bit.src) .. "]] as a string interpolation value, since it's not an expression.")
end
insert(concat_parts, expr)
end
end
return concat(concat_parts)
end
local lua_code
lua_code = function(self, vars)
local lua = nomsu_string_as_lua(self, vars.code)
return nil, lua
end
self:defmacro("lua> %code", lua_code)
local lua_value
lua_value = function(self, vars)
local lua = nomsu_string_as_lua(self, vars.code)
return lua, nil
end
self:defmacro("=lua %code", lua_value)
self:defmacro("__src__ %level", function(self, vars)
return self:repr(self:source_code(self:tree_to_value(vars.level)))
end)
self:def("derp \\%foo derp \\%bar", function(self, vars)
local lua = "local x = " .. repr((function()
local _accum_0 = { }
local _len_0 = 1
local _list_0 = vars.foo.value
for _index_0 = 1, #_list_0 do
local t = _list_0[_index_0]
_accum_0[_len_0] = t.stub
_len_0 = _len_0 + 1
end
return _accum_0
end)()) .. ";\nlocal y = " .. self:tree_to_lua(vars.bar)
return print(colored.green(lua))
end)
local run_file
run_file = function(self, vars)
if vars.filename:match(".*%.lua") then
return dofile(vars.filename)(self, vars)
end
if vars.filename:match(".*%.nom") then
if not self.skip_precompiled then
local file = io.open(vars.filename:gsub("%.nom", ".nomc"), "r")
end
local file = file or io.open(vars.filename)
if not file then
self:error("File does not exist: " .. tostring(vars.filename))
end
local contents = file:read('*a')
file:close()
return self:run(contents, vars.filename)
else
return self:error("Invalid filetype for " .. tostring(vars.filename))
end
end
self:def("run file %filename", run_file)
local _require
_require = function(self, vars)
local loaded = self.defs["#loaded_files"]
if not loaded[vars.filename] then
loaded[vars.filename] = run_file(self, {
filename = vars.filename
}) or true
end
return loaded[vars.filename]
end
return self:def("require %filename", _require)
end
}
_base_0.__index = _base_0
_class_0 = setmetatable({
__init = function(self, parent)
self.write = function(self, ...)
return io.write(...)
end
self.write_err = function(self, ...)
return io.stderr:write(...)
end
self.defs = {
["#vars"] = { },
["#loaded_files"] = { }
}
if parent then
setmetatable(self.defs, {
__index = parent.defs
})
setmetatable(self.defs["#vars"], {
__index = parent["#vars"]
})
setmetatable(self.defs["#loaded_files"], {
__index = parent["#loaded_files"]
})
end
self.callstack = { }
self.compilestack = { }
self.debug = false
self.utils = utils
self.repr = function(self, ...)
return repr(...)
end
self.stringify = function(self, ...)
return stringify(...)
end
if not parent then
return self:initialize_core()
end
end,
__base = _base_0,
__name = "NomsuCompiler"
}, {
__index = _base_0,
__call = function(cls, ...)
local _self_0 = setmetatable({}, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
local self = _class_0
self.def_number = 0
self.math_patt = re.compile([[ "%" (" " [*/^+-] " %")+ ]])
self.unescape_string = function(self, str)
return Cs(((P("\\\\") / "\\") + (P("\\\"") / '"') + ESCAPE_CHAR + P(1)) ^ 0):match(str)
end
self.comma_separated_items = function(self, open, items, close)
local bits = {
open
}
local so_far = 0
for i, item in ipairs(items) do
if i < #items then
item = item .. ", "
end
insert(bits, item)
so_far = so_far + #item
if so_far >= 80 then
insert(bits, "\n")
so_far = 0
end
end
insert(bits, close)
return concat(bits)
end
self.stub_patt = re.compile("{|(' '+ / '\n..' / {'\\'? '%' %id*} / {%id+} / {%op})*|}", {
id = IDENT_CHAR,
op = OPERATOR_CHAR
})
NomsuCompiler = _class_0
end
if arg then
colors = require('consolecolors')
local parser = re.compile([[ args <- {| {:flags: flags? :} ({:input: input :} ";" ("-o;"{:output: output :} ";")?)? (";")? |} !.
flags <- (({| ({flag} ";")* |}) -> set)
flag <- "-c" / "-i" / "-p" / "-O" / "--help" / "-h" / "-v"
input <- "-" / [^;]+
output <- "-" / [^;]+
]], {
set = set
})
local args = concat(arg, ";") .. ";"
args = parser:match(args) or { }
if not args or not args.flags or args.flags["--help"] or args.flags["-h"] then
print("Usage: lua nomsu.lua [-c] [-i] [-p] [-O] [--help] [input [-o output]]")
os.exit()
end
local c = NomsuCompiler()
if args.flags["-v"] then
c.debug = true
end
c.skip_precompiled = not args.flags["-O"]
if args.input then
if args.flags["-c"] and not args.output then
args.output = args.input:gsub("%.nom", ".nomc")
end
local compiled_output = nil
if args.flags["-p"] then
local _write = c.write
c.write = function() end
compiled_output = io.output()
elseif args.output then
compiled_output = io.open(args.output, 'w')
end
if args.input:match(".*%.lua") then
local retval = dofile(args.input)(c, { })
else
local input
if args.input == '-' then
input = io.read('*a')
else
input = io.open(args.input):read("*a")
end
local vars = { }
local retval, code = c:run(input, args.input, vars, nil, compiled_output)
end
if args.flags["-p"] then
c.write = _write
end
end
if args.flags["-i"] then
local vars = { }
c:run('require "lib/core.nom"', "stdin")
while true do
local buff = ""
while true do
io.write(">> ")
local line = io.read("*L")
if line == "\n" or not line then
break
end
buff = buff .. line
end
if #buff == 0 then
break
end
local ok, ret = pcall(function()
return c:run(buff, "stdin", vars)
end)
if ok and ret ~= nil then
print("= " .. repr(ret))
end
end
end
end
return NomsuCompiler