To: vim_dev@googlegroups.com Subject: Patch 8.2.0719 Fcc: outbox From: Bram Moolenaar Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ------------ Patch 8.2.0719 Problem: Vim9: more expressions can be evaluated at compile time Solution: Recognize has('name'). Files: src/vim9compile.c, src/testdir/test_vim9_disassemble.vim, src/testdir/test_vim9_expr.vim *** ../vim-8.2.0718/src/vim9compile.c 2020-05-08 19:10:30.782336716 +0200 --- src/vim9compile.c 2020-05-09 15:34:32.888630958 +0200 *************** *** 136,144 **** static char e_var_notfound[] = N_("E1001: variable not found: %s"); static char e_syntax_at[] = N_("E1002: Syntax error at %s"); ! static int compile_expr1(char_u **arg, cctx_T *cctx); ! static int compile_expr2(char_u **arg, cctx_T *cctx); ! static int compile_expr3(char_u **arg, cctx_T *cctx); static void delete_def_function_contents(dfunc_T *dfunc); static void arg_type_mismatch(type_T *expected, type_T *actual, int argidx); static int check_type(type_T *expected, type_T *actual, int give_msg); --- 136,142 ---- static char e_var_notfound[] = N_("E1001: variable not found: %s"); static char e_syntax_at[] = N_("E1002: Syntax error at %s"); ! static int compile_expr0(char_u **arg, cctx_T *cctx); static void delete_def_function_contents(dfunc_T *dfunc); static void arg_type_mismatch(type_T *expected, type_T *actual, int argidx); static int check_type(type_T *expected, type_T *actual, int give_msg); *************** *** 744,767 **** } /* ! * Generate an ISN_COMPARE* instruction with a boolean result. */ ! static int ! generate_COMPARE(cctx_T *cctx, exptype_T exptype, int ic) { isntype_T isntype = ISN_DROP; - isn_T *isn; - garray_T *stack = &cctx->ctx_type_stack; - vartype_T type1; - vartype_T type2; - - RETURN_OK_IF_SKIP(cctx); - // Get the known type of the two items on the stack. If they are matching - // use a type-specific instruction. Otherwise fall back to runtime type - // checking. - type1 = ((type_T **)stack->ga_data)[stack->ga_len - 2]->tt_type; - type2 = ((type_T **)stack->ga_data)[stack->ga_len - 1]->tt_type; if (type1 == VAR_UNKNOWN) type1 = VAR_ANY; if (type2 == VAR_UNKNOWN) --- 742,755 ---- } /* ! * Get the instruction to use for comparing "type1" with "type2" ! * Return ISN_DROP when failed. */ ! static isntype_T ! get_compare_isn(exptype_T exptype, vartype_T type1, vartype_T type2) { isntype_T isntype = ISN_DROP; if (type1 == VAR_UNKNOWN) type1 = VAR_ANY; if (type2 == VAR_UNKNOWN) *************** *** 796,802 **** { semsg(_("E1037: Cannot use \"%s\" with %s"), exptype == EXPR_IS ? "is" : "isnot" , vartype_name(type1)); ! return FAIL; } if (isntype == ISN_DROP || ((exptype != EXPR_EQUAL && exptype != EXPR_NEQUAL --- 784,790 ---- { semsg(_("E1037: Cannot use \"%s\" with %s"), exptype == EXPR_IS ? "is" : "isnot" , vartype_name(type1)); ! return ISN_DROP; } if (isntype == ISN_DROP || ((exptype != EXPR_EQUAL && exptype != EXPR_NEQUAL *************** *** 809,816 **** { semsg(_("E1072: Cannot compare %s with %s"), vartype_name(type1), vartype_name(type2)); ! return FAIL; } if ((isn = generate_instr(cctx, isntype)) == NULL) return FAIL; --- 797,829 ---- { semsg(_("E1072: Cannot compare %s with %s"), vartype_name(type1), vartype_name(type2)); ! return ISN_DROP; } + return isntype; + } + + /* + * Generate an ISN_COMPARE* instruction with a boolean result. + */ + static int + generate_COMPARE(cctx_T *cctx, exptype_T exptype, int ic) + { + isntype_T isntype; + isn_T *isn; + garray_T *stack = &cctx->ctx_type_stack; + vartype_T type1; + vartype_T type2; + + RETURN_OK_IF_SKIP(cctx); + + // Get the known type of the two items on the stack. If they are matching + // use a type-specific instruction. Otherwise fall back to runtime type + // checking. + type1 = ((type_T **)stack->ga_data)[stack->ga_len - 2]->tt_type; + type2 = ((type_T **)stack->ga_data)[stack->ga_len - 1]->tt_type; + isntype = get_compare_isn(exptype, type1, type2); + if (isntype == ISN_DROP) + return FAIL; if ((isn = generate_instr(cctx, isntype)) == NULL) return FAIL; *************** *** 2340,2345 **** --- 2353,2443 ---- return OK; } + // Structure passed between the compile_expr* functions to keep track of + // constants that have been parsed but for which no code was produced yet. If + // possible expressions on these constants are applied at compile time. If + // that is not possible, the code to push the constants needs to be generated + // before other instructions. + typedef struct { + typval_T pp_tv[10]; // stack of ppconst constants + int pp_used; // active entries in pp_tv[] + } ppconst_T; + + /* + * Generate a PUSH instruction for "tv". + * "tv" will be consumed or cleared. + * Nothing happens if "tv" is NULL or of type VAR_UNKNOWN; + */ + static int + generate_tv_PUSH(cctx_T *cctx, typval_T *tv) + { + if (tv != NULL) + { + switch (tv->v_type) + { + case VAR_UNKNOWN: + break; + case VAR_BOOL: + generate_PUSHBOOL(cctx, tv->vval.v_number); + break; + case VAR_SPECIAL: + generate_PUSHSPEC(cctx, tv->vval.v_number); + break; + case VAR_NUMBER: + generate_PUSHNR(cctx, tv->vval.v_number); + break; + #ifdef FEAT_FLOAT + case VAR_FLOAT: + generate_PUSHF(cctx, tv->vval.v_float); + break; + #endif + case VAR_BLOB: + generate_PUSHBLOB(cctx, tv->vval.v_blob); + tv->vval.v_blob = NULL; + break; + case VAR_STRING: + generate_PUSHS(cctx, tv->vval.v_string); + tv->vval.v_string = NULL; + break; + default: + iemsg("constant type not supported"); + clear_tv(tv); + return FAIL; + } + tv->v_type = VAR_UNKNOWN; + } + return OK; + } + + /* + * Generate code for any ppconst entries. + */ + static int + generate_ppconst(cctx_T *cctx, ppconst_T *ppconst) + { + int i; + int ret = OK; + + for (i = 0; i < ppconst->pp_used; ++i) + if (generate_tv_PUSH(cctx, &ppconst->pp_tv[i]) == FAIL) + ret = FAIL; + ppconst->pp_used = 0; + return ret; + } + + /* + * Clear ppconst constants. Used when failing. + */ + static void + clear_ppconst(ppconst_T *ppconst) + { + int i; + + for (i = 0; i < ppconst->pp_used; ++i) + clear_tv(&ppconst->pp_tv[i]); + ppconst->pp_used = 0; + } + /* * Generate an instruction to load script-local variable "name", without the * leading "s:". *************** *** 2526,2550 **** } else { ! if ((len == 4 && STRNCMP("true", *arg, 4) == 0) ! || (len == 5 && STRNCMP("false", *arg, 5) == 0)) ! res = generate_PUSHBOOL(cctx, **arg == 't' ! ? VVAL_TRUE : VVAL_FALSE); ! else ! { ! // "var" can be script-local even without using "s:" if it ! // already exists. ! if (SCRIPT_ITEM(current_sctx.sc_sid)->sn_version ! == SCRIPT_VERSION_VIM9 ! || lookup_script(*arg, len) == OK) ! res = compile_load_scriptvar(cctx, name, *arg, &end, ! FALSE); ! ! // When the name starts with an uppercase letter or "x:" it ! // can be a user defined function. ! if (res == FAIL && (ASCII_ISUPPER(*name) || name[1] == ':')) ! res = generate_funcref(cctx, name); ! } } } if (gen_load) --- 2624,2641 ---- } else { ! // "var" can be script-local even without using "s:" if it ! // already exists. ! if (SCRIPT_ITEM(current_sctx.sc_sid)->sn_version ! == SCRIPT_VERSION_VIM9 ! || lookup_script(*arg, len) == OK) ! res = compile_load_scriptvar(cctx, name, *arg, &end, ! FALSE); ! ! // When the name starts with an uppercase letter or "x:" it ! // can be a user defined function. ! if (res == FAIL && (ASCII_ISUPPER(*name) || name[1] == ':')) ! res = generate_funcref(cctx, name); } } if (gen_load) *************** *** 2588,2594 **** return OK; } ! if (compile_expr1(&p, cctx) == FAIL) return FAIL; ++*argcount; --- 2679,2685 ---- return OK; } ! if (compile_expr0(&p, cctx) == FAIL) return FAIL; ++*argcount; *************** *** 2621,2627 **** * BCALL / DCALL / UCALL */ static int ! compile_call(char_u **arg, size_t varlen, cctx_T *cctx, int argcount_init) { char_u *name = *arg; char_u *p; --- 2712,2723 ---- * BCALL / DCALL / UCALL */ static int ! compile_call( ! char_u **arg, ! size_t varlen, ! cctx_T *cctx, ! ppconst_T *ppconst, ! int argcount_init) { char_u *name = *arg; char_u *p; *************** *** 2633,2638 **** --- 2729,2764 ---- ufunc_T *ufunc; int res = FAIL; + // we can evaluate "has('name')" at compile time + if (varlen == 3 && STRNCMP(*arg, "has", 3) == 0) + { + char_u *s = skipwhite(*arg + varlen + 1); + typval_T argvars[2]; + + argvars[0].v_type = VAR_UNKNOWN; + if (*s == '"') + (void)get_string_tv(&s, &argvars[0], TRUE); + else if (*s == '\'') + (void)get_lit_string_tv(&s, &argvars[0], TRUE); + s = skipwhite(s); + if (*s == ')' && argvars[0].v_type == VAR_STRING) + { + typval_T *tv = &ppconst->pp_tv[ppconst->pp_used]; + + *arg = s + 1; + argvars[1].v_type = VAR_UNKNOWN; + tv->v_type = VAR_NUMBER; + tv->vval.v_number = 0; + f_has(argvars, tv); + clear_tv(&argvars[0]); + ++ppconst->pp_used; + return OK; + } + } + + if (generate_ppconst(cctx, ppconst) == FAIL) + return FAIL; + if (varlen >= sizeof(namebuf)) { semsg(_("E1011: name too long: %s"), name); *************** *** 2791,2797 **** p += STRLEN(p); break; } ! if (compile_expr1(&p, cctx) == FAIL) break; ++count; if (*p == ',') --- 2917,2923 ---- p += STRLEN(p); break; } ! if (compile_expr0(&p, cctx) == FAIL) break; ++count; if (*p == ',') *************** *** 2929,2935 **** { isn_T *isn; ! if (compile_expr1(arg, cctx) == FAIL) return FAIL; // TODO: check type is string isn = ((isn_T *)instr->ga_data) + instr->ga_len - 1; --- 3055,3061 ---- { isn_T *isn; ! if (compile_expr0(arg, cctx) == FAIL) return FAIL; // TODO: check type is string isn = ((isn_T *)instr->ga_data) + instr->ga_len - 1; *************** *** 2974,2980 **** *arg = skipwhite(*arg); } ! if (compile_expr1(arg, cctx) == FAIL) return FAIL; ++count; --- 3100,3106 ---- *arg = skipwhite(*arg); } ! if (compile_expr0(arg, cctx) == FAIL) return FAIL; ++count; *************** *** 3626,3715 **** return OK; } ! // Structure passed between the compile_expr* functions to keep track of ! // constants that have been parsed but for which no code was produced yet. If ! // possible expressions on these constants are applied at compile time. If ! // that is not possible, the code to push the constants needs to be generated ! // before other instructions. ! typedef struct { ! typval_T pp_tv[10]; // stack of ppconst constants ! int pp_used; // active entries in pp_tv[] ! } ppconst_T; ! ! /* ! * Generate a PUSH instruction for "tv". ! * "tv" will be consumed or cleared. ! * Nothing happens if "tv" is NULL or of type VAR_UNKNOWN; ! */ ! static int ! generate_tv_PUSH(cctx_T *cctx, typval_T *tv) ! { ! if (tv != NULL) ! { ! switch (tv->v_type) ! { ! case VAR_UNKNOWN: ! break; ! case VAR_BOOL: ! generate_PUSHBOOL(cctx, tv->vval.v_number); ! break; ! case VAR_SPECIAL: ! generate_PUSHSPEC(cctx, tv->vval.v_number); ! break; ! case VAR_NUMBER: ! generate_PUSHNR(cctx, tv->vval.v_number); ! break; ! #ifdef FEAT_FLOAT ! case VAR_FLOAT: ! generate_PUSHF(cctx, tv->vval.v_float); ! break; ! #endif ! case VAR_BLOB: ! generate_PUSHBLOB(cctx, tv->vval.v_blob); ! tv->vval.v_blob = NULL; ! break; ! case VAR_STRING: ! generate_PUSHS(cctx, tv->vval.v_string); ! tv->vval.v_string = NULL; ! break; ! default: ! iemsg("constant type not supported"); ! clear_tv(tv); ! return FAIL; ! } ! tv->v_type = VAR_UNKNOWN; ! } ! return OK; ! } ! ! /* ! * Generate code for any ppconst entries. ! */ ! static int ! generate_ppconst(cctx_T *cctx, ppconst_T *ppconst) ! { ! int i; ! int ret = OK; ! ! for (i = 0; i < ppconst->pp_used; ++i) ! if (generate_tv_PUSH(cctx, &ppconst->pp_tv[i]) == FAIL) ! ret = FAIL; ! ppconst->pp_used = 0; ! return ret; ! } ! ! /* ! * Clear ppconst constants. Used when failing. ! */ ! static void ! clear_ppconst(ppconst_T *ppconst) ! { ! int i; ! ! for (i = 0; i < ppconst->pp_used; ++i) ! clear_tv(&ppconst->pp_tv[i]); ! ppconst->pp_used = 0; ! } /* * Compile code to apply '-', '+' and '!'. --- 3752,3758 ---- return OK; } ! static int compile_expr3(char_u **arg, cctx_T *cctx, ppconst_T *ppconst); /* * Compile code to apply '-', '+' and '!'. *************** *** 3825,3831 **** return FAIL; } // TODO: base value may not be the first argument ! if (compile_call(arg, p - *arg, cctx, 1) == FAIL) return FAIL; } } --- 3868,3874 ---- return FAIL; } // TODO: base value may not be the first argument ! if (compile_call(arg, p - *arg, cctx, ppconst, 1) == FAIL) return FAIL; } } *************** *** 3841,3847 **** // TODO: more arguments // TODO: dict member dict['name'] *arg = skipwhite(*arg + 1); ! if (compile_expr1(arg, cctx) == FAIL) return FAIL; if (**arg != ']') --- 3884,3890 ---- // TODO: more arguments // TODO: dict member dict['name'] *arg = skipwhite(*arg + 1); ! if (compile_expr0(arg, cctx) == FAIL) return FAIL; if (**arg != ']') *************** *** 3991,3996 **** --- 4034,4067 ---- break; /* + * "true" constant + */ + case 't': if (STRNCMP(*arg, "true", 4) == 0 + && !eval_isnamec((*arg)[4])) + { + *arg += 4; + rettv->v_type = VAR_BOOL; + rettv->vval.v_number = VVAL_TRUE; + } + else + ret = NOTDONE; + break; + + /* + * "false" constant + */ + case 'f': if (STRNCMP(*arg, "false", 5) == 0 + && !eval_isnamec((*arg)[5])) + { + *arg += 5; + rettv->v_type = VAR_BOOL; + rettv->vval.v_number = VVAL_FALSE; + } + else + ret = NOTDONE; + break; + + /* * List: [expr, expr] */ case '[': ret = compile_list(arg, cctx); *************** *** 4047,4053 **** * nested expression: (expression). */ case '(': *arg = skipwhite(*arg + 1); ! ret = compile_expr1(arg, cctx); // recursive! *arg = skipwhite(*arg); if (**arg == ')') ++*arg; --- 4118,4124 ---- * nested expression: (expression). */ case '(': *arg = skipwhite(*arg + 1); ! ret = compile_expr0(arg, cctx); // recursive! *arg = skipwhite(*arg); if (**arg == ')') ++*arg; *************** *** 4074,4091 **** } start_leader = end_leader; // don't apply again below ! // A constant expression can possibly be handled compile time, return ! // the value instead of generating code. ! ++ppconst->pp_used; } else if (ret == NOTDONE) { char_u *p; int r; - if (generate_ppconst(cctx, ppconst) == FAIL) - return FAIL; - if (!eval_isnamec1(**arg)) { semsg(_("E1015: Name expected: %s"), *arg); --- 4145,4162 ---- } start_leader = end_leader; // don't apply again below ! if (cctx->ctx_skip == TRUE) ! clear_tv(rettv); ! else ! // A constant expression can possibly be handled compile time, ! // return the value instead of generating code. ! ++ppconst->pp_used; } else if (ret == NOTDONE) { char_u *p; int r; if (!eval_isnamec1(**arg)) { semsg(_("E1015: Name expected: %s"), *arg); *************** *** 4095,4103 **** // "name" or "name()" p = to_name_end(*arg, TRUE); if (*p == '(') ! r = compile_call(arg, p - *arg, cctx, 0); else r = compile_load(arg, p, cctx, TRUE); if (r == FAIL) return FAIL; } --- 4166,4180 ---- // "name" or "name()" p = to_name_end(*arg, TRUE); if (*p == '(') ! { ! r = compile_call(arg, p - *arg, cctx, ppconst, 0); ! } else + { + if (generate_ppconst(cctx, ppconst) == FAIL) + return FAIL; r = compile_load(arg, p, cctx, TRUE); + } if (r == FAIL) return FAIL; } *************** *** 4105,4112 **** // Handle following "[]", ".member", etc. // Then deal with prefixed '-', '+' and '!', if not done already. if (compile_subscript(arg, cctx, &start_leader, end_leader, ! ppconst) == FAIL ! || compile_leader(cctx, start_leader, end_leader) == FAIL) return FAIL; return OK; } --- 4182,4198 ---- // Handle following "[]", ".member", etc. // Then deal with prefixed '-', '+' and '!', if not done already. if (compile_subscript(arg, cctx, &start_leader, end_leader, ! ppconst) == FAIL) ! return FAIL; ! if (ppconst->pp_used > 0) ! { ! // apply the '!', '-' and '+' before the constant ! rettv = &ppconst->pp_tv[ppconst->pp_used - 1]; ! if (apply_leader(rettv, start_leader, end_leader) == FAIL) ! return FAIL; ! return OK; ! } ! if (compile_leader(cctx, start_leader, end_leader) == FAIL) return FAIL; return OK; } *************** *** 4117,4126 **** * % number modulo */ static int ! compile_expr6( ! char_u **arg, ! cctx_T *cctx, ! ppconst_T *ppconst) { char_u *op; int ppconst_used = ppconst->pp_used; --- 4203,4209 ---- * % number modulo */ static int ! compile_expr6(char_u **arg, cctx_T *cctx, ppconst_T *ppconst) { char_u *op; int ppconst_used = ppconst->pp_used; *************** *** 4191,4211 **** * .. string concatenation */ static int ! compile_expr5(char_u **arg, cctx_T *cctx) { char_u *op; int oplen; ! ppconst_T ppconst; ! int ppconst_used = 0; ! ! CLEAR_FIELD(ppconst); // get the first variable ! if (compile_expr6(arg, cctx, &ppconst) == FAIL) ! { ! clear_ppconst(&ppconst); return FAIL; - } /* * Repeat computing, until no "+", "-" or ".." is following. --- 4274,4288 ---- * .. string concatenation */ static int ! compile_expr5(char_u **arg, cctx_T *cctx, ppconst_T *ppconst) { char_u *op; int oplen; ! int ppconst_used = ppconst->pp_used; // get the first variable ! if (compile_expr6(arg, cctx, ppconst) == FAIL) return FAIL; /* * Repeat computing, until no "+", "-" or ".." is following. *************** *** 4221,4227 **** { char_u buf[3]; - clear_ppconst(&ppconst); vim_strncpy(buf, op, oplen); semsg(_(e_white_both), buf); return FAIL; --- 4298,4303 ---- *************** *** 4229,4255 **** *arg = skipwhite(op + oplen); if (may_get_next_line(op + oplen, arg, cctx) == FAIL) - { - clear_ppconst(&ppconst); return FAIL; - } // get the second expression ! if (compile_expr6(arg, cctx, &ppconst) == FAIL) ! { ! clear_ppconst(&ppconst); return FAIL; - } ! if (ppconst.pp_used == ppconst_used + 2 && (*op == '.' ! ? (ppconst.pp_tv[ppconst_used].v_type == VAR_STRING ! && ppconst.pp_tv[ppconst_used + 1].v_type == VAR_STRING) ! : (ppconst.pp_tv[ppconst_used].v_type == VAR_NUMBER ! && ppconst.pp_tv[ppconst_used + 1].v_type == VAR_NUMBER))) { ! typval_T *tv1 = &ppconst.pp_tv[ppconst_used]; ! typval_T *tv2 = &ppconst.pp_tv[ppconst_used + 1]; // concat/subtract/add constant numbers if (*op == '+') --- 4305,4325 ---- *arg = skipwhite(op + oplen); if (may_get_next_line(op + oplen, arg, cctx) == FAIL) return FAIL; // get the second expression ! if (compile_expr6(arg, cctx, ppconst) == FAIL) return FAIL; ! if (ppconst->pp_used == ppconst_used + 2 && (*op == '.' ! ? (ppconst->pp_tv[ppconst_used].v_type == VAR_STRING ! && ppconst->pp_tv[ppconst_used + 1].v_type == VAR_STRING) ! : (ppconst->pp_tv[ppconst_used].v_type == VAR_NUMBER ! && ppconst->pp_tv[ppconst_used + 1].v_type == VAR_NUMBER))) { ! typval_T *tv1 = &ppconst->pp_tv[ppconst_used]; ! typval_T *tv2 = &ppconst->pp_tv[ppconst_used + 1]; // concat/subtract/add constant numbers if (*op == '+') *************** *** 4266,4272 **** tv1->vval.v_string = alloc((int)(len1 + STRLEN(s2) + 1)); if (tv1->vval.v_string == NULL) { ! clear_ppconst(&ppconst); return FAIL; } mch_memmove(tv1->vval.v_string, s1, len1); --- 4336,4342 ---- tv1->vval.v_string = alloc((int)(len1 + STRLEN(s2) + 1)); if (tv1->vval.v_string == NULL) { ! clear_ppconst(ppconst); return FAIL; } mch_memmove(tv1->vval.v_string, s1, len1); *************** *** 4274,4292 **** vim_free(s1); vim_free(s2); } ! --ppconst.pp_used; } else { ! generate_ppconst(cctx, &ppconst); if (*op == '.') { if (may_generate_2STRING(-2, cctx) == FAIL || may_generate_2STRING(-1, cctx) == FAIL) - { - clear_ppconst(&ppconst); return FAIL; - } generate_instr_drop(cctx, ISN_CONCAT, 1); } else --- 4344,4359 ---- vim_free(s1); vim_free(s2); } ! --ppconst->pp_used; } else { ! generate_ppconst(cctx, ppconst); if (*op == '.') { if (may_generate_2STRING(-2, cctx) == FAIL || may_generate_2STRING(-1, cctx) == FAIL) return FAIL; generate_instr_drop(cctx, ISN_CONCAT, 1); } else *************** *** 4294,4302 **** } } - // TODO: move to caller - generate_ppconst(cctx, &ppconst); - return OK; } --- 4361,4366 ---- *************** *** 4318,4332 **** * COMPARE one of the compare instructions */ static int ! compile_expr4(char_u **arg, cctx_T *cctx) { exptype_T type = EXPR_UNKNOWN; char_u *p; int len = 2; int type_is = FALSE; // get the first variable ! if (compile_expr5(arg, cctx) == FAIL) return FAIL; p = skipwhite(*arg); --- 4382,4397 ---- * COMPARE one of the compare instructions */ static int ! compile_expr4(char_u **arg, cctx_T *cctx, ppconst_T *ppconst) { exptype_T type = EXPR_UNKNOWN; char_u *p; int len = 2; int type_is = FALSE; + int ppconst_used = ppconst->pp_used; // get the first variable ! if (compile_expr5(arg, cctx, ppconst) == FAIL) return FAIL; p = skipwhite(*arg); *************** *** 4369,4378 **** if (may_get_next_line(p + len, arg, cctx) == FAIL) return FAIL; ! if (compile_expr5(arg, cctx) == FAIL) return FAIL; ! generate_COMPARE(cctx, type, ic); } return OK; --- 4434,4467 ---- if (may_get_next_line(p + len, arg, cctx) == FAIL) return FAIL; ! if (compile_expr5(arg, cctx, ppconst) == FAIL) return FAIL; ! if (ppconst->pp_used == ppconst_used + 2) ! { ! typval_T * tv1 = &ppconst->pp_tv[ppconst->pp_used - 2]; ! typval_T *tv2 = &ppconst->pp_tv[ppconst->pp_used - 1]; ! int ret; ! ! // Both sides are a constant, compute the result now. ! // First check for a valid combination of types, this is more ! // strict than typval_compare(). ! if (get_compare_isn(type, tv1->v_type, tv2->v_type) == ISN_DROP) ! ret = FAIL; ! else ! { ! ret = typval_compare(tv1, tv2, type, ic); ! tv1->v_type = VAR_BOOL; ! tv1->vval.v_number = tv1->vval.v_number ! ? VVAL_TRUE : VVAL_FALSE; ! clear_tv(tv2); ! --ppconst->pp_used; ! } ! return ret; ! } ! ! generate_ppconst(cctx, ppconst); ! return generate_COMPARE(cctx, type, ic); } return OK; *************** *** 4382,4388 **** * Compile || or &&. */ static int ! compile_and_or(char_u **arg, cctx_T *cctx, char *op) { char_u *p = skipwhite(*arg); int opchar = *op; --- 4471,4482 ---- * Compile || or &&. */ static int ! compile_and_or( ! char_u **arg, ! cctx_T *cctx, ! char *op, ! ppconst_T *ppconst, ! int ppconst_used UNUSED) { char_u *p = skipwhite(*arg); int opchar = *op; *************** *** 4404,4409 **** --- 4498,4506 ---- return FAIL; } + // TODO: use ppconst if the value is a constant + generate_ppconst(cctx, ppconst); + if (ga_grow(&end_ga, 1) == FAIL) { ga_clear(&end_ga); *************** *** 4419,4432 **** if (may_get_next_line(p + 2, arg, cctx) == FAIL) return FAIL; ! if ((opchar == '|' ? compile_expr3(arg, cctx) ! : compile_expr4(arg, cctx)) == FAIL) { ga_clear(&end_ga); return FAIL; } p = skipwhite(*arg); } // Fill in the end label in all jumps. while (end_ga.ga_len > 0) --- 4516,4530 ---- if (may_get_next_line(p + 2, arg, cctx) == FAIL) return FAIL; ! if ((opchar == '|' ? compile_expr3(arg, cctx, ppconst) ! : compile_expr4(arg, cctx, ppconst)) == FAIL) { ga_clear(&end_ga); return FAIL; } p = skipwhite(*arg); } + generate_ppconst(cctx, ppconst); // Fill in the end label in all jumps. while (end_ga.ga_len > 0) *************** *** 4456,4469 **** * end: */ static int ! compile_expr3(char_u **arg, cctx_T *cctx) { // get the first variable ! if (compile_expr4(arg, cctx) == FAIL) return FAIL; // || and && work almost the same ! return compile_and_or(arg, cctx, "&&"); } /* --- 4554,4569 ---- * end: */ static int ! compile_expr3(char_u **arg, cctx_T *cctx, ppconst_T *ppconst) { + int ppconst_used = ppconst->pp_used; + // get the first variable ! if (compile_expr4(arg, cctx, ppconst) == FAIL) return FAIL; // || and && work almost the same ! return compile_and_or(arg, cctx, "&&", ppconst, ppconst_used); } /* *************** *** 4478,4491 **** * end: */ static int ! compile_expr2(char_u **arg, cctx_T *cctx) { // eval the first expression ! if (compile_expr3(arg, cctx) == FAIL) return FAIL; // || and && work almost the same ! return compile_and_or(arg, cctx, "||"); } /* --- 4578,4593 ---- * end: */ static int ! compile_expr2(char_u **arg, cctx_T *cctx, ppconst_T *ppconst) { + int ppconst_used = ppconst->pp_used; + // eval the first expression ! if (compile_expr3(arg, cctx, ppconst) == FAIL) return FAIL; // || and && work almost the same ! return compile_and_or(arg, cctx, "||", ppconst, ppconst_used); } /* *************** *** 4500,4511 **** * end: */ static int ! compile_expr1(char_u **arg, cctx_T *cctx) { char_u *p; // Evaluate the first expression. ! if (compile_expr2(arg, cctx) == FAIL) return FAIL; p = skipwhite(*arg); --- 4602,4614 ---- * end: */ static int ! compile_expr1(char_u **arg, cctx_T *cctx, ppconst_T *ppconst) { char_u *p; + int ppconst_used = ppconst->pp_used; // Evaluate the first expression. ! if (compile_expr2(arg, cctx, ppconst) == FAIL) return FAIL; p = skipwhite(*arg); *************** *** 4518,4523 **** --- 4621,4629 ---- isn_T *isn; type_T *type1; type_T *type2; + int has_const_expr = FALSE; + int const_value = FALSE; + int save_skip = cctx->ctx_skip; if (!IS_WHITE_OR_NUL(**arg) || !IS_WHITE_OR_NUL(p[1])) { *************** *** 4525,4550 **** return FAIL; } ! 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) == FAIL) return FAIL; ! // 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 = skipwhite(*arg); --- 4631,4674 ---- return FAIL; } ! if (ppconst->pp_used == ppconst_used + 1) ! { ! // the condition is a constant, we know whether the ? or the : ! // 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 == TRUE || !const_value; ! } ! 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; ! if (!has_const_expr) ! { ! 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 = skipwhite(*arg); *************** *** 4560,4584 **** } // evaluate the third expression *arg = skipwhite(p + 1); if (may_get_next_line(p + 1, arg, cctx) == FAIL) return FAIL; ! ! if (compile_expr1(arg, cctx) == FAIL) return FAIL; ! // 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; } return OK; } /* * compile "return [expr]" */ static char_u * --- 4684,4735 ---- } // evaluate the third expression + if (has_const_expr) + cctx->ctx_skip = save_skip == TRUE || const_value; *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; ! } ! ! cctx->ctx_skip = save_skip; } return OK; } /* + * Toplevel expression. + */ + static int + compile_expr0(char_u **arg, cctx_T *cctx) + { + ppconst_T ppconst; + + CLEAR_FIELD(ppconst); + if (compile_expr1(arg, cctx, &ppconst) == FAIL) + { + clear_ppconst(&ppconst); + return FAIL; + } + if (generate_ppconst(cctx, &ppconst) == FAIL) + return FAIL; + return OK; + } + + /* * compile "return [expr]" */ static char_u * *************** *** 4591,4597 **** if (*p != NUL && *p != '|' && *p != '\n') { // compile return argument into instructions ! if (compile_expr1(&p, cctx) == FAIL) return NULL; stack_type = ((type_T **)stack->ga_data)[stack->ga_len - 1]; --- 4742,4748 ---- if (*p != NUL && *p != '|' && *p != '\n') { // compile return argument into instructions ! if (compile_expr0(&p, cctx) == FAIL) return NULL; stack_type = ((type_T **)stack->ga_data)[stack->ga_len - 1]; *************** *** 5075,5081 **** --cctx->ctx_locals.ga_len; instr_count = instr->ga_len; p = skipwhite(p + oplen); ! r = compile_expr1(&p, cctx); if (new_local) ++cctx->ctx_locals.ga_len; if (r == FAIL) --- 5226,5232 ---- --cctx->ctx_locals.ga_len; instr_count = instr->ga_len; p = skipwhite(p + oplen); ! r = compile_expr0(&p, cctx); if (new_local) ++cctx->ctx_locals.ga_len; if (r == FAIL) *************** *** 5526,5545 **** { char_u *p = arg; garray_T *instr = &cctx->ctx_instr; scope_T *scope; ! typval_T tv; ! // compile "expr"; if we know it evaluates to FALSE skip the block ! tv.v_type = VAR_UNKNOWN; ! if (evaluate_const_expr1(&p, cctx, &tv) == OK) ! cctx->ctx_skip = tv2bool(&tv) ? FALSE : TRUE; else - cctx->ctx_skip = MAYBE; - clear_tv(&tv); - if (cctx->ctx_skip == MAYBE) { ! p = arg; ! if (compile_expr1(&p, cctx) == FAIL) return NULL; } --- 5677,5704 ---- { char_u *p = arg; garray_T *instr = &cctx->ctx_instr; + int instr_count = instr->ga_len; scope_T *scope; ! ppconst_T ppconst; ! CLEAR_FIELD(ppconst); ! if (compile_expr1(&p, cctx, &ppconst) == FAIL) ! { ! clear_ppconst(&ppconst); ! return NULL; ! } ! if (instr->ga_len == instr_count && ppconst.pp_used == 1) ! { ! // The expression results in a constant. ! // TODO: how about nesting? ! cctx->ctx_skip = tv2bool(&ppconst.pp_tv[0]) ? FALSE : TRUE; ! clear_ppconst(&ppconst); ! } else { ! // Not a constant, generate instructions for the expression. ! cctx->ctx_skip = MAYBE; ! if (generate_ppconst(cctx, &ppconst) == FAIL) return NULL; } *************** *** 5595,5601 **** if (cctx->ctx_skip == MAYBE) { p = arg; ! if (compile_expr1(&p, cctx) == FAIL) return NULL; // "where" is set when ":elseif", "else" or ":endif" is found --- 5754,5760 ---- if (cctx->ctx_skip == MAYBE) { p = arg; ! if (compile_expr0(&p, cctx) == FAIL) return NULL; // "where" is set when ":elseif", "else" or ":endif" is found *************** *** 5754,5760 **** // compile "expr", it remains on the stack until "endfor" arg = p; ! if (compile_expr1(&arg, cctx) == FAIL) { drop_scope(cctx); return NULL; --- 5913,5919 ---- // compile "expr", it remains on the stack until "endfor" arg = p; ! if (compile_expr0(&arg, cctx) == FAIL) { drop_scope(cctx); return NULL; *************** *** 5844,5850 **** scope->se_u.se_while.ws_top_label = instr->ga_len; // compile "expr" ! if (compile_expr1(&p, cctx) == FAIL) return NULL; // "while_end" is set when ":endwhile" is found --- 6003,6009 ---- scope->se_u.se_while.ws_top_label = instr->ga_len; // compile "expr" ! if (compile_expr0(&p, cctx) == FAIL) return NULL; // "while_end" is set when ":endwhile" is found *************** *** 6219,6225 **** { char_u *p = skipwhite(arg); ! if (compile_expr1(&p, cctx) == FAIL) return NULL; if (may_generate_2STRING(-1, cctx) == FAIL) return NULL; --- 6378,6384 ---- { char_u *p = skipwhite(arg); ! if (compile_expr0(&p, cctx) == FAIL) return NULL; if (may_generate_2STRING(-1, cctx) == FAIL) return NULL; *************** *** 6243,6249 **** for (;;) { ! if (compile_expr1(&p, cctx) == FAIL) return NULL; ++count; p = skipwhite(p); --- 6402,6408 ---- for (;;) { ! if (compile_expr0(&p, cctx) == FAIL) return NULL; ++count; p = skipwhite(p); *************** *** 6305,6311 **** ++count; } p += 2; ! if (compile_expr1(&p, cctx) == FAIL) return NULL; may_generate_2STRING(-1, cctx); ++count; --- 6464,6470 ---- ++count; } p += 2; ! if (compile_expr0(&p, cctx) == FAIL) return NULL; may_generate_2STRING(-1, cctx); ++count; *************** *** 6423,6429 **** ufunc->uf_def_arg_idx[i] = instr->ga_len; arg = ((char_u **)(ufunc->uf_def_args.ga_data))[i]; ! if (compile_expr1(&arg, &cctx) == FAIL) goto erret; // If no type specified use the type of the default value. --- 6582,6588 ---- ufunc->uf_def_arg_idx[i] = instr->ga_len; arg = ((char_u **)(ufunc->uf_def_args.ga_data))[i]; ! if (compile_expr0(&arg, &cctx) == FAIL) goto erret; // If no type specified use the type of the default value. *************** *** 6609,6615 **** if (ea.cmdidx == CMD_eval) { p = ea.cmd; ! if (compile_expr1(&p, &cctx) == FAIL) goto erret; // drop the return value --- 6768,6774 ---- if (ea.cmdidx == CMD_eval) { p = ea.cmd; ! if (compile_expr0(&p, &cctx) == FAIL) goto erret; // drop the return value *** ../vim-8.2.0718/src/testdir/test_vim9_disassemble.vim 2020-05-08 19:10:30.782336716 +0200 --- src/testdir/test_vim9_disassemble.vim 2020-05-09 15:21:35.170848668 +0200 *************** *** 814,859 **** enddef def Test_disassemble_compare() - " TODO: COMPAREFUNC let cases = [ ! ['true == false', 'COMPAREBOOL =='], ! ['true != false', 'COMPAREBOOL !='], ! ['v:none == v:null', 'COMPARESPECIAL =='], ! ['v:none != v:null', 'COMPARESPECIAL !='], ! ! ['111 == 222', 'COMPARENR =='], ! ['111 != 222', 'COMPARENR !='], ! ['111 > 222', 'COMPARENR >'], ! ['111 < 222', 'COMPARENR <'], ! ['111 >= 222', 'COMPARENR >='], ! ['111 <= 222', 'COMPARENR <='], ! ['111 =~ 222', 'COMPARENR =\~'], ! ['111 !~ 222', 'COMPARENR !\~'], ! ! ['"xx" != "yy"', 'COMPARESTRING !='], ! ['"xx" > "yy"', 'COMPARESTRING >'], ! ['"xx" < "yy"', 'COMPARESTRING <'], ! ['"xx" >= "yy"', 'COMPARESTRING >='], ! ['"xx" <= "yy"', 'COMPARESTRING <='], ! ['"xx" =~ "yy"', 'COMPARESTRING =\~'], ! ['"xx" !~ "yy"', 'COMPARESTRING !\~'], ! ['"xx" is "yy"', 'COMPARESTRING is'], ! ['"xx" isnot "yy"', 'COMPARESTRING isnot'], ! ! ['0z11 == 0z22', 'COMPAREBLOB =='], ! ['0z11 != 0z22', 'COMPAREBLOB !='], ! ['0z11 is 0z22', 'COMPAREBLOB is'], ! ['0z11 isnot 0z22', 'COMPAREBLOB isnot'], ! ! ['[1,2] == [3,4]', 'COMPARELIST =='], ! ['[1,2] != [3,4]', 'COMPARELIST !='], ! ['[1,2] is [3,4]', 'COMPARELIST is'], ! ['[1,2] isnot [3,4]', 'COMPARELIST isnot'], ! ! ['#{a:1} == #{x:2}', 'COMPAREDICT =='], ! ['#{a:1} != #{x:2}', 'COMPAREDICT !='], ! ['#{a:1} is #{x:2}', 'COMPAREDICT is'], ! ['#{a:1} isnot #{x:2}', 'COMPAREDICT isnot'], ['{->33} == {->44}', 'COMPAREFUNC =='], ['{->33} != {->44}', 'COMPAREFUNC !='], --- 814,858 ---- enddef def Test_disassemble_compare() let cases = [ ! ['true == isFalse', 'COMPAREBOOL =='], ! ['true != isFalse', 'COMPAREBOOL !='], ! ['v:none == isNull', 'COMPARESPECIAL =='], ! ['v:none != isNull', 'COMPARESPECIAL !='], ! ! ['111 == aNumber', 'COMPARENR =='], ! ['111 != aNumber', 'COMPARENR !='], ! ['111 > aNumber', 'COMPARENR >'], ! ['111 < aNumber', 'COMPARENR <'], ! ['111 >= aNumber', 'COMPARENR >='], ! ['111 <= aNumber', 'COMPARENR <='], ! ['111 =~ aNumber', 'COMPARENR =\~'], ! ['111 !~ aNumber', 'COMPARENR !\~'], ! ! ['"xx" != aString', 'COMPARESTRING !='], ! ['"xx" > aString', 'COMPARESTRING >'], ! ['"xx" < aString', 'COMPARESTRING <'], ! ['"xx" >= aString', 'COMPARESTRING >='], ! ['"xx" <= aString', 'COMPARESTRING <='], ! ['"xx" =~ aString', 'COMPARESTRING =\~'], ! ['"xx" !~ aString', 'COMPARESTRING !\~'], ! ['"xx" is aString', 'COMPARESTRING is'], ! ['"xx" isnot aString', 'COMPARESTRING isnot'], ! ! ['0z11 == aBlob', 'COMPAREBLOB =='], ! ['0z11 != aBlob', 'COMPAREBLOB !='], ! ['0z11 is aBlob', 'COMPAREBLOB is'], ! ['0z11 isnot aBlob', 'COMPAREBLOB isnot'], ! ! ['[1, 2] == aList', 'COMPARELIST =='], ! ['[1, 2] != aList', 'COMPARELIST !='], ! ['[1, 2] is aList', 'COMPARELIST is'], ! ['[1, 2] isnot aList', 'COMPARELIST isnot'], ! ! ['#{a: 1} == aDict', 'COMPAREDICT =='], ! ['#{a: 1} != aDict', 'COMPAREDICT !='], ! ['#{a: 1} is aDict', 'COMPAREDICT is'], ! ['#{a: 1} isnot aDict', 'COMPAREDICT isnot'], ['{->33} == {->44}', 'COMPAREFUNC =='], ['{->33} != {->44}', 'COMPAREFUNC !='], *************** *** 871,892 **** ['77 is g:xx', 'COMPAREANY is'], ['77 isnot g:xx', 'COMPAREANY isnot'], ] if has('float') cases->extend([ ! ['1.1 == 2.2', 'COMPAREFLOAT =='], ! ['1.1 != 2.2', 'COMPAREFLOAT !='], ! ['1.1 > 2.2', 'COMPAREFLOAT >'], ! ['1.1 < 2.2', 'COMPAREFLOAT <'], ! ['1.1 >= 2.2', 'COMPAREFLOAT >='], ! ['1.1 <= 2.2', 'COMPAREFLOAT <='], ! ['1.1 =~ 2.2', 'COMPAREFLOAT =\~'], ! ['1.1 !~ 2.2', 'COMPAREFLOAT !\~'], ]) endif let nr = 1 for case in cases writefile(['def TestCase' .. nr .. '()', ' if ' .. case[0], ' echo 42' ' endif', --- 870,902 ---- ['77 is g:xx', 'COMPAREANY is'], ['77 isnot g:xx', 'COMPAREANY isnot'], ] + let floatDecl = '' if has('float') cases->extend([ ! ['1.1 == aFloat', 'COMPAREFLOAT =='], ! ['1.1 != aFloat', 'COMPAREFLOAT !='], ! ['1.1 > aFloat', 'COMPAREFLOAT >'], ! ['1.1 < aFloat', 'COMPAREFLOAT <'], ! ['1.1 >= aFloat', 'COMPAREFLOAT >='], ! ['1.1 <= aFloat', 'COMPAREFLOAT <='], ! ['1.1 =~ aFloat', 'COMPAREFLOAT =\~'], ! ['1.1 !~ aFloat', 'COMPAREFLOAT !\~'], ]) + floatDecl = 'let aFloat = 2.2' endif let nr = 1 for case in cases + " declare local variables to get a non-constant with the right type writefile(['def TestCase' .. nr .. '()', + ' let isFalse = false', + ' let isNull = v:null', + ' let aNumber = 222', + ' let aString = "yy"', + ' let aBlob = 0z22', + ' let aList = [3, 4]', + ' let aDict = #{x: 2}', + floatDecl, ' if ' .. case[0], ' echo 42' ' endif', *************** *** 896,902 **** assert_match('TestCase' .. nr .. '.*' .. 'if ' .. substitute(case[0], '[[~]', '\\\0', 'g') .. '.*' .. '\d \(PUSH\|FUNCREF\).*' .. ! '\d \(PUSH\|FUNCREF\|LOADG\).*' .. '\d ' .. case[1] .. '.*' .. '\d JUMP_IF_FALSE -> \d\+.*', instr) --- 906,912 ---- assert_match('TestCase' .. nr .. '.*' .. 'if ' .. substitute(case[0], '[[~]', '\\\0', 'g') .. '.*' .. '\d \(PUSH\|FUNCREF\).*' .. ! '\d \(PUSH\|FUNCREF\|LOAD\).*' .. '\d ' .. case[1] .. '.*' .. '\d JUMP_IF_FALSE -> \d\+.*', instr) *** ../vim-8.2.0718/src/testdir/test_vim9_expr.vim 2020-05-07 22:18:58.260836097 +0200 --- src/testdir/test_vim9_expr.vim 2020-05-09 15:36:59.004186370 +0200 *************** *** 429,435 **** call CheckDefFailure(["let x = 1 == '2'"], 'Cannot compare number with string') call CheckDefFailure(["let x = '1' == 2"], 'Cannot compare string with number') ! call CheckDefFailure(["let x = 1 == RetVoid()"], 'Cannot use void value') call CheckDefFailure(["let x = RetVoid() == 1"], 'Cannot compare void with number') call CheckDefFailure(["let x = true > false"], 'Cannot compare bool with bool') --- 429,435 ---- call CheckDefFailure(["let x = 1 == '2'"], 'Cannot compare number with string') call CheckDefFailure(["let x = '1' == 2"], 'Cannot compare string with number') ! call CheckDefFailure(["let x = 1 == RetVoid()"], 'Cannot compare number with void') call CheckDefFailure(["let x = RetVoid() == 1"], 'Cannot compare void with number') call CheckDefFailure(["let x = true > false"], 'Cannot compare bool with bool') *** ../vim-8.2.0718/src/version.c 2020-05-09 13:06:20.224712254 +0200 --- src/version.c 2020-05-09 15:07:36.069628668 +0200 *************** *** 748,749 **** --- 748,751 ---- { /* Add new patch number below this line */ + /**/ + 719, /**/ -- Q: How many legs does a giraffe have? A: Eight: two in front, two behind, two on the left and two on the right /// 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 ///