Skip to content

Commit

Permalink
Feature nullness :: Bugfixes (#17102)
Browse files Browse the repository at this point in the history
* Ignore Nullness applied on structs (C# allows T? when when T is a struct)
* Bigfix: Working with CLI events in Fsharp
* Bugfix: Mutable binding initially assigned to null should not need type annotation
* Solving `let mutable cache = null` via type inference
* Enforcing TyparConstraint.IsReferenceType when WithNull type is used
* Nullness-related constraint consistency
* Bugfix for emitting Nullable attrs for C#
  • Loading branch information
T-Gro authored May 21, 2024
1 parent 13e50b4 commit e2e698d
Show file tree
Hide file tree
Showing 32 changed files with 1,134 additions and 283 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,5 @@ tests/FSharp.Compiler.Service.Tests/FSharp.CompilerService.SurfaceArea.netstanda
*.vsp
/tests/AheadOfTime/Trimming/output.txt
*.svclog
micro.exe
positive.exe
10 changes: 8 additions & 2 deletions src/Compiler/AbstractIL/ilread.fs
Original file line number Diff line number Diff line change
Expand Up @@ -931,8 +931,9 @@ let mkCacheInt32 lowMem _inbase _nm _sz =
(fun f x -> f x)
else
let mutable cache: ConcurrentDictionary<int32, _> MaybeNull = null // TODO NULLNESS: this explicit annotation should not be needed
let mutable count = 0
#if STATISTICS
let mutable count = 0

addReport (fun oc ->
if count <> 0 then
oc.WriteLine((_inbase + string count + " " + _nm + " cache hits"): string))
Expand All @@ -948,7 +949,9 @@ let mkCacheInt32 lowMem _inbase _nm _sz =

match cache.TryGetValue idx with
| true, res ->
#if STATISTICS
count <- count + 1
#endif
res
| _ ->
let res = f idx
Expand All @@ -960,8 +963,9 @@ let mkCacheGeneric lowMem _inbase _nm _sz =
(fun f x -> f x)
else
let mutable cache: ConcurrentDictionary<_, _> MaybeNull = null // TODO NULLNESS: this explicit annotation should not be needed
let mutable count = 0
#if STATISTICS
let mutable count = 0

addReport (fun oc ->
if !count <> 0 then
oc.WriteLine((_inbase + string !count + " " + _nm + " cache hits"): string))
Expand All @@ -977,7 +981,9 @@ let mkCacheGeneric lowMem _inbase _nm _sz =

match cache.TryGetValue idx with
| true, v ->
#if STATISTICS
count <- count + 1
#endif
v
| _ ->
let res = f idx
Expand Down
5 changes: 4 additions & 1 deletion src/Compiler/Checking/CheckExpressions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,7 @@ let TcAddNullnessToType (warn: bool) (cenv: cenv) (env: TcEnv) nullness innerTyC

if not g.compilingFSharpCore || not (isTyparTy g innerTyC) then
AddCxTypeDefnNotSupportsNull env.DisplayEnv cenv.css m NoTrace innerTyC
AddCxTypeIsReferenceType env.DisplayEnv cenv.css m NoTrace innerTyC

if not g.compilingFSharpCore && isTyparTy g innerTyC then
// A typar might be later infered into a type not supporting `| null|, like tuple or anon.
Expand Down Expand Up @@ -5314,6 +5315,7 @@ and TcExprFlex (cenv: cenv) flex compat (desiredTy: TType) (env: TcEnv) tpenv (s

if flex then
let argTy = NewInferenceType g
(destTyparTy g argTy).SetSupportsNullFlex(true)
if compat then
(destTyparTy g argTy).SetIsCompatFlex(true)

Expand Down Expand Up @@ -9601,7 +9603,8 @@ and TcEventItemThen (cenv: cenv) overallTy env tpenv mItem mExprAndItem objDetai
| None, false -> error (Error (FSComp.SR.tcEventIsNotStatic nm, mItem))
| _ -> ()

let delTy = einfo.GetDelegateType(cenv.amap, mItem)
// The F# wrappers around events are null safe (impl is in FSharp.Core). Therefore, from an F# perspective, the type of the delegate can be considered Not Null.
let delTy = einfo.GetDelegateType(cenv.amap, mItem) |> replaceNullnessOfTy KnownWithoutNull
let (SigOfFunctionForDelegate(delInvokeMeth, delArgTys, _, _)) = GetSigOfFunctionForDelegate cenv.infoReader delTy mItem ad
let objArgs = Option.toList (Option.map fst objDetails)
MethInfoChecks g cenv.amap true None objArgs env.eAccessRights mItem delInvokeMeth
Expand Down
47 changes: 41 additions & 6 deletions src/Compiler/Checking/ConstraintSolver.fs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,9 @@ type ConstraintSolverEnv =
// Is this speculative, with a trace allowing undo, and trial method overload resolution
IsSpeculativeForMethodOverloading: bool

// Can this ignore the 'must support null' constraint, e.g. in a mutable assignment scenario
IsSupportsNullFlex: bool

/// Indicates that when unifying ty1 = ty2, only type variables in ty1 may be solved. Constraints
/// can't be added to type variables in ty2
MatchingOnly: bool
Expand Down Expand Up @@ -344,6 +347,7 @@ let MakeConstraintSolverEnv contextInfo css m denv =
EquivEnv = TypeEquivEnv.Empty
DisplayEnv = denv
IsSpeculativeForMethodOverloading = false
IsSupportsNullFlex = false
ExtraRigidTypars = emptyFreeTypars
}

Expand Down Expand Up @@ -953,6 +957,13 @@ let rec SolveTyparEqualsTypePart1 (csenv: ConstraintSolverEnv) m2 (trace: Option
// Record the solution before we solve the constraints, since
// We may need to make use of the equation when solving the constraints.
// Record a entry in the undo trace if one is provided

//let ty1AllowsNull = r.Constraints |> List.exists (function | TyparConstraint.SupportsNull _ -> true | _ -> false )
//let tyAllowsNull() = TypeNullIsExtraValueNew csenv.g m2 ty
//if ty1AllowsNull && not (tyAllowsNull()) then
// trace.Exec (fun () -> r.typar_solution <- Some (ty |> replaceNullnessOfTy csenv.g.knownWithNull)) (fun () -> r.typar_solution <- None)
//else
// trace.Exec (fun () -> r.typar_solution <- Some ty) (fun () -> r.typar_solution <- None)
trace.Exec (fun () -> r.typar_solution <- Some ty) (fun () -> r.typar_solution <- None)
}

Expand Down Expand Up @@ -1218,6 +1229,12 @@ and SolveTypeEqualsType (csenv: ConstraintSolverEnv) ndeep m2 (trace: OptionalTr
let sty1 = stripTyEqnsA csenv.g canShortcut ty1
let sty2 = stripTyEqnsA csenv.g canShortcut ty2

let csenv =
match ty1 with
| TType.TType_var(r,_) when r.typar_flags.IsSupportsNullFlex ->
{ csenv with IsSupportsNullFlex = true}
| _ -> csenv

match sty1, sty2 with
// type vars inside forall-types may be alpha-equivalent
| TType_var (tp1, nullness1), TType_var (tp2, nullness2) when typarEq tp1 tp2 || (match aenv.EquivTypars.TryFind tp1 with | Some tpTy1 when typeEquiv g tpTy1 ty2 -> true | _ -> false) ->
Expand Down Expand Up @@ -1273,6 +1290,15 @@ and SolveTypeEqualsType (csenv: ConstraintSolverEnv) ndeep m2 (trace: OptionalTr
// Unifying 'T1? and 'T2?
| ValueSome NullnessInfo.WithNull, ValueSome NullnessInfo.WithNull ->
SolveTyparEqualsType csenv ndeep m2 trace sty1 (replaceNullnessOfTy g.knownWithoutNull sty2)
| ValueSome NullnessInfo.WithoutNull, ValueSome NullnessInfo.WithoutNull when
csenv.IsSupportsNullFlex &&
isAppTy g sty2 &&
tp1.Constraints |> List.exists (function TyparConstraint.SupportsNull _ -> true | _ -> false) ->
let tpNew = NewCompGenTypar(TyparKind.Type, TyparRigidity.Flexible, TyparStaticReq.None, TyparDynamicReq.No, false)
trackErrors {
do! SolveTypeEqualsType csenv ndeep m2 trace cxsln (TType_var(tpNew, g.knownWithoutNull)) sty2
do! SolveTypeEqualsType csenv ndeep m2 trace cxsln ty1 (TType_var(tpNew, g.knownWithNull))
}
// Unifying 'T1 % and 'T2 %
//| ValueSome NullnessInfo.AmbivalentToNull, ValueSome NullnessInfo.AmbivalentToNull ->
// SolveTyparEqualsType csenv ndeep m2 trace sty1 (replaceNullnessOfTy g.knownWithoutNull sty2)
Expand Down Expand Up @@ -2354,6 +2380,10 @@ and EnforceConstraintConsistency (csenv: ConstraintSolverEnv) ndeep m2 trace ret
| TyparConstraint.IsNonNullableStruct _, TyparConstraint.IsReferenceType _
| TyparConstraint.IsReferenceType _, TyparConstraint.IsNonNullableStruct _ ->
return! ErrorD (Error(FSComp.SR.csStructConstraintInconsistent(), m))

| TyparConstraint.SupportsNull _, TyparConstraint.NotSupportsNull _
| TyparConstraint.NotSupportsNull _, TyparConstraint.SupportsNull _ ->
return! ErrorD (Error(FSComp.SR.csNullNotNullConstraintInconsistent(), m))

| TyparConstraint.IsUnmanaged _, TyparConstraint.IsReferenceType _
| TyparConstraint.IsReferenceType _, TyparConstraint.IsUnmanaged _ ->
Expand Down Expand Up @@ -2534,16 +2564,18 @@ and SolveTypeUseSupportsNull (csenv: ConstraintSolverEnv) ndeep m2 trace ty =
return! ErrorD (ConstraintSolverError(FSComp.SR.csNullableTypeDoesNotHaveNull(NicePrint.minimalStringOfType denv ty), m, m2))
else
match tryDestTyparTy g ty with
| ValueSome tp ->
| ValueSome tp ->
let nullness = nullnessOfTy g ty
match nullness.TryEvaluate() with
// NULLNESS TODO: This rule means turning on checkNullness changes type inference results for the cases
// mentioned in the comment above. THat's OK but needs to be documented in the RFC.
| ValueNone when not g.checkNullness ->
return! AddConstraint csenv ndeep m2 trace tp (TyparConstraint.SupportsNull m)
| ValueSome NullnessInfo.WithoutNull ->
return! AddConstraint csenv ndeep m2 trace tp (TyparConstraint.SupportsNull m)
| ValueSome NullnessInfo.WithoutNull ->
return! AddConstraint csenv ndeep m2 trace tp (TyparConstraint.SupportsNull m)
| _ ->
if tp.Constraints |> List.exists (function | TyparConstraint.IsReferenceType _ -> true | _ -> false) |> not then
do! AddConstraint csenv ndeep m2 trace tp (TyparConstraint.IsReferenceType m)
return! SolveNullnessSupportsNull csenv ndeep m2 trace ty nullness
| _ ->
let nullness = nullnessOfTy g ty
Expand Down Expand Up @@ -2580,9 +2612,10 @@ and SolveNullnessSupportsNull (csenv: ConstraintSolverEnv) ndeep m2 (trace: Opti
match n1 with
| NullnessInfo.AmbivalentToNull -> ()
| NullnessInfo.WithNull -> ()
| NullnessInfo.WithoutNull ->
| NullnessInfo.WithoutNull ->
if g.checkNullness then
return! WarnD(ConstraintSolverNullnessWarningWithType(denv, ty, n1, m, m2))
// TODO nullness: Shouldn't this be an error? We have a 'must support null' situation which is not being met.
return! WarnD(ConstraintSolverNullnessWarningWithType(denv, ty, n1, m, m2))
}

and SolveTypeUseNotSupportsNull (csenv: ConstraintSolverEnv) ndeep m2 trace ty =
Expand Down Expand Up @@ -2635,7 +2668,9 @@ and SolveTypeCanCarryNullness (csenv: ConstraintSolverEnv) ty nullness =
let m = csenv.m
let strippedTy = stripTyEqnsA g true ty
match tryAddNullnessToTy nullness strippedTy with
| Some _ -> ()
| Some _ ->
if isTyparTy g strippedTy && not (isReferenceTyparTy g strippedTy) then
return! AddConstraint csenv 0 m NoTrace (destTyparTy g strippedTy) (TyparConstraint.IsReferenceType m)
| None ->
let tyString = NicePrint.minimalStringOfType csenv.DisplayEnv strippedTy
return! ErrorD(Error(FSComp.SR.tcTypeDoesNotHaveAnyNull(tyString), m))
Expand Down
12 changes: 9 additions & 3 deletions src/Compiler/CodeGen/IlxGen.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5648,17 +5648,23 @@ and GenGenericParam cenv eenv (tp: Typar) =
| TyparConstraint.IsNonNullableStruct _ -> true
| _ -> false)

let notNullReferenceTypeConstraint =
let nullnessOfTypar =
if g.langFeatureNullness && g.checkNullness then
let hasNotSupportsNull =
tp.Constraints
|> List.exists (function
| TyparConstraint.NotSupportsNull _ -> true
| _ -> false)

let hasSupportsNull () =
tp.Constraints
|> List.exists (function
| TyparConstraint.SupportsNull _ -> true
| _ -> false)

if hasNotSupportsNull || notNullableValueTypeConstraint then
NullnessInfo.WithoutNull
elif hasNotSupportsNull || refTypeConstraint then
elif refTypeConstraint || hasSupportsNull () then
NullnessInfo.WithNull
else
NullnessInfo.AmbivalentToNull
Expand Down Expand Up @@ -5711,7 +5717,7 @@ and GenGenericParam cenv eenv (tp: Typar) =
yield! GenAttrs cenv eenv tp.Attribs
if emitUnmanagedInIlOutput then
yield (GetIsUnmanagedAttribute g)
match notNullReferenceTypeConstraint with
match nullnessOfTypar with
| Some nullInfo -> yield GetNullableAttribute g [ nullInfo ]
| _ -> ()
]
Expand Down
2 changes: 1 addition & 1 deletion src/Compiler/CodeGen/IlxGenSupport.fs
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,8 @@ let rec GetNullnessFromTType (g: TcGlobals) ty =
]
| TType_forall _
| TType_ucase _
| TType_var _
| TType_measure _ -> []
| TType_var(nullness = nullness) -> [ nullness.Evaluate() ]

let GenNullnessIfNecessary (g: TcGlobals) ty =
if g.langFeatureNullness && g.checkNullness then
Expand Down
36 changes: 27 additions & 9 deletions src/Compiler/TypedTree/TypedTree.fs
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ type TyparRigidity =
[<Struct>]
type TyparFlags(flags: int32) =

new (kind: TyparKind, rigidity: TyparRigidity, isFromError: bool, isCompGen: bool, staticReq: TyparStaticReq, dynamicReq: TyparDynamicReq, equalityDependsOn: bool, comparisonDependsOn: bool) =
new (kind: TyparKind, rigidity: TyparRigidity, isFromError: bool, isCompGen: bool, staticReq: TyparStaticReq, dynamicReq: TyparDynamicReq, equalityDependsOn: bool, comparisonDependsOn: bool, supportsNullFlex: bool) =
TyparFlags((if isFromError then 0b00000000000000010 else 0) |||
(if isCompGen then 0b00000000000000100 else 0) |||
(match staticReq with
Expand All @@ -321,7 +321,11 @@ type TyparFlags(flags: int32) =
| TyparDynamicReq.No -> 0b00000000000000000
| TyparDynamicReq.Yes -> 0b00000010000000000) |||
(if equalityDependsOn then
0b00000100000000000 else 0))
0b00000100000000000 else 0) |||
// 0b00001000100000000 is being checked by x.Kind, but never set in this version of the code
// 0b00010000000000000 is taken by compat flex
(if supportsNullFlex then
0b00100000000000000 else 0))

