To: vim_dev@googlegroups.com Subject: Patch 8.2.1794 Fcc: outbox From: Bram Moolenaar Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ------------ Patch 8.2.1794 Problem: No falsy Coalescing operator. Solution: Add the "??" operator. Fix mistake with function argument count. Files: runtime/doc/eval.txt, src/eval.c, src/vim9compile.c, src/vim9type.c, src/testdir/test_expr.vim, src/testdir/test_vim9_expr.vim, src/testdir/test_vim9_disassemble.vim *** ../vim-8.2.1793/runtime/doc/eval.txt 2020-09-29 20:59:13.557602899 +0200 --- runtime/doc/eval.txt 2020-10-03 20:09:45.420335175 +0200 *************** *** 873,882 **** All expressions within one level are parsed from left to right. ! expr1 *expr1* *E109* ----- ! expr2 ? expr1 : expr1 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 ':', --- 897,909 ---- All expressions within one level are parsed from left to right. ! expr1 *expr1* *trinary* *falsy-operator* *E109* ----- ! 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 ':', *************** *** 899,904 **** --- 926,948 ---- 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* --------------- *** ../vim-8.2.1793/src/eval.c 2020-09-26 15:08:52.881779910 +0200 --- src/eval.c 2020-10-03 18:11:19.004062663 +0200 *************** *** 2110,2115 **** --- 2110,2116 ---- /* * 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,2140 **** --- 2136,2142 ---- 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,2178 **** { int error = FALSE; ! if (in_vim9script()) result = tv2bool(rettv); else if (tv_get_number_chk(rettv, &error) != 0) result = TRUE; ! clear_tv(rettv); if (error) return FAIL; } --- 2170,2181 ---- { int error = FALSE; ! 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,2185 **** --- 2183,2190 ---- /* * 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,2248 **** 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 = orig_flags; return FAIL; } ! /* ! * Check for the ":". ! */ ! p = eval_next_non_blank(*arg, evalarg_used, &getnext); ! if (*p != ':') { ! emsg(_(e_missing_colon)); ! if (evaluate && result) ! clear_tv(rettv); ! evalarg_used->eval_flags = orig_flags; ! 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); ! 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; ! if (eval1(arg, &var2, evalarg_used) == FAIL) ! { ! if (evaluate && result) ! clear_tv(rettv); ! evalarg_used->eval_flags = orig_flags; ! return FAIL; } - if (evaluate && !result) - *rettv = var2; if (evalarg == NULL) clear_evalarg(&local_evalarg, NULL); --- 2192,2258 ---- return FAIL; } *arg = skipwhite_and_linebreak(*arg + 1, evalarg_used); ! 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 ":". ! */ ! p = eval_next_non_blank(*arg, evalarg_used, &getnext); ! if (*p != ':') ! { ! emsg(_(e_missing_colon)); ! if (evaluate && result) ! clear_tv(rettv); ! evalarg_used->eval_flags = orig_flags; ! 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); 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; ! if (eval1(arg, &var2, evalarg_used) == FAIL) ! { ! if (evaluate && result) ! clear_tv(rettv); ! evalarg_used->eval_flags = orig_flags; ! return FAIL; ! } ! if (evaluate && !result) ! *rettv = var2; } if (evalarg == NULL) clear_evalarg(&local_evalarg, NULL); *** ../vim-8.2.1793/src/vim9compile.c 2020-10-03 13:41:49.959173003 +0200 --- src/vim9compile.c 2020-10-03 19:23:55.292338879 +0200 *************** *** 4132,4145 **** /* * Toplevel expression: expr2 ? expr1a : expr1b - * * Produces instructions: ! * EVAL expr2 Push result of "expr" * JUMP_IF_FALSE alt jump if false * EVAL expr1a * JUMP_ALWAYS end * alt: EVAL expr1b * end: */ static int compile_expr1(char_u **arg, cctx_T *cctx, ppconst_T *ppconst) --- 4132,4151 ---- /* * Toplevel expression: expr2 ? expr1a : expr1b * Produces instructions: ! * 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,4174 **** p = may_peek_next_line(cctx, *arg, &next); if (*p == '?') { 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; --- 4168,4180 ---- 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; int has_const_expr = FALSE; int const_value = FALSE; int save_skip = cctx->ctx_skip; *************** *** 4179,4187 **** p = skipwhite(*arg); } ! if (!IS_WHITE_OR_NUL(**arg) || !IS_WHITE_OR_NUL(p[1])) { ! semsg(_(e_white_space_required_before_and_after_str), "?"); return FAIL; } --- 4185,4194 ---- p = skipwhite(*arg); } ! if (!IS_WHITE_OR_NUL(**arg) || !IS_WHITE_OR_NUL(p[1 + op_falsy])) { ! semsg(_(e_white_space_required_before_and_after_str), ! op_falsy ? "??" : "?"); return FAIL; } *************** *** 4191,4210 **** // expression is to be evaluated. has_const_expr = TRUE; const_value = tv2bool(&ppconst->pp_tv[ppconst_used]); ! 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); } // evaluate the second expression; any type is accepted ! *arg = skipwhite(p + 1); ! if (may_get_next_line(p + 1, arg, cctx) == FAIL) return FAIL; if (compile_expr1(arg, cctx, ppconst) == FAIL) return FAIL; --- 4198,4229 ---- // 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; ! } } else { generate_ppconst(cctx, ppconst); ! 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 + 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,4268 **** { generate_ppconst(cctx, ppconst); ! // remember the type and drop it ! --stack->ga_len; ! type1 = ((type_T **)stack->ga_data)[stack->ga_len]; ! ! end_idx = instr->ga_len; ! generate_JUMP(cctx, JUMP_ALWAYS, 0); ! ! // jump here from JUMP_IF_FALSE ! isn = ((isn_T *)instr->ga_data) + alt_idx; ! isn->isn_arg.jump.jump_where = instr->ga_len; ! } ! ! // Check for the ":". ! p = may_peek_next_line(cctx, *arg, &next); ! if (*p != ':') ! { ! emsg(_(e_missing_colon)); ! return FAIL; ! } ! 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])) ! { ! semsg(_(e_white_space_required_before_and_after_str), ":"); ! return FAIL; ! } ! ! // evaluate the third expression ! if (has_const_expr) ! cctx->ctx_skip = save_skip == SKIP_YES || const_value ? SKIP_YES : SKIP_NOT; ! *arg = skipwhite(p + 1); ! if (may_get_next_line(p + 1, arg, cctx) == FAIL) ! return FAIL; ! if (compile_expr1(arg, cctx, ppconst) == FAIL) ! return FAIL; if (!has_const_expr) { 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); ! // jump here from JUMP_ALWAYS isn = ((isn_T *)instr->ga_data) + end_idx; isn->isn_arg.jump.jump_where = instr->ga_len; } --- 4232,4295 ---- { 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]; ! ! end_idx = instr->ga_len; ! generate_JUMP(cctx, JUMP_ALWAYS, 0); ! ! // jump here from JUMP_IF_FALSE ! 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 != ':') ! { ! emsg(_(e_missing_colon)); ! return FAIL; ! } ! 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])) ! { ! semsg(_(e_white_space_required_before_and_after_str), ":"); ! return FAIL; ! } ! ! // evaluate the third expression ! if (has_const_expr) ! cctx->ctx_skip = save_skip == SKIP_YES || const_value ? SKIP_YES : SKIP_NOT; ! *arg = skipwhite(p + 1); ! if (may_get_next_line(p + 1, arg, cctx) == FAIL) ! 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. ! typep = ((type_T **)stack->ga_data) + stack->ga_len - 1; ! common_type(type1, *typep, typep, cctx->ctx_type_list); ! // 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; } *** ../vim-8.2.1793/src/vim9type.c 2020-09-27 17:44:59.545923881 +0200 --- src/vim9type.c 2020-10-03 19:55:22.426441882 +0200 *************** *** 924,929 **** --- 924,933 ---- } 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; } } *** ../vim-8.2.1793/src/testdir/test_expr.vim 2020-08-06 11:23:30.575073657 +0200 --- src/testdir/test_expr.vim 2020-10-03 20:10:19.472247290 +0200 *************** *** 42,47 **** --- 42,69 ---- 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['']) *** ../vim-8.2.1793/src/testdir/test_vim9_expr.vim 2020-10-03 13:41:49.959173003 +0200 --- src/testdir/test_vim9_expr.vim 2020-10-03 19:57:58.381984495 +0200 *************** *** 12,18 **** enddef " test cond ? expr : expr ! def Test_expr1() assert_equal('one', true ? 'one' : 'two') assert_equal('one', 1 ? 'one' : --- 12,18 ---- enddef " test cond ? expr : expr ! def Test_expr1_trinary() assert_equal('one', true ? 'one' : 'two') assert_equal('one', 1 ? 'one' : *************** *** 61,67 **** assert_equal(123, Z(3)) enddef ! def Test_expr1_vimscript() # check line continuation var lines =<< trim END vim9script --- 61,67 ---- assert_equal(123, Z(3)) enddef ! def Test_expr1_trinary_vimscript() # check line continuation var lines =<< trim END vim9script *************** *** 139,145 **** CheckScriptSuccess(lines) enddef ! func Test_expr1_fails() call CheckDefFailure(["var x = 1 ? 'one'"], "Missing ':' after '?'", 1) let msg = "White space required before and after '?'" --- 139,145 ---- CheckScriptSuccess(lines) enddef ! func Test_expr1_trinary_fails() call CheckDefFailure(["var x = 1 ? 'one'"], "Missing ':' after '?'", 1) let msg = "White space required before and after '?'" *************** *** 160,165 **** --- 160,193 ---- \ '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) *** ../vim-8.2.1793/src/testdir/test_vim9_disassemble.vim 2020-09-27 22:47:01.880163387 +0200 --- src/testdir/test_vim9_disassemble.vim 2020-10-03 19:08:40.338745010 +0200 *************** *** 1326,1331 **** --- 1326,1358 ---- 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('\\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], *** ../vim-8.2.1793/src/version.c 2020-10-03 17:04:34.114272292 +0200 --- src/version.c 2020-10-03 18:34:51.506873594 +0200 *************** *** 752,753 **** --- 752,755 ---- { /* Add new patch number below this line */ + /**/ + 1794, /**/ -- I'm not familiar with this proof, but I'm aware of a significant following of toddlers who believe that peanut butter is the solution to all of life's problems... -- Tim Hammerquist /// Bram Moolenaar -- Bram@Moolenaar.net -- http://www.Moolenaar.net \\\ /// sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ \\\ \\\ an exciting new programming language -- http://www.Zimbu.org /// \\\ help me help AIDS victims -- http://ICCF-Holland.org ///