fsharp / fsharp/fslang-suggestions
Make F# string functions fast and AOT/linker friendly
- Dominant language
- No language data
- Stars
- 373
- Forks
- 21
- PR merge metrics
- No merged PRs in 30d
Description
### 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](https://github.com/dotnet/designs/blob/master/accepted/2020/form-factors.md)) 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](https://github.com/dotnet/corert/issues/6055) 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):
```fsharp
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:
```fsharp
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(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](https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1001-StringInterpolation.md).
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`: https://github.com/dotnet/fsharp/issues/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
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the linked F# Support and string interpolation design documents, then inspect the current F# string methods and sprintf behavior described in the issue. Compare the generated record and DU ToString() methods in SharpLab. Done would require an agreed design and implementation that avoids runtime code generation where possible while preserving required sprintf compatibility for older F# libraries.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- fsharp
- Domain
- compilers, performance
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 22/100