diff options
| author | Bruce Hill <bitbucket@bruce-hill.com> | 2018-06-19 01:27:32 -0700 |
|---|---|---|
| committer | Bruce Hill <bitbucket@bruce-hill.com> | 2018-06-19 01:27:41 -0700 |
| commit | 59c79bdf57ce31d5f2fe15dbbdcc6ff345adc651 (patch) | |
| tree | c242e811d9bb322bef9e006a7ad17a6a33a3e4e9 /nomsu_compiler.moon | |
| parent | 1c8c84f8d2bc27a49de2b9ffca7e08448cc9406d (diff) | |
Split up CLI and NomsuCompiler
Diffstat (limited to 'nomsu_compiler.moon')
| -rw-r--r-- | nomsu_compiler.moon | 797 |
1 files changed, 797 insertions, 0 deletions
diff --git a/nomsu_compiler.moon b/nomsu_compiler.moon new file mode 100644 index 0000000..8226db4 --- /dev/null +++ b/nomsu_compiler.moon @@ -0,0 +1,797 @@ +-- 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") +-- 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 +} + +export all_files +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}") + +return NomsuCompiler |
