diff options
Diffstat (limited to 'nomsu.moon')
| -rwxr-xr-x | nomsu.moon | 567 |
1 files changed, 567 insertions, 0 deletions
diff --git a/nomsu.moon b/nomsu.moon new file mode 100755 index 0000000..243dd64 --- /dev/null +++ b/nomsu.moon @@ -0,0 +1,567 @@ +#!/usr/bin/env moon +re = require 're' +lpeg = require 'lpeg' +utils = require 'utils' + +-- TODO: +-- string interpolation +-- comprehensions? +-- dicts? +-- better scoping? +-- first-class functions + +INDENT = " " +lpeg.setmaxstack 10000 -- whoa +{:P,:V,:S,:Cg,:C,:Cp,:B,:Cmt} = lpeg + +wordchar = P(1)-S(' \t\n\r%:;,.{}[]()"') +comment = re.compile [[comment <- "(#" (comment / ((! "#)") .))* "#)"]] +whitespace = (S(" \t") + comment)^1 +nl = P("\n") +blank_line = whitespace^-1 * nl + +get_line_indentation = (line)-> + indent_amounts = {[" "]:1, ["\t"]:4} + with sum = 0 + leading_space = line\gsub("([\t ]*).*", "%1") + for c in leading_space\gmatch "[\t ]" + sum += indent_amounts[c] + +make_parser = (lingo, extra_definitions)-> + indent_stack = {0} + push = (n)-> table.insert indent_stack, n + pop = ()-> table.remove indent_stack + check_indent = (subject,end_pos,spaces)-> + num_spaces = get_line_indentation(spaces) + if num_spaces <= indent_stack[#indent_stack] then return nil + push num_spaces + return end_pos + check_dedent = (subject,end_pos,spaces)-> + num_spaces = get_line_indentation(spaces) + if num_spaces >= indent_stack[#indent_stack] then return nil + pop! + return end_pos + check_nodent = (subject,end_pos,spaces)-> + num_spaces = get_line_indentation(spaces) + if num_spaces != indent_stack[#indent_stack] then return nil + return end_pos + + defs = + :wordchar, :nl, ws:whitespace, :comment + eol: #nl + (P("")-P(1)) + word_boundary: whitespace + B(P("..")) + B(S("\";)]")) + #S("\":([") + #((whitespace + nl)^0 * P("..")) + indent: #(nl * blank_line^0 * Cmt(whitespace^-1, check_indent)) + dedent: #(nl * blank_line^0 * Cmt(whitespace^-1, check_dedent)) + new_line: nl * blank_line^0 * Cmt(whitespace^-1, check_nodent) + error_handler: (src,pos,errors)-> + line_no = 1 + for _ in src\sub(1,-#errors)\gmatch("\n") do line_no += 1 + err_pos = #src - #errors + 1 + if errors\sub(1,1) == "\n" + -- Indentation error + err_pos += #errors\match("[ \t]*", 2) + start_of_err_line = err_pos + while src\sub(start_of_err_line, start_of_err_line) != "\n" do start_of_err_line -= 1 + start_of_prev_line = start_of_err_line - 1 + while src\sub(start_of_prev_line, start_of_prev_line) != "\n" do start_of_prev_line -= 1 + + prev_line,err_line,next_line = src\match("([^\n]*)\n([^\n]*)\n([^\n]*)", start_of_prev_line+1) + + pointer = ("-")\rep(err_pos - start_of_err_line + 0) .. "^" + error("\nParse error on line #{line_no}:\n\n#{prev_line}\n#{err_line}\n#{pointer}\n#{next_line}\n") + + if extra_definitions + for k,v in pairs(extra_definitions) do defs[k] = v + + setmetatable(defs, { + __index: (t,key)-> + fn = (src, value, errors)-> + token = {type: key, :src, :value, :errors} + return token + t[key] = fn + return fn + }) + return re.compile lingo, defs + +class Compiler + new:(parent)=> + @defs = setmetatable({}, {__index:parent and parent.defs}) + @callstack = {} + @debug = false + @initialize_core! + + call: (fn_name,...)=> + fn_info = @defs[fn_name] + if fn_info == nil + @error "Attempt to call undefined function: #{fn_name}" + if fn_info.is_macro + @error "Attempt to call macro at runtime: #{fn_name}" + unless @check_permission(fn_name) + @error "You do not have the authority to call: #{fn_name}" + table.insert @callstack, fn_name + {:fn, :arg_names} = fn_info + args = {name, select(i,...) for i,name in ipairs(arg_names)} + if @debug + print "Calling #{fn_name} with args: #{utils.repr(args)}" + ret = fn(self, args) + table.remove @callstack + return ret + + check_permission: (fn_name)=> + fn_info = @defs[fn_name] + if fn_info == nil + @error "Undefined function: #{fn_name}" + if fn_info.whiteset == nil then return true + for caller in *@callstack + if fn_info.whiteset[caller] + return true + return false + + def: (spec, fn)=> + if @debug + print "Defining rule: #{spec}" + invocations,arg_names = @get_invocations spec + fn_info = {:fn, :arg_names, :invocations, is_macro:false} + for invocation in *invocations + @defs[invocation] = fn_info + + get_invocations:(text)=> + if type(text) == 'string' then text = {text} + invocations = {} + local arg_names + for _text in *text + invocation = _text\gsub("%%%S+","%%") + _arg_names = [arg for arg in _text\gmatch("%%(%S+)")] + table.insert(invocations, invocation) + if arg_names + if not utils.equivalent(utils.set(arg_names), utils.set(_arg_names)) + @error("Conflicting argument names #{utils.repr(arg_names)} and #{utils.repr(_arg_names)} for #{utils.repr(text)}") + else arg_names = _arg_names + return invocations, arg_names + + defmacro: (spec, lua_gen_fn)=> + invocations,arg_names = @get_invocations spec + fn_info = {fn:lua_gen_fn, :arg_names, :invocations, is_macro:true} + for invocation in *invocations + @defs[invocation] = fn_info + + run: (text)=> + if @debug + print "RUNNING TEXT:\n#{text}" + -- This will execute each chunk as it goes along + code = @compile(text) + if @debug + print "\nGENERATED LUA CODE:\n#{code}" + return code + + parse: (str)=> + if @debug + print("PARSING:\n#{str}") + lingo = [=[ + file <- ({ {| %ws? %new_line? {:body: block :} %new_line? %ws? (errors)? |} }) -> File + errors <- (({.+}) => error_handler) + block <- ({ {| statement (%new_line statement)* |} }) -> Block + statement <- ({ (functioncall / expression) }) -> Statement + one_liner <- ({ {| + (({ + (({ {| + (expression (%word_boundary fn_bit)+) / (word (%word_boundary fn_bit)*) + |} }) -> FunctionCall) + / (expression) + }) -> Statement) + |} }) -> Block + + functioncall <- ({ {| (expression %word_boundary fn_bits) / (word (%word_boundary fn_bits)?) |} }) -> FunctionCall + fn_bit <- (expression / word) + fn_bits <- + ((".." %ws? (%indent %new_line indented_fn_bits %dedent) (%new_line ".." %ws? fn_bits)?) + / (%new_line ".." fn_bit (%word_boundary fn_bits)?) + / (fn_bit (%word_boundary fn_bits)?)) + indented_fn_bits <- + fn_bit ((%new_line / %word_boundary) indented_fn_bits)? + + thunk <- + ({ ":" %ws? + ((%indent %new_line block ((%dedent (%new_line "..")?) / errors)) + / (one_liner (%ws? (%new_line? ".."))?)) }) -> Thunk + + word <- ({ !number {%wordchar+} }) -> Word + expression <- ({ (longstring / string / number / variable / list / thunk / subexpression) }) -> Expression + + string <- ({ (!longstring) '"' {(("\" .) / [^"])*} '"' }) -> String + longstring <- ({ '".."' %ws? %indent {(%new_line "|" [^%nl]*)+} ((%dedent (%new_line '..')?) / errors) }) -> Longstring + number <- ({ {'-'? [0-9]+ ("." [0-9]+)?} }) -> Number + variable <- ({ ("%" {%wordchar+}) }) -> Var + + subexpression <- + (!%comment "(" %ws? (functioncall / expression) %ws? ")") + / ("(..)" %ws? %indent %new_line ((({ {| indented_fn_bits |} }) -> FunctionCall) / expression) %dedent (%new_line "..")?) + + list <- ({ {| + ("[..]" %ws? %indent %new_line indented_list ","? ((%dedent (%new_line "..")?) / errors)) + / ("[" %ws? (list_items ","?)? %ws?"]") + |} }) -> List + list_items <- ((functioncall / expression) (list_sep list_items)?) + list_sep <- %ws? "," %ws? + indented_list <- + (functioncall / expression) (((list_sep %new_line?) / %new_line) indented_list)? + ]=] + lingo = make_parser lingo + + tree = lingo\match(str\gsub("\r","").."\n") + if @debug + print("\nPARSE TREE:") + @print_tree(tree) + assert tree, "Failed to parse: #{str}" + return tree + + tree_to_value: (tree)=> + code = "return (function(compiler, vars)\nreturn #{@tree_to_lua(tree)}\nend)" + lua_thunk, err = load(code) + if not lua_thunk + error("Failed to compile generated code:\n#{code}\n\n#{err}") + return (lua_thunk!)(self, {}) + + tree_to_lua: (tree, kind="Expression")=> + assert tree, "No tree provided." + indent = "" + buffer = {} + + to_lua = (t,kind)-> + ret = @tree_to_lua(t,kind) + return ret + + add = (code)-> table.insert(buffer, code) + + switch tree.type + when "File" + add [[return (function(compiler, vars) + local ret]] + vars = {} + for statement in *tree.value.body.value + code = to_lua(statement) + -- Run the fuckers as we go + lua_thunk, err = load("return (function(compiler, vars)\n#{code}\nend)") + if not lua_thunk + error("Failed to compile generated code:\n#{code}\n\n#{err}") + ok,err = pcall(lua_thunk) + if not ok then error(err) + ok,err = pcall(err, self, vars) + if not ok then @error(err) + add code + add [[ + return ret + end) + ]] + + when "Block" + for statement in *tree.value + add to_lua(statement) + + when "Thunk" + assert tree.value.type == "Block", "Non-block value in Thunk" + add [[ + (function(compiler, vars) + local ret]] + add to_lua(tree.value) + add [[ + return ret + end) + ]] + + when "Statement" + -- This case here is to prevent "ret =" from getting prepended when the macro might not want it + if tree.value.type == "FunctionCall" + name = @fn_name_from_tree(tree.value) + if @defs[name] and @defs[name].is_macro + add @run_macro(tree.value, "Statement") + else + add "ret = "..(to_lua(tree.value)\match("%s*(.*)")) + else + add "ret = "..(to_lua(tree.value)\match("%s*(.*)")) + + when "Expression" + add to_lua(tree.value) + + when "FunctionCall" + name = @fn_name_from_tree(tree) + if @defs[name] and @defs[name].is_macro + add @run_macro(tree, "Expression") + else + args = [to_lua(a) for a in *tree.value when a.type != "Word"] + table.insert args, 1, utils.repr(name, true) + add @@comma_separated_items("compiler:call(", args, ")") + + when "String" + escapes = n:"\n", t:"\t", b:"\b", a:"\a", v:"\v", f:"\f", r:"\r" + unescaped = tree.value\gsub("\\(.)", ((c)-> escapes[c] or c)) + add utils.repr(unescaped, true) + + when "Longstring" + -- TODO: handle comments here? + result = [line for line in tree.value\gmatch("[ \t]*|([^\n]*)")] + add utils.repr(table.concat(result, "\n"), true) + + when "Number" + add tree.value + + when "List" + if #tree.value == 0 + add "{}" + elseif #tree.value == 1 + add "{#{to_lua(tree.value[1])}}" + else + add @@comma_separated_items("{", [to_lua(item) for item in *tree.value], "}") + + when "Var" + add "vars[#{utils.repr(tree.value,true)}]" + + else + error("Unknown/unimplemented thingy: #{tree.type}") + + -- TODO: make indentation clean + buffer = table.concat(buffer, "\n") + return buffer + + @comma_separated_items: (open, items, close)=> + utils.accumulate "\n", -> + buffer = open + so_far = 0 + for i,item in ipairs(items) + if i < #items then item ..= ", " + if so_far + #item >= 80 and #buffer > 0 + coroutine.yield buffer + so_far -= #buffer + buffer = item + else + so_far += #item + buffer ..= item + buffer ..= close + coroutine.yield buffer + + fn_name_from_tree: (tree)=> + assert(tree.type == "FunctionCall", "Attempt to get fn name from non-functioncall tree: #{tree.type}") + name_bits = {} + for token in *tree.value + table.insert name_bits, if token.type == "Word" then token.value else "%" + table.concat(name_bits, " ") + + run_macro: (tree, kind="Expression")=> + name = @fn_name_from_tree(tree) + unless @defs[name] and @defs[name].is_macro + @error("Macro not found: #{name}") + unless @check_permission(name) + @error "You do not have the authority to call: #{name}" + {:fn, :arg_names} = @defs[name] + args = [a for a in *tree.value when a.type != "Word"] + args = {name,args[i] for i,name in ipairs(arg_names)} + table.insert @callstack, name + ret, manual_mode = fn(self, args, kind) + table.remove @callstack + if not ret + @error("No return value for macro: #{name}") + if kind == "Statement" and not manual_mode + ret = "ret = "..ret + return ret + + _yield_tree: (tree, indent_level=0)=> + ind = (s) -> INDENT\rep(indent_level)..s + switch tree.type + when "File" + coroutine.yield(ind"File:") + @_yield_tree(tree.value.body, indent_level+1) + + when "Errors" + coroutine.yield(ind"Error:\n#{tree.value}") + + when "Block" + for chunk in *tree.value + @_yield_tree(chunk, indent_level) + + when "Thunk" + coroutine.yield(ind"Thunk:") + @_yield_tree(tree.value, indent_level+1) + + when "Statement" + @_yield_tree(tree.value, indent_level) + + when "Expression" + @_yield_tree(tree.value, indent_level) + + when "FunctionCall" + name = @fn_name_from_tree(tree) + args = [a for a in *tree.value when a.type != "Word"] + if #args == 0 + coroutine.yield(ind"Call [#{name}]!") + else + coroutine.yield(ind"Call [#{name}]:") + for a in *args + @_yield_tree(a, indent_level+1) + + when "String" + -- TODO: Better implement + coroutine.yield(ind(utils.repr(tree.value, true))) + + when "Longstring" + -- TODO: Better implement + coroutine.yield(ind(utils.repr(tree.value, true))) + + when "Number" + coroutine.yield(ind(tree.value)) + + when "List" + if #tree.value == 0 + coroutine.yield(ind("<Empty List>")) + else + coroutine.yield(ind"List:") + for item in *tree.value + @_yield_tree(item, indent_level+1) + + when "Var" + coroutine.yield ind"Var[#{utils.repr(tree.value)}]" + + else + error("Unknown/unimplemented thingy: #{tree.type}") + return nil -- to prevent tail calls + + print_tree:(tree)=> + for line in coroutine.wrap(-> @_yield_tree(tree)) + print(line) + + stringify_tree:(tree)=> + result = {} + for line in coroutine.wrap(-> @_yield_tree(tree)) + table.insert(result, line) + return table.concat result, "\n" + + compile: (src, output_file=nil)=> + if @debug + print "COMPILING:\n#{src}" + tree = @parse(src) + assert tree, "Tree failed to compile: #{src}" + code = @tree_to_lua(tree) + if output_file + output = io.open(output_file, "w") + output\write(code) + return code + + error: (...)=> + print(...) + print("Callstack:") + for i=#@callstack,1,-1 + print " #{@callstack[i]}" + error! + + test: (src, expected)=> + i = 1 + while i != nil + start,stop = src\find("\n\n", i) + + test = src\sub(i,start) + i = stop + start,stop = test\find"===" + if not start or not stop then + @error("WHERE'S THE ===? in:\n#{test}") + test_src, expected = test\sub(1,start-1), test\sub(stop+1,-1) + expected = expected\match'[\n]*(.*[^\n])' + tree = @parse(test_src) + got = @stringify_tree(tree.value.body) + if got != expected + @error"TEST FAILED!\nSource:\n#{test_src}\nExpected:\n#{expected}\n\nGot:\n#{got}" + + + initialize_core: => + -- Sets up some core functionality + as_lua_code = (str)=> + switch str.type + when "String" + escapes = n:"\n", t:"\t", b:"\b", a:"\a", v:"\v", f:"\f", r:"\r" + unescaped = str.value\gsub("\\(.)", ((c)-> escapes[c] or c)) + return unescaped + + when "Longstring" + -- TODO: handle comments? + result = [line for line in str.value\gmatch("[ \t]*|([^\n]*)")] + return table.concat(result, "\n") + else + return @tree_to_lua(str) + + @defmacro [[lua block %lua_code]], (vars, kind)=> + if kind == "Expression" then error("Expected to be in statement.") + lua_code = vars.lua_code.value + switch lua_code.type + when "List" + -- TODO: handle subexpressions + return table.concat([as_lua_code(@, i.value) for i in *lua_code.value]), true + else + return as_lua_code(@, lua_code), true + + @defmacro [[lua expr %lua_code]], (vars, kind)=> + lua_code = vars.lua_code.value + switch lua_code.type + when "List" + -- TODO: handle subexpressions + return table.concat([as_lua_code(@, i.value) for i in *lua_code.value]) + else + return as_lua_code(@, lua_code) + + @def "rule %spec %body", (vars)=> + @def vars.spec, vars.body + + @defmacro [[macro %spec %body]], (vars, kind)=> + if kind == "Expression" + error("Macro definitions cannot be used as expressions.") + @defmacro @tree_to_value(vars.spec), @tree_to_value(vars.body) + return "", true + + @defmacro [[macro block %spec %body]], (vars, kind)=> + if kind == "Expression" + error("Macro definitions cannot be used as expressions.") + invocation = @tree_to_value(vars.spec) + fn = @tree_to_value(vars.body) + @defmacro invocation, ((vars,kind)=> + if kind == "Expression" + error("Macro: #{invocation} was defined to be a block, not an expression.") + return fn(@,vars,kind), true) + return "", true + + @def "run file %filename", (vars)=> + file = io.open(vars.filename) + return @run(file\read('*a')) + + +-- Run on the command line via "./nomsu.moon input_file.nom" to execute +-- and "./nomsu.moon input_file.nom output_file.lua" to compile (use "-" to compile to stdout) +if arg[1] + c = Compiler() + input = io.open(arg[1])\read("*a") + -- Kinda hacky, if run via "./nomsu.moon file.nom -", then silence print and io.write + -- during execution and re-enable them to print out the generated source code + _print = print + _io_write = io.write + if arg[2] == "-" + export print + nop = -> + print, io.write = nop, nop + code = c\run(input) + if arg[2] + output = if arg[2] == "-" + export print + print, io.write = _print, _io_write + io.output() + else io.open(arg[2], 'w') + + output\write [[ + local load = function() + ]] + output\write(code) + output\write [[ + + end + local utils = require('utils') + local Compiler = require('nomsu') + local c = Compiler(require('core')) + load()(c, {}) + ]] + +return Compiler |
