nomsu/nomic_whitespace.moon
2017-08-20 02:02:15 -07:00

326 lines
10 KiB
Plaintext

re = require 're'
lpeg = require 'lpeg'
utils = require 'utils'
moon = require 'moon'
lpeg.setmaxstack 10000 -- whoa
linebreak = lpeg.P("\r")^-1 * lpeg.P("\n")
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 " --(" ")\rep(indent_stack[#indent_stack]+1).."{\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, "}" --(" ")\rep(indent_stack[#indent_stack]+1).."}"
table.insert tokens, " "
table.insert result, table.concat(tokens, "\n")
else
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
lingo = [=[
file <- ({} {| {:body: (" " block) :} ({:errors: errors :})? |} {}) -> File
errors <- ({} {.+} {}) -> Errors
block <- ({} {| statement (%nodent statement)* |} {}) -> Block
one_liner <- ({} {| statement |} {}) -> Block
statement <- ({} functioncall {}) -> Statement
functioncall <- ({} {| fn_bits |} {}) -> FunctionCall
fn_bit <- (expression / word)
fn_bits <-
((".." (%indent %nodent indented_fn_bits %dedent))
/ fn_bit) (fn_sep fn_bits)?
indented_fn_bits <-
fn_bit ((%ws / %nodent) indented_fn_bits)?
fn_sep <- (%nodent ".." %ws?) / %ws / (&":") / (&"..") / (&'"') / (&"[")
thunk <-
({} ":" %ws? ((one_liner (%ws? ";")?) / (%indent %nodent block %dedent)) {}) -> Thunk
word <- ({} {%wordchar+} {}) -> Word
expression <- string / number / variable / list / thunk / subexpression
string <- ({} '"' {(("\\" .) / [^"])*} '"' {}) -> String
number <- ({} {'-'? [0-9]+ ("." [0-9]+)?} {}) -> Number
variable <- ({} ("%" {%wordchar+}) {}) -> Var
subexpression <- "(" %ws? (expression / functioncall) %ws? ")"
list <- ({} {|
("[..]" %indent %nodent indented_list ","? %dedent)
/ ("[" %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
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("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 "returned #{utils.repr(ret,true)}"
return ret
def: (spec, fn)=>
invocation,arg_names = self\get_invocation spec
@defs[invocation] = {fn, arg_names}
get_invocation:(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, " "
return invocation, arg_names
defmacro: (spec, fn, advanced_mode=false)=>
invocation,arg_names = self\get_invocation spec
if advanced_mode
@macros[invocation] = {fn, arg_names}
return
text_manipulator = fn
fn = (args, transform,src,indent_level,macros)->
text_args = [transform(src,a,indent_level,macros) for a in *args]
return text_manipulator(unpack(text_args))
@macros[invocation] = {fn, arg_names}
run: (text)=>
if @debug
print("Running text:\n")
print(text)
indentified = add_indent_tokens(text)
print("Indentified:\n[[#{indentified}]]")
print("\nCompiling...")
code = self\compile(text)
if @debug
print(code)
lua_thunk, err = loadstring(code)
if not lua_thunk
error("Failed to compile")
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
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("\nRESULT:\n#{utils.repr(tree)}")
assert tree, "Failed to parse: #{str}"
return tree
transform: (tree, indent_level=0)=>
indented = (fn)->
export indent_level
indent_level += 1
fn!
indent_level -= 1
transform = (t)-> self\transform(t, indent_level)
ind = (line) -> (" ")\rep(indent_level)..line
ded = (lines)-> lines\match"^%s*(.*)"
ret_lines = {}
lua = (line, skip_indent=false)->
unless skip_indent
line = ind(ded(line))
table.insert ret_lines, 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 > 1
return transform(tree.value.errors)
lua "return (function(game, vars)"
indented ->
lua transform(tree.value.body)
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"
lua transform(tree.value)
lua "return ret"
lua "end)"
when "Statement"
lua "ret = #{ded(transform(tree.value))}"
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]
-- TODO: figure out args
args = [a for a in *tree.value when a.type != "Word"]
return @macros[name][1](self, args, transform)
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: #{utils.repr(tree)}")
return table.concat ret_lines, "\n"
compile: (src)=>
tree = self\parse(src)
code = self\transform(tree,0)
return code
return Game