diff --git a/error_handling.lua b/error_handling.lua index 27022aa..8d6e7d9 100644 --- a/error_handling.lua +++ b/error_handling.lua @@ -1,5 +1,6 @@ local debug_getinfo = debug.getinfo local Files = require("files") +local pretty_error = require("pretty_errors") local RED = "\027[31m" local BRIGHT_RED = "\027[31;1m" local RESET = "\027[0m" @@ -54,16 +55,7 @@ debug.getinfo = function(thread, f, what) end info.short_src = info.source:match('@([^[]*)') or info.short_src if info.name then - do - local tmp = info.name:match("^A_([a-zA-Z0-9_]*)$") - if tmp then - info.name = tmp:gsub("_", " "):gsub("x([0-9A-F][0-9A-F])", function(self) - return string.char(tonumber(self, 16)) - end) - else - info.name = info.name - end - end + info.name = "action '" .. tostring(calling_fn.name:from_lua_id()) .. "'" else info.name = "main chunk" end @@ -72,12 +64,78 @@ debug.getinfo = function(thread, f, what) end return info end -local print_error -print_error = function(error_message, start_fn, stop_fn) - io.stderr:write(tostring(RED) .. "ERROR: " .. tostring(BRIGHT_RED) .. tostring(error_message or "") .. tostring(RESET) .. "\n") - io.stderr:write("stack traceback:\n") - local level = 1 - local found_start = false +local enhance_error +enhance_error = function(error_message, start_fn, stop_fn) + if not (error_message and error_message:match("\x1b")) then + error_message = error_message or "" + do + local fn = error_message:match("attempt to call a nil value %(global '(.*)'%)") + if fn then + if fn:match("x[0-9A-F][0-9A-F]") then + error_message = "The action '" .. tostring(fn:from_lua_id()) .. "' is not defined." + end + end + end + local level = 2 + while true do + local calling_fn = debug_getinfo(level) + if not calling_fn then + break + end + level = level + 1 + local filename, file, line_num + do + local map = SOURCE_MAP and SOURCE_MAP[calling_fn.source] + if map then + if calling_fn.currentline then + line_num = assert(map[calling_fn.currentline]) + end + local start, stop + filename, start, stop = calling_fn.source:match('@([^[]*)%[([0-9]+):([0-9]+)]') + if not filename then + filename, start = calling_fn.source:match('@([^[]*)%[([0-9]+)]') + end + 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' then + local char = MOON_SOURCE_MAP[file][calling_fn.currentline] + line_num = file:line_number_at(char) + else + line_num = calling_fn.currentline + end + end + end + if file and filename and line_num then + local start = 1 + local lines = file:lines() + for i = 1, line_num - 1 do + start = start + #lines[i] + 1 + end + local stop = start + #lines[line_num] + start = start + #lines[line_num]:match("^ *") + error_message = pretty_error({ + title = "Error", + error = error_message, + source = file, + start = start, + stop = stop, + filename = filename + }) + break + end + if calling_fn.func == xpcall then + break + end + end + end + local ret = { + tostring(RED) .. "ERROR: " .. tostring(BRIGHT_RED) .. tostring(error_message or "") .. tostring(RESET), + "stack traceback:" + } + local level = 2 while true do local _continue_0 = false repeat @@ -85,19 +143,15 @@ print_error = function(error_message, start_fn, stop_fn) if not calling_fn then break end - level = level + 1 - if not (found_start) then - if calling_fn.func == start_fn then - found_start = true - end - _continue_0 = true + if calling_fn.func == xpcall then break end + level = level + 1 local name = calling_fn.name and "function '" .. tostring(calling_fn.name) .. "'" or nil if calling_fn.linedefined == 0 then name = "main chunk" end - if name == "run_lua_fn" then + if name == "function 'run_lua_fn'" then _continue_0 = true break end @@ -120,16 +174,7 @@ print_error = function(error_message, start_fn, stop_fn) end assert(filename) if calling_fn.name then - do - local tmp = calling_fn.name:match("^A_([a-zA-Z0-9_]*)$") - if tmp then - name = "action '" .. tostring(tmp:gsub("_", " "):gsub("x([0-9A-F][0-9A-F])", function(self) - return string.char(tonumber(self, 16)) - end)) .. "'" - else - name = "action '" .. tostring(calling_fn.name) .. "'" - end - end + name = "action '" .. tostring(calling_fn.name:from_lua_id()) .. "'" else name = "main chunk" end @@ -192,10 +237,7 @@ print_error = function(error_message, start_fn, stop_fn) end if file and (calling_fn.short_src:match("%.moon$") or file:match("^#![^\n]*moon\n")) and type(MOON_SOURCE_MAP[file]) == 'table' then local char = MOON_SOURCE_MAP[file][calling_fn.currentline] - line_num = 1 - for _ in file:sub(1, char):gmatch("\n") do - line_num = line_num + 1 - end + line_num = file:line_number_at(char) line = tostring(CYAN) .. tostring(calling_fn.short_src) .. ":" .. tostring(line_num) .. " in " .. tostring(name or '?') .. tostring(RESET) else line_num = calling_fn.currentline @@ -216,12 +258,9 @@ print_error = function(error_message, start_fn, stop_fn) end end end - io.stderr:write(line, "\n") + table.insert(ret, line) if calling_fn.istailcall then - io.stderr:write(" " .. tostring(DIM) .. "(...tail calls...)" .. tostring(RESET) .. "\n") - end - if calling_fn.func == stop_fn then - break + table.insert(ret, " " .. tostring(DIM) .. "(...tail calls...)" .. tostring(RESET)) end _continue_0 = true until true @@ -229,19 +268,20 @@ print_error = function(error_message, start_fn, stop_fn) break end end - return io.stderr:flush() + return table.concat(ret, "\n") end local guard guard = function(fn) - local error_handler - error_handler = function(error_message) - print_error(error_message, error_handler, fn) - local EXIT_FAILURE = 1 - return os.exit(EXIT_FAILURE) + local err + ok, err = xpcall(fn, enhance_error) + if not ok then + io.stderr:write(err) + io.stderr:flush() + return os.exit(1) end - return xpcall(fn, error_handler) end return { guard = guard, + enhance_error = enhance_error, print_error = print_error } diff --git a/error_handling.moon b/error_handling.moon index c22a3ae..3252727 100644 --- a/error_handling.moon +++ b/error_handling.moon @@ -1,6 +1,7 @@ -- 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" @@ -31,7 +32,7 @@ debug.getinfo = (thread,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: get name properly + -- TODO: reduce duplicate code if map = SOURCE_MAP[info.source] if info.currentline info.currentline = assert(map[info.currentline]) @@ -40,31 +41,71 @@ debug.getinfo = (thread,f,what)-> if info.lastlinedefined info.lastlinedefined = assert(map[info.lastlinedefined]) info.short_src = info.source\match('@([^[]*)') or info.short_src - -- TODO: get name properly info.name = if info.name - if tmp = info.name\match("^A_([a-zA-Z0-9_]*)$") - tmp\gsub("_"," ")\gsub("x([0-9A-F][0-9A-F])", => string.char(tonumber(@, 16))) - else info.name + "action '#{calling_fn.name\from_lua_id!}'" else "main chunk" return info -print_error = (error_message, start_fn, stop_fn)-> - io.stderr\write("#{RED}ERROR: #{BRIGHT_RED}#{error_message or ""}#{RESET}\n") - io.stderr\write("stack traceback:\n") +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 - level = 1 - found_start = false + 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 - unless found_start - if calling_fn.func == start_fn then found_start = true - continue name = calling_fn.name and "function '#{calling_fn.name}'" or nil if calling_fn.linedefined == 0 then name = "main chunk" - if name == "run_lua_fn" then continue + if name == "function 'run_lua_fn'" then continue line = nil if map = SOURCE_MAP and SOURCE_MAP[calling_fn.source] if calling_fn.currentline @@ -78,11 +119,8 @@ print_error = (error_message, start_fn, stop_fn)-> if not filename filename,start = calling_fn.source\match('@([^[]*)%[([0-9]+)]') assert(filename) - -- TODO: get name properly name = if calling_fn.name - if tmp = calling_fn.name\match("^A_([a-zA-Z0-9_]*)$") - "action '#{tmp\gsub("_"," ")\gsub("x([0-9A-F][0-9A-F])", => string.char(tonumber(@, 16)))}'" - else "action '#{calling_fn.name}'" + "action '#{calling_fn.name\from_lua_id!}'" else "main chunk" file = Files.read(filename) @@ -123,8 +161,7 @@ print_error = (error_message, start_fn, stop_fn)-> 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 = 1 - for _ in file\sub(1,char)\gmatch("\n") do line_num += 1 + 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 @@ -137,18 +174,17 @@ print_error = (error_message, start_fn, stop_fn)-> if err_line = lines[line_num] offending_statement = "#{BRIGHT_RED}#{err_line\match("^[ ]*(.*)$")}#{RESET}" line ..= "\n "..offending_statement - io.stderr\write(line,"\n") + table.insert ret, line if calling_fn.istailcall - io.stderr\write(" #{DIM}(...tail calls...)#{RESET}\n") - if calling_fn.func == stop_fn then break + table.insert ret, " #{DIM}(...tail calls...)#{RESET}" - io.stderr\flush! + return table.concat(ret, "\n") guard = (fn)-> - error_handler = (error_message)-> - print_error error_message, error_handler, fn - EXIT_FAILURE = 1 - os.exit(EXIT_FAILURE) - xpcall(fn, error_handler) + ok, err = xpcall(fn, enhance_error) + if not ok + io.stderr\write err + io.stderr\flush! + os.exit 1 -return {:guard, :print_error} +return {:guard, :enhance_error, :print_error} diff --git a/lib/core/errors.nom b/lib/core/errors.nom index 8027228..a84b580 100644 --- a/lib/core/errors.nom +++ b/lib/core/errors.nom @@ -25,7 +25,7 @@ use "core/control_flow" local _a, _b = \($a as lua expr), \($b as lua expr) if _a ~= _b then at_1_fail(\(quote "\($a.source)"), - "Assumption failed: This value was "..tostring(_a).." when it was supposed to be "..tostring(_b)..".") + "Assumption failed: This value was "..tostring(_a).." but it was expected to be "..tostring(_b)..".") end end ") @@ -35,7 +35,7 @@ use "core/control_flow" local _a, _b = \($a as lua expr), \($b as lua expr) if _a == _b then at_1_fail(\(quote "\($a.source)"), - "Assumption failed: This value was "..tostring(_a).." when it wasn't supposed to be.") + "Assumption failed: This value was "..tostring(_a).." but it wasn't expected to be.") end end ") @@ -76,10 +76,10 @@ test: Lua (" do local _fell_through = false - local _result = {pcall(function() + local _result = {xpcall(function() \($action as lua) _fell_through = true - end)} + end, enhance_error)} if _result[1] then \$success_lua else @@ -123,10 +123,10 @@ test: (do $action then always $final_action) compiles to (" do -- do/then always local _fell_through = false - local _results = {pcall(function() + local _results = {xpcall(function() \($action as lua) _fell_through = true - end)} + end, enhance_error)} \($final_action as lua) if not _results[1] then error(_results[2], 0) end if not _fell_through then return table.unpack(_results, 2) end diff --git a/nomsu_environment.lua b/nomsu_environment.lua index 204b151..d9014c3 100644 --- a/nomsu_environment.lua +++ b/nomsu_environment.lua @@ -10,6 +10,7 @@ do end local SyntaxTree = require("syntax_tree") local Files = require("files") +local Errhand = require("error_handling") local make_parser = require("parser") local pretty_error = require("pretty_errors") local make_tree @@ -137,6 +138,7 @@ nomsu_environment = Importer({ NomsuCode_from = (function(src, ...) return NomsuCode:from(src, ...) end), + enhance_error = Errhand.enhance_error, SOURCE_MAP = { }, getfenv = getfenv, _1_as_nomsu = tree_to_nomsu, diff --git a/nomsu_environment.moon b/nomsu_environment.moon index 522b4c9..f26a7e9 100644 --- a/nomsu_environment.moon +++ b/nomsu_environment.moon @@ -4,6 +4,7 @@ {:List, :Dict, :Text} = require 'containers' SyntaxTree = require "syntax_tree" Files = require "files" +Errhand = require "error_handling" make_parser = require("parser") pretty_error = require("pretty_errors") @@ -61,6 +62,7 @@ nomsu_environment = Importer{ :LuaCode, :NomsuCode, :Source LuaCode_from: ((src, ...)-> LuaCode\from(src, ...)), NomsuCode_from: ((src, ...)-> NomsuCode\from(src, ...)), + enhance_error: Errhand.enhance_error SOURCE_MAP: {}, getfenv:getfenv,