aboutsummaryrefslogtreecommitdiff
path: root/error_handling.moon
blob: f28a8c8a0297a2904ffa18ffa3843d21ffd1de49 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
-- This file contains the logic for making nicer error messages
files = require "files"
debug_getinfo = debug.getinfo
export SOURCE_MAP

ok, to_lua = pcall -> require('moonscript.base').to_lua
if not ok then to_lua = -> nil
MOON_SOURCE_MAP = setmetatable {},
    __index: (file)=>
        _, line_table = to_lua(file)
        self[file] = line_table or false
        return line_table or false

-- Make a better version of debug.getinfo that provides info about the original source
-- where the error came from, even if that's in another language.
debug.getinfo = (thread,f,what)->
    if what == nil
        f,what,thread = thread,f,nil
    if type(f) == 'number' then f += 1 -- Account for this wrapper function
    info = if thread == nil
        debug_getinfo(f,what)
    else debug_getinfo(thread,f,what)
    if not info or not info.func then return info
    if info.short_src or info.source or info.linedefine or info.currentline
        -- TODO: get name properly
        if map = SOURCE_MAP[info.source]
            if info.currentline
                info.currentline = assert(map[info.currentline])
            if info.linedefined
                info.linedefined = assert(map[info.linedefined])
            if info.lastlinedefined
                info.lastlinedefined = assert(map[info.lastlinedefined])
            info.short_src = info.source\match('@([^[]*)') or info.short_src
            -- TODO: get name properly
            info.name = if info.name
                if tmp = info.name\match("^A_([a-zA-Z0-9_]*)$")
                    tmp\gsub("_"," ")\gsub("x([0-9A-F][0-9A-F])", => string.char(tonumber(@, 16)))
                else info.name
            else "main chunk"
    return info

print_error = (error_message, start_fn, stop_fn)->
    io.stderr\write("#{colored.red "ERROR:"} #{colored.bright colored.red (error_message or "")}\n")
    io.stderr\write("stack traceback:\n")

    level = 1
    found_start = false
    while true
        -- TODO: reduce duplicate code
        calling_fn = debug_getinfo(level)
        if not calling_fn then break
        level += 1
        unless found_start
            if calling_fn.func == start_fn then found_start = true
            continue
        name = calling_fn.name and "function '#{calling_fn.name}'" or nil
        if calling_fn.linedefined == 0 then name = "main chunk"
        if name == "run_lua_fn" then continue
        line = nil
        if map = SOURCE_MAP[calling_fn.source]
            if calling_fn.currentline
                calling_fn.currentline = assert(map[calling_fn.currentline])
            if calling_fn.linedefined
                calling_fn.linedefined = assert(map[calling_fn.linedefined])
            if calling_fn.lastlinedefined
                calling_fn.lastlinedefined = assert(map[calling_fn.lastlinedefined])
            --calling_fn.short_src = calling_fn.source\match('"([^[]*)')
            filename,start,stop = calling_fn.source\match('@([^[]*)%[([0-9]+):([0-9]+)]')
            assert(filename)
            file = files.read(filename)
            err_line = files.get_line(file, calling_fn.currentline)
            offending_statement = colored.bright(colored.red(err_line\match("^[ ]*(.*)")))
            -- TODO: get name properly
            name = if calling_fn.name
                if tmp = calling_fn.name\match("^A_([a-zA-Z0-9_]*)$")
                    "action '#{tmp\gsub("_"," ")\gsub("x([0-9A-F][0-9A-F])", => string.char(tonumber(@, 16)))}'"
                else "action '#{calling_fn.name}'"
            else "main chunk"
            line = colored.yellow("#{filename}:#{calling_fn.currentline} in #{name}\n        #{offending_statement}")
        else
            ok, file = pcall ->files.read(calling_fn.short_src)
            if not ok then file = nil
            local line_num
            if name == nil
                search_level = level
                _info = debug.getinfo(search_level)
                while true
                    search_level += 1
                    _info = debug.getinfo(search_level)
                    break unless _info
                    for i=1,999
                        varname, val = debug.getlocal(search_level, i)
                        if not varname then break
                        if val == calling_fn.func
                            name = "local '#{varname}'"
                            if not varname\match("%(")
                                break
                    unless name
                        for i=1,_info.nups
                            varname, val = debug.getupvalue(_info.func, i)
                            if not varname then break
                            if val == calling_fn.func
                                name = "upvalue '#{varname}'"
                                if not varname\match("%(")
                                    break
            
            if file and (calling_fn.short_src\match("%.moon$") or file\match("^#![^\n]*moon\n")) and type(MOON_SOURCE_MAP[file]) == 'table'
                char = MOON_SOURCE_MAP[file][calling_fn.currentline]
                line_num = 1
                for _ in file\sub(1,char)\gmatch("\n") do line_num += 1
                line = colored.cyan("#{calling_fn.short_src}:#{line_num} in #{name or '?'}")
            else
                line_num = calling_fn.currentline
                if calling_fn.short_src == '[C]'
                    line = colored.green("#{calling_fn.short_src} in #{name or '?'}")
                else
                    line = colored.blue("#{calling_fn.short_src}:#{calling_fn.currentline} in #{name or '?'}")

            if file
                err_line = files.get_line(file, line_num)
                offending_statement = colored.bright(colored.red(err_line\match("^[ ]*(.*)$")))
                line ..= "\n        "..offending_statement
        io.stderr\write(line,"\n")
        if calling_fn.istailcall
            io.stderr\write("    #{colored.dim colored.white "  (...tail calls...)"}\n")
        if calling_fn.func == stop_fn then break

    io.stderr\flush!

guard = (fn)->
    error_handler = (error_message)->
        print_error error_message, error_handler, fn
        EXIT_FAILURE = 1
        os.exit(EXIT_FAILURE)
    xpcall(fn, error_handler)

return {:guard, :print_error}