From b3b8c4d731b0983d5b12c56fc245a8d7c1d631f4 Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Mon, 4 Dec 2017 17:35:47 -0800 Subject: [PATCH] Some stuff changed to allow escaped args and some other ports from the two_defs branch. --- lib/collections.nom | 2 +- lib/control_flow.nom | 49 +++++- lib/permissions.nom | 56 ++----- lib/utils.nom | 21 --- lib/utils2.nom | 42 +++++ nomsu.lua | 376 +++++++++++++++++++++++++++++++++---------- nomsu.moon | 224 ++++++++++++++++++-------- 7 files changed, 549 insertions(+), 221 deletions(-) diff --git a/lib/collections.nom b/lib/collections.nom index c58d2f7..02729c5 100644 --- a/lib/collections.nom +++ b/lib/collections.nom @@ -93,7 +93,7 @@ rule [dict from entries %items] =: %dict -> (%pair -> 1) = (%pair -> 2) %dict -compile [dict %items] to: +compile [dict %items, d %items] to: if ((%items's "type") == "Thunk"): %item_codes = [] for %func_call in (%items's "value"): diff --git a/lib/control_flow.nom b/lib/control_flow.nom index aeb63ef..2d1a6a4 100644 --- a/lib/control_flow.nom +++ b/lib/control_flow.nom @@ -218,11 +218,48 @@ compile [when %branch_value == ? %body] to code: |end --when == ? %result -# With statement -compile [with %thing = %value %action] to code: ".." +# Try/except +compile [..] + try %action and if it succeeds %success or if it fails %fallback + try %action and if it fails %fallback or if it succeeds %success +..to code: ".." |do - | local old_value = \(%thing as lua); - | \(%thing as lua) = \(%value as lua); - | \(%action as lua statements); - | \(%thing as lua) = old_value; + | local fell_through = false; + | local ok, ret1, ret2 = pcall(function(nomsu, vars) + | \(%action as lua statements) + | fell_through = true; + | end, nomsu, vars); + | if ok then + | \(%success as lua statements) + | end + | if not ok then + | \(%fallback as lua statements) + | elseif not fell_through then + | return ret1, ret2; + | end |end +parse [try %action] as: + try %action and if it succeeds {pass} or if it fails {pass} +parse [try %action and if it fails %fallback] as: + try %action and if it succeeds {pass} or if it fails %fallback +parse [try %action and if it succeeds %success] as: + try %action and if it succeeds %success or if it fails {pass} + +# Do/finally: +compile [do %action then always %final_action] to code: ".." + |do + | local fell_through = false; + | local ok, ret1, ret2 = pcall(function(nomsu, vars) + | \(%action as lua statements) + | fell_through = true; + | end, nomsu, vars); + | local ok2, _ = pcall(function(nomsu, vars) + | \(%final_action as lua statements) + | end, nomsu, vars); + | if not ok then nomsu:error(ret1); end + | if not ok2 then nomsu:error(ret2); end + | if not fell_through then + | return ret1, ret2; + | end + |end + diff --git a/lib/permissions.nom b/lib/permissions.nom index 887f4fa..1811ee8 100644 --- a/lib/permissions.nom +++ b/lib/permissions.nom @@ -3,52 +3,16 @@ require "lib/control_flow.nom" require "lib/operators.nom" require "lib/collections.nom" -# Permission functions -rule [standardize rules %rules] =: - if ((type of %rules) == "string"): %rules = [%rules] - %stubs = (nomsu "get_stubs" [%rules]) - %result = [] - for %stub in %stubs: - %def = ((nomsu's "defs")->%stub) - if %def: - %aliases = (%def's "aliases") - for all %aliases: add % to %result - ..else: add %def to %result - unique %result +rule [called by %whitelist] =: + if ((%whitelist's "type") != "List"): %whitelist = [%whitelist] + %defs = (..) + dict ([(nomsu's "defs")->(nomsu "get_stub" [%]), yes] for all %whitelist) + for %caller in (nomsu's "callstack"): + if (%caller == "#macro"): do next %caller + if (%defs -> (nomsu "get_stub" [%caller's 1])): return (yes) + return (no) -rule [restrict %rules to within %elite_rules] =: - %rules = (standardize rules %rules) - %elite_rules = (standardize rules %elite_rules) - for all (flatten [%elite_rules, %rules]): - assert ((nomsu's "defs") has key %) "Undefined function: \(%)" - for %rule in %rules: - assert (nomsu "check_permission" [%]) ".." - |You do not have permission to restrict permissions for function: \(%) - ((nomsu) ->* ["defs",%rule,"whiteset"]) = (..) - dict ([%, yes] for all %elite_rules) +parse [fail unless called by %whitelist] as: + unless (called by %whitelist): error "Failed to find \(%whitelist) in callstack." -rule [allow %elite_rules to use %rules] =: - %rules = (standardize rules %rules) - %elite_rules = (standardize rules %elite_rules) - for all (flatten [%elite_rules, %rules]): - assert ((nomsu's "defs") has key %) "Undefined function: \(%)" - for %rule in %rules: - assert (nomsu "check_permission" [%rule]) ".." - |You do not have permission to grant permissions for function: \(%rule) - %whiteset = ((nomsu) ->* ["defs",%rule,"whiteset"]) - if (not %whiteset): go to next %rule - for all %elite_rules: %whiteset -> % = (yes) -rule [forbid %pleb_rules to use %rules] =: - %rules = (standardize rules %rules) - %pleb_rules = (standardize rules %pleb_rules) - for all (flatten [%pleb_rules, %used]): - assert ((nomsu's "defs") has key %) "Undefined function: \(%)" - for all %rules: - assert (nomsu "check_permission" [%]) ".." - |You do not have permission to grant permissions for function: \(%) - %whiteset = ((nomsu) ->* ["defs",%,"whiteset"]) - assert %whiteset ".." - |Cannot individually restrict permissions for \(%) because it is currently - |available to everyone. Perhaps you meant to use "restrict % to within %" instead? - for all %pleb_rules: %whiteset's % = (nil) diff --git a/lib/utils.nom b/lib/utils.nom index 965d338..ef0e61e 100644 --- a/lib/utils.nom +++ b/lib/utils.nom @@ -130,24 +130,3 @@ lua> ".." | end; |end; -compile [try %action and if it fails %fallback] to code: ".." - |do - | local _write_err = nomsu.write_err - | nomsu.write_err = function() end; - | local had_return = true; - | local ok, ret1, ret2 = pcall(function(nomsu, vars) - | local ret - | \(%action as lua statements) - | had_return = false; - | return ret; - | end, nomsu, vars); - | nomsu.write_err = _write_err; - | if not ok then - | \(%fallback as lua statements) - | elseif had_return then - | return ret1, ret2; - | else - | ret = ret1; - | end - |end - diff --git a/lib/utils2.nom b/lib/utils2.nom index 2562471..415f8c3 100644 --- a/lib/utils2.nom +++ b/lib/utils2.nom @@ -2,9 +2,51 @@ require "lib/metaprogramming.nom" require "lib/utils.nom" require "lib/control_flow.nom" require "lib/operators.nom" +require "lib/collections.nom" + compile [say %str] to: if ((%str's "type") == "String"): "nomsu:writeln(\(%str as lua))" ..else: "nomsu:writeln(nomsu:stringify(\(%str as lua)))" + +compile [do %action] to code: + if ((%action's "type") == "Thunk"): + %action as lua statements + ..else: + "(\(%action as lua))(nomsu, vars);" + +# With statement +compile [with %assignments %action] to code: + %data = [] + for %i = %assignment in (%assignments' "value"): + %tokens = (%assignment's "value") + %var = (%tokens -> 1) + %eq = (%tokens -> 2) + assert (=lua "vars.eq and vars.eq.type == 'Word' and vars.eq.value == '='") ".." + |Invalid format for 'with' statement. List entries must have the form %var = (value) + %value = (%tokens -> 3) + add (d{i=%i; var=%var; value=%value}) to %data + %foo = (..) + join (..) + "local old_value\(%->"i") = \((%->"var") as lua); \((%->"var") as lua) = \((%->"value") as lua);" + ..for all %data + ..with glue "\n " + ".." + |do + | \(%foo) + | local fell_through = false; + | local ok, ret1, ret2 = pcall(function(nomsu, vars) + | \(%action as lua statements); + | fell_through = true; + | end, nomsu, vars); + | \(join ("\((%->"var") as lua) = old_value\(%->"i");" for all %data) with glue "\n ") + | if not ok then nomsu:error(ret1); end + | if not fell_through then + | return ret1, ret2; + | end + |end +parse [with %thing = %value %action] as: with [%thing = %value] %action + + diff --git a/nomsu.lua b/nomsu.lua index fa77c1c..23522f6 100644 --- a/nomsu.lua +++ b/nomsu.lua @@ -19,6 +19,19 @@ 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, V, S, Cg, C, Cp, B, Cmt P, V, S, Cg, C, Cp, B, Cmt = lpeg.P, lpeg.V, lpeg.S, lpeg.Cg, lpeg.C, lpeg.Cp, lpeg.B, lpeg.Cmt @@ -166,12 +179,23 @@ local defs = { return pos, tostring(CURRENT_FILE) .. ":" .. tostring(line_no) end, FunctionCall = function(src, line_no, value, errors) + 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)(), " ") return { type = "FunctionCall", src = src, line_no = line_no, value = value, - errors = errors + errors = errors, + stub = stub } end, error = function(src, pos, errors, err_msg) @@ -241,12 +265,22 @@ do end assert(type(thunk) == 'function', "Bad thunk: " .. tostring(repr(thunk))) local canonical_args = nil + local canonical_escaped_args = nil local aliases = { } - self.def_number = self.def_number + 1 + 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 - stub, arg_names = _des_0[1], _des_0[2] + local stub, arg_names, escaped_args + stub, arg_names, escaped_args = _des_0[1], _des_0[2], _des_0[3] 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)))) @@ -263,38 +297,104 @@ do else canonical_args = utils.set(arg_names) end - insert(aliases, stub) - self.defs[stub] = { - thunk = thunk, + if canonical_escaped_args then + assert(utils.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, - src = src, - is_macro = is_macro, - aliases = aliases, - def_number = self.def_number - } + 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, - serialize_defs = function(self, after) + scoped = function(self, thunk) + local old_defs = self.defs + self.defs = setmetatable({ }, { + __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 = 0 end - defs = { } - for _, def in pairs(self.defs) do - defs[def.def_number] = def.src or "" + 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 - local keys = utils.keys(defs) table.sort(keys) local buff = { } - for _index_0 = 1, #keys do - local i = keys[_index_0] - if i > after and #defs[i] > 0 then - insert(buff, defs[i]) + 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, ...) @@ -306,7 +406,7 @@ do stub, line_no }) - if def == nil then + if not (def) then self:error("Attempt to call undefined function: " .. tostring(stub)) end if not (def.is_macro) then @@ -326,14 +426,16 @@ do 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 stub = self:get_stub(tree) local args do local _accum_0 = { } @@ -349,14 +451,55 @@ do args = _accum_0 end if self.debug then - self:write(tostring(colored.bright("RUNNING MACRO")) .. " " .. tostring(colored.underscore(colored.magenta(stub))) .. " ") + 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(stub, tree.line_no, unpack(args)) + 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 @@ -451,7 +594,7 @@ do local ok, expr, statements = pcall(self.tree_to_lua, self, statement) if not ok then self:errorln(tostring(colored.red("Error occurred in statement:")) .. "\n" .. tostring(colored.bright(colored.yellow(statement.src)))) - self:error(expr) + error(expr) end local code_for_statement = ([[return (function(nomsu, vars) %s @@ -480,7 +623,7 @@ end);]]):format(statements or "", expr or "ret") if not ok then self:errorln(tostring(colored.red("Error occurred in statement:")) .. "\n" .. tostring(colored.yellow(statement.src))) self:errorln(debug.traceback()) - self:error(ret) + error(ret) end if statements then insert(buffer, statements) @@ -514,10 +657,6 @@ end);]]):format(concat(buffer, "\n")) if force_inline == nil then force_inline = false end - local indent - indent = function(s) - return s:gsub("\n", "\n ") - end assert(tree, "No tree provided.") if not tree.type then self:errorln(debug.traceback()) @@ -553,7 +692,7 @@ end);]]):format(concat(buffer, "\n")) return _accum_0 end)(), "; ")), true else - return ":" .. indent("\n" .. concat((function() + return ":" .. self:indent("\n" .. concat((function() local _accum_0 = { } local _len_0 = 1 local _list_0 = tree.value @@ -569,17 +708,23 @@ end);]]):format(concat(buffer, "\n")) local buff = "" local sep = "" local inline = true - local do_arg - do_arg = function(arg) end + local line_len = 0 local _list_0 = tree.value for _index_0 = 1, #_list_0 do local arg = _list_0[_index_0] local arg_inline nomsu, arg_inline = self:tree_to_nomsu(arg, force_inline) - buff = buff .. sep + 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 @@ -587,7 +732,7 @@ end);]]):format(concat(buffer, "\n")) if arg_inline then buff = buff .. "(" .. tostring(nomsu) .. ")" else - buff = buff .. "(..)\n " .. tostring(indent(nomsu)) + buff = buff .. "(..)\n " .. tostring(self:indent(nomsu)) end else buff = buff .. nomsu @@ -658,6 +803,41 @@ end);]]):format(concat(buffer, "\n")) 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 utils.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 + else + return error("Unsupported value_to_nomsu type: " .. tostring(type(value))) + end + end, tree_to_lua = function(self, tree) assert(tree, "No tree provided.") if not tree.type then @@ -668,7 +848,7 @@ end);]]):format(concat(buffer, "\n")) if "File" == _exp_0 then return error("Should not be converting File to lua through this function.") elseif "Nomsu" == _exp_0 then - return "nomsu:parse(" .. tostring(repr(tree.value.src)) .. ").value[1]", nil + return "nomsu:parse(" .. tostring(repr(tree.value.src)) .. ", " .. tostring(repr(CURRENT_FILE)) .. ").value[1]", nil elseif "Thunk" == _exp_0 then local lua_bits = { } local _list_0 = tree.value @@ -688,24 +868,42 @@ local ret; return ret; end)]]):format(concat(lua_bits, "\n")) elseif "FunctionCall" == _exp_0 then - local stub = self:get_stub(tree) - local def = self.defs[stub] + local def = self.defs[tree.stub] if def and def.is_macro then local expr, statement = self:run_macro(tree) if def.whiteset then if expr then - expr = "(nomsu:assert_permission(" .. tostring(repr(stub)) .. ") and " .. tostring(expr) .. ")" + expr = "(nomsu:assert_permission(" .. tostring(repr(tree.stub)) .. ") and " .. tostring(expr) .. ")" end if statement then - statement = "nomsu:assert_permission(" .. tostring(repr(stub)) .. ");\n" .. statement + statement = "nomsu:assert_permission(" .. tostring(repr(tree.stub)) .. ");\n" .. statement end end return expr, statement end local args = { - repr(stub), + 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 @@ -715,11 +913,18 @@ end)]]):format(concat(lua_bits, "\n")) _continue_0 = true break end + if escaped_args[arg_names[arg_num]] then + arg = { + type = "Nomsu", + value = arg + } + end local expr, statement = self:tree_to_lua(arg) if statement then self:error("Cannot use [[" .. tostring(arg.src) .. "]] as a function argument, since it's not an expression.") end insert(args, expr) + arg_num = arg_num + 1 _continue_0 = true until true if not _continue_0 then @@ -883,41 +1088,38 @@ end)]]):format(concat(lua_bits, "\n")) self:error("Nothing to get stub from") end if type(x) == 'string' then - local stub = x:gsub("([" .. tostring(wordbreaker) .. "]+)", " %1 "):gsub("%%%S+", "%%"):gsub("%s+", " "):gsub("^%s*", ""):gsub("%s*$", "") + x = x:gsub("\n%s*%.%.", " "):gsub("([" .. tostring(wordbreaker) .. "]+)", " %1 "):gsub("%s+", " ") + x = x:gsub("^%s*", ""):gsub("%s*$", "") + local stub = x:gsub("%%%S+", "%%"):gsub("\\", "") local arg_names do local _accum_0 = { } local _len_0 = 1 - for arg in x:gmatch("%%([^%s']*)") do + for arg in x:gmatch("%%([^%s]*)") do _accum_0[_len_0] = arg _len_0 = _len_0 + 1 end arg_names = _accum_0 end - return stub, arg_names + local escaped_args = utils.set((function() + local _accum_0 = { } + local _len_0 = 1 + for arg in x:gmatch("\\%%([^%s]*)") do + _accum_0[_len_0] = arg + _len_0 = _len_0 + 1 + end + return _accum_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 - local stub, arg_names = { }, { }, { } - local _list_0 = x.value - for _index_0 = 1, #_list_0 do - local token = _list_0[_index_0] - local _exp_1 = token.type - if "Word" == _exp_1 then - insert(stub, token.value) - elseif "Var" == _exp_1 then - insert(stub, "%") - if arg_names then - insert(arg_names, token.value) - end - else - insert(stub, "%") - arg_names = nil - end - end - return concat(stub, " "), arg_names + return self:get_stub(x.src) else return self:error("Unsupported get stub type: " .. tostring(x.type) .. " for " .. tostring(repr(x))) end @@ -974,11 +1176,11 @@ end)]]):format(concat(lua_bits, "\n")) end)) end, error = function(self, msg) - self:errorln((colored.red("ERROR!"))) + local error_msg = colored.red("ERROR!") if msg then - self:errorln(colored.bright(colored.yellow(colored.onred(msg)))) + error_msg = error_msg .. ("\n" .. (colored.bright(colored.yellow(colored.onred(msg))))) end - self:errorln("Callstack:") + error_msg = error_msg .. "\nCallstack:" local maxlen = utils.max((function() local _accum_0 = { } local _len_0 = 1 @@ -994,12 +1196,12 @@ end)]]):format(concat(lua_bits, "\n")) end)()) for i = #self.callstack, 1, -1 do if self.callstack[i] ~= "#macro" then - self:errorln(" " .. tostring(("%-" .. tostring(maxlen) .. "s"):format(self.callstack[i][2])) .. "| " .. tostring(self.callstack[i][1])) + error_msg = error_msg .. "\n " .. tostring(("%-" .. tostring(maxlen) .. "s"):format(self.callstack[i][2])) .. "| " .. tostring(self.callstack[i][1]) end end - self:errorln(" ") + error_msg = error_msg .. "\n " self.callstack = { } - return error() + return error(error_msg, 3) end, typecheck = function(self, vars, varname, desired_type) local x = vars[varname] @@ -1009,17 +1211,19 @@ end)]]):format(concat(lua_bits, "\n")) if type(x) == 'table' and x.type == desired_type then return x end - return self:error("Invalid type for %" .. tostring(varname) .. ". Expected " .. tostring(desired_type) .. ", but got " .. tostring(x.type) .. ".") + return self:error("Invalid type for %" .. tostring(varname) .. ". Expected " .. tostring(desired_type) .. ", but got " .. tostring(repr(x)) .. ".") end, initialize_core = function(self) local nomsu_string_as_lua - nomsu_string_as_lua = function(self, code) + nomsu_string_as_lua = function(self, code, tree) 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) + elseif type(bit) == "table" and bit.type == "FunctionCall" and bit.src == "__src__" then + insert(concat_parts, repr(tree.src)) else local expr, statement = self:tree_to_lua(bit) if statement then @@ -1070,12 +1274,13 @@ end)]]):format(concat(lua_bits, "\n")) self:def("run file %filename", run_file) local _require _require = function(self, vars) - if not self.loaded_files[vars.filename] then - self.loaded_files[vars.filename] = run_file(self, { + 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 self.loaded_files[vars.filename] + return loaded[vars.filename] end return self:def("require %filename", _require) end @@ -1089,10 +1294,21 @@ end)]]):format(concat(lua_bits, "\n")) self.write_err = function(self, ...) return io.stderr:write(...) end - self.defs = setmetatable({ }, { - __index = parent and parent.defs - }) - self.def_number = 0 + 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.debug = false self.utils = utils @@ -1102,9 +1318,6 @@ end)]]):format(concat(lua_bits, "\n")) self.stringify = function(self, ...) return utils.stringify(...) end - self.loaded_files = setmetatable({ }, { - __index = parent and parent.loaded_files - }) if not parent then return self:initialize_core() end @@ -1121,6 +1334,7 @@ end)]]):format(concat(lua_bits, "\n")) }) _base_0.__class = _class_0 local self = _class_0 + self.def_number = 0 self.unescape_string = function(self, str) return str:gsub("\\(.)", (function(c) return STRING_ESCAPES[c] or c diff --git a/nomsu.moon b/nomsu.moon index 899b1fc..676f9b8 100755 --- a/nomsu.moon +++ b/nomsu.moon @@ -19,6 +19,12 @@ colors = setmetatable({}, {__index:->""}) colored = setmetatable({}, {__index:(_,color)-> ((msg)-> colors[color]..msg..colors.reset)}) {:insert, :remove, :concat} = table --pcall = (fn,...)-> true, fn(...) +if _VERSION == "Lua 5.1" + xp = xpcall + xpcall = (f, errhandler, ...)-> + args = {n:select("#", ...), ...} + return xp((...)-> f(unpack(args,1,args.n))), errhandler +--pcall = (fn, ...) -> xpcall(fn, debug.traceback, ...) -- TODO: -- Maybe get GOTOs working at file scope. @@ -28,7 +34,6 @@ colored = setmetatable({}, {__index:(_,color)-> ((msg)-> colors[color]..msg..col -- better scoping? -- better error reporting -- fix propagation of filename for error reporting --- add line numbers of function calls -- type checking? -- Fix compiler bug that breaks when file ends with a block comment -- Add compiler options for optimization level (compile-fast vs. run-fast, etc.) @@ -162,7 +167,8 @@ defs = for _ in src\sub(1,pos)\gmatch("\n") do line_no += 1 return pos, "#{CURRENT_FILE}:#{line_no}" FunctionCall: (src, line_no, value, errors)-> - {type: "FunctionCall", :src, :line_no, :value, :errors} + stub = concat([(t.type == "Word" and t.value or "%") for t in *value], " ") + {type: "FunctionCall", :src, :line_no, :value, :errors, :stub} error: (src,pos,errors,err_msg)-> line_no = 1 for _ in src\sub(1,-#errors)\gmatch("\n") do line_no += 1 @@ -190,17 +196,21 @@ setmetatable(defs, { nomsu = re.compile(nomsu, defs) class NomsuCompiler + @def_number: 0 new:(parent)=> @write = (...)=> io.write(...) @write_err = (...)=> io.stderr\write(...) - @defs = setmetatable({}, {__index:parent and parent.defs}) - @def_number = 0 + -- Use # to prevent someone from defining a function that has a namespace collision. + @defs = {["#vars"]:{}, ["#loaded_files"]:{}} + if parent + setmetatable(@defs, {__index:parent.defs}) + setmetatable(@defs["#vars"], {__index:parent["#vars"]}) + setmetatable(@defs["#loaded_files"], {__index:parent["#loaded_files"]}) @callstack = {} @debug = false @utils = utils @repr = (...)=> repr(...) @stringify = (...)=> utils.stringify(...) - @loaded_files = setmetatable({}, {__index:parent and parent.loaded_files}) if not parent @initialize_core! @@ -219,9 +229,12 @@ class NomsuCompiler signature = @get_stubs signature assert type(thunk) == 'function', "Bad thunk: #{repr thunk}" canonical_args = nil + canonical_escaped_args = nil aliases = {} - @def_number += 1 - for {stub, arg_names} in *signature + @@def_number += 1 + def = {:thunk, :src, :is_macro, aliases:{}, def_number:@@def_number, defs:@defs} + where_defs_go = ((getmetatable(@defs) or {}).__newindex) or @defs + for {stub, arg_names, escaped_args} in *signature assert stub, "NO STUB FOUND: #{repr signature}" if @debug then @writeln "#{colored.bright "DEFINING RULE:"} #{colored.underscore colored.magenta repr(stub)} #{colored.bright "WITH ARGS"} #{colored.dim repr(arg_names)}" for i=1,#arg_names-1 do for j=i+1,#arg_names @@ -229,22 +242,58 @@ class NomsuCompiler if canonical_args assert utils.equivalent(utils.set(arg_names), canonical_args), "Mismatched args" else canonical_args = utils.set(arg_names) - insert aliases, stub - @defs[stub] = {:thunk, :stub, :arg_names, :src, :is_macro, :aliases, def_number:@def_number} + if canonical_escaped_args + assert utils.equivalent(escaped_args, canonical_escaped_args), "Mismatched escaped args" + else + canonical_escaped_args = escaped_args + def.escaped_args = escaped_args + insert def.aliases, stub + stub_def = setmetatable({:stub, :arg_names, :escaped_args}, {__index:def}) + rawset(where_defs_go, stub, stub_def) defmacro: (signature, thunk, src)=> @def(signature, thunk, src, true) - - serialize_defs: (after=0)=> - defs = {} - for _, def in pairs(@defs) - defs[def.def_number] = def.src or "" - keys = utils.keys(defs) + + scoped: (thunk)=> + old_defs = @defs + @defs = setmetatable({}, {__index:old_defs}) + ok, ret1, ret2 = pcall thunk, @ + @defs = old_defs + if not ok then @error(ret1) + return ret1, ret2 + + serialize_defs: (scope=nil, after=0)=> + scope or= @defs + defs_by_num = {} + for stub, def in pairs(scope) + if def and stub\sub(1,1) != "#" + defs_by_num[def.def_number] = def + keys = [k for k,v in pairs(defs_by_num)] table.sort(keys) + buff = {} - for i in *keys - if i > after and #defs[i] > 0 - insert buff, defs[i] + k_i = 1 + _using = nil + _using_do = {} + for k_i,i in ipairs(keys) + if i <= after then continue + def = defs_by_num[i] + if def.defs == scope + if def.src + insert buff, def.src + continue + if _using == def.defs + if def.src + insert _using_do, def.src + else + _using = def.defs + _using_do = {def.src} + if k_i == #keys or defs_by_num[keys[k_i+1]].defs != _using + insert buff, "using:\n #{@indent @serialize_defs(_using)}\n..do:\n #{@indent concat(_using_do, "\n")}" + + for k,v in pairs(scope["#vars"] or {}) + insert buff, "<@#{k}> = #{@value_to_nomsu v}" + return concat buff, "\n" call: (stub,line_no,...)=> @@ -254,7 +303,7 @@ class NomsuCompiler if def and def.is_macro and @callstack[#@callstack] != "#macro" @error "Attempt to call macro at runtime: #{stub}\nThis can be caused by using a macro in a function that is defined before the macro." insert @callstack, {stub, line_no} - if def == nil + unless def @error "Attempt to call undefined function: #{stub}" unless def.is_macro @assert_permission(stub) @@ -263,22 +312,41 @@ class NomsuCompiler if @debug @write "#{colored.bright "CALLING"} #{colored.magenta(colored.underscore stub)} " @writeln "#{colored.bright "WITH ARGS:"} #{colored.dim repr(args)}" - -- TODO: optimize, but still allow multiple return values? + old_defs, @defs = @defs, def.defs rets = {thunk(self,args)} + @defs = old_defs remove @callstack return unpack(rets) run_macro: (tree)=> - stub = @get_stub tree args = [arg for arg in *tree.value when arg.type != "Word"] if @debug - @write "#{colored.bright "RUNNING MACRO"} #{colored.underscore colored.magenta(stub)} " + @write "#{colored.bright "RUNNING MACRO"} #{colored.underscore colored.magenta(tree.stub)} " @writeln "#{colored.bright "WITH ARGS:"} #{colored.dim repr args}" insert @callstack, "#macro" - expr, statement = @call(stub, tree.line_no, unpack(args)) + expr, statement = @call(tree.stub, tree.line_no, unpack(args)) remove @callstack return expr, statement + dedent: (code)=> + unless code\find("\n") + return code + spaces, indent_spaces = math.huge, math.huge + for line in code\gmatch("\n([^\n]*)") + if line\match("^%s*#.*") + continue + elseif s = line\match("^(%s*)%.%..*") + spaces = math.min(spaces, #s) + elseif s = line\match("^(%s*)%S.*") + indent_spaces = math.min(indent_spaces, #s) + if spaces != math.huge and spaces < indent_spaces + return (code\gsub("\n"..(" ")\rep(spaces), "\n")) + else + return (code\gsub("\n"..(" ")\rep(indent_spaces), "\n ")) + + indent: (code)=> + (code\gsub("\n","\n ")) + assert_permission: (stub)=> fn_def = @defs[stub] unless fn_def @@ -343,7 +411,7 @@ class NomsuCompiler ok,expr,statements = pcall(@tree_to_lua, self, statement) if not ok @errorln "#{colored.red "Error occurred in statement:"}\n#{colored.bright colored.yellow statement.src}" - @error(expr) + error(expr) code_for_statement = ([[ return (function(nomsu, vars) %s @@ -365,7 +433,7 @@ end);]])\format(statements or "", expr or "ret") if not ok @errorln "#{colored.red "Error occurred in statement:"}\n#{colored.yellow statement.src}" @errorln debug.traceback! - @error(ret) + error(ret) if statements insert buffer, statements if expr @@ -392,8 +460,6 @@ end);]])\format(concat(buffer, "\n")) tree_to_nomsu: (tree, force_inline=false)=> -- Return , - indent = (s)-> - s\gsub("\n","\n ") assert tree, "No tree provided." if not tree.type @errorln debug.traceback() @@ -410,27 +476,31 @@ end);]])\format(concat(buffer, "\n")) if force_inline return "{#{concat([@tree_to_nomsu(v, true) for v in *tree.value], "; ")}", true else - return ":"..indent("\n"..concat([@tree_to_nomsu v for v in *tree.value], "\n")), false + return ":"..@indent("\n"..concat([@tree_to_nomsu v for v in *tree.value], "\n")), false when "FunctionCall" buff = "" sep = "" inline = true - do_arg = (arg)-> - + line_len = 0 for arg in *tree.value nomsu, arg_inline = @tree_to_nomsu(arg, force_inline) - buff ..= sep + if sep == " " and line_len + #nomsu > 80 + sep = "\n.." + unless sep == " " and not arg_inline and nomsu\sub(1,1) == ":" + buff ..= sep if arg_inline sep = " " + line_len += 1 + #nomsu else + line_len = 0 inline = false sep = "\n.." if arg.type == 'FunctionCall' if arg_inline buff ..= "(#{nomsu})" else - buff ..= "(..)\n #{indent nomsu}" + buff ..= "(..)\n #{@indent nomsu}" else buff ..= nomsu return buff, inline @@ -491,6 +561,22 @@ end);]])\format(concat(buffer, "\n")) else @error("Unknown/unimplemented thingy: #{tree.type}") + value_to_nomsu: (value)=> + switch type(value) + when "nil" + return "(nil)" + when "bool" + return value and "(yes)" or "(no)" + when "number" + return repr(value) + when "table" + if utils.is_list(value) + return "[#{concat [@value_to_nomsu(v) for v in *value], ", "}]" + else + return "(d{#{concat ["#{@value_to_nomsu(k)}=#{@value_to_nomsu(v)}" for k,v in pairs(value)], "; "}})" + else + error("Unsupported value_to_nomsu type: #{type(value)}") + tree_to_lua: (tree)=> -- Return , assert tree, "No tree provided." @@ -502,7 +588,7 @@ end);]])\format(concat(buffer, "\n")) error("Should not be converting File to lua through this function.") when "Nomsu" - return "nomsu:parse(#{repr tree.value.src}).value[1]", nil + return "nomsu:parse(#{repr tree.value.src}, #{repr CURRENT_FILE}).value[1]", nil when "Thunk" lua_bits = {} @@ -518,23 +604,31 @@ return ret; end)]])\format(concat(lua_bits, "\n")) when "FunctionCall" - stub = @get_stub(tree) - def = @defs[stub] + def = @defs[tree.stub] if def and def.is_macro expr, statement = @run_macro(tree) if def.whiteset if expr - expr = "(nomsu:assert_permission(#{repr stub}) and #{expr})" + expr = "(nomsu:assert_permission(#{repr tree.stub}) and #{expr})" if statement - statement = "nomsu:assert_permission(#{repr stub});\n"..statement + statement = "nomsu:assert_permission(#{repr tree.stub});\n"..statement return expr, statement - args = {repr(stub), repr(tree.line_no)} + args = {repr(tree.stub), repr(tree.line_no)} + local arg_names, escaped_args + if def + arg_names, escaped_args = def.arg_names, def.escaped_args + else + arg_names, escaped_args = [w.value for w in *tree.value when w.type == "Word"], {} + arg_num = 1 for arg in *tree.value if arg.type == 'Word' then continue + if escaped_args[arg_names[arg_num]] + arg = {type:"Nomsu", value:arg} expr,statement = @tree_to_lua arg if statement @error "Cannot use [[#{arg.src}]] as a function argument, since it's not an expression." insert args, expr + arg_num += 1 return @@comma_separated_items("nomsu:call(", args, ")"), nil when "String" @@ -658,27 +752,22 @@ end)]])\format(concat(lua_bits, "\n")) get_stub: (x)=> if not x @error "Nothing to get stub from" - -- Returns a single stub ("say %"), and list of arg names ({"msg"}) from a single rule def + -- Returns a single stub ("say %"), list of arg names ({"msg"}), and set of arg + -- names that should not be evaluated from a single rule def -- (e.g. "say %msg") or function call (e.g. FunctionCall({Word("say"), Var("msg"))) if type(x) == 'string' - stub = x\gsub("([#{wordbreaker}]+)"," %1 ")\gsub("%%%S+","%%")\gsub("%s+"," ")\gsub("^%s*","")\gsub("%s*$","") - arg_names = [arg for arg in x\gmatch("%%([^%s']*)")] - return stub, arg_names + -- Standardize format to stuff separated by spaces + x = x\gsub("\n%s*%.%.", " ")\gsub("([#{wordbreaker}]+)", " %1 ")\gsub("%s+"," ") + x = x\gsub("^%s*","")\gsub("%s*$","") + stub = x\gsub("%%%S+","%%")\gsub("\\","") + arg_names = [arg for arg in x\gmatch("%%([^%s]*)")] + escaped_args = utils.set [arg for arg in x\gmatch("\\%%([^%s]*)")] + return stub, arg_names, escaped_args + if type(x) != 'table' + @error "Invalid type for getting stub: #{type(x)} for:\n#{repr x}" switch x.type when "String" then return @get_stub(x.value) - when "FunctionCall" - stub, arg_names = {}, {}, {} - for token in *x.value - switch token.type - when "Word" - insert stub, token.value - when "Var" - insert stub, "%" - if arg_names then insert arg_names, token.value - else - insert stub, "%" - arg_names = nil - return concat(stub," "), arg_names + when "FunctionCall" then return @get_stub(x.src) else @error "Unsupported get stub type: #{x.type} for #{repr x}" get_stubs: (x)=> @@ -699,31 +788,33 @@ end)]])\format(concat(lua_bits, "\n")) if verboten == "_" then "__" else ("_%x")\format(verboten\byte!)) error: (msg)=> - @errorln (colored.red "ERROR!") + error_msg = colored.red "ERROR!" if msg - @errorln(colored.bright colored.yellow colored.onred msg) - @errorln("Callstack:") + error_msg ..= "\n" .. (colored.bright colored.yellow colored.onred msg) + error_msg ..= "\nCallstack:" maxlen = utils.max([#c[2] for c in *@callstack when c != "#macro"]) for i=#@callstack,1,-1 if @callstack[i] != "#macro" - @errorln " #{"%-#{maxlen}s"\format @callstack[i][2]}| #{@callstack[i][1]}" - @errorln " " + error_msg ..= "\n #{"%-#{maxlen}s"\format @callstack[i][2]}| #{@callstack[i][1]}" + error_msg ..= "\n " @callstack = {} - error! + error error_msg, 3 typecheck: (vars, varname, desired_type)=> x = vars[varname] if type(x) == desired_type then return x if type(x) == 'table' and x.type == desired_type then return x - @error "Invalid type for %#{varname}. Expected #{desired_type}, but got #{x.type}." + @error "Invalid type for %#{varname}. Expected #{desired_type}, but got #{repr x}." initialize_core: => -- Sets up some core functionality - nomsu_string_as_lua = (code)=> + nomsu_string_as_lua = (code, tree)=> concat_parts = {} for bit in *code.value if type(bit) == "string" insert concat_parts, bit + elseif type(bit) == "table" and bit.type == "FunctionCall" and bit.src == "__src__" + insert concat_parts, repr(tree.src) else expr, statement = @tree_to_lua bit if statement @@ -763,9 +854,10 @@ end)]])\format(concat(lua_bits, "\n")) @def "run file %filename", run_file _require = (vars)=> - if not @loaded_files[vars.filename] - @loaded_files[vars.filename] = run_file(self, {filename:vars.filename}) or true - return @loaded_files[vars.filename] + loaded = @defs["#loaded_files"] + if not loaded[vars.filename] + loaded[vars.filename] = run_file(self, {filename:vars.filename}) or true + return loaded[vars.filename] @def "require %filename", _require