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

Support Source Generators #864

Open
5 tasks done
praeclarum opened this issue Apr 29, 2020 · 76 comments
Open
5 tasks done

Support Source Generators #864

praeclarum opened this issue Apr 29, 2020 · 76 comments

Comments

@praeclarum
Copy link

praeclarum commented Apr 29, 2020

Support Source Generators

Add support similar to C# Source Generators

The idea is to execute the compiler in two passes:

  1. Pass 1 Parse and type check the project code (the type check may be optional as it will contain errors)
  2. Send that information to Source Generators that output new code files or syntax trees
  3. Pass 2 Combine all code, type check, emit

The existing ways of approaching this problem in F# are:

  1. TypeProviders which take specialized knowledge to author.
  2. Custom build steps to emit code

Pros and Cons

The advantages of making this adjustment to F# are an easy form of meta programming. It's basically all the benefits of type providers without the complexity.

The disadvantages are the repetition of a feature and the compiler performance penalty of executing the type checker twice when this feature is used.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): M (depending on what data is passed to the generators)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this
@Happypig375
Copy link
Contributor

You can also write an F# script to generate an F# file today.

@kerams
Copy link

kerams commented Apr 30, 2020

It's basically all the benefits of type providers without the complexity.

A huge part of type providers is design-time support. Based on a quick look, source generators don't seem to be anything like that; more akin to/just an evolution of T4. I'd personally rather see https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1023-type-providers-generate-types-from-types.md than this.

@robkuz
Copy link

robkuz commented Apr 30, 2020

@kerams everybody seems to agree that type provider are enormously brittle (and that seems to be an understatement still) so having a standard, integrated, lightweight source generation facility would be more than welcome.

@Krzysztof-Cieslak
Copy link

Krzysztof-Cieslak commented Apr 30, 2020

</>

@Swoorup
Copy link

Swoorup commented Apr 30, 2020

I think its worth targetting mid-level IR/AST representation rather than IL or just generating source code directly.

@realvictorprm
Copy link
Member

Completely agree. Myriad is a better way of handling all this.

@charlesroddie
Copy link

charlesroddie commented Apr 30, 2020

@Krzysztof-Cieslak I think what this suggestion wants is some form of generating code or types that is better than the current state of type providers. I agree that generating strings is not the best approach.

Myriad can be in the mix of things to discuss here, as is an upgrade to type providers, as is something similar to C# source generators. If there is any link to a high-level write-up of Myriad it would be useful so it can be considered. I can't find anything online apart from this blog about its development process.

@cartermp
Copy link
Member

@kerams the Source Generators feature is currently in a very early preview. There's extensive design-time support planned, which you can read the beginnings of here: https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md#ide-integration.

There's some interesting challenges to solve that are not unlike what Type Providers struggle with today. For example, you want generated source to be up to date, so the safest thing to do is regenerate on every keystroke. This is effectively what Type Provider do today since they're asked to provide "fresh" types whenever the language service needs to re-typecheck things. The upside is correctness, but the downside is a huge hit to design-time performance. We somewhat work around this today in Type Providers with a series of caches that were added in the VS 2019 16.0/16.1 timeframe. They also serve as a band-aid around some more fundamental architectural flaws that force the TPSDK to hunt for and load big binaries in memory (when the compiler already has all the data it's looking for), leading to large Large Object Heap (LOH) allocations that ultimately kill IDE perf.

Currently the C# Source Generators simplify a lot of things by generating files in memory. But that offers some downsides; namely, not much C# tooling has a good understanding of them today. So there's a lot of design work that will probably go back and forth between compiler and IDE design until something acceptable emerges. Any F# implementation would likely utilize a similar mechanism once it stabilizes.

@cartermp
Copy link
Member

As for the suggestion:

I think this is something we'll want to wait a bit for. Firstly, C# Source Generators are just in their first preview and could undergo complete design overhauls between previews based on feedback and scenarios that become more apparent. The current experience exists mostly so that early adopters can try things out, see what is missing or needs to change, and let the team know how it needs to change. There's also a lot of work to do in good IDE integration. Finally, although the string concatenation approach is extremely flexible, it's not necessarily going to stick or be the only way to do things. I personally prefer it to having to learn some complicated API with its own set of bugs and design flaws, but I could see how others would feel the opposite considering that there's pretty much no guarantees around correctness when you're just concatenating strings.

However, I suspect we'll eventually want to implement something that can "hook in" to the libraries that will ultimately end up using Source Generators. The blog post hints at some Microsoft frameworks and libraries adopting them. Realistically that won't happen for quite a while, largely because none of the "adopt source generators" work was costed for the .NET 5 timeframe. Perhaps an early prototype will emerge if Source Generators stabilize early enough. But in the long-term, I expect a lot of the .NET ecosystem to offer a "Source Generator path" for performance and better AOT-compilability. For F# to take part in these benefits, the F# compiler would also need a compatible feature. When the time comes, I think we'll likely look at a variety of options:

  • Encouraging the use of Myriad or a similar library, with some adjustments in the library and/or different frameworks to accept what it emits
  • Implement a new component for the compiler that, perhaps inspired by some of Myriad, allows for a more "F# way" to emit source code and/or other files
  • Integrate with the language, perhaps by extending Type Providers, to offer a very different experience for F# developers
  • Just copy whatever C# ultimately does

Table stakes would be ensuring that what is emitted can be consumed by .NET components so that F# developers can partake in the performance and AOT-ability gains. Post-.NET 5 it is highly likely that the .NET runtime team will focus heavily on AOT since it addresses a lot of pains people have with using .NET in production. This naturally means that some of heavy reliance on reflection in the .NET ecosystem may need a replacement. A Source Generator-like mechanism could be a key part of that for F#. Seriously considering these things is at least a year away though.

@realvictorprm
Copy link
Member

@charlesroddie if there's interest I'm happy to write more about how to use Myriad / how to create plugins for Myriad etc.

@OnurGumus
Copy link

i think the proper way is to do it like nemerle macros.

@cartermp
Copy link
Member

cartermp commented May 2, 2020

@OnurGumus I don't think we'll end up supporting syntactic macros: #210

@praeclarum
Copy link
Author

Hi everyone, thanks for considering this. I want to be clear where I stand: I don't think that Source Generators are a wündertool of meta programming. I do think it's a very practical solution to a very real problem. I appreciate everyone wanting to do original research on hygienic macros, but this suggestion is specifically not that. :-)

