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

Generic Attributes #17258

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open

Generic Attributes #17258

wants to merge 26 commits into from

Conversation

mflibby
Copy link

@mflibby mflibby commented May 28, 2024

Description

Resolves FSLang 965

RFC FS-1143

Intention:

The intent of this PR is to implement Generic Attribute support, e.g.:

type ARecordAttribute<^T>()=
    inherit Attribute()


type AFieldAttribute<^T>()=
    inherit Attribute()

[<ARecord<int>>]
type SomeRecord =
    {
        [<AField<string>>]
        someField : string
    }

Checklist

  • Test cases added
  • Performance benchmarks added in case of performance changes
  • Release notes entry updated:

Copy link
Contributor

github-actions bot commented May 28, 2024

❗ Release notes required


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/8.0.400.md
LanguageFeatures.fsi docs/release-notes/.Language/preview.md

@mflibby
Copy link
Author

mflibby commented May 28, 2024

Adding of test cases, and fullfilment of minutia (documentation, various "feature" tags, general cleanup, etc) still WIP.

Currently, [<SomeAttribute<int>()>] compiles down to the expected IL. I plan to explore what will be required for inferencing before marking as ready.

Drafting now while I complete the minor details in case anyone wants to provide feedback in the meantime.

@mflibby
Copy link
Author

mflibby commented Jun 1, 2024

@dotnet-policy-service agree company="Hillcrest Research and Development LLC"

@mflibby mflibby marked this pull request as ready for review June 1, 2024 10:41
@mflibby mflibby requested a review from a team as a code owner June 1, 2024 10:41
@abelbraaksma
Copy link
Contributor

