vim-patch:9.1.0984: exception handling can be improved

Problem:  exception handling can be improved
Solution: add v:stacktrace and getstacktrace()

closes: vim/vim#16360

663d18d610

Co-authored-by: ichizok <gclient.gaap@gmail.com>
Co-authored-by: Naruhiko Nishino <naru123456789@gmail.com>
This commit is contained in:
zeertzjq 2025-01-03 20:12:15 +08:00
parent 06ff5480ce
commit d5308637bf
15 changed files with 294 additions and 9 deletions

View File

@ -4182,6 +4182,21 @@ getscriptinfo([{opts}]) *getscriptinfo()*
Return: ~ Return: ~
(`vim.fn.getscriptinfo.ret[]`) (`vim.fn.getscriptinfo.ret[]`)
getstacktrace() *getstacktrace()*
Returns the current stack trace of Vim scripts.
Stack trace is a |List|, of which each item is a |Dictionary|
with the following items:
funcref The funcref if the stack is at the function,
otherwise this item is not exist.
event The string of the event description if the
stack is at autocmd event, otherwise this item
is not exist.
lnum The line number of the script on the stack.
filepath The file path of the script on the stack.
Return: ~
(`table[]`)
gettabinfo([{tabnr}]) *gettabinfo()* gettabinfo([{tabnr}]) *gettabinfo()*
If {tabnr} is not specified, then information about all the If {tabnr} is not specified, then information about all the
tab pages is returned as a |List|. Each List item is a tab pages is returned as a |List|. Each List item is a

View File

@ -2848,7 +2848,8 @@ in the variable |v:exception|: >
: echo "Number thrown. Value is" v:exception : echo "Number thrown. Value is" v:exception
You may also be interested where an exception was thrown. This is stored in You may also be interested where an exception was thrown. This is stored in
|v:throwpoint|. Note that "v:exception" and "v:throwpoint" are valid for the |v:throwpoint|. And you can obtain the stack trace from |v:stacktrace|.
Note that "v:exception", "v:stacktrace" and "v:throwpoint" are valid for the
exception most recently caught as long it is not finished. exception most recently caught as long it is not finished.
Example: > Example: >

View File

@ -1103,7 +1103,8 @@ Various: *various-functions*
did_filetype() check if a FileType autocommand was used did_filetype() check if a FileType autocommand was used
eventhandler() check if invoked by an event handler eventhandler() check if invoked by an event handler
getpid() get process ID of Vim getpid() get process ID of Vim
getscriptinfo() get list of sourced vim scripts getscriptinfo() get list of sourced Vim scripts
getstacktrace() get current stack trace of Vim scripts
libcall() call a function in an external library libcall() call a function in an external library
libcallnr() idem, returning a number libcallnr() idem, returning a number

View File

@ -6,7 +6,8 @@
Predefined variables *vvars* Predefined variables *vvars*
Some variables can be set by the user, but the type cannot be changed. Most variables are read-only, when a variable can be set by the user, it will
be mentioned at the variable description below. The type cannot be changed.
Type |gO| to see the table of contents. Type |gO| to see the table of contents.
@ -195,7 +196,8 @@ v:event
*v:exception* *exception-variable* *v:exception* *exception-variable*
v:exception v:exception
The value of the exception most recently caught and not The value of the exception most recently caught and not
finished. See also |v:throwpoint| and |throw-variables|. finished. See also |v:stacktrace|, |v:throwpoint|, and
|throw-variables|.
Example: >vim Example: >vim
try try
throw "oops" throw "oops"
@ -586,6 +588,13 @@ v:shell_error
endif endif
< <
*v:stacktrace* *stacktrace-variable*
v:stacktrace
The stack trace of the exception most recently caught and
not finished. Refer to |getstacktrace()| for the structure of
stack trace. See also |v:exception|, |v:throwpoint|, and
|throw-variables|.
*v:statusmsg* *statusmsg-variable* *v:statusmsg* *statusmsg-variable*
v:statusmsg v:statusmsg
Last given status message. Last given status message.
@ -679,7 +688,7 @@ v:this_session
v:throwpoint v:throwpoint
The point where the exception most recently caught and not The point where the exception most recently caught and not
finished was thrown. Not set when commands are typed. See finished was thrown. Not set when commands are typed. See
also |v:exception| and |throw-variables|. also |v:exception|, |v:stacktrace|, and |throw-variables|.
Example: >vim Example: >vim
try try
throw "oops" throw "oops"

View File

