Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e8f9625
Add failing FCS tooltip tests for overloaded CE custom operators (#11…
May 29, 2026
5fd6c99
Show all overloads in QuickInfo for overloaded CE custom operators (#…
May 29, 2026
6c5c951
Add PR link to release notes entry
May 29, 2026
de10d70
Move release notes entry for #11612 to 11.0.100.md
May 29, 2026
4ba013a
Resolve CE [<CustomOperation>] overload sink to the actually-picked M…
T-Gro Jun 4, 2026
0eb958f
Apply review feedback: simplify CE custom-op deferred sink
T-Gro Jun 4, 2026
2d9b6bc
Second review round: extract helpers, drop unused params, restore joi…
T-Gro Jun 4, 2026
fa5b9f6
Extract deferred CO sink machinery into its own module
T-Gro Jun 4, 2026
e27bbd6
R1 review pass: rename extracted module + helpers, fix stale xrefs
T-Gro Jun 4, 2026
109698d
R2 review pass: add prior-art comment citing TcMethodItemThen
T-Gro Jun 4, 2026
bd68ff6
R3 review pass: use Range.comparer for Dictionary hashing
T-Gro Jun 4, 2026
217769a
Rewrite CheckComputationExpressionsCustomOps via 5-agent + 3-voter co…
T-Gro Jun 4, 2026
7f5043e
Drop mutable record field, Candidates list, and MethInfosEquivByNameA…
T-Gro Jun 4, 2026
0b6f597
Document and test the [mi] vs multi-element MethodGroup case
T-Gro Jun 4, 2026
28ff883
Strip code comments from product code
T-Gro Jun 4, 2026
7e0710e
Merge remote-tracking branch 'origin/main' into fix/issue-11612
Jun 11, 2026
04a4f2d
Merge remote-tracking branch 'origin/main' into fix/issue-11612
T-Gro Jun 23, 2026
67bc84f
Merge remote-tracking branch 'origin/main' into fix/issue-11612
T-Gro Jun 23, 2026
b815763
Merge branch 'main' into fix/issue-11612
T-Gro Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@
* Fix signature conformance: overloaded member with unit parameter `M(())` now matches sig `member M: unit -> unit`. ([Issue #19596](https://github.com/dotnet/fsharp/issues/19596), [PR #19615](https://github.com/dotnet/fsharp/pull/19615))
* Fix `--quiet` not suppressing NuGet restore output on stdout in F# Interactive ([Issue #18086](https://github.com/dotnet/fsharp/issues/18086))
* Reference assembly MVIDs are now deterministic across compiler invocations. Previously, `--refout` / `<ProduceReferenceAssembly>true</ProduceReferenceAssembly>` produced a different MVID every build because the implied signature hash used .NET's randomized `String.GetHashCode()`. ([Issue #19751](https://github.com/dotnet/fsharp/issues/19751), [PR #19801](https://github.com/dotnet/fsharp/pull/19801))
* Parser: recover on unfinished if and binary expressions
([PR #19724](https://github.com/dotnet/fsharp/pull/19724))
* Parser: recover on unfinished if and binary expressions ([PR #19724](https://github.com/dotnet/fsharp/pull/19724))
* Fix QuickInfo / `GetAllUsesOfAllSymbolsInFile` for overloaded CE `[<CustomOperation>]` keywords to report the actually-resolved overload instead of the first-registered one. ([Issue #11612](https://github.com/dotnet/fsharp/issues/11612), [Issue #15206](https://github.com/dotnet/fsharp/issues/15206), [PR #19865](https://github.com/dotnet/fsharp/pull/19865))
* Fix `open` declaration insertion in `.fsx`/`.fsscript` scripts being placed before `#r`/`#load` directives, producing invalid script files. ([Issue #16271](https://github.com/dotnet/fsharp/issues/16271), [PR #19879](https://github.com/dotnet/fsharp/pull/19879))
* Warn FS3888 when a compiler-semantic attribute on a value/member or type/module is present in the `.fs` but missing from the `.fsi`. Such attributes were previously ignored at the consumer side. Under the `ErrorOnMissingSignatureAttribute` preview language feature, FS3888 is an error. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880))
* Emit debug points at a stack-empty position ([PR #19877](https://github.com/dotnet/fsharp/pull/19877))
Expand Down
84 changes: 47 additions & 37 deletions src/Compiler/Checking/Expressions/CheckComputationExpressions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module internal FSharp.Compiler.CheckComputationExpressions
open Internal.Utilities.Library
open FSharp.Compiler.AccessibilityLogic
open FSharp.Compiler.AttributeChecking
open FSharp.Compiler.CheckComputationExpressionsCustomOps
open FSharp.Compiler.CheckExpressionsOps
open FSharp.Compiler.CheckExpressions
open FSharp.Compiler.CheckBasics
Expand Down Expand Up @@ -61,6 +62,7 @@ type ComputationExpressionContext<'a> =
origComp: SynExpr
mWhole: range
emptyVarSpace: LazyWithContext<list<Val> * TcEnv, range>
deferredCustomOpSinks: ResizeArray<DeferredCustomOpSink>
}

let inline noTailCall ceenv = { ceenv with tailCall = false }
Expand Down Expand Up @@ -178,6 +180,26 @@ let transferVarSpaceReferences (expr: Expr) =
for v in vals do
v.SetHasBeenReferenced()

let TryGetCustomOperationName g m (methInfo: MethInfo) : string option =
TryBindMethInfoAttribute
g
m
g.attrib_CustomOperationAttribute
methInfo
IgnoreAttribute // We do not respect this attribute for IL methods
(fun attr ->
// NOTE: right now, we support of custom operations with spaces in them ([<CustomOperation("foo bar")>])
// In the parameterless CustomOperationAttribute - we use the method name, and also allow it to be ````-quoted (member _.``foo bar`` _ = ...)
match attr with
// Empty string and parameterless constructor - we use the method name
| Attrib(unnamedArgs = [ AttribStringArg "" ]) // Empty string as parameter
| Attrib(unnamedArgs = []) -> // No parameters, same as empty string for compat reasons.
Some methInfo.LogicalName
// Use the specified name
| Attrib(unnamedArgs = [ AttribStringArg msg ]) -> Some msg
| _ -> None)
IgnoreAttribute // We do not respect this attribute for provided methods

let hasMethInfo nm cenv env mBuilderVal ad builderTy =
match TryFindIntrinsicOrExtensionMethInfo ResultCollectionSettings.AtMostOneResult cenv env mBuilderVal ad nm builderTy with
| [] -> false
Expand All @@ -198,25 +220,7 @@ let getCustomOperationMethods (cenv: TcFileState) (env: TcEnv) ad mBuilderVal bu
[
for methInfo in allMethInfos do
if IsMethInfoAccessible cenv.amap mBuilderVal ad methInfo then
let nameSearch =
TryBindMethInfoAttribute
cenv.g
mBuilderVal
cenv.g.attrib_CustomOperationAttribute
methInfo
IgnoreAttribute // We do not respect this attribute for IL methods
(fun attr ->
// NOTE: right now, we support of custom operations with spaces in them ([<CustomOperation("foo bar")>])
// In the parameterless CustomOperationAttribute - we use the method name, and also allow it to be ````-quoted (member _.``foo bar`` _ = ...)
match attr with
// Empty string and parameterless constructor - we use the method name
| Attrib(unnamedArgs = [ AttribStringArg "" ]) // Empty string as parameter
| Attrib(unnamedArgs = []) -> // No parameters, same as empty string for compat reasons.
Some methInfo.LogicalName
// Use the specified name
| Attrib(unnamedArgs = [ AttribStringArg msg ]) -> Some msg
| _ -> None)
IgnoreAttribute // We do not respect this attribute for provided methods
let nameSearch = TryGetCustomOperationName cenv.g mBuilderVal methInfo

match nameSearch with
| None -> ()
Expand Down Expand Up @@ -1124,15 +1128,16 @@ let rec TryTranslateComputationExpression
| Some opDatas ->
let opName, _, _, _, _, _, _, _, methInfo = opDatas[0]

// Record the resolution of the custom operation for posterity
let item =
Item.CustomOperation(opName, (fun () -> customOpUsageText ceenv nm), Some methInfo)

// FUTURE: consider whether we can do better than emptyTyparInst here, in order to display instantiations
// of type variables in the quick info provided in the IDE.
CallNameResolutionSink
cenv.tcSink
(nm.idRange, ceenv.env.NameEnv, item, emptyTyparInst, ItemOccurrence.Use, ceenv.env.eAccessRights)
enqueueDeferredCustomOpSink
ceenv.cenv.tcSink
ceenv.env.NameEnv
ceenv.env.eAccessRights
ceenv.deferredCustomOpSinks
nm
opName
(fun () -> customOpUsageText ceenv nm)
(mOpCore.MakeSynthetic())
methInfo

let mkJoinExpr keySelector1 keySelector2 innerPat e =
let mSynthetic = mOpCore.MakeSynthetic()
Expand Down Expand Up @@ -2430,15 +2435,16 @@ and ConsumeCustomOpClauses

let isLikeGroupJoin = customOperationIsLikeZip ceenv nm

// Record the resolution of the custom operation for posterity
let item =
Item.CustomOperation(opName, (fun () -> customOpUsageText ceenv nm), Some methInfo)

// FUTURE: consider whether we can do better than emptyTyparInst here, in order to display instantiations
// of type variables in the quick info provided in the IDE.
CallNameResolutionSink
enqueueDeferredCustomOpSink
ceenv.cenv.tcSink
(nm.idRange, ceenv.env.NameEnv, item, emptyTyparInst, ItemOccurrence.Use, ceenv.env.eAccessRights)
ceenv.env.NameEnv
ceenv.env.eAccessRights
ceenv.deferredCustomOpSinks
nm
opName
(fun () -> customOpUsageText ceenv nm)
(mClause.MakeSynthetic())
methInfo

if isLikeZip || isLikeJoin || isLikeGroupJoin then
errorR (Error(FSComp.SR.tcBinaryOperatorRequiresBody (nm.idText, Option.get (customOpUsageText ceenv nm)), nm.idRange))
Expand Down Expand Up @@ -3024,6 +3030,8 @@ let TcComputationExpression (cenv: TcFileState) env (overallTy: OverallTy) tpenv

let origComp = comp

let deferredCustomOpSinks = ResizeArray<DeferredCustomOpSink>()

let ceenv =
{
cenv = cenv
Expand All @@ -3041,6 +3049,7 @@ let TcComputationExpression (cenv: TcFileState) env (overallTy: OverallTy) tpenv
origComp = origComp
mWhole = mWhole
emptyVarSpace = LazyWithContext.NotLazy([], env)
deferredCustomOpSinks = deferredCustomOpSinks
}

/// Inside the 'query { ... }' use a modified name environment that contains fake 'CustomOperation' entries
Expand Down Expand Up @@ -3114,7 +3123,8 @@ let TcComputationExpression (cenv: TcFileState) env (overallTy: OverallTy) tpenv
| _ -> env

let lambdaExpr, tpenv =
TcExpr cenv (MustEqual(mkFunTy cenv.g builderTy overallTy)) env tpenv lambdaExpr
captureCustomOperationOverloads cenv.tcSink deferredCustomOpSinks (fun () ->
TcExpr cenv (MustEqual(mkFunTy cenv.g builderTy overallTy)) env tpenv lambdaExpr)

// For queries, transfer HasBeenReferenced from compiler-generated varSpace Vals to user Vals
if isQuery then
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

/// Sinks the resolved overload's `MethInfo` at the keyword range of an overloaded
/// `[<CustomOperation>]` usage in a computation expression — fixes #11612 / #15206.
module internal FSharp.Compiler.CheckComputationExpressionsCustomOps

open System.Collections.Generic
open FSharp.Compiler.AccessibilityLogic
open FSharp.Compiler.Infos
open FSharp.Compiler.NameResolution
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FSharp.Compiler.TypedTree
open FSharp.Compiler.TypedTreeOps

[<NoComparison; NoEquality>]
type DeferredCustomOpSink =
{
KeywordRange: range
OpName: string
UsageText: unit -> string option
SyntheticCallRange: range
Fallback: MethInfo
NameEnv: NameResolutionEnv
AccessRights: AccessorDomain
}

let private makeCustomOpResolutionCapturingSink
(forwardTo: ITypecheckResultsSink)
(capturedResolutions: Dictionary<range, string * MethInfo * TyparInstantiation>)
: ITypecheckResultsSink =

let tryCapture (m: range) (item: Item) (tpinst: TyparInstantiation) =
match item with
| Item.MethodGroup(name, [ mi ], _) ->
match capturedResolutions.TryGetValue m with
| true, (expectedName, _, _) when name = expectedName -> capturedResolutions[m] <- (expectedName, mi, tpinst)
| _ -> ()
| _ -> ()

{ new ITypecheckResultsSink with
member _.NotifyEnvWithScope(m, nenv, ad) =
forwardTo.NotifyEnvWithScope(m, nenv, ad)

member _.NotifyExprHasType(ty, nenv, ad, m) =
forwardTo.NotifyExprHasType(ty, nenv, ad, m)

member _.NotifyExprHasTypeSynthetic(ty, nenv, ad, m) =
forwardTo.NotifyExprHasTypeSynthetic(ty, nenv, ad, m)

member _.NotifyNameResolution(endPos, item, tpinst, occurrenceType, nenv, ad, m, replace) =
tryCapture m item tpinst
forwardTo.NotifyNameResolution(endPos, item, tpinst, occurrenceType, nenv, ad, m, replace)

member _.NotifyMethodGroupNameResolution(endPos, item, itemMethodGroup, tpinst, occurrenceType, nenv, ad, m, replace) =
tryCapture m item tpinst
forwardTo.NotifyMethodGroupNameResolution(endPos, item, itemMethodGroup, tpinst, occurrenceType, nenv, ad, m, replace)

member _.NotifyFormatSpecifierLocation(m, numArgs) =
forwardTo.NotifyFormatSpecifierLocation(m, numArgs)

member _.NotifyRelatedSymbolUse(m, item, kind) =
forwardTo.NotifyRelatedSymbolUse(m, item, kind)

member _.NotifyOpenDeclaration openDeclaration =
forwardTo.NotifyOpenDeclaration openDeclaration

member _.CurrentSourceText = forwardTo.CurrentSourceText

member _.FormatStringCheckContext = forwardTo.FormatStringCheckContext
}

let enqueueDeferredCustomOpSink
(sink: TcResultsSink)
(nenv: NameResolutionEnv)
(ad: AccessorDomain)
(queue: ResizeArray<DeferredCustomOpSink>)
(nm: Ident)
opName
usageText
syntheticCallRange
(fallback: MethInfo)
=
let fallbackItem = Item.CustomOperation(opName, usageText, Some fallback)

CallNameResolutionSink sink (nm.idRange, nenv, fallbackItem, emptyTyparInst, ItemOccurrence.Use, ad)

queue.Add
{
KeywordRange = nm.idRange
OpName = opName
UsageText = usageText
SyntheticCallRange = syntheticCallRange
Fallback = fallback
NameEnv = nenv
AccessRights = ad
}

let captureCustomOperationOverloads (sink: TcResultsSink) (queue: ResizeArray<DeferredCustomOpSink>) (action: unit -> 'T) : 'T =
match sink.CurrentSink with
| Some oldSink when queue.Count > 0 ->
let capturedResolutions =
Dictionary<range, string * MethInfo * TyparInstantiation>(Range.comparer)

for entry in queue do
capturedResolutions[entry.SyntheticCallRange] <- (entry.Fallback.LogicalName, entry.Fallback, emptyTyparInst)

let captureSink = makeCustomOpResolutionCapturingSink oldSink capturedResolutions

let result =
use _holder = WithNewTypecheckResultsSink(captureSink, sink)
action ()

for entry in queue do
let _, resolved, tpinst = capturedResolutions[entry.SyntheticCallRange]

if not (MethInfo.MethInfosUseIdenticalDefinitions resolved entry.Fallback) then
Comment thread
T-Gro marked this conversation as resolved.
let item = Item.CustomOperation(entry.OpName, entry.UsageText, Some resolved)

CallNameResolutionSinkReplacing
sink
(entry.KeywordRange, entry.NameEnv, item, tpinst, ItemOccurrence.Use, entry.AccessRights)

result
| _ -> action ()
1 change: 1 addition & 0 deletions src/Compiler/FSharp.Compiler.Service.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@
<Compile Include="Checking\Expressions\CheckExpressions.fs" />
<Compile Include="Checking\CheckPatterns.fsi" />
<Compile Include="Checking\CheckPatterns.fs" />
<Compile Include="Checking\Expressions\CheckComputationExpressionsCustomOps.fs" />
<Compile Include="Checking\Expressions\CheckComputationExpressions.fsi" />
<Compile Include="Checking\Expressions\CheckComputationExpressions.fs" />
<Compile Include="Checking\Expressions\CheckSequenceExpressions.fs" />
Expand Down
Loading
Loading