Skip to content
Open
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
bf2a7b8
Add failing test: namespace global roundtrip (found by corpus sweep)
T-Gro Apr 15, 2026
73636b2
Add sig-gen roundtrip failures from corpus sweep (positive code only)
T-Gro Apr 15, 2026
2abe16d
Fix #19597: single-case struct DU gets spurious bar in signature
T-Gro Apr 15, 2026
6e3c142
Fix #19592: backticked active pattern case names lose escaping in sig
T-Gro Apr 15, 2026
bbaac92
Fix #19595: type params with special chars get backtick escaping
T-Gro Apr 16, 2026
9e56e09
Fix #19593: namespace global dropped from generated signature
T-Gro Apr 16, 2026
b129563
Add skipped test for #19596: overloaded member with unit parameter
T-Gro Apr 16, 2026
b639193
Fix #19594: SRTP constraints use explicit type param syntax in signat…
T-Gro Apr 16, 2026
c90c6e4
Add tooltip tests for signature generation display changes
T-Gro Apr 17, 2026
7958943
Remove sweep tooling (investigation artifacts, not production test in…
T-Gro Apr 17, 2026
21cf652
Add skipped test for #19596: overloaded member with unit parameter
T-Gro Apr 17, 2026
6f09ad5
Add missing roundtrip test for #19595: type param backtick escaping
T-Gro Apr 17, 2026
1394182
Fix namespace global + module layout and unskip 3 passing tests
T-Gro Apr 17, 2026
d4781a0
Add release notes for additional signature generation fixes
T-Gro Apr 17, 2026
4401842
Fix formatting in PrettyNaming.fs
T-Gro Apr 18, 2026
ef3d97c
Fix PR numbers in release notes
T-Gro Apr 19, 2026
4b01962
Fix CI failures: NRE null guard, SRTP constraint dedup, active patter…
T-Gro Apr 20, 2026
405668d
Fix tooltip display: skip prefix SRTP form in shortConstraints mode
T-Gro Apr 20, 2026
3608686
Update baselines for SRTP prefix constraint display format
T-Gro Apr 20, 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
5 changes: 5 additions & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
* Fix signature generation: backtick escaping for identifiers containing backticks. ([Issue #15389](https://github.com/dotnet/fsharp/issues/15389), [PR #19586](https://github.com/dotnet/fsharp/pull/19586))
* Fix signature generation: `private` keyword placement for prefix-style type abbreviations. ([Issue #15560](https://github.com/dotnet/fsharp/issues/15560), [PR #19586](https://github.com/dotnet/fsharp/pull/19586))
* Fix signature generation: missing `[<Class>]` attribute for types without visible constructors. ([Issue #16531](https://github.com/dotnet/fsharp/issues/16531), [PR #19586](https://github.com/dotnet/fsharp/pull/19586))
* Fix signature generation: single-case struct DU gets spurious bar causing FS0300. ([Issue #19597](https://github.com/dotnet/fsharp/issues/19597), [PR #19609](https://github.com/dotnet/fsharp/pull/19609))
* Fix signature generation: backticked active pattern case names lose escaping. ([Issue #19592](https://github.com/dotnet/fsharp/issues/19592), [PR #19609](https://github.com/dotnet/fsharp/pull/19609))
* 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))

### Added

Expand Down
59 changes: 49 additions & 10 deletions src/Compiler/Checking/NicePrint.fs
Original file line number Diff line number Diff line change
Expand Up @@ -730,11 +730,13 @@ module PrintTypes =
| _, _ -> squareAngleL (sepListL RightL.semicolon ((match kind with TyparKind.Type -> [] | TyparKind.Measure -> [wordL (tagText "Measure")]) @ List.map (layoutAttrib denv) attrs)) ^^ restL

and layoutTyparRef denv (typar: Typar) =
let rawName = typar.DeclaredName |> Option.defaultValue typar.Name
let name = if System.String.IsNullOrEmpty rawName then rawName else NormalizeIdentifierBackticks rawName
tagTypeParameter
(sprintf "%s%s%s"
(if denv.showStaticallyResolvedTyparAnnotations then prefixOfStaticReq typar.StaticReq else "'")
(if denv.showInferenceTyparAnnotations then prefixOfInferenceTypar typar else "")
(typar.DeclaredName |> Option.defaultValue typar.Name))
name)
|> mkNav typar.Range
|> wordL

Expand Down Expand Up @@ -1200,10 +1202,15 @@ module PrintTypes =
let (prettyTyparInst, prettyArgInfos, prettyRetTy), cxs = PrettyTypes.PrettifyInstAndUncurriedSig denv.g (typarInst, argInfos, retTy)
prettyTyparInst, prettyLayoutOfTopTypeInfoAux denv [prettyArgInfos] prettyRetTy cxs

let prettyLayoutOfCurriedMemberSig denv typarInst argInfos retTy parentTyparTys =
let prettyLayoutOfCurriedMemberSig denv typarInst argInfos retTy parentTyparTys excludeSrtpConstraints =
let (prettyTyparInst, parentTyparTys, argInfos, retTy), cxs = PrettyTypes.PrettifyInstAndCurriedSig denv.g (typarInst, parentTyparTys, argInfos, retTy)
// Filter out the parent typars, which don't get shown in the member signature
let cxs = cxs |> List.filter (fun (tp, _) -> not (parentTyparTys |> List.exists (fun ty -> match tryDestTyparTy denv.g ty with ValueSome destTypar -> typarEq tp destTypar | _ -> false)))
// When SRTP method typars are shown on explicit type param declarations, exclude their constraints from postfix
let cxs =
if excludeSrtpConstraints then
cxs |> List.filter (fun (tp, _) -> tp.StaticReq <> TyparStaticReq.HeadType)
else cxs
prettyTyparInst, prettyLayoutOfTopTypeInfoAux denv argInfos retTy cxs

let prettyArgInfos denv allTyparInst =
Expand All @@ -1224,7 +1231,8 @@ module PrintTypes =
// aren't chosen as names for displayed variables.
let memberParentTypars = List.map fst memberToParentInst
let parentTyparTys = List.map (mkTyparTy >> instType allTyparInst) memberParentTypars
let prettyTyparInst, layout = prettyLayoutOfCurriedMemberSig denv typarInst argInfos retTy parentTyparTys
let hasStaticallyResolvedTypars = niceMethodTypars |> List.exists (fun tp -> tp.StaticReq = TyparStaticReq.HeadType)
let prettyTyparInst, layout = prettyLayoutOfCurriedMemberSig denv typarInst argInfos retTy parentTyparTys hasStaticallyResolvedTypars

prettyTyparInst, niceMethodTypars, layout

Expand Down Expand Up @@ -1355,8 +1363,10 @@ module PrintTastMemberOrVals =
|> Seq.exists (fun tp -> parentTyparNames.Contains tp.typar_id.idText)

let typarOrderMismatch = isTyparOrderMismatch niceMethodTypars argInfos
let hasStaticallyResolvedTypars =
niceMethodTypars |> List.exists (fun tp -> tp.StaticReq = TyparStaticReq.HeadType)
let nameL =
if denv.showTyparBinding || typarOrderMismatch || memberHasSameTyparNameAsParentTypeTypars then
if denv.showTyparBinding || typarOrderMismatch || memberHasSameTyparNameAsParentTypeTypars || hasStaticallyResolvedTypars then
layoutTyparDecls denv nameL true niceMethodTypars
else
nameL
Expand Down Expand Up @@ -1526,10 +1536,19 @@ module PrintTastMemberOrVals =
let isTyFunction = v.IsTypeFunction // Bug: 1143, and innerpoly tests
let typarOrderMismatch = isTyparOrderMismatch tps argInfos

let hasStaticallyResolvedTypars =
tps |> List.exists (fun tp -> tp.StaticReq = TyparStaticReq.HeadType) &&
not (IsLogicalOpName v.LogicalName) &&
not denv.shortConstraints
let typarBindingsL =
if isTyFunction || isOverGeneric || denv.showTyparBinding || typarOrderMismatch then
if isTyFunction || isOverGeneric || denv.showTyparBinding || typarOrderMismatch || hasStaticallyResolvedTypars then
layoutTyparDecls denv nameL true tps
else nameL
// When SRTP method typars are shown on explicit type param declarations, exclude their constraints from postfix
let cxs =
if hasStaticallyResolvedTypars then
cxs |> List.filter (fun (tp, _) -> tp.StaticReq <> TyparStaticReq.HeadType)
else cxs
let valAndTypeL = (WordL.keywordVal ^^ (typarBindingsL |> addColonL)) --- layoutTopType denv env argInfos retTy cxs
let valAndTypeL =
match denv.generatedValueLayout v with
Expand Down Expand Up @@ -1901,8 +1920,12 @@ module TastDefinitionPrinting =
| fields -> (prefixL ^^ nmL ^^ WordL.keywordOf) --- layoutUnionCaseFields denv infoReader true enclosingTcref fields
layoutXmlDocOfUnionCase denv infoReader (UnionCaseRef(enclosingTcref, ucase.Id.idText)) caseL

let layoutUnionCases denv infoReader enclosingTcref ucases =
let prefixL = WordL.bar // See bug://2964 - always prefix in case preceded by accessibility modifier
let layoutUnionCases denv infoReader isStruct enclosingTcref ucases =
let prefixL =
match ucases with
// Single-case struct: bar changes base type semantics (FS0300), so omit it
| [ _ ] when isStruct -> emptyL
| _ -> WordL.bar // See bug://2964 - always prefix in case preceded by accessibility modifier
List.map (layoutUnionCase denv infoReader prefixL enclosingTcref) ucases

/// When to force a break? "type tyname = <HERE> repn"
Expand Down Expand Up @@ -2331,8 +2354,9 @@ module TastDefinitionPrinting =

| TFSharpTyconRepr { fsobjmodel_kind = TFSharpUnion } ->
let denv = denv.AddAccessibility tycon.TypeReprAccessibility
let isStruct = tycon.IsStructOrEnumTycon
tycon.UnionCasesAsList
|> layoutUnionCases denv infoReader tcref
|> layoutUnionCases denv infoReader isStruct tcref
|> applyMaxMembers denv.maxMembers
|> aboveListL
|> addReprAccessL
Expand Down Expand Up @@ -2582,6 +2606,16 @@ module InferredSigPrinting =

let (@@*) = if denv.printVerboseSignatures then (@@----) else (@@--)

// Detect namespace global: bare types/vals at root level (not wrapped in Module binding)
let rec hasBareToplevelTypes x =
match x with
| TMDefRec(_, _, tycons, _, _) -> not (List.isEmpty tycons)
| TMDefLet _ | TMDefDo _ -> true
| TMDefOpens _ -> false
| TMDefs defs -> defs |> List.exists hasBareToplevelTypes

let isGlobalNamespace = hasBareToplevelTypes expr

let rec isConcreteNamespace x =
match x with
| TMDefRec(_, _opens, tycons, mbinds, _) ->
Expand Down Expand Up @@ -2707,7 +2741,7 @@ module InferredSigPrinting =
if showHeader then
// OK, we're not in F# Interactive
// Check if this is an outer module with no namespace
if isNil outerPath then
if isNil outerPath && not isGlobalNamespace then
// If so print a "module" declaration, no indentation
modNameL @@ basic
else
Expand Down Expand Up @@ -2745,7 +2779,12 @@ module InferredSigPrinting =
| EmptyModuleOrNamespaces mspecs when showHeader ->
List.map emptyModuleOrNamespace mspecs
|> aboveListL
| expr -> imdefL denv expr
| expr ->
let layout = imdefL denv expr
if isGlobalNamespace then
WordL.keywordNamespace ^^ wordL (TaggedText.tagNamespace "global") @@* layout
else
layout

//--------------------------------------------------------------------------

Expand Down
22 changes: 20 additions & 2 deletions src/Compiler/SyntaxTree/PrettyNaming.fs
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,24 @@ let ConvertValLogicalNameToDisplayNameCore opName =
else
opName

/// Escape active pattern case names that need backticks for display/signatures.
/// E.g. |A B| becomes |``A B``| (only for display, not for name resolution)
let EscapeActivePatternCases (opName: string) =
if IsActivePatternName opName then
let inner = opName.[1 .. opName.Length - 2]
let cases = inner.Split('|')

let escapedCases =
cases
|> Array.map (fun c ->
if c = "_" then c
elif not (IsIdentifierName c) then "``" + c + "``"
else c)

"|" + (escapedCases |> String.concat "|") + "|"
else
opName

let DoesIdentifierNeedBackticks (name: string) : bool =
not (IsUnencodedOpName name)
&& not (IsIdentifierName name)
Expand Down Expand Up @@ -538,7 +556,7 @@ let ConvertValLogicalNameToDisplayName isBaseVal name =
if isBaseVal && name = "base" then
"base"
elif IsUnencodedOpName name || IsPossibleOpName name || IsActivePatternName name then
let nm = ConvertValLogicalNameToDisplayNameCore name
let nm = ConvertValLogicalNameToDisplayNameCore name |> EscapeActivePatternCases
// Check for no decompilation, e.g. op_Implicit, op_NotAMangledOpName, op_A-B
if IsPossibleOpName name && (nm = name) then
AddBackticksToIdentifierIfNeeded nm
Expand All @@ -563,7 +581,7 @@ let ConvertValLogicalNameToDisplayLayout isBaseVal nonOpLayout name =
if isBaseVal && name = "base" then
nonOpLayout "base"
elif IsUnencodedOpName name || IsPossibleOpName name || IsActivePatternName name then
let nm = ConvertValLogicalNameToDisplayNameCore name
let nm = ConvertValLogicalNameToDisplayNameCore name |> EscapeActivePatternCases
// Check for no decompilation, e.g. op_Implicit, op_NotAMangledOpName, op_A-B
if IsPossibleOpName name && (nm = name) then
ConvertLogicalNameToDisplayLayout nonOpLayout name
Expand Down
3 changes: 3 additions & 0 deletions src/Compiler/SyntaxTree/PrettyNaming.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ val internal ConvertLogicalNameToDisplayName: name: string -> string
/// If not, the it is likely this should be replaced by ConvertValLogicalNameToDisplayName.
val ConvertValLogicalNameToDisplayNameCore: opName: string -> string

/// Escape active pattern case names that need backticks for display/signatures.
val EscapeActivePatternCases: opName: string -> string

/// Take a core display name for a value (e.g. op_Addition or PropertyName) and convert it to display text
/// Foo --> Foo
/// + --> ``+``
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,24 +80,24 @@ let main _ =

[<Theory>]
[<InlineData("let inline f0 (x: ^T) = x",
"val inline f0: x: ^T -> ^T")>]
"val inline f0<^T> : x: ^T -> ^T")>]

[<InlineData("""
let inline f0 (x: ^T) = x
let g0 (x: 'T) = f0 x""",
"val g0: x: 'T -> 'T")>]

[<InlineData("let inline f1 (x: ^T) = (^T : (static member A: int) ())",
"val inline f1: x: ^T -> int when ^T: (static member A: int)")>]
"val inline f1<^T when ^T: (static member A: int)> : x: ^T -> int")>]

[<InlineData("let inline f2 (x: 'T) = ((^T or int) : (static member A: int) ())",
"val inline f2: x: ^T -> int when (^T or int) : (static member A: int)")>]
"val inline f2<^T when (^T or int) : (static member A: int)> : x: ^T -> int")>]

[<InlineData("let inline f3 (x: 'T) = ((^U or 'T) : (static member A: int) ())",
"val inline f3: x: ^T -> int when (^U or ^T) : (static member A: int)")>]
"val inline f3<^T,^U when (^U or ^T) : (static member A: int)> : x: ^T -> int")>]

