Semantic Diff

#9085 Complete the constructors of extensible variants

8 commits

799a82f05acda24ba672ceef431b2ac99b418995..pr-9085

bb5311f88 · cknitt + Claude · Oct 27, 2026, 13:06 UTC

Complete the constructors of extensible variants

Why

Editor analysis didn't record the constructors added by type t += ..., and an open type has none in its declaration, so completion offered no constructor where a value of the type was expected. They are now registered as constructors of an open type, and typed completion of an open type offers those declared in the type's file and in the current file. Since other files can extend the type, the exhaustive switch snippet for one adds a _ case.

modifiedDiagnostic completion of an extensible variant

type attr = ..type attr += Str(string) | Num(int) // let a: attr = Nu
text
Before tests/analysis_tests/tests/src/CompletionExtensibleVariant.res
After tests/analysis_tests/tests/src/CompletionExtensibleVariant.res
Nullable, NullNum(_), Nullable, Null

modifiedDiagnostic exhaustive switch snippet

let value: attr = Str("s")// switch valu
rescript
After
switch value {| Str(_) => %todo| Num(_) => %todo| Flag => %todo| _ => %todo}

Review

  • Which extensions are offered? Those in the file that declares the type and in the current file; an extension in a third file is not known to the current file's analysis.
d479914b7 · cknitt + Claude · Oct 27, 2026, 17:50 UTC

Match extension constructors by type and name them by module path

Codex review on #9085: extensions were matched to an open type by its last name only, so A.t got the constructors of B.t, and a constructor declared in a nested module was inserted without its module, which doesn't compile because an extension constructor can't be found from the expected type. An extension now records the type it extends and is matched by that type's file and stamp, and it is named by its module path, for example Sub.Flag; a prefix may match the last part.

e3dfe393a · cknitt + Claude · Oct 27, 2026, 20:29 UTC

Name extension constructors relative to where completion runs

Codex review on #9085: extension constructors were named relative to the root of their file and to the file of the completed value. Inside the declaring module that inserted Sub.Flag, which is not in scope there, and for a value imported from another file the constructors were not qualified with its module, while extensions in the edited file were missed. A completion request now records the edited file and the module path at the cursor; constructors of that file drop the module path shared with the cursor, constructors of other files get their module's name, and extensions are collected from the edited file.

7cb8065b5 · cknitt + Claude · Oct 27, 2026, 21:45 UTC

Qualify extension constructors of namespaced modules by source path

Codex review on #9085: the module name of another file comes from its compiled name, which in a namespaced project is Base-MyNamespace, so completion inserted Base-MyNamespace.Flag. The name is turned into its source path MyNamespace.Base first.

e7b22c157 · cknitt + Claude · Oct 28, 2026, 07:47 UTC

Offer extensions in scope at the cursor and from interface modules

Codex review on #9085: an extension of the edited file declared after the cursor was offered although it isn't in scope there, and an extension inside a module of an interface (module Sub: { type attr += Flag }) was never registered, because only typed-tree extensions were. The completion scope now records the cursor position and skips later extensions of the edited file, and extensions in signatures are registered too.

5acbbcf83 · cknitt + Claude · Oct 29, 2026, 06:34 UTC

Qualify extensions of nested interface modules and complete their payloads

Codex review on #9085: an extension two modules deep in an interface got only the outer module in its path, because the signature walk didn't extend the module path when entering a module, and completion inside the payload of a qualified constructor such as Sub.Toggle(...) found no constructor, because the payload position keeps only Toggle. Signature modules now extend the path, and payload lookup also matches the last part of a qualified constructor name.

720904e4c · cknitt + Claude · Oct 29, 2026, 10:10 UTC

Type extension payloads in their own file and match qualified constructors

Codex review on #9085: a payload type declared next to the extension, as in type Base.attr += Color(color) with a local color, was resolved in the open type's file, so payload completion lost it; and of two same-named extensions, B.Value(...) completed the payload of the first one, A.Value, because the payload position kept only Value. Each extension constructor now carries the environment of its file, and the payload position keeps the written path: the constructor sharing the longest suffix with it is meant, ties going to the most recent open, then to the last declared one.

3f10535df · cknitt + Claude · Oct 29, 2026, 10:33 UTC

Look up unqualified extension payloads in the scope at the cursor

Codex review on #9085: ranking same-named extension constructors let an open outrank a local extension declared after it, so after open A and a later local Value(...), completion used A.Value's payload; and an extension inside a module type, module type S = { type attr += Phantom }, was offered as S.Phantom and added to exhaustive switches. A qualified constructor is now the one its written path names, and an unqualified one is looked up in the completion scope as the compiler does, which now records extension constructors, the most recent declaration or open winning. Extensions in module types are skipped.