diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index a2c1a2d4e9..23c45f0a65 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -10733,8 +10733,18 @@ and TcMatchClause cenv inputTy (resultTy: OverallTy) env isFirst tpenv synMatchC let inputTypeForNextPatterns= let removeNull t = - let stripped = stripTyEqns cenv.g t - replaceNullnessOfTy KnownWithoutNull stripped + // Preserve original type structure while refining nullness + match stripTyEqns cenv.g t with + | TType_app (tcref, _, _) when not tcref.Deref.IsStructOrEnumTycon -> + // Apply to original type to preserve aliases + match t with + | TType_app (tcrefOrig, tinstOrig, _) -> TType_app (tcrefOrig, tinstOrig, KnownWithoutNull) + | _ -> replaceNullnessOfTy KnownWithoutNull t + | TType_var _ -> + match t with + | TType_var (tpOrig, _) -> TType_var (tpOrig, KnownWithoutNull) + | _ -> replaceNullnessOfTy KnownWithoutNull t + | _ -> t let rec isWild (p:Pattern) = match p with | TPat_wild _ -> true diff --git a/src/Compiler/TypedTree/TypedTreeOps.fs b/src/Compiler/TypedTree/TypedTreeOps.fs index 368dd9a99c..e6f6e12111 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.fs @@ -782,6 +782,27 @@ let rec stripTyEqnsA g canShortcut ty = let stripTyEqns g ty = stripTyEqnsA g false ty +/// Try to refine a type by removing 'null' from its top-level nullness, preserving any type abbreviations. +/// - Strip type equations/abbreviations only for the purpose of deciding if we can remove 'null'. +/// - If applicable, apply the refinement to the original 'ty' using replaceNullnessOfTy, so aliases are not discarded. +/// - Only refine reference-like heads (including type variables). +let tryRefineToNonNullPreservingAbbrev (g: TcGlobals) (ty: TType) : TType option = + // Use stripTyEqns to decide if we can refine, but apply to original type + let stripped = stripTyEqns g ty + match stripped with + | TType_app (tcref, _, _) when not tcref.Deref.IsStructOrEnumTycon -> + // Apply refinement to original type structure to preserve aliases + match ty with + | TType_app (tcrefOrig, tinstOrig, _) -> Some (TType_app (tcrefOrig, tinstOrig, KnownWithoutNull)) + | TType_var (tpOrig, _) -> Some (TType_var (tpOrig, KnownWithoutNull)) + | TType_fun (dOrig, rOrig, _) -> Some (TType_fun (dOrig, rOrig, KnownWithoutNull)) + | _ -> Some (replaceNullnessOfTy KnownWithoutNull ty) + | TType_var _ -> + match ty with + | TType_var (tpOrig, _) -> Some (TType_var (tpOrig, KnownWithoutNull)) + | _ -> Some (replaceNullnessOfTy KnownWithoutNull ty) + | _ -> None + let evalTupInfoIsStruct aexpr = match aexpr with | TupInfo.Const b -> b diff --git a/src/Compiler/TypedTree/TypedTreeOps.fsi b/src/Compiler/TypedTree/TypedTreeOps.fsi index 06200be47f..1375ee1c54 100755 --- a/src/Compiler/TypedTree/TypedTreeOps.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.fsi @@ -609,6 +609,12 @@ val stripTyEqnsA: TcGlobals -> bool -> TType -> TType val stripTyEqns: TcGlobals -> TType -> TType +/// Try to refine a type by removing 'null' from its top-level nullness, preserving any type abbreviations. +/// - Strip type equations/abbreviations only for the purpose of deciding if we can remove 'null'. +/// - If applicable, apply the refinement to the original 'ty' using replaceNullnessOfTy, so aliases are not discarded. +/// - Only refine reference-like heads (including type variables). +val tryRefineToNonNullPreservingAbbrev: TcGlobals -> TType -> TType option + val stripTyEqnsAndMeasureEqns: TcGlobals -> TType -> TType val tryNormalizeMeasureInType: TcGlobals -> TType -> TType diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/Match_Null_DefaultingAndAlias.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/Match_Null_DefaultingAndAlias.fs new file mode 100644 index 0000000000..5bf2fab538 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/Match_Null_DefaultingAndAlias.fs @@ -0,0 +1,28 @@ +module Nullness.Match_Null_DefaultingAndAlias + +type objnull = obj | null +type stringnull = string | null + +// 1) Defaulting case: result unconstrained; null pattern forces a nullable top type. +let getEnvDefault (_: string) = failwith "" + +let valueDefault = + match "ENVVAR" |> getEnvDefault with + | null -> "missing" + | x -> x.ToString() // x must be refined to obj (non-null) + +// 2) Alias to obj | null +let getEnvAliasObj (_: string) : objnull = failwith "stub" + +let valueAliasObj = + match getEnvAliasObj "ENVVAR" with + | null -> "missing" + | x -> x.ToString() // x must be refined to obj (non-null) + +// 3) Alias to string | null +let getEnvAliasStr (_: string) : stringnull = failwith "stub" + +let valueAliasStr = + match getEnvAliasStr "ENVVAR" with + | null -> 0 + | s -> s.Length // s must be refined to string (non-null) \ No newline at end of file diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs index 4d0710ba89..dca43b4da6 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs @@ -1554,3 +1554,38 @@ let y = x :> IEquatable // Should not warn about nullness |> asLibrary |> typeCheckWithStrictNullness |> shouldSucceed + +[] +let ``Match null pattern refines type to non-null preserving aliases`` () = + FSharp """module Test + +type objnull = obj | null +type stringnull = string | null + +// 1) Defaulting case: result unconstrained; null pattern forces a nullable top type. +let getEnvDefault (_: string) = failwith "" + +let valueDefault = + match "ENVVAR" |> getEnvDefault with + | null -> "missing" + | x -> x.ToString() // x must be refined to obj (non-null) + +// 2) Alias to obj | null +let getEnvAliasObj (_: string) : objnull = failwith "stub" + +let valueAliasObj = + match getEnvAliasObj "ENVVAR" with + | null -> "missing" + | x -> x.ToString() // x must be refined to obj (non-null) + +// 3) Alias to string | null +let getEnvAliasStr (_: string) : stringnull = failwith "stub" + +let valueAliasStr = + match getEnvAliasStr "ENVVAR" with + | null -> 0 + | s -> s.Length // s must be refined to string (non-null) + """ + |> withNullnessOptions + |> typecheck + |> shouldSucceed