csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

Permit non-void return types for partial methods (VS 16.8, .NET 5)

Open AaronRobinsonMSFT opened this issue 5 years ago • 70 comments

The Source Generator feature is being investigated to create a runtime agnostic and AOT compatible P/Invoke stub generator. One of the hard requirements of Source Generators is user written code is read only and cannot be altered by any Source Generator implementation. This requirement makes it difficult to declare methods and use them, but have a Source Generator provide an implementation in the future (i.e. build time).

Possible work-arounds exist by declaring a partial class and having a convention.

// User defined
[FutureMethod("Foo", typeof(int))]
partial class GenClasses
{
}

// Generated in another TU
partial class GenClasses
{
   public static int Foo() { ... }
}

The above could be made to work, but there are issues.

  • How does documentation for Foo work?
  • What is the IDE scenario here for IntelliSense?
  • Users would probably see many red squiggles that make development annoying.

An alternative approach would be a type of forward declaration using partial methods. The below would relax the requirement to have a void return type and thus the generation of the corresponding method could occur. IntelliSense would make sense as would a location for XML documentation in the user defined code.

Note partial methods are also required to be private. The below example does adhere to that requirement, but relaxing that would also be beneficial although not strictly needed.

// User defined
partial class GenClasses
{
   static partial int FooImpl();
   public static Foo() { return FooImpl(); }
}

// Generated in another TU
partial class GenClasses
{
   static partial int FooImpl() { ... }
}

/cc @jaredpar @davidwrighton @jkotas @stephentoub

AaronRobinsonMSFT avatar Mar 23 '20 23:03 AaronRobinsonMSFT

I presume that unlike void private methods, an implementation of the partial method would be required?

YairHalberstadt avatar Mar 24 '20 04:03 YairHalberstadt

@YairHalberstadt I personally have no objections to that, but it would be up to the language teams to make the final decision. I would imagine some complexity if that wasn't the case though.

AaronRobinsonMSFT avatar Mar 24 '20 04:03 AaronRobinsonMSFT

I independently thought of the same thing when @chsienki and I were brainstorming a while ago. Seems useful. I assumed that non-void return types would require an implementation method.

agocke avatar Mar 24 '20 07:03 agocke

I presume that unlike void private methods, an implementation of the partial method would be required?

Yes indeed. It would also mean that partial methods which have non-void return methods cannot be erased at the call site; there will always be an invocation of the method.

partial methods are also required to be private. The below example does adhere to that requirement, but relaxing that would also be beneficial although not strictly needed.

I think we'd also want to lift that restriction. Think the best way to approach that part is to say that partial methods which have a non-private accessibility are required to have an implementation. That gives it similar rules as non-void return types.

jaredpar avatar Mar 24 '20 15:03 jaredpar

If private void partial-methods are not required, and non-void partial methods (any accessibility) are required, would non-private void partial methods be required?

Would it be worth something allowing the user to specify that the void partial method (any accessibility) is required to help catch issues where they expect it to be provided by a source generator and want to error if it isn't?

tannergooding avatar Mar 24 '20 15:03 tannergooding