My thoughts on the above criticisms:

  • Doesn't integrate with the IDE like TPs That's not true - the design specifically allows for excellent IDE integration. Adhoc script generation thrown into project files does not. This pattern with multi-pass compiler support can give us an IDE experience just as simple as or better (an opportunity to see generated code) than type providers.

  • Just use <insert hygienic macro systems> Yeah, not what I want. F# has tried the "let's do macros without doing macros" thing. It was a good experiment. But it's too easy to overcomplicate the solution to this problem. While I love that F# community is made up of super programmers, it wouldn't be bad, just this once, to implement a simple feature with small ambitions.

  • We don't need this, just write a script Anyone who has gone this route knows the problems. The biggest is always needing a bootstrapping compile run that always fails. You know about conflicts with other build steps and getting the order right. You know about finding paths in MSBuild environments. You know about file system permissions. Writing MSBuild tasks is also a fraught experience given the propensity of MSBuilds to change the way they work.

  • ASTs are superior to text No. We program using text editors for a reason. Forcing us to write against these will require a huge investment of time to learn the F# AST. Every programmer knows how to generate a text file. We have large powerful libraries for working with text. While ASTs can save you from a few syntactic errors (the easy part), they don't save you from anything else. It is a whole lot of pain to put on a programmer just for pedantry's sake.

Closing thoughts:

The C# ecosystem is about to be flooded with Source Generators. Already F# lags behind C# in tooling support. For example, Xamarin supports code generation for CoreML for C# but does not offer it for F#. The same is true for Storyboard support and XIB support and XML code behind. With every year, F# tooling falls behind C#. It is my opinion that it would benefit the F# community greatly to make writing tooling for F# easier.

@cartermp
Copy link
Member

cartermp commented May 2, 2020

@praeclarum Just a note about tooling, I think that view is quite Xamarin-focused and not representative of where most developers are.

Xamarin is heavily C#-focused today, in large part due to how project integration tooling works, since it uses the so-called "legacy" project system and flavoring. This old technology is feature-rich but inflexible. Most of the .NET Core/Standard-based stuff uses a different, far more flexible system that has led to tooling that is about as equally available for F# as it is for C#. One example is the Azure-based tooling that is equally available for F# projects. Additionally, some of the excellent API design of things like ASP.NET Core has allowed for more F#-friendly entry points to emerge (ASP.NET Core supports F# async without requiring conversion to task, Giraffe and Saturn build directly atop its abstractions, etc.)

Perhaps future Xamarin components can be designed in a more pluggable way, like ASP.NET Core, and not require things like the enormous amount of work that was required to light up Fabulous (which also cannot plug into lots of the Visual Studio-based tooling). I anticipate it being easier to support F# in the future with Xamarin with the team moving their project integration tooling to the same system that .NET SDK-style projects use.

Tooling for consuming source generators written in C# is also something to consider, and this would fall square in the "F# team that does tooling" realm to implement. I expect this to be important as more are available.

@praeclarum
Copy link
Author

I will 100% concede that I work on an uncommon platform compared to the rest of the community, but I hope you'll welcome diverse perspectives. Plus, Microsoft states that Source Generators are the recommended solution to the problems linkers present in .NET - a problem Xamarin devs have over a decade of experience with that is now becoming a very real problem in .NET Core.

Instead, build-time source generators will be the recommended mitigation for arbitrary reflection use. -Jan Kotas

