- Added shebangs to generated code output - SyntaxTree:map() -> SyntaxTree:with(), and corresponding changes to metaprogramming API - Added (return Lua 1) shorthand for (return (Lua 1)) - (1 and 2 and 3) compile rule mapping to -> (1 and (*extra arguments*)) - Don't scan for errors, just report them when compiling - Syntax changes: - Added prefix actions (e.g. #$foo) - Operator chars now include utf8 chars - Ditch "escaped nomsu" type (use (\ 1) compile action instead)
354 lines
14 KiB
Plaintext
354 lines
14 KiB
Plaintext
--
|
|
-- This file contains the source code of the Nomsu compiler.
|
|
--
|
|
unpack or= table.unpack
|
|
{:match, :sub, :gsub, :format, :byte, :find} = string
|
|
{:LuaCode, :Source} = require "code_obj"
|
|
require "text"
|
|
SyntaxTree = require "syntax_tree"
|
|
Files = require "files"
|
|
|
|
pretty_error = require("pretty_errors")
|
|
fail_at = (source, msg)->
|
|
local file
|
|
if SyntaxTree\is_instance(source)
|
|
file = source\get_source_file!
|
|
source = source.source
|
|
elseif type(source) == 'string'
|
|
source = Source\from_string(source)
|
|
elseif not Source\is_instance(source)
|
|
-- debug.getinfo() output:
|
|
assert(source.short_src and source.currentline)
|
|
file = Files.read(source.short_src)
|
|
assert file, "Could not find #{source.short_src}"
|
|
lines = file\lines!
|
|
start = 1
|
|
for i=1,source.currentline-1
|
|
start += #lines[i]
|
|
stop = start + #lines[source.currentline]
|
|
source = Source(source.short_src, start, stop)
|
|
|
|
if source and not file
|
|
file = Files.read(source.filename)
|
|
|
|
title, err_msg, hint = msg\match("([^:]*):[ \n]+(.*)[ \n]+Hint: (.*)")
|
|
if not err_msg
|
|
err_msg, hint = msg\match("(.*)[ \n]+Hint:[ \n]+(.*)")
|
|
title = "Failure"
|
|
if not err_msg
|
|
title, err_msg = msg\match("([^:]*):[ \n]+(.*)")
|
|
if not err_msg
|
|
err_msg = msg
|
|
title = "Failure"
|
|
|
|
err_str = pretty_error{
|
|
title: title,
|
|
error: err_msg, hint: hint, source: file,
|
|
start:source.start, stop:source.stop, filename:source.filename,
|
|
}
|
|
error(err_str, 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.
|
|
re = require 're'
|
|
math_expression = re.compile [[ (([*/^+-] / [0-9]+) " ")* [*/^+-] !. ]]
|
|
|
|
MAX_LINE = 80 -- For beautification purposes, try not to make lines much longer than this value
|
|
compile = (tree)=>
|
|
switch tree.type
|
|
when "Action"
|
|
stub = tree.stub
|
|
compile_action = @COMPILE_RULES[stub]
|
|
if not compile_action and math_expression\match(stub)
|
|
lua = LuaCode\from(tree.source)
|
|
for i,tok in ipairs tree
|
|
if type(tok) == 'string'
|
|
lua\add tok
|
|
else
|
|
tok_lua = @compile(tok)
|
|
-- TODO: this is overly eager, should be less aggressive
|
|
tok_lua\parenthesize! if tok.type == "Action"
|
|
lua\add tok_lua
|
|
lua\add " " if i < #tree
|
|
return lua
|
|
|
|
if not compile_action
|
|
seen_words = {}
|
|
words = {}
|
|
for word in stub\gmatch("[^0-9 ][^ ]*")
|
|
unless seen_words[word]
|
|
seen_words[word] = true
|
|
table.insert words, word
|
|
table.sort(words)
|
|
stub2 = table.concat(words, " ")
|
|
compile_action = @COMPILE_RULES[stub2]
|
|
if compile_action
|
|
if debug.getinfo(compile_action, 'u').isvararg
|
|
stub = stub2
|
|
else compile_action = nil
|
|
|
|
if compile_action
|
|
args = [arg for arg in *tree when type(arg) != "string"]
|
|
ret = compile_action(@, tree, unpack(args))
|
|
if ret == nil
|
|
info = debug.getinfo(compile_action, "S")
|
|
filename = Source\from_string(info.source).filename
|
|
fail_at tree,
|
|
("Compile error: The compile-time action here (#{stub}) failed to return any value. "..
|
|
"Hint: Look at the implementation of (#{stub}) in #{filename}:#{info.linedefined} and make sure it's returning something.")
|
|
unless SyntaxTree\is_instance(ret)
|
|
ret.source or= tree.source
|
|
return ret
|
|
if ret != tree
|
|
return @compile(ret)
|
|
|
|
lua = LuaCode\from(tree.source)
|
|
lua\add((stub)\as_lua_id!,"(")
|
|
for argnum, arg in ipairs tree\get_args!
|
|
arg_lua = @compile(arg)
|
|
if arg.type == "Block" and #arg > 1
|
|
arg_lua = LuaCode\from(arg.source, "(function()\n ", arg_lua, "\nend)()")
|
|
if lua\trailing_line_len! + #arg_lua\text! > MAX_LINE
|
|
lua\add(argnum > 1 and ",\n " or "\n ")
|
|
elseif argnum > 1
|
|
lua\add ", "
|
|
lua\add arg_lua
|
|
lua\add ")"
|
|
return lua
|
|
|
|
when "MethodCall"
|
|
stub = tree\get_stub!
|
|
compile_action = @COMPILE_RULES[stub]
|
|
if compile_action
|
|
args = tree\get_args!
|
|
ret = compile_action(@, tree, unpack(args))
|
|
if ret == nil
|
|
info = debug.getinfo(compile_action, "S")
|
|
filename = Source\from_string(info.source).filename
|
|
fail_at tree,
|
|
("Compile error: The compile-time method here (#{stub}) failed to return any value. "..
|
|
"Hint: Look at the implementation of (#{stub}) in #{filename}:#{info.linedefined} "..
|
|
"and make sure it's returning something.")
|
|
unless SyntaxTree\is_instance(ret)
|
|
ret.source or= tree.source
|
|
return ret
|
|
if ret != tree
|
|
return @compile(ret)
|
|
|
|
lua = LuaCode\from tree.source
|
|
target_lua = @compile tree[1]
|
|
target_text = target_lua\text!
|
|
-- TODO: this parenthesizing is maybe overly conservative
|
|
if not (target_text\match("^%(.*%)$") or target_text\match("^[_a-zA-Z][_a-zA-Z0-9.]*$") or
|
|
tree[1].type == "IndexChain")
|
|
target_lua\parenthesize!
|
|
|
|
self_lua = #tree > 2 and "_self" or target_lua
|
|
if #tree > 2
|
|
lua\add "(function(", self_lua, ")\n "
|
|
for i=2,#tree
|
|
lua\add "\n " if i > 2
|
|
if i > 2 and i == #tree
|
|
lua\add "return "
|
|
lua\add self_lua, ":"
|
|
lua\add((tree[i].stub)\as_lua_id!,"(")
|
|
for argnum, arg in ipairs tree[i]\get_args!
|
|
arg_lua = @compile(arg)
|
|
if arg.type == "Block" and #arg > 1
|
|
arg_lua = LuaCode\from(arg.source, "(function()\n ", arg_lua, "\nend)()")
|
|
if lua\trailing_line_len! + #arg_lua\text! > MAX_LINE
|
|
lua\add(argnum > 1 and ",\n " or "\n ")
|
|
elseif argnum > 1
|
|
lua\add ", "
|
|
lua\add arg_lua
|
|
lua\add ")"
|
|
if #tree > 2
|
|
lua\add "\nend)(", target_lua, ")"
|
|
return lua
|
|
|
|
when "EscapedNomsu"
|
|
lua = LuaCode\from tree.source, "SyntaxTree{"
|
|
needs_comma, i = false, 1
|
|
as_lua = (x)->
|
|
if type(x) == 'number'
|
|
tostring(x)
|
|
elseif SyntaxTree\is_instance(x)
|
|
@compile(x)
|
|
elseif Source\is_instance(x)
|
|
tostring(x)\as_lua!
|
|
else x\as_lua!
|
|
|
|
for k,v in pairs((SyntaxTree\is_instance(tree[1]) and tree[1].type == "EscapedNomsu" and tree) or tree[1])
|
|
entry_lua = LuaCode!
|
|
if k == i
|
|
i += 1
|
|
elseif type(k) == 'string' and match(k,"[_a-zA-Z][_a-zA-Z0-9]*")
|
|
entry_lua\add(k, "= ")
|
|
else
|
|
entry_lua\add("[", as_lua(k), "]= ")
|
|
entry_lua\add as_lua(v)
|
|
if needs_comma then lua\add ","
|
|
if lua\trailing_line_len! + #(entry_lua\text!\match("^[\n]*")) > MAX_LINE
|
|
lua\add "\n "
|
|
elseif needs_comma
|
|
lua\add " "
|
|
lua\add entry_lua
|
|
needs_comma = true
|
|
lua\add "}"
|
|
return lua
|
|
|
|
when "Block"
|
|
lua = LuaCode\from(tree.source)
|
|
for i, line in ipairs tree
|
|
if i > 1 then lua\add "\n"
|
|
lua\add @compile(line)
|
|
return lua
|
|
|
|
when "Text"
|
|
if #tree == 0
|
|
return LuaCode\from(tree.source, '""')
|
|
if #tree == 1 and type(tree[1]) == 'string'
|
|
return LuaCode\from(tree.source, tree[1]\as_lua!)
|
|
lua = LuaCode\from(tree.source, "Text(")
|
|
added = 0
|
|
string_buffer = ""
|
|
add_bit = (bit)->
|
|
if added > 0
|
|
if lua\trailing_line_len! + #bit > MAX_LINE
|
|
lua\add ",\n "
|
|
else
|
|
lua\add ", "
|
|
lua\add bit
|
|
added += 1
|
|
|
|
for i, bit in ipairs tree
|
|
if type(bit) == "string"
|
|
string_buffer ..= bit
|
|
continue
|
|
if string_buffer != ""
|
|
for i=1,#string_buffer,MAX_LINE
|
|
add_bit string_buffer\sub(i, i+MAX_LINE-1)\as_lua!
|
|
string_buffer = ""
|
|
|
|
bit_lua = @compile(bit)
|
|
if bit.type == "Block"
|
|
bit_lua = LuaCode\from bit.source, "a_List(function(add)",
|
|
"\n ", bit_lua,
|
|
"\nend):joined()"
|
|
add_bit bit_lua
|
|
|
|
if string_buffer != ""
|
|
for i=1,#string_buffer,MAX_LINE
|
|
add_bit string_buffer\sub(i, i+MAX_LINE-1)\as_lua!
|
|
string_buffer = ""
|
|
|
|
if added == 0
|
|
return LuaCode\from(tree.source, '""')
|
|
lua\add ")"
|
|
return lua
|
|
|
|
when "List", "Dict"
|
|
typename = "a_"..tree.type
|
|
if #tree == 0
|
|
return LuaCode\from tree.source, typename, "{}"
|
|
|
|
lua = LuaCode\from tree.source
|
|
chunks = 0
|
|
i = 1
|
|
while tree[i]
|
|
if tree[i].type == 'Block'
|
|
lua\add " + " if chunks > 0
|
|
lua\add typename, "(function(", (tree.type == 'List' and "add" or ("add, "..("add 1 =")\as_lua_id!)), ")"
|
|
lua\add "\n ", @compile(tree[i]), "\nend)"
|
|
chunks += 1
|
|
i += 1
|
|
else
|
|
lua\add " + " if chunks > 0
|
|
sep = ''
|
|
items_lua = LuaCode\from tree[i].source
|
|
while tree[i]
|
|
if tree[i].type == "Block"
|
|
break
|
|
item_lua = @compile tree[i]
|
|
if item_lua\text!\match("^%.[a-zA-Z_]")
|
|
item_lua = item_lua\text!\sub(2)
|
|
if tree.type == 'Dict' and tree[i].type == 'Index'
|
|
item_lua = LuaCode\from tree[i].source, item_lua, "=true"
|
|
items_lua\add sep, item_lua
|
|
if tree[i].type == "Comment"
|
|
items_lua\add "\n"
|
|
sep = ''
|
|
elseif items_lua\trailing_line_len! > MAX_LINE
|
|
sep = items_lua\text!\sub(-1) == ";" and "\n " or ",\n "
|
|
else
|
|
sep = items_lua\text!\sub(-1) == ";" and " " or ", "
|
|
i += 1
|
|
if items_lua\is_multiline!
|
|
lua\add LuaCode\from items_lua.source, typename, "{\n ", items_lua, "\n}"
|
|
else
|
|
lua\add LuaCode\from items_lua.source, typename, "{", items_lua, "}"
|
|
chunks += 1
|
|
|
|
return lua
|
|
|
|
when "Index"
|
|
key_lua = @compile(tree[1])
|
|
key_str = match(key_lua\text!, '^"([a-zA-Z_][a-zA-Z0-9_]*)"$')
|
|
return if key_str and key_str\is_lua_id!
|
|
LuaCode\from tree.source, ".", key_str
|
|
elseif sub(key_lua\text!,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\from tree.source, "[ ",key_lua,"]"
|
|
else
|
|
LuaCode\from tree.source, "[",key_lua,"]"
|
|
|
|
when "DictEntry"
|
|
key = tree[1]
|
|
if key.type != "Index"
|
|
key = SyntaxTree{type:"Index", source:key.source, key}
|
|
return LuaCode\from tree.source, @compile(key),"=",(tree[2] and @compile(tree[2]) or "true")
|
|
|
|
when "IndexChain"
|
|
lua = @compile(tree[1])
|
|
if lua\text!\match("['\"}]$") or lua\text!\match("]=*]$")
|
|
lua\parenthesize!
|
|
if lua\text! == "..."
|
|
return LuaCode\from(tree.source, "select(", @compile(tree[2][1]), ", ...)")
|
|
for i=2,#tree
|
|
key = tree[i]
|
|
-- TODO: remove this shim
|
|
if key.type != "Index"
|
|
key = SyntaxTree{type:"Index", source:key.source, key}
|
|
lua\add @compile(key)
|
|
return lua
|
|
|
|
when "Number"
|
|
return LuaCode\from(tree.source, tostring(tree[1]))
|
|
|
|
when "Var"
|
|
return LuaCode\from(tree.source, tree\as_var!\as_lua_id!)
|
|
|
|
when "FileChunks"
|
|
error("Can't convert FileChunks to a single block of lua, since each chunk's "..
|
|
"compilation depends on the earlier chunks")
|
|
|
|
when "Comment"
|
|
return LuaCode\from(tree.source, "-- ", (tree[1]\gsub('\n', '\n-- ')))
|
|
|
|
when "Error"
|
|
err_msg = pretty_error{
|
|
title:"Parse error"
|
|
error:tree.error, hint:tree.hint, source:tree\get_source_file!
|
|
start:tree.source.start, stop:tree.source.stop, filename:tree.source.filename
|
|
}
|
|
-- Coroutine yield here?
|
|
error(err_msg)
|
|
|
|
else
|
|
error("Unknown type: #{tree.type}")
|
|
|
|
return {:compile, :fail_at}
|