vim-patch:8.2.1794: no falsy Coalescing operator

Problem:    No falsy Coalescing operator.
Solution:   Add the "??" operator.  Fix mistake with function argument count.

92f26c256e

Cherry-pick tv2bool() into eval/typval.c.
Cherry-pick *??* tag from Vim runtime.

Co-authored-by: Bram Moolenaar <Bram@vim.org>
This commit is contained in:
zeertzjq 2023-04-14 11:00:17 +08:00
parent 7caf0eafd8
commit d6e2804ab4
5 changed files with 137 additions and 31 deletions

View File

@ -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*
expr2 ? expr1 : expr1
The ternary operator: expr2 ? expr1 : expr1
The falsy operator: expr2 ?? 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*

View File

@ -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;
if (op_falsy) {
(*arg)++;
}
// 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;
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 {

View File

@ -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;
}

View File

@ -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[''])

View File

@ -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 :?