Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFCs FS-1050, FS-1051, FS-1052 discussion (Span, IsByRefLike, IsReadOnly) #287

Closed
dsyme opened this issue May 12, 2018 · 41 comments
Closed
Milestone

Comments

@dsyme
Copy link
Contributor

dsyme commented May 12, 2018

Discussion thread for (RFC FS-1053](https://github.com/fsharp/fslang-design/blob/master/FSharp-4.5/FS-1053-span.md) covering all of

  • voidptr
  • IsReadOnly
  • ByRefLike
  • "return byrefs"
  • "byref this extension members"
  • Span
  • Memory
@zpodlovics
Copy link

This comment was moved from the pull request dotnet/fsharp#4888

Please note ref local reassignment is now allowed in C#:

dotnet/csharplang#933
https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.3/ref-local-reassignment.md
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-10-02.md

An another ref feature is conditional references, this seems possible to express already (at least in compiled code) unless it used with FSI:
https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.2/conditional-ref.md

FSI session:

let arr1 = [|1;2|]
let arr2 = [|3;4|]
let x = if true then &arr1.[0] else &arr2.[0]
FS0431: A byref typed value would be stored here. Top-level let-bound byref values are not permitted.

@zpodlovics
Copy link

zpodlovics commented May 12, 2018

Well, F# struct are assumed as readonly, but it's not really. At least the whole struct could be replaced from a method (I saw an evil example earlier in a C# as this was the reason why readonly structs are introduced). I guess similar replacement is possible with struct tuples struct records any other struct types too. Please note readonly and immutable are not the same, you can have a readonly reference to a mutable thing.

open System
open System.Runtime.InteropServices
open System.Runtime.CompilerServices

[<Struct>]
type S = 
    val Value: int
    new(v) = { Value=v }
    member this.Replace(newValue: S) =
        this <- newValue

[<IsReadOnly;Struct>]
type ROS = 
    val Value: int
    new(v) = { Value=v }
    member this.Replace(newValue: ROS) =
        this <- newValue

let test() = 
    let s1 = S(1)
    s1.Replace(S(2))
    printfn "S1: %d" s1.Value

    let ros1 = ROS(1)
    ros1.Replace(ROS(2))
    printfn "ROS1: %d" ros1.Value

let test2() = 
    let mutable ms1 = S(1)
    ms1.Replace(S(2))
    printfn "mutable S1: %d" ms1.Value

    let mutable mros1 = ROS(1)
    mros1.Replace(ROS(2))
    printfn "mutable ROS1: %d" mros1.Value

test()
test2()

However introducing the readonly structs will fix this, the most important thing for this is to prevent struct copy for readonly/immutable values by allowing passing readonly references on readonly/immutable things (CanTakeAddressOfImmutableVal)

Would it be possible to have different type (or type alias or constraint) for readonly references? How the function method signatures will (or should?) reflect these type restrictions?

@dsyme
Copy link
Contributor Author

dsyme commented May 13, 2018

@zpodlovics Thanks for helping with this, it's really useful having someone providing evil examples:

The main initial aim is to allow effective consumption of APIs using Span, Memory and ref-returns. The PR is quite close to achieving that.

The main niggles left to sort out are indeed about readonly references, also ref this extension members.

There is also a set of questions about how many of the constraints/conditions we check for new declarations of ref structs and explicit uses of IsReadOnly etc.

You're correct that F# structs are not always readonly/immutable.

  1. You can declare val mutable
  2. You can do this <- v wholesale replacement of the contents of the struct.
  3. You can hide mutable fields behind signatures

For (2) I'm not sure if we will be able to make a change to make the this parameter be readonly, that would be a breaking change. We could start to emit a warning on wholesale replacement. I'll note it in the RFC.

Currently in the prototype, adding IsReadOnly to a struct is unchecked - it is an assertion that the struct can be regarded as immutable, and thus defensive copies do not need to be taken when calling operations. For your examples the attribute doesn't make any difference (with or without the PR) as the compiler has already assumed the structs to be immutable (since it doesn't know about this <- x "wholesale replacement"). With the prototype PR, a mutable struct declared ReadOnly will be treated as if it is immutable, and non defensive copies will be taken.

unless it used with FSI:

Just to mention that that limitation is just with top-level bindings, you can do this in a function:

let f(arr1: int[], arr2: int[])  =
    let x = if true then &arr1.[0] else &arr2.[0]
    x <- 1
> let f(arr1: int[], arr2: int[])  =
-     let x = if true then &arr1.[0] else &arr2.[0]
-     x <- 1
- ;;
val f : arr1:int [] * arr2:int [] -> unit

@zpodlovics
Copy link

zpodlovics commented May 15, 2018

@dsyme The byref capability design looks really good, especially with the inref outref aliases! Immediately reminded me to Pony capablity design [1] [2]. In fact it's soo good, that it may worth adopting some "ideas" from there. Usually the more restriction the type have the more optimization opportunity the compiler will have.

