Commit: patch 9.2.0971: "=~" and the match functions compile their pattern on every call

Christian Brabandt <[email protected]>
Newsgroups gmane.editors.vim.devel
Message-ID <[email protected]>
patch 9.2.0971: "=~" and the match functions compile their pattern on every call

Commit: https://github.com/vim/vim/commit/be1616190eb6ed00aaef4ddfb6c835019c1453bc
Author: Samuel Schlesinger <[email protected]>
Date:   Tue Aug 18 19:41:13 2026 +0000

    patch 9.2.0971: "=~" and the match functions compile their pattern on every call
    
    Problem:  "=~", match(), matchstr(), matchlist(), matchbufline(),
              matchstrlist() and split() compile their pattern on every
              call.  A script matching a list of items against one
              pattern compiles it once per item, which can dwarf the
              cost of the match itself.
    Solution: Keep the compiled program of the last pattern in a cache
              (Samuel Schlesinger).
    
    The cache owns the program only between uses: eval_regcomp() hands
    it to the caller and empties the cache, and eval_regfree() adopts
    the program the caller ends up with, so the automatic engine
    falling back to the backtracking engine, which frees the original
    program, needs no special handling and vim_regfree() is unchanged.
    The key covers the pattern, 'encoding' and 'regexpengine', sampled
    when compiling; when user code run from a nested evaluation changes
    an option in the key, or re-enters the compile/free pair, the
    program is freed instead of cached.  Patterns whose compilation
    depends on state outside the key are not cached: "~", bracket
    classes, the cursor-relative atoms \%.l, \%.c and \%.v, and a
    "\%#=" engine prefix.  'ignorecase' applies at execution time as
    before.
    
    Benchmarks (min of 3, macOS arm64, uncached to cached): filter() of
    100000 items with a plugin-sized pattern 0.211s to 0.048s; a
    generated 200-branch alternation over 10000 items 0.455s to 0.037s,
    and with 'regexpengine' 1 forcing the backtracking engine 0.851s to
    0.021s; a short inline pattern over 100000 items 0.109s to 0.061s.
    
    closes: #20941
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Samuel Schlesinger <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/src/alloc.c b/src/alloc.c
index d6eb6f29c..69d484018 100644
--- a/src/alloc.c
+++ b/src/alloc.c
@@ -447,6 +447,7 @@ free_all_mem(void)
     free_signs();
 # endif
 # ifdef FEAT_EVAL
+    free_eval_regcomp_cache();
     set_expr_line(NULL, NULL);
 # endif
 # ifdef FEAT_DIFF
diff --git a/src/eval.c b/src/eval.c
index d5373c099..759c4acb4 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -3188,6 +3188,178 @@ set_context_for_expression(
     xp->xp_pattern = arg;
 }
 
+/*
+ * Cache with the compiled program of the last pattern used by
+ * pattern_match() and the match functions.  Script loops often evaluate the
+ * same pattern many times; reusing the program avoids compiling it for
+ * every evaluation.  The cache owns the program only between uses:
+ * eval_regcomp() hands it to the caller and empties the cache, and
+ * eval_regfree() adopts the program the caller ends up with.  Thus when
+ * executing replaced the program (the automatic engine falling back to
+ * backtracking frees the original) no freed program is left behind in the
+ * cache.
+ * Adopting the caller's program also means that after a fallback the
+ * backtracking program stays cached until the pattern or an option in the
+ * key changes; the automatic engine is not tried again for it.
+ * The cache must only be used where the pattern is compiled with
+ * RE_MAGIC + RE_STRING and 'cpoptions' made empty, as every caller here
+ * does; both functions check these conditions and fall back to plain
+ * compilation and freeing when they do not hold.
+ * eval_prog_pat and eval_prog_enc are non-NULL whenever eval_prog_cache is
+ * non-NULL.
+ */
+static regprog_T    *eval_prog_cache = NULL;
+static char_u	    *eval_prog_pat = NULL;	// pattern it was compiled for
+static char_u	    *eval_prog_enc = NULL;	// 'encoding' when compiled
+static long	    eval_prog_re;		// 'regexpengine' when compiled
+// State sampled by eval_regcomp() for the eval_regfree() call that gives the
+// program back: user code run in between, e.g. an object's string() method
+// invoked by match() on a list, can change these options, and then the
+// program must not be cached under the changed values.  eval_compile_enc is
+// only ever compared against p_enc, never dereferenced.
+static long	    eval_compile_re;
+static char_u	    *eval_compile_enc;
+static int	    eval_prog_busy = 0;	// programs eval_regcomp() handed out
+					// that were not given back yet
+static int	    eval_prog_reentered = FALSE;  // eval_regcomp() ran while
+						  // a program was handed out
+
+/*
+ * Return true when compiling "pat" depends on more state than the cache key
+ * covers: "~" is replaced with the previous substitute string, bracket
+ * classes like [:alpha:], [=a=] and [.a.] can depend on the locale, the
+ * cursor-relative atoms \%.l, \%.c and \%.v compile the current cursor
+ * position into the program, and a "\%#=" engine prefix makes vim_regcomp()
+ * report E864 for a bad value on every compilation.  The check is
+ * intentionally over-approximate, e.g. a literal "~" or "%." also matches;
+ * such a pattern is compiled every time, which is never wrong.
+ */
+    static bool
+eval_prog_volatile(char_u *pat)
+{
+    char_u *p;
+
+    if (STRNCMP(pat, "\%#=", 4) == 0)
+	return true;
+    for (p = pat; *p != NUL; ++p)
+	if (*p == '~'
+		|| (p[0] == '[' && (p[1] == ':' || p[1] == '=' || p[1] == '.'))
+		|| (p[0] == '%' && (p[1] == '.'
+			|| ((p[1] == '<' || p[1] == '>') && p[2] == '.'))))
+	    return true;
+    return false;
+}
+
+/*
+ * Compile pattern "pat" like vim_regcomp(pat, RE_MAGIC + RE_STRING) would,
+ * but reuse the cached program when it was compiled for the same pattern.
+ * Free the result with eval_regfree(), not with vim_regfree().
+ */
+    regprog_T *
+eval_regcomp(char_u *pat)
+{
+    regprog_T	*prog;
+
+    // Sample the option state the key covers, eval_regfree() checks that it
+    // did not change while the caller was using the program.
+    eval_compile_re = p_re;
+    eval_compile_enc = p_enc;
+    if (eval_prog_busy > 0)
+	// The samples above no longer describe the program already handed
+	// out: its eval_regfree() must drop it instead of caching it.
+	eval_prog_reentered = TRUE;
+
+    if (eval_prog_cache != NULL
+	    && *p_cpo == NUL
+	    && eval_prog_cache->re_flags == RE_MAGIC + RE_STRING
+	    && eval_prog_re == p_re
+	    && STRCMP(eval_prog_pat, pat) == 0
+	    && STRCMP(eval_prog_enc, p_enc) == 0)
+    {
+	prog = eval_prog_cache;
+	// The caller now owns the program, eval_regfree() adopts the
+	// program the caller ends up with.
+	eval_prog_cache = NULL;
+    }
+    else
+	prog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
+    if (prog != NULL)
+	++eval_prog_busy;	// paired with the eval_regfree() call
+    return prog;
+}
+
+/*
+ * Free program "prog", obtained with eval_regcomp() for pattern "pat", by
+ * keeping it in the cache for the next use.  "pat" must be the same string
+ * the program was compiled for and must still be valid.
+ */
+    void
+eval_regfree(char_u *pat, regprog_T *prog)
+{
+    int	    reentered = eval_prog_reentered;
+
+    if (eval_prog_busy > 0 && --eval_prog_busy == 0)
+	eval_prog_reentered = FALSE;
+    if (prog == NULL)
+	return;
+    if (reentered || *p_cpo != NUL
+	    || eval_compile_re != p_re || eval_compile_enc != p_enc)
+    {
+	// 'cpoptions' is not empty, an option in the cache key changed, or
+	// another program was handed out while the caller was using this
+	// one: the samples may not describe this program, do not cache it.
+	vim_regfree(prog);
+	return;
+    }
+    if (eval_prog_pat == NULL || STRCMP(eval_prog_pat, pat) != 0
+	    || STRCMP(eval_prog_enc, p_enc) != 0)
+    {
+	char_u	*pat_copy;
+	char_u	*enc_copy;
+
+	// A cached pattern is known not to be volatile, thus the scan is
+	// only needed when installing a new key.
+	if (eval_prog_volatile(pat))
+	{
+	    // compiling depends on state the cache key does not cover
+	    vim_regfree(prog);
+	    return;
+	}
+	pat_copy = vim_strsave(pat);
+	enc_copy = vim_strsave(p_enc);
+	if (pat_copy == NULL || enc_copy == NULL)
+	{
+	    vim_free(pat_copy);
+	    vim_free(enc_copy);
+	    vim_regfree(prog);
+	    return;
+	}
+	vim_free(eval_prog_pat);
+	vim_free(eval_prog_enc);
+	eval_prog_pat = pat_copy;
+	eval_prog_enc = enc_copy;
+    }
+    // On a cache miss eval_regcomp() left the previous program in the
+    // cache, and a nested evaluation may have stored another one: keep the
+    // most recent program.
+    vim_regfree(eval_prog_cache);
+    eval_prog_cache = prog;
+    eval_prog_re = eval_compile_re;
+}
+
+#if defined(EXITFREE) || defined(PROTO)
+    void
+free_eval_regcomp_cache(void)
+{
+    vim_regfree(eval_prog_cache);
+    eval_prog_cache = NULL;
+    VIM_CLEAR(eval_prog_pat);
+    VIM_CLEAR(eval_prog_enc);
+    eval_prog_busy = 0;
+    eval_prog_reentered = FALSE;
+}
+#endif
+
 /*
  * Return TRUE if "pat" matches "text".
  * Does not use 'cpo' and always uses 'magic'.
@@ -3202,12 +3374,12 @@ pattern_match(char_u *pat, char_u *text, int ic)
     // avoid 'l' flag in 'cpoptions'
     save_cpo = p_cpo;
     p_cpo = empty_option;
-    regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
+    regmatch.regprog = eval_regcomp(pat);
     if (regmatch.regprog != NULL)
     {
 	regmatch.rm_ic = ic;
 	matches = vim_regexec_nl(&regmatch, text, (colnr_T)0);
-	vim_regfree(regmatch.regprog);
+	eval_regfree(pat, regmatch.regprog);
     }
     p_cpo = save_cpo;
     return matches;
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 6820cd8c9..bf8f00f7a 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -9228,7 +9228,7 @@ find_some_match(typval_T *argvars, typval_T *rettv, matchtype_T type)
 	    goto theend;
     }
 
-    regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
+    regmatch.regprog = eval_regcomp(pat);
     if (regmatch.regprog != NULL)
     {
 	regmatch.rm_ic = p_ic;
@@ -9346,7 +9346,7 @@ find_some_match(typval_T *argvars, typval_T *rettv, matchtype_T type)
 	}
 	if (l != NULL)
 	    l->lv_lock = prev_lock;
-	vim_regfree(regmatch.regprog);
+	eval_regfree(pat, regmatch.regprog);
     }
 
 theend:
@@ -9520,7 +9520,7 @@ f_matchbufline(typval_T *argvars, typval_T *rettv)
     save_cpo = p_cpo;
     p_cpo = empty_option;
 
-    regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
+    regmatch.regprog = eval_regcomp(pat);
     if (regmatch.regprog == NULL)
 	goto theend;
     regmatch.rm_ic = p_ic;
@@ -9535,7 +9535,7 @@ f_matchbufline(typval_T *argvars, typval_T *rettv)
     }
 
 cleanup:
-    vim_regfree(regmatch.regprog);
+    eval_regfree(pat, regmatch.regprog);
 
 theend:
     p_cpo = save_cpo;
@@ -9611,7 +9611,7 @@ f_matchstrlist(typval_T *argvars, typval_T *rettv)
     save_cpo = p_cpo;
     p_cpo = empty_option;
 
-    regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
+    regmatch.regprog = eval_regcomp(pat);
     if (regmatch.regprog == NULL)
 	goto theend;
     regmatch.rm_ic = p_ic;
@@ -9650,7 +9650,7 @@ f_matchstrlist(typval_T *argvars, typval_T *rettv)
     }
 
 cleanup:
-    vim_regfree(regmatch.regprog);
+    eval_regfree(pat, regmatch.regprog);
 
 theend:
     p_cpo = save_cpo;
@@ -12200,7 +12200,7 @@ f_split(typval_T *argvars, typval_T *rettv)
     if (typeerr)
 	goto theend;
 
-    regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
+    regmatch.regprog = eval_regcomp(pat);
     if (regmatch.regprog != NULL)
     {
 	regmatch.rm_ic = FALSE;
@@ -12232,7 +12232,7 @@ f_split(typval_T *argvars, typval_T *rettv)
 	    str = regmatch.endp[0];
 	}
 
-	vim_regfree(regmatch.regprog);
+	eval_regfree(pat, regmatch.regprog);
     }
 
 theend:
diff --git a/src/proto/eval.pro b/src/proto/eval.pro
index 2a0312175..448110981 100644
--- a/src/proto/eval.pro
+++ b/src/proto/eval.pro
@@ -34,6 +34,9 @@ void skip_for_lines(void *fi_void, evalarg_T *evalarg);
 int next_for_item(void *fi_void, char_u *arg);
 void free_for_info(void *fi_void);
 void set_context_for_expression(expand_T *xp, char_u *arg, cmdidx_T cmdidx);
+regprog_T *eval_regcomp(char_u *pat);
+void eval_regfree(char_u *pat, regprog_T *prog);
+void free_eval_regcomp_cache(void);
 int pattern_match(char_u *pat, char_u *text, int ic);
 char_u *eval_next_non_blank(char_u *arg, evalarg_T *evalarg, int *getnext);
 char_u *eval_next_line(char_u *arg, evalarg_T *evalarg);
diff --git a/src/testdir/test_eval_stuff.vim b/src/testdir/test_eval_stuff.vim
index e1afbfbe2..69f9b26a6 100644
--- a/src/testdir/test_eval_stuff.vim
+++ b/src/testdir/test_eval_stuff.vim
@@ -1385,4 +1385,65 @@ func Test_clipboard_provider_recursive()
   unlet g:vim_copy_recursive
 endfunc
 
+" Test that caching the compiled pattern of "=~" and the match functions
+" does not change the semantics.
+func Test_eval_pattern_cache()
+  let save_ic = &ignorecase
+  defer execute('let &ignorecase = ' .. save_ic)
+
+  " "~" in a pattern stands for the previous substitute string, a compiled
+  " program must not be reused across a :substitute
+  new
+  call setline(1, 'one')
+  s/one/AAA/
+  call assert_true('xAAAy' =~ 'x~y')
+  call assert_false('xBBBy' =~ 'x~y')
+  call setline(1, 'AAA')
+  s/AAA/BBB/
+  call assert_true('xBBBy' =~ 'x~y')
+  call assert_false('xAAAy' =~ 'x~y')
+  bwipe!
+
+  " 'ignorecase' is applied at execution time, also with a cached program
+  set noignorecase
+  call assert_false('ABC' =~ 'abc')
+  set ignorecase
+  call assert_true('ABC' =~ 'abc')
+  call assert_true('ABC' =~ 'abc')
+  set noignorecase
+  call assert_false('ABC' =~ 'abc')
+
+  " changing 'regexpengine' compiles the pattern again
+  for re in [0, 1, 2]
+    exe 'set re=' .. re
+    call assert_true('abc123' =~ 'a\+bc\d\+')
+    call assert_false('xyz' =~ 'a\+bc\d\+')
+  endfor
+  set re&
+
+  " \%.l, \%.c and \%.v compile the current cursor position into the
+  " program, such a pattern must not be reused after the cursor moved
+  new
+  call setline(1, 'abcdef')
+  call cursor(1, 4)
+  call assert_true('abcdef' =~# '\%.cd')
+  call cursor(1, 2)
+  call assert_false('abcdef' =~# '\%.cd')
+  call assert_true('abcdef' =~# '\%.cb')
+  bwipe!
+
+  " when the automatic engine replaces the program by falling back to the
+  " backtracking engine, the replaced program is the one that is kept
+  call test_override('nfa_fail', 1)
+  defer test_override('nfa_fail', 0)
+  for i in range(3)
+    call assert_true('fallback' =~ 'fall\%(back\)\?')
+    call assert_false('nomatch' =~ 'fall\%(back\)\?')
+  endfor
+  call test_override('nfa_fail', 0)
+  for i in range(3)
+    call assert_true('fallback' =~ 'fall\%(back\)\?')
+  endfor
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index 921e57820..c03f5e545 100644
--- a/src/version.c
+++ b/src/version.c
@@ -763,6 +763,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    971,
 /**/
     970,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups "vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].
To view this discussion visit https://groups.google.com/d/msgid/vim_dev/E1wwPkA-00AePd-Iw%40256bit.org.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.