(To clarify, I mean a specific instance is required so we don't break back-compat).

tannergooding avatar Mar 24 '20 15:03 tannergooding

non-private void should almost certainly be required. They're part of a public api, and having them silently do nothing would break abstraction boundaries.

333fred avatar Mar 24 '20 15:03 333fred

@tannergooding

If private void partial-methods are not required, and non-void partial methods (any accessibility) are required, would non-private void partial methods be required?

Yes. As my comment said: partial methods which have a non-private accessibility are required to have an implementation. I deliberately left out return type in that statement.

Would it be worth something allowing the user to specify that the void partial method (any accessibility) is required to help catch issues where they expect it to be provided by a source generator and want to error if it isn't?

Possibly but I'd wait to see a compelling case before adding a new feature to support this. So far we've been discussing relaxing existing restrictions to support source generators. This would be adding a new restriction and is essentially a new feature (need syntax or attribute to support).

jaredpar avatar Mar 24 '20 15:03 jaredpar

Previous discussion: https://github.com/dotnet/csharplang/issues/753

alrz avatar Mar 24 '20 16:03 alrz

Previous discussion: #753

@alrz That is definitely being discussed here. Should we close that issue in lieu of this one since this is a super-set?

AaronRobinsonMSFT avatar Mar 24 '20 16:03 AaronRobinsonMSFT

Should we close that issue in lieu of this one since this is a super-set?

Sure, just wanted to add the link just in case. Closed.

alrz avatar Mar 24 '20 17:03 alrz

I don't actually understand the need here for this. Addressing hte OP:

How does documentation for Foo work?

The generated Foo includes its documentation. That documentation flows along the same way as nodemal code.

What is the IDE scenario here for IntelliSense?

IDE works because it sees the generated code.

Users would probably see many red squiggles that make development annoying.

That should not happen. The source generator will run and will have generated the method the user can call.

a

CyrusNajmabadi avatar Mar 25 '20 23:03 CyrusNajmabadi

How does documentation for Foo work?

The generated Foo includes its documentation. That documentation flows along the same way as nodemal code.

The auto-generated code isn't what the user wrote, so how would it know the documentation for the API? In the below code, where does the XML documentation for the function Foo reside? Any existing documentation tool would be confused by documenting the attribute with the typical XML documentation for a method.

// User defined
[FutureMethod("Foo", typeof(int))]
partial class GenClasses
{
}

What is the IDE scenario here for IntelliSense?

IDE works because it sees the generated code.

That is only true if source generators are continually running in the background. It is entirely possible for this to work similar to XAML, but there are terrible cases there especially during a copy/paste of large swathes of XAML where the editor needs to wait for the language service to keep up. There would be squiggles and general confusion since Foo's signature is being generated and defined by the user in a dynamic manner. As opposed to XAML where there is an inheritance contract to conform to and possibly optimize for the limited set of API signatures that would exist.

In the proposed case, there is a function declaration a head of time which satisfies the location for XML documentation and completely addresses the IntelliSense issue without putting undo pressure on the IDE to try and generate potentially large amounts of code during the edit cycle.

AaronRobinsonMSFT avatar Mar 26 '20 00:03 AaronRobinsonMSFT

The generated Foo includes its documentation. That documentation flows along the same way as nodemal code.

Flows from where?

Let's say this is a regex generator. I want to be able to write this:

/// <summary>Creates a regex that finds hello followed by world, with anything in the middle</summary>
[RegexGenerator("hello.*world", RegexOptions.IgnoreCase)]
internal static partial Regex HelloWorldRegex();

With that I don't have to have a string-based name. I've written the method I'll be calling like any other, except the generator is filling in the implementation, rather than the method magically appearing out of thing air. I've been able to put documentation on the method like I would on any other method. The regular expression that's tied to this method is right there in the method's attribute, not somewhere else in the assembly attributing some less specific thing. Etc.

How does this look without the non-void returning support?

stephentoub avatar Mar 26 '20 00:03 stephentoub

That is only true if source generators are continually running in the background.

Yes. That's the plan here.

CyrusNajmabadi avatar Mar 26 '20 00:03 CyrusNajmabadi

et's say this is a regex generator. I want to be able to write this:

I would not write it like that. i would just have:

[RegexGenerator("hello.*world", RegexOptions.IgnoreCase, "HelloWorldRegex")]
partial class Whatever
{
}

SourceGenerator would take that and make another part for Whatever that has the HelloWorldRegex property with teh right code injected for it. All features looking at Whatever, will see that member, because that member exists.

CyrusNajmabadi avatar Mar 26 '20 00:03 CyrusNajmabadi

I would not write it like that. i would just have:

Yeah, to me personally that's much worse, for all the reasons I outlined. And, to the documentation point, it lacks it.

stephentoub avatar Mar 26 '20 00:03 stephentoub

The auto-generated code isn't what the user wrote, so how would it know the documentation for the API? In the below code,

I'm not really understanding what you mean. When the generator generates the new code it can put whatever comments on it using whatever scheme you want to use to convey information to your generator. It could be embedded in attributes, or comments, or something else.

CyrusNajmabadi avatar Mar 26 '20 00:03 CyrusNajmabadi

With that I don't have to have a string-based name. I've written the method I'll be calling like any other, except the generator is filling in the implementation

This sounds like the original/replace approach that we decided to cancel. these scenarios have a suitable approach with the 'generate a new file' system. It may not be really glamorous, but i don't expect or want it to be. I'd rather have a simple system that can do this, instead of more and more complex features (like actually changing the language) in order to make this happen.

CyrusNajmabadi avatar Mar 26 '20 00:03 CyrusNajmabadi

With that I don't have to have a string-based name.

you don't need a string based name, the following works fine:

[RegexGenerator("hello.*world", RegexOptions.IgnoreCase, nameof(HelloWorldRegex)]

there's a duality here. teh generator can see the above and use it to make the method called HelloWorldRegex. The attribute can also then see that new method and then compile.

CyrusNajmabadi avatar Mar 26 '20 00:03 CyrusNajmabadi

And, to the documentation point, it lacks it.

// This is the documentation to generate on the member.  The source generator can look for this
// and move it over.  The source generator can totally make an informal mechanism to allow its domain
// to pass it the information it needs
[RegexGenerator("hello.*world", RegexOptions.IgnoreCase, nameof(HelloWorldRegex)]

CyrusNajmabadi avatar Mar 26 '20 00:03 CyrusNajmabadi

The source generator can totally make an informal mechanism

Why would I want that when we already have a long-standing, compiler-supported mechanism for this? I don't want to invent my own mechanisms, worry that an arbitrary comment in just the wrong place is going to be misconstrued by the ad-hoc conventions of the generator.

stephentoub avatar Mar 26 '20 00:03 stephentoub

There would be squiggles and general confusion since Foo's signature is being generated and defined by the user in a dynamic manner.

That would be weird. We're not going to run diagnostics on hte code prior to source generation. Expectation is we run it after source generation. IN other words, it's part of our design that the code prior to gen is normally expected to be in error because it will depend on the generated code.

In other words, the thing you're trying to fix/avoid here is precisely the use model we're trying heavily to move toward. the expectation is absolutely to not have the definitions or aything else informing the original source code. but all IDE features and whatnot only operate on the post-transformed code.

CyrusNajmabadi avatar Mar 26 '20 00:03 CyrusNajmabadi

Why would I want that when we already have a long-standing, compiler-supported mechanism for this?

Because it would work? And it requires no language changes at all.

I don't want to invent my own mechanisms,

But you're asking us to invent a mechanism.

Right now the benefit of the invention we have is that it is entirely divorced from the language. It is simply: you provide information however you want, and you generate however you want.

We only assume that the generated state of the world is valid and worth doing anything with. We don't need any sort of language handshakes to invent. It's all just an api. Just like analyzers don't require anything special and can rev entirely separately from the lang, we'd like the same for generators.

CyrusNajmabadi avatar Mar 26 '20 00:03 CyrusNajmabadi

In other words, the thing you're trying to fix/avoid here is precisely the use model we're trying heavily to move toward. the expectation is absolutely to not have the definitions or aything else informing the original source code. but all IDE features and whatnot only operate on the post-transformed code.

Then how is the generated code ever used? A key invariant of the source generators is that they can't modify any user defined code. How is this new code called/consumed without use of reflection?

AaronRobinsonMSFT avatar Mar 26 '20 00:03 AaronRobinsonMSFT

But you're asking us to invent a mechanism.

I'm asking the language to add a small feature that makes the source generator way more usable and user-friendly, in my opinion.

these scenarios have a suitable approach with the 'generate a new file' system. It may not be really glamorous, but i don't expect or want it to be. I'd rather have a simple system that can do this, instead of more and more complex features (like actually changing the language) in order to make this happen.

And the feedback I'm sharing, and that others here are sharing, and offline conversations from even more folks sharing this, is that this is very suboptimal.

stephentoub avatar Mar 26 '20 00:03 stephentoub

We don't need any sort of language handshakes to invent. It's all just an api.

This isn't a requirement for sure. This simply makes the Source Generator feature usable in more scenarios (e.g. AOT).

AaronRobinsonMSFT avatar Mar 26 '20 00:03 AaronRobinsonMSFT

ow is this new code called/consumed without use of reflection?

// this is normal user source:
void Foo()
{
     var v = Whatever.HellowWorldRegex;
}

[Regex(...)] // see above for example
partial class Whatever
{
}

This just compiles. It works because the generator sees whatever it needs and generates:

partial class Whatever
{
    public Regex HelloWorldRegex => ... 
}

It is very intentionally by design that the original source on its own with any compiler would not compile. And it can only compile and have meaning with the generator doing its thing.

CyrusNajmabadi avatar Mar 26 '20 01:03 CyrusNajmabadi

I'm asking the language to add a small feature that makes the source generator way more usable and user-friendly, in my opinion.

Can you explain how it's more user friendly? For users consuming the generator it's going to be the same right? They'll simply see the final state of things and all things like intellisense, docs and whatnot will be the same.

In terms of people creating the generator, i'm happy to work on all sorts of IDE features to make that a pleasant experience. but i'm not seeing the need for a language change.

CyrusNajmabadi avatar Mar 26 '20 01:03 CyrusNajmabadi

note: gotta run to the vet, will be back on later.

CyrusNajmabadi avatar Mar 26 '20 01:03 CyrusNajmabadi