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

Allow extension methods without type attribute. #13839

Merged
merged 17 commits into from
Nov 3, 2022

Conversation

nojaf
Copy link
Contributor

@nojaf nojaf commented Sep 5, 2022

@edgarfgp and I are trying to address fsharp/fslang-suggestions#835.

It currently works when the extension method is being consumed in the same F# project.
It doesn't appear to be exposed when consumed in a C# project.
I'm probably still missing something and I could use a pointer.

@nojaf nojaf force-pushed the csharp-extension-method branch from 283ea86 to dc66673 Compare September 5, 2022 07:41
@vzarytovskii
Copy link
Member

vzarytovskii commented Sep 5, 2022

As discussed on Slack:
C# compiler expects type to have ExtensionAttribute to "see" its methods and treat them as extensions.

Probably a different approach will work better here - if type has static method(s) with ExtensionAttribute, then add this attribute to the type itself if it is missing it.

@abelbraaksma
Copy link
Contributor

abelbraaksma commented Sep 5, 2022

As @vzarytovskii mentions, and same is true for assembly, and possibly module (the latter I'm not sure of, it wasn't clear from the suggestions thread, I don't know if C# also adds it to .NET modules). Also, assuming there's an upcoming RFC, can we specify there what kind of members will be "detected" and what rules specifically apply?

@vzarytovskii
Copy link
Member

vzarytovskii commented Sep 5, 2022

can we specify there what kind of members will be "detected" and what rules specifically apply?

Regarding of detecting those, it is already in place, and probably should be changed to not check enclosing type, may be used as reference:

let IsMethInfoPlainCSharpStyleExtensionMember g m isEnclExtTy (minfo: MethInfo) =
// Method must be static, have 'Extension' attribute, must not be curried, must have at least one argument
isEnclExtTy &&
not minfo.IsInstance &&
not minfo.IsExtensionMember &&
(match minfo.NumArgs with [x] when x >= 1 -> true | _ -> false) &&
MethInfoHasAttribute g m g.attrib_ExtensionAttribute minfo

@nojaf nojaf force-pushed the csharp-extension-method branch from dc66673 to 44044af Compare September 9, 2022 12:00
@nojaf nojaf force-pushed the csharp-extension-method branch from 4870254 to f34a854 Compare September 16, 2022 14:02
@edgarfgp
Copy link
Contributor

Humm. There seems to be some error regarding vsintegration that have not touched . Maybe we could rerun the CI to see if that helps

@nojaf
Copy link
Contributor Author

nojaf commented Sep 21, 2022

@edgarfgp I should open the VisualFSharp.sln when I find some time.
Another that struct me is that we didn't consider the impact of signature files in this PR.

What happens when you have:

module Foo

[<System.Runtime.CompilerServices.Extension>]
val PlusOne: a: int -> int

and

module Foo

[<System.Runtime.CompilerServices.Extension>]
let PlusOne (a:int) = a + 1

will that still be picked up?

@edgarfgp
Copy link
Contributor

@edgarfgp I should open the VisualFSharp.sln when I find some time. Another that struct me is that we didn't consider the impact of signature files in this PR.

What happens when you have:

module Foo

[<System.Runtime.CompilerServices.Extension>]
val PlusOne: a: int -> int

and

module Foo

[<System.Runtime.CompilerServices.Extension>]
let PlusOne (a:int) = a + 1

will that still be picked up?

I think for the sake of completeness. We should look at it and see how doable is :)

@edgarfgp
Copy link
Contributor

@vzarytovskii Should we put this under a preview flag ?

@smoothdeveloper
Copy link
Contributor

@edgarfgp can there be a test that it works with VB.NET too?

@edgarfgp
Copy link
Contributor

edgarfgp commented Sep 21, 2022

@smoothdeveloper As far I can see there is not way to do that in the compiler. If you know how please let me know :)

Update : We have tested with a local compiler version with a VB project and works Ok

@smoothdeveloper
Copy link
Contributor

@Edgarfp this test checks things among F#, C# & VB.NET:

fsharp/tests/fsharp/tests.fs

Lines 2069 to 2074 in eaa723d

let ``property setter in method or constructor`` () =
let cfg = testConfig "core/members/set-only-property"
csc cfg @"%s /target:library /out:cs.dll" cfg.csc_flags ["cs.cs"]
vbc cfg @"%s /target:library /out:vb.dll" cfg.vbc_flags ["vb.vb"]
fsc cfg @"%s /target:library /out:fs.dll" cfg.fsc_flags ["fs.fs"]
singleNegTest cfg "calls"

Maybe there is a way which is similar with string literal with code in it in the unit tests, albeit I don't favour having those written like this, and generally prefer to deal with standalone code files.

Update : We have tested with a local compiler version with a VB project and works Ok

Can you confirm if this works without adding the Extension attribute to the assembly?

@vzarytovskii
Copy link
Member

@vzarytovskii Should we put this under a preview flag ?

I would say so, yes. Better guard it by the preview version.

