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

Make F# string functions fast and AOT/linker friendly #919

Open
charlesroddie opened this issue Sep 7, 2020 · 56 comments
Open

Make F# string functions fast and AOT/linker friendly #919

charlesroddie opened this issue Sep 7, 2020 · 56 comments

Comments

@charlesroddie
Copy link

charlesroddie commented Sep 7, 2020

The need for AOT and linker support

.Net code is deployed to devices where performance - including startup time - and download size are important.
JIT is ruled out by this and may be explicitly prohibited (UWP and iOS) or result in unacceptably slow application startup (Xamarin.Android).

Current .Net plans (.Net Form Factors) involve a supported AOT option across .Net, with greater usage of linkers:

We will introduce analyzers that report errors or warnings for code patterns that are not compatible with the specific form factors.

We will introduce a linker compatibility analyzer to detect code patterns that cannot be reliably analyzed by the linker.

Source generators are going to be the preferred [reflection/runtime code generation] mitigation for AOT compatibility, same as for linker compatibility above.

F# string problems affecting AOT/linker support

F# Support lists incompatibilities with CoreRT and .Net Native. These pick out the aspects of F# that are likely to be problematic for any performant AOT and linkers. (Note: F# is generally compatible with current mono AOT but this is not performant, and compatibility may not include linking.)

The largest problem here is F# string methods:

  • F# string methods use reflection and codegen, so are slow and AOT and linker unfriendly (i.e. are liable to crash on runtime).
  • The variety of string methods is difficult to understand: it is hard to impossible for F# users to find out what string, ToString(), and sprintf %A do.

The offending part of FSharp is lacking in type safety, with everything done via casting, uses reflection without restraint, and encapsulates poorly (e.g. .ToString() methods in FSharp record and DU types just calling sprintf "%A" on the entire record).

Solution part 1: localize by generating ToString() overrides on F# types

We have effectively, on F# record and DU types (just check SharpLab to confirm this):

override t.ToString() = sprintf "%A" t

The sprintf "function" doesn't have legitimate access to the data needed to generate the string, so has to use reflection to get the structure.

Instead this method should be compiled:

type DU = | Case0 of int | Case1 of Record | Case2 of obj

// A compiled version of the following should be generated:
override t.ToString() =
    match t with
    | Case0 i ->
        // I.e. "Case0" + i.ToString() + ")"
        // The inner string results from CompiledToString<int>(i)
        "Case0(%s{i.ToString()})" // i.e. "Case0" + i.ToString() + ")"
    | Case1 r -> "Case1(%s{r.ToSting()})"
    | Case2 o ->
        // if we absolutely need to preserve backwards compat
        "Case1(%A{o})" // i.e. "Case1(" + FSharp.FormatObj o + ")"
        // otherwise
	"Case2(%s{o.ToString()})" // i.e. "Case1(" + o.ToString() + ")"

Note that once this is done, CompiledToString does not need to know how to print records and DUs.

Solution part 2: compile

A method (represented above as pseudocode CompiledToString<'t>) should be created to generate compiled code for any concrete type 't, to be used in place of the current dynamic sprintf code where possible.

Where the method sees only obj it could preferably use .ToString(), or else use a very light form of reflection, making sure that codegen is not used.

Solution part 3: integrate

We need to decide where to use the method CompiledToString<'t>:

  • String interpolation.
    This has so much nicer ergonomics than sprintf that it is likely to replace sprintf in most F# code. If string interpolation is always compiled and ToString() methods work as above, then it will be easy for F# users to avoid AOT/linker incompatibilities.
  • sprintf: Compile sprintf where possible dotnet/fsharp#8621 . Note that we'd need to preserve some functionality for displaying records to deal with F# libraries compiled with earlier versions of F#: if they have override t.ToString() = sprintf "%A" t, then sprintf "%A" t can't use ToString().

It may be simplest to start by doing this for string interpolation as the first step, adding extra methods and preserving existing sprintf code, and afterwards migrate this work to sprintf.

TBD

Generics/inlines

@abelbraaksma
Copy link
Member

I like this a lot. Ideally, there wouldn't be any %A leftovers in the generated ToString code. In fact, I think generated code wouldn't need sprintf at all, nor interpolation. Both methods are very slow, and I think that the compiler has enough information to create better optimized code here.

Undoubtedly, there'll be corner cases where we may have to resort to old style reflection to prevent backwards compatibility issues, but hopefully not many.

@kerams
Copy link

kerams commented Jun 6, 2021

So if I understand this correctly (see below), parts 1 and 2+3 could be worked on separately, with completely independent RFCs?

Part 1 - Compiler emitting specialized ToString for records and unions, maybe using StringBuilder (or plain concatenation if it proves faster for a small number of fields), with hardcoded field/case names and calls to ToString on individual field values.

Part 2 + 3 - Compiler emitting specialized formatted printing instructions every time it encounters *printf and interpolated strings. So the compiler would no longer emit any Printf.*Format unless a string is explicitly annotated as such?

An official verdict would be nice here.

@charlesroddie
Copy link
Author

@dsyme can this be approved in principle and should it be two RFCs as @kerams suggests? Part 1 does have value independent of 2/3 and it should make the process of RFC speccing and implementation more efficient if split up.

@dsyme
Copy link
Collaborator

dsyme commented Jun 7, 2021

Part 1 - Compiler emitting specialized ToString for records and unions,

I'm ok with approving this though compat may be very tricky. I don't think it needs an RFC.

Part2 - F# string methods use reflection and codegen, so are slow and AOT and linker unfriendly (i.e. are liable to crash on runtime).

These are bugs in the .NET native systems - TBH I'm not a fan of buggy and incomplete implementations of things calling themselves .NET that are not .NET, and feeel it's a bit of a waste of time trying to chase them down. That said, it makes sense to make the codegen independent of printf for performance reasons alone. :)

