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
48baf1b
Fix nullness bugs: #17539, #18013, #18021, #18334, #19042
T-Gro Feb 8, 2026
24ebb80
Add KnownFromConstructor DU case to Nullness and update constraint so…
T-Gro Feb 9, 2026
ef447ce
Revert TypeNullIsExtraValueNew to sound AllowNullLiteral logic and up…
T-Gro Feb 9, 2026
5b86a7d
Fix code quality: restore docs, add comments, remove meaningless test
T-Gro Feb 9, 2026
95ae81e
Fixup: deduplicate ILookup test source, remove redundant comments
T-Gro Feb 9, 2026
f17771d
Fixup: narrow context passing to nullness-only in TcExprFlex and Unif…
T-Gro Feb 9, 2026
6f50b3d
Fixup: extract nullnessContextOnly helper, remove test-only public AP…
T-Gro Feb 9, 2026
ff1ddef
Fixup: remove dead KnownFromConstructor DU case and clean up cosmetic…
T-Gro Feb 9, 2026
23c6dd2
Fixup: use ListSet.isSubsetOf in typarsAEquivWithFilter instead of in…
T-Gro Feb 9, 2026
82a0bc3
Fixup: remove leftover NullnessInternalsTests, restore TypeNullIsExtr…
T-Gro Feb 9, 2026
dbab7a8
Add sprint file 28_Fixup_3.md for HONEST-ASSESSMENT scope verification
T-Gro Feb 9, 2026
e6828bf
AllowNullLiteral not warn if used right after .ctor call
T-Gro Feb 10, 2026
dc43a02
After assignment, strip KnownFromConstructor
T-Gro Feb 10, 2026
95de804
Fix FS1182: move 'let g' back inside #if !NO_TYPEPROVIDERS in TcCtorI…
T-Gro Feb 11, 2026
d33b220
Extract shared AllowNullLiteral+consumeNonNull test preamble to reduc…
T-Gro Feb 11, 2026
d731353
Delete 28_Fixup_3.md
T-Gro Feb 12, 2026
6a320ff
Merge branch 'main' into nullness-bugs
T-Gro Feb 13, 2026
754e5b7
fix |> MyType. usage
T-Gro Feb 13, 2026
453947b
adjust baseline
T-Gro Feb 13, 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/10.0.300.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
### Fixed

