running files leaking when errors occurred (causing spurious circular import errors), and improved tutorial.
184 lines
7.8 KiB
Plaintext
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}
|