# HG changeset patch # User Bram Moolenaar # Date 1651839303 -7200 # Node ID 723c7d940cba4dfcff9619042c8b7f5d9f2bccbb # Parent 3953457538c98357b04487ebdc3ce3c0e5765cc0 patch 8.2.4883: string interpolation only works in heredoc Commit: https://github.com/vim/vim/commit/2eaef106e4a7fc9dc74a7e672b5f550ec1f9786e Author: LemonBoy 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) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- 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 diff --git a/src/errors.h b/src/errors.h --- 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 diff --git a/src/eval.c b/src/eval.c --- 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; /* diff --git a/src/evalvars.c b/src/evalvars.c --- 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); diff --git a/src/proto/evalvars.pro b/src/proto/evalvars.pro --- 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 : */ diff --git a/src/proto/typval.pro b/src/proto/typval.pro --- 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); diff --git a/src/proto/vim9compile.pro b/src/proto/vim9compile.pro --- 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); diff --git a/src/testdir/test_debugger.vim b/src/testdir/test_debugger.vim --- 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) diff --git a/src/testdir/test_expr.vim b/src/testdir/test_expr.vim --- 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 diff --git a/src/testdir/test_let.vim b/src/testdir/test_let.vim --- 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) diff --git a/src/testdir/test_vim9_assign.vim b/src/testdir/test_vim9_assign.vim --- 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 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 @@ -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 diff --git a/src/typval.c b/src/typval.c --- 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. diff --git a/src/version.c b/src/version.c --- 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, diff --git a/src/vim9compile.c b/src/vim9compile.c --- 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 `=`). 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 `=` 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; } diff --git a/src/vim9expr.c b/src/vim9expr.c --- 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; /*