Hi @mflibby, this looks very interesting! In your original description, can you link to the reported issue that this is fixing? And if it is a language change (i.e., a new feature that is being added), can you link to the RFC (in https://github.com/fsharp/fslang-design) and the language proposal or discussion chat (https://github.com/fsharp/fslang-suggestions)?

If none exist, can you update the OP with a summary of what this change is doing and why?

@mflibby
Copy link
Author

mflibby commented Jun 2, 2024

@abelbraaksma I've updated the description with the desired information. Please let me know if this is satisfactory, or if more information will be helpful :)

@mflibby
Copy link
Author

mflibby commented Jun 2, 2024

I'm not sure why, but commit 64607ef passed all checks, yet after a few cleanup/a minor change/merges to other changes on main, a large number of the checks have been failing (the only substantive change I made since the passing commit was a minor change to the LexFilter that allowed the type arguments immediately adjacent to the close of an attribute list to be properly parsed).

I'm wondering why I am all of a sudden getting a bunch of seemingly unrelated tests failing? Additionally, windows defender seems to be flagging a bunch of tests, and occasionally quarantining innocuous tests like "Conformance/../E_Literals02.fs".

The investigation continues.


module Attribute =

let verifyCompilation compilation =
Copy link
Member

Choose a reason for hiding this comment

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

Could you please add test cases(s) with mutual recursion (either via and or via a rec module) between the attribute declaration, the type argument and the type it is being applied on?

Copy link
Author

@mflibby mflibby Jun 7, 2024

Choose a reason for hiding this comment

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

Do we have a particular point of failure we are looking for here? Off the top of my head this is what comes to mind:
(in a module rec)

type MyAttribute<^T>() =
    inherit Attribute()

[<MyAttribute<AnActualType>>]
let x = 1

type AnActualType = class end

(Note: it appears that in a rec module, the usage of an attribute on a binding needs to occur after the definition, but on a type it can occur before the definition.)
and maybe something like

(in a regular module)

[<MyAttribute<AnActualType>>]
type SomeClass(arg) =
    member _.AGoodProperty = arg

and MyAttribute<^T>() =
       inherit Attribute()

and AnActualType = int

Copy link
Author

Choose a reason for hiding this comment

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

Oh great call, both rec and and fail right now (made sure to check they pass when there are no type args and both do)

Copy link
Member

Choose a reason for hiding this comment

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

Glad I could suggest a point of failure - I did not have a particular one in mind, I just know that rec modules are a common point of failure for new typechecking features.

Maybe also a type applying "itself" as the type argument to the attribute?

[<MyAttribute<SomeClass<SomeClass<SomeClass<MyAttribute>>>>>]
type SomeClass<'a>(arg) =

Copy link
Author

Choose a reason for hiding this comment

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

I still need to figure out why the rec/and cases failed, but I implemented the self referencing case (great case btw, I know there must be some cases where it will be useful but this in particular made me laugh quite hard), and was surprised to find it working right out of the gate!

@abelbraaksma
Copy link
Contributor

I've updated the description with the desired information. Please let me know if this is satisfactory, or if more information will be helpful :)

Hey @mflibby, excellent, thanks, now we know where it's coming from and that it was approved 👍. We still need an RFC, though. I may be able to help with that. Language changes require an RFC for documentation and consensus. They capture the what, how and scope of any new feature. The go here: https://github.com/fsharp/fslang-design. It is also used to set up a discussion thread to deal with any unresolved questions.

I can help with the process.

@mflibby
Copy link
Author

mflibby commented Jun 4, 2024

@abelbraaksma Oh well the order in which I've done things certainly should make drawing up the RFC easier, I will get on that ASAP - thank you for the heads up!

Copy link
Member

@auduchinok auduchinok left a comment

Choose a reason for hiding this comment

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

@mflibby Thanks for this!

Could we please have more tests for various error cases?

Please also add tests for cases where type inference is important, like the one from the RFC:

type MyAttribute<^T>(context : ^T)=
  inherit Attribute()

[<MyAttribute(24)>]
let x = 1

@@ -2252,7 +2258,7 @@ and u_attribkind st =

and u_attrib st : Attrib =
let a, b, c, d, e, f = u_tup6 u_tcref u_attribkind (u_list u_attrib_expr) (u_list u_attrib_arg) u_bool u_dummy_range st
Attrib(a, b, c, d, e, None, f) // AttributeTargets are not preserved
Attrib(a, b, [], c, d, e, None, f) // AttributeTargets are not preserved (u_list u_ty)
Copy link
Member

Choose a reason for hiding this comment

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

What will happen if an older compiler sees metadata containing type arguments?

Copy link
Author

Choose a reason for hiding this comment

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

If I understand your question correctly, this relates to a situation where in an assembly that actually uses generic attributes (i.e. actually applying attributes to "things") is referenced by another assembly being built by an older version of fsc. In that case, I think the type arguments of used attributes will need to be digested into the "TyConRef" field of the typed Attrib; i.e., it would need to become a new type definition?

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I've read this change wrong. Not keeping the data is sad, but at the same time changing the metadata format is probably a no-go here for now.

Copy link
Author

Choose a reason for hiding this comment

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

Would that kind of change just be another RFC/PR, or are there other complexities that need to be dealt with?

Copy link
Member

@auduchinok auduchinok Jun 18, 2024

Choose a reason for hiding this comment

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

The problem is we try to maintain the metadata compatibility with the older compilers, and currently there's no preferred way to add new data to it without breaking existing compilers.

@vzarytovskii Do you know if there been any changes in this direction?

Copy link
Member

Choose a reason for hiding this comment

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

The precedence we used to take before:

  • Absolutely maintain backwards compatibility for existing source code (i.e. code not using the feature)
  • Have a compiler error if this is used in FSharp.Core
  • New code using this feature will force consumers of such code to upgrade their tooling (IDE,SDK).

There is an alternative approach of not breaking the pickle format and instead loading the needed info from .NET IL metadata ;; but I do not think this path has been taken before (even though it would be safer from a compatibility perspective, because we do not need to know how to load it for interop scenarios anyway)

Copy link
Author

Choose a reason for hiding this comment

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

I would assume that the purpose of the pickled resources is for the sake of performance improvements (correct me if I'm wrong), in which case I would think that there are some kind of benchmarks concerning the gain of using pickling as opposed to loading the same resources from the IL? Assuming that the cost in this case wouldn't be great, and that it is feasible, loading the info from the IL and not breaking backward compatibility sounds like the better option.

src/Compiler/lex.fsl Outdated Show resolved Hide resolved
@@ -1506,26 +1506,52 @@ attributeListElements:


/* One custom attribute */
attribute:
attribute:
Copy link
Member

@auduchinok auduchinok Jun 11, 2024

Choose a reason for hiding this comment

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

Would it be possible to add recovery for unfinished attributes? For example, consider typing a new attribute and then adding type arguments to it. We want the parser (and the rest of the analysis) to be able to parse the rest of the file while the attribute is being typed, if that is possible with the language grammar.

[<Attr<>]
()
[<Attr<>>]
()
[<Attr<arg>]
()
[<Attr<_>]
()
[<Attr<arg; Attr2>]
()
[<Attr<(LiteralArg)>]
()
[<Attr< LiteralArg>]
()

Copy link
Author

Choose a reason for hiding this comment

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

I'm not entirely certain how this would be done at the moment, but I will begin playing the idea now.

There is a complication with the lexer and typars being immediately adjacent to a closing >] that I think might end up being an obstacle in this case - I include in case I am missing something and someone can enlighten me on the inner workings on the lexer:
In order to allow for the closing >] to be immediately adjacent to a typar, e.g.:

