]> granicus.if.org Git - vim/commitdiff
patch 8.2.1794: no falsy Coalescing operator v8.2.1794
authorBram Moolenaar <Bram@vim.org>
Sat, 3 Oct 2020 18:17:30 +0000 (20:17 +0200)
committerBram Moolenaar <Bram@vim.org>
Sat, 3 Oct 2020 18:17:30 +0000 (20:17 +0200)
Problem:    No falsy Coalescing operator.
Solution:   Add the "??" operator.  Fix mistake with function argument count.

runtime/doc/eval.txt
src/eval.c
src/testdir/test_expr.vim
src/testdir/test_vim9_disassemble.vim
src/testdir/test_vim9_expr.vim
src/version.c
src/vim9compile.c
src/vim9type.c

index 6bf205d4e35fd3b7da48899d05d244e5d32b0342..b2b16007ecca3e2aca28ebed6b022967c640ddcd 100644 (file)
@@ -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-continuation| is suggested: >
 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*
 ---------------
index c8c4f6e5f4a158c3498b5a63f90f30d5eb9ded78..911f0abc671b8a3fa8f5517c8f2cf78e4733cb72 100644 (file)
@@ -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, evalarg_T *evalarg)
     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, evalarg_T *evalarg)
        {
            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, evalarg_T *evalarg)
        /*
         * 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, evalarg_T *evalarg)
            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;
        }
+       if (!op_falsy || !result)
+           *rettv = var2;
 
-       /*
-        * 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 (!op_falsy)
        {
-           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
+           *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 (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 (evaluate && !result)
-           *rettv = var2;
 
        if (evalarg == NULL)
            clear_evalarg(&local_evalarg, NULL);
index cfae760d4910f61f16f0ef43015e4990a4d5a414..1086534dcae0803ac972737f859eba0edce3bb6a 100644 (file)
@@ -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[''])
index f1f358af9b7466ff0d93d0c178043eeb2e0b736c..a8dbe1a3cc34fbfb3d63331f07016fc9d4b06726 100644 (file)
@@ -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('\<SNR>\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],
index fa33089e841e9a0f52f3d482b52aa7a91ba5164b..253e469bfa52a9c95b75e9e9c7df4612ab997638 100644 (file)
@@ -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)
index beca185c764375349de02594c3e6d7566d9d815f..bd57233f03c163033dc32012d1ffc19719772d2c 100644 (file)
@@ -750,6 +750,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    1794,
 /**/
     1793,
 /**/
index 32def6c7962cb0604119e475dcb57081ee000fad..88da80044e17c85269d61b3b08e974df2d7d240b 100644 (file)
@@ -4132,14 +4132,20 @@ compile_expr2(char_u **arg, cctx_T *cctx, ppconst_T *ppconst)
 
 /*
  * 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 *cctx, ppconst_T *ppconst)
     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 *cctx, ppconst_T *ppconst)
            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 *cctx, ppconst_T *ppconst)
            // 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 *cctx, ppconst_T *ppconst)
        {
            generate_ppconst(cctx, ppconst);
 
-           // remember the type and drop it
-           --stack->ga_len;
-           type1 = ((type_T **)stack->ga_data)[stack->ga_len];
+           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);
+               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;
+               // 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 != ':')
+       if (!op_falsy)
        {
-           emsg(_(e_missing_colon));
-           return FAIL;
-       }
-       if (next != NULL)
-       {
-           *arg = next_line_from_context(cctx, TRUE);
-           p = skipwhite(*arg);
-       }
+           // 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;
-       }
+           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
+           // 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;
+           *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);
+           typep = ((type_T **)stack->ga_data) + stack->ga_len - 1;
+           common_type(type1, *typep, typep, cctx->ctx_type_list);
 
-           // jump here from JUMP_ALWAYS
+           // 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;
        }
index 931b575b6673b98821154512a6a3cc15098ea89a..b5866bc400b7351fcaaa9a2763463c97b9d08419 100644 (file)
@@ -924,6 +924,10 @@ common_type(type_T *type1, type_T *type2, type_T **dest, garray_T *type_gap)
            }
            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;
        }
     }