* Nullness: Fix UoM ToString returning `string | null` for value types. ([Issue #17539](https://github.com/dotnet/fsharp/issues/17539), [PR #19262](https://github.com/dotnet/fsharp/pull/19262))
* Nullness: Fix pipe operator nullness warning location to point at nullable argument. ([Issue #18013](https://github.com/dotnet/fsharp/issues/18013), [PR #19262](https://github.com/dotnet/fsharp/pull/19262))
* Nullness: Fix false positive warning when passing non-null AllowNullLiteral constructor result. ([Issue #18021](https://github.com/dotnet/fsharp/issues/18021), [PR #19262](https://github.com/dotnet/fsharp/pull/19262))
* Nullness: Allow `not null` constraint on type extensions. ([Issue #18334](https://github.com/dotnet/fsharp/issues/18334), [PR #19262](https://github.com/dotnet/fsharp/pull/19262))
* Nullness: Simplify tuple null elimination to prevent over-inference of non-null. ([Issue #19042](https://github.com/dotnet/fsharp/issues/19042), [PR #19262](https://github.com/dotnet/fsharp/pull/19262))
* Fixed Find All References not correctly finding active pattern cases in signature files. ([Issue #19173](https://github.com/dotnet/fsharp/issues/19173), [Issue #14969](https://github.com/dotnet/fsharp/issues/14969), [PR #19252](https://github.com/dotnet/fsharp/pull/19252))
* Fixed Rename not correctly handling operators containing `.` (e.g., `-.-`). ([Issue #17221](https://github.com/dotnet/fsharp/issues/17221), [Issue #14057](https://github.com/dotnet/fsharp/issues/14057), [PR #19252](https://github.com/dotnet/fsharp/pull/19252))
* Fixed Find All References not correctly applying `#line` directive remapping. ([Issue #9928](https://github.com/dotnet/fsharp/issues/9928), [PR #19252](https://github.com/dotnet/fsharp/pull/19252))
Expand Down
10 changes: 8 additions & 2 deletions src/Compiler/Checking/CheckDeclarations.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4269,13 +4269,19 @@ module TcDeclarations =
let _tpenv = TcTyparConstraints cenv NoNewTypars CheckCxs ItemOccurrence.UseInType envForTycon emptyUnscopedTyparEnv synTyparCxs
declaredTypars |> List.iter (SetTyparRigid envForDecls.DisplayEnv m)

let checkTyparsForExtension () =
if g.checkNullness then
typarsAEquivWithAddedNotNullConstraintsAllowed g (TypeEquivEnv.EmptyWithNullChecks g) reqTypars declaredTypars
else
typarsAEquiv g TypeEquivEnv.EmptyIgnoreNulls reqTypars declaredTypars

if tcref.TypeAbbrev.IsSome then
ExtrinsicExtensionBinding, tcref, declaredTypars
elif isInSameModuleOrNamespace && not isInterfaceOrDelegateOrEnum then
// For historical reasons we only give a warning for incorrect type parameters on intrinsic extensions
if nReqTypars <> synTypars.Length then
errorR(Error(FSComp.SR.tcDeclaredTypeParametersForExtensionDoNotMatchOriginal(tcref.DisplayNameWithStaticParametersAndUnderscoreTypars), m))
if not (typarsAEquiv g (TypeEquivEnv.EmptyWithNullChecks g) reqTypars declaredTypars) then
if not (checkTyparsForExtension()) then
warning(Error(FSComp.SR.tcDeclaredTypeParametersForExtensionDoNotMatchOriginal(tcref.DisplayNameWithStaticParametersAndUnderscoreTypars), m))
// Note we return 'reqTypars' for intrinsic extensions since we may only have given warnings
IntrinsicExtensionBinding, tcref, reqTypars
Expand All @@ -4284,7 +4290,7 @@ module TcDeclarations =
errorR(Error(FSComp.SR.tcMembersThatExtendInterfaceMustBePlacedInSeparateModule(), tcref.Range))
if nReqTypars <> synTypars.Length then
error(Error(FSComp.SR.tcDeclaredTypeParametersForExtensionDoNotMatchOriginal(tcref.DisplayNameWithStaticParametersAndUnderscoreTypars), m))
if not (typarsAEquiv g (TypeEquivEnv.EmptyWithNullChecks g) reqTypars declaredTypars) then
if not (checkTyparsForExtension()) then
errorR(Error(FSComp.SR.tcDeclaredTypeParametersForExtensionDoNotMatchOriginal(tcref.DisplayNameWithStaticParametersAndUnderscoreTypars), m))
ExtrinsicExtensionBinding, tcref, declaredTypars

Expand Down
49 changes: 39 additions & 10 deletions src/Compiler/Checking/ConstraintSolver.fs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ type ContextInfo =
/// The type equation comes from a sequence expression.
| SequenceExpression of TType

/// The type equation comes from a nullness check of a captured argument (e.g., pipe operators).
/// The range points to the original argument location.
| NullnessCheckOfCapturedArg of range

/// Captures relevant information for a particular failed overload resolution.
type OverloadInformation =
{
Expand Down Expand Up @@ -1032,9 +1036,17 @@ and shouldWarnUselessNullCheck (csenv:ConstraintSolverEnv) =
csenv.g.checkNullness &&
csenv.SolverState.WarnWhenUsingWithoutNullOnAWithNullTarget.IsSome

and getNullnessWarningRange (csenv: ConstraintSolverEnv) =
match csenv.eContextInfo with
| ContextInfo.NullnessCheckOfCapturedArg capturedArgRange -> capturedArgRange
| _ -> csenv.m

// nullness1: actual
// nullness2: expected
and SolveNullnessEquiv (csenv: ConstraintSolverEnv) m2 (trace: OptionalTrace) ty1 ty2 nullness1 nullness2 =
and SolveNullnessEquiv (csenv: ConstraintSolverEnv) m2 (trace: OptionalTrace) ty1 ty2 (nullness1: Nullness) (nullness2: Nullness) =
// KnownFromConstructor behaves identically to WithoutNull for unification
let nullness1 = nullness1.Normalize()
let nullness2 = nullness2.Normalize()
match nullness1, nullness2 with
| Nullness.Variable nv1, Nullness.Variable nv2 when nv1 === nv2 ->
CompleteD
Expand Down Expand Up @@ -1062,13 +1074,16 @@ and SolveNullnessEquiv (csenv: ConstraintSolverEnv) m2 (trace: OptionalTrace) ty
| NullnessInfo.WithNull, NullnessInfo.WithoutNull -> CompleteD
| _ ->
if csenv.g.checkNullness then
WarnD(ConstraintSolverNullnessWarningEquivWithTypes(csenv.DisplayEnv, ty1, ty2, n1, n2, csenv.m, m2))
WarnD(ConstraintSolverNullnessWarningEquivWithTypes(csenv.DisplayEnv, ty1, ty2, n1, n2, getNullnessWarningRange csenv, m2))
else
CompleteD
| Nullness.KnownFromConstructor, _ | _, Nullness.KnownFromConstructor -> CompleteD // Unreachable after Normalize()

// nullness1: target
// nullness2: source
and SolveNullnessSubsumesNullness (csenv: ConstraintSolverEnv) m2 (trace: OptionalTrace) ty1 ty2 nullness1 nullness2 =
and SolveNullnessSubsumesNullness (csenv: ConstraintSolverEnv) m2 (trace: OptionalTrace) ty1 ty2 (nullness1: Nullness) (nullness2: Nullness) =
let nullness1 = nullness1.Normalize()
let nullness2 = nullness2.Normalize()
match nullness1, nullness2 with
| Nullness.Variable nv1, Nullness.Variable nv2 when nv1 === nv2 ->
CompleteD
Expand Down Expand Up @@ -1099,9 +1114,10 @@ and SolveNullnessSubsumesNullness (csenv: ConstraintSolverEnv) m2 (trace: Option
CompleteD
| NullnessInfo.WithoutNull, NullnessInfo.WithNull ->
if csenv.g.checkNullness then
WarnD(ConstraintSolverNullnessWarningWithTypes(csenv.DisplayEnv, ty1, ty2, n1, n2, csenv.m, m2))
WarnD(ConstraintSolverNullnessWarningWithTypes(csenv.DisplayEnv, ty1, ty2, n1, n2, getNullnessWarningRange csenv, m2))
else
CompleteD
| Nullness.KnownFromConstructor, _ | _, Nullness.KnownFromConstructor -> CompleteD // Unreachable after Normalize()

and SolveTyparEqualsType (csenv: ConstraintSolverEnv) ndeep m2 (trace: OptionalTrace) ty1 ty =
trackErrors {
Expand Down Expand Up @@ -2697,6 +2713,7 @@ and SolveLegacyTypeUseSupportsNullLiteral (csenv: ConstraintSolverEnv) ndeep m2
}

and SolveNullnessSupportsNull (csenv: ConstraintSolverEnv) ndeep m2 (trace: OptionalTrace) ty nullness =
let nullness = nullness.Normalize()
trackErrors {
let g = csenv.g
let m = csenv.m
Expand All @@ -2718,9 +2735,10 @@ and SolveNullnessSupportsNull (csenv: ConstraintSolverEnv) ndeep m2 (trace: Opti
// If a type would allow null in older rules of F#, we can just emit a warning.
// In the opposite case, we keep this as an error to avoid generating incorrect code (e.g. assigning null to an int)
if (TypeNullIsExtraValue g m ty) then
return! WarnD(ConstraintSolverNullnessWarningWithType(denv, ty, n1, m, m2))
return! WarnD(ConstraintSolverNullnessWarningWithType(denv, ty, n1, getNullnessWarningRange csenv, m2))
else
return! ErrorD (ConstraintSolverError(FSComp.SR.csTypeDoesNotHaveNull(NicePrint.minimalStringOfType denv ty), m, m2))
| Nullness.KnownFromConstructor -> () // Unreachable after Normalize()
}

and SolveTypeUseNotSupportsNull (csenv: ConstraintSolverEnv) ndeep m2 trace ty =
Expand All @@ -2732,10 +2750,16 @@ and SolveTypeUseNotSupportsNull (csenv: ConstraintSolverEnv) ndeep m2 trace ty =
if TypeNullIsTrueValue g ty then
// We can only give warnings here as F# 5.0 introduces these constraints into existing
// code via Option.ofObj and Option.toObj
do! WarnD (ConstraintSolverNullnessWarning(FSComp.SR.csTypeHasNullAsTrueValue(NicePrint.minimalStringOfType denv ty), m, m2))
do! WarnD (ConstraintSolverNullnessWarning(FSComp.SR.csTypeHasNullAsTrueValue(NicePrint.minimalStringOfType denv ty), getNullnessWarningRange csenv, m2))
elif TypeNullIsExtraValueNew g m ty then
if g.checkNullness then
do! WarnD (ConstraintSolverNullnessWarning(FSComp.SR.csTypeHasNullAsExtraValue(NicePrint.minimalStringOfTypeWithNullness denv ty), m, m2))
// Constructor results are provably non-null even for AllowNullLiteral types
let isFromConstructor =
match stripTyEqns g ty with
| TType_app(_, _, Nullness.KnownFromConstructor) -> true
| _ -> false
if not isFromConstructor then
do! WarnD (ConstraintSolverNullnessWarning(FSComp.SR.csTypeHasNullAsExtraValue(NicePrint.minimalStringOfTypeWithNullness denv ty), getNullnessWarningRange csenv, m2))
else
match tryDestTyparTy g ty with
| ValueSome tp ->
Expand All @@ -2746,6 +2770,7 @@ and SolveTypeUseNotSupportsNull (csenv: ConstraintSolverEnv) ndeep m2 trace ty =
}

and SolveNullnessNotSupportsNull (csenv: ConstraintSolverEnv) ndeep m2 (trace: OptionalTrace) ty nullness =
let nullness = nullness.Normalize()
trackErrors {
let g = csenv.g
let m = csenv.m
Expand All @@ -2762,7 +2787,8 @@ and SolveNullnessNotSupportsNull (csenv: ConstraintSolverEnv) ndeep m2 (trace: O
| NullnessInfo.WithoutNull -> ()
| NullnessInfo.WithNull ->
if g.checkNullness && TypeNullIsExtraValueNew g m ty then
return! WarnD(ConstraintSolverNullnessWarning(FSComp.SR.csTypeHasNullAsExtraValue(NicePrint.minimalStringOfTypeWithNullness denv ty), m, m2))
return! WarnD(ConstraintSolverNullnessWarning(FSComp.SR.csTypeHasNullAsExtraValue(NicePrint.minimalStringOfTypeWithNullness denv ty), getNullnessWarningRange csenv, m2))
| Nullness.KnownFromConstructor -> () // Unreachable after Normalize()
}

and SolveTypeCanCarryNullness (csenv: ConstraintSolverEnv) ty nullness =
Expand Down Expand Up @@ -3989,12 +4015,15 @@ let UndoIfFailedOrWarnings f =
trace.Undo()
false

let AddCxTypeEqualsTypeUndoIfFailed denv css m ty1 ty2 =
let AddCxTypeEqualsTypeUndoIfFailedWithContext contextInfo denv css m ty1 ty2 =
UndoIfFailed (fun trace ->
let csenv = MakeConstraintSolverEnv ContextInfo.NoContext css m denv
let csenv = MakeConstraintSolverEnv contextInfo css m denv
let csenv = { csenv with ErrorOnFailedMemberConstraintResolution = true }
SolveTypeEqualsTypeKeepAbbrevs csenv 0 m (WithTrace trace) ty1 ty2)

let AddCxTypeEqualsTypeUndoIfFailed denv css m ty1 ty2 =
AddCxTypeEqualsTypeUndoIfFailedWithContext ContextInfo.NoContext denv css m ty1 ty2

let AddCxTypeEqualsTypeUndoIfFailedOrWarnings denv css m ty1 ty2 =
UndoIfFailedOrWarnings (fun trace ->
let csenv = MakeConstraintSolverEnv ContextInfo.NoContext css m denv
Expand Down
7 changes: 7 additions & 0 deletions src/Compiler/Checking/ConstraintSolver.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ type ContextInfo =
/// The type equation comes from a sequence expression.
| SequenceExpression of TType

/// The type equation comes from a nullness check of a captured argument (e.g., pipe operators).
/// The range points to the original argument location.
| NullnessCheckOfCapturedArg of range

/// Captures relevant information for a particular failed overload resolution.
type OverloadInformation =
{ methodSlot: CalledMeth<Expr>
Expand Down Expand Up @@ -273,6 +277,9 @@ val AddCxTypeEqualsType: ContextInfo -> DisplayEnv -> ConstraintSolverState -> r

val AddCxTypeEqualsTypeUndoIfFailed: DisplayEnv -> ConstraintSolverState -> range -> TType -> TType -> bool

val AddCxTypeEqualsTypeUndoIfFailedWithContext:
ContextInfo -> DisplayEnv -> ConstraintSolverState -> range -> TType -> TType -> bool

val AddCxTypeEqualsTypeUndoIfFailedOrWarnings: DisplayEnv -> ConstraintSolverState -> range -> TType -> TType -> bool

val AddCxTypeEqualsTypeMatchingOnlyUndoIfFailed: DisplayEnv -> ConstraintSolverState -> range -> TType -> TType -> bool
Expand Down
Loading
Loading