vim9.txt revisions, standardizing SS1,3-7 by kennypete · Pull Request #20706 · vim/vim · GitHub
Skip to content

vim9.txt revisions, standardizing SS1,3-7 - #20706

Closed
kennypete wants to merge 6 commits into
vim:masterfrom
kennypete:vim9.txt-SS134567-tidy
Closed

kennypete wants to merge 6 commits into
vim:masterfrom
kennypete:vim9.txt-SS134567-tidy

Conversation

@kennypete

Copy link
Copy Markdown
Contributor

vim9.txt: conclusion to the rewrite and enhancements - part 1 of 2

In this “part 1”, several global changes and improvements are made to vim9.txt (except for sections 2 and 8, which will be addressed in “part 2”). “Part 2” is written already, but is large. So it is better separated from this PR.

The changes are:

  • Minimisation of spacing with two-space indents for the scripts, more use of tabs, etc., and reflect the discussion at PR19298.
  • Where is makes sense, tags are indented consistently with seven tabs.
  • Vim9 script code blocks have >vim9 on the blank line that separates the text from the code block. (This makes maintenance easier too with fewer lines getting wrapped unnecessarily when using gq.)
  • The Notes: blocks are reformatted. They now have no indent, as discussed and agreed in PR19250.
  • Line length is kept to 78 characters (displayed width, excluding concealed characters). Therefore, lines like 3464, which is only 72 display characters' width is okay (despite having 80 characters if the concealed | and ` characters are counted).

Locations of changes are indicated by reference to *tag* in the updated file.

Introduction

3. New style functions

  • “If the script the :def function is defined in is a Vim9...”: The paragraph and example are extended and qualified to cover exists_compiled().
  • The following example has the “unus” and “duo” changed to “one” and “two” to match the re-work of the prior example and now demonstrates the exists() versus exists_compiled() difference.

4. Types

*tuple-type*

  • The initial five examples are made sourceable.

*variadic-tuple*

  • The second sentence now demonstrates E1539 specifically and, like the initial five examples, is re-worked to be tuples “t6” to “t8”, and is sourceable.

*vim9-func-declaration*

  • Tags *E1005* and *E1007* are moved to the end of the passage on the func type as they are specific func related errors.

  • Regarding the func list in the current help:

    • It was the only type where examples are not provided. (I’d thought about it when updating Section 4 last year, but skipped past it at the time.) It needs examples because they are not all obvious. Using a similar function-func pattern makes them relatively short (<=7 lines each).
    • The current final example, func({type}, ?{type}, ...list<{type}>): {type}, is sufficiently problematic to warrant it being omitted. Although it can “work” in some scenarios, the danger is that it is interpreted as meaning the optional parameter 2 always may be omitted when parameter 3 is provided. It (or more correctly, they, if the [: void] variant was also included) could be reverted, and included as the last examples:
      func({type}, ?{type}, ...list<{type}>)[: void]
      		- typed mandatory argument
      		- typed optional argument
      		- typed list for variable number of arguments
      		- does not return a value
      
      func({type}, ?{type}, ...list<{type}>): {type}
      		- typed mandatory argument
      		- typed optional argument
      		- typed list for variable number of arguments
      		- returns a typed value
    

    However, doing so would need to come with warnings regarding unintended consequences and errors. A mostly “working” example is this (echoing 9 for all output except the last line):

     vim9script
     def D(n: number, o: float = 0.1, ...l: list<number>): number
       return (n + o + max(sort(l, (x, y) => x + y)))->float2nr()
     enddef
     const F: func(number, ?float, ...list<number>): number = D->function()
     echo F(9)
     echo F(8, 1.0)
     echo F(7, 1.0, 0, 1)
     echo F(6, 2.0, 1)
     echo F(5, 3, 1)
     echo F(4, 5)
     echo F(8, 1)  # E1013
    

    The problem with this example is that, although it appears to be successfully skipping the float, that’s not what it’s doing. What is happening is that the number (3 in the third-to-last echo, then 5 in the penultimate echo) is being coerced to a float. This can be proven by adding an echo l[0] to D():

     vim9script
     def D(n: number, o: float = 0.1, ...l: list<number>): number
       echo $'({l[0]} is the first list item)'
       return (n + o + max(sort(l, (x, y) => x + y)))->float2nr()
     enddef
     const F: func(number, ?float, ...list<number>): number = D->function()
     echo F(1, 2.0, 1, 6)
     echo F(1, 2, 6)
    

    This shows that 6 is the first list item in the second echo, not 2, which it would be if the optional float was truly being skipped.
    It feels wrong to be documenting a questionable “way”: As this example shows, the optional parameter cannot actually be skipped. It is always filled positionally, either by coercion or error. So, it does not feel helpful suggesting this “way”.

    • Did not provide other “ways”. Now the following are included:

      func(?{type}): {type}

      func(...list<{type}>): {type}

      func({type}, ...list<{type}>)[: void]

      func({type}, ...list<{type}>): {type}
    • “If the return type is "void" the function does not return a value.”: This makes sense to retain, though it is benefited from listing the variations up front and using [: void] for the implicit/explicit indicator of no typed return value.

