diff options
| author | Bruce Hill <bitbucket@bruce-hill.com> | 2017-08-22 01:02:41 -0700 |
|---|---|---|
| committer | Bruce Hill <bitbucket@bruce-hill.com> | 2017-08-22 01:02:41 -0700 |
| commit | 0e916161b1b47758e0bb15051028d8c92c6446e7 (patch) | |
| tree | 4d60014362c1101efe99224416e40ca43f5de655 /nomic.moon | |
| parent | 4713d7db0dcb713b4a36e63e5a5322829fca7c97 (diff) | |
Updated core with new syntax.
Diffstat (limited to 'nomic.moon')
| -rw-r--r-- | nomic.moon | 666 |
1 files changed, 419 insertions, 247 deletions
@@ -2,268 +2,440 @@ re = require 're' lpeg = require 'lpeg' utils = require 'utils' moon = require 'moon' -type = moon.type - -currently_parsing = nil -macros = nil -indentation = 0 -indents = -> - (" ")\rep(indentation) -indent = -> - export indentation - indentation += 1 -dedent = -> - export indentation - indentation -= 1 -indent_block = (block)-> - block = block\gsub("\n", "\n"..indents!) - return indents!..block -add_line = (lines, new_line)-> - table.insert lines, (indents!..new_line) - -compactify_invocation = (raw_invocation)-> - name_bits = {} - arg_names = {} - for chunk in raw_invocation\gmatch("%S+") - if chunk\sub(1,1) == "$" - table.insert name_bits, "$" - table.insert arg_names, chunk\sub(2,-1) - else - table.insert name_bits, chunk - invocation = table.concat name_bits, " " - return invocation, arg_names - -Number = (s)-> s - -String = (s)-> '"'..s..'"' - -List = (t)-> "{" .. table.concat(t, ", ") .. "}" - -Var = (s)-> "locals[\"#{s}\"]" - -Word = (s)-> setmetatable({type:'word', text:s}, {__tostring:=> error("Cannot convert word \"#{@text}\" to string")}) - -Conditional = (condition, if_block, else_block)-> - ret = {} - add_line ret, "(function()" - indent! - add_line ret, "local ret" - table.insert ret, indent_block("local condition = #{condition}") - - add_line ret, "if condition then" - indent! - table.insert ret, indent_block("ret = (#{if_block})(game, locals)") - if else_block - dedent! - add_line ret, "else" - indent! - table.insert ret, indent_block("ret = (#{else_block})(game, locals)") - dedent! - add_line ret, "end" - add_line ret, "return ret" - dedent! - add_line ret, "end)()" - code = table.concat(ret, "\n") - return code - -FunctionCall = (tokens)-> - words = [(if t.type == 'word' then t.text else "$") for t in *tokens] - args = [t for t in *tokens when t.type != 'word'] - rule_name = table.concat(words, " ") - if rule_name == "$" - error("Empty rule: #{utils.repr(tokens)}") - - if macros[rule_name] - return macros[rule_name](unpack(args)) - - if #args == 0 - return indent_block("game:call(\"#{rule_name}\")") - - ret = {} - add_line ret, "game:call(\"#{rule_name}\"," - indent! - arg_strs = [indent_block(arg) for arg in *args] - dedent! - table.insert ret, table.concat(arg_strs, ",\n") - add_line ret, ")" - - code = table.concat(ret, "\n") - return code - -Thunk = (statements)-> - ret = {} - add_line ret, "function(game, locals)" - indent! - for i,statement in ipairs statements - -- TODO: clean up? This is a bit hacky. I should *know* if this is a var assignment. - if statement\match "locals%[\".*\"%] = .*" - table.insert ret, indent_block(statement) - elseif i == #statements - table.insert ret, indent_block("return "..statement..";") - else - table.insert ret, indent_block(statement..";") - dedent! - add_line ret, "end" - return table.concat(ret, "\n") - -lingo = [[ - actions <- {| {:startPos: {}:} (%ws*) (%nl %ws*)* (action ((%nl %ws*)+ action)*)? (%nl %ws*)* {:endPos: {}:} |} -> Thunk - action <- (&conditional conditional) / ({| token (%ws+ token)* %ws* |} -> FunctionCall) - conditional <- ("if" !%wordchars %ws* expression %ws* thunk (%ws* "else" %ws* thunk %ws*)?) -> Conditional - token <- expression / (!"$" {%wordchars+} -> Word) - expression <- number / string / list / variable / thunk / subexpression - number <- ('-'? [0-9]+ ("." [0-9]+)?) -> Number - string <- ('"' {(("\\" .) / [^"])*} '"') -> String - list <- ({| '[' %ws* (%nl %ws*)* (expression (',' %ws* (%nl %ws*)* expression)*)? %ws* (%nl %ws*)* ']' |}) -> List - variable <- ("$" {%wordchars+}) -> Var - subexpression <- ("(" %ws* action ")") - thunk <- ("{" actions "}") - keywords <- "if" / "else" / "let" -]] -defs = { - :Var - :Word - :String - :Number - :List - :FunctionCall - :Thunk - :Conditional - ws: lpeg.S(" \t") - wordchars: lpeg.P(1)-lpeg.S(' \t\n,{}[]()"') -} -lingo = re.compile lingo, defs -cross_compile = (nomic_code)-> - export currently_parsing - old_parsing = currently_parsing - currently_parsing = nomic_code - lua_code = lingo\match nomic_code - currently_parsing = old_parsing - return lua_code +lpeg.setmaxstack 10000 -- whoa -defaulttable = (table,key)-> - new = {} - table[key] = new - return new +linebreak = lpeg.P("\r")^-1 * lpeg.P("\n") -class Game - new:(@parent)=> - @rules = setmetatable({}, {__index:parent and parent.rules or nil}) - @macros = setmetatable({}, {__index:parent and parent.macros or nil}) - @arg_names = setmetatable({}, {__index:parent and parent.arg_names or nil}) - @relations = setmetatable({}, {__index:parent and parent.relations or defaulttable}) - @invocations = setmetatable({}, {__index:parent and parent.invocations or nil}) - @authorized = setmetatable({}, {__index:parent and parent.authorized or nil}) - @callstack = {} - @debug = false - @you = "Anonymous" - - def: (invocations, fn)=> - if not fn then fn = false - invocations = if type(invocations) == 'table' then invocations else {invocations} - if fn then @invocations[fn] = {} - else @invocations[fn] = false - for raw_invocation in *invocations - invocation, arg_names = compactify_invocation raw_invocation - if invocation == "$" - error("Anonymous function: #{raw_invocation}") - @rules[invocation] = fn - if fn - table.insert @invocations[fn], invocation - @arg_names[invocation] = arg_names +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] + +pos_to_line = (str,pos)-> + line_no = 1 + for line in str\gmatch("[^%n]+") + if #line >= pos then return line, line_no + pos -= (#line + 1) + line_no += 1 + error "Failed to find position #{pos} in str" + +pos_to_line_no = (str,pos)-> + with line = 1 + for _ in str\sub(1, pos)\gmatch("\n") + line += 1 + +add_indent_tokens = (str)-> + indent_stack = {0} + result = {} + -- TODO: Store mapping from new line numbers to old ones + defs = + linebreak: linebreak + process_line: (line)-> + -- Remove blank lines + unless line\match"[^ \t\n]" + return + indent = get_line_indentation(line) + if indent > indent_stack[#indent_stack] + table.insert result, "{\n " + table.insert indent_stack, indent + elseif indent < indent_stack[#indent_stack] + dedents = 0 + tokens = {} + while indent < indent_stack[#indent_stack] + table.remove indent_stack + table.insert tokens, "}" + table.insert tokens, " " + table.insert result, table.concat(tokens, "\n") else - @arg_names[invocation] = false - - macro: (invocation, fn)=> - invocation, _ = compactify_invocation invocation - @macros[invocation] = fn - - undefine: (invocations)=> - @\def invocations, false - - all_aliases: (invocations)=> - if type(invocations) != 'table' then invocations = {invocations} - all_aliases = {} - for i in *invocations - all_aliases[i] = true - if not @invocations[@rules[i]] - error "Could not find aliases of [[#{i}]]" - for alias in *@invocations[@rules[i]] - all_aliases[alias] = true - return [a for a in pairs(all_aliases)] - - canonicalize: (invocations)=> - if type(invocations) == 'string' - return @invocations[@rules[invocations]][1] - canonicals = {} - for i in *invocations - if @rules[i] == nil - error "Attempt to canonicalize invalid invocation: #{i}" - canonicals[@invocations[@rules[i]][1]] = true - return [c for c in pairs canonicals] - - set_whitelist: (actions, whitelist)=> - if utils.is_list whitelist then whitelist = {w,true for w in *whitelist} - for action in *@all_aliases(actions) - @authorized[action] = whitelist + table.insert result, " " + -- Delete leading whitespace + --line = line\gsub("[ \t]*", "", 1) + -- Delete trailing whitespace and carriage returns + line = line\gsub("[ \t\r]*\n", "\n", 1) + table.insert result, line + + indentflagger = [=[ + file <- line* + line <- ((string / [^%linebreak])* %linebreak) -> process_line + string <- '"' (("\\" .) / [^"])* '"' + ]=] + indentflagger = re.compile indentflagger, defs + indentflagger\match(str) + while #indent_stack > 1 + table.remove indent_stack + table.insert result, "}\n" + return (table.concat result)\sub(1,-2) + +lingo = [=[ + file <- ({} {| {:body: (" " block) :} ({:errors: errors :})? |} {}) -> File + errors <- ({} {.+} {}) -> Errors + block <- ({} {| statement (%nodent statement)* |} {}) -> Block + statement <- ({} (functioncall / expression) {}) -> Statement + one_liner <- ({} {| + (({} + (({} {| + (expression (%word_boundary fn_bit)+) / (word (%word_boundary fn_bit)*) + |} {}) -> FunctionCall) + {}) -> Statement) + |} {}) -> Block + + functioncall <- ({} {| (expression %word_boundary fn_bits) / (word (%word_boundary fn_bits)?) |} {}) -> FunctionCall + fn_bit <- (expression / word) + fn_bits <- + ((".." %ws? (%indent %nodent indented_fn_bits %dedent) (%nodent ".." %ws? fn_bits)?) + / (%nodent ".." fn_bit fn_bits) + / (fn_bit (%word_boundary fn_bits)?)) + indented_fn_bits <- + fn_bit ((%nodent / %word_boundary) indented_fn_bits)? - check_authorization:(action)=> - authority = @authorized[action] - return true if authority == nil - for call in *@callstack - if authority[call] then return true - return false + thunk <- + ({} ":" %ws? + ((%indent %nodent block %dedent (%nodent "..")?) + / (one_liner (%ws? ((%nodent? "..")))?)) {}) -> Thunk + + word <- ({} !number {%wordchar+} {}) -> Word + expression <- ({} (string / number / variable / list / thunk / subexpression) {}) -> Expression - run: (str, user)=> - user or= "anon" + string <- ({} '"' {(("\\" .) / [^"])*} '"' {}) -> String + number <- ({} {'-'? [0-9]+ ("." [0-9]+)?} {}) -> Number + variable <- ({} ("%" {%wordchar+}) {}) -> Var + subexpression <- + ("(" %ws? (functioncall / expression) %ws? ")") + / ("(..)" %ws? %indent %nodent (expression / (({} {| indented_fn_bits |} {}) -> FunctionCall)) %dedent (%nodent "..")?) + + list <- ({} {| + ("[..]" %ws? %indent %nodent indented_list ","? %dedent (%nodent "..")?) + / ("[" %ws? (list_items ","?)? %ws?"]") + |} {}) -> List + list_items <- (expression (list_sep list_items)?) + list_sep <- %ws? "," %ws? + indented_list <- + expression (((list_sep %nodent?) / %nodent) indented_list)? +]=] + +defs = + eol: #(linebreak) + (lpeg.P("")-lpeg.P(1)) + ws: lpeg.S(" \t")^1 + wordchar: lpeg.P(1)-lpeg.S(' \t\n\r%:;,.{}[]()"') + indent: linebreak * lpeg.P("{") * lpeg.S(" \t")^0 + nodent: linebreak * lpeg.P(" ") * lpeg.S(" \t")^0 + dedent: linebreak * lpeg.P("}") * lpeg.S(" \t")^0 + word_boundary: lpeg.S(" \t")^1 + lpeg.B(lpeg.P("..")) + lpeg.B(lpeg.S("\";)]")) + #lpeg.S("\":([") + #lpeg.P("..") + +setmetatable(defs, { + __index: (t,key)-> + --print("WORKING for #{key}") + fn = (start, value, stop, ...)-> + token = {type: key, range:{start,stop}, value: value} + return token + t[key] = fn + return fn +}) +lingo = re.compile lingo, defs + + +class Game + new:=> + @defs = {} + @macros = {} + @debug = false + + call: (fn_name,...)=> + if @defs[fn_name] == nil + error "Attempt to call undefined function: #{fn_name}" + {fn, arg_names} = @defs[fn_name] if @debug - print("SOURCE NOMIC CODE:\n#{str}") - export macros - old_macros = macros - macros = (self.macros) - lua_code = cross_compile str - macros = old_macros + print("Calling #{fn_name}...") + args = {} + for i,name in ipairs(arg_names) + args[name] = select(i,...) + if @debug + print("arg #{utils.repr(name,true)} = #{select(i,...)}") + ret = fn(self, args) if @debug - print("\nGENERATED LUA CODE:\n#{lua_code}") + print "returned #{utils.repr(ret,true)}" + return ret + + def: (spec, fn)=> + invocations,arg_names = self\get_invocations spec + for invocation in *invocations + @defs[invocation] = {fn, arg_names} - lua_thunk, err = loadstring("return "..lua_code) + get_invocations:(text)=> + if type(text) == 'string' then text = {text} + invocations = {} + local arg_names + for _text in *text + name_bits = {} + _arg_names = {} + for chunk in _text\gmatch("%S+") + if chunk\sub(1,1) == "%" + table.insert name_bits, "%" + table.insert _arg_names, chunk\sub(2,-1) + else + table.insert name_bits, chunk + invocation = table.concat name_bits, " " + table.insert(invocations, invocation) + if arg_names and 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)}") + arg_names = _arg_names + return invocations, arg_names + + defmacro: (spec, fn)=> + invocations,arg_names = self\get_invocations spec + for invocation in *invocations + @macros[invocation] = {fn, arg_names} + + run: (text)=> + if @debug + print("RUNNING TEXT:\n") + print(text) + code = self\compile(text) + if @debug + print("\nGENERATED LUA CODE:") + print(code) + lua_thunk, err = loadstring(code) if not lua_thunk - print("Parsing: "..lua_code) - error(err) - lua_fn = lua_thunk! - - ret = lua_fn @, {} + error("Failed to compile generated code:\n#{code}") + action = lua_thunk! + if @debug + print("Running...") + return action(self, {}) + + run_debug:(text)=> + old_debug = @debug + @debug = true + ret = self\run(text) + @debug = old_debug return ret - call: (invocation, ...)=> - if not @rules[invocation] - error "Could not find rule: '#{invocation}'" - if not @\check_authorization invocation - print "Not authorized to #{invocation} from callstack: #{utils.repr(@callstack)}" - return - table.insert @callstack, invocation - arg_names = @arg_names[invocation] - args = {...} - ret = (@rules[invocation])(@, {arg_names[i],arg for i, arg in ipairs(args)}) - table.remove @callstack + parse: (str)=> + if @debug + print("PARSING:\n#{str}") + indentified = add_indent_tokens str + if @debug + print("\nINDENTIFIED:\n#{indentified}") + tree = lingo\match indentified + if @debug + print("\nPARSE TREE:") + self\print_tree(tree) + assert tree, "Failed to parse: #{str}" + return tree + + transform: (tree, indent_level=0, parent=nil)=> + indented = (fn)-> + export indent_level + indent_level += 1 + fn! + indent_level -= 1 + transform = (t,parent)-> self\transform(t, indent_level, parent or tree) + ind = (line) -> (" ")\rep(indent_level)..line + ded = (lines)-> + if not lines.match then error("WTF: #{utils.repr(lines)}") + lines\match"^%s*(.*)" + + ret_lines = {} + lua = (line, skip_indent=false)-> + unless skip_indent + line = ind(ded(line)) + table.insert ret_lines, line + return line + + comma_separated_items = (open, items, close)-> + buffer = open + so_far = indent_level*2 + indented -> + export buffer,so_far + for i,item in ipairs(items) + if i < #items then item ..= ", " + if so_far + #item >= 80 and #buffer > 0 + lua buffer + so_far -= #buffer + buffer = item + else + so_far += #item + buffer ..= item + buffer ..= close + lua buffer + + switch tree.type + when "File" + if tree.value.errors and #tree.value.errors.value > 0 + ret = transform(tree.value.errors) + return ret + + lua "return (function(game, vars)" + indented -> + lua "local ret" + lua transform(tree.value.body) + lua "return ret" + lua "end)" + + when "Errors" + -- TODO: Better error reporting via tree.range[1] + error("\nParse error on: #{tree.value}") + + when "Block" + for chunk in *tree.value + lua transform(chunk) + + when "Thunk" + if not tree.value + error("Thunk without value: #{utils.repr(tree)}") + lua "(function(game,vars)" + indented -> + lua "local ret" + assert tree.value.type == "Block", "Non-block value in Thunk" + lua transform(tree.value) + lua "return ret" + lua "end)" + + when "Statement" + ret = transform(tree.value) + return ret + + when "Expression" + ret = transform(tree.value) + if parent.type == "Statement" + ret = "ret = "..ded(ret) + return ret + + when "FunctionCall" + name_bits = {} + for token in *tree.value + table.insert name_bits, if token.type == "Word" then token.value else "%" + name = table.concat(name_bits, " ") + if @macros[name] + {fn, arg_names} = @macros[name] + helpers = {:indented, :transform, :ind, :ded, :lua, :comma_separated_items} + args = [a for a in *tree.value when a.type != "Word"] + args = {name,args[i] for i,name in ipairs(arg_names)} + helpers.var = (varname)-> + ded(transform(args[varname])) + m = fn(self, args, helpers, parent.type) + if m != nil then return m + else + if parent.type == "Statement" + lua "ret =" + args = [ded(transform(a)) for a in *tree.value when a.type != "Word"] + table.insert args, 1, utils.repr(name, true) + comma_separated_items("game:call(", args, ")") + + when "String" + lua utils.repr(tree.value, true) + + when "Number" + lua tree.value + + when "List" + if #tree.value == 0 + lua "{}" + elseif #tree.value == 1 + lua "{#{transform(tree.value)}}" + else + comma_separated_items("{", [ded(transform(item)) for item in *tree.value], "}") + + when "Var" + lua "vars[#{utils.repr(tree.value,true)}]" + + else + error("Unknown/unimplemented thingy: #{tree.type}") + + ret = table.concat ret_lines, "\n" return ret - run_debug:(...)=> - @debug = true - print("Debugging:") - @run ... - @debug = false + _yield_tree: (tree, indent_level=0)=> + ind = (s) -> (" ")\rep(indent_level)..s + switch tree.type + when "File" + coroutine.yield(ind"File:") + self\_yield_tree(tree.value.body, indent_level+1) + + when "Errors" + coroutine.yield(ind"Error:\n#{tree.value}") + + when "Block" + for chunk in *tree.value + self\_yield_tree(chunk, indent_level) + + when "Thunk" + coroutine.yield(ind"Thunk:") + self\_yield_tree(tree.value, indent_level+1) + + when "Statement" + self\_yield_tree(tree.value, indent_level) + + when "Expression" + self\_yield_tree(tree.value, indent_level) + + when "FunctionCall" + name_bits = {} + for token in *tree.value + table.insert name_bits, if token.type == "Word" then token.value else "%" + name = table.concat(name_bits, " ") + if #[a for a in *tree.value when a.type != "Word"] == 0 + coroutine.yield(ind"Call [#{name}]!") + else + coroutine.yield(ind"Call [#{name}]:") + for a in *tree.value + if a.type != "Word" + self\_yield_tree(a, indent_level+1) + + when "String" + 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 + self\_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(-> self\_yield_tree(tree)) + print(line) + + stringify_tree:(tree)=> + result = {} + for line in coroutine.wrap(-> self\_yield_tree(tree)) + table.insert(result, line) + return table.concat result, "\n" + + compile: (src)=> + tree = self\parse(src) + code = self\transform(tree,0) + return code + + test: (src, expected)=> + if expected == nil + start,stop = src\find"===" + if not start or not stop then + error("WHERE'S THE ===? in:\n#{src}") + src, expected = src\sub(1,start-1), src\sub(stop+1,-1) + expected = expected\match'[\n]*(.*[^\n])' + if not expected then error("WTF???") + tree = self\parse(src) + got = if tree.value.errors and #tree.value.errors.value > 0 + self\stringify_tree(tree.value.errors) + else + self\stringify_tree(tree.value.body) + if got != expected + error"TEST FAILED!\nSource:\n#{src}\nExpected:\n#{expected}\n\nGot:\n#{got}" - repl:=> - while true - io.write(">> ") - buf = "" - while buf\sub(-2,-1) != "\n\n" - buf ..= io.read("*line").."\n" - if buf == "exit\n\n" or buf == "quit\n\n" - break - @\run buf return Game |