I could have listed examples other than Xamarin. Protocol buffers could have been another example. I have been playing with a new version of sqlite-net that uses source generators (though I might end up with an IL masher to make it work with F#). I am also currently working on a library to assist mapping functional structures to object-oriented components (Fom) that could benefit from this technology.

Anyway, thanks again for the consideration!

@Tarmil
Copy link

Tarmil commented May 2, 2020

One small thing should be noted: file ordering puts F# at a disadvantage vs C# regarding source generators (or Myriad). If I have a generator that, say, generates serialization code for annotated types, in F# I can't declare a type and use its serialization within the same file, because the generated code would need to be in-between. Whereas in C#, that's not a problem, there's just a cyclic reference between your file and the generated one. Type providers don't have this inconvenient either because they generate code at the right place.

  • ASTs are superior to text No. We program using text editors for a reason. Forcing us to write against these will require a huge investment of time to learn the F# AST. Every programmer knows how to generate a text file. We have large powerful libraries for working with text. While ASTs can save you from a few syntactic errors (the easy part), they don't save you from anything else. It is a whole lot of pain to put on a programmer just for pedantry's sake.

I don't fully agree with your points here, but I still think that generating text is a good idea for a simple reason: it's much easier to cater to both preferences by making a helper library that provides an AST and generates text, than the other way around.

@cartermp
Copy link
Member

cartermp commented May 2, 2020

@Tarmil Yeah, file ordering does limit what you could do with F# in that way. I think that scenario in particular would be confusing to never enable, but without doing something special like treating the file and the generated file as if their constructs were recursively declared, it would be the way things are.

Another thing to consider is what supporting allowing one generator to depend on the output of another would look like. This implies another form of ordering, which I'm not particularly fond of given how top-down ordering is already difficult for beginners to grok.

@charlesroddie
Copy link

charlesroddie commented May 3, 2020

Interesting point about file ordering @Tarmil .

Type providers have better safety than source generators as you can provide them with the input directly. They don't analyze your entire source (unless you are crazy enough to point them at your .fs files) and you only use the results in the places you specify. They suit F# as a safer, more explicit language.

Enhancements to type provieers mentioned here:

  1. Making them easier to write. (AST helpers? A type provider to generate an AST from a string @praeclarum ?),
  2. Performance in IDEs, including compiling only when needed.
  3. Generate types from types (@kerams). This would deal with some of the cases here, in particular serialization (to replace reflection) including protocol buffers.

How much would remain if this work were done?

@charlesroddie
Copy link

charlesroddie commented May 3, 2020

Storyboard support and XIB support and XML

Here it's a matter of interop because the economics don't support F#-specific solutions for everything.

Can type providers be used in C#? Imagine we relegate erasing type providers to a historical footnote. Then you can use them by referencing F# projects. Could they be used directly? For example you write some annotation [TypeProvider(TypeProvider,TypeOrStringToAnalyze)] in a C# project. Then a C# source generator looks at it that automatically gets the type provider to generate the type, which gets compiled to IL and referenced in the source generation step.

Can C# source generators be used in F#? Say you have ProtoBuf generator which takes the source for a type as input. Then from F# you have a type, compile it to IL, decompile it to C#, feed it to the source generator, get enlarged source as output, compile it to IL, and reference it. Feasible or too many steps?

I agree with @praeclarum that we need to think hard about how people using these language features can create .Net solutions rather than language-specific solutions.

@7sharp9
Copy link
Member

7sharp9 commented May 4, 2020

Im happy Myriad was mentioned here, feel free to add any ideas, improvements, ideas etc to the issues: https://github.com/MoiraeSoftware/myriad

@Swoorup
Copy link

Swoorup commented May 4, 2020

One reason, I dislike text based generation is adding complexity of multi-pass compilation and adding to the performance bloat in the compiler. Another reason, I dislike is, F# being a white-space sensitive language, it will probably make incredibly harder to get source generation right. I feel Myriad provide a good base to build features on top of it and suggestion to pass types to TP is the way forward.

@7sharp9
Copy link
Member

7sharp9 commented May 4, 2020

When I was building Myriad I did think of removing the quotation aspect of Type Providers and instead have just AST input rather than quotations. I think quotations not quite mapping 1 to 1 over the F# language can be a big limitation with regards to generating source, especially as quotations transform the input into a quoted from and cannot represent types either. Myriad could be called as part of the compile chain as there is an input into the compiler accepting an AST. Currently it can be integrated via MSBuild or by calling it direct with the CLI tool.

@Thorium
Copy link

Thorium commented May 7, 2020

When I was building Myriad I did think of removing the quotation aspect of Type Providers and instead have just AST input rather than quotations. I think quotations not quite mapping 1 to 1 over the F# language can be a big limitation with regards to generating source, especially as quotations transform the input into a quoted from and cannot represent types either. Myriad could be called as part of the compile chain as there is an input into the compiler accepting an AST. Currently it can be integrated via MSBuild or by calling it direct with the CLI tool.

I hope we could deal with existing ASTs instead of creating more and more of them.
fsharp/fsharp-compiler-docs#938

@7sharp9
Copy link
Member

7sharp9 commented May 7, 2020

There the typed and untyped last, the typed AST is not user constructible so it only really leaves the untyped one, which also has an entry path into the compiler and fantoms for turning back into F# source.

The typed AST is only really currently useful for transpiling an F# cast to another language as it has no API for modification or construction.

You can convert a quotation back to an AST other the process is not perfects as data is lost in the initial quotation literal process, programmatic quotation construction does not cover the whole F# language either so not ideal.

@yatli
Copy link

yatli commented May 7, 2020

Now that we're talking about AST... :)
I did a lot of C# expression tree (and the lambda syntax sugar) metaprogramming, and really wish the F# counterpart is on par with that.
A full-fledged AST, convertible with quotations, can compile and run, would be even more useful than source generators in my opinion:

  • Runs faster, and does it right (no parser involvement and no syntax issues)
  • Composable (think about embedding a piece of code in a source generator? escapes? indents? name scopes? side effects? all kinds of stuff. With AST it's just children, dictionaries etc.)
  • Usable at both design-time and run-time. (a source generator in the runtime, without proper AST, is a security issue, and prone to injection attacks)
  • Easy to type-annotate.

@yatli
Copy link

yatli commented May 7, 2020

btw, a lot of projects (MS Bond, protobuf, GraphEngine etc.) already have this code generation workflow by using custom MSBuild tasks. So I don't think the workflow is something new, but how the mechanism generates executable bits (source? AST? etc.) is to be carefully designed.

I wrote both versions of codegen for GraphEngine (it generates millions of lines of code for modeling strongly-typed knowledge graph) -- the first version is done in C# with string concatenation and the coding/debugging experience is horrible.

In the second version I came up with something pretty unique -- it's a meta-template system that generates code generators. I made rules that the meta templates must compile fine themselves, with the "holes" properly annotated. The meta generator then transforms the meta template into generators, which takes user input, and generate source code.

If F# is indeed going to implement the source generators, I wish it is not a CodeDom style API compile: string -> Assembly but more like the "meta generator" :)

@7sharp9
Copy link
Member

7sharp9 commented May 7, 2020

@yatli Have a look at Myriad, I recently updated the readme to be a little more descriptive.

@yatli
Copy link

yatli commented May 7, 2020

@7sharp9 thanks! I surfed through the README.md and also your blog.
It takes types as input, and use plugins to generate AST and then translate back to source code, right?

@Thorium
Copy link

Thorium commented May 7, 2020

The typed AST is only really currently useful for transpiling an F# cast to another language as it has no API for modification or construction

Could some kind of typed AST API construction be the right way to continue? (I'm sure this needs to be approved in principal first, not just a new PR.) The untyped AST needs quite lot of work to be used (and is potentially even a bit dangerous: the parser should really understand everything, as F# is not side-effect-free language).

@7sharp9
Copy link
Member

7sharp9 commented May 7, 2020

@yatli Technically it takes an AST as input, then creates an AST fragment from it then translates that to source code, which is included in your project. It could take other things as input too, its just the current API is an input file/AST.

@baronfel
Copy link
Contributor

There are issues in prominent projects (Like PInvoke.Net) where the authors are considering moving to a primarily Source Generator-driven workflow, which is concerning because those libraries then become inaccessible to F#.

@AraHaan
Copy link

AraHaan commented Mar 18, 2021

I agree, source generating p/invoke calls (like MiniDumpWriteDump for example) is better than manually writing the code and getting it all wrong, especially when you need to maintain an MINIDUMP_TYPE enum manually and it's far better to just source generate that enum instead because you never known when a newer version of dbghelp adds more values to that enum and then breaks your mini dumping with error codes and stuff, or if they ever decided to change the value of some flag on the windows sdk, however your c# code does not change said value and it causes conflicts that can be avoided with source generating it all instead.

However I do have a nit about source generating MiniDumpWriteDump at the moment and that is because:

  • the generated code does not call and return the win32 error code instead of the bool result that can be discarded so that way it does error code checking on api functions that has SetLastError set to true on the source generated wrapper method code to wrap the p/invoke call.

@davidglassborow
Copy link

A good discussion on source generators from the author of Dapper https://blog.marcgravell.com/2021/05/is-era-of-reflection-heavy-c-libraries.html

@pbiggar
Copy link

pbiggar commented Aug 20, 2021

I've been moving to .NET6. One of the major use cases of .NET6 for me is support for AOT compilation of Blazor to WebAssembly. As I understand it, this is currently blocked on F# lacking source generators for System.Text.Json, as reflection cannot be used with AOT compilation of Blazor.

I feel that even if F# does not get source generators, there needs to be some interop story for using .NET apis which use source generators.

@AraHaan
Copy link

AraHaan commented Aug 20, 2021

I agree it would suck to require reflection in F# but at the same time for C# and VB to recommend moving away from it due to Source Generators providing a performance improvement due to using source generation to replace the need for reflection.

After which time I feel like everything with reflection other than checking of a type inherits from another type within a specific assembly and System.Reflection.Assembly should be deprecated. But even then I think that can be done without reflection too.

@dsyme
Copy link
Collaborator

dsyme commented Aug 22, 2021

@pbiggar To be honest I've historically been sceptical of attempts to make .NET "AOT static" and especially when this is placed on the critical path for delivery. There are just so many .NET practices that require JIT code generation and/or a modicum of reflection - LINQ, generic virtual methods, a bunch of reflection things, some things in FSharp.Core. I've seen related agendas for .NET static compilation fail to deliver several times - delivering buggy, incomplete versions of things that look like .NET but aren't. In my eyes these shouldn't even be allowed to use the name .NET unless they actually implement the .NET standards (or minimal-expectations) including reflection.

That said, I understand the .NET team are taking another stab at this, I'm not really following the technical details though. But the number of F# scenarios where static compilation to WebAssembly without any runtime codegen or reflection is critical seems small (and the number of C# scenarios where programmers don't end up using some technique or another requiring reflection is also small).

That is, I don't currently see anyone across the F# world signing up to deliver this goal of end-to-end JIT-free, reflection-free execution of real-world F# code using rich libraries - both because it's difficult and and rarely necessary. The AOT compiler you're referring to does give some of this - and it's been available on Mono for a while including iOS execution with reflection - but adding the requirement to remove all libraries that use reflection is too onerous I think.

So in this case I just don't think I'd encoourage F# users to sign up to that pat of the .NET 6 story - or if you do the responsibility is to understand that there are likely to be blocking points like the one you mention along the way, which require either C# code, or using a different reflection-free Json library.

@dsyme
Copy link
Collaborator

dsyme commented Aug 22, 2021

there needs to be some interop story for using .NET apis which use source generators.

For the forseeable future that story will be "add a C# project and generate the source there".

@charlesroddie
Copy link

@dsyme There are very good reasons for the aot and linker friendliness plans in dotnet, so it's not reasonable to insist on the original spec without considering the benefits and costs of the changes.

To deploy apps that run on end-user systems you want some form of AOT to avoid slow startup and JIT-lags and to avoid deploying code that is trivial to decompile. MonoAOT (for ios/android/wasm) is tolerant of reflection, but there is a lot of work being done on linking which could change this. NativeAOT is coming with much better size/performance characteristics, and this is less tolerant (with an optional reflection-free mode for minimum size).

Reflection is overpowered, giving the ability to break any number of constraints. A lot of typical reasoning about F# code, particularly about types, has the qualification "in the absence of reflection". Therefore removing reflection creates a stronger form of F#, a safer version where constraints expressed by types are respected. Reflection usage outside of compilers is a code smell, and investigation into F# incompatibilities with corert/netnative picked out parts of F# that are badly written.

These two aspects go together: the reason that reflection is performant-AOT-unfriendly and linker-unfriendly is that it limits the ability to reason about code.

Overall the aot/linker moves (of which source generators are a part) are creating a superior form of dotnet, with performance benefits to end users but also enforcement of code constraints. It is a wide-ranging attempt to do dotnet properly and F# code should be the most naturally compatible as it favours type safety and explicitness over "magic".

@dsyme
Copy link
Collaborator

dsyme commented Aug 22, 2021

so it's not reasonable to insist on the original spec without considering the benefits and costs of the changes.

I do insist on conformance to the estabilished specs for .NET - both in ECMA and certainly since .NET Core. .NET isn't a play thing for its engineers to make what they want it to be on each iteration, it's a set of de-facto and actual standards.

My fundamental belief is that .NET is simply not a statically compiled system (instead it's "majority statically compiled with occasional JIT or slow fallback-code") and anyone who thinks it can be is making a highly expensive category error. The .NET 1.0/2.0 addition of both reflection and generic-with-code-expansion-and-generic-virtual-methods (and the very widespread use of both of these) ensured that it never will be.

Now, maybe "this time it's different" but, to put it another way, the costs are indeed not being properly considered. The costs of breaking established specs are vast, staggering, colossal. Probably about a trillion dollars of IT investment sits on top of .NET in one way or another - much of it on .NET Core. For example, there is zero costing for modifying F# for this, let alone all its libraries, let alone the broader universe of .NET libraries (e.g. the cost of revamping FSharp.Core to be reflection-free, possibly breaking into pieces, annotating it... ) As an aside, while I'm no fan of reflection from a soundness perspective, the theoretical benefits of F# for this kind of work are too theoretical for me to find compelling, given there's no costing here.

To deploy apps that run on end-user systems you want some form of AOT ...

Sure, it's useful, but I still believe it's basically a pipe-dream to ever think you can have the majority of .NET coding be without JIT and reflection. It ain't going to happen. At best you end up with some niche tool for constrained-coding-app-on-device scenarios. However the programming model ends up so compromised (whether attributes, annotations, code-generation-tools, configuration files) beyond hello-world demonstrators that it's likely just some weird version of .NET that relatively few people know. At best, it bi-furcates the .NET ecosystem (as we saw with .NET for Silverlight, Windows Phone, .NET Native, whatever ....).

Anyway, I want to emphasise that JIT-free .NET execution hasn't featured in our planning for people employed directly to work on F# at Microsoft.

  • We love FsBolero etc. but WASM deployment of F# is not a thing of any particular focus for us. We're happy for others in the community to own this topic.

  • Static .NET native code generation is critical for us in the "crossgen" manifestation of the tooling (which implements complete .NET and we rely on for good compiler startup) but JIT-free deployment of F# code is of no specific signficance to us.

  • I'd imagine the teams who implement AOT must in some sense make it work for F# even in non-JIT mode (since F# generates .NET code, and they must run all .NET code). I'd imagine that includes FSharp.Core but if technical modifications to FSharp.Core are suggested I haven't heard anything about that.

More positively - from the F# perspective I'd be really grateful if the community lead on this - for example, why not create a tooling RFC under https://github.com/fsharp/fslang-design/tree/main/tooling where you can together document and record the whole matter of F# and jit-free AOT? That would create a single point of reference. A comparable RFC is this one: https://github.com/fsharp/fslang-design/blob/main/tooling/FST-1033-analyzers.md

@AraHaan
Copy link

AraHaan commented Aug 22, 2021

A comparable RFC is this one: https://github.com/fsharp/fslang-design/blob/main/tooling/FST-1033-analyzers.md
🤢 Why not change that design to instead eventually integrate all of the C# code analysis into Roslyn as Microsoft.CodeAnalysis.FSharp, and all the other things that then the F# compiler could itself use and then it could be less expensive to implement source generator support?

Also I disagree, source generator support should not break anything at all, only just gives a specific set of developers (myself included) another tool in our tool chest. Besides I do not think people wanted FSharp.Core to stop using reflection but rather used it as an example where some of it's reflection can be replaced better and be more performant than just using the reflection. I have put in place (even though I am an open source developer relying on my (currently 5$/month) donations to write and ship complete and open source .NET projects (all targeting .NET Standard 2.0) that can be used for everyone (even companies) just so they do not need to reinvent the wheel and end up doing it themselves which saves them time and money already (installing a nuget package ~10 seconds of their time nowdays). While I understand your argument that it can cost billions of dollars in costs, look at the open source developers like me who rely on donations and do not explicitly on our licenses say "To use this open source code you must donate at least x$ per month." just because we don't want to chase our user-bases away because they might be other people like me, making their own libraries that depends on some of my code to do their stuff which then they ship as a package (yet again) for others to consume as well.

At this point in time I think the most the "money" argument is just that, an argument to try to justify not giving someone a tool in their toolchest to optimize for performance. I remember when source generators did not exist at all for Visual Basic and C# and look at the performance tank back then, System.Text.Json was over 30x slower than it is now, ASP.NET Core was about 40x slower in some reflection spots, System.Windows.Forms had the same issues as well, WPF also (all written in C# with the exception of WPF that contains some C++ bits). My point is, if they had that argument like you guys claim when specing for C# and Visual Basic source generators on roslyn back then chances are Source Generators would have never existed there and then those improvements would have never been made, ASP.NET would have been at the same performance level as the one from before Source Generators became an actual thing. I argue that source generators are being used to actually augment code that can be used in the place of reflection in some situations (if not all of them).

Anyway, I want to emphasise that JIT-free .NET execution hasn't featured in our planning for people employed directly to work on F# at Microsoft.

While that may be true, not all .NET Developers (even in the @dotnet organization and maybe also in the @fsharp organization here on github) do not work for Microsoft. Not all of them thing that JIT-free .NET execution is a must for some targets (for example what if you want to use F# to make an iOS application for an older iPhone that explicitly bans any form of JIT in order to run any apps) While iOS 15 might allow it somewhat (unless you apply to get it into the Apple App Store as it's still banned there) Some companies still face what us non-company bound programmers face everyday, WHEN you want to actually ship to the Apple AppStore (that means you cant use reflection anywhere in the execution phase at runtime and no JITer must be invoked as well to be accepted) making your application denied and a nonrefundable loss of 100$ each time to try to push that application (written in F#) up to the AppStore for Apple to look at and then use it to fund the whole project. Some open source developers actually do that with the C# made projects that made it into the AppStore thanks to these few things:

  • They provide their own runtime they made from scratch that does not implement reflection api's at all (they throw PNSE)
  • Every piece of code is then tested for if they throw, if they throw then they know it uses reflection at runtime somewhere and so they remove it using source generators to keep the same functionality intact.

After that is all said and done they eventually push it to the AppStore and apple is then happy and approves it. The same is done at the company level. (You think none of the iPhone apps in the AppStore is not made in C#, I know for like Git2Go that was on my iPhone 5s that used libgit2sharp so that tells you something, they used C# and I think it. Other git based phone applications are also made in C# say for example the Github app for iPhones I think is made in C# and uses libgit2sharp as well. And ironically @github is owned by @microsoft.

I think that applications for iPhones for example should not be limited to just C#, or Visual Basic.NET, but also include F# but for real world applications, source generators is a must. Hello-world applications and foo-bar applications are just examples of valid real world applications, but they do not properly benefit being source generated at all just because they normally do not use any reflection anyway and so no gains are made at all if you do or do not use source generators there. Infact it might be why you guys are thinking that it's not worth it but it's not entirely true, besides on the runtime side (the runtime team for .NET which works at @microsoft) also think that any place that does not use reflection in the runtime is a critical performance increase that should be made because then all users of .NET would be happy for a mor performant .NET. Heck that was the main argument between .NET Core (back when .NET Core was not rebranded to .NET) and .NET Framework back then because .NET Core optimized .NET Framework apis further and with it made performance increases of .NET Framework 4.x and made a valid reason for non-company based programmers, and company based ones to jump into .NET Core and use that instead of .NET Framework (where performance is critical because you process a lot of data and need good performance to reduce CPU load while not reducing the rate of processing data so then everyone is happy). This is why everyone uses things like System.Memory for Span<T>, Memory<T> and of the like for performance due to less memory of their code being wasted, faster memory allocations, etc and when you combine that with source generators with reflections you can very easily see an 90% improvement in code (I sometimes see cases where I consume an closed source library in my projects that is written back in the days before System.Memory was a thing, and source generators was a thing and using Mono.Cecil to decode it to IL and patch it accordingly to use System.Memory and replace all usages of reflection that is for calling private or internal members for some things that can now be accessed in public versions of those members) actually seen about 90% improvements in execution times.

I would also like to take some time to mention Rick Brewser (creator of Paint.NET) who might agree with me on all of this @rickbrew.

@charlesroddie
Copy link

Discussion has moved beyond source generators so I will just make a brief reply to @dsyme with links.

why not create a tooling RFC under https://github.com/fsharp/fslang-design/tree/main/tooling where you can together document and record the whole matter of F# and jit-free AOT

Last writeup from 3 years ago is dotnet/corert#6055 (comment) . NativeAOT will be more compatible now (@kant2002 has tried it recently) and at some point I will do an up to date summary. The largest issue is string functions documented here: #919 . That will unblock running the test suites (dotnet/fsharp#5340).

F# mostly supports AOT by virtue of the compiler being mostly sensible. I think this can largely done by the community with some helpfulness from the F# team: willingness to take small behavioural changes, treat the ability to run performance F# apps on user devices as having some importance, not repeat the F#/UWP debacle.

My fundamental belief is that .NET is simply not a statically compiled system instead it's "majority statically compiled with occasional JIT... It's a pipe-dream ... majority of .NET coding be without JIT and reflection... the programming model ends up so compromised

F# apps are already running without JIT and with minimal reflection across iOS, Android, Windows and Mac devices, via MonoAOT and netnative. Very soon they will be running on web browsers via wasm. Really almost no constraints for writing AOT-friendly apps in F#. The nugets we want to use all work, and one just needs to take care about string functions in practice. You will get much more potential for nuget incompatibilites resulting from source generators.

@charlesroddie
Copy link

I would find it helpful if people started to link examples where
they encounter C# source generators in practice in nuget packages they wish to use
where there's really no good easy replacement for the technique
where they think the use of source generators will "stand the test of time"

Regex source generators

Usage example: https://www.meziantou.net/regex-source-generator.htm

// The Source Generator generates the code of the method at compile time
[RegexGenerator("^[a-z]+$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)]
private static partial Regex LowercaseLettersRegex();

public static bool IsLowercase(string value)
{
    return LowercaseLettersRegex().IsMatch(value);
}

@kasperk81
Copy link

[EventSource]
[GeneratedRegex] // renamed from [RegexGenerator] in 7.0 rc1
[JSExport]
[JSImport]
[JsonSerializable]
[LibraryImport]
[LoggerMessage]

are provided by runtime libraries, and they avoid aot-incompatible reflection apis.

@roboz0r
Copy link

roboz0r commented Jan 23, 2023

I looked at some of the samples for C# source generators and they seem to be a C# and not a .NET feature as in the sample implementations they are literally doing C# source string concatenation. With that in mind I see it as a fool's errand to attempt to expose source generators without ever having to look at C# code.

The two general cases I see is code augmentation of partial classes (e.g. automatic json serializers) and total generation of values that implement a known type signature (classes / interfaces / functions).

Total Generation from Type Signature

For this case I think we can take some inspiration from the way Fable does native (JavaScript) interop:

// The member name is taken from decorated value, here `myFunction`
[<ImportMember("my-module")>]
let myFunction(x: int): int = jsNative

[<Import("DataManager", from="library/data")>]
type DataManager<'Model> (conf: Config) =
    member _.delete(data: 'Model): Promise<'Model> = jsNative
    member _.insert(data: 'Model): Promise<'Model> = jsNative
    member _.update(data: 'Model): Promise<'Model> = jsNative

jsNative is a special value with no implementation but instead signals to the compiler to replace the implementation with generated native code. In the .NET case, the "native" code would be IL and F# already supports inserting IL as an implementation. Normally it is used extremely sparingly but I see no reason the use couldn't be expanded to inject IL generated from source generators.

I would propose that csNative becomes a similar special value that allows the F# compiler to insert an implementation of IL based on the output of a C# source generator. Hopefully it wouldn't be too difficult to forward the attributes and type signatures to C# (Roslyn?) and subsequently dissect an assembly to insert the IL.

For the LibraryImport case in C#:

[LibraryImport(
    "nativelib",
    EntryPoint = "to_lower",
    StringMarshalling = StringMarshalling.Utf16)]
internal static partial string ToLower(string str);

would become:

[<LibraryImport(
    "nativelib",
    EntryPoint = "to_lower",
    StringMarshalling = StringMarshalling.Utf16)>]
let internal ToLower(str: string):string = csNative

The RegexGenerator case would be similar except that the generated IL is a class that inherits Regex instead of a function:

// The Source Generator generates the code of the method at compile time
[RegexGenerator("^[a-z]+$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)]
private static partial Regex LowercaseLettersRegex();

becomes

[<RegexGenerator("^[a-z]+$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)>]
let private LowercaseLettersRegex(): Regex = csNative

Code Augmentation of Partial Classes

In this case as @dsyme suggests here we have a ready mechanism with type providers. e.g. for STJ source generation

type internal SourceGenerationContext = CSharpProvider<"""
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(WeatherForecast))]
internal partial class SourceGenerationContext : JsonSerializerContext
{
}
""">

The challenge here comes where WeatherForecast is defined in the same project as SourceGenerationContext. In this case you would need to implement switching between F# and C# compilation with shared assembly information but for the first-pass implementation I don't think that's necessary.

A much simpler case would be where WeatherForecast is defined in an external assembly so all the type information is already available or where WeatherForecast is also defined in the C# as inner classes:

type WeatherForecastOuter = CSharpProvider<"""
public class WeatherForecastOuter
{
    public class WeatherForecast
    {
        public DateTime Date { get; set; }
        public int TemperatureCelsius { get; set; }
        public string? Summary { get; set; }
    }

    [JsonSourceGenerationOptions(WriteIndented = true)]
    [JsonSerializable(typeof(WeatherForecast))]
    internal partial class SourceGenerationContext : JsonSerializerContext
    {
    }
}
""">

We could even embed C# syntax highlighting and auto complete as is done for html templates with Fable.Lit but that's certainly not required and is probably mostly an editor-level feature rather than a compiler feature.

@mrange
Copy link

mrange commented Jan 23, 2023

@roboz0r - I admit I don't fully understand your proposal so I am assuming you already considered it but here comes my question:

With C# source generators the C# classes are generated as partial classes which allow the user to inject behavior using partial methods. So if a source generator generates C# and it's injected into F# as IL how can I use partial methods to inject behavior in the generated code?

@roboz0r
Copy link

roboz0r commented Jan 23, 2023

@mrange The idea is that anything to do with partial methods or classes would have to be included within the CSharpProvider<C# string>. Until F# supports partial in general I don't think it can be any other way and trying to design partial into F# at the same time as source generators seems like a mistake.

I think the order to create something like this would be:

  1. Consuming source generators that involve entirely C# code with no dependencies embedded in an F# project

In this first case the embedded C# is entirely unaware that it lives within an F# project, and F# has no input to what happens in C#-land besides expecting that some IL will pop out eventually. The "Total Generation from Type Signature" case is a bit of a departure from this ideal but considering that type signatures and attributes are entirely static they should be far easier to forward / transpile into C#.

  1. Source generators that take dependencies / type information / partial implementation from separate projects / dlls
  2. Source generators that take dependencies / type information from the current project
  3. Add partial to F# and allow partial implementations to cross the boundary from the current project

2, 3, and 4 could be a long way away and potentially in a different order but if 1 can be completed that gives us a way forward to consume the most obvious use cases for source generators so far without major changes to F#.

@jkone27
Copy link

jkone27 commented Jun 8, 2023

is this coming soon? Since the big support now for source generators for many things in C#, like e.g. json serialization for example, https://github.com/amis92/csharp-source-generators should be good to have this compatibility in F# too?

The two concrete use cases that immediately jump out to me are:

@vzarytovskii
Copy link

vzarytovskii commented Jun 8, 2023

is this coming soon? Since the big support now for source generators for many things in C#, like e.g. json serialization for example, https://github.com/amis92/csharp-source-generators should be good to have this compatibility in F# too?

The two concrete use cases that immediately jump out to me are:

No, it's hasn't been planned yet, and will definitely not going to make it in 8.

Don't get me wrong, we want to have it, but it's a huge feature.

Designing it alone will probably take months, since all source generators rely on roslyn (and C#-only features), and will likely involve us using roslyn one way or another.

We will also have to sort VS story out, they're slow enough natively in roslyn, and we will need to account for it too.

We're not even sure how to approach it, since there are multiple existing flavors as well as new versions which are being designed now.

Latest thoughts are here: dotnet/fsharp#14300

@jkone27
Copy link

jkone27 commented Sep 30, 2024

This one would also be great to interop with virtual actor framework Orleans, which would make F# interesting for BEAM/Elixir/Erlang otp/Gleam users ? From what i undersand Orleans relies on source generators for grain interfaces > https://learn.microsoft.com/en-us/dotnet/orleans/grains/code-generation?pivots=orleans-7-0

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

No branches or pull requests