changeset 24490:08050e45bd06 v8.2.2785

patch 8.2.2785: Vim9: cannot redirect to local variable Commit: https://github.com/vim/vim/commit/2d1c57ed3dd25c44b41b9ddd4cf63c01ae89007e Author: Bram Moolenaar <Bram@vim.org> 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.
author Bram Moolenaar <Bram@vim.org>
date Mon, 19 Apr 2021 21:00:04 +0200
parents c2b93bfb68f8
children a03ddeba7252
files src/errors.h src/evalvars.c src/proto/evalvars.pro src/testdir/test_vim9_cmd.vim src/testdir/test_vim9_disassemble.vim src/version.c src/vim9.h src/vim9compile.c src/vim9execute.c
diffstat 9 files changed, 335 insertions(+), 97 deletions(-) [+]
line wrap: on
line diff
--- 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"));
--- 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
--- 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);
--- 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
--- 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('<SNR>\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
--- 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,
--- 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;
 
--- 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;
     }
--- 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;