It would be really good to have some kind of constraints to represent IsReadOnly and ByRefLike (and 'blittable' types - similar to 'unmanaged' constraints but it's more stricter than that [3] [4] as it does not allow reference fields), even if the compiler optimizations not fully there yet. However the user codes could immediately profit from these type restrictions. These constraint could allow the developers to write tight generic code without using reflection "magic" to checks whether if these constraints holds. Custom constraints would be also good but cannot exploited by the compiler to provide even more performant code.

Constraints proposal related to Span<'T>:

readonly -> IsReadOnly attribute
byreflike -> ByRefLike attribute
blittable -> unmanaged + no managed refs and other restrictions from [3] to make sure it has the same in memory raw representation everywhere
packed -> have Pack=1 attribute (should not combined with blittable as native interop/representations may have holes/reserved fields)
[<Struct>]
[<StructLayout(LayoutKind.Sequential, Pack=1, Size=128)
[<IsReadOnly>]
type PaddedHistogram4 = 
  val Count1: int64
  val Count2: int64
  val Count3: int64
  val Count4: int64

let msg<'T where 'T: readonly and 'T: blittable and 'T: packed and 'T: byreflike > (v: inbyref<'T>, host: RDMACloudHost) =

The readonly constraint required to make sure the value raw "internals" will change during the transport call, the blittable and packed constraint require to make sure it represented in the same was on all host (within the same endiannes, no bigendian/littleendian conversion) and the byref like attribute required to make sure there is no heap allocation will happen with the type.

Question: should we add the IsReadOnly attribute automatically when an F# struct is inferred to be readonly, or should the programmer need to make it explicit?

I would be suprised if anybody use the this replacement I showed in the 'evil' example.

Inferring readonly could introduce snowflake/wavefront like changes in the code. If the change is non-readonly -> readonly (promote to readonly), it's good. However when readonly demoted to non-readonly, would be a disaster. Small change in the code (assuming proper functional type composition or in an external library), and suddenly most of the generated code path runs on a less/non-optimized code path with lot's of defensive struct copy. This would be a nightmare for developing performance critical code. In performance critical code predictability is everything. Yes, I can and I will annotate my types with readonly attribute, but what about the F# library code and existing library code out there that I have no way to control? This is why the readonly, byreflike, blittable, packed constraints would be important - if something changes in other libraries at least I would be immediately notified on compilation if something will go wrong.

If it possible/feasible to restrict - the assumed readonly/immutable F# types should really be readonly/immutable added explictly with these types. eg.: struct tupes, struct record types.

Represents a sequential struct layout (QUESTION: should we emit Sequential?)

It is critical to have byref struct with the some control about it as a plain struct type. For example I can use sequential layout with Pack=1 to represent basic blittable types / packets / messages. I can use the Explicit layout with Pack=1 to represent the (overlapped) union of these types / packets / messages. In order to avoid cache false sharing I can use sequential/explicit fields combined with Size attribute. So if possible I would be really happy to use the byref struct layout control and implement everything in pure F# (instead of using C# code for some performance critical code that F# cannot express...).

They can not implement interfaces (QUESTION: should we check this)

I would be more happy to have span support and have this check in a later F# version, than delaying the span support for later version.

Can implement ToString but you can't call it

I would be more happy to have span support and have this check in a later F# version, than delaying the span support for later version.

[1] https://blog.acolyer.org/2016/02/17/deny-capabilities/
[2] https://tutorial.ponylang.org/capabilities/
[3] https://docs.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types
[4] dotnet/csharplang#187

@zpodlovics
Copy link

zpodlovics commented May 16, 2018

+1 for allowing support for performant extension members in F#, because extension members already used in several places eg.: SRTP

The second attribute syntax looks a bit more familiar [1] and [<Extension>] already used in F# for creating C# compatible extension methods, however I would be happy to use any syntax that's available.

I guess for C# compatible extension methods F# may (if it represented in the same way in C#, I did not checked this) already have a way to express the ref struct extension members:

[<Extension>]
type ExtraCSharpStyleExtensionMethodsInFSharp() =
    [<Extension>]
    static member inline Sum(x: inref<MyStruct>) = x.Field1 + x.Field2

But I do not know the real-world usability of these F# created C# compatible extension members in F# code and how well it works in type inference, SRTPs, etc. Therefore native F# extension methods may still preferred for this purpose.

[1] https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/type-extensions

@dsyme
Copy link
Contributor Author

dsyme commented May 16, 2018

@dsyme The byref capability design looks really good, especially with the inref outref aliases!

Thanks, yes, I'm very happy with how this turned out. It's tempting to try to generalise the mechanism to sets of attributes on tags on types, (ala units of measure but with set semantics instead of kgm = mkg abelian group semantics).

Immediately reminded me to Pony capablity design [1] [2].

Yes, adding capabilities like this is common in the ML/OCaml tradition. And I work with the wonderful @sylvanc, the Pony designer, who is at Microsoft Research Cambridge

In fact it's soo good, that it may worth adopting some "ideas" from there.

The ideas in Pony dealing with concurrency and capabilities are great but would have to be slightly separate or additional to this. I'll talk to Sylvan about it to check we're not doing anything silly and we're not closing the door to adding further capabilities later.

It would be really good to have some kind of constraints to represent IsReadOnly and ByRefLike ...

ByRefLike types aren't usable as generic type parameters - not even for inline code.

I'm not sure what to do about constraints for other things. We'll first have to consume the C# constraints being added recently.

Represents a sequential struct layout (QUESTION: should we emit Sequential?)

It is critical to have byref struct with the some control about it as a plain struct type.

The above thing about "Sequential" was taken from some C# design note I read. But we just have to do whatever C# does here.

Can implement ToString but you can't call it

This is taken from C# design note as well. We don't have any special compiler rule either way

@zpodlovics
Copy link

Update: moved from fsharp/fslang-suggestions#648 (comment)

How the overload resolution interop supposed to work between F# -> C# and C# -> F#? Original Source:
https://blogs.msdn.microsoft.com/mazhou/2018/01/08/c-7-series-part-8-in-parameters/

Example:

using System;

namespace inattributeoverload
{
    public class C
    {
        public void A(int a)
        {
            Console.WriteLine("int a");
        }

        public void A(in int a)
        {
            Console.WriteLine("in int a");
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            C c = new C();
            int x = 1;
            c.A(in x); // A(in int)
            //c.A(x); // A(int)            
        }
    }
}

Right now when the second c.A(x) enabled the C# compiler will complain (with <LangVersion>latest</LangVersion>):

Program.cs(28,15): error CS0121: The call is ambiguous between the following methods or properties: 'C.A(int)' and 'C.A(in int)' [/tmp/inattributeoverload/inattributeoverload.csproj]

The build failed. Please fix the build errors and run again.

And I have noticed that the generated IL callwith will pass the x argument with an [<Out>] attribute.

    .method private static hidebysig 
           default void Main (string[] args)  cil managed 
    {
        // Method begins at RVA 0x2078
	.entrypoint
	// Code size 30 (0x1e)
	.maxstack 2
	.locals init (
		class inattributeoverload.C	V_0,
		int32	V_1)
	IL_0000:  nop 
	IL_0001:  ldstr "Hello World!"
	IL_0006:  call void class [mscorlib]System.Console::WriteLine(string)
	IL_000b:  nop 
	IL_000c:  newobj instance void class inattributeoverload.C::'.ctor'()
	IL_0011:  stloc.0 
	IL_0012:  ldc.i4.1 
	IL_0013:  stloc.1 
	IL_0014:  ldloc.0 
	IL_0015:  ldloca.s 1
	IL_0017:  callvirt instance void class inattributeoverload.C::A([out] int32&)
	IL_001c:  nop 
	IL_001d:  ret 
    } // end of method Program::Main
}

