changeset 30594:586b5b3aacf9 v9.0.0632

patch 9.0.0632: calling a function from an "expr" option has overhead Commit: https://github.com/vim/vim/commit/87b4e5c5db9d1cfd6f2e79656e1a6cff3c69d15f Author: Bram Moolenaar <Bram@vim.org> Date: Sat Oct 1 15:32:46 2022 +0100 patch 9.0.0632: calling a function from an "expr" option has overhead Problem: Calling a function from an "expr" option has too much overhead. Solution: Add call_simple_func() and use it for 'foldexpr'
author Bram Moolenaar <Bram@vim.org>
date Sat, 01 Oct 2022 16:45:04 +0200
parents 56ab58fcefa6
children dea0b7889674
files runtime/doc/fold.txt runtime/doc/vim9.txt src/eval.c src/proto/userfunc.pro src/testdir/test_fold.vim src/userfunc.c src/version.c src/vim9execute.c
diffstat 8 files changed, 144 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/fold.txt
+++ b/runtime/doc/fold.txt
@@ -74,8 +74,6 @@ method.  The value of the 'foldexpr' opt
 of a line.  Examples:
 This will create a fold for all consecutive lines that start with a tab: >
 	:set foldexpr=getline(v:lnum)[0]==\"\\t\"
-This will call a function to compute the fold level: >
-	:set foldexpr=MyFoldLevel(v:lnum)
 This will make a fold out of paragraphs separated by blank lines: >
 	:set foldexpr=getline(v:lnum)=~'^\\s*$'&&getline(v:lnum+1)=~'\\S'?'<1':1
 This does the same: >
@@ -84,6 +82,10 @@ This does the same: >
 Note that backslashes must be used to escape characters that ":set" handles
 differently (space, backslash, double quote, etc., see |option-backslash|).
 
+The most efficient is to call a compiled function without arguments: >
+	:set foldexpr=MyFoldLevel()
+The function must use v:lnum.  See |expr-option-function|.
+
 These are the conditions with which the expression is evaluated:
 - The current buffer and window are set for the line.
 - The variable "v:lnum" is set to the line number.
--- a/runtime/doc/vim9.txt
+++ b/runtime/doc/vim9.txt
@@ -1410,6 +1410,21 @@ to a Vim9 function:
 		'three'
 		]
 
+
+Calling a function in an expr option ~
+							*expr-option-function*
+A few options, such as 'foldexpr', are an expresison that is evaluated to get
+a value.  The evaluation can have quite a bit of overhead.  One way to
+minimize the overhead, and also to keep the option value very simple, is to
+defined a compiled function and set the option to call it without arguments.
+Example: >
+	vim9script
+	def MyFoldFunc(): any
+	   ... compute fold level for line v:lnum
+	   return level
+	enddef
+	set foldexpr=s:MyFoldFunc()
+
 ==============================================================================
 
 4. Types					*vim9-types*
