Repository navigation
V3 answers, one shared check per edit, renames that check the new name, and generic constraints in the editor - #525
Conversation
Line info and diagnostics:
- V3 answers hover, definition, signature help, completion and inlay hints
(v3_line_info.v): through the `query` requests of the diagnostics server of
the V in use, or in a V3 process of its own where it runs none. It answers
in a copy of the program kept between requests, where each request writes
only the files that changed. V1 answers what V3 cannot, such as a file that
does not parse yet. Completion writes a placeholder name after the dot for
V3. The lookups that references, rename and document highlights make one
after another are asked in one check per program.
- Diagnostics come from the diagnostics server of the V in use when it runs
one (diagnostics_server.v): builtin is parsed once, a newer check stops an
older one, and each check keeps the same paths.
- A file is checked as part of its program: a module from the program that
imports it, a module that no program imports as a library (`-shared`), and
the check answers for every open file of that program.
- Inlay hints come from the compiler (`ih^`), cached per buffer.
Editing:
- Rename (F2) edits every occurrence of the symbol under the cursor, which
the compiler's go-to-definition confirms, or refuses (rename.v).
- Completion offers the modules a file can import, from the project, the
installed packages and vlib, and adds the `import` line (module_imports.v).
- Completion offers the builtin functions, `dump` included.
- Hover documents the members V gives types (`from`, `zero`, `has`, `str`...).
- Semantic tokens: an unbraced `$name` is text, as V interpolates only `${}`.
The README tells how to work on code behind a compile-time flag (VFLAGS).
The rename tests asked V1 alone, while VLS asks V3 first and V1 only what V3 does not answer. Their test app now does the same when the V in use has a V3 that answers `-line-info`, and ends the V3 servers it started. Two of them need an answer V1 does not give: where a method reached through an embedded struct, or a name inside a generic function that another file calls, is declared. Without such a V3 they are skipped. Also writes a test constant with single quotes, as vfmt wants.
V needs no declaration to implement an interface, so the types that implement one must keep the names of its members, and a rename cannot see which types those are. With V3 answering where an interface method is declared, a rename of `area` changed the interface and its calls but not `Circle.area`, and the program no longer compiled. Before, it was refused only because nothing answered there. A rename of a member of an interface of the project, or of a method or a field with the name of one, or of `msg` and `code`, the methods of `IError`, is now refused, as the other renames that could break the program are. Also tests a field of an embedded struct reached through the struct that embeds it, declared in another file: from its declaration a rename left `user.id` behind while V3 did not follow the embedding.
V builds a test file with the files of its module, as a program of its own, and the program of the directory, `.`, leaves it out: V3 had no answer for a position in a `_test.v` file, and a rename could not verify its occurrences there. The questions about a test file are now asked with that file as the program, each in its own check (and its own diagnostics server); the other questions keep asking `.` together.
A rename asked V3 again where the name at the cursor is declared, right after prepareRename had asked it, with the files as they were: about a second of each rename. What a rename finds out is now kept for the next one while no file changes; a question that got no answer is asked again, as the compiler may have failed for a moment. The server of a test file, a program of its own, no longer pushes out the server of the program of the directory, `.`, which every other question goes to. And a server that does not end when told to, as one whose child hangs, is killed after a second: waiting for it froze VLS. On the code of VLS, the three renames measured take 1.3 s, 2.8 s and 6.1 s, from 2.5 s, 3.9 s and 7.5 s.
The copy V3 is asked about links the project's files it was not asked to write: by a symbolic link to the file, by a hard link to a file of an imported module, or by a symbolic link to a whole directory. A file opened after the copy was built was written through such a link: its buffer, not saved yet, or a completion's placeholder, overwrote the file in the project. Only a symbolic link to the file itself was replaced. A write now makes the directories above the file the copy's own (a linked directory becomes one whose entries link the project's), removes the file's link, and writes a new file. An empty buffer is written too: a file the copy never wrote no longer passes for one that already holds it.
The files the persistent compilers check lived in one directory for the whole VLS process. Over TCP, VLS serves each client with an App of its own, with its own buffers, servers and record of what it wrote into a copy: two clients editing one project shared the copy of it, so one could rebuild or overwrite what the other was checking, and the other, finding its own buffer recorded as written, asked about the first one's text. Stopping the servers of either client removed the copies of all of them. Two programs of one v.mod project shared a copy in the same way within one App. Each pool of servers now keeps its files in a directory of its own, and removes only that one when it stops; a program's questions go to a copy of that program alone. A session that ends without a shutdown stops its compilers and removes its files as well.
The copy V3 is asked about holds the files of the modules that the program imports from the project by hard links, which show a file written in place but not one replaced by another: a checkout does that, and so does an editor that saves by renaming a new file over the old one. No client needs to say so: the copy went on holding the old file, and what V3 answered about that module came from it. Before each question the copy now compares each file it holds by a hard link, or as a copy where no link could be made, with the project's (inode, size and time of the last change), takes again one that changed, and removes one that is gone.
A hover shows the declaration as the source writes it, and of one without a body it took the first line only: `pub type Number = int`, followed in the source by `| i8`, `| i16` and more on lines of their own, as vfmt writes a long sum type, showed `int` as its only variant. The lines after such a declaration that start with `|` are now part of it: the variants of a sum type, or the operands of an `|` written over several lines.
A hover on the name of a function, a method or a method of an interface showed its documentation alone where it is declared. Where it is used it showed the compiler's re-print of its signature, without its receiver, its type parameters or the type of a static method, or only a documentation found for its name, which was often another declaration's: the type `map` for the method `map` of an array, the `first` of arrays for a function named `first`, the `to_upper` of a rune for that of a string. VLS now asks V3 in one question where the name is declared and what it says of it. A function, a method or a method of an interface shows its declaration as the source writes it, with the documentation written above it, the same at its declaration and at each use: `fn (b Box[T]) label() string`, or `pub fn (a array) map(callback fn (voidptr) voidptr) array` with builtin's documentation of it. Anything else keeps what the compiler says of it, with the documentation of its own declaration; a local or a parameter gets none. Without V3, the index says where a function is declared. On the way: - the index finds a generic function by its name: it named its symbol `first[T]`, which no use matches; - a declaration without a body, as builtin declares the methods of arrays, ends with its line; - a member of a value whose declaration is not found gets no documentation found for its name alone, and the methods of arrays and maps are looked up on builtin's `array` and `map`; - a local shows at its declaration what it shows at its uses, and a method named without a call, `g := u.greet`, is a function value, `fn () string`.
…entation A local that nothing gave a type showed the statement that declares it, as `lengths := xs.map(it.name.len)`, or the documentation of another declaration of its name: a local named `value` showed "for cgen", the documentation of the method `value` of builtin's `DenseArray`. Where V3 says a name is a local or a parameter and knows no type for it, the hover is empty: nothing else can tell what it holds. The declaration of a local is never shown as its hover, and a local gets no documentation found for its name. A name used alone is never a method either: the search of the documentation by name, in a file and in the index, skips methods, which are documented where the type of their value is known.
A local `a` passed to a user's `s.sort(a)` is that local, not the `a` of an array's comparator: the rename of `a := 7` must change that use, and leave the `a` of `sorted.sort(a < b)` alone. It asks V3 where each occurrence is declared, so it needs a V3 that tells them apart.
vlib documents most of its examples as `// Example: <code>`, which `v doc` shows as V code, while the hover showed them as text. Consecutive ones now share a ```v block under `Example:` or `Examples:`, and the lines of a ``` block keep their indentation.
The pool held its lock for the whole request, so a hover or a completion waited for a check of the program running on another server. Each server now has a lock of its own, and the pool's covers only its map; a server in use is never stopped: the pool drops it, and the last request to leave stops it.
A check waited 25 ms after each change and the worker slept 5 ms before looking for it: on a small program that was a quarter of the time to its diagnostics. A newer change already stops the check that runs, and a question no longer waits for a check, so the check starts at once. The tests that look at the jobs a change leaves pause the scheduler, which a check started at once would otherwise empty.
After a change an editor asks at once for the semantic tokens and for the highlights, the hover or the inlay hints at the cursor. The loop answered them in order, so the tokens, which come from the text alone, waited for a question to the compiler, which can take a whole check: 100 ms on a small program, 490 ms on a large one. The messages are now read on a thread of their own, and a request that never asks the compiler goes before those that may, among the requests that arrived together; no request moves past a notification or any other request.
A V3 diagnostics server started with V_DIAGNOSTICS_PREPARE parses and collects builtin and the modules it imports once, before its first check, and each check continues from there. The pool of the diagnostics asks for it: its servers only check. The pool of the questions does not.
The server that answers hover, highlights and inlay hints re-checks the program after each edit, as the one of the diagnostics does: it now prepares builtin and the modules it imports once as well (V_DIAGNOSTICS_PREPARE). Its answers to 22,000 questions over eight projects are those of an unprepared server.
Each change of a program was checked twice: by the server of the diagnostics, in a copy of the program rebuilt for each check, and by the server of the questions (highlights, inlay hints, a hover), in a copy of its own. The questions now go to the servers of the diagnostics, which run with V_DIAGNOSTICS_SHARED, and the check writes into the copy the questions use: a request holds the lock of the copy while it writes what the editor holds and asks about it. The child of the server that checked a version of the program answers the check and the questions about it, whichever came first: one check per change. The questions drop `-w`, which changes no answer of V3, so that both have one command line, and one server.
The servers of the diagnostics run with V_DIAGNOSTICS_PARTIAL: a check whose end is slow (the instances of generic functions above all) sends the errors it found first, ended by a `partial` line. The client publishes them at once, while the job is current, and the whole answer replaces them. The errors of a check are read by split_check_errors now, for both answers. The greeting of a server is read with its newline, which leaked into the first answer.
…tch` can narrow
VLS answered the hover of a local or a parameter from its index, with the
type its declaration writes: `a A` for `a A` in the branch of
`$if A is f64 {`, and `s Shape` in the branch of `if s is Circle {`, and
never asked the compiler, which knows what the value is there.
When the type of the binding names a type parameter of the generic function
around it, or is a sum type or an interface, the compiler answers first,
`a f64`; the index answers when the compiler cannot, and alone for any other
type, as before.
vfmt writes a long sum type with each type on a line of its own:
`type Shape2 = Circle`, and `| Square` below it. type_declaration read its
first line only and took `Shape2` for an alias of `Circle`: completion
after `s.` offered the fields of `Circle`, which `s` may not have, and the
hover of `s` in the branch of `if s is Circle {` did not ask the compiler,
which knows it is a `Circle` there.
The lines that go on with `|` are read too, past comments between them.
generic_list_names read `T Named` in `fn take[T Named](x T)` as no name at all, so where the compiler does not answer, a `T` of that function counted as the type `T` of the module, and a rename of that type went into the function. It takes the first word of each part now; binding_type_narrows reads the type parameters of a header with it too. The rename test needs the compiler that declares a type parameter in its list (vlang v experiment), which answers first.
A rename edited every occurrence of the symbol and nothing else, but did not look at the new name: a local renamed to the name of another local, of a parameter or of a function called there broke the program, and one renamed to the name of a constant made `if total > limit` compare the local with itself, with only a warning from V. A type parameter renamed to a name of several letters broke it too. The rename now renames in the copy of the program that V answers questions in, checks it, and asks again where each occurrence of the old and the new name leads (rename_conflicts.v). It is refused, saying why, when the check reports an error or a warning that it did not before, or when an occurrence would name another declaration than it did, or one that the rename makes. Where the rename already knows where an occurrence led, it does not ask again. `__global`, `__offsetof`, `_likely_` and `_unlikely_` are keywords too.
V says without a position that a function is declared twice, `builder error: redefinition of function `f``, and then where each declaration is, as `file:line:col: conflicting declaration: ...`. VLS placed the first line on the first declaration it found and read none of the others: the second declaration, often the one just written, showed no error. Each `conflicting declaration` line is now that error at its place, and the line without a position is not placed again on its own. The check of a rename reads a check's output the same way.
CI checks `v fmt -verify vls/` with the latest V, and 17 files failed it; the same 17 fail on master. This runs `v fmt -w` on them: alignment, line wrapping and quote style only, no change to the code.
The pull request ran `v fmt -w` on the 17 files that CI's `v fmt -verify` rejects. The merge keeps the experiment's code and runs the same `v fmt -w` (the V of vlang/v master) over it: those files and v3_line_info_test.v.
`pairs := [Pair{ left: 1, right: 2 }]` showed `pairs []Pair` and
`right := Pair{...}.right` showed `right Pair`: the index holds a generic
struct as `Pair[T]`, reads a literal that infers its arguments as the bare
`Pair`, and a field of it as its declared `T`. A type that names a type the
index does not know now goes to the compiler, which writes `[]Pair[int]` and
`int`. The index also reads a value written over several lines to its end:
`r := Point{ ... }.x` was a `Point` because only the first line was typed,
on the declaration and where it is used.
vlang/v#29134 added `pub fn recover() ?string` to vlib/builtin. As one of v_builtins it completes with that signature and inserts `recover()`, it is colored as a builtin function, and a rename refuses it as a new name. Its hover, definition, signature and the type of `msg` in `if msg := recover() {` already come from the compiler.
…the value
`head := Host{}.first_of(values)` hovered as `head Host`: where the type of
the value could not be told, the fallback that reads a leading literal as a
constructor took `Host{}` for the whole value. A literal followed by a member
or a call is no constructor of it: the index answers nothing, and the
compiler answers.
The cache of the hints of a document took its stamp from the open documents and the generation of the project, which only a watched file notification moves: a client without file watchers kept the old hints when a file that is not open changed on disk (a checkout, another editor), until an open document changed. Without watchers the stamp now holds what the .v files of the project are on disk, their sizes and modification times.
read_until and read_until_or_cancel went back to the top of their loop after a chunk of output, past the deadline and the question whether to cancel: a child that kept writing without the marker was never timed out or cancelled. They also searched all they had read for the marker after each chunk. Now the deadline and the question come after every read, chunk or not, and the marker is searched only from where it can still start.
…review rename_conflicts.v stays as the experiment has it (the shared copy of the program), and the expected reason for renaming a function to another's name is the redefinition that the V of the experiment reports.
V3 sets `v3_backend` for itself, and since vlang/v#29180 vlib/builtin keeps what only V3 builds in `*_d_v3_backend.v` files: the methods of i128 and u128 (`str`, `hex`, `hex_full`, `bin`, `str_base`, `char_str`) among them. VLS judged the files of a module with no define at all, so the completion of `n.` for an `i128` or a `u128` lost those methods. The files of a module, saved or not, are now judged with the defines V3 sets for itself (v3_own_defines): a `_d_v3_backend` file is part of it and a `_notd_v3_backend` one is not.
vlang/v#29112 removed the cJSON based `json` module, which the test of the modules of vlib expected (deprecated); `json2` replaces it and is not deprecated as a module.
The list of the builtin types of V (`i128` and `u128` in it) is wrapped as the `v fmt` of vlang/v master wraps it; no change to the code.
# Conflicts: # diagnostics_scheduler.v # handlers.v # interop.v # lsp.v # main.v
# Conflicts: # diagnostics_scheduler.v # diagnostics_server.v # handlers.v # handlers_test.v # interop.v # interop_test.v # main.v # rename.v # rename_conflicts.v # v3_line_info.v # v3_line_info_test.v
medvednikov
left a comment
There was a problem hiding this comment.
Independent review approved this exact commit after different fix and review agents completed the
source review and live LSP checks.
The fixes preserve symbol ownership during rename, refuse unsafe or unavailable validation, bound
inlay cache work, preserve source-copy ownership and watcher invalidation, validate compiler child
identifiers, recover from broken pipes, and cancel, join and reap compiler workers on session exit.
Reader borrowing and cache copies are explicit for V1 source compatibility.
The full nine-file local suite passed. Final protocol, reader, cache and completion tests cover the
later narrow changes; native builds and formatting pass. Shared process fixes passed optimized
regressions. Independent live checks on the final builds confirmed rename safety, both compiler
shutdown paths, closed-input recovery, and TCP session exit while the peer keeps its connection
open.
V1 syntax fixtures pass; a complete V1 build against latest vlib is blocked by upstream constrained-
generic syntax. Hosted CI remains queued or running.
The merge against master preserves the reviewed and tested source tree exactly.
Extends #524 with shared, prepared compiler checks, partial diagnostics, and compiler
answers for constrained generic code when the configured compiler supports them.
Diagnostics and queries share a locked program copy. Independent servers have independent
request locks. Bounded request batches allow fast text-only requests to run promptly while
preserving notification order.
Hover handles narrowed types, multiline sum types, inferred generic values, and documentation
examples. Builtin completion reflects current V types and functions. Rename validates the
new name, platform variants, and constrained type parameters.
Review fixes refuse unavailable rename validation, compare owning types, bound cache scans
and content reads, preserve source-copy ownership, protect against broken compiler pipes,
join the client reader, and wait for canceled diagnostics and compiler cleanup on exit.
Compiler child identifiers are validated before cancellation can signal a process.
Capability-sensitive tests probe the configured compiler before using newer syntax.
Validation: The full nine-file local suite passes; final protocol, reader, cache, and completion
regressions cover the last changes. Native V3 builds, formatter, independent live LSP checks,
and optimized shutdown regressions pass. Isolated V1 syntax fixtures pass; a complete V1 build
against latest vlib is blocked by upstream constrained-generic syntax. Hosted CI is pending.