Skip to content

Complete the constructors of extensible variants - #9085

Open
cknitt wants to merge 18 commits into
masterfrom
fix-9073-extensible-variant-completion
Open

cknitt wants to merge 18 commits into
masterfrom
fix-9073-extensible-variant-completion

Conversation

@cknitt

@cknitt cknitt commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Fixes #9073.

Analysis didn't record constructors added by type t += ..., so completion offered none for an extensible variant. They are now registered as constructors of an open type. Typed completion of an open type, in both expressions and patterns, offers the ones declared in the type's file and in the current file. The exhaustive-switch snippet for an open type ends with a _ case, since other files can extend it.

New analysis test: CompletionExtensibleVariant.

Constructors are named so that they are in scope where completion runs: unqualified inside their declaring module, Sub.Flag elsewhere in the file, and Module.Flag from another file, using source paths in namespaced projects.

Known limitation: an extension declared inside a module whose signature hides it (module Hidden: {} = { type attr += Flag }) is still offered. Filtering through the visible signature is left for a follow-up.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Changelog

🐛 Bug fix

  • Editor completion offers the constructors of an extensible variant declared in the current file or in the type's file, and the exhaustive switch snippet for one ends with a _ case.

Semantic Diff

18 commits · Full page

Complete the constructors of extensible variants

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

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.

Diagnostic modified (completion of an extensible variant)

type attr = ..
type attr += Str(string) | Num(int)

// let a: attr = Nu

text · tests/analysis_tests/tests/src/CompletionExtensibleVariant.res

-Nullable, Null
+Num(_), Nullable, Null