*E1005*

  • Tag *E1005* is distinctly addressed. It was not stated anywhere what this is relates to. Providing an example of, “No more than 19 argument types may be used...”, takes only a few lines to illustrate the point. A working, extreme example with precisely 19 arguments (for anyone who wants it 😀️) is:
     vim9script
     var X: func
     X = (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) => 0
     # The lambda X() can have up to 20 arguments (19 are shown here):
     echo X(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 19)
     # The Funcref F() can have up to 19 arguments:
     var F: func(any, any, any, any, any, any, any, any, any, any, any,
       \ any, any, any, any, any, any, any, any): any = X->function()
     echo F(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 19)
    

*E1007*

  • Tag *E1007* is treated separately too. It is another specific error related to Funcrefs - i.e., trying to use a mandatory argument after an optional argument. (Incidental: It has some parallels to the removed, problematic, func({type}, ?{type}, ...list<{type}>): {type}.)

5. Generic functions

This section receives the same global changes applied elsewhere (2-space body indent, standalone >vim9 syntax blocks, trailing-comment condensation, “Note:” treatment, etc.); there are no substantive content changes.

6. Namespace, Import and Export

*E1304*

  • The tag is moved above the paragraph, which is improved and extended, now noting the other error (E1016, with a hot-link), and two examples are provided to illustrate E1016 and E1304.

Export

*:export*

  • Tag *:exp* is removed because using that invalid shortened form gives E1065.
  • The list is made more consistent and the conflicting final someValue/const somevalue is removed. Also,function is added: legacy functions, not just :def functions, can be exported.

*E1043*

  • This tag now stands alone, is explained (the current help is silent), and a one-line script demonstrates it occurring.

*E1042*

  • This tag and its explanation are refined to note “:export can only be used in a Vim9 script scope.” This is more accurate than the current help, which says, “at the script level”, which is inaccurate. For example, this script will export a constant, and it is not at the script level:
     vim9script
     b:tmp = $'{tempname()}.vim'->substitute('\\', '/', 'g')
     ['vim9script',
     'export def D(): void',
     '  export const C = "C is exported!"',
     'enddef']->writefile(b:tmp)
     import b:tmp as imported
     imported.D()
     echo $"Exported constant 'C' in 'imported' ({b:tmp}):\n" .. imported.C
    

*E1044*

  • This tag now also stands alone, and an example is provided to demonstrate it occurring.

Import

*:import*

  • Tag *:imp* is relocated because, like tag *:exp*, it is not helpful implying :imp is allowed in Vim9 script (it gives E1065, which is “command cannot be shortened”). It is relocated to the *import-legacy* passage, thought, because it is permitted in legacy Vim script.
  • Error tags currently appearing along with *:import* are relocated to their own distinct places, along with explanations and examples.

*E1094*

  • This is addressed separately and early because it is a general importing error relating to the limitation of only being able to import from a script local scope. An example shows how E1094 is given if it is not (such as from a function-local scope).

*E1053* *E1071*

  • Similarly, these are general importing errors, now explained succinctly and with one-line examples.

*:import-as*

  • The tags *E1257* and *E1261* are relocated later with distinct explanations and examples.
  • The introductory paragraphs and non-sourceable examples are re-worked into comprehensive explanations and a self-contained example of using :export and :import-as. It is helpful because, from personal experience, trying to understand the nuances of exporting and importing is not easy using the current help. Hopefully, providing working examples and more detailed explanations will make it easier.

*E1047* to *E1262*

  • The 12 errors are now itemised with their own tags, explanations, and examples. Many were not clearly touched on, let alone explained, in the current help. Examples either import ccomplete.vim in Vim's $VIMRUNTIME path or temporarily write a Vim9 script to tempname(), then import it.

