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 :" executed
To test for a non-empty string, use empty(): > To test for a non-empty string, use empty(): >
:if !empty("foo") :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* *non-zero-arg*
Function arguments often behave slightly different from |TRUE|: If the 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 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. 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 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 ':', |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 You should always put a space before the ':', otherwise it can be mistaken for
use in a variable such as "a:1". 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* expr2 and expr3 *expr2* *expr3*
--------------- ---------------

View File

@@ -2110,6 +2110,7 @@ eval0(
/* /*
* Handle top level expression: * Handle top level expression:
* expr2 ? expr1 : expr1 * expr2 ? expr1 : expr1
* expr2 ?? expr1
* *
* "arg" must point to the first non-white of the expression. * "arg" must point to the first non-white of the expression.
* "arg" is advanced to just after the recognized 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); p = eval_next_non_blank(*arg, evalarg, &getnext);
if (*p == '?') if (*p == '?')
{ {
int op_falsy = p[1] == '?';
int result; int result;
typval_T var2; typval_T var2;
evalarg_T *evalarg_used = evalarg; evalarg_T *evalarg_used = evalarg;
@@ -2168,11 +2170,12 @@ eval1(char_u **arg, typval_T *rettv, evalarg_T *evalarg)
{ {
int error = FALSE; int error = FALSE;
if (in_vim9script()) if (in_vim9script() || op_falsy)
result = tv2bool(rettv); result = tv2bool(rettv);
else if (tv_get_number_chk(rettv, &error) != 0) else if (tv_get_number_chk(rettv, &error) != 0)
result = TRUE; result = TRUE;
clear_tv(rettv); if (error || !op_falsy || !result)
clear_tv(rettv);
if (error) if (error)
return FAIL; return FAIL;
} }
@@ -2180,6 +2183,8 @@ eval1(char_u **arg, typval_T *rettv, evalarg_T *evalarg)
/* /*
* Get the second variable. Recursive! * Get the second variable. Recursive!
*/ */
if (op_falsy)
++*arg;
if (evaluate && in_vim9script() && !IS_WHITE_OR_NUL((*arg)[1])) if (evaluate && in_vim9script() && !IS_WHITE_OR_NUL((*arg)[1]))
{ {
error_white_both(p, 1); error_white_both(p, 1);
@@ -2187,62 +2192,67 @@ eval1(char_u **arg, typval_T *rettv, evalarg_T *evalarg)
return FAIL; return FAIL;
} }
*arg = skipwhite_and_linebreak(*arg + 1, evalarg_used); *arg = skipwhite_and_linebreak(*arg + 1, evalarg_used);
evalarg_used->eval_flags = result ? orig_flags evalarg_used->eval_flags = (op_falsy ? !result : result)
: orig_flags & ~EVAL_EVALUATE; ? orig_flags : orig_flags & ~EVAL_EVALUATE;
if (eval1(arg, rettv, evalarg_used) == FAIL) if (eval1(arg, &var2, evalarg_used) == FAIL)
{ {
evalarg_used->eval_flags = orig_flags; evalarg_used->eval_flags = orig_flags;
return FAIL; return FAIL;
} }
if (!op_falsy || !result)
*rettv = var2;
/* if (!op_falsy)
* Check for the ":".
*/
p = eval_next_non_blank(*arg, evalarg_used, &getnext);
if (*p != ':')
{ {
emsg(_(e_missing_colon)); /*
if (evaluate && result) * Check for the ":".
clear_tv(rettv); */
evalarg_used->eval_flags = orig_flags; p = eval_next_non_blank(*arg, evalarg_used, &getnext);
return FAIL; if (*p != ':')
} {
if (getnext) emsg(_(e_missing_colon));
*arg = eval_next_line(evalarg_used); if (evaluate && result)
else clear_tv(rettv);
{ evalarg_used->eval_flags = orig_flags;
if (evaluate && in_vim9script() && !VIM_ISWHITE(p[-1])) 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); error_white_both(p, 1);
clear_tv(rettv); clear_tv(rettv);
evalarg_used->eval_flags = orig_flags; evalarg_used->eval_flags = orig_flags;
return FAIL; return FAIL;
} }
*arg = p; *arg = skipwhite_and_linebreak(*arg + 1, evalarg_used);
} evalarg_used->eval_flags = !result ? orig_flags
/*
* 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
: orig_flags & ~EVAL_EVALUATE; : orig_flags & ~EVAL_EVALUATE;
if (eval1(arg, &var2, evalarg_used) == FAIL) if (eval1(arg, &var2, evalarg_used) == FAIL)
{ {
if (evaluate && result) if (evaluate && result)
clear_tv(rettv); clear_tv(rettv);
evalarg_used->eval_flags = orig_flags; evalarg_used->eval_flags = orig_flags;
return FAIL; return FAIL;
}
if (evaluate && !result)
*rettv = var2;
} }
if (evaluate && !result)
*rettv = var2;
if (evalarg == NULL) if (evalarg == NULL)
clear_evalarg(&local_evalarg, NULL); clear_evalarg(&local_evalarg, NULL);

View File

@@ -42,6 +42,28 @@ func Test_version()
call assert_false(has('patch-9.9.1')) call assert_false(has('patch-9.9.1'))
endfunc 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() func Test_dict()
let d = {'': 'empty', 'a': 'a', 0: 'zero'} let d = {'': 'empty', 'a': 'a', 0: 'zero'}
call assert_equal('empty', d['']) call assert_equal('empty', d[''])

View File

@@ -1326,6 +1326,33 @@ def Test_disassemble_compare()
delete('Xdisassemble') delete('Xdisassemble')
enddef 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() def Test_disassemble_compare_const()
var cases = [ var cases = [
['"xx" == "yy"', false], ['"xx" == "yy"', false],

View File

@@ -12,7 +12,7 @@ def FuncTwo(arg: number): number
enddef enddef
" test cond ? expr : expr " test cond ? expr : expr
def Test_expr1() def Test_expr1_trinary()
assert_equal('one', true ? 'one' : 'two') assert_equal('one', true ? 'one' : 'two')
assert_equal('one', 1 ? assert_equal('one', 1 ?
'one' : 'one' :
@@ -61,7 +61,7 @@ def Test_expr1()
assert_equal(123, Z(3)) assert_equal(123, Z(3))
enddef enddef
def Test_expr1_vimscript() def Test_expr1_trinary_vimscript()
# check line continuation # check line continuation
var lines =<< trim END var lines =<< trim END
vim9script vim9script
@@ -139,7 +139,7 @@ def Test_expr1_vimscript()
CheckScriptSuccess(lines) CheckScriptSuccess(lines)
enddef enddef
func Test_expr1_fails() func Test_expr1_trinary_fails()
call CheckDefFailure(["var x = 1 ? 'one'"], "Missing ':' after '?'", 1) call CheckDefFailure(["var x = 1 ? 'one'"], "Missing ':' after '?'", 1)
let msg = "White space required before and after '?'" let msg = "White space required before and after '?'"
@@ -160,6 +160,34 @@ func Test_expr1_fails()
\ 'Z()'], 'E119:', 4) \ 'Z()'], 'E119:', 4)
endfunc 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 " TODO: define inside test function
def Record(val: any): any def Record(val: any): any
g:vals->add(val) g:vals->add(val)

View File

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

View File

@@ -4132,14 +4132,20 @@ compile_expr2(char_u **arg, cctx_T *cctx, ppconst_T *ppconst)
/* /*
* Toplevel expression: expr2 ? expr1a : expr1b * Toplevel expression: expr2 ? expr1a : expr1b
*
* Produces instructions: * Produces instructions:
* EVAL expr2 Push result of "expr" * EVAL expr2 Push result of "expr2"
* JUMP_IF_FALSE alt jump if false * JUMP_IF_FALSE alt jump if false
* EVAL expr1a * EVAL expr1a
* JUMP_ALWAYS end * JUMP_ALWAYS end
* alt: EVAL expr1b * alt: EVAL expr1b
* end: * 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 static int
compile_expr1(char_u **arg, cctx_T *cctx, ppconst_T *ppconst) 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); p = may_peek_next_line(cctx, *arg, &next);
if (*p == '?') if (*p == '?')
{ {
int op_falsy = p[1] == '?';
garray_T *instr = &cctx->ctx_instr; garray_T *instr = &cctx->ctx_instr;
garray_T *stack = &cctx->ctx_type_stack; garray_T *stack = &cctx->ctx_type_stack;
int alt_idx = instr->ga_len; int alt_idx = instr->ga_len;
int end_idx = 0; int end_idx = 0;
isn_T *isn; isn_T *isn;
type_T *type1 = NULL; type_T *type1 = NULL;
type_T *type2;
int has_const_expr = FALSE; int has_const_expr = FALSE;
int const_value = FALSE; int const_value = FALSE;
int save_skip = cctx->ctx_skip; int save_skip = cctx->ctx_skip;
@@ -4179,9 +4185,10 @@ compile_expr1(char_u **arg, cctx_T *cctx, ppconst_T *ppconst)
p = skipwhite(*arg); 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; return FAIL;
} }
@@ -4191,20 +4198,32 @@ compile_expr1(char_u **arg, cctx_T *cctx, ppconst_T *ppconst)
// expression is to be evaluated. // expression is to be evaluated.
has_const_expr = TRUE; has_const_expr = TRUE;
const_value = tv2bool(&ppconst->pp_tv[ppconst_used]); const_value = tv2bool(&ppconst->pp_tv[ppconst_used]);
clear_tv(&ppconst->pp_tv[ppconst_used]); cctx->ctx_skip = save_skip == SKIP_YES ||
--ppconst->pp_used; (op_falsy ? const_value : !const_value) ? SKIP_YES : SKIP_NOT;
cctx->ctx_skip = save_skip == SKIP_YES || !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 else
{ {
generate_ppconst(cctx, ppconst); 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 // evaluate the second expression; any type is accepted
*arg = skipwhite(p + 1); *arg = skipwhite(p + 1 + op_falsy);
if (may_get_next_line(p + 1, arg, cctx) == FAIL) if (may_get_next_line(p + 1 + op_falsy, arg, cctx) == FAIL)
return FAIL; return FAIL;
if (compile_expr1(arg, cctx, ppconst) == FAIL) if (compile_expr1(arg, cctx, ppconst) == FAIL)
return FAIL; return FAIL;
@@ -4213,56 +4232,64 @@ compile_expr1(char_u **arg, cctx_T *cctx, ppconst_T *ppconst)
{ {
generate_ppconst(cctx, ppconst); generate_ppconst(cctx, ppconst);
// remember the type and drop it if (!op_falsy)
--stack->ga_len; {
type1 = ((type_T **)stack->ga_data)[stack->ga_len]; // remember the type and drop it
--stack->ga_len;
type1 = ((type_T **)stack->ga_data)[stack->ga_len];
end_idx = instr->ga_len; end_idx = instr->ga_len;
generate_JUMP(cctx, JUMP_ALWAYS, 0); generate_JUMP(cctx, JUMP_ALWAYS, 0);
// jump here from JUMP_IF_FALSE // jump here from JUMP_IF_FALSE
isn = ((isn_T *)instr->ga_data) + alt_idx; isn = ((isn_T *)instr->ga_data) + alt_idx;
isn->isn_arg.jump.jump_where = instr->ga_len; isn->isn_arg.jump.jump_where = instr->ga_len;
}
} }
// Check for the ":". if (!op_falsy)
p = may_peek_next_line(cctx, *arg, &next);
if (*p != ':')
{ {
emsg(_(e_missing_colon)); // Check for the ":".
return FAIL; p = may_peek_next_line(cctx, *arg, &next);
} if (*p != ':')
if (next != NULL) {
{ emsg(_(e_missing_colon));
*arg = next_line_from_context(cctx, TRUE); return FAIL;
p = skipwhite(*arg); }
} 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])) if (!IS_WHITE_OR_NUL(**arg) || !IS_WHITE_OR_NUL(p[1]))
{ {
semsg(_(e_white_space_required_before_and_after_str), ":"); semsg(_(e_white_space_required_before_and_after_str), ":");
return FAIL; return FAIL;
} }
// evaluate the third expression // evaluate the third expression
if (has_const_expr) if (has_const_expr)
cctx->ctx_skip = save_skip == SKIP_YES || const_value cctx->ctx_skip = save_skip == SKIP_YES || const_value
? SKIP_YES : SKIP_NOT; ? SKIP_YES : SKIP_NOT;
*arg = skipwhite(p + 1); *arg = skipwhite(p + 1);
if (may_get_next_line(p + 1, arg, cctx) == FAIL) if (may_get_next_line(p + 1, arg, cctx) == FAIL)
return FAIL; return FAIL;
if (compile_expr1(arg, cctx, ppconst) == FAIL) if (compile_expr1(arg, cctx, ppconst) == FAIL)
return FAIL; return FAIL;
}
if (!has_const_expr) if (!has_const_expr)
{ {
type_T **typep;
generate_ppconst(cctx, ppconst); generate_ppconst(cctx, ppconst);
// If the types differ, the result has a more generic type. // If the types differ, the result has a more generic type.
type2 = ((type_T **)stack->ga_data)[stack->ga_len - 1]; typep = ((type_T **)stack->ga_data) + stack->ga_len - 1;
common_type(type1, type2, &type2, cctx->ctx_type_list); 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_T *)instr->ga_data) + end_idx;
isn->isn_arg.jump.jump_where = instr->ga_len; 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 else
*dest = alloc_func_type(common, -1, type_gap); *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; return;
} }
} }