1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
|
-- 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 '(.*)'%)")
if fn\match "x[0-9A-F][0-9A-F]"
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}
|