@zpodlovics
Copy link

Would it be possible to have a "non extractive/non-stack-allocating" (does not allocate the "extracted" struct on stack when accessing these readonly struct cases / patterns) way to do pattern matching in F#? Right now the "extraction" reference type patterns / small value types seems cheap, but larger value types could be costly to do pattern maching (even if the "extracted" cases are unused).

What I mean by "non-extractive/non-stack-allocating" pattern maching on readonly types? The syntax is ad-hoc and fictional (probably better way to express it is exists). Consider the following example, when the match expression will not allocate the struct case on the stack as it match BigStructDU.Case1 because it used as an inref type.

    let matchBigStructDUByRef (x: BigStructDU inref) (v: int) = 
        match x with
        | BigStructDU.Case1 inref _ -> Baseline.Invoke(v,1)
        | BigStructDU.Case2 inref _ -> Baseline.Invoke(v,2)
        | BigStructDU.Case3 inref _ -> Baseline.Invoke(v,3)
        | BigStructDU.Case4 inref _  -> Baseline.Invoke(v,4)
        | BigStructDU.Case5 inref _ -> Baseline.Invoke(v,5)
        | BigStructDU.Case6 inref _ -> Baseline.Invoke(v,6)
        | BigStructDU.Case7 inref _ -> Baseline.Invoke(v,7)
        | BigStructDU.Case8 inref _ -> Baseline.Invoke(v,8)
        | BigStructDU.Case9 inref _ -> Baseline.Invoke(v,9)
        | BigStructDU.Case10 inref _ -> Baseline.Invoke(v,10)

From the RFC:

Note that the struct is immutable, except for a Replace method. The this parameter will now be considered inref<S> and an error will be reported suggesting to add a mutable field if the struc is to be mutated.

Allowing this assignment was never intended in the F# design and I consider this as fixing a bug in the F# compiler now we have the machinery to express read-only references.

As pattern matching is in the core of most F# code, this could be greatly improve the performance of most of the exsting F# codebase with a few changes or without a few changes (if the compier could infer it).

To demonstrate the larger value types pattern matching existing overhead I have attached a benchmark project. Please feel free to use it for any purpose (eg.: benchmark, test,..). Pattern matching on larger value types could (and should) be as fast as reference or small value types.

StructDU vs BigStructDU Benchmark and the result:
bigstructmatchbenchmark.zip

// * Summary *

BenchmarkDotNet=v0.10.13, OS=ubuntu 16.04
AMD A10-7850K Radeon R7, 12 Compute Cores 4C+8G, 1 CPU, 4 logical cores and 2 physical cores
.NET Core SDK=2.1.4
  [Host]     : .NET Core ? (CoreCLR 4.6.00001.0, CoreFX 4.6.25519.03), 64bit RyuJIT DEBUG
  DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT


            Method |      Mean |     Error |    StdDev |
------------------ |----------:|----------:|----------:|
 BaselineLocalUnit |  1.852 ns | 0.0354 ns | 0.0314 ns |
      BaselineUnit |  5.439 ns | 0.0631 ns | 0.0590 ns |
    BaselineInvoke |  5.380 ns | 0.0637 ns | 0.0596 ns |
          StructDU |  8.523 ns | 0.1367 ns | 0.1142 ns |
       BigStructDU | 89.835 ns | 0.4124 ns | 0.3857 ns |

// * Hints *
Outliers
  MatchBenchmarkDotNet.BaselineLocalUnit: Default -> 1 outlier  was  removed
  MatchBenchmarkDotNet.StructDU: Default          -> 2 outliers were removed

// * Legends *
  Mean   : Arithmetic mean of all measurements
  Error  : Half of 99.9% confidence interval
  StdDev : Standard deviation of all measurements
  1 ns   : 1 Nanosecond (0.000000001 sec)

.NET Core Versions:

ii  dotnet-host                                                 2.1.0-rc1-1                                                 amd64        Microsoft .NET Core Host - 2.1.0 Release Candidate 1
ii  dotnet-hostfxr-1.1.0                                        1.1.0-1                                                     amd64        Microsoft .NET Core 1.1.3 - Host FX Resolver 1.1.0
ii  dotnet-hostfxr-2.0.5                                        2.0.5-1                                                     amd64        Microsoft .NET Core Host FX Resolver - 2.0.5 2.0.5
ii  dotnet-hostfxr-2.0.6                                        2.0.6-1                                                     amd64        Microsoft .NET Core Host FX Resolver - 2.0.6 2.0.6
ii  dotnet-hostfxr-2.1.0-rc1                                    2.1.0-rc1-1                                                 amd64        Microsoft .NET Core Host FX Resolver - 2.1.0 Release Candidate 1 2.1.0-rc1
ii  dotnet-runtime-2.0.5                                        2.0.5-1                                                     amd64        Microsoft .NET Core Runtime - 2.0.5 Microsoft.NETCore.App 2.0.5
ii  dotnet-runtime-2.0.6                                        2.0.6-1                                                     amd64        Microsoft .NET Core Runtime - 2.0.6 Microsoft.NETCore.App 2.0.6
ii  dotnet-runtime-2.1.0-rc1                                    2.1.0-rc1-1                                                 amd64        Microsoft .NET Core Runtime - 2.1.0 Release Candidate 1 Microsoft.NETCore.App 2.1.0-rc1
ii  dotnet-runtime-deps-2.1.0-rc1                               2.1.0-rc1-1                                                 amd64        dotnet-runtime-deps-2.1.0-rc1 2.1.0-rc1
ii  dotnet-sdk-2.1.4                                            2.1.4-1                                                     amd64        Microsoft .NET Core SDK - 2.1.4
ii  dotnet-sharedframework-microsoft.netcore.app-1.1.8          1.1.8-1                                                     amd64        Microsoft .NET Core 1.1.8 - Runtime Microsoft.NETCore.App 1.1.8

@NinoFloris
Copy link
Contributor

Design looks great, really impressed with the inoutref rules. Would a way to express stackalloc also be on the table now that it's not under unsafe in C# anymore?

@dsyme
Copy link
Contributor Author

dsyme commented May 19, 2018