[<Attr<_>>]

We need to deal with the fact that the lexer is going to collect the whole series of greater-thans. Based on my current understanding of how the lexer works, I think the only place we can deal with this is in the LexFilterImpl.peekAdjacentTypars function in Compiler\SyntaxTree\LexFilter.fs, in particular when the stack is being constructed via scan ahead (I assume the scan ahead section is the best place as we need to know that a ] is immediately adjacent in order to be sure we can smash the last gt off the end of what the lexer collects).

@@ -353,7 +353,7 @@ type UnsharedRow(elems: RowElement[]) =
type ILTypeWriterEnv = { EnclosingTyparCount: int }
let envForTypeDef (tdef: ILTypeDef) = { EnclosingTyparCount=tdef.GenericParams.Length }
let envForMethodRef env (ty: ILType) = { EnclosingTyparCount=(match ty with ILType.Array _ -> env.EnclosingTyparCount | _ -> ty.GenericArgs.Length) }
let envForNonGenericMethodRef _mref = { EnclosingTyparCount=Int32.MaxValue }
//let envForNonGenericMethodRef _mref = { EnclosingTyparCount=Int32.MaxValue } do we need this for attributes? It doesn't appear so
Copy link
Author

Choose a reason for hiding this comment

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

I realize it's generally a "no-no" to do this to obscure functionality (i.e. I dont yet understand what this was for), but it seems that with the introduction of generic attributes, we will need to fully qualify the environment for attribute usage. Since this function was only used in the generation of attributes, my current position is that this function can be deleted - though I am still curious why the count was being maxed for the non-generic case, if anyone has that wisdom on hand.

Copy link
Member

Choose a reason for hiding this comment

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

@abonie abonie requested a review from vzarytovskii June 24, 2024 17:28
@vzarytovskii
Copy link
Member

The problem is we try to maintain the metadata compatibility with the older compilers, and currently there's no preferred way to add new data to it without breaking existing compilers.

@vzarytovskii Do you know if there been any changes in this direction?

Rule of thumb is usually:

  1. Don't break it if possible (duh). Problem with this is that we don't really have a mechanism of merging pickled data with IL metadata (in this case - merging FSharpAttribute with ILAttribute) as well as not all the information we would like to have, is there (like aliases, or type measures for example), which, as a result will get lost cross-assembly.
  2. If breaking change is necessary - make it explicit - i.e. only pickle new data when library author explicitly opted in to use that feature, and put the feature under language flag and don't use it in FSharp.Core (for example - let bindings in types, or, in this case, generic attributes).

We have one more case - when we introduced another resource for nullability data, it has multiple problems unfortunately:

  1. We don't have standard mechanisms for managing those, code for handling every single one has to be literally copy-pasted, which kinda makes it prone to errors.
  2. It turns out to be suboptimal - a lot of additional excessive data - a bunch of new tables, instead of just new bit/type somewhere.
  3. It needs to be taken care of in the trimming config files every time it's introduced.

We probably want to think of such mechanism in future, but it will require a very thorough design and will be a huge chunk of work.

I would assume that the purpose of the pickled resources is for the sake of performance improvements (correct me if I'm wrong)i n which case I would think that there are some kind of benchmarks concerning the gain of using pickling as opposed to loading the same resources from the IL?

Not necessary, main purpose is to have/store additional metadata, which we want to use cross-assembly (mostly). I don't really think it will affect perf significatly.

Assuming that the cost in this case wouldn't be great, and that it is feasible, loading the info from the IL and not breaking backward compatibility sounds like the better option.

Yeah, but please, refer to part of this comment below - not all the info we would like to have is in the IL metadata.

TL;DR - I would personally go with second point in the beginning of my comment if possible. We have done it before. We will likely do it in future. And at the moment we, unfortunately, don't have a better mechanism for it.

cc @T-Gro @dsyme

@KevinRansom
Copy link
Member

I would like to see more interop tests,

  • using C# Generic Attributes from F#
  • using F# Generic Attributes from C# and VB - I suppose VB supports them.
  • tests using reflection to access the attribute values, Although I can't imagine much problem with that.
  • tests from C# using reflection to access the attributes.
  • test in FSI
  • Declare and use an attrribute when scripting.
    
  • Consume C# and F# dlls with GenericAttributes from scripts
    

There probably needs to be work to ensure that the IDE experience of Generic Attributes works OK, but I think that can be a separate PR.

Needs a language flag so at least we have about a year to work through the kinks.

Needs a much more detailed writeup of what's enabled, why, and what the interop experience is supposed to be.

This looks like a good start, thanks for taking care of it.

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

Successfully merging this pull request may close these issues.

Generic attributes
6 participants