changeset 28813:3626ca6a20ea v8.2.4930

patch 8.2.4930: interpolated string expression requires escaping Commit: https://github.com/vim/vim/commit/0abc2871c105882ed1c1effb9a7757fad8a395bd Author: Bram Moolenaar <Bram@vim.org> Date: Tue May 10 13:24:30 2022 +0100 patch 8.2.4930: interpolated string expression requires escaping Problem: Interpolated string expression requires escaping. Solution: Do not require escaping in the expression.
author Bram Moolenaar <Bram@vim.org>
date Tue, 10 May 2022 14:30:04 +0200
parents 483371d05cd5
children 748050f8ed41
files src/alloc.c src/dict.c src/eval.c src/evalvars.c src/proto/alloc.pro src/proto/evalvars.pro src/proto/typval.pro src/proto/vim9compile.pro src/testdir/test_expr.vim src/testdir/test_let.vim src/typval.c src/version.c src/vim9compile.c src/vim9expr.c src/vim9instr.c
diffstat 15 files changed, 288 insertions(+), 123 deletions(-) [+]
line wrap: on
line diff
--- a/src/alloc.c
+++ b/src/alloc.c
@@ -832,7 +832,7 @@ ga_add_string(garray_T *gap, char_u *p)
 
 /*
  * Concatenate a string to a growarray which contains bytes.
- * When "s" is NULL does not do anything.
+ * When "s" is NULL memory allocation fails does not do anything.
  * Note: Does NOT copy the NUL at the end!
  */
     void