@kerams
Copy link

kerams commented Jun 7, 2021

though compat may be very tricky

Can you please elaborate on this? I somewhat naively thought the only tricky part would be preserving the same indentation.

@dsyme
Copy link
Collaborator

dsyme commented Jun 7, 2021

Can you please elaborate on this? I somewhat naively thought the only tricky part would be preserving the same indentation.

The problem is that in theory the recursive structural calls for printing would have to do what %A does - which is not a simple thing at all.

We could consider a breaking change and simply call ToString() recursively. However that might bite us quite badly - e.g. %A handles deep and recursive object graphs

@Happypig375
Copy link
Contributor

We could use an IndentedTextWriter which handles indentation quite nicely.

@charlesroddie
Copy link
Author

charlesroddie commented Jun 7, 2021

We could consider a breaking change and simply call ToString() recursively. However that might bite us quite badly - e.g. %A handles deep and recursive object graphs

Hopefully some shortcuts that result in cleaner code will be allowed, but we probably would need some degree of going into object graphs. I believe none of the string printing functions ever had a spec so are full of inconsistencies and some of them might need cleaning up in the process.

For fidelity:

  • When it's a DU or record type or tuple then .ToString() is fine and won't be the causes of any change in behaviour. (Except for the case where .ToString on inner types is overriden - in which case the user most likely would prefer to use the override anyway.)
  • When it's an option then it will need special casing because of nulls.
  • voptions can be ToString()ed, provided that .ToString for voption is fixed to do the same as DUs.
ValueSome(0, (None:int option)).ToString() // "(0, )" - should be "ValueSome(0, None)"
  • Arrays need going into to print them nicely
  • Lists: can go into these or else adjust ToString() on these:
[Some 0; None].ToString() // "[Some(0); null]"
sprintf "%A" [Some 0; None] // "[Some 0; None]"
  • Tuples need going into:
(0, (None: int option)).ToString() // "(5, )"
sprintf "%A" (0, (None:int option)) // "(5, None)"

So sometimes ToString() will be good, and sometimes we need a "function" recString that is less clever than sprintf that recursively goes into collections and tuples and massages options. This could be fully compiled. But it's not essential as recString, while it would have type tests, will be reflection/codegen-free.

@kerams
Copy link

kerams commented Jun 7, 2021

@Happypig375, that part's not the problem. Every time a record has another record in a field, the indentation goes up.

type R = { S: int; Z: R option }
let r = { S = 3; Z = Some { S = 2; Z = Some { S = 5; Z = None } } }

r.ToString()
(*
{ S = 3
  Z = Some { S = 2
             Z = Some { S = 5
                        Z = None } } }
*)

So you can't just call ToString when printing the recursive field's value, because you'd get

{ S = 3
  Z = Some { S = 2
  Z = Some { S = 5
  Z = None } } }

I think the solution would be to provide another ToString overload where you'd be able to pass an indented text writer or string builder with indentation. That one instance would have to be threaded all the way down with increasing indentation. Now the big problem arrives when you use a record from an old binary where ToString is just sprintf %A. Not sure what to do then, possibly back to reflection.

@Happypig375
Copy link
Contributor

We can split line breaks and indent.

@kerams
Copy link

kerams commented Jun 7, 2021

That's going to work until you encounter a field containing a multiline string :).

@Happypig375
Copy link
Contributor

Then you have to think about whether sudden unindentations like that are visually appealing.

@dsyme
Copy link
Collaborator

dsyme commented Jun 7, 2021

Hopefully some shortcuts that result in cleaner code will be allowed, but we probably would need some degree of going into object graphs. I believe none of the string printing functions ever had a spec so are full of inconsistencies and some of them might need cleaning up in the process.

The behaviour of '%A' itself is now better documented, including some of the inconsistencies. https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/plaintext-formatting#a-formatting . However it's just not a spec that can be implemented in compositional fragments by using ToString() alone, since it takes a more holistic view (object graph, depth, indentation etc.)