What I mean by "non-extractive/non-stack-allocating" pattern maching on readonly types?

Yes, it's possible. I'm not going to be able to do it in this PR however.

Basically we need to rejig the pattern match compiler in F# so it can accept an LValue as its starting point rather than RValue. It's fairly conceptually straightforward but needs care.

Design looks great, really impressed with the inoutref rules. Would a way to express stackalloc also be on the table now that it's not under unsafe in C# anymore?

There is already NativePtr.stackalloc, I think that's sufficient for F# purposes but let me know what you think

@dsyme
Copy link
Contributor Author

dsyme commented May 19, 2018

And I have noticed that the generated IL callwith will pass the x argument with an [out] attribute.

@zpodlovics Thank you for examining the generated code! I agree that this looks incorrect (though I think benign) and will double check the corresponding generated C#

@dsyme
Copy link
Contributor Author

dsyme commented May 21, 2018

@zpodlovics

Right now when the second c.A(x) enabled the C# compiler will complain (with latest):

I don't get an error when your second line is enabled:

C:\GitHub\dsyme\visualfsharp>csc /langversion:7.2 C:\GitHub\dsyme\visualfsharp\a.cs
Microsoft (R) Visual C# Compiler version 2.7.0.62715 (db02128e)
Copyright (C) Microsoft Corporation. All rights reserved.

C:\GitHub\dsyme\visualfsharp>

Which exact version of the C# compiler are you using?

@dsyme
Copy link
Contributor Author

dsyme commented May 21, 2018

Oddly I also don't get the [out] showing in ILDASM:

  IL_0015:  ldloca.s   V_1
  IL_0017:  callvirt   instance void inattributeoverload.C::A(int32&)
  IL_001c:  nop
  IL_001d:  ldloc.0

@dsyme
Copy link
Contributor Author

dsyme commented May 21, 2018

@zpodlovics I am updating to Visual Studio 15.7.1 to check if behaviour changed (the above was with 15.6.7)

@zpodlovics
Copy link

zpodlovics commented May 21, 2018

@dsyme

Removed the 2.1.4 sdk as an experiment and reinstalled the 2.0.3, however the C# compiler will not recognize the in construct:

Earlier sdk (2.0.3) will not recognize the in keyword:

dotnet build -c Release
Microsoft (R) Build Engine version 15.4.8.50001 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

Program.cs(12,23): error CS1041: Identifier expected; 'in' is a keyword [/tmp/inattributeoverload/inattributeoverload.csproj]
Program.cs(27,17): error CS1041: Identifier expected; 'in' is a keyword [/tmp/inattributeoverload/inattributeoverload.csproj]

Build FAILED.

Program.cs(12,23): error CS1041: Identifier expected; 'in' is a keyword [/tmp/inattributeoverload/inattributeoverload.csproj]
Program.cs(27,17): error CS1041: Identifier expected; 'in' is a keyword [/tmp/inattributeoverload/inattributeoverload.csproj]
    0 Warning(s)
    2 Error(s)

Time Elapsed 00:00:01.67

WIth 2.1.4 dotnet sdk installed (I used this at the time of the report).

ii  dotnet-host                                                 2.1.0-rc1-1                                                 amd64        Microsoft .NET Core Host - 2.1.0 Release Candidate 1
ii  dotnet-hostfxr-1.1.0                                        1.1.0-1                                                     amd64        Microsoft .NET Core 1.1.3 - Host FX Resolver 1.1.0
ii  dotnet-hostfxr-2.0.5                                        2.0.5-1                                                     amd64        Microsoft .NET Core Host FX Resolver - 2.0.5 2.0.5
ii  dotnet-hostfxr-2.0.6                                        2.0.6-1                                                     amd64        Microsoft .NET Core Host FX Resolver - 2.0.6 2.0.6
ii  dotnet-hostfxr-2.1.0-rc1                                    2.1.0-rc1-1                                                 amd64        Microsoft .NET Core Host FX Resolver - 2.1.0 Release Candidate 1 2.1.0-rc1
ii  dotnet-runtime-2.0.5                                        2.0.5-1                                                     amd64        Microsoft .NET Core Runtime - 2.0.5 Microsoft.NETCore.App 2.0.5
ii  dotnet-runtime-2.0.6                                        2.0.6-1                                                     amd64        Microsoft .NET Core Runtime - 2.0.6 Microsoft.NETCore.App 2.0.6
ii  dotnet-runtime-2.1.0-rc1                                    2.1.0-rc1-1                                                 amd64        Microsoft .NET Core Runtime - 2.1.0 Release Candidate 1 Microsoft.NETCore.App 2.1.0-rc1
ii  dotnet-runtime-deps-2.1.0-rc1                               2.1.0-rc1-1                                                 amd64        dotnet-runtime-deps-2.1.0-rc1 2.1.0-rc1
ii  dotnet-sdk-2.1.4                                            2.1.4-1                                                     amd64        Microsoft .NET Core SDK - 2.1.4
ii  dotnet-sharedframework-microsoft.netcore.app-1.1.8          1.1.8-1                                                     amd64        Microsoft .NET Core 1.1.8 - Runtime Microsoft.NETCore.App 1.1.8

Build log:

dotnet build -c Release
Microsoft (R) Build Engine version 15.5.180.51428 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restoring packages for /tmp/inattributeoverload/inattributeoverload.csproj...
  Generating MSBuild file /tmp/inattributeoverload/obj/inattributeoverload.csproj.nuget.g.props.
  Generating MSBuild file /tmp/inattributeoverload/obj/inattributeoverload.csproj.nuget.g.targets.
  Restore completed in 198.61 ms for /tmp/inattributeoverload/inattributeoverload.csproj.
Program.cs(25,15): error CS0121: The call is ambiguous between the following methods or properties: 'C.A(int)' and 'C.A(in int)' [/tmp/inattributeoverload/inattributeoverload.csproj]
Program.cs(28,15): error CS0121: The call is ambiguous between the following methods or properties: 'C.A(int)' and 'C.A(in int)' [/tmp/inattributeoverload/inattributeoverload.csproj]

Build FAILED.

