Commit: patch 9.2.1140: Vim9: :def in legacy script can create a script variable

1 view
Skip to first unread message

Christian Brabandt

unread,
3:30 PM (8 hours ago) 3:30 PM
to vim...@googlegroups.com
patch 9.2.1140: Vim9: :def in legacy script can create a script variable

Commit: https://github.com/vim/vim/commit/6175ce87625f6d4fb1dacbbda27fcab8b1e92577
Author: Hirohito Higashi <h.eas...@gmail.com>
Date: Mon Sep 28 19:22:15 2026 +0000

patch 9.2.1140: Vim9: :def in legacy script can create a script variable

Problem: A :def function in a legacy script can create a script-local
variable by assigning to it, while declaring one with "var"
gives E1101.
Solution: Give E1089 for assigning to an unknown script-local variable
in any :def function, not only in one defined in a Vim9
script. Remove uf_script_ctx_version, which was only used
for this check. Do not give it with ":source ++dryrun".

runtime/menu.vim created s:bmenu_short, s:bmenu_count, s:suglist,
s:fromword, s:changeitem, s:additem and s:ignoreitem this way, define
them at the script level.

":source ++dryrun" does not execute ":let" in a legacy script, thus
every script variable would be unknown when compiling. Document that
reading or calling such a variable without "s:" still gives an error,
and separate the paragraphs of ":source-dryrun" with blank lines.

Test_unlet() created s:somevar this way, define it at the script level
instead. Test_using_s_var_in_function() checks that assigning to
s:scriptlevel in a :def function of a legacy script gives E1089 when
the variable does not exist. Test_source_dryrun() checks that
assigning to a script variable with "s:" in a :def function of a
legacy script gives no error with ":source ++dryrun".

closes: #21379

Assisted-by: Claude
Signed-off-by: Hirohito Higashi <h.eas...@gmail.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/runtime/doc/repeat.txt b/runtime/doc/repeat.txt
index 5e77a8100..0436007ff 100644
--- a/runtime/doc/repeat.txt
+++ b/runtime/doc/repeat.txt
@@ -1,4 +1,4 @@
-*repeat.txt* For Vim version 9.2. Last change: 2026 Sep 25
+*repeat.txt* For Vim version 9.2. Last change: 2026 Sep 28


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -248,6 +248,7 @@ For writing a Vim script, see chapter 41 of the user manual |usr_41.txt|.
Read the script without running it: only what defines
something is executed, then every |:def| function in
it is compiled and the errors are reported as usual.
+
What is executed: `vim9script`, `import`, `def`,
`function`, `class`, `interface`, `enum`, `type` and a
declaration with `var`, `const` or `final`. The
@@ -256,17 +257,27 @@ For writing a Vim script, see chapter 41 of the user manual |usr_41.txt|.
expression is not evaluated. A static class variable
is treated the same way and an enum value is not
created.
+
Everything else is skipped, also `finish` and the
condition of an `if`, `while` or `for`; a definition
inside such a block is executed whether the condition
holds or not. A function may therefore be defined
twice, the last definition counts.
+
+ In a legacy script `let` is not executed, thus a
+ script variable created by it does not exist. A
+ |:def| function can use it with "s:", but reading it
+ or calling it without "s:" gives an error. Defining a
+ function in a Dictionary it holds, such as
+ `function s:obj.Method()`, fails as well.
+
A script that is imported is read the same way, but an
autoload script is not loaded. No |SourceCmd|,
|SourcePre| or |SourcePost| autocommand is triggered.
A class or enum in an autoload script is defined again
by a dry run, and after one by a normal load, since no
object was made from it; see |vim9-reload|.
+
Note: An error in a definition does not stop the
reading, the rest of the script is read as well. And
an error in a line of a function does not stop
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index fd0178896..f2145933c 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -1,4 +1,4 @@
-*version9.txt* For Vim version 9.2. Last change: 2026 Sep 25
+*version9.txt* For Vim version 9.2. Last change: 2026 Sep 28


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -52719,6 +52719,8 @@ Changed ~
- The highlighting groups |hl-CursorLineFold| and |hl-CursorLineSign| are
always used when 'cursorline' is set.
- Virtual text above a line is now counted by |screenpos()| and |virtcol()|.
+- A |:def| function in a legacy script can no longer create a script-local
+ variable by assigning to it, this gives |E1089|.

*added-9.3*
Added ~
diff --git a/runtime/doc/vim9.txt b/runtime/doc/vim9.txt
index bf4d0087e..dd7fef9dd 100644
--- a/runtime/doc/vim9.txt
+++ b/runtime/doc/vim9.txt
@@ -1,4 +1,4 @@
-*vim9.txt* For Vim version 9.2. Last change: 2026 Sep 25
+*vim9.txt* For Vim version 9.2. Last change: 2026 Sep 28


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -3610,7 +3610,9 @@ or within a `:function`. The three rules are:
regardless of the function's parent context. Similarly, it gives E1268 in
the script-local scope of a Vim9 script.
3. In a legacy Vim script, "s:" is optional in a `:def` function, regardless
- of the function's parent context.
+ of the function's parent context. A `:def` function cannot create a
+ script-local variable, assigning to one that does not exist when the
+ function is compiled gives |E1089|.

The following three scripts demonstrate these rules:
>vim
diff --git a/runtime/menu.vim b/runtime/menu.vim
index 6a1d947da..7b0aaeb6c 100644
--- a/runtime/menu.vim
+++ b/runtime/menu.vim
@@ -2,7 +2,7 @@
" You can also use this as a start for your own set of menus.
"
" Maintainer: The Vim Project <https://github.com/vim/vim>
-" Last Change: 2026 Jan 19
+" Last Change: 2026 Sep 28
" Former Maintainer: Bram Moolenaar <Br...@vim.org>

