Skip to content

Commit d845820

Browse files
committed
Add much more documentation
1 parent 8642e8c commit d845820

File tree

4 files changed

+112
-52
lines changed

4 files changed

+112
-52
lines changed

src/Compiler/Symbols/Exprs.fs

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -509,30 +509,46 @@ module FSharpExprConvert =
509509
and GetWitnessArgs cenv (env: ExprTranslationEnv) (vref: ValRef) m tps tyargs : FSharpExpr list =
510510
let g = cenv.g
511511
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+
512526
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 #)
517531
// in FSharp.Core.
518532
| ErrorResult _ when vref.LogicalName = "op_LeftShift" && List.isSingleton tyargs -> []
519-
// Auto-generated structural comparison/equality code for generic types may call
520-
// comparison/equality intrinsics with type parameters that have ComparisonConditionalOn
521-
// or EqualityConditionalOn but not actual constraints. In this case, there's no witness
522-
// available at compile time - the constraint is satisfied conditionally at runtime.
523-
// For nested types like Outer<'a> with field Inner<'a>, the tyargs contains Inner<'a>,
524-
// so we must recursively check type arguments of applied types.
525-
// See https://github.com/ionide/FSharp.Analyzers.SDK/issues/276
526-
| ErrorResult _ when
527-
let rec hasConditionalTypar ty =
528-
match tryDestTyparTy g ty with
529-
| ValueSome tp -> tp.ComparisonConditionalOn || tp.EqualityConditionalOn
530-
| ValueNone ->
531-
// For applied types like Inner<'a>, recursively check type arguments
532-
match ty with
533-
| AppTy g (_, tinst) -> tinst |> List.exists hasConditionalTypar
534-
| _ -> false
535-
tyargs |> List.exists hasConditionalTypar -> []
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 -> []
536552
| res -> CommitOperationResult res
537553
let env = { env with suppressWitnesses = true }
538554
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: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3520,34 +3520,40 @@ module internal ProjectForWitnessConditionalComparison =
35203520
/// This triggers the bug because it forces conversion of auto-generated comparison code
35213521
let walkAllExpressions (source : string) =
35223522
let fileName1 = System.IO.Path.ChangeExtension(getTemporaryFileName (), ".fs")
3523-
FileSystem.OpenFileForWriteShim(fileName1).Write(source)
3524-
let options = createProjectOptions [source] []
3525-
let exprChecker = FSharpChecker.Create(keepAssemblyContents=true, useTransparentCompiler=CompilerAssertHelpers.UseTransparentCompiler)
3526-
let wholeProjectResults = exprChecker.ParseAndCheckProject(options) |> Async.RunImmediate
3527-
3528-
if wholeProjectResults.Diagnostics.Length > 0 then
3529-
for diag in wholeProjectResults.Diagnostics do
3530-
printfn "Diagnostic: %s" diag.Message
3531-
3532-
for implFile in wholeProjectResults.AssemblyContents.ImplementationFiles do
3533-
// Walk all declarations and their expressions, including ImmediateSubExpressions
3534-
let rec walkExpr (e: FSharpExpr) =
3535-
// Access ImmediateSubExpressions - this is what triggers the bug
3536-
for subExpr in e.ImmediateSubExpressions do
3537-
walkExpr subExpr
3538-
3539-
let rec walkDecl d =
3540-
match d with
3541-
| FSharpImplementationFileDeclaration.Entity (_, subDecls) ->
3542-
for subDecl in subDecls do
3543-
walkDecl subDecl
3544-
| FSharpImplementationFileDeclaration.MemberOrFunctionOrValue (_, _, e) ->
3545-
walkExpr e
3546-
| FSharpImplementationFileDeclaration.InitAction e ->
3547-
walkExpr e
3548-
3549-
for decl in implFile.Declarations do
3550-
walkDecl decl
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+
| _ -> ()
35513557

35523558
[<Fact>]
35533559
let ``ImmediateSubExpressions - generic DU with no constraints should not crash`` () =
@@ -3559,7 +3565,7 @@ module M
35593565
type Bar<'appEvent> =
35603566
| Wibble of 'appEvent
35613567
"""
3562-
// This should not throw - before the fix it crashes with ConstraintSolverMissingConstraint
3568+
// This should not throw. Before the fix, it crashed with ConstraintSolverMissingConstraint.
35633569
ProjectForWitnessConditionalComparison.walkAllExpressions source
35643570

35653571
[<Fact>]
@@ -3609,7 +3615,8 @@ and Inner<'b> =
36093615

36103616
[<Fact>]
36113617
let ``ImmediateSubExpressions - generic DU with explicit comparison constraint works`` () =
3612-
// When the type parameter HAS the comparison constraint, witness generation should work
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.
36133620
let source = """
36143621
module M
36153622

@@ -3620,7 +3627,7 @@ type WithConstraint<'a when 'a : comparison> =
36203627

36213628
[<Fact>]
36223629
let ``ImmediateSubExpressions - non-generic DU works`` () =
3623-
// Non-generic types should work fine (no generics = no witness issues)
3630+
// Non-generic types always worked fine (no generics = no witness issues). This test is here for completeness.
36243631
let source = """
36253632
module M
36263633

@@ -3632,7 +3639,8 @@ type SimpleUnion =
36323639

36333640
[<Fact>]
36343641
let ``ImmediateSubExpressions - generic DU with NoComparison attribute should not crash`` () =
3635-
// With NoComparison, no comparison code is generated, so no crash should occur
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.
36363644
let source = """
36373645
module M
36383646

0 commit comments

Comments
 (0)