From e7bbd8256b8c701205389be431bbafd8743c72a9 Mon Sep 17 00:00:00 2001 From: ZyX Date: Fri, 4 Mar 2016 21:55:28 +0300 Subject: eval: Add luaeval function No tests yet, no documentation update, no :lua* stuff, no vim module. converter.c should also work with typval_T, not Object. Known problem: luaeval("1", {}) results in PANIC: unprotected error in call to Lua API (attempt to index a nil value) Ref #3823 --- CMakeLists.txt | 3 + scripts/gendispatch.lua | 334 ----------------------- scripts/generate_vim_module.lua | 38 +++ scripts/genmsgpack.lua | 469 ++++++++++++++++++++++++++++++++ src/nvim/CMakeLists.txt | 40 ++- src/nvim/eval.c | 43 +++ src/nvim/eval.lua | 1 + src/nvim/viml/executor/converter.c | 537 +++++++++++++++++++++++++++++++++++++ src/nvim/viml/executor/converter.h | 12 + src/nvim/viml/executor/executor.c | 270 +++++++++++++++++++ src/nvim/viml/executor/executor.h | 23 ++ src/nvim/viml/executor/vim.lua | 2 + 12 files changed, 1429 insertions(+), 343 deletions(-) delete mode 100644 scripts/gendispatch.lua create mode 100644 scripts/generate_vim_module.lua create mode 100644 scripts/genmsgpack.lua create mode 100644 src/nvim/viml/executor/converter.c create mode 100644 src/nvim/viml/executor/converter.h create mode 100644 src/nvim/viml/executor/executor.c create mode 100644 src/nvim/viml/executor/executor.h create mode 100644 src/nvim/viml/executor/vim.lua diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a058f2bff..b746dd460e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -310,6 +310,9 @@ include_directories(SYSTEM ${LIBUV_INCLUDE_DIRS}) find_package(Msgpack 1.0.0 REQUIRED) include_directories(SYSTEM ${MSGPACK_INCLUDE_DIRS}) +find_package(LuaJit REQUIRED) +include_directories(SYSTEM ${LUAJIT_INCLUDE_DIRS}) + if(UNIX) option(FEAT_TUI "Enable the Terminal UI" ON) else() diff --git a/scripts/gendispatch.lua b/scripts/gendispatch.lua deleted file mode 100644 index 45a4f4a4de..0000000000 --- a/scripts/gendispatch.lua +++ /dev/null @@ -1,334 +0,0 @@ -lpeg = require('lpeg') -mpack = require('mpack') - --- lpeg grammar for building api metadata from a set of header files. It --- ignores comments and preprocessor commands and parses a very small subset --- of C prototypes with a limited set of types -P, R, S = lpeg.P, lpeg.R, lpeg.S -C, Ct, Cc, Cg = lpeg.C, lpeg.Ct, lpeg.Cc, lpeg.Cg - -any = P(1) -- (consume one character) -letter = R('az', 'AZ') + S('_$') -num = R('09') -alpha = letter + num -nl = P('\r\n') + P('\n') -not_nl = any - nl -ws = S(' \t') + nl -fill = ws ^ 0 -c_comment = P('//') * (not_nl ^ 0) -c_preproc = P('#') * (not_nl ^ 0) -typed_container = - (P('ArrayOf(') + P('DictionaryOf(')) * ((any - P(')')) ^ 1) * P(')') -c_id = ( - typed_container + - (letter * (alpha ^ 0)) -) -c_void = P('void') -c_param_type = ( - ((P('Error') * fill * P('*') * fill) * Cc('error')) + - (C(c_id) * (ws ^ 1)) - ) -c_type = (C(c_void) * (ws ^ 1)) + c_param_type -c_param = Ct(c_param_type * C(c_id)) -c_param_list = c_param * (fill * (P(',') * fill * c_param) ^ 0) -c_params = Ct(c_void + c_param_list) -c_proto = Ct( - Cg(c_type, 'return_type') * Cg(c_id, 'name') * - fill * P('(') * fill * Cg(c_params, 'parameters') * fill * P(')') * - Cg(Cc(false), 'async') * - (fill * Cg((P('FUNC_API_SINCE(') * C(num ^ 1)) * P(')'), 'since') ^ -1) * - (fill * Cg((P('FUNC_API_ASYNC') * Cc(true)), 'async') ^ -1) * - (fill * Cg((P('FUNC_API_NOEXPORT') * Cc(true)), 'noexport') ^ -1) * - (fill * Cg((P('FUNC_API_NOEVAL') * Cc(true)), 'noeval') ^ -1) * - fill * P(';') - ) -grammar = Ct((c_proto + c_comment + c_preproc + ws) ^ 1) - --- we need at least 4 arguments since the last two are output files -assert(#arg >= 3) -functions = {} - -local nvimsrcdir = arg[1] -package.path = nvimsrcdir .. '/?.lua;' .. package.path - --- names of all headers relative to the source root (for inclusion in the --- generated file) -headers = {} - --- output h file with generated dispatch functions -dispatch_outputf = arg[#arg-2] --- output h file with packed metadata -funcs_metadata_outputf = arg[#arg-1] --- output metadata mpack file, for use by other build scripts -mpack_outputf = arg[#arg] - --- set of function names, used to detect duplicates -function_names = {} - --- read each input file, parse and append to the api metadata -for i = 2, #arg - 3 do - local full_path = arg[i] - local parts = {} - for part in string.gmatch(full_path, '[^/]+') do - parts[#parts + 1] = part - end - headers[#headers + 1] = parts[#parts - 1]..'/'..parts[#parts] - - local input = io.open(full_path, 'rb') - local tmp = grammar:match(input:read('*all')) - for i = 1, #tmp do - local fn = tmp[i] - if not fn.noexport then - functions[#functions + 1] = tmp[i] - function_names[fn.name] = true - if #fn.parameters ~= 0 and fn.parameters[1][2] == 'channel_id' then - -- this function should receive the channel id - fn.receives_channel_id = true - -- remove the parameter since it won't be passed by the api client - table.remove(fn.parameters, 1) - end - if #fn.parameters ~= 0 and fn.parameters[#fn.parameters][1] == 'error' then - -- function can fail if the last parameter type is 'Error' - fn.can_fail = true - -- remove the error parameter, msgpack has it's own special field - -- for specifying errors - fn.parameters[#fn.parameters] = nil - end - end - end - input:close() -end - -local function shallowcopy(orig) - local copy = {} - for orig_key, orig_value in pairs(orig) do - copy[orig_key] = orig_value - end - return copy -end - -local function startswith(String,Start) - return string.sub(String,1,string.len(Start))==Start -end - --- Export functions under older deprecated names. --- These will be removed eventually. -local deprecated_aliases = require("api.dispatch_deprecated") -for i,f in ipairs(shallowcopy(functions)) do - local ismethod = false - if startswith(f.name, "nvim_") then - if f.since == nil then - print("Function "..f.name.." lacks since field.\n") - os.exit(1) - end - f.since = tonumber(f.since) - if startswith(f.name, "nvim_buf_") then - ismethod = true - elseif startswith(f.name, "nvim_win_") then - ismethod = true - elseif startswith(f.name, "nvim_tabpage_") then - ismethod = true - end - else - f.noeval = true - f.since = 0 - f.deprecated_since = 1 - end - f.method = ismethod - local newname = deprecated_aliases[f.name] - if newname ~= nil then - if function_names[newname] then - -- duplicate - print("Function "..f.name.." has deprecated alias\n" - ..newname.." which has a separate implementation.\n".. - "Please remove it from src/nvim/api/dispatch_deprecated.lua") - os.exit(1) - end - local newf = shallowcopy(f) - newf.name = newname - if newname == "ui_try_resize" then - -- The return type was incorrectly set to Object in 0.1.5. - -- Keep it that way for clients that rely on this. - newf.return_type = "Object" - end - newf.impl_name = f.name - newf.noeval = true - newf.since = 0 - newf.deprecated_since = 1 - functions[#functions+1] = newf - end -end - --- don't expose internal attributes like "impl_name" in public metadata -exported_attributes = {'name', 'parameters', 'return_type', 'method', - 'since', 'deprecated_since'} -exported_functions = {} -for _,f in ipairs(functions) do - local f_exported = {} - for _,attr in ipairs(exported_attributes) do - f_exported[attr] = f[attr] - end - exported_functions[#exported_functions+1] = f_exported -end - - -funcs_metadata_output = io.open(funcs_metadata_outputf, 'wb') -funcs_metadata_output:write([[ -static const uint8_t funcs_metadata[] = { -]]) - --- serialize the API metadata using msgpack and embed into the resulting --- binary for easy querying by clients -packed_exported_functions = mpack.pack(exported_functions) -for i = 1, #packed_exported_functions do - funcs_metadata_output:write(string.byte(packed_exported_functions, i)..', ') - if i % 10 == 0 then - funcs_metadata_output:write('\n ') - end -end -funcs_metadata_output:write([[ -}; -]]) -funcs_metadata_output:close() - --- start building the dispatch wrapper output -output = io.open(dispatch_outputf, 'wb') - -local function real_type(type) - local rv = type - if typed_container:match(rv) then - if rv:match('Array') then - rv = 'Array' - else - rv = 'Dictionary' - end - end - return rv -end - --- start the handler functions. Visit each function metadata to build the --- handler function with code generated for validating arguments and calling to --- the real API. -for i = 1, #functions do - local fn = functions[i] - if fn.impl_name == nil then - local args = {} - - output:write('Object handle_'..fn.name..'(uint64_t channel_id, Array args, Error *error)') - output:write('\n{') - output:write('\n Object ret = NIL;') - -- Declare/initialize variables that will hold converted arguments - for j = 1, #fn.parameters do - local param = fn.parameters[j] - local converted = 'arg_'..j - output:write('\n '..param[1]..' '..converted..';') - end - output:write('\n') - output:write('\n if (args.size != '..#fn.parameters..') {') - output:write('\n snprintf(error->msg, sizeof(error->msg), "Wrong number of arguments: expecting '..#fn.parameters..' but got %zu", args.size);') - output:write('\n error->set = true;') - output:write('\n goto cleanup;') - output:write('\n }\n') - - -- Validation/conversion for each argument - for j = 1, #fn.parameters do - local converted, convert_arg, param, arg - param = fn.parameters[j] - converted = 'arg_'..j - local rt = real_type(param[1]) - if rt ~= 'Object' then - if rt:match('^Buffer$') or rt:match('^Window$') or rt:match('^Tabpage$') then - -- Buffer, Window, and Tabpage have a specific type, but are stored in integer - output:write('\n if (args.items['..(j - 1)..'].type == kObjectType'..rt..' && args.items['..(j - 1)..'].data.integer >= 0) {') - output:write('\n '..converted..' = (handle_T)args.items['..(j - 1)..'].data.integer;') - else - output:write('\n if (args.items['..(j - 1)..'].type == kObjectType'..rt..') {') - output:write('\n '..converted..' = args.items['..(j - 1)..'].data.'..rt:lower()..';') - end - if rt:match('^Buffer$') or rt:match('^Window$') or rt:match('^Tabpage$') or rt:match('^Boolean$') then - -- accept nonnegative integers for Booleans, Buffers, Windows and Tabpages - output:write('\n } else if (args.items['..(j - 1)..'].type == kObjectTypeInteger && args.items['..(j - 1)..'].data.integer >= 0) {') - output:write('\n '..converted..' = (handle_T)args.items['..(j - 1)..'].data.integer;') - end - output:write('\n } else {') - output:write('\n snprintf(error->msg, sizeof(error->msg), "Wrong type for argument '..j..', expecting '..param[1]..'");') - output:write('\n error->set = true;') - output:write('\n goto cleanup;') - output:write('\n }\n') - else - output:write('\n '..converted..' = args.items['..(j - 1)..'];\n') - end - - args[#args + 1] = converted - end - - -- function call - local call_args = table.concat(args, ', ') - output:write('\n ') - if fn.return_type ~= 'void' then - -- has a return value, prefix the call with a declaration - output:write(fn.return_type..' rv = ') - end - - -- write the function name and the opening parenthesis - output:write(fn.name..'(') - - if fn.receives_channel_id then - -- if the function receives the channel id, pass it as first argument - if #args > 0 or fn.can_fail then - output:write('channel_id, '..call_args) - else - output:write('channel_id') - end - else - output:write(call_args) - end - - if fn.can_fail then - -- if the function can fail, also pass a pointer to the local error object - if #args > 0 then - output:write(', error);\n') - else - output:write('error);\n') - end - -- and check for the error - output:write('\n if (error->set) {') - output:write('\n goto cleanup;') - output:write('\n }\n') - else - output:write(');\n') - end - - if fn.return_type ~= 'void' then - output:write('\n ret = '..string.upper(real_type(fn.return_type))..'_OBJ(rv);') - end - output:write('\n\ncleanup:'); - - output:write('\n return ret;\n}\n\n'); - end -end - --- Generate a function that initializes method names with handler functions -output:write([[ -void msgpack_rpc_init_method_table(void) -{ - methods = map_new(String, MsgpackRpcRequestHandler)(); - -]]) - -for i = 1, #functions do - local fn = functions[i] - output:write(' msgpack_rpc_add_method_handler('.. - '(String) {.data = "'..fn.name..'", '.. - '.size = sizeof("'..fn.name..'") - 1}, '.. - '(MsgpackRpcRequestHandler) {.fn = handle_'.. (fn.impl_name or fn.name).. - ', .async = '..tostring(fn.async)..'});\n') - -end - -output:write('\n}\n\n') -output:close() - -mpack_output = io.open(mpack_outputf, 'wb') -mpack_output:write(mpack.pack(functions)) -mpack_output:close() diff --git a/scripts/generate_vim_module.lua b/scripts/generate_vim_module.lua new file mode 100644 index 0000000000..954f1c38be --- /dev/null +++ b/scripts/generate_vim_module.lua @@ -0,0 +1,38 @@ +assert(#arg == 2) + +module_file = arg[1] +target_file = arg[2] + +module = io.open(module_file, 'r') +target = io.open(target_file, 'w') + +target:write('#include \n\n') +target:write('static const uint8_t vim_module[] = {\n') + +num_bytes = 0 +MAX_NUM_BYTES = 15 -- 78 / 5: maximum number of bytes on one line +target:write(' ') + +increase_num_bytes = function() + num_bytes = num_bytes + 1 + if num_bytes == MAX_NUM_BYTES then + num_bytes = 0 + target:write('\n ') + end +end + +for line in module:lines() do + for i = 1,string.len(line) do + byte = string.byte(line, i) + assert(byte ~= 0) + target:write(string.format(' %3u,', byte)) + increase_num_bytes() + end + target:write(string.format(' %3u,', string.byte('\n', 1))) + increase_num_bytes() +end + +target:write(' 0};\n') + +module:close() +target:close() diff --git a/scripts/genmsgpack.lua b/scripts/genmsgpack.lua new file mode 100644 index 0000000000..dd3caab5e4 --- /dev/null +++ b/scripts/genmsgpack.lua @@ -0,0 +1,469 @@ +lpeg = require('lpeg') +mpack = require('mpack') + +-- lpeg grammar for building api metadata from a set of header files. It +-- ignores comments and preprocessor commands and parses a very small subset +-- of C prototypes with a limited set of types +P, R, S = lpeg.P, lpeg.R, lpeg.S +C, Ct, Cc, Cg = lpeg.C, lpeg.Ct, lpeg.Cc, lpeg.Cg + +any = P(1) -- (consume one character) +letter = R('az', 'AZ') + S('_$') +num = R('09') +alpha = letter + num +nl = P('\r\n') + P('\n') +not_nl = any - nl +ws = S(' \t') + nl +fill = ws ^ 0 +c_comment = P('//') * (not_nl ^ 0) +c_preproc = P('#') * (not_nl ^ 0) +typed_container = + (P('ArrayOf(') + P('DictionaryOf(')) * ((any - P(')')) ^ 1) * P(')') +c_id = ( + typed_container + + (letter * (alpha ^ 0)) +) +c_void = P('void') +c_param_type = ( + ((P('Error') * fill * P('*') * fill) * Cc('error')) + + (C(c_id) * (ws ^ 1)) + ) +c_type = (C(c_void) * (ws ^ 1)) + c_param_type +c_param = Ct(c_param_type * C(c_id)) +c_param_list = c_param * (fill * (P(',') * fill * c_param) ^ 0) +c_params = Ct(c_void + c_param_list) +c_proto = Ct( + Cg(c_type, 'return_type') * Cg(c_id, 'name') * + fill * P('(') * fill * Cg(c_params, 'parameters') * fill * P(')') * + Cg(Cc(false), 'async') * + (fill * Cg((P('FUNC_API_SINCE(') * C(num ^ 1)) * P(')'), 'since') ^ -1) * + (fill * Cg((P('FUNC_API_ASYNC') * Cc(true)), 'async') ^ -1) * + (fill * Cg((P('FUNC_API_NOEXPORT') * Cc(true)), 'noexport') ^ -1) * + (fill * Cg((P('FUNC_API_NOEVAL') * Cc(true)), 'noeval') ^ -1) * + fill * P(';') + ) +grammar = Ct((c_proto + c_comment + c_preproc + ws) ^ 1) + +-- we need at least 4 arguments since the last two are output files +if arg[1] == '--help' then + print('Usage: genmsgpack.lua args') + print('Args: 1: source directory') + print(' 2: dispatch output file (dispatch_wrappers.generated.h)') + print(' 3: functions metadata output file (funcs_metadata.generated.h)') + print(' 4: API metadata output file (api_metadata.mpack)') + print(' 5: lua C bindings output file (msgpack_lua_c_bindings.generated.c)') + print(' rest: C files where API functions are defined') +end +assert(#arg >= 4) +functions = {} + +local nvimsrcdir = arg[1] +package.path = nvimsrcdir .. '/?.lua;' .. package.path + +-- names of all headers relative to the source root (for inclusion in the +-- generated file) +headers = {} + +-- output h file with generated dispatch functions +dispatch_outputf = arg[2] +-- output h file with packed metadata +funcs_metadata_outputf = arg[3] +-- output metadata mpack file, for use by other build scripts +mpack_outputf = arg[4] +lua_c_bindings_outputf = arg[5] + +-- set of function names, used to detect duplicates +function_names = {} + +-- read each input file, parse and append to the api metadata +for i = 6, #arg do + local full_path = arg[i] + local parts = {} + for part in string.gmatch(full_path, '[^/]+') do + parts[#parts + 1] = part + end + headers[#headers + 1] = parts[#parts - 1]..'/'..parts[#parts] + + local input = io.open(full_path, 'rb') + local tmp = grammar:match(input:read('*all')) + for i = 1, #tmp do + local fn = tmp[i] + if not fn.noexport then + functions[#functions + 1] = tmp[i] + function_names[fn.name] = true + if #fn.parameters ~= 0 and fn.parameters[1][2] == 'channel_id' then + -- this function should receive the channel id + fn.receives_channel_id = true + -- remove the parameter since it won't be passed by the api client + table.remove(fn.parameters, 1) + end + if #fn.parameters ~= 0 and fn.parameters[#fn.parameters][1] == 'error' then + -- function can fail if the last parameter type is 'Error' + fn.can_fail = true + -- remove the error parameter, msgpack has it's own special field + -- for specifying errors + fn.parameters[#fn.parameters] = nil + end + end + end + input:close() +end + +local function shallowcopy(orig) + local copy = {} + for orig_key, orig_value in pairs(orig) do + copy[orig_key] = orig_value + end + return copy +end + +local function startswith(String,Start) + return string.sub(String,1,string.len(Start))==Start +end + +-- Export functions under older deprecated names. +-- These will be removed eventually. +local deprecated_aliases = require("api.dispatch_deprecated") +for i,f in ipairs(shallowcopy(functions)) do + local ismethod = false + if startswith(f.name, "nvim_") then + if f.since == nil then + print("Function "..f.name.." lacks since field.\n") + os.exit(1) + end + f.since = tonumber(f.since) + if startswith(f.name, "nvim_buf_") then + ismethod = true + elseif startswith(f.name, "nvim_win_") then + ismethod = true + elseif startswith(f.name, "nvim_tabpage_") then + ismethod = true + end + else + f.noeval = true + f.since = 0 + f.deprecated_since = 1 + end + f.method = ismethod + local newname = deprecated_aliases[f.name] + if newname ~= nil then + if function_names[newname] then + -- duplicate + print("Function "..f.name.." has deprecated alias\n" + ..newname.." which has a separate implementation.\n".. + "Please remove it from src/nvim/api/dispatch_deprecated.lua") + os.exit(1) + end + local newf = shallowcopy(f) + newf.name = newname + if newname == "ui_try_resize" then + -- The return type was incorrectly set to Object in 0.1.5. + -- Keep it that way for clients that rely on this. + newf.return_type = "Object" + end + newf.impl_name = f.name + newf.noeval = true + newf.since = 0 + newf.deprecated_since = 1 + functions[#functions+1] = newf + end +end + +-- don't expose internal attributes like "impl_name" in public metadata +exported_attributes = {'name', 'parameters', 'return_type', 'method', + 'since', 'deprecated_since'} +exported_functions = {} +for _,f in ipairs(functions) do + local f_exported = {} + for _,attr in ipairs(exported_attributes) do + f_exported[attr] = f[attr] + end + exported_functions[#exported_functions+1] = f_exported +end + + +funcs_metadata_output = io.open(funcs_metadata_outputf, 'wb') +funcs_metadata_output:write([[ +static const uint8_t funcs_metadata[] = { +]]) + +-- serialize the API metadata using msgpack and embed into the resulting +-- binary for easy querying by clients +packed_exported_functions = mpack.pack(exported_functions) +for i = 1, #packed_exported_functions do + funcs_metadata_output:write(string.byte(packed_exported_functions, i)..', ') + if i % 10 == 0 then + funcs_metadata_output:write('\n ') + end +end +funcs_metadata_output:write([[ +}; +]]) +funcs_metadata_output:close() + +-- start building the dispatch wrapper output +output = io.open(dispatch_outputf, 'wb') + +local function real_type(type) + local rv = type + if typed_container:match(rv) then + if rv:match('Array') then + rv = 'Array' + else + rv = 'Dictionary' + end + end + return rv +end + +-- start the handler functions. Visit each function metadata to build the +-- handler function with code generated for validating arguments and calling to +-- the real API. +for i = 1, #functions do + local fn = functions[i] + if fn.impl_name == nil then + local args = {} + + output:write('Object handle_'..fn.name..'(uint64_t channel_id, Array args, Error *error)') + output:write('\n{') + output:write('\n Object ret = NIL;') + -- Declare/initialize variables that will hold converted arguments + for j = 1, #fn.parameters do + local param = fn.parameters[j] + local converted = 'arg_'..j + output:write('\n '..param[1]..' '..converted..';') + end + output:write('\n') + output:write('\n if (args.size != '..#fn.parameters..') {') + output:write('\n snprintf(error->msg, sizeof(error->msg), "Wrong number of arguments: expecting '..#fn.parameters..' but got %zu", args.size);') + output:write('\n error->set = true;') + output:write('\n goto cleanup;') + output:write('\n }\n') + + -- Validation/conversion for each argument + for j = 1, #fn.parameters do + local converted, convert_arg, param, arg + param = fn.parameters[j] + converted = 'arg_'..j + local rt = real_type(param[1]) + if rt ~= 'Object' then + if rt:match('^Buffer$') or rt:match('^Window$') or rt:match('^Tabpage$') then + -- Buffer, Window, and Tabpage have a specific type, but are stored in integer + output:write('\n if (args.items['..(j - 1)..'].type == kObjectType'..rt..' && args.items['..(j - 1)..'].data.integer >= 0) {') + output:write('\n '..converted..' = (handle_T)args.items['..(j - 1)..'].data.integer;') + else + output:write('\n if (args.items['..(j - 1)..'].type == kObjectType'..rt..') {') + output:write('\n '..converted..' = args.items['..(j - 1)..'].data.'..rt:lower()..';') + end + if rt:match('^Buffer$') or rt:match('^Window$') or rt:match('^Tabpage$') or rt:match('^Boolean$') then + -- accept nonnegative integers for Booleans, Buffers, Windows and Tabpages + output:write('\n } else if (args.items['..(j - 1)..'].type == kObjectTypeInteger && args.items['..(j - 1)..'].data.integer >= 0) {') + output:write('\n '..converted..' = (handle_T)args.items['..(j - 1)..'].data.integer;') + end + output:write('\n } else {') + output:write('\n snprintf(error->msg, sizeof(error->msg), "Wrong type for argument '..j..', expecting '..param[1]..'");') + output:write('\n error->set = true;') + output:write('\n goto cleanup;') + output:write('\n }\n') + else + output:write('\n '..converted..' = args.items['..(j - 1)..'];\n') + end + + args[#args + 1] = converted + end + + -- function call + local call_args = table.concat(args, ', ') + output:write('\n ') + if fn.return_type ~= 'void' then + -- has a return value, prefix the call with a declaration + output:write(fn.return_type..' rv = ') + end + + -- write the function name and the opening parenthesis + output:write(fn.name..'(') + + if fn.receives_channel_id then + -- if the function receives the channel id, pass it as first argument + if #args > 0 or fn.can_fail then + output:write('channel_id, '..call_args) + else + output:write('channel_id') + end + else + output:write(call_args) + end + + if fn.can_fail then + -- if the function can fail, also pass a pointer to the local error object + if #args > 0 then + output:write(', error);\n') + else + output:write('error);\n') + end + -- and check for the error + output:write('\n if (error->set) {') + output:write('\n goto cleanup;') + output:write('\n }\n') + else + output:write(');\n') + end + + if fn.return_type ~= 'void' then + output:write('\n ret = '..string.upper(real_type(fn.return_type))..'_OBJ(rv);') + end + output:write('\n\ncleanup:'); + + output:write('\n return ret;\n}\n\n'); + end +end + +-- Generate a function that initializes method names with handler functions +output:write([[ +void msgpack_rpc_init_method_table(void) +{ + methods = map_new(String, MsgpackRpcRequestHandler)(); + +]]) + +for i = 1, #functions do + local fn = functions[i] + output:write(' msgpack_rpc_add_method_handler('.. + '(String) {.data = "'..fn.name..'", '.. + '.size = sizeof("'..fn.name..'") - 1}, '.. + '(MsgpackRpcRequestHandler) {.fn = handle_'.. (fn.impl_name or fn.name).. + ', .async = '..tostring(fn.async)..'});\n') + +end + +output:write('\n}\n\n') +output:close() + +mpack_output = io.open(mpack_outputf, 'wb') +mpack_output:write(mpack.pack(functions)) +mpack_output:close() + +local function include_headers(output, headers) + for i = 1, #headers do + if headers[i]:sub(-12) ~= '.generated.h' then + output:write('\n#include "nvim/'..headers[i]..'"') + end + end +end + +local function write_shifted_output(output, str) + str = str:gsub('\n ', '\n') + str = str:gsub('^ ', '') + str = str:gsub(' +$', '') + output:write(str) +end + +-- start building lua output +output = io.open(lua_c_bindings_outputf, 'wb') + +output:write([[ +#include +#include +#include + +#include "nvim/func_attr.h" +#include "nvim/api/private/defs.h" +#include "nvim/viml/executor/converter.h" +]]) +include_headers(output, headers) +output:write('\n') + +lua_c_functions = {} + +local function process_function(fn) + lua_c_function_name = ('nlua_msgpack_%s'):format(fn.name) + write_shifted_output(output, string.format([[ + + static int %s(lua_State *lstate) + { + Error err = {.set = false}; + ]], lua_c_function_name)) + lua_c_functions[#lua_c_functions + 1] = { + binding=lua_c_function_name, + api=fn.name + } + cparams = '' + for j, param in ipairs(fn.parameters) do + cparam = string.format('arg%u', j) + if param[1]:match('^ArrayOf') then + param_type = 'Array' + else + param_type = param[1] + end + write_shifted_output(output, string.format([[ + %s %s = nlua_pop_%s(lstate, &err); + if (err.set) { + lua_pushstring(lstate, err.msg); + return lua_error(lstate); + } + ]], param[1], cparam, param_type)) + cparams = cparams .. cparam .. ', ' + end + if fn.receives_channel_id then + cparams = 'INTERNAL_CALL, ' .. cparams + end + if fn.can_fail then + cparams = cparams .. '&err' + else + cparams = cparams:gsub(', $', '') + end + local name = fn.impl_name or fn.name + if fn.return_type ~= 'void' then + if fn.return_type:match('^ArrayOf') then + return_type = 'Array' + else + return_type = fn.return_type + end + write_shifted_output(output, string.format([[ + %s ret = %s(%s); + if (err.set) { + lua_pushstring(lstate, err.msg); + return lua_error(lstate); + } + nlua_push_%s(lstate, ret); + return 1; + ]], fn.return_type, name, cparams, return_type)) + else + write_shifted_output(output, string.format([[ + %s(%s); + if (err.set) { + lua_pushstring(lstate, err.msg); + return lua_error(lstate); + } + return 0; + ]], name, cparams)) + end + write_shifted_output(output, [[ + } + ]]) +end + +for _, fn in ipairs(functions) do + if not fn.noeval then + process_function(fn) + end +end + +output:write(string.format([[ +void nlua_add_api_functions(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + lua_createtable(lstate, 0, %u); +]], #lua_c_functions)) +for _, func in ipairs(lua_c_functions) do + output:write(string.format([[ + lua_pushcfunction(lstate, &%s); + lua_setfield(lstate, -2, "%s"); + ]], func.binding, func.api)) +end +output:write([[ + lua_setfield(lstate, -2, "api"); +} +]]) + +output:close() diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index 22cf1f3a3d..a47a8e49c7 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -11,11 +11,10 @@ endif() endif() set(GENERATED_DIR ${PROJECT_BINARY_DIR}/src/nvim/auto) -set(DISPATCH_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/gendispatch.lua) -file(GLOB API_HEADERS api/*.h) -file(GLOB MSGPACK_RPC_HEADERS msgpack_rpc/*.h) +set(MSGPACK_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/genmsgpack.lua) set(API_METADATA ${PROJECT_BINARY_DIR}/api_metadata.mpack) set(FUNCS_DATA ${PROJECT_BINARY_DIR}/funcs_data.mpack) +set(MSGPACK_LUA_C_BINDINGS ${GENERATED_DIR}/msgpack_lua_c_bindings.generated.c) set(HEADER_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/gendeclarations.lua) set(GENERATED_INCLUDES_DIR ${PROJECT_BINARY_DIR}/include) set(GENERATED_API_DISPATCH ${GENERATED_DIR}/api/private/dispatch_wrappers.generated.h) @@ -37,8 +36,14 @@ set(EVAL_DEFS_FILE ${PROJECT_SOURCE_DIR}/src/nvim/eval.lua) set(OPTIONS_LIST_FILE ${PROJECT_SOURCE_DIR}/src/nvim/options.lua) set(UNICODE_TABLES_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/genunicodetables.lua) set(UNICODE_DIR ${PROJECT_SOURCE_DIR}/unicode) -file(GLOB UNICODE_FILES ${UNICODE_DIR}/*.txt) set(GENERATED_UNICODE_TABLES ${GENERATED_DIR}/unicode_tables.generated.h) +set(VIM_MODULE_FILE ${GENERATED_DIR}/viml/executor/vim_module.generated.h) +set(VIM_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/src/nvim/viml/executor/vim.lua) +set(VIM_MODULE_GENERATOR ${PROJECT_SOURCE_DIR}/scripts/generate_vim_module.lua) + +file(GLOB API_HEADERS api/*.h) +file(GLOB MSGPACK_RPC_HEADERS msgpack_rpc/*.h) +file(GLOB UNICODE_FILES ${UNICODE_DIR}/*.txt) include_directories(${GENERATED_DIR}) include_directories(${CACHED_GENERATED_DIR}) @@ -57,6 +62,8 @@ foreach(subdir tui event eval + viml + viml/executor ) if(${subdir} MATCHES "tui" AND NOT FEAT_TUI) continue() @@ -198,18 +205,30 @@ add_custom_command(OUTPUT ${GENERATED_UNICODE_TABLES} ${UNICODE_FILES} ) -add_custom_command(OUTPUT ${GENERATED_API_DISPATCH} ${GENERATED_FUNCS_METADATA} - ${API_METADATA} - COMMAND ${LUA_PRG} ${DISPATCH_GENERATOR} ${CMAKE_CURRENT_LIST_DIR} - ${API_HEADERS} ${GENERATED_API_DISPATCH} +add_custom_command( + OUTPUT ${GENERATED_API_DISPATCH} ${GENERATED_FUNCS_METADATA} + ${API_METADATA} ${MSGPACK_LUA_C_BINDINGS} + COMMAND ${LUA_PRG} ${MSGPACK_GENERATOR} ${CMAKE_CURRENT_LIST_DIR} + ${GENERATED_API_DISPATCH} ${GENERATED_FUNCS_METADATA} ${API_METADATA} + ${MSGPACK_LUA_C_BINDINGS} + ${API_HEADERS} DEPENDS ${API_HEADERS} ${MSGPACK_RPC_HEADERS} - ${DISPATCH_GENERATOR} + ${MSGPACK_GENERATOR} ${CMAKE_CURRENT_LIST_DIR}/api/dispatch_deprecated.lua ) +add_custom_command( + OUTPUT ${VIM_MODULE_FILE} + COMMAND ${LUA_PRG} ${VIM_MODULE_GENERATOR} ${VIM_MODULE_SOURCE} + ${VIM_MODULE_FILE} + DEPENDS + ${VIM_MODULE_GENERATOR} + ${VIM_MODULE_SOURCE} +) + list(APPEND NEOVIM_GENERATED_SOURCES "${PROJECT_BINARY_DIR}/config/auto/pathdef.c" "${GENERATED_API_DISPATCH}" @@ -219,6 +238,8 @@ list(APPEND NEOVIM_GENERATED_SOURCES "${GENERATED_EVENTS_NAMES_MAP}" "${GENERATED_OPTIONS}" "${GENERATED_UNICODE_TABLES}" + "${MSGPACK_LUA_C_BINDINGS}" + "${VIM_MODULE_FILE}" ) add_custom_command(OUTPUT ${GENERATED_EX_CMDS_ENUM} ${GENERATED_EX_CMDS_DEFS} @@ -270,6 +291,7 @@ list(APPEND NVIM_LINK_LIBRARIES ${LIBTERMKEY_LIBRARIES} ${UNIBILIUM_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} + ${LUAJIT_LIBRARIES} ) if(UNIX) diff --git a/src/nvim/eval.c b/src/nvim/eval.c index d49fcbf17e..c9141fbcbf 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -95,6 +95,7 @@ #include "nvim/lib/khash.h" #include "nvim/lib/queue.h" #include "nvim/eval/typval_encode.h" +#include "nvim/viml/executor/executor.h" #define DICT_MAXNEST 100 /* maximum nesting of lists and dicts */ @@ -13376,6 +13377,48 @@ static void get_maparg(typval_T *argvars, typval_T *rettv, int exact) } } +/// luaeval() function implementation +static void f_luaeval(typval_T *argvars, typval_T *rettv, FunPtr fptr) + FUNC_ATTR_NONNULL_ALL +{ + char *const str = (char *) get_tv_string(&argvars[0]); + if (str == NULL) { + return; + } + + Object arg; + if (argvars[1].v_type == VAR_UNKNOWN) { + arg = NIL; + } else { + arg = vim_to_object(&argvars[1]); + } + + // TODO(ZyX-I): Create function which converts lua objects directly to VimL + // objects, not to API objects. + Error err; + String err_str; + Object ret = executor_eval_lua(cstr_as_string(str), arg, &err, &err_str); + if (err.set) { + if (err_str.size) { + EMSG3(_("E971: Failed to eval lua string: %s (%s)"), err.msg, + err_str.data); + } else { + EMSG2(_("E971: Failed to eval lua string: %s"), err.msg); + } + } + + api_free_string(err_str); + + if (!err.set) { + if (!object_to_vim(ret, rettv, &err)) { + EMSG2(_("E972: Failed to convert resulting API object to VimL: %s"), + err.msg); + } + } + + api_free_object(ret); +} + /* * "map()" function */ diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index e3c5981b32..9db90ce05d 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -193,6 +193,7 @@ return { localtime={}, log={args=1, func="float_op_wrapper", data="&log"}, log10={args=1, func="float_op_wrapper", data="&log10"}, + luaeval={args={1, 2}}, map={args=2}, maparg={args={1, 4}}, mapcheck={args={1, 3}}, diff --git a/src/nvim/viml/executor/converter.c b/src/nvim/viml/executor/converter.c new file mode 100644 index 0000000000..2105beb08a --- /dev/null +++ b/src/nvim/viml/executor/converter.c @@ -0,0 +1,537 @@ +#include +#include +#include +#include + +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" +#include "nvim/func_attr.h" +#include "nvim/memory.h" +#include "nvim/assert.h" + +#include "nvim/viml/executor/converter.h" +#include "nvim/viml/executor/executor.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "viml/executor/converter.c.generated.h" +#endif + +#define NLUA_PUSH_IDX(lstate, type, idx) \ + do { \ + STATIC_ASSERT(sizeof(type) <= sizeof(lua_Number), \ + "Number sizes do not match"); \ + const type src = idx; \ + lua_Number tgt; \ + memset(&tgt, 0, sizeof(tgt)); \ + memcpy(&tgt, &src, sizeof(src)); \ + lua_pushnumber(lstate, tgt); \ + } while (0) + +#define NLUA_POP_IDX(lstate, type, stack_idx, idx) \ + do { \ + STATIC_ASSERT(sizeof(type) <= sizeof(lua_Number), \ + "Number sizes do not match"); \ + const lua_Number src = lua_tonumber(lstate, stack_idx); \ + type tgt; \ + memcpy(&tgt, &src, sizeof(tgt)); \ + idx = tgt; \ + } while (0) + +/// Push value which is a type index +/// +/// Used for all “typed” tables: i.e. for all tables which represent VimL +/// values. +static inline void nlua_push_type_idx(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + lua_pushboolean(lstate, true); +} + +/// Push value which is a locks index +/// +/// Used for containers tables. +static inline void nlua_push_locks_idx(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + lua_pushboolean(lstate, false); +} + +/// Push value which is a value index +/// +/// Used for tables which represent scalar values, like float value. +static inline void nlua_push_val_idx(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + lua_pushnumber(lstate, (lua_Number) 0); +} + +/// Push type +/// +/// Type is a value in vim.types table. +/// +/// @param[out] lstate Lua state. +/// @param[in] type Type to push (key in vim.types table). +static inline void nlua_push_type(lua_State *lstate, const char *const type) +{ + lua_getglobal(lstate, "vim"); + lua_getfield(lstate, -1, "types"); + lua_remove(lstate, -2); + lua_getfield(lstate, -1, type); + lua_remove(lstate, -2); +} + +/// Create lua table which has an entry that determines its VimL type +/// +/// @param[out] lstate Lua state. +/// @param[in] narr Number of “array” entries to be populated later. +/// @param[in] nrec Number of “dictionary” entries to be populated later. +/// @param[in] type Type of the table. +static inline void nlua_create_typed_table(lua_State *lstate, + const size_t narr, + const size_t nrec, + const char *const type) + FUNC_ATTR_NONNULL_ALL +{ + lua_createtable(lstate, (int) narr, (int) (1 + nrec)); + nlua_push_type_idx(lstate); + nlua_push_type(lstate, type); + lua_rawset(lstate, -3); +} + + +/// Convert given String to lua string +/// +/// Leaves converted string on top of the stack. +void nlua_push_String(lua_State *lstate, const String s) + FUNC_ATTR_NONNULL_ALL +{ + lua_pushlstring(lstate, s.data, s.size); +} + +/// Convert given Integer to lua number +/// +/// Leaves converted number on top of the stack. +void nlua_push_Integer(lua_State *lstate, const Integer n) + FUNC_ATTR_NONNULL_ALL +{ + lua_pushnumber(lstate, (lua_Number) n); +} + +/// Convert given Float to lua table +/// +/// Leaves converted table on top of the stack. +void nlua_push_Float(lua_State *lstate, const Float f) + FUNC_ATTR_NONNULL_ALL +{ + nlua_create_typed_table(lstate, 0, 1, "float"); + nlua_push_val_idx(lstate); + lua_pushnumber(lstate, (lua_Number) f); + lua_rawset(lstate, -3); +} + +/// Convert given Float to lua boolean +/// +/// Leaves converted value on top of the stack. +void nlua_push_Boolean(lua_State *lstate, const Boolean b) + FUNC_ATTR_NONNULL_ALL +{ + lua_pushboolean(lstate, b); +} + +static inline void nlua_add_locks_table(lua_State *lstate) +{ + nlua_push_locks_idx(lstate); + lua_newtable(lstate); + lua_rawset(lstate, -3); +} + +/// Convert given Dictionary to lua table +/// +/// Leaves converted table on top of the stack. +void nlua_push_Dictionary(lua_State *lstate, const Dictionary dict) + FUNC_ATTR_NONNULL_ALL +{ + nlua_create_typed_table(lstate, 0, 1 + dict.size, "dict"); + nlua_add_locks_table(lstate); + for (size_t i = 0; i < dict.size; i++) { + nlua_push_String(lstate, dict.items[i].key); + nlua_push_Object(lstate, dict.items[i].value); + lua_rawset(lstate, -3); + } +} + +/// Convert given Array to lua table +/// +/// Leaves converted table on top of the stack. +void nlua_push_Array(lua_State *lstate, const Array array) + FUNC_ATTR_NONNULL_ALL +{ + nlua_create_typed_table(lstate, array.size, 1, "float"); + nlua_add_locks_table(lstate); + for (size_t i = 0; i < array.size; i++) { + nlua_push_Object(lstate, array.items[i]); + lua_rawseti(lstate, -3, (int) i + 1); + } +} + +#define GENERATE_INDEX_FUNCTION(type) \ +void nlua_push_##type(lua_State *lstate, const type item) \ + FUNC_ATTR_NONNULL_ALL \ +{ \ + NLUA_PUSH_IDX(lstate, type, item); \ +} + +GENERATE_INDEX_FUNCTION(Buffer) +GENERATE_INDEX_FUNCTION(Window) +GENERATE_INDEX_FUNCTION(Tabpage) + +#undef GENERATE_INDEX_FUNCTION + +/// Convert given Object to lua value +/// +/// Leaves converted value on top of the stack. +void nlua_push_Object(lua_State *lstate, const Object obj) + FUNC_ATTR_NONNULL_ALL +{ + switch (obj.type) { + case kObjectTypeNil: { + lua_pushnil(lstate); + break; + } +#define ADD_TYPE(type, data_key) \ + case kObjectType##type: { \ + nlua_push_##type(lstate, obj.data.data_key); \ + break; \ + } + ADD_TYPE(Boolean, boolean) + ADD_TYPE(Integer, integer) + ADD_TYPE(Float, floating) + ADD_TYPE(String, string) + ADD_TYPE(Array, array) + ADD_TYPE(Dictionary, dictionary) +#undef ADD_TYPE +#define ADD_REMOTE_TYPE(type) \ + case kObjectType##type: { \ + nlua_push_##type(lstate, (type)obj.data.integer); \ + break; \ + } + ADD_REMOTE_TYPE(Buffer) + ADD_REMOTE_TYPE(Window) + ADD_REMOTE_TYPE(Tabpage) +#undef ADD_REMOTE_TYPE + } +} + + +/// Convert lua value to string +/// +/// Always pops one value from the stack. +String nlua_pop_String(lua_State *lstate, Error *err) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + String ret; + + ret.data = (char *) lua_tolstring(lstate, -1, &(ret.size)); + + if (ret.data == NULL) { + lua_pop(lstate, 1); + set_api_error("Expected lua string", err); + return (String) { .size = 0, .data = NULL }; + } + + ret.data = xmemdupz(ret.data, ret.size); + lua_pop(lstate, 1); + + return ret; +} + +/// Convert lua value to integer +/// +/// Always pops one value from the stack. +Integer nlua_pop_Integer(lua_State *lstate, Error *err) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + Integer ret = 0; + + if (!lua_isnumber(lstate, -1)) { + lua_pop(lstate, 1); + set_api_error("Expected lua integer", err); + return ret; + } + ret = (Integer) lua_tonumber(lstate, -1); + lua_pop(lstate, 1); + + return ret; +} + +/// Convert lua value to boolean +/// +/// Always pops one value from the stack. +Boolean nlua_pop_Boolean(lua_State *lstate, Error *err) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + Boolean ret = lua_toboolean(lstate, -1); + lua_pop(lstate, 1); + return ret; +} + +static inline bool nlua_check_type(lua_State *lstate, Error *err, + const char *const type) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (lua_type(lstate, -1) != LUA_TTABLE) { + set_api_error("Expected lua table", err); + return true; + } + + nlua_push_type_idx(lstate); + lua_rawget(lstate, -2); + nlua_push_type(lstate, type); + if (!lua_rawequal(lstate, -2, -1)) { + lua_pop(lstate, 2); + set_api_error("Expected lua table with float type", err); + return true; + } + lua_pop(lstate, 2); + + return false; +} + +/// Convert lua table to float +/// +/// Always pops one value from the stack. +Float nlua_pop_Float(lua_State *lstate, Error *err) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + Float ret = 0; + + if (nlua_check_type(lstate, err, "float")) { + lua_pop(lstate, 1); + return 0; + } + + nlua_push_val_idx(lstate); + lua_rawget(lstate, -2); + + if (!lua_isnumber(lstate, -1)) { + lua_pop(lstate, 2); + set_api_error("Value field should be lua number", err); + return ret; + } + ret = lua_tonumber(lstate, -1); + lua_pop(lstate, 2); + + return ret; +} + +/// Convert lua table to array +/// +/// Always pops one value from the stack. +Array nlua_pop_Array(lua_State *lstate, Error *err) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + Array ret = { .size = 0, .items = NULL }; + + if (nlua_check_type(lstate, err, "list")) { + lua_pop(lstate, 1); + return ret; + } + + for (int i = 1; ; i++, ret.size++) { + lua_rawgeti(lstate, -1, i); + + if (lua_isnil(lstate, -1)) { + lua_pop(lstate, 1); + break; + } + lua_pop(lstate, 1); + } + + if (ret.size == 0) { + lua_pop(lstate, 1); + return ret; + } + + ret.items = xcalloc(ret.size, sizeof(*ret.items)); + for (size_t i = 1; i <= ret.size; i++) { + Object val; + + lua_rawgeti(lstate, -1, (int) i); + + val = nlua_pop_Object(lstate, err); + if (err->set) { + ret.size = i; + lua_pop(lstate, 1); + api_free_array(ret); + return (Array) { .size = 0, .items = NULL }; + } + ret.items[i - 1] = val; + } + lua_pop(lstate, 1); + + return ret; +} + +/// Convert lua table to dictionary +/// +/// Always pops one value from the stack. Does not check whether +/// `vim.is_dict(table[type_idx])` or whether topmost value on the stack is +/// a table. +Dictionary nlua_pop_Dictionary_unchecked(lua_State *lstate, Error *err) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + Dictionary ret = { .size = 0, .items = NULL }; + + lua_pushnil(lstate); + + while (lua_next(lstate, -2)) { + if (lua_type(lstate, -2) == LUA_TSTRING) { + ret.size++; + } + lua_pop(lstate, 1); + } + + if (ret.size == 0) { + lua_pop(lstate, 1); + return ret; + } + ret.items = xcalloc(ret.size, sizeof(*ret.items)); + + lua_pushnil(lstate); + for (size_t i = 0; lua_next(lstate, -2);) { + // stack: dict, key, value + + if (lua_type(lstate, -2) == LUA_TSTRING) { + lua_pushvalue(lstate, -2); + // stack: dict, key, value, key + + ret.items[i].key = nlua_pop_String(lstate, err); + // stack: dict, key, value + + if (!err->set) { + ret.items[i].value = nlua_pop_Object(lstate, err); + // stack: dict, key + } else { + lua_pop(lstate, 1); + // stack: dict, key + } + + if (err->set) { + ret.size = i; + api_free_dictionary(ret); + lua_pop(lstate, 2); + // stack: + return (Dictionary) { .size = 0, .items = NULL }; + } + i++; + } else { + lua_pop(lstate, 1); + // stack: dict, key + } + } + lua_pop(lstate, 1); + + return ret; +} + +/// Convert lua table to dictionary +/// +/// Always pops one value from the stack. +Dictionary nlua_pop_Dictionary(lua_State *lstate, Error *err) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (nlua_check_type(lstate, err, "dict")) { + lua_pop(lstate, 1); + return (Dictionary) { .size = 0, .items = NULL }; + } + + return nlua_pop_Dictionary_unchecked(lstate, err); +} + +/// Convert lua table to object +/// +/// Always pops one value from the stack. +Object nlua_pop_Object(lua_State *lstate, Error *err) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + Object ret = { .type = kObjectTypeNil }; + + switch (lua_type(lstate, -1)) { + case LUA_TNIL: { + ret.type = kObjectTypeNil; + lua_pop(lstate, 1); + break; + } + case LUA_TSTRING: { + ret.type = kObjectTypeString; + ret.data.string = nlua_pop_String(lstate, err); + break; + } + case LUA_TNUMBER: { + ret.type = kObjectTypeInteger; + ret.data.integer = nlua_pop_Integer(lstate, err); + break; + } + case LUA_TBOOLEAN: { + ret.type = kObjectTypeBoolean; + ret.data.boolean = nlua_pop_Boolean(lstate, err); + break; + } + case LUA_TTABLE: { + lua_getglobal(lstate, "vim"); + // stack: obj, vim +#define CHECK_TYPE(Type, key, vim_type) \ + lua_getfield(lstate, -1, "is_" #vim_type); \ + /* stack: obj, vim, checker */ \ + lua_pushvalue(lstate, -3); \ + /* stack: obj, vim, checker, obj */ \ + lua_call(lstate, 1, 1); \ + /* stack: obj, vim, result */ \ + if (lua_toboolean(lstate, -1)) { \ + lua_pop(lstate, 2); \ + /* stack: obj */ \ + ret.type = kObjectType##Type; \ + ret.data.key = nlua_pop_##Type(lstate, err); \ + /* stack: */ \ + break; \ + } \ + lua_pop(lstate, 1); \ + // stack: obj, vim + CHECK_TYPE(Float, floating, float) + CHECK_TYPE(Array, array, list) + CHECK_TYPE(Dictionary, dictionary, dict) +#undef CHECK_TYPE + lua_pop(lstate, 1); + // stack: obj + ret.type = kObjectTypeDictionary; + ret.data.dictionary = nlua_pop_Dictionary_unchecked(lstate, err); + break; + } + default: { + lua_pop(lstate, 1); + set_api_error("Cannot convert given lua type", err); + break; + } + } + if (err->set) { + ret.type = kObjectTypeNil; + } + + return ret; +} + +#define GENERATE_INDEX_FUNCTION(type) \ +type nlua_pop_##type(lua_State *lstate, Error *err) \ + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT \ +{ \ + type ret; \ + NLUA_POP_IDX(lstate, type, -1, ret); \ + lua_pop(lstate, 1); \ + return ret; \ +} + +GENERATE_INDEX_FUNCTION(Buffer) +GENERATE_INDEX_FUNCTION(Window) +GENERATE_INDEX_FUNCTION(Tabpage) + +#undef GENERATE_INDEX_FUNCTION diff --git a/src/nvim/viml/executor/converter.h b/src/nvim/viml/executor/converter.h new file mode 100644 index 0000000000..e11d0cef19 --- /dev/null +++ b/src/nvim/viml/executor/converter.h @@ -0,0 +1,12 @@ +#ifndef NVIM_VIML_EXECUTOR_CONVERTER_H +#define NVIM_VIML_EXECUTOR_CONVERTER_H + +#include +#include +#include "nvim/api/private/defs.h" +#include "nvim/func_attr.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "viml/executor/converter.h.generated.h" +#endif +#endif // NVIM_VIML_EXECUTOR_CONVERTER_H diff --git a/src/nvim/viml/executor/executor.c b/src/nvim/viml/executor/executor.c new file mode 100644 index 0000000000..6f1e847649 --- /dev/null +++ b/src/nvim/viml/executor/executor.c @@ -0,0 +1,270 @@ +#include +#include +#include + +#include "nvim/misc1.h" +#include "nvim/getchar.h" +#include "nvim/garray.h" +#include "nvim/func_attr.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/vim.h" +#include "nvim/vim.h" +#include "nvim/message.h" + +#include "nvim/viml/executor/executor.h" +#include "nvim/viml/executor/converter.h" + +typedef struct { + Error err; + String lua_err_str; +} LuaError; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "viml/executor/vim_module.generated.h" +# include "viml/executor/executor.c.generated.h" +#endif + +/// Name of the run code for use in messages +#define NLUA_EVAL_NAME "" + +/// Call C function which does not expect any arguments +/// +/// @param function Called function +/// @param numret Number of returned arguments +#define NLUA_CALL_C_FUNCTION_0(lstate, function, numret) \ + do { \ + lua_pushcfunction(lstate, &function); \ + lua_call(lstate, 0, numret); \ + } while (0) +/// Call C function which expects four arguments +/// +/// @param function Called function +/// @param numret Number of returned arguments +/// @param a… Supplied argument (should be a void* pointer) +#define NLUA_CALL_C_FUNCTION_3(lstate, function, numret, a1, a2, a3) \ + do { \ + lua_pushcfunction(lstate, &function); \ + lua_pushlightuserdata(lstate, a1); \ + lua_pushlightuserdata(lstate, a2); \ + lua_pushlightuserdata(lstate, a3); \ + lua_call(lstate, 3, numret); \ + } while (0) +/// Call C function which expects five arguments +/// +/// @param function Called function +/// @param numret Number of returned arguments +/// @param a… Supplied argument (should be a void* pointer) +#define NLUA_CALL_C_FUNCTION_4(lstate, function, numret, a1, a2, a3, a4) \ + do { \ + lua_pushcfunction(lstate, &function); \ + lua_pushlightuserdata(lstate, a1); \ + lua_pushlightuserdata(lstate, a2); \ + lua_pushlightuserdata(lstate, a3); \ + lua_pushlightuserdata(lstate, a4); \ + lua_call(lstate, 4, numret); \ + } while (0) + +static void set_lua_error(lua_State *lstate, LuaError *lerr) + FUNC_ATTR_NONNULL_ALL +{ + const char *const str = lua_tolstring(lstate, -1, &lerr->lua_err_str.size); + lerr->lua_err_str.data = xmemdupz(str, lerr->lua_err_str.size); + lua_pop(lstate, 1); + + // FIXME? More specific error? + set_api_error("Error while executing lua code", &lerr->err); +} + +/// Compare two strings, ignoring case +/// +/// Expects two values on the stack: compared strings. Returns one of the +/// following numbers: 0, -1 or 1. +/// +/// Does no error handling: never call it with non-string or with some arguments +/// omitted. +static int nlua_stricmp(lua_State *lstate) FUNC_ATTR_NONNULL_ALL +{ + const char *s1 = luaL_checklstring(lstate, 1, NULL); + const char *s2 = luaL_checklstring(lstate, 2, NULL); + const int ret = STRICMP(s1, s2); + lua_pop(lstate, 2); + lua_pushnumber(lstate, (lua_Number) ((ret > 0) - (ret < 0))); + return 1; +} + +/// Evaluate lua string +/// +/// Expects three values on the stack: string to evaluate, pointer to the +/// location where result is saved, pointer to the location where error is +/// saved. Always returns nothing (from the lua point of view). +static int nlua_exec_lua_string(lua_State *lstate) FUNC_ATTR_NONNULL_ALL +{ + String *str = (String *) lua_touserdata(lstate, 1); + Object *obj = (Object *) lua_touserdata(lstate, 2); + LuaError *lerr = (LuaError *) lua_touserdata(lstate, 3); + lua_pop(lstate, 3); + + if (luaL_loadbuffer(lstate, str->data, str->size, NLUA_EVAL_NAME)) { + set_lua_error(lstate, lerr); + return 0; + } + if (lua_pcall(lstate, 0, 1, 0)) { + set_lua_error(lstate, lerr); + return 0; + } + *obj = nlua_pop_Object(lstate, &lerr->err); + return 0; +} + +/// Initialize lua interpreter state +/// +/// Called by lua interpreter itself to initialize state. +static int nlua_state_init(lua_State *lstate) FUNC_ATTR_NONNULL_ALL +{ + lua_pushcfunction(lstate, &nlua_stricmp); + lua_setglobal(lstate, "stricmp"); + if (luaL_dostring(lstate, (char *) &vim_module[0])) { + LuaError lerr; + set_lua_error(lstate, &lerr); + return 1; + } + nlua_add_api_functions(lstate); + lua_setglobal(lstate, "vim"); + return 0; +} + +/// Initialize lua interpreter +/// +/// Crashes NeoVim if initialization fails. Should be called once per lua +/// interpreter instance. +static lua_State *init_lua(void) + FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT +{ + lua_State *lstate = luaL_newstate(); + if (lstate == NULL) { + EMSG(_("E970: Failed to initialize lua interpreter")); + preserve_exit(); + } + luaL_openlibs(lstate); + NLUA_CALL_C_FUNCTION_0(lstate, nlua_state_init, 0); + return lstate; +} + +static Object exec_lua_string(lua_State *lstate, String str, LuaError *lerr) + FUNC_ATTR_NONNULL_ALL +{ + Object ret = { kObjectTypeNil, { false } }; + NLUA_CALL_C_FUNCTION_3(lstate, nlua_exec_lua_string, 0, &str, &ret, lerr); + return ret; +} + +static lua_State *global_lstate = NULL; + +/// Execute lua string +/// +/// Used for :lua. +/// +/// @param[in] str String to execute. +/// @param[out] err Location where error will be saved. +/// @param[out] err_str Location where lua error string will be saved, if any. +/// +/// @return Result of the execution. +Object executor_exec_lua(String str, Error *err, String *err_str) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (global_lstate == NULL) { + global_lstate = init_lua(); + } + + LuaError lerr = { + .err = { .set = false }, + .lua_err_str = STRING_INIT, + }; + + Object ret = exec_lua_string(global_lstate, str, &lerr); + + *err = lerr.err; + *err_str = lerr.lua_err_str; + + return ret; +} + +/// Evaluate lua string +/// +/// Used for luaeval(). Expects three values on the stack: +/// +/// 1. String to evaluate. +/// 2. _A value. +/// 3. Pointer to location where result is saved. +/// 4. Pointer to location where error will be saved. +/// +/// @param[in,out] lstate Lua interpreter state. +static int nlua_eval_lua_string(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + String *str = (String *) lua_touserdata(lstate, 1); + Object *arg = (Object *) lua_touserdata(lstate, 2); + Object *ret = (Object *) lua_touserdata(lstate, 3); + LuaError *lerr = (LuaError *) lua_touserdata(lstate, 4); + + garray_T str_ga; + ga_init(&str_ga, 1, 80); +#define EVALHEADER "local _A=select(1,...) return " + ga_concat_len(&str_ga, EVALHEADER, sizeof(EVALHEADER) - 1); +#undef EVALHEADER + ga_concat_len(&str_ga, str->data, str->size); + if (luaL_loadbuffer(lstate, str_ga.ga_data, (size_t) str_ga.ga_len, + NLUA_EVAL_NAME)) { + set_lua_error(lstate, lerr); + return 0; + } + ga_clear(&str_ga); + + nlua_push_Object(lstate, *arg); + if (lua_pcall(lstate, 1, 1, 0)) { + set_lua_error(lstate, lerr); + return 0; + } + *ret = nlua_pop_Object(lstate, &lerr->err); + + return 0; +} + +static Object eval_lua_string(lua_State *lstate, String str, Object arg, + LuaError *lerr) + FUNC_ATTR_NONNULL_ALL +{ + Object ret = { kObjectTypeNil, { false } }; + NLUA_CALL_C_FUNCTION_4(lstate, nlua_eval_lua_string, 0, + &str, &arg, &ret, lerr); + return ret; +} + +/// Evaluate lua string +/// +/// Used for luaeval(). +/// +/// @param[in] str String to execute. +/// @param[out] err Location where error will be saved. +/// @param[out] err_str Location where lua error string will be saved, if any. +/// +/// @return Result of the execution. +Object executor_eval_lua(String str, Object arg, Error *err, String *err_str) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (global_lstate == NULL) { + global_lstate = init_lua(); + } + + LuaError lerr = { + .err = { .set = false }, + .lua_err_str = STRING_INIT, + }; + + Object ret = eval_lua_string(global_lstate, str, arg, &lerr); + + *err = lerr.err; + *err_str = lerr.lua_err_str; + + return ret; +} diff --git a/src/nvim/viml/executor/executor.h b/src/nvim/viml/executor/executor.h new file mode 100644 index 0000000000..85cb3550e7 --- /dev/null +++ b/src/nvim/viml/executor/executor.h @@ -0,0 +1,23 @@ +#ifndef NVIM_VIML_EXECUTOR_EXECUTOR_H +#define NVIM_VIML_EXECUTOR_EXECUTOR_H + +#include + +#include "nvim/api/private/defs.h" +#include "nvim/func_attr.h" + +// Generated by msgpack-gen.lua +void nlua_add_api_functions(lua_State *lstate) REAL_FATTR_NONNULL_ALL; + +#define set_api_error(s, err) \ + do { \ + Error *err_ = (err); \ + err_->type = kErrorTypeException; \ + err_->set = true; \ + memcpy(&err_->msg[0], s, sizeof(s)); \ + } while (0) + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "viml/executor/executor.h.generated.h" +#endif +#endif // NVIM_VIML_EXECUTOR_EXECUTOR_H diff --git a/src/nvim/viml/executor/vim.lua b/src/nvim/viml/executor/vim.lua new file mode 100644 index 0000000000..8d1c5bdf4f --- /dev/null +++ b/src/nvim/viml/executor/vim.lua @@ -0,0 +1,2 @@ +-- TODO(ZyX-I): Create compatibility layer. +return {} -- cgit