Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
77 changes: 77 additions & 0 deletions EXECUTIVE_SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# FSharpPlus curryN Regression Fix - Executive Summary

## The Bug

FSharpPlus `curryN` pattern worked in F# SDK 8.0 but failed in SDK 9.0+ with error FS0030 (value restriction):

```fsharp
let _x1 = curryN f1 100 // SDK 8: OK | SDK 9+: FS0030 error
```

## Root Cause

In `CheckDeclarations.fs`, the `ApplyDefaults` function processes unsolved type variables at the end of type checking. The existing code only solved typars with `StaticReq <> None`:

```fsharp
if (tp.StaticReq <> TyparStaticReq.None) then
ChooseTyparSolutionAndSolve cenv.css denvAtEnd tp
```

**The problem**: Some SRTP (Statically Resolved Type Parameter) typars have `MayResolveMember` constraints but `StaticReq=None`. These typars were being skipped, leaving them unsolved. When `CheckValueRestriction` ran next, it found unsolved typars and reported FS0030.

## Why This Is a Bug

The `ApplyDefaults` code checks `StaticReq <> None` to identify SRTP typars that need solving. However, a typar may participate in an SRTP constraint (having a `MayResolveMember` constraint) without having `StaticReq` set. This can happen when:

1. The typar is the **result type** of an SRTP method call, not the head type
2. The typar is constrained through SRTP constraints but isn't directly marked with `^`

**Key insight from instrumentation:**
```
- ? (StaticReq=None, Constraints=[MayResolveMember, CoercesTo]) ← HAS SRTP CONSTRAINT!
[ApplyDefaults] After processing: 17 still unsolved ← SRTP typar SKIPPED because StaticReq=None
```

The condition `tp.StaticReq <> None` was too narrow - it missed typars that have SRTP constraints but no explicit static requirement.

## Regression Analysis - Git Blame Evidence

**Root Cause: PR #15181 (commit `b73be1584`) - Nullness Checking Feature**

The regression was introduced by `FreshenTypar` added in PR #15181:

```fsharp
// src/Compiler/Checking/NameResolution.fs:1600-1604
let FreshenTypar (g: TcGlobals) rigid (tp: Typar) =
let clearStaticReq = g.langVersion.SupportsFeature LanguageFeature.InterfacesWithAbstractStaticMembers
let staticReq = if clearStaticReq then TyparStaticReq.None else tp.StaticReq // ← BUG!
...
```

**The Mechanism:**

1. **SDK 8**: `FreshenTypar` did not exist. When typars were freshened, `StaticReq` was preserved from the original typar.

2. **SDK 9+**: When `InterfacesWithAbstractStaticMembers` is enabled (always on), `FreshenTypar` **clears `StaticReq` to `None`** unconditionally.

3. **Effect**: SRTP typars still have `MayResolveMember` constraints, but lose their `StaticReq` marker.

4. **Consequence**: `ApplyDefaults` checks `if tp.StaticReq <> None` → returns false → typar never solved → FS0030 error.

**The fix** adds an alternative check for `MayResolveMember` constraints directly, making `ApplyDefaults` robust against this `StaticReq` clearing.

## The Fix

Added a check for `MayResolveMember` constraints in addition to `StaticReq`:

```fsharp
let hasSRTPConstraint = tp.Constraints |> List.exists (function TyparConstraint.MayResolveMember _ -> true | _ -> false)
if (tp.StaticReq <> TyparStaticReq.None) || hasSRTPConstraint then
ChooseTyparSolutionAndSolve cenv.css denvAtEnd tp
```

## Verification

- ✅ New curryN-style regression test passes
- ✅ FSharpPlus `curryN` pattern compiles without type annotations
- ✅ No regressions introduced in SRTP-related tests
14 changes: 14 additions & 0 deletions azure-pipelines-PR.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# CI and PR triggers

Check failure on line 1 in azure-pipelines-PR.yml

View check run for this annotation

Azure Pipelines / fsharp-ci (Build FsharpPlus_NET10_FullTestSuite Regression Test)

azure-pipelines-PR.yml#L1

azure-pipelines-PR.yml(,): error : Regression test failed: FsharpPlus_NET10_FullTestSuite
trigger:
branches:
include:
Expand Down Expand Up @@ -914,3 +914,17 @@
buildScript: build.sh
displayName: FSharpPlus_Linux
useVmImage: $(UbuntuMachineQueueName)
- repo: fsprojects/FSharpPlus
commit: 2648efe
buildScript: build.cmd
displayName: FsharpPlus_NET10
# remove this before merging
- repo: fsprojects/FSharpPlus
commit: 2648efe
buildScript: dotnet msbuild build.proj -target:Build;Pack;Test;AllDocs
displayName: FsharpPlus_NET10_FullTestSuite
- repo: fsprojects/FSharpPlus
commit: 2648efe
buildScript: build.sh
displayName: FsharpPlus_Net10_Linux
useVmImage: $(UbuntuMachineQueueName)
7 changes: 5 additions & 2 deletions src/Compiler/Checking/CheckDeclarations.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5672,10 +5672,13 @@ let ApplyDefaults (cenv: cenv) g denvAtEnd m moduleContents extraAttribs =
// the defaults will be propagated to the new type variable.
ApplyTyparDefaultAtPriority denvAtEnd cenv.css priority tp)

// OK, now apply defaults for any unsolved TyparStaticReq.HeadType
// OK, now apply defaults for any unsolved TyparStaticReq.HeadType or typars with SRTP (MayResolveMember) constraints
// Note: We also check for MayResolveMember constraints because some SRTP typars may not have StaticReq set
// (this can happen when the typar is involved in an SRTP constraint but isn't the "head type" itself)
unsolved |> List.iter (fun tp ->
if not tp.IsSolved then
if (tp.StaticReq <> TyparStaticReq.None) then
let hasSRTPConstraint = tp.Constraints |> List.exists (function TyparConstraint.MayResolveMember _ -> true | _ -> false)
if (tp.StaticReq <> TyparStaticReq.None) || hasSRTPConstraint then
ChooseTyparSolutionAndSolve cenv.css denvAtEnd tp)
with RecoverableException exn ->
errorRecovery exn m
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1767,3 +1767,39 @@ printfn "Success: %d" result
|> compileAndRun
|> shouldSucceed

// Regression test for GitHub issue #18344 and FSharpPlus curryN pattern
// This tests that SRTP typars with MayResolveMember constraints are properly solved
// even when StaticReq is None
[<Fact>]
let ``SRTP curryN-style pattern should compile without value restriction error`` () =
FSharp """
module CurryNTest

open System

// Minimal reproduction of the FSharpPlus curryN pattern
type Curry =
static member inline Invoke f =
let inline call_2 (a: ^a, b: ^b) = ((^a or ^b) : (static member Curry: _*_ -> _) b, a)
call_2 (Unchecked.defaultof<Curry>, Unchecked.defaultof<'t>) (f: 't -> 'r) : 'args

static member Curry (_: Tuple<'t1> , _: Curry) = fun f t1 -> f (Tuple<_> t1)
static member Curry ((_, _) , _: Curry) = fun f t1 t2 -> f (t1, t2)
static member Curry ((_, _, _) , _: Curry) = fun f t1 t2 t3 -> f (t1, t2, t3)

let inline curryN f = Curry.Invoke f

// Test functions
let f1 (x: Tuple<_>) = [x.Item1]
let f2 (x, y) = [x + y]
let f3 (x, y, z) = [x + y + z]

// These should compile without value restriction error (regression test for #18344)
let _x1 = curryN f1 100
let _x2 = curryN f2 1 2
let _x3 = curryN f3 1 2 3
"""
|> asLibrary
|> compile
|> shouldSucceed

Loading