*import-map*

  • After the Note sentence, a self-contained example of using <ScriptCmd> is provided.

*import-legacy* *legacy-import* *:imp*

  • The tag :imp is moved here since the shortened form, :imp, is only valid in a legacy Vim script.
  • The sentence “And using the...” is merged with the following one which notes “the namespace cannot be resolved on its own”. A legacy Vim script sourceable example demonstrates both points.

7. Classes and interfaces

  • The second paragraph adds, “, |vim9class.txt|, though there are some examples in this help file such as at |vim9-class-type|, |vim9-enum-type|, and elsewhere.” The examples in vim9.txt are generally more detailed than those in vim9class.txt, so it is worth pointing users to them.

Signed-off-by: kennypete <64727695+kennypete@users.noreply.github.com>
@chrisbra

Copy link
Copy Markdown
Member

@kennypete

Copy link
Copy Markdown
Contributor Author

sorry for being slow, but this will take some time to review.

Totally fine, and I'm sorry for it being so large, though there's a lot of good stuff in there.

(And even more so', in advance, for the Section 2 PR once this prerequisite one gets over the line - Section 2 is where most of the gold is, with lots of fixes, useful scripts, etc., so it is a very big one.)

@chrisbra
chrisbra requested a review from Copilot July 22, 2026 20:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

This PR finalizes “part 1” of a large rewrite of vim9.txt, focusing on formatting standardization and adding/improving sourceable examples and error-tag documentation (notably for function types, tuples, and import/export).

Changes:

  • Standardizes help formatting (indentation, tabs, >vim9 block placement, and Note(s): formatting) across sections 1, 3–7.
  • Expands and clarifies documentation with new/adjusted sourceable examples (e.g., exists_compiled(), tuple/variadic tuple, Funcref types, and several import/export errors).
  • Adds an external “short primer” reference link and reorganizes error tags to be more discoverable.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread runtime/doc/vim9.txt
Comment on lines 1652 to +1658
var flist: list<func>
def ClosureEg(n: number): void
var outloop: number = 0 # outloop is declared outside the loop!
for i in range(n)
outloop = i
flist[i] = (): number => outloop # Closures ref the same var
endfor
echo range(n)->map((i, _) => flist[i]())
var outloop: number = 0 # outloop is declared outside the loop!
for i in range(n)
outloop = i
flist[i] = (): number => outloop # Closures ref the same var
endfor

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works as explained in the preceding paragraph, which is shown in the screenshot, below. flist[i] = ... inside for i in range(n) assigns at index i, which is exactly the list's current length at that point in the loop (0, then 1, and so on). It would only error if the index exceeded the current length by one or more, so CoPilot's claim appears to be off the mark in this "High" (confidence) comment, and its "likely not sourceable as claimed" assertion is demonstrably incorrect.

image

Comment thread runtime/doc/vim9.txt
Comment on lines 1670 to +1675
var flist: list<func>
def ClosureEg(n: number): void
for i in range(n)
var inloop: number = i # inloop is declared inside the loop
flist[i] = (): number => inloop # Closures ref each inloop
endfor
echo range(n)->map((i, _) => flist[i]())
for i in range(n)
var inloop: number = i # inloop is declared inside the loop
flist[i] = (): number => inloop # Closures ref each inloop
endfor

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, this works as explained in the paragraph preceding the script. So, this "same issue as the earlier closure example" appears to be another red herring. CoPilot's claim is off the mark in this "High" comment: its "likely to fail when sourced" assertion is demonstrably incorrect, as shown in the following screenshot of the script, echoing [0, 1, 2, 3], including the explanation too.

image

Comment thread runtime/doc/vim9.txt
Comment on lines 2144 to +2146
const FAILS: func = (): string => {
echo $"{Ln}" # E1105: Cannot convert typealias to string
}
<
*vim9-class-type* *vim9-interface-type*
echo $"{Ln}" # E1105: Cannot convert typealias to string
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gives E1105, as intended, when sourced. That is proven by the screenshot, below. Note the error text: "Error detected while compiling ...function ", triggered by the lambda's compilation when assigned to FAILS. Copilot's claim that non-invocation means E1105 can't be demonstrated is incorrect. The concern about a missing return statement is not what happens in practice.

image

Comment thread runtime/doc/vim9.txt Outdated
Comment on lines +3408 to +3411
def F1094(): void
import 'nah.vim'
enddef
F1094() # E1094: Import can only be used in a script

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F1094() (or defcompile) is required to force attempted, failing compilation, which is the point. Agreed, the comment should be on line 3409, not 3411, however, so this will be updated to:

	vim9script
	def F1094(): void
	  import 'nah.vim'  # E1094: Import can only be used in a script
	enddef
	F1094()

Comment thread runtime/doc/vim9.txt Outdated
vim9script
def F_1073()
enddef
def F_1073() # E1073: Name already defined: <SNR>...
def F_1073() # E1073: Name already defined: <SNR>F…

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All ellipsis characters used for truncation will be reverted to ... as discussed at #20706 (comment) (noting a few comments are a little less helpful when characters in cols 76 and 77 become ..).

@chrisbra chrisbra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I reviewed most of it and found a few more or less minor issues.

Comment thread runtime/doc/vim9.txt Outdated
vim9script
def F_1073()
enddef
def F_1073() # E1073: Name already defined: <SNR>...
def F_1073() # E1073: Name already defined: <SNR>F…

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep the non utf8 version of the ellipsis.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, sure. The reason for them in # comments was to reduce truncation. I will back all 50 of them out (and do the same in the upcoming Section 2), though noting in a few instances they may make the comment a little less helpful. Not a big deal either way.

Comment thread runtime/doc/vim9.txt Outdated
vim9script
def F_1123(a: number, b: number): void
echo max(a b)
# E1123: Missing comma before argument: b)
echo max(a b) # E1123: Missing comma before argu…

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the 50, which will be reverted/amended to ... (#20706 (comment)).

Comment thread runtime/doc/vim9.txt Outdated
enddef
F_1027()
< >vim9
vim9script
def F_1096(): void
return false # E1096: Returning a value ...
return false # E1096: Returning a value in a fun…

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the 50, which will be reverted/amended to ... (#20706 (comment)).

Comment thread runtime/doc/vim9.txt Outdated
enddef
def F_1059() : bool
# E1059: No white space allowed before colon:...
# E1059: No white space allowed before colon: : bo…

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the 50, which will be reverted/amended to ... (#20706 (comment)).

Comment thread runtime/doc/vim9.txt Outdated
To recognize a file that can be imported the `vim9script` command must appear
as the first line in the file, however, see |vim9-mix| for an exception. It
tells Vim to interpret the script in its own namespace, instead of the global
namespace. If a file starts with: >

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
namespace. If a file starts with: >
namespace. If a file starts with:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unnecessary > will be deleted when re-pushed.

Comment thread runtime/doc/vim9.txt Outdated
" ERROR: E1060: Expected dot after name: s:that
When using the "as name" form, the namespace cannot be resolved on its own
(see also |E1060|). This example demonstrates using `:imp` with `as` successfully,
then the error: >

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
then the error: >
then the error:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unnecessary > will be deleted when re-pushed.

Comment thread runtime/doc/vim9.txt Outdated
It tells Vim to interpret the script in its own namespace, instead of the
global namespace. If a file starts with: >
To recognize a file that can be imported the `vim9script` command must appear
as the first line in the file, however, see |vim9-mix| for an exception. It

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think statement was fine, or make it explicit, that comments are allowed before

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed "statement" to "command" for consistency with :h vim9script which uses "command" (like everywhere else = six more places). I take your point about comments, though.

More important is what follows, I think. On re-reading it now, it should be clearer. Here's the "To recognize..." part addressed, and what follows:

To recognize a file that can be imported, the `vim9script` command must appear
as the first command in the file (though see |vim9-mix| for an exception).
It tells Vim to interpret the script in its own namespace, instead of the
global namespace.  Consider this script:
>vim9
	vim9script
	var myvar = 'yes'
<
The variable "myvar" will only exist in this script's scope.  That is
different from legacy Vim script where "let myvar" would make "myvar"
available to other scripts and functions (as `g:myvar`).

Comment thread runtime/doc/vim9.txt
export class MyClass ...
export interface MyClass ...
export interface MyInterface ...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing abstract and type?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed below at #20706 (comment)

Comment thread runtime/doc/vim9.txt Outdated

< *E1043*
As this suggests, constants, variables, functions, classes, interfaces,
and enums can be exported. Trying to export something else gives E1043:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should add abstract classes and types

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point - I'll update it to:

Exporting an item can be written as: >
	export var myvar ...
	export const MYCONST ...
	export final myvar ...
	export def MyDef() ...
	export function MyFunc() ...
	export class MyClass ...
	export abstract class MyAbstractClass ...
	export interface MyInterface ...
	export enum MyEnum ...
	export type MyType ...
<							*E1043*
As this suggests, variables, constants, functions, classes (including abstract
classes), interfaces, enums, and types can be exported.  Trying to export
something else gives E1043:

…o exportables

Signed-off-by: Peter Kenny <64727695+kennypete@users.noreply.github.com>
@chrisbra

Copy link
Copy Markdown
Member

Thanks for the update. Since most the changes where just formatting/cleanup things, i'll just await feedback for a few more days and merge it then.

Comment thread runtime/doc/vim9.txt Outdated
the former, so, this is okay: >vim9

the former, so, this is okay:
>vim9
vim9cmd echo [1, 2]->extend(['3']) # [1, 2, 3]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will return [1, 2, '3'] rather than [1, 2, 3].

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good spot - I'll fix that with the (hopefully) final commit for this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you going to update this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was/am going to, though have not had any feedback on my proposed changes addressing the "s:" passage and had been waiting for that (and had been a bit busy with other things anyway). Those are not just formatting, though I'm confident they improve and address the points, so I'm good to close this out, if those are considered okay, in the next day or two.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think no comments means everybody is fine with the proposed changes :) So yes please finish this up if you can and then I'll merge the doc changes. Thanks 🙏
If you are too busy, just let me know, and I'll merge the doc changes as is. It's not like those are set in stone :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think no comments means everybody is fine with the proposed changes :) So yes please finish this up if you can and then I'll merge the doc changes. Thanks 🙏 If you are too busy, just let me know, and I'll merge the doc changes as is. It's not like those are set in stone :)

Sure thing, I will make the changes in the next day or two. Not "set in stone" indeed. 😄

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... I'll merge the doc changes as is. It's not like those are set in stone :)

It should be there now, I think.

I'll then get back to finalising section 2, though it will be a few days more after this now because there are a few additions to the current section 2 within the last month or so that need consideration in that re-write.

Comment thread runtime/doc/vim9.txt
at runtime, cannot be used conditionally to skip undeclared variables, though
|exists_compiled()|, which is evaluated at compile time, may be used.
For example:
>vim9

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We better pay attention to the immediate language context as
well and exercise care when using :legacy etc. or function
declaration nesting:

deferred.vim
vim9script

def DeferredLocal1(): number
    function FetchLocal()
        return exists("s:local") ? s:local + 0 : 42
    endfunction
    return FetchLocal()
enddef

def DeferredLocal2(): number
    legacy return exists("s:local") ? s:local + 0 : 42
enddef

function DeferredLocal3()
    def FetchLocal(): any
        return eval("local")
    enddef
    return exists("s:local") ? FetchLocal() + 0 : 42
endfunction

defcompile

if 1
    var local: string = "1"
    echo DeferredLocal1() + 2
    echo DeferredLocal2() + 2
    echo DeferredLocal3() + 2
else
    echo DeferredLocal1()
    echo DeferredLocal2()
    echo DeferredLocal3()
endif

Confounding the language context of the source file itself
with something along the lines of:

In Vim9 context, script-local variables cannot be prefixed by "s:"; at compile
time, all their references must be either resolved or conditionally skipped
with |exists_compiled()|.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I agree, there are context factors that make this tricky, especially when trying to explain it all without making it too complicated.

Having now:

  1. Considered your examples, and
  2. Tested all combinations of script-local, :def, :function, and (one-layer only) nested :def/:function combinations in both Vim9 script and legacy Vim script,

I think the two passages (and their code examples) at the paragraphs If the script the :def function is defined in is [Vim 9 script/legacy Vim script], would be better rewritten as:

							*vim9-s:var*
When referencing a script-local variable, the context determines whether using
the "s:" prefix is either mandatory, optional, or gives |E1268|.  The context
factors are whether the script version is Vim9 script or legacy Vim script and
whether the reference is at the script-local level, within a `:def` function,
or within a `:function`.  The three rules are:

 1. In a `:function`, "s:" is always mandatory.  This is regardless of the
    script type or the function's parent context (such as nested within
    another function).  Similarly, "s:" is also mandatory in the script-local
    scope of a legacy Vim script.
 2. In a Vim9 script, "s:" always gives |E1268| when used in a `:def` function,
    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.

The following three scripts demonstrate these rules:
>vim
	" 1. In a :function, "s:" is always mandatory.  It is also mandatory
	" in a legacy Vim script's script-local scope
	let s:MyVar = v:true
	echo s:MyVar		| " v:true
	" echo MyVar		  (Would give E121: Undefined variable: MyVar)
	vim9cmd echo MyVar	  # true
	function! MyFunc()
	  echo s:MyVar		| " v:true
	  " echo MyVar		  (Would give E121: Undefined variable: MyVar)
	  vim9cmd echo MyVar	  # true
	endfunction
	call MyFunc()
< >vim9
	vim9script
	# 2. In a Vim9 script, "s:" gives E1268 when used in any :def function
	# and in the script-local scope
	var MyVar: bool = true
	echo MyVar		  # true
	# echo s:MyVar		  (Would give E1268: Cannot use s: in Vim9...)
	legacy echo s:MyVar	| # v:true
	def MyFunc()
	  echo MyVar		# true
	  # echo s:MyVar	  (Would give E1268: Cannot use s: in Vim9...)
	  legacy echo s:MyVar	| # v:true
	enddef
	MyFunc()
< >vim
	" 3. In a legacy Vim script, "s:" is optional in a :def function
	let s:MyVar = v:true
	function! Outer()
	  def! MyFunc()
	    echo MyVar		# true
	    echo s:MyVar	# true
	  enddef
	  call MyFunc()
	endfunction
	call Outer()
<
Using |exists()|, which is evaluated at runtime, cannot be used conditionally
to skip undeclared variables, though |exists_compiled()|, which is evaluated at
compile time, may be used.  For example:
>vim9
	vim9script
	def MyDef()
	  if exists_compiled('MyVar')	# evaluated at compile time
	    echo $"MyVar = {MyVar}"	# MyVar = 1
	  endif
	  if exists_compiled('MyVar2')	# evaluated at compile time
	    echo $"MyVar2 = {MyVar2}"	# not reached
	  else
	    echo "MyVar2 does not exist at compile time"
	  endif
	  if exists('MyVar')		# evaluated at runtime
	    echo $"MyVar = {MyVar}"	# MyVar = 1
	  endif
	  if exists('MyVar2')		# evaluated at runtime
	    # The following would give E1001: Variable not found: MyVar2
	    # echo MyVar2
	  else
	    echo "MyVar2 does not exist at runtime"
	  endif
	enddef
	var MyVar: number = 1	# Declared before MyDef() is compiled
	MyDef()
	var MyVar2: number = 2	# Declared after MyDef() is compiled
<

This addresses all scenarios and, specifically, your:

  • DeferredLocal1() scenario is addressed by script 1,
  • DeferredLocal2()'s use of :legacy by script 2, and
  • DeferredLocal3()'s eval() isn't specifically addressed by scripts 1-3 because it does not use either local or s:local directly. However, the new passage's separated exists()/exists_compiled() example covers your closing point. (Previously, the exists()/exists_compiled() distinction was interwoven with the s: material/examples.)

I think this *vim9-s:var* passage would improve on what was already an improved passage about s:. Before the prior PR, it simply said s: was not allowed, which was an insufficient generalisation. The complete picture is provided now, with the interesting nuance of mandatory/optional/prohibited s: scenarios and the three corroborating scripts.

Comment thread runtime/doc/vim9.txt Outdated
Comment on lines 1490 to 1491
If the script the `:def` function is defined in is legacy Vim script,
script-local variables may be accessed with or without the "s:" prefix.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking the above example script and dropping vim9script and
experimenting with s:-prefix optionality presents a similar
necessity in tracking the immediate language context.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is covered above. The three "rules" were based off these findings:

image

(I could provide the corroborating scripts for this, if they're wanted, but have left them out for now.)

@dkearns

dkearns commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

@kennypete could you please also update the reference to :exp[ort] in index.txt?

Signed-off-by: Peter Kenny <64727695+kennypete@users.noreply.github.com>
@kennypete

Copy link
Copy Markdown
Contributor Author

@kennypete could you please also update the reference to :exp[ort] in index.txt?

Sure, though would be better with the section 2 update, I think, which will be very soon after this (mostly) tidying one's merged.

@chrisbra

chrisbra commented Aug 21, 2026

Copy link
Copy Markdown
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants