nomsu/error_handling.moon

190 lines
7.9 KiB
Plaintext

-- This file contains the logic for making nicer error messages
debug_getinfo = debug.getinfo
Files = require "files"
pretty_error = require("pretty_errors")
export SOURCE_MAP
RED = "\027[31m"
BRIGHT_RED = "\027[31;1m"
RESET = "\027[0m"
YELLOW = "\027[33m"
CYAN = "\027[36m"
GREEN = "\027[32m"
BLUE = "\027[34m"
DIM = "\027[37;2m"
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)->
unless error_message and error_message\match("\x1b")
error_message or= ""
if fn = error_message\match("attempt to call a nil value %(global '(.*)'%)")
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 = {
"#{RED}ERROR: #{BRIGHT_RED}#{error_message or ""}#{RESET}"
"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 = "#{BRIGHT_RED}#{err_line\match("^[ ]*(.*)")}#{RESET}"
line = "#{YELLOW}#{filename}:#{calling_fn.currentline} in #{name}\n #{offending_statement}#{RESET}"
else
line = "#{YELLOW}#{filename}:#{calling_fn.currentline} in #{name}#{RESET}"
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 = "#{CYAN}#{calling_fn.short_src}:#{line_num} in #{name or '?'}#{RESET}"
else
line_num = calling_fn.currentline
if calling_fn.short_src == '[C]'
line = "#{GREEN}#{calling_fn.short_src} in #{name or '?'}#{RESET}"
else
line = "#{BLUE}#{calling_fn.short_src}:#{calling_fn.currentline} in #{name or '?'}#{RESET}"
if file
if err_line = lines[line_num]
offending_statement = "#{BRIGHT_RED}#{err_line\match("^[ ]*(.*)$")}#{RESET}"
line ..= "\n "..offending_statement
table.insert ret, line
if calling_fn.istailcall
table.insert ret, " #{DIM}(...tail calls...)#{RESET}"
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}