To: vim_dev@googlegroups.com Subject: Patch 7.3.1153 Fcc: outbox From: Bram Moolenaar Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ------------ Patch 7.3.1153 Problem: New regexp engine: Some look-behind matches are very expensive. Solution: Pospone invisible matches further, until a match is almost found. Files: src/regexp_nfa.c *** ../vim-7.3.1152/src/regexp_nfa.c 2013-06-08 23:30:00.000000000 +0200 --- src/regexp_nfa.c 2013-06-09 16:11:41.000000000 +0200 *************** *** 3354,3369 **** typedef struct nfa_pim_S nfa_pim_T; struct nfa_pim_S { ! nfa_state_T *state; ! int result; /* NFA_PIM_TODO, NFA_PIM_[NO]MATCH */ ! nfa_pim_T *pim; /* another PIM at the same position */ regsubs_T subs; /* submatch info, only party used */ }; /* Values for done in nfa_pim_T. */ ! #define NFA_PIM_TODO 0 ! #define NFA_PIM_MATCH 1 ! #define NFA_PIM_NOMATCH -1 /* nfa_thread_T contains execution information of a NFA state */ --- 3354,3374 ---- typedef struct nfa_pim_S nfa_pim_T; struct nfa_pim_S { ! int result; /* NFA_PIM_*, see below */ ! nfa_state_T *state; /* the invisible match start state */ regsubs_T subs; /* submatch info, only party used */ + union + { + lpos_T pos; + char_u *ptr; + } end; /* where the match must end */ }; /* Values for done in nfa_pim_T. */ ! #define NFA_PIM_UNUSED 0 /* pim not used */ ! #define NFA_PIM_TODO 1 /* pim not done yet */ ! #define NFA_PIM_MATCH 2 /* pim executed, matches */ ! #define NFA_PIM_NOMATCH 3 /* pim executed, no match */ /* nfa_thread_T contains execution information of a NFA state */ *************** *** 3371,3377 **** { nfa_state_T *state; int count; ! nfa_pim_T *pim; /* if not NULL: postponed invisible match */ regsubs_T subs; /* submatch info, only party used */ } nfa_thread_T; --- 3376,3383 ---- { nfa_state_T *state; int count; ! nfa_pim_T pim; /* if pim.result != NFA_PIM_UNUSED: postponed ! * invisible match */ regsubs_T subs; /* submatch info, only party used */ } nfa_thread_T; *************** *** 3424,3434 **** --- 3430,3457 ---- e == NULL ? "NULL" : e); } } + + static char * + pim_info(nfa_pim_T *pim) + { + static char buf[30]; + + if (pim == NULL || pim->result == NFA_PIM_UNUSED) + buf[0] = NUL; + else + { + sprintf(buf, " PIM col %d", REG_MULTI ? (int)pim->end.pos.col + : (int)(pim->end.ptr - reginput)); + } + return buf; + } + #endif /* Used during execution: whether a match has been found. */ static int nfa_match; + static void copy_pim __ARGS((nfa_pim_T *to, nfa_pim_T *from)); static void clear_sub __ARGS((regsub_T *sub)); static void copy_sub __ARGS((regsub_T *to, regsub_T *from)); static void copy_sub_off __ARGS((regsub_T *to, regsub_T *from)); *************** *** 3436,3444 **** static int has_state_with_pos __ARGS((nfa_list_T *l, nfa_state_T *state, regsubs_T *subs)); static int match_follows __ARGS((nfa_state_T *startstate, int depth)); static int state_in_list __ARGS((nfa_list_T *l, nfa_state_T *state, regsubs_T *subs)); ! static void addstate __ARGS((nfa_list_T *l, nfa_state_T *state, regsubs_T *subs, int off)); static void addstate_here __ARGS((nfa_list_T *l, nfa_state_T *state, regsubs_T *subs, nfa_pim_T *pim, int *ip)); static void clear_sub(sub) regsub_T *sub; --- 3459,3485 ---- static int has_state_with_pos __ARGS((nfa_list_T *l, nfa_state_T *state, regsubs_T *subs)); static int match_follows __ARGS((nfa_state_T *startstate, int depth)); static int state_in_list __ARGS((nfa_list_T *l, nfa_state_T *state, regsubs_T *subs)); ! static void addstate __ARGS((nfa_list_T *l, nfa_state_T *state, regsubs_T *subs, nfa_pim_T *pim, int off)); static void addstate_here __ARGS((nfa_list_T *l, nfa_state_T *state, regsubs_T *subs, nfa_pim_T *pim, int *ip)); + /* + * Copy postponed invisible match info from "from" to "to". + */ + static void + copy_pim(to, from) + nfa_pim_T *to; + nfa_pim_T *from; + { + to->result = from->result; + to->state = from->state; + copy_sub(&to->subs.norm, &from->subs.norm); + #ifdef FEAT_SYN_HL + if (nfa_has_zsubexpr) + copy_sub(&to->subs.synt, &from->subs.synt); + #endif + to->end = from->end; + } + static void clear_sub(sub) regsub_T *sub; *************** *** 3583,3589 **** #ifdef ENABLE_LOG static void ! report_state(char *action, regsub_T *sub, nfa_state_T *state, int lid) { int col; --- 3624,3634 ---- #ifdef ENABLE_LOG static void ! report_state(char *action, ! regsub_T *sub, ! nfa_state_T *state, ! int lid, ! nfa_pim_T *pim) { int col; *************** *** 3594,3601 **** else col = (int)(sub->list.line[0].start - regline); nfa_set_code(state->c); ! fprintf(log_fd, "> %s state %d to list %d. char %d: %s (start col %d)\n", ! action, abs(state->id), lid, state->c, code, col); } #endif --- 3639,3647 ---- else col = (int)(sub->list.line[0].start - regline); nfa_set_code(state->c); ! fprintf(log_fd, "> %s state %d to list %d. char %d: %s (start col %d)%s\n", ! action, abs(state->id), lid, state->c, code, col, ! pim_info(pim)); } #endif *************** *** 3646,3651 **** --- 3692,3701 ---- switch (state->c) { case NFA_MATCH: + case NFA_MCLOSE: + case NFA_END_INVISIBLE: + case NFA_END_INVISIBLE_NEG: + case NFA_END_PATTERN: return TRUE; case NFA_SPLIT: *************** *** 3727,3736 **** } static void ! addstate(l, state, subs, off) nfa_list_T *l; /* runtime state list */ nfa_state_T *state; /* state to update */ regsubs_T *subs; /* pointers to subexpressions */ int off; /* byte offset, when -1 go to next line */ { int subidx; --- 3777,3787 ---- } static void ! addstate(l, state, subs, pim, off) nfa_list_T *l; /* runtime state list */ nfa_state_T *state; /* state to update */ regsubs_T *subs; /* pointers to subexpressions */ + nfa_pim_T *pim; /* postponed look-behind match */ int off; /* byte offset, when -1 go to next line */ { int subidx; *************** *** 3856,3876 **** state->lastlist[nfa_ll_index] = l->id; thread = &l->t[l->n++]; thread->state = state; ! thread->pim = NULL; copy_sub(&thread->subs.norm, &subs->norm); #ifdef FEAT_SYN_HL if (nfa_has_zsubexpr) copy_sub(&thread->subs.synt, &subs->synt); #endif #ifdef ENABLE_LOG ! report_state("Adding", &thread->subs.norm, state, l->id); did_print = TRUE; #endif } #ifdef ENABLE_LOG if (!did_print) ! report_state("Processing", &subs->norm, state, l->id); #endif switch (state->c) { --- 3907,3930 ---- state->lastlist[nfa_ll_index] = l->id; thread = &l->t[l->n++]; thread->state = state; ! if (pim == NULL) ! thread->pim.result = NFA_PIM_UNUSED; ! else ! copy_pim(&thread->pim, pim); copy_sub(&thread->subs.norm, &subs->norm); #ifdef FEAT_SYN_HL if (nfa_has_zsubexpr) copy_sub(&thread->subs.synt, &subs->synt); #endif #ifdef ENABLE_LOG ! report_state("Adding", &thread->subs.norm, state, l->id, pim); did_print = TRUE; #endif } #ifdef ENABLE_LOG if (!did_print) ! report_state("Processing", &subs->norm, state, l->id, pim); #endif switch (state->c) { *************** *** 3880,3893 **** case NFA_SPLIT: /* order matters here */ ! addstate(l, state->out, subs, off); ! addstate(l, state->out1, subs, off); break; case NFA_SKIP_CHAR: case NFA_NOPEN: case NFA_NCLOSE: ! addstate(l, state->out, subs, off); break; case NFA_MOPEN: --- 3934,3947 ---- case NFA_SPLIT: /* order matters here */ ! addstate(l, state->out, subs, pim, off); ! addstate(l, state->out1, subs, pim, off); break; case NFA_SKIP_CHAR: case NFA_NOPEN: case NFA_NCLOSE: ! addstate(l, state->out, subs, pim, off); break; case NFA_MOPEN: *************** *** 3983,3989 **** sub->list.line[subidx].start = reginput + off; } ! addstate(l, state->out, subs, off); if (save_in_use == -1) { --- 4037,4043 ---- sub->list.line[subidx].start = reginput + off; } ! addstate(l, state->out, subs, pim, off); if (save_in_use == -1) { *************** *** 4001,4007 **** { /* Do not overwrite the position set by \ze. If no \ze * encountered end will be set in nfa_regtry(). */ ! addstate(l, state->out, subs, off); break; } case NFA_MCLOSE1: --- 4055,4061 ---- { /* Do not overwrite the position set by \ze. If no \ze * encountered end will be set in nfa_regtry(). */ ! addstate(l, state->out, subs, pim, off); break; } case NFA_MCLOSE1: *************** *** 4070,4076 **** sub->list.line[subidx].end = reginput + off; } ! addstate(l, state->out, subs, off); if (REG_MULTI) sub->list.multi[subidx].end = save_lpos; --- 4124,4130 ---- sub->list.line[subidx].end = reginput + off; } ! addstate(l, state->out, subs, pim, off); if (REG_MULTI) sub->list.multi[subidx].end = save_lpos; *************** *** 4098,4112 **** int tlen = l->n; int count; int listidx = *ip; - int i; /* first add the state(s) at the end, so that we know how many there are */ ! addstate(l, state, subs, 0); ! ! /* fill in the "pim" field in the new states */ ! if (pim != NULL) ! for (i = tlen; i < l->n; ++i) ! l->t[i].pim = pim; /* when "*ip" was at the end of the list, nothing to do */ if (listidx + 1 == tlen) --- 4152,4160 ---- int tlen = l->n; int count; int listidx = *ip; /* first add the state(s) at the end, so that we know how many there are */ ! addstate(l, state, subs, pim, 0); /* when "*ip" was at the end of the list, nothing to do */ if (listidx + 1 == tlen) *************** *** 4355,4369 **** return val == pos; } ! static int recursive_regmatch __ARGS((nfa_state_T *state, nfa_regprog_T *prog, regsubs_T *submatch, regsubs_T *m, int **listids)); static int nfa_regmatch __ARGS((nfa_regprog_T *prog, nfa_state_T *start, regsubs_T *submatch, regsubs_T *m)); /* * Recursively call nfa_regmatch() */ static int ! recursive_regmatch(state, prog, submatch, m, listids) nfa_state_T *state; nfa_regprog_T *prog; regsubs_T *submatch; regsubs_T *m; --- 4403,4420 ---- return val == pos; } ! static int recursive_regmatch __ARGS((nfa_state_T *state, nfa_pim_T *pim, nfa_regprog_T *prog, regsubs_T *submatch, regsubs_T *m, int **listids)); static int nfa_regmatch __ARGS((nfa_regprog_T *prog, nfa_state_T *start, regsubs_T *submatch, regsubs_T *m)); /* * Recursively call nfa_regmatch() + * "pim" is NULL or contains info about a Postponed Invisible Match (start + * position). */ static int ! recursive_regmatch(state, pim, prog, submatch, m, listids) nfa_state_T *state; + nfa_pim_T *pim; nfa_regprog_T *prog; regsubs_T *submatch; regsubs_T *m; *************** *** 4380,4397 **** int result; int need_restore = FALSE; if (state->c == NFA_START_INVISIBLE_BEFORE || state->c == NFA_START_INVISIBLE_BEFORE_NEG) { ! /* The recursive match must end at the current position. */ endposp = &endpos; if (REG_MULTI) { ! endpos.se_u.pos.col = (int)(reginput - regline); ! endpos.se_u.pos.lnum = reglnum; } else ! endpos.se_u.ptr = reginput; /* Go back the specified number of bytes, or as far as the * start of the previous line, to try matching "\@<=" or --- 4431,4468 ---- int result; int need_restore = FALSE; + if (pim != NULL) + { + /* start at the position where the postponed match was */ + if (REG_MULTI) + reginput = regline + pim->end.pos.col; + else + reginput = pim->end.ptr; + } + if (state->c == NFA_START_INVISIBLE_BEFORE || state->c == NFA_START_INVISIBLE_BEFORE_NEG) { ! /* The recursive match must end at the current position. When "pim" is ! * not NULL it specifies the current position. */ endposp = &endpos; if (REG_MULTI) { ! if (pim == NULL) ! { ! endpos.se_u.pos.col = (int)(reginput - regline); ! endpos.se_u.pos.lnum = reglnum; ! } ! else ! endpos.se_u.pos = pim->end.pos; } else ! { ! if (pim == NULL) ! endpos.se_u.ptr = reginput; ! else ! endpos.se_u.ptr = pim->end.ptr; ! } /* Go back the specified number of bytes, or as far as the * start of the previous line, to try matching "\@<=" or *************** *** 4784,4790 **** int add_here; int add_count; int add_off; - garray_T pimlist; int toplevel = start->c == NFA_MOPEN; #ifdef NFA_REGEXP_DEBUG_LOG FILE *debug = fopen(NFA_REGEXP_DEBUG_LOG, "a"); --- 4855,4860 ---- *************** *** 4796,4802 **** } #endif nfa_match = FALSE; - ga_init2(&pimlist, sizeof(nfa_pim_T), 5); /* Allocate memory for the lists of nodes. */ size = (nstate + 1) * sizeof(nfa_thread_T); --- 4866,4871 ---- *************** *** 4845,4854 **** else m->norm.list.line[0].start = reginput; m->norm.in_use = 1; ! addstate(thislist, start->out, m, 0); } else ! addstate(thislist, start, m, 0); #define ADD_STATE_IF_MATCH(state) \ if (result) { \ --- 4914,4923 ---- else m->norm.list.line[0].start = reginput; m->norm.in_use = 1; ! addstate(thislist, start->out, m, NULL, 0); } else ! addstate(thislist, start, m, NULL, 0); #define ADD_STATE_IF_MATCH(state) \ if (result) { \ *************** *** 4890,4897 **** thislist->id = nfa_listid; nextlist->id = nfa_listid + 1; - pimlist.ga_len = 0; - #ifdef ENABLE_LOG fprintf(log_fd, "------------------------------------------\n"); fprintf(log_fd, ">>> Reginput is \"%s\"\n", reginput); --- 4959,4964 ---- *************** *** 4935,4942 **** else col = (int)(t->subs.norm.list.line[0].start - regline); nfa_set_code(t->state->c); ! fprintf(log_fd, "(%d) char %d %s (start col %d) ... \n", ! abs(t->state->id), (int)t->state->c, code, col); } #endif --- 5002,5010 ---- else col = (int)(t->subs.norm.list.line[0].start - regline); nfa_set_code(t->state->c); ! fprintf(log_fd, "(%d) char %d %s (start col %d)%s ... \n", ! abs(t->state->id), (int)t->state->c, code, col, ! pim_info(&t->pim)); } #endif *************** *** 5028,5048 **** case NFA_START_INVISIBLE_BEFORE: case NFA_START_INVISIBLE_BEFORE_NEG: { - nfa_pim_T *pim; int cout = t->state->out1->out->c; /* Do it directly when what follows is possibly end of * match (closing paren). * Postpone when it is \@<= or \@pim and check multiple - * where it's used? * Otherwise first do the one that has the highest chance * of failing. */ if ((cout >= NFA_MCLOSE && cout <= NFA_MCLOSE9) #ifdef FEAT_SYN_HL || (cout >= NFA_ZCLOSE && cout <= NFA_ZCLOSE9) #endif ! || t->pim != NULL || (t->state->c != NFA_START_INVISIBLE_BEFORE && t->state->c != NFA_START_INVISIBLE_BEFORE_NEG && failure_chance(t->state->out1->out, 0) --- 5096,5114 ---- case NFA_START_INVISIBLE_BEFORE: case NFA_START_INVISIBLE_BEFORE_NEG: { int cout = t->state->out1->out->c; /* Do it directly when what follows is possibly end of * match (closing paren). + * Do it directly if there already is a PIM. * Postpone when it is \@<= or \@= NFA_MCLOSE && cout <= NFA_MCLOSE9) #ifdef FEAT_SYN_HL || (cout >= NFA_ZCLOSE && cout <= NFA_ZCLOSE9) #endif ! || t->pim.result != NFA_PIM_UNUSED || (t->state->c != NFA_START_INVISIBLE_BEFORE && t->state->c != NFA_START_INVISIBLE_BEFORE_NEG && failure_chance(t->state->out1->out, 0) *************** *** 5052,5058 **** * First try matching the invisible match, then what * follows. */ ! result = recursive_regmatch(t->state, prog, submatch, m, &listids); /* for \@! and \@state, NULL, prog, submatch, m, &listids); /* for \@! and \@state = t->state; ! pim->pim = NULL; ! pim->result = NFA_PIM_TODO; /* t->state->out1 is the corresponding END_INVISIBLE * node; Add its out to the current list (zero-width * match). */ addstate_here(thislist, t->state->out1->out, &t->subs, ! pim, &listidx); } } break; --- 5143,5175 ---- } else { + nfa_pim_T pim; + /* ! * First try matching what follows. Only if a match ! * is found verify the invisible match matches. Add a ! * nfa_pim_T to the following states, it contains info ! * about the invisible match. */ ! pim.state = t->state; ! pim.result = NFA_PIM_TODO; ! pim.subs.norm.in_use = 0; ! #ifdef FEAT_SYN_HL ! pim.subs.synt.in_use = 0; ! #endif ! if (REG_MULTI) ! { ! pim.end.pos.col = (int)(reginput - regline); ! pim.end.pos.lnum = reglnum; ! } ! else ! pim.end.ptr = reginput; /* t->state->out1 is the corresponding END_INVISIBLE * node; Add its out to the current list (zero-width * match). */ addstate_here(thislist, t->state->out1->out, &t->subs, ! &pim, &listidx); } } break; *************** *** 5144,5150 **** } /* First try matching the pattern. */ ! result = recursive_regmatch(t->state, prog, submatch, m, &listids); if (result) { --- 5217,5223 ---- } /* First try matching the pattern. */ ! result = recursive_regmatch(t->state, NULL, prog, submatch, m, &listids); if (result) { *************** *** 5798,5809 **** if (add_state != NULL) { ! /* Handle the postponed invisible match before advancing to ! * the next character and for a zero-width match if the match ! * might end without advancing. */ ! if (t->pim != NULL && (!add_here || match_follows(add_state, 0))) { ! if (t->pim->result == NFA_PIM_TODO) { #ifdef ENABLE_LOG fprintf(log_fd, "\n"); --- 5871,5888 ---- if (add_state != NULL) { ! nfa_pim_T *pim; ! ! if (t->pim.result == NFA_PIM_UNUSED) ! pim = NULL; ! else ! pim = &t->pim; ! ! /* Handle the postponed invisible match if the match might end ! * without advancing and before the end of the line. */ ! if (pim != NULL && (clen == 0 || match_follows(add_state, 0))) { ! if (pim->result == NFA_PIM_TODO) { #ifdef ENABLE_LOG fprintf(log_fd, "\n"); *************** *** 5811,5868 **** fprintf(log_fd, "Postponed recursive nfa_regmatch()\n"); fprintf(log_fd, "\n"); #endif ! result = recursive_regmatch(t->pim->state, prog, submatch, m, &listids); ! t->pim->result = result ? NFA_PIM_MATCH ! : NFA_PIM_NOMATCH; /* for \@! and \@pim->state->c ! == NFA_START_INVISIBLE_NEG ! || t->pim->state->c == NFA_START_INVISIBLE_BEFORE_NEG)) { /* Copy submatch info from the recursive call */ ! copy_sub_off(&t->pim->subs.norm, &m->norm); #ifdef FEAT_SYN_HL if (nfa_has_zsubexpr) ! copy_sub_off(&t->pim->subs.synt, &m->synt); #endif } } else { ! result = (t->pim->result == NFA_PIM_MATCH); #ifdef ENABLE_LOG fprintf(log_fd, "\n"); ! fprintf(log_fd, "Using previous recursive nfa_regmatch() result, result == %d\n", t->pim->result); fprintf(log_fd, "MATCH = %s\n", result == TRUE ? "OK" : "FALSE"); fprintf(log_fd, "\n"); #endif } /* for \@! and \@pim->state->c == NFA_START_INVISIBLE_NEG ! || t->pim->state->c == NFA_START_INVISIBLE_BEFORE_NEG)) { /* Copy submatch info from the recursive call */ ! copy_sub_off(&t->subs.norm, &t->pim->subs.norm); #ifdef FEAT_SYN_HL if (nfa_has_zsubexpr) ! copy_sub_off(&t->subs.synt, &t->pim->subs.synt); #endif } else /* look-behind match failed, don't add the state */ continue; } if (add_here) ! addstate_here(thislist, add_state, &t->subs, t->pim, &listidx); else { ! addstate(nextlist, add_state, &t->subs, add_off); if (add_count > 0) nextlist->t[nextlist->n - 1].count = add_count; } --- 5890,5949 ---- fprintf(log_fd, "Postponed recursive nfa_regmatch()\n"); fprintf(log_fd, "\n"); #endif ! result = recursive_regmatch(pim->state, pim, prog, submatch, m, &listids); ! pim->result = result ? NFA_PIM_MATCH : NFA_PIM_NOMATCH; /* for \@! and \@state->c == NFA_START_INVISIBLE_NEG ! || pim->state->c == NFA_START_INVISIBLE_BEFORE_NEG)) { /* Copy submatch info from the recursive call */ ! copy_sub_off(&pim->subs.norm, &m->norm); #ifdef FEAT_SYN_HL if (nfa_has_zsubexpr) ! copy_sub_off(&pim->subs.synt, &m->synt); #endif } } else { ! result = (pim->result == NFA_PIM_MATCH); #ifdef ENABLE_LOG fprintf(log_fd, "\n"); ! fprintf(log_fd, "Using previous recursive nfa_regmatch() result, result == %d\n", pim->result); fprintf(log_fd, "MATCH = %s\n", result == TRUE ? "OK" : "FALSE"); fprintf(log_fd, "\n"); #endif } /* for \@! and \@state->c == NFA_START_INVISIBLE_NEG ! || pim->state->c == NFA_START_INVISIBLE_BEFORE_NEG)) { /* Copy submatch info from the recursive call */ ! copy_sub_off(&t->subs.norm, &pim->subs.norm); #ifdef FEAT_SYN_HL if (nfa_has_zsubexpr) ! copy_sub_off(&t->subs.synt, &pim->subs.synt); #endif } else /* look-behind match failed, don't add the state */ continue; + + /* Postponed invisible match was handled, don't add it to + * following states. */ + pim = NULL; } if (add_here) ! addstate_here(thislist, add_state, &t->subs, pim, &listidx); else { ! addstate(nextlist, add_state, &t->subs, pim, add_off); if (add_count > 0) nextlist->t[nextlist->n - 1].count = add_count; } *************** *** 5941,5951 **** (colnr_T)(reginput - regline) + clen; else m->norm.list.line[0].start = reginput + clen; ! addstate(nextlist, start->out, m, clen); } } else ! addstate(nextlist, start, m, clen); } #ifdef ENABLE_LOG --- 6022,6032 ---- (colnr_T)(reginput - regline) + clen; else m->norm.list.line[0].start = reginput + clen; ! addstate(nextlist, start->out, m, NULL, clen); } } else ! addstate(nextlist, start, m, NULL, clen); } #ifdef ENABLE_LOG *************** *** 5982,5988 **** vim_free(list[0].t); vim_free(list[1].t); vim_free(listids); - ga_clear(&pimlist); #undef ADD_STATE_IF_MATCH #ifdef NFA_REGEXP_DEBUG_LOG fclose(debug); --- 6063,6068 ---- *** ../vim-7.3.1152/src/version.c 2013-06-08 23:30:00.000000000 +0200 --- src/version.c 2013-06-09 15:21:03.000000000 +0200 *************** *** 730,731 **** --- 730,733 ---- { /* Add new patch number below this line */ + /**/ + 1153, /**/ -- "Computers in the future may weigh no more than 1.5 tons." Popular Mechanics, 1949 /// 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 ///