changeset 28718:723c7d940cba

patch 8.2.4883: string interpolation only works in heredoc Commit: https://github.com/vim/vim/commit/2eaef106e4a7fc9dc74a7e672b5f550ec1f9786e Author: LemonBoy <thatlemon@gmail.com> Date: Fri May 6 13:14:50 2022 +0100 patch 8.2.4883: string interpolation only works in heredoc Problem: String interpolation only works in heredoc. Solution: Support interpolated strings. Use syntax for heredoc consistent with strings, similar to C#. (closes #10327)
author Bram Moolenaar <Bram@vim.org>
date Fri, 06 May 2022 14:15:03 +0200
parents 3953457538c9
children 62cef8d5dd2b
files runtime/doc/eval.txt src/errors.h src/eval.c src/evalvars.c src/proto/evalvars.pro src/proto/typval.pro src/proto/vim9compile.pro src/testdir/test_debugger.vim src/testdir/test_expr.vim src/testdir/test_let.vim src/testdir/test_vim9_assign.vim src/testdir/test_vim9_disassemble.vim src/typval.c src/version.c src/vim9compile.c src/vim9expr.c
diffstat 16 files changed, 366 insertions(+), 139 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -1523,6 +1523,25 @@ to be doubled.  These two commands are e
 	if a =~ '\s*'
 
 
+interpolated-string					*interp-string* *E256*
+--------------------
+$"string"		interpolated string constant		*expr-$quote*
+$'string'		interpolated literal string constant	*expr-$'*
+
+Interpolated strings are an extension of the |string| and |literal-string|,
+allowing the inclusion of Vim script expressions (see |expr1|).  Any
+expression returning a value can be enclosed between curly braces.  The value
+is converted to a string.  All the text and results of the expressions
+are concatenated to make a new string.
+
+To include an opening brace '{' or closing brace '}' in the string content
+double it.
+
+Examples: >
+	let your_name = input("What's your name? ")
+	echo $"Hello, {your_name}!"
+	echo $"The square root of 9 is {sqrt(9)}"
+
 option						*expr-option* *E112* *E113*
 ------
 &option			option value, local value if possible
--- a/src/errors.h
+++ b/src/errors.h
@@ -3268,4 +3268,8 @@ EXTERN char e_illegal_map_mode_string_st
 EXTERN char e_channel_job_feature_not_available[]
 	INIT(= N_("E1277: Channel and job feature is not available"));
 # endif
+EXTERN char e_stray_closing_curly_str[]
+	INIT(= N_("E1278: Stray '}' without a matching '{': %s"));
+EXTERN char e_missing_close_curly_str[]
+	INIT(= N_("E1279: Missing '}': %s"));
 #endif