However the problem isn't so much the spec/not spec, it's that code may depend on existing behaviour regardless. We hit this recently when we tried to change %A for RequireQualfiedAccess types to print TheType.UnionCaseA instead of UnionCaseA. This affected the ToString and broke some consumers who were relying on the simpler name for reflection or something.

That said I'm guessing that the compat issue would be manageable by making sure all primitive non-nested records/unions print identically. However if not, another option might be to have an attribute that guides the generation of a printf-free ToString. That makes it opt-in and avoids the compat issue.

@charlesroddie
Copy link
Author

charlesroddie commented Aug 22, 2021

@dsyme I'm ok with approving this though compat may be very tricky. I don't think it needs an RFC.

Can you mark the issue approved in principle then please?

That said I'm guessing that the compat issue would be manageable by making sure all primitive non-nested records/unions print identically.

Yes

@dsyme , @Happypig375 and @kerams have raised indentation as a difficult problem. I propose dispensing with line breaks altogether (for ToString() on record types). That eliminates the indentation problem. Alternatively, regularize the formatting so that indentation happens to entire output of an inner string.

// This logic is simple and only needs to know about ToString() on inner elements. (Use knowledge that the inner thing is a record to avoid extra brackets.)
{ S = 3; Z = Some { S = 2; Z = Some { S = 5; Z = None } } }

// Similarly this will also work without knowing more than ToString() on inners.
{   S = 3 // single-line
    Z =
        Some // multi-line inner record string, so put it all on separate lines and indent together
            {
                S = 2
                Z =
                    Some
                        {
                            S = 5
                            Z = None
                        }
            }
}

I prefer the first version with a single line.

@dsyme
Copy link
Collaborator

dsyme commented Aug 23, 2021

I don't really understand the technical proposal here. I believe replacing the default ToString implementations just won't be possible without breaking compat - that is, in some sense the existing object-to-layout engine in FSharp.Core will need to be used, though maybe not via %A. Perhaps there's a technique to do it where each F# type we care about implements an interface that provides the appropriate part of the processing.

May I ask - why not just have the user who cares it implement their own ToString on each type?

@knocte
Copy link

knocte commented Aug 24, 2021

why not just have the user who cares it implement their own ToString on each type?

Don, are you serious?

Just so that you know how painful this is, take a look at our workaround's diff:

nblockchain/geewallet@cd5ce12