Diagnostic modified (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.
Match extension constructors by type and name them by module path

5639dff · cknitt + Claude · Oct 27, 2026, 17:50 UTC

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.

Name extension constructors relative to where completion runs

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

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.

Qualify extension constructors of namespaced modules by source path

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

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.

Offer extensions in scope at the cursor and from interface modules

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

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.

Qualify extensions of nested interface modules and complete their payloads

52ee62d · cknitt + Claude · Oct 29, 2026, 06:34 UTC

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.

Type extension payloads in their own file and match qualified constructors

b03db7b · cknitt + Claude · Oct 29, 2026, 10:10 UTC

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.

Look up unqualified extension payloads in the scope at the cursor

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

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.

Keep a catch-all when expanding or switching on an open type

fb97e50 · cknitt + Claude · Oct 29, 2026, 18:02 UTC

Why

Codex review on #9085: now that an open type extracts as a variant, the Expand catch-all and Exhaustive switch code actions replaced or built the switch from the constructors known here, dropping the _ that matches constructors added in other files.

Codeaction modified (Expand catch-all)

type attr = ..
type attr += Str(string) | Num(int)
let f = (value: attr) =>
  switch value {
  | Str(_) => "str"
  | _ => "other"
  }

text · the _ is replaced by

-Num(_)
+Num(_) | _

Codeaction modified (Exhaustive switch)

type attr = ..
type attr += Str(string) | Num(int)
let value: attr = Str("s")
value

rescript

 switch value {
 | Str(_) => %todo
 | Num(_) => %todo
+| _ => %todo
 }

Review

  • Why keep the _ as an alternative instead of a separate case? Replacing only the pattern needs no copy of the case body, and the expanded or-pattern lists the known constructors for splitting by hand.
Resolve an open's extension constructor only through its module

347aa66 · cknitt + Claude · Oct 29, 2026, 18:03 UTC

Why

Codex review on #9085: an open A after a local extension Value resolved the payload of an unqualified Value(...) to the local constructor. Matching A.Value against the constructors also accepted the bare local Value as a suffix, and the later declaration won. A constructor named without a module is declared around the cursor, so an open no longer brings it into scope.

Completion modified (extension payload after an open)

type attr = ..
module A = {
  type attr += Value(bool)
}
type attr += Value(option<int>)
open A
let x: attr = Value()

text · payload completion inside Value() completes

-option<int>, the payload of the local Value
+bool, the payload of A.Value: true, false
Resolve module aliases in the path of an extension constructor

fbbb47d · cknitt + Claude · Nov 01, 2026, 06:37 UTC

Why

Codex review on #9085: an extension constructor written through a module alias, Alias.Value(...), was matched by its text against the recorded B.Value, so the payload of another constructor, or none, was completed. After open Alias the lookup found no constructor of the alias and, with nothing left to pick from, crashed on an empty list. The written path, qualified or from an open, now has its leading module alias declared in the scope at the cursor replaced by the path it aliases before the constructor is selected.

Completion modified (extension payload through a module alias)

type attr = ..
module B = {
  type attr += Value(option<int>)
}
type attr += Value(bool)
module Alias = B
let x: attr = Alias.Value()

text · payload completion inside Alias.Value() completes

-bool, the payload of the local Value
+option<int>, the payload of B.Value: None, Some(_)

Completion modified (extension payload after an open of an alias)

type attr = ..
module B = {
  type attr += Value(option<int>)
}
module Alias = B
let v: attr = B.Value(None)
open Alias
switch v { | Value() }

text · Before: payload completion inside Value() · After: payload completion inside Value() completes

-Fatal error: exception Invalid_argument("List.nth")
+option<int>, the payload of B.Value: None, Some(_)

Review

  • Why only a leading alias? The path is named from the cursor, so its first module is the one the scope there declares; an alias deeper in the path would need the enclosing module's structure, which the naming of extension constructors doesn't walk.
Name extension constructors in code actions from the action's scope

7735c23 · cknitt + Claude · Nov 01, 2026, 06:37 UTC

Why

Codex review on #9085: the Expand catch-all and Exhaustive switch code actions found the type of the switched expression through completion, whose scope ended before they extracted the constructors of an open type. Extension constructors were then named from the top of the file and not filtered by position, so inside module Nested the action wrote Nested.Inner, which isn't in scope there, and offered constructors declared after the action. The scope now lasts through the whole action: Xform.with_type_of_expr runs the action in it.

Codeaction modified (Exhaustive switch)

type attr = ..
type attr += Str(string)
let value: attr = Str("s")
module Nested = {
  type attr += Inner
  value
  type attr += AfterAction
}

rescript · on value

 switch value {
 | Str(_) => %todo
-| Nested.Inner => %todo
-| Nested.AfterAction => %todo
+| Inner => %todo
 | _ => %todo
 }

Codeaction modified (Expand catch-all)

type attr = ..
type attr += Str(string) | Num(int)
let value: attr = Str("s")
let f = () =>
  switch value {
  | Str(_) => "str"
  | _ => "other"
  }
module Nested = {
  type attr += Inner
}

text · the _ is replaced by

-Num(_) | Nested.Inner | _
+Num(_) | _

Review

  • Why keep the scope in the global instead of passing it to Type_utils.extract_type? Extension constructors are named deep inside type extraction, which completion, hover and code generation reach through many callers; a continuation around the code action keeps the existing mechanism and changes only its three call sites.
Look up every extension constructor path in one walk of the scope

a2c632c · cknitt + Claude · Nov 01, 2026, 07:10 UTC

Why

Review of #9085: after several review rounds, payload constructors were found in three ways, a scope walk for unqualified names, a text match with suffixes in both directions for qualified paths, and module alias resolution on top. A qualified path ignored the opens in scope, so after open Outer the path Inner.Deep still named a local Inner declared before the open. One walk of the scope now resolves every path as the compiler does: the most recent declaration of the constructor, of the path's first module (unaliased), or open naming it decides; a path that names its modules from further out than the cursor stays the fallback.

Completion modified (qualified extension payload after an open)

type attr = ..
type color = Red | Blue
module Outer = {
  module Inner = {
    type attr += Deep(bool)
  }
}
module Inner = {
  type attr += Deep(option<color>)
}
open Outer
let x: attr = Inner.Deep()

text · payload completion inside Inner.Deep() completes

-option<color>, the payload of the local Inner.Deep
+bool, the payload of Outer.Inner.Deep: true, false

Review

  • Why does an open try only the exact path? Constructors are named from the cursor, so the opened module's path followed by the written one is the name of the constructor it brings into scope; a suffix match accepted unrelated constructors, and recursing into the outer opens would be exponential in their number.
Show signature help for qualified extension constructors

ed70932 · cknitt + Claude · Nov 01, 2026, 07:10 UTC

Why

Review of #9085: signature help looked up the constructor of an open type by its last name, Toggle, among constructors named by their module path, Sub.Toggle, and found none. It now compares the last part of the path.

Signaturehelp modified (qualified extension constructor)

type attr = ..
module Sub = {
  type attr += Toggle(bool)
}
let toggled: attr = Sub.Toggle(true)

text · signature help inside Sub.Toggle(true)

-null
+Sub.Toggle(bool), parameter 0 active
Keep the completion scope and an open type's catch-all in Type_utils

aeda3bf · cknitt + Claude · Nov 01, 2026, 07:11 UTC

Why

Review of #9085: the scope that names extension constructors was set through two wrappers, Type_utils' and one in Completion_back_end that only added the module path at the cursor, and the catch-all an open type adds to a switch was decided in Xform and again in Completion_back_end. Type_utils.with_completion_scope now computes the module path itself and is the one wrapper, used by completion and by code actions, and Type_utils.open_type_catch_all serves the exhaustive switch completion and both catch-all expansions. Analysis output is unchanged.

Test extensions of interface modules in one fixture

284953f · cknitt + Claude · Nov 01, 2026, 07:12 UTC

Why

Review of #9085: CompletionExtensibleIface and CompletionExtensiblePayloadIface both declared an open type with extensions in modules of an interface, one to test their names, the other the payload of one of them. The payload case moves into CompletionExtensibleIface, whose Sub now also declares Toned(tone), and the second fixture is removed. The exhaustive switch of the remaining fixture gains a Toned(_) case; the payload completion is the one the removed fixture had.

✨ Resolve the written constructor path in signature help

c41f3c8 · cknitt + Claude · Nov 01, 2026, 07:54 UTC · new

Why

Codex review on #9085: signature help kept only the last name of the constructor and took the first extension constructor of that name, so inside B.Value(...) it showed A.Value(bool). It now resolves the written path with the lookup payload completion uses, in the scope at the cursor. That scope comes from Completion_front_end.scope_at, the scope the completion parser has reached at the cursor, since a payload being typed usually offers nothing to complete.

Signaturehelp modified (same-named extension constructors)

type attr = ..
module A = {
  type attr += Value(bool)
}
module B = {
  type attr += Value(option<int>)
}
open B
let b: attr = B.Value(None)
let unqualified: attr = Value(None)

text · signature help inside B.Value(None) and Value(None)

-A.Value(bool)
+B.Value(option<int>)
✨ Offer extensions of expression-local modules only in their block

0e45dd0 · cknitt + Claude · Nov 01, 2026, 07:57 UTC · new

Why

Codex review on #9085: an extension declared in a module inside an expression, such as module Local = {...} in a function body, was offered after the function, and from other files, where Local is not in scope. The contents of such a module now get a module path that starts out of sight (NotVisible, as the module itself already had), and its extensions are offered only in the current file where the scope's most recent module of that name is that local module, declared before the extension.

Completion modified (extension of an expression-local module)

type attr = ..
type attr += Str(string)
let make = () => {
  module Local = {
    type attr += InLocal
  }
  let inBlock: attr = InL
  Local.InLocal
}
let afterMake: attr = InL

text · completion of InL inside the block and after make

-Local.InLocal, Local.InLocal
+Local.InLocal, nothing

Codeaction modified (Expand catch-all after the block)

let w = switch value {
| Str(_) => 1
| _ => 2
}

text · the _ is replaced by

-Local.InLocal | _
+_

Review

  • Why match the local module by name and position instead of its block's extent? The completion scope already holds the modules in scope at the cursor with their locations; requiring the most recent one of the name to be a local module declared before the extension needs no new data from the typed tree. Inside the local module's own structure the module isn't in scope yet, so its extensions are not offered there.

@codecov

codecov Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.17094% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.80%. Comparing base (25e3dfb) to head (0e45dd0).
⚠️ Report is 10 commits behind head on master.

Files with missing lines Patch % Lines
analysis/src/type_utils.ml 88.67% 18 Missing ⚠️
analysis/src/xform.ml 82.35% 3 Missing ⚠️
analysis/src/completion_front_end.ml 81.81% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9085      +/-   ##
==========================================
+ Coverage   81.72%   81.80%   +0.08%     
==========================================
  Files         460      460              
  Lines       64817    65145     +328     
==========================================
+ Hits        52969    53291     +322     
- Misses      11848    11854       +6     
Files with missing lines Coverage Δ
analysis/src/completion_back_end.ml 81.72% <100.00%> (+0.23%) ⬆️
analysis/src/completion_expressions.ml 88.52% <100.00%> (+1.96%) ⬆️
analysis/src/completion_patterns.ml 88.88% <100.00%> (+0.31%) ⬆️
analysis/src/process_cmt.ml 85.67% <100.00%> (+1.25%) ⬆️
analysis/src/shared_types.ml 81.03% <ø> (+0.34%) ⬆️
analysis/src/signature_help.ml 80.43% <100.00%> (+0.06%) ⬆️
analysis/src/completion_front_end.ml 81.96% <81.81%> (+0.34%) ⬆️
analysis/src/xform.ml 88.64% <82.35%> (+0.19%) ⬆️
analysis/src/type_utils.ml 69.89% <88.67%> (+3.72%) ⬆️

... and 43 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new

pkg-pr-new Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@9085

@rescript/belt

npm i https://pkg.pr.new/@rescript/belt@9085

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@9085

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@9085

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@9085

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@9085

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@9085

commit: 0e45dd0

@github-actions

Copy link
Copy Markdown

@cknitt
cknitt marked this pull request as ready for review October 27, 2026 14:20
@cknitt

cknitt commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-01T08:16:33.547993Z 0e45dd0 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98ecc79d93

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread analysis/src/type_utils.ml Resolved
Comment thread analysis/src/type_utils.ml Resolved
cknitt added a commit that referenced this pull request Oct 27, 2026
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt

cknitt commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f678bb32e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread analysis/src/type_utils.ml Resolved
Comment thread analysis/src/type_utils.ml Resolved
cknitt added a commit that referenced this pull request Oct 27, 2026
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt

cknitt commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56cff12ba2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread analysis/src/type_utils.ml Resolved
Comment thread analysis/src/type_utils.ml Resolved
cknitt added a commit that referenced this pull request Oct 27, 2026
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt

cknitt commented Oct 28, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3408b27a6e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread analysis/src/type_utils.ml Resolved
Comment thread analysis/src/process_cmt.ml Resolved
cknitt added a commit that referenced this pull request Oct 28, 2026
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt
cknitt force-pushed the fix-9073-extensible-variant-completion branch from 3408b27 to 84e1386 Compare October 28, 2026 08:10
cknitt added a commit that referenced this pull request Oct 28, 2026
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Signed-off-by: Christoph Knittel <ck@cca.io>
cknitt added a commit that referenced this pull request Oct 28, 2026
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Signed-off-by: Christoph Knittel <ck@cca.io>
cknitt added a commit that referenced this pull request Oct 28, 2026
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt

cknitt commented Oct 28, 2026

Copy link
Copy Markdown
Member Author

@codex review

@cknitt cknitt mentioned this pull request Oct 28, 2026
8 of 12 tasks

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 84e1386175

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread analysis/src/process_cmt.ml Resolved
Comment thread analysis/src/type_utils.ml Resolved
cknitt added a commit that referenced this pull request Oct 29, 2026
…loads

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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt

cknitt commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

@codex review

1 similar comment
@cknitt

cknitt commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3cb8834566

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread analysis/src/type_utils.ml Resolved
Comment thread analysis/src/type_utils.ml Resolved
cknitt added a commit that referenced this pull request Oct 29, 2026
…ctors

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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gGkPma9LAZimuQjnrP
Signed-off-by: Christoph Knittel <ck@cca.io>
cknitt added a commit that referenced this pull request Oct 29, 2026
…loads

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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
cknitt added a commit that referenced this pull request Oct 29, 2026
…ctors

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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gGkPma9LAZimuQjnrP
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
cknitt added a commit that referenced this pull request Oct 29, 2026
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gGkPma9LAZimuQjnrP
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
cknitt added a commit that referenced this pull request Oct 29, 2026
Why:
    Codex review on #9085: now that an open type extracts as a variant,
    the Expand catch-all and Exhaustive switch code actions replaced or
    built the switch from the constructors known here, dropping the `_`
    that matches constructors added in other files.

Codeaction modified (Expand catch-all):
Example:
    type attr = ..
    type attr += Str(string) | Num(int)
    let f = (value: attr) =>
      switch value {
      | Str(_) => "str"
      | _ => "other"
      }
Before (text): the `_` is replaced by
    Num(_)
After (text): the `_` is replaced by
    Num(_) | _

Codeaction modified (Exhaustive switch):
Example:
    type attr = ..
    type attr += Str(string) | Num(int)
    let value: attr = Str("s")
    value
Before (rescript):
    switch value {
    | Str(_) => %todo
    | Num(_) => %todo
    }
After (rescript):
    switch value {
    | Str(_) => %todo
    | Num(_) => %todo
    | _ => %todo
    }

Review: Why keep the `_` as an alternative instead of a separate case?
Replacing only the pattern needs no copy of the case body, and the
expanded or-pattern lists the known constructors for splitting by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
cknitt added a commit that referenced this pull request Oct 29, 2026
Why:
    Codex review on #9085: an `open A` after a local extension `Value`
    resolved the payload of an unqualified `Value(...)` to the local
    constructor. Matching `A.Value` against the constructors also accepted
    the bare local `Value` as a suffix, and the later declaration won. A
    constructor named without a module is declared around the cursor, so
    an open no longer brings it into scope.

Completion modified (extension payload after an open):
Example:
    type attr = ..
    module A = {
      type attr += Value(bool)
    }
    type attr += Value(option<int>)
    open A
    let x: attr = Value()
Before (text): payload completion inside `Value()` completes
    option<int>, the payload of the local Value
After (text): payload completion inside `Value()` completes
    bool, the payload of A.Value: true, false

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt

cknitt commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 417ad95d74

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread analysis/src/type_utils.ml Resolved
Comment thread analysis/src/completion_back_end.ml Resolved
cknitt and others added 10 commits October 30, 2026 09:04
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.

Diagnostic modified (completion of an extensible variant):
Example:
    type attr = ..
    type attr += Str(string) | Num(int)

    // let a: attr = Nu
Before (text): tests/analysis_tests/tests/src/CompletionExtensibleVariant.res
    Nullable, Null
After (text): tests/analysis_tests/tests/src/CompletionExtensibleVariant.res
    Num(_), Nullable, Null

Diagnostic modified (exhaustive switch snippet):
Example:
    let value: attr = Str("s")
    // switch valu
After (rescript):
    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.

Fixes: #9073
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
…loads

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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
…ctors

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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gGkPma9LAZimuQjnrP
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gGkPma9LAZimuQjnrP
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
    Codex review on #9085: now that an open type extracts as a variant,
    the Expand catch-all and Exhaustive switch code actions replaced or
    built the switch from the constructors known here, dropping the `_`
    that matches constructors added in other files.

Codeaction modified (Expand catch-all):
Example:
    type attr = ..
    type attr += Str(string) | Num(int)
    let f = (value: attr) =>
      switch value {
      | Str(_) => "str"
      | _ => "other"
      }
Before (text): the `_` is replaced by
    Num(_)
After (text): the `_` is replaced by
    Num(_) | _

Codeaction modified (Exhaustive switch):
Example:
    type attr = ..
    type attr += Str(string) | Num(int)
    let value: attr = Str("s")
    value
Before (rescript):
    switch value {
    | Str(_) => %todo
    | Num(_) => %todo
    }
After (rescript):
    switch value {
    | Str(_) => %todo
    | Num(_) => %todo
    | _ => %todo
    }

Review: Why keep the `_` as an alternative instead of a separate case?
Replacing only the pattern needs no copy of the case body, and the
expanded or-pattern lists the known constructors for splitting by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
    Codex review on #9085: an `open A` after a local extension `Value`
    resolved the payload of an unqualified `Value(...)` to the local
    constructor. Matching `A.Value` against the constructors also accepted
    the bare local `Value` as a suffix, and the later declaration won. A
    constructor named without a module is declared around the cursor, so
    an open no longer brings it into scope.

Completion modified (extension payload after an open):
Example:
    type attr = ..
    module A = {
      type attr += Value(bool)
    }
    type attr += Value(option<int>)
    open A
    let x: attr = Value()
Before (text): payload completion inside `Value()` completes
    option<int>, the payload of the local Value
After (text): payload completion inside `Value()` completes
    bool, the payload of A.Value: true, false

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt
cknitt force-pushed the fix-9073-extensible-variant-completion branch from 417ad95 to 347aa66 Compare October 30, 2026 09:05
cknitt and others added 6 commits November 1, 2026 06:37
Why:
    Codex review on #9085: an extension constructor written through a
    module alias, `Alias.Value(...)`, was matched by its text against the
    recorded `B.Value`, so the payload of another constructor, or none,
    was completed. After `open Alias` the lookup found no constructor of
    the alias and, with nothing left to pick from, crashed on an empty
    list. The written path, qualified or from an open, now has its leading
    module alias declared in the scope at the cursor replaced by the path
    it aliases before the constructor is selected.

Completion modified (extension payload through a module alias):
Example:
    type attr = ..
    module B = {
      type attr += Value(option<int>)
    }
    type attr += Value(bool)
    module Alias = B
    let x: attr = Alias.Value()
Before (text): payload completion inside `Alias.Value()` completes
    bool, the payload of the local Value
After (text): payload completion inside `Alias.Value()` completes
    option<int>, the payload of B.Value: None, Some(_)

Completion modified (extension payload after an open of an alias):
Example:
    type attr = ..
    module B = {
      type attr += Value(option<int>)
    }
    module Alias = B
    let v: attr = B.Value(None)
    open Alias
    switch v { | Value() }
Before (text): payload completion inside `Value()`
    Fatal error: exception Invalid_argument("List.nth")
After (text): payload completion inside `Value()` completes
    option<int>, the payload of B.Value: None, Some(_)

Review: Why only a leading alias? The path is named from the cursor, so
its first module is the one the scope there declares; an alias deeper in
the path would need the enclosing module's structure, which the naming of
extension constructors doesn't walk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
    Codex review on #9085: the Expand catch-all and Exhaustive switch code
    actions found the type of the switched expression through completion,
    whose scope ended before they extracted the constructors of an open
    type. Extension constructors were then named from the top of the file
    and not filtered by position, so inside `module Nested` the action
    wrote `Nested.Inner`, which isn't in scope there, and offered
    constructors declared after the action. The scope now lasts through
    the whole action: `Xform.with_type_of_expr` runs the action in it.

Codeaction modified (Exhaustive switch):
Example:
    type attr = ..
    type attr += Str(string)
    let value: attr = Str("s")
    module Nested = {
      type attr += Inner
      value
      type attr += AfterAction
    }
Before (rescript): on `value`
    switch value {
    | Str(_) => %todo
    | Nested.Inner => %todo
    | Nested.AfterAction => %todo
    | _ => %todo
    }
After (rescript): on `value`
    switch value {
    | Str(_) => %todo
    | Inner => %todo
    | _ => %todo
    }

Codeaction modified (Expand catch-all):
Example:
    type attr = ..
    type attr += Str(string) | Num(int)
    let value: attr = Str("s")
    let f = () =>
      switch value {
      | Str(_) => "str"
      | _ => "other"
      }
    module Nested = {
      type attr += Inner
    }
Before (text): the `_` is replaced by
    Num(_) | Nested.Inner | _
After (text): the `_` is replaced by
    Num(_) | _

Review: Why keep the scope in the global instead of passing it to
`Type_utils.extract_type`? Extension constructors are named deep inside
type extraction, which completion, hover and code generation reach
through many callers; a continuation around the code action keeps the
existing mechanism and changes only its three call sites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
    Review of #9085: after several review rounds, payload constructors
    were found in three ways, a scope walk for unqualified names, a
    text match with suffixes in both directions for qualified paths, and
    module alias resolution on top. A qualified path ignored the opens in
    scope, so after `open Outer` the path `Inner.Deep` still named a local
    `Inner` declared before the open. One walk of the scope now resolves
    every path as the compiler does: the most recent declaration of the
    constructor, of the path's first module (unaliased), or open naming
    it decides; a path that names its modules from further out than the
    cursor stays the fallback.

Completion modified (qualified extension payload after an open):
Example:
    type attr = ..
    type color = Red | Blue
    module Outer = {
      module Inner = {
        type attr += Deep(bool)
      }
    }
    module Inner = {
      type attr += Deep(option<color>)
    }
    open Outer
    let x: attr = Inner.Deep()
Before (text): payload completion inside `Inner.Deep()` completes
    option<color>, the payload of the local Inner.Deep
After (text): payload completion inside `Inner.Deep()` completes
    bool, the payload of Outer.Inner.Deep: true, false

Review: Why does an open try only the exact path? Constructors are
named from the cursor, so the opened module's path followed by the
written one is the name of the constructor it brings into scope; a
suffix match accepted unrelated constructors, and recursing into the
outer opens would be exponential in their number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
    Review of #9085: signature help looked up the constructor of an open
    type by its last name, `Toggle`, among constructors named by their
    module path, `Sub.Toggle`, and found none. It now compares the last
    part of the path.

Signaturehelp modified (qualified extension constructor):
Example:
    type attr = ..
    module Sub = {
      type attr += Toggle(bool)
    }
    let toggled: attr = Sub.Toggle(true)
Before (text): signature help inside `Sub.Toggle(true)`
    null
After (text): signature help inside `Sub.Toggle(true)`
    Sub.Toggle(bool), parameter 0 active

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
    Review of #9085: the scope that names extension constructors was set
    through two wrappers, Type_utils' and one in Completion_back_end that
    only added the module path at the cursor, and the catch-all an open
    type adds to a switch was decided in Xform and again in
    Completion_back_end. `Type_utils.with_completion_scope` now computes
    the module path itself and is the one wrapper, used by completion and
    by code actions, and `Type_utils.open_type_catch_all` serves the
    exhaustive switch completion and both catch-all expansions. Analysis
    output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
    Review of #9085: CompletionExtensibleIface and
    CompletionExtensiblePayloadIface both declared an open type with
    extensions in modules of an interface, one to test their names, the
    other the payload of one of them. The payload case moves into
    CompletionExtensibleIface, whose `Sub` now also declares
    `Toned(tone)`, and the second fixture is removed. The exhaustive
    switch of the remaining fixture gains a `Toned(_)` case; the payload
    completion is the one the removed fixture had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt

cknitt commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 284953f3d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread analysis/src/signature_help.ml Resolved
Comment thread analysis/src/type_utils.ml Resolved
cknitt and others added 2 commits November 1, 2026 07:54
Why:
    Codex review on #9085: signature help kept only the last name of the
    constructor and took the first extension constructor of that name, so
    inside `B.Value(...)` it showed `A.Value(bool)`. It now resolves the
    written path with the lookup payload completion uses, in the scope at
    the cursor. That scope comes from `Completion_front_end.scope_at`, the
    scope the completion parser has reached at the cursor, since a
    payload being typed usually offers nothing to complete.

Signaturehelp modified (same-named extension constructors):
Example:
    type attr = ..
    module A = {
      type attr += Value(bool)
    }
    module B = {
      type attr += Value(option<int>)
    }
    open B
    let b: attr = B.Value(None)
    let unqualified: attr = Value(None)
Before (text): signature help inside `B.Value(None)` and `Value(None)`
    A.Value(bool)
After (text): signature help inside `B.Value(None)` and `Value(None)`
    B.Value(option<int>)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
    Codex review on #9085: an extension declared in a module inside an
    expression, such as `module Local = {...}` in a function body, was
    offered after the function, and from other files, where `Local` is
    not in scope. The contents of such a module now get a module path
    that starts out of sight (`NotVisible`, as the module itself already
    had), and its extensions are offered only in the current file where
    the scope's most recent module of that name is that local module,
    declared before the extension.

Completion modified (extension of an expression-local module):
Example:
    type attr = ..
    type attr += Str(string)
    let make = () => {
      module Local = {
        type attr += InLocal
      }
      let inBlock: attr = InL
      Local.InLocal
    }
    let afterMake: attr = InL
Before (text): completion of `InL` inside the block and after `make`
    Local.InLocal, Local.InLocal
After (text): completion of `InL` inside the block and after `make`
    Local.InLocal, nothing

Codeaction modified (Expand catch-all after the block):
Example:
    let w = switch value {
    | Str(_) => 1
    | _ => 2
    }
Before (text): the `_` is replaced by
    Local.InLocal | _
After (text): the `_` is replaced by
    _

Review: Why match the local module by name and position instead of its
block's extent? The completion scope already holds the modules in scope
at the cursor with their locations; requiring the most recent one of the
name to be a local module declared before the extension needs no new
data from the typed tree. Inside the local module's own structure the
module isn't in scope yet, so its extensions are not offered there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt

cknitt commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e45dd058d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let opened =
List.filteri (fun i _ -> i < List.length opened - 1) opened
in
match exact (unalias scope (opened @ path)) with

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve opened module paths through preceding opens

When a module is opened through an earlier open, payload lookup cannot resolve its constructors. For example, with Outer.Inner.Value(bool) extending the open type, followed by open Outer and open Inner, completing inside unqualified Value(...) finds no constructor: this branch compares Inner.Value against the recorded Outer.Inner.Value, while unalias only follows module aliases and ignores preceding opens. Expression and pattern payload completion consequently lose boolean suggestions, and signature help also fails. Resolve the opened module in its declaration scope before matching its constructor.

Useful? React with 👍 / 👎.

Comment on lines +569 to +571
| Some loc ->
is_local_module loc && Loc.start loc <= Loc.start declared.extent_loc
| None -> false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain local extensions inside their declaring module body

For an expression-local module containing both an extension and code using it, this check excludes the extension even inside that module. For example, inside module Local = { type attr += Flag; ... } within a function, typed completion of an attr expression after the declaration cannot offer Flag. The Pexp_letmodule traversal in completion_front_end.ml adds Local to the scope only after traversing its module expression, so no matching Module scope item exists while visiting its body and this branch returns false. Recognize positions inside the declaring local module as visible as well as positions in the subsequent binding body.

Useful? React with 👍 / 👎.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No completion for extensible variant constructors

1 participant