Conversation
Signed-off-by: kennypete <64727695+kennypete@users.noreply.github.com>
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.) |
There was a problem hiding this comment.
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,
>vim9block placement, andNote(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.
| 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 |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| def F1094(): void | ||
| import 'nah.vim' | ||
| enddef | ||
| F1094() # E1094: Import can only be used in a script |
There was a problem hiding this comment.
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()
| vim9script | ||
| def F_1073() | ||
| enddef | ||
| def F_1073() # E1073: Name already defined: <SNR>... | ||
| def F_1073() # E1073: Name already defined: <SNR>F… |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
I think I reviewed most of it and found a few more or less minor issues.
| vim9script | ||
| def F_1073() | ||
| enddef | ||
| def F_1073() # E1073: Name already defined: <SNR>... | ||
| def F_1073() # E1073: Name already defined: <SNR>F… |
There was a problem hiding this comment.
I think we should keep the non utf8 version of the ellipsis.
There was a problem hiding this comment.
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.
| 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… |
There was a problem hiding this comment.
One of the 50, which will be reverted/amended to ... (#20706 (comment)).
| enddef | ||
| F_1027() | ||
| < >vim9 | ||
| vim9script | ||
| def F_1096(): void | ||
| return false # E1096: Returning a value ... | ||
| return false # E1096: Returning a value in a fun… |
There was a problem hiding this comment.
One of the 50, which will be reverted/amended to ... (#20706 (comment)).
| enddef | ||
| def F_1059() : bool | ||
| # E1059: No white space allowed before colon:... | ||
| # E1059: No white space allowed before colon: : bo… |
There was a problem hiding this comment.
One of the 50, which will be reverted/amended to ... (#20706 (comment)).
| 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: > |
There was a problem hiding this comment.
| namespace. If a file starts with: > | |
| namespace. If a file starts with: |
There was a problem hiding this comment.
The unnecessary > will be deleted when re-pushed.
| " 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: > |
There was a problem hiding this comment.
| then the error: > | |
| then the error: |
There was a problem hiding this comment.
The unnecessary > will be deleted when re-pushed.
| 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 |
There was a problem hiding this comment.
I think statement was fine, or make it explicit, that comments are allowed before
There was a problem hiding this comment.
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`).
| export class MyClass ... | ||
| export interface MyClass ... | ||
| export interface MyInterface ... |
|
|
||
| < *E1043* | ||
| As this suggests, constants, variables, functions, classes, interfaces, | ||
| and enums can be exported. Trying to export something else gives E1043: |
There was a problem hiding this comment.
should add abstract classes and types
There was a problem hiding this comment.
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>
|
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. |
| the former, so, this is okay: >vim9 | ||
|
|
||
| the former, so, this is okay: | ||
| >vim9 | ||
| vim9cmd echo [1, 2]->extend(['3']) # [1, 2, 3] |
There was a problem hiding this comment.
This will return [1, 2, '3'] rather than [1, 2, 3].
There was a problem hiding this comment.
Thanks, good spot - I'll fix that with the (hopefully) final commit for this.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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. 😄
There was a problem hiding this comment.
... 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.
| 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 |
There was a problem hiding this comment.
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()
endifConfounding 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()|.
There was a problem hiding this comment.
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:
- Considered your examples, and
- Tested all combinations of script-local,
:def,:function, and (one-layer only) nested:def/:functioncombinations 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:legacyby script 2, andDeferredLocal3()'seval()isn't specifically addressed by scripts 1-3 because it does not use eitherlocalors:localdirectly. However, the new passage's separatedexists()/exists_compiled()example covers your closing point. (Previously, theexists()/exists_compiled()distinction was interwoven with thes: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.
| 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. |
There was a problem hiding this comment.
Taking the above example script and dropping vim9script and
experimenting with s:-prefix optionality presents a similar
necessity in tracking the immediate language context.
There was a problem hiding this comment.
This is covered above. The three "rules" were based off these findings:
(I could provide the corroborating scripts for this, if they're wanted, but have left them out for now.)
|
@kennypete could you please also update the reference to |
Signed-off-by: Peter Kenny <64727695+kennypete@users.noreply.github.com>
Sure, though would be better with the section 2 update, I think, which will be very soon after this (mostly) tidying one's merged. |

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:
>vim9on the blank line that separates the text from the code block. (This makes maintenance easier too with fewer lines getting wrapped unnecessarily when usinggq.)Notes:blocks are reformatted. They now have no indent, as discussed and agreed in PR19250.Locations of changes are indicated by reference to *tag* in the updated file.
Introduction
vim9.txtbeing a very large, comprehensive help file, shorter introductions probably are worth referencing. This aligns with other references, e.g., https://github.com/lacygoill/wiki/blob/master/vim/vim9.md, which at one point appeared in several places.3. New style functions
:deffunction is defined in is a Vim9...”: The paragraph and example are extended and qualified to coverexists_compiled().exists()versusexists_compiled()difference.4. Types
*tuple-type*
*variadic-tuple*
*vim9-func-declaration*
Tags
*E1005*and*E1007*are moved to the end of the passage on thefunctype as they are specificfuncrelated errors.Regarding the
funclist in the current help: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:However, doing so would need to come with warnings regarding unintended consequences and errors. A mostly “working” example is this (echoing
9for all output except the last line):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]toD():This shows that
6is the first list item in the secondecho, not2, 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”.
func(?{type}): {type}func(...list<{type}>): {type}func({type}, ...list<{type}>)[: void]func({type}, ...list<{type}>): {type}[: void]for the implicit/explicit indicator of no typed return value.*E1005*
*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:*E1007*
*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
>vim9syntax blocks, trailing-comment condensation, “Note:” treatment, etc.); there are no substantive content changes.6. Namespace, Import and Export
*E1304*
Export
*:export*
*:exp*is removed because using that invalid shortened form gives E1065.final someValue/const somevalueis removed. Also,functionis added: legacy functions, not just:deffunctions, can be exported.*E1043*
*E1042*
:exportcan 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:*E1044*
Import
*:import*
*:imp*is relocated because, like tag*:exp*, it is not helpful implying:impis 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.*:import*are relocated to their own distinct places, along with explanations and examples.*E1094*
*E1053* *E1071*
*:import-as*
*E1257*and*E1261*are relocated later with distinct explanations and examples.:exportand: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*
ccomplete.vimin Vim's$VIMRUNTIMEpath or temporarily write a Vim9 script totempname(), then import it.*import-map*
Notesentence, a self-contained example of using<ScriptCmd>is provided.*import-legacy* *legacy-import* *:imp*
:impis moved here since the shortened form,:imp, is only valid in a legacy Vim script.7. Classes and interfaces
vim9.txtare generally more detailed than those invim9class.txt, so it is worth pointing users to them.