(The important bits are in FSharpUtil.fs, the rest are a consequence of that, but as you see, we've had to implement a task for CI that detects when people use sprintf/printf/failwithf and pokes the developer to use our ugly SPrintF1,SPrintF2,...,SPrintFn workarounds instead.)

@knocte
Copy link

knocte commented Aug 24, 2021

(Not to mention that any F# dependencies we have, we gotta create our own forks that don't use sprintf/printf/failwithf...) It doesn't scale. We actually de-prioritised UWP because of this nightmare.

@dsyme
Copy link
Collaborator

dsyme commented Aug 24, 2021

Just so that you know how painful this is, take a look at our workaround's diff:

Well, it's painful because you were running on an incomplete .NET implementation. I'm not surprised you de-prioritised it.

More positively, I can see the advantages of allowing people a path to tighten up their code to be free of implicit reflection, much as with implicit boxing. What's a practical path to that without bifurcation? Would an assembly-level attribute that turns off .ToString() generation for record/union/struct and gives warnings on all problematic constructs (sprintf, %A in interpolated strings) be sufficient? Also is sprintf OK apart from %A?

@knocte
Copy link

knocte commented Aug 24, 2021

I think the world should have been telling the UWP team "this is not a valid implementation of .NET, please don't call it .NET".

Not sure why you deleted this part of your message, but the thing is, me, as a ISV, how do I deal with this issue? I cannot publish an F# app on the Windows Store. I don't care about UWP, I just need something that allows me to publish to the WindowsStore. And fixing this github issue would allow me to.

@dsyme
Copy link
Collaborator

dsyme commented Aug 24, 2021

how do I deal with this issue

I don't really know, sorry. My vague understanding is that other routes have since opened up for publishing to Windows Store using either JS front-ends (use Fable?) or sandboxed .NET Framework+WPF or something, but I could be wrong. Were your workarounds to lint for problematic constructs sufficient? If the use of %A is the only known problem then it sounds like they would be?

@knocte
Copy link

knocte commented Aug 24, 2021

Were your workarounds to lint for problematic constructs sufficient?

We're not 100% sure because we de-prioritized the effort. After committing the workaround commit you saw, we still had issues but didn't have time to investigate if they come from the fact that our dependencies still use printf/sprintf/failwithf, or from other AOT-related problems; because finding a workaround for the dependencies problems is too time consuming. However, fixing this github issue (to make FSharp.Core not use reflection for the simplest cases) would be much simpler.

@knocte
Copy link

knocte commented Aug 24, 2021

either JS front-ends (use Fable?)

Having to rewrite my frontend which already works in so many platforms (Android, iOS, macOS, Linux)* with a single codebase? Quite ironic that using .NET one cannot really support the Windows platform... thanks F# :'-(

* thanks to XamarinForms (soon Maui)

@cartermp
Copy link
Member

I cannot publish an F# app on the Windows Store.

This is the never-ending saga of the awfulness that is the Windows Store and Win10 apps 😢. It's unsurprising to see that 5+ years later, it's still as terrible as ever.

I hear that at some point they're going to allow actual, bona fide windows apps running against a real .NET implementation into the store. Or so I was told year after year, and yet that never came.

My honest advice is to abandon Windows app development for the store in general. It is not a stable platform.

@charlesroddie
Copy link
Author

If the criterion of making real apps is that the user's machine has to JIT compile the code, I am happy to deliver fake apps that load quickly. Even the F# team is not completely indifferent to the performance benefits of AOT.

Of late this thread has got confused and even if we ignore everything except the comments of @dsyme it is not easy to follow. Let me try to piece this together.

@dsyme I'm ok with approving this though compat may be very tricky. I don't think it needs an RFC.
@dsyme I don't really understand the technical proposal here.

Can we know how to progress? Either mark approved in principle (so work can be started with some flexibility in the degree of mimicing of existing behavior) or require an RFC if it needs fully speccing first.

@dsyme We could consider a breaking change and simply call ToString() recursively. However that might bite us quite badly - e.g. %A handles deep and recursive object graphs
@dsyme However the problem isn't so much the spec/not spec, it's that code may depend on existing behaviour regardless. We hit this recently when we tried to change %A for RequireQualfiedAccess types to print TheType.UnionCaseA instead of UnionCaseA. This affected the ToString and broke some consumers who were relying on the simpler name for reflection or something. That said I'm guessing that the compat issue would be manageable by making sure all primitive non-nested records/unions print identically.

Yes we are agreed on this. Printing of simple union cases is much more likely to lead to embarrased F# devs depending on the old behavior than, say, the formatting of records. So we should get a version working which requires enough fidelity to be a good representation and not to break users, but which doesn't require completely identitcal output in all situations.

@dsyme I believe replacing the default ToString implementations just won't be possible without breaking compat - that is, in some sense the existing object-to-layout engine in FSharp.Core will need to be used, though maybe not via %A. Perhaps there's a technique to do it where each F# type we care about implements an interface that provides the appropriate part of the processing.

It might be possible to achieve something close to an exact mimic of existing behavior by adding extra methods, but it reduces the chance that this ever gets done, which then reduces the ability of F# developers to write performant apps. We should allow any approach that keeps reasonable output, and is unlikely to break users, as we discussed above.

@dsyme
Copy link
Collaborator

dsyme commented Aug 24, 2021

I do go back and forth on this stuff. When optimistic I say "yes, sure", when realistic I say "no, compat".

The history of this goes back so far.

  • 2005: Compact Framework exists. People try to run F# on it. Is it important? Goodness knows. What a waste of our time

  • 2006: SQL Server has a trimmed down .NET with its own adhoc undocumented verification rules. We try to modify F# code to suit those rules, many ugly changes are made, some are still in our codegen today. Now absolutely nobody uses this.

  • 2008: Singularity/Midori exists internally, .NET IL but mscorlib is renamed to "corlib" and the entire BSL is different. We modify F# to accept this, it's a mess and no one ever uses it and we live with the legacy for years and years

  • 2009: .NET for Silverlight is all the rage. Mission critical, F# must support it. We put hacks and changes right through the system. Ugh

  • 2011: .NET for Windows Phone. Enough said

  • 2012-18: Various variations on UWP, .NET for Windows Store, Windows Runtime, .NET Native, Native for .NET whatever. On each iteration I get more and more skeptical.

  • 2015-18: .NET Core 1.x is a wildly trimmed out platform making life very difficult for us.

You can see why I sometimes a little of jaded of these "alternative .NETs" - these variations on .NET have caused 1000s of lost hours and huge opportunity costs for myself and the projects I've been associated with over the years. As an aside one of the amazing things about Mono is that it always did all of .NET (it took them several iterations for iOS though).

Anyway, this seems like a viable approach to me:

More positively, I can see the advantages of allowing people a path to tighten up their code to be free of implicit reflection, much as with implicit boxing. What's a practical path to that without bifurcation? Would an assembly-level attribute that turns off .ToString() generation for record/union/struct and gives warnings on all problematic constructs (sprintf, %A in interpolated strings) be sufficient? Also is sprintf OK apart from %A?

That is, a single assembly-level attribute [<SimplifiedCodeGen>] or [<ReflectionFreeCode>] or something that causes the compiler to turn off or simplify problematic code-generation and warn and warn and warn as much as you like. As far as I'm concerned that approach is perfect

  1. The community can own the spec of this, and I largely don't need to think about it.
  2. It's very easy to check that any changes to compiler behaviour are made conditional on the attribute
  3. It's simple to use
  4. Those using this can advocate for other people to use this switch in libraries you depend on

Sound ok?

@vzarytovskii
Copy link

vzarytovskii commented Jan 24, 2022

What would be the behaviour if we run on an older runtime, which doesn't have this flag?

I did not think about that when I wrote that. If exactly same code run on all platforms, then this is probable not an option. Would like that somebody educate me on compatibility for this solution.
Also the more I think about how to solve the problem without deep dive into compiler, the more I fall closer to Don's proposal. Probably I'm over-react to initial idea of implementing change in FSharp.Core.

We shouldn't rely on runtime features which are only in the specific (quite recent) version of runtime. We still need to support the full framework and netstandard versions.

That said, I guess the attribute-driven approach should be the way to go. It's more explicit (we don't choose different codegen based on runtime), backwards compatible, and can be easily hidden under the compiler flag.

We will probably need an RFC for it, so we have the scope of this feature documented and agreed upon.

@Happypig375
Copy link
Contributor

We shouldn't rely on runtime features which are only in the specific (quite recent) version of runtime. We still need to support the full framework and netstandard versions.

Support is still there. Just not new changes.

@vzarytovskii
Copy link

What would be the behaviour if we run on an older runtime, which doesn't have this flag?

Keeping the status quo and just don't modify behaviour on older platforms. This would be an easy way out.

We probably don't want different codegen based on runtime alone.

@Happypig375
Copy link
Contributor

We probably don't want different codegen based on runtime alone.

Improved platforms and APIs, for improved codegen. Is that not desirable?

@vzarytovskii
Copy link

vzarytovskii commented Jan 24, 2022

We probably don't want different codegen based on runtime alone.

Improved platforms and APIs, for improved codegen. Is that not desirable?

In this case, we don't give a choice to use the simplified version if you run on "unsupported" runtime (I.e. we are locking the feature to runtime version). You will still be able to do it in the attribute-driven approach.

@Happypig375
Copy link
Contributor

What about only requiring the attribute on platforms without RuntimeFeature.IsDynamicCodeSupported?

@vzarytovskii
Copy link

What about only requiring the attribute on platforms without RuntimeFeature.IsDynamicCodeSupported?

Attribute is compile-time, whereas RuntimeFeatures is, well, runtime. So we will be requiring attribute all the time, just in case we run on an unsupported runtime?

@Happypig375
Copy link
Contributor

RuntimeFeature.IsDynamicCodeSupported the property is not available compile-time?

@Happypig375
Copy link
Contributor

Happypig375 commented Jan 24, 2022

Not having the property is just equivalent to this returning true, and an attribute would interpret it as false.

@vzarytovskii
Copy link

RuntimeFeature.IsDynamicCodeSupported the property is not available compile-time?

It is a runtime flag, yeah.

Personally, I think we should go for an attribute, it's most explicit and straightforward, and will behave the same everywhere no matter what runtime.

@dsyme
Copy link
Collaborator

dsyme commented Apr 8, 2022

I would like to make progress on this. The current proposal is:

An assembly-level attribute [<SimplifiedCodeGen>] or [<ReflectionFreeCode>] that turns off .ToString() generation for record/union/struct and gives warnings on all problematic constructs (sprintf, %A in interpolated strings) and any other problematic code-generation and warn and warn and warn

I'd like to change this to a flag --reflectionfree. This makes it much easier to apply to all code during type checking as well.

@kant2002
Copy link

kant2002 commented Apr 8, 2022

@dsyme any other work happens in this area? I have very early attempt on hacking (nothing special) this which is stale right now, so I would like to know, if I somehow would be affected in other ways.

@dsyme
Copy link
Collaborator

dsyme commented Apr 8, 2022

I have a prototype, will share it in a bit.

The following construct in FSharp.Core uses '%A' formatting: Quotation ToString/GetLayout. However Quotations in general use a lot of reflection so I think this is expected.

@dsyme
Copy link
Collaborator

dsyme commented Apr 8, 2022

Here is my work in progress. Please contribute and try it out. I want this to be largely community led if possible :)

