diff options
-rw-r--r-- | runtime/doc/eval.txt | 46 | ||||
-rw-r--r-- | src/nvim/eval.c | 65 | ||||
-rw-r--r-- | src/nvim/eval/typval.c | 31 | ||||
-rw-r--r-- | test/old/testdir/test_expr.vim | 22 | ||||
-rw-r--r-- | test/old/testdir/test_help_tagjump.vim | 4 |
5 files changed, 137 insertions, 31 deletions
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index fe15ba6115..9f0a6ceb25 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -93,7 +93,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 @@ -841,9 +861,12 @@ All expressions within one level are parsed from left to right. ------------------------------------------------------------------------------ -expr1 *expr1* *ternary* *E109* +expr1 *expr1* *ternary* *falsy-operator* *??* *E109* + +The ternary operator: expr2 ? expr1 : expr1 +The falsy operator: expr2 ?? expr1 -expr2 ? expr1 : expr1 +Ternary 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 ':', @@ -866,6 +889,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* diff --git a/src/nvim/eval.c b/src/nvim/eval.c index dab3afb212..59e998d50f 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -2336,6 +2336,7 @@ int eval0(char *arg, typval_T *rettv, exarg_T *eap, evalarg_T *const evalarg) /// Handle top level expression: /// expr2 ? expr1 : expr1 +/// expr2 ?? expr1 /// /// "arg" must point to the first non-white of the expression. /// "arg" is advanced to the next non-white after the recognized expression. @@ -2352,6 +2353,7 @@ int eval1(char **arg, typval_T *rettv, evalarg_T *const evalarg) char *p = *arg; if (*p == '?') { + const bool op_falsy = p[1] == '?'; evalarg_T *evalarg_used = evalarg; evalarg_T local_evalarg; if (evalarg == NULL) { @@ -2365,49 +2367,62 @@ int eval1(char **arg, typval_T *rettv, evalarg_T *const evalarg) if (evaluate) { bool error = false; - if (tv_get_number_chk(rettv, &error) != 0) { + if (op_falsy) { + result = tv2bool(rettv); + } else if (tv_get_number_chk(rettv, &error) != 0) { result = true; } - tv_clear(rettv); + if (error || !op_falsy || !result) { + tv_clear(rettv); + } if (error) { return FAIL; } } // Get the second variable. Recursive! - *arg = skipwhite(*arg + 1); - evalarg_used->eval_flags = result ? orig_flags : orig_flags & ~EVAL_EVALUATE; - if (eval1(arg, rettv, evalarg_used) == FAIL) { - evalarg_used->eval_flags = orig_flags; - return FAIL; - } - - // Check for the ":". - p = *arg; - if (*p != ':') { - emsg(_("E109: Missing ':' after '?'")); - if (evaluate && result) { - tv_clear(rettv); - } - evalarg_used->eval_flags = orig_flags; - return FAIL; + if (op_falsy) { + (*arg)++; } - - // Get the third variable. Recursive! *arg = skipwhite(*arg + 1); - evalarg_used->eval_flags = !result ? orig_flags : orig_flags & ~EVAL_EVALUATE; + evalarg_used->eval_flags = (op_falsy ? !result : result) + ? orig_flags : orig_flags & ~EVAL_EVALUATE; typval_T var2; if (eval1(arg, &var2, evalarg_used) == FAIL) { - if (evaluate && result) { - tv_clear(rettv); - } evalarg_used->eval_flags = orig_flags; return FAIL; } - if (evaluate && !result) { + if (!op_falsy || !result) { *rettv = var2; } + if (!op_falsy) { + // Check for the ":". + p = *arg; + if (*p != ':') { + emsg(_("E109: Missing ':' after '?'")); + if (evaluate && result) { + tv_clear(rettv); + } + evalarg_used->eval_flags = orig_flags; + return FAIL; + } + + // Get the third variable. Recursive! + *arg = skipwhite(*arg + 1); + evalarg_used->eval_flags = !result ? orig_flags : orig_flags & ~EVAL_EVALUATE; + if (eval1(arg, &var2, evalarg_used) == FAIL) { + if (evaluate && result) { + tv_clear(rettv); + } + evalarg_used->eval_flags = orig_flags; + return FAIL; + } + if (evaluate && !result) { + *rettv = var2; + } + } + if (evalarg == NULL) { clear_evalarg(&local_evalarg, NULL); } else { diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index 3e67571053..91be41751e 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -4201,3 +4201,34 @@ const char *tv_get_string_buf(const typval_T *const tv, char *const buf) return res != NULL ? res : ""; } + +/// Return true when "tv" is not falsy: non-zero, non-empty string, non-empty +/// list, etc. Mostly like what JavaScript does, except that empty list and +/// empty dictionary are false. +bool tv2bool(const typval_T *const tv) +{ + switch (tv->v_type) { + case VAR_NUMBER: + return tv->vval.v_number != 0; + case VAR_FLOAT: + return tv->vval.v_float != 0.0; + case VAR_PARTIAL: + return tv->vval.v_partial != NULL; + case VAR_FUNC: + case VAR_STRING: + return tv->vval.v_string != NULL && *tv->vval.v_string != NUL; + case VAR_LIST: + return tv->vval.v_list != NULL && tv->vval.v_list->lv_len > 0; + case VAR_DICT: + return tv->vval.v_dict != NULL && tv->vval.v_dict->dv_hashtab.ht_used > 0; + case VAR_BOOL: + return tv->vval.v_bool == kBoolVarTrue; + case VAR_SPECIAL: + return tv->vval.v_special == kSpecialVarNull; + case VAR_BLOB: + return tv->vval.v_blob != NULL && tv->vval.v_blob->bv_ga.ga_len > 0; + case VAR_UNKNOWN: + break; + } + return false; +} diff --git a/test/old/testdir/test_expr.vim b/test/old/testdir/test_expr.vim index 292a504df9..66a59f2c44 100644 --- a/test/old/testdir/test_expr.vim +++ b/test/old/testdir/test_expr.vim @@ -39,6 +39,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/test/old/testdir/test_help_tagjump.vim b/test/old/testdir/test_help_tagjump.vim index eae1a241e3..8a58d2f13c 100644 --- a/test/old/testdir/test_help_tagjump.vim +++ b/test/old/testdir/test_help_tagjump.vim @@ -35,9 +35,7 @@ func Test_help_tagjump() help ?? call assert_equal("help", &filetype) - " *??* tag needs patch 8.2.1794 - " call assert_true(getline('.') =~ '\*??\*') - call assert_true(getline('.') =~ '\*g??\*') + call assert_true(getline('.') =~ '\*??\*') helpclose help :? |