--- a/src/eval.c
+++ b/src/eval.c
@@ -3769,8 +3769,12 @@ eval7(
 
     /*
      * Environment variable: $VAR.
+     * Interpolated string: $"string" or $'string'.
      */
-    case '$':	ret = eval_env_var(arg, rettv, evaluate);
+    case '$':	if ((*arg)[1] == '"' || (*arg)[1] == '\'')
+		    ret = eval_interp_string(arg, rettv, evaluate);
+		else
+		    ret = eval_env_var(arg, rettv, evaluate);
 		break;
 
     /*
--- a/src/evalvars.c
+++ b/src/evalvars.c
@@ -603,59 +603,88 @@ list_script_vars(int *first)
 }
 
 /*
- * Evaluate all the Vim expressions (`=expr`) in string "str" and return the
+ * Evaluate all the Vim expressions ({expr}) in string "str" and return the
  * resulting string.  The caller must free the returned string.
  */
-    static char_u *
+    char_u *
 eval_all_expr_in_str(char_u *str)
 {
     garray_T	ga;
-    char_u	*s;
     char_u	*p;
     char_u	save_c;
-    char_u	*exprval;
-    int		status;
+    char_u	*expr_val;
 
     ga_init2(&ga, 1, 80);
     p = str;
 
-    // Look for `=expr`, evaluate the expression and replace `=expr` with the
-    // result.
     while (*p != NUL)
     {
-	s = p;
-	while (*p != NUL && (*p != '`' || p[1] != '='))
-	    p++;
-	ga_concat_len(&ga, s, p - s);
-	if (*p == NUL)
-	    break;		// no backtick expression found
-
-	s = p;
-	p += 2;		// skip `=
-
-	status = *p == NUL ? OK : skip_expr(&p, NULL);
-	if (status == FAIL || *p != '`')
+	char_u	*lit_start;
+	char_u	*block_start;
+	char_u	*block_end;
+	int	escaped_brace = FALSE;
+
+	// Look for a block start.
+	lit_start = p;
+	while (*p != '{' && *p != '}' && *p != NUL)
+	    ++p;
+
+	if (*p != NUL && *p == p[1])
 	{
-	    // invalid expression or missing ending backtick
-	    if (status != FAIL)
-		emsg(_(e_missing_backtick));
-	    vim_free(ga.ga_data);
+	    // Escaped brace, unescape and continue.
+	    // Include the brace in the literal string.
+	    ++p;
+	    escaped_brace = TRUE;
+	}
+	else if (*p == '}')
+	{
+	    semsg(_(e_stray_closing_curly_str), str);
+	    ga_clear(&ga);
 	    return NULL;
 	}
-	s += 2;		// skip `=
-	save_c = *p;
-	*p = NUL;
-	exprval = eval_to_string(s, TRUE);
-	*p = save_c;
-	p++;
-	if (exprval == NULL)
+
+	// Append the literal part.
+	ga_concat_len(&ga, lit_start, (size_t)(p - lit_start));
+
+	if (*p == NUL)
+	    break;
+
+	if (escaped_brace)
 	{
-	    // expression evaluation failed
-	    vim_free(ga.ga_data);
+	    // Skip the second brace.
+	    ++p;
+	    continue;
+	}
+
+	// Skip the opening {.
+	block_start = ++p;
+	block_end = block_start;
+	if (*block_start != NUL && skip_expr(&block_end, NULL) == FAIL)
+	{
+	    ga_clear(&ga);
 	    return NULL;
 	}
-	ga_concat(&ga, exprval);
-	vim_free(exprval);
+	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);
 
@@ -825,7 +854,7 @@ heredoc_get(exarg_T *eap, char_u *cmd, i
 	str = theline + ti;
 	if (vim9compile)
 	{
-	    if (compile_heredoc_string(str, evalstr, cctx) == FAIL)
+	    if (compile_all_expr_in_str(str, evalstr, cctx) == FAIL)
 	    {
 		vim_free(theline);
 		vim_free(text_indent);
--- a/src/proto/evalvars.pro
+++ b/src/proto/evalvars.pro
@@ -105,4 +105,6 @@ 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
@@ -72,6 +72,7 @@ int eval_string(char_u **arg, typval_T *
 int eval_lit_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,7 +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);
-int compile_heredoc_string(char_u *str, int evalstr, 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);
 int get_var_dest(char_u *name, assign_dest_T *dest, cmdidx_T cmdidx, int *option_scope, int *vimvaridx, type_T **type, cctx_T *cctx);
--- a/src/testdir/test_debugger.vim
+++ b/src/testdir/test_debugger.vim
@@ -377,7 +377,7 @@ func Test_Debugger_breakadd_expr()
   let expected =<< eval trim END
     Oldval = "10"
     Newval = "11"
-    `=fnamemodify('Xtest.vim', ':p')`
+    {fnamemodify('Xtest.vim', ':p')}
     line 1: let g:Xtest_var += 1
   END
   call RunDbgCmd(buf, ':source %', expected)
@@ -385,7 +385,7 @@ func Test_Debugger_breakadd_expr()
   let expected =<< eval trim END
     Oldval = "11"
     Newval = "12"
-    `=fnamemodify('Xtest.vim', ':p')`
+    {fnamemodify('Xtest.vim', ':p')}
     line 1: let g:Xtest_var += 1
   END
   call RunDbgCmd(buf, ':source %', expected)
--- a/src/testdir/test_expr.vim
+++ b/src/testdir/test_expr.vim
@@ -890,4 +890,60 @@ func Test_float_compare()
   call v9.CheckLegacyAndVim9Success(lines)
 endfunc
 
+func Test_string_interp()
+  let lines =<< trim END
+    call assert_equal('', $"")
+    call assert_equal('foobar', $"foobar")
+    #" Escaping rules.
+    call assert_equal('"foo"{bar}', $"\"foo\"{{bar}}")
+    call assert_equal('"foo"{bar}', $'"foo"{{bar}}')
+    call assert_equal('foobar', $"{\"foo\"}" .. $'{''bar''}')
+    #" Whitespace before/after the expression.
+    call assert_equal('3', $"{ 1 + 2 }")
+    #" String conversion.
+    call assert_equal('hello from ' .. v:version, $"hello from {v:version}")
+    call assert_equal('hello from ' .. v:version, $'hello from {v:version}')
+    #" Paper over a small difference between VimScript behaviour.
+    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")
+    VAR var1 = "sun"
+    VAR var2 = "shine"
+    call assert_equal('sunshine', $"{var1}{var2}")
+    call assert_equal('sunsunsun', $"{var1->repeat(3)}")
+    #" Multibyte strings.
+    call assert_equal('say ハロー・ワールド', $"say {'ハロー・ワールド'}")
+    #" Nested.
+    call assert_equal('foobarbaz', $"foo{$\"{'bar'}\"}baz")
+    #" Do not evaluate blocks when the expr is skipped.
+    VAR tmp = 0
+    if v:false
+      echo "${ LET tmp += 1 }"
+    endif
+    call assert_equal(0, tmp)
+
+    #" Stray closing brace.
+    call assert_fails('echo $"moo}"', 'E1278:')
+    #" Undefined variable in expansion.
+    call assert_fails('echo $"{moo}"', 'E121:')
+    #" Empty blocks are rejected.
+    call assert_fails('echo $"{}"', 'E15:')
+    call assert_fails('echo $"{   }"', 'E15:')
+  END
+  call v9.CheckLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+    call assert_equal('5', $"{({x -> x + 1})(4)}")
+  END
+  call v9.CheckLegacySuccess(lines)
+
+  let lines =<< trim END
+    call assert_equal('5', $"{((x) => x + 1)(4)}")
+    call assert_fails('echo $"{ # foo }"', 'E1279:')
+  END
+  call v9.CheckDefAndScriptSuccess(lines)
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
--- a/src/testdir/test_let.vim
+++ b/src/testdir/test_let.vim
@@ -381,6 +381,17 @@ END
   call assert_equal(['Text', 'with', 'indent'], text)
 endfunc
 
+func Test_let_interpolated()
+  call assert_equal('{text}', $'{{text}}')
+  call assert_equal('{{text}}', $'{{{{text}}}}')
+  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 .. \"{{\"}")
+endfunc
+
 " Test for the setting a variable using the heredoc syntax.
 " Keep near the end, this messes up highlighting.
 func Test_let_heredoc()
@@ -498,72 +509,72 @@ END
   call assert_equal(['     x', '     \y', '     z'], [a, b, c])
 endfunc
 
-" Test for evaluating Vim expressions in a heredoc using `=expr`
+" Test for evaluating Vim expressions in a heredoc using {expr}
 " Keep near the end, this messes up highlighting.
 func Test_let_heredoc_eval()
   let str = ''
   let code =<< trim eval END
-    let a = `=5 + 10`
-    let b = `=min([10, 6])` + `=max([4, 6])`
-    `=str`
-    let c = "abc`=str`d"
+    let a = {5 + 10}
+    let b = {min([10, 6])} + {max([4, 6])}
+    {str}
+    let c = "abc{str}d"
   END
   call assert_equal(['let a = 15', 'let b = 6 + 6', '', 'let c = "abcd"'], code)
 
   let $TESTVAR = "Hello"
   let code =<< eval trim END
-    let s = "`=$TESTVAR`"
+    let s = "{$TESTVAR}"
   END
   call assert_equal(['let s = "Hello"'], code)
 
   let code =<< eval END
-    let s = "`=$TESTVAR`"
+    let s = "{$TESTVAR}"
 END
   call assert_equal(['    let s = "Hello"'], code)
 
   let a = 10
   let data =<< eval END
-`=a`
+{a}
 END
   call assert_equal(['10'], data)
 
   let x = 'X'
   let code =<< eval trim END
-    let a = `abc`
-    let b = `=x`
-    let c = `
+    let a = {{abc}}
+    let b = {x}
+    let c = {{
   END
-  call assert_equal(['let a = `abc`', 'let b = X', 'let c = `'], code)
+  call assert_equal(['let a = {abc}', 'let b = X', 'let c = {'], code)
 
   let code = 'xxx'
   let code =<< eval trim END
-    let n = `=5 +
-    6`
+    let n = {5 +
+    6}
   END
   call assert_equal('xxx', code)
 
   let code =<< eval trim END
-     let n = `=min([1, 2]` + `=max([3, 4])`
+     let n = {min([1, 2]} + {max([3, 4])}
   END
   call assert_equal('xxx', code)
 
   let lines =<< trim LINES
       let text =<< eval trim END
-        let b = `=
+        let b = {
       END
   LINES
-  call v9.CheckScriptFailure(lines, 'E1083:')
+  call v9.CheckScriptFailure(lines, 'E1279:')
 
   let lines =<< trim LINES
       let text =<< eval trim END
-        let b = `=abc
+        let b = {abc
       END
   LINES
-  call v9.CheckScriptFailure(lines, 'E1083:')
+  call v9.CheckScriptFailure(lines, 'E1279:')
 
   let lines =<< trim LINES
       let text =<< eval trim END
-        let b = `=`
+        let b = {}
       END
   LINES
   call v9.CheckScriptFailure(lines, 'E15:')
@@ -571,7 +582,7 @@ END
   " skipped heredoc
   if 0
     let msg =<< trim eval END
-        n is: `=n`
+        n is: {n}
     END
   endif
 
@@ -583,7 +594,7 @@ END
   let lines =<< trim END
     let Xvar =<< eval CODE
     let a = 1
-    let b = `=5+`
+    let b = {5+}
     let c = 2
     CODE
     let g:Count += 1
@@ -592,10 +603,10 @@ END
   let g:Count = 0
   call assert_fails('source', 'E15:')
   call assert_equal(1, g:Count)
-  call setline(3, 'let b = `=abc`')
+  call setline(3, 'let b = {abc}')
   call assert_fails('source', 'E121:')
   call assert_equal(2, g:Count)
-  call setline(3, 'let b = `=abc` + `=min([9, 4])` + 2')
+  call setline(3, 'let b = {abc} + {min([9, 4])} + 2')
   call assert_fails('source', 'E121:')
   call assert_equal(3, g:Count)
   call assert_equal('test', g:Xvar)
--- a/src/testdir/test_vim9_assign.vim
+++ b/src/testdir/test_vim9_assign.vim
@@ -2670,10 +2670,10 @@ def Test_heredoc_expr()
     var a3 = "3"
     var a4 = ""
     var code =<< trim eval END
-      var a = `=5 + 10`
-      var b = `=min([10, 6])` + `=max([4, 6])`
-      var c = "`=s`"
-      var d = x`=a1`x`=a2`x`=a3`x`=a4`
+      var a = {5 + 10}
+      var b = {min([10, 6])} + {max([4, 6])}
+      var c = "{s}"
+      var d = x{a1}x{a2}x{a3}x{a4}
     END
     assert_equal(['var a = 15', 'var b = 6 + 6', 'var c = "local"', 'var d = x1x2x3x'], code)
   CODE
@@ -2681,7 +2681,7 @@ def Test_heredoc_expr()
 
   lines =<< trim CODE
     var code =<< eval trim END
-      var s = "`=$SOME_ENV_VAR`"
+      var s = "{$SOME_ENV_VAR}"
     END
     assert_equal(['var s = "somemore"'], code)
   CODE
@@ -2689,7 +2689,7 @@ def Test_heredoc_expr()
 
   lines =<< trim CODE
     var code =<< eval END
-      var s = "`=$SOME_ENV_VAR`"
+      var s = "{$SOME_ENV_VAR}"
     END
     assert_equal(['  var s = "somemore"'], code)
   CODE
@@ -2697,34 +2697,34 @@ def Test_heredoc_expr()
 
   lines =<< trim CODE
     var code =<< eval trim END
-      let a = `abc`
-      let b = `=g:someVar`
-      let c = `
+      let a = {{abc}}
+      let b = {g:someVar}
+      let c = {{
     END
-    assert_equal(['let a = `abc`', 'let b = X', 'let c = `'], code)
+    assert_equal(['let a = {abc}', 'let b = X', 'let c = {'], code)
   CODE
   v9.CheckDefAndScriptSuccess(lines)
 
   lines =<< trim LINES
       var text =<< eval trim END
-        let b = `=
+        let b = {
       END
   LINES
-  v9.CheckDefAndScriptFailure(lines, ['E1143: Empty expression: ""', 'E1083: Missing backtick'])
+  v9.CheckDefAndScriptFailure(lines, "E1279: Missing '}'")
 
   lines =<< trim LINES
       var text =<< eval trim END
-        let b = `=abc
+        let b = {abc
       END
   LINES
-  v9.CheckDefAndScriptFailure(lines, ['E1001: Variable not found: abc', 'E1083: Missing backtick'])
+  v9.CheckDefAndScriptFailure(lines, "E1279: Missing '}'")
 
   lines =<< trim LINES
       var text =<< eval trim END
-        let b = `=`
+        let b = {}
       END
   LINES
-  v9.CheckDefAndScriptFailure(lines, ['E1015: Name expected: `', 'E15: Invalid expression: "`"'])
+  v9.CheckDefAndScriptFailure(lines, 'E15: Invalid expression: "}"')
 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
@@ -2840,6 +2840,25 @@ def Test_disassemble_after_reload()
     delfunc g:ThatFunc
 enddef
 
+def s:MakeString(x: number): string
+  return $"x={x} x^2={x * x}"
+enddef
 
+def Test_disassemble_string_interp()
+  var instr = execute('disassemble s:MakeString')
+  assert_match('MakeString\_s*' ..
+        'return $"x={x} x^2={x \* x}"\_s*' ..
+        '0 PUSHS "x="\_s*' ..
+        '1 LOAD arg\[-1\]\_s*' ..
+        '2 2STRING stack\[-1\]\_s*' ..
+        '3 PUSHS " x^2="\_s*' ..
+        '4 LOAD arg\[-1\]\_s*' ..
+        '5 LOAD arg\[-1\]\_s*' ..
+        '6 OPNR \*\_s*' ..
+        '7 2STRING stack\[-1\]\_s*' ..
+        '8 CONCAT size 4\_s*' ..
+        '9 RETURN\_s*',
+        instr)
+enddef
 
 " vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker
--- a/src/typval.c
+++ b/src/typval.c
@@ -2266,6 +2266,32 @@ eval_lit_string(char_u **arg, typval_T *
     return OK;
 }
 
+    int
+eval_interp_string(char_u **arg, typval_T *rettv, int evaluate)
+{
+    typval_T	tv;
+    int		ret;
+
+    // *arg is on the '$' character.
+    (*arg)++;
+
+    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)
+	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;
+}
+
 /*
  * Return a string with the string representation of a variable.
  * If the memory is allocated "tofree" is set to it, otherwise NULL.
--- 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 */
 /**/
+    4883,
+/**/
     4882,
 /**/
     4881,
--- a/src/vim9compile.c
+++ b/src/vim9compile.c
@@ -969,79 +969,102 @@ theend:
 }
 
 /*
- * Compile a heredoc 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 in a Vim9 def function.
- * Vim9 instructions are generated to push strings, evaluate expressions,
- * concatenate them and create a list of lines.  When "evalstr" is TRUE, Vim
- * expressions in "str" are evaluated.
+ * 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
+ * in a Vim9 def function.  Vim9 instructions are generated to push strings,
+ * evaluate expressions, concatenate them and create a list of lines.  When
+ * "evalstr" is TRUE, Vim expressions in "str" are evaluated.
  */
     int
-compile_heredoc_string(char_u *str, int evalstr, cctx_T *cctx)
+compile_all_expr_in_str(char_u *str, int evalstr, cctx_T *cctx)
 {
-    char_u	*p;
+    char_u	*p = str;
     char_u	*val;
+    char_u	save_c;
+    int		count = 0;
 
     if (cctx->ctx_skip == SKIP_YES)
 	return OK;
 
-    if (evalstr && (p = (char_u *)strstr((char *)str, "`=")) != NULL)
+    if (!evalstr || *str == NUL)
+    {
+	// Literal string, possibly empty.
+	val = *str != NUL ? vim_strsave(str) : NULL;
+	return generate_PUSHS(cctx, &val);
+    }
+
+    // Push all the string pieces to the stack, followed by a ISN_CONCAT.
+    while (*p != NUL)
     {
-	char_u	*start = str;
-	int	count = 0;
-
-	// Need to evaluate expressions of the form `=<expr>` in the string.
-	// Split the string into literal strings and Vim expressions and
-	// generate instructions to concatenate the literal strings and the
-	// result of evaluating the Vim expressions.
-	for (;;)
+	char_u	*lit_start;
+	char_u	*block_start;
+	char_u	*block_end;
+	int	escaped_brace = FALSE;
+
+	// Look for a block start.
+	lit_start = p;
+	while (*p != '{' && *p != '}' && *p != NUL)
+	    ++p;
+
+	if (*p != NUL && *p == p[1])
 	{
-	    if (p > start)
-	    {
-		// literal string before the expression
-		val = vim_strnsave(start, p - start);
-		generate_PUSHS(cctx, &val);
-		count++;
-	    }
-	    p += 2;
-
-	    // evaluate the Vim expression and convert the result to string.
-	    if (compile_expr0(&p, cctx) == FAIL)
+	    // Escaped brace, unescape and continue.
+	    // Include the brace in the literal string.
+	    ++p;
+	    escaped_brace = TRUE;
+	}
+	else if (*p == '}')
+	{
+	    semsg(_(e_stray_closing_curly_str), str);
+	    return FAIL;
+	}
+
+	// Append the literal part.
+	if (p != lit_start)
+	{
+	    val = vim_strnsave(lit_start, (size_t)(p - lit_start));
+	    if (generate_PUSHS(cctx, &val) == FAIL)
 		return FAIL;
-	    may_generate_2STRING(-1, TRUE, cctx);
-	    count++;
-
-	    p = skipwhite(p);
-	    if (*p != '`')
-	    {
-		emsg(_(e_missing_backtick));
-		return FAIL;
-	    }
-	    start = p + 1;
-
-	    p = (char_u *)strstr((char *)start, "`=");
-	    if (p == NULL)
-	    {
-		// no more Vim expressions left to process
-		if (*skipwhite(start) != NUL)
-		{
-		    val = vim_strsave(start);
-		    generate_PUSHS(cctx, &val);
-		    count++;
-		}
-		break;
-	    }
+	    ++count;
+	}
+
+	if (*p == NUL)
+	    break;
+
+	if (escaped_brace)
+	{
+	    // Skip the second brace.
+	    ++p;
+	    continue;
 	}
 
-	if (count > 1)
-	    generate_CONCAT(cctx, count);
+	// Skip the opening {.
+	block_start = skipwhite(p + 1);
+	block_end = block_start;
+	if (*block_start != NUL &&skip_expr(&block_end, NULL) == FAIL)
+	    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;
+	}
+	save_c = *block_end;
+	*block_end = NUL;
+	if (compile_expr0(&block_start, cctx) == FAIL)
+	    return FAIL;
+	*block_end = save_c;
+	may_generate_2STRING(-1, TRUE, cctx);
+	++count;
+
+	p = block_end + 1;
     }
-    else
-    {
-	// literal string
-	val = vim_strsave(str);
-	generate_PUSHS(cctx, &val);
-    }
+
+    // Small optimization, if there's only a single piece skip the ISN_CONCAT.
+    if (count != 1)
+	return generate_CONCAT(cctx, count);
 
     return OK;
 }
--- a/src/vim9expr.c
+++ b/src/vim9expr.c
@@ -1375,6 +1375,33 @@ compile_get_env(char_u **arg, cctx_T *cc
 }
 
 /*
+ * Compile "$"string"" or "$'string'".
+ */
+    static int
+compile_interp_string(char_u **arg, cctx_T *cctx)
+{
+    typval_T	tv;
+    int		ret;
+    int		evaluate = cctx->ctx_skip != SKIP_YES;
+
+    // *arg is on the '$' character.
+    (*arg)++;
+
+    if (**arg == '"')
+	ret = eval_string(arg, &tv, evaluate);
+    else
+	ret = eval_lit_string(arg, &tv, evaluate);
+
+    if (ret == FAIL || !evaluate)
+	return ret;
+
+    ret = compile_all_expr_in_str(tv.vval.v_string, TRUE, cctx);
+    clear_tv(&tv);
+
+    return ret;
+}
+
+/*
  * Compile "@r".
  */
     static int
@@ -2226,10 +2253,14 @@ compile_expr8(
 
 	/*
 	 * Environment variable: $VAR.
+	 * Interpolated string: $"string" or $'string'.
 	 */
 	case '$':	if (generate_ppconst(cctx, ppconst) == FAIL)
 			    return FAIL;
-			ret = compile_get_env(arg, cctx);
+			if ((*arg)[1] == '"' || (*arg)[1] == '\'')
+			    ret = compile_interp_string(arg, cctx);
+			else
+			    ret = compile_get_env(arg, cctx);
 			break;
 
 	/*