aboutsummaryrefslogtreecommitdiff

– This file contains the source code of the Nomsu compiler.

unpack or= table.unpack {:match, :sub, :gsub, :format, :byte, :find} = string {:LuaCode, :Source} = require “codeobj” require “text” SyntaxTree = require “syntaxtree” Files = require “files”

prettyerror = require(“prettyerrors”) failat = (source, msg)-> local file if SyntaxTree\isinstance(source) file = source\getsourcefile! source = source.source elseif type(source) == ‘string’ source = Source\fromstring(source) elseif not Source\isinstance(source) – debug.getinfo() output: assert(source.shortsrc and source.currentline) file = Files.read(source.shortsrc) assert file, “Could not find #{source.shortsrc}” lines = file\lines! start = 1 for i=1,source.currentline-1 start += #lines[i] stop = start + #lines[source.currentline] source = Source(source.shortsrc, start, stop)

if source and not file
    file = Files.read(source.filename)
if not file
    if NOMSU_PREFIX
        path = "#{NOMSU_PREFIX}/share/nomsu/#{table.concat NOMSU_VERSION, "."}/#{source.filename}"
        file = Files.read(path)
if not file
    error("Can't find file: "..tostring(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 2x + 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)=> if tree == nil error(“No tree was passed in.”)

-- Automatically upgrade trees from older versions:
if tree.version and tree.version < @NOMSU_VERSION\up_to(#tree.version) and @_1_upgraded_from_2_to
    tree = @._1_upgraded_from_2_to(tree, tree.version, @NOMSU_VERSION)

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 > 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 > 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\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 line.type == "Error"
                return @compile(line)
        for i, line in ipairs tree
            if i > 1 then lua\add "\n"
            line_lua = @compile(line)
            lua\add line_lua
            unless line_lua\last(1) == ";" or line_lua\last(4)\match("[^_a-zA-Z0-9]end$")
                lua\add ";"
        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!)), ")"
                body = @compile(tree[i])
                body\declare_locals!
                lua\add "\n    ", body, "\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\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\last(1) == ";" and "\n    " or ",\n    "
                    else
                        sep = items_lua\last(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 = key_lua\match('^"([a-zA-Z_][a-zA-Z0-9_]*)"$')
        return if key_str and key_str\is_lua_id!
            LuaCode\from tree.source, ".", key_str
        elseif key_lua\first(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\match("['\"}]$") or lua\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"
        number = tostring(tree[1])\gsub("_", "")
        return LuaCode\from(tree.source, number)

    when "Var"
        if tree[1].type == "MethodCall"
            return LuaCode\from(tree.source, @compile(tree[1][1]), ".", tree[1][2]\get_stub!\as_lua_id!)
        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}