nomsu/error_handling.moon
Bruce Hill bc41cc3a24 Switched to have colors/utf8 be optional, fixed an issue with currently
running files leaking when errors occurred (causing spurious circular
import errors), and improved tutorial.
2019-01-29 16:17:33 -08:00

184 lines
7.8 KiB
Plaintext

-- This file contains the logic for making nicer error messages
debug_getinfo = debug.getinfo
Files = require "files"
C = require "colors"
pretty_error = require("pretty_errors")
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: reduce duplicate code
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
info.name = if info.name
"action '#{info.name\from_lua_id!}'"
else "main chunk"
return info
enhance_error = (error_message, start_fn, stop_fn)->
-- Hacky: detect the line numbering
unless error_message and error_message\match("%d|")
error_message or= ""
if fn = (error_message\match("attempt to call a nil value %(global '(.*)'%)") or
error_message\match("attempt to call global '(.*)' %(a nil value%)"))
error_message = "The action '#{fn\from_lua_id!}' is not defined."
level = 2
while true
-- TODO: reduce duplicate code
calling_fn = debug_getinfo(level)
if not calling_fn then break
level += 1
local filename, file, line_num
if map = SOURCE_MAP and SOURCE_MAP[calling_fn.source]
if calling_fn.currentline
line_num = assert(map[calling_fn.currentline])
filename,start,stop = calling_fn.source\match('@([^[]*)%[([0-9]+):([0-9]+)]')
if not filename
filename,start = calling_fn.source\match('@([^[]*)%[([0-9]+)]')
assert(filename)
file = Files.read(filename)
else
filename = calling_fn.short_src
file = Files.read(filename)
if calling_fn.short_src\match("%.moon$") and type(MOON_SOURCE_MAP[file]) == 'table'
char = MOON_SOURCE_MAP[file][calling_fn.currentline]
line_num = file\line_number_at(char)
else
line_num = calling_fn.currentline
if file and filename and line_num
start = 1
lines = file\lines!
for i=1,line_num-1 do start += #lines[i] + 1
stop = start + #lines[line_num]
start += #lines[line_num]\match("^ *")
error_message = pretty_error{
title:"Error"
error:error_message, source:file
start:start, stop:stop, filename:filename
}
break
if calling_fn.func == xpcall then break
ret = {
C('bold red', error_message or "Error")
"stack traceback:"
}
level = 2
while true
-- TODO: reduce duplicate code
calling_fn = debug_getinfo(level)
if not calling_fn then break
if calling_fn.func == xpcall then break
level += 1
name = calling_fn.name and "function '#{calling_fn.name}'" or nil
if calling_fn.linedefined == 0 then name = "main chunk"
if name == "function 'run_lua_fn'" then continue
line = nil
if map = SOURCE_MAP and 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]+)]')
if not filename
filename,start = calling_fn.source\match('@([^[]*)%[([0-9]+)]')
assert(filename)
name = if calling_fn.name
"action '#{calling_fn.name\from_lua_id!}'"
else "main chunk"
file = Files.read(filename)
lines = file and file\lines! or {}
if err_line = lines[calling_fn.currentline]
offending_statement = C('bright red', err_line\match("^[ ]*(.*)"))
line = C('yellow', "#{filename}:#{calling_fn.currentline} in #{name}\n #{offending_statement}")
else
line = C('yellow', "#{filename}:#{calling_fn.currentline} in #{name}")
else
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
local file, lines
if file = Files.read(calling_fn.short_src)
lines = file\lines!
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 = file\line_number_at(char)
line = C('cyan', "#{calling_fn.short_src}:#{line_num} in #{name or '?'}")
else
line_num = calling_fn.currentline
if calling_fn.short_src == '[C]'
line = C('green', "#{calling_fn.short_src} in #{name or '?'}")
else
line = C('blue', "#{calling_fn.short_src}:#{calling_fn.currentline} in #{name or '?'}")
if file
if err_line = lines[line_num]
offending_statement = C('bright red', "#{err_line\match("^[ ]*(.*)$")}")
line ..= "\n "..offending_statement
table.insert ret, line
if calling_fn.istailcall
table.insert ret, C('dim', " (...tail calls...)")
return table.concat(ret, "\n")
guard = (fn)->
ok, err = xpcall(fn, enhance_error)
if not ok
io.stderr\write err
io.stderr\flush!
os.exit 1
return {:guard, :enhance_error, :print_error}