From 62abe6a34477e386b8cbbb2cf343e2c1323937ea Mon Sep 17 00:00:00 2001 From: Edgar Gonzalez Date: Thu, 3 Sep 2026 10:27:11 +0200 Subject: [PATCH 1/6] Report a proper error for a bare 'enum' constraint (#14580) --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/pars.fsy | 7 ++- .../Constraints/ConstraintSyntax.fs | 59 +++++++++++++++++++ .../FSharp.Compiler.ComponentTests.fsproj | 1 + .../SynTyparDecl/Constraint - Enum 01.fs | 3 + .../SynTyparDecl/Constraint - Enum 01.fs.bsl | 23 ++++++++ .../Constraint - Unknown identifier 01.fs | 3 + .../Constraint - Unknown identifier 01.fs.bsl | 23 ++++++++ .../Constraint - Unknown identifier 02.fs | 3 + .../Constraint - Unknown identifier 02.fs.bsl | 23 ++++++++ .../Constraint - Unknown identifier 03.fs | 3 + .../Constraint - Unknown identifier 03.fs.bsl | 33 +++++++++++ 12 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/ConstraintSyntax.fs create mode 100644 tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Enum 01.fs create mode 100644 tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Enum 01.fs.bsl create mode 100644 tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 01.fs create mode 100644 tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 01.fs.bsl create mode 100644 tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 02.fs create mode 100644 tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 02.fs.bsl create mode 100644 tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 03.fs create mode 100644 tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 03.fs.bsl diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 9441f8587e0..f8981c8d0de 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,5 +1,6 @@ ### Fixed +* Better diagnostic for a bare `enum` constraint: `'T : enum` now reports FS0699 "An 'enum' constraint must be of the form 'enum'" instead of "Unexpected identifier: 'enum (4)'", and parser messages for invalid constraints no longer leak internal ` (2)`/` (3)`/` (4)` markers. ([Issue #14580](https://github.com/dotnet/fsharp/issues/14580), [PR #XXXXX](https://github.com/dotnet/fsharp/pull/XXXXX)) * Fix `NativePtr.stackalloc` nested in a larger expression (e.g. a call argument or the right of an assignment) producing an assembly that throws `InvalidProgramException` at load. ([Issue #8083](https://github.com/dotnet/fsharp/issues/8083), [PR #20302](https://github.com/dotnet/fsharp/pull/20302)) * Fix internal error "Unexpected generalized type variables when compiling an active pattern" when an active pattern is used in a `let` binding whose right-hand side is a generic value, e.g. `let (T) = id`. Such a binding is now checked like the equivalent `match` and is not generalized. ([Issue #16856](https://github.com/dotnet/fsharp/issues/16856), [PR #20383](https://github.com/dotnet/fsharp/pull/20383)) * Fix Release-only (`--optimize+`) `System.InvalidProgramException` from `Seq.collect` / `yield!` over a value-type (struct) collection implementing `seq<'T>` (e.g. `ImmutableArray<_>`) when materialised with `List.ofSeq` / `Seq.toList` / `Seq.toArray` or a list/array comprehension. The collector lowering now boxes a struct sub-collection to `seq<'T>` before calling `AddMany`/`AddManyAndClose` (matching the coercion the type checker already inserts for `yield!`), and uses `unit` as the try/finally result type instead of the body type (removing a spurious `ldnull` store). ([Issue #20203](https://github.com/dotnet/fsharp/issues/20203)) diff --git a/src/Compiler/pars.fsy b/src/Compiler/pars.fsy index 8d325fa7537..56e7f2da9ab 100644 --- a/src/Compiler/pars.fsy +++ b/src/Compiler/pars.fsy @@ -2719,7 +2719,7 @@ typeConstraint: { SynTypeConstraint.WhereTyparSupportsNull($1, lhs parseState) } | typar COLON IDENT NULL - { if $3 <> "not" then reportParseErrorAt (rhs parseState 3) (FSComp.SR.parsUnexpectedIdentifier($3 + " (2)")) + { if $3 <> "not" then reportParseErrorAt (rhs parseState 3) (FSComp.SR.parsUnexpectedIdentifier($3)) let trivia : SynTypeConstraintWhereTyparNotSupportsNullTrivia = { ColonRange = rhs parseState 2; NotRange = rhs parseState 3 } SynTypeConstraint.WhereTyparNotSupportsNull($1, lhs parseState, trivia) } @@ -2741,14 +2741,15 @@ typeConstraint: | "enum" -> let _ltm, _gtm, args, _commas, mWhole = $4 SynTypeConstraint.WhereTyparIsEnum($1, args, unionRanges $1.Range mWhole) - | nm -> raiseParseErrorAt (rhs parseState 3) (FSComp.SR.parsUnexpectedIdentifier(nm + " (3)")) } + | nm -> raiseParseErrorAt (rhs parseState 3) (FSComp.SR.parsUnexpectedIdentifier(nm)) } | typar COLON IDENT { match $3 with | "comparison" -> SynTypeConstraint.WhereTyparIsComparable($1, lhs parseState) | "equality" -> SynTypeConstraint.WhereTyparIsEquatable($1, lhs parseState) | "unmanaged" -> SynTypeConstraint.WhereTyparIsUnmanaged($1, lhs parseState) - | nm -> raiseParseErrorAt (rhs parseState 3) (FSComp.SR.parsUnexpectedIdentifier(nm + " (4)")) } + | "enum" -> raiseParseErrorAt (rhs parseState 3) (FSComp.SR.tcInvalidEnumConstraint()) + | nm -> raiseParseErrorAt (rhs parseState 3) (FSComp.SR.parsUnexpectedIdentifier(nm)) } | appTypeWithoutNull { SynTypeConstraint.WhereSelfConstrained($1, lhs parseState) } diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/ConstraintSyntax.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/ConstraintSyntax.fs new file mode 100644 index 00000000000..d87d014c183 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/ConstraintSyntax.fs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Conformance.Constraints + +open Xunit +open FSharp.Test.Compiler + +module ConstraintSyntax = + + // https://github.com/dotnet/fsharp/issues/14580 + [] + let ``Bare 'enum' constraint reports the enum constraint form error`` () = + Fsx """ +type I<'T when 'T : enum> = interface end + """ + |> withOptions ["--test:ErrorRanges"] + |> typecheck + |> shouldFail + |> withSingleDiagnostic (Error 699, Line 2, Col 21, Line 2, Col 25, "An 'enum' constraint must be of the form 'enum'") + + [] + let ``Unknown identifier constraint reports the identifier without internal markers`` () = + Fsx """ +type I<'T when 'T : notAConstraint> = interface end + """ + |> withOptions ["--test:ErrorRanges"] + |> typecheck + |> shouldFail + |> withSingleDiagnostic (Error 571, Line 2, Col 21, Line 2, Col 35, "Unexpected identifier: 'notAConstraint'") + + [] + let ``Unknown identifier constraint with type arguments reports the identifier without internal markers`` () = + Fsx """ +type I<'T when 'T : notAConstraint> = interface end + """ + |> withOptions ["--test:ErrorRanges"] + |> typecheck + |> shouldFail + |> withSingleDiagnostic (Error 571, Line 2, Col 21, Line 2, Col 35, "Unexpected identifier: 'notAConstraint'") + + [] + let ``Unknown identifier before 'null' constraint reports the identifier without internal markers`` () = + Fsx """ +type I<'T when 'T : maybe null> = interface end + """ + |> withOptions ["--test:ErrorRanges"] + |> typecheck + |> shouldFail + |> withSingleDiagnostic (Error 571, Line 2, Col 21, Line 2, Col 26, "Unexpected identifier: 'maybe'") + + [] + let ``'enum' constraint with an underlying type is accepted`` () = + Fsx """ +type I<'T when 'T : enum> = interface end +type E = A = 1 +type Ok = I + """ + |> typecheck + |> shouldSucceed diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index cc7e109373f..c621468851e 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -84,6 +84,7 @@ + diff --git a/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Enum 01.fs b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Enum 01.fs new file mode 100644 index 00000000000..4f37c6b40c9 --- /dev/null +++ b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Enum 01.fs @@ -0,0 +1,3 @@ +module Module + +type I<'T when 'T : enum> = interface end diff --git a/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Enum 01.fs.bsl b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Enum 01.fs.bsl new file mode 100644 index 00000000000..9a072d592f4 --- /dev/null +++ b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Enum 01.fs.bsl @@ -0,0 +1,23 @@ +ImplFile + (ParsedImplFileInput + ("/root/SynTyparDecl/Constraint - Enum 01.fs", false, + QualifiedNameOfFile Module, [], + [SynModuleOrNamespace + ([Module], false, NamedModule, + [Types + ([SynTypeDefn + (SynComponentInfo + ([], None, [], None, + PreXmlDoc ((3,0), FSharp.Compiler.Xml.XmlDocCollector), + false, None, (3,5--3,25)), + ObjectModel (Interface, [], (3,28--3,41)), [], None, + (3,5--3,41), { LeadingKeyword = Type (3,0--3,4) + EqualsRange = Some (3,26--3,27) + WithKeyword = None })], (3,0--3,41))], + PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector), [], None, + (1,0--3,41), { LeadingKeyword = Module (1,0--1,6) })], (true, true), + { ConditionalDirectives = [] + WarnDirectives = [] + CodeComments = [] }, set [])) + +(3,20)-(3,24) parse error An 'enum' constraint must be of the form 'enum' diff --git a/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 01.fs b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 01.fs new file mode 100644 index 00000000000..84baff9a18a --- /dev/null +++ b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 01.fs @@ -0,0 +1,3 @@ +module Module + +type I<'T when 'T : notAConstraint> = interface end diff --git a/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 01.fs.bsl b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 01.fs.bsl new file mode 100644 index 00000000000..d155c15dec3 --- /dev/null +++ b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 01.fs.bsl @@ -0,0 +1,23 @@ +ImplFile + (ParsedImplFileInput + ("/root/SynTyparDecl/Constraint - Unknown identifier 01.fs", false, + QualifiedNameOfFile Module, [], + [SynModuleOrNamespace + ([Module], false, NamedModule, + [Types + ([SynTypeDefn + (SynComponentInfo + ([], None, [], None, + PreXmlDoc ((3,0), FSharp.Compiler.Xml.XmlDocCollector), + false, None, (3,5--3,35)), + ObjectModel (Interface, [], (3,38--3,51)), [], None, + (3,5--3,51), { LeadingKeyword = Type (3,0--3,4) + EqualsRange = Some (3,36--3,37) + WithKeyword = None })], (3,0--3,51))], + PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector), [], None, + (1,0--3,51), { LeadingKeyword = Module (1,0--1,6) })], (true, true), + { ConditionalDirectives = [] + WarnDirectives = [] + CodeComments = [] }, set [])) + +(3,20)-(3,34) parse error Unexpected identifier: 'notAConstraint' diff --git a/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 02.fs b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 02.fs new file mode 100644 index 00000000000..b5fbfaa09bc --- /dev/null +++ b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 02.fs @@ -0,0 +1,3 @@ +module Module + +type I<'T when 'T : notAConstraint> = interface end diff --git a/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 02.fs.bsl b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 02.fs.bsl new file mode 100644 index 00000000000..4ab9f81fc49 --- /dev/null +++ b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 02.fs.bsl @@ -0,0 +1,23 @@ +ImplFile + (ParsedImplFileInput + ("/root/SynTyparDecl/Constraint - Unknown identifier 02.fs", false, + QualifiedNameOfFile Module, [], + [SynModuleOrNamespace + ([Module], false, NamedModule, + [Types + ([SynTypeDefn + (SynComponentInfo + ([], None, [], None, + PreXmlDoc ((3,0), FSharp.Compiler.Xml.XmlDocCollector), + false, None, (3,5--3,39)), + ObjectModel (Interface, [], (3,43--3,56)), [], None, + (3,5--3,56), { LeadingKeyword = Type (3,0--3,4) + EqualsRange = Some (3,41--3,42) + WithKeyword = None })], (3,0--3,56))], + PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector), [], None, + (1,0--3,56), { LeadingKeyword = Module (1,0--1,6) })], (true, true), + { ConditionalDirectives = [] + WarnDirectives = [] + CodeComments = [] }, set [])) + +(3,20)-(3,34) parse error Unexpected identifier: 'notAConstraint' diff --git a/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 03.fs b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 03.fs new file mode 100644 index 00000000000..da6caf02a35 --- /dev/null +++ b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 03.fs @@ -0,0 +1,3 @@ +module Module + +type I<'T when 'T : maybe null> = interface end diff --git a/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 03.fs.bsl b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 03.fs.bsl new file mode 100644 index 00000000000..92b03773acd --- /dev/null +++ b/tests/service/data/SyntaxTree/SynTyparDecl/Constraint - Unknown identifier 03.fs.bsl @@ -0,0 +1,33 @@ +ImplFile + (ParsedImplFileInput + ("/root/SynTyparDecl/Constraint - Unknown identifier 03.fs", false, + QualifiedNameOfFile Module, [], + [SynModuleOrNamespace + ([Module], false, NamedModule, + [Types + ([SynTypeDefn + (SynComponentInfo + ([], + Some + (PostfixList + ([SynTyparDecl + ([], SynTypar (T, None, false), [], + { AmpersandRanges = [] })], + [WhereTyparNotSupportsNull + (SynTypar (T, None, false), (3,15--3,30), + { ColonRange = (3,18--3,19) + NotRange = (3,20--3,25) })], (3,6--3,31))), [], + Some (LongIdent (SynLongIdent ([I], [], [None]))), + PreXmlDoc ((3,0), FSharp.Compiler.Xml.XmlDocCollector), + true, None, (3,5--3,6)), + ObjectModel (Interface, [], (3,34--3,47)), [], None, + (3,5--3,47), { LeadingKeyword = Type (3,0--3,4) + EqualsRange = Some (3,32--3,33) + WithKeyword = None })], (3,0--3,47))], + PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector), [], None, + (1,0--3,47), { LeadingKeyword = Module (1,0--1,6) })], (true, true), + { ConditionalDirectives = [] + WarnDirectives = [] + CodeComments = [] }, set [])) + +(3,20)-(3,25) parse error Unexpected identifier: 'maybe' From 18ec6296a4cf77167cc88d217d2285fc4cf3b12c Mon Sep 17 00:00:00 2001 From: Edgar Gonzalez Date: Sat, 5 Sep 2026 09:07:06 +0200 Subject: [PATCH 2/6] Add PR number to release notes --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index f8981c8d0de..944e259a28b 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,6 +1,6 @@ ### Fixed -* Better diagnostic for a bare `enum` constraint: `'T : enum` now reports FS0699 "An 'enum' constraint must be of the form 'enum'" instead of "Unexpected identifier: 'enum (4)'", and parser messages for invalid constraints no longer leak internal ` (2)`/` (3)`/` (4)` markers. ([Issue #14580](https://github.com/dotnet/fsharp/issues/14580), [PR #XXXXX](https://github.com/dotnet/fsharp/pull/XXXXX)) +* Better diagnostic for a bare `enum` constraint: `'T : enum` now reports FS0699 "An 'enum' constraint must be of the form 'enum'" instead of "Unexpected identifier: 'enum (4)'", and parser messages for invalid constraints no longer leak internal ` (2)`/` (3)`/` (4)` markers. ([Issue #14580](https://github.com/dotnet/fsharp/issues/14580), [PR #20454](https://github.com/dotnet/fsharp/pull/20454)) * Fix `NativePtr.stackalloc` nested in a larger expression (e.g. a call argument or the right of an assignment) producing an assembly that throws `InvalidProgramException` at load. ([Issue #8083](https://github.com/dotnet/fsharp/issues/8083), [PR #20302](https://github.com/dotnet/fsharp/pull/20302)) * Fix internal error "Unexpected generalized type variables when compiling an active pattern" when an active pattern is used in a `let` binding whose right-hand side is a generic value, e.g. `let (T) = id`. Such a binding is now checked like the equivalent `match` and is not generalized. ([Issue #16856](https://github.com/dotnet/fsharp/issues/16856), [PR #20383](https://github.com/dotnet/fsharp/pull/20383)) * Fix Release-only (`--optimize+`) `System.InvalidProgramException` from `Seq.collect` / `yield!` over a value-type (struct) collection implementing `seq<'T>` (e.g. `ImmutableArray<_>`) when materialised with `List.ofSeq` / `Seq.toList` / `Seq.toArray` or a list/array comprehension. The collector lowering now boxes a struct sub-collection to `seq<'T>` before calling `AddMany`/`AddManyAndClose` (matching the coercion the type checker already inserts for `yield!`), and uses `unit` as the try/finally result type instead of the body type (removing a spurious `ldnull` store). ([Issue #20203](https://github.com/dotnet/fsharp/issues/20203)) From 5539ed4755927548a103b3e986edc6cccc74bd0d Mon Sep 17 00:00:00 2001 From: Edgar Gonzalez Date: Sun, 6 Sep 2026 19:51:45 +0200 Subject: [PATCH 3/6] Solve enum constraints once the representations of a recursive group are established (#14580) --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Checking/ConstraintSolver.fs | 18 ++- .../TypedTreeOps.ExprConstruction.fs | 47 +++++--- .../TypedTreeOps.ExprConstruction.fsi | 4 + .../Constraints/ConstraintSyntax.fs | 110 ++++++++++++++++++ 5 files changed, 160 insertions(+), 20 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 944e259a28b..de524de3f67 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,5 +1,6 @@ ### Fixed +* Fix internal error "no 'value__' field found for enumeration type" when an `enum` constraint is checked against an enum declared in the same recursive group (`module rec` or an `and` group). The constraint is now solved once the representations of the group are established. ([Issue #14580](https://github.com/dotnet/fsharp/issues/14580), [PR #20454](https://github.com/dotnet/fsharp/pull/20454)) * Better diagnostic for a bare `enum` constraint: `'T : enum` now reports FS0699 "An 'enum' constraint must be of the form 'enum'" instead of "Unexpected identifier: 'enum (4)'", and parser messages for invalid constraints no longer leak internal ` (2)`/` (3)`/` (4)` markers. ([Issue #14580](https://github.com/dotnet/fsharp/issues/14580), [PR #20454](https://github.com/dotnet/fsharp/pull/20454)) * Fix `NativePtr.stackalloc` nested in a larger expression (e.g. a call argument or the right of an assignment) producing an assembly that throws `InvalidProgramException` at load. ([Issue #8083](https://github.com/dotnet/fsharp/issues/8083), [PR #20302](https://github.com/dotnet/fsharp/pull/20302)) * Fix internal error "Unexpected generalized type variables when compiling an active pattern" when an active pattern is used in a `let` binding whose right-hand side is a generic value, e.g. `let (T) = id`. Such a binding is now checked like the equivalent `match` and is not generalized. ([Issue #16856](https://github.com/dotnet/fsharp/issues/16856), [PR #20383](https://github.com/dotnet/fsharp/pull/20383)) diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index fcdfa301dd0..4ce72dbf078 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -3121,7 +3121,23 @@ and SolveTypeIsEnum (csenv: ConstraintSolverEnv) ndeep m2 trace ty underlying = AddConstraint csenv ndeep m2 trace destTypar (TyparConstraint.IsEnum(underlying, m)) | _ -> if isEnumTy g ty then - SolveTypeEqualsTypeKeepAbbrevs csenv ndeep m2 trace underlying (underlyingTypeOfEnumTy g ty) + match tryUnderlyingTypeOfEnumTy g ty with + | ValueSome underlyingTyOfEnum -> + SolveTypeEqualsTypeKeepAbbrevs csenv ndeep m2 trace underlying underlyingTyOfEnum + | ValueNone -> + // The enum is part of the recursive group being checked, so its underlying type is not known + // yet. Solve the constraint once the representations of the group are established. + csenv.SolverState.PushPostInferenceCheck( + false, + fun () -> + PostponeOnFailedMemberConstraintResolution + csenv + NoTrace + (fun csenv -> SolveTypeIsEnum csenv ndeep m2 NoTrace ty underlying) + (fun res -> ErrorD(ErrorFromAddingConstraint(denv, res, m))) + |> RaiseOperationResult) + + CompleteD else ErrorD (ConstraintSolverError(FSComp.SR.csTypeIsNotEnumType(NicePrint.minimalRichTextOfType denv ty), m, m2)) diff --git a/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs b/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs index fb19654a38b..44b833e216d 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs @@ -1267,12 +1267,15 @@ module internal TypeTesters = | _ -> getErasedTypes g domainTy false @ getErasedTypes g rangeTy false | TType_measure _ -> [ ty ] - let underlyingTypeOfEnumTy (g: TcGlobals) ty = + /// Determine the underlying type of an enum type (normally int32). + /// ValueNone while the representation of an F# enum is still being established: the 'value__' field + /// is only added once the representations of a recursive group are known. + let tryUnderlyingTypeOfEnumTy (g: TcGlobals) ty = assert (isEnumTy g ty) match metadataOfTy g ty with #if !NO_TYPEPROVIDERS - | ProvidedTypeMetadata info -> info.UnderlyingTypeOfEnum() + | ProvidedTypeMetadata info -> ValueSome(info.UnderlyingTypeOfEnum()) #endif | ILTypeMetadata(TILObjectReprData(_, _, tdef)) -> @@ -1280,25 +1283,31 @@ module internal TypeTesters = let ilTy = getTyOfILEnumInfo info match ilTy.TypeSpec.Name with - | "System.Byte" -> g.byte_ty - | "System.SByte" -> g.sbyte_ty - | "System.Int16" -> g.int16_ty - | "System.Int32" -> g.int32_ty - | "System.Int64" -> g.int64_ty - | "System.UInt16" -> g.uint16_ty - | "System.UInt32" -> g.uint32_ty - | "System.UInt64" -> g.uint64_ty - | "System.Single" -> g.float32_ty - | "System.Double" -> g.float_ty - | "System.Char" -> g.char_ty - | "System.Boolean" -> g.bool_ty - | _ -> g.int32_ty + | "System.Byte" -> ValueSome g.byte_ty + | "System.SByte" -> ValueSome g.sbyte_ty + | "System.Int16" -> ValueSome g.int16_ty + | "System.Int32" -> ValueSome g.int32_ty + | "System.Int64" -> ValueSome g.int64_ty + | "System.UInt16" -> ValueSome g.uint16_ty + | "System.UInt32" -> ValueSome g.uint32_ty + | "System.UInt64" -> ValueSome g.uint64_ty + | "System.Single" -> ValueSome g.float32_ty + | "System.Double" -> ValueSome g.float_ty + | "System.Char" -> ValueSome g.char_ty + | "System.Boolean" -> ValueSome g.bool_ty + | _ -> ValueSome g.int32_ty | FSharpOrArrayOrByrefOrTupleOrExnTypeMetadata -> - let tycon = (tcrefOfAppTy g ty).Deref + match (tcrefOfAppTy g ty).Deref.GetFieldByName "value__" with + | Some rf -> ValueSome rf.FormalType + | None -> ValueNone - match tycon.GetFieldByName "value__" with - | Some rf -> rf.FormalType - | None -> error (InternalError("no 'value__' field found for enumeration type " + tycon.LogicalName, tycon.Range)) + /// Determine the underlying type of an enum type (normally int32) + let underlyingTypeOfEnumTy (g: TcGlobals) ty = + match tryUnderlyingTypeOfEnumTy g ty with + | ValueSome underlyingTy -> underlyingTy + | ValueNone -> + let tycon = (tcrefOfAppTy g ty).Deref + error (InternalError("no 'value__' field found for enumeration type " + tycon.LogicalName, tycon.Range)) let normalizeEnumTy g ty = (if isEnumTy g ty then underlyingTypeOfEnumTy g ty else ty) diff --git a/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fsi b/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fsi index de866c4175d..a6a7717865f 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fsi @@ -636,6 +636,10 @@ module internal TypeTesters = // Return all components of this type expression that cannot be tested at runtime val getErasedTypes: TcGlobals -> TType -> checkForNullness: bool -> TType list + /// Determine the underlying type of an enum type (normally int32). + /// ValueNone while the representation of an F# enum is still being established. + val tryUnderlyingTypeOfEnumTy: TcGlobals -> TType -> TType voption + /// Determine the underlying type of an enum type (normally int32) val underlyingTypeOfEnumTy: TcGlobals -> TType -> TType diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/ConstraintSyntax.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/ConstraintSyntax.fs index d87d014c183..65b51ba44d2 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/ConstraintSyntax.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/ConstraintSyntax.fs @@ -57,3 +57,113 @@ type Ok = I """ |> typecheck |> shouldSucceed + + // https://github.com/dotnet/fsharp/issues/14580 + [] + let ``'enum' constraint on a type of the same recursive group is accepted`` () = + FSharp """ +module rec MyModule + +type MyEnum = + | Alpha = 1 + | Beta = 2 + +type MyInter<'TEnum when 'TEnum : enum> = interface end + +type MyAlias = MyInter + """ + |> asLibrary + |> compile + |> shouldSucceed + + [] + let ``'enum' constraint on a type of the same 'and' group is accepted`` () = + FSharp """ +module MyModule + +type MyEnum = + | Alpha = 1 + | Beta = 2 + +and MyInter<'TEnum when 'TEnum : enum> = interface end + +and MyAlias = MyInter + """ + |> asLibrary + |> compile + |> shouldSucceed + + [] + let ``'enum' constraint on an inherited interface of the same recursive group is accepted`` () = + FSharp """ +module rec MyModule + +type MyEnum = + | Alpha = 1 + +type MyInter<'TEnum when 'TEnum : enum> = interface end + +type C() = + interface MyInter + """ + |> asLibrary + |> compile + |> shouldSucceed + + [] + let ``Mismatched 'enum' constraint on a type of the same recursive group is reported`` () = + FSharp """ +module rec MyModule + +type MyEnum = + | Alpha = 1 + +type MyInter<'TEnum when 'TEnum : enum> = interface end + +type MyAlias = MyInter + """ + |> asLibrary + |> withOptions ["--test:ErrorRanges"] + |> typecheck + |> shouldFail + |> withDiagnosticMessageMatches "The type 'int64' does not match the type 'int'" + + [] + let ``Mismatched 'enum' constraint outside a recursive group is reported`` () = + FSharp """ +module MyModule + +type MyEnum = + | Alpha = 1 + +type MyInter<'TEnum when 'TEnum : enum> = interface end + +type MyAlias = MyInter + """ + |> asLibrary + |> withOptions ["--test:ErrorRanges"] + |> typecheck + |> shouldFail + |> withSingleDiagnostic (Error 1, Line 9, Col 16, Line 9, Col 31, "The type 'int64' does not match the type 'int'") + + [] + let ``Generic code over an 'enum' constraint in a recursive module runs`` () = + FSharp """ +module rec MyModule + +type Color = + | Red = 1 + | Blue = 4 + +let combine<'T when 'T : enum> (a: 'T) (b: 'T) : 'T = + LanguagePrimitives.EnumOfValue (LanguagePrimitives.EnumToValue a ||| LanguagePrimitives.EnumToValue b) + +[] +let main _ = + if LanguagePrimitives.EnumToValue (combine Color.Red Color.Blue) <> 5 then + failwith "expected Red ||| Blue to be 5" + 0 + """ + |> asExe + |> compileExeAndRun + |> shouldSucceed From 3ee74dd15b9af5f1c26f45936d786d241c4051f5 Mon Sep 17 00:00:00 2001 From: Edgar Gonzalez Date: Mon, 7 Sep 2026 07:00:37 +0200 Subject: [PATCH 4/6] Trim comments --- src/Compiler/Checking/ConstraintSolver.fs | 3 +-- src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index 4ce72dbf078..0e94d2de591 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -3124,9 +3124,8 @@ and SolveTypeIsEnum (csenv: ConstraintSolverEnv) ndeep m2 trace ty underlying = match tryUnderlyingTypeOfEnumTy g ty with | ValueSome underlyingTyOfEnum -> SolveTypeEqualsTypeKeepAbbrevs csenv ndeep m2 trace underlying underlyingTyOfEnum + // The underlying type is unknown until the representations of the recursive group are established | ValueNone -> - // The enum is part of the recursive group being checked, so its underlying type is not known - // yet. Solve the constraint once the representations of the group are established. csenv.SolverState.PushPostInferenceCheck( false, fun () -> diff --git a/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs b/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs index 44b833e216d..bcacc77c267 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs @@ -1268,8 +1268,7 @@ module internal TypeTesters = | TType_measure _ -> [ ty ] /// Determine the underlying type of an enum type (normally int32). - /// ValueNone while the representation of an F# enum is still being established: the 'value__' field - /// is only added once the representations of a recursive group are known. + /// ValueNone while the representation of an F# enum is still being established. let tryUnderlyingTypeOfEnumTy (g: TcGlobals) ty = assert (isEnumTy g ty) From 1f47b0c2165f888d8baed73efa92c0fc4f173971 Mon Sep 17 00:00:00 2001 From: Edgar Gonzalez Date: Mon, 7 Sep 2026 07:04:56 +0200 Subject: [PATCH 5/6] Keep the xml docs in the signature file only --- src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs b/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs index bcacc77c267..f37ea1148b0 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs @@ -1267,8 +1267,6 @@ module internal TypeTesters = | _ -> getErasedTypes g domainTy false @ getErasedTypes g rangeTy false | TType_measure _ -> [ ty ] - /// Determine the underlying type of an enum type (normally int32). - /// ValueNone while the representation of an F# enum is still being established. let tryUnderlyingTypeOfEnumTy (g: TcGlobals) ty = assert (isEnumTy g ty) @@ -1300,7 +1298,6 @@ module internal TypeTesters = | Some rf -> ValueSome rf.FormalType | None -> ValueNone - /// Determine the underlying type of an enum type (normally int32) let underlyingTypeOfEnumTy (g: TcGlobals) ty = match tryUnderlyingTypeOfEnumTy g ty with | ValueSome underlyingTy -> underlyingTy From 85658acef1052bf7c0be17e88e74a9e88bb1533b Mon Sep 17 00:00:00 2001 From: Edgar Gonzalez Date: Sun, 13 Sep 2026 00:52:40 +0200 Subject: [PATCH 6/6] Run the postponed post-inference checks in signature files (#14580) An enum constraint applied to an enum in the same recursive group is postponed until the representations of that group are established. CheckOneSigFile never drained that queue, so the mismatch went unreported in a signature file. Drain it as CheckOneImplFile does. --- src/Compiler/Checking/CheckDeclarations.fs | 8 ++++ .../Constraints/ConstraintSyntax.fs | 37 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index c663b819996..99ddff148e2 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -6432,6 +6432,14 @@ let CheckOneSigFile (g, amap, thisCcu, checkForErrors, conditionalDefines, tcSin FinalTypeDefinitionChecksAtEndOfInferenceScope(cenv.infoReader, tcEnv.NameEnv, cenv.tcSink, false, tcEnv.DisplayEnv, tycon)) with RecoverableException exn -> errorRecovery exn sigFile.QualifiedName.Range + // Run any additional checks registered to be run at the end of inference + conditionallySuppressErrorReporting (checkForErrors()) (fun () -> + for check in cenv.css.GetPostInferenceChecksFinal() do + try + check() + with RecoverableException exn -> + errorRecovery exn m) + UpdatePrettyTyparNames.updateModuleOrNamespaceType sigFileType return (tcEnv, sigFileType, cenv.createsGeneratedProvidedTypes) diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/ConstraintSyntax.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/ConstraintSyntax.fs index 65b51ba44d2..422c3e7f662 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/ConstraintSyntax.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/ConstraintSyntax.fs @@ -167,3 +167,40 @@ let main _ = |> asExe |> compileExeAndRun |> shouldSucceed + + // https://github.com/dotnet/fsharp/issues/14580 + // The implementation file has an extra line, so the two diagnostics have distinct ranges. + [] + let ``Mismatched 'enum' constraint in a recursive group is reported in the signature file`` () = + let signature = """module rec Mismatch + +type MyEnum = + | Alpha = 1 + +type MyInter<'TEnum when 'TEnum : enum> = interface end + +type MyAlias = MyInter +""" + + let implementation = """module rec Mismatch + + +type MyEnum = + | Alpha = 1 + +type MyInter<'TEnum when 'TEnum : enum> = interface end + +type MyAlias = MyInter +""" + + FsiSource signature + |> fsFromString + |> FS + |> withAdditionalSourceFile (FsSource implementation) + |> withOptions ["--test:ErrorRanges"] + |> compile + |> shouldFail + |> withDiagnostics [ + (Error 43, Line 8, Col 16, Line 8, Col 31, "The type 'int64' does not match the type 'int'") + (Error 43, Line 9, Col 16, Line 9, Col 31, "The type 'int64' does not match the type 'int'") + ]