Repository navigation
v3: constraints on type parameters, editor answers in generic code, faster checks for editors - #29254
Conversation
…nd unknown enum values V1 reports these three mistakes; V3 accepted them, so they only failed later (in the C compiler or at run time) or not at all, and editors showed nothing. - `.wait()` on an array that is not an array of threads reports V1's error. A `wait()` declared on an alias of an array (or an alias of that alias) is still allowed. - A method maps do not have (`m.foo()`) reports "unknown function". - `Color.nope` and a `.nope` branch in a `match` on a `Color` report "unknown enum field". Static functions declared for the enum, methods passed as values and escaped keywords (`Mode.@none`) are still accepted.
Review follow-up: the diagnostic tests covered `.wait()` on arrays that are not thread handles and user `wait` methods, but not the builtin call they must keep accepting. The new test calls it on `[]thread int` and `[]thread` and uses the results as values of the element type. An alias of a thread handle type is left out: V1 rejects it (`aliases of `thread` types are not allowed`) and reports the same `.wait()` error this check does.
master now words unknown function errors as `unknown function: m.close` (2dcb74c), so the exact-message assertion of this test failed once the branch was merged with it. Assert the error kind and the named method instead: that is what this check adds, and the wording belongs to the existing unknown-call path.
`<`, `>`, `<=` and `>=` had their operands checked only when both were
arrays, fn values or structs. Anything else went to the C backend as it
was: a thread handle, map, sum type or struct compared with a number
failed in the C compiler, and a bool, enum, channel or fn value compared
with a number compiled into a comparison that means nothing. They now get
the compatibility check of `==`, with the messages V1 gives for them:
numbers compare with any number, a pointer with an integer, a `voidptr`
with what C can order it with, and a struct with its own `<` only with
its own type.
V1 let three more kinds of operands reach the C compiler, and they are
rejected too: maps, interfaces, and an option against `none`.
`unsafe { x == 0 }` is accepted only when `x` is a struct reached through
a pointer, such as a `mut` parameter, and only there does the checker
suggest `unsafe`: a struct value compared with zero failed in the C
compiler.
The conditions of `match` branches, as in `match true { a < b {} }`, now
get the checks an `if` condition gets; none of them ran before.
Finding the operator of a comparison for a diagnostic no longer slices
past the end of the V file that template code is attributed to, whose
offsets can run beyond it.
A match condition such as `!(t > 1)`, `(t > 1)` or `is_true(t > 1)` has a prefix, parenthesis or call node at its root, so the comparison inside it was still left unchecked. Every condition that is an expression now gets the checks of an `if` condition; names, which can stand for types, type patterns, ranges, enum shorthands and `none` keep the checks of their own. The test helpers quote the paths they pass to the shell, so a checkout or a temporary directory whose path has spaces works.
A match condition such as `predicate(t > 1).value` has a selector at its root, and every selector was left unchecked because `mod.Type` is one too. Only a chain of names, which can be a qualified type, is left out now; a selector on a call, a struct initializer, an index or any other expression gets the checks of an `if` condition, so a comparison in its receiver is reported. The contexts test also checks that each error is a mismatch of the operands, reported at the comparison.
The fixture is about a method that duplicates a builtin method of an enum, but that method's body compared a flag enum with an int literal, `f & f2 > 0`, which V1 rejects too and which this check now reports, before the error the fixture is about. The body casts to int, as V asks, and the fixture shows its own error only.
A server mode for editors: with V_DIAGNOSTICS_SERVER set, the compiler parses builtin once and answers each `check <token>` line on stdin in a forked child that finishes the one-shot run of the same command line. The child enters its input directory again by name, gives the worker pools new threads (a pool from an earlier fork generation revives on first use), keeps a memory watchdog of its own, and its pid is announced so a client can stop a stale check. The server also asks for V_CHECK_SELECTED_FILES_ONLY: library bodies and signatures, which can only report on themselves, are not checked, and the check-only markused pass, whose errors were discarded anyway, is skipped. Faster for every compilation, with the same output: - closest_identifier_span walked the whole file, byte by byte, for a name that is not written at all (a method's qualified `Type.name`), once per declaration; it now searches outward with memmem. - remap_template_source rendered every template line again for each generated line it compared it with; each line is rendered once. - the parser looked for a duplicate method in every node of the program; only the current file's nodes can match. - import_is_used walked every node of the program for every import; it walks the import's own file. `v -check` now also prints the warnings of a program without errors.
A build reports the private functions and constants that nothing uses once markused knows what is reachable. `v -check`, which an editor runs, returned before that point and never reported them; it now reports the same ones. Markused runs only when a private function is neither called nor named as a value. Every other function is taken as used, a callback included, so an ordinary check only adds a scan of the names. A callback named only by code that is itself unused, such as an unused method, can then go unreported where a build reports it. The notices are computed before the check-only monomorphization of programs with globals rewrites the tree: after it, markused missed functions a build finds reachable, and two callbacks of VLS were reported as unused.
An editor shows the diagnostics of a program while its errors are being fixed. A check with errors returned before looking for unused private declarations, so their notices went away with the first error and came back with the last fix. With errors, what is reachable cannot be told: a call the checker could not resolve would leave its function looking unused. There, only the functions and constants that nothing calls or names are reported, and markused does not run. A syntax error still stops a check at the parser, with no type errors and no notices.
An editor checks a module that no program imports on its own, as a library, and a check without a main function reported no unused declarations at all. Now it reports the private functions and constants that nothing names. The public declarations of a library are what other programs use, so they are not reported, and a function exported to C or kept with `@[markused]` is used although no V code names it. Without a program, what a private declaration serves cannot be told beyond that: one that is named anywhere counts as used, as in a check with errors.
VLS asks the compiler about a position of a file with
`-vls-mode -line-info <file>:<line>:<code><column>`: hover (`hv^`),
go-to-definition (`gd^`), signature help (`fn^`), completion (a bare column)
and inlay hints (`ih^`). Only V1, frozen at 0.5.2, answered them; V3 now does
too, from the checked program and in the formats V1 prints, so that VLS can
move to V3.
The answers come from the flat AST and the tables of the type checker, after
a serial check (vlib/v/types/vls_*.v):
- hover: the declaration of the name under the cursor as V1 writes it
(`p main.Point`, `fn total(base int, nums ...int) int`, `struct Point`),
also for the variables of `if x := f()`, `err` and `it`, and the value of
an enum field (`Color.blue = 6`, `Perm.write = 0b10 (2)`);
- go-to-definition: `path:line:column`, a file of the checked directory
written with that directory as the command line wrote it (`./main.v` for
`.`), as V1 did;
- signature help: the signature of the call around the cursor, with the
argument under the cursor as the active parameter;
- completion: the fields and methods of the value before a dot, the public
members of a module, or the function a call names; the client writes a
placeholder name after the dot, as the code has to parse;
- inlay hints: the types of variables and consts, parameter names, the
implicit `err` of the `or {}` and guard `else {}` blocks after a Result,
range bounds, enum values, the fields of a positional struct literal and a
field listed out of declaration order.
The column is the 0-based byte of the cursor, and a name counts from its first
byte to just past its last one: V1 subtracted one and missed the first letter.
The launcher still sends `-vls-mode` to V1 unless `-new-compiler` is given.
Besides `check <token>`, the server now reads `query <token> <file>:<line>:<code><column>`: its child answers the question as `-line-info` does, in place of the diagnostics, and the server ends the answer as it ends a check. The token comes first, as the path may hold spaces. VLS writes the file as the editor holds it before it asks, as it does for a check, and gets its answer without starting a compiler. A one-shot `-line-info` also leaves the library bodies unchecked, as the server does: they cannot change an answer about the program's own files.
A client that verifies many positions, as a rename or a references request does, can now ask them all from one check: the questions go in one `-line-info` value, or one `query` request, separated by tabs. Each gets an answer on a line of its own after its index, `<index>\t<answer>`, empty when there is none, so that other output cannot shift them. One question is answered as before.
`err` names the error an `or {}` block handles, and the error of the call of
an `if x := call() {` guard in the plain `else` block right after it. V3 gave
every other `err` the type `IError` too, so a program that used it anywhere
passed `v -check` and then failed in the C compiler, where no `err` exists.
The `else` block of a guard now declares `err` in a scope of its own, and an
`err` anywhere else is an undefined identifier, unless the program declares
something with that name. A check that keeps no scope for such a block, as the
one of an instance of a generic function, finds `err` by where it is written.
A call that returns a Result was reported only in some places, such as an
assignment, an operand or an argument of a type. As a statement, as an
argument of `println` or `dump`, in a string interpolation, in a `defer`,
assigned to `_` or as an element of an array literal it went through: the
program ran on without the error, or printed the Result as if it were the
value.
There it is now a warning. `!` at the end passes the error on and an
`or {}` block handles it, as does `if x := call() {`; returning the call
from a function that returns a Result, or spawning it, passes it on too.
In a `defer`, where `!` cannot pass the error out, the warning asks for an
`or {}` block. Where the checker already reports an error, no warning is
added. With -W it is an error.
A check without errors prints no warnings, so the test builds each
program, as `v run` does before running it.
Before it renames a name, VLS asks where each of its occurrences is declared, the declaring one too. With the cursor on the name of a struct field, a function or a method, a method's receiver, a const, a method of an interface, a local of `:=` or a loop variable, V3 answered nothing, and VLS depended on a V1 patched to answer there. A local named like a symbol of an imported module, `offset := 7` or `time := [1, 2]` with `import time`, led to that symbol instead. The declaration of such a name is now the name itself, as V1 answered. A struct embedded by its name still stands for its type.
`u.id`, where `id` is a field of a struct that the struct of `u` embeds, led nowhere: the embedded struct declares that field. A rename then saw `u.id` as unresolved, and which occurrences it changed depended on where it started. The definition is now the field of the embedded struct, when one embedded struct declares it; with two the name is ambiguous, and V rejects it.
The parser builds a slice `s[a..b]` from a `.range` node whose bounds the slice then takes as its own children, and leaves the range out of the tree. The index of parents can name that range as the parent of a bound, so the answers to VLS that walk up from a name stopped there: a local in the bounds was not found, and one named like a function, `close` in `text[begin..close + 1]`, led to that function (`C.close`). A rename then left it behind and broke the program. The query engine now takes the slice as the parent of its bounds.
The statements of a loop body are children of the loop node, not of a
block, and a loop without an init or a step gets `.empty` placeholders for
them, which the parser may add after the body. The walk that looks for the
declaration of a local looked at the variables of a `for ... in` and at the
init of a C-style loop only, and it stopped at the first statement placed
after the use, a placeholder: a local declared in a loop body, `c :=
content[i]` in `for i < content.len {`, was not found further on in that
body. In the code of VLS that left 14 of the positions a rename checks
unanswered, and a rename slow or refused.
The walk now looks through the statements before the use in any node that
holds them, skipping placeholders. The variables of a `for ... in` exist in
its body only, not in its container.
The checker does not type the body of a generic function, and resolves no call there: a method called on a receiver, `app.stop_run_commands()` in `handle_requests[T]`, had no definition, hover or signature, and a rename could not verify that occurrence. Where the checker resolved no call, the method is now the one of the declared type of the receiver, or of the struct that type embeds, as it already was for the builtin methods of arrays, strings and maps.
To find the node a question is about, V3 looks at every node of the file, and for some it took a copy of all the file before the node: before the value of a `name: value` field, before a method's name to find its receiver, before an argument that may follow `mut`. On the 252 KB handlers.v of VLS, a question took 321 MB and 35 ms more than on a small file. The same comparisons are now made in place, and the name of a receiver is looked for in the text the question read already: a question there takes 0.4 MB.
A query child checked the whole program for each question, a second on the code of VLS, while an editor asks one question after another about the same files. The child that answered now stays, with the program it checked: it answers the next query in a few milliseconds while every file it read holds the same bytes (their SHA-256 when it answered, then a quick sum) and no file was added next to one of them or removed, and a new child checks the program otherwise. The server names the child only once it knows that it answers, so a client sees the same protocol. What a child allocates is never freed: once it has grown by V_DIAGNOSTICS_RETIRE_MB (1024 by default) since its first answer, it answers its last question and leaves. The server keeps nothing for it and reads its short replies through one buffer, so the server does not grow. A child also ends with the server. On the code of VLS, with the VLS of the vls-v3 branch, three renames went from 1.3 s, 2.8 s and 6.1 s to 0.15 s, 1.3 s and 3.4 s.
A local name was looked up among the declarations of the code first, out
to the parameters of its function, and only then among the variables the
language declares: `err` in an `or {}` block and in the `else` of an
`if x := f()`, `it` in the argument of `.map()` and the like, `a` and `b`
in that of `.sort()`. With a parameter `err` outside, the `err` of an
`else` inside led to the parameter: go-to-definition answered it, hover
showed its type, and a rename of the parameter changed the other one too.
Likewise a local `it` or `a` outside `.map()` or `.sort()`.
The walk out from the use now finds both kinds together, and the nearest
one is the one, as in the scopes of the checker: go-to-definition answers
the `{` of the block or the name of the method, and hover the type of the
implicit variable.
A child that answered a query answers the next ones while the files it read hold the same bytes and their directories the same names. Which module an unchanged import means depends on more: the directories searched before the one it was found in, which held no such module and were read nowhere. With `-path first|second|@vlib` and `helper` in `second` only, a `helper` added to `first` changed nothing the child watched, and it went on answering about the module in `second`, where a new check would read the one in `first`. The child now keeps what each import resolved to, and before it answers again resolves each one again as the compiler does, from its own module roots, the project root and the directory of a file that imports it; a different directory, or one found for an import that found none, makes it stale, as a changed file does. On the code of VLS this takes 2 ms of the 7 ms of an answer, and 0.6 MB that the child never frees: it answers some 1400 queries before it leaves, where it answered some 12000.
A generic function is checked with its type parameters open, where `x.name` cannot be judged, and nothing checked its concrete instances again: `show(1)` with `x.name` in the body passed `v -check` and failed in the C compiler. V1 reported "`int` has no property `name`" there. When a diagnosed file declares a generic function, a check now monomorphizes from the program's own functions and checks each concrete clone of its generics, keeping only what the concrete types decide: a field or a method such a type lacks. The full check of a clone stays limited to fixtures: outside them it failed 266 of 498 valid generic programs of the tree. With only member errors kept, those 498 give the same output as before. The clones of library generics are not checked and their bodies are not lowered, and the parent index covers each clone before it is checked; otherwise every parent query of the clone scanned the whole tree.
V3's parser read the name of each type parameter and skipped what followed
it up to the next `,` or `]`: `fn longest[T Named]` was `fn longest[T]`,
`fn id[T Foo + 42]` compiled, and `v fmt` deleted the constraint. It now
reads a constraint after the name, a type name qualified by its module or
not, with type arguments or not, and reports anything else there
("unexpected token `+`, expecting `,` or `]`"); after a name it reported
already, it skips the rest as before. Functions, methods, structs,
interfaces and type declarations keep the constraints next to their generic
params, in the node payload, through the passes that move the params to
longer-lived memory, and `v fmt` writes them back.
For a type parameter that names an interface, `fn longest[T Named](a T, b T) T`:
- a call whose type argument does not implement the interface is reported
at the argument that decided it, with the message V gives for a value
passed to an interface parameter; a generic struct, `Box[int]{}`, too.
- in the body, a field or a method used on a value of type `T` that the
interface does not declare is reported there, whether a call instantiates
the function or not: "type `T` has no field named `age`: its constraint
`Named` does not declare it".
- a constraint that names no interface, `[T User]`, is reported.
A type parameter without a constraint works as before.
…4 | f64` A type parameter can also name a set of types, declared at the top level with `constraint` (and `pub constraint`; `constraint` stays a name anywhere else, as vlib uses it): - a call whose type argument is not in the set is reported at the argument that decided it, "cannot use `string` as `T`: it is not in its constraint `Number`"; a type argument of a generic struct too. - in the body, a field or a method used on a value of type `T` must be one that every type of the set has, as `x.str()` for numbers: "type `T` has no method `greet`: `Pet`, in its constraint `Animal`, does not have it". - a type of the set that does not exist is reported. `v fmt` keeps the declarations, and a module that has one is read from its sources, not from a cached header. Operators on a value of such a `T` are not checked yet. Also tests that a program with constraints builds and runs.
Inside `fn longest[T Named](a T, b T) T`, completion after `a.` offered nothing, and hover and go-to-definition on `a.name` had no answer: the type of `a` is an open type parameter. A value of a type parameter that names an interface as its constraint now has the members of that interface: `a.` completes `greet` and `name`, hover on `a.name` shows `name string`, and go-to-definition leads to `name` in `interface Named`. For a set of types, `constraint Number = int | f64`, completion offers what every type of the set has, as `str`.
The conflict, in driver.v: master types a slice in the scan for implicit imports (vlang#29180) where the experiment passes the whole scan index to implicit_normalize_type; the slice is taken with the experiment's call. The new implicit_match_type of master is adapted to the experiment's index the same way, and notes its read of the fields index (index.read), which a prepared server's replay of the scan needs to see every key a scan asked.
vlang#29186 (unqualified os consts of a .vsh) and vlang#29177 (toml) merge without conflicts.
vlang#29184 (main types kept in imported generics reached through compile-time fields) merges without conflicts with the monomorphization of the experiment.
vlang#29181 (map value types and wrapper layers when matching generic type names) merges without conflicts with the constraint checks of the experiment in checker.v and checker_tail_stmt.v.
36 commits of master, none in conflict with the experiment; what the experiment adds over master stays the same (84 files, the same lines). Master's vlang#29197 passes `&&flat.Node` where V3 takes `&flat.Node` in transform/array.v, so no test that imports v.transform compiles: the next commit fixes it.
… holds Master's vlang#29197 wrote `t.a.child_node(&node, 0)` with `node := t.a.node(...)`, already a `&flat.Node`, and `t.a.child(&field, 0)` with `field` one too: V3 rejects the `&&flat.Node`, so no test that imports v.transform compiled (every test of vlib/v/driver, for one).
The conflict, in token/token_test.v: master adds a test of the source digest of a cloned file (vlang#29240) where the experiment adds one of the quick sums; both stay. Master's vlang#29252 fixes the `&&flat.Node` of transform/array.v as 5c695d9 did.
medvednikov
left a comment
There was a problem hiding this comment.
Reviewed current head f93481d. Confirmed merge blockers with independent reproductions; leaving this PR open.
medvednikov
left a comment
There was a problem hiding this comment.
Implemented the reported review fixes in 48aed49.
Nested generic sum constraints retain every distinct instantiated variant while bounding recursive growth and preserving finite alias-nested instances.
Validation:
- ./v self final rebuild passed.
- Independent bounded function/method, finite nesting/alias nesting, direct/mutual/argument-hidden expanding sum controls pass.
- Full strict Clang generic-constraints suite passed 1/1, including all existing function/method checks and all new controls.
- Both known diagnostic fixture outputs are byte-identical on exact original and fixed PR compilers.
- Formatting and git diff --check passed.
- Added documentation prose passes check-md; all original fenced code is unchanged.
- Full explicit-old docs check completed: 363 examples, 307 OK, 2 warnings, 12 errors, 4 format differences in unchanged examples.
- Full strict types suite passed 86/86 combined: 15 Clang passes, one TCC trial pass, all 70 remaining TCC cases passed.
- Extra finite/alias-nested Clang runtime probe reproduces byte-identical generic-sum forward-declaration errors on exact-original unconstrained control; cached and -nocache modes tested.
Validation limits:
- Broad compiler validation has pre-existing comparing_typesymbol/private-import diagnostic fixture mismatches; exact-original comparisons are recorded. No .out expectations changed.
- Only the identified review blocker and associated constraint behavior are covered; original experimental 84-file PR is not exhaustively reviewed.
- Explicit-old Markdown tooling does not support all existing modern compiler examples, including constraints; full docs check remains baseline-incompatible.
- Existing generic-sum runtime codegen can omit Part_First/Part_Part_int/Part_string forward declarations; exact-original unconstrained control reproduces byte-identical C errors. Only checker acceptance blocker is fixed.
This follow-up covers the reported generic-constraint finding and its recursion guard.
Reported findings are fixed in 48aed49; verified source push, focused regressions and independent fix review are complete.
medvednikov
left a comment
There was a problem hiding this comment.
Reviewed revision 48aed49 against f93481d. The earlier finding about Part[int] | Part[string] losing the second instance is fixed: the isolated compiler rebuild, original independent string probe, and five relevant constraint test functions pass. A second source review found no new high-confidence issue in the finite-instance/active-expansion changes.
GitHub currently reports this branch CONFLICTING with master, so it cannot be squash merged as submitted. The original large experimental-feature review stopped after its concrete blocker; this targeted re-review does not constitute approval of the entire 84-file feature. Conflict resolution and the remaining full-feature audit are still needed before merge.
An extra recursive-argument probe accepts int but rejects []int; the same behavior reproduces on the prior head. It is not a regression in this revision, and the targeted checks do not settle the intended recursive-constraint semantics.
medvednikov
left a comment
There was a problem hiding this comment.
Reviewed the complete PR at 48aed49 with parallel source audits and independent reproductions. The earlier distinct generic-sum-instance issue is fixed, but the attached comments identify four P2 issues in promoted generic method values, constraint import accounting, recursive sum membership and editor declaration scope, plus a P3 embedding-depth limit.
Validation: 21 focused types/transform/generic/markused test files, five driver/editor/digest files, 19 parser/token/flat/formatter files and the embedded method-value integration test passed. The two remaining parser files could not complete because nested tool builds require an unavailable libgc.a. The full VLS suite hit its nested compiler memory limit; a large supplemental transform suite was deliberately stopped. Linux diagnostic-server lifecycle remains unverified. Independent failure cases and positive controls reproduce the findings at this exact head.
The PR also currently has merge conflicts. Please address the findings and update the branch before merging.
|
@codex review |
medvednikov
left a comment
There was a problem hiding this comment.
Re-reviewed ec7b2ae after the master merge. The merge conflict is resolved, but all five existing findings still reproduce on this head:
- P2: Specialize the declaring embedded receiver for generic method values.
- P2: Count module names used only in generic constraints as imports in use.
- P2: Preserve finite reachable membership or reject growing recursive constraints.
- P3: Avoid rejecting valid struct families past seventeen embedding levels.
- P2: Restrict fallback type parameters to declaration scope.
Fresh exact-head compiler rebuild, 18 independent probes with passing controls, five focused authored constraint tests, and an independent editor baseline check completed. The earlier distinct generic-sum-instance fix remains correct. No duplicate inline threads were added. Changes are still required; this is not approval of the broader experimental feature.
medvednikov
left a comment
There was a problem hiding this comment.
Checked the review continuity at current head ec7b2ae against the previously fully reviewed 48aed49.
The latest update is a master merge resolving checker configuration conflicts. It does not repair the outstanding feature findings: the recursive-constraint and VLS finding files are unchanged; promoted generic method-value declaration lookup still omits the embedded declaring receiver; and the import-usage changes address SQL without reading generic-constraint payloads. The previous changes-requested review remains applicable, including its embedding-depth finding.
This is a source comparison of the merge update, not a repeat of the full 84-file feature audit. No tests were rerun for this continuity check; the independent reproductions and validation are recorded in the existing complete-feature review. Please address those findings before merging.
Bind promoted generic method values to the concrete declaring embedded receiver. Keep editor type-parameter fallback within its declaration, including cached function-type returns. Preserve constraint-only imports, reject growing recursive sum constraints and accept finite deep embedding. Document the behavior and update the intended growing-sum expectation. Integrate current master without rewriting either source history.
medvednikov
left a comment
There was a problem hiding this comment.
Implemented the reported review fixes through ad6ca28.
Nested generic sum constraints retain every distinct instantiated variant while bounding recursive growth and preserving finite alias-nested instances. Specialized promoted generic method values through their concrete declaring embedded receiver. Restricted editor generic-parameter fallback to its declaration, including cached function-type returns. Counted constraint-only imports, rejected type-growing recursive sum constraints and accepted finite struct embeddings without a fixed depth cutoff. Updated the intended growing-sum compile-error expectation and public documentation. Preserved incoming source/master history and all unrelated upstream tool/driver changes.
Validation of the latest fix:
- Matching native-TCC ./v self passed with fallback disabled after the final cached-header follow-up; touched V files formatted and git diff checks passed.
- Complete unfiltered type-checker suite passed89/89 on the final combined source, including constraint/import/deep-embedding and 42 cached-header scope cases.
- Complete focused suite passed5/5: generic constraints, VLS generic scope, promoted method values, existing generic method-value type arguments and monomorphize.
- Complete compiler output suite passed102 cases:90 ordinary,11 expected panics,1 existing skip,0 errors.
- Independent strict Clang method/runtime controls passed, including padded/deep/generic embedded receivers and imported homonyms; combined checker/source-preservation audits found no blocker.
- Full check-md completed362 documentation fences with317 OK and one inherited SQLite formatting error; its one whitespace-only canonical formatting correction passed exact-fence check-md1/1. No remaining changed documentation fence failure.
- Selected mandatory compiler diagnostics reproduced two known baseline expectation mismatches. Explicitly filtered broader compiler harness passed1/1; no .out files changed.
Validation limits:
- Broad compiler validation has pre-existing comparing_typesymbol/private-import diagnostic fixture mismatches; exact-original comparisons are recorded. No .out expectations changed.
- Only the identified review blocker and associated constraint behavior are covered; original experimental 84-file PR is not exhaustively reviewed.
- Explicit-old Markdown tooling does not support all existing modern compiler examples, including constraints; full docs check remains baseline-incompatible.
- Existing generic-sum runtime codegen can omit Part_First/Part_Part_int/Part_string forward declarations; exact-original unconstrained control reproduces byte-identical C errors. Only checker acceptance blocker is fixed.
- Two old-tooling warnings came from inherited VFLAGS being forwarded as tool path arguments; added prose/report checks were rerun with VFLAGS unset and passed cleanly.
- The full compiler suite was not run; broader compiler harness used explicit selectors. Two selected baseline diagnostics differ in v.ast/private import expectations; no .out edits were used.
- Direct pointer receiver generic method values already fail C generation on exact48 baseline and were not broadened by this focused embedded receiver fix.
- Existing VLS line-info assertions passed through an external setup-only copy adding the compiler memory flag and anchoring fixture paths; the assertions were unchanged.
- The full documentation command was not repeated after the single SQLite whitespace correction; the corrected final fence passed an exact-byte focused recheck.
- Runtime validation is macOS ARM64; native Windows and other backends were not executed.
Reported findings are addressed in verified source head ad6ca28; relevant regression tests and an independent fix review completed.
* tests: assert a lowered minimum really checks on the pool check_program sets VJOBS=4 and builds the checker pool, so the parallel path runs on Windows too. The `$if windows` branch asserted !was_parallel, which contradicted the test's own name and failed there. It has been there since the file was added in #29254. * tests: compare the module source root with matching separators current_file_module_source_root() answers with '/' separated paths because it applies .replace('\\', '/'), while the raw os.real_path(project) expectation is '\\' separated on Windows, so the two could never be equal there. * tests: set VMODULES in-process instead of execing env env is not on PATH on Windows, so the exec failed with CreateProcess code 2 before the child compiler started. Setting it in the test process, and restoring it afterwards, gives the child the same environment.
This draft is the
experimentbranch of my fork: everything built on top of #28973 (VLS support in V3), #28794 and#28854 since those were opened. Its main part is new in V: constraints on type parameters,
fn longest[T Named],checked at the calls and in the bodies, with the editor answers for them. It also has the speedups of the
diagnostics server that VLS uses (prepared, shared, warm and incremental checks), checks of generic instances in V
instead of in the C compiler, and fixes found on the way. The VLS side is vlang/vls#525 (draft).
It is a draft to show the whole work in one place, to run the CI on the platforms I cannot test (macOS, Windows,
the BSDs), and above all to ask whether the design of the constraints fits V before splitting this into PRs that
can be reviewed one by one (a split is proposed at the end).
About the commit list: master comes into the branch with merges, never a rebase, and the branch still holds the
original commits of #28973, #28794 and #28854, which master has as squashes, so the Commits tab lists them too.
The Files tab shows what is new: 84 files, +21,981 −829: code 38 files (+12,369 −653), tests and fixtures 45 files (+9,572 −176), docs 1 file
(+40). Every file is listed at the end with what it is for.
1. Constraints on type parameters (new in V)
What a constraint is
A type parameter can name, after it, what its type arguments must be. There is no new keyword: the constraint is an
ordinary type.
fn longest[T Named](a T, b T) Ttype Number = int | i64 | f64,fn half[T Number](x T) Tfn greet[T User](x T) stringtype Value = Named | int[T Comparable[T]],[T Comparable[int]][C Container[T], T Named][T int]The same sum type stays a sum type where it is the type of a value. A constraint can be written on the type
parameters of functions, methods (also a method's own,
fn (s Shelf[T]) pair[U Named]()), structs, interfaces,sum types, aliases and function types:
struct Box[T Named],interface Shelf[T Named],type Picker[T Named] = fn (T) T,type Tree[T Named] = Leaf[T] | Twig[T].A type parameter without a constraint works as before. The sweeps that check written types and calls of constrained
generics run only in a program that declares a constraint: a program without any is checked as before (the checks
of instances in section 2 are the exception, as they apply to every generic).
Before and after
master: V3's parser skipped what followed the name of a type parameter up to the next
,or], so the constraintwas dropped:
fn longest[T Named]wasfn longest[T](andfn id[T Foo + 42]compiled).v -checksays nothing,v runfails in the C compiler, and then the V1 fallback rejects the syntax:and
v fmtwrites the function back asfn longest[T](a T, b T) T.This branch:
age_ofis reported althoughUserhas anage: the body may use only what the constraint declares, so that anytype the constraint admits works. A set of types:
master:
v -checksays nothing;v runfails in the C compiler (invalid operands to binary / (have 'string' and 'int')inhalf_T_string), then V1 rejects[T Number]. This branch:At the call
V gives for that kind of type:
`int` doesn't implement field `name` of interface `Named`,cannot use `string` as `T`: it is not in its constraint `Number`,cannot use `int` as `T`: it is not `User` and does not embed it.longest[int](1, 2), at the type argument as written.struct literals with explicit or inferred type arguments (
Box{ item: 1 }),map[string]Box[int]{},sizeof(Box[int]),typeof,isreftype, casts (&Box[int](p),?Box[int](none)),chan Box[int]{}, thesignature of a function literal, the fields of an anonymous struct,
map[string][]Box[Box[User]]; and the samefor generic interfaces, sum types, aliases and function types.
f := longest[int].fn wrap[T](x T) Box[T]withstruct Box[T Named]is reported, and told which one to take ("give it one,[T Named]"). An interface satisfies another that it implements (by embedding it, for instance); a set satisfiesan interface when all its types implement it, and another set when all its types are in it; a struct family
satisfies a constraint that takes its whole family.
$if T is intaround the use counts, and the receiver of amethod keeps the constraints of its struct.
Odd, whoselesstakes anint, is not aComparable[Odd]. One generic interface constraint satisfies another when both name the samebound interface.
not repeat the error (in the full checks of instances, as the checker fixtures run them).
unwrap_name(Box[User]{...})withfn unwrap_name[C Container[T], T Named](c C)bindsTtoUserfrom theget() TofContainer[T](before, the call made no instance, passed-check, and failed in the C compiler).Methods infer the same way. A type passed for a generic interface binds its type parameters from its methods
and fields:
take_now(shelf)withfn take_now[T Named](s Shelf[T]) Tand aUserShelfwhoseget()returns aUserbuilds and runs (it failed in the C compiler, also without constraints).In the body
Thas what the constraint provides: the members of the interface, or of the struct; for a set, themembers every type of it has, builtin ones included (
x.lenof[]int | string,x.str()of numbers). A fieldof a constrained type keeps it:
b.item.namein a method ofBox[T Named].type `T` has no field named `age`: its constraint `Named` does not declare it, or for a settype `T` has no method `greet`: `Pet`, in its constraint `Animal`, does not have it.+=and the other assignments,-x,!x,~x,x++,x--andx[i].An interface declares no operators, so its
Thas==,!=andin; a struct, only==and!=; with a set,an operator has to work for every type of the set against what is on its other side (another
T, a literal, avalue of a builtin type):
operator `%` is not defined on type `T`: `f64`, in its constraint `Number`, does not have it. The operators a type declares count (fn (a Vec) + (b Vec) Vec), as do those an alias inherits, and<brings>,<=and>=. A test holds these rules to the checker: for 43 types, aTconstrained to one typeis reported on exactly the lines where a value of that type is, 30,745 operations in all.
TareTs:x := a,x, y := a, b, the value of anif, amatch,(a),&aor anor {}, an index of a[]Tor amap[K]T, the variables of afor ... in, the guard ofif x := opt. A generic call there is typed with the caller's type parameters bound from its type arguments, itsreceiver and its arguments.
it,aandbofmap,filter,any,all,count,sortandsortedare checked:xs.map(it.age),xs.sort(a.age < b.age),xs.map(|x| x.age), and a lambda passed toanother generic takes the function type the callee declares with the type parameters the call binds.
ison a value ofTis reported when a type of the constraint is neither a sum type nor aninterface, with the way to say it:
`is` can only be used with sum type or interface values, not `T`: `int`, in its constraint `Number`, is neither; test `T` with `$if x is f64`. Amatchby type on a value ofTisreported once, as an
is, and narrows its branches; amatchon values narrows nothing.$if T is f64 {narrowsTin its branch, and so does a test on a value,$if x is f64 {, with!is,in,!in,$intand the other groups, alone or joined with||and&&. The$elsehas the rest of the set; withan interface constraint it keeps the interface. A branch that no type of the set reaches is not checked.
-preallocand no GC failed everyeditor query with "invalid memory access" once a program had enough
$if T ischains (eight functions of threebranches). The editor tests build their V3 as
./v selfdoes, so they would see it.The whole body, when every type parameter has a constraint
When all the type parameters of a function or a method have a constraint (its own and the ones the type of its
receiver declares), its body is checked as the body of any other function, with each type parameter as each type
its constraint admits: the interface itself, which stands for any type that implements it, or each type of a set.
An error says for which:
cannot use `f64` as type `int` in return argument: when `T` is `f64`, in its constraint `Number`. A generic with a type parameter without a constraint leaves its body to its instances, as before.type parameters meet with every two of their types (pairwise testing: 187 checks for three type parameters of 13
types each instead of 2,197), plus, for each way the
$ifs of the body can go, the combinations of the groups oftypes its conditions tell apart (up to 1,024 checks for all the ways, then one type at a time). A check costs what
checking a function with that body does (0.46 ms for one of 25 lines).
$ifthat tests the type parameter against a type that implements it ($if T is User,$if x is User,$if y is Userfor a localy := x,$if x in [User, Admin]) adds a check of the bodywith that type:
when `T` is `User`, which implements `Named`.[T Comparable[T]]keepsTopen inside its own type; a constraint that names another type parameter,[C Container[T], T Named], is checked with the other one put in,CasContainer[Named]; a cycle,[C Container[T], T Wrapper[C]], keeps the repeated one open.$if x in [...]and!inare decided in those checks asisis, one type of the list at a time.`A` is `f32` or `f64`,`A` is not `i8`), anda constraint that names itself is written as the type it unfolds,
T.v run,v build) check these bodies as well: their errors came from the C compiler.Methods behave as functions
The tests of constraints check every program a second time in its method form: each generic function becomes a
method, with the same type parameters, of a struct without type parameters (
vlib/v/compiler_tests/method_form),which has to give the same errors at the same places, and the same editor answers. The differences it found are
fixed: a method checked with the types of its constraints took the
UofComparable[U]for a type left to infer;h.first[int]was parsed as an index (it is now the method ofT = intbound toh, asfirst[int]is thefunction, with its type arguments checked as those of a call and its instance made);
f := h.longest[User]leftfwithout a type; and a method did not infer a type parameter from the constraint that names it. Methods ofgeneric structs are checked as functions with all their type parameters:
Shelf[T Named].pair[U Named]has itsbody checked as a normal function's,
Query[T].map[U Named]its calls and what its constraint declares.Formatting and documentation
v fmtkeeps the constraints of functions, methods (with their own type parameters, on any receiver) and typedeclarations.
doc/docs.mdhas a section Constraints after the generic functions (its example builds and runs;v check-mdpasses), and the checker has fixtures for the errors at calls and in bodies, for functions and formethods (
generic_constraint_{call,body,method_call,method_body}_err.vv).2. Generic instances checked in V, in
-checkand in buildsThis applies to every generic, with or without constraints. A generic body without constraints is not checked with
its type parameters open; nothing checked its concrete instances either, so
show(1)withreturn x.nameinfn show[T](x T) stringpassedv -checkand failed in the C compiler (request for member 'name' in something not a structure or union). V1 reported "inthas no propertyname".v -checkmonomorphizes from the program's own functions and checks each concrete clone of its generics, keepingwhat the concrete types decide: a field, a method or an operator that such a type lacks (
`int` has no property `name`). The clones of library generics are not checked; in a check they are made without their bodies unlessa type argument reaches the program through an interface, a generic type or a method with type parameters of its
own (seven cases, each with a test;
V_CHECK_LIBRARY_BODIES=1keeps every body).measurable cost (663-714 ms against 658-700 ms on a 660-line program);
V_CHECK_INSTANCES_IN_BUILD=0turns itoff. On Windows (no
fork) a build leaves them to the C compiler, as before;-checkreports them there too.el.val.pack()of anElm[Octet]withtype Octet = stringandfn (o Octet) pack()is valid (generic_static_call_test.v).3. Editor answers in generic code (the query engine VLS uses)
The checker does not type a generic body with its type parameters open, so hover, definition and completion had
little to answer there. Now (each with tests in
vls_line_info_test.v):a.completesnameandgreet,hover on
a.nameshowsname string, go-to-definition leads to the interface; for a set, what every type has.x f64in the branch of$if T is f64 {(or of
$if x is f64, or of a$ifon a local that holds it),a main.Userin$if a is User {; elsewherex Twith what it can be on a line of its own:T: int | i64 | f64,T: implements main.Named,T: main.User or a struct that embeds it. The$ifs on the way are read whole (tests,&&,||,!,parentheses) and taken together,
inand!inincluded; a test it cannot follow may go either way.[T Number]and that line; constraint names have a hover and a definition;go-to-definition (and so a rename) of a type parameter leads to its list,
[T Named], never to a type of themodule with the same name.
fn longest[T Named](a T, b T) T, and a parameter is whatisor a branch ofmatchmakes it there,s main.Circle, as a local was.it,a,b,errand the parameters of short lambdas have types (it T), and a lambda's parameter has adefinition.
lengths []intforlengths := xs.map(it.name.len),first T, the values of a call that returns several, a loop's index, key and element,T(0),[]T{},map[string]T{}, operations (total := a + b + c, comparisons arebool), anifor amatchused as a value,array, map and generic struct literals and their elements and fields (
pair Pair[N],left N),or {},dump(),&x,a.str(),values.map(it * 2). What no type parameter decides (separator := match sep {...}) is typed bychecking the body once more with its type parameters open; a generic call is bound as the checks bind it
(
same Aforidentity(a), not the callee'sT), also with explicit type arguments (first_of[A](values)) anda struct literal receiver (
Host{}.first_of(values)).Box[A]shows whatAcan be.xs.map()overxs []T, a method ofBox[T], a local that holds a function literal or a method value (double fn (int) int), a generic functioncalled with type arguments, a method named without a call (
label_of := b.label), the method of the interfaceof a constraint (
a.less(b)with[T Comparable[T]]) or of a generic interface (s.get()withShelf[User]:fn get() main.User), and a method value of an alias (the alias's own method first, as the call does).[1, 2].map(it * 2)findsmap.$if, which has no node of its own (cin$if c is $float {), is answeredas that value there, with what the
$ifs around it leave:c CwithC: i8 | ... | f64.$if js,$if windows,$if flag ?) parses the file again withevery branch, as
v fmtdoes, and answers there, once per process; diagnostics never see those nodes.4. Diagnostics server for editors
#28973 added the diagnostics server (
V_DIAGNOSTICS_SERVER): builtin parsed once, each check in a forked child.This builds on it; every mode is opt-in through the environment and VLS turns them on.
V_DIAGNOSTICS_PREPARE): the server also parses strconv, strings and math.bits (which builtinimports) and collects all the declarations of builtin and those modules once, before its first check; each check
continues from there. The order-dependent results stay those of a one-shot check (the files are checked in its
order; a program that declares a key a prepared module also declares, 1.3 % of the corpus, collects anew; one that
would resolve a prepared module otherwise runs as a one-shot check). It also prepares once: the scan for unused
declarations, the C includes, builtin's field index, the scan for implicit imports (replayed when the program wrote
none of the keys it read), each file's imports, whether each prepared struct implements
IError, and room in thenode caches for the user's nodes.
V_DIAGNOSTICS_SHARED): the child that checked a version of the program answers the check andthe questions (hover, highlights, inlay hints) about it, whichever comes first: one check per edit instead of two.
The diagnostics come from a grandchild that finishes the check into a file in memory, as the end of a check
rewrites the tree that answers come from. A grandchild outlives no child.
V_DIAGNOSTICS_WARM_CHILDREN, 3 by default): the children of the last versions of a program stay,each request is offered to all of them, and the one whose version is on disk answers: undo and redo answer at once.
V_DIAGNOSTICS_PARTIAL): when the end of a check (unused declarations, instances of generics)takes more than 3 ms, the child prints the errors it already has, ended by
v-diagnostics-server: partial <code> <token>, and the whole answer when it ends.V_DIAGNOSTICS_INCREMENTAL=0turns them off): each check recordswhat every function body reported, relative to its region (from its line to the next declaration), in a file in
memory that every child sees; the next check of the program, with the same files and declarations, checks only the
bodies whose region changed and puts back what the others reported. The rest of the check reads from the record
what it needs of the bodies left out (functions used as values, calls of selective imports, unhandled Results). A
body that names its own place (
@LINE), reads another file ($tmpl) or reported outside its region is checkedanew, and a program under 2,048 nodes (
V_DIAGNOSTICS_INCREMENTAL_MIN_NODES) is checked whole. The instances ofgenerics are checked again only when a body that changed touches something generic; otherwise their errors are put
back with their regions. What waits for the bodies left out (an editor question, the instances) has them checked on
the worker pool.
V_DIAGNOSTICS_INCREMENTAL_VERIFYhas the child compare what it put back with a full check.takes up to eight jobs, and a question checks on the pool too, with the per-node types promoted from the workers.
of before, and watches only what it parsed itself.
V_DIAGNOSTICS_TRACEtells what the server did and why: a check run one-shot instead, a prepared scan redone,how many library instances were made without bodies, a difference found by the incremental verification.
-line-info); a failedcheck replayed by the launcher never becomes a diagnostics server itself (it hung); the server's flag is
shares_children, assharedis V1's keyword and broke the V1 fallback of a failed build; the Linux-only calls(
memfd_create,prctl) are behind$if linux, sovc/v.cstill builds on macOS (as v.diagserver: guard prctl so vc/v.c builds on macOS again #29176 did for theprctlofserve), with a test of the portable C. The server importsv.tokenunder a name of its own (a-checkof the module reported the requests'tokenlocals as shadowing it), and a verbose (-v) check keeps thepreparation: verbosity is no longer part of the key that matches a check with the prepared checker.
Measured through VLS by LSP, from a change to its diagnostics, on programs of 100 / 1,850 / 18,600 lines: 80 / 134 /
257 ms before these changes, 17 / 35 / ~150 ms with them; going back to a text checked before (undo): 1.5 / 2 / 6 ms.
On the code of VLS (45,700 lines, with generics), the complete answer after editing a body without generics: about
1,150 ms -> 330 ms. These were measured on my machine (Linux, 16 threads) when each change was made; the commit
messages have the details. The output was compared with the one-shot check on a corpus of 1,645 programs after each
change.
5. Faster checks and builds for everyone
libraries included (walking a method's file back byte by byte): ~115 ms of every
v -checkand build, now 0.5 ms.v -checkof VLS 2,224 ms ->1,184 ms.
unsafeblock stops at its function (it scanned the whole arenafor the parent of each specialized function); a generic call is told from an index by its brackets first; a check
lowers no library body late; generic declarations are found by node kind without copying every node.
program that exports nothing; a check prepares no native C inputs; the implicit-import scan neither copies every
type text nor makes maps for each function, and import resolution continues it instead of starting over; the
user's file nodes are found once; a source path is normalized only for a cached module header; only a line that
starts with
importcan hold several imports; a check of selected files leaves the values of the other filesunchecked; only a declaration that can be reported is an unused candidate.
6. Other fixes found on the way (the same in master)
g := s.getwiths Shelf[User]andd := p.describewith an embeddedBase, washas no field named: it is typed as its call (fn () User), alsothrough several embeddings, an alias or a generic struct, and across modules; the C wrapper of an interface method
value returns the type argument; two embedded structs that declare the method make it ambiguous; a
&Basereceiveron a stack struct asks for
@[heap], and amutreceiver on the stack cannot escape.d := a.describewithfn (a Alias) describe(), called the struct's method orfailed in C: it calls the alias's.
getter := b.getwithb Box[T], failed in C: each instance asksfor the method of its receiver.
Box{ item: a }, kept the bareBoxin an instance, in an array(
[Box{ item: 3 }]) and before a selector (Box{ item: 2 }.item): it takes the type arguments its fields give.IntHolder{ Holder: Holder[int]{ item: 5 } }lost the embedded generic struct silently (h.itemwas 0): theliteral matches its fields as a field access does.
h.own()withfn (h Holder[T]) own[T]() T,infers it from the embedded struct.
b.own[string]()on aBox[int], is an error of V:`own` takes `T` from its receiver `Box[int]`: `T` is `int`, not `string`(it failed in C, or was dropped).xs.map(|x| x.name.to_upper())after a localfn (x T) intin a generic body:maptook the lambda's functiontype for its element type; it takes the type of the lambda's body.
v fmtwrote$if a in [f32, f64] && bas[f32, f64]&& b,!(a is f64)as!( a is f64 ),sizeof(A)assizeof ( A ),$pkgconfig('x')as$pkgconfig ( 'x' )and[]&&intas[]&& int; the condition text thechecker reads keeps its spacing.
7. Environment variables
V_DIAGNOSTICS_PREPAREV_DIAGNOSTICS_SHAREDV_DIAGNOSTICS_WARM_CHILDRENV_DIAGNOSTICS_PARTIALV_DIAGNOSTICS_INCREMENTAL=0V_DIAGNOSTICS_INCREMENTAL_MIN_NODESV_DIAGNOSTICS_INCREMENTAL_VERIFYV_DIAGNOSTICS_TRACEV_CHECK_LIBRARY_BODIES=1V_CHECK_INSTANCES_IN_BUILD=08. Tests
New:
generic_constraints_test.v(calls, written types, bodies, sets, families, generic interfaces, inference,another module, programs that build and run, #28797's
format_thousandswith[T Number], method forms),generic_constraint_operators_test.v,generic_body_check_test.v,generic_body_combinations_test.v,check_generic_instances_test.v,explicit_receiver_type_args_test.v,method_form/method_form_test.v, thechecker fixtures above,
incremental_check_test.v,parallel_check_min_items_test.v,scoped_check_batches_test.v,module_import_lines_test.v,vls_comptime_ways_test.v,digest_test.v,job_limit_test.v, and run tests invlib/v/tests/{generics,fns,aliases,structs}for each fix of section 6;vls_line_info_test.vgrew with the editoranswers,
gen_test.vwith the formatting,cross_output_codegen_test.vwith the Linux guards, and the reviewregression tests with the method values. Each test was seen failing before its change.
Verification on Linux (gcc 16), each time master came in (the last one
6f23f7a6f4):c2b96c9b33: 640 test files (those master changed since the previous merge, ours, every test of thedirectories whose files both sides changed and all of
vlib/v/tests/generics): 624 pass, 2 are skipped, and the 14that fail fail the same way on master alone (same functions and cases):
review_checker_regressions_test(14cases),
review_transform_regressions_test,orm_alias_method_visibility_test,copy_ownership_codegen_test,mut_address_arg_local_storage_codegen_test,owned_array_borrow_storage_codegen_test,escape_binding_storage_test,fixed_array_mut_arg_escape_test,fn_field_address_c_arg_test,pointer_alias_len_test,tcc_libatomic_linux_test, two serial builds (native_inputs_serial_build_testandparallel_transform_serial_build_test:fn_parallel_d_v3_no_parallel.vdeclarescollect_gen_info_fn_prepswithtwo parameters and
cleanc.vpasses three) andcross_output_codegen_test;6f23f7a6f4: 544 test files (the same choice of tests): 539 pass, and the 5 that fail fail the sameway on master alone:
review_checker_regressions_test,review_transform_regressions_test,native_inputs_serial_build_test,fixed_array_mut_arg_escape_testandcross_output_codegen_test;cross_output_codegen_test.v, master'stest_cross_output_lets_the_target_libc_pick_the_poll_headerand thisbranch's test of the Linux guards both fail because master no longer generates the portable C of a program that
uses
os.get_raw_line(os.c.v:628: infix expr: cannot use int (right expression) as voidptr, in its Windowsbranch); the test of the guards passed when it was added, on master
3a9a816dbf;9. Known limits and open questions
fork);-checkdoes.isandinconditions:$if T.name == 'f64'is not evaluated by V3.checkfor the same version loses the warnings that come from parsing(
match is always trueof a$match):vlib/v/tests/comptime/comptime_match_assign_test.v.-checkdoes not check the instances that a$forin a function without type parameters asks for (a build doesnot report their members either; the V1 fallback does).
[T Named]; a sum type as the constraint (and still a sum typeas a value type); a struct standing for its family; the rule that only fully constrained bodies are checked as
normal functions; and the budget of combinations (256, then pairwise).
10. How this could be split
v fmt: keep the constraints (no checks yet).-checkand builds.Files
vlib/v/compiler_tests/vls_line_info_test.vvlib/v/types/checker_generic_constraints.vvlib/v/compiler_tests/generic_constraints_test.vvlib/v/types/incremental_check.vvlib/v/driver/driver.vvlib/v/types/checker_generic_bodies.v$ifways, messages)vlib/v/types/vls_hover.vvlib/v/compiler_tests/generic_body_check_test.vvlib/v/types/checker.vvlib/v/diagserver/diagserver_linux.c.vvlib/v/types/checker_prepared.vvlib/v/types/vls_comptime_ways.v$ifon the wayvlib/v/types/vls_generic_locals.vvlib/v/compiler_tests/generic_constraint_operators_test.vTagainst the checker, 43 typesvlib/v/compiler_tests/check_generic_instances_test.v-checkand builds; library instances without bodiesvlib/v/transform/monomorphize.vvlib/v/transform/library_bodies.vvlib/v/types/incremental_check_test.vvlib/v/compiler_tests/method_form/method_form.vvlib/v/types/checker_parallel.vvlib/v/types/checker_tail_stmt.vvlib/v/compiler_tests/testdata/format_thousands_28797.v[T Number]vlib/v/types/checker_tail.vvlib/v/compiler_tests/explicit_receiver_type_args_test.vvlib/v/tests/fns/method_value_of_generic_interface_or_embedded_struct_test.vvlib/v/parser/parser.vv fmtspacing of compile-time conditionsvlib/v/types/vls_query.v$ifconditions, branches left out, generic bodiesvlib/v/types/vls_definition.vvlib/v/transform/transform.vvlib/v/types/parallel_check_min_items_test.vvlib/v/tests/generics/method_value_in_generic_body_test.vvlib/v/types/generic_body_combinations_test.vvlib/v/compiler_tests/review_transform_regressions_test.vvlib/v/types/vls_skipped.vvlib/v/tests/generics/generic_method_value_type_args_test.vh.first[int]as a method valuevlib/v/driver/instance_check_nix.c.vvlib/v/tests/generics/method_repeated_receiver_type_param_test.vvlib/v/compiler_tests/method_form/method_form_test.vvlib/v/types/vls_comptime_ways_test.v$ifconditionsvlib/v/tests/structs/embed_generic_struct_init_by_name_test.vvlib/v/tests/aliases/alias_struct_method_value_test.vvlib/v/gen/v/gen_test.vv fmtkeeps constraints; spacing of compile-time conditionsvlib/v/flat/flat.vvlib/v/compiler_tests/review_checker_regressions_test.vvlib/v/token/position.vvlib/v/types/module_import_lines_test.vvlib/v/checker/tests/generic_constraint_method_call_err.vvvlib/v/tests/generics/embedded_generic_method_own_type_param_test.vvlib/v/tests/generics/generic_struct_literal_type_args_from_fields_test.vdoc/docs.mdvlib/v/checker/tests/generic_constraint_call_err.vvvlib/v/diagserver/diagserver_default.c.vvlib/v/types/vls_completion.vvlib/v/gen/v/gen.vv fmtwrites constraintsvlib/v/checker/tests/generic_constraint_call_err.outvlib/v/checker/tests/generic_constraint_method_call_err.outvlib/v/checker/tests/generic_constraint_method_body_err.vvvlib/v/types/vls_locals.vit,a,b,errand lambda parameters; branches left outvlib/v/tests/generics/generic_fn_literal_then_lambda_same_param_test.vmapvlib/v/checker/tests/generic_constraint_body_err.outvlib/v/checker/tests/generic_constraint_method_body_err.outvlib/v/checker/tests/generic_constraint_body_err.vvvlib/v/transform/fn.vvlib/v/diagserver/digest.vvlib/v/token/token_test.vvlib/v/tests/generics/generics_from_modules/embedded_generic_struct_init_test.vvlib/v/compiler_tests/cross_output_codegen_test.vvlib/v/types/scoped_check_batches_test.vvlib/v/diagserver/digest_test.vvlib/v/driver/environment_test.vvlib/v/parser/parser_parallel_notd_v3_no_parallel.vvlib/v/tests/generics/generics_from_modules/embedded_generic_method_own_type_param_test.vvlib/v/driver/instance_check_windows.c.vvlib/v/types/checker_comptime.vunsafewalk stops at a function;$if x inin body checksvlib/v/tests/generics/generics_from_modules/method_value_in_generic_body_test.vvlib/v/driver/job_limit_test.vvlib/v/driver/instance_check.vvlib/v/markused/markused.vvlib/v/transform/array.vmapwith a lambdacmd/v/v3_fallback_diagnostics_compiler_error_test.vvlib/v/transform/struct.vvlib/v/driver/native_inputs_serial_build_test.vvlib/v/gen/c/fn.vcmd/v/v3_fallback_diagnostics.v