" Note that ":an" (short for ":anoremenu") is often used to make a menu work
@@ -677,6 +677,8 @@ if !exists("no_buffers_menu")
" wait with building the menu until after loading 'session' files. Makes
" startup faster.
let s:bmenu_wait = 1
+let s:bmenu_short = 1
+let s:bmenu_count = 0

" Dictionary of buffer number to name. This helps prevent problems where a
" buffer as renamed and we didn't keep track of that.
@@ -1008,6 +1010,12 @@ cnoremenu <script> <silent> 1.100 PopUp.Select\ &All <C-U>call <SID>SelectAll()<
if has("spell")
" Spell suggestions in the popup menu. Note that this will slow down the
" appearance of the menu!
+ let s:suglist = []
+ let s:fromword = ''
+ let s:changeitem = ''
+ let s:additem = ''
+ let s:ignoreitem = ''
+
def s:SpellPopup()
if exists("s:changeitem") && s:changeitem != ''
call s:SpellDel()
diff --git a/src/structs.h b/src/structs.h
index 613a9ce47..5a0cc2917 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -2083,7 +2083,6 @@ struct ufunc_S
sctx_T uf_script_ctx; // SCTX where function was defined,
// used for s: variables; sc_version changed
// for :function
- int uf_script_ctx_version; // original sc_version of SCTX
int uf_refcount; // reference count, see func_name_refcount()

funccall_T *uf_scoped; // l: local variables for closure
diff --git a/src/testdir/test_source.vim b/src/testdir/test_source.vim
index 554c46be8..07084fa0e 100644
--- a/src/testdir/test_source.vim
+++ b/src/testdir/test_source.vim
@@ -859,6 +859,19 @@ func Test_source_dryrun()
call assert_true(exists('*DryrunLegacy'))
delfunc DryrunLegacy

+ " a :def function in a legacy script can assign to a script variable
+ " with "s:", although the ":let" that creates it is not executed
+ let lines =<< trim END
+ let s:count = 0
+ def DryrunLegacyDef()
+ s:count += 1
+ enddef
+ END
+ call writefile(lines, 'Xdryrun_legacydef.vim', 'D')
+ source ++dryrun Xdryrun_legacydef.vim
+ call assert_true(exists('*DryrunLegacyDef'))
+ delfunc DryrunLegacyDef
+
" no SourceCmd, SourcePre or SourcePost autocommand, also not for the
" imported script
let g:dryrun_events = ''
diff --git a/src/testdir/test_vim9_assign.vim b/src/testdir/test_vim9_assign.vim
index 287299c2d..4f2c391e0 100644
--- a/src/testdir/test_vim9_assign.vim
+++ b/src/testdir/test_vim9_assign.vim
@@ -2682,6 +2682,8 @@ def Test_cannot_use_let()
v9.CheckDefAndScriptFailure(['let a = 34'], 'E1126:', 1)
enddef

+let s:somevar = ''
+
def Test_unlet()
g:somevar = 'yes'
assert_true(exists('g:somevar'))
@@ -3141,6 +3143,15 @@ def Test_using_s_var_in_function()
call assert_equal(456, s:scriptlevel)
END
v9.CheckScriptSuccess(lines)
+
+ # but a :def function cannot create one
+ lines =<< trim END
+ def s:SomeFunc()
+ s:scriptlevel = 456
+ enddef
+ call s:SomeFunc()
+ END
+ v9.CheckScriptFailure(lines, 'E1089: Unknown variable: s:scriptlevel', 1)
enddef

" Test for specifying a type in assignment
diff --git a/src/userfunc.c b/src/userfunc.c
index bd1947adf..b6a4d2f8d 100644
--- a/src/userfunc.c
+++ b/src/userfunc.c
@@ -1624,7 +1624,6 @@ lambda_function_body(
if (!ASCII_ISUPPER(*ufunc->uf_name))
ufunc->uf_flags |= FC_VIM9;
ufunc->uf_script_ctx = current_sctx;
- ufunc->uf_script_ctx_version = current_sctx.sc_version;
ufunc->uf_script_ctx.sc_lnum += sourcing_lnum_top;
set_function_type(ufunc);

@@ -5739,7 +5738,6 @@ define_function(
fp->uf_calls = 0;
fp->uf_cleared = FALSE;
fp->uf_script_ctx = current_sctx;
- fp->uf_script_ctx_version = current_sctx.sc_version;
fp->uf_script_ctx.sc_lnum += sourcing_lnum_top;
if (is_export)
{
diff --git a/src/version.c b/src/version.c
index f67c2bd9c..bbfa29c5f 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 */
+/**/
+ 1140,
/**/
1139,
/**/
diff --git a/src/vim9compile.c b/src/vim9compile.c
index d36e11eef..0152a575b 100644
--- a/src/vim9compile.c
+++ b/src/vim9compile.c
@@ -1905,9 +1905,9 @@ compile_lhs_script_var(
lhs->lhs_name);
return FAIL;
}
- else if (cctx->ctx_ufunc->uf_script_ctx_version == SCRIPT_VERSION_VIM9
- && script_namespace
- && !script_var && import == NULL)
+ // With ":source ++dryrun" the ":let" that creates it was not executed.
+ else if (script_namespace && !script_var && import == NULL
+ && !source_dryrun)
{
semsg(_(e_unknown_variable_str), lhs->lhs_name);
return FAIL;
Reply all
Reply to author
Forward
0 new messages