From 9318ef43f95efe51ac8e27f6a915b8cd30a93550 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 15 May 2026 14:43:26 +0200 Subject: [PATCH 1/4] Add failing TDD tests for #19657: MeasureAnnotatedAbbreviation over reference type with | null Add four tests in NullableReferenceTypesTests.fs covering: - MeasureAnnotatedAbbreviation over string accepts | null in let bindings, parameters, returns, type abbreviations, and inline instantiations (fails today). - MeasureAnnotatedAbbreviation over user-defined reference class accepts | null (fails today). - MeasureAnnotatedAbbreviation over value types still rejects | null (non-regression, passes today). - Nullness flow and not-null constraints work through the abbreviation (fails today; compilation does not reach the flow assertions). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Nullness/NullableReferenceTypesTests.fs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs index c36c51b3a15..dd6119c5505 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs @@ -2065,6 +2065,104 @@ let processMyStr (x:mystring) = Error 3261, Line 12, Col 27, Line 12, Col 39, "Nullness warning: A non-nullable 'string' was expected but this expression is nullable. Consider either changing the target to also be nullable, or use pattern matching to safely handle the null case of this expression." ] +[] +let ``MeasureAnnotatedAbbreviation over string allows nullable annotation in all positions`` () = + FSharp """module MyLibrary + +[] +type string<[] 'm> = string + +[] type test +type TestType = string + +let x: (TestType | null) = Unchecked.defaultof + +let consume (s: TestType | null) = () + +let produce () : TestType | null = null + +type NullableTestType = TestType | null + +let y: (string | null) = null +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldSucceed + +[] +let ``MeasureAnnotatedAbbreviation over user reference type allows nullable annotation`` () = + FSharp """module MyLibrary + +type MyRef() = member _.Hello = "hi" + +[] +type MyRef<[] 'm> = MyRef + +[] type tag +type Tagged = MyRef + +let f (x: Tagged | null) : Tagged | null = x +let g : Tagged | null = null +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldSucceed + +[] +let ``MeasureAnnotatedAbbreviation over value type rejects nullable annotation`` () = + FSharp """module MyLibrary + +[] +type int<[] 'm> = int + +[] +type DateTime<[] 'm> = System.DateTime + +[] type kg +[] type s + +let bad1 : (int | null) = null +let bad2 : (DateTime | null) = null +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldFail + |> withDiagnostics [ + Error 3260, Line 12, Col 13, Line 12, Col 27, "The type 'int' does not support a nullness qualification." + Error 43, Line 12, Col 13, Line 12, Col 27, "A generic construct requires that the type 'int' have reference semantics, but it does not, i.e. it is a struct" + Error 3260, Line 13, Col 13, Line 13, Col 31, "The type 'DateTime' does not support a nullness qualification." + Error 43, Line 13, Col 13, Line 13, Col 31, "A generic construct requires that the type 'DateTime' have reference semantics, but it does not, i.e. it is a struct" + ] + +[] +let ``Nullness flow and not-null constraints work with MeasureAnnotatedAbbreviation over string`` () = + FSharp """module MyLibrary + +[] +type string<[] 'm> = string + +[] type tag +type S = string + +let onlyNotNull (s: S) = () +let onlyNotNullPlain (s: string) = () + +let widen (x: S) : S | null = x + +let narrowBad (x: S | null) : S = x + +let matched (x: S | null) = + match x with + | null -> () + | nn -> onlyNotNull nn +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldFail + |> withDiagnostics [ + Error 3261, Line 16, Col 35, Line 16, Col 36, "Nullness warning: The types 'string' and 'string | null' do not have compatible nullability." + ] + [] let ``ToString on reference type still returns nullable string`` () = FSharp """module MyLibrary From 04550817b0b7f4c0d7f051070819c368b8bbdda6 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 15 May 2026 15:04:30 +0200 Subject: [PATCH 2/4] Fix #19657: strip measure equations in SolveTypeIsReferenceType MeasureAnnotatedAbbreviation tycons over a reference type (e.g. UMX-style `type string<[] 'm> = string`) previously failed the reference-type constraint check used by the `| null` syntax, producing FS0043. The surface type is a TType_app over a MeasureableReprTycon, which isRefTy does not recognise. Strip measure equations before testing reference semantics so the underlying erased representation is consulted, consistent with TypeNullNever and IsReferenceTyparTy. Value-type erasure still correctly fails FS0043. Also corrects a Sprint 1 test expectation that listed a phantom diagnostic which the compiler does not emit (the matched-on null case is fully covered). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/ConstraintSolver.fs | 6 +++++- .../Language/Nullness/NullableReferenceTypesTests.fs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index 8a567e0ecef..bb4e93330fe 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -2990,7 +2990,11 @@ and SolveTypeIsReferenceType (csenv: ConstraintSolverEnv) ndeep m2 trace ty = | ValueSome destTypar -> AddConstraint csenv ndeep m2 trace destTypar (TyparConstraint.IsReferenceType m) | _ -> - if isRefTy g ty then CompleteD + // For MeasureAnnotatedAbbreviation tycons (e.g. UMX-style `type string<[] 'm> = string`) + // the surface type is a `TType_app` over a MeasureableReprTycon, which `isRefTy` does not recognise. + // Strip measure equations so we test the underlying erased representation. See dotnet/fsharp#19657. + let strippedTy = stripTyEqnsAndMeasureEqns g ty + if isRefTy g strippedTy then CompleteD else ErrorD (ConstraintSolverError(FSComp.SR.csGenericConstructRequiresReferenceSemantics(NicePrint.minimalStringOfType denv ty), m, m)) and SolveTypeRequiresDefaultConstructor (csenv: ConstraintSolverEnv) ndeep m2 trace origTy = diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs index dd6119c5505..791154a1b39 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs @@ -2160,7 +2160,7 @@ let matched (x: S | null) = |> typeCheckWithStrictNullness |> shouldFail |> withDiagnostics [ - Error 3261, Line 16, Col 35, Line 16, Col 36, "Nullness warning: The types 'string' and 'string | null' do not have compatible nullability." + Error 3261, Line 14, Col 35, Line 14, Col 36, "Nullness warning: A non-nullable 'S' was expected but this expression is nullable. Consider either changing the target to also be nullable, or use pattern matching to safely handle the null case of this expression." ] [] From 2d9641fd7585f898561f636b0aa0e48306f03945 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 15 May 2026 15:32:33 +0200 Subject: [PATCH 3/4] Release notes for #19657 MeasureAnnotatedAbbreviation + | null fix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + 1 file changed, 1 insertion(+) 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 3e2c18a6ef0..763645a4133 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -53,6 +53,7 @@ * Fix parallel compilation of scripts ([PR #19649](https://github.com/dotnet/fsharp/pull/19649)) * Fix parser recovery, name resolution, and code completion for unfinished enum patterns ([PR #19708](https://github.com/dotnet/fsharp/pull/19708)) * Parser: fix unexpected diagnostics in debug builds, improve error messages ([PR #19730](https://github.com/dotnet/fsharp/pull/19730)) +* Allow `| null` nullable annotation on a `[]` over a reference type (e.g. the FSharp.UMX `type string<[] 'm> = string` pattern). ([Issue #19657](https://github.com/dotnet/fsharp/issues/19657)) ### Added From 6cf553c703d8deeafb67bb1d64c88e7a36519a81 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Tue, 19 May 2026 11:52:06 +0200 Subject: [PATCH 4/4] Address review findings: rename strippedTy, add edge-case tests - Rename strippedTy -> underlyingTy for consistency with SolveTypeIsNonNullableValueType - Condense comment to single line - Add test: not-struct constraint satisfied by MeasureAnnotatedAbbreviation - Add test: MAA over obj, interface, and chained abbreviation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/ConstraintSolver.fs | 8 ++-- .../Nullness/NullableReferenceTypesTests.fs | 46 ++++++++++++++++++- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index bb4e93330fe..b739cede2de 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -2990,11 +2990,9 @@ and SolveTypeIsReferenceType (csenv: ConstraintSolverEnv) ndeep m2 trace ty = | ValueSome destTypar -> AddConstraint csenv ndeep m2 trace destTypar (TyparConstraint.IsReferenceType m) | _ -> - // For MeasureAnnotatedAbbreviation tycons (e.g. UMX-style `type string<[] 'm> = string`) - // the surface type is a `TType_app` over a MeasureableReprTycon, which `isRefTy` does not recognise. - // Strip measure equations so we test the underlying erased representation. See dotnet/fsharp#19657. - let strippedTy = stripTyEqnsAndMeasureEqns g ty - if isRefTy g strippedTy then CompleteD + // Strip measure equations so we test the underlying erased representation — see dotnet/fsharp#19657. + let underlyingTy = stripTyEqnsAndMeasureEqns g ty + if isRefTy g underlyingTy then CompleteD else ErrorD (ConstraintSolverError(FSComp.SR.csGenericConstructRequiresReferenceSemantics(NicePrint.minimalStringOfType denv ty), m, m)) and SolveTypeRequiresDefaultConstructor (csenv: ConstraintSolverEnv) ndeep m2 trace origTy = diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs index 791154a1b39..c45d73553e2 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs @@ -2145,7 +2145,6 @@ type string<[] 'm> = string type S = string let onlyNotNull (s: S) = () -let onlyNotNullPlain (s: string) = () let widen (x: S) : S | null = x @@ -2160,9 +2159,52 @@ let matched (x: S | null) = |> typeCheckWithStrictNullness |> shouldFail |> withDiagnostics [ - Error 3261, Line 14, Col 35, Line 14, Col 36, "Nullness warning: A non-nullable 'S' was expected but this expression is nullable. Consider either changing the target to also be nullable, or use pattern matching to safely handle the null case of this expression." + Error 3261, Line 13, Col 35, Line 13, Col 36, "Nullness warning: A non-nullable 'S' was expected but this expression is nullable. Consider either changing the target to also be nullable, or use pattern matching to safely handle the null case of this expression." ] +[] +let ``MeasureAnnotatedAbbreviation satisfies not-struct constraint`` () = + FSharp """module MyLibrary + +[] +type string<[] 'm> = string + +[] type tag +type S = string + +let needsRef<'T when 'T : not struct> (x: 'T) = () +let callIt (s: S) = needsRef s +let callNullable (s: S | null) = needsRef s +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldSucceed + +[] +let ``MeasureAnnotatedAbbreviation over obj, interface, and chained abbreviation allows nullable`` () = + FSharp """module MyLibrary + +[] +type obj<[] 'm> = obj + +[] +type IDisposable<[] 'm> = System.IDisposable + +[] +type string<[] 'm> = string + +type chainedString<[] 'm> = string<'m> + +[] type tag + +let a : obj | null = null +let b : IDisposable | null = null +let c : chainedString | null = null +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldSucceed + [] let ``ToString on reference type still returns nullable string`` () = FSharp """module MyLibrary