diff --git a/.github/skills/pr-build-status/SKILL.md b/.github/skills/pr-build-status/SKILL.md index fa63062994b..50c5cc139c8 100644 --- a/.github/skills/pr-build-status/SKILL.md +++ b/.github/skills/pr-build-status/SKILL.md @@ -1,62 +1,177 @@ --- name: pr-build-status -description: "Retrieve Azure DevOps build information for GitHub Pull Requests, including build IDs, stage status, and failed jobs." +description: "Retrieve and analyze Azure DevOps build failures for GitHub PRs. Use when CI fails. CRITICAL: Collect ALL errors from ALL platforms FIRST, write hypotheses to file, then fix systematically." metadata: author: dotnet-maui - version: "1.0" + version: "2.0" compatibility: Requires GitHub CLI (gh) authenticated with access to dotnet/fsharp repository. --- # PR Build Status Skill -Retrieve Azure DevOps build information for GitHub Pull Requests. +Retrieve and systematically analyze Azure DevOps build failures for GitHub PRs. -## Tools Required +## CRITICAL: Collect-First Workflow -This skill uses `bash` together with `pwsh` (PowerShell 7+) to run the PowerShell scripts. No file editing or other tools are required. +**DO NOT push fixes until ALL errors are collected and reproduced locally.** -## When to Use +LLMs tend to focus on the first error found and ignore others. This causes: +- Multiple push/wait/fail cycles +- CI results being overwritten before full analysis +- Missing platform-specific failures (Linux vs Windows vs MacOS) -- User asks about CI/CD status for a PR -- User asks about failed checks or builds -- User asks "what's failing on PR #XXXXX" -- User wants to see test results +### Mandatory Workflow + +``` +1. COLLECT ALL → Get errors from ALL jobs across ALL platforms +2. DOCUMENT → Write CI_ERRORS.md with hypotheses per platform +3. REPRODUCE → Run each failing test LOCALLY (in isolation!) +4. FIX → Fix each issue, verify locally +5. PUSH → Only after ALL issues verified fixed +``` ## Scripts All scripts are in `.github/skills/pr-build-status/scripts/` ### 1. Get Build IDs for a PR -```bash +```powershell pwsh .github/skills/pr-build-status/scripts/Get-PrBuildIds.ps1 -PrNumber ``` -### 2. Get Build Status -```bash +### 2. Get Build Status (List ALL Failed Jobs) +```powershell +# Get overview of all stages and jobs pwsh .github/skills/pr-build-status/scripts/Get-BuildInfo.ps1 -BuildId -# For failed jobs only: + +# Get ONLY failed jobs (use this to see all failing platforms) pwsh .github/skills/pr-build-status/scripts/Get-BuildInfo.ps1 -BuildId -FailedOnly ``` ### 3. Get Build Errors and Test Failures -```bash -# Get all errors (build errors + test failures) +```powershell +# Get ALL errors (build errors + test failures) - USE THIS FIRST pwsh .github/skills/pr-build-status/scripts/Get-BuildErrors.ps1 -BuildId -# Get only build/compilation errors -pwsh .github/skills/pr-build-status/scripts/Get-BuildErrors.ps1 -BuildId -ErrorsOnly +# Filter to specific job (after getting overview) +pwsh .github/skills/pr-build-status/scripts/Get-BuildErrors.ps1 -BuildId -JobFilter "*Linux*" +pwsh .github/skills/pr-build-status/scripts/Get-BuildErrors.ps1 -BuildId -JobFilter "*Windows*" +pwsh .github/skills/pr-build-status/scripts/Get-BuildErrors.ps1 -BuildId -JobFilter "*MacOS*" +``` + +### 4. Direct API Access (for detailed logs) +```powershell +# Get timeline with all jobs +$uri = "https://dev.azure.com/dnceng-public/public/_apis/build/builds//timeline?api-version=7.1" +Invoke-RestMethod -Uri $uri | Select-Object -ExpandProperty records | Where-Object { $_.result -eq "failed" } -# Get only test failures -pwsh .github/skills/pr-build-status/scripts/Get-BuildErrors.ps1 -BuildId -TestsOnly +# Get specific log content +$logUri = "https://dev.azure.com/dnceng-public/cbb18261-c48f-4abb-8651-8cdcb5474649/_apis/build/builds//logs/" +Invoke-RestMethod -Uri $logUri | Select-String "Failed|Error|FAIL" ``` -## Workflow +## Step-by-Step Analysis Procedure + +### Step 1: Get Failed Build ID +```powershell +pwsh .github/skills/pr-build-status/scripts/Get-PrBuildIds.ps1 -PrNumber XXXXX +# Note the BuildId with FAILED state +``` -1. Get build IDs: `scripts/Get-PrBuildIds.ps1 -PrNumber XXXXX` -2. For each build, get status: `scripts/Get-BuildInfo.ps1 -BuildId YYYYY` -3. For failed builds, get error details: `scripts/Get-BuildErrors.ps1 -BuildId YYYYY` +### Step 2: List ALL Failed Jobs (Cross-Platform!) +```powershell +pwsh .github/skills/pr-build-status/scripts/Get-BuildInfo.ps1 -BuildId YYYYY -FailedOnly +``` +**IMPORTANT**: Note jobs from EACH platform: +- Linux jobs +- Windows jobs +- MacOS jobs +- Different test configurations (net10.0 vs net472, etc.) + +### Step 3: Get Errors Per Platform +```powershell +# Collect errors from EACH platform separately +pwsh .github/skills/pr-build-status/scripts/Get-BuildErrors.ps1 -BuildId YYYYY -JobFilter "*Linux*" +pwsh .github/skills/pr-build-status/scripts/Get-BuildErrors.ps1 -BuildId YYYYY -JobFilter "*Windows*" +pwsh .github/skills/pr-build-status/scripts/Get-BuildErrors.ps1 -BuildId YYYYY -JobFilter "*MacOS*" +``` + +### Step 4: Write CI_ERRORS.md +Create a file in session workspace with ALL findings: +```markdown +# CI Errors for PR #XXXXX - Build YYYYY + +## Failed Jobs Summary +| Platform | Job Name | Error Type | +|----------|----------|------------| +| Linux | ... | Test | +| Windows | ... | Test | + +## Hypothesis Per Platform + +### Linux/MacOS Failures +- Error: "The type 'int' is not defined" +- Hypothesis: Missing FSharp.Core reference in test setup +- Reproduction: `dotnet test ... -f net10.0` + +### Windows Failures +- Error: "Expected cache hits for generic patterns" +- Hypothesis: Flaky test assertion, passes with other tests +- Reproduction: `dotnet test ... --filter "FullyQualifiedName~rigid generic"` + +## Reproduction Commands +... + +## Fix Verification Checklist +- [ ] Linux error reproduced locally +- [ ] Windows error reproduced locally +- [ ] Fix verified for Linux +- [ ] Fix verified for Windows +- [ ] Tests run IN ISOLATION (not just with other tests) +``` + +### Step 5: Reproduce Locally BEFORE Fixing +```powershell +# Run failing tests IN ISOLATION (critical!) +dotnet test ... --filter "FullyQualifiedName~FailingTestName" -f net10.0 + +# Run multiple times to check for flakiness +for ($i = 1; $i -le 3; $i++) { dotnet test ... } +``` + +### Step 6: Fix and Verify +Only after ALL issues reproduced: +1. Fix each issue +2. Verify each fix locally (run test in isolation!) +3. Run full test suite +4. Check formatting +5. THEN push + +## Common Pitfalls + +### ❌ Mistake: Focus on First Error Only +``` +See Linux error → Fix → Push → Wait → See Windows error → Fix → Push → ... +``` + +### ✅ Correct: Collect All First +``` +See Linux error → See Windows error → See MacOS error → Document all → +Fix all → Verify all locally → Push once +``` + +### ❌ Mistake: Run Tests Together +``` +dotnet test ... --filter "OverloadCacheTests" # All 8 pass together +``` + +### ✅ Correct: Run Tests in Isolation +``` +dotnet test ... --filter "FullyQualifiedName~specific test name" # May fail alone! +``` ## Prerequisites - `gh` (GitHub CLI) - authenticated -- `pwsh` (PowerShell 7+) \ No newline at end of file +- `pwsh` (PowerShell 7+) +- Local build environment matching CI \ No newline at end of file diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md index 03f073b96b2..334398bfa7d 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.300.md @@ -27,6 +27,7 @@ ### Changed * Centralized product TFM (Target Framework Moniker) into MSBuild props file `eng/TargetFrameworks.props`. Changing the target framework now only requires editing one file, and it integrates with MSBuild's `--getProperty` for scripts. +* Overload resolution results are now cached, providing compile time improvements for code with repeated method calls. ([Issue #18807](https://github.com/dotnet/fsharp/issues/18807)) * Symbols: safer qualified name getting ([PR #19298](https://github.com/dotnet/fsharp/pull/19298)) diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index 62dd231d664..8ccb24cb207 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -1,5 +1,7 @@ ### Added +* Added `MethodOverloadsCache` language feature (preview) that caches overload resolution results for repeated method calls, significantly improving compilation performance. ([PR #19072](https://github.com/dotnet/fsharp/pull/19072)) + ### Fixed ### Changed \ No newline at end of file diff --git a/src/Compiler/Checking/ConstraintSolver.fs b/src/Compiler/Checking/ConstraintSolver.fs index 30eeb7b669a..c6df0d4dd0e 100644 --- a/src/Compiler/Checking/ConstraintSolver.fs +++ b/src/Compiler/Checking/ConstraintSolver.fs @@ -48,8 +48,8 @@ open Internal.Utilities.Library open Internal.Utilities.Library.Extras open Internal.Utilities.Rational -open FSharp.Compiler -open FSharp.Compiler.AbstractIL +open FSharp.Compiler +open FSharp.Compiler.AbstractIL open FSharp.Compiler.AccessibilityLogic open FSharp.Compiler.AttributeChecking open FSharp.Compiler.DiagnosticsLogger @@ -59,6 +59,7 @@ open FSharp.Compiler.InfoReader open FSharp.Compiler.Infos open FSharp.Compiler.MethodCalls open FSharp.Compiler.NameResolution +open FSharp.Compiler.OverloadResolutionCache open FSharp.Compiler.Syntax open FSharp.Compiler.Syntax.PrettyNaming open FSharp.Compiler.SyntaxTreeOps @@ -79,6 +80,7 @@ open FSharp.Compiler.TypeProviders // compilation environment, which currently corresponds to the scope // of the constraint resolution carried out by type checking. //------------------------------------------------------------------------- + let compgenId = mkSynId range0 unassignedTyparName @@ -250,11 +252,11 @@ exception UnresolvedConversionOperator of displayEnv: DisplayEnv * TType * TType type TcValF = ValRef -> ValUseFlag -> TType list -> range -> Expr * TType -type ConstraintSolverState = - { +type ConstraintSolverState = + { g: TcGlobals - amap: ImportMap + amap: ImportMap InfoReader: InfoReader @@ -284,7 +286,7 @@ type ConstraintSolverState = TcVal = tcVal PostInferenceChecksPreDefaults = ResizeArray() PostInferenceChecksFinal = ResizeArray() - WarnWhenUsingWithoutNullOnAWithNullTarget = None } + WarnWhenUsingWithoutNullOnAWithNullTarget = None } member this.PushPostInferenceCheck (preDefaults, check) = if preDefaults then @@ -357,16 +359,16 @@ let MakeConstraintSolverEnv contextInfo css m denv = } /// Check whether a type variable occurs in the r.h.s. of a type, e.g. to catch -/// infinite equations such as +/// infinite equations such as /// 'a = 'a list -let rec occursCheck g un ty = - match stripTyEqns g ty with +let rec occursCheck g un ty = + match stripTyEqns g ty with | TType_ucase(_, l) | TType_app (_, l, _) | TType_anon(_, l) | TType_tuple (_, l) -> List.exists (occursCheck g un) l | TType_fun (domainTy, rangeTy, _) -> occursCheck g un domainTy || occursCheck g un rangeTy - | TType_var (r, _) -> typarEq un r + | TType_var (r, _) -> typarEq un r | TType_forall (_, tau) -> occursCheck g un tau | _ -> false @@ -3471,9 +3473,120 @@ and AssumeMethodSolvesTrait (csenv: ConstraintSolverEnv) (cx: TraitConstraintInf | _ -> None +/// Core implementation of overload resolution (extracted for caching) +and ResolveOverloadingCore + (csenv: ConstraintSolverEnv) + methodName + ndeep + cx + (callerArgs: CallerArgs) + ad + (calledMethGroup: CalledMeth list) + (candidates: CalledMeth list) + permitOptArgs + (reqdRetTyOpt: OverallTy option) + isOpConversion + (retTyOpt: TType option) + (anyHasOutArgs: bool) + (cacheKeyOpt: OverloadResolutionCacheKey voption) + (cache: Caches.Cache) + : CalledMeth option * OperationResult * OptionalTrace = + + let infoReader = csenv.InfoReader + let m = csenv.m + + // Always take the return type into account for + // -- op_Explicit, op_Implicit + // -- candidate method sets that potentially use tupling of unfilled out args + let alwaysCheckReturn = + isOpConversion || anyHasOutArgs + + // Exact match rule. + // + // See what candidates we have based on current inferred type information + // and exact matches of argument types. + let exactMatchCandidates = + candidates |> FilterEachThenUndo (fun newTrace calledMeth -> + let csenv = { csenv with IsSpeculativeForMethodOverloading = true } + let cxsln = AssumeMethodSolvesTrait csenv cx m (WithTrace newTrace) calledMeth + CanMemberSigsMatchUpToCheck + csenv + permitOptArgs + alwaysCheckReturn + (TypesEquiv csenv ndeep (WithTrace newTrace) cxsln) // instantiations equivalent + (TypesMustSubsume csenv ndeep (WithTrace newTrace) cxsln m) // obj can subsume + (ReturnTypesMustSubsumeOrConvert csenv ad ndeep (WithTrace newTrace) cxsln cx.IsSome m) // return can subsume or convert + (ArgsEquivOrConvert csenv ad ndeep (WithTrace newTrace) cxsln cx.IsSome) // args exact + reqdRetTyOpt + calledMeth) + + match exactMatchCandidates with + | [(calledMeth, warns, _, _usesTDC)] -> + OverloadResolutionCache.storeCacheResult csenv.g cache cacheKeyOpt calledMethGroup callerArgs retTyOpt anyHasOutArgs (ValueSome calledMeth) + Some calledMeth, OkResult (warns, ()), NoTrace + + | _ -> + // Now determine the applicable methods. + // Subsumption on arguments is allowed. + let applicable = + candidates |> FilterEachThenUndo (fun newTrace candidate -> + let csenv = { csenv with IsSpeculativeForMethodOverloading = true } + let cxsln = AssumeMethodSolvesTrait csenv cx m (WithTrace newTrace) candidate + CanMemberSigsMatchUpToCheck + csenv + permitOptArgs + alwaysCheckReturn + (TypesEquiv csenv ndeep (WithTrace newTrace) cxsln) // instantiations equivalent + (TypesMustSubsume csenv ndeep (WithTrace newTrace) cxsln m) // obj can subsume + (ReturnTypesMustSubsumeOrConvert csenv ad ndeep (WithTrace newTrace) cxsln cx.IsSome m) // return can subsume or convert + (ArgsMustSubsumeOrConvertWithContextualReport csenv ad ndeep (WithTrace newTrace) cxsln cx.IsSome candidate) // args can subsume + reqdRetTyOpt + candidate) + + match applicable with + | [] -> + // OK, we failed. Collect up the errors from overload resolution and the possible overloads + OverloadResolutionCache.storeCacheResult csenv.g cache cacheKeyOpt calledMethGroup callerArgs retTyOpt anyHasOutArgs ValueNone + + let errors = + candidates + |> List.choose (fun calledMeth -> + match CollectThenUndo (fun newTrace -> + let csenv = { csenv with IsSpeculativeForMethodOverloading = true } + let cxsln = AssumeMethodSolvesTrait csenv cx m (WithTrace newTrace) calledMeth + CanMemberSigsMatchUpToCheck + csenv + permitOptArgs + alwaysCheckReturn + (TypesEquiv csenv ndeep (WithTrace newTrace) cxsln) + (TypesMustSubsume csenv ndeep (WithTrace newTrace) cxsln m) + (ReturnTypesMustSubsumeOrConvert csenv ad ndeep (WithTrace newTrace) cxsln cx.IsSome m) + (ArgsMustSubsumeOrConvertWithContextualReport csenv ad ndeep (WithTrace newTrace) cxsln cx.IsSome calledMeth) + reqdRetTyOpt + calledMeth) with + | OkResult _ -> None + | ErrorResult(_warnings, exn) -> + Some {methodSlot = calledMeth; infoReader = infoReader; error = exn }) + + let err = FailOverloading csenv calledMethGroup reqdRetTyOpt isOpConversion callerArgs (NoOverloadsFound (methodName, errors, cx)) m + + None, ErrorD err, NoTrace + + | [(calledMeth, warns, t, _usesTDC)] -> + OverloadResolutionCache.storeCacheResult csenv.g cache cacheKeyOpt calledMethGroup callerArgs retTyOpt anyHasOutArgs (ValueSome calledMeth) + Some calledMeth, OkResult (warns, ()), WithTrace t + + | applicableMeths -> + let result = GetMostApplicableOverload csenv ndeep candidates applicableMeths calledMethGroup reqdRetTyOpt isOpConversion callerArgs methodName cx m + match result with + | (Some calledMeth, _, _) -> + OverloadResolutionCache.storeCacheResult csenv.g cache cacheKeyOpt calledMethGroup callerArgs retTyOpt anyHasOutArgs (ValueSome calledMeth) + | _ -> () + result + // Resolve the overloading of a method // This is used after analyzing the types of arguments -and ResolveOverloading +and ResolveOverloading (csenv: ConstraintSolverEnv) trace // The undo trace, if any methodName // The name of the method being called, for error reporting @@ -3487,7 +3600,6 @@ and ResolveOverloading : CalledMeth option * OperationResult = let g = csenv.g - let infoReader = csenv.InfoReader let m = csenv.m let isOpConversion = @@ -3527,86 +3639,39 @@ and ResolveOverloading | _, _ -> - // Always take the return type into account for - // -- op_Explicit, op_Implicit - // -- candidate method sets that potentially use tupling of unfilled out args - let alwaysCheckReturn = - isOpConversion || - candidates |> List.exists (fun cmeth -> cmeth.HasOutArgs) - - // Exact match rule. - // - // See what candidates we have based on current inferred type information - // and exact matches of argument types. - let exactMatchCandidates = - candidates |> FilterEachThenUndo (fun newTrace calledMeth -> - let csenv = { csenv with IsSpeculativeForMethodOverloading = true } - let cxsln = AssumeMethodSolvesTrait csenv cx m (WithTrace newTrace) calledMeth - CanMemberSigsMatchUpToCheck - csenv - permitOptArgs - alwaysCheckReturn - (TypesEquiv csenv ndeep (WithTrace newTrace) cxsln) // instantiations equivalent - (TypesMustSubsume csenv ndeep (WithTrace newTrace) cxsln m) // obj can subsume - (ReturnTypesMustSubsumeOrConvert csenv ad ndeep (WithTrace newTrace) cxsln cx.IsSome m) // return can subsume or convert - (ArgsEquivOrConvert csenv ad ndeep (WithTrace newTrace) cxsln cx.IsSome) // args exact - reqdRetTyOpt - calledMeth) - - match exactMatchCandidates with - | [(calledMeth, warns, _, _usesTDC)] -> - Some calledMeth, OkResult (warns, ()), NoTrace - - | _ -> - // Now determine the applicable methods. - // Subsumption on arguments is allowed. - let applicable = - candidates |> FilterEachThenUndo (fun newTrace candidate -> - let csenv = { csenv with IsSpeculativeForMethodOverloading = true } - let cxsln = AssumeMethodSolvesTrait csenv cx m (WithTrace newTrace) candidate - CanMemberSigsMatchUpToCheck - csenv - permitOptArgs - alwaysCheckReturn - (TypesEquiv csenv ndeep (WithTrace newTrace) cxsln) // instantiations equivalent - (TypesMustSubsume csenv ndeep (WithTrace newTrace) cxsln m) // obj can subsume - (ReturnTypesMustSubsumeOrConvert csenv ad ndeep (WithTrace newTrace) cxsln cx.IsSome m) // return can subsume or convert - (ArgsMustSubsumeOrConvertWithContextualReport csenv ad ndeep (WithTrace newTrace) cxsln cx.IsSome candidate) // args can subsume - reqdRetTyOpt - candidate) - - match applicable with - | [] -> - // OK, we failed. Collect up the errors from overload resolution and the possible overloads - let errors = - candidates - |> List.choose (fun calledMeth -> - match CollectThenUndo (fun newTrace -> - let csenv = { csenv with IsSpeculativeForMethodOverloading = true } - let cxsln = AssumeMethodSolvesTrait csenv cx m (WithTrace newTrace) calledMeth - CanMemberSigsMatchUpToCheck - csenv - permitOptArgs - alwaysCheckReturn - (TypesEquiv csenv ndeep (WithTrace newTrace) cxsln) - (TypesMustSubsume csenv ndeep (WithTrace newTrace) cxsln m) - (ReturnTypesMustSubsumeOrConvert csenv ad ndeep (WithTrace newTrace) cxsln cx.IsSome m) - (ArgsMustSubsumeOrConvertWithContextualReport csenv ad ndeep (WithTrace newTrace) cxsln cx.IsSome calledMeth) - reqdRetTyOpt - calledMeth) with - | OkResult _ -> None - | ErrorResult(_warnings, exn) -> - Some {methodSlot = calledMeth; infoReader = infoReader; error = exn }) - - let err = FailOverloading csenv calledMethGroup reqdRetTyOpt isOpConversion callerArgs (NoOverloadsFound (methodName, errors, cx)) m - - None, ErrorD err, NoTrace - - | [(calledMeth, warns, t, _usesTDC)] -> - Some calledMeth, OkResult (warns, ()), WithTrace t - - | applicableMeths -> - GetMostApplicableOverload csenv ndeep candidates applicableMeths calledMethGroup reqdRetTyOpt isOpConversion callerArgs methodName cx m + let retTyOpt = reqdRetTyOpt |> Option.map (fun oty -> oty.Commit) + let anyHasOutArgs = calledMethGroup |> List.exists (fun cm -> cm.HasOutArgs) + + let cacheKeyOpt = + if g.langVersion.SupportsFeature LanguageFeature.MethodOverloadsCache && + not isOpConversion && cx.IsNone && candidates.Length > 1 then + OverloadResolutionCache.tryComputeOverloadCacheKey g calledMethGroup callerArgs retTyOpt anyHasOutArgs + else + ValueNone + + let cache = OverloadResolutionCache.getOverloadResolutionCache g + + let cachedHit = + match cacheKeyOpt with + | ValueSome cacheKey -> + let mutable cachedResult = Unchecked.defaultof + if cache.TryGetValue(cacheKey, &cachedResult) then + match cachedResult with + | CachedResolved idx when idx >= 0 && idx < calledMethGroup.Length -> + let calledMeth = calledMethGroup[idx] + if calledMeth.HasCorrectGenericArity then + Some (Some calledMeth, CompleteD, NoTrace) + else + None + | _ -> None + else + None + | ValueNone -> None + + match cachedHit with + | Some result -> result + | None -> + ResolveOverloadingCore csenv methodName ndeep cx callerArgs ad calledMethGroup candidates permitOptArgs reqdRetTyOpt isOpConversion retTyOpt anyHasOutArgs cacheKeyOpt cache // If we've got a candidate solution: make the final checks - no undo here! // Allow subsumption on arguments. Include the return type. @@ -3869,7 +3934,7 @@ and GetMostApplicableOverload csenv ndeep candidates applicableMeths calledMethG | [(calledMeth, warns, t, _)] -> Some calledMeth, OkResult (warns, ()), WithTrace t - | bestMethods -> + | bestMethods -> let methods = let getMethodSlotsAndErrors methodSlot errors = [ match errors with @@ -4178,7 +4243,7 @@ let CreateCodegenState tcVal g amap = InfoReader = InfoReader(g, amap) PostInferenceChecksPreDefaults = ResizeArray() PostInferenceChecksFinal = ResizeArray() - WarnWhenUsingWithoutNullOnAWithNullTarget = None} + WarnWhenUsingWithoutNullOnAWithNullTarget = None } /// Determine if a codegen witness for a trait will require witness args to be available, e.g. in generic code let CodegenWitnessExprForTraitConstraintWillRequireWitnessArgs tcVal g amap m (traitInfo:TraitConstraintInfo) = @@ -4274,7 +4339,7 @@ let IsApplicableMethApprox g amap m (minfo: MethInfo) availObjTy = InfoReader = InfoReader(g, amap) PostInferenceChecksPreDefaults = ResizeArray() PostInferenceChecksFinal = ResizeArray() - WarnWhenUsingWithoutNullOnAWithNullTarget = None} + WarnWhenUsingWithoutNullOnAWithNullTarget = None } let csenv = MakeConstraintSolverEnv ContextInfo.NoContext css m (DisplayEnv.Empty g) let minst = FreshenMethInfo m minfo match minfo.GetObjArgTypes(amap, m, minst) with @@ -4289,4 +4354,4 @@ let IsApplicableMethApprox g amap m (minfo: MethInfo) availObjTy = |> CommitOperationResult | _ -> true else - true \ No newline at end of file + true diff --git a/src/Compiler/Checking/ConstraintSolver.fsi b/src/Compiler/Checking/ConstraintSolver.fsi index 5c6ed47f17b..8d21270f901 100644 --- a/src/Compiler/Checking/ConstraintSolver.fsi +++ b/src/Compiler/Checking/ConstraintSolver.fsi @@ -9,6 +9,7 @@ open FSharp.Compiler.Import open FSharp.Compiler.Infos open FSharp.Compiler.InfoReader open FSharp.Compiler.MethodCalls +open FSharp.Compiler.OverloadResolutionCache open FSharp.Compiler.Syntax open FSharp.Compiler.TcGlobals open FSharp.Compiler.Text diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 933bcff225b..f0f36a3a0b7 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -10099,6 +10099,8 @@ and TcMethodApplication_UniqueOverloadInference let callerArgs = { Unnamed = unnamedCurriedCallerArgs; Named = namedCurriedCallerArgs } + let arityFilteredCandidates = candidateMethsAndProps + let makeOneCalledMeth (minfo, pinfoOpt, usesParamArrayConversion) = let minst = FreshenMethInfo mItem minfo let callerTyArgs = @@ -10108,7 +10110,7 @@ and TcMethodApplication_UniqueOverloadInference CalledMeth(cenv.infoReader, Some(env.NameEnv), isCheckingAttributeCall, FreshenMethInfo, mMethExpr, ad, minfo, minst, callerTyArgs, pinfoOpt, callerObjArgTys, callerArgs, usesParamArrayConversion, true, objTyOpt, staticTyOpt) let preArgumentTypeCheckingCalledMethGroup = - [ for minfo, pinfoOpt in candidateMethsAndProps do + [ for minfo, pinfoOpt in arityFilteredCandidates do let meth = makeOneCalledMeth (minfo, pinfoOpt, true) yield meth if meth.UsesParamArrayConversion then diff --git a/src/Compiler/Checking/MethodCalls.fs b/src/Compiler/Checking/MethodCalls.fs index 8f4f814325d..285dfa4fa8c 100644 --- a/src/Compiler/Checking/MethodCalls.fs +++ b/src/Compiler/Checking/MethodCalls.fs @@ -574,6 +574,51 @@ type CalledMeth<'T> | Some pinfo when pinfo.HasSetter && minfo.LogicalName.StartsWithOrdinal("set_") && (List.concat fullCurriedCalledArgs).Length >= 2 -> true | _ -> false + // Deferred until needed - property lookups are expensive and most candidates get filtered out + let computeAssignedNamedProps (unassignedItems: CallerNamedArg<'T> list) = + let returnedObjTy = methodRetTy + unassignedItems |> List.splitChoose (fun (CallerNamedArg(id, e) as arg) -> + let nm = id.idText + let pinfos = GetIntrinsicPropInfoSetsOfType infoReader (Some nm) ad AllowMultiIntfInstantiations.Yes IgnoreOverrides id.idRange returnedObjTy + let pinfos = pinfos |> ExcludeHiddenOfPropInfos g infoReader.amap m + match pinfos with + | [pinfo] when pinfo.HasSetter && not pinfo.IsStatic && not pinfo.IsIndexer -> + let pminfo = pinfo.SetterMethod + let pminst = freshenMethInfo m pminfo + let propStaticTyOpt = if isTyparTy g returnedObjTy then Some returnedObjTy else None + Choice1Of2(AssignedItemSetter(id, AssignedPropSetter(propStaticTyOpt, pinfo, pminfo, pminst), e)) + | _ -> + let epinfos = + match nameEnv with + | Some ne -> ExtensionPropInfosOfTypeInScope ResultCollectionSettings.AllResults infoReader ne (Some nm) LookupIsInstance.Ambivalent ad m returnedObjTy + | _ -> [] + + match epinfos with + | [pinfo] when pinfo.HasSetter && not pinfo.IsStatic && not pinfo.IsIndexer -> + let pminfo = pinfo.SetterMethod + let pminst = + match minfo with + | MethInfo.FSMeth(_, TType_app(_, types, _), _, _) -> types + | _ -> freshenMethInfo m pminfo + + let pminst = + match tyargsOpt with + | Some(TType_app(_, types, _)) -> types + | _ -> pminst + + let propStaticTyOpt = if isTyparTy g returnedObjTy then Some returnedObjTy else None + Choice1Of2(AssignedItemSetter(id, AssignedPropSetter(propStaticTyOpt, pinfo, pminfo, pminst), e)) + | _ -> + match infoReader.GetILFieldInfosOfType(Some(nm), ad, m, returnedObjTy) with + | finfo :: _ when not finfo.IsStatic -> + Choice1Of2(AssignedItemSetter(id, AssignedILFieldSetter(finfo), e)) + | _ -> + match infoReader.TryFindRecdOrClassFieldInfoOfType(nm, m, returnedObjTy) with + | ValueSome rfinfo when not rfinfo.IsStatic -> + Choice1Of2(AssignedItemSetter(id, AssignedRecdFieldSetter(rfinfo), e)) + | _ -> + Choice2Of2(arg)) + let argSetInfos = (callerArgs.CurriedCallerArgs, fullCurriedCalledArgs) ||> List.map2 (fun (unnamedCallerArgs, namedCallerArgs) fullCalledArgs -> // Find the arguments not given by name @@ -670,50 +715,6 @@ type CalledMeth<'T> else [] - let assignedNamedProps, unassignedNamedItems = - let returnedObjTy = methodRetTy - unassignedNamedItems |> List.splitChoose (fun (CallerNamedArg(id, e) as arg) -> - let nm = id.idText - let pinfos = GetIntrinsicPropInfoSetsOfType infoReader (Some nm) ad AllowMultiIntfInstantiations.Yes IgnoreOverrides id.idRange returnedObjTy - let pinfos = pinfos |> ExcludeHiddenOfPropInfos g infoReader.amap m - match pinfos with - | [pinfo] when pinfo.HasSetter && not pinfo.IsStatic && not pinfo.IsIndexer -> - let pminfo = pinfo.SetterMethod - let pminst = freshenMethInfo m pminfo - let propStaticTyOpt = if isTyparTy g returnedObjTy then Some returnedObjTy else None - Choice1Of2(AssignedItemSetter(id, AssignedPropSetter(propStaticTyOpt, pinfo, pminfo, pminst), e)) - | _ -> - let epinfos = - match nameEnv with - | Some ne -> ExtensionPropInfosOfTypeInScope ResultCollectionSettings.AllResults infoReader ne (Some nm) LookupIsInstance.Ambivalent ad m returnedObjTy - | _ -> [] - - match epinfos with - | [pinfo] when pinfo.HasSetter && not pinfo.IsStatic && not pinfo.IsIndexer -> - let pminfo = pinfo.SetterMethod - let pminst = - match minfo with - | MethInfo.FSMeth(_, TType_app(_, types, _), _, _) -> types - | _ -> freshenMethInfo m pminfo - - let pminst = - match tyargsOpt with - | Some(TType_app(_, types, _)) -> types - | _ -> pminst - - let propStaticTyOpt = if isTyparTy g returnedObjTy then Some returnedObjTy else None - Choice1Of2(AssignedItemSetter(id, AssignedPropSetter(propStaticTyOpt, pinfo, pminfo, pminst), e)) - | _ -> - match infoReader.GetILFieldInfosOfType(Some(nm), ad, m, returnedObjTy) with - | finfo :: _ when not finfo.IsStatic -> - Choice1Of2(AssignedItemSetter(id, AssignedILFieldSetter(finfo), e)) - | _ -> - match infoReader.TryFindRecdOrClassFieldInfoOfType(nm, m, returnedObjTy) with - | ValueSome rfinfo when not rfinfo.IsStatic -> - Choice1Of2(AssignedItemSetter(id, AssignedRecdFieldSetter(rfinfo), e)) - | _ -> - Choice2Of2(arg)) - let names = System.Collections.Generic.HashSet<_>() for CallerNamedArg(nm, _) in namedCallerArgs do if not (names.Add nm.idText) then @@ -721,14 +722,21 @@ type CalledMeth<'T> let argSet = { UnnamedCalledArgs=unnamedCalledArgs; UnnamedCallerArgs=unnamedCallerArgs; ParamArrayCalledArgOpt=paramArrayCalledArgOpt; ParamArrayCallerArgs=paramArrayCallerArgs; AssignedNamedArgs=assignedNamedArgs } - (argSet, assignedNamedProps, unassignedNamedItems, attributeAssignedNamedItems, unnamedCalledOptArgs, unnamedCalledOutArgs)) + (argSet, unassignedNamedItems, attributeAssignedNamedItems, unnamedCalledOptArgs, unnamedCalledOutArgs)) + + let argSets = argSetInfos |> List.map (fun (x, _, _, _, _) -> x) + let unassignedNamedItemsRaw = argSetInfos |> List.collect (fun (_, x, _, _, _) -> x) + let attributeAssignedNamedItems = argSetInfos |> List.collect (fun (_, _, x, _, _) -> x) + let unnamedCalledOptArgs = argSetInfos |> List.collect (fun (_, _, _, x, _) -> x) + let unnamedCalledOutArgs = argSetInfos |> List.collect (fun (_, _, _, _, x) -> x) - let argSets = argSetInfos |> List.map (fun (x, _, _, _, _, _) -> x) - let assignedNamedProps = argSetInfos |> List.collect (fun (_, x, _, _, _, _) -> x) - let unassignedNamedItems = argSetInfos |> List.collect (fun (_, _, x, _, _, _) -> x) - let attributeAssignedNamedItems = argSetInfos |> List.collect (fun (_, _, _, x, _, _) -> x) - let unnamedCalledOptArgs = argSetInfos |> List.collect (fun (_, _, _, _, x, _) -> x) - let unnamedCalledOutArgs = argSetInfos |> List.collect (fun (_, _, _, _, _, x) -> x) + let lazyAssignedNamedPropsAndUnassigned = lazy (computeAssignedNamedProps unassignedNamedItemsRaw) + let assignedNamedProps () = fst (lazyAssignedNamedPropsAndUnassigned.Value) + let unassignedNamedItems () = snd (lazyAssignedNamedPropsAndUnassigned.Value) + + let hasNoUnassignedNamedItems () = + if isNil unassignedNamedItemsRaw then true // Fast path: no items to look up + else isNil (unassignedNamedItems()) // Slow path: force lazy and check member x.infoReader = infoReader @@ -771,13 +779,13 @@ type CalledMeth<'T> else mkRefTupledTy g (retTy :: outArgTys) /// Named setters - member x.AssignedItemSetters = assignedNamedProps + member x.AssignedItemSetters = assignedNamedProps() /// The property related to the method we're attempting to call, if any member x.AssociatedPropertyInfo = pinfoOpt /// Unassigned args - member x.UnassignedNamedArgs = unassignedNamedItems + member x.UnassignedNamedArgs = unassignedNamedItems() /// Args assigned to specify values for attribute fields and properties (these are not necessarily "property sets") member x.AttributeAssignedNamedArgs = attributeAssignedNamedItems @@ -820,7 +828,7 @@ type CalledMeth<'T> member x.NumCallerTyArgs = x.CallerTyArgs.Length - member x.AssignsAllNamedArgs = isNil x.UnassignedNamedArgs + member x.AssignsAllNamedArgs = hasNoUnassignedNamedItems() member x.HasCorrectArity = (x.NumCalledTyArgs = x.NumCallerTyArgs) && diff --git a/src/Compiler/Checking/OverloadResolutionCache.fs b/src/Compiler/Checking/OverloadResolutionCache.fs new file mode 100644 index 00000000000..cbf6b1aca03 --- /dev/null +++ b/src/Compiler/Checking/OverloadResolutionCache.fs @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +/// Caching infrastructure for overload resolution results +module internal FSharp.Compiler.OverloadResolutionCache + +open Internal.Utilities.Library +open Internal.Utilities.TypeHashing +open Internal.Utilities.TypeHashing.StructuralUtilities + +open FSharp.Compiler +open FSharp.Compiler.Caches +open FSharp.Compiler.Features +open FSharp.Compiler.Infos +open FSharp.Compiler.MethodCalls +open FSharp.Compiler.TcGlobals +open FSharp.Compiler.Text +open FSharp.Compiler.TypedTree +open FSharp.Compiler.TypedTreeOps + +#if !NO_TYPEPROVIDERS +open FSharp.Compiler.TypeProviders +#endif + +/// Cache key for overload resolution: combines method group identity with caller argument types and return type +type OverloadResolutionCacheKey = + { + /// Hash combining all method identities in the method group + MethodGroupHash: int + /// Type structures for caller object arguments (the 'this' argument for instance/extension methods) + /// This is critical for extension methods where the 'this' type determines the overload + ObjArgTypeStructures: TypeStructure[] + /// Type structures for each caller argument (only used when all types are stable) + ArgTypeStructures: TypeStructure[] + /// Type structure for expected return type (if any), to differentiate calls with different expected types + ReturnTypeStructure: TypeStructure voption + /// Number of caller-provided type arguments (to distinguish calls with different type instantiations) + CallerTyArgCount: int + } + +/// Result of cached overload resolution +[] +type OverloadResolutionCacheResult = + /// Resolution succeeded - index of the resolved method in the original calledMethGroup list + | CachedResolved of methodIndex: int + +/// Gets a per-TcGlobals overload resolution cache. +/// Uses WeakMap to tie cache lifetime to TcGlobals (per-compilation isolation). +let getOverloadResolutionCache = + let factory (g: TcGlobals) = + let options = + match g.compilationMode with + | CompilationMode.OneOff -> + Caches.CacheOptions.getDefault HashIdentity.Structural + |> Caches.CacheOptions.withNoEviction + | _ -> + { Caches.CacheOptions.getDefault HashIdentity.Structural with + TotalCapacity = 4096 + HeadroomPercentage = 50 + } + + new Caches.Cache(options, "overloadResolutionCache") + + Internal.Utilities.Library.Extras.WeakMap.getOrCreate factory + +/// Check if a token array contains any Unsolved tokens (flexible unsolved typars) +let private hasUnsolvedTokens (tokens: TypeToken[]) = + tokens + |> Array.exists (function + | TypeToken.Unsolved _ -> true + | _ -> false) + +/// Try to get a type structure for caching in the overload resolution context. +/// +/// In this context, we accept Unstable structures that are unstable ONLY because +/// of solved typars (not unsolved flexible typars). This is safe because: +/// 1. The cache key is computed BEFORE FilterEachThenUndo runs +/// 2. Caller argument types were resolved before overload resolution +/// 3. Solved typars in those types won't be reverted by Trace.Undo +/// +/// We reject structures containing Unsolved tokens because unsolved flexible typars +/// could resolve to different types in different contexts, leading to wrong cache hits. +let tryGetTypeStructureForOverloadCache (g: TcGlobals) (ty: TType) : TypeStructure voption = + let ty = stripTyEqns g ty + + match tryGetTypeStructureOfStrippedType ty with + | ValueSome(Stable tokens) -> ValueSome(Stable tokens) + | ValueSome(Unstable tokens) -> + if hasUnsolvedTokens tokens then + ValueNone + else + ValueSome(Stable tokens) + | ValueSome PossiblyInfinite -> ValueNone + | ValueNone -> ValueNone + +let rec computeMethInfoHash (minfo: MethInfo) : int = + match minfo with + | FSMeth(_, _, vref, _) -> HashingPrimitives.combineHash (hash vref.Stamp) (hash vref.LogicalName) + | ILMeth(_, ilMethInfo, _) -> HashingPrimitives.combineHash (hash ilMethInfo.ILName) (hash ilMethInfo.DeclaringTyconRef.Stamp) + | DefaultStructCtor(_, _) -> hash "DefaultStructCtor" + | MethInfoWithModifiedReturnType(original, _) -> computeMethInfoHash original +#if !NO_TYPEPROVIDERS + | ProvidedMeth(_, mb, _, _) -> + let name, declTypeName = + mb.PUntaint((fun m -> m.Name, (nonNull m.DeclaringType).FullName |> string), Range.range0) + + HashingPrimitives.combineHash (hash name) (hash declTypeName) +#endif + +/// Try to compute a cache key for overload resolution. +/// Returns None if the resolution cannot be cached (e.g., unresolved type variables, named arguments). +let tryComputeOverloadCacheKey + (g: TcGlobals) + (calledMethGroup: CalledMeth<'T> list) + (callerArgs: CallerArgs<'T>) + (reqdRetTyOpt: TType option) + (anyHasOutArgs: bool) + : OverloadResolutionCacheKey voption = + + let hasNamedArgs = + callerArgs.Named |> List.exists (fun namedList -> not (List.isEmpty namedList)) + + if hasNamedArgs then + ValueNone + else + + let mutable methodGroupHash = 0 + + for cmeth in calledMethGroup do + let methHash = computeMethInfoHash cmeth.Method + methodGroupHash <- HashingPrimitives.combineHash methodGroupHash methHash + + let objArgStructures = ResizeArray() + let mutable allStable = true + + match calledMethGroup with + | cmeth :: _ -> + for objArgTy in cmeth.CallerObjArgTys do + match tryGetTypeStructureForOverloadCache g objArgTy with + | ValueSome ts -> objArgStructures.Add(ts) + | ValueNone -> allStable <- false + | [] -> () + + if not allStable then + ValueNone + else + + let argStructures = ResizeArray() + + for argList in callerArgs.Unnamed do + for callerArg in argList do + let argTy = callerArg.CallerArgumentType + + match tryGetTypeStructureForOverloadCache g argTy with + | ValueSome ts -> argStructures.Add(ts) + | ValueNone -> allStable <- false + + if not allStable then + ValueNone + else + let retTyStructure = + match reqdRetTyOpt with + | Some retTy -> + match tryGetTypeStructureForOverloadCache g retTy with + | ValueSome ts -> ValueSome ts + | ValueNone -> if anyHasOutArgs then ValueNone else ValueSome(Stable [||]) + | None -> ValueSome(Stable [||]) + + match retTyStructure with + | ValueNone -> ValueNone + | retStruct -> + let callerTyArgCount = + match calledMethGroup with + | cmeth :: _ -> cmeth.NumCallerTyArgs + | [] -> 0 + + ValueSome + { + MethodGroupHash = methodGroupHash + ObjArgTypeStructures = objArgStructures.ToArray() + ArgTypeStructures = argStructures.ToArray() + ReturnTypeStructure = retStruct + CallerTyArgCount = callerTyArgCount + } + +/// Compute cache result from resolution outcome +let computeCacheResult + (calledMethGroup: CalledMeth<'T> list) + (calledMethOpt: CalledMeth<'T> voption) + : OverloadResolutionCacheResult option = + match calledMethOpt with + | ValueSome calledMeth -> + calledMethGroup + |> List.tryFindIndex (fun cm -> obj.ReferenceEquals(cm, calledMeth)) + |> Option.map CachedResolved + | ValueNone -> None + +/// Stores an overload resolution result in the cache. +/// For successful resolutions, finds the method's index in calledMethGroup and stores CachedResolved. +/// Failed resolutions are not cached. +/// +/// Also computes and stores under an "after" key if types were solved during resolution. +/// This allows future calls with already-solved types to hit the cache directly. +let storeCacheResult + (g: TcGlobals) + (cache: Caches.Cache) + (cacheKeyOpt: OverloadResolutionCacheKey voption) + (calledMethGroup: CalledMeth<'T> list) + (callerArgs: CallerArgs<'T>) + (reqdRetTyOpt: TType option) + (anyHasOutArgs: bool) + (calledMethOpt: CalledMeth<'T> voption) + = + if not (g.langVersion.SupportsFeature LanguageFeature.MethodOverloadsCache) then + () + else + match cacheKeyOpt with + | ValueSome cacheKey -> + match computeCacheResult calledMethGroup calledMethOpt with + | Some res -> + cache.TryAdd(cacheKey, res) |> ignore + + match tryComputeOverloadCacheKey g calledMethGroup callerArgs reqdRetTyOpt anyHasOutArgs with + | ValueSome afterKey when afterKey <> cacheKey -> cache.TryAdd(afterKey, res) |> ignore + | _ -> () + | None -> () + | ValueNone -> + match tryComputeOverloadCacheKey g calledMethGroup callerArgs reqdRetTyOpt anyHasOutArgs with + | ValueSome afterKey -> + match computeCacheResult calledMethGroup calledMethOpt with + | Some res -> cache.TryAdd(afterKey, res) |> ignore + | None -> () + | ValueNone -> () diff --git a/src/Compiler/Checking/OverloadResolutionCache.fsi b/src/Compiler/Checking/OverloadResolutionCache.fsi new file mode 100644 index 00000000000..b4bc80a5bbc --- /dev/null +++ b/src/Compiler/Checking/OverloadResolutionCache.fsi @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +/// Caching infrastructure for overload resolution results +module internal FSharp.Compiler.OverloadResolutionCache + +open Internal.Utilities.TypeHashing.StructuralUtilities + +open FSharp.Compiler.Caches +open FSharp.Compiler.Infos +open FSharp.Compiler.MethodCalls +open FSharp.Compiler.TcGlobals +open FSharp.Compiler.TypedTree + +/// Cache key for overload resolution: combines method group identity with caller argument types and return type +type OverloadResolutionCacheKey = + { + /// Hash combining all method identities in the method group + MethodGroupHash: int + /// Type structures for caller object arguments (the 'this' argument for instance/extension methods) + /// This is critical for extension methods where the 'this' type determines the overload + ObjArgTypeStructures: TypeStructure[] + /// Type structures for each caller argument (only used when all types are stable) + ArgTypeStructures: TypeStructure[] + /// Type structure for expected return type (if any), to differentiate calls with different expected types + ReturnTypeStructure: TypeStructure voption + /// Number of caller-provided type arguments (to distinguish calls with different type instantiations) + CallerTyArgCount: int + } + +/// Result of cached overload resolution +[] +type OverloadResolutionCacheResult = + /// Resolution succeeded - index of the resolved method in the original calledMethGroup list + | CachedResolved of methodIndex: int + +/// Gets a per-TcGlobals overload resolution cache. +/// Uses WeakMap to tie cache lifetime to TcGlobals (per-compilation isolation). +val getOverloadResolutionCache: (TcGlobals -> Cache) + +/// Compute a hash for a method info for caching purposes +val computeMethInfoHash: MethInfo -> int + +/// Try to get a type structure for caching in the overload resolution context. +/// +/// In this context, we accept Unstable structures that are unstable ONLY because +/// of solved typars (not unsolved flexible typars). This is safe because: +/// 1. The cache key is computed BEFORE FilterEachThenUndo runs +/// 2. Caller argument types were resolved before overload resolution +/// 3. Solved typars in those types won't be reverted by Trace.Undo +/// +/// We reject structures containing Unsolved tokens because unsolved flexible typars +/// could resolve to different types in different contexts, leading to wrong cache hits. +val tryGetTypeStructureForOverloadCache: g: TcGlobals -> ty: TType -> TypeStructure voption + +/// Try to compute a cache key for overload resolution. +/// Returns None if the resolution cannot be cached (e.g., unresolved type variables, named arguments). +val tryComputeOverloadCacheKey: + g: TcGlobals -> + calledMethGroup: CalledMeth<'T> list -> + callerArgs: CallerArgs<'T> -> + reqdRetTyOpt: TType option -> + anyHasOutArgs: bool -> + OverloadResolutionCacheKey voption + +/// Compute cache result from resolution outcome +val computeCacheResult: + calledMethGroup: CalledMeth<'T> list -> + calledMethOpt: CalledMeth<'T> voption -> + OverloadResolutionCacheResult option + +/// Stores an overload resolution result in the cache. +/// For successful resolutions, finds the method's index in calledMethGroup and stores CachedResolved. +/// Failed resolutions are not cached. +/// +/// Also computes and stores under an "after" key if types were solved during resolution. +/// This allows future calls with already-solved types to hit the cache directly. +val storeCacheResult: + g: TcGlobals -> + cache: Cache -> + cacheKeyOpt: OverloadResolutionCacheKey voption -> + calledMethGroup: CalledMeth<'T> list -> + callerArgs: CallerArgs<'T> -> + reqdRetTyOpt: TType option -> + anyHasOutArgs: bool -> + calledMethOpt: CalledMeth<'T> voption -> + unit diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index f8f9749f99b..c1b6736f158 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1803,6 +1803,7 @@ featureAllowLetOrUseBangTypeAnnotationWithoutParens,"Allow let! and use! type an 3878,tcAttributeIsNotValidForUnionCaseWithFields,"This attribute is not valid for use on union cases with fields." 3879,xmlDocNotFirstOnLine,"XML documentation comments should be the first non-whitespace text on a line." featureReturnFromFinal,"Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder." +featureMethodOverloadsCache,"Support for caching method overload resolution results for improved compilation performance." featureImplicitDIMCoverage,"Implicit dispatch slot coverage for default interface member implementations" 3880,optsLangVersionOutOfSupport,"Language version '%s' is out of support. The last .NET SDK supporting it is available at https://dotnet.microsoft.com/en-us/download/dotnet/%s" 3881,optsUnrecognizedLanguageFeature,"Unrecognized language feature name: '%s'. Use a valid feature name such as 'NameOf' or 'StringInterpolation'." \ No newline at end of file diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj index 6bd4b798cda..cc8865d2578 100644 --- a/src/Compiler/FSharp.Compiler.Service.fsproj +++ b/src/Compiler/FSharp.Compiler.Service.fsproj @@ -368,6 +368,8 @@ + + diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs index 803af098f10..f683dd053e9 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fs +++ b/src/Compiler/Facilities/LanguageFeatures.fs @@ -104,6 +104,7 @@ type LanguageFeature = | ErrorOnInvalidDeclsInTypeDefinitions | AllowTypedLetUseAndBang | ReturnFromFinal + | MethodOverloadsCache | ImplicitDIMCoverage /// LanguageVersion management @@ -252,6 +253,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) // F# preview (still preview in 10.0) LanguageFeature.FromEndSlicing, previewVersion // Unfinished features --- needs work + LanguageFeature.MethodOverloadsCache, previewVersion // Performance optimization for overload resolution LanguageFeature.ImplicitDIMCoverage, languageVersion110 ] @@ -442,6 +444,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) | LanguageFeature.ErrorOnInvalidDeclsInTypeDefinitions -> FSComp.SR.featureErrorOnInvalidDeclsInTypeDefinitions () | LanguageFeature.AllowTypedLetUseAndBang -> FSComp.SR.featureAllowLetOrUseBangTypeAnnotationWithoutParens () | LanguageFeature.ReturnFromFinal -> FSComp.SR.featureReturnFromFinal () + | LanguageFeature.MethodOverloadsCache -> FSComp.SR.featureMethodOverloadsCache () | LanguageFeature.ImplicitDIMCoverage -> FSComp.SR.featureImplicitDIMCoverage () /// Get a version string associated with the given feature. diff --git a/src/Compiler/Facilities/LanguageFeatures.fsi b/src/Compiler/Facilities/LanguageFeatures.fsi index 4e0deca7c7e..6e9194bd02f 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fsi +++ b/src/Compiler/Facilities/LanguageFeatures.fsi @@ -95,6 +95,7 @@ type LanguageFeature = | ErrorOnInvalidDeclsInTypeDefinitions | AllowTypedLetUseAndBang | ReturnFromFinal + | MethodOverloadsCache | ImplicitDIMCoverage /// LanguageVersion management diff --git a/src/Compiler/Service/service.fs b/src/Compiler/Service/service.fs index de3635f516f..c0dd6e21d09 100644 --- a/src/Compiler/Service/service.fs +++ b/src/Compiler/Service/service.fs @@ -7,6 +7,7 @@ open Internal.Utilities.Collections open Internal.Utilities.Library open FSharp.Compiler open FSharp.Compiler.AbstractIL.ILBinaryReader +open FSharp.Compiler.Caches open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.CodeAnalysis.TransparentCompiler open FSharp.Compiler.CompilerConfig @@ -622,6 +623,9 @@ type FSharpChecker static member Instance = globalInstance.Force() + static member internal CreateOverloadCacheMetricsListener() = + new CacheMetrics.CacheMetricsListener("overloadResolutionCache") + member internal _.FrameworkImportsCache = backgroundCompiler.FrameworkImportsCache /// Tokenize a single line, returning token information and a tokenization state represented by an integer diff --git a/src/Compiler/Service/service.fsi b/src/Compiler/Service/service.fsi index 2120cab1eef..ae2b253c676 100644 --- a/src/Compiler/Service/service.fsi +++ b/src/Compiler/Service/service.fsi @@ -6,6 +6,7 @@ namespace FSharp.Compiler.CodeAnalysis open System open FSharp.Compiler.AbstractIL.ILBinaryReader +open FSharp.Compiler.Caches open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.CodeAnalysis.TransparentCompiler open FSharp.Compiler.Diagnostics @@ -505,6 +506,9 @@ type public FSharpChecker = [] static member Instance: FSharpChecker + /// Creates a listener for overload resolution cache metrics, aggregating across all compilations. + static member internal CreateOverloadCacheMetricsListener: unit -> CacheMetrics.CacheMetricsListener + member internal FrameworkImportsCache: FrameworkImportsCache member internal ReferenceResolver: LegacyReferenceResolver diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index 210d1a83dfe..fb024844e09 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -123,9 +123,8 @@ module CacheMetrics = Console.WriteLine(StatsToString()) } - // Currently the Cache emits telemetry for raw cache events: hits, misses, evictions etc. - // This type observes those counters and keeps a snapshot of readings. It is used in tests and can be used to print cache stats in debug mode. - type CacheMetricsListener(cacheTags: TagList) = + [] + type CacheMetricsListener(cacheTags: TagList, ?nameOnlyFilter: string) = let stats = Stats() let listener = new MeterListener() @@ -135,20 +134,37 @@ module CacheMetrics = listener.EnableMeasurementEvents instrument listener.SetMeasurementEventCallback(fun instrument v tags _ -> - let tagsMatch = tags[0] = cacheTags[0] && tags[1] = cacheTags[1] - - if tagsMatch then + let shouldIncrement = + match nameOnlyFilter with + | Some filterName -> + match tags[0].Value with + | :? string as name when name = filterName -> true + | _ -> false + | None -> tags[0] = cacheTags[0] && tags[1] = cacheTags[1] + + if shouldIncrement then stats.Incr instrument.Name v) listener.Start() + /// Creates a listener that aggregates metrics across all cache instances with the given name. + new(cacheName: string) = new CacheMetricsListener(TagList(), nameOnlyFilter = cacheName) + interface IDisposable with member _.Dispose() = listener.Dispose() + /// Gets the current totals for each metric type. member _.GetTotals() = stats.GetTotals() + /// Gets the current hit ratio (hits / (hits + misses)). member _.Ratio = stats.Ratio + /// Gets the total number of cache hits. + member _.Hits = stats.GetTotals().[hits.Name] + + /// Gets the total number of cache misses. + member _.Misses = stats.GetTotals().[misses.Name] + override _.ToString() = stats.ToString() [] diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index 809911f5116..3e1c98e9bb1 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -4,19 +4,27 @@ open System open System.Collections.Generic open System.Diagnostics.Metrics -module internal CacheMetrics = +module CacheMetrics = /// Global telemetry Meter for all caches. Exposed for testing purposes. /// Set FSHARP_OTEL_EXPORT environment variable to enable OpenTelemetry export to external collectors in tests. val Meter: Meter - val ListenToAll: unit -> IDisposable - val StatsToString: unit -> string - val CaptureStatsAndWriteToConsole: unit -> IDisposable + val internal ListenToAll: unit -> IDisposable + val internal StatsToString: unit -> string + val internal CaptureStatsAndWriteToConsole: unit -> IDisposable - /// A local listener that can be created for a specific Cache instance to get its metrics. For testing purposes only. - [] - type internal CacheMetricsListener = - member Ratio: float + /// A listener that captures cache metrics, matching by cache name or exact cache tags. + [] + type CacheMetricsListener = + /// Creates a listener that aggregates metrics across all cache instances with the given name. + new: cacheName: string -> CacheMetricsListener + /// Gets the current totals for each metric type. member GetTotals: unit -> Map + /// Gets the current hit ratio (hits / (hits + misses)). + member Ratio: float + /// Gets the total number of cache hits. + member Hits: int64 + /// Gets the total number of cache misses. + member Misses: int64 interface IDisposable [] diff --git a/src/Compiler/Utilities/TypeHashing.fs b/src/Compiler/Utilities/TypeHashing.fs index 98445648a43..7723e8b04df 100644 --- a/src/Compiler/Utilities/TypeHashing.fs +++ b/src/Compiler/Utilities/TypeHashing.fs @@ -45,6 +45,11 @@ module internal HashingPrimitives = let (@@) (h1: Hash) (h2: Hash) = combineHash h1 h2 + /// Maximum number of tokens emitted when generating type structure fingerprints. + /// Limits memory usage and prevents infinite type loops. + [] + let MaxTokenCount = 256 + [] module internal HashUtilities = @@ -410,7 +415,7 @@ module StructuralUtilities = type private GenerationContext() = member val TyparMap = System.Collections.Generic.Dictionary(4) - member val Tokens = ResizeArray(256) + member val Tokens = ResizeArray(MaxTokenCount) member val EmitNullness = false with get, set member val Stable = true with get, set @@ -440,7 +445,7 @@ module StructuralUtilities = let out = ctx.Tokens - if out.Count < 256 then + if out.Count < MaxTokenCount then match n.TryEvaluate() with | ValueSome k -> out.Add(TypeToken.Nullness(encodeNullness k)) | ValueNone -> out.Add(TypeToken.NullnessUnsolved) @@ -448,20 +453,20 @@ module StructuralUtilities = let inline private emitStamp (ctx: GenerationContext) (stamp: Stamp) = let out = ctx.Tokens - if out.Count < 256 then + if out.Count < MaxTokenCount then // Emit low 32 bits first let lo = int (stamp &&& 0xFFFFFFFFL) out.Add(TypeToken.Stamp lo) // If high 32 bits are non-zero, emit them as another token let hi64 = stamp >>> 32 - if hi64 <> 0L && out.Count < 256 then + if hi64 <> 0L && out.Count < MaxTokenCount then out.Add(TypeToken.Stamp(int hi64)) let rec private emitMeasure (ctx: GenerationContext) (m: Measure) = let out = ctx.Tokens - if out.Count >= 256 then + if out.Count >= MaxTokenCount then () else match m with @@ -475,21 +480,21 @@ module StructuralUtilities = | Measure.RationalPower(m1, r) -> emitMeasure ctx m1 - if out.Count < 256 then + if out.Count < MaxTokenCount then out.Add(TypeToken.MeasureNumerator(GetNumerator r)) out.Add(TypeToken.MeasureDenominator(GetDenominator r)) let rec private emitTType (ctx: GenerationContext) (ty: TType) = let out = ctx.Tokens - if out.Count >= 256 then + if out.Count >= MaxTokenCount then () else match ty with | TType_ucase(u, tinst) -> emitStamp ctx u.TyconRef.Stamp - if out.Count < 256 then + if out.Count < MaxTokenCount then out.Add(TypeToken.UCase(hashText u.CaseName)) for arg in tinst do @@ -545,7 +550,7 @@ module StructuralUtilities = match r.Solution with | Some ty -> emitTType ctx ty | None -> - if out.Count < 256 then + if out.Count < MaxTokenCount then if r.Rigidity = TyparRigidity.Rigid then out.Add(TypeToken.Rigid typarId) else @@ -560,7 +565,7 @@ module StructuralUtilities = let out = ctx.Tokens // If the sequence got too long, just drop it, we could be dealing with an infinite type. - if out.Count >= 256 then PossiblyInfinite + if out.Count >= MaxTokenCount then PossiblyInfinite elif not ctx.Stable then Unstable(out.ToArray()) else Stable(out.ToArray()) diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 7a896996f65..f8c72037e1e 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type Sdílení podkladových polí v rozlišeném sjednocení [<Struct>] za předpokladu, že mají stejný název a typ diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index fa7733a4144..f9271e5b32f 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type Teilen sie zugrunde liegende Felder in einen [<Struct>]-diskriminierten Union, solange sie denselben Namen und Typ aufweisen. diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index b6c504f0f45..1989ceb6078 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type Compartir campos subyacentes en una unión discriminada [<Struct>] siempre y cuando tengan el mismo nombre y tipo diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 0a4e78176e8..a707b886f6e 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type Partager les champs sous-jacents dans une union discriminée [<Struct>] tant qu’ils ont le même nom et le même type diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index 1d6d5b280bf..5bfc10f12a6 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type Condividi i campi sottostanti in un'unione discriminata di [<Struct>] purché abbiano lo stesso nome e tipo diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index 983f9e6d7e3..1e31efbbe42 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type 名前と型が同じである限り、[<Struct>] 判別可能な共用体で基になるフィールドを共有する diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index 4ad8d187cc1..4312ec97276 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type 이름과 형식이 같으면 [<Struct>] 구분된 공용 구조체에서 기본 필드 공유 diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 48efa8e8735..bc96b9e1716 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type Udostępnij pola źródłowe w unii rozłącznej [<Struct>], o ile mają taką samą nazwę i ten sam typ diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 7f23dd65fcf..72bc274c921 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type Compartilhar campos subjacentes em uma união discriminada [<Struct>], desde que tenham o mesmo nome e tipo diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 9e75ed6a1a1..5ef2a6eef3e 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type Совместное использование базовых полей в дискриминируемом объединении [<Struct>], если они имеют одинаковое имя и тип. diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index 94a8f55db4f..8db95b959f0 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type Aynı ada ve türe sahip oldukları sürece temel alınan alanları [<Struct>] ayırt edici birleşim biçiminde paylaşın diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index a0c142ce346..6eddc15e91d 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type 只要它们具有相同的名称和类型,即可在 [<Struct>] 中共享基础字段 diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index a7de59c8f11..61468e74752 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -612,6 +612,11 @@ Support for ReturnFromFinal/YieldFromFinal in computation expressions to enable tailcall optimization when available on the builder. + + Support for caching method overload resolution results for improved compilation performance. + Support for caching method overload resolution results for improved compilation performance. + + Share underlying fields in a [<Struct>] discriminated union as long as they have same name and type 只要 [<Struct>] 具有相同名稱和類型,就以強制聯集共用基礎欄位 diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/MemberDefinitions/OverloadingMembers/ArityFilteringTest.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/MemberDefinitions/OverloadingMembers/ArityFilteringTest.fs new file mode 100644 index 00000000000..fa8dfcb8d76 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/MemberDefinitions/OverloadingMembers/ArityFilteringTest.fs @@ -0,0 +1,70 @@ +// #Conformance #DeclarationElements #MemberDefinitions #Overloading + +open System + +type OverloadTest() = + member this.Method() = "no-args" + member this.Method(x: int) = "one-int" + member this.Method(x: int, y: int) = "two-ints" + member this.Method(x: int, y: int, z: int) = "three-ints" + + static member StaticMethod() = "static-no-args" + static member StaticMethod(x: int) = "static-one-int" + static member StaticMethod(x: int, y: int) = "static-two-ints" + + member this.OptMethod(x: int, ?y: int) = + match y with + | Some v -> sprintf "opt-%d-%d" x v + | None -> sprintf "opt-%d-none" x + + member this.ParamArrayMethod([] args: int[]) = + sprintf "params-%d" args.Length + +// Simulate Assert.Equal-like pattern with many overloads at different arities +type MockAssert = + static member Equal(expected: int, actual: int) = "int-int" + static member Equal(expected: string, actual: string) = "string-string" + static member Equal(expected: float, actual: float) = "float-float" + static member Equal(expected: obj, actual: obj) = "obj-obj" + + static member Equal(expected: float, actual: float, precision: int) = "float-float-precision" + static member Equal(expected: int, actual: int, comparer: System.Collections.Generic.IEqualityComparer) = "int-int-comparer" + static member Equal(expected: string, actual: string, comparer: System.Collections.Generic.IEqualityComparer) = "string-string-comparer" + + static member Single(x: int) = "single-int" + + static member Quad(a: int, b: int, c: int, d: int) = "quad" + + static member WithCallerInfo(x: int, [] ?callerName: string) = + match callerName with + | Some n -> sprintf "caller-%s" n + | None -> "caller-none" + +let test = OverloadTest() + +if test.Method() <> "no-args" then failwith "Failed: no-args" +if test.Method(1) <> "one-int" then failwith "Failed: one-int" +if test.Method(1, 2) <> "two-ints" then failwith "Failed: two-ints" +if test.Method(1, 2, 3) <> "three-ints" then failwith "Failed: three-ints" + +if OverloadTest.StaticMethod() <> "static-no-args" then failwith "Failed: static-no-args" +if OverloadTest.StaticMethod(1) <> "static-one-int" then failwith "Failed: static-one-int" +if OverloadTest.StaticMethod(1, 2) <> "static-two-ints" then failwith "Failed: static-two-ints" + +if test.OptMethod(42) <> "opt-42-none" then failwith "Failed: opt with none" +if test.OptMethod(42, 10) <> "opt-42-10" then failwith "Failed: opt with value" + +if test.ParamArrayMethod() <> "params-0" then failwith "Failed: params-0" +if test.ParamArrayMethod(1) <> "params-1" then failwith "Failed: params-1" +if test.ParamArrayMethod(1, 2) <> "params-2" then failwith "Failed: params-2" +if test.ParamArrayMethod(1, 2, 3, 4, 5) <> "params-5" then failwith "Failed: params-5" + +if MockAssert.Equal(1, 2) <> "int-int" then failwith "Failed: Equal int-int" +if MockAssert.Equal("a", "b") <> "string-string" then failwith "Failed: Equal string-string" +if MockAssert.Equal(1.0, 2.0) <> "float-float" then failwith "Failed: Equal float-float" + +if MockAssert.Equal(1.0, 2.0, 5) <> "float-float-precision" then failwith "Failed: Equal with precision" + +if MockAssert.WithCallerInfo(42).StartsWith("caller-") |> not then failwith "Failed: WithCallerInfo" + +printfn "All arity filtering tests passed!" diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/MemberDefinitions/OverloadingMembers/OverloadingMembers.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/MemberDefinitions/OverloadingMembers/OverloadingMembers.fs index bba418a78ee..c15706a3e3c 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/MemberDefinitions/OverloadingMembers/OverloadingMembers.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/MemberDefinitions/OverloadingMembers/OverloadingMembers.fs @@ -40,6 +40,20 @@ module MemberDefinitions_OverloadingMembers = |> verifyCompileAndRun |> shouldSucceed + // SOURCE=ArityFilteringTest.fs # ArityFilteringTest.fs + [] + let ``ArityFilteringTest_fs`` compilation = + compilation + |> verifyCompileAndRun + |> shouldSucceed + + // SOURCE=TypeCompatibilityFilterTest.fs # TypeCompatibilityFilterTest.fs + [] + let ``TypeCompatibilityFilterTest_fs`` compilation = + compilation + |> verifyCompileAndRun + |> shouldSucceed + // SOURCE=E_InferredTypeNotUnique01.fs SCFLAGS="--test:ErrorRanges" # E_InferredTypeNotUnique01.fs [] let ``E_InferredTypeNotUnique01_fs`` compilation = diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/MemberDefinitions/OverloadingMembers/TypeCompatibilityFilterTest.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/MemberDefinitions/OverloadingMembers/TypeCompatibilityFilterTest.fs new file mode 100644 index 00000000000..22df91b8e90 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/MemberDefinitions/OverloadingMembers/TypeCompatibilityFilterTest.fs @@ -0,0 +1,98 @@ +// #Conformance #DeclarationElements #MemberDefinitions #Overloading + +open System +open System.Collections.Generic + +type TypeCompatTest() = + static member Process(x: int) = "int" + static member Process(x: string) = "string" + static member Process(x: float) = "float" + static member Process(x: bool) = "bool" + static member Process(x: byte) = "byte" + + static member Generic<'T>(x: 'T) = sprintf "generic-%s" (typeof<'T>.Name) + + static member WithInterface(x: IComparable) = "IComparable" + static member WithInterface(x: IEnumerable) = "IEnumerable" + + static member WithObject(x: obj) = "obj" + + static member WithTuple(x: int * int) = "tuple2" + static member WithTuple(x: int * int * int) = "tuple3" + + static member WithArray(x: int[]) = "array1d" + static member WithArray(x: int[,]) = "array2d" + + static member Multi(x: int, y: int) = "int-int" + static member Multi(x: string, y: string) = "string-string" + static member Multi(x: int, y: string) = "int-string" + static member Multi(x: string, y: int) = "string-int" + + static member WithNullable(x: Nullable) = "nullable-int" + static member WithNullable(x: Nullable) = "nullable-float" + + static member NumericConversions(x: int64) = "int64" + static member NumericConversions(x: nativeint) = "nativeint" + +if TypeCompatTest.Process(42) <> "int" then failwith "Failed: Process int" +if TypeCompatTest.Process("hello") <> "string" then failwith "Failed: Process string" +if TypeCompatTest.Process(3.14) <> "float" then failwith "Failed: Process float" +if TypeCompatTest.Process(true) <> "bool" then failwith "Failed: Process bool" +if TypeCompatTest.Process(42uy) <> "byte" then failwith "Failed: Process byte" + +if TypeCompatTest.Generic(42) <> "generic-Int32" then failwith "Failed: Generic int" +if TypeCompatTest.Generic("test") <> "generic-String" then failwith "Failed: Generic string" + +if TypeCompatTest.WithInterface(42 :> IComparable) <> "IComparable" then failwith "Failed: WithInterface IComparable" +if TypeCompatTest.WithInterface([1; 2; 3] :> IEnumerable) <> "IEnumerable" then failwith "Failed: WithInterface IEnumerable" + +if TypeCompatTest.WithObject(42) <> "obj" then failwith "Failed: WithObject int" +if TypeCompatTest.WithObject("test") <> "obj" then failwith "Failed: WithObject string" + +if TypeCompatTest.WithTuple((1, 2)) <> "tuple2" then failwith "Failed: WithTuple 2" +if TypeCompatTest.WithTuple((1, 2, 3)) <> "tuple3" then failwith "Failed: WithTuple 3" + +if TypeCompatTest.WithArray([| 1; 2; 3 |]) <> "array1d" then failwith "Failed: WithArray 1d" +if TypeCompatTest.WithArray(Array2D.init 2 2 (fun i j -> i + j)) <> "array2d" then failwith "Failed: WithArray 2d" + +if TypeCompatTest.Multi(1, 2) <> "int-int" then failwith "Failed: Multi int-int" +if TypeCompatTest.Multi("a", "b") <> "string-string" then failwith "Failed: Multi string-string" +if TypeCompatTest.Multi(1, "b") <> "int-string" then failwith "Failed: Multi int-string" +if TypeCompatTest.Multi("a", 2) <> "string-int" then failwith "Failed: Multi string-int" + +if TypeCompatTest.WithNullable(Nullable(42)) <> "nullable-int" then failwith "Failed: WithNullable int" +if TypeCompatTest.WithNullable(Nullable(3.14)) <> "nullable-float" then failwith "Failed: WithNullable float" + +if TypeCompatTest.NumericConversions(42L) <> "int64" then failwith "Failed: NumericConversions int64" +if TypeCompatTest.NumericConversions(42n) <> "nativeint" then failwith "Failed: NumericConversions nativeint" + +type ParamArrayTypeTest() = + static member Process([] args: int[]) = sprintf "ints-%d" args.Length + static member Process([] args: string[]) = sprintf "strings-%d" args.Length + static member Process([] args: obj[]) = sprintf "objs-%d" args.Length + + static member Mixed(prefix: string, [] values: int[]) = sprintf "%s-%d" prefix values.Length + static member Mixed(prefix: string, [] values: string[]) = sprintf "%s-strs-%d" prefix values.Length + +if ParamArrayTypeTest.Process(1, 2, 3) <> "ints-3" then failwith "Failed: ParamArray int" +if ParamArrayTypeTest.Process("a", "b") <> "strings-2" then failwith "Failed: ParamArray string" + +if ParamArrayTypeTest.Mixed("test", 1, 2) <> "test-2" then failwith "Failed: Mixed ParamArray int" +if ParamArrayTypeTest.Mixed("test", "a", "b", "c") <> "test-strs-3" then failwith "Failed: Mixed ParamArray string" + +type OptionalArgsTypeTest() = + static member Complex(x: int, y: int, ?comparer: IComparable) = + match comparer with + | Some _ -> "with-comparer" + | None -> "no-comparer" + static member Complex(x: int, y: string, ?list: IEnumerable) = + match list with + | Some _ -> "with-list" + | None -> "no-list" + +if OptionalArgsTypeTest.Complex(42, 10) <> "no-comparer" then failwith "Failed: Optional Complex int-int no-opt" +if OptionalArgsTypeTest.Complex(42, 10, comparer = (42 :> IComparable)) <> "with-comparer" then failwith "Failed: Optional Complex with-comparer" +if OptionalArgsTypeTest.Complex(42, "test") <> "no-list" then failwith "Failed: Optional Complex int-string no-opt" +if OptionalArgsTypeTest.Complex(42, "test", list = [1; 2; 3]) <> "with-list" then failwith "Failed: Optional Complex with-list" + +printfn "All type compatibility filtering tests passed!" diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/Expressions/OverloadResolutionAdversarialTests.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/Expressions/OverloadResolutionAdversarialTests.fs new file mode 100644 index 00000000000..bbf13fe7784 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/Expressions/OverloadResolutionAdversarialTests.fs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +module CacheBustingTests + +open System +open System.Collections.Generic + +type GenericOverload = + static member Process<'T>(x: 'T, y: 'T) = typeof<'T>.Name + +type SubtypeOverload = + static member Accept(x: obj) = "obj" + static member Accept(x: string) = "string" + static member Accept(x: int) = "int" + static member Accept(x: float) = "float" + +type NestedGeneric = + static member Process<'T>(x: List<'T>) = "List<" + typeof<'T>.Name + ">" + static member Process<'T>(x: 'T[]) = "Array<" + typeof<'T>.Name + ">" + +type ByrefOverload = + static member TryGet(key: string, [] result: byref) = result <- 100; true + static member TryGet(key: string, [] result: byref) = result <- "value"; true + +type MixedParamArray = + static member Call(x: int, y: int) = "two-int" + static member Call([] args: int[]) = sprintf "params-int[%d]" args.Length + static member Call(x: string, [] rest: string[]) = sprintf "string+params[%d]" rest.Length + +let inline List (xs: 'a seq) = List<'a>(xs) + +let stressSequence () = + let results = ResizeArray() + for i in 1..50 do + results.Add(SubtypeOverload.Accept(i)) + results.Add(SubtypeOverload.Accept(sprintf "s%d" i)) + let intOk = results |> Seq.indexed |> Seq.filter (fun (i,_) -> i % 2 = 0) |> Seq.forall (fun (_,v) -> v = "int") + let strOk = results |> Seq.indexed |> Seq.filter (fun (i,_) -> i % 2 = 1) |> Seq.forall (fun (_,v) -> v = "string") + if intOk && strOk then "alternating-correct" else "CORRUPTED" + +[] +let main _ = + let results = [ + "Int32", GenericOverload.Process(1, 2) + "String", GenericOverload.Process("a", "b") + "Boolean", GenericOverload.Process(true, false) + "Double", GenericOverload.Process(1.0, 2.0) + "Int32", GenericOverload.Process(3, 4) + + "string", SubtypeOverload.Accept("hello") + "int", SubtypeOverload.Accept(42) + "float", SubtypeOverload.Accept(3.14) + "obj", SubtypeOverload.Accept(box [1;2;3]) + "string", SubtypeOverload.Accept("world") + "int", SubtypeOverload.Accept(99) + + "List", NestedGeneric.Process([1;2;3] |> List) + "List", NestedGeneric.Process(["a";"b"] |> List) + "Array", NestedGeneric.Process([|1;2;3|]) + "Array", NestedGeneric.Process([|"a";"b"|]) + + "100", (let mutable v = 0 in if ByrefOverload.TryGet("k", &v) then sprintf "%d" v else "failed") + "value", (let mutable v = "" in if ByrefOverload.TryGet("k", &v) then v else "failed") + "100", (let mutable v = 0 in if ByrefOverload.TryGet("x", &v) then sprintf "%d" v else "failed") + + "two-int", MixedParamArray.Call(1, 2) + "params-int[3]", MixedParamArray.Call(1, 2, 3) + "params-int[4]", MixedParamArray.Call(1, 2, 3, 4) + "string+params[3]", MixedParamArray.Call("x", "a", "b", "c") + + "alternating-correct", stressSequence() + ] + + let mutable failures = 0 + for (expected, actual) in results do + if actual = expected then printfn "PASS: %s" actual + else printfn "FAIL: got %s (expected %s)" actual expected; failures <- failures + 1 + + if failures = 0 then printfn "All %d adversarial tests passed!" results.Length + else printfn "%d of %d adversarial tests failed!" failures results.Length + failures diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/Expressions/OverloadResolutionBasicTests.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/Expressions/OverloadResolutionBasicTests.fs new file mode 100644 index 00000000000..6279f45a8ab --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/Expressions/OverloadResolutionBasicTests.fs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +module OverloadTests + +open System +open System.Collections.Generic + +type BasicOverload = + static member Pick(x: int) = "int" + static member Pick(x: string) = "string" + static member Pick(x: float) = "float" + static member Pick(x: bool) = "bool" + static member Pick<'T>(x: 'T) = "generic<" + typeof<'T>.Name + ">" + +type MultiArg = + static member Pick(a: int, b: int) = "int,int" + static member Pick(a: string, b: string) = "string,string" + static member Pick(a: int, b: string) = "int,string" + static member Pick(a: string, b: int) = "string,int" + static member Pick<'T>(a: 'T, b: 'T) = "generic<" + typeof<'T>.Name + ">,same" + +type ConstrainedCheck = + static member Pick<'T when 'T :> IComparable>(x: 'T) = "IComparable<" + typeof<'T>.Name + ">" + static member Pick(x: obj) = "obj" + +type OutArgOverload = + static member TryGet(key: string, [] value: byref) = value <- 42; true + static member TryGet(key: string, [] value: byref) = value <- "found"; true + +type ParamArrayOverload = + static member Pick([] args: int[]) = sprintf "int[%d]" args.Length + static member Pick([] args: string[]) = sprintf "string[%d]" args.Length + static member Pick(single: int) = "single-int" + static member Pick(single: string) = "single-string" + +type Animal() = class end +type Dog() = inherit Animal() +type Cat() = inherit Animal() + +type HierarchyOverload = + static member Accept(x: Animal) = "Animal" + static member Accept(x: Dog) = "Dog" + static member Accept(x: Cat) = "Cat" + static member Accept<'T when 'T :> Animal>(items: seq<'T>) = "seq<" + typeof<'T>.Name + ">" + +[] +module Extensions = + type String with + member this.ExtPick(x: int) = "String.ExtPick(int)" + member this.ExtPick(x: string) = "String.ExtPick(string)" + type Int32 with + member this.ExtPick(x: int) = "Int32.ExtPick(int)" + member this.ExtPick(x: string) = "Int32.ExtPick(string)" + +type OptionalOverload = + static member Pick(x: int, ?y: int) = match y with Some v -> sprintf "int,%d" v | None -> "int,none" + static member Pick(x: string, ?y: string) = match y with Some v -> sprintf "string,%s" v | None -> "string,none" + +type NamedArgOverload = + static member Pick(first: int, second: string) = "first:int,second:string" + static member Pick(first: string, second: int) = "first:string,second:int" + +type TDCOverload = + static member Pick(x: int64) = "int64" + static member Pick(x: int) = "int" + static member Pick(x: float) = "float" + +type TupleOverload = + static member Pick(x: int * string) = "tuple" + static member Pick(x: int, y: string) = "separate" + +let inline pickRigid<'T> (x: 'T) = BasicOverload.Pick(x) + +let cacheStress () = + let mutable all = true + for i in 1..100 do if BasicOverload.Pick(i) <> "int" then all <- false + if all then "all-int" else "MISMATCH" + +let cacheAlternating () = + sprintf "%s,%s,%s,%s,%s,%s" + (BasicOverload.Pick 1) (BasicOverload.Pick "a") + (BasicOverload.Pick 2) (BasicOverload.Pick "b") + (BasicOverload.Pick 3) (BasicOverload.Pick "c") + +[] +let main _ = + let results = [ + "int", BasicOverload.Pick(42) + "string", BasicOverload.Pick("hello") + "float", BasicOverload.Pick(3.14) + "bool", BasicOverload.Pick(true) + "generic", BasicOverload.Pick([1;2;3]) + + "int,int", MultiArg.Pick(1, 2) + "string,string", MultiArg.Pick("a", "b") + "int,string", MultiArg.Pick(1, "b") + "string,int", MultiArg.Pick("a", 2) + "generic,same", MultiArg.Pick(true, false) + + "int[0]", ParamArrayOverload.Pick([||] : int[]) + "int[3]", ParamArrayOverload.Pick(1, 2, 3) + "string[3]", ParamArrayOverload.Pick("a", "b", "c") + "single-int", ParamArrayOverload.Pick(42) + "single-string", ParamArrayOverload.Pick("single") + + "Animal", HierarchyOverload.Accept(Animal()) + "Dog", HierarchyOverload.Accept(Dog()) + "Cat", HierarchyOverload.Accept(Cat()) + "seq", HierarchyOverload.Accept([Dog(); Dog()]) + + "String.ExtPick(int)", "hello".ExtPick(42) + "String.ExtPick(string)", "hello".ExtPick("world") + "Int32.ExtPick(int)", (5).ExtPick(10) + "Int32.ExtPick(string)", (5).ExtPick("ten") + + "int,none", OptionalOverload.Pick(1) + "int,2", OptionalOverload.Pick(1, 2) + "string,none", OptionalOverload.Pick("a") + "string,b", OptionalOverload.Pick("a", "b") + + "all-int", cacheStress() + "int,string,int,string,int,string", cacheAlternating() + + "generic", pickRigid 42 + "generic", pickRigid "hello" + "generic", pickRigid true + + "tuple", TupleOverload.Pick((1, "a")) + "separate", TupleOverload.Pick(1, "a") + + "first:int,second:string", NamedArgOverload.Pick(1, "a") + "first:string,second:int", NamedArgOverload.Pick("a", 1) + "first:int,second:string", NamedArgOverload.Pick(first = 1, second = "b") + + "obj", ConstrainedCheck.Pick(42) + "obj", ConstrainedCheck.Pick("hi") + + "int", TDCOverload.Pick(42) + "int64", TDCOverload.Pick(42L) + "float", TDCOverload.Pick(3.14) + + "success:42", (let mutable v = 0 in if OutArgOverload.TryGet("k", &v) then sprintf "success:%d" v else "failed") + "success:found", (let mutable v = "" in if OutArgOverload.TryGet("k", &v) then sprintf "success:%s" v else "failed") + ] + + let mutable failures = 0 + for (expected, actual) in results do + if actual = expected then printfn "PASS: %s" actual + else printfn "FAIL: got %s (expected %s)" actual expected; failures <- failures + 1 + + if failures = 0 then printfn "All %d tests passed!" results.Length + else printfn "%d of %d tests failed!" failures results.Length + failures diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/Expressions/OverloadResolutionCacheE2ETests.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/Expressions/OverloadResolutionCacheE2ETests.fs new file mode 100644 index 00000000000..56da4f35f92 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/Expressions/OverloadResolutionCacheE2ETests.fs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module FSharp.Compiler.ComponentTests.OverloadResolutionCacheE2ETests + +open System.IO +open Xunit +open FSharp.Test.Compiler + +[] +let ``Overload resolution picks correct overloads and cache does not corrupt results`` () = + FSharp (loadSourceFromFile (Path.Combine(__SOURCE_DIRECTORY__, "OverloadResolutionBasicTests.fs"))) + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + |> verifyOutputContains [|"All 44 tests passed!"|] + +[] +let ``Adversarial tests: cache does not get poisoned by alternating types`` () = + FSharp (loadSourceFromFile (Path.Combine(__SOURCE_DIRECTORY__, "OverloadResolutionAdversarialTests.fs"))) + |> withLangVersionPreview + |> compileExeAndRun + |> shouldSucceed + |> verifyOutputContains [|"All 23 adversarial tests passed!"|] diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 68717660c48..68142347e1a 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -91,6 +91,7 @@ + diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index 78114ab29cc..feecc4eecd8 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -2011,6 +2011,18 @@ FSharp.Compiler.AbstractIL.ILBinaryReader: FSharp.Compiler.AbstractIL.ILBinaryRe FSharp.Compiler.AbstractIL.ILBinaryReader: FSharp.Compiler.AbstractIL.ILBinaryReader+MetadataOnlyFlag FSharp.Compiler.AbstractIL.ILBinaryReader: FSharp.Compiler.AbstractIL.ILBinaryReader+ReduceMemoryFlag FSharp.Compiler.AbstractIL.ILBinaryReader: FSharp.Compiler.AbstractIL.ILBinaryReader+Shim +FSharp.Compiler.Caches.CacheMetrics+CacheMetricsListener: Double Ratio +FSharp.Compiler.Caches.CacheMetrics+CacheMetricsListener: Double get_Ratio() +FSharp.Compiler.Caches.CacheMetrics+CacheMetricsListener: Int64 Hits +FSharp.Compiler.Caches.CacheMetrics+CacheMetricsListener: Int64 Misses +FSharp.Compiler.Caches.CacheMetrics+CacheMetricsListener: Int64 get_Hits() +FSharp.Compiler.Caches.CacheMetrics+CacheMetricsListener: Int64 get_Misses() +FSharp.Compiler.Caches.CacheMetrics+CacheMetricsListener: Microsoft.FSharp.Collections.FSharpMap`2[System.String,System.Int64] GetTotals() +FSharp.Compiler.Caches.CacheMetrics+CacheMetricsListener: System.String ToString() +FSharp.Compiler.Caches.CacheMetrics+CacheMetricsListener: Void .ctor(System.String) +FSharp.Compiler.Caches.CacheMetrics: FSharp.Compiler.Caches.CacheMetrics+CacheMetricsListener +FSharp.Compiler.Caches.CacheMetrics: System.Diagnostics.Metrics.Meter Meter +FSharp.Compiler.Caches.CacheMetrics: System.Diagnostics.Metrics.Meter get_Meter() FSharp.Compiler.Cancellable: Boolean HasCancellationToken FSharp.Compiler.Cancellable: Boolean get_HasCancellationToken() FSharp.Compiler.Cancellable: System.Threading.CancellationToken Token diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj index 3e5e2c58941..e44fd2594f8 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj @@ -80,6 +80,7 @@ + diff --git a/tests/FSharp.Compiler.Service.Tests/OverloadCacheTests.fs b/tests/FSharp.Compiler.Service.Tests/OverloadCacheTests.fs new file mode 100644 index 00000000000..cf6032afb3a --- /dev/null +++ b/tests/FSharp.Compiler.Service.Tests/OverloadCacheTests.fs @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +[] +module FSharp.Compiler.Service.Tests.OverloadCacheTests + +open System +open System.IO +open System.Text +open Xunit +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Diagnostics +open FSharp.Compiler.Text +open FSharp.Compiler.Caches +open FSharp.Test.Assert +open FSharp.Compiler.Service.Tests.Common +open TestFramework + +let checkSourceHasNoErrors (source: string) = + let file = Path.ChangeExtension(getTemporaryFileName (), ".fsx") + let _, checkResults = parseAndCheckScriptPreview (file, source) + let errors = checkResults.Diagnostics |> Array.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error) + errors |> shouldBeEmpty + checkResults + +let generateRepetitiveOverloadCalls (callCount: int) = + let sb = StringBuilder() + sb.AppendLine("open System") |> ignore + sb.AppendLine() |> ignore + + sb.AppendLine("type TestAssert =") |> ignore + sb.AppendLine(" static member Equal(expected: int, actual: int) = expected = actual") |> ignore + sb.AppendLine(" static member Equal(expected: string, actual: string) = expected = actual") |> ignore + sb.AppendLine(" static member Equal(expected: float, actual: float) = expected = actual") |> ignore + sb.AppendLine(" static member Equal(expected: bool, actual: bool) = expected = actual") |> ignore + sb.AppendLine(" static member Equal(expected: byte, actual: byte) = expected = actual") |> ignore + sb.AppendLine(" static member Equal(expected: int16, actual: int16) = expected = actual") |> ignore + sb.AppendLine(" static member Equal(expected: int64, actual: int64) = expected = actual") |> ignore + sb.AppendLine(" static member Equal(expected: obj, actual: obj) = obj.Equals(expected, actual)") |> ignore + sb.AppendLine() |> ignore + + sb.AppendLine("let runTests() =") |> ignore + sb.AppendLine(" let mutable x: int = 0") |> ignore + sb.AppendLine(" let mutable y: int = 0") |> ignore + for i in 1 .. callCount do + sb.AppendLine(sprintf " x <- %d" i) |> ignore + sb.AppendLine(sprintf " y <- %d" (i + 1)) |> ignore + sb.AppendLine(" ignore (TestAssert.Equal(x, y))") |> ignore + + sb.AppendLine() |> ignore + sb.AppendLine("runTests()") |> ignore + + sb.ToString() + + +[] +let ``Overload cache hit rate exceeds 70 percent for repetitive int-int calls`` () = + use listener = FSharpChecker.CreateOverloadCacheMetricsListener() + checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() + + let callCount = 150 + let source = generateRepetitiveOverloadCalls callCount + checkSourceHasNoErrors source |> ignore + + let hits = listener.Hits + let misses = listener.Misses + Assert.True(hits + misses > 0L, "Expected cache activity but got no hits or misses - is the cache enabled?") + Assert.True(listener.Ratio > 0.70, sprintf "Expected hit ratio > 70%%, but got %.2f%%" (listener.Ratio * 100.0)) + +[] +let ``Overload cache returns correct resolution`` () = + use listener = FSharpChecker.CreateOverloadCacheMetricsListener() + checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() + + let source = """ +type Overloaded = + static member Process(x: int) = "int" + static member Process(x: string) = "string" + static member Process(x: float) = "float" + +let r1 = Overloaded.Process(1) +let r2 = Overloaded.Process(2) +let r3 = Overloaded.Process(3) +let r4 = Overloaded.Process(4) +let r5 = Overloaded.Process(5) + +let s1 = Overloaded.Process("a") +let s2 = Overloaded.Process("b") + +let f1 = Overloaded.Process(1.0) +let f2 = Overloaded.Process(2.0) +""" + + checkSourceHasNoErrors source |> ignore + Assert.True(listener.Hits > 0L, "Expected cache hits for repeated overload calls") + +let overloadCorrectnessTestCases () : obj[] seq = + seq { + [| "type inference" :> obj; + """ +type Overloaded = + static member Process(x: int) = "int" + static member Process(x: string) = "string" + static member Process(x: float) = "float" + static member Process(x: 'T list) = "list" + +let inferredInt = Overloaded.Process(42) +let inferredString = Overloaded.Process("hello") +let inferredFloat = Overloaded.Process(3.14) +let inferredIntList = Overloaded.Process([1;2;3]) +let inferredStringList = Overloaded.Process(["a";"b"]) +let explicitInt: string = Overloaded.Process(100) +let explicitString: string = Overloaded.Process("world") +""" :> obj |] + + [| "nested generics" :> obj; + """ +type Container<'T> = { Value: 'T } + +type Processor = + static member Handle(x: Container) = "int container" + static member Handle(x: Container) = "string container" + static member Handle(x: Container>) = "nested int container" + +let c1 = { Value = 42 } +let c2 = { Value = "hello" } +let c3 = { Value = { Value = 99 } } +let r1 = Processor.Handle(c1) +let r2 = Processor.Handle(c2) +let r3 = Processor.Handle(c3) +let r4 = Processor.Handle({ Value = 123 }) +let r5 = Processor.Handle({ Value = "world" }) +""" :> obj |] + + [| "out args" :> obj; + """ +open System + +let test1 = Int32.TryParse("42") +let test2 = Double.TryParse("3.14") +let test3 = Boolean.TryParse("true") +let (success1: bool, value1: int) = test1 +let (success2: bool, value2: float) = test2 +let (success3: bool, value3: bool) = test3 +let a = Int32.TryParse("1") +let b = Int32.TryParse("2") +let c = Int32.TryParse("3") +""" :> obj |] + + [| "type abbreviations" :> obj; + """ +type IntList = int list +type StringList = string list + +type Processor = + static member Handle(x: int list) = "int list" + static member Handle(x: string list) = "string list" + static member Handle(x: int) = "int" + +let myIntList: IntList = [1; 2; 3] +let myStringList: StringList = ["a"; "b"] +let r1 = Processor.Handle(myIntList) +let r2 = Processor.Handle(myStringList) +let r3 = Processor.Handle([1; 2; 3]) +let r4 = Processor.Handle(["x"; "y"]) +let r5 = Processor.Handle(myIntList) +let r6 = Processor.Handle([4; 5; 6]) +""" :> obj |] + + [| "inference variables" :> obj; + """ +type Overloaded = + static member Process(x: int) = "int" + static member Process(x: string) = "string" + static member Process<'T>(x: 'T) = "generic" + +let a = Overloaded.Process(42) +let b = Overloaded.Process("hello") +let c = Overloaded.Process(true) +let x1 = Overloaded.Process(1) +let x2 = Overloaded.Process(2) +let x3 = Overloaded.Process(3) +let y1 = Overloaded.Process("a") +let y2 = Overloaded.Process("b") +let y3 = Overloaded.Process("c") +""" :> obj |] + + [| "type subsumption" :> obj; + """ +open System.Collections.Generic + +type Animal() = class end +type Dog() = inherit Animal() +type Cat() = inherit Animal() + +type Zoo = + static member Accept(animals: IEnumerable) = "animals" + static member Accept(dogs: IList) = "dogs" + static member Accept(x: obj) = "obj" + +let dogs: IList = [Dog(); Dog()] |> ResizeArray :> IList +let animals: IEnumerable = [Animal(); Dog(); Cat()] |> Seq.ofList +let r1 = Zoo.Accept(dogs) +let r2 = Zoo.Accept(animals) +let r3 = Zoo.Accept(42) +let d1 = Zoo.Accept(dogs) +let d2 = Zoo.Accept(dogs) +let d3 = Zoo.Accept(dogs) +let a1 = Zoo.Accept(animals) +let a2 = Zoo.Accept(animals) +let a3 = Zoo.Accept(animals) +let inline testWith<'T when 'T :> Animal>(items: seq<'T>) = Zoo.Accept(items) +let dogSeq = [Dog(); Dog()] |> Seq.ofList +let catSeq = [Cat(); Cat()] |> Seq.ofList +let t1 = testWith dogSeq +let t2 = testWith catSeq +""" :> obj |] + + [| "known vs inferred types" :> obj; + """ +type Overloaded = + static member Call(x: int) = "int" + static member Call(x: string) = "string" + static member Call(x: float) = "float" + +let r1 = Overloaded.Call(42) +let r2 = Overloaded.Call("hello") +let r3 = Overloaded.Call(3.14) +let a1 = Overloaded.Call(1) +let a2 = Overloaded.Call(2) +let a3 = Overloaded.Call(3) +let a4 = Overloaded.Call(4) +let a5 = Overloaded.Call(5) +let s1 = Overloaded.Call("a") +let s2 = Overloaded.Call("b") +let s3 = Overloaded.Call("c") +""" :> obj |] + + [| "generic overloads" :> obj; + """ +type GenericOverload = + static member Process(x: int) = "int" + static member Process(x: string) = "string" + static member Process<'T>(x: 'T) = "generic" + +let r1 = GenericOverload.Process(42) +let r2 = GenericOverload.Process("hello") +let r3 = GenericOverload.Process(true) +let x1 = GenericOverload.Process(1) +let x2 = GenericOverload.Process(2) +let x3 = GenericOverload.Process(3) +""" :> obj |] + + [| "nested generic types" :> obj; + """ +type Processor = + static member Handle(x: int list) = "int list" + static member Handle(x: string list) = "string list" + static member Handle(x: float list) = "float list" + +let r1 = Processor.Handle([1; 2; 3]) +let r2 = Processor.Handle(["a"; "b"; "c"]) +let r3 = Processor.Handle([1.0; 2.0; 3.0]) +let a1 = Processor.Handle([1]) +let a2 = Processor.Handle([2]) +let a3 = Processor.Handle([3]) +""" :> obj |] + } + +[] +[] +let ``Overload resolution correctness`` (_scenario: string, source: string) = + checkSourceHasNoErrors source |> ignore + +[] +let ``Overload cache benefits from rigid generic type parameters`` () = + use listener = FSharpChecker.CreateOverloadCacheMetricsListener() + checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() + + let source = """ +type Assert = + static member Equal(expected: int, actual: int) = expected = actual + static member Equal(expected: string, actual: string) = expected = actual + static member Equal(expected: float, actual: float) = expected = actual + static member Equal<'T when 'T: equality>(expected: 'T, actual: 'T) = expected = actual + +let inline check<'T when 'T: equality>(x: 'T, y: 'T) = Assert.Equal(x, y) + +let test1() = check(1, 2) +let test2() = check(3, 4) +let test3() = check(5, 6) +let test4() = check("a", "b") +let test5() = check("c", "d") +let test6() = check(1.0, 2.0) +let test7() = check(3.0, 4.0) + +let d1 = Assert.Equal(10, 20) +let d2 = Assert.Equal(30, 40) +let d3 = Assert.Equal("x", "y") +let d4 = Assert.Equal("z", "w") +""" + + checkSourceHasNoErrors source |> ignore