Program.cs(25,15): error CS0121: The call is ambiguous between the following methods or properties: 'C.A(int)' and 'C.A(in int)' [/tmp/inattributeoverload/inattributeoverload.csproj]
Program.cs(28,15): error CS0121: The call is ambiguous between the following methods or properties: 'C.A(int)' and 'C.A(in int)' [/tmp/inattributeoverload/inattributeoverload.csproj]
    0 Warning(s)
    2 Error(s)

Time Elapsed 00:00:02.87

After removing the offending lines, the output dll IL extracted with monodis:

ii  mono-utils                                                  5.12.0.226-0xamarin3+ubuntu1604b1                           amd64        Mono utilities
monodis /tmp/inattributeoverload/bin/Release/netcoreapp2.0/inattributeoverload.dll
.assembly extern System.Runtime
{
  .ver 4:2:0:0
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
}
.assembly extern System.Console
{
  .ver 4:1:0:0
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
}
.assembly 'inattributeoverload'
{
  .custom instance void class [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::'.ctor'(int32) =  (01 00 08 00 00 00 00 00 ) // ........

  .custom instance void class [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::'.ctor'() =  (
		01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78   // ....T..WrapNonEx
		63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01       ) // ceptionThrows.

  .custom instance void class [mscorlib]System.Diagnostics.DebuggableAttribute::'.ctor'(valuetype [mscorlib]System.Diagnostics.DebuggableAttribute/DebuggingModes) =  (01 00 02 00 00 00 00 00 ) // ........

  .custom instance void class [mscorlib]System.Runtime.Versioning.TargetFrameworkAttribute::'.ctor'(string) =  (
		01 00 18 2E 4E 45 54 43 6F 72 65 41 70 70 2C 56   // ....NETCoreApp,V
		65 72 73 69 6F 6E 3D 76 32 2E 30 01 00 54 0E 14   // ersion=v2.0..T..
		46 72 61 6D 65 77 6F 72 6B 44 69 73 70 6C 61 79   // FrameworkDisplay
		4E 61 6D 65 00                                  ) // Name.

  .custom instance void class [mscorlib]System.Reflection.AssemblyCompanyAttribute::'.ctor'(string) =  (
		01 00 13 69 6E 61 74 74 72 69 62 75 74 65 6F 76   // ...inattributeov
		65 72 6C 6F 61 64 00 00                         ) // erload..

  .custom instance void class [mscorlib]System.Reflection.AssemblyConfigurationAttribute::'.ctor'(string) =  (01 00 07 52 65 6C 65 61 73 65 00 00 ) // ...Release..

  .custom instance void class [mscorlib]System.Reflection.AssemblyFileVersionAttribute::'.ctor'(string) =  (01 00 07 31 2E 30 2E 30 2E 30 00 00 ) // ...1.0.0.0..

  .custom instance void class [mscorlib]System.Reflection.AssemblyInformationalVersionAttribute::'.ctor'(string) =  (01 00 05 31 2E 30 2E 30 00 00 ) // ...1.0.0..

  .custom instance void class [mscorlib]System.Reflection.AssemblyProductAttribute::'.ctor'(string) =  (
		01 00 13 69 6E 61 74 74 72 69 62 75 74 65 6F 76   // ...inattributeov
		65 72 6C 6F 61 64 00 00                         ) // erload..

  .custom instance void class [mscorlib]System.Reflection.AssemblyTitleAttribute::'.ctor'(string) =  (
		01 00 13 69 6E 61 74 74 72 69 62 75 74 65 6F 76   // ...inattributeov
		65 72 6C 6F 61 64 00 00                         ) // erload..

  .hash algorithm 0x00008004
  .ver  1:0:0:0
}
.module inattributeoverload.dll // GUID = {A3946165-F0C2-4FE5-9E2F-2CD86A31DCF2}


.namespace inattributeoverload
{
  .class public auto ansi beforefieldinit C
  	extends [System.Runtime]System.Object
  {

    // method line 1
    .method public hidebysig 
           instance default void A (int32 a)  cil managed 
    {
        // Method begins at RVA 0x2050
	// Code size 11 (0xb)
	.maxstack 8
	IL_0000:  ldstr "int a"
	IL_0005:  call void class [mscorlib]System.Console::WriteLine(string)
	IL_000a:  ret 
    } // end of method C::A

    // method line 2
    .method public hidebysig 
           instance default void A (int32& a)  cil managed 
    {
	.param [1]
	.custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() =  (01 00 00 00 ) // ....

        // Method begins at RVA 0x205c
	// Code size 11 (0xb)
	.maxstack 8
	IL_0000:  ldstr "in int a"
	IL_0005:  call void class [mscorlib]System.Console::WriteLine(string)
	IL_000a:  ret 
    } // end of method C::A

    // method line 3
    .method public hidebysig specialname rtspecialname 
           instance default void '.ctor' ()  cil managed 
    {
        // Method begins at RVA 0x2068
	// Code size 7 (0x7)
	.maxstack 8
	IL_0000:  ldarg.0 
	IL_0001:  call instance void object::'.ctor'()
	IL_0006:  ret 
    } // end of method C::.ctor

  } // end of class inattributeoverload.C
}

.namespace inattributeoverload
{
  .class private auto ansi beforefieldinit Program
  	extends [System.Runtime]System.Object
  {

    // method line 4
    .method private static hidebysig 
           default void Main (string[] args)  cil managed 
    {
        // Method begins at RVA 0x2070
	.entrypoint
	// Code size 25 (0x19)
	.maxstack 2
	.locals init (
		int32	V_0)
	IL_0000:  ldstr "Hello World!"
	IL_0005:  call void class [mscorlib]System.Console::WriteLine(string)
	IL_000a:  newobj instance void class inattributeoverload.C::'.ctor'()
	IL_000f:  ldc.i4.1 
	IL_0010:  stloc.0 
	IL_0011:  ldloca.s 0
	IL_0013:  callvirt instance void class inattributeoverload.C::A([out] int32&)
	IL_0018:  ret 
    } // end of method Program::Main

    // method line 5
    .method public hidebysig specialname rtspecialname 
           instance default void '.ctor' ()  cil managed 
    {
        // Method begins at RVA 0x2095
	// Code size 7 (0x7)
	.maxstack 8
	IL_0000:  ldarg.0 
	IL_0001:  call instance void object::'.ctor'()
	IL_0006:  ret 
    } // end of method Program::.ctor

  } // end of class inattributeoverload.Program
}

Full project with compiled binaries in case monodis was buggy:
inattributeoverload.zip

@zpodlovics
Copy link

@dsyme I have managed to get ildasm working on linux (with some symlink hacks to enable coreclr initialization), here is the ildasm output:

//  Microsoft (R) .NET Framework IL Disassembler.  Version 4.5.22220.0



// Metadata version: v4.0.30319
.assembly extern System.Runtime
{
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         // .?_....:
  .ver 4:2:0:0
}
.assembly extern System.Console
{
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         // .?_....:
  .ver 4:1:0:0
}
.assembly inattributeoverload
{
  .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
  .custom instance void [System.Runtime]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78   // ....T..WrapNonEx
                                                                                                                   63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 )       // ceptionThrows.

  // --- The following custom attribute is added automatically, do not uncomment -------
  //  .custom instance void [System.Runtime]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggableAttribute/DebuggingModes) = ( 01 00 02 00 00 00 00 00 ) 

  .custom instance void [System.Runtime]System.Runtime.Versioning.TargetFrameworkAttribute::.ctor(string) = ( 01 00 18 2E 4E 45 54 43 6F 72 65 41 70 70 2C 56   // ....NETCoreApp,V
                                                                                                              65 72 73 69 6F 6E 3D 76 32 2E 30 01 00 54 0E 14   // ersion=v2.0..T..
                                                                                                              46 72 61 6D 65 77 6F 72 6B 44 69 73 70 6C 61 79   // FrameworkDisplay
                                                                                                              4E 61 6D 65 00 )                                  // Name.
  .custom instance void [System.Runtime]System.Reflection.AssemblyCompanyAttribute::.ctor(string) = ( 01 00 13 69 6E 61 74 74 72 69 62 75 74 65 6F 76   // ...inattributeov
                                                                                                      65 72 6C 6F 61 64 00 00 )                         // erload..
  .custom instance void [System.Runtime]System.Reflection.AssemblyConfigurationAttribute::.ctor(string) = ( 01 00 07 52 65 6C 65 61 73 65 00 00 )             // ...Release..
  .custom instance void [System.Runtime]System.Reflection.AssemblyFileVersionAttribute::.ctor(string) = ( 01 00 07 31 2E 30 2E 30 2E 30 00 00 )             // ...1.0.0.0..
  .custom instance void [System.Runtime]System.Reflection.AssemblyInformationalVersionAttribute::.ctor(string) = ( 01 00 05 31 2E 30 2E 30 00 00 )                   // ...1.0.0..
  .custom instance void [System.Runtime]System.Reflection.AssemblyProductAttribute::.ctor(string) = ( 01 00 13 69 6E 61 74 74 72 69 62 75 74 65 6F 76   // ...inattributeov
                                                                                                      65 72 6C 6F 61 64 00 00 )                         // erload..
  .custom instance void [System.Runtime]System.Reflection.AssemblyTitleAttribute::.ctor(string) = ( 01 00 13 69 6E 61 74 74 72 69 62 75 74 65 6F 76   // ...inattributeov
                                                                                                    65 72 6C 6F 61 64 00 00 )                         // erload..
  .hash algorithm 0x00008004
  .ver 1:0:0:0
}
.module inattributeoverload.dll
// MVID: {a3946165-f0c2-4fe5-9e2f-2cd86a31dcf2}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x00007FBBE2563000


// =============== CLASS MEMBERS DECLARATION ===================

.class public auto ansi beforefieldinit inattributeoverload.C
       extends [System.Runtime]System.Object
{
  .method public hidebysig instance void 
          A(int32 a) cil managed
  {
    // Code size       11 (0xb)
    .maxstack  8
    IL_0000:  ldstr      "int a"
    IL_0005:  call       void [System.Console]System.Console::WriteLine(string)
    IL_000a:  ret
  } // end of method C::A

  .method public hidebysig instance void 
          A(int32& a) cil managed
  {
    .param [1]
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) 
    // Code size       11 (0xb)
    .maxstack  8
    IL_0000:  ldstr      "in int a"
    IL_0005:  call       void [System.Console]System.Console::WriteLine(string)
    IL_000a:  ret
  } // end of method C::A

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [System.Runtime]System.Object::.ctor()
    IL_0006:  ret
  } // end of method C::.ctor

} // end of class inattributeoverload.C