@@ -869,14 +869,14 @@ ga_concat_len(garray_T *gap, char_u *s, 
 /*
  * Append one byte to a growarray which contains bytes.
  */
-    void
+    int
 ga_append(garray_T *gap, int c)
 {
-    if (ga_grow(gap, 1) == OK)
-    {
-	*((char *)gap->ga_data + gap->ga_len) = c;
-	++gap->ga_len;
-    }
+    if (ga_grow(gap, 1) == FAIL)
+	return FAIL;
+    *((char *)gap->ga_data + gap->ga_len) = c;
+    ++gap->ga_len;
+    return OK;
 }
 
 #if (defined(UNIX) && !defined(USE_SYSTEM)) || defined(MSWIN) \
--- a/src/dict.c
+++ b/src/dict.c
@@ -866,13 +866,13 @@ get_literal_key(char_u **arg)
 
     if (**arg == '\'')
     {
-	if (eval_lit_string(arg, &rettv, TRUE) == FAIL)
+	if (eval_lit_string(arg, &rettv, TRUE, FALSE) == FAIL)
 	    return NULL;
 	key = rettv.vval.v_string;
     }
     else if (**arg == '"')
     {
-	if (eval_string(arg, &rettv, TRUE) == FAIL)
+	if (eval_string(arg, &rettv, TRUE, FALSE) == FAIL)
 	    return NULL;
 	key = rettv.vval.v_string;
     }
--- a/src/eval.c
+++ b/src/eval.c
@@ -3726,13 +3726,13 @@ eval7(
     /*
      * String constant: "string".
      */
-    case '"':	ret = eval_string(arg, rettv, evaluate);
+    case '"':	ret = eval_string(arg, rettv, evaluate, FALSE);
 		break;
 
     /*
      * Literal string constant: 'str''ing'.
      */
-    case '\'':	ret = eval_lit_string(arg, rettv, evaluate);
+    case '\'':	ret = eval_lit_string(arg, rettv, evaluate, FALSE);
 		break;
 
     /*
--- a/src/evalvars.c
+++ b/src/evalvars.c
@@ -603,16 +603,52 @@ list_script_vars(int *first)
 }
 
 /*
- * Evaluate all the Vim expressions ({expr}) in string "str" and return the
- * resulting string.  The caller must free the returned string.
+ * Evaluate one Vim expression {expr} in string "p" and append the
+ * resulting string to "gap".  "p" points to the opening "{".
+ * Return a pointer to the character after "}", NULL for an error.
+ */
+    char_u *
+eval_one_expr_in_str(char_u *p, garray_T *gap)
+{
+    char_u	*block_start = skipwhite(p + 1);  // skip the opening {
+    char_u	*block_end = block_start;
+    char_u	*expr_val;
+
+    if (*block_start == NUL)
+    {
+	semsg(_(e_missing_close_curly_str), p);
+	return NULL;
+    }
+    if (skip_expr(&block_end, NULL) == FAIL)
+	return NULL;
+    block_end = skipwhite(block_end);
+    if (*block_end != '}')
+    {
+	semsg(_(e_missing_close_curly_str), p);
+	return NULL;
+    }
+    *block_end = NUL;
+    expr_val = eval_to_string(block_start, TRUE);
+    *block_end = '}';
+    if (expr_val == NULL)
+	return NULL;
+    ga_concat(gap, expr_val);
+    vim_free(expr_val);
+
+    return block_end + 1;
+}
+
+/*
+ * Evaluate all the Vim expressions {expr} in "str" and return the resulting
+ * string in allocated memory.  "{{" is reduced to "{" and "}}" to "}".
+ * Used for a heredoc assignment.
+ * Returns NULL for an error.
  */
     char_u *
 eval_all_expr_in_str(char_u *str)
 {
     garray_T	ga;
     char_u	*p;
-    char_u	save_c;
-    char_u	*expr_val;
 
     ga_init2(&ga, 1, 80);
     p = str;
@@ -620,8 +656,6 @@ eval_all_expr_in_str(char_u *str)
     while (*p != NUL)
     {
 	char_u	*lit_start;
-	char_u	*block_start;
-	char_u	*block_end;
 	int	escaped_brace = FALSE;
 
 	// Look for a block start.
@@ -656,35 +690,13 @@ eval_all_expr_in_str(char_u *str)
 	    continue;
 	}
 
-	// Skip the opening {.
-	block_start = ++p;
-	block_end = block_start;
-	if (*block_start != NUL && skip_expr(&block_end, NULL) == FAIL)
+	// Evaluate the expression and append the result.
+	p = eval_one_expr_in_str(p, &ga);
+	if (p == NULL)
 	{
 	    ga_clear(&ga);
 	    return NULL;
 	}
-	block_end = skipwhite(block_end);
-	// The block must be closed by a }.
-	if (*block_end != '}')
-	{
-	    semsg(_(e_missing_close_curly_str), str);
-	    ga_clear(&ga);
-	    return NULL;
-	}
-	save_c = *block_end;
-	*block_end = NUL;
-	expr_val = eval_to_string(block_start, TRUE);
-	*block_end = save_c;
-	if (expr_val == NULL)
-	{
-	    ga_clear(&ga);
-	    return NULL;
-	}
-	ga_concat(&ga, expr_val);
-	vim_free(expr_val);
-
-	p = block_end + 1;
     }
     ga_append(&ga, NUL);
 
--- a/src/proto/alloc.pro
+++ b/src/proto/alloc.pro
@@ -19,13 +19,13 @@ int ga_copy_strings(garray_T *from, garr
 void ga_init(garray_T *gap);
 void ga_init2(garray_T *gap, size_t itemsize, int growsize);
 int ga_grow(garray_T *gap, int n);
-int ga_grow_id(garray_T *gap, int n, alloc_id_T id UNUSED);
+int ga_grow_id(garray_T *gap, int n, alloc_id_T id);
 int ga_grow_inner(garray_T *gap, int n);
 char_u *ga_concat_strings(garray_T *gap, char *sep);
 int ga_copy_string(garray_T *gap, char_u *p);
 int ga_add_string(garray_T *gap, char_u *p);
 void ga_concat(garray_T *gap, char_u *s);
 void ga_concat_len(garray_T *gap, char_u *s, size_t len);
-void ga_append(garray_T *gap, int c);
+int ga_append(garray_T *gap, int c);
 void append_ga_line(garray_T *gap);
 /* vim: set ft=c : */
--- a/src/proto/evalvars.pro
+++ b/src/proto/evalvars.pro
@@ -13,6 +13,8 @@ list_T *eval_spell_expr(char_u *badword,
 int get_spellword(list_T *list, char_u **pp);
 void prepare_vimvar(int idx, typval_T *save_tv);
 void restore_vimvar(int idx, typval_T *save_tv);
+char_u *eval_one_expr_in_str(char_u *p, garray_T *gap);
+char_u *eval_all_expr_in_str(char_u *str);
 list_T *heredoc_get(exarg_T *eap, char_u *cmd, int script_get, int vim9compile);
 void ex_var(exarg_T *eap);
 void ex_let(exarg_T *eap);
@@ -105,6 +107,4 @@ void set_callback(callback_T *dest, call
 void copy_callback(callback_T *dest, callback_T *src);
 void expand_autload_callback(callback_T *cb);
 void free_callback(callback_T *callback);
-char_u *eval_all_expr_in_str(char_u *str);
-
 /* vim: set ft=c : */
--- a/src/proto/typval.pro
+++ b/src/proto/typval.pro
@@ -68,11 +68,11 @@ int tv_islocked(typval_T *tv);
 int tv_equal(typval_T *tv1, typval_T *tv2, int ic, int recursive);
 int eval_option(char_u **arg, typval_T *rettv, int evaluate);
 int eval_number(char_u **arg, typval_T *rettv, int evaluate, int want_string);
-int eval_string(char_u **arg, typval_T *rettv, int evaluate);
-int eval_lit_string(char_u **arg, typval_T *rettv, int evaluate);
+int eval_string(char_u **arg, typval_T *rettv, int evaluate, int interpolate);
+int eval_lit_string(char_u **arg, typval_T *rettv, int evaluate, int interpolate);
+int eval_interp_string(char_u **arg, typval_T *rettv, int evaluate);
 char_u *tv2string(typval_T *tv, char_u **tofree, char_u *numbuf, int copyID);
 int eval_env_var(char_u **arg, typval_T *rettv, int evaluate);
-int eval_interp_string(char_u **arg, typval_T *rettv, int evaluate);
 linenr_T tv_get_lnum(typval_T *argvars);
 linenr_T tv_get_lnum_buf(typval_T *argvars, buf_T *buf);
 buf_T *tv_get_buf(typval_T *tv, int curtab_only);
--- a/src/proto/vim9compile.pro
+++ b/src/proto/vim9compile.pro
@@ -16,6 +16,7 @@ int may_get_next_line(char_u *whitep, ch
 int may_get_next_line_error(char_u *whitep, char_u **arg, cctx_T *cctx);
 void fill_exarg_from_cctx(exarg_T *eap, cctx_T *cctx);
 int func_needs_compiling(ufunc_T *ufunc, compiletype_T compile_type);
+char_u *compile_one_expr_in_str(char_u *p, cctx_T *cctx);
 int compile_all_expr_in_str(char_u *str, int evalstr, cctx_T *cctx);
 int assignment_len(char_u *p, int *heredoc);
 void vim9_declare_error(char_u *name);
--- a/src/testdir/test_expr.vim
+++ b/src/testdir/test_expr.vim
@@ -897,7 +897,7 @@ func Test_string_interp()
     #" Escaping rules.
     call assert_equal('"foo"{bar}', $"\"foo\"{{bar}}")
     call assert_equal('"foo"{bar}', $'"foo"{{bar}}')
-    call assert_equal('foobar', $"{\"foo\"}" .. $'{''bar''}')
+    call assert_equal('foobar', $"{"foo"}" .. $'{'bar'}')
     #" Whitespace before/after the expression.
     call assert_equal('3', $"{ 1 + 2 }")
     #" String conversion.
@@ -907,8 +907,8 @@ func Test_string_interp()
     call assert_equal(string(v:true), $"{v:true}")
     call assert_equal('(1+1=2)', $"(1+1={1 + 1})")
     #" Hex-escaped opening brace: char2nr('{') == 0x7b
-    call assert_equal('esc123ape', $"esc\x7b123}ape")
-    call assert_equal('me{}me', $"me{\x7b}\x7dme")
+    call assert_equal('esc123ape', $"esc{123}ape")
+    call assert_equal('me{}me', $"me{"\x7b"}\x7dme")
     VAR var1 = "sun"
     VAR var2 = "shine"
     call assert_equal('sunshine', $"{var1}{var2}")
@@ -916,7 +916,7 @@ func Test_string_interp()
     #" Multibyte strings.
     call assert_equal('say ハロー・ワールド', $"say {'ハロー・ワールド'}")
     #" Nested.
-    call assert_equal('foobarbaz', $"foo{$\"{'bar'}\"}baz")
+    call assert_equal('foobarbaz', $"foo{$"{'bar'}"}baz")
     #" Do not evaluate blocks when the expr is skipped.
     VAR tmp = 0
     if v:false
--- a/src/testdir/test_let.vim
+++ b/src/testdir/test_let.vim
@@ -387,9 +387,8 @@ func Test_let_interpolated()
   let text = 'text'
   call assert_equal('text{{', $'{text .. "{{"}')
   call assert_equal('text{{', $"{text .. '{{'}")
-  " FIXME: should not need to escape quotes in the expression
-  call assert_equal('text{{', $'{text .. ''{{''}')
-  call assert_equal('text{{', $"{text .. \"{{\"}")
+  call assert_equal('text{{', $'{text .. '{{'}')
+  call assert_equal('text{{', $"{text .. "{{"}")
 endfunc
 
 " Test for the setting a variable using the heredoc syntax.
--- a/src/typval.c
+++ b/src/typval.c
@@ -2065,19 +2065,23 @@ eval_number(
 }
 
 /*
- * Allocate a variable for a string constant.
+ * Evaluate a string constant and put the result in "rettv".
+ * "*arg" points to the double quote or to after it when "interpolate" is TRUE.
+ * When "interpolate" is TRUE reduce "{{" to "{", reduce "}}" to "}" and stop
+ * at a single "{".
  * Return OK or FAIL.
  */
     int
-eval_string(char_u **arg, typval_T *rettv, int evaluate)
+eval_string(char_u **arg, typval_T *rettv, int evaluate, int interpolate)
 {
     char_u	*p;
     char_u	*end;
-    int		extra = 0;
+    int		extra = interpolate ? 1 : 0;
+    int		off = interpolate ? 0 : 1;
     int		len;
 
     // Find the end of the string, skipping backslashed characters.
-    for (p = *arg + 1; *p != NUL && *p != '"'; MB_PTR_ADV(p))
+    for (p = *arg + off; *p != NUL && *p != '"'; MB_PTR_ADV(p))
     {
 	if (*p == '\\' && p[1] != NUL)
 	{
@@ -2088,9 +2092,21 @@ eval_string(char_u **arg, typval_T *rett
 	    if (*p == '<')
 		extra += 5;
 	}
+	else if (interpolate && (*p == '{' || *p == '}'))
+	{
+	    if (*p == '{' && p[1] != '{') // start of expression
+		break;
+	    ++p;
+	    if (p[-1] == '}' && *p != '}') // single '}' is an error
+	    {
+		semsg(_(e_stray_closing_curly_str), *arg);
+		return FAIL;
+	    }
+	    --extra;  // "{{" becomes "{", "}}" becomes "}"
+	}
     }
 
-    if (*p != '"')
+    if (*p != '"' && !(interpolate && *p == '{'))
     {
 	semsg(_(e_missing_double_quote_str), *arg);
 	return FAIL;
@@ -2099,7 +2115,7 @@ eval_string(char_u **arg, typval_T *rett
     // If only parsing, set *arg and return here
     if (!evaluate)
     {
-	*arg = p + 1;
+	*arg = p + off;
 	return OK;
     }
 
@@ -2112,7 +2128,7 @@ eval_string(char_u **arg, typval_T *rett
 	return FAIL;
     end = rettv->vval.v_string;
 
-    for (p = *arg + 1; *p != NUL && *p != '"'; )
+    for (p = *arg + off; *p != NUL && *p != '"'; )
     {
 	if (*p == '\\')
 	{
@@ -2192,15 +2208,23 @@ eval_string(char_u **arg, typval_T *rett
 			  }
 			  // FALLTHROUGH
 
-		default:  MB_COPY_CHAR(p, end);
+		default: MB_COPY_CHAR(p, end);
 			  break;
 	    }
 	}
 	else
+	{
+	    if (interpolate && (*p == '{' || *p == '}'))
+	    {
+		if (*p == '{' && p[1] != '{') // start of expression
+		    break;
+		++p;  // reduce "{{" to "{" and "}}" to "}"
+	    }
 	    MB_COPY_CHAR(p, end);
+	}
     }
     *end = NUL;
-    if (*p != NUL) // just in case
+    if (*p == '"' && !interpolate)
 	++p;
     *arg = p;
 
@@ -2209,17 +2233,20 @@ eval_string(char_u **arg, typval_T *rett
 
 /*
  * Allocate a variable for a 'str''ing' constant.
- * Return OK or FAIL.
+ * When "interpolate" is TRUE reduce "{{" to "{" and stop at a single "{".
+ * Return OK when a "rettv" was set to the string.
+ * Return FAIL on error, "rettv" is not set.
  */
     int
-eval_lit_string(char_u **arg, typval_T *rettv, int evaluate)
+eval_lit_string(char_u **arg, typval_T *rettv, int evaluate, int interpolate)
 {
     char_u	*p;
     char_u	*str;
-    int		reduce = 0;
+    int		reduce = interpolate ? -1 : 0;
+    int		off = interpolate ? 0 : 1;
 
     // Find the end of the string, skipping ''.
-    for (p = *arg + 1; *p != NUL; MB_PTR_ADV(p))
+    for (p = *arg + off; *p != NUL; MB_PTR_ADV(p))
     {
 	if (*p == '\'')
 	{
@@ -2228,9 +2255,29 @@ eval_lit_string(char_u **arg, typval_T *
 	    ++reduce;
 	    ++p;
 	}
+	else if (interpolate)
+	{
+	    if (*p == '{')
+	    {
+		if (p[1] != '{')
+		    break;
+		++p;
+		++reduce;
+	    }
+	    else if (*p == '}')
+	    {
+		++p;
+		if (*p != '}')
+		{
+		    semsg(_(e_stray_closing_curly_str), *arg);
+		    return FAIL;
+		}
+		++reduce;
+	    }
+	}
     }
 
-    if (*p != '\'')
+    if (*p != '\'' && !(interpolate && *p == '{'))
     {
 	semsg(_(e_missing_single_quote_str), *arg);
 	return FAIL;
@@ -2239,18 +2286,19 @@ eval_lit_string(char_u **arg, typval_T *
     // If only parsing return after setting "*arg"
     if (!evaluate)
     {
-	*arg = p + 1;
+	*arg = p + off;
 	return OK;
     }
 
-    // Copy the string into allocated memory, handling '' to ' reduction.
+    // Copy the string into allocated memory, handling '' to ' reduction and
+    // any expressions.
     str = alloc((p - *arg) - reduce);
     if (str == NULL)
 	return FAIL;
     rettv->v_type = VAR_STRING;
     rettv->vval.v_string = str;
 
-    for (p = *arg + 1; *p != NUL; )
+    for (p = *arg + off; *p != NUL; )
     {
 	if (*p == '\'')
 	{
@@ -2258,38 +2306,82 @@ eval_lit_string(char_u **arg, typval_T *
 		break;
 	    ++p;
 	}
+	else if (interpolate && (*p == '{' || *p == '}'))
+	{
+	    if (*p == '{' && p[1] != '{')
+		break;
+	    ++p;
+	}
 	MB_COPY_CHAR(p, str);
     }
     *str = NUL;
-    *arg = p + 1;
+    *arg = p + off;
 
     return OK;
 }
 
+/*
+ * Evaluate a single or double quoted string possibly containing expressions.
+ * "arg" points to the '$'.  The result is put in "rettv".
+ * Returns OK or FAIL.
+ */
     int
 eval_interp_string(char_u **arg, typval_T *rettv, int evaluate)
 {
     typval_T	tv;
-    int		ret;
+    int		ret = OK;
+    int		quote;
+    garray_T	ga;
+    char_u	*p;
+
+    ga_init2(&ga, 1, 80);
+
+    // *arg is on the '$' character, move it to the first string character.
+    ++*arg;
+    quote = **arg;
+    ++*arg;
 
-    // *arg is on the '$' character.
-    (*arg)++;
+    for (;;)
+    {
+	// Get the string up to the matching quote or to a single '{'.
+	// "arg" is advanced to either the quote or the '{'.
+	if (quote == '"')
+	    ret = eval_string(arg, &tv, evaluate, TRUE);
+	else
+	    ret = eval_lit_string(arg, &tv, evaluate, TRUE);
+	if (ret == FAIL)
+	    break;
+	if (evaluate)
+	{
+	    ga_concat(&ga, tv.vval.v_string);
+	    clear_tv(&tv);
+	}
+
+	if (**arg != '{')
+	{
+	    // found terminating quote
+	    ++*arg;
+	    break;
+	}
+	p = eval_one_expr_in_str(*arg, &ga);
+	if (p == NULL)
+	{
+	    ret = FAIL;
+	    break;
+	}
+	*arg = p;
+    }
 
     rettv->v_type = VAR_STRING;
-
-    if (**arg == '"')
-	ret = eval_string(arg, &tv, evaluate);
-    else
-	ret = eval_lit_string(arg, &tv, evaluate);
+    if (ret == FAIL || !evaluate || ga_append(&ga, NUL) == FAIL)
+    {
+	ga_clear(&ga);
+	rettv->vval.v_string = NULL;
+	return ret;
+    }
 
-    if (ret == FAIL || !evaluate)
-	return ret;
-
-    rettv->vval.v_string = eval_all_expr_in_str(tv.vval.v_string);
-
-    clear_tv(&tv);
-
-    return rettv->vval.v_string != NULL ? OK : FAIL;
+    rettv->vval.v_string = ga.ga_data;
+    return OK;
 }
 
 /*
--- a/src/version.c
+++ b/src/version.c
@@ -747,6 +747,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    4930,
+/**/
     4929,
 /**/
     4928,
--- a/src/vim9compile.c
+++ b/src/vim9compile.c
@@ -969,6 +969,36 @@ theend:
 }
 
 /*
+ * Compile one Vim expression {expr} in string "p".
+ * "p" points to the opening "{".
+ * Return a pointer to the character after "}", NULL for an error.
+ */
+    char_u *
+compile_one_expr_in_str(char_u *p, cctx_T *cctx)
+{
+    char_u	*block_start;
+    char_u	*block_end;
+
+    // Skip the opening {.
+    block_start = skipwhite(p + 1);
+    block_end = block_start;
+    if (*block_start != NUL && skip_expr(&block_end, NULL) == FAIL)
+	return NULL;
+    block_end = skipwhite(block_end);
+    // The block must be closed by a }.
+    if (*block_end != '}')
+    {
+	semsg(_(e_missing_close_curly_str), p);
+	return NULL;
+    }
+    if (compile_expr0(&block_start, cctx) == FAIL)
+	return NULL;
+    may_generate_2STRING(-1, TRUE, cctx);
+
+    return block_end + 1;
+}
+
+/*
  * Compile a string "str" (either containing a literal string or a mix of
  * literal strings and Vim expressions of the form `{expr}`).  This is used
  * when compiling a heredoc assignment to a variable or an interpolated string
@@ -997,8 +1027,6 @@ compile_all_expr_in_str(char_u *str, int
     while (*p != NUL)
     {
 	char_u	*lit_start;
-	char_u	*block_start;
-	char_u	*block_end;
 	int	escaped_brace = FALSE;
 
 	// Look for a block start.
@@ -1038,28 +1066,14 @@ compile_all_expr_in_str(char_u *str, int
 	    continue;
 	}
 
-	// Skip the opening {.
-	block_start = skipwhite(p + 1);
-	block_end = block_start;
-	if (*block_start != NUL && skip_expr(&block_end, NULL) == FAIL)
+	p = compile_one_expr_in_str(p, cctx);
+	if (p == NULL)
 	    return FAIL;
-	block_end = skipwhite(block_end);
-	// The block must be closed by a }.
-	if (*block_end != '}')
-	{
-	    semsg(_(e_missing_close_curly_str), str);
-	    return FAIL;
-	}
-	if (compile_expr0(&block_start, cctx) == FAIL)
-	    return FAIL;
-	may_generate_2STRING(-1, TRUE, cctx);
 	++count;
-
-	p = block_end + 1;
     }
 
     // Small optimization, if there's only a single piece skip the ISN_CONCAT.
-    if (count != 1)
+    if (count > 1)
 	return generate_CONCAT(cctx, count);
 
     return OK;
--- a/src/vim9expr.c
+++ b/src/vim9expr.c
@@ -762,9 +762,9 @@ compile_call(
 
 	argvars[0].v_type = VAR_UNKNOWN;
 	if (*s == '"')
-	    (void)eval_string(&s, &argvars[0], TRUE);
+	    (void)eval_string(&s, &argvars[0], TRUE, FALSE);
 	else if (*s == '\'')
-	    (void)eval_lit_string(&s, &argvars[0], TRUE);
+	    (void)eval_lit_string(&s, &argvars[0], TRUE, FALSE);
 	s = skipwhite(s);
 	if (*s == ')' && argvars[0].v_type == VAR_STRING
 	       && ((is_has && !dynamic_feature(argvars[0].vval.v_string))
@@ -1375,30 +1375,73 @@ compile_get_env(char_u **arg, cctx_T *cc
 }
 
 /*
- * Compile "$"string"" or "$'string'".
+ * Compile $"string" or $'string'.
  */
     static int
 compile_interp_string(char_u **arg, cctx_T *cctx)
 {
     typval_T	tv;
     int		ret;
+    int		quote;
     int		evaluate = cctx->ctx_skip != SKIP_YES;
+    int		count = 0;
+    char_u	*p;
 
-    // *arg is on the '$' character.
-    (*arg)++;
+    // *arg is on the '$' character, move it to the first string character.
+    ++*arg;
+    quote = **arg;
+    ++*arg;
 
-    if (**arg == '"')
-	ret = eval_string(arg, &tv, evaluate);
-    else
-	ret = eval_lit_string(arg, &tv, evaluate);
+    for (;;)
+    {
+	// Get the string up to the matching quote or to a single '{'.
+	// "arg" is advanced to either the quote or the '{'.
+	if (quote == '"')
+	    ret = eval_string(arg, &tv, evaluate, TRUE);
+	else
+	    ret = eval_lit_string(arg, &tv, evaluate, TRUE);
+	if (ret == FAIL)
+	    break;
+	if (evaluate)
+	{
+	    if ((tv.vval.v_string != NULL && *tv.vval.v_string != NUL)
+		    || (**arg != '{' && count == 0))
+	    {
+		// generate non-empty string or empty string if it's the only
+		// one
+		if (generate_PUSHS(cctx, &tv.vval.v_string) == FAIL)
+		    return FAIL;
+		tv.vval.v_string = NULL;  // don't free it now
+		++count;
+	    }
+	    clear_tv(&tv);
+	}
+
+	if (**arg != '{')
+	{
+	    // found terminating quote
+	    ++*arg;
+	    break;
+	}
+
+	p = compile_one_expr_in_str(*arg, cctx);
+	if (p == NULL)
+	{
+	    ret = FAIL;
+	    break;
+	}
+	++count;
+	*arg = p;
+    }
 
     if (ret == FAIL || !evaluate)
 	return ret;
 
-    ret = compile_all_expr_in_str(tv.vval.v_string, TRUE, cctx);
-    clear_tv(&tv);
+    // Small optimization, if there's only a single piece skip the ISN_CONCAT.
+    if (count > 1)
+	return generate_CONCAT(cctx, count);
 
-    return ret;
+    return OK;
 }
 
 /*
@@ -2161,14 +2204,14 @@ compile_expr8(
 	/*
 	 * String constant: "string".
 	 */
-	case '"':   if (eval_string(arg, rettv, TRUE) == FAIL)
+	case '"':   if (eval_string(arg, rettv, TRUE, FALSE) == FAIL)
 			return FAIL;
 		    break;
 
 	/*
 	 * Literal string constant: 'str''ing'.
 	 */
-	case '\'':  if (eval_lit_string(arg, rettv, TRUE) == FAIL)
+	case '\'':  if (eval_lit_string(arg, rettv, TRUE, FALSE) == FAIL)
 			return FAIL;
 		    break;
 
--- a/src/vim9instr.c
+++ b/src/vim9instr.c
@@ -726,6 +726,8 @@ generate_PUSHF(cctx_T *cctx, float_T fnu
 /*
  * Generate an ISN_PUSHS instruction.
  * Consumes "*str".  When freed *str is set to NULL, unless "str" is NULL.
+ * Note that if "str" is used in the instruction OK is returned and "*str" is
+ * not set to NULL.
  */
     int
 generate_PUSHS(cctx_T *cctx, char_u **str)