@ -3770,6 +3770,20 @@ function vim.fn.getregtype(regname) end
--- @return vim.fn.getscriptinfo.ret[] --- @return vim.fn.getscriptinfo.ret[]
function vim.fn.getscriptinfo(opts) end function vim.fn.getscriptinfo(opts) end
--- Returns the current stack trace of Vim scripts.
--- Stack trace is a |List|, of which each item is a |Dictionary|
--- with the following items:
--- funcref The funcref if the stack is at the function,
--- otherwise this item is not exist.
--- event The string of the event description if the
--- stack is at autocmd event, otherwise this item
--- is not exist.
--- lnum The line number of the script on the stack.
--- filepath The file path of the script on the stack.
---
--- @return table[]
function vim.fn.getstacktrace() end
--- If {tabnr} is not specified, then information about all the --- If {tabnr} is not specified, then information about all the
--- tab pages is returned as a |List|. Each List item is a --- tab pages is returned as a |List|. Each List item is a
--- |Dictionary|. Otherwise, {tabnr} specifies the tab page --- |Dictionary|. Otherwise, {tabnr} specifies the tab page

View File

@ -203,7 +203,8 @@ vim.v.errors = ...
vim.v.event = ... vim.v.event = ...
--- The value of the exception most recently caught and not --- The value of the exception most recently caught and not
--- finished. See also `v:throwpoint` and `throw-variables`. --- finished. See also `v:stacktrace`, `v:throwpoint`, and
--- `throw-variables`.
--- Example: --- Example:
--- ---
--- ```vim --- ```vim
@ -616,6 +617,13 @@ vim.v.servername = ...
--- @type integer --- @type integer
vim.v.shell_error = ... vim.v.shell_error = ...
--- The stack trace of the exception most recently caught and
--- not finished. Refer to `getstacktrace()` for the structure of
--- stack trace. See also `v:exception`, `v:throwpoint`, and
--- `throw-variables`.
--- @type table[]
vim.v.stacktrace = ...
--- Last given status message. --- Last given status message.
--- Modifiable (can be set). --- Modifiable (can be set).
--- @type string --- @type string
@ -718,7 +726,7 @@ vim.v.this_session = ...
--- The point where the exception most recently caught and not --- The point where the exception most recently caught and not
--- finished was thrown. Not set when commands are typed. See --- finished was thrown. Not set when commands are typed. See
--- also `v:exception` and `throw-variables`. --- also `v:exception`, `v:stacktrace`, and `throw-variables`.
--- Example: --- Example:
--- ---
--- ```vim --- ```vim

View File