dotnet/fsharp#12960

@dsyme
Copy link
Collaborator

dsyme commented Apr 8, 2022

@charlesroddie at al - I'd really like to understand if it is only %A that is a problem, or if any use of sprintf is a problem.

@kant2002
Copy link

kant2002 commented Apr 8, 2022

From my testing it’s only %A problem. Most other printf problems are annoying but not a showstopper.

@am11
Copy link

am11 commented May 24, 2022

I was testing .NET 7 preview 5 SDK (daily build), which has added out of the box support for NativeAot. It seems to handle %A with a simple program (despite some errors and warnings in ILC step).

A quick How-To

OS: Ubuntu 20.04 x64

Setup:

mkdir ~/.dotnet7
curl -sSL https://aka.ms/dotnet/7.0.1xx/daily/dotnet-sdk-linux-x64.tar.gz | tar xzf - -C ~/.dotnet7
alias dotnet7=~/.dotnet7/dotnet

cat > ~/NuGet.config <<EOF
<configuration>
  <packageSources>
    <add key="dotnet7" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json" />
  </packageSources>
</configuration>
EOF

Create project:

dotnet7 new console -n fsharpnative1 --language f#
cd fsharpnative1

Program.fs

// snippet copy & paste from https://fsharpforfunandprofit.com/posts/printf/