--- a/src/eval.c
+++ b/src/eval.c
@@ -899,13 +899,14 @@ eval_foldexpr(win_T *wp, int *cp)
 {
     char_u	*arg;
     typval_T	tv;
+    int		r = NOTDONE;
     varnumber_T	retval;
     char_u	*s;
     sctx_T	saved_sctx = current_sctx;
     int		use_sandbox = was_set_insecurely((char_u *)"foldexpr",
 								   OPT_LOCAL);
 
-    arg = wp->w_p_fde;
+    arg = skipwhite(wp->w_p_fde);
     current_sctx = wp->w_p_script_ctx[WV_FDE];
 
     ++emsg_off;
@@ -913,7 +914,21 @@ eval_foldexpr(win_T *wp, int *cp)
 	++sandbox;
     ++textlock;
     *cp = NUL;
-    if (eval0(arg, &tv, NULL, &EVALARG_EVALUATE) == FAIL)
+
+    // If the expression is "FuncName()" then we can skip a lot of overhead.
+    char_u *parens = (char_u *)strstr((char *)arg, "()");
+    if (parens != NULL && *skipwhite(parens + 2) == NUL)
+    {
+	char_u *p = STRNCMP(arg, "<SNR>", 5) == 0 ? skipdigits(arg + 5) : arg;
+
+	if (to_name_end(p, TRUE) == parens)
+	    r = call_simple_func(arg, (int)(parens - arg), &tv);
+    }
+
+    if (r == NOTDONE)
+	r = eval0(arg, &tv, NULL, &EVALARG_EVALUATE);
+
+    if (r == FAIL)
 	retval = 0;
     else
     {
--- a/src/proto/userfunc.pro
+++ b/src/proto/userfunc.pro
@@ -36,8 +36,9 @@ int func_call(char_u *name, typval_T *ar
 int get_callback_depth(void);
 int call_callback(callback_T *callback, int len, typval_T *rettv, int argcount, typval_T *argvars);
 varnumber_T call_callback_retnr(callback_T *callback, int argcount, typval_T *argvars);
-void user_func_error(int error, char_u *name, funcexe_T *funcexe);
+void user_func_error(int error, char_u *name, int found_var);
 int call_func(char_u *funcname, int len, typval_T *rettv, int argcount_in, typval_T *argvars_in, funcexe_T *funcexe);
+int call_simple_func(char_u *funcname, int len, typval_T *rettv);
 char_u *printable_func_name(ufunc_T *fp);
 char_u *trans_function_name(char_u **pp, int *is_global, int skip, int flags, funcdict_T *fdp, partial_T **partial, type_T **type);
 char_u *get_scriptlocal_funcname(char_u *funcname);
--- a/src/testdir/test_fold.vim
+++ b/src/testdir/test_fold.vim
@@ -249,6 +249,31 @@ func Test_foldexpr_no_interrupt_addsub()
   set foldmethod& foldexpr&
 endfunc
 
+" Fold function defined in another script
+func Test_foldexpr_compiled()
+  new
+  let lines =<< trim END
+      vim9script
+      def FoldFunc(): number
+        return v:lnum
+      enddef
+
+      set foldmethod=expr
+      set foldexpr=s:FoldFunc()
+  END
+  call writefile(lines, 'XfoldExpr', 'D')
+  source XfoldExpr
+
+  call setline(1, ['one', 'two', 'three'])
+  redraw
+  call assert_equal(1, foldlevel(1))
+  call assert_equal(2, foldlevel(2))
+  call assert_equal(3, foldlevel(3))
+
+  bwipe!
+  set foldmethod& foldexpr&
+endfunc
+
 func Check_foldlevels(expected)
   call assert_equal(a:expected, map(range(1, line('$')), 'foldlevel(v:val)'))
 endfunc
--- a/src/userfunc.c
+++ b/src/userfunc.c
@@ -3447,12 +3447,12 @@ call_callback_retnr(
  * Nothing if "error" is FCERR_NONE.
  */
     void
-user_func_error(int error, char_u *name, funcexe_T *funcexe)
+user_func_error(int error, char_u *name, int found_var)
 {
     switch (error)
     {
 	case FCERR_UNKNOWN:
-		if (funcexe->fe_found_var)
+		if (found_var)
 		    emsg_funcname(e_not_callable_type_str, name);
 		else
 		    emsg_funcname(e_unknown_function_str, name);
@@ -3702,7 +3702,8 @@ theend:
      * cancelled due to an aborting error, an interrupt, or an exception.
      */
     if (!aborting())
-	user_func_error(error, (name != NULL) ? name : funcname, funcexe);
+	user_func_error(error, (name != NULL) ? name : funcname,
+							funcexe->fe_found_var);
 
     // clear the copies made from the partial
     while (argv_clear > 0)
@@ -3714,6 +3715,77 @@ theend:
     return ret;
 }
 
+/*
+ * Call a function without arguments, partial or dict.
+ * This is like call_func() when the call is only "FuncName()".
+ * To be used by "expr" options.
+ * Returns NOTDONE when the function could not be found.
+ */
+    int
+call_simple_func(
+    char_u	*funcname,	// name of the function
+    int		len,		// length of "name" or -1 to use strlen()
+    typval_T	*rettv)		// return value goes here
+{
+    int		ret = FAIL;
+    int		error = FCERR_NONE;
+    char_u	fname_buf[FLEN_FIXED + 1];
+    char_u	*tofree = NULL;
+    char_u	*name;
+    char_u	*fname;
+    char_u	*rfname;
+    int		is_global = FALSE;
+    ufunc_T	*fp;
+
+    rettv->v_type = VAR_NUMBER;	// default rettv is number zero
+    rettv->vval.v_number = 0;
+
+    // Make a copy of the name, an option can be changed in the function.
+    name = vim_strnsave(funcname, len);
+    if (name == NULL)
+	return ret;
+
+    fname = fname_trans_sid(name, fname_buf, &tofree, &error);
+
+    // Skip "g:" before a function name.
+    if (fname[0] == 'g' && fname[1] == ':')
+    {
+	is_global = TRUE;
+	rfname = fname + 2;
+    }
+    else
+	rfname = fname;
+    fp = find_func(rfname, is_global);
+    if (fp != NULL && !is_global && in_vim9script()
+						 && func_requires_g_prefix(fp))
+	// In Vim9 script g: is required to find a global non-autoload
+	// function.
+	fp = NULL;
+    if (fp == NULL)
+	ret = NOTDONE;
+    else if (fp != NULL && (fp->uf_flags & FC_DELETED))
+	error = FCERR_DELETED;
+    else if (fp != NULL)
+    {
+	typval_T argvars[1];
+	funcexe_T	funcexe;
+
+	argvars[0].v_type = VAR_UNKNOWN;
+	CLEAR_FIELD(funcexe);
+	funcexe.fe_evaluate = TRUE;
+
+	error = call_user_func_check(fp, 0, argvars, rettv, &funcexe, NULL);
+	if (error == FCERR_NONE)
+	    ret = OK;
+    }
+
+    user_func_error(error, name, FALSE);
+    vim_free(tofree);
+    vim_free(name);
+
+    return ret;
+}
+
     char_u *
 printable_func_name(ufunc_T *fp)
 {
@@ -5676,7 +5748,7 @@ ex_defer_inner(
 
 		if (error != FCERR_UNKNOWN)
 		{
-		    user_func_error(error, name, NULL);
+		    user_func_error(error, name, FALSE);
 		    r = FAIL;
 		}
 	    }
--- a/src/version.c
+++ b/src/version.c
@@ -700,6 +700,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    632,
+/**/
     631,
 /**/
     630,
--- a/src/vim9execute.c
+++ b/src/vim9execute.c
@@ -1267,7 +1267,8 @@ call_ufunc(
 
     if (error != FCERR_NONE)
     {
-	user_func_error(error, printable_func_name(ufunc), &funcexe);
+	user_func_error(error, printable_func_name(ufunc),
+							 funcexe.fe_found_var);
 	return FAIL;
     }
     if (did_emsg > did_emsg_before)
@@ -4244,7 +4245,7 @@ exec_instructions(ectx_T *ectx)
 		    if (jump)
 			ectx->ec_iidx = iptr->isn_arg.whileloop.while_end;
 
-		    // Store the current funccal count, may be used by
+		    // Store the current funcref count, may be used by
 		    // ISN_ENDLOOP later
 		    tv = STACK_TV_VAR(
 				    iptr->isn_arg.whileloop.while_funcref_idx);