# HG changeset patch # User Bram Moolenaar # Date 1618858804 -7200 # Node ID 08050e45bd060b6bd2059dfd56fcfdf84f6813c2 # Parent c2b93bfb68f8325288279b600a00d3909f05024f patch 8.2.2785: Vim9: cannot redirect to local variable Commit: https://github.com/vim/vim/commit/2d1c57ed3dd25c44b41b9ddd4cf63c01ae89007e Author: Bram Moolenaar Date: Mon Apr 19 20:50:03 2021 +0200 patch 8.2.2785: Vim9: cannot redirect to local variable Problem: Vim9: cannot redirect to local variable. Solution: Compile :redir when redirecting to a variable. diff --git a/src/errors.h b/src/errors.h --- a/src/errors.h +++ b/src/errors.h @@ -405,3 +405,7 @@ EXTERN char e_cannot_use_range_with_assi INIT(= N_("E1183: Cannot use a range with an assignment operator: %s")); EXTERN char e_blob_not_set[] INIT(= N_("E1184: Blob not set")); +EXTERN char e_cannot_nest_redir[] + INIT(= N_("E1185: Cannot nest :redir")); +EXTERN char e_missing_redir_end[] + INIT(= N_("E1185: Missing :redir END")); diff --git a/src/evalvars.c b/src/evalvars.c --- a/src/evalvars.c +++ b/src/evalvars.c @@ -3778,6 +3778,27 @@ static garray_T redir_ga; // only valid static char_u *redir_endp = NULL; static char_u *redir_varname = NULL; + int +alloc_redir_lval(void) +{ + redir_lval = ALLOC_CLEAR_ONE(lval_T); + if (redir_lval == NULL) + return FAIL; + return OK; +} + + void +clear_redir_lval(void) +{ + VIM_CLEAR(redir_lval); +} + + void +init_redir_ga(void) +{ + ga_init2(&redir_ga, (int)sizeof(char), 500); +} + /* * Start recording command output to a variable * When "append" is TRUE append to an existing variable. @@ -3801,15 +3822,14 @@ var_redir_start(char_u *name, int append if (redir_varname == NULL) return FAIL; - redir_lval = ALLOC_CLEAR_ONE(lval_T); - if (redir_lval == NULL) + if (alloc_redir_lval() == FAIL) { var_redir_stop(); return FAIL; } // The output is stored in growarray "redir_ga" until redirection ends. - ga_init2(&redir_ga, (int)sizeof(char), 500); + init_redir_ga(); // Parse the variable name (can be a dict or list entry). redir_endp = get_lval(redir_varname, NULL, redir_lval, FALSE, FALSE, 0, @@ -3922,6 +3942,20 @@ var_redir_stop(void) } /* + * Get the collected redirected text and clear redir_ga. + */ + char_u * +get_clear_redir_ga(void) +{ + char_u *res; + + ga_append(&redir_ga, NUL); // Append the trailing NUL. + res = redir_ga.ga_data; + redir_ga.ga_data = NULL; + return res; +} + +/* * "gettabvar()" function */ void diff --git a/src/proto/evalvars.pro b/src/proto/evalvars.pro --- a/src/proto/evalvars.pro +++ b/src/proto/evalvars.pro @@ -71,7 +71,7 @@ void vars_clear(hashtab_T *ht); void vars_clear_ext(hashtab_T *ht, int free_val); void delete_var(hashtab_T *ht, hashitem_T *hi); void set_var(char_u *name, typval_T *tv, int copy); -void set_var_const(char_u *name, type_T *type, typval_T *tv_arg, int copy, int flags, int var_idx); +void set_var_const(char_u *name, type_T *type, typval_T *tv_arg, int copy, int flags_arg, int var_idx); int var_check_permission(dictitem_T *di, char_u *name); int var_check_ro(int flags, char_u *name, int use_gettext); int var_check_lock(int flags, char_u *name, int use_gettext); @@ -82,9 +82,13 @@ int valid_varname(char_u *varname, int a void reset_v_option_vars(void); void assert_error(garray_T *gap); int var_exists(char_u *var); +int alloc_redir_lval(void); +void clear_redir_lval(void); +void init_redir_ga(void); int var_redir_start(char_u *name, int append); void var_redir_str(char_u *value, int value_len); void var_redir_stop(void); +char_u *get_clear_redir_ga(void); void f_gettabvar(typval_T *argvars, typval_T *rettv); void f_gettabwinvar(typval_T *argvars, typval_T *rettv); void f_getwinvar(typval_T *argvars, typval_T *rettv); diff --git a/src/testdir/test_vim9_cmd.vim b/src/testdir/test_vim9_cmd.vim --- a/src/testdir/test_vim9_cmd.vim +++ b/src/testdir/test_vim9_cmd.vim @@ -1194,5 +1194,23 @@ def Test_substitute_expr() bwipe! enddef +def Test_redir_to_var() + var result: string + redir => result + echo 'something' + redir END + assert_equal("\nsomething", result) + + redir =>> result + echo 'more' + redir END + assert_equal("\nsomething\nmore", result) + + var lines =<< trim END + redir => notexist + END + CheckDefFailure(lines, 'E1089:') +enddef + " vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker 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 @@ -140,6 +140,33 @@ def Test_disassemble_substitute() res) enddef +def s:RedirVar() + var result: string + redir =>> result + echo "text" + redir END +enddef + +def Test_disassemble_redir_var() + var res = execute('disass s:RedirVar') + assert_match('\d*_RedirVar.*' .. + ' var result: string\_s*' .. + '\d PUSHS "\[NULL\]"\_s*' .. + '\d STORE $0\_s*' .. + ' redir =>> result\_s*' .. + '\d REDIR\_s*' .. + ' echo "text"\_s*' .. + '\d PUSHS "text"\_s*' .. + '\d ECHO 1\_s*' .. + ' redir END\_s*' .. + '\d LOAD $0\_s*' .. + '\d REDIR END\_s*' .. + '\d CONCAT\_s*' .. + '\d STORE $0\_s*' .. + '\d RETURN 0', + res) +enddef + def s:YankRange() norm! m[jjm] :'[,']yank diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -751,6 +751,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 2785, +/**/ 2784, /**/ 2783, diff --git a/src/vim9.h b/src/vim9.h --- a/src/vim9.h +++ b/src/vim9.h @@ -169,6 +169,9 @@ typedef enum { ISN_SHUFFLE, // move item on stack up or down ISN_DROP, // pop stack and discard value + ISN_REDIRSTART, // :redir => + ISN_REDIREND, // :redir END, isn_arg.number == 1 for append + ISN_FINISH // end marker in list of instructions } isntype_T; diff --git a/src/vim9compile.c b/src/vim9compile.c --- a/src/vim9compile.c +++ b/src/vim9compile.c @@ -114,6 +114,52 @@ typedef struct { int lv_arg; // when TRUE this is an argument } lvar_T; +// Destination for an assignment or ":unlet" with an index. +typedef enum { + dest_local, + dest_option, + dest_env, + dest_global, + dest_buffer, + dest_window, + dest_tab, + dest_vimvar, + dest_script, + dest_reg, + dest_expr, +} assign_dest_T; + +// Used by compile_lhs() to store information about the LHS of an assignment +// and one argument of ":unlet" with an index. +typedef struct { + assign_dest_T lhs_dest; // type of destination + + char_u *lhs_name; // allocated name including + // "[expr]" or ".name". + size_t lhs_varlen; // length of the variable without + // "[expr]" or ".name" + char_u *lhs_dest_end; // end of the destination, including + // "[expr]" or ".name". + + int lhs_has_index; // has "[expr]" or ".name" + + int lhs_new_local; // create new local variable + int lhs_opt_flags; // for when destination is an option + int lhs_vimvaridx; // for when destination is a v:var + + lvar_T lhs_local_lvar; // used for existing local destination + lvar_T lhs_arg_lvar; // used for argument destination + lvar_T *lhs_lvar; // points to destination lvar + int lhs_scriptvar_sid; + int lhs_scriptvar_idx; + + int lhs_has_type; // type was specified + type_T *lhs_type; + type_T *lhs_member_type; + + int lhs_append; // used by ISN_REDIREND +} lhs_T; + /* * Context for compiling lines of Vim script. * Stores info about the local variables and condition stack. @@ -146,6 +192,9 @@ struct cctx_S { garray_T *ctx_type_list; // list of pointers to allocated types int ctx_has_cmdmod; // ISN_CMDMOD was generated + + lhs_T ctx_redir_lhs; // LHS for ":redir => var", valid when + // lhs_name is not NULL }; static void delete_def_function_contents(dfunc_T *dfunc, int mark_deleted); @@ -5460,50 +5509,6 @@ static char *reserved[] = { NULL }; -// Destination for an assignment or ":unlet" with an index. -typedef enum { - dest_local, - dest_option, - dest_env, - dest_global, - dest_buffer, - dest_window, - dest_tab, - dest_vimvar, - dest_script, - dest_reg, - dest_expr, -} assign_dest_T; - -// Used by compile_lhs() to store information about the LHS of an assignment -// and one argument of ":unlet" with an index. -typedef struct { - assign_dest_T lhs_dest; // type of destination - - char_u *lhs_name; // allocated name including - // "[expr]" or ".name". - size_t lhs_varlen; // length of the variable without - // "[expr]" or ".name" - char_u *lhs_dest_end; // end of the destination, including - // "[expr]" or ".name". - - int lhs_has_index; // has "[expr]" or ".name" - - int lhs_new_local; // create new local variable - int lhs_opt_flags; // for when destination is an option - int lhs_vimvaridx; // for when destination is a v:var - - lvar_T lhs_local_lvar; // used for existing local destination - lvar_T lhs_arg_lvar; // used for argument destination - lvar_T *lhs_lvar; // points to destination lvar - int lhs_scriptvar_sid; - int lhs_scriptvar_idx; - - int lhs_has_type; // type was specified - type_T *lhs_type; - type_T *lhs_member_type; -} lhs_T; - /* * Generate the load instruction for "name". */ @@ -5779,6 +5784,44 @@ generate_store_var( } static int +generate_store_lhs(cctx_T *cctx, lhs_T *lhs, int instr_count) +{ + if (lhs->lhs_dest != dest_local) + return generate_store_var(cctx, lhs->lhs_dest, + lhs->lhs_opt_flags, lhs->lhs_vimvaridx, + lhs->lhs_scriptvar_idx, lhs->lhs_scriptvar_sid, + lhs->lhs_type, lhs->lhs_name); + + if (lhs->lhs_lvar != NULL) + { + garray_T *instr = &cctx->ctx_instr; + isn_T *isn = ((isn_T *)instr->ga_data) + instr->ga_len - 1; + + // optimization: turn "var = 123" from ISN_PUSHNR + ISN_STORE into + // ISN_STORENR + if (lhs->lhs_lvar->lv_from_outer == 0 + && instr->ga_len == instr_count + 1 + && isn->isn_type == ISN_PUSHNR) + { + varnumber_T val = isn->isn_arg.number; + garray_T *stack = &cctx->ctx_type_stack; + + isn->isn_type = ISN_STORENR; + isn->isn_arg.storenr.stnr_idx = lhs->lhs_lvar->lv_idx; + isn->isn_arg.storenr.stnr_val = val; + if (stack->ga_len > 0) + --stack->ga_len; + } + else if (lhs->lhs_lvar->lv_from_outer > 0) + generate_STOREOUTER(cctx, lhs->lhs_lvar->lv_idx, + lhs->lhs_lvar->lv_from_outer); + else + generate_STORE(cctx, ISN_STORE, lhs->lhs_lvar->lv_idx, NULL); + } + return OK; +} + + static int is_decl_command(int cmdidx) { return cmdidx == CMD_let || cmdidx == CMD_var @@ -6084,6 +6127,36 @@ compile_lhs( } /* + * Figure out the LHS and check a few errors. + */ + static int +compile_assign_lhs( + char_u *var_start, + lhs_T *lhs, + int cmdidx, + int is_decl, + int heredoc, + int oplen, + cctx_T *cctx) +{ + if (compile_lhs(var_start, lhs, cmdidx, heredoc, oplen, cctx) == FAIL) + return FAIL; + + if (!lhs->lhs_has_index && lhs->lhs_lvar == &lhs->lhs_arg_lvar) + { + semsg(_(e_cannot_assign_to_argument), lhs->lhs_name); + return FAIL; + } + if (!is_decl && lhs->lhs_lvar != NULL + && lhs->lhs_lvar->lv_const && !lhs->lhs_has_index) + { + semsg(_(e_cannot_assign_to_constant), lhs->lhs_name); + return FAIL; + } + return OK; +} + +/* * For an assignment with an index, compile the "idx" in "var[idx]" or "key" in * "var.key". */ @@ -6454,21 +6527,9 @@ compile_assignment(char_u *arg, exarg_T /* * Figure out the LHS type and other properties. */ - if (compile_lhs(var_start, &lhs, cmdidx, heredoc, oplen, cctx) == FAIL) - goto theend; - - if (!lhs.lhs_has_index && lhs.lhs_lvar == &lhs.lhs_arg_lvar) - { - semsg(_(e_cannot_assign_to_argument), lhs.lhs_name); + if (compile_assign_lhs(var_start, &lhs, cmdidx, + is_decl, heredoc, oplen, cctx) == FAIL) goto theend; - } - if (!is_decl && lhs.lhs_lvar != NULL - && lhs.lhs_lvar->lv_const && !lhs.lhs_has_index) - { - semsg(_(e_cannot_assign_to_constant), lhs.lhs_name); - goto theend; - } - if (!heredoc) { if (cctx->ctx_skip == SKIP_YES) @@ -6728,39 +6789,8 @@ compile_assignment(char_u *arg, exarg_T // also in legacy script. generate_SETTYPE(cctx, lhs.lhs_type); - if (lhs.lhs_dest != dest_local) - { - if (generate_store_var(cctx, lhs.lhs_dest, - lhs.lhs_opt_flags, lhs.lhs_vimvaridx, - lhs.lhs_scriptvar_idx, lhs.lhs_scriptvar_sid, - lhs.lhs_type, lhs.lhs_name) == FAIL) - goto theend; - } - else if (lhs.lhs_lvar != NULL) - { - isn_T *isn = ((isn_T *)instr->ga_data) - + instr->ga_len - 1; - - // optimization: turn "var = 123" from ISN_PUSHNR + - // ISN_STORE into ISN_STORENR - if (lhs.lhs_lvar->lv_from_outer == 0 - && instr->ga_len == instr_count + 1 - && isn->isn_type == ISN_PUSHNR) - { - varnumber_T val = isn->isn_arg.number; - - isn->isn_type = ISN_STORENR; - isn->isn_arg.storenr.stnr_idx = lhs.lhs_lvar->lv_idx; - isn->isn_arg.storenr.stnr_val = val; - if (stack->ga_len > 0) - --stack->ga_len; - } - else if (lhs.lhs_lvar->lv_from_outer > 0) - generate_STOREOUTER(cctx, lhs.lhs_lvar->lv_idx, - lhs.lhs_lvar->lv_from_outer); - else - generate_STORE(cctx, ISN_STORE, lhs.lhs_lvar->lv_idx, NULL); - } + if (generate_store_lhs(cctx, &lhs, instr_count) == FAIL) + goto theend; } if (var_idx + 1 < var_count) @@ -8541,6 +8571,67 @@ compile_substitute(char_u *arg, exarg_T return compile_exec(arg, eap, cctx); } + static char_u * +compile_redir(char_u *line, exarg_T *eap, cctx_T *cctx) +{ + char_u *arg = eap->arg; + + if (cctx->ctx_redir_lhs.lhs_name != NULL) + { + if (STRNCMP(arg, "END", 3) == 0) + { + if (cctx->ctx_redir_lhs.lhs_append) + { + if (compile_load_lhs(&cctx->ctx_redir_lhs, + cctx->ctx_redir_lhs.lhs_name, NULL, cctx) == FAIL) + return NULL; + if (cctx->ctx_redir_lhs.lhs_has_index) + emsg("redir with index not implemented yet"); + } + + // Gets the redirected text and put it on the stack, then store it + // in the variable. + generate_instr_type(cctx, ISN_REDIREND, &t_string); + + if (cctx->ctx_redir_lhs.lhs_append) + generate_instr_drop(cctx, ISN_CONCAT, 1); + + if (generate_store_lhs(cctx, &cctx->ctx_redir_lhs, -1) == FAIL) + return NULL; + + VIM_CLEAR(cctx->ctx_redir_lhs.lhs_name); + return arg + 3; + } + emsg(_(e_cannot_nest_redir)); + return NULL; + } + + if (arg[0] == '=' && arg[1] == '>') + { + int append = FALSE; + + // redirect to a variable is compiled + arg += 2; + if (*arg == '>') + { + ++arg; + append = TRUE; + } + arg = skipwhite(arg); + + if (compile_assign_lhs(arg, &cctx->ctx_redir_lhs, CMD_redir, + FALSE, FALSE, 1, cctx) == FAIL) + return NULL; + generate_instr(cctx, ISN_REDIRSTART); + cctx->ctx_redir_lhs.lhs_append = append; + + return arg + cctx->ctx_redir_lhs.lhs_varlen; + } + + // other redirects are handled like at script level + return compile_exec(line, eap, cctx); +} + /* * Add a function to the list of :def functions. * This sets "ufunc->uf_dfunc_idx" but the function isn't compiled yet. @@ -9082,6 +9173,11 @@ compile_def_function( } break; + case CMD_redir: + ea.arg = p; + line = compile_redir(line, &ea, &cctx); + break; + // TODO: any other commands with an expression argument? case CMD_append: @@ -9217,6 +9313,16 @@ erret: emsg(_(e_compiling_def_function_failed)); } + if (cctx.ctx_redir_lhs.lhs_name != NULL) + { + if (ret == OK) + { + emsg(_(e_missing_redir_end)); + ret = FAIL; + } + vim_free(cctx.ctx_redir_lhs.lhs_name); + } + current_sctx = save_current_sctx; estack_compiling = save_estack_compiling; if (do_estack_push) @@ -9463,6 +9569,7 @@ delete_instr(isn_T *isn) case ISN_NEWLIST: case ISN_OPANY: case ISN_OPFLOAT: + case ISN_FINISH: case ISN_OPNR: case ISN_PCALL: case ISN_PCALL_END: @@ -9473,15 +9580,17 @@ delete_instr(isn_T *isn) case ISN_PUSHNR: case ISN_PUSHSPEC: case ISN_PUT: + case ISN_REDIREND: + case ISN_REDIRSTART: case ISN_RETURN: case ISN_RETURN_ZERO: case ISN_SHUFFLE: case ISN_SLICE: case ISN_STORE: case ISN_STOREINDEX: - case ISN_STORERANGE: case ISN_STORENR: case ISN_STOREOUTER: + case ISN_STORERANGE: case ISN_STOREREG: case ISN_STOREV: case ISN_STRINDEX: @@ -9491,7 +9600,6 @@ delete_instr(isn_T *isn) case ISN_UNLETINDEX: case ISN_UNLETRANGE: case ISN_UNPACK: - case ISN_FINISH: // nothing allocated break; } diff --git a/src/vim9execute.c b/src/vim9execute.c --- a/src/vim9execute.c +++ b/src/vim9execute.c @@ -1409,6 +1409,37 @@ exec_instructions(ectx_T *ectx) case ISN_FINISH: goto done; + case ISN_REDIRSTART: + // create a dummy entry for var_redir_str() + if (alloc_redir_lval() == FAIL) + goto on_error; + + // The output is stored in growarray "redir_ga" until + // redirection ends. + init_redir_ga(); + redir_vname = 1; + break; + + case ISN_REDIREND: + { + char_u *res = get_clear_redir_ga(); + + // End redirection, put redirected text on the stack. + clear_redir_lval(); + redir_vname = 0; + + if (GA_GROW(&ectx->ec_stack, 1) == FAIL) + { + vim_free(res); + return FAIL; + } + tv = STACK_TV_BOT(0); + tv->v_type = VAR_STRING; + tv->vval.v_string = res; + ++ectx->ec_stack.ga_len; + } + break; + // execute Ex command from pieces on the stack case ISN_EXECCONCAT: { @@ -4332,6 +4363,13 @@ list_instructions(char *pfx, isn_T *inst case ISN_EXEC: smsg("%s%4d EXEC %s", pfx, current, iptr->isn_arg.string); break; + case ISN_REDIRSTART: + smsg("%s%4d REDIR", pfx, current); + break; + case ISN_REDIREND: + smsg("%s%4d REDIR END%s", pfx, current, + iptr->isn_arg.number ? " append" : ""); + break; case ISN_SUBSTITUTE: { subs_T *subs = &iptr->isn_arg.subs;