# HG changeset patch # User Bram Moolenaar # Date 1662237903 -7200 # Node ID 6cf788ab844cc5265d65db8ff36ca3e36fb6a2da # Parent a8f1fbaa43c8cccc77b5e9a19540f5b839e99555 patch 9.0.0370: cleaning up afterwards can make a function messy Commit: https://github.com/vim/vim/commit/1d84f7608f1e41dad03b8cc7925895437775f7c0 Author: Bram Moolenaar Date: Sat Sep 3 21:35:53 2022 +0100 patch 9.0.0370: cleaning up afterwards can make a function messy Problem: Cleaning up afterwards can make a function messy. Solution: Add the :defer command. diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -2979,6 +2979,62 @@ is used as a method: > let y = GetList()->Filter() +CLEANING UP IN A FUNCTION ~ + *:defer* +:defer {func}({args}) Call {func} when the current function is done. + {args} are evaluated here. + +Quite often a command in a function has a global effect, which must be undone +when the function finishes. Handling this in all kinds of situations can be a +hassle. Especially when an unexpected error is encountered. This can be done +with `try` / `finally` blocks, but this gets complicated when there is more +than one. + +A much simpler solution is using `defer`. It schedules a function call when +the function is returning, no matter if there is an error. Example: > + func Filter(text) + call writefile(a:text, 'Tempfile') + call system('filter < Tempfile > Outfile') + call Handle('Outfile') + call delete('Tempfile') + call delete('Outfile') + endfunc + +Here 'Tempfile' and 'Outfile' will not be deleted if something causes the +function to abort. `:defer` can be used to avoid that: > + func Filter(text) + call writefile(a:text, 'Tempfile') + defer delete('Tempfile') + defer delete('Outfile') + call system('filter < Tempfile > Outfile') + call Handle('Outfile') + endfunc + +Note that deleting "Outfile" is scheduled before calling system(), since it +can be created even when `system()` fails. + +The defered functions are called in reverse order, the last one added is +executed first. A useless example: > + func Useless() + for s in range(3) + defer execute('echomsg "number ' .. s .. '"') + endfor + endfunc + +Now `:messages` shows: + number 2 + number 1 + number 0 + +Any return value of the deferred function is discarded. The function cannot +be followed by anything, such as "->func" or ".member". Currently `:defer +GetArg()->TheFunc()` does not work, it may work in a later version. + +Errors are reported but do not cause aborting execution of deferred functions. + +No range is accepted. + + AUTOMATICALLY LOADING FUNCTIONS ~ *autoload-functions* When using many or large functions, it's possible to automatically define them diff --git a/src/ex_cmdidxs.h b/src/ex_cmdidxs.h --- a/src/ex_cmdidxs.h +++ b/src/ex_cmdidxs.h @@ -9,28 +9,28 @@ static const unsigned short cmdidxs1[26] /* b */ 21, /* c */ 45, /* d */ 112, - /* e */ 137, - /* f */ 166, - /* g */ 183, - /* h */ 189, - /* i */ 199, - /* j */ 219, - /* k */ 221, - /* l */ 226, - /* m */ 289, - /* n */ 307, - /* o */ 327, - /* p */ 339, - /* q */ 378, - /* r */ 381, - /* s */ 401, - /* t */ 471, - /* u */ 517, - /* v */ 528, - /* w */ 549, - /* x */ 563, - /* y */ 573, - /* z */ 574 + /* e */ 138, + /* f */ 167, + /* g */ 184, + /* h */ 190, + /* i */ 200, + /* j */ 220, + /* k */ 222, + /* l */ 227, + /* m */ 290, + /* n */ 308, + /* o */ 328, + /* p */ 340, + /* q */ 379, + /* r */ 382, + /* s */ 402, + /* t */ 472, + /* u */ 518, + /* v */ 529, + /* w */ 550, + /* x */ 564, + /* y */ 574, + /* z */ 575 }; /* @@ -44,7 +44,7 @@ static const unsigned char cmdidxs2[26][ /* a */ { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 0, 0, 0, 8, 17, 0, 18, 0, 0, 0, 0, 0 }, /* b */ { 2, 0, 0, 5, 6, 8, 0, 0, 0, 0, 0, 9, 10, 11, 12, 13, 0, 14, 0, 0, 0, 0, 23, 0, 0, 0 }, /* c */ { 3, 12, 16, 18, 20, 22, 25, 0, 0, 0, 0, 33, 38, 41, 47, 57, 59, 60, 61, 0, 63, 0, 66, 0, 0, 0 }, - /* d */ { 0, 0, 0, 0, 0, 0, 0, 0, 8, 18, 0, 19, 0, 0, 20, 0, 0, 22, 23, 0, 0, 0, 0, 0, 0, 0 }, + /* d */ { 0, 0, 0, 0, 0, 0, 0, 0, 9, 19, 0, 20, 0, 0, 21, 0, 0, 23, 24, 0, 0, 0, 0, 0, 0, 0 }, /* e */ { 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 9, 11, 12, 0, 0, 0, 0, 0, 0, 0, 23, 0, 24, 0, 0 }, /* f */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0 }, /* g */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 4, 5, 0, 0, 0, 0 }, @@ -69,4 +69,4 @@ static const unsigned char cmdidxs2[26][ /* z */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }; -static const int command_count = 591; +static const int command_count = 592; diff --git a/src/ex_cmds.h b/src/ex_cmds.h --- a/src/ex_cmds.h +++ b/src/ex_cmds.h @@ -467,6 +467,9 @@ EXCMD(CMD_def, "def", ex_function, EXCMD(CMD_defcompile, "defcompile", ex_defcompile, EX_SBOXOK|EX_CMDWIN|EX_LOCK_OK|EX_TRLBAR|EX_EXTRA, ADDR_NONE), +EXCMD(CMD_defer, "defer", ex_call, + EX_NEEDARG|EX_EXTRA|EX_NOTRLCOM|EX_EXPR_ARG|EX_SBOXOK|EX_CMDWIN|EX_LOCK_OK, + ADDR_NONE), EXCMD(CMD_delcommand, "delcommand", ex_delcommand, EX_NEEDARG|EX_WORD1|EX_TRLBAR|EX_CMDWIN|EX_LOCK_OK, ADDR_NONE), diff --git a/src/proto/userfunc.pro b/src/proto/userfunc.pro --- a/src/proto/userfunc.pro +++ b/src/proto/userfunc.pro @@ -58,6 +58,7 @@ void func_ptr_unref(ufunc_T *fp); void func_ref(char_u *name); void func_ptr_ref(ufunc_T *fp); void ex_return(exarg_T *eap); +void handle_defer(void); void ex_call(exarg_T *eap); int do_return(exarg_T *eap, int reanimate, int is_cmd, void *rettv); void discard_pending_return(void *rettv); diff --git a/src/proto/vim9cmds.pro b/src/proto/vim9cmds.pro --- a/src/proto/vim9cmds.pro +++ b/src/proto/vim9cmds.pro @@ -21,6 +21,7 @@ char_u *compile_finally(char_u *arg, cct char_u *compile_endtry(char_u *arg, cctx_T *cctx); char_u *compile_throw(char_u *arg, cctx_T *cctx); char_u *compile_eval(char_u *arg, cctx_T *cctx); +char_u *compile_defer(char_u *arg_start, cctx_T *cctx); char_u *compile_mult_expr(char_u *arg, int cmdidx, cctx_T *cctx); char_u *compile_put(char_u *arg, exarg_T *eap, cctx_T *cctx); char_u *compile_exec(char_u *line_arg, exarg_T *eap, cctx_T *cctx); diff --git a/src/proto/vim9expr.pro b/src/proto/vim9expr.pro --- a/src/proto/vim9expr.pro +++ b/src/proto/vim9expr.pro @@ -4,6 +4,7 @@ void clear_ppconst(ppconst_T *ppconst); int compile_member(int is_slice, int *keeping_dict, cctx_T *cctx); int compile_load_scriptvar(cctx_T *cctx, char_u *name, char_u *start, char_u **end); int compile_load(char_u **arg, char_u *end_arg, cctx_T *cctx, int is_expr, int error); +int compile_arguments(char_u **arg, cctx_T *cctx, int *argcount, ca_special_T special_fn); char_u *to_name_end(char_u *arg, int use_namespace); char_u *to_name_const_end(char_u *arg); int get_lambda_tv_and_compile(char_u **arg, typval_T *rettv, int types_optional, evalarg_T *evalarg); diff --git a/src/proto/vim9instr.pro b/src/proto/vim9instr.pro --- a/src/proto/vim9instr.pro +++ b/src/proto/vim9instr.pro @@ -23,7 +23,7 @@ int generate_PUSHS(cctx_T *cctx, char_u int generate_PUSHCHANNEL(cctx_T *cctx); int generate_PUSHJOB(cctx_T *cctx); int generate_PUSHBLOB(cctx_T *cctx, blob_T *blob); -int generate_PUSHFUNC(cctx_T *cctx, char_u *name, type_T *type); +int generate_PUSHFUNC(cctx_T *cctx, char_u *name, type_T *type, int may_prefix); int generate_AUTOLOAD(cctx_T *cctx, char_u *name, type_T *type); int generate_GETITEM(cctx_T *cctx, int index, int with_op); int generate_SLICE(cctx_T *cctx, int count); @@ -52,6 +52,7 @@ int generate_BLOBAPPEND(cctx_T *cctx); int generate_CALL(cctx_T *cctx, ufunc_T *ufunc, int pushed_argcount); int generate_UCALL(cctx_T *cctx, char_u *name, int argcount); int generate_PCALL(cctx_T *cctx, int argcount, char_u *name, type_T *type, int at_top); +int generate_DEFER(cctx_T *cctx, int var_idx, int argcount); int generate_STRINGMEMBER(cctx_T *cctx, char_u *name, size_t len); int generate_ECHO(cctx_T *cctx, int with_white, int count); int generate_MULT_EXPR(cctx_T *cctx, isntype_T isn_type, int count); diff --git a/src/structs.h b/src/structs.h --- a/src/structs.h +++ b/src/structs.h @@ -1753,6 +1753,7 @@ struct funccall_S linenr_T breakpoint; // next line with breakpoint or zero int dbg_tick; // debug_tick when breakpoint was set int level; // top nesting level of executed function + garray_T fc_defer; // functions to be called on return #ifdef FEAT_PROFILE proftime_T prof_child; // time spent in a child #endif @@ -1767,6 +1768,14 @@ struct funccall_S // "func" }; +// structure used as item in "fc_defer" +typedef struct +{ + char_u *dr_name; // function name, allocated + typval_T dr_argvars[MAX_FUNC_ARGS + 1]; + int dr_argcount; +} defer_T; + /* * Struct used by trans_function_name() */ @@ -2850,7 +2859,7 @@ struct file_buffer int b_u_synced; // entry lists are synced long b_u_seq_last; // last used undo sequence number long b_u_save_nr_last; // counter for last file write - long b_u_seq_cur; // hu_seq of header below which we are now + long b_u_seq_cur; // uh_seq of header below which we are now time_T b_u_time_cur; // uh_time of header below which we are now long b_u_save_nr_cur; // file write nr after which we are now diff --git a/src/testdir/test_user_func.vim b/src/testdir/test_user_func.vim --- a/src/testdir/test_user_func.vim +++ b/src/testdir/test_user_func.vim @@ -529,4 +529,36 @@ func Test_funcdef_alloc_failure() bw! endfunc +func AddDefer(arg) + call extend(g:deferred, [a:arg]) +endfunc + +func WithDeferTwo() + call extend(g:deferred, ['in Two']) + for nr in range(3) + defer AddDefer('Two' .. nr) + endfor + call extend(g:deferred, ['end Two']) +endfunc + +func WithDeferOne() + call extend(g:deferred, ['in One']) + call writefile(['text'], 'Xfuncdefer') + defer delete('Xfuncdefer') + defer AddDefer('One') + call WithDeferTwo() + call extend(g:deferred, ['end One']) +endfunc + +func Test_defer() + let g:deferred = [] + call WithDeferOne() + + call assert_equal(['in One', 'in Two', 'end Two', 'Two2', 'Two1', 'Two0', 'end One', 'One'], g:deferred) + unlet g:deferred + + call assert_equal('', glob('Xfuncdefer')) +endfunc + + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/testdir/test_vim9_disassemble.vim b/src/testdir/test_vim9_disassemble.vim --- a/src/testdir/test_vim9_disassemble.vim +++ b/src/testdir/test_vim9_disassemble.vim @@ -2900,4 +2900,19 @@ def Test_disassemble_bitshift() '10 RETURN void', instr) enddef +def s:OneDefer() + defer delete("file") +enddef + +def Test_disassemble_defer() + var instr = execute('disassemble s:OneDefer') + assert_match('OneDefer\_s*' .. + 'defer delete("file")\_s*' .. + '\d PUSHFUNC "delete"\_s*' .. + '\d PUSHS "file"\_s*' .. + '\d DEFER 1 args\_s*' .. + '\d RETURN\_s*', + instr) +enddef + " vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker diff --git a/src/testdir/test_vim9_func.vim b/src/testdir/test_vim9_func.vim --- a/src/testdir/test_vim9_func.vim +++ b/src/testdir/test_vim9_func.vim @@ -4272,6 +4272,36 @@ def Test_cexpr_errmsg_line_number() v9.CheckScriptFailure(lines, 'E777', 2) enddef +def AddDefer(s: string) + g:deferred->extend([s]) +enddef + +def DeferTwo() + g:deferred->extend(['in Two']) + for n in range(3) + defer g:AddDefer('two' .. n) + endfor + g:deferred->extend(['end Two']) +enddef + +def DeferOne() + g:deferred->extend(['in One']) + defer g:AddDefer('one') + g:DeferTwo() + g:deferred->extend(['end One']) + + writefile(['text'], 'XdeferFile') + defer delete('XdeferFile') +enddef + +def Test_defer() + g:deferred = [] + g:DeferOne() + assert_equal(['in One', 'in Two', 'end Two', 'two2', 'two1', 'two0', 'end One', 'one'], g:deferred) + unlet g:deferred + assert_equal('', glob('XdeferFile')) +enddef + " The following messes up syntax highlight, keep near the end. if has('python3') def Test_python3_command() diff --git a/src/userfunc.c b/src/userfunc.c --- a/src/userfunc.c +++ b/src/userfunc.c @@ -1728,44 +1728,36 @@ emsg_funcname(char *ermsg, char_u *name) } /* - * Allocate a variable for the result of a function. - * Return OK or FAIL. + * Get function arguments at "*arg" and advance it. + * Return them in "*argvars[MAX_FUNC_ARGS + 1]" and the count in "argcount". */ - int -get_func_tv( - char_u *name, // name of the function - int len, // length of "name" or -1 to use strlen() - typval_T *rettv, - char_u **arg, // argument, pointing to the '(' - evalarg_T *evalarg, // for line continuation - funcexe_T *funcexe) // various values + static int +get_func_arguments( + char_u **arg, + evalarg_T *evalarg, + int partial_argc, + typval_T *argvars, + int *argcount) { - char_u *argp; + char_u *argp = *arg; int ret = OK; - typval_T argvars[MAX_FUNC_ARGS + 1]; // vars for arguments - int argcount = 0; // number of arguments found int vim9script = in_vim9script(); int evaluate = evalarg == NULL ? FALSE : (evalarg->eval_flags & EVAL_EVALUATE); - /* - * Get the arguments. - */ - argp = *arg; - while (argcount < MAX_FUNC_ARGS - (funcexe->fe_partial == NULL ? 0 - : funcexe->fe_partial->pt_argc)) + while (*argcount < MAX_FUNC_ARGS - partial_argc) { // skip the '(' or ',' and possibly line breaks argp = skipwhite_and_linebreak(argp + 1, evalarg); if (*argp == ')' || *argp == ',' || *argp == NUL) break; - if (eval1(&argp, &argvars[argcount], evalarg) == FAIL) + if (eval1(&argp, &argvars[*argcount], evalarg) == FAIL) { ret = FAIL; break; } - ++argcount; + ++*argcount; // The comma should come right after the argument, but this wasn't // checked previously, thus only enforce it in Vim9 script. if (vim9script) @@ -1791,11 +1783,41 @@ get_func_tv( break; } } + argp = skipwhite_and_linebreak(argp, evalarg); if (*argp == ')') ++argp; else ret = FAIL; + *arg = argp; + return ret; +} + +/* + * Call a function and put the result in "rettv". + * Return OK or FAIL. + */ + int +get_func_tv( + char_u *name, // name of the function + int len, // length of "name" or -1 to use strlen() + typval_T *rettv, + char_u **arg, // argument, pointing to the '(' + evalarg_T *evalarg, // for line continuation + funcexe_T *funcexe) // various values +{ + char_u *argp; + int ret = OK; + typval_T argvars[MAX_FUNC_ARGS + 1]; // vars for arguments + int argcount = 0; // number of arguments found + int vim9script = in_vim9script(); + int evaluate = evalarg == NULL + ? FALSE : (evalarg->eval_flags & EVAL_EVALUATE); + + argp = *arg; + ret = get_func_arguments(&argp, evalarg, + (funcexe->fe_partial == NULL ? 0 : funcexe->fe_partial->pt_argc), + argvars, &argcount); if (ret == OK) { @@ -2884,6 +2906,9 @@ call_user_func( do_cmdline(NULL, get_func_line, (void *)fc, DOCMD_NOWAIT|DOCMD_VERBOSE|DOCMD_REPEAT); + // Invoke functions added with ":defer". + handle_defer(); + --RedrawingDisabled; // when the function was aborted because of an error, return -1 @@ -5457,8 +5482,164 @@ ex_return(exarg_T *eap) clear_evalarg(&evalarg, eap); } + static int +ex_call_inner( + exarg_T *eap, + char_u *name, + char_u **arg, + char_u *startarg, + funcexe_T *funcexe_init, + evalarg_T *evalarg) +{ + linenr_T lnum; + int doesrange; + typval_T rettv; + int failed = FALSE; + + /* + * When skipping, evaluate the function once, to find the end of the + * arguments. + * When the function takes a range, this is discovered after the first + * call, and the loop is broken. + */ + if (eap->skip) + { + ++emsg_skip; + lnum = eap->line2; // do it once, also with an invalid range + } + else + lnum = eap->line1; + for ( ; lnum <= eap->line2; ++lnum) + { + funcexe_T funcexe; + + if (!eap->skip && eap->addr_count > 0) + { + if (lnum > curbuf->b_ml.ml_line_count) + { + // If the function deleted lines or switched to another buffer + // the line number may become invalid. + emsg(_(e_invalid_range)); + break; + } + curwin->w_cursor.lnum = lnum; + curwin->w_cursor.col = 0; + curwin->w_cursor.coladd = 0; + } + *arg = startarg; + + funcexe = *funcexe_init; + funcexe.fe_doesrange = &doesrange; + rettv.v_type = VAR_UNKNOWN; // clear_tv() uses this + if (get_func_tv(name, -1, &rettv, arg, evalarg, &funcexe) == FAIL) + { + failed = TRUE; + break; + } + if (has_watchexpr()) + dbg_check_breakpoint(eap); + + // Handle a function returning a Funcref, Dictionary or List. + if (handle_subscript(arg, NULL, &rettv, + eap->skip ? NULL : &EVALARG_EVALUATE, TRUE) == FAIL) + { + failed = TRUE; + break; + } + + clear_tv(&rettv); + if (doesrange || eap->skip) + break; + + // Stop when immediately aborting on error, or when an interrupt + // occurred or an exception was thrown but not caught. + // get_func_tv() returned OK, so that the check for trailing + // characters below is executed. + if (aborting()) + break; + } + if (eap->skip) + --emsg_skip; + return failed; +} + +/* + * Core part of ":defer func(arg)". "arg" points to the "(" and is advanced. + * Returns FAIL or OK. + */ + static int +ex_defer_inner(char_u *name, char_u **arg, evalarg_T *evalarg) +{ + typval_T argvars[MAX_FUNC_ARGS + 1]; // vars for arguments + int argcount = 0; // number of arguments found + defer_T *dr; + int ret = FAIL; + char_u *saved_name; + + if (current_funccal == NULL) + { + semsg(_(e_str_not_inside_function), "defer"); + return FAIL; + } + if (get_func_arguments(arg, evalarg, FALSE, argvars, &argcount) == FAIL) + goto theend; + saved_name = vim_strsave(name); + if (saved_name == NULL) + goto theend; + + if (current_funccal->fc_defer.ga_itemsize == 0) + ga_init2(¤t_funccal->fc_defer, sizeof(defer_T), 10); + if (ga_grow(¤t_funccal->fc_defer, 1) == FAIL) + goto theend; + dr = ((defer_T *)current_funccal->fc_defer.ga_data) + + current_funccal->fc_defer.ga_len++; + dr->dr_name = saved_name; + dr->dr_argcount = argcount; + while (argcount > 0) + { + --argcount; + dr->dr_argvars[argcount] = argvars[argcount]; + } + ret = OK; + +theend: + while (--argcount >= 0) + clear_tv(&argvars[argcount]); + return ret; +} + +/* + * Invoked after a functions has finished: invoke ":defer" functions. + */ + void +handle_defer(void) +{ + int idx; + + for (idx = current_funccal->fc_defer.ga_len - 1; idx >= 0; --idx) + { + funcexe_T funcexe; + typval_T rettv; + defer_T *dr = ((defer_T *)current_funccal->fc_defer.ga_data) + idx; + int i; + + CLEAR_FIELD(funcexe); + funcexe.fe_evaluate = TRUE; + + rettv.v_type = VAR_UNKNOWN; // clear_tv() uses this + call_func(dr->dr_name, -1, &rettv, + dr->dr_argcount, dr->dr_argvars, &funcexe); + clear_tv(&rettv); + vim_free(dr->dr_name); + for (i = dr->dr_argcount - 1; i >= 0; --i) + clear_tv(&dr->dr_argvars[i]); + } + ga_clear(¤t_funccal->fc_defer); +} + /* * ":1,25call func(arg1, arg2)" function call. + * ":defer func(arg1, arg2)" deferred function call. */ void ex_call(exarg_T *eap) @@ -5468,9 +5649,6 @@ ex_call(exarg_T *eap) char_u *name; char_u *tofree; int len; - typval_T rettv; - linenr_T lnum; - int doesrange; int failed = FALSE; funcdict_T fudi; partial_T *partial = NULL; @@ -5482,6 +5660,8 @@ ex_call(exarg_T *eap) fill_evalarg_from_eap(&evalarg, eap, eap->skip); if (eap->skip) { + typval_T rettv; + // trans_function_name() doesn't work well when skipping, use eval0() // instead to skip to any following command, e.g. for: // :if 0 | call dict.foo().bar() | endif @@ -5531,82 +5711,29 @@ ex_call(exarg_T *eap) goto end; } - /* - * When skipping, evaluate the function once, to find the end of the - * arguments. - * When the function takes a range, this is discovered after the first - * call, and the loop is broken. - */ - if (eap->skip) - { - ++emsg_skip; - lnum = eap->line2; // do it once, also with an invalid range + if (eap->cmdidx == CMD_defer) + { + arg = startarg; + failed = ex_defer_inner(name, &arg, &evalarg) == FAIL; } else - lnum = eap->line1; - for ( ; lnum <= eap->line2; ++lnum) { funcexe_T funcexe; - if (!eap->skip && eap->addr_count > 0) - { - if (lnum > curbuf->b_ml.ml_line_count) - { - // If the function deleted lines or switched to another buffer - // the line number may become invalid. - emsg(_(e_invalid_range)); - break; - } - curwin->w_cursor.lnum = lnum; - curwin->w_cursor.col = 0; - curwin->w_cursor.coladd = 0; - } - arg = startarg; - CLEAR_FIELD(funcexe); + funcexe.fe_check_type = type; + funcexe.fe_partial = partial; + funcexe.fe_selfdict = fudi.fd_dict; funcexe.fe_firstline = eap->line1; funcexe.fe_lastline = eap->line2; - funcexe.fe_doesrange = &doesrange; + funcexe.fe_found_var = found_var; funcexe.fe_evaluate = !eap->skip; - funcexe.fe_partial = partial; - funcexe.fe_selfdict = fudi.fd_dict; - funcexe.fe_check_type = type; - funcexe.fe_found_var = found_var; - rettv.v_type = VAR_UNKNOWN; // clear_tv() uses this - if (get_func_tv(name, -1, &rettv, &arg, &evalarg, &funcexe) == FAIL) - { - failed = TRUE; - break; - } - if (has_watchexpr()) - dbg_check_breakpoint(eap); - - // Handle a function returning a Funcref, Dictionary or List. - if (handle_subscript(&arg, NULL, &rettv, - eap->skip ? NULL : &EVALARG_EVALUATE, TRUE) == FAIL) - { - failed = TRUE; - break; - } - - clear_tv(&rettv); - if (doesrange || eap->skip) - break; - - // Stop when immediately aborting on error, or when an interrupt - // occurred or an exception was thrown but not caught. - // get_func_tv() returned OK, so that the check for trailing - // characters below is executed. - if (aborting()) - break; - } - if (eap->skip) - --emsg_skip; + failed = ex_call_inner(eap, name, &arg, startarg, &funcexe, &evalarg); + } // When inside :try we need to check for following "| catch" or "| endtry". // Not when there was an error, but do check if an exception was thrown. - if ((!aborting() || did_throw) - && (!failed || eap->cstack->cs_trylevel > 0)) + if ((!aborting() || did_throw) && (!failed || eap->cstack->cs_trylevel > 0)) { // Check for trailing illegal characters and a following command. arg = skipwhite(arg); diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -708,6 +708,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 370, +/**/ 369, /**/ 368, diff --git a/src/vim9.h b/src/vim9.h --- a/src/vim9.h +++ b/src/vim9.h @@ -113,6 +113,7 @@ typedef enum { ISN_FUNCREF, // push a function ref to dfunc isn_arg.funcref ISN_NEWFUNC, // create a global function from a lambda function ISN_DEF, // list functions + ISN_DEFER, // :defer argument count is isn_arg.number // expression operations ISN_JUMP, // jump if condition is matched isn_arg.jump @@ -419,6 +420,12 @@ typedef struct { int dbg_break_lnum; // first line to break after } debug_T; +// arguments to ISN_DEFER +typedef struct { + int defer_var_idx; // local variable index for defer list + int defer_argcount; // number of arguments +} deferins_T; + /* * Instruction */ @@ -468,6 +475,7 @@ struct isn_S { tobool_T tobool; getitem_T getitem; debug_T debug; + deferins_T defer; } isn_arg; }; @@ -498,6 +506,9 @@ struct dfunc_S { int df_varcount; // number of local variables int df_has_closure; // one if a closure was created + int df_defer_var_idx; // index of local variable that has a list + // of deferred function calls; zero if not + // set }; // Number of entries used by stack frame for a function call. @@ -735,6 +746,15 @@ struct cctx_S { // lhs_name is not NULL }; +/* + * List of special functions for "compile_arguments()". + */ +typedef enum { + CA_NOT_SPECIAL, + CA_SEARCHPAIR, // {skip} in searchpair() and searchpairpos() + CA_SUBSTITUTE, // {sub} in substitute(), when prefixed with \= +} ca_special_T; + // flags for typval2type() #define TVTT_DO_MEMBER 1 #define TVTT_MORE_SPECIFIC 2 // get most specific type for member diff --git a/src/vim9cmds.c b/src/vim9cmds.c --- a/src/vim9cmds.c +++ b/src/vim9cmds.c @@ -1654,6 +1654,9 @@ compile_throw(char_u *arg, cctx_T *cctx return p; } +/* + * Compile an expression or function call. + */ char_u * compile_eval(char_u *arg, cctx_T *cctx) { @@ -1682,6 +1685,67 @@ compile_eval(char_u *arg, cctx_T *cctx) } /* + * Compile "defer func(arg)". + */ + char_u * +compile_defer(char_u *arg_start, cctx_T *cctx) +{ + char_u *p; + char_u *arg = arg_start; + int argcount = 0; + dfunc_T *dfunc; + type_T *type; + int func_idx; + + // Get a funcref for the function name. + // TODO: better way to find the "(". + p = vim_strchr(arg, '('); + if (p == NULL) + { + semsg(_(e_missing_parenthesis_str), arg); + return NULL; + } + *p = NUL; + func_idx = find_internal_func(arg); + if (func_idx >= 0) + // TODO: better type + generate_PUSHFUNC(cctx, (char_u *)internal_func_name(func_idx), + &t_func_any, FALSE); + else if (compile_expr0(&arg, cctx) == FAIL) + return NULL; + *p = '('; + + // check for function type + type = get_type_on_stack(cctx, 0); + if (type->tt_type != VAR_FUNC) + { + emsg(_(e_function_name_required)); + return NULL; + } + + // compile the arguments + arg = skipwhite(p + 1); + if (compile_arguments(&arg, cctx, &argcount, CA_NOT_SPECIAL) == FAIL) + return NULL; + + // TODO: check argument count with "type" + + dfunc = ((dfunc_T *)def_functions.ga_data) + cctx->ctx_ufunc->uf_dfunc_idx; + if (dfunc->df_defer_var_idx == 0) + { + lvar_T *lvar = reserve_local(cctx, (char_u *)"@defer@", 7, + TRUE, &t_list_any); + if (lvar == NULL) + return NULL; + dfunc->df_defer_var_idx = lvar->lv_idx + 1; + } + if (generate_DEFER(cctx, dfunc->df_defer_var_idx - 1, argcount) == FAIL) + return NULL; + + return skipwhite(arg); +} + +/* * compile "echo expr" * compile "echomsg expr" * compile "echoerr expr" diff --git a/src/vim9compile.c b/src/vim9compile.c --- a/src/vim9compile.c +++ b/src/vim9compile.c @@ -2373,7 +2373,7 @@ compile_assignment(char_u *arg, exarg_T r = generate_PUSHBLOB(cctx, blob_alloc()); break; case VAR_FUNC: - r = generate_PUSHFUNC(cctx, NULL, &t_func_void); + r = generate_PUSHFUNC(cctx, NULL, &t_func_void, TRUE); break; case VAR_LIST: r = generate_NEWLIST(cctx, 0, FALSE); @@ -2748,6 +2748,7 @@ compile_def_function( // Was compiled in this mode before: Free old instructions. delete_def_function_contents(dfunc, FALSE); ga_clear_strings(&dfunc->df_var_names); + dfunc->df_defer_var_idx = 0; } else { @@ -3249,6 +3250,10 @@ compile_def_function( line = compile_eval(p, &cctx); break; + case CMD_defer: + line = compile_defer(p, &cctx); + break; + case CMD_echo: case CMD_echon: case CMD_echoconsole: diff --git a/src/vim9execute.c b/src/vim9execute.c --- a/src/vim9execute.c +++ b/src/vim9execute.c @@ -101,9 +101,15 @@ struct ectx_S { static garray_T profile_info_ga = {0, 0, sizeof(profinfo_T), 20, NULL}; #endif +// Get pointer to item in the stack. +#define STACK_TV(idx) (((typval_T *)ectx->ec_stack.ga_data) + idx) + // Get pointer to item relative to the bottom of the stack, -1 is the last one. #define STACK_TV_BOT(idx) (((typval_T *)ectx->ec_stack.ga_data) + ectx->ec_stack.ga_len + (idx)) +// Get pointer to a local variable on the stack. Negative for arguments. +#define STACK_TV_VAR(idx) (((typval_T *)ectx->ec_stack.ga_data) + ectx->ec_frame_idx + STACK_FRAME_SIZE + idx) + void to_string_error(vartype_T vartype) { @@ -610,9 +616,6 @@ call_dfunc( return OK; } -// Get pointer to item in the stack. -#define STACK_TV(idx) (((typval_T *)ectx->ec_stack.ga_data) + idx) - // Double linked list of funcstack_T in use. static funcstack_T *first_funcstack = NULL; @@ -843,6 +846,89 @@ set_ref_in_funcstacks(int copyID) } /* + * Handle ISN_DEFER. Stack has a function reference and "argcount" arguments. + * The local variable that lists deferred functions is "var_idx". + * Returns OK or FAIL. + */ + static int +add_defer_func(int var_idx, int argcount, ectx_T *ectx) +{ + typval_T *defer_tv = STACK_TV_VAR(var_idx); + list_T *defer_l; + typval_T *func_tv; + list_T *l; + int i; + typval_T listval; + + if (defer_tv->v_type != VAR_LIST) + { + // first one, allocate the list + if (rettv_list_alloc(defer_tv) == FAIL) + return FAIL; + } + defer_l = defer_tv->vval.v_list; + + l = list_alloc_with_items(argcount + 1); + if (l == NULL) + return FAIL; + listval.v_type = VAR_LIST; + listval.vval.v_list = l; + listval.v_lock = 0; + if (list_insert_tv(defer_l, &listval, + defer_l == NULL ? NULL : defer_l->lv_first) == FAIL) + { + vim_free(l); + return FAIL; + } + + func_tv = STACK_TV_BOT(-argcount - 1); + // TODO: check type is a funcref + list_set_item(l, 0, func_tv); + + for (i = 1; i <= argcount; ++i) + list_set_item(l, i, STACK_TV_BOT(-argcount + i - 1)); + ectx->ec_stack.ga_len -= argcount + 1; + return OK; +} + +/* + * Invoked when returning from a function: Invoke any deferred calls. + */ + static void +invoke_defer_funcs(ectx_T *ectx) +{ + dfunc_T *dfunc = ((dfunc_T *)def_functions.ga_data) + + ectx->ec_dfunc_idx; + typval_T *defer_tv = STACK_TV_VAR(dfunc->df_defer_var_idx - 1); + listitem_T *li; + + if (defer_tv->v_type != VAR_LIST) + return; // no function added + for (li = defer_tv->vval.v_list->lv_first; li != NULL; li = li->li_next) + { + list_T *l = li->li_tv.vval.v_list; + typval_T rettv; + typval_T argvars[MAX_FUNC_ARGS]; + int i; + listitem_T *arg_li = l->lv_first; + funcexe_T funcexe; + + for (i = 0; i < l->lv_len - 1; ++i) + { + arg_li = arg_li->li_next; + argvars[i] = arg_li->li_tv; + } + + CLEAR_FIELD(funcexe); + funcexe.fe_evaluate = TRUE; + rettv.v_type = VAR_UNKNOWN; + (void)call_func(l->lv_first->li_tv.vval.v_string, -1, + &rettv, l->lv_len - 1, argvars, &funcexe); + clear_tv(&rettv); + } +} + +/* * Return from the current function. */ static int @@ -876,6 +962,9 @@ func_return(ectx_T *ectx) } #endif + if (dfunc->df_defer_var_idx > 0) + invoke_defer_funcs(ectx); + // No check for uf_refcount being zero, cannot think of a way that would // happen. --dfunc->df_ufunc->uf_calls; @@ -949,8 +1038,6 @@ func_return(ectx_T *ectx) return OK; } -#undef STACK_TV - /* * Prepare arguments and rettv for calling a builtin or user function. */ @@ -1732,16 +1819,6 @@ typedef struct subs_expr_S { int subs_status; } subs_expr_T; -// Get pointer to item in the stack. -#define STACK_TV(idx) (((typval_T *)ectx->ec_stack.ga_data) + idx) - -// Get pointer to item at the bottom of the stack, -1 is the bottom. -#undef STACK_TV_BOT -#define STACK_TV_BOT(idx) (((typval_T *)ectx->ec_stack.ga_data) + ectx->ec_stack.ga_len + idx) - -// Get pointer to a local variable on the stack. Negative for arguments. -#define STACK_TV_VAR(idx) (((typval_T *)ectx->ec_stack.ga_data) + ectx->ec_frame_idx + STACK_FRAME_SIZE + idx) - // Set when calling do_debug(). static ectx_T *debug_context = NULL; static int debug_var_count; @@ -3670,6 +3747,13 @@ exec_instructions(ectx_T *ectx) } break; + // :defer func(arg) + case ISN_DEFER: + if (add_defer_func(iptr->isn_arg.defer.defer_var_idx, + iptr->isn_arg.defer.defer_argcount, ectx) == FAIL) + goto on_error; + break; + // return from a :def function call without a value case ISN_RETURN_VOID: if (GA_GROW_FAILS(&ectx->ec_stack, 1)) @@ -5024,6 +5108,14 @@ on_fatal_error: done: ret = OK; theend: + { + dfunc_T *dfunc = ((dfunc_T *)def_functions.ga_data) + + ectx->ec_dfunc_idx; + + if (dfunc->df_defer_var_idx > 0) + invoke_defer_funcs(ectx); + } + dict_stack_clear(dict_stack_len_at_start); ectx->ec_trylevel_at_start = save_trylevel_at_start; return ret; @@ -5903,6 +5995,10 @@ list_instructions(char *pfx, isn_T *inst case ISN_PCALL_END: smsg("%s%4d PCALL end", pfx, current); break; + case ISN_DEFER: + smsg("%s%4d DEFER %d args", pfx, current, + (int)iptr->isn_arg.defer.defer_argcount); + break; case ISN_RETURN: smsg("%s%4d RETURN", pfx, current); break; diff --git a/src/vim9expr.c b/src/vim9expr.c --- a/src/vim9expr.c +++ b/src/vim9expr.c @@ -313,7 +313,7 @@ compile_load_scriptvar( // name. If a '(' follows it must be a function. Otherwise we // don't know, it can be "script.Func". if (cc == '(' || paren_follows_after_expr) - res = generate_PUSHFUNC(cctx, auto_name, &t_func_any); + res = generate_PUSHFUNC(cctx, auto_name, &t_func_any, TRUE); else res = generate_AUTOLOAD(cctx, auto_name, &t_any); vim_free(auto_name); @@ -329,7 +329,7 @@ compile_load_scriptvar( char_u sid_name[MAX_FUNC_NAME_LEN]; func_name_with_sid(exp_name, import->imp_sid, sid_name); - res = generate_PUSHFUNC(cctx, sid_name, &t_func_any); + res = generate_PUSHFUNC(cctx, sid_name, &t_func_any, TRUE); } else res = generate_OLDSCRIPT(cctx, ISN_LOADEXPORT, exp_name, @@ -353,7 +353,7 @@ compile_load_scriptvar( if (ufunc != NULL) { // function call or function reference - generate_PUSHFUNC(cctx, ufunc->uf_name, NULL); + generate_PUSHFUNC(cctx, ufunc->uf_name, NULL, TRUE); return OK; } return FAIL; @@ -387,7 +387,7 @@ generate_funcref(cctx_T *cctx, char_u *n if (func_needs_compiling(ufunc, compile_type) && compile_def_function(ufunc, TRUE, compile_type, NULL) == FAIL) return FAIL; - return generate_PUSHFUNC(cctx, ufunc->uf_name, ufunc->uf_func_type); + return generate_PUSHFUNC(cctx, ufunc->uf_name, ufunc->uf_func_type, TRUE); } /* @@ -610,19 +610,10 @@ compile_string(isn_T *isn, cctx_T *cctx, } /* - * List of special functions for "compile_arguments". - */ -typedef enum { - CA_NOT_SPECIAL, - CA_SEARCHPAIR, // {skip} in searchpair() and searchpairpos() - CA_SUBSTITUTE, // {sub} in substitute(), when prefixed with \= -} ca_special_T; - -/* * Compile the argument expressions. * "arg" points to just after the "(" and is advanced to after the ")" */ - static int + int compile_arguments( char_u **arg, cctx_T *cctx, diff --git a/src/vim9instr.c b/src/vim9instr.c --- a/src/vim9instr.c +++ b/src/vim9instr.c @@ -616,7 +616,7 @@ generate_tv_PUSH(cctx_T *cctx, typval_T case VAR_FUNC: if (tv->vval.v_string != NULL) iemsg("non-null function constant not supported"); - generate_PUSHFUNC(cctx, NULL, &t_func_unknown); + generate_PUSHFUNC(cctx, NULL, &t_func_unknown, TRUE); break; case VAR_PARTIAL: if (tv->vval.v_partial != NULL) @@ -796,9 +796,11 @@ generate_PUSHBLOB(cctx_T *cctx, blob_T * /* * Generate an ISN_PUSHFUNC instruction with name "name". + * When "may_prefix" is TRUE prefix "g:" unless "name" is script-local or + * autoload. */ int -generate_PUSHFUNC(cctx_T *cctx, char_u *name, type_T *type) +generate_PUSHFUNC(cctx_T *cctx, char_u *name, type_T *type, int may_prefix) { isn_T *isn; char_u *funcname; @@ -808,7 +810,8 @@ generate_PUSHFUNC(cctx_T *cctx, char_u * return FAIL; if (name == NULL) funcname = NULL; - else if (*name == K_SPECIAL // script-local + else if (!may_prefix + || *name == K_SPECIAL // script-local || vim_strchr(name, AUTOLOAD_CHAR) != NULL) // autoload funcname = vim_strsave(name); else @@ -1679,6 +1682,22 @@ generate_PCALL( } /* + * Generate an ISN_DEFER instruction. + */ + int +generate_DEFER(cctx_T *cctx, int var_idx, int argcount) +{ + isn_T *isn; + + RETURN_OK_IF_SKIP(cctx); + if ((isn = generate_instr_drop(cctx, ISN_DEFER, argcount + 1)) == NULL) + return FAIL; + isn->isn_arg.defer.defer_var_idx = var_idx; + isn->isn_arg.defer.defer_argcount = argcount; + return OK; +} + +/* * Generate an ISN_STRINGMEMBER instruction. */ int @@ -2240,6 +2259,7 @@ delete_instr(isn_T *isn) case ISN_CONCAT: case ISN_COND2BOOL: case ISN_DEBUG: + case ISN_DEFER: case ISN_DROP: case ISN_ECHO: case ISN_ECHOCONSOLE: @@ -2296,21 +2316,21 @@ delete_instr(isn_T *isn) case ISN_STOREINDEX: case ISN_STORENR: case ISN_SOURCE: - case ISN_STOREOUTER: - case ISN_STORERANGE: - case ISN_STOREREG: - case ISN_STOREV: - case ISN_STRINDEX: - case ISN_STRSLICE: - case ISN_THROW: - case ISN_TRYCONT: - case ISN_UNLETINDEX: - case ISN_UNLETRANGE: - case ISN_UNPACK: - case ISN_USEDICT: + case ISN_STOREOUTER: + case ISN_STORERANGE: + case ISN_STOREREG: + case ISN_STOREV: + case ISN_STRINDEX: + case ISN_STRSLICE: + case ISN_THROW: + case ISN_TRYCONT: + case ISN_UNLETINDEX: + case ISN_UNLETRANGE: + case ISN_UNPACK: + case ISN_USEDICT: // nothing allocated break; -} + } } void