aboutsummaryrefslogtreecommitdiff
path: root/nomic.moon
diff options
context:
space:
mode:
authorBruce Hill <bitbucket@bruce-hill.com>2017-08-22 01:02:41 -0700
committerBruce Hill <bitbucket@bruce-hill.com>2017-08-22 01:02:41 -0700
commit0e916161b1b47758e0bb15051028d8c92c6446e7 (patch)
tree4d60014362c1101efe99224416e40ca43f5de655 /nomic.moon
parent4713d7db0dcb713b4a36e63e5a5322829fca7c97 (diff)
Updated core with new syntax.
Diffstat (limited to 'nomic.moon')
-rw-r--r--nomic.moon666
1 files changed, 419 insertions, 247 deletions
diff --git a/nomic.moon b/nomic.moon
index 12bfb08..a8eb4d5 100644
--- a/nomic.moon
+++ b/nomic.moon
@@ -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