fsharp / fsharp/fslang-suggestions

Make F# string functions fast and AOT/linker friendly

Open
#919 56 comments 35 reactions 0 assignees View on GitHub
area: library
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.