Skip to content

Commit 92f26c2

Browse files
committed
patch 8.2.1794: no falsy Coalescing operator
Problem: No falsy Coalescing operator. Solution: Add the "??" operator. Fix mistake with function argument count.
1 parent c8fe645 commit 92f26c2

File tree

8 files changed

+257
-97
lines changed

8 files changed

+257
-97
lines changed

runtime/doc/eval.txt

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,27 @@ non-zero number it means TRUE: >
133133
:" executed
134134
To test for a non-empty string, use empty(): >
135135
:if !empty("foo")
136-
<
136+
137+
< *falsy* *truthy*
138+
An expression can be used as a condition, ignoring the type and only using
139+
whether the value is "sort of true" or "sort of false". Falsy is:
140+
the number zero
141+
empty string, blob, list or dictionary
142+
Other values are truthy. Examples:
143+
0 falsy
144+
1 truthy
145+
-1 truthy
146+
0.0 falsy
147+
0.1 truthy
148+
'' falsy
149+
'x' truthy
150+
[] falsy
151+
[0] truthy
152+
{} falsy
153+
#{x: 1} truthy
154+
0z falsy
155+
0z00 truthy
156+
137157
*non-zero-arg*
138158
Function arguments often behave slightly different from |TRUE|: If the
139159
argument is present and it evaluates to a non-zero Number, |v:true| or a
@@ -877,10 +897,13 @@ Example: >
877897
All expressions within one level are parsed from left to right.
878898

879899

880-
expr1 *expr1* *trinary* *E109*
900+
expr1 *expr1* *trinary* *falsy-operator* *E109*
881901
-----
882902

883-
expr2 ? expr1 : expr1
903+
The trinary operator: expr2 ? expr1 : expr1
904+
The falsy operator: expr2 ?? expr1
905+
906+
Trinary operator ~
884907

885908
The expression before the '?' is evaluated to a number. If it evaluates to
886909
|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: >
903926
You should always put a space before the ':', otherwise it can be mistaken for
904927
use in a variable such as "a:1".
905928

929+
Falsy operator ~
930+
931+
This is also known as the "null coalescing operator", but that's too
932+
complicated, thus we just call it the falsy operator.
933+
934+
The expression before the '??' is evaluated. If it evaluates to
935+
|truthy|, this is used as the result. Otherwise the expression after the '??'
936+
is evaluated and used as the result. This is most useful to have a default
937+
value for an expression that may result in zero or empty: >
938+
echo theList ?? 'list is empty'
939+
echo GetName() ?? 'unknown'
940+
941+
These are similar, but not equal: >
942+
expr2 ?? expr1
943+
expr2 ? expr2 : expr1
944+
In the second line "expr2" is evaluated twice.
945+
906946

907947
expr2 and expr3 *expr2* *expr3*
908948
---------------

src/eval.c

