code / nomsu

Lines6.6K Lua5.1K PEG1.3K make117
2 others 83
Markdown60 Bourne Again Shell23
(265 lines)
1 -- This file contains the logic for making nicer error messages
2 debug_getinfo = debug.getinfo
3 Files = require "files"
4 C = require "colors"
5 pretty_error = require("pretty_errors")
6 export SOURCE_MAP
8 ok, to_lua = pcall -> require('moonscript.base').to_lua
9 if not ok then to_lua = -> nil
10 MOON_SOURCE_MAP = setmetatable {},
11 __index: (file)=>
12 _, line_table = to_lua(file)
13 self[file] = line_table or false
14 return line_table or false
16 -- Make a better version of debug.getinfo that provides info about the original source
17 -- where the error came from, even if that's in another language.
18 debug.getinfo = (thread,f,what)->
19 if what == nil
20 f,what,thread = thread,f,nil
21 if type(f) == 'number' then f += 1 -- Account for this wrapper function
22 info = if thread == nil
23 debug_getinfo(f,what)
24 else debug_getinfo(thread,f,what)
25 if not info or not info.func then return info
26 if info.short_src or info.source or info.linedefine or info.currentline
27 -- TODO: reduce duplicate code
28 if map = SOURCE_MAP[info.source]
29 if info.currentline
30 info.currentline = assert(map[info.currentline])
31 if info.linedefined
32 info.linedefined = assert(map[info.linedefined])
33 if info.lastlinedefined
34 info.lastlinedefined = assert(map[info.lastlinedefined])
35 info.short_src = info.source\match('@([^[]*)') or info.short_src
36 info.name = if info.name
37 "action '#{info.name\from_lua_id!}'"
38 else "main chunk"
39 return info
41 -- This uses a slightly modified Damerau-Levenshtein distance:
42 strdist = (a,b,cache={})->
43 if a == b then return 0
44 if #a < #b then a,b = b,a
45 if b == "" then return #a
46 k = a..'\003'..b
47 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)
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 numbering
61 unless error_message and error_message\match("%d|")
62 error_message or= ""
63 -- When calling 'nil' actions, make a better error message
64 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 '(.*)'%)") or
68 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 fn
76 func = debug.getinfo(2,'f').func
77 local env
78 if _VERSION == "Lua 5.1"
79 env = getfenv(func)
80 else
81 ename,env = debug.getupvalue(func, 1)
82 unless ename == "_ENV" or ename == "_G"
83 func = debug.getinfo(3,'f').func
84 ename,env = debug.getupvalue(func, 1)
86 THRESHOLD = math.min(4.5, .9*#action_name) -- Ignore matches with strdist > THRESHOLD
87 candidates = {}
88 cache = {}
90 -- Locals:
91 for i=1,99
92 k, v = debug.getlocal(2, i)
93 break if k == nil
94 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, k
99 -- Upvalues:
100 for i=1,debug.getinfo(func, 'u').nups
101 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, k
107 -- Globals and global compile rules:
108 scan = (t, is_lua_id)->
109 return unless t
110 for k,v in pairs(t)
111 if type(k) == 'string' and type(v) == 'function'
112 k = k\from_lua_id! unless is_lua_id
113 if strdist(k, action_name, cache) <= THRESHOLD and k != ""
114 table.insert candidates, k
115 scan env.COMPILE_RULES, true
116 scan env.COMPILE_RULES._IMPORTS, true
117 scan env
118 scan env._IMPORTS
120 if #candidates > 0
121 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 == 1
125 error_message ..= "\n\x1b[3mSuggestion: Maybe you meant '#{candidates[1]}'? "
126 elseif #candidates > 0
127 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 = 2
131 while true
132 -- TODO: reduce duplicate code
133 calling_fn = debug_getinfo(level)
134 if not calling_fn then break
135 level += 1
136 local filename, file, line_num
137 if map = SOURCE_MAP and SOURCE_MAP[calling_fn.source]
138 if calling_fn.currentline
139 line_num = assert(map[calling_fn.currentline])
140 filename,start,stop = calling_fn.source\match('@([^[]*)%[([0-9]+):([0-9]+)]')
141 if not filename
142 filename,start = calling_fn.source\match('@([^[]*)%[([0-9]+)]')
143 assert(filename)
144 file = Files.read(filename)
145 else
146 filename = calling_fn.short_src
147 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 else
152 line_num = calling_fn.currentline
154 if file and filename and line_num
155 start = 1
156 lines = file\lines!
157 for i=1,line_num-1 do start += #lines[i] + 1
158 stop = start + #lines[line_num]
159 start += #lines[line_num]\match("^ *")
160 error_message = pretty_error{
161 title:"Error"
162 error:error_message, source:file
163 start:start, stop:stop, filename:filename
165 break
166 if calling_fn.func == xpcall then break
169 ret = {
170 C('bold red', error_message or "Error")
171 "stack traceback:"
174 level = 2
175 while true
176 -- TODO: reduce duplicate code
177 calling_fn = debug_getinfo(level)
178 if not calling_fn then break
179 if calling_fn.func == xpcall then break
180 level += 1
181 name = calling_fn.name and "function '#{calling_fn.name}'" or nil
182 if calling_fn.linedefined == 0 then name = "main chunk"
183 if name == "function 'run_lua_fn'" then continue
184 line = nil
185 if map = SOURCE_MAP and SOURCE_MAP[calling_fn.source]
186 if calling_fn.currentline
187 calling_fn.currentline = assert(map[calling_fn.currentline])
188 if calling_fn.linedefined
189 calling_fn.linedefined = assert(map[calling_fn.linedefined])
190 if calling_fn.lastlinedefined
191 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 filename
195 filename,start = calling_fn.source\match('@([^[]*)%[([0-9]+)]')
196 assert(filename)
197 name = if calling_fn.name
198 "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 else
207 line = C('yellow', "#{filename}:#{calling_fn.currentline} in #{name}")
208 else
209 local line_num
210 if name == nil
211 search_level = level
212 _info = debug.getinfo(search_level)
213 while true
214 search_level += 1
215 _info = debug.getinfo(search_level)
216 break unless _info
217 for i=1,999
218 varname, val = debug.getlocal(search_level, i)
219 if not varname then break
220 if val == calling_fn.func
221 name = "local '#{varname}'"
222 if not varname\match("%(")
223 break
224 unless name
225 for i=1,_info.nups
226 varname, val = debug.getupvalue(_info.func, i)
227 if not varname then break
228 if val == calling_fn.func
229 name = "upvalue '#{varname}'"
230 if not varname\match("%(")
231 break
233 local file, lines
234 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 else
242 line_num = calling_fn.currentline
243 if calling_fn.short_src == '[C]'
244 line = C('green', "#{calling_fn.short_src} in #{name or '?'}")
245 else
246 line = C('blue', "#{calling_fn.short_src}:#{calling_fn.currentline} in #{name or '?'}")
248 if file
249 if err_line = lines[line_num]
250 offending_statement = C('bright red', "#{err_line\match("^[ ]*(.*)$")}")
251 line ..= "\n "..offending_statement
252 table.insert ret, line
253 if calling_fn.istailcall
254 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 ok
261 io.stderr\write err
262 io.stderr\flush!
263 os.exit 1
265 return {:guard, :enhance_error, :print_error}