aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/eval.txt46
-rw-r--r--src/nvim/eval.c65
-rw-r--r--src/nvim/eval/typval.c31
-rw-r--r--test/old/testdir/test_expr.vim22
-rw-r--r--test/old/testdir/test_help_tagjump.vim4
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 :?