@nojaf
Copy link
Contributor Author

nojaf commented Sep 22, 2022

Can you confirm if this works without adding the Extension attribute to the assembly?

This is outside the scope of our proposal. The fact that VB still needs the assembly attribute is no change in the current behaviour.

@nojaf nojaf force-pushed the csharp-extension-method branch 2 times, most recently from dc12d16 to 248cc8e Compare September 30, 2022 13:06
@nojaf nojaf force-pushed the csharp-extension-method branch from 08041ab to c97ca98 Compare October 7, 2022 13:04
@nojaf nojaf force-pushed the csharp-extension-method branch from 31e32b2 to a336508 Compare October 11, 2022 07:59
@nojaf nojaf marked this pull request as ready for review October 12, 2022 07:34
@T-Gro T-Gro self-assigned this Nov 2, 2022
@T-Gro T-Gro added this to the November-2022 milestone Nov 2, 2022
@T-Gro
Copy link
Member

T-Gro commented Nov 3, 2022

Passing now, please approve :)

@T-Gro T-Gro requested review from edgarfgp, vzarytovskii, KevinRansom, psfinaki, abonie and 0101 and removed request for edgarfgp November 3, 2022 15:44
@edgarfgp
Copy link
Contributor

edgarfgp commented Nov 3, 2022

@T-Gro You are an absolute legend. Thanks for the help.

@T-Gro T-Gro merged commit df393d0 into dotnet:main Nov 3, 2022
@nojaf nojaf deleted the csharp-extension-method branch November 3, 2022 15:58
Comment on lines +611 to +634
let thisTyconRef =
try
let rs =
match metadataOfTycon tcrefOfStaticClass.Deref, minfo with
| ILTypeMetadata (TILObjectReprData(scoref, _, _)), ILMeth(_, ILMethInfo(_, _, _, ilMethod, _), _) ->
match ilMethod.ParameterTypes with
| firstTy :: _ ->
match firstTy with
| ILType.Boxed tspec | ILType.Value tspec ->
let tref = (tspec |> rescopeILTypeSpec scoref).TypeRef
if Import.CanImportILTypeRef amap m tref then
let tcref = tref |> Import.ImportILTypeRef amap m
if isCompiledTupleTyconRef g tcref || tyconRefEq g tcref g.fastFunc_tcr then None
else Some tcref
else None
| _ -> None
| _ -> None
| _ ->
// The results are indexed by the TyconRef of the first 'this' argument, if any.
// So we need to go and crack the type of the 'this' argument.
let thisTy = minfo.GetParamTypes(amap, m, generalizeTypars minfo.FormalMethodTypars).Head.Head
match thisTy with
| AppTy g (tcrefOfTypeExtended, _) when not (isByrefTy g thisTy) -> Some tcrefOfTypeExtended
| _ -> None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to duplicate the code up above - could we have factored this out to prevent the duplication?

Comment on lines 535 to +539
let ty = generalizedTyconRef g tcrefOfStaticClass

let minfos =
GetImmediateIntrinsicMethInfosOfType (None, AccessorDomain.AccessibleFromSomeFSharpCode) g amap m ty
|> List.filter (IsMethInfoPlainCSharpStyleExtensionMember g m true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned about the performance implications of this preview feature. If my late-night analysis is correct it seems to me we're now looking into the methods of every type in every opened namespace. This must be traversing a lot more metadata than previously. This will be very significant both from a perf question - think what happens if there are types in referenced assemblies that were previously unused that contain 100,000 methods. It also has some correctness implications see #15158.

Given the feature being implemented this is a little tricky to avoid. We really have to be looking for the ExtensionAttribute that mark the types before we traverse them, and avoiding the traversal for types that do not contain extension methods. That's really what the attributes are for.

Now, these attributes will be correctly present in compiled reference assemblies because they are either explicit or added in CheckDeclarations, so for referenced assemblies we can just look at them. However these attributes are not necessarily present yet in the assembly being compiled - e.g. in the case of namespace rec where one part of the recursive set of types opens another before the attributes have been fully stitched in.

My (late night dont-take-as-gospel) recommendation is that this code be adjusted to look for ExtensionAttribute, except in the assembly being compiled. So something like:

    if g.langVersion.SupportsFeature(LanguageFeature.CSharpExtensionAttributeNotRequired) then
        let ty = generalizedTyconRef g tcrefOfStaticClass
        let csharpStyleExtensionMembers = 
            if IsTyconRefUsedForCSharpStyleExtensionMembers g m tcrefOfStaticClass || tcrefOfStaticClass.IsLocalRef then
                GetImmediateIntrinsicMethInfosOfType (None, AccessorDomain.AccessibleFromSomeFSharpCode) g amap m ty
                |> List.filter (IsMethInfoPlainCSharpStyleExtensionMember g m true)
            else
                []
                
        if not minfos.IsEmpty then
                let pri = NextExtensionMethodPriority()

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

Successfully merging this pull request may close these issues.

7 participants