– This file contains the logic for making nicer error messages debuggetinfo = debug.getinfo Files = require “files” C = require “colors” prettyerror = require(“prettyerrors”) export SOURCEMAP
ok, tolua = pcall -> require(‘moonscript.base’).tolua if not ok then tolua = -> nil MOONSOURCE_MAP = setmetatable {}, __index: (file)=> , linetable = tolua(file) self[file] = linetable 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 debuggetinfo(f,what) else debuggetinfo(thread,f,what) if not info or not info.func then return info if info.shortsrc or info.source or info.linedefine or info.currentline – TODO: reduce duplicate code if map = SOURCEMAP[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.shortsrc = info.source\match(‘@([^[]*)’) or info.shortsrc info.name = if info.name “action ‘#{info.name\fromluaid!}’” else “main chunk” return info
– This uses a slightly modified Damerau-Levenshtein distance: strdist = (a,b,cache={})-> if a == b then return 0 if #a < #b then a,b = b,a if b == ““ then return #a k = a..’\003’..b unless cache[k] – Insert, delete, substitute (given weight 1.1 as a heuristic) cache[k] = math.min( strdist(a\sub(1,-2),b,cache) + 1, strdist(a,b\sub(1,-2),cache) + 1, strdist(a\sub(1,-2),b\sub(1,-2),cache) + (a\sub(-1) ~= b\sub(-1) and 1.1 or 0) ) – Transposition: if #a >= 2 and #b >= 2 and a\sub(-1,-1) == b\sub(-2,-2) and a\sub(-2,-2) == b\sub(-1,-1) cache[k] = math.min(cache[k], strdist(a\sub(1,-3),b\sub(1,-3),cache) + 1) return cache[k]
enhanceerror = (errormessage)-> – Hacky: detect the line numbering unless errormessage and errormessage\match(“%d|”) errormessage or= ““ – When calling ‘nil’ actions, make a better error message if fnname = errormessage\match(“attempt to call a nil value %(method ‘(.*)’%)”) actionname = fnname\fromluaid! errormessage = “This object does not have the method ‘#{actionname}’.” elseif fnname = (errormessage\match(“attempt to call a nil value %(global ‘(.*)’%)”) or errormessage\match(“attempt to call global ‘(.*)’ %(a nil value%)”))
action_name = fn_name\from_lua_id!
error_message = "The action '#{action_name}' is not defined."
-- Look for simple misspellings:
-- This check is necessary for handling both top-level code and code inside a fn
func = debug.getinfo(2,'f').func
local env
if _VERSION == "Lua 5.1"
env = getfenv(func)
else
ename,env = debug.getupvalue(func, 1)
unless ename == "_ENV" or ename == "_G"
func = debug.getinfo(3,'f').func
ename,env = debug.getupvalue(func, 1)
THRESHOLD = math.min(4.5, .9*#action_name) -- Ignore matches with strdist > THRESHOLD
candidates = {}
cache = {}
-- Locals:
for i=1,99
k, v = debug.getlocal(2, i)
break if k == nil
unless k\sub(1,1) == "(" or type(v) != 'function'
k = k\from_lua_id!
if strdist(k, action_name, cache) <= THRESHOLD and k != ""
table.insert candidates, k
-- Upvalues:
for i=1,debug.getinfo(func, 'u').nups
k, v = debug.getupvalue(func, i)
unless k\sub(1,1) == "(" or type(v) != 'function'
k = k\from_lua_id!
if strdist(k, action_name, cache) <= THRESHOLD and k != ""
table.insert candidates, k
-- Globals and global compile rules:
scan = (t, is_lua_id)->
return unless t
for k,v in pairs(t)
if type(k) == 'string' and type(v) == 'function'
k = k\from_lua_id! unless is_lua_id
if strdist(k, action_name, cache) <= THRESHOLD and k != ""
table.insert candidates, k
scan env.COMPILE_RULES, true
scan env.COMPILE_RULES._IMPORTS, true
scan env
scan env._IMPORTS
if #candidates > 0
for c in *candidates do THRESHOLD = math.min(THRESHOLD, strdist(c, action_name, cache))
candidates = [c for c in *candidates when strdist(c, action_name, cache) <= THRESHOLD]
--candidates = ["#{c}[#{strdist(c,action_name,cache)}/#{THRESHOLD}]" for c in *candidates]
if #candidates == 1
error_message ..= "\n\x1b[3mSuggestion: Maybe you meant '#{candidates[1]}'? "
elseif #candidates > 0
last = table.remove(candidates)
error_message ..= "\n"..C('italic', "Suggestion: Maybe you meant '#{table.concat candidates, "', '"}'#{#candidates > 1 and ',' or ''} or '#{last}'? ")
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, :enhanceerror, :printerror}