[<InlineData("let inline f4 (x: 'T when 'T : (static member A: int) ) = 'T.A",
"val inline f4: x: ^T -> int when ^T: (static member A: int)")>]
"val inline f4<^T when ^T: (static member A: int)> : x: ^T -> int")>]

[<InlineData("""
let inline f5 (x: ^T) = printfn "%d" x
Expand All @@ -106,11 +106,11 @@ let main _ =
[<InlineData("""
let inline f5 (x: ^T) = printfn "%d" x
let inline h5 (x: 'T) = f5 x""",
"val inline h5: x: ^T -> unit when ^T: (byte|int16|int32|int64|sbyte|uint16|uint32|uint64|nativeint|unativeint)")>]
"val inline h5<^T when ^T: (byte|int16|int32|int64|sbyte|uint16|uint32|uint64|nativeint|unativeint)> : x: ^T -> unit")>]
[<InlineData("""
let inline uint32 (value: ^T) = (^T : (static member op_Explicit: ^T -> uint32) (value))
let inline uint value = uint32 value""",
"val inline uint: value: ^a -> uint32 when ^a: (static member op_Explicit: ^a -> uint32)")>]
"val inline uint<^a when ^a: (static member op_Explicit: ^a -> uint32)> : value: ^a -> uint32")>]

