# HG changeset patch # User Bram Moolenaar # Date 1601749804 -7200 # Node ID 0e03ef68e73842cb98a7baae179a8e4c33ceae94 # Parent 2ebfb710e32067fdd1b7cfa3c37055b28d7e3e85 patch 8.2.1794: no falsy Coalescing operator Commit: https://github.com/vim/vim/commit/92f26c256e06277ff2ec4ce7adea1eb58c85abe0 Author: Bram Moolenaar Date: Sat Oct 3 20:17:30 2020 +0200 patch 8.2.1794: no falsy Coalescing operator Problem: No falsy Coalescing operator. Solution: Add the "??" operator. Fix mistake with function argument count. diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -133,7 +133,27 @@ non-zero number it means TRUE: > :" executed To test for a non-empty string, use empty(): > :if !empty("foo") -< + +< *falsy* *truthy* +An expression can be used as a condition, ignoring the type and only using +whether the value is "sort of true" or "sort of false". Falsy is: + the number zero + empty string, blob, list or dictionary +Other values are truthy. Examples: + 0 falsy + 1 truthy + -1 truthy + 0.0 falsy + 0.1 truthy + '' falsy + 'x' truthy + [] falsy + [0] truthy + {} falsy + #{x: 1} truthy + 0z falsy + 0z00 truthy + *non-zero-arg* Function arguments often behave slightly different from |TRUE|: If the argument is present and it evaluates to a non-zero Number, |v:true| or a @@ -877,10 +897,13 @@ Example: > All expressions within one level are parsed from left to right. -expr1 *expr1* *trinary* *E109* +expr1 *expr1* *trinary* *falsy-operator* *E109* ----- -expr2 ? expr1 : expr1 +The trinary operator: expr2 ? expr1 : expr1 +The falsy operator: expr2 ?? expr1 + +Trinary operator ~ The expression before the '?' is evaluated to a number. If it evaluates to |TRUE|, the result is the value of the expression between the '?' and ':', @@ -903,6 +926,23 @@ To keep this readable, using |line-conti You should always put a space before the ':', otherwise it can be mistaken for use in a variable such as "a:1". +Falsy operator ~ + +This is also known as the "null coalescing operator", but that's too +complicated, thus we just call it the falsy operator. + +The expression before the '??' is evaluated. If it evaluates to +|truthy|, this is used as the result. Otherwise the expression after the '??' +is evaluated and used as the result. This is most useful to have a default +value for an expression that may result in zero or empty: > + echo theList ?? 'list is empty' + echo GetName() ?? 'unknown' + +These are similar, but not equal: > + expr2 ?? expr1 + expr2 ? expr2 : expr1 +In the second line "expr2" is evaluated twice. + expr2 and expr3 *expr2* *expr3* --------------- diff --git a/src/eval.c b/src/eval.c --- a/src/eval.c +++ b/src/eval.c @@ -2110,6 +2110,7 @@ eval0( /* * Handle top level expression: * expr2 ? expr1 : expr1 + * expr2 ?? expr1 * * "arg" must point to the first non-white of the expression. * "arg" is advanced to just after the recognized expression. @@ -2135,6 +2136,7 @@ eval1(char_u **arg, typval_T *rettv, eva p = eval_next_non_blank(*arg, evalarg, &getnext); if (*p == '?') { + int op_falsy = p[1] == '?'; int result; typval_T var2; evalarg_T *evalarg_used = evalarg; @@ -2168,11 +2170,12 @@ eval1(char_u **arg, typval_T *rettv, eva { int error = FALSE; - if (in_vim9script()) + if (in_vim9script() || op_falsy) result = tv2bool(rettv); else if (tv_get_number_chk(rettv, &error) != 0) result = TRUE; - clear_tv(rettv); + if (error || !op_falsy || !result) + clear_tv(rettv); if (error) return FAIL; } @@ -2180,6 +2183,8 @@ eval1(char_u **arg, typval_T *rettv, eva /* * Get the second variable. Recursive! */ + if (op_falsy) + ++*arg; if (evaluate && in_vim9script() && !IS_WHITE_OR_NUL((*arg)[1])) { error_white_both(p, 1); @@ -2187,62 +2192,67 @@ eval1(char_u **arg, typval_T *rettv, eva return FAIL; } *arg = skipwhite_and_linebreak(*arg + 1, evalarg_used); - evalarg_used->eval_flags = result ? orig_flags - : orig_flags & ~EVAL_EVALUATE; - if (eval1(arg, rettv, evalarg_used) == FAIL) + evalarg_used->eval_flags = (op_falsy ? !result : result) + ? orig_flags : orig_flags & ~EVAL_EVALUATE; + if (eval1(arg, &var2, evalarg_used) == FAIL) { evalarg_used->eval_flags = orig_flags; return FAIL; } - - /* - * Check for the ":". - */ - p = eval_next_non_blank(*arg, evalarg_used, &getnext); - if (*p != ':') + if (!op_falsy || !result) + *rettv = var2; + + if (!op_falsy) { - emsg(_(e_missing_colon)); - if (evaluate && result) - clear_tv(rettv); - evalarg_used->eval_flags = orig_flags; - return FAIL; - } - if (getnext) - *arg = eval_next_line(evalarg_used); - else - { - if (evaluate && in_vim9script() && !VIM_ISWHITE(p[-1])) + /* + * Check for the ":". + */ + p = eval_next_non_blank(*arg, evalarg_used, &getnext); + if (*p != ':') + { + emsg(_(e_missing_colon)); + if (evaluate && result) + clear_tv(rettv); + evalarg_used->eval_flags = orig_flags; + return FAIL; + } + if (getnext) + *arg = eval_next_line(evalarg_used); + else + { + if (evaluate && in_vim9script() && !VIM_ISWHITE(p[-1])) + { + error_white_both(p, 1); + clear_tv(rettv); + evalarg_used->eval_flags = orig_flags; + return FAIL; + } + *arg = p; + } + + /* + * Get the third variable. Recursive! + */ + if (evaluate && in_vim9script() && !IS_WHITE_OR_NUL((*arg)[1])) { error_white_both(p, 1); clear_tv(rettv); evalarg_used->eval_flags = orig_flags; return FAIL; } - *arg = p; - } - - /* - * Get the third variable. Recursive! - */ - if (evaluate && in_vim9script() && !IS_WHITE_OR_NUL((*arg)[1])) - { - error_white_both(p, 1); - clear_tv(rettv); - evalarg_used->eval_flags = orig_flags; - return FAIL; + *arg = skipwhite_and_linebreak(*arg + 1, evalarg_used); + evalarg_used->eval_flags = !result ? orig_flags + : orig_flags & ~EVAL_EVALUATE; + if (eval1(arg, &var2, evalarg_used) == FAIL) + { + if (evaluate && result) + clear_tv(rettv); + evalarg_used->eval_flags = orig_flags; + return FAIL; + } + if (evaluate && !result) + *rettv = var2; } - *arg = skipwhite_and_linebreak(*arg + 1, evalarg_used); - evalarg_used->eval_flags = !result ? orig_flags - : orig_flags & ~EVAL_EVALUATE; - if (eval1(arg, &var2, evalarg_used) == FAIL) - { - if (evaluate && result) - clear_tv(rettv); - evalarg_used->eval_flags = orig_flags; - return FAIL; - } - if (evaluate && !result) - *rettv = var2; if (evalarg == NULL) clear_evalarg(&local_evalarg, NULL); 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 @@ -42,6 +42,28 @@ func Test_version() call assert_false(has('patch-9.9.1')) endfunc +func Test_op_falsy() + call assert_equal(v:true, v:true ?? 456) + call assert_equal(123, 123 ?? 456) + call assert_equal('yes', 'yes' ?? 456) + call assert_equal(0z00, 0z00 ?? 456) + call assert_equal([1], [1] ?? 456) + call assert_equal(#{one: 1}, #{one: 1} ?? 456) + if has('float') + call assert_equal(0.1, 0.1 ?? 456) + endif + + call assert_equal(456, v:false ?? 456) + call assert_equal(456, 0 ?? 456) + call assert_equal(456, '' ?? 456) + call assert_equal(456, 0z ?? 456) + call assert_equal(456, [] ?? 456) + call assert_equal(456, {} ?? 456) + if has('float') + call assert_equal(456, 0.0 ?? 456) + endif +endfunc + func Test_dict() let d = {'': 'empty', 'a': 'a', 0: 'zero'} call assert_equal('empty', d['']) 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 @@ -1326,6 +1326,33 @@ def Test_disassemble_compare() delete('Xdisassemble') enddef +def s:FalsyOp() + echo g:flag ?? "yes" + echo [] ?? "empty list" + echo "" ?? "empty string" +enddef + +def Test_dsassemble_falsy_op() + var res = execute('disass s:FalsyOp') + assert_match('\\d*_FalsyOp\_s*' .. + 'echo g:flag ?? "yes"\_s*' .. + '0 LOADG g:flag\_s*' .. + '1 JUMP_AND_KEEP_IF_TRUE -> 3\_s*' .. + '2 PUSHS "yes"\_s*' .. + '3 ECHO 1\_s*' .. + 'echo \[\] ?? "empty list"\_s*' .. + '4 NEWLIST size 0\_s*' .. + '5 JUMP_AND_KEEP_IF_TRUE -> 7\_s*' .. + '6 PUSHS "empty list"\_s*' .. + '7 ECHO 1\_s*' .. + 'echo "" ?? "empty string"\_s*' .. + '\d\+ PUSHS "empty string"\_s*' .. + '\d\+ ECHO 1\_s*' .. + '\d\+ PUSHNR 0\_s*' .. + '\d\+ RETURN', + res) +enddef + def Test_disassemble_compare_const() var cases = [ ['"xx" == "yy"', false], diff --git a/src/testdir/test_vim9_expr.vim b/src/testdir/test_vim9_expr.vim --- a/src/testdir/test_vim9_expr.vim +++ b/src/testdir/test_vim9_expr.vim @@ -12,7 +12,7 @@ def FuncTwo(arg: number): number enddef " test cond ? expr : expr -def Test_expr1() +def Test_expr1_trinary() assert_equal('one', true ? 'one' : 'two') assert_equal('one', 1 ? 'one' : @@ -61,7 +61,7 @@ def Test_expr1() assert_equal(123, Z(3)) enddef -def Test_expr1_vimscript() +def Test_expr1_trinary_vimscript() # check line continuation var lines =<< trim END vim9script @@ -139,7 +139,7 @@ def Test_expr1_vimscript() CheckScriptSuccess(lines) enddef -func Test_expr1_fails() +func Test_expr1_trinary_fails() call CheckDefFailure(["var x = 1 ? 'one'"], "Missing ':' after '?'", 1) let msg = "White space required before and after '?'" @@ -160,6 +160,34 @@ func Test_expr1_fails() \ 'Z()'], 'E119:', 4) endfunc +def Test_expr1_falsy() + var lines =<< trim END + assert_equal(v:true, v:true ?? 456) + assert_equal(123, 123 ?? 456) + assert_equal('yes', 'yes' ?? 456) + assert_equal([1], [1] ?? 456) + assert_equal(#{one: 1}, #{one: 1} ?? 456) + if has('float') + assert_equal(0.1, 0.1 ?? 456) + endif + + assert_equal(456, v:false ?? 456) + assert_equal(456, 0 ?? 456) + assert_equal(456, '' ?? 456) + assert_equal(456, [] ?? 456) + assert_equal(456, {} ?? 456) + if has('float') + assert_equal(456, 0.0 ?? 456) + endif + END + CheckDefAndScriptSuccess(lines) + + var msg = "White space required before and after '??'" + call CheckDefFailure(["var x = 1?? 'one' : 'two'"], msg, 1) + call CheckDefFailure(["var x = 1 ??'one' : 'two'"], msg, 1) + call CheckDefFailure(["var x = 1??'one' : 'two'"], msg, 1) +enddef + " TODO: define inside test function def Record(val: any): any g:vals->add(val) diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -751,6 +751,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 1794, +/**/ 1793, /**/ 1792, diff --git a/src/vim9compile.c b/src/vim9compile.c --- a/src/vim9compile.c +++ b/src/vim9compile.c @@ -4132,14 +4132,20 @@ compile_expr2(char_u **arg, cctx_T *cctx /* * Toplevel expression: expr2 ? expr1a : expr1b - * * Produces instructions: - * EVAL expr2 Push result of "expr" + * EVAL expr2 Push result of "expr2" * JUMP_IF_FALSE alt jump if false * EVAL expr1a * JUMP_ALWAYS end * alt: EVAL expr1b * end: + * + * Toplevel expression: expr2 ?? expr1 + * Produces instructions: + * EVAL expr2 Push result of "expr2" + * JUMP_AND_KEEP_IF_TRUE end jump if true + * EVAL expr1 + * end: */ static int compile_expr1(char_u **arg, cctx_T *cctx, ppconst_T *ppconst) @@ -4162,13 +4168,13 @@ compile_expr1(char_u **arg, cctx_T *cct p = may_peek_next_line(cctx, *arg, &next); if (*p == '?') { + int op_falsy = p[1] == '?'; garray_T *instr = &cctx->ctx_instr; garray_T *stack = &cctx->ctx_type_stack; int alt_idx = instr->ga_len; int end_idx = 0; isn_T *isn; type_T *type1 = NULL; - type_T *type2; int has_const_expr = FALSE; int const_value = FALSE; int save_skip = cctx->ctx_skip; @@ -4179,9 +4185,10 @@ compile_expr1(char_u **arg, cctx_T *cct p = skipwhite(*arg); } - if (!IS_WHITE_OR_NUL(**arg) || !IS_WHITE_OR_NUL(p[1])) + if (!IS_WHITE_OR_NUL(**arg) || !IS_WHITE_OR_NUL(p[1 + op_falsy])) { - semsg(_(e_white_space_required_before_and_after_str), "?"); + semsg(_(e_white_space_required_before_and_after_str), + op_falsy ? "??" : "?"); return FAIL; } @@ -4191,20 +4198,32 @@ compile_expr1(char_u **arg, cctx_T *cct // expression is to be evaluated. has_const_expr = TRUE; const_value = tv2bool(&ppconst->pp_tv[ppconst_used]); - clear_tv(&ppconst->pp_tv[ppconst_used]); - --ppconst->pp_used; - cctx->ctx_skip = save_skip == SKIP_YES || !const_value - ? SKIP_YES : SKIP_NOT; + cctx->ctx_skip = save_skip == SKIP_YES || + (op_falsy ? const_value : !const_value) ? SKIP_YES : SKIP_NOT; + + if (op_falsy && cctx->ctx_skip == SKIP_YES) + // "left ?? right" and "left" is truthy: produce "left" + generate_ppconst(cctx, ppconst); + else + { + clear_tv(&ppconst->pp_tv[ppconst_used]); + --ppconst->pp_used; + } } else { generate_ppconst(cctx, ppconst); - generate_JUMP(cctx, JUMP_IF_FALSE, 0); + if (op_falsy) + end_idx = instr->ga_len; + generate_JUMP(cctx, op_falsy + ? JUMP_AND_KEEP_IF_TRUE : JUMP_IF_FALSE, 0); + if (op_falsy) + type1 = ((type_T **)stack->ga_data)[stack->ga_len]; } // evaluate the second expression; any type is accepted - *arg = skipwhite(p + 1); - if (may_get_next_line(p + 1, arg, cctx) == FAIL) + *arg = skipwhite(p + 1 + op_falsy); + if (may_get_next_line(p + 1 + op_falsy, arg, cctx) == FAIL) return FAIL; if (compile_expr1(arg, cctx, ppconst) == FAIL) return FAIL; @@ -4213,56 +4232,64 @@ compile_expr1(char_u **arg, cctx_T *cct { generate_ppconst(cctx, ppconst); - // remember the type and drop it - --stack->ga_len; - type1 = ((type_T **)stack->ga_data)[stack->ga_len]; - - end_idx = instr->ga_len; - generate_JUMP(cctx, JUMP_ALWAYS, 0); - - // jump here from JUMP_IF_FALSE - isn = ((isn_T *)instr->ga_data) + alt_idx; - isn->isn_arg.jump.jump_where = instr->ga_len; - } - - // Check for the ":". - p = may_peek_next_line(cctx, *arg, &next); - if (*p != ':') - { - emsg(_(e_missing_colon)); - return FAIL; + if (!op_falsy) + { + // remember the type and drop it + --stack->ga_len; + type1 = ((type_T **)stack->ga_data)[stack->ga_len]; + + end_idx = instr->ga_len; + generate_JUMP(cctx, JUMP_ALWAYS, 0); + + // jump here from JUMP_IF_FALSE + isn = ((isn_T *)instr->ga_data) + alt_idx; + isn->isn_arg.jump.jump_where = instr->ga_len; + } } - if (next != NULL) + + if (!op_falsy) { - *arg = next_line_from_context(cctx, TRUE); - p = skipwhite(*arg); - } - - if (!IS_WHITE_OR_NUL(**arg) || !IS_WHITE_OR_NUL(p[1])) - { - semsg(_(e_white_space_required_before_and_after_str), ":"); - return FAIL; + // Check for the ":". + p = may_peek_next_line(cctx, *arg, &next); + if (*p != ':') + { + emsg(_(e_missing_colon)); + return FAIL; + } + if (next != NULL) + { + *arg = next_line_from_context(cctx, TRUE); + p = skipwhite(*arg); + } + + if (!IS_WHITE_OR_NUL(**arg) || !IS_WHITE_OR_NUL(p[1])) + { + semsg(_(e_white_space_required_before_and_after_str), ":"); + return FAIL; + } + + // evaluate the third expression + if (has_const_expr) + cctx->ctx_skip = save_skip == SKIP_YES || const_value + ? SKIP_YES : SKIP_NOT; + *arg = skipwhite(p + 1); + if (may_get_next_line(p + 1, arg, cctx) == FAIL) + return FAIL; + if (compile_expr1(arg, cctx, ppconst) == FAIL) + return FAIL; } - // evaluate the third expression - if (has_const_expr) - cctx->ctx_skip = save_skip == SKIP_YES || const_value - ? SKIP_YES : SKIP_NOT; - *arg = skipwhite(p + 1); - if (may_get_next_line(p + 1, arg, cctx) == FAIL) - return FAIL; - if (compile_expr1(arg, cctx, ppconst) == FAIL) - return FAIL; - if (!has_const_expr) { + type_T **typep; + generate_ppconst(cctx, ppconst); // If the types differ, the result has a more generic type. - type2 = ((type_T **)stack->ga_data)[stack->ga_len - 1]; - common_type(type1, type2, &type2, cctx->ctx_type_list); - - // jump here from JUMP_ALWAYS + typep = ((type_T **)stack->ga_data) + stack->ga_len - 1; + common_type(type1, *typep, typep, cctx->ctx_type_list); + + // jump here from JUMP_ALWAYS or JUMP_AND_KEEP_IF_TRUE isn = ((isn_T *)instr->ga_data) + end_idx; isn->isn_arg.jump.jump_where = instr->ga_len; } diff --git a/src/vim9type.c b/src/vim9type.c --- a/src/vim9type.c +++ b/src/vim9type.c @@ -924,6 +924,10 @@ common_type(type_T *type1, type_T *type2 } else *dest = alloc_func_type(common, -1, type_gap); + // Use the minimum of min_argcount. + (*dest)->tt_min_argcount = + type1->tt_min_argcount < type2->tt_min_argcount + ? type1->tt_min_argcount : type2->tt_min_argcount; return; } }