Lines changed: 54 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2110,6 +2110,7 @@ eval0(
21102110
/*
21112111
* Handle top level expression:
21122112
* expr2 ? expr1 : expr1
2113+
* expr2 ?? expr1
21132114
*
21142115
* "arg" must point to the first non-white of the expression.
21152116
* "arg" is advanced to just after the recognized expression.
@@ -2135,6 +2136,7 @@ eval1(char_u **arg, typval_T *rettv, evalarg_T *evalarg)
21352136
p = eval_next_non_blank(*arg, evalarg, &getnext);
21362137
if (*p == '?')
21372138
{
2139+
int op_falsy = p[1] == '?';
21382140
int result;
21392141
typval_T var2;
21402142
evalarg_T *evalarg_used = evalarg;
@@ -2168,81 +2170,89 @@ eval1(char_u **arg, typval_T *rettv, evalarg_T *evalarg)
21682170
{
21692171
int error = FALSE;
21702172

2171-
if (in_vim9script())
2173+
if (in_vim9script() || op_falsy)
21722174
result = tv2bool(rettv);
21732175
else if (tv_get_number_chk(rettv, &error) != 0)
21742176
result = TRUE;
2175-
clear_tv(rettv);
2177+
if (error || !op_falsy || !result)
2178+
clear_tv(rettv);
21762179
if (error)
21772180
return FAIL;
21782181
}
21792182

21802183
/*
21812184
* Get the second variable. Recursive!
21822185
*/
2186+
if (op_falsy)
2187+
++*arg;
21832188
if (evaluate && in_vim9script() && !IS_WHITE_OR_NUL((*arg)[1]))
21842189
{
21852190
error_white_both(p, 1);
21862191
clear_tv(rettv);
21872192
return FAIL;
21882193
}
21892194
*arg = skipwhite_and_linebreak(*arg + 1, evalarg_used);
2190-
evalarg_used->eval_flags = result ? orig_flags
2191-
: orig_flags & ~EVAL_EVALUATE;
2192-
if (eval1(arg, rettv, evalarg_used) == FAIL)
2195+
evalarg_used->eval_flags = (op_falsy ? !result : result)
2196+
? orig_flags : orig_flags & ~EVAL_EVALUATE;
2197+
if (eval1(arg, &var2, evalarg_used) == FAIL)
21932198
{
21942199
evalarg_used->eval_flags = orig_flags;
21952200
return FAIL;
21962201
}
2202+
if (!op_falsy || !result)
2203+
*rettv = var2;
21972204

2198-
/*
2199-
* Check for the ":".
2200-
*/
2201-
p = eval_next_non_blank(*arg, evalarg_used, &getnext);
2202-
if (*p != ':')
2203-
{
2204-
emsg(_(e_missing_colon));
2205-
if (evaluate && result)
2206-
clear_tv(rettv);
2207-
evalarg_used->eval_flags = orig_flags;
2208-
return FAIL;
2209-
}
2210-
if (getnext)
2211-
*arg = eval_next_line(evalarg_used);
2212-
else
2205+
if (!op_falsy)
22132206
{
2214-
if (evaluate && in_vim9script() && !VIM_ISWHITE(p[-1]))
2207+
/*
2208+
* Check for the ":".
2209+
*/
2210+
p = eval_next_non_blank(*arg, evalarg_used, &getnext);
2211+
if (*p != ':')
2212+
{
2213+
emsg(_(e_missing_colon));
2214+
if (evaluate && result)
2215+
clear_tv(rettv);
2216+
evalarg_used->eval_flags = orig_flags;
2217+
return FAIL;
2218+
}
2219+
if (getnext)
2220+
*arg = eval_next_line(evalarg_used);
2221+
else
2222+
{
2223+
if (evaluate && in_vim9script() && !VIM_ISWHITE(p[-1]))
2224+
{
2225+
error_white_both(p, 1);
2226+
clear_tv(rettv);
2227+
evalarg_used->eval_flags = orig_flags;
2228+
return FAIL;
2229+
}
2230+
*arg = p;
2231+
}
2232+
2233+
/*
2234+
* Get the third variable. Recursive!
2235+
*/
2236+
if (evaluate && in_vim9script() && !IS_WHITE_OR_NUL((*arg)[1]))
22152237
{
22162238
error_white_both(p, 1);
22172239
clear_tv(rettv);
22182240
evalarg_used->eval_flags = orig_flags;
22192241
return FAIL;
22202242
}
2221-
*arg = p;
2222-
}
2223-
2224-
/*
2225-
* Get the third variable. Recursive!
2226-
*/
2227-
if (evaluate && in_vim9script() && !IS_WHITE_OR_NUL((*arg)[1]))
2228-
{
2229-
error_white_both(p, 1);
2230-
clear_tv(rettv);
2231-
evalarg_used->eval_flags = orig_flags;
2232-
return FAIL;
2233-
}
2234-
*arg = skipwhite_and_linebreak(*arg + 1, evalarg_used);
2235-
evalarg_used->eval_flags = !result ? orig_flags
2243+
*arg = skipwhite_and_linebreak(*arg + 1, evalarg_used);
2244+
evalarg_used->eval_flags = !result ? orig_flags
22362245
: orig_flags & ~EVAL_EVALUATE;
2237-
if (eval1(arg, &var2, evalarg_used) == FAIL)
2238-
{
2239-
if (evaluate && result)
2240-
clear_tv(rettv);
2241-
evalarg_used->eval_flags = orig_flags;
2242-
return FAIL;
2246+
if (eval1(arg, &var2, evalarg_used) == FAIL)
2247+
{
2248+
if (evaluate && result)
2249+
clear_tv(rettv);
2250+
evalarg_used->eval_flags = orig_flags;
2251+
return FAIL;
2252+
}
2253+
if (evaluate && !result)
2254+
*rettv = var2;
22432255
}
2244-
if (evaluate && !result)
2245-
*rettv = var2;
22462256

22472257
if (evalarg == NULL)
22482258
clear_evalarg(&local_evalarg, NULL);

src/testdir/test_expr.vim

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,28 @@ func Test_version()
4242
call assert_false(has('patch-9.9.1'))
4343
endfunc
4444

45+
func Test_op_falsy()
46+
call assert_equal(v:true, v:true ?? 456)
47+
call assert_equal(123, 123 ?? 456)
48+
call assert_equal('yes', 'yes' ?? 456)
49+
call assert_equal(0z00, 0z00 ?? 456)
50+
call assert_equal([1], [1] ?? 456)
51+
call assert_equal(#{one: 1}, #{one: 1} ?? 456)
52+
if has('float')
53+
call assert_equal(0.1, 0.1 ?? 456)
54+
endif
55+
56+
call assert_equal(456, v:false ?? 456)
57+
call assert_equal(456, 0 ?? 456)
58+
call assert_equal(456, '' ?? 456)
59+
call assert_equal(456, 0z ?? 456)
60+
call assert_equal(456, [] ?? 456)
61+
call assert_equal(456, {} ?? 456)
62+
if has('float')
63+
call assert_equal(456, 0.0 ?? 456)
64+
endif
65+
endfunc
66+
4567
func Test_dict()
4668
let d = {'': 'empty', 'a': 'a', 0: 'zero'}
4769
call assert_equal('empty', d[''])

src/testdir/test_vim9_disassemble.vim

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,6 +1326,33 @@ def Test_disassemble_compare()
13261326
delete('Xdisassemble')
13271327
enddef
13281328

1329+
def s:FalsyOp()
1330+
echo g:flag ?? "yes"
1331+
echo [] ?? "empty list"
1332+
echo "" ?? "empty string"
1333+
enddef
1334+
1335+
def Test_dsassemble_falsy_op()
1336+
var res = execute('disass s:FalsyOp')
1337+
assert_match('\<SNR>\d*_FalsyOp\_s*' ..
1338+
'echo g:flag ?? "yes"\_s*' ..
1339+
'0 LOADG g:flag\_s*' ..
1340+
'1 JUMP_AND_KEEP_IF_TRUE -> 3\_s*' ..
1341+
'2 PUSHS "yes"\_s*' ..
1342+
'3 ECHO 1\_s*' ..
1343+
'echo \[\] ?? "empty list"\_s*' ..
1344+
'4 NEWLIST size 0\_s*' ..
1345+
'5 JUMP_AND_KEEP_IF_TRUE -> 7\_s*' ..
1346+
'6 PUSHS "empty list"\_s*' ..
1347+
'7 ECHO 1\_s*' ..
1348+
'echo "" ?? "empty string"\_s*' ..
1349+
'\d\+ PUSHS "empty string"\_s*' ..
1350+
'\d\+ ECHO 1\_s*' ..
1351+
'\d\+ PUSHNR 0\_s*' ..
1352+
'\d\+ RETURN',
1353+
res)
1354+
enddef
1355+
13291356
def Test_disassemble_compare_const()
13301357
var cases = [
13311358
['"xx" == "yy"', false],

src/testdir/test_vim9_expr.vim

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def FuncTwo(arg: number): number
1212
enddef
1313

1414
" test cond ? expr : expr
15-
def Test_expr1()
15+
def Test_expr1_trinary()
1616
assert_equal('one', true ? 'one' : 'two')
1717
assert_equal('one', 1 ?
1818
'one' :
@@ -61,7 +61,7 @@ def Test_expr1()
6161
assert_equal(123, Z(3))
6262
enddef
6363

64-
def Test_expr1_vimscript()
64+
def Test_expr1_trinary_vimscript()
6565
# check line continuation
6666
var lines =<< trim END
6767
vim9script
@@ -139,7 +139,7 @@ def Test_expr1_vimscript()
139139
CheckScriptSuccess(lines)
140140
enddef
141141

142-
func Test_expr1_fails()
142+
func Test_expr1_trinary_fails()
143143
call CheckDefFailure(["var x = 1 ? 'one'"], "Missing ':' after '?'", 1)
144144

145145
let msg = "White space required before and after '?'"
@@ -160,6 +160,34 @@ func Test_expr1_fails()
160160
\ 'Z()'], 'E119:', 4)
161161
endfunc
162162

163+
def Test_expr1_falsy()
164+
var lines =<< trim END
165+
assert_equal(v:true, v:true ?? 456)
166+
assert_equal(123, 123 ?? 456)
167+
assert_equal('yes', 'yes' ?? 456)
168+
assert_equal([1], [1] ?? 456)
169+
assert_equal(#{one: 1}, #{one: 1} ?? 456)
170+
if has('float')
171+
assert_equal(0.1, 0.1 ?? 456)
172+
endif
173+
174+
assert_equal(456, v:false ?? 456)
175+
assert_equal(456, 0 ?? 456)
176+
assert_equal(456, '' ?? 456)
177+
assert_equal(456, [] ?? 456)
178+
assert_equal(456, {} ?? 456)
179+
if has('float')
180+
assert_equal(456, 0.0 ?? 456)
181+
endif
182+
END
183+
CheckDefAndScriptSuccess(lines)
184+
185+
var msg = "White space required before and after '??'"
186+
call CheckDefFailure(["var x = 1?? 'one' : 'two'"], msg, 1)
187+
call CheckDefFailure(["var x = 1 ??'one' : 'two'"], msg, 1)
188+
call CheckDefFailure(["var x = 1??'one' : 'two'"], msg, 1)
189+
enddef
190+
163191
" TODO: define inside test function
164192
def Record(val: any): any
165193
g:vals->add(val)

src/version.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,8 @@ static char *(features[]) =
750750

751751
static int included_patches[] =
752752
{ /* Add new patch number below this line */
753+
/**/
754+
1794,
753755
/**/
754756
1793,
755757
/**/

0 commit comments

Comments
 (0)