/// Indicates if the type inference variable was generated after an error when type checking expressions or patterns
member x.IsFromError = (flags &&& 0b00000000000000010) <> 0x0
Expand Down Expand Up @@ -380,8 +384,20 @@ type TyparFlags(flags: int32) =
else
TyparFlags(flags &&& ~~~0b00010000000000000)

/// Indicates that whether this type parameter is flexible for 'supports null' constraint, e.g. in the case of assignment to a mutable value
member x.IsSupportsNullFlex =
(flags &&& 0b00100000000000000) <> 0x0

member x.WithSupportsNullFlex b =
if b then
TyparFlags(flags ||| 0b00100000000000000)
else
TyparFlags(flags &&& ~~~0b00100000000000000)



member x.WithStaticReq staticReq =
TyparFlags(x.Kind, x.Rigidity, x.IsFromError, x.IsCompilerGenerated, staticReq, x.DynamicReq, x.EqualityConditionalOn, x.ComparisonConditionalOn)
TyparFlags(x.Kind, x.Rigidity, x.IsFromError, x.IsCompilerGenerated, staticReq, x.DynamicReq, x.EqualityConditionalOn, x.ComparisonConditionalOn, x.IsSupportsNullFlex)

/// Get the flags as included in the F# binary metadata. We pickle this as int64 to allow for future expansion
member x.PickledBits = flags
Expand Down Expand Up @@ -2321,6 +2337,8 @@ type Typar =
/// Set whether this type parameter is a compat-flex type parameter (i.e. where "expr :> tp" only emits an optional warning)
member x.SetIsCompatFlex b = x.typar_flags <- x.typar_flags.WithCompatFlex b