[<InlineData("let checkReflexive f x y = (f x y = - f y x)",
"val checkReflexive: f: ('a -> 'a -> int) -> x: 'a -> y: 'a -> bool")>]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,50 @@ let ```a` b`` (a:int) (b:int) = ()
module Foo

val ```a` b`` : a: int -> b: int -> unit"""

// Found by corpus-wide roundtrip sweep. Fixed: #19593
[<Fact>]
let ``Namespace global with class type roundtrips`` () =
let implSource =
"""
namespace global

type Foo() =
member _.X = 1
"""

let generatedSignature =
FSharp implSource
|> printSignatures

Fsi generatedSignature
|> withAdditionalSourceFile (FsSource implSource)
|> ignoreWarnings
|> compile
|> shouldSucceed
|> ignore

// Namespace global with nested module — fixed by moving ns global detection into NicePrint
[<Fact>]
let ``Namespace global with module roundtrips`` () =
let implSource =
"""
namespace global

type Foo() =
member _.X = 1

module Utils =
let f (x:Foo) = x
"""

let generatedSignature =
FSharp implSource
|> printSignatures

Fsi generatedSignature
|> withAdditionalSourceFile (FsSource implSource)
|> ignoreWarnings
|> compile
|> shouldSucceed
|> ignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,12 @@ let assertSingleSignatureBinding implementation signature =
FSharp $"module A\n\n{implementation}"
|> printSignatures
|> assertEqualIgnoreLineEnding $"\nmodule A\n\n{signature}"

let assertSignatureRoundtrip (implSource: string) =
let generatedSignature = FSharp implSource |> printSignatures
Fsi generatedSignature
|> withAdditionalSourceFile (FsSource implSource)
|> ignoreWarnings
|> compile
|> shouldSucceed
|> ignore
Loading
Loading