Skip to content

Commit 89d7886

Browse files
authored
Don't find witnesses for typars with conditional constraints (#19123)
1 parent 3686818 commit 89d7886

File tree

5 files changed

+386
-7
lines changed

5 files changed

+386
-7
lines changed

src/Compiler/Symbols/Exprs.fs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -508,14 +508,47 @@ module FSharpExprConvert =
508508

509509
and GetWitnessArgs cenv (env: ExprTranslationEnv) (vref: ValRef) m tps tyargs : FSharpExpr list =
510510
let g = cenv.g
511-
if g.langVersion.SupportsFeature(Features.LanguageFeature.WitnessPassing) && not env.suppressWitnesses then
512-
let witnessExprs =
511+
if g.langVersion.SupportsFeature(Features.LanguageFeature.WitnessPassing) && not env.suppressWitnesses then
512+
/// There are two *conditional* properties a typar can have: equality and comparison.
513+
/// A generic type having that constraint may be conditional on whether a specific type parameter to that generic has that
514+
/// constraint.
515+
/// This function returns `true` iff after unification, the type definition contains any conditional typars.
516+
///
517+
/// Note that these conditions are only marked on typars that actually appear in the code, *not* on phantom types.
518+
/// So `hasConditionalTypar` should tell us exactly when the type parameter is actually being used in the type's equality or
519+
/// comparison.
520+
let rec hasConditionalTypar ty =
521+
match stripTyEqns g ty with
522+
| TType_var (tp, _) -> tp.ComparisonConditionalOn || tp.EqualityConditionalOn
523+
| TType_app (_, tinst, _) -> tinst |> List.exists hasConditionalTypar
524+
| _ -> false
525+
526+
let witnessExprs =
513527
match ConstraintSolver.CodegenWitnessesForTyparInst cenv.tcValF g cenv.amap m tps tyargs with
514528
// There is a case where optimized code makes expressions that do a shift-left on the 'char'
515529
// type. There is no witness for this case. This is due to the code
516530
// let inline HashChar (x:char) = (# "or" (# "shl" x 16 : int #) x : int #)
517-
// in FSharp.Core.
531+
// in FSharp.Core.
518532
| ErrorResult _ when vref.LogicalName = "op_LeftShift" && List.isSingleton tyargs -> []
533+
// We don't need a witness either at compile time or runtime when there are conditional typars.
534+
// Attempting to call a comparison operation with the type causes a compile-time check that all the generic type args
535+
// support comparison (thanks to the ComparisonConditionalOn mechanism); the compile-time check doesn't need witnesses,
536+
// it's just pure constraint solving.
537+
// Nor do we need a witness for runtime logic: the compiler generates a `CompareTo` method (see
538+
// `MakeValsForCompareAugmentation`) which handles the comparison by dynamically type-testing, not going through a witness.
539+
//
540+
// So we don't need to generate a witness.
541+
//
542+
// In fact, we *can't* generate a witness, because the constraint on the type parameter is only conditional: a rigid type
543+
// parameter, defined without the `comparison` constraint, cannot have the constraint added to it later (that's what "rigid"
544+
// means). It would change the type signature of the type to add this constraint to the type parameter!
545+
//
546+
// This code path is only reached through the auto-generated comparison/equality code, which only calls single-constraint
547+
// intrinsics: there's exactly one constraint per type parameter in each of those two cases.
548+
// In theory, if a function had an autogenerated `'a : comparison and 'b : SomethingElse`, where the `SomethingElse` was
549+
// not comparison and failed for a different reason, we'd spuriously hide that failure here; but in fact the only code
550+
// paths which get here have no other constraints.
551+
| ErrorResult _ when List.exists hasConditionalTypar tyargs -> []
519552
| res -> CommitOperationResult res
520553
let env = { env with suppressWitnesses = true }
521554
witnessExprs |> List.map (fun arg ->

src/Compiler/TypedTree/TypedTreeBasics.fsi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ val tryShortcutSolvedUnitPar: canShortcut: bool -> r: Typar -> Measure
141141

142142
val stripUnitEqnsAux: canShortcut: bool -> unt: Measure -> Measure
143143

144+
/// Follows type variable solutions: when a type variable has been solved by unifying it with another type,
145+
/// replaces that type variable with its solution.
144146
val stripTyparEqnsAux: nullness0: Nullness -> canShortcut: bool -> ty: TType -> TType
145147

146148
val replaceNullnessOfTy: nullness: Nullness -> ty: TType -> TType

src/Compiler/TypedTree/TypedTreeOps.fsi

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -604,9 +604,41 @@ val reduceTyconRefMeasureableOrProvided: TcGlobals -> TyconRef -> TypeInst -> TT
604604

605605
val reduceTyconRefAbbrevMeasureable: TyconRef -> Measure
606606

607-
/// set bool to 'true' to allow shortcutting of type parameter equation chains during stripping
608-
val stripTyEqnsA: TcGlobals -> bool -> TType -> TType
609-
607+
/// <summary>
608+
/// Normalizes types.
609+
/// </summary>
610+
/// <remarks>
611+
/// Normalizes a type by:
612+
/// <list>
613+
/// <item>replacing type variables with their solutions found by unification</item>
614+
/// <item>expanding type abbreviations</item>
615+
/// </list>
616+
/// as well as a couple of special-case normalizations:
617+
/// <list>
618+
/// <item>identifying <c>int&lt;1&gt;</c> with <c>int</c> (for any measurable type)</item>
619+
/// <item>identifying <c>byref&lt;'T&gt;</c> with <c>byref&lt;'T, ByRefKinds.InOut&gt;</c></item>
620+
/// </list>
621+
/// </remarks>
622+
/// <param name="canShortcut">
623+
/// <c>true</c> to allow shortcutting of type parameter equation chains during stripping
624+
/// </param>
625+
val stripTyEqnsA: TcGlobals -> canShortcut: bool -> TType -> TType
626+
627+
/// <summary>
628+
/// Normalizes types.
629+
/// </summary>
630+
/// <remarks>
631+
/// Normalizes a type by:
632+
/// <list>
633+
/// <item>replacing type variables with their solutions found by unification</item>
634+
/// <item>expanding type abbreviations</item>
635+
/// </list>
636+
/// as well as a couple of special-case normalizations:
637+
/// <list>
638+
/// <item>identifying <c>int&lt;1&gt;</c> with <c>int</c> (for any measurable type)</item>
639+
/// <item>identifying <c>byref&lt;'T&gt;</c> with <c>byref&lt;'T, ByRefKinds.InOut&gt;</c></item>
640+
/// </list>
641+
/// </remarks>
610642
val stripTyEqns: TcGlobals -> TType -> TType
611643

612644
val stripTyEqnsAndMeasureEqns: TcGlobals -> TType -> TType
@@ -707,6 +739,8 @@ val tcrefOfAppTy: TcGlobals -> TType -> TyconRef
707739

708740
val tryTcrefOfAppTy: TcGlobals -> TType -> TyconRef voption
709741

742+
/// Returns ValueSome if this type is a type variable, even after abbreviations are expanded and
743+
/// variables have been solved through unification.
710744
val tryDestTyparTy: TcGlobals -> TType -> Typar voption
711745

712746
val tryDestFunTy: TcGlobals -> TType -> (TType * TType) voption

tests/FSharp.Compiler.Service.Tests/ExprTests.fs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3504,3 +3504,193 @@ let ``Test ProjectForWitnesses4 GetWitnessPassingInfo`` () =
35043504
printfn "actual:\n\n%A" actual
35053505
actual
35063506
|> shouldEqual expected
3507+
3508+
//---------------------------------------------------------------------------------------------------------
3509+
// Regression tests for ImmediateSubExpressions on generic types with conditional comparison/equality
3510+
// https://github.com/dotnet/fsharp/issues/19118
3511+
//
3512+
// The bug: FCS crashes when accessing ImmediateSubExpressions on auto-generated comparison code
3513+
// for generic DUs/records whose type parameters have ComparisonConditionalOn but not actual
3514+
// comparison constraints. This is because GetWitnessArgs tries to generate witnesses for the
3515+
// comparison constraint, but fails because the type parameter is rigid and can't have constraints added.
3516+
3517+
module internal ProjectForWitnessConditionalComparison =
3518+
3519+
/// Helper to walk ALL expressions in a file, including ImmediateSubExpressions
3520+
/// This triggers the bug because it forces conversion of auto-generated comparison code
3521+
let walkAllExpressions (source : string) =
3522+
let fileName1 = System.IO.Path.ChangeExtension(getTemporaryFileName (), ".fs")
3523+
try
3524+
FileSystem.OpenFileForWriteShim(fileName1).Write(source)
3525+
let options = createProjectOptions [source] []
3526+
let exprChecker = FSharpChecker.Create(keepAssemblyContents=true, useTransparentCompiler=CompilerAssertHelpers.UseTransparentCompiler)
3527+
let wholeProjectResults = exprChecker.ParseAndCheckProject(options) |> Async.RunImmediate
3528+
3529+
if wholeProjectResults.Diagnostics.Length > 0 then
3530+
for diag in wholeProjectResults.Diagnostics do
3531+
printfn "Diagnostic: %s" diag.Message
3532+
3533+
for implFile in wholeProjectResults.AssemblyContents.ImplementationFiles do
3534+
// Walk all declarations and their expressions, including ImmediateSubExpressions
3535+
let rec walkExpr (e: FSharpExpr) =
3536+
// Access ImmediateSubExpressions - this is what triggered #19118
3537+
for subExpr in e.ImmediateSubExpressions do
3538+
walkExpr subExpr
3539+
3540+
let rec walkDecl d =
3541+
match d with
3542+
| FSharpImplementationFileDeclaration.Entity (_, subDecls) ->
3543+
for subDecl in subDecls do
3544+
walkDecl subDecl
3545+
| FSharpImplementationFileDeclaration.MemberOrFunctionOrValue (_, _, e) ->
3546+
walkExpr e
3547+
| FSharpImplementationFileDeclaration.InitAction e ->
3548+
walkExpr e
3549+
3550+
for decl in implFile.Declarations do
3551+
walkDecl decl
3552+
finally
3553+
try
3554+
FileSystem.FileDeleteShim fileName1
3555+
with
3556+
| _ -> ()
3557+
3558+
[<Fact>]
3559+
let ``ImmediateSubExpressions - generic DU with no constraints should not crash`` () =
3560+
// This is the core bug repro - a generic DU where the type parameter has
3561+
// ComparisonConditionalOn but no actual comparison constraint
3562+
let source = """
3563+
module M
3564+
3565+
type Bar<'appEvent> =
3566+
| Wibble of 'appEvent
3567+
"""
3568+
// This should not throw. Before the fix, it crashed with ConstraintSolverMissingConstraint.
3569+
ProjectForWitnessConditionalComparison.walkAllExpressions source
3570+
3571+
[<Fact>]
3572+
let ``ImmediateSubExpressions - generic DU with multiple type parameters should not crash`` () =
3573+
let source = """
3574+
module M
3575+
3576+
type MultiParam<'a, 'b, 'c> =
3577+
| Case1 of 'a
3578+
| Case2 of 'b * 'c
3579+
| Case3 of 'a * 'b * 'c
3580+
"""
3581+
ProjectForWitnessConditionalComparison.walkAllExpressions source
3582+
3583+
[<Fact>]
3584+
let ``ImmediateSubExpressions - generic record with no constraints should not crash`` () =
3585+
let source = """
3586+
module M
3587+
3588+
type MyRecord<'t> = { Value: 't; Name: string }
3589+
"""
3590+
ProjectForWitnessConditionalComparison.walkAllExpressions source
3591+
3592+
[<Fact>]
3593+
let ``ImmediateSubExpressions - generic struct DU should not crash`` () =
3594+
let source = """
3595+
module M
3596+
3597+
[<Struct>]
3598+
type StructDU<'a> =
3599+
| StructCase of value: 'a
3600+
"""
3601+
ProjectForWitnessConditionalComparison.walkAllExpressions source
3602+
3603+
[<Fact>]
3604+
let ``ImmediateSubExpressions - nested generic types should not crash`` () =
3605+
let source = """
3606+
module M
3607+
3608+
type Outer<'a> =
3609+
| OuterCase of Inner<'a>
3610+
3611+
and Inner<'b> =
3612+
| InnerCase of 'b
3613+
"""
3614+
ProjectForWitnessConditionalComparison.walkAllExpressions source
3615+
3616+
[<Fact>]
3617+
let ``ImmediateSubExpressions - generic DU with explicit comparison constraint works`` () =
3618+
// When the type parameter has the comparison constraint, witness generation should work;
3619+
// no crash occurred even before the bug was fixed. This test is here for completeness.
3620+
let source = """
3621+
module M
3622+
3623+
type WithConstraint<'a when 'a : comparison> =
3624+
| Constrained of 'a
3625+
"""
3626+
ProjectForWitnessConditionalComparison.walkAllExpressions source
3627+
3628+
[<Fact>]
3629+
let ``ImmediateSubExpressions - non-generic DU works`` () =
3630+
// Non-generic types always worked fine (no generics = no witness issues). This test is here for completeness.
3631+
let source = """
3632+
module M
3633+
3634+
type SimpleUnion =
3635+
| Case1 of int
3636+
| Case2 of string
3637+
"""
3638+
ProjectForWitnessConditionalComparison.walkAllExpressions source
3639+
3640+
[<Fact>]
3641+
let ``ImmediateSubExpressions - generic DU with NoComparison attribute should not crash`` () =
3642+
// With NoComparison, no comparison code is generated, so no crash ever occurred even before the bug was fixed.
3643+
// This test is here for completeness.
3644+
let source = """
3645+
module M
3646+
3647+
[<NoComparison>]
3648+
type NoCompare<'a> =
3649+
| NoCompareCase of 'a
3650+
"""
3651+
ProjectForWitnessConditionalComparison.walkAllExpressions source
3652+
3653+
[<Fact>]
3654+
let ``ImmediateSubExpressions - generic DU with NoEquality attribute should not crash`` () =
3655+
let source = """
3656+
module M
3657+
3658+
[<NoEquality; NoComparison>]
3659+
type NoEq<'a> =
3660+
| NoEqCase of 'a
3661+
"""
3662+
ProjectForWitnessConditionalComparison.walkAllExpressions source
3663+
3664+
[<Fact>]
3665+
let ``ImmediateSubExpressions - generic DU used in function should not crash`` () =
3666+
// Test that using the generic DU in actual code still works
3667+
let source = """
3668+
module M
3669+
3670+
type Option2<'t> =
3671+
| Some2 of 't
3672+
| None2
3673+
3674+
let mapOption2 f opt =
3675+
match opt with
3676+
| Some2 x -> Some2 (f x)
3677+
| None2 -> None2
3678+
"""
3679+
ProjectForWitnessConditionalComparison.walkAllExpressions source
3680+
3681+
[<Fact>]
3682+
let ``ImmediateSubExpressions - complex generic type hierarchy should not crash`` () =
3683+
let source = """
3684+
module M
3685+
3686+
type Result<'ok, 'err> =
3687+
| Ok of 'ok
3688+
| Error of 'err
3689+
3690+
type Validated<'a> = Result<'a, string list>
3691+
3692+
let validate pred msg value : Validated<'a> =
3693+
if pred value then Ok value
3694+
else Error [msg]
3695+
"""
3696+
ProjectForWitnessConditionalComparison.walkAllExpressions source

0 commit comments

Comments
 (0)