@ -270,6 +270,7 @@ static struct vimvar {
VV(VV_COLLATE, "collate", VAR_STRING, VV_RO), VV(VV_COLLATE, "collate", VAR_STRING, VV_RO),
VV(VV_EXITING, "exiting", VAR_NUMBER, VV_RO), VV(VV_EXITING, "exiting", VAR_NUMBER, VV_RO),
VV(VV_MAXCOL, "maxcol", VAR_NUMBER, VV_RO), VV(VV_MAXCOL, "maxcol", VAR_NUMBER, VV_RO),
VV(VV_STACKTRACE, "stacktrace", VAR_LIST, VV_RO),
// Neovim // Neovim
VV(VV_STDERR, "stderr", VAR_NUMBER, VV_RO), VV(VV_STDERR, "stderr", VAR_NUMBER, VV_RO),
VV(VV_MSGPACK_TYPES, "msgpack_types", VAR_DICT, VV_RO), VV(VV_MSGPACK_TYPES, "msgpack_types", VAR_DICT, VV_RO),

View File

@ -167,6 +167,7 @@ typedef enum {
VV_COLLATE, VV_COLLATE,
VV_EXITING, VV_EXITING,
VV_MAXCOL, VV_MAXCOL,
VV_STACKTRACE,
// Nvim // Nvim
VV_STDERR, VV_STDERR,
VV_MSGPACK_TYPES, VV_MSGPACK_TYPES,

View File

@ -4670,6 +4670,25 @@ M.funcs = {
returns = 'vim.fn.getscriptinfo.ret[]', returns = 'vim.fn.getscriptinfo.ret[]',
signature = 'getscriptinfo([{opts}])', signature = 'getscriptinfo([{opts}])',
}, },
getstacktrace = {
args = 0,
desc = [=[
Returns the current stack trace of Vim scripts.
Stack trace is a |List|, of which each item is a |Dictionary|
with the following items:
funcref The funcref if the stack is at the function,
otherwise this item is not exist.
event The string of the event description if the
stack is at autocmd event, otherwise this item
is not exist.
lnum The line number of the script on the stack.
filepath The file path of the script on the stack.
]=],
name = 'getstacktrace',
params = {},
returns = 'table[]',
signature = 'getstacktrace()',
},
gettabinfo = { gettabinfo = {
args = { 0, 1 }, args = { 0, 1 },
base = 1, base = 1,

View File

@ -2633,6 +2633,30 @@ int tv_dict_add_allocated_str(dict_T *const d, const char *const key, const size
return OK; return OK;
} }
/// Add a function entry to dictionary.
///
/// @param[out] d Dictionary to add entry to.
/// @param[in] key Key to add.
/// @param[in] key_len Key length.
/// @param[in] fp Function to add.
///
/// @return OK in case of success, FAIL when key already exists.
int tv_dict_add_func(dict_T *const d, const char *const key, const size_t key_len,
ufunc_T *const fp)
FUNC_ATTR_NONNULL_ARG(1, 2, 4)
{
dictitem_T *const item = tv_dict_item_alloc_len(key, key_len);
item->di_tv.v_type = VAR_FUNC;
item->di_tv.vval.v_string = xstrdup(fp->uf_name);
if (tv_dict_add(d, item) == FAIL) {
tv_dict_item_free(item);
return FAIL;
}
func_ref(item->di_tv.vval.v_string);
return OK;
}
//{{{2 Operations on the whole dict //{{{2 Operations on the whole dict
/// Clear all the keys of a Dictionary. "d" remains a valid empty Dictionary. /// Clear all the keys of a Dictionary. "d" remains a valid empty Dictionary.

View File

@ -479,6 +479,9 @@ static int throw_exception(void *value, except_type_T type, char *cmdname)
excp->throw_lnum = SOURCING_LNUM; excp->throw_lnum = SOURCING_LNUM;
} }
excp->stacktrace = stacktrace_create();
tv_list_ref(excp->stacktrace);
if (p_verbose >= 13 || debug_break_level > 0) { if (p_verbose >= 13 || debug_break_level > 0) {
int save_msg_silent = msg_silent; int save_msg_silent = msg_silent;
@ -563,6 +566,7 @@ static void discard_exception(except_T *excp, bool was_finished)
free_msglist(excp->messages); free_msglist(excp->messages);
} }
xfree(excp->throw_name); xfree(excp->throw_name);
tv_list_unref(excp->stacktrace);
xfree(excp); xfree(excp);
} }
@ -584,6 +588,7 @@ static void catch_exception(except_T *excp)
excp->caught = caught_stack; excp->caught = caught_stack;
caught_stack = excp; caught_stack = excp;
set_vim_var_string(VV_EXCEPTION, excp->value, -1); set_vim_var_string(VV_EXCEPTION, excp->value, -1);
set_vim_var_list(VV_STACKTRACE, excp->stacktrace);
if (*excp->throw_name != NUL) { if (*excp->throw_name != NUL) {
if (excp->throw_lnum != 0) { if (excp->throw_lnum != 0) {
vim_snprintf(IObuff, IOSIZE, _("%s, line %" PRId64), vim_snprintf(IObuff, IOSIZE, _("%s, line %" PRId64),
@ -633,6 +638,7 @@ static void finish_exception(except_T *excp)
caught_stack = caught_stack->caught; caught_stack = caught_stack->caught;
if (caught_stack != NULL) { if (caught_stack != NULL) {
set_vim_var_string(VV_EXCEPTION, caught_stack->value, -1); set_vim_var_string(VV_EXCEPTION, caught_stack->value, -1);
set_vim_var_list(VV_STACKTRACE, caught_stack->stacktrace);
if (*caught_stack->throw_name != NUL) { if (*caught_stack->throw_name != NUL) {
if (caught_stack->throw_lnum != 0) { if (caught_stack->throw_lnum != 0) {
vim_snprintf(IObuff, IOSIZE, vim_snprintf(IObuff, IOSIZE,
@ -651,6 +657,7 @@ static void finish_exception(except_T *excp)
} else { } else {
set_vim_var_string(VV_EXCEPTION, NULL, -1); set_vim_var_string(VV_EXCEPTION, NULL, -1);
set_vim_var_string(VV_THROWPOINT, NULL, -1); set_vim_var_string(VV_THROWPOINT, NULL, -1);
set_vim_var_list(VV_STACKTRACE, NULL);
} }
// Discard the exception, but use the finish message for 'verbose'. // Discard the exception, but use the finish message for 'verbose'.

View File

@ -2,6 +2,7 @@
#include <stdbool.h> #include <stdbool.h>
#include "nvim/eval/typval_defs.h"
#include "nvim/pos_defs.h" #include "nvim/pos_defs.h"
/// A list used for saving values of "emsg_silent". Used by ex_try() to save the /// A list used for saving values of "emsg_silent". Used by ex_try() to save the
@ -107,6 +108,7 @@ struct vim_exception {
msglist_T *messages; ///< message(s) causing error exception msglist_T *messages; ///< message(s) causing error exception
char *throw_name; ///< name of the throw point char *throw_name; ///< name of the throw point
linenr_T throw_lnum; ///< line number of the throw point linenr_T throw_lnum; ///< line number of the throw point
list_T *stacktrace; ///< stacktrace
except_T *caught; ///< next exception on the caught stack except_T *caught; ///< next exception on the caught stack
}; };

View File

@ -228,6 +228,72 @@ char *estack_sfile(estack_arg_T which)
return (char *)ga.ga_data; return (char *)ga.ga_data;
} }
static void stacktrace_push_item(list_T *const l, ufunc_T *const fp, const char *const event,
const linenr_T lnum, char *const filepath,
const bool filepath_alloced)
{
dict_T *const d = tv_dict_alloc_lock(VAR_FIXED);
typval_T tv = {
.v_type = VAR_DICT,
.v_lock = VAR_LOCKED,
.vval.v_dict = d,
};
if (fp != NULL) {
tv_dict_add_func(d, S_LEN("funcref"), fp);
}
if (event != NULL) {
tv_dict_add_str(d, S_LEN("event"), event);
}
tv_dict_add_nr(d, S_LEN("lnum"), lnum);
if (filepath_alloced) {
tv_dict_add_allocated_str(d, S_LEN("filepath"), filepath);
} else {
tv_dict_add_str(d, S_LEN("filepath"), filepath);
}
tv_list_append_tv(l, &tv);
}
/// Create the stacktrace from exestack.
list_T *stacktrace_create(void)
{
list_T *const l = tv_list_alloc(exestack.ga_len);
for (int i = 0; i < exestack.ga_len; i++) {
estack_T *const entry = &((estack_T *)exestack.ga_data)[i];
linenr_T lnum = entry->es_lnum;
if (entry->es_type == ETYPE_SCRIPT) {
stacktrace_push_item(l, NULL, NULL, lnum, entry->es_name, false);
} else if (entry->es_type == ETYPE_UFUNC) {
ufunc_T *const fp = entry->es_info.ufunc;
const sctx_T sctx = fp->uf_script_ctx;
bool filepath_alloced = false;
char *filepath = sctx.sc_sid > 0
? get_scriptname((LastSet){ .script_ctx = sctx },
&filepath_alloced) : "";
lnum += sctx.sc_lnum;
stacktrace_push_item(l, fp, NULL, lnum, filepath, filepath_alloced);
} else if (entry->es_type == ETYPE_AUCMD) {
const sctx_T sctx = entry->es_info.aucmd->script_ctx;
bool filepath_alloced = false;
char *filepath = sctx.sc_sid > 0
? get_scriptname((LastSet){ .script_ctx = sctx },
&filepath_alloced) : "";
lnum += sctx.sc_lnum;
stacktrace_push_item(l, NULL, entry->es_name, lnum, filepath, filepath_alloced);
}
}
return l;
}
/// getstacktrace() function
void f_getstacktrace(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
tv_list_set_ret(rettv, stacktrace_create());
}
static bool runtime_search_path_valid = false; static bool runtime_search_path_valid = false;
static int *runtime_search_path_ref = NULL; static int *runtime_search_path_ref = NULL;
static RuntimeSearchPath runtime_search_path; static RuntimeSearchPath runtime_search_path;

View File

@ -220,7 +220,8 @@ M.vars = {
type = 'string', type = 'string',
desc = [=[ desc = [=[
The value of the exception most recently caught and not The value of the exception most recently caught and not
finished. See also |v:throwpoint| and |throw-variables|. finished. See also |v:stacktrace|, |v:throwpoint|, and
|throw-variables|.
Example: >vim Example: >vim
try try
throw "oops" throw "oops"
@ -701,6 +702,15 @@ M.vars = {
< <
]=], ]=],
}, },
stacktrace = {
type = 'table[]',
desc = [=[
The stack trace of the exception most recently caught and
not finished. Refer to |getstacktrace()| for the structure of
stack trace. See also |v:exception|, |v:throwpoint|, and
|throw-variables|.
]=],
},
statusmsg = { statusmsg = {
type = 'string', type = 'string',
desc = [=[ desc = [=[
@ -823,7 +833,7 @@ M.vars = {
desc = [=[ desc = [=[
The point where the exception most recently caught and not The point where the exception most recently caught and not
finished was thrown. Not set when commands are typed. See finished was thrown. Not set when commands are typed. See
also |v:exception| and |throw-variables|. also |v:exception|, |v:stacktrace|, and |throw-variables|.
Example: >vim Example: >vim
try try
throw "oops" throw "oops"

View File

@ -0,0 +1,107 @@
" Test for getstacktrace() and v:stacktrace
let s:thisfile = expand('%:p')
let s:testdir = s:thisfile->fnamemodify(':h')
func Filepath(name)
return s:testdir .. '/' .. a:name
endfunc
func AssertStacktrace(expect, actual)
call assert_equal(#{lnum: 581, filepath: Filepath('runtest.vim')}, a:actual[0])
call assert_equal(a:expect, a:actual[-len(a:expect):])
endfunc
func Test_getstacktrace()
let g:stacktrace = []
let lines1 =<< trim [SCRIPT]
" Xscript1
source Xscript2
func Xfunc1()
" Xfunc1
call Xfunc2()
endfunc
[SCRIPT]
let lines2 =<< trim [SCRIPT]
" Xscript2
func Xfunc2()
" Xfunc2
let g:stacktrace = getstacktrace()
endfunc
[SCRIPT]
call writefile(lines1, 'Xscript1', 'D')
call writefile(lines2, 'Xscript2', 'D')
source Xscript1
call Xfunc1()
call AssertStacktrace([
\ #{funcref: funcref('Test_getstacktrace'), lnum: 35, filepath: s:thisfile},
\ #{funcref: funcref('Xfunc1'), lnum: 5, filepath: Filepath('Xscript1')},
\ #{funcref: funcref('Xfunc2'), lnum: 4, filepath: Filepath('Xscript2')},
\ ], g:stacktrace)
unlet g:stacktrace
endfunc
func Test_getstacktrace_event()
let g:stacktrace = []
let lines1 =<< trim [SCRIPT]
" Xscript1
func Xfunc()
" Xfunc
let g:stacktrace = getstacktrace()
endfunc
augroup test_stacktrace
autocmd SourcePre * call Xfunc()
augroup END
[SCRIPT]
let lines2 =<< trim [SCRIPT]
" Xscript2
[SCRIPT]
call writefile(lines1, 'Xscript1', 'D')
call writefile(lines2, 'Xscript2', 'D')
source Xscript1
source Xscript2
call AssertStacktrace([
\ #{funcref: funcref('Test_getstacktrace_event'), lnum: 62, filepath: s:thisfile},
\ #{event: 'SourcePre Autocommands for "*"', lnum: 7, filepath: Filepath('Xscript1')},
\ #{funcref: funcref('Xfunc'), lnum: 4, filepath: Filepath('Xscript1')},
\ ], g:stacktrace)
augroup test_stacktrace
autocmd!
augroup END
unlet g:stacktrace
endfunc
func Test_vstacktrace()
let lines1 =<< trim [SCRIPT]
" Xscript1
source Xscript2
func Xfunc1()
" Xfunc1
call Xfunc2()
endfunc
[SCRIPT]
let lines2 =<< trim [SCRIPT]
" Xscript2
func Xfunc2()
" Xfunc2
throw 'Exception from Xfunc2'
endfunc
[SCRIPT]
call writefile(lines1, 'Xscript1', 'D')
call writefile(lines2, 'Xscript2', 'D')
source Xscript1
call assert_equal([], v:stacktrace)
try
call Xfunc1()
catch
let stacktrace = v:stacktrace
endtry
call assert_equal([], v:stacktrace)
call AssertStacktrace([
\ #{funcref: funcref('Test_vstacktrace'), lnum: 95, filepath: s:thisfile},
\ #{funcref: funcref('Xfunc1'), lnum: 5, filepath: Filepath('Xscript1')},
\ #{funcref: funcref('Xfunc2'), lnum: 4, filepath: Filepath('Xscript2')},
\ ], stacktrace)
endfunc
" vim: shiftwidth=2 sts=2 expandtab