-
Notifications
You must be signed in to change notification settings - Fork 787
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
Reflection free code gen #12960
Reflection free code gen #12960
Conversation
Could this effort be paired with emitting specialized |
@jkotas writes
|
I'd personally keep it separate features/contributions. |
This is really cool! If you want to verify that your app is safe for AOT and trimming, the linker can produce trim warnings whenever there is code that is not statically understood. Instructions are available at https://docs.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-self-contained Looking forward, I think the biggest trimming concern that I would have in F# is serialization. Unfortunately, most serialization patterns are simply too complex to statically annotate. The recommended substitution is to use a source-generated serializer, like the JSON source generators or my serde-dn library. Unfortunately, those are reliant on C# source generators in their implementation (serde-dn works by having the source generator implement the serialization interfaces automatically), so some way of providing similar functionality in F# would be very interesting. |
The clearest parallel for F# to source generators IMO are generative Type Providers, which are compiler-hosted components that emit code mid-compilation. There are some rough edges around the UX for them, and debugging the generated code as you are writing one of them can be a little gnarly, but the overall mechanism is pretty well understood. |
Yup, I'll just speak for myself here since I don't know exactly what the flexibility around JSON source generation is: serde-dn operates fundamentally around a series of interfaces that ensure type safety of serialization, ease of customization, and flexibility of format. In general, the preferred pattern for types that appear in source is to implement the appropriate interfaces on the types directly. There are always cases where you can't implement an interface directly (like on a type you don't own) and there is a system for accommodating that (generating wrapper structs), but the model really prefers implementing interfaces directly when possible. My understanding is that type providers wouldn't provide that capability, but I might be out of date here. |
Sort of. One important thing to understand is how FSharp.Data achieves strong typed data access (including serialization/de-serialization) without using reflection or code generation. This quite counter-intuitive because the erasing/façade techniques are not possible in C#. so it can be hard to grok from the C# perspective (or indeed any language that doesn't support either erased types or type providers). In detail - when FSharp.Data is pointed at an external CSV, JSON or XML schema it is at entirely reflection-free at runtime. It also does not really do source generation. How does this work?
The equivalent C# feature would be source generators with a Erasing type providers have some issues (no runtime types, have to be careful across multiple assemblies), but being reflection-free-at-runtime and tree-shaker friendly is not intrinsically one of them. It will still be important to make sure that FSharp.Data is really tree-shaker friendly in practice, and this will need to be put under test. The main practical problem is that these require currently an explicit schema (normally via a sample) and so can't easily be used for scenarios where schema is being implied by the types defined in the current assembly (see fsharp/fslang-suggestions#212) Note erasing isn't critical in achieving the above - a generative type provider can do the same - but it's harder work because the code to convert from the heterogeneous representation to the type-specific one must be generated - this is a nop for erased provided types. To summarise, I think people mistakenly believe that FSharp.Data is doing reflection at runtime - it isn't. Anyway, realistically I'd expect it to play out like this:
|
TBH I think erased type provides are also pretty feasible - see notes above on FSharp.Data today. |
Co-authored-by: Ilja Nosik <[email protected]>
@dsyme Very cool. Yup, FSharp.Data looks like it should work just fine with trimming. |
Linking this: fsharp/fslang-suggestions#919 (comment) |
@0101 asked what next steps should be here I think the first thing to determine is "does it matter if some things in FSharp.Core use reflection, and what should we do about it"? Specific things
Overall it would be great to get community and expert advice on this, and also get an understanding for what will go wrong if it doesn't work. |
I guess there are several parts of this - reflection-free codegen by compiler itself ("%A", quotations, dynamic imlpementations, etc), reducing/reviewing reflection in FSharp.Core and making FSharp.Core adjustments for easier trimmability. |
Yes - and note we can also bank this PR and move on to FSharp.Core and actual trim testing next - I think we're satisfied with it? |
Reading custom attributes is compatible with trimming and AOT compilation. (I would not call it "reflection-free". Custom attributes are reflection by design.)
RequiresUnreferencedCodeAttribute allows static analyzers to provide better diagnostic messages. For example, if you do not sprikle RequiresUnreferencedCodeAttribute over If you sprinkle RequiresUnreferencedCodeAttribute over
Roslyn itself is not aware of RequiresUnreferencedCodeAttribute.
We have Roslyn analyzer that checks RequiresUnreferencedCodeAttribute and friends in the code that you are compiling from source. This analyzer is good for dev inner loop. You would have to reimplement this analyzer for F#. The IL linker and AOT compiler have same analyzer that works on the IL of the whole app. You will get this analyzer for free for F#. |
I'd say we should merge this first set of changes and then continue working on corelib from there. |
There is a strong desire in the .NET and F# community to set up .NET and F# to be more friendly to native compilation tool chains and "tree shaking" compilation.
See also #12819 and #11891
Historically this has been a tricky area for both F# and .NET generally, some of the history is mentioned here: fsharp/fslang-suggestions#919. Looking forward, there are recent strong efforts in this area led by @jkotas and others.
Now, this is as an area where "small but deadly" things can kill you - one generated construct that a native toolchain can't handle. or one reference through to some large chunk of library code that gets linked in. Problems we definitely know of are
F# currently implicitly emits implementations of
ToString()
for records, unions and structs (alsoget_Message()
for exception definitions) and these in turn usesprintf
with%A
formatting. F# also emits DebuggerDisplayAttribute for union types that do likewise.Users may use
%A
formattingUsers may use quotations (either implicitly or explicitly)
The user may end up using libraries that use these constructs.
The proposal in fsharp/fslang-suggestions#919 is to address these by add a command line option
--reflectionfree
that adjusts F# code generation to both avoid the generation of the problematic constructs, and to give an error if%A
is used.Note that (1) is not a problem in FSharp.Core as the code generation is already explicitly suppressed in that case
(2) is potentially a problem - this PR removes a few explicit uses of
%A
in FSharp.Core(3) is probably better dealt with by the user simply avoiding quotations, but not banning their use outright.
Note that using this option would affect both the ToString semantics (no implicit implementation is given) and the debugging experience