(265 lines)
1 -- This file contains the logic for making nicer error messages2 debug_getinfo = debug.getinfo3 Files = require "files"4 C = require "colors"5 pretty_error = require("pretty_errors")6 export SOURCE_MAP8 ok, to_lua = pcall -> require('moonscript.base').to_lua9 if not ok then to_lua = -> nil10 MOON_SOURCE_MAP = setmetatable {},11 __index: (file)=>12 _, line_table = to_lua(file)13 self[file] = line_table or false14 return line_table or false16 -- Make a better version of debug.getinfo that provides info about the original source17 -- where the error came from, even if that's in another language.18 debug.getinfo = (thread,f,what)->19 if what == nil20 f,what,thread = thread,f,nil21 if type(f) == 'number' then f += 1 -- Account for this wrapper function22 info = if thread == nil23 debug_getinfo(f,what)24 else debug_getinfo(thread,f,what)25 if not info or not info.func then return info26 if info.short_src or info.source or info.linedefine or info.currentline27 -- TODO: reduce duplicate code28 if map = SOURCE_MAP[info.source]29 if info.currentline30 info.currentline = assert(map[info.currentline])31 if info.linedefined32 info.linedefined = assert(map[info.linedefined])33 if info.lastlinedefined34 info.lastlinedefined = assert(map[info.lastlinedefined])35 info.short_src = info.source\match('@([^[]*)') or info.short_src36 info.name = if info.name37 "action '#{info.name\from_lua_id!}'"38 else "main chunk"39 return info41 -- This uses a slightly modified Damerau-Levenshtein distance:42 strdist = (a,b,cache={})->43 if a == b then return 044 if #a < #b then a,b = b,a45 if b == "" then return #a46 k = a..'\003'..b47 unless cache[k]48 -- Insert, delete, substitute (given weight 1.1 as a heuristic)49 cache[k] = math.min(50 strdist(a\sub(1,-2),b,cache) + 1,51 strdist(a,b\sub(1,-2),cache) + 1,52 strdist(a\sub(1,-2),b\sub(1,-2),cache) + (a\sub(-1) ~= b\sub(-1) and 1.1 or 0)53 )54 -- Transposition:55 if #a >= 2 and #b >= 2 and a\sub(-1,-1) == b\sub(-2,-2) and a\sub(-2,-2) == b\sub(-1,-1)56 cache[k] = math.min(cache[k], strdist(a\sub(1,-3),b\sub(1,-3),cache) + 1)57 return cache[k]59 enhance_error = (error_message)->60 -- Hacky: detect the line numbering61 unless error_message and error_message\match("%d|")62 error_message or= ""63 -- When calling 'nil' actions, make a better error message64 if fn_name = error_message\match("attempt to call a nil value %(method '(.*)'%)")65 action_name = fn_name\from_lua_id!66 error_message = "This object does not have the method '#{action_name}'."67 elseif fn_name = (error_message\match("attempt to call a nil value %(global '(.*)'%)") or68 error_message\match("attempt to call global '(.*)' %(a nil value%)"))70 action_name = fn_name\from_lua_id!71 error_message = "The action '#{action_name}' is not defined."73 -- Look for simple misspellings:75 -- This check is necessary for handling both top-level code and code inside a fn76 func = debug.getinfo(2,'f').func77 local env78 if _VERSION == "Lua 5.1"79 env = getfenv(func)80 else81 ename,env = debug.getupvalue(func, 1)82 unless ename == "_ENV" or ename == "_G"83 func = debug.getinfo(3,'f').func84 ename,env = debug.getupvalue(func, 1)86 THRESHOLD = math.min(4.5, .9*#action_name) -- Ignore matches with strdist > THRESHOLD87 candidates = {}88 cache = {}90 -- Locals:91 for i=1,9992 k, v = debug.getlocal(2, i)93 break if k == nil94 unless k\sub(1,1) == "(" or type(v) != 'function'95 k = k\from_lua_id!96 if strdist(k, action_name, cache) <= THRESHOLD and k != ""97 table.insert candidates, k99 -- Upvalues:100 for i=1,debug.getinfo(func, 'u').nups101 k, v = debug.getupvalue(func, i)102 unless k\sub(1,1) == "(" or type(v) != 'function'103 k = k\from_lua_id!104 if strdist(k, action_name, cache) <= THRESHOLD and k != ""105 table.insert candidates, k107 -- Globals and global compile rules:108 scan = (t, is_lua_id)->109 return unless t110 for k,v in pairs(t)111 if type(k) == 'string' and type(v) == 'function'112 k = k\from_lua_id! unless is_lua_id113 if strdist(k, action_name, cache) <= THRESHOLD and k != ""114 table.insert candidates, k115 scan env.COMPILE_RULES, true116 scan env.COMPILE_RULES._IMPORTS, true117 scan env118 scan env._IMPORTS120 if #candidates > 0121 for c in *candidates do THRESHOLD = math.min(THRESHOLD, strdist(c, action_name, cache))122 candidates = [c for c in *candidates when strdist(c, action_name, cache) <= THRESHOLD]123 --candidates = ["#{c}[#{strdist(c,action_name,cache)}/#{THRESHOLD}]" for c in *candidates]124 if #candidates == 1125 error_message ..= "\n\x1b[3mSuggestion: Maybe you meant '#{candidates[1]}'? "126 elseif #candidates > 0127 last = table.remove(candidates)128 error_message ..= "\n"..C('italic', "Suggestion: Maybe you meant '#{table.concat candidates, "', '"}'#{#candidates > 1 and ',' or ''} or '#{last}'? ")130 level = 2131 while true132 -- TODO: reduce duplicate code133 calling_fn = debug_getinfo(level)134 if not calling_fn then break135 level += 1136 local filename, file, line_num137 if map = SOURCE_MAP and SOURCE_MAP[calling_fn.source]138 if calling_fn.currentline139 line_num = assert(map[calling_fn.currentline])140 filename,start,stop = calling_fn.source\match('@([^[]*)%[([0-9]+):([0-9]+)]')141 if not filename142 filename,start = calling_fn.source\match('@([^[]*)%[([0-9]+)]')143 assert(filename)144 file = Files.read(filename)145 else146 filename = calling_fn.short_src147 file = Files.read(filename)148 if calling_fn.short_src\match("%.moon$") and type(MOON_SOURCE_MAP[file]) == 'table'149 char = MOON_SOURCE_MAP[file][calling_fn.currentline]150 line_num = file\line_number_at(char)151 else152 line_num = calling_fn.currentline154 if file and filename and line_num155 start = 1156 lines = file\lines!157 for i=1,line_num-1 do start += #lines[i] + 1158 stop = start + #lines[line_num]159 start += #lines[line_num]\match("^ *")160 error_message = pretty_error{161 title:"Error"162 error:error_message, source:file163 start:start, stop:stop, filename:filename164 }165 break166 if calling_fn.func == xpcall then break169 ret = {170 C('bold red', error_message or "Error")171 "stack traceback:"172 }174 level = 2175 while true176 -- TODO: reduce duplicate code177 calling_fn = debug_getinfo(level)178 if not calling_fn then break179 if calling_fn.func == xpcall then break180 level += 1181 name = calling_fn.name and "function '#{calling_fn.name}'" or nil182 if calling_fn.linedefined == 0 then name = "main chunk"183 if name == "function 'run_lua_fn'" then continue184 line = nil185 if map = SOURCE_MAP and SOURCE_MAP[calling_fn.source]186 if calling_fn.currentline187 calling_fn.currentline = assert(map[calling_fn.currentline])188 if calling_fn.linedefined189 calling_fn.linedefined = assert(map[calling_fn.linedefined])190 if calling_fn.lastlinedefined191 calling_fn.lastlinedefined = assert(map[calling_fn.lastlinedefined])192 --calling_fn.short_src = calling_fn.source\match('"([^[]*)')193 filename,start,stop = calling_fn.source\match('@([^[]*)%[([0-9]+):([0-9]+)]')194 if not filename195 filename,start = calling_fn.source\match('@([^[]*)%[([0-9]+)]')196 assert(filename)197 name = if calling_fn.name198 "action '#{calling_fn.name\from_lua_id!}'"199 else "main chunk"201 file = Files.read(filename)202 lines = file and file\lines! or {}203 if err_line = lines[calling_fn.currentline]204 offending_statement = C('bright red', err_line\match("^[ ]*(.*)"))205 line = C('yellow', "#{filename}:#{calling_fn.currentline} in #{name}\n #{offending_statement}")206 else207 line = C('yellow', "#{filename}:#{calling_fn.currentline} in #{name}")208 else209 local line_num210 if name == nil211 search_level = level212 _info = debug.getinfo(search_level)213 while true214 search_level += 1215 _info = debug.getinfo(search_level)216 break unless _info217 for i=1,999218 varname, val = debug.getlocal(search_level, i)219 if not varname then break220 if val == calling_fn.func221 name = "local '#{varname}'"222 if not varname\match("%(")223 break224 unless name225 for i=1,_info.nups226 varname, val = debug.getupvalue(_info.func, i)227 if not varname then break228 if val == calling_fn.func229 name = "upvalue '#{varname}'"230 if not varname\match("%(")231 break233 local file, lines234 if file = Files.read(calling_fn.short_src)235 lines = file\lines!237 if file and (calling_fn.short_src\match("%.moon$") or file\match("^#![^\n]*moon\n")) and type(MOON_SOURCE_MAP[file]) == 'table'238 char = MOON_SOURCE_MAP[file][calling_fn.currentline]239 line_num = file\line_number_at(char)240 line = C('cyan', "#{calling_fn.short_src}:#{line_num} in #{name or '?'}")241 else242 line_num = calling_fn.currentline243 if calling_fn.short_src == '[C]'244 line = C('green', "#{calling_fn.short_src} in #{name or '?'}")245 else246 line = C('blue', "#{calling_fn.short_src}:#{calling_fn.currentline} in #{name or '?'}")248 if file249 if err_line = lines[line_num]250 offending_statement = C('bright red', "#{err_line\match("^[ ]*(.*)$")}")251 line ..= "\n "..offending_statement252 table.insert ret, line253 if calling_fn.istailcall254 table.insert ret, C('dim', " (...tail calls...)")256 return table.concat(ret, "\n")258 guard = (fn)->259 ok, err = xpcall(fn, enhance_error)260 if not ok261 io.stderr\write err262 io.stderr\flush!263 os.exit 1265 return {:guard, :enhance_error, :print_error}