]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 8.2.4883: string interpolation only works in heredoc v8.2.4883
authorLemonBoy <thatlemon@gmail.com>
Fri, 6 May 2022 12:14:50 +0000 (13:14 +0100)
committerBram Moolenaar <Bram@vim.org>
Fri, 6 May 2022 12:14:50 +0000 (13:14 +0100)
Problem:    String interpolation only works in heredoc.
Solution:   Support interpolated strings.  Use syntax for heredoc consistent
            with strings, similar to C#. (closes #10327)

16 files changed:
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

index 953227d21d21009aefc3a57061991edc1683b8a3..39208e48dc6c14dc0f24dbf2c5d2155d1b9590c0 100644 (file)
@@ -1523,6 +1523,25 @@ to be doubled.  These two commands are equivalent: >
        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
index 2edbb63b9a59b4ee1ff10f7ac738b0ffc621ccec..0106d1d502d9198e056d9077fb15d8f058abdbd6 100644 (file)
@@ -3268,4 +3268,8 @@ EXTERN char e_illegal_map_mode_string_str[]
 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
index 0c6d2e5ac1e29cae6f85ac8f39deec6994fcd929..18e196f443d8fa64e9163e5de1968ac6920bc4b5 100644 (file)
@@ -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;
 
     /*
index c51b5976d478fff1bce3b4b8522a6e0c8a469b9e..2f285189b6df7f169cd3e82c34475e26e0ff3c67 100644 (file)
@@ -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);
+       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])
+       {
+           // 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;
+       }
+
+       // Append the literal part.
+       ga_concat_len(&ga, lit_start, (size_t)(p - lit_start));
+
        if (*p == NUL)
-           break;              // no backtick expression found
+           break;
 
-       s = p;
-       p += 2;         // skip `=
+       if (escaped_brace)
+       {
+           // Skip the second brace.
+           ++p;
+           continue;
+       }
 
-       status = *p == NUL ? OK : skip_expr(&p, NULL);
-       if (status == FAIL || *p != '`')
+       // Skip the opening {.
+       block_start = ++p;
+       block_end = block_start;
+       if (*block_start != NUL && skip_expr(&block_end, NULL) == FAIL)
        {
-           // invalid expression or missing ending backtick
-           if (status != FAIL)
-               emsg(_(e_missing_backtick));
-           vim_free(ga.ga_data);
+           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)
+       block_end = skipwhite(block_end);
+       // The block must be closed by a }.
+       if (*block_end != '}')
        {
-           // expression evaluation failed
-           vim_free(ga.ga_data);
+           semsg(_(e_missing_close_curly_str), str);
+           ga_clear(&ga);
            return NULL;
        }
-       ga_concat(&ga, exprval);
-       vim_free(exprval);
+       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, int script_get, int vim9compile)
        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);
index ab4320d32db6ae65e8d03a2b75e1a9a1fd4f8b12..97bec7df3304f9886ded47fcf3a2c7873df47f79 100644 (file)
@@ -105,4 +105,6 @@ void set_callback(callback_T *dest, callback_T *src);
 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 : */
index 459cf7c73846cf93721d42a5527f978955969a7f..5b1f97431fd84a1dca863c5e551fabba08da88b1 100644 (file)
@@ -72,6 +72,7 @@ int eval_string(char_u **arg, typval_T *rettv, int evaluate);
 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);
index 1fe46f0075b62fc75a07f3c9dba9308e597e744b..03f7df1a675a7b3d6a84ef4839751174ae57cfb3 100644 (file)
@@ -16,7 +16,7 @@ int may_get_next_line(char_u *whitep, char_u **arg, cctx_T *cctx);
 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);
index 5aa7babc239b82ac1158c01c41d1f5b94596c1d9..17d3616a0e0f3de717e427eaf433539a0cf03d0f 100644 (file)
@@ -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)
index 3c61d270369f578c637523de691346d15ed1dc38..3ea59eb267106e921c3bf55575e83007528ef7b9 100644 (file)
@@ -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
index 32f91f65bcc6ffa07ce87a89bbc80a9afe148931..7e1a9232ef7fe0649de72226f5ef81152645c5a4 100644 (file)
@@ -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)
index 2c3db657e84845897df759b743f2b59707d46863..97a69701d5cd1b923cd695c632c10c11dcaa0333 100644 (file)
@@ -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
index e42ee78470b4bceb68aaaaebc191ad8059a7edbe..3ab3c0d25deeec6864c289cd2de829ad6560f3ea 100644 (file)
@@ -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
index b131ba2c1516dbeecd62470b4cdb6b5678ee2807..afe4e620bbac7ef09f4e816779aa9127f4fd644c 100644 (file)
@@ -2266,6 +2266,32 @@ eval_lit_string(char_u **arg, typval_T *rettv, int evaluate)
     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.
index 4cb930fd6156ed3341d021e2c3fca706d29a0c23..d997897797283577018c9c658455f24ddeb2a534 100644 (file)
@@ -746,6 +746,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    4883,
 /**/
     4882,
 /**/
index b0cf6a787f46bc90cb5e68e7d653b738444887ef..9b7108b83fa718b9c2bcf5a4127859566831fba3 100644 (file)
@@ -969,80 +969,103 @@ 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)
     {
-       char_u  *start = str;
-       int     count = 0;
+       // Literal string, possibly empty.
+       val = *str != NUL ? vim_strsave(str) : NULL;
+       return generate_PUSHS(cctx, &val);
+    }
 
-       // 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 (;;)
+    // Push all the string pieces to the stack, followed by a ISN_CONCAT.
+    while (*p != NUL)
+    {
+       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;
+           // 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;
+       }
 
-           // evaluate the Vim expression and convert the result to string.
-           if (compile_expr0(&p, cctx) == 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++;
+           ++count;
+       }
 
-           p = skipwhite(p);
-           if (*p != '`')
-           {
-               emsg(_(e_missing_backtick));
-               return FAIL;
-           }
-           start = p + 1;
+       if (*p == NUL)
+           break;
 
-           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;
-           }
+       if (escaped_brace)
+       {
+           // Skip the second brace.
+           ++p;
+           continue;
        }
 
-       if (count > 1)
-           generate_CONCAT(cctx, count);
-    }
-    else
-    {
-       // literal string
-       val = vim_strsave(str);
-       generate_PUSHS(cctx, &val);
+       // 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;
     }
 
+    // Small optimization, if there's only a single piece skip the ISN_CONCAT.
+    if (count != 1)
+       return generate_CONCAT(cctx, count);
+
     return OK;
 }
 
index d002dc9b2707e4e3e4a7c8b3dc1f8077502b8046..8bda461de27a9c4ad8a07b3457d3b398b7390bd5 100644 (file)
@@ -1374,6 +1374,33 @@ compile_get_env(char_u **arg, cctx_T *cctx)
     return ret;
 }
 
+/*
+ * 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".
  */
@@ -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;
 
        /*