0
0
mirror of https://github.com/vim/vim.git synced 2025-09-23 03:43:49 -04:00

patch 8.2.1794: no falsy Coalescing operator

Problem:    No falsy Coalescing operator.
Solution:   Add the "??" operator.  Fix mistake with function argument count.
This commit is contained in:
Bram Moolenaar
2020-10-03 20:17:30 +02:00
parent c8fe645c19
commit 92f26c256e
8 changed files with 257 additions and 97 deletions

View 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*
---------------

View 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,10 +2170,11 @@ 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;
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,14 +2192,18 @@ 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;
if (!op_falsy)
{
/*
* Check for the ":".
*/
@@ -2243,6 +2252,7 @@ eval1(char_u **arg, typval_T *rettv, evalarg_T *evalarg)
}
if (evaluate && !result)
*rettv = var2;
}
if (evalarg == NULL)
clear_evalarg(&local_evalarg, NULL);

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

View 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],

View 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)

View File

@@ -750,6 +750,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
1794,
/**/
1793,
/**/

View 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]);
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;
cctx->ctx_skip = save_skip == SKIP_YES || !const_value
? SKIP_YES : SKIP_NOT;
}
}
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,6 +4232,8 @@ compile_expr1(char_u **arg, cctx_T *cctx, ppconst_T *ppconst)
{
generate_ppconst(cctx, ppconst);
if (!op_falsy)
{
// remember the type and drop it
--stack->ga_len;
type1 = ((type_T **)stack->ga_data)[stack->ga_len];
@@ -4224,7 +4245,10 @@ compile_expr1(char_u **arg, cctx_T *cctx, ppconst_T *ppconst)
isn = ((isn_T *)instr->ga_data) + alt_idx;
isn->isn_arg.jump.jump_where = instr->ga_len;
}
}
if (!op_falsy)
{
// Check for the ":".
p = may_peek_next_line(cctx, *arg, &next);
if (*p != ':')
@@ -4253,16 +4277,19 @@ compile_expr1(char_u **arg, cctx_T *cctx, ppconst_T *ppconst)
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;
}

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