From b9519e7ad070fac202f6038bf62423a3351c99ef Mon Sep 17 00:00:00 2001 From: Petr Date: Mon, 4 Mar 2024 19:13:02 +0100 Subject: [PATCH 1/7] Docs on equality in F# (#16537) --- docs/optimizations-equality.md | 326 +++++++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 docs/optimizations-equality.md diff --git a/docs/optimizations-equality.md b/docs/optimizations-equality.md new file mode 100644 index 00000000000..acabd8caa2b --- /dev/null +++ b/docs/optimizations-equality.md @@ -0,0 +1,326 @@ +# Compiling Equality + +This spec covers how equality is compiled and executed by the F# compiler and library, based mainly on the types involved in the equality operation after all inlining, type specialization and other optimizations have been applied. + +## What do we mean by an equality operation? + +This spec is about the semantics and performance of the following coding constructs + +* `a = b` +* `a <> b` + +It is also about the semantics and performance of uses of the following `FSharp.Core` constructs which, after inlining, generate code that contains an equality check at the specific `EQTYPE` +* `HashIdentity.Structural<'T>` +* `{Array,Seq,List}.contains` +* `{Array,Seq,List}.countBy` +* `{Array,Seq,List}.groupBy` +* `{Array,Seq,List}.distinct` +* `{Array,Seq,List}.distinctBy` +* `{Array,Seq,List}.except` + +All of which have implied equality checks. Some of these operations are inlined, see below, which in turn affects the semantics and performance of the overall operation. + +## ER vs PER equality + +In math, a (binary) relation is a way to describe a relationship between the elements of sets. "Greater than" is a relation for numbers, "Subset of" is a relation for sets. + +Here we talk about 3 particular relations: +1) **Reflexivity** - every element is related to itself + - For integers, `=` is reflexive (`a = a` is always true) and `>` is not (`a > a` is never true) +2) **Symmetry** - if `a` is related to `b`, then `b` is related to `a` + - For integers, `=` is symmetric (`a = b` -> `b = a`) and `>` is not (if `a > b` then `b > a` is false) +3) **Transitivity** - if `a` is related to `b`, and `b` is related to `c`, then `a` is also related `c` + - For integers, `>` is transitive (`a > b` && `b > c` -> `a > c`) and `√` is not (`a = √b` && `b = √c` doesn't mean `a = √c`) + +If a relation has 1, 2, and 3, we talk about **Equivalence Relation (ER)**. If a relation only has 2 and 3, we talk about **Partial Equivalence Relation (PER)**. + +This matters in comparing floats since they include [NaN](https://en.wikipedia.org/wiki/NaN). Depending on if we consider `NaN = NaN` true or false, we talk about ER or PER comparison respectively. + +## What is the type known to the compiler and library for an equality operation? + +The static type known to the F# compiler is crucial to determining the performance of the operation. The runtime type of the equality check is also significant in some situations. + +Here we define the relevant static type `EQTYPE` for the different constructs above: + +### Basics + +* `a = b`: `EQTYPE` is the statically known type of `a` or `b` +* `a <> b`: `EQTYPE` is the statically known type of `a` or `b` + +### Inlined constructs + +* `HashIdentity.Structural<'T>`, `EQTYPE` is the **inlined** `'T` (results in specialized equality) +* `Array.contains<'T>`, `EQTYPE` is the **inlined** `'T` (results in specialized equality) +* `List.contains` likewise +* `Seq.contains` likewise + +These only result in naked generic equality if themselves used from a non-inlined generic context. + +### Non-inlined constructs always resulting in naked generic equality + +* `Array.groupBy<'Key, 'T> f array`, `EQTYPE` is non-inlined `'Key`, results in naked generic equality +* `Array.countBy array` likewise for `'T` +* `Array.distinct<'T> array` likewise +* `Array.distinctBy array` likewise +* `Array.except array` likewise +* `List.groupBy` likewise +* `List.countBy` likewise +* `List.distinct` likewise +* `List.distinctBy` likewise +* `List.except` likewise +* `Seq.groupBy` likewise +* `Seq.countBy` likewise +* `Seq.distinct` likewise +* `Seq.distinctBy` likewise +* `Seq.except` likewise + +These **always** result in naked generic equality checks. + +Example 1: + +```fsharp +let x = HashIdentity.Structural // EQTYPE known to compiler is `byte` +``` + +Example 2 (a non-inlined "naked" generic context): + +```fsharp +let f2<'T> () = + ... some long code + // EQTYPE known to the compiler is `'T` + // RUNTIME-EQTYPE known to the library is `byte` + let x = HashIdentity.Structural<'T> + ... some long code + +f2() // performance of this is determined by EQTYPE<'T> and RUNTIME-EQTYPE +``` + +Example 3 (an inlined generic context): + +```fsharp +let f3<'T> () = + ... some long code + // EQTYPE known to the compiler is `byte` + // RUNTIME-EQTYPE known to the library is `byte` + let x = HashIdentity.Structural<'T> + ... some long code + +f3() // performance of this is determined by EQTYPE and RUNTIME-EQTYPE +``` + +Example 4 (a generic struct type in a non-inline generic context): + +```fsharp +let f4<'T> () = + ... some long code + // EQTYPE known to the compiler is `SomeStructType<'T>` + // RUNTIME-EQTYPE known to the library is `SomeStructType` + let x = HashIdentity.Structural> + ... some long code + +f4() // performance of this determined by EQTYPE> and RUNTIME-EQTYPE> +``` + +## How we compile equality "a = b" + +This very much depends on the `EQTYPE` involved in the equality as known by the compiler + +Aim here is to flesh these all out with: +* **Semantics**: what semantics the user expects, and what the semantics actually is +* **Perf expectation**: what perf the user expects +* **Compilation today**: How we actually compile today +* **Perf today**: What is the perf we achieve today +* (Optional) sharplab.io link to how things are in whatever version is selected in sharplab +* (Optional) notes + +### primitive integer types (`int32`, `int64`, ...) + +```fsharp +let f (x: int) (y: int) = (x = y) +``` + +* Semantics: equality on primitive +* Perf: User expects full performance down to native +* Compilation today: compiles to IL instruction ✅ +* Perf today: good ✅ +* [sharplab int32](https://sharplab.io/#v2:DYLgZgzgNAJiDUAfYBTALgAjBgFADxAwEsA7NASlwE9DSKMBeXPRjK8gWACgg===) + +### primitive floating point types (`float32`, `float64`) + +```fsharp +let f (x: float32) (y: float32) = (x = y) +``` + +* Semantics: IEEE floating point equality (respecting NaN etc.) +* Perf: User expects full performance down to native +* Compilation today: compiles to IL instruction ✅ +* Perf today: good ✅ +* [sharplab float32](https://sharplab.io/#v2:DYLgZgzgNAJiDUAfYBTALgAjBgFADxC2AHsBDNAZgCYBKXAT0LBPOroF5c8NP6aBYAFBA===) + +### primitive `string`, `decimal` + +* Semantics: .NET equivalent equality, non-localized for strings +* Perf: User expects full performance down to native +* Compilation today: compiles to `String.Equals` or `Decimal.op_Equality` call ✅ +* Perf today: good ✅ +* [sharplab decimal](https://sharplab.io/#v2:DYLgZgzgNALiCWwoBMQGoA+wCmMAEYeAFAB4h7LYDG8AtgIbACUxAnuZTQ83gLzEk+eVkwCwAKCA) +* [sharplab string](https://sharplab.io/#v2:DYLgZgzgNALiCWwoBMQGoA+wCmMAEYeAFAB4h4QwBO8AdgOYCUxAnuZTQ8wLzEl68WjALAAoIA==) + +### reference tuple type (size <= 5) + +* Semantics: User expects structural +* Perf: User expects flattening to constituent checks +* Compilation today: tuple equality is flattened to constituent checks ✅ +* Perf today: good ✅ +* [sharplab (int * double * 'T), with example reductions/optimizations noted](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVAEwHsBXAIyVTIHIAVASjdQE9UBeLbH25t48TCVFxB/LpIC0cosCJEA5goB8kgOKJCiAE74AxgFEAjtQCGy5D0Gy48BUpWF1crU7gAJKxAALAGFKAFsABysDRAA6XX0jM0sbfDsAMX80B1R5RUJlQjVNHT1DEwtrWy4ASWIjQggTAB4WAEZGVBYAJg6WAGYNVAdcgHlw5HxQ/AAvQ00sckQAN3wDNHiypMrUmrqiRuMRbwyIZAqbCBZqcKQ+1AAZK3drVUQABSMpiaXECDjSxIhCJRQwCVoAGmwXUhfU4mC4EK40K4sNyrkK7mK3iQaGMYUi0QMQkezysrw+k1S+B+fw2gPxIIM8Dp5WSVQA6qlggzCSdcTzQdh2gjUAAyUXMgGs7Z2TnIbnA3mZVB4xWCnpIsUSuAsrYpWVcoEEwx8lUConYO4o3KDSQ4s1qon8EmqF7vT5Umn/BImI2M+DGRDmIbC9rigNBoYanrhnVSvUcw3m2rIeoHB3Gi1WvqSEhHeBAA==) + +### reference tuple type (size > 5) + +* Semantics: User expects structural +* Perf: User expects flattening to constituent checks +* Compilation today: not flattened, compiled to `GenericEqualityIntrinsic` +* Perf today: the check does type tests, does virtual calls via `IStructuralEqualityComparer`, boxes etc. ❌(Problem1) +* [sharplab for size 6](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVI0841MimqyigSidQE9UBeLbL9p+EA==) + +### struct tuple type + +* Semantics: User expects structural +* Perf: User expects flattening to constituent checks or at least the same optimizations as tuples +* Compilation today: compiled to `GenericEqualityIntrinsic` +* Perf today: boxes, does type tests, does virtual calls via `IStructuralEqualityComparer` etc. ❌(Problem2) +* [sharplab for size 3](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lRZAJwFcBjNTASwDs0AqVG+x2gSldQE9UBeLbXl1bwgA=) + +### C# or F# enum type + +* Semantics: User expects identical to equality on the underlying type +* Perf: User expects same perf as flattening to underlying type +* Compilation today: flattens to underlying type +* Perf today: good ✅ +* [sharplab for C# enum int](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUMApjABGHAFAB4g4DKAnhDJgLYB0AIgIYUDyYA6ppgNYCUOCjgC8hIqKH90QA===) +* [sharplab for F# enum int](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUDAngBwKYAIBRfAXn3X0qXwEFT8BGCq/AIXoCZ11hcZ8w+ABQAPEEQCU+TPVH1ME9EA=) + +### C# struct type + +* Semantics: User expects call to `IEquatable` if present, but F# spec says call `this.Equals(box that)`, in practice these are the same +* Perf expected: no boxing +* Compilation today: `GenericEqualityIntrinsic` +* Perf today: always boxes (Problem3 ❌) +* [sharplab](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUMApjABGHAFAB4g4DKAnhDJgLYB0AIgIY0Aq8tmA8mJNgEocFHAF5CRMcIHogA==) +* Note: [#16615](https://github.com/dotnet/fsharp/pull/16615) will improve things here since we'll start avoiding boxing + +### F# struct type (records, tuples - with compiler-generated structural equality) + +* Semantics: User expects field-by-field structural equality with no boxing +* Perf expected: no boxing +* Compilation today: `GenericEqualityIntrinsic` +* Perf today: always boxes (Problem3 ❌) +* [sharplab](https://sharplab.io/#v2:DYLgZgzgNALiCWwA+BYAUAbQDwGUYCcBXAYxgD4BddGATwAcBTAAhwHsBbBvI0gCgDcQTeADsYUJoSGiYASiYBedExVNO7AEYN8TAPoA6AGqKm/ZavVadBgKonC6dMAYwmYJrwAeQtp24k5JhoTLxMaWXQgA) +* Note: the optimization path is a bit strange here, see the reductions below + +
+ +Details + +```fsharp +(x = y) + +--inline--> + +GenericEquality x y + +--inline--> + +GenericEqualityFast x y + +--inline--> + +GenericEqualityIntrinsic x y + +--devirtualize--> + +x.Equals(box y, LanguagePrimitives.GenericEqualityComparer); +``` + +The struct type has these generated methods: +```csharp + override bool Equals(object y) + override bool Equals(SomeStruct obj) + override bool Equals(object obj, IEqualityComparer comp) //with EqualsVal +``` + +These call each other in sequence, boxing then unboxing then boxing. We do NOT generate this method, we probably should: + +```csharp + override bool Equals(SomeStruct obj, IEqualityComparer comp) //with EqualsValUnboxed +``` + +If we did, the devirtualizing optimization should reduce to this directly, which would result in no boxing. + +
+ +### array type (byte[], int[], some-struct-type[], ...) + +* Semantics: User expects structural +* Perf expected: User expects perf is sum of constituent parts +* Compilation today: `GenericEqualityIntrinsic` +* Perf today: hand-optimized ([here](https://github.com/dotnet/fsharp/blob/611e4f350e119a4173a2b235eac65539ac2b61b6/src/FSharp.Core/prim-types.fs#L1562)) for some primitive element types ✅ but boxes each element if "other" is struct or generic, see Problem3 ❌, Problem4 ❌ +* [sharplab for `byte[]`](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4lQIwE9lEBtAXQEpVDUBeLbemy+IA=) +* Note: ([#16615](https://github.com/dotnet/fsharp/pull/16615)) will improve this compiling to either ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` or ``FSharpEqualityComparer_PER`1::get_EqualityComparer().Equals(...)`` + +### F# large reference record/union type + +Here "large" means the compiler-generated structural equality is NOT inlined. + +* Semantics: User expects structural by default +* Perf expected: User expects perf is sum of constituent parts, type-specialized if generic +* Compilation today: direct call to `Equals(T)` +* Perf today: the call to `Equals(T)` has specialized code but boxes fields if struct or generic, see Problem3 ❌, Problem4 ❌ + +### F# tiny reference (anonymous) record or union type + +Here "tiny" means the compiler-generated structural equality IS inlined. + +* Semantics: User expects structural by default +* Perf expected: User expects perf is sum of constituent parts, type-specialized if generic +* Compilation today: flattened, calling `GenericEqualityERIntrinsic` on struct and generic fields +* Perf today: boxes on struct and generic fields, see Problem3 ❌, Problem4 ❌ +* Note: [#16615](https://github.com/dotnet/fsharp/pull/16615) will help, compiling to ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` on struct and generic fields + +### Generic `'T` in non-inlined generic code + +* Semantics: User expects the PER equality semantics of whatever `'T` actually is +* Perf expected: User expects no boxing +* Compilation today: `GenericEqualityERIntrinsic` +* Perf today: boxes if `'T` is any non-reference type (Problem4 ❌) +* Note: [#16615](https://github.com/dotnet/fsharp/pull/16615) will improve this compiling to ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` + +### Generic `'T` in recursive position in structural comparison + +This case happens in structural equality for tuple types and other structural types + +* Semantics: User expects the PER equality semantics of whatever `'T` actually is +* Perf: User expects no boxing +* Compilation today: `GenericEqualityWithComparerIntrinsic LanguagePrimitives.GenericComparer` +* Perf today: boxes for if `'T` is any non-reference type (Problem4 ❌) +* [Sharplab](https://sharplab.io/#v2:DYLgZgzgPgsAUMApgFwARlQCgB4iwSwDs0AqVAEwHsBXAIyVTIHIAVASjdQE9UBeLbH25t48TCVFxB/LpIC0cosCJEA5goB8kgOKJCiAE74AxgFEAjtQCGy5D0Gy48BUpWF1crU7gAJKxAALAGFKAFsABysDRAA6XX0jM0sbfDsAMX80B1R5RUJlQjVNHT1DEwtrWy4ASWIjQggTAB4WAEZGVBYAJg6WAGYNVAdcgHlw5HxQ/AAvQ00sckQAN3wDNHiypMrUmrqiRuMRbwyIZAqbCBZqcKQ+1AAZK3drVUQABSMpiaXECDjSxIhCJRQwCVoAGmwXUhfU4mC4EK40K4sNyrkK7mK3iQaGMYUi0QMQkezysrw+k1S+B+fw2gPxIIM8Dp5WSVQA6qlggzCSdcTzQdh2gjUAAyUXMgGs7Z2TnIbnA3mZVB4xWCnpIsUSuAsrYpWVcoEEwx8lUConYO4o3KDSQ4s1qon8EmqF7vT5Umn/BImI2M+DGRDmIbC9rigNBoYanrhnVSvUcw3m2rIeoHB3Gi1WvqSEhHeBAA==) +* Note: [#16615](https://github.com/dotnet/fsharp/pull/16615) will compile to ``FSharpEqualityComparer_ER`1::get_EqualityComparer().Equals(...)`` and avoid boxing in many cases + +## Techniques available to us + +1. Flatten and inline +2. RCG: Use reflective code generation internally in FSharp.Core +3. KFS: Rely on known semantics of F# structural types and treat those as special +4. TS: Hand-code type-specializations using static optimization conditions in FSharp.Core +5. TT: Type-indexed tables of baked (poss by reflection) equality comparers and functions, where some pre-computation is done +6. DV: De-virtualization +7. DEQ: Use `EqualityComparer<'T>.Default` where possible + +## Notes on previous attempts to improve things + +### [#5112](https://github.com/dotnet/fsharp/pull/5112) + +* Uses TT, DEQ, KFS, DV +* Focuses on solving Problem4 +* 99% not breaking, apart from the case of value types with custom equality implemented differently than the `EqualityComparer.Default` - the change would lead to the usage of the custom implementation which is reasonable + +Note: this included [changes to the optimizer to reduce GenericEqualityIntrinsic](https://github.com/dotnet/fsharp/pull/5112/files#diff-be48dbef2f0baca27a783ac4a31ec0aedb2704c7f42ea3a2b8228513f9904cfbR2360-R2363) down to a type-indexed table lookup fetching an `IEqualityComparer` and calling it. These hand-coded reductions appear unnecessary as the reduction doesn't open up any further optimizations. \ No newline at end of file From 9877cfec82ac5b70c6acb33aa15bbddfdb7188de Mon Sep 17 00:00:00 2001 From: Petr Date: Mon, 4 Mar 2024 20:01:52 +0100 Subject: [PATCH 2/7] Faster equality in generic contexts (#16615) --- azure-pipelines.yml | 6 +- docs/release-notes/.FSharp.Core/8.0.300.md | 2 + eng/Build.ps1 | 2 +- src/FSharp.Core/prim-types.fs | 252 +++++++++++++----- tests/AheadOfTime/Equality/Equality.fsproj | 35 +++ tests/AheadOfTime/Equality/Program.fs | 77 ++++++ tests/AheadOfTime/Equality/check.ps1 | 32 +++ tests/AheadOfTime/Trimming/check.cmd | 2 - tests/AheadOfTime/Trimming/check.ps1 | 4 +- tests/AheadOfTime/check.cmd | 3 + .../MicroPerf/Benchmarks.fs | 10 +- .../MicroPerf/Equality/Arrays.fs | 23 ++ .../MicroPerf/Equality/BasicTypes.fs | 63 +++++ .../MicroPerf/Equality/FSharpCoreFunctions.fs | 102 +++++++ .../MicroPerf/Equality/Floats.fs | 20 ++ .../MicroPerf/Equality/Misc.fs | 78 ++++++ .../MicroPerf/Equality/OptionsAndCo.fs | 45 ++++ .../MicroPerf/Equality/Structs.fs | 39 +++ .../MicroPerf/Equality/Tuples.fs | 67 +++++ .../MicroPerf/Equality/ValueTypes.fs | 39 +++ .../MicroPerf/MicroPerf.fsproj | 11 +- 21 files changed, 836 insertions(+), 76 deletions(-) create mode 100644 tests/AheadOfTime/Equality/Equality.fsproj create mode 100644 tests/AheadOfTime/Equality/Program.fs create mode 100644 tests/AheadOfTime/Equality/check.ps1 delete mode 100644 tests/AheadOfTime/Trimming/check.cmd create mode 100644 tests/AheadOfTime/check.cmd create mode 100644 tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Arrays.fs create mode 100644 tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/BasicTypes.fs create mode 100644 tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/FSharpCoreFunctions.fs create mode 100644 tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Floats.fs create mode 100644 tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Misc.fs create mode 100644 tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/OptionsAndCo.fs create mode 100644 tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Structs.fs create mode 100644 tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Tuples.fs create mode 100644 tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/ValueTypes.fs diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e7a71227a63..a3f9bb53187 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -794,7 +794,7 @@ stages: condition: always() # Test trimming on Windows - - job: Build_And_Test_Trimming_Windows + - job: Build_And_Test_AOT_Windows pool: name: $(DncEngPublicBuildPool) demands: ImageOverride -equals $(WindowsMachineQueueName) @@ -825,9 +825,9 @@ stages: env: NativeToolsOnMachine: true displayName: Initial build and prepare packages. - - script: $(Build.SourcesDirectory)/tests/AheadOfTime/Trimming/check.cmd + - script: $(Build.SourcesDirectory)/tests/AheadOfTime/check.cmd displayName: Build, trim, publish and check the state of the trimmed app. - workingDirectory: $(Build.SourcesDirectory)/tests/AheadOfTime/Trimming + workingDirectory: $(Build.SourcesDirectory)/tests/AheadOfTime - task: PublishPipelineArtifact@1 displayName: Publish Trim Tests Logs inputs: diff --git a/docs/release-notes/.FSharp.Core/8.0.300.md b/docs/release-notes/.FSharp.Core/8.0.300.md index a978645b641..7c3911ae98f 100644 --- a/docs/release-notes/.FSharp.Core/8.0.300.md +++ b/docs/release-notes/.FSharp.Core/8.0.300.md @@ -1,6 +1,8 @@ ### Added * Minor tweaks to inline specifications to support Visibility PR ([PR #15484](https://github.com/dotnet/fsharp/pull/15484), [#PR 16427](https://github.com/dotnet/fsharp/pull/15484) +* Optimize equality in generic contexts. ([PR #16615](https://github.com/dotnet/fsharp/pull/16615)) ### Fixed + * Preserve original stack traces in resumable state machines generated code if available. ([PR #16568](https://github.com/dotnet/fsharp/pull/16568)) diff --git a/eng/Build.ps1 b/eng/Build.ps1 index 5bf5d00bdc5..bb449e05665 100644 --- a/eng/Build.ps1 +++ b/eng/Build.ps1 @@ -680,7 +680,7 @@ try { } if ($testAOT) { - Push-Location "$RepoRoot\tests\AheadOfTime\Trimming" + Push-Location "$RepoRoot\tests\AheadOfTime" ./check.cmd Pop-Location } diff --git a/src/FSharp.Core/prim-types.fs b/src/FSharp.Core/prim-types.fs index a6d5bcc6ac0..346c1a64e0e 100644 --- a/src/FSharp.Core/prim-types.fs +++ b/src/FSharp.Core/prim-types.fs @@ -1549,8 +1549,8 @@ namespace Microsoft.FSharp.Core // Run in either PER or ER mode. In PER mode, equality involving a NaN returns "false". // In ER mode, equality on two NaNs returns "true". // - // If "er" is true the "iec" is fsEqualityComparerNoHashingER - // If "er" is false the "iec" is fsEqualityComparerNoHashingPER + // If "er" is true the "iec" is fsEqualityComparerUnlimitedHashingER + // If "er" is false the "iec" is fsEqualityComparerUnlimitedHashingPER let rec GenericEqualityObj (er:bool) (iec:IEqualityComparer) ((xobj:obj),(yobj:obj)) : bool = (*if objEq xobj yobj then true else *) match xobj,yobj with @@ -1654,28 +1654,186 @@ namespace Microsoft.FSharp.Core else i <- i + 1 res + let isStructuralEquatable (ty: Type) = typeof.IsAssignableFrom ty + let isArray (ty: Type) = ty.IsArray || (typeof.IsAssignableFrom ty) + let isFloat (ty: Type) = Type.op_Equality(ty, typeof) || Type.op_Equality(ty, typeof) + + let isValueTuple (ty: Type) = + if ty.IsGenericType + then + let typeDef = ty.GetGenericTypeDefinition().FullName + typeDef.Equals "System.ValueTuple`1" || + typeDef.Equals "System.ValueTuple`2" || + typeDef.Equals "System.ValueTuple`3" || + typeDef.Equals "System.ValueTuple`4" || + typeDef.Equals "System.ValueTuple`5" || + typeDef.Equals "System.ValueTuple`6" || + typeDef.Equals "System.ValueTuple`7" || + typeDef.Equals "System.ValueTuple`8" + else false + + let isOptionOrResult (ty: Type) = + if ty.IsGenericType + then + let typeDef = ty.GetGenericTypeDefinition().FullName + typeDef.Equals "Microsoft.FSharp.Core.FSharpOption`1" || + typeDef.Equals "Microsoft.FSharp.Core.FSharpValueOption`1" || + typeDef.Equals "Microsoft.FSharp.Core.FSharpResult`2" + else false + + let isNullable (ty: Type) = + ty.IsGenericType && + ty.GetGenericTypeDefinition().FullName.Equals "System.Nullable`1" + + // A bit hard to grasp right away but think of this as if it was: + // + // let canUseDefaultEqualityComparer rootType = + // let processed = HashSet() + // + // let rec checkTypes types = + // match types with + // | [] -> true + // | ty :: rest -> + // if (processed.Add ty) then + // ... do all the checks + // checkTypes rest + // else + // checkTypes rest + // + // checkTypes [ rootType ] + // + // ... it's just that here in prim types, not much stuff is available yet + // so we have to resort to very basic techniques + let canUseDefaultEqualityComparer er rootType = + let processed = HashSet() - /// One of the two unique instances of System.Collections.IEqualityComparer. Implements PER semantics - /// where equality on NaN returns "false". - let fsEqualityComparerNoHashingPER = - { new IEqualityComparer with - override iec.Equals(x:obj,y:obj) = GenericEqualityObj false iec (x,y) // PER Semantics - override iec.GetHashCode(x:obj) = raise (InvalidOperationException (SR.GetString(SR.notUsedForHashing))) } - - /// One of the two unique instances of IEqualityComparer. Implements ER semantics - /// where equality on NaN returns "true". - let fsEqualityComparerNoHashingER = - { new IEqualityComparer with - override iec.Equals(x:obj,y:obj) = GenericEqualityObj true iec (x,y) // ER Semantics - override iec.GetHashCode(x:obj) = raise (InvalidOperationException (SR.GetString(SR.notUsedForHashing))) } + let rec checkType index (types: Type array) = + if types.Length = index then true + else + let ty = get types index + + if processed.Add ty + then + // avoid any types that need special handling in GenericEqualityObj + // GenericEqualityObj handles string as a special case, but internally routes to same equality + + // covers enum and value types + // reference types need to be sealed as derived class might implement IStructuralEquatable + ty.IsSealed + + // handled elsewhere in a specialized fashion + && not (isArray ty) + && (er || not (isFloat ty)) + + // analyze generic types + && (match ty with + | ty when isNullable ty || + isStructuralEquatable ty && isValueTuple ty || + isStructuralEquatable ty && isOptionOrResult ty -> checkType 0 (ty.GetGenericArguments()) + + | ty when isStructuralEquatable ty -> false + + | _ -> true) + + && checkType (index + 1) types + + else + checkType (index + 1) types + + checkType 0 [| rootType |] + + let arrayEqualityComparer<'T> er comparer = + let arrayEquals (er: bool) (iec: IEqualityComparer) (xobj: obj) (yobj: obj) : bool = + match xobj, yobj with + | null, null -> true + | null, _ -> false + | _, null -> false + | (:? (obj array) as arr1), (:? (obj array) as arr2) -> GenericEqualityObjArray er iec arr1 arr2 + | (:? (byte array) as arr1), (:? (byte array) as arr2) -> GenericEqualityByteArray arr1 arr2 + | (:? (int32 array) as arr1), (:? (int32 array) as arr2) -> GenericEqualityInt32Array arr1 arr2 + | (:? (int64 array) as arr1), (:? (int64 array) as arr2) -> GenericEqualityInt64Array arr1 arr2 + | (:? (char array) as arr1), (:? (char array) as arr2) -> GenericEqualityCharArray arr1 arr2 + | (:? (float32 array) as arr1), (:? (float32 array) as arr2) -> GenericEqualitySingleArray er arr1 arr2 + | (:? (float array) as arr1), (:? (float array) as arr2) -> GenericEqualityDoubleArray er arr1 arr2 + | (:? Array as arr1), (:? Array as arr2) -> GenericEqualityArbArray er iec arr1 arr2 + | _ -> raise (Exception "invalid logic - expected array") + + let getHashCode (iec, xobj: obj) = + match xobj with + | null -> 0 + | :? (obj array) as oa -> GenericHashObjArray iec oa + | :? (byte array) as ba -> GenericHashByteArray ba + | :? (int array) as ia -> GenericHashInt32Array ia + | :? (int64 array) as ia -> GenericHashInt64Array ia + | :? Array as a -> GenericHashArbArray iec a + | _ -> raise (Exception "invalid logic - expected array") + + { new EqualityComparer<'T>() with + member _.Equals (x, y) = arrayEquals er comparer (box x) (box y) + member _.GetHashCode x = getHashCode (fsEqualityComparerUnlimitedHashingPER, box x) } + + let structuralEqualityComparer<'T> comparer = + { new EqualityComparer<'T>() with + member _.Equals (x,y) = + match box x, box y with + | null, null -> true + | null, _ -> false + | _, null -> false + | (:? IStructuralEquatable as x1), yobj -> x1.Equals (yobj, comparer) + | _ -> raise (Exception "invalid logic - expected IStructuralEquatable") + + member _.GetHashCode x = + match box x with + | null -> 0 + | :? IStructuralEquatable as a -> a.GetHashCode fsEqualityComparerUnlimitedHashingPER + | _ -> raise (Exception "invalid logic - expected IStructuralEquatable") } + + let getEqualityComparer<'T> er = + let ty = typeof<'T> + + match er with + | _ when canUseDefaultEqualityComparer er ty -> EqualityComparer<'T>.Default + + | true when isArray ty -> arrayEqualityComparer true fsEqualityComparerUnlimitedHashingER + | false when isArray ty -> arrayEqualityComparer false fsEqualityComparerUnlimitedHashingPER + | true when isStructuralEquatable ty -> structuralEqualityComparer fsEqualityComparerUnlimitedHashingER + | false when isStructuralEquatable ty -> structuralEqualityComparer fsEqualityComparerUnlimitedHashingPER + + | false when Type.op_Equality(ty, typeof) -> unboxPrim (box { + new EqualityComparer() with + member _.Equals (x,y) = (# "ceq" x y : bool #) + member _.GetHashCode x = x.GetHashCode () }) + + | false when Type.op_Equality(ty, typeof) -> unboxPrim (box { + new EqualityComparer() with + member _.Equals (x,y) = (# "ceq" x y : bool #) + member _.GetHashCode x = x.GetHashCode () }) + + | false -> { + new EqualityComparer<'T>() with + member _.Equals (x,y) = GenericEqualityObj false fsEqualityComparerUnlimitedHashingPER (box x, box y) + member _.GetHashCode x = GenericHashParamObj fsEqualityComparerUnlimitedHashingPER (box x) } + + | true -> { + new EqualityComparer<'T>() with + member _.Equals (x,y) = GenericEqualityObj true fsEqualityComparerUnlimitedHashingER (box x, box y) + member _.GetHashCode x = GenericHashParamObj fsEqualityComparerUnlimitedHashingER (box x) } + + type EqualityComparerER<'T> () = + + static member val Comparer = getEqualityComparer<'T> true + + type EqualityComparerPER<'T> () = + + static member val Comparer = getEqualityComparer<'T> false /// Implements generic equality between two values, with PER semantics for NaN (so equality on two NaN values returns false) // // The compiler optimizer is aware of this function (see use of generic_equality_per_inner_vref in opt.fs) // and devirtualizes calls to it based on "T". let GenericEqualityIntrinsic (x : 'T) (y : 'T) : bool = - GenericEqualityObj false fsEqualityComparerNoHashingPER ((box x), (box y)) - + EqualityComparerPER<'T>.Comparer.Equals (x, y) + /// Implements generic equality between two values, with ER semantics for NaN (so equality on two NaN values returns true) // // ER semantics is used for recursive calls when implementing .Equals(that) for structural data, see the code generated for record and union types in augment.fs @@ -1683,15 +1841,20 @@ namespace Microsoft.FSharp.Core // The compiler optimizer is aware of this function (see use of generic_equality_er_inner_vref in opt.fs) // and devirtualizes calls to it based on "T". let GenericEqualityERIntrinsic (x : 'T) (y : 'T) : bool = - GenericEqualityObj true fsEqualityComparerNoHashingER ((box x), (box y)) + EqualityComparerER<'T>.Comparer.Equals (x, y) /// Implements generic equality between two values using "comp" for recursive calls. // // The compiler optimizer is aware of this function (see use of generic_equality_withc_inner_vref in opt.fs) // and devirtualizes calls to it based on "T", and under the assumption that "comp" - // is either fsEqualityComparerNoHashingER or fsEqualityComparerNoHashingPER. + // is either fsEqualityComparerUnlimitedHashingER or fsEqualityComparerUnlimitedHashingPER. let GenericEqualityWithComparerIntrinsic (comp : IEqualityComparer) (x : 'T) (y : 'T) : bool = - comp.Equals((box x),(box y)) + if obj.ReferenceEquals (comp, fsEqualityComparerUnlimitedHashingPER) then + EqualityComparerPER<'T>.Comparer.Equals (x, y) + elif obj.ReferenceEquals (comp, fsEqualityComparerUnlimitedHashingER) then + EqualityComparerER<'T>.Comparer.Equals (x, y) + else + comp.Equals ((box x), (box y)) /// Implements generic equality between two values, with ER semantics for NaN (so equality on two NaN values returns true) @@ -1810,7 +1973,7 @@ namespace Microsoft.FSharp.Core // // NOTE: The compiler optimizer is aware of this function (see uses of generic_hash_inner_vref in opt.fs) // and devirtualizes calls to it based on type "T". - let GenericHashIntrinsic input = GenericHashParamObj fsEqualityComparerUnlimitedHashingPER (box input) + let GenericHashIntrinsic input = EqualityComparerPER<'T>.Comparer.GetHashCode input /// Intrinsic for calls to depth-limited structural hashing that were not optimized by static conditionals. let LimitedGenericHashIntrinsic limit input = GenericHashParamObj (CountLimitedHasherPER(limit)) (box input) @@ -1823,7 +1986,10 @@ namespace Microsoft.FSharp.Core // NOTE: The compiler optimizer is aware of this function (see uses of generic_hash_withc_inner_vref in opt.fs) // and devirtualizes calls to it based on type "T". let GenericHashWithComparerIntrinsic<'T> (comp : IEqualityComparer) (input : 'T) : int = - GenericHashParamObj comp (box input) + if obj.ReferenceEquals (comp, fsEqualityComparerUnlimitedHashingPER) then + EqualityComparerPER<'T>.Comparer.GetHashCode input + else + GenericHashParamObj comp (box input) let inline HashString (s:string) = match s with @@ -2136,46 +2302,8 @@ namespace Microsoft.FSharp.Core member _.GetHashCode(x) = GenericLimitedHash limit x member _.Equals(x,y) = GenericEquality x y } - let BoolIEquality = MakeGenericEqualityComparer() - let CharIEquality = MakeGenericEqualityComparer() - let StringIEquality = MakeGenericEqualityComparer() - let SByteIEquality = MakeGenericEqualityComparer() - let Int16IEquality = MakeGenericEqualityComparer() - let Int32IEquality = MakeGenericEqualityComparer() - let Int64IEquality = MakeGenericEqualityComparer() - let IntPtrIEquality = MakeGenericEqualityComparer() - let ByteIEquality = MakeGenericEqualityComparer() - let UInt16IEquality = MakeGenericEqualityComparer() - let UInt32IEquality = MakeGenericEqualityComparer() - let UInt64IEquality = MakeGenericEqualityComparer() - let UIntPtrIEquality = MakeGenericEqualityComparer() - let FloatIEquality = MakeGenericEqualityComparer() - let Float32IEquality = MakeGenericEqualityComparer() - let DecimalIEquality = MakeGenericEqualityComparer() - - type FastGenericEqualityComparerTable<'T>() = - static let f : IEqualityComparer<'T> = - match typeof<'T> with - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box BoolIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box ByteIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box Int32IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box UInt32IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box CharIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box SByteIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box Int16IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box Int64IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box IntPtrIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box UInt16IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box UInt64IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box UIntPtrIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box FloatIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box Float32IEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box DecimalIEquality) - | ty when Type.op_Equality(ty, typeof) -> unboxPrim (box StringIEquality) - | _ -> MakeGenericEqualityComparer<'T>() - static member Function : IEqualityComparer<'T> = f - - let FastGenericEqualityComparerFromTable<'T> = FastGenericEqualityComparerTable<'T>.Function + let FastGenericEqualityComparerFromTable<'T> = + HashCompare.EqualityComparerPER<'T>.Comparer : IEqualityComparer<'T> // This is the implementation of HashIdentity.Structural. In most cases this just becomes // FastGenericEqualityComparerFromTable. diff --git a/tests/AheadOfTime/Equality/Equality.fsproj b/tests/AheadOfTime/Equality/Equality.fsproj new file mode 100644 index 00000000000..f5a243c65b9 --- /dev/null +++ b/tests/AheadOfTime/Equality/Equality.fsproj @@ -0,0 +1,35 @@ + + + + Exe + net8.0 + preview + true + + + + true + true + true + true + win-x64 + + + + $(MSBuildThisFileDirectory)../../../artifacts/bin/fsc/Release/net8.0/fsc.dll + $(MSBuildThisFileDirectory)../../../artifacts/bin/fsc/Release/net8.0/fsc.dll + False + True + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/AheadOfTime/Equality/Program.fs b/tests/AheadOfTime/Equality/Program.fs new file mode 100644 index 00000000000..052d2875da3 --- /dev/null +++ b/tests/AheadOfTime/Equality/Program.fs @@ -0,0 +1,77 @@ +open System.Collections.Generic +open NonStructuralComparison + +let failures = HashSet() + +let reportFailure (s: string) = + stderr.Write " NO: " + stderr.WriteLine s + failures.Add s |> ignore + +let test testName x y = + let result = HashIdentity.Structural.Equals(x, y) + if result = false + then + stderr.WriteLine($"\n***** {testName}: Expected: 'true' Result: '{result}' = FAIL\n"); + reportFailure testName + +module BasicTypes = + test "test000" true true + test "test001" 1y 1y + test "test002" 1uy 1uy + test "test003" 1s 1s + test "test004" 1us 1us + test "test005" 1 1 + test "test006" 1u 1u + test "test007" 1L 1L + test "test008" 1UL 1UL + test "test009" (nativeint 1) (nativeint 1) + test "test010" (unativeint 1) (unativeint 1) + test "test011" 'a' 'a' + test "test012" "a" "a" + test "test013" 1m 1m + test "test014" 1.0 1.0 + test "test015" 1.0f 1.0f + +module Arrays = + test "test100" [|1|] [|1|] + test "test101" [|1L|] [|1L|] + test "test102" [|1uy|] [|1uy|] + test "test103" [|box 1|] [|box 1|] + +module Structs = + test "test200" struct (1, 1) struct (1, 1) + test "test201" struct (1, 1, 1) struct (1, 1, 1) + test "test202" struct (1, 1, 1, 1) struct (1, 1, 1, 1) + test "test203" struct (1, 1, 1, 1, 1) struct (1, 1, 1, 1, 1) + test "test204" struct (1, 1, 1, 1, 1, 1) struct (1, 1, 1, 1, 1, 1) + test "test205" struct (1, 1, 1, 1, 1, 1, 1) struct (1, 1, 1, 1, 1, 1, 1) + test "test206" struct (1, 1, 1, 1, 1, 1, 1, 1) struct (1, 1, 1, 1, 1, 1, 1, 1) + +module OptionsAndCo = + open System + + test "test301" (Some 1) (Some 1) + test "test302" (ValueSome 1) (ValueSome 1) + test "test303" (Ok 1) (Ok 1) + test "test304" (Nullable 1) (Nullable 1) + +module Enums = + open System + + type SomeEnum = + | Case0 = 0 + | Case1 = 1 + + test "test401" (enum(1)) (enum(1)) + test "test402" (enum(1)) (enum(1)) + +[] +let main _ = + match failures with + | set when set.Count = 0 -> + stdout.WriteLine "All tests passed" + exit 0 + | _ -> + stdout.WriteLine "Some tests failed" + exit 1 diff --git a/tests/AheadOfTime/Equality/check.ps1 b/tests/AheadOfTime/Equality/check.ps1 new file mode 100644 index 00000000000..7913d24599c --- /dev/null +++ b/tests/AheadOfTime/Equality/check.ps1 @@ -0,0 +1,32 @@ +Write-Host "Publish and Execute: net8.0 - Equality" + +$build_output = dotnet publish -restore -c release -f:net8.0 $(Join-Path $PSScriptRoot Equality.fsproj) + +# Checking that the build succeeded +if ($LASTEXITCODE -ne 0) +{ + Write-Error "Build failed with exit code ${LASTEXITCODE}" + Write-Error "${build_output}" -ErrorAction Stop +} + +$process = Start-Process ` + -FilePath $(Join-Path $PSScriptRoot bin\release\net8.0\win-x64\publish\Equality.exe) ` + -Wait ` + -NoNewWindow ` + -PassThru ` + -RedirectStandardOutput $(Join-Path $PSScriptRoot output.txt) + +$output = Get-Content $(Join-Path $PSScriptRoot output.txt) + +# Checking that it is actually running. +if ($LASTEXITCODE -ne 0) +{ + Write-Error "Test failed with exit code ${LASTEXITCODE}" -ErrorAction Stop +} + +# Checking that the output is as expected. +$expected = "All tests passed" +if ($output -ne $expected) +{ + Write-Error "Test failed with unexpected output:`nExpected:`n`t${expected}`nActual`n`t${output}" -ErrorAction Stop +} diff --git a/tests/AheadOfTime/Trimming/check.cmd b/tests/AheadOfTime/Trimming/check.cmd deleted file mode 100644 index 4eefff011c5..00000000000 --- a/tests/AheadOfTime/Trimming/check.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0check.ps1"""" diff --git a/tests/AheadOfTime/Trimming/check.ps1 b/tests/AheadOfTime/Trimming/check.ps1 index 1dcdf28f8af..7412b21232c 100644 --- a/tests/AheadOfTime/Trimming/check.ps1 +++ b/tests/AheadOfTime/Trimming/check.ps1 @@ -39,7 +39,7 @@ function CheckTrim($root, $tfm, $outputfile, $expected_len) { # error NETSDK1124: Trimming assemblies requires .NET Core 3.0 or higher. # Check net7.0 trimmed assemblies -CheckTrim -root "SelfContained_Trimming_Test" -tfm "net8.0" -outputfile "FSharp.Core.dll" -expected_len 287232 +CheckTrim -root "SelfContained_Trimming_Test" -tfm "net8.0" -outputfile "FSharp.Core.dll" -expected_len 284160 # Check net8.0 trimmed assemblies -CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net8.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 8820736 +CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net8.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 8817152 diff --git a/tests/AheadOfTime/check.cmd b/tests/AheadOfTime/check.cmd new file mode 100644 index 00000000000..a52368e2c6f --- /dev/null +++ b/tests/AheadOfTime/check.cmd @@ -0,0 +1,3 @@ +@echo off +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Trimming\check.ps1"""" +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Equality\check.ps1"""" diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Benchmarks.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Benchmarks.fs index a081437e1f0..cce09739135 100644 --- a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Benchmarks.fs +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Benchmarks.fs @@ -1,11 +1,11 @@ -namespace TaskPerf +namespace MicroPerf open BenchmarkDotNet.Running -module Main = +module Main = [] - let main _ = + let main args = printfn "Running benchmarks..." - let _ = BenchmarkRunner.Run() - 0 + BenchmarkSwitcher.FromAssembly(typeof.Assembly).Run(args) |> ignore + 0 diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Arrays.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Arrays.fs new file mode 100644 index 00000000000..742385d7720 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Arrays.fs @@ -0,0 +1,23 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +type Arrays() = + + let numbers = Array.init 1000 id + + [] + member _.Int32() = + numbers |> Array.countBy (fun n -> [| n % 7 |]) + + [] + member _.Int64() = + numbers |> Array.countBy (fun n -> [| int64 (n % 7) |]) + + [] + member _.Byte() = + numbers |> Array.countBy (fun n -> [| byte (n % 7) |]) + + [] + member _.Obj() = + numbers |> Array.countBy (fun n -> [| box (n % 7) |]) diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/BasicTypes.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/BasicTypes.fs new file mode 100644 index 00000000000..219284fe732 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/BasicTypes.fs @@ -0,0 +1,63 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +[] +type BasicTypes() = + + let bools = Array.init 1000 (fun n -> n % 2 = 0) + let sbytes = Array.init 1000 sbyte + let bytes = Array.init 1000 byte + let int16s = Array.init 1000 int16 + let uint16s = Array.init 1000 uint16 + let int32s = Array.init 1000 id + let uint32s = Array.init 1000 uint32 + let int64s = Array.init 1000 int64 + let uint64s = Array.init 1000 uint64 + let intptrs = Array.init 1000 nativeint + let uintptrs = Array.init 1000 unativeint + let chars = Array.init 1000 char + let strings = Array.init 1000 string + let decimals = Array.init 1000 decimal + + [] + member _.Bool() = bools |> Array.distinct + + [] + member _.SByte() = sbytes |> Array.distinct + + [] + member _.Byte() = bytes |> Array.distinct + + [] + member _.Int16() = int16s |> Array.distinct + + [] + member _.UInt16() = uint16s |> Array.distinct + + [] + member _.Int32() = int32s |> Array.distinct + + [] + member _.UInt32() = uint32s |> Array.distinct + + [] + member _.Int64() = int64s |> Array.distinct + + [] + member _.UInt64() = uint64s |> Array.distinct + + [] + member _.IntPtr() = intptrs |> Array.distinct + + [] + member _.UIntPtr() = uintptrs |> Array.distinct + + [] + member _.Char() = chars |> Array.distinct + + [] + member _.String() = strings |> Array.distinct + + [] + member _.Decimal() = decimals |> Array.distinct diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/FSharpCoreFunctions.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/FSharpCoreFunctions.fs new file mode 100644 index 00000000000..7b26d287ab9 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/FSharpCoreFunctions.fs @@ -0,0 +1,102 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +[] +type SomeStruct = + val A : int + new a = { A = a } + +type FSharpCoreFunctions() = + + let array = Array.init 1000 id + let list = List.init 1000 id + let seq = Seq.init 1000 id + + [] + member _.ArrayCountBy() = + array + |> Array.countBy (fun n -> SomeStruct(n % 7)) + + [] + member _.ArrayGroupBy() = + array + |> Array.groupBy (fun n -> SomeStruct(n % 7)) + + [] + member _.ArrayDistinct() = + array + |> Array.map (fun n -> SomeStruct(n % 7)) + |> Array.distinct + + [] + member _.ArrayDistinctBy() = + array + |> Array.distinctBy (fun n -> SomeStruct(n % 7)) + + [] + member _.ArrayExcept() = + array + |> Array.map SomeStruct + |> Array.except ([| SomeStruct 42 |]) + + [] + member _.ListCountBy() = + list + |> List.countBy (fun n -> SomeStruct(n % 7)) + + [] + member _.ListGroupBy() = + list + |> List.groupBy (fun n -> SomeStruct(n % 7)) + + [] + member _.ListDistinct() = + list + |> List.map (fun n -> SomeStruct(n % 7)) + |> List.distinct + + [] + member _.ListDistinctBy() = + list + |> List.distinctBy (fun n -> SomeStruct(n % 7)) + + [] + member _.ListExcept() = + List.init 1000 id + |> List.map SomeStruct + |> List.except ([| SomeStruct 42 |]) + + [] + member _.SeqCountBy() = + seq + |> Seq.countBy (fun n -> SomeStruct(n % 7)) + |> Seq.last + + [] + member _.SeqGroupBy() = + seq + |> Seq.groupBy (fun n -> SomeStruct(n % 7)) + |> Seq.last + + [] + member _.SeqDistinct() = + seq + |> Seq.map (fun n -> SomeStruct(n % 7)) + |> Seq.distinct + |> Seq.last + + [] + member _.SeqDistinctBy() = + seq + |> Seq.distinctBy (fun n -> SomeStruct(n % 7)) + |> Seq.last + + [] + member _.SeqExcept() = + seq + |> Seq.map SomeStruct + |> Seq.except ([| SomeStruct 42 |]) + |> Seq.last + + diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Floats.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Floats.fs new file mode 100644 index 00000000000..22234e0da5e --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Floats.fs @@ -0,0 +1,20 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +[] +type Floats() = + + let numbers = Array.init 1000 (fun id -> id % 7) + + [] + member _.FloatER() = numbers |> Array.groupBy float + + [] + member _.Float32ER() = numbers |> Array.groupBy float32 + + [] + member _.FloatPER() = numbers |> Array.Parallel.groupBy float + + [] + member _.Float32PER() = numbers |> Array.Parallel.groupBy float32 \ No newline at end of file diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Misc.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Misc.fs new file mode 100644 index 00000000000..fa7c445c620 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Misc.fs @@ -0,0 +1,78 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +open System + +[] +type BigStruct = + val A : int64 + val B : int64 + new (a, b) = { A = a; B = b } + +[] +type Container<'a> = + val Item : 'a + new i = { Item = i } + +type RandomRecord = { + Field1 : int + Field2 : string + Field3 : byte +} + +[] +type RandomRecordStruct = { + Field1S : int + Field2S : string + Field3S : byte +} + +type RandomGeneric<'a> = RandomGeneric of 'a * 'a + +[] +type Misc() = + + let createBigStruct() = + let n = Random().NextInt64() + Container (BigStruct (n, n)) + + [] + member _.BigStruct() = + let set = Set.empty + for _ = 0 to 200000 do + set.Add (createBigStruct ()) |> ignore + + [] + member _.Record() = + let array = Array.init 1000 id + array |> Array.countBy (fun n -> { + Field1 = n + Field2 = string n + Field3 = byte n + }) + + [] + member _.RecordStruct() = + let array = Array.init 1000 id + array |> Array.countBy (fun n -> { + Field1S = n + Field2S = string n + Field3S = byte n + }) + + // BDN can't work with F# anon records yet + // https://github.com/dotnet/BenchmarkDotNet/issues/2530 + // [] + member _.AnonymousRecord() = + let array = Array.init 1000 id + array |> Array.countBy (fun n -> {| + Field1A = n + Field2A = string n + Field3A = byte n + |}) + + [] + member _.GenericUnion() = + let array = Array.init 1000 id + array |> Array.countBy (fun n -> RandomGeneric(n, n)) diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/OptionsAndCo.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/OptionsAndCo.fs new file mode 100644 index 00000000000..a31f53e3367 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/OptionsAndCo.fs @@ -0,0 +1,45 @@ +namespace Equality + +open System +open BenchmarkDotNet.Attributes + +[] +type OptionsAndCo() = + + let numbers = Array.init 1000 id + + let createOption x = + match x with + | x when x % 2 = 0 -> Some x + | _ -> None + + let createValueOption x = + match x with + | x when x % 2 = 0 -> ValueSome x + | _ -> ValueNone + + let createResult x = + match x with + | x when x % 2 = 0 -> Ok x + | x -> Error x + + let createNullable x = + match x with + | x when x % 2 = 0 -> Nullable x + | _ -> Nullable 42 + + [] + member _.Option() = + numbers |> Array.countBy createOption + + [] + member _.ValueOption() = + numbers |> Array.countBy createValueOption + + [] + member _.Result() = + numbers |> Array.countBy createResult + + [] + member _.Nullable() = + numbers |> Array.countBy createNullable diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Structs.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Structs.fs new file mode 100644 index 00000000000..51f6f1689aa --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Structs.fs @@ -0,0 +1,39 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +[] +type Structs() = + + let numbers = Array.init 1000 id + + let createStruct3 (x: int) = struct (x, x, x) + let createStruct4 (x: int) = struct (x, x, x, x) + let createStruct5 (x: int) = struct (x, x, x, x, x) + let createStruct6 (x: int) = struct (x, x, x, x, x, x) + let createStruct7 (x: int) = struct (x, x, x, x, x, x, x) + let createStruct8 (x: int) = struct (x, x, x, x, x, x, x, x) + + [] + member _.Struct3() = + numbers |> Array.countBy (fun n -> createStruct3 n) + + [] + member _.Struct4() = + numbers |> Array.countBy (fun n -> createStruct4 n) + + [] + member _.Struct5() = + numbers |> Array.countBy (fun n -> createStruct5 n) + + [] + member _.Struct6() = + numbers |> Array.countBy (fun n -> createStruct6 n) + + [] + member _.Struct7() = + numbers |> Array.countBy (fun n -> createStruct7 n) + + [] + member _.Struct8() = + numbers |> Array.countBy (fun n -> createStruct8 n) \ No newline at end of file diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Tuples.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Tuples.fs new file mode 100644 index 00000000000..49b0dcd428c --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/Tuples.fs @@ -0,0 +1,67 @@ +namespace Equality + +open BenchmarkDotNet.Attributes + +type SmallNonGenericTuple = SmallNonGenericTuple of int * string + +type SmallGenericTuple<'a> = SmallGenericTuple of int * 'a + +type BigNonGenericTuple = BigNonGenericTuple of int * string * byte * int * string * byte + +type BigGenericTuple<'a> = BigGenericTuple of int * 'a * byte * int * 'a * byte + +[] +type SmallNonGenericTupleStruct = SmallNonGenericTupleStruct of int * string + +[] +type SmallGenericTupleStruct<'a> = SmallGenericTupleStruct of int * 'a + +[] +type BigNonGenericTupleStruct = BigNonGenericTupleStruct of int * string * byte * int * string * byte + +[] +type BigGenericTupleStruct<'a> = BigGenericTupleStruct of int * 'a * byte * int * 'a * byte + +type ReferenceTuples() = + + let numbers = Array.init 1000 id + + [] + member _.SmallNonGenericTuple() = + numbers + |> Array.countBy (fun n -> SmallNonGenericTuple(n, string n)) + + [] + member _.SmallGenericTuple() = + numbers + |> Array.countBy (fun n -> SmallGenericTuple(n, string n)) + + [] + member _.BigNonGenericTuple() = + numbers + |> Array.countBy (fun n -> BigNonGenericTuple(n, string n, byte n, n, string n, byte n)) + + [] + member _.BigGenericTuple() = + numbers + |> Array.countBy (fun n -> BigGenericTuple(n, string n, byte n, n, string n, byte n)) + + [] + member _.SmallNonGenericTupleStruct() = + numbers + |> Array.countBy (fun n -> SmallNonGenericTupleStruct(n, string n)) + + [] + member _.SmallGenericTupleStruct() = + numbers + |> Array.countBy (fun n -> SmallGenericTupleStruct(n, string n)) + + [] + member _.BigNonGenericTupleStruct() = + numbers + |> Array.countBy (fun n -> BigNonGenericTupleStruct(n, string n, byte n, n, string n, byte n)) + + [] + member _.BigGenericTupleStruct() = + numbers + |> Array.countBy (fun n -> BigGenericTupleStruct(n, string n, byte n, n, string n, byte n)) diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/ValueTypes.fs b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/ValueTypes.fs new file mode 100644 index 00000000000..5e48a1c6a91 --- /dev/null +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/Equality/ValueTypes.fs @@ -0,0 +1,39 @@ +namespace Equality + +open System +open BenchmarkDotNet.Attributes + +type SomeEnum = + | Case0 = 0 + | Case1 = 1 + | Case2 = 2 + +[] +type ValueTypes() = + + let numbers = Array.init 1000 id + let now = DateTimeOffset.Now + + let createFSharpStruct (x: int) = + struct (x % 7, x % 7) + + let createFSharpEnum (x: int) = + enum(x % 3) + + let createCSharpStruct (x: int) = + now.AddMinutes x + + let createCSharpEnum (x: int) = + enum(x % 7) + + [] + member _.FSharpStruct() = numbers |> Array.countBy createFSharpStruct + + [] + member _.FSharpEnum() = numbers |> Array.countBy createFSharpEnum + + [] + member _.CSharpStruct() = numbers |> Array.countBy createCSharpStruct + + [] + member _.CSharpEnum() = numbers |> Array.countBy createCSharpEnum diff --git a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/MicroPerf.fsproj b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/MicroPerf.fsproj index c96d342298f..fc36640d7e6 100644 --- a/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/MicroPerf.fsproj +++ b/tests/benchmarks/CompiledCodeBenchmarks/MicroPerf/MicroPerf.fsproj @@ -1,4 +1,4 @@ - + $(FSharpNetCoreProductTargetFramework) Exe @@ -11,6 +11,15 @@ $(OtherFlags) --define:PREVIEW + + + + + + + + + From ce1a1b95d32e4dfb65ee9e78c43f766eedee2afc Mon Sep 17 00:00:00 2001 From: Edgar Gonzalez Date: Tue, 5 Mar 2024 09:44:57 +0000 Subject: [PATCH 3/7] Update union-case declaration AttributeTargets.Property (#16807) --- src/Compiler/Checking/CheckDeclarations.fs | 25 +++++++++++++------ .../AttributeTargetsIsProperty.fs | 10 ++++++++ .../AttributeUsage/AttributeUsage.fs | 15 +++++++++++ 3 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/AttributeUsage/AttributeTargetsIsProperty.fs diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index c82a5edf6f3..457a3489576 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -511,14 +511,6 @@ module TcRecdUnionAndEnumDeclarations = let TcUnionCaseDecl (cenv: cenv) env parent thisTy thisTyInst tpenv hasRQAAttribute (SynUnionCase(Attributes synAttrs, SynIdent(id, _), args, xmldoc, vis, m, _)) = let g = cenv.g - let attrs = - // The attributes of a union case decl get attached to the generated "static factory" method - // Enforce that the union-cases can only be targeted by attributes with AttributeTargets.Method - if g.langVersion.SupportsFeature(LanguageFeature.EnforceAttributeTargetsUnionCaseDeclarations) then - TcAttributes cenv env AttributeTargets.Method synAttrs - else - TcAttributes cenv env AttributeTargets.UnionCaseDecl synAttrs - let vis, _ = ComputeAccessAndCompPath g env None m vis None parent let vis = CombineReprAccess parent vis @@ -571,6 +563,23 @@ module TcRecdUnionAndEnumDeclarations = let checkXmlDocs = cenv.diagnosticOptions.CheckXmlDocs let xmlDoc = xmldoc.ToXmlDoc(checkXmlDocs, Some names) + let attrs = + (* + The attributes of a union case decl get attached to the generated "static factory" method. + Enforce union-cases AttributeTargets: + - AttributeTargets.Method + type SomeUnion = + | Case1 of int // Compiles down to a static method + - AttributeTargets.Property + type SomeUnion = + | Case1 // Compiles down to a static property + *) + if g.langVersion.SupportsFeature(LanguageFeature.EnforceAttributeTargetsUnionCaseDeclarations) then + let target = if rfields.IsEmpty then AttributeTargets.Property else AttributeTargets.Method + TcAttributes cenv env target synAttrs + else + TcAttributes cenv env AttributeTargets.UnionCaseDecl synAttrs + Construct.NewUnionCase id rfields recordTy attrs xmlDoc vis let TcUnionCaseDecls (cenv: cenv) env (parent: ParentRef) (thisTy: TType) (thisTyInst: TypeInst) hasRQAAttribute tpenv unionCases = diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/AttributeUsage/AttributeTargetsIsProperty.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/AttributeUsage/AttributeTargetsIsProperty.fs new file mode 100644 index 00000000000..42c94f1e474 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/AttributeUsage/AttributeTargetsIsProperty.fs @@ -0,0 +1,10 @@ + +open System + +[] +type PropertyLevelAttribute() = + inherit Attribute() + +type U = + | [] A + | [] B diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/AttributeUsage/AttributeUsage.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/AttributeUsage/AttributeUsage.fs index 5d930ea1e14..0bdc9306cb7 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/AttributeUsage/AttributeUsage.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/CustomAttributes/AttributeUsage/AttributeUsage.fs @@ -71,6 +71,21 @@ module CustomAttributes_AttributeUsage = |> withLangVersionPreview |> verifyCompileAndRun |> shouldSucceed + + // SOURCE=AttributeTargetsIsProperty.fs # AttributeTargetsIsProperty.fs + [] + let ``AttributeTargetsIsProperty_fs`` compilation = + compilation + |> verifyCompile + |> shouldSucceed + + // SOURCE=AttributeTargetsIsProperty.fs # AttributeTargetsIsProperty.fs + [] + let ``AttributeTargetsIsProperty_fs preview`` compilation = + compilation + |> withLangVersionPreview + |> verifyCompile + |> shouldSucceed // SOURCE=ConditionalAttribute.fs # ConditionalAttribute.fs [] From d73bb4b6118f4d7fddb2d144ef50a67be5088cc3 Mon Sep 17 00:00:00 2001 From: "Kevin Ransom (msft)" Date: Tue, 5 Mar 2024 04:55:05 -0800 Subject: [PATCH 4/7] Fix 13926 - Disallow using base to invoke an abstract base method. (#16773) * Fix #13926 - BadImageFormatException : Bad IL format when using base * fantomas * Update src/Compiler/AbstractIL/il.fs Co-authored-by: Tomas Grosup * Update src/Compiler/FSharp.Compiler.Service.fsproj Co-authored-by: Tomas Grosup * Update 8.0.md --------- Co-authored-by: Tomas Grosup Co-authored-by: Petr --- .../.FSharp.Compiler.Service/8.0.300.md | 1 + docs/release-notes/.Language/8.0.md | 8 ++- src/Compiler/AbstractIL/il.fs | 6 +- src/Compiler/Checking/PostInferenceChecks.fs | 12 ++-- src/Compiler/FSharp.Compiler.Service.fsproj | 20 +++--- .../ImportDeclarations/ImportDeclarations.fs | 71 +++++++++++++++++++ .../ErrorMessages/SuggestionsTests.fs | 6 +- .../FSharp.Compiler.ComponentTests.fsproj | 19 +++-- 8 files changed, 108 insertions(+), 35 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md index 83a1b23aa9f..35cb6319ddc 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md @@ -15,6 +15,7 @@ * Fix release inline optimization, which leads to MethodAccessException if used with `assembly:InternalsVisibleTo`` attribute. ([Issue #16105](https://github.com/dotnet/fsharp/issues/16105), ([PR #16737](https://github.com/dotnet/fsharp/pull/16737)) * Enforce AttributeTargets on let values and functions. ([PR #16692](https://github.com/dotnet/fsharp/pull/16692)) * Enforce AttributeTargets on union case declarations. ([PR #16764](https://github.com/dotnet/fsharp/pull/16764)) +* Disallow using base to invoke an abstract base method. ([Issue #13926](https://github.com/dotnet/fsharp/issues/13926), [PR #16773](https://github.com/dotnet/fsharp/pull/16773)) ### Added diff --git a/docs/release-notes/.Language/8.0.md b/docs/release-notes/.Language/8.0.md index ad829f0a6b9..1e2dc00f2cd 100644 --- a/docs/release-notes/.Language/8.0.md +++ b/docs/release-notes/.Language/8.0.md @@ -1,3 +1,7 @@ -### Added +### Fixed -* `while!` ([Language suggestion #1038](https://github.com/fsharp/fslang-suggestions/issues/1038), [PR #14238](https://github.com/dotnet/fsharp/pull/14238)) \ No newline at end of file +* Disallow using base to invoke an abstract base method ([Issue #13926](https://github.com/dotnet/fsharp/issues/13926), [PR #16773](https://github.com/dotnet/fsharp/pull/16773)) + +### Added + +* `while!` ([Language suggestion #1038](https://github.com/fsharp/fslang-suggestions/issues/1038), [PR #14238](https://github.com/dotnet/fsharp/pull/14238)) diff --git a/src/Compiler/AbstractIL/il.fs b/src/Compiler/AbstractIL/il.fs index 7f303178553..fecefad1434 100644 --- a/src/Compiler/AbstractIL/il.fs +++ b/src/Compiler/AbstractIL/il.fs @@ -5640,10 +5640,8 @@ let resolveILMethodRefWithRescope r (td: ILTypeDef) (mref: ILMethodRef) = possibles |> List.filter (fun md -> mref.CallingConv = md.CallingConv - && - // REVIEW: this uses equality on ILType. For CMOD_OPTIONAL this is not going to be correct - (md.Parameters, argTypes) - ||> List.lengthsEqAndForall2 (fun p1 p2 -> r p1.Type = p2) + && (md.Parameters, argTypes) + ||> List.lengthsEqAndForall2 (fun p1 p2 -> r p1.Type = p2) && // REVIEW: this uses equality on ILType. For CMOD_OPTIONAL this is not going to be correct r md.Return.Type = retType) diff --git a/src/Compiler/Checking/PostInferenceChecks.fs b/src/Compiler/Checking/PostInferenceChecks.fs index e15d9e9463f..852be12aff0 100644 --- a/src/Compiler/Checking/PostInferenceChecks.fs +++ b/src/Compiler/Checking/PostInferenceChecks.fs @@ -1307,14 +1307,14 @@ and CheckILBaseCall cenv env (ilMethRef, enclTypeInst, methInst, retTypes, tyarg match tryTcrefOfAppTy g baseVal.Type with | ValueSome tcref when tcref.IsILTycon -> try - // This is awkward - we have to explicitly re-resolve back to the IL metadata to determine if the method is abstract. - // We believe this may be fragile in some situations, since we are using the Abstract IL code to compare - // type equality, and it would be much better to remove any F# dependency on that implementation of IL type - // equality. It would be better to make this check in tc.fs when we have the Abstract IL metadata for the method to hand. - let mdef = resolveILMethodRef tcref.ILTyconRawMetadata ilMethRef + let mdef = + match tcref.ILTyconInfo with + | TILObjectReprData(scoref, _, _) -> + resolveILMethodRefWithRescope (rescopeILType scoref) tcref.ILTyconRawMetadata ilMethRef + if mdef.IsAbstract then errorR(Error(FSComp.SR.tcCannotCallAbstractBaseMember(mdef.Name), m)) - with _ -> () // defensive coding + with _ -> () | _ -> () CheckTypeInstNoByrefs cenv env m tyargs diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj index e1fe24ea421..5c11a4eebbf 100644 --- a/src/Compiler/FSharp.Compiler.Service.fsproj +++ b/src/Compiler/FSharp.Compiler.Service.fsproj @@ -76,8 +76,8 @@ - - + + @@ -365,8 +365,8 @@ - - + + @@ -504,12 +504,12 @@ - - - - - - + + + + + + diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/ImportDeclarations/ImportDeclarations.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/ImportDeclarations/ImportDeclarations.fs index 1f93d4b3bab..851746df35c 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/ImportDeclarations/ImportDeclarations.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/ImportDeclarations/ImportDeclarations.fs @@ -107,4 +107,75 @@ module ImportDeclarations = |> verifyCompileAndRun |> shouldSucceed + [] + let ImplementImportedAbstractBaseMethodsFailsIfUsed ()= + FSharp """ +module Testing +open System.Text.Json +open System.Text.Json.Serialization + +type StringTrimJsonSerializer(o: JsonSerializerOptions) = + inherit JsonConverter() + + override this.Read(reader, _, _) = + match reader.TokenType with + | JsonTokenType.String -> reader.GetString().Trim() + | _ -> JsonException("Type is not a string") |> raise + + override this.Write(writer, objectToWrite, options) = base.Write(writer, objectToWrite, options) + +type SomeType = { AField: string } + +let serialize item = + let options = JsonSerializerOptions() + StringTrimJsonSerializer options |> options.Converters.Add + JsonSerializer.Serialize(item, options) + +[] +let main _ = + { AField = "a" } + |> serialize + |> ignore + 0""" + |> verifyCompile + |> shouldFail + |> withDiagnostics [ + (Error 1201, Line 15, Col 59, Line 15, Col 101, "Cannot call an abstract base member: 'Write'") + ] + + + [] + let ImplementImportedAbstractBaseMethodsFailsIfNotUsed ()= + FSharp """ +module Testing + +open System.Text.Json +open System.Text.Json.Serialization + +type StringTrimJsonSerializer(o: JsonSerializerOptions) = + inherit JsonConverter() + override this.Read(reader, _, _) = + match reader.TokenType with + | JsonTokenType.String -> reader.GetString().Trim() + | _ -> JsonException("Type is not a string") |> raise + override this.Write(writer, objectToWrite, options) = base.Write(writer, objectToWrite, options) + +type SomeType = { AField: int } + +let serialize item = + let options = JsonSerializerOptions() + StringTrimJsonSerializer options |> options.Converters.Add + JsonSerializer.Serialize(item, options) + +[] +let main _ = + { AField = 1 } + |> serialize + |>ignore + 0""" + |> verifyCompile + |> shouldFail + |> withDiagnostics [ + (Error 1201, Line 13, Col 59, Line 13, Col 101, "Cannot call an abstract base member: 'Write'") + ] diff --git a/tests/FSharp.Compiler.ComponentTests/ErrorMessages/SuggestionsTests.fs b/tests/FSharp.Compiler.ComponentTests/ErrorMessages/SuggestionsTests.fs index 50837863d5a..76bf2d3e88c 100644 --- a/tests/FSharp.Compiler.ComponentTests/ErrorMessages/SuggestionsTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/ErrorMessages/SuggestionsTests.fs @@ -139,12 +139,12 @@ open Collectons [] let ``Suggest Namespaces`` () = FSharp """ -open System.Collectons +open System.Lema """ |> typecheck |> shouldFail - |> withSingleDiagnostic (Error 39, Line 2, Col 13, Line 2, Col 23, - "The namespace 'Collectons' is not defined.") + |> withSingleDiagnostic (Error 39, Line 2, Col 13, Line 2, Col 17, + "The namespace 'Lema' is not defined.") [] let ``Suggest Record Labels`` () = diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 38937398473..e1704555421 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -19,7 +19,7 @@ $(DefineConstants);DEBUG true - true + true @@ -228,9 +228,9 @@ - - - + + + @@ -280,7 +280,7 @@ - + @@ -313,14 +313,12 @@ - - %(RelativeDir)TestSource\%(Filename)%(Extension) - - - + + %(RelativeDir)TestSource\%(Filename)%(Extension) + %(RelativeDir)\BaseLine\%(Filename)%(Extension) @@ -331,6 +329,7 @@ + From 9b197108cad7b8aec14e8559390a549fc67bb51f Mon Sep 17 00:00:00 2001 From: Vlad Zarytovskii Date: Tue, 5 Mar 2024 17:10:34 +0100 Subject: [PATCH 5/7] Fix plain dotnet build (#16814) --- src/FSharp.Core/FSharp.Core.fsproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharp.Core/FSharp.Core.fsproj b/src/FSharp.Core/FSharp.Core.fsproj index c2ad1d456a3..0f9fe0fb624 100644 --- a/src/FSharp.Core/FSharp.Core.fsproj +++ b/src/FSharp.Core/FSharp.Core.fsproj @@ -26,7 +26,7 @@ $(OtherFlags) --nowarn:3513 $(OtherFlags) --compiling-fslib --compiling-fslib-40 --maxerrors:100 --extraoptimizationloops:1 - $(OtherFlags) --realsig- + $(OtherFlags) --realsig- true true From e78bacb0816f385402e1f24d3c9e4c0c33c09010 Mon Sep 17 00:00:00 2001 From: Viktor Hofer Date: Wed, 6 Mar 2024 05:57:18 +0100 Subject: [PATCH 6/7] Condition source-build only stuff on source-build (#16815) * Condition source-build only stuff on source-build * Update SourceBuild.props --- eng/SourceBuild.props | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/eng/SourceBuild.props b/eng/SourceBuild.props index 550215bd643..3bfd9f80fc4 100644 --- a/eng/SourceBuild.props +++ b/eng/SourceBuild.props @@ -11,7 +11,9 @@ The build script passes in the full path of the sln to build. This must be overridden in order to build the cloned source in the inner build. --> - + $(InnerBuildArgs) /p:Projects="$(InnerSourceBuildRepoRoot)\Microsoft.FSharp.Compiler.sln" @@ -23,7 +25,8 @@ --> + BeforeTargets="RunInnerSourceBuildCommand" + Condition="'$(ArcadeBuildFromSource)' == 'true' or '$(DotNetBuildSourceOnly)' == 'true'"> --tfm $(SourceBuildBootstrapTfm) From e335646aa7714cbd8ba5eda8e16097bb04030b42 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 06:05:26 +0000 Subject: [PATCH 7/7] Update dependencies from https://github.com/dotnet/source-build-reference-packages build 20240304.2 (#16810) Microsoft.SourceBuild.Intermediate.source-build-reference-packages From Version 9.0.0-alpha.1.24127.3 -> To Version 9.0.0-alpha.1.24154.2 Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index e683f21d846..41c7f6365b1 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,9 +1,9 @@ - + https://github.com/dotnet/source-build-reference-packages - d1c092f24a18f5ed76631fc6c865f706aca5d90f + 936b4a4b4b8a74b65098983660c5814fb4afee15