.class private auto ansi beforefieldinit inattributeoverload.Program
       extends [System.Runtime]System.Object
{
  .method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       25 (0x19)
    .maxstack  2
    .locals init (int32 V_0)
    IL_0000:  ldstr      "Hello World!"
    IL_0005:  call       void [System.Console]System.Console::WriteLine(string)
    IL_000a:  newobj     instance void inattributeoverload.C::.ctor()
    IL_000f:  ldc.i4.1
    IL_0010:  stloc.0
    IL_0011:  ldloca.s   V_0
    IL_0013:  callvirt   instance void inattributeoverload.C::A(int32&)
    IL_0018:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [System.Runtime]System.Object::.ctor()
    IL_0006:  ret
  } // end of method Program::.ctor

} // end of class inattributeoverload.Program


// =============================================================

// *********** DISASSEMBLY COMPLETE ***********************

@dsyme
Copy link
Contributor Author

dsyme commented May 21, 2018

@zpodlovics I'm not getting any warning or error when compiling this using C# in Visual Studio 2017 15.7.1

1>------ Build started: Project: inattributeoverload, Configuration: Debug Any CPU ------
1>inattributeoverload -> C:\Users\dsyme\Downloads\inattributeoverload\inattributeoverload\bin\Debug\netcoreapp2.0\inattributeoverload.dll
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

@dsyme
Copy link
Contributor Author

dsyme commented May 21, 2018

I suspect the presence of [out] in the signature is an artefact of monodis, it's not there in ILDASM output.

@dsyme
Copy link
Contributor Author

dsyme commented May 21, 2018

Anyway, if I understand correctly, to follow the C# behaviour then the following should call the first overload (without complaint), and not the second.

    type C() = 
         static member M(x: System.DateTime) = x.AddDays(1.0)
         static member M(x: inref<System.DateTime>) = x.AddDays(2.0)
    let v =  C.M(System.DateTime.Now)

@svick
Copy link

svick commented May 21, 2018