member x.SetSupportsNullFlex b = x.typar_flags <- x.typar_flags.WithSupportsNullFlex b

/// Indicates whether a type variable can be instantiated by types or units-of-measure.
member x.Kind = x.typar_flags.Kind

Expand Down Expand Up @@ -2425,12 +2443,12 @@ type Typar =
/// Sets the rigidity of a type variable
member x.SetRigidity b =
let flags = x.typar_flags
x.typar_flags <- TyparFlags(flags.Kind, b, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, flags.ComparisonConditionalOn)
x.typar_flags <- TyparFlags(flags.Kind, b, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex)

/// Sets whether a type variable is compiler generated
member x.SetCompilerGenerated b =
let flags = x.typar_flags
x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, b, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, flags.ComparisonConditionalOn)
x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, b, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex)

/// Sets whether a type variable has a static requirement
member x.SetStaticReq b =
Expand All @@ -2439,17 +2457,17 @@ type Typar =
/// Sets whether a type variable is required at runtime
member x.SetDynamicReq b =
let flags = x.typar_flags
x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, b, flags.EqualityConditionalOn, flags.ComparisonConditionalOn)
x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, b, flags.EqualityConditionalOn, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex)

/// Sets whether the equality constraint of a type definition depends on this type variable
member x.SetEqualityDependsOn b =
let flags = x.typar_flags
x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, b, flags.ComparisonConditionalOn)
x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, b, flags.ComparisonConditionalOn, flags.IsSupportsNullFlex)

/// Sets whether the comparison constraint of a type definition depends on this type variable
member x.SetComparisonDependsOn b =
let flags = x.typar_flags
x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, b)
x.typar_flags <- TyparFlags(flags.Kind, flags.Rigidity, flags.IsFromError, flags.IsCompilerGenerated, flags.StaticReq, flags.DynamicReq, flags.EqualityConditionalOn, b, flags.IsSupportsNullFlex)

[<DebuggerBrowsable(DebuggerBrowsableState.Never)>]
member x.DebugText = x.ToString()
Expand Down Expand Up @@ -6118,7 +6136,7 @@ type Construct() =
Typar.New
{ typar_id = id
typar_stamp = newStamp()
typar_flags= TyparFlags(kind, rigid, isFromError, isCompGen, staticReq, dynamicReq, eqDep, compDep)
typar_flags= TyparFlags(kind, rigid, isFromError, isCompGen, staticReq, dynamicReq, eqDep, compDep, false)
typar_solution = None
typar_astype = Unchecked.defaultof<_>
typar_opt_data =
Expand Down
Loading

0 comments on commit e2e698d

Please sign in to comment.