Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 12 additions & 2 deletions src/Compiler/Checking/Expressions/CheckExpressions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions src/Compiler/TypedTree/TypedTreeOps.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/Compiler/TypedTree/TypedTreeOps.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -1554,3 +1554,38 @@ let y = x :> IEquatable<string> // Should not warn about nullness
|> asLibrary
|> typeCheckWithStrictNullness
|> shouldSucceed

[<Fact>]
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
Loading