Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
475b111
Add failing test: namespace global roundtrip (found by corpus sweep)
T-Gro Apr 15, 2026
cf2775c
Add sig-gen roundtrip failures from corpus sweep (positive code only)
T-Gro Apr 15, 2026
1f36673
Fix #19597: single-case struct DU gets spurious bar in signature
T-Gro Apr 15, 2026
34e3868
Fix #19592: backticked active pattern case names lose escaping in sig
T-Gro Apr 15, 2026
66a2bc5
Fix #19595: type params with special chars get backtick escaping
T-Gro Apr 16, 2026
985f830
Fix #19593: namespace global dropped from generated signature
T-Gro Apr 16, 2026
91f19b1
Add skipped test for #19596: overloaded member with unit parameter
T-Gro Apr 16, 2026
8325bc8
Fix #19594: SRTP constraints use explicit type param syntax in signat…
T-Gro Apr 16, 2026
72fe41f
Add tooltip tests for signature generation display changes
T-Gro Apr 17, 2026
3aca69b
Remove sweep tooling (investigation artifacts, not production test in…
T-Gro Apr 17, 2026
6f0cda6
Add skipped test for #19596: overloaded member with unit parameter
T-Gro Apr 17, 2026
ec0dd10
Add missing roundtrip test for #19595: type param backtick escaping
T-Gro Apr 17, 2026
8784370
Fix namespace global + module layout and unskip 3 passing tests
T-Gro Apr 17, 2026
36ba04a
Add release notes for additional signature generation fixes
T-Gro Apr 17, 2026
9e388f8
Merge branch 'main' into sig-roundtrip-sweep
T-Gro Apr 18, 2026
5570959
Fix formatting in PrettyNaming.fs
T-Gro Apr 18, 2026
53b9e61
Fix PR numbers in release notes
T-Gro Apr 19, 2026
4a0fc41
Fix CI failures from signature roundtrip changes
T-Gro Apr 20, 2026
c075528
Fix #19596: overloaded member with unit parameter conformance check
T-Gro Apr 18, 2026
c373cc0
Update release notes: add #19596 fix, fix PR numbers
T-Gro Apr 18, 2026
03521b2
Add IL verification: M(()) and M() produce identical IL method signat…
T-Gro Apr 19, 2026
e4bc6ae
Add inverse conformance tests for unit param overload
T-Gro Apr 19, 2026
9e5e4d3
Fix release note PR reference and add trailing newline
T-Gro Apr 20, 2026
66e6da3
Merge origin/sig-roundtrip-sweep: resolve conflicts in NicePrint.fs, …
Copilot Apr 21, 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
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
* Fix signature generation: `namespace global` header dropped from generated signature. ([Issue #19593](https://github.com/dotnet/fsharp/issues/19593), [PR #19609](https://github.com/dotnet/fsharp/pull/19609))
* Fix signature generation: SRTP constraints use postfix syntax that fails conformance, now uses explicit type param declarations. ([Issue #19594](https://github.com/dotnet/fsharp/issues/19594), [PR #19609](https://github.com/dotnet/fsharp/pull/19609))
* Fix signature generation: type params with special characters missing backtick escaping. ([Issue #19595](https://github.com/dotnet/fsharp/issues/19595), [PR #19609](https://github.com/dotnet/fsharp/pull/19609))
* 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))

### Added

Expand Down
9 changes: 9 additions & 0 deletions src/Compiler/Checking/NicePrint.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,15 @@ module PrintTastMemberOrVals =
let layoutNonMemberVal denv (tps, v: Val, tau, cxs) =
let env = SimplifyTypes.CollectInfo true [tau] cxs
let cxs = env.postfixConstraints
let hasStaticallyResolvedTypars =
tps |> List.exists (fun (tp: Typar) -> tp.StaticReq = TyparStaticReq.HeadType) &&
not (IsLogicalOpName v.LogicalName)
// When SRTP typars are shown on explicit type param declarations, exclude
// their constraints from the postfix to avoid duplicated "when" clauses.
let cxs =
if hasStaticallyResolvedTypars then
cxs |> List.filter (fun (tp, _) -> tp.StaticReq <> TyparStaticReq.HeadType)
else cxs
let valReprInfo = arityOfValForDisplay v
let argInfos, retTy = GetTopTauTypeInFSharpForm denv.g valReprInfo.ArgInfos tau v.Range
let nameL =
Expand Down
49 changes: 43 additions & 6 deletions src/Compiler/Checking/SignatureConformance.fs
Original file line number Diff line number Diff line change
Expand Up @@ -295,19 +295,36 @@ type Checker(g, amap, denv, remapInfo: SignatureRepackageInfo, checkingSig) =
err(fun(x, y, z) -> FSComp.SR.ValueNotContainedMutabilityGenericParametersDiffer(x, y, z, string mtps, string ntps))
elif implValInfo.KindsOfTypars <> sigValInfo.KindsOfTypars then
err(FSComp.SR.ValueNotContainedMutabilityGenericParametersAreDifferentKinds)
elif not (nSigArgInfos <= implArgInfos.Length && List.forall2 (fun x y -> List.length x <= List.length y) sigArgInfos (fst (List.splitAt nSigArgInfos implArgInfos))) then
else
// Check arg group arities. An empty impl group [] is compatible with
// a singleton sig group [_] when the member takes unit (e.g. member M(()) vs member M: unit -> unit)
let argGroupsCompatible =
nSigArgInfos <= implArgInfos.Length &&
List.forall2
(fun (sigGroup: ArgReprInfo list) (implGroup: ArgReprInfo list) ->
List.length sigGroup <= List.length implGroup
|| (List.length implGroup = 0 && List.length sigGroup = 1))
sigArgInfos
(fst (List.splitAt nSigArgInfos implArgInfos))

if not argGroupsCompatible then
err(fun(x, y, z) -> FSComp.SR.ValueNotContainedMutabilityAritiesDiffer(x, y, z, id.idText, string nSigArgInfos, id.idText, id.idText))
else
let implArgInfos = implArgInfos |> List.truncate nSigArgInfos
let implArgInfos = (implArgInfos, sigArgInfos) ||> List.map2 (fun l1 l2 -> l1 |> List.take l2.Length)
// When impl has empty group [] and sig has [unit_arg], use min to avoid taking more than available
let implArgInfos = (implArgInfos, sigArgInfos) ||> List.map2 (fun l1 l2 -> l1 |> List.take (min l1.Length l2.Length))
// Propagate some information signature to implementation.

// Check the attributes on each argument, and update the ValReprInfo for
// the value to reflect the information in the signature.
// This ensures that the compiled form of the value matches the signature rather than
// the implementation. This also propagates argument names from signature to implementation
let res =
(implArgInfos, sigArgInfos) ||> List.forall2 (List.forall2 (fun implArgInfo sigArgInfo ->
(implArgInfos, sigArgInfos) ||> List.forall2 (fun (implGroup: ArgReprInfo list) (sigGroup: ArgReprInfo list) ->
// When impl group is empty (unit param like member M(())), skip arg-level checks
if implGroup.IsEmpty then true
else
(implGroup, sigGroup) ||> List.forall2 (fun implArgInfo sigArgInfo ->
checkAttribs aenv (implArgInfo.Attribs.AsList()) (sigArgInfo.Attribs.AsList()) (fun attribs ->
match implArgInfo.Name, sigArgInfo.Name with
| Some iname, Some sname when sname.idText <> iname.idText ->
Expand Down Expand Up @@ -661,7 +678,7 @@ type Checker(g, amap, denv, remapInfo: SignatureRepackageInfo, checkingSig) =
let fkey = fv.GetLinkagePartialKey()
(akey.MemberParentMangledName = fkey.MemberParentMangledName) &&
(akey.LogicalName = fkey.LogicalName) &&
(akey.TotalArgCount = fkey.TotalArgCount)
(akey.TotalArgCount = fkey.TotalArgCount)

(implModType.AllValsAndMembersByLogicalNameUncached, signModType.AllValsAndMembersByLogicalNameUncached)
||> NameMap.suball2
Expand All @@ -683,9 +700,29 @@ type Checker(g, amap, denv, remapInfo: SignatureRepackageInfo, checkingSig) =
| None -> None
| Some av -> Some(fv, av))

// For unmatched sig vals, try relaxed matching for unit-parameter equivalence:
// member M(()) has TotalArgCount 1, sig member M: unit -> unit has TotalArgCount 2,
// but their types are both unit -> unit.
let matchedAvs = matchingPairs |> List.map snd
let matchedFvs = matchingPairs |> List.map fst
let unmatchedFvs = fvs |> List.filter (fun fv -> not (List.exists (fun fv2 -> System.Object.ReferenceEquals(fv, fv2)) matchedFvs))
let unmatchedAvs = avs |> List.filter (fun av -> not (List.exists (fun av2 -> System.Object.ReferenceEquals(av, av2)) matchedAvs))
let relaxedPairs =
unmatchedFvs |> List.choose (fun fv ->
let fkey = fv.GetLinkagePartialKey()
match unmatchedAvs |> List.tryFind (fun av ->
let akey = av.GetLinkagePartialKey()
akey.MemberParentMangledName = fkey.MemberParentMangledName &&
akey.LogicalName = fkey.LogicalName &&
av.IsMember && fv.IsMember &&
typeAEquivAux EraseAll g aenv av.Type fv.Type) with
| None -> None
| Some av -> Some(fv, av))
let allMatchingPairs = matchingPairs @ relaxedPairs

// Check the ones with matching linkage
let allPairsOk = matchingPairs |> List.map (fun (fv, av) -> checkVal implModRef aenv infoReader av fv) |> List.forall id
let someNotOk = matchingPairs.Length < fvs.Length
let allPairsOk = allMatchingPairs |> List.map (fun (fv, av) -> checkVal implModRef aenv infoReader av fv) |> List.forall id
let someNotOk = allMatchingPairs.Length < fvs.Length
// Report an error for those that don't. Try pairing up by enclosing-type/name
if someNotOk then
let noMatches, partialMatchingPairs =
Expand Down
188 changes: 183 additions & 5 deletions tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -762,10 +762,90 @@ let (|A|B|) (x: int) = if x > 0 then A else B
"""

// Sweep: overloaded member with unit parameter (FS0193) — #19596
// Roundtrip fails: member M(()) generates sig 'member M: unit -> unit' but
// conformance checker can't match it when M is overloaded. The sig syntax
// is correct but the conformance check for unit-parameter overloads is broken.
[<Fact(Skip = "Sig conformance: member M(()) vs member M: unit -> unit fails when overloaded - FS0193")>]
// Testing both directions and consumer access to understand conformance boundaries.

// Direction 1: handwritten sig says "member M: unit -> unit", impl has "member M(()) = ()"
[<Fact>]
let ``Unit param overload - sig with consumer compiles`` () =
let sigSource = """
module Lib

type R1 =
{ f1: int }

type D =
new: unit -> D
member M: unit -> unit
member M: y: R1 -> unit
member N: unit
"""
let implSource = """
module Lib
type R1 = { f1 : int }
type D() =
member x.N = x.M { f1 = 3 }
member x.M((y: R1)) = ()
member x.M(()) = ()
"""
let consumerSource = """
module Consumer
open Lib
let test() =
let d = D()
d.M(())
d.M { f1 = 42 }
d.N
"""
Fsi sigSource
|> withAdditionalSourceFile (FsSourceWithFileName "Lib.fs" implSource)
|> withAdditionalSourceFile (FsSourceWithFileName "Consumer.fs" consumerSource)
|> ignoreWarnings
|> compile
|> shouldSucceed
|> ignore

// Direction 2: impl without explicit unit parens "member x.M() = ()"
[<Fact>]
let ``Unit param overload - non-paren impl with sig and consumer`` () =
let sigSource = """
module Lib

type R1 =
{ f1: int }

type D =
new: unit -> D
member M: unit -> unit
member M: y: R1 -> unit
member N: unit
"""
let implSource = """
module Lib
type R1 = { f1 : int }
type D() =
member x.N = x.M { f1 = 3 }
member x.M((y: R1)) = ()
member x.M() = ()
"""
let consumerSource = """
module Consumer
open Lib
let test() =
let d = D()
d.M()
d.M { f1 = 42 }
d.N
"""
Fsi sigSource
|> withAdditionalSourceFile (FsSourceWithFileName "Lib.fs" implSource)
|> withAdditionalSourceFile (FsSourceWithFileName "Consumer.fs" consumerSource)
|> ignoreWarnings
|> compile
|> shouldSucceed
|> ignore

// Roundtrip: generated sig from impl, then compile sig+impl+consumer
[<Fact>]
let ``Sweep - overloaded member with unit param roundtrips`` () =
assertSignatureRoundtrip """
module Repro
Expand All @@ -774,4 +854,102 @@ type D() =
member x.N = x.M { f1 = 3 }
member x.M((y: R1)) = ()
member x.M(()) = ()
"""
"""

// Inverse direction 1: impl M(()) but consumer calls d.M() (no parens) — must fail with FS0503
[<Fact>]
let ``Unit param overload - consumer cannot call M() when impl is M(()) with overloads`` () =
let sigSource = """
module Lib

type R1 =
{ f1: int }

type D =
new: unit -> D
member M: unit -> unit
member M: y: R1 -> unit
member N: unit
"""
let implSource = """
module Lib
type R1 = { f1 : int }
type D() =
member x.N = x.M { f1 = 3 }
member x.M((y: R1)) = ()
member x.M(()) = ()
"""
let consumerSource = """
module Consumer
open Lib
let test() =
let d = D()
d.M()
d.M { f1 = 42 }
d.N
"""
Fsi sigSource
|> withAdditionalSourceFile (FsSourceWithFileName "Lib.fs" implSource)
|> withAdditionalSourceFile (FsSourceWithFileName "Consumer.fs" consumerSource)
|> ignoreWarnings
|> compile
|> shouldFail
|> withErrorCode 503
|> ignore

// Inverse direction 2: impl M() (no explicit unit), sig M: unit -> unit, consumer calls d.M()
[<Fact>]
let ``Unit param overload - consumer calls M() with normal impl and sig`` () =
let sigSource = """
module Lib

type R1 =
{ f1: int }

type D =
new: unit -> D
member M: unit -> unit
member M: y: R1 -> unit
member N: unit
"""
let implSource = """
module Lib
type R1 = { f1 : int }
type D() =
member x.N = x.M { f1 = 3 }
member x.M((y: R1)) = ()
member x.M() = ()
"""
let consumerSource = """
module Consumer
open Lib
let test() =
let d = D()
d.M()
d.M { f1 = 42 }
d.N
"""
Fsi sigSource
|> withAdditionalSourceFile (FsSourceWithFileName "Lib.fs" implSource)
|> withAdditionalSourceFile (FsSourceWithFileName "Consumer.fs" consumerSource)
|> ignoreWarnings
|> compile
|> shouldSucceed
|> ignore

// Verify M(()) and M() produce identical IL method signatures
[<Fact>]
let ``Unit param - M(()) and M() produce same IL method signature`` () =
FSharp """
module Test
type D() =
member x.M(()) = 1
member x.M(y: int) = y
"""
|> compile
|> shouldSucceed
|> verifyILContains [
".method public hidebysig instance int32 M() cil managed"
".method public hidebysig instance int32 M(int32 y) cil managed"
]
|> ignore