From ceccca7780c54d932dd183517c4c04a340486dd7 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 4 Aug 2026 13:28:26 -0400 Subject: [PATCH] fix(options): crash on ":let &l:autoread = v:true" #41158 Problem: Assigning to the local value of a global-local boolean option ('autoread', 'autocomplete', 'fsync') aborts: Assertion failed: (curval.type == newval.type), function ex_let_option, file vars.c, line 1408. ex_let_one ex_let_vars ex_let execute_cmd0 do_cmdline call_user_func ... eval_map_expr vgetorpeek vgetc state_enter main A global-local option may have local value `kObjectTypeUnset`, but `ex_let_option()` guards only `kObjectTypeNil`. Solution: When curval is Unset, resolve it to the inherited global value. --- src/nvim/eval/vars.c | 9 +++++---- test/functional/vimscript/let_spec.lua | 13 ++++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index 7d59f5e872..52b6fce761 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -1404,10 +1404,11 @@ static char *ex_let_option(char *arg, typval_T *const tv, const bool is_const, goto theend; } - // Current value and new value must have the same type. - assert(curval.type == newval.type); - const bool is_num = curval.type == kObjectTypeInteger || curval.type == kObjectTypeBoolean; - const bool is_string = curval.type == kObjectTypeString; + // Global-local w/ unset local: assigning -1 unsets it again, so `curval` and `newval` may differ. + // in type. Apply a compound operator only to concrete same-kind values. + const bool is_num = (curval.type == kObjectTypeInteger || curval.type == kObjectTypeBoolean) + && (newval.type == kObjectTypeInteger || newval.type == kObjectTypeBoolean); + const bool is_string = curval.type == kObjectTypeString && newval.type == kObjectTypeString; if (op != NULL && *op != '=') { if (!hidden && is_num) { // number or bool diff --git a/test/functional/vimscript/let_spec.lua b/test/functional/vimscript/let_spec.lua index 54153db049..19b53f18bd 100644 --- a/test/functional/vimscript/let_spec.lua +++ b/test/functional/vimscript/let_spec.lua @@ -115,7 +115,7 @@ describe(':let', function() eq(false, api.nvim_get_option_value('equalalways', {})) end) - it('assigning bool/special to a string option gives E928, not a crash', function() + it('no crash: assigning bool/special to a string option gives E928', function() for _, v in ipairs({ 'v:true', 'v:false', 'v:null' }) do -- Regular string option. eq('Vim(let):E928: String required', t.pcall_err(command, 'let &makeprg = ' .. v)) @@ -123,6 +123,17 @@ describe(':let', function() eq('Vim(let):E928: String required', t.pcall_err(command, 'let &t_Co = ' .. v)) end end) + + it('no crash: assigning to local value of a global-local option', function() + -- Unset local reads as Unset, and -1 unsets it again, so curval/newval types may differ. + for _, o in ipairs({ 'autoread', 'autocomplete', 'fsync' }) do + command(('setglobal %s'):format(o)) -- global = on + command(('let &l:%s = v:false'):format(o)) -- unset local <- bool: sets local off + eq(false, api.nvim_get_option_value(o, {})) + command(('let &l:%s = -1'):format(o)) -- set local <- -1: unsets local, inheriting global + eq(true, api.nvim_get_option_value(o, {})) + end + end) end) describe(':let and :const', function()