patch 9.1.0984: exception handling can be improved Commit: https://github.com/vim/vim/commit/663d18d6102f40d14e36096ec590445e61026ed6 Author: ichizok <gclient.g...@gmail.com> Date: Thu Jan 2 18:06:00 2025 +0100
patch 9.1.0984: exception handling can be improved Problem: exception handling can be improved Solution: add v:stacktrace and getstacktrace() closes: #16360 Co-authored-by: Naruhiko Nishino <naru123456...@gmail.com> Signed-off-by: ichizok <gclient.g...@gmail.com> Signed-off-by: Christian Brabandt <c...@256bit.org> diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index 48ba03b8b..d7ed7320f 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -279,6 +279,7 @@ getregionpos({pos1}, {pos2} [, {opts}]) List get a list of positions for a region getregtype([{regname}]) String type of a register getscriptinfo([{opts}]) List list of sourced scripts +getstacktrace() List get current stack trace of Vim scripts gettabinfo([{expr}]) List list of tab pages gettabvar({nr}, {varname} [, {def}]) any variable {varname} in tab {nr} or {def} @@ -4997,6 +4998,21 @@ getscriptinfo([{opts}]) *getscriptinfo()* Return type: list<dict<any>> +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 type: list<dict<any>> + + gettabinfo([{tabnr}]) *gettabinfo()* If {tabnr} is not specified, then information about all the tab pages is returned as a |List|. Each List item is a diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index af5b3a954..2348cdddb 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1,4 +1,4 @@ -*eval.txt* For Vim version 9.1. Last change: 2024 Dec 23 +*eval.txt* For Vim version 9.1. Last change: 2025 Jan 02 VIM REFERENCE MANUAL by Bram Moolenaar @@ -1953,7 +1953,8 @@ variables for each buffer. Use local buffer variables instead |b:var|. PREDEFINED VIM VARIABLES *vim-variable* *v:var* *v:* *E963* *E1063* -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. *v:argv* *argv-variable* v:argv The command line arguments Vim was invoked with. This is a @@ -2172,7 +2173,8 @@ v:event Dictionary containing information about the current < *v:exception* *exception-variable* v:exception 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: > :try : throw "oops" @@ -2548,6 +2550,12 @@ v:sizeofpointer Number of bytes in a pointer. Depends on how Vim was compiled. This is only useful for deciding whether a test will give the expected result. + *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 Last given status message. It's allowed to set this variable. @@ -2676,7 +2684,7 @@ v:this_session Full filename of the last loaded or saved session file. See *v:throwpoint* *throwpoint-variable* v:throwpoint The point where the exception most recently caught and not 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: > :try : throw "oops" @@ -3856,7 +3864,8 @@ in the variable |v:exception|: > : echo "Number thrown. Value is" v:exception 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. Example: > diff --git a/runtime/doc/tags b/runtime/doc/tags index 6458224fc..a2996794f 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -7908,6 +7908,7 @@ getscript-history pi_getscript.txt /*getscript-history* getscript-plugins pi_getscript.txt /*getscript-plugins* getscript-start pi_getscript.txt /*getscript-start* getscriptinfo() builtin.txt /*getscriptinfo()* +getstacktrace() builtin.txt /*getstacktrace()* gettabinfo() builtin.txt /*gettabinfo()* gettabvar() builtin.txt /*gettabvar()* gettabwinvar() builtin.txt /*gettabwinvar()* @@ -10218,6 +10219,7 @@ sqrt() builtin.txt /*sqrt()* squirrel.vim syntax.txt /*squirrel.vim* srand() builtin.txt /*srand()* sscanf eval.txt /*sscanf* +stacktrace-variable eval.txt /*stacktrace-variable* standard-plugin usr_05.txt /*standard-plugin* standard-plugin-list help.txt /*standard-plugin-list* standout syntax.txt /*standout* @@ -11038,6 +11040,7 @@ v:shell_error eval.txt /*v:shell_error* v:sizeofint eval.txt /*v:sizeofint* v:sizeoflong eval.txt /*v:sizeoflong* v:sizeofpointer eval.txt /*v:sizeofpointer* +v:stacktrace eval.txt /*v:stacktrace* v:statusmsg eval.txt /*v:statusmsg* v:swapchoice eval.txt /*v:swapchoice* v:swapcommand eval.txt /*v:swapcommand* diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt index fdb993442..ded30e35d 100644 --- a/runtime/doc/usr_41.txt +++ b/runtime/doc/usr_41.txt @@ -1,4 +1,4 @@ -*usr_41.txt* For Vim version 9.1. Last change: 2024 Dec 30 +*usr_41.txt* For Vim version 9.1. Last change: 2025 Jan 02 VIM USER MANUAL - by Bram Moolenaar @@ -1399,7 +1399,8 @@ Various: *various-functions* eventhandler() check if invoked by an event handler getcellpixels() get List of cell pixel size 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 getimstatus() check if IME status is active interrupt() interrupt script execution windowsversion() get MS-Windows version diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index cfb472d44..bcb0b78f7 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -41623,6 +41623,8 @@ Changed~ for the ghostty terminal emulator (using kitty protocol) - |complete_info()| returns the list of matches shown in the poppu menu via the "matches" key +- |v:stacktrace| The stack trace of the exception most recently caught and + not finished *added-9.2* Added ~ @@ -41642,6 +41644,7 @@ Functions: ~ |getcmdprompt()| get prompt for input()/confirm() |getregion()| get a region of text from a buffer |getregionpos()| get a list of positions for a region +|getstacktrace()| get current stack trace of Vim scripts |id()| get unique identifier for a Dict, List, Object, Channel or Blob variable |matchbufline()| all the matches of a pattern in a buffer diff --git a/src/dict.c b/src/dict.c index d3636f3bc..8f0db7edc 100644 --- a/src/dict.c +++ b/src/dict.c @@ -531,6 +531,29 @@ dict_add_callback(dict_T *d, char *key, callback_T *cb) return OK; } +/* + * Add a function entry to dictionary "d". + * Returns FAIL when out of memory and when key already exists. + */ + int +dict_add_func(dict_T *d, char *key, ufunc_T *fp) +{ + dictitem_T *item; + + item = dictitem_alloc((char_u *)key); + if (item == NULL) + return FAIL; + item->di_tv.v_type = VAR_FUNC; + item->di_tv.vval.v_string = vim_strsave(fp->uf_name); + if (dict_add(d, item) == FAIL) + { + dictitem_free(item); + return FAIL; + } + func_ref(item->di_tv.vval.v_string); + return OK; +} + /* * Initializes "iter" for iterating over dictionary items with * dict_iterate_next(). diff --git a/src/evalfunc.c b/src/evalfunc.c index ec108b0cb..d81480b50 100644 --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -2170,6 +2170,8 @@ static funcentry_T global_functions[] = ret_string, f_getregtype}, {"getscriptinfo", 0, 1, 0, arg1_dict_any, ret_list_dict_any, f_getscriptinfo}, + {"getstacktrace", 0, 0, 0, NULL, + ret_list_dict_any, f_getstacktrace}, {"gettabinfo", 0, 1, FEARG_1, arg1_number, ret_list_dict_any, f_gettabinfo}, {"gettabvar", 2, 3, FEARG_1, arg3_number_string_any, diff --git a/src/evalvars.c b/src/evalvars.c index 038d7ed5a..f5fef04ad 100644 --- a/src/evalvars.c +++ b/src/evalvars.c @@ -160,7 +160,8 @@ static struct vimvar {VV_NAME("python3_version", VAR_NUMBER), NULL, VV_RO}, {VV_NAME("t_typealias", VAR_NUMBER), NULL, VV_RO}, {VV_NAME("t_enum", VAR_NUMBER), NULL, VV_RO}, - {VV_NAME("t_enumvalue", VAR_NUMBER), NULL, VV_RO} + {VV_NAME("t_enumvalue", VAR_NUMBER), NULL, VV_RO}, + {VV_NAME("stacktrace", VAR_LIST), &t_list_string, VV_RO}, }; // shorthand diff --git a/src/ex_eval.c b/src/ex_eval.c index 79e9d9435..e996ce2a1 100644 --- a/src/ex_eval.c +++ b/src/ex_eval.c @@ -562,6 +562,10 @@ throw_exception(void *value, except_type_T type, char_u *cmdname) excp->throw_lnum = SOURCING_LNUM; } + excp->stacktrace = stacktrace_create(); + if (excp->stacktrace != NULL) + excp->stacktrace->lv_refcount = 1; + if (p_verbose >= 13 || debug_break_level > 0) { int save_msg_silent = msg_silent; @@ -647,6 +651,7 @@ discard_exception(except_T *excp, int was_finished) if (excp->type == ET_ERROR) free_msglist(excp->messages); vim_free(excp->throw_name); + list_unref(excp->stacktrace); vim_free(excp); } @@ -671,6 +676,7 @@ catch_exception(except_T *excp) excp->caught = caught_stack; caught_stack = excp; set_vim_var_string(VV_EXCEPTION, (char_u *)excp->value, -1); + set_vim_var_list(VV_STACKTRACE, excp->stacktrace); if (*excp->throw_name != NUL) { if (excp->throw_lnum != 0) @@ -721,6 +727,7 @@ finish_exception(except_T *excp) if (caught_stack != NULL) { set_vim_var_string(VV_EXCEPTION, (char_u *)caught_stack->value, -1); + set_vim_var_list(VV_STACKTRACE, caught_stack->stacktrace); if (*caught_stack->throw_name != NUL) { if (caught_stack->throw_lnum != 0) @@ -741,6 +748,7 @@ finish_exception(except_T *excp) { set_vim_var_string(VV_EXCEPTION, 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'. diff --git a/src/proto/dict.pro b/src/proto/dict.pro index b1ceeccd4..421b16946 100644 --- a/src/proto/dict.pro +++ b/src/proto/dict.pro @@ -22,6 +22,7 @@ int dict_add_string_len(dict_T *d, char *key, char_u *str, int len); int dict_add_list(dict_T *d, char *key, list_T *list); int dict_add_tv(dict_T *d, char *key, typval_T *tv); int dict_add_callback(dict_T *d, char *key, callback_T *cb); +int dict_add_func(dict_T *d, char *key, ufunc_T *fp); void dict_iterate_start(typval_T *var, dict_iterator_T *iter); char_u *dict_iterate_next(dict_iterator_T *iter, typval_T **tv_result); int dict_add_dict(dict_T *d, char *key, dict_T *dict); diff --git a/src/proto/scriptfile.pro b/src/proto/scriptfile.pro index c8ff04a11..e81d16b97 100644 --- a/src/proto/scriptfile.pro +++ b/src/proto/scriptfile.pro @@ -5,6 +5,8 @@ estack_T *estack_push_ufunc(ufunc_T *ufunc, long lnum); int estack_top_is_ufunc(ufunc_T *ufunc, long lnum); estack_T *estack_pop(void); char_u *estack_sfile(estack_arg_T which); +list_T *stacktrace_create(void); +void f_getstacktrace(typval_T *argvars, typval_T *rettv); void ex_runtime(exarg_T *eap); void set_context_in_runtime_cmd(expand_T *xp, char_u *arg); int find_script_by_name(char_u *name); diff --git a/src/scriptfile.c b/src/scriptfile.c index 711f576c0..856d00fbb 100644 --- a/src/scriptfile.c +++ b/src/scriptfile.c @@ -237,6 +237,89 @@ estack_sfile(estack_arg_T which UNUSED) #endif } +#ifdef FEAT_EVAL + static void +stacktrace_push_item( + list_T *l, + ufunc_T *fp, + char_u *event, + linenr_T lnum, + char_u *filepath) +{ + dict_T *d; + typval_T tv; + + d = dict_alloc_lock(VAR_FIXED); + if (d == NULL) + return; + + tv.v_type = VAR_DICT; + tv.v_lock = VAR_LOCKED; + tv.vval.v_dict = d; + + if (fp != NULL) + dict_add_func(d, "funcref", fp); + if (event != NULL) + dict_add_string(d, "event", event); + dict_add_number(d, "lnum", lnum); + dict_add_string(d, "filepath", filepath); + + list_append_tv(l, &tv); +} + +/* + * Create the stacktrace from exestack. + */ + list_T * +stacktrace_create(void) +{ + list_T *l; + int i; + + l = list_alloc(); + if (l == NULL) + return NULL; + + for (i = 0; i < exestack.ga_len; ++i) + { + estack_T *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); + else if (entry->es_type == ETYPE_UFUNC) + { + ufunc_T *fp = entry->es_info.ufunc; + sctx_T sctx = fp->uf_script_ctx; + char_u *filepath = sctx.sc_sid > 0 ? + get_scriptname(sctx.sc_sid) : (char_u *)""; + + lnum += sctx.sc_lnum; + stacktrace_push_item(l, fp, NULL, lnum, filepath); + } + else if (entry->es_type == ETYPE_AUCMD) + { + sctx_T sctx = *acp_script_ctx(entry->es_info.aucmd); + char_u *filepath = sctx.sc_sid > 0 ? + get_scriptname(sctx.sc_sid) : (char_u *)""; + + lnum += sctx.sc_lnum; + stacktrace_push_item(l, NULL, entry->es_name, lnum, filepath); + } + } + return l; +} + +/* + * getstacktrace() function + */ + void +f_getstacktrace(typval_T *argvars UNUSED, typval_T *rettv) +{ + rettv_list_set(rettv, stacktrace_create()); +} +#endif + /* * Get DIP_ flags from the [where] argument of a :runtime command. * "*argp" is advanced to after the [where] argument if it is found. diff --git a/src/structs.h b/src/structs.h index c036bf15a..87aca3074 100644 --- a/src/structs.h +++ b/src/structs.h @@ -63,6 +63,17 @@ typedef struct growarray #define GA_EMPTY {0, 0, 0, 0, NULL} +// On rare systems "char" is unsigned, sometimes we really want a signed 8-bit +// value. +typedef signed char int8_T; +typedef double float_T; + +typedef struct typval_S typval_T; +typedef struct listvar_S list_T; +typedef struct dictvar_S dict_T; +typedef struct partial_S partial_T; +typedef struct blobvar_S blob_T; + typedef struct window_S win_T; typedef struct wininfo_S wininfo_T; typedef struct frame_S frame_T; @@ -1087,6 +1098,7 @@ struct vim_exception struct msglist *messages; // message(s) causing error exception char_u *throw_name; // name 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 }; @@ -1447,18 +1459,6 @@ typedef long_u hash_T; // Type for hi_hash # endif #endif -// On rare systems "char" is unsigned, sometimes we really want a signed 8-bit -// value. -typedef signed char int8_T; - -typedef double float_T; - -typedef struct typval_S typval_T; -typedef struct listvar_S list_T; -typedef struct dictvar_S dict_T; -typedef struct partial_S partial_T; -typedef struct blobvar_S blob_T; - // Struct that holds both a normal function name and a partial_T, as used for a // callback argument. // When used temporarily "cb_name" is not allocated. The refcounts to either diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak index 728535483..13f483031 100644 --- a/src/testdir/Make_all.mak +++ b/src/testdir/Make_all.mak @@ -292,6 +292,7 @@ NEW_TESTS = \ test_spell_utf8 \ test_spellfile \ test_spellrare \ + test_stacktrace \ test_startup \ test_startup_utf8 \ test_stat \ @@ -545,6 +546,7 @@ NEW_TESTS_RES = \ test_spell_utf8.res \ test_spellfile.res \ test_spellrare.res \ + test_stacktrace.res \ test_startup.res \ test_stat.res \ test_statusline.res \ diff --git a/src/testdir/test_stacktrace.vim b/src/testdir/test_stacktrace.vim new file mode 100644 index 000000000..8cdb6efb5 --- /dev/null +++ b/src/testdir/test_stacktrace.vim @@ -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: 617, 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 diff --git a/src/version.c b/src/version.c index 48a060653..cb1129d80 100644 --- a/src/version.c +++ b/src/version.c @@ -704,6 +704,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 984, /**/ 983, /**/ diff --git a/src/vim.h b/src/vim.h index f8090bf7d..25c9d120d 100644 --- a/src/vim.h +++ b/src/vim.h @@ -2190,7 +2190,8 @@ typedef int sock_T; #define VV_TYPE_TYPEALIAS 107 #define VV_TYPE_ENUM 108 #define VV_TYPE_ENUMVALUE 109 -#define VV_LEN 110 // number of v: vars +#define VV_STACKTRACE 110 +#define VV_LEN 111 // number of v: vars // used for v_number in VAR_BOOL and VAR_SPECIAL #define VVAL_FALSE 0L // VAR_BOOL -- -- You received this message from the "vim_dev" maillist. Do not top-post! Type your reply below the text you are replying to. For more information, visit http://www.vim.org/maillist.php --- You received this message because you are subscribed to the Google Groups "vim_dev" group. To unsubscribe from this group and stop receiving emails from it, send an email to vim_dev+unsubscr...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/vim_dev/E1tTOmo-00364g-1C%40256bit.org.