From 59c79bdf57ce31d5f2fe15dbbdcc6ff345adc651 Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Tue, 19 Jun 2018 01:27:32 -0700 Subject: Split up CLI and NomsuCompiler --- nomsu.moon | 1073 ++++++++---------------------------------------------------- 1 file changed, 141 insertions(+), 932 deletions(-) (limited to 'nomsu.moon') diff --git a/nomsu.moon b/nomsu.moon index 2435920..9dc256e 100755 --- a/nomsu.moon +++ b/nomsu.moon @@ -1,821 +1,6 @@ #!/usr/bin/env moon --- This file contains the source code of the Nomsu compiler. --- Nomsu is a programming language that cross-compiles to Lua. It was designed to be good --- at natural-language-like code that is highly self-modifying and flexible. --- The only dependency is LPEG, which can be installed using "luarocks install lpeg" --- File usage: --- Either, in a lua/moonscript file: --- Nomsu = require "nomsu" --- nomsu = Nomsu() --- nomsu:run(your_nomsu_code) --- Or from the command line: --- lua nomsu.lua your_file.nom -lpeg = require 'lpeg' -re = require 're' -lpeg.setmaxstack 10000 -utils = require 'utils' -{:repr, :stringify, :equivalent} = utils -export colors, colored -colors = require 'consolecolors' -colored = setmetatable({}, {__index:(_,color)-> ((msg)-> colors[color]..tostring(msg or '')..colors.reset)}) -{:insert, :remove, :concat} = table -unpack or= table.unpack -{:match, :sub, :rep, :gsub, :format, :byte, :match, :find} = string -{:NomsuCode, :LuaCode, :Source} = require "code_obj" -AST = require "nomsu_tree" -parse = require("parser") -STDIN, STDOUT, STDERR = "/dev/fd/0", "/dev/fd/1", "/dev/fd/2" --- Mapping from source string (e.g. "@core/metaprogramming.nom[1:100]") to a mapping --- from lua line number to nomsu line number -export SOURCE_MAP -SOURCE_MAP = {} - -string.as_lua_id = (str)-> - argnum = 0 - -- Cut up escape-sequence-like chunks - str = gsub str, "x([0-9A-F][0-9A-F])", "x\0%1" - -- Alphanumeric unchanged, spaces to underscores, and everything else to hex escape sequences - str = gsub str, "%W", (c)-> - if c == ' ' then '_' - elseif c == '%' then - argnum += 1 - tostring(argnum) - else format("x%02X", byte(c)) - return '_'..str - -table.map = (fn)=> [fn(v) for _,v in ipairs(@)] -table.fork = (t, values)-> setmetatable(values or {}, {__index:t}) - --- TODO: --- consider non-linear codegen, rather than doing thunks for things like comprehensions --- Add a ((%x foo %y) where {x:"asdf", y:"fdsa"}) compile-time action for substitution --- Re-implement nomsu-to-lua comment translation? - -export FILE_CACHE --- FILE_CACHE is a map from filename (string) -> string of file contents -FILE_CACHE = setmetatable {}, { - __index: (filename)=> - file = io.open(filename) - return nil unless file - contents = file\read("*a") - file\close! - self[filename] = contents - return contents -} - -iterate_single = (item, prev) -> if item == prev then nil else item -all_files = (path)-> - -- Sanitize path - if match(path, "%.nom$") or match(path, "%.lua$") or match(path, "^/dev/fd/[012]$") - return iterate_single, path - -- TODO: improve sanitization - path = gsub(path,"\\","\\\\") - path = gsub(path,"`","") - path = gsub(path,'"','\\"') - path = gsub(path,"$","") - return coroutine.wrap -> - f = io.popen('find -L "'..path..'" -not -path "*/\\.*" -type f -name "*.nom"') - for line in f\lines! - coroutine.yield(line) - success = f\close! - unless success - error("Invalid file path: "..tostring(path)) - -line_counter = re.compile([[ - lines <- {| line (%nl line)* |} - line <- {} (!%nl .)* -]], nl:lpeg.P("\r")^-1 * lpeg.P("\n")) -get_lines = re.compile([[ - lines <- {| line (%nl line)* |} - line <- {[^%nl]*} -]], nl:lpeg.P("\r")^-1 * lpeg.P("\n")) --- Mapping from line number -> character offset -export LINE_STARTS --- LINE_STARTS is a mapping from strings to a table that maps line number to character positions -LINE_STARTS = setmetatable {}, { - __mode:"k" - __index: (k)=> - -- Implicitly convert Lua and Nomsu objects to strings - if type(k) != 'string' - k = tostring(k) - if v = rawget(self, k) - return v - line_starts = line_counter\match(k) - self[k] = line_starts - return line_starts -} -export pos_to_line -pos_to_line = (str, pos)-> - line_starts = LINE_STARTS[str] - -- Binary search for line number of position - lo, hi = 1, #line_starts - while lo <= hi - mid = math.floor((lo+hi)/2) - if line_starts[mid] > pos - hi = mid-1 - else lo = mid+1 - return hi - --- Use + operator for string coercive concatenation (note: "asdf" + 3 == "asdf3") --- Use [] for accessing string characters, or s[{3,4}] for s:sub(3,4) --- Note: This globally affects all strings in this instance of Lua! -do - STRING_METATABLE = getmetatable("") - STRING_METATABLE.__add = (other)=> @ .. stringify(other) - STRING_METATABLE.__index = (i)=> - ret = string[i] - if ret != nil then return ret - if type(i) == 'number' then return sub(@, i, i) - elseif type(i) == 'table' then return sub(@, i[1], i[2]) - --- List and Dict classes to provide basic equality/tostring functionality for the tables --- used in Nomsu. This way, they retain a notion of whether they were originally lists or dicts. -_list_mt = - __eq:equivalent - -- Could consider adding a __newindex to enforce list-ness, but would hurt performance - __tostring: => - "["..concat([repr(b) for b in *@], ", ").."]" -list = (t)-> setmetatable(t, _list_mt) - -_dict_mt = - __eq:equivalent - __tostring: => - "{"..concat(["#{repr(k)}: #{repr(v)}" for k,v in pairs @], ", ").."}" -dict = (t)-> setmetatable(t, _dict_mt) - -MAX_LINE = 80 -- For beautification purposes, try not to make lines much longer than this value -NomsuCompiler = setmetatable({}, {__index: (k)=> if _self = rawget(@, "self") then _self[k] else nil}) -with NomsuCompiler - ._ENV = NomsuCompiler - .nomsu = NomsuCompiler - .parse = (...)=> parse(...) - - -- Discretionary/convenience stuff - to_add = { - repr:repr, stringify:stringify, utils:utils, lpeg:lpeg, re:re, - -- Lua stuff: - :next, :unpack, :setmetatable, :coroutine, :rawequal, :getmetatable, :pcall, - :error, :package, :os, :require, :tonumber, :tostring, :string, :xpcall, :module, - :print, :loadfile, :rawset, :_VERSION, :collectgarbage, :rawget, :rawlen, - :table, :assert, :dofile, :loadstring, :type, :select, :debug, :math, :io, :load, - :pairs, :ipairs, - -- Nomsu types: - :list, :dict, - } - for k,v in pairs(to_add) do NomsuCompiler[k] = v - for k,v in pairs(AST) do NomsuCompiler[k] = v - .LuaCode = LuaCode - .NomsuCode = NomsuCode - .Source = Source - .ALIASES = setmetatable({}, {__mode:"k"}) - .LOADED = {} - .AST = AST - - .compile_error = (tok, err_format_string, ...)=> - file = FILE_CACHE[tok.source.filename] - line_no = pos_to_line(file, tok.source.start) - line_start = LINE_STARTS[file][line_no] - src = colored.dim(file\sub(line_start, tok.source.start-1)) - src ..= colored.underscore colored.bright colored.red(file\sub(tok.source.start, tok.source.stop-1)) - end_of_line = (LINE_STARTS[file][pos_to_line(file, tok.source.stop) + 1] or 0) - 1 - src ..= colored.dim(file\sub(tok.source.stop, end_of_line-1)) - src = ' '..src\gsub('\n', '\n ') - err_msg = err_format_string\format(src, ...) - error("#{tok.source.filename}:#{line_no}: "..err_msg, 0) - - -- This is a bit of a hack, but this code handles arbitrarily complex - -- math expressions like 2*x + 3^2 without having to define a single - -- action for every possibility. - math_expression = re.compile [[ ([+-] " ")* "%" (" " [*/^+-] (" " [+-])* " %")+ !. ]] - add_lua_bits = (lua, code)=> - for bit in *code - if type(bit) == "string" - lua\append bit - else - bit_lua = @compile(bit) - unless bit_lua.is_value - @compile_error bit, - "Cannot use:\n%s\nas a string interpolation value, since it's not an expression." - lua\append bit_lua - return lua - - add_lua_string_bits = (lua, code)=> - line_len = 0 - if code.type != "Text" - lua\append ", ", @compile(code) - return - for bit in *code - bit_lua = if type(bit) == "string" - repr(bit) - else - bit_lua = @compile(bit) - unless bit_lua.is_value - @compile_error bit, - "Cannot use:\n%s\nas a string interpolation value, since it's not an expression." - bit_lua - line_len += #tostring(bit_lua) - if line_len > MAX_LINE - lua\append ",\n " - line_len = 4 - else - lua\append ", " - lua\append bit_lua - - .COMPILE_ACTIONS = setmetatable { - ["# compile math expr #"]: (tree, ...)=> - lua = LuaCode.Value(tree.source) - for i,tok in ipairs tree - if type(tok) == 'string' - lua\append tok - else - tok_lua = @compile(tok) - unless tok_lua.is_value - @compile_error tok, "Non-expression value inside math expression:\n%s" - if tok.type == "Action" - tok_lua\parenthesize! - lua\append tok_lua - if i < #tree - lua\append " " - return lua - - ["Lua %"]: (tree, _code)=> - lua = LuaCode.Value(_code.source, "LuaCode(", repr(tostring _code.source)) - add_lua_string_bits(@, lua, _code) - lua\append ")" - return lua - - ["Lua value %"]: (tree, _code)=> - lua = LuaCode.Value(_code.source, "LuaCode.Value(", repr(tostring _code.source)) - add_lua_string_bits(@, lua, _code) - lua\append ")" - return lua - - ["lua > %"]: (tree, _code)=> - if _code.type != "Text" - return LuaCode tree.source, "nomsu:run_lua(", @compile(_code), ");" - return add_lua_bits(@, LuaCode(tree.source), _code) - - ["= lua %"]: (tree, _code)=> - if _code.type != "Text" - return LuaCode.Value tree.source, "nomsu:run_lua(", @compile(_code), ":as_statements('return '))" - return add_lua_bits(@, LuaCode.Value(tree.source), _code) - - ["use %"]: (tree, _path)=> - unless _path.type == 'Text' and #_path == 1 and type(_path[1]) == 'string' - return LuaCode(tree.source, "nomsu:run_file(#{@compile(_path)});") - path = _path[1] - @run_file(path) - return LuaCode(tree.source, "nomsu:run_file(#{repr path});") - }, { - __index: (stub)=> - if math_expression\match(stub) - return @["# compile math expr #"] - } - - .run = (to_run, source=nil)=> - tree = if AST.is_syntax_tree(to_run) then tree else @parse(to_run, source or to_run.source) - if tree == nil -- Happens if pattern matches, but there are no captures, e.g. an empty string - return nil - if tree.type == "FileChunks" - -- Each chunk's compilation is affected by the code in the previous chunks - -- (typically), so each chunk needs to compile and run before the next one - -- compiles. - ret = nil - all_lua = {} - for chunk in *tree - lua = @compile(chunk)\as_statements! - lua\declare_locals! - lua\prepend "-- File: #{chunk.source or ""}\n" - insert all_lua, tostring(lua) - ret = @run_lua(lua) - if @on_compile - self.on_compile(concat(all_lua, "\n"), (source or to_run.source).filename) - return ret - else - lua = @compile(tree, compile_actions)\as_statements! - lua\declare_locals! - lua\prepend "-- File: #{source or to_run.source or ""}\n" - if @on_compile - self.on_compile(lua, (source or to_run.source).filename) - return @run_lua(lua) - - _running_files = {} -- For detecting circular imports - .run_file = (filename)=> - if @LOADED[filename] - return @LOADED[filename] - ret = nil - for filename in all_files(filename) - if ret = @LOADED[filename] - continue - - -- Check for circular import - for i,running in ipairs _running_files - if running == filename - loop = [_running_files[j] for j=i,#_running_files] - insert loop, filename - error("Circular import, this loops forever: #{concat loop, " -> "}...") - - insert _running_files, filename - if match(filename, "%.lua$") - file = assert(FILE_CACHE[filename], "Could not find file: #{filename}") - ret = @run_lua file, Source(filename, 1, #file) - elseif match(filename, "%.nom$") or match(filename, "^/dev/fd/[012]$") - ran_lua = if not @skip_precompiled -- Look for precompiled version - lua_filename = gsub(filename, "%.nom$", ".lua") - if file = FILE_CACHE[lua_filename] - ret = @run_lua file, Source(lua_filename, 1, #file) - true - unless ran_lua - file = file or FILE_CACHE[filename] - if not file - error("File does not exist: #{filename}", 0) - ret = @run file, Source(filename,1,#file) - else - error("Invalid filetype for #{filename}", 0) - @LOADED[filename] = ret or true - remove _running_files - - @LOADED[filename] = ret or true - return ret - - .run_lua = (lua, source=nil)=> - lua_string = tostring(lua) - run_lua_fn, err = load(lua_string, nil and tostring(source or lua.source), "t", self) - if not run_lua_fn - line_numbered_lua = concat( - [format("%3d|%s",i,line) for i, line in ipairs get_lines\match(lua_string)], - "\n") - error("Failed to compile generated code:\n#{colored.bright colored.blue colored.onblack line_numbered_lua}\n\n#{err}", 0) - source_key = tostring(source or lua.source) - unless SOURCE_MAP[source_key] - map = {} - offset = 1 - source or= lua.source - nomsu_str = tostring(FILE_CACHE[source.filename]\sub(source.start, source.stop)) - lua_line = 1 - nomsu_line = pos_to_line(nomsu_str, source.start) - fn = (s)-> - if type(s) == 'string' - for nl in s\gmatch("\n") - map[lua_line] or= nomsu_line - lua_line += 1 - else - old_line = nomsu_line - if s.source - nomsu_line = pos_to_line(nomsu_str, s.source.start) - for b in *s.bits do fn(b) - fn(lua) - map[lua_line] or= nomsu_line - map[0] = 0 - -- Mapping from lua line number to nomsu line numbers - SOURCE_MAP[source_key] = map - - return run_lua_fn! - - .compile = (tree)=> - switch tree.type - when "Action" - stub = tree.stub - if compile_action = @COMPILE_ACTIONS[stub] - args = [arg for arg in *tree when type(arg) != "string"] - -- Force Lua to avoid tail call optimization for debugging purposes - -- TODO: use tail call? - ret = compile_action(@, tree, unpack(args)) - if not ret - @compile_error tree, - "Compile-time action:\n%s\nfailed to produce any Lua" - return ret - - lua = LuaCode.Value(tree.source, "A",string.as_lua_id(stub),"(") - args = {} - for i, tok in ipairs tree - if type(tok) == "string" then continue - arg_lua = @compile(tok) - unless arg_lua.is_value - @compile_error tok, - "Cannot use:\n%s\nas an argument to %s, since it's not an expression, it produces: %s", - stub, repr arg_lua - insert args, arg_lua - lua\concat_append args, ", " - lua\append ")" - return lua - - when "EscapedNomsu" - make_tree = (t)-> - unless AST.is_syntax_tree(t) - return repr(t) - bits = [make_tree(bit) for bit in *t] - return t.type.."("..repr(tostring t.source)..", "..table.concat(bits, ", ")..")" - LuaCode.Value tree.source, make_tree(tree[1]) - - when "Block" - lua = LuaCode(tree.source) - lua\concat_append([@compile(line)\as_statements! for line in *tree], "\n") - return lua - - when "Text" - lua = LuaCode.Value(tree.source) - string_buffer = "" - for i, bit in ipairs tree - if type(bit) == "string" - string_buffer ..= bit - continue - if string_buffer ~= "" - if #lua.bits > 0 then lua\append ".." - lua\append repr(string_buffer) - string_buffer = "" - bit_lua = @compile(bit) - unless bit_lua.is_value - src = ' '..gsub(tostring(@tree_to_nomsu(bit)), '\n','\n ') - line = "#{bit.source.filename}:#{pos_to_line(FILE_CACHE[bit.source.filename], bit.source.start)}" - @compile_error bit, - "Cannot use:\n%s\nas a string interpolation value, since it's not an expression." - if #lua.bits > 0 then lua\append ".." - if bit.type != "Text" - bit_lua = LuaCode.Value(bit.source, "stringify(",bit_lua,")") - lua\append bit_lua - - if string_buffer ~= "" or #lua.bits == 0 - if #lua.bits > 0 then lua\append ".." - lua\append repr(string_buffer) - - if #lua.bits > 1 - lua\parenthesize! - return lua - - when "List" - lua = LuaCode.Value tree.source, "list{" - items = {} - for i, item in ipairs tree - item_lua = @compile(item) - unless item_lua.is_value - @compile_error item, - "Cannot use:\n%s\nas a list item, since it's not an expression." - items[i] = item_lua - lua\concat_append(items, ", ", ",\n ") - lua\append "}" - return lua - - when "Dict" - lua = LuaCode.Value tree.source, "dict{" - lua\concat_append([@compile(e) for e in *tree], ", ", ",\n ") - lua\append "}" - return lua - - when "DictEntry" - key, value = tree[1], tree[2] - key_lua = @compile(key) - unless key_lua.is_value - @compile_error tree[1], - "Cannot use:\n%s\nas a dict key, since it's not an expression." - value_lua = value and @compile(value) or LuaCode.Value(key.source, "true") - unless value_lua.is_value - @compile_error tree[2], - "Cannot use:\n%s\nas a dict value, since it's not an expression." - -- TODO: support arbitrary words here, like operators and unicode - key_str = match(tostring(key_lua), [=[["']([a-zA-Z_][a-zA-Z0-9_]*)['"]]=]) - return if key_str - LuaCode tree.source, key_str,"=",value_lua - elseif sub(tostring(key_lua),1,1) == "[" - -- NOTE: this *must* use a space after the [ to avoid freaking out - -- Lua's parser if the inner expression is a long string. Lua - -- parses x[[[y]]] as x("[y]"), not as x["y"] - LuaCode tree.source, "[ ",key_lua,"]=",value_lua - else - LuaCode tree.source, "[",key_lua,"]=",value_lua - - when "IndexChain" - lua = @compile(tree[1]) - unless lua.is_value - @compile_error tree[1], - "Cannot index:\n%s\nsince it's not an expression." - first_char = sub(tostring(lua),1,1) - if first_char == "{" or first_char == '"' or first_char == "[" - lua\parenthesize! - - for i=2,#tree - key = tree[i] - key_lua = @compile(key) - unless key_lua.is_value - @compile_error key, - "Cannot use:\n%s\nas an index, since it's not an expression." - key_lua_str = tostring(key_lua) - if lua_id = match(key_lua_str, "^['\"]([a-zA-Z_][a-zA-Z0-9_]*)['\"]$") - lua\append ".#{lua_id}" - elseif sub(key_lua_str,1,1) == '[' - -- NOTE: this *must* use a space after the [ to avoid freaking out - -- Lua's parser if the inner expression is a long string. Lua - -- parses x[[[y]]] as x("[y]"), not as x["y"] - lua\append "[ ",key_lua," ]" - else - lua\append "[",key_lua,"]" - return lua - - when "Number" - LuaCode.Value(tree.source, tostring(tree[1])) - - when "Var" - LuaCode.Value(tree.source, string.as_lua_id(tree[1])) - - when "FileChunks" - error("Cannot convert FileChunks to a single block of lua, since each chunk's ".. - "compilation depends on the earlier chunks") - - else - error("Unknown type: #{tree.type}") - - .tree_to_nomsu = (tree, inline=false, can_use_colon=false)=> - switch tree.type - when "FileChunks" - return nil if inline - nomsu = NomsuCode(tree.source) - nomsu\concat_append [@tree_to_nomsu(c) for c in *tree], "\n#{("~")\rep(80)}\n" - return nomsu - - when "Action" - if inline - nomsu = NomsuCode(tree.source) - for i,bit in ipairs tree - if type(bit) == "string" - if i > 1 - nomsu\append " " - nomsu\append bit - else - arg_nomsu = @tree_to_nomsu(bit,true) - return nil unless arg_nomsu - unless i == 1 - nomsu\append " " - if bit.type == "Action" or bit.type == "Block" - arg_nomsu\parenthesize! - nomsu\append arg_nomsu - return nomsu - else - nomsu = NomsuCode(tree.source) - next_space = "" - line_len, last_colon = 0, nil - for i,bit in ipairs tree - if type(bit) == "string" - line_len += #next_space + #bit - nomsu\append next_space, bit - next_space = " " - else - arg_nomsu = if last_colon == i-1 and bit.type == "Action" then nil - elseif bit.type == "Block" then nil - else @tree_to_nomsu(bit,true) - - if arg_nomsu and line_len + #tostring(arg_nomsu) < MAX_LINE - if bit.type == "Action" - if can_use_colon and i > 1 - nomsu\append match(next_space,"[^ ]*"), ": ", arg_nomsu - next_space = "\n.." - line_len = 2 - last_colon = i - else - nomsu\append next_space, "(", arg_nomsu, ")" - line_len += #next_space + 2 + #tostring(arg_nomsu) - next_space = " " - else - nomsu\append next_space, arg_nomsu - line_len += #next_space + #tostring(arg_nomsu) - next_space = " " - else - arg_nomsu = @tree_to_nomsu(bit, nil, true) - return nil unless nomsu - -- These types carry their own indentation - if bit.type != "List" and bit.type != "Dict" and bit.type != "Text" - if i == 1 - arg_nomsu = NomsuCode(bit.source, "(..)\n ", arg_nomsu) - else - arg_nomsu = NomsuCode(bit.source, "\n ", arg_nomsu) - - if last_colon == i-1 and (bit.type == "Action" or bit.type == "Block") - next_space = "" - nomsu\append next_space, arg_nomsu - next_space = "\n.." - line_len = 2 - - if next_space == " " and #(match(tostring(nomsu),"[^\n]*$")) > MAX_LINE - next_space = "\n.." - return nomsu - - when "EscapedNomsu" - nomsu = @tree_to_nomsu(tree[1], true) - if nomsu == nil and not inline - nomsu = @tree_to_nomsu(tree[1]) - return nomsu and NomsuCode tree.source, "\\:\n ", nomsu - return nomsu and NomsuCode tree.source, "\\(", nomsu, ")" - - when "Block" - if inline - nomsu = NomsuCode(tree.source) - for i,line in ipairs tree - if i > 1 - nomsu\append "; " - line_nomsu = @tree_to_nomsu(line,true) - return nil unless line_nomsu - nomsu\append line_nomsu - return nomsu - nomsu = NomsuCode(tree.source) - for i, line in ipairs tree - line = assert(@tree_to_nomsu(line, nil, true), "Could not convert line to nomsu") - nomsu\append line - if i < #tree - nomsu\append "\n" - if match(tostring(line), "\n") - nomsu\append "\n" - return nomsu - - when "Text" - if inline - nomsu = NomsuCode(tree.source, '"') - for bit in *tree - if type(bit) == 'string' - -- TODO: unescape better? - nomsu\append (gsub(gsub(gsub(bit,"\\","\\\\"),"\n","\\n"),'"','\\"')) - else - interp_nomsu = @tree_to_nomsu(bit, true) - if interp_nomsu - if bit.type != "Var" and bit.type != "List" and bit.type != "Dict" and bit.type != "Text" - interp_nomsu\parenthesize! - nomsu\append "\\", interp_nomsu - else return nil - nomsu\append '"' - return nomsu - else - inline_version = @tree_to_nomsu(tree, true) - if inline_version and #inline_version <= MAX_LINE - return inline_version - nomsu = NomsuCode(tree.source, '".."\n ') - for i, bit in ipairs tree - if type(bit) == 'string' - bit_lines = get_lines\match(bit) - for j, line in ipairs bit_lines - if j > 1 then nomsu\append "\n " - if #line > 1.25*MAX_LINE - remainder = line - while #remainder > 0 - split = find(remainder, " ", MAX_LINE, true) - if split - chunk, remainder = sub(remainder, 1, split), sub(remainder, split+1, -1) - nomsu\append chunk - elseif #remainder > 1.75*MAX_LINE - split = math.floor(1.5*MAX_LINE) - chunk, remainder = sub(remainder, 1, split), sub(remainder, split+1, -1) - nomsu\append chunk - else - nomsu\append remainder - break - if #remainder > 0 then nomsu\append "\\\n .." - else - nomsu\append line - else - interp_nomsu = @tree_to_nomsu(bit, true) - if interp_nomsu - if bit.type != "Var" and bit.type != "List" and bit.type != "Dict" and bit.type != "Text" - interp_nomsu\parenthesize! - nomsu\append "\\", interp_nomsu - else - interp_nomsu = assert(@tree_to_nomsu(bit)) - return nil unless interp_nomsu - nomsu\append "\\\n ", interp_nomsu - if i < #tree - nomsu\append "\n .." - return nomsu - - when "List" - if inline - nomsu = NomsuCode(tree.source, "[") - for i, item in ipairs tree - item_nomsu = @tree_to_nomsu(item, true) - return nil unless item_nomsu - if i > 1 - nomsu\append ", " - nomsu\append item_nomsu - nomsu\append "]" - return nomsu - else - inline_version = @tree_to_nomsu(tree, true) - if inline_version and #inline_version <= MAX_LINE - return inline_version - nomsu = NomsuCode(tree.source, "[..]") - line = NomsuCode(tree.source, "\n ") - for item in *tree - item_nomsu = @tree_to_nomsu(item, true) - if item_nomsu and #line + #", " + #item_nomsu <= MAX_LINE - if #line.bits > 1 - line\append ", " - line\append item_nomsu - else - unless item_nomsu - item_nomsu = @tree_to_nomsu(item) - return nil unless item_nomsu - if #line.bits > 1 - nomsu\append line - line = NomsuCode(line.source, "\n ") - line\append item_nomsu - if #line.bits > 1 - nomsu\append line - return nomsu - - when "Dict" - if inline - nomsu = NomsuCode(tree.source, "{") - for i, entry in ipairs tree - entry_nomsu = @tree_to_nomsu(entry, true) - return nil unless entry_nomsu - if i > 1 - nomsu\append ", " - nomsu\append entry_nomsu - nomsu\append "}" - return nomsu - else - inline_version = @tree_to_nomsu(tree, true) - if inline_version then return inline_version - nomsu = NomsuCode(tree.source, "{..}") - line = NomsuCode(tree.source, "\n ") - for entry in *tree - entry_nomsu = @tree_to_nomsu(entry) - return nil unless entry_nomsu - if #line + #tostring(entry_nomsu) <= MAX_LINE - if #line.bits > 1 - line\append ", " - line\append entry_nomsu - else - if #line.bits > 1 - nomsu\append line - line = NomsuCode(line.source, "\n ") - line\append entry_nomsu - if #line.bits > 1 - nomsu\append line - return nomsu - - when "DictEntry" - key, value = tree[1], tree[2] - key_nomsu = @tree_to_nomsu(key, true) - return nil unless key_nomsu - if key.type == "Action" or key.type == "Block" - key_nomsu\parenthesize! - value_nomsu = if value - @tree_to_nomsu(value, true) - else NomsuCode(tree.source, "") - if inline and not value_nomsu then return nil - if not value_nomsu - return nil if inline - value_nomsu = @tree_to_nomsu(value) - return nil unless value_nomsu - return NomsuCode tree.source, key_nomsu, ":", value_nomsu - - when "IndexChain" - nomsu = NomsuCode(tree.source) - for i, bit in ipairs tree - if i > 1 - nomsu\append "." - local bit_nomsu - if bit.type == "Text" and #bit == 1 and type(bit[1]) == 'string' - -- TODO: support arbitrary words here, including operators and unicode - if bit[1]\match("[_a-zA-Z][_a-zA-Z0-9]*") - bit_nomsu = bit[1] - unless bit_nomsu then bit_nomsu = @tree_to_nomsu(bit, true) - return nil unless bit_nomsu - switch bit.type - when "Action", "Block", "IndexChain" - bit_nomsu\parenthesize! - when "Number" - if i < #tree - bit_nomsu\parenthesize! - nomsu\append bit_nomsu - return nomsu - - when "Number" - return NomsuCode(tree.source, tostring(tree[1])) - - when "Var" - return NomsuCode(tree.source, "%", tree[1]) - - else - error("Unknown type: #{tree.type}") - - --- Command line interface: --- Only run this code if this file was run directly with command line arguments, and not require()'d: -if arg and debug.getinfo(2).func != require - parser = re.compile([[ - args <- {| (flag ";")* {:inputs: {| ({file} ";")* |} :} {:nomsu_args: {| ("--;" ({[^;]*} ";")*)? |} :} ";"? |} !. - flag <- - {:interactive: ("-i" -> true) :} - / {:optimized: ("-O" -> true) :} - / {:format: ("-f" -> true) :} - / {:syntax: ("-s" -> true) :} - / {:print_file: "-p" ";" {file} :} - / {:compile: ("-c" -> true) :} - / {:verbose: ("-v" -> true) :} - / {:help: (("-h" / "--help") -> true) :} - file <- "-" / [^;]+ - ]], {true: -> true}) - args = concat(arg, ";")..";" - args = parser\match(args) - if not args or args.help - print [=[ +-- This file contains the command-line Nomsu runner. +usage = [=[ Nomsu Compiler Usage: (lua nomsu.lua | moon nomsu.moon) [-i] [-O] [-v] [-c] [-f] [-s] [--help] [-p print_file] file1 file2... [-- nomsu args...] @@ -831,121 +16,145 @@ OPTIONS -p Print to the specified file instead of stdout. Input file can be "-" to use stdin. ]=] - os.exit! - - nomsu = NomsuCompiler - nomsu.arg = args.nomsu_args - - run = -> - - for i,input in ipairs args.inputs - if input == "-" then args.inputs[i] = STDIN - if #args.inputs == 0 and not args.interactive - args.inputs = {"core"} - args.interactive = true - - print_file = if args.print_file == "-" then io.stdout - elseif args.print_file then io.open(args.print_file, 'w') - else io.stdout +lpeg = require 'lpeg' +re = require 're' +run_safely = require "error_handling" +NomsuCompiler = require "nomsu_compiler" +{:NomsuCode, :LuaCode, :Source} = require "code_obj" +STDIN, STDOUT, STDERR = "/dev/fd/0", "/dev/fd/1", "/dev/fd/2" - nomsu.skip_precompiled = not args.optimized - if print_file == nil - nomsu.print = -> - elseif print_file != io.stdout - nomsu.print = (...)-> - N = select("#",...) - if N > 0 - print_file\write(tostring(select(1,...))) - for i=2,N - print_file\write('\t',tostring(select(1,...))) - print_file\write('\n') +-- If this file was reached via require(), then just return the Nomsu compiler +if not arg or debug.getinfo(2).func == require + return NomsuCompiler + +parser = re.compile([[ + args <- {| (flag ";")* {:inputs: {| ({file} ";")* |} :} {:nomsu_args: {| ("--;" ({[^;]*} ";")*)? |} :} ";"? |} !. + flag <- + {:interactive: ("-i" -> true) :} + / {:optimized: ("-O" -> true) :} + / {:format: ("-f" -> true) :} + / {:syntax: ("-s" -> true) :} + / {:print_file: "-p" ";" {file} :} + / {:compile: ("-c" -> true) :} + / {:verbose: ("-v" -> true) :} + / {:help: (("-h" / "--help") -> true) :} + file <- "-" / [^;]+ +]], {true: -> true}) +args = table.concat(arg, ";")..";" +args = parser\match(args) +if not args or args.help + print usage + os.exit! + +nomsu = NomsuCompiler +nomsu.arg = args.nomsu_args + +run = -> + for i,input in ipairs args.inputs + if input == "-" then args.inputs[i] = STDIN + + if #args.inputs == 0 and not args.interactive + args.inputs = {"core"} + args.interactive = true + + print_file = if args.print_file == "-" then io.stdout + elseif args.print_file then io.open(args.print_file, 'w') + else io.stdout + + nomsu.skip_precompiled = not args.optimized + if print_file == nil + nomsu.print = -> + elseif print_file != io.stdout + nomsu.print = (...)-> + N = select("#",...) + if N > 0 + print_file\write(tostring(select(1,...))) + for i=2,N + print_file\write('\t',tostring(select(1,...))) + print_file\write('\n') + print_file\flush! + + input_files = {} + to_run = {} + for input in *args.inputs + for f in all_files(input) + input_files[#input_files+1] = f + to_run[f] = true + + if args.compile or args.verbose + nomsu.on_compile = (code, from_file)-> + return unless to_run[from_file] + if args.verbose + io.write(tostring(code), "\n") + if args.compile and from_file\match("%.nom$") + output_filename = from_file\gsub("%.nom$", ".lua") + output_file = io.open(output_filename, 'w') + output_file\write(tostring(code)) + output_file\flush! + print ("Compiled %-25s -> %s")\format(from_file, output_filename) + output_file\close! + + parse_errs = {} + for filename in *input_files + if args.syntax + -- Check syntax: + file_contents = io.open(filename)\read('*a') + ok,err = pcall nomsu.parse, nomsu, file_contents, Source(filename, 1, #file_contents) + if not ok + table.insert parse_errs, err + elseif print_file + print_file\write("Parse succeeded: #{filename}\n") print_file\flush! - - input_files = {} - to_run = {} - for input in *args.inputs - for f in all_files(input) - input_files[#input_files+1] = f - to_run[f] = true - - nomsu.on_compile = if args.compile or args.verbose - (code, from_file)-> - if to_run[from_file] - if args.verbose - io.write(tostring(code), "\n") - if args.compile and from_file\match("%.nom$") - output_filename = from_file\gsub("%.nom$", ".lua") - output_file = io.open(output_filename, 'w') - output_file\write(tostring(code)) - output_file\flush! - print ("Compiled %-25s -> %s")\format(from_file, output_filename) - output_file\close! - else nil - - parse_errs = {} - for filename in *input_files - if args.syntax - -- Check syntax: - file_contents = io.open(filename)\read('*a') - ok,err = pcall nomsu.parse, nomsu, file_contents, Source(filename, 1, #file_contents) - if not ok - insert parse_errs, err - elseif print_file - print_file\write("Parse succeeded: #{filename}\n") - print_file\flush! - elseif args.format - -- Auto-format - file = FILE_CACHE[filename] - if not file - error("File does not exist: #{filename}", 0) - tree = nomsu\parse(file, Source(filename,1,#file)) - formatted = tostring(nomsu\tree_to_nomsu(tree)) - if print_file - print_file\write(formatted, "\n") - print_file\flush! - elseif filename == STDIN - file = io.input!\read("*a") - FILE_CACHE.stdin = file - nomsu\run(file, Source('stdin',1,#file)) - else - nomsu\run_file(filename) - - if #parse_errs > 0 - io.stderr\write concat(parse_errs, "\n\n") - io.stderr\flush! - os.exit(false, true) - elseif args.syntax - os.exit(true, true) - - if args.interactive - -- REPL - for repl_line=1,math.huge - io.write(colored.bright colored.yellow ">> ") - buff = {} - while true - line = io.read("*L") - if line == "\n" or not line - if #buff > 0 - io.write("\027[1A\027[2K") - break -- Run buffer - line = line\gsub("\t", " ") - insert buff, line - io.write(colored.dim colored.yellow ".. ") - if #buff == 0 - break -- Exit - - buff = concat(buff) - FILE_CACHE["REPL#"..repl_line] = buff - err_hand = (error_message)-> - print_err_msg error_message - ok, ret = xpcall(nomsu.run, err_hand, nomsu, buff, Source("REPL#"..repl_line, 1, #buff)) - if ok and ret != nil - print "= "..repr(ret) - elseif not ok - print_err_msg ret - - run_safely = require "error_handling" - run_safely(run) - -return NomsuCompiler + elseif args.format + -- Auto-format + file = FILE_CACHE[filename] + if not file + error("File does not exist: #{filename}", 0) + tree = nomsu\parse(file, Source(filename,1,#file)) + formatted = tostring(nomsu\tree_to_nomsu(tree)) + if print_file + print_file\write(formatted, "\n") + print_file\flush! + elseif filename == STDIN + file = io.input!\read("*a") + FILE_CACHE.stdin = file + nomsu\run(file, Source('stdin',1,#file)) + else + nomsu\run_file(filename) + + if #parse_errs > 0 + io.stderr\write table.concat(parse_errs, "\n\n") + io.stderr\flush! + os.exit(false, true) + elseif args.syntax + os.exit(true, true) + + if args.interactive + -- REPL + for repl_line=1,math.huge + io.write(colored.bright colored.yellow ">> ") + buff = {} + while true + line = io.read("*L") + if line == "\n" or not line + if #buff > 0 + io.write("\027[1A\027[2K") + break -- Run buffer + line = line\gsub("\t", " ") + table.insert buff, line + io.write(colored.dim colored.yellow ".. ") + if #buff == 0 + break -- Exit + + buff = table.concat(buff) + FILE_CACHE["REPL#"..repl_line] = buff + err_hand = (error_message)-> + print_err_msg error_message + ok, ret = xpcall(nomsu.run, err_hand, nomsu, buff, Source("REPL#"..repl_line, 1, #buff)) + if ok and ret != nil + print "= "..repr(ret) + elseif not ok + print_err_msg ret + +run_safely(run) -- cgit v1.2.3