@zpodlovics @dsyme As far as I can tell, the ambiguity error you're talking about was resolved in Roslyn 2.7 (VS 15.6, .Net Core SDK 2.1.100). See dotnet/csharplang#945.

So it's expected that with .Net Core SDK 2.1.4, you will get the error (2.1.4 < 2.1.100).

@zpodlovics
Copy link

zpodlovics commented May 21, 2018

Well, I would rather vote for explicit type signatures without auto conversion + explicit conversion functionality in case it needed. It may worth adopting their ref readonly testsuite cases and port it to F# plus testing the C# -> F# C# -> F# interop too.

It's still a hot topic for csharplang and roslyn:

dotnet/csharplang#945
https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-12-04.md#in-vs-value-parameter-ambiguity

dotnet/roslyn#19216
dotnet/roslyn#17547
dotnet/docs#3717

Example use case: assuming I have a large struct something like I posted earlier, and I would like to have functionality that could consume this large struct by value (eg.: to process on an another thread running on different physical cpu - do not share data between physical cpus to avoid cpu-cpu interconnect traffic) and by readonly reference (eg.: to process on thread using the same physical cpu). Would it be possible to have also an explicit 'T -> inref<'T> addressof/"cast"/whatever operator? Something like this:

type C() = 
         static member M(x: System.DateTime) = x.AddDays(1.0)
         static member M(x: inref<System.DateTime>) = x.AddDays(2.0)
let t = System.DateTime.Now
let tAsInRref = &t
let v1 =  C.M(t)
let v2 = C.M((inref<System.DateTime>) t)
let v3 = C.M(&t)

@jaredpar
Copy link

module NativePtr = 
    val toVoidPtr : address:nativeptr<'T> -> voidptr
    val ofVoidPtr : voidptr -> nativeptr<'T>

More curiosity than anything here. Why were these described as val instead of let?

Feedback around [<IsReadOnly>]:

  • Does this imply [<Struct>] or do both attributes have to be specified?
  • Is it an error to have a mutable field in a type that has [<IsReadOnly>]?

The this parameter on struct members is now inref when the struct type has no mutable fields or sub-structures.

Will such structs also be implicitly marked as [<IsReadOnly>]? That would help consumers get the benefits as well.

@OmarTawfik
Copy link

A couple of questions from #306:

  1. For compiling against older frameworks (for example, desktop 4.5), the C# compiler generates and embeds these attributes. Are you planning on doing the same?
  2. For interop, the F# compiler will probably need to generate the appropriate modreq required modifiers on these signatures. Are you planning on adding that support in F# beforehand?

@dsyme
Copy link
Contributor Author

dsyme commented May 22, 2018

@jaredpar @OmarTawfik Please keep reviewing and asking questions, thanks! Each question is making me go and check both the implementation and, critically, the test suite in the PR.

Does this imply [<Struct>] or do both attributes have to be specified?

@jaredpar Both need to be specified. I'll add an unresolved question about what happens if one or both is absent

Is it an error to have a mutable field in a type that has [<IsReadOnly>]?

@jaredpar yes. That made me go and review the negative tests in the PR and there are some problems with them, so I've added a TODO about that, thanks!

The this parameter on struct members is now inref when the struct type has no mutable fields or sub-structures.

Will such structs also be implicitly marked as []? That would help consumers get the benefits as well.

@jaredpar Yes, the IsReadOnly modreq is emitted on both argument inref<T> and readonly return parameters, which is what I assume you mean. The test suite covers this for return items but we need to add a check that the modreq is being emitted for arguments too. I've added a TODO about that in the PR.

For compiling against older frameworks (for example, desktop 4.5), the C# compiler generates and embeds these attributes. Are you planning on doing the same?

@OmarTawfik I don't think so. Authoring these types in F# for consumption by down-level C# consumers will be extremely rare (if it ever happens at all) in F#. Down-level consumption by F# consumers will also never happen. I'll add that to the RFC.

For interop, the F# compiler will probably need to generate the appropriate modreq required modifiers on these signatures. Are you planning on adding that support in F# beforehand?

@OmarTawfik Yes, the latest version of the implementation does this, see here and its callsites

@NinoFloris
Copy link
Contributor

NinoFloris commented May 22, 2018

@dsyme I'm assuming @jaredpar meant if structs found immutable will automatically get [<IsReadOnly>] applied to the struct.

(Also why is the attribute name "IsReadOnly", and not just "ReadOnly" which fits better in line with the others?)

@dsyme
Copy link
Contributor Author

dsyme commented May 23, 2018

@dsyme I'm assuming @jaredpar meant if structs found immutable will automatically get [] applied to the struct.

The proposed design is that this doesn't happen, partly because it depends on whether the attribute is actually available to emit at all.

(Also why is the attribute name "IsReadOnly", and not just "ReadOnly" which fits better in line with the others?)

@NinoFloris The attribute is defined in .NET libraries.

@TIHan
Copy link
Contributor

TIHan commented May 31, 2018

How are ref returns handled for assignment? ex:

let write8 (data: Span<byte>) offset value =
        data.[offset] <- byte value

or

something.GetByRef() <- value

Today you can't do this, so I'm wondering what the rules/intent are here.

@jaredpar
Copy link

@TIHan had a similar question around implicit dereference. Is the intent of F# to always implicitly deref here? In general C# implicitly derefs but there are a few cases in which the value is not implicitly derefed:

  • During assignment: M() = new Widget(). Here the return of M() is treated as a ref and 42 is assigned into that location.
  • During member invocation: M().Move(). Here the invocation of Move occurs on the reference returned, not on a copy (assuming there is no readonly in play here). This is really only interesting though for structs.

For the above assume we have the following C# code

Assume for a second we have a C# method with the following signature:

struct Widget { 
  internal void Move() { ... }
}
ref Widget M() => ...

Not trying to imply that F# should necessarily behavior like C# here. But wanted to understand the intent and where the languages may differ.

Also how does type inference work here with implicit deref? Consider this:

let array = [| 0 |]
let main() =
  let f (): byref<int> = &array.[0]
  let g () = f ()

Is the type of g here meant to be byref<int> or int?

@dsyme
Copy link
Contributor Author

dsyme commented May 31, 2018

@jaredpar asks:

Re ref returns and implicit deference. Is the intent of F# to always implicitly deref here? In general C# implicitly derefs but there are a few cases in which the value is not implicitly derefed:

  • During assignment: M() = new Widget(). Here the return of M() is treated as a ref and 42 is assigned into that location.
  • During member invocation: M().Move(). Here the invocation of Move occurs on the reference returned, not on a copy (assuming there is no readonly in play here). This is really only interesting though for structs.

Thanks for highlighting these cases. Assignment to ref-returns is not currently supported, so

type C() = 
    static let mutable v = System.DateTime.Now
    static member M() = &v

let F1() = 
    C.M() <-  System.DateTime.Now

gives an error. I will add that as an unresolved issue. You are right it is reasonable to support this.

Member invocation compiles correctly, e.g.

let F4() = 
    C.M().Day

gives IL code:

.method public static int32  F4() cil managed
{
  // Code size       11 (0xb)
  .maxstack  8
  IL_0000:  call       valuetype [mscorlib]System.DateTime& A/C::M()
  IL_0005:  call       instance int32 [mscorlib]System.DateTime::get_Day()
  IL_000a:  ret
} // end of method A::F4

Also how does type inference work here with implicit deref? Consider this, Is the type of g here meant to be byref or int?

let array = [| 0 |]
let main() = 
  let f (): byref<int> = &array.[0]
  let g () = f ()

Currently in the RFC let-bound functions do not have implicit dereference, this is noted in the RFC. I've spoken with Will Smith and he said that during your trialling of the feature you noticed this as an oddity. I think I made the wrong call here and implicit dereference of byref returns should happen for let-bound functions too. I will add it as an unresolved issue.

But with regard to type inference, this code:

type C() = 
    static let mutable v = System.DateTime.Now
    static member M() = &v
    static member G() = C.M()

gives:

    static member M : unit -> byref<System.DateTime>
    static member G : unit -> System.DateTime

whereas this code:

type C() = 
    static let mutable v = System.DateTime.Now
    static member M() = &v
    static member G() = &C.M()

gives:

    static member M : unit -> byref<System.DateTime>
    static member G : unit -> byref<System.DateTime>

@TIHan
Copy link
Contributor

TIHan commented May 31, 2018

@dsyme and I had a discussion and we believe we should have implicit dereferencing on ref returns when calling let-bound functions, just like member functions. That would change the RFC here.

@dsyme
Copy link
Contributor Author

dsyme commented May 31, 2018

@jaredpar @TIHan Unresolved issues recorded in RFC here #307

@dsyme
Copy link
Contributor Author

dsyme commented Jun 1, 2018

@jaredpar @TIHan All design issues are now listed as resolved in the RFC

@zpodlovics
Copy link

zpodlovics commented Jun 2, 2018

From: dotnet/fsharp#4888

On parity with C# support also means producting not only consuming and using Span! Please do not limit the usefullness of Span support by limiting IsByRefLike struct creations, at least allow it with an experimental attribute, compiler warning (this feature may change in the future or whatever) or something like that. Some of the developers may only interested to consume and use Span and byreflike from C# but I would like to also able to produce it.

It would be really hard to sell F# when the core libraries will still have to write in C#...

@jaredpar
Copy link

jaredpar commented Jun 2, 2018

@dsyme reading through the spec again this morning noticed this line I'd missed in the first reading:

ByRefLike structs can have byref members, e.g.

This isn't allow in C# because it ends up complicating the safety rules considerably for consumers of Span<T>. Consider this method as an example:

[<IsByRefLike; Struct>]
type S(x: byref<int>) = 
    member this.X = x
    member this.Evil (y: byref<int>) = this <- S(y) 

In order for the Evil method to be called safely the compiler must ensure the safe-to-escape and ref-safe-to-escape scope of y is at least as big as S. This makes calling it a bit problematic and unintuitive:

let f (s: S) = 
  let y = 42
  s.Evil(&y) // Error!!!

This pattern actually shows up a lot in CoreFx and it was really hurting the ability to write usable APIs. Eventually we concluded the best way to move forward was to eliminate the possibility of a ref struct logically contain a ref field. C# has no way to author a ref field hence all that was needed to enforce this was to remove the ability to create a one length Span<T> over a ref value.

I realized yesterday in talking with @TIHan that we'd forgotten to write this down in our span safety rules. I sent a PR out last night to correct this.

dotnet/csharplang#1593

There is an addendum at the end on how we thought we could support this in the future in a more sensible way.

@cartermp
Copy link
Member

cartermp commented Jun 3, 2018

@dsyme quick spec question.

Note that [<ReadOnly>] does not imply [<Struct>] both attributes have to be specified.

Should this be:

Note that [<IsByRef>] does not imply [<Struct>]. Both attributes have to be specified.

?

@dsyme
Copy link
Contributor Author

dsyme commented Jun 7, 2018

@cartermp @jaredpar Feedback integrated here: #313

@cartermp
Copy link
Member

Closing as the feature (set) is implemented and shipping as-is with the latest documented scoping rules.

Further changes will likely just be bug fixes. Bigger changes in behavior will necessitate a new RFC, and thus, a new discussion isssue.

@cartermp
Copy link
Member

@dsyme I have a question about this: https://github.com/fsharp/fslang-design/blob/master/FSharp-4.5/FS-1053-span.md#ignoring-obsolete-attribute-on-existing-byreflike-definitions

The F# compiler doesn't emit these attributes when defining ByRefLike types. Authoring these types in F# for consumption by down-level C# consumers will be extremely rare (if it ever happens at all). Down-level consumption by F# consumers will also never happen and if it does the consumer will discover extremely quickly that the later edition of F# is required.

What is the reason for not doing this for F#? It's kind a gross solution, but it does make it unambiguous why an issue is here because the message says outright that your compiler does not understand this type. Otherwise, users with downlevel compilers (e.g., F# 4.1 targeting .NET Core 2.1) could be very, very confused about consuming an F# component that emits a ByRefLike type.

@cartermp cartermp added this to the F# 4.5 milestone Oct 3, 2018
@dsyme
Copy link
Contributor Author

dsyme commented Oct 9, 2018

Apologies for the late reply

What is the reason for not doing this for F#?

I think it is just a feature limitation due to time constraints and the much lower probability of this scenario happening in significant ways (it is just rare to consume F# components using very latest features in down-level compilers)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants