diff --git a/runtime/doc/dev_arch.txt b/runtime/doc/dev_arch.txt index dac2831ecb..7604497d84 100644 --- a/runtime/doc/dev_arch.txt +++ b/runtime/doc/dev_arch.txt @@ -112,6 +112,12 @@ Many of the editor concepts are defined as Lua data files: - Ex (cmdline) commands: src/nvim/ex_cmds.lua - Options: src/nvim/options.lua - Vimscript functions: src/nvim/eval.lua + - Functions can be implemented in C (`f_foo` in eval/funcs.c), or in Lua + (set `func_lua` in eval.lua, implement in _core/vimfn.lua). Lua impl is + preferred, for performance and maintainability. + - VimScript callers go through `lua_wrapper` (typval <=> Object conversion). + - Lua callers (`vim.fn.foo()`) take a fast path in `nlua_call()` that calls + the Lua function directly, avoiding any conversion. - v: variables: src/nvim/vvars.lua ============================================================================== diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index aa255ce9ed..6b30a8b9e3 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -158,7 +158,10 @@ OPTIONS PERFORMANCE -• todo +• Nvim architecture allows pure-Lua implementations of some `vim.fn` + functions, which skips the Vimscript <=> Lua "bridge" (no data + conversion/marshalling) entirely, if the `vim.fn` function is called from + Lua. PLUGINS diff --git a/runtime/doc/vimfn.txt b/runtime/doc/vimfn.txt index 85b96cc975..04830089d4 100644 --- a/runtime/doc/vimfn.txt +++ b/runtime/doc/vimfn.txt @@ -4943,8 +4943,7 @@ hlexists({name}) *hlexists()* hostname() *hostname()* Returns the hostname of the machine on which the Nvim server - (not the UI client) is currently running. Names greater than - 256 characters long are truncated. + (not the UI client) is currently running. Return: ~ (`string`) diff --git a/runtime/lua/vim/_core/vimfn.lua b/runtime/lua/vim/_core/vimfn.lua new file mode 100644 index 0000000000..12f7da1e74 --- /dev/null +++ b/runtime/lua/vim/_core/vimfn.lua @@ -0,0 +1,14 @@ +-- Lua implementations of "vimfn" builtin functions (via `func_lua`). +-- +-- Functions defined here are pure Lua, they don't have any explicit C impl, so they are named with +-- the "f_xx" convention, for discoverability. + +local M = {} + +--- Returns the hostname of the machine. +--- @return string +function M.f_hostname() + return vim.uv.os_gethostname() +end + +return M diff --git a/runtime/lua/vim/_meta/vimfn.gen.lua b/runtime/lua/vim/_meta/vimfn.gen.lua index 8c72d7f111..8d6053e71e 100644 --- a/runtime/lua/vim/_meta/vimfn.gen.lua +++ b/runtime/lua/vim/_meta/vimfn.gen.lua @@ -4475,8 +4475,7 @@ function vim.fn.hlID(name) end function vim.fn.hlexists(name) end --- Returns the hostname of the machine on which the Nvim server ---- (not the UI client) is currently running. Names greater than ---- 256 characters long are truncated. +--- (not the UI client) is currently running. --- --- @return string function vim.fn.hostname() end diff --git a/src/gen/gen_eval.lua b/src/gen/gen_eval.lua index 7ba60e9263..4489af7e85 100644 --- a/src/gen/gen_eval.lua +++ b/src/gen/gen_eval.lua @@ -54,10 +54,16 @@ hashpipe:write([[ ]]) local funcs = loadfile(eval_file)().funcs -for _, func in pairs(funcs) do - if func.float_func then +for name, func in pairs(funcs) do + local n = (func.func_float and 1 or 0) + (func.func_lua and 1 or 0) + (func.func and 1 or 0) + assert(n <= 1, name .. ': only one of func, func_float, func_lua can be set') + + if func.func_float then func.func = 'float_op_wrapper' - func.data = '{ .float_func = &' .. func.float_func .. ' }' + func.data = '{ .func_float = &' .. func.func_float .. ' }' + elseif func.func_lua then + func.func = 'lua_wrapper' + func.data = '{ .func_lua = "' .. func.func_lua .. '" }' end end @@ -67,7 +73,7 @@ for _, fun in ipairs(metadata) do funcs[fun.name] = { args = #fun.parameters, func = 'api_wrapper', - data = '{ .api_handler = &method_handlers[' .. fun.handler_id .. '] }', + data = '{ .func_api = &method_handlers[' .. fun.handler_id .. '] }', } end end diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 50a38feb4f..4d47fadbf1 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -1,18 +1,12 @@ --- File containing table with all functions. --- --- Keys: +-- Defines all "vimfn" (builtin/"eval"/"Vimscript") functions. -- --- @class vim.EvalFn --- @field name? string ---- @field args? integer|integer[] Number of arguments, list with maximum and minimum number of arguments ---- or list with a minimum number of arguments only. Defaults to zero ---- arguments. ---- @field base? integer For methods: the argument to use as the base argument (1-indexed): ---- base->method() ---- Defaults to BASE_NONE (function cannot be used as a method). ---- @field func? string Name of the C function which implements the Vimscript function. Defaults to ---- `f_{funcname}`. ---- @field float_func? string +--- @field args? integer|integer[] (default: 0) Number of arguments, list with maximum and minimum number of arguments or list with a minimum number of arguments only. +--- @field base? integer For methods: the argument to use as the base argument (1-indexed): base->method(). Defaults to BASE_NONE (function cannot be used as a method). +--- @field func? string (default: "f_{funcname}") C function which implements the vimfn. +--- @field func_float? string Floating-point C function. Sets func="float_op_wrapper". +--- @field func_lua? string Function name in `vim._core.vimfn.*` (e.g. "f_hostname"). Sets func="lua_wrapper". --- @field fast? boolean Function can run in |api-fast| events. Defaults to false. --- @field deprecated? true --- @field returns? string|false @@ -76,7 +70,7 @@ M.funcs = { < 2.094395 ]=], - float_func = 'acos', + func_float = 'acos', name = 'acos', params = { { 'expr', 'number' } }, returns = 'number', @@ -276,7 +270,7 @@ M.funcs = { < -0.523599 ]=], - float_func = 'asin', + func_float = 'asin', name = 'asin', params = { { 'expr', 'any' } }, returns = 'number', @@ -567,7 +561,7 @@ M.funcs = { < -1.326405 ]=], - float_func = 'atan', + func_float = 'atan', name = 'atan', params = { { 'expr', 'number' } }, returns = 'number', @@ -1022,7 +1016,7 @@ M.funcs = { Returns 0.0 if {expr} is not a |Float| or a |Number|. ]=], - float_func = 'ceil', + func_float = 'ceil', name = 'ceil', params = { { 'expr', 'number' } }, returns = 'number', @@ -1597,7 +1591,7 @@ M.funcs = { < -0.646043 ]=], - float_func = 'cos', + func_float = 'cos', name = 'cos', params = { { 'expr', 'number' } }, returns = 'number', @@ -1618,7 +1612,7 @@ M.funcs = { < -1.127626 ]=], - float_func = 'cosh', + func_float = 'cosh', name = 'cosh', params = { { 'expr', 'number' } }, returns = 'number', @@ -2357,7 +2351,7 @@ M.funcs = { < 0.367879 ]=], - float_func = 'exp', + func_float = 'exp', name = 'exp', params = { { 'expr', 'number' } }, signature = 'exp({expr})', @@ -2882,7 +2876,7 @@ M.funcs = { < 4.0 ]=], - float_func = 'floor', + func_float = 'floor', name = 'floor', params = { { 'expr', 'number' } }, signature = 'floor({expr})', @@ -5515,10 +5509,10 @@ M.funcs = { hostname = { desc = [=[ Returns the hostname of the machine on which the Nvim server - (not the UI client) is currently running. Names greater than - 256 characters long are truncated. + (not the UI client) is currently running. ]=], fast = true, + func_lua = 'f_hostname', name = 'hostname', params = {}, returns = 'string', @@ -6576,7 +6570,7 @@ M.funcs = { < 5.0 ]=], - float_func = 'log', + func_float = 'log', name = 'log', params = { { 'expr', 'number' } }, returns = 'number', @@ -6596,7 +6590,7 @@ M.funcs = { < -2.0 ]=], - float_func = 'log10', + func_float = 'log10', name = 'log10', params = { { 'expr', 'number' } }, returns = 'number', @@ -9058,7 +9052,7 @@ M.funcs = { < -5.0 ]=], - float_func = 'round', + func_float = 'round', name = 'round', params = { { 'expr', 'number' } }, returns = 'number', @@ -11047,7 +11041,7 @@ M.funcs = { < 0.763301 ]=], - float_func = 'sin', + func_float = 'sin', name = 'sin', params = { { 'expr', 'number' } }, returns = 'number', @@ -11068,7 +11062,7 @@ M.funcs = { < -1.026517 ]=], - float_func = 'sinh', + func_float = 'sinh', name = 'sinh', params = { { 'expr', 'number' } }, signature = 'sinh({expr})', @@ -11335,7 +11329,7 @@ M.funcs = { NaN may be different, it depends on system libraries. ]=], - float_func = 'sqrt', + func_float = 'sqrt', name = 'sqrt', params = { { 'expr', 'number' } }, signature = 'sqrt({expr})', @@ -12517,7 +12511,7 @@ M.funcs = { < -1.181502 ]=], - float_func = 'tan', + func_float = 'tan', name = 'tan', params = { { 'expr', 'number' } }, returns = 'number', @@ -12538,7 +12532,7 @@ M.funcs = { < -0.761594 ]=], - float_func = 'tanh', + func_float = 'tanh', name = 'tanh', params = { { 'expr', 'number' } }, returns = 'number', @@ -12798,7 +12792,7 @@ M.funcs = { < 4.0 ]=], - float_func = 'trunc', + func_float = 'trunc', name = 'trunc', params = { { 'expr', 'number' } }, returns = 'integer', diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index bbf49ee2f3..a5101d1cb8 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -227,7 +227,7 @@ char *get_expr_name(expand_T *xp, int idx) return get_user_var_name(xp, ++intidx); } -/// Find internal function in hash functions +/// Gets a builtin (aka "vimfn", "eval") function from the generated hash table. /// /// @param[in] name Name of the function. /// @@ -240,6 +240,17 @@ const EvalFuncDef *find_internal_func(const char *const name) return index >= 0 ? &functions[index] : NULL; } +/// Gets the Lua name of a Lua-implemented "vimfn" function, or NULL if not found. +const char *find_internal_func_lua(const char *const name) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE FUNC_ATTR_NONNULL_ALL +{ + const EvalFuncDef *const fdef = find_internal_func(name); + if (fdef && fdef->func == &lua_wrapper) { + return fdef->data.func_lua; + } + return NULL; +} + /// Check the argument count to use for internal function "fdef". /// @return -1 for failure, 0 if no method base accepted, 1 if method base is /// first argument, 2 if method base is second argument, etc. @@ -325,7 +336,7 @@ static bool non_zero_arg(typval_T *argvars) && *argvars[0].vval.v_string != NUL)); } -/// Apply a floating point C function on a typval with one float_T. +/// Apply a floating point C function on a typval with one float_T (`func_float` in eval.lua). /// /// Some versions of glibc on i386 have an optimization that makes it harder to /// call math functions indirectly from inside an inlined function, causing @@ -336,19 +347,23 @@ static void float_op_wrapper(typval_T *argvars, typval_T *rettv, EvalFuncData fp rettv->v_type = VAR_FLOAT; if (tv_get_float_chk(argvars, &f)) { - rettv->vval.v_float = fptr.float_func(f); + rettv->vval.v_float = fptr.func_float(f); } else { rettv->vval.v_float = 0.0; } } +/// Invokes an API (nvim_) function from Vimscript. +/// +/// Converts `argvars` to API Objects, calls the API handler, and converts the result back. +/// Used by `gen_eval.lua` for `eval=true` API functions. static void api_wrapper(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { if (check_secure()) { return; } - MsgpackRpcRequestHandler handler = *fptr.api_handler; + MsgpackRpcRequestHandler handler = *fptr.func_api; MAXSIZE_TEMP_ARRAY(args, MAX_FUNC_ARGS); Arena arena = ARENA_EMPTY; @@ -375,11 +390,43 @@ end: api_clear_error(&err); } +/// Invokes a Lua-implemented vimfn/"f_xx" function from Vimscript (`func_lua` in eval.lua). +/// +/// - Converts argvars to API Objects, calls the Lua function, converts the result back. +/// - NOT used when called from Lua; `nlua_call()` calls the function directly. +static void lua_wrapper(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) +{ + MAXSIZE_TEMP_ARRAY(args, MAX_FUNC_ARGS); + Arena arena = ARENA_EMPTY; + + for (typval_T *tv = argvars; tv->v_type != VAR_UNKNOWN; tv++) { + ADD_C(args, vim_to_object(tv, &arena, false)); + } + + // `func_lua` is the function name (e.g. "f_hostname") in `_core/vimfn.lua`. + char buf[256]; + snprintf(buf, sizeof(buf), "return require('vim._core.vimfn').%s(...)", fptr.func_lua); + + Error err = ERROR_INIT; + Object result = nlua_exec(cstr_as_string(buf), NULL, args, kRetObject, &arena, &err); + + if (ERROR_SET(&err)) { + semsg_multiline("emsg", e_api_error, err.msg); + goto end; + } + + object_to_vim_take_luaref(&result, rettv, true, &err); + +end: + arena_mem_free(arena_finish(&arena)); + api_clear_error(&err); +} + /// "abs(expr)" function static void f_abs(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { if (argvars[0].v_type == VAR_FLOAT) { - float_op_wrapper(argvars, rettv, (EvalFuncData){ .float_func = &fabs }); + float_op_wrapper(argvars, rettv, (EvalFuncData){ .func_float = &fabs }); } else { bool error = false; @@ -2901,16 +2948,6 @@ static void f_hlexists(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) rettv->vval.v_number = highlight_exists(tv_get_string(&argvars[0])); } -/// "hostname()" function -static void f_hostname(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) -{ - char hostname[256]; - - os_get_hostname(hostname, 256); - rettv->v_type = VAR_STRING; - rettv->vval.v_string = xstrdup(hostname); -} - /// "index()" function static void f_index(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 7c4e42420a..4cd81e365d 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -1207,6 +1207,7 @@ static bool viml_func_is_fast(const char *name) return false; } +/// "vim.call()", aka "vim.fn". int nlua_call(lua_State *lstate) { Error err = ERROR_INIT; @@ -1224,6 +1225,27 @@ int nlua_call(lua_State *lstate) return luaL_error(lstate, "Function called with too many arguments"); } + // Fast path: if the vimfn is defined with `func_lua`, call the Lua function + // directly without Lua→typval→Object→Lua round-trip. + const char *func_lua = find_internal_func_lua(name); + if (func_lua) { + lua_getglobal(lstate, "require"); + lua_pushliteral(lstate, "vim._core.vimfn"); + if (lua_pcall(lstate, 1, 1, 0) != 0) { + return lua_error(lstate); + } + lua_getfield(lstate, -1, func_lua); + lua_remove(lstate, -2); // remove module table + // Push args (still on the stack as Lua values). + for (int j = 0; j < nargs; j++) { + lua_pushvalue(lstate, j + 2); + } + if (lua_pcall(lstate, nargs, 1, 0) != 0) { + return lua_error(lstate); + } + return 1; + } + typval_T vim_args[MAX_FUNC_ARGS + 1]; int i = 0; // also used for freeing the variables for (; i < nargs; i++) { diff --git a/src/nvim/types_defs.h b/src/nvim/types_defs.h index 05c1f12fee..4c00d993ef 100644 --- a/src/nvim/types_defs.h +++ b/src/nvim/types_defs.h @@ -32,8 +32,9 @@ typedef double float_T; typedef struct MsgpackRpcRequestHandler MsgpackRpcRequestHandler; typedef union { - float_T (*float_func)(float_T); - const MsgpackRpcRequestHandler *api_handler; + float_T (*func_float)(float_T); + const MsgpackRpcRequestHandler *func_api; // Vimscript bridge to API fn. + const char *func_lua; ///< Lua-implemented vimfn. void *null; } EvalFuncData; diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua index 90183efd88..bd1903cc44 100644 --- a/test/functional/core/main_spec.lua +++ b/test/functional/core/main_spec.lua @@ -231,6 +231,7 @@ describe('vim._core', function() 'vim._core.system', 'vim._core.ui2', 'vim._core.util', + 'vim._core.vimfn', 'vim._init_packages', 'vim.filetype', 'vim.fs', diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 2fde79a806..00ac46eb88 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -1391,6 +1391,15 @@ describe('lua stdlib', function() ) end) + it('vim.fn `func_lua` (fast path for Lua-implemented builtins)', function() + -- hostname() is implemented via func_lua, calling Lua directly when invoked from Lua. + local lua_result = exec_lua([[return vim.fn.hostname()]]) + eq(type(lua_result), 'string') + assert(#lua_result > 0, 'hostname() should return a non-empty string') + -- VimScript path (lua_wrapper) should return the same result. + eq(lua_result, eval('hostname()')) + end) + it('vim.call fails in fast context', function() local screen = Screen.new(120, 10) exec_lua([[ @@ -2003,14 +2012,14 @@ describe('lua stdlib', function() local errmsg = api.nvim_get_vvar('errmsg') matches( - [[ -^vim%.on%_key%(%) callbacks:.* -With ns%_id %d+: .*: Dumb Error -stack traceback: -.*: in function 'error' -.*: in function 'ErrF2' -.*: in function 'ErrF1' -.*]], + t.dedent [[ + ^vim%.on%_key%(%) callbacks:.* + With ns%_id %d+: .*: Dumb Error + stack traceback: + .*: in function 'error' + .*: in function 'ErrF2' + .*: in function 'ErrF1' + .*]], errmsg ) end) @@ -3268,8 +3277,8 @@ describe('vim.keymap', function() end) end) -describe('Vimscript function exists()', function() - it('can check a lua function', function() +describe('vim.fn.exists()', function() + it('can check a Lua function', function() eq( 1, exec_lua [[ diff --git a/test/functional/vimscript/eval_spec.lua b/test/functional/vimscript/eval_spec.lua index 29f17fee19..b4e924104c 100644 --- a/test/functional/vimscript/eval_spec.lua +++ b/test/functional/vimscript/eval_spec.lua @@ -215,10 +215,10 @@ describe('listing functions using :function', function() command('let A = {-> 1}') local num = exec_capture('echo A'):match("function%('(%d+)'%)") eq( - ([[ - function %s(...) -1 return 1 - endfunction]]):format(num), + t.dedent([[ + function %s(...) + 1 return 1 + endfunction]]):format(num), exec_capture(('function %s'):format(num)) ) end)