open System

// tuple printing
let t = (1,2)
Console.WriteLine("A tuple: {0}", t)
printfn "A tuple: %A" t

// record printing
type Person = {First:string; Last:string}
let johnDoe = {First="John"; Last="Doe"}
Console.WriteLine("A record: {0}", johnDoe )
printfn "A record: %A" johnDoe

// union types printing
type Temperature = F of int | C of int
let freezing = F 32
Console.WriteLine("A union: {0}", freezing )
printfn "A union: %A" freezing

Publish:

dotnet7 publish -c Release --use-current-runtime -p:PublishAot=true

there are warnings/errors, but the build succeeds:

Microsoft (R) Build Engine version 17.3.0-preview-22263-02+78bb45f0f for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  Restored /home/am11/projects/fsharpnative1/fsharpnative1.fsproj (in 436 ms).
/home/am11/.dotnet7/sdk/7.0.100-preview.5.22273.1/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.RuntimeIdentifierInference.targets(219,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj]
  fsharpnative1 -> /home/am11/projects/fsharpnative1/bin/Release/net7.0/linux-x64/fsharpnative1.dll
  Generating compatible native code. To optimize for size or speed, visit https://aka.ms/OptimizeNativeAOT
/home/am11/.nuget/packages/fsharp.core/6.0.5-beta.22253.3/lib/netstandard2.1/FSharp.Core.dll : warning IL2104: Assembly 'FSharp.Core' produced trim warnings. For more information see https://aka.ms/dotnet-illink/libraries [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj]
/home/am11/.nuget/packages/fsharp.core/6.0.5-beta.22253.3/lib/netstandard2.1/FSharp.Core.dll : warning IL3053: Assembly 'FSharp.Core' produced AOT analysis warnings. [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj]
/home/am11/.nuget/packages/runtime.linux-x64.microsoft.dotnet.ilcompiler/7.0.0-preview.5.22271.4/framework/System.Linq.Expressions.dll : warning IL3053: Assembly 'System.Linq.Expressions' produced AOT analysis warnings. [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj]
/_/src/libraries/System.Private.CoreLib/src/System/Resources/ManifestBasedResourceGroveler.cs(239): Trim analysis warning IL2026: System.Resources.ManifestBasedResourceGroveler.CreateResourceSet(Stream,Assembly): Using member 'System.Resources.ManifestBasedResourceGroveler.InternalGetResourceSetFromSerializedData(Stream,String,String,ResourceManagerMediator)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The CustomResourceTypesSupport feature switch has been enabled for this app which is being trimmed. Custom readers as well as custom objects on the resources file are not observable by the trimmer and so required assemblies, types and members may be removed. [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj]
/home/am11/.nuget/packages/runtime.linux-x64.microsoft.dotnet.ilcompiler/7.0.0-preview.5.22271.4/framework/System.Formats.Asn1.dll : warning IL3053: Assembly 'System.Formats.Asn1' produced AOT analysis warnings. [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj]
/home/am11/.nuget/packages/runtime.linux-x64.microsoft.dotnet.ilcompiler/7.0.0-preview.5.22271.4/framework/System.Diagnostics.DiagnosticSource.dll : warning IL3053: Assembly 'System.Diagnostics.DiagnosticSource' produced AOT analysis warnings. [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj]
  ILC: Method '[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`6<System.__Canon,System.__Canon,System.__Canon,System.__Canon,System.__Canon,System.__Canon>..ctor()' will always throw because: Failed to load type 'Microsoft.FSharp.Core.FSharpFunc`2<T1_System.__Canon, Microsoft.FSharp.Core.FSharpFunc`2<T2_System.__Canon, Microsoft.FSharp.Core.FSharpFunc`2<T3_System.__Canon, Microsoft.FSharp.Core.FSharpFunc`2<T4_System.__Canon, Microsoft.FSharp.Core.FSharpFunc`2<T5_System.__Canon, TResult_System.__Canon>>>>>' from assembly 'FSharp.Core, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
  fsharpnative1 -> /home/am11/projects/fsharpnative1/bin/Release/net7.0/linux-x64/publish/

Run it:

bin/Release/net7.0/linux-x64/publish/fsharpnative1 

Output:

A tuple: (1, 2)
A tuple: (1, 2)
A record: { First = "John"
  Last = "Doe" }
A record: { First = "John"
  Last = "Doe" }
A union: F
A union: F

Despite the error:

Failed to load type 'Microsoft.FSharp.Core.FSharpFunc2<T1_System.__Canon, Microsoft.FSharp.Core.FSharpFunc2<T2_System.__Canon, Microsoft.FSharp.Core.FSharpFunc2<T3_System.__Canon, Microsoft.FSharp.Core.FSharpFunc2<T4_System.__Canon, Microsoft.FSharp.Core.FSharpFunc`2<T5_System.__Canon, TResult_System.__Canon>>>>>' from assembly 'FSharp.Core, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'

ILC succeeds. If we can work this error out first in .NET 7 timeframe, that would be cool (since it seems benign, and showing up for a simple "Hello from F#" app as well).

cc @MichalStrehovsky

@kant2002
Copy link

Regular NativeAOT works just fine with %A but there extra goal to make it work in reflection-free mode which is not supported (but working).

Having reflection obviously fine, but being not rely on it is also goal which a lot of people want. This issue is to improve reflectionfree mode too.

https://github.com/kant2002/RdXmlLibrary/blob/main/FSharp.Core.xml

You can plug this file with

<RdXmlFile Include="FSharp.Core.rd.xml" />

@MichalStrehovsky
Copy link

MichalStrehovsky commented May 25, 2022

Failed to load type 'Microsoft.FSharp.Core.FSharpFunc2<T1_System.__Canon, Microsoft.FSharp.Core.FSharpFunc2<T2_System.__Canon, Microsoft.FSharp.Core.FSharpFunc2<T3_System.__Canon, Microsoft.FSharp.Core.FSharpFunc2<T4_System.__Canon, Microsoft.FSharp.Core.FSharpFunc`2<T5_System.__Canon, TResult_System.__Canon>>>>>' from assembly 'FSharp.Core, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'

The compiler detected there's a generic cycle in the assembly and instead of compiling until it runs out of memory (or the heath death of the universe, whichever comes first) cut off the generic expansion at the point when it ran over the cutoff. If the reported method is reached at runtime, it will throw.

FWIW, the cycle(s) involve following entities. The compiler should print them, not sure why it's not kicking in here:

    [0]: {[FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2}
    [1]: {[FSharp.Core]<StartupCode$FSharp-Core>.$Prim-types+op_Implicit@3421}
    [2]: {[FSharp.Core]<StartupCode$FSharp-Core>.$Prim-types+op_Implicit@3426-1}
    [3]: {[FSharp.Core]<StartupCode$FSharp-Core>.$Prim-types+op_Implicit@3429-2}
    [4]: {[FSharp.Core]<StartupCode$FSharp-Core>.$Prim-types+op_Implicit@3432-3}
    [5]: {[FSharp.Core]<StartupCode$FSharp-Core>.$Prim-types+FromConverter@3434}
    [6]: {[FSharp.Core]<StartupCode$FSharp-Core>.$Prim-types+ToConverter@3436}
    [7]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`3}
    [8]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`4}
    [9]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`5}
    [10]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`6}
    [11]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Invoke@3302}
    [12]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3309}
    [13]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Invoke@3316-1}
    [14]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3325-1}
    [15]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3329-2}
    [16]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3343-3}
    [17]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3348-4}
    [18]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3352-5}
    [19]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Invoke@3355-2}
    [20]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Invoke@3363-3}
    [21]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3373-6}
    [22]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3378-7}
    [23]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3383-8}
    [24]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3387-9}

@kant2002
Copy link

Just for information about other reflection-free mode limitations.

Construct $"test {name}" produce exceptions like this.

Unhandled Exception: EETypeRva:0x004CDE08: TypeInitialization_Type_NoTypeAvailable
 ---> EETypeRva:0x004CDE08: TypeInitialization_Type_NoTypeAvailable
 ---> EETypeRva:0x004CD0E8: Reflection_Disabled
   at Internal.Reflection.RuntimeTypeInfo.GetMethodImpl(String, BindingFlags, Binder, CallingConventions, Type[], ParameterModifier[]) + 0x33
   at System.Type.GetMethod(String, BindingFlags) + 0x27
   at <StartupCode$FSharp-Core>.$Printf..cctor() + 0x3c
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0xc6
   Exception_EndOfInnerExceptionStack
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0x167
   at System.Runtime.CompilerServices.ClassConstructorRunner.CheckStaticClassConstructionReturnNonGCStaticBase(StaticClassConstructionContext*, IntPtr) + 0xd
   at Microsoft.FSharp.Core.PrintfImpl..cctor() + 0x9
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0xc6
   Exception_EndOfInnerExceptionStack
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0x167
   at System.Runtime.CompilerServices.ClassConstructorRunner.CheckStaticClassConstructionReturnNonGCStaticBase(StaticClassConstructionContext*, IntPtr) + 0xd
   at Microsoft.FSharp.Core.PrintfImpl.buildStep$cont@1141(PrintfImpl.FormatSpecifier, Type[], String, Unit) + 0x16
   at Microsoft.FSharp.Core.PrintfImpl.FormatParser`4.parseAndCreateStepsForCapturedFormatAux(FSharpList`1, String, Int32&) + 0xb3
   at Microsoft.FSharp.Core.PrintfImpl.FormatParser`4.parseAndCreateStepsForCapturedFormat() + 0x3a
   at Microsoft.FSharp.Core.PrintfImpl.FormatParser`4.GetStepsForCapturedFormat() + 0x17
   at Microsoft.FSharp.Core.PrintfModule.PrintFormatToStringThen[T](PrintfFormat`4) + 0x4b
   at [email protected](IImageProcessingContext) + 0xcf
   at Program.drawScene(Image`1, Font, JS.DataView, Int32, PlatformModel.Texture[], Model.Game) + 0x26b
   at [email protected](PlatformModel.Texture[], Model.Game) + 0x2d
   at [email protected](Model.Game, Double) + 0x2b
   at Program.render(Double) + 0x4a
   at [email protected](Double) + 0x9
   at Silk.NET.Windowing.Internals.ViewImplementationBase.DoRender() + 0x16e
   at Silk.NET.Windowing.Internals.ViewImplementationBase.Run(Action) + 0x15
   at Silk.NET.Windowing.WindowExtensions.Run(IView) + 0x61
   at Program.main(String[]) + 0x227
   at FSharpWolfenstein.Desktop!<BaseAddress>+0x381723

@kant2002
Copy link

I also receive this error with string interpolation in regular NativeAOT

Unhandled Exception: System.Collections.Generic.KeyNotFoundException: An index satisfying the predicate was not found in the collection.
   at Microsoft.FSharp.Collections.ArrayModule.loop@596-36[T, TResult](FSharpFunc`2, T[], Int32) + 0x9e
   at Microsoft.FSharp.Reflection.Impl.getUnionCaseTyp(Type, Int32, BindingFlags) + 0x3e
   at Microsoft.FSharp.Reflection.Impl.fieldsPropsOfUnionCase(Type, Int32, BindingFlags) + 0x143
   at Microsoft.FSharp.Reflection.FSharpValue.GetUnionFields(Object, Type, FSharpOption`1) + 0x6f
   at Microsoft.FSharp.Text.StructuredPrintfImpl.ReflectUtils.Value.GetValueInfoOfObject$cont@525(BindingFlags, Object, Type, Unit) + 0x57
   at Microsoft.FSharp.Text.StructuredPrintfImpl.Display.ObjectGraphFormatter.objL(Display.ShowMode, Int32, Display.Precedence, Object, Type) + 0x3f
   at Microsoft.FSharp.Text.StructuredPrintfImpl.Display.Format@1515.Invoke(Int32, Display.Precedence, Tuple`2) + 0x3a
   at Microsoft.FSharp.Text.StructuredPrintfImpl.Display.ObjectGraphFormatter.Format[a](Display.ShowMode, a, Type) + 0x83
   at Microsoft.FSharp.Text.StructuredPrintfImpl.Display.anyToStringForPrintf[T](FormatOptions, BindingFlags, T, Type) + 0x5c
   at Microsoft.FSharp.Core.PrintfImpl.ObjectPrinter.GenericToStringCore[T](T, FormatOptions, BindingFlags) + 0x47
   at [email protected](A) + 0x37
   at System.Text.ValueStringBuilder.AppendFormatHelper(IFormatProvider, String, ParamsArray) + 0x644
   at System.String.FormatHelper(IFormatProvider, String, ParamsArray) + 0xae
   at [email protected](Object) + 0x75
   at Microsoft.FSharp.Core.PrintfImpl.PrintfEnv`3.RunSteps(Object[], Type[], PrintfImpl.Step[]) + 0xb4
   at Microsoft.FSharp.Core.PrintfModule.PrintFormatToStringThen[T](PrintfFormat`4) + 0x6a
   at App.AI.preProcess(Model.Game, Model.Enemy) + 0x110
   at App.AI.applyAi(Double, Model.Game, Model.GameObject) + 0x5b
   at [email protected](Int32, Model.GameObject) + 0x95
   at Microsoft.FSharp.Primitives.Basics.List.mapiToFreshConsTail[a, b](FSharpList`1, OptimizedClosures.FSharpFunc`3, FSharpList`1, Int32) + 0x5c
   at Microsoft.FSharp.Primitives.Basics.List.mapi[T, TResult](FSharpFunc`2, FSharpList`1) + 0xb1
   at App.Update.updateEnemies@251(Double, Model.WallRenderingResult, Model.Game, Boolean) + 0x67
   at App.Update.updateFrame(Model.Game, Double, Model.WallRenderingResult) + 0x295
   at Program.render(Double) + 0x4a
   at [email protected](Double) + 0x9
   at Silk.NET.Windowing.Internals.ViewImplementationBase.DoRender() + 0x16e
   at Silk.NET.Windowing.Internals.ViewImplementationBase.Run(Action) + 0x15
   at Silk.NET.Windowing.WindowExtensions.Run(IView) + 0x61
   at Program.main(String[]) + 0x149
   at FSharpWolfenstein.Desktop!<BaseAddress>+0x462583

@abelbraaksma
Copy link
Member

Related, adding for prosperity: #429

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