F# frontend
Compile a subset of F# into a Morphir IR model.
The F# frontend compiles F# source documents into a Morphir IR model. It writes
IR versions 1, 2 and 3 (the classic format) and version 4. It uses the F#
compiler service to parse and type-check the source, so a model has the types
that F# infers.
The frontend accepts a subset of F#: immutable data and pure functions. It
refuses every other construct with an error diagnostic. The
coverage reference lists each refused construct, with its code and
an example.
Status
The frontend has two forms:
- In process.
Morphir.Frontend.FSharp.FSharpFrontend.registration is an
IFrontend from Morphir.Compilation. The dotnet-morphir tool uses it. - MEP extension.
morphir-fsharp-frontend is a Morphir Extension Protocol
(MEP) 0.2.0-draft.1 guest. Hosts that cannot load the F# compiler service,
such as the AOT morphir executable, start it as a process. See
Run the frontend as a MEP extension.
To compile F# from the command line, see
compile.
A compilation gets:
- a package name, such as
acme/car-rental; - the modules that the package exposes;
- the source documents, in compilation order, as F# requires.
Each F# module, nested modules included, becomes one Morphir module. The module
path is the namespace and module names without the package path.
namespace Acme.CarRental
module Pricing =
type Car = { Plate: string; Seats: int }
type Status =
| Available
| Rented of days: int
let dailyRate (car: Car) : int = if car.Seats > 4 then 60 else 40
let charge (car: Car) (status: Status) : int =
match status with
| Available -> 0
| Rented days -> dailyRate car * days
Supported subset
| Area | Supported |
|---|
| Types | Type abbreviations, records, unions with named or positional fields, generic parameters, tuples, functions, unit, bool, int, int64, float, decimal, char, string, option, Result and list |
| Values | Literals, variables, references, constructor application, tuples, lists, record construction, field access, record update, application, lambdas, let, let rec and let rec ... and, if/then/else, and match |
| Patterns | Wildcard, variable, literal, tuple, constructor, empty list, cons, as and unit, in match and in function and let parameters |
| Operators and library functions | Arithmetic, comparison, equality, Boolean operators, |>, <|, >> and <<, and mapped functions of List, Option, Result and String |
The frontend keeps the order of fields, constructor arguments and match cases,
and the currying and evaluation structure of the source. A pipe or a
composition becomes a direct application, not a function in the model.
Operators and library functions map to Morphir SDK operations through an
explicit table. The type of an operand selects the operation: a + b on
decimal values is SDK Decimal.add. A reference with no entry in the table
gives fsharp.unsupported_reference and names the F# symbol.
Names and access
F# names become Morphir names: TaxRate and taxRate are both tax rate.
Two declarations in one scope with the same Morphir name give
fsharp.name_collision. To give a declaration a different Morphir name, use the
[<MorphirName("...")>] attribute from the Morphir.FSharp.Annotations
package (namespace Morphir). The name is in canonical Morphir syntax.
A module is public when the list of exposed modules names it, or when the
compilation has no such list. An empty list exposes no module. An exposed module
that matches no compiled module gives compile.unknown_exposed_module. A
private or internal declaration is private.
Diagnostics
Diagnostics use the MEP diagnostic shape: a severity, a stable code, a message
and a location. Positions are zero-based, and characters count UTF-16 code
units. An error stops the compilation, and the frontend writes no model.
Warnings and information diagnostics come with a successful model.
F# compiler errors have the code fsharp.compiler. Refused constructs have the
codes in the coverage reference. When the frontend cannot give the
compiler its references, for example in a self-contained folder whose deps
file is missing or unreadable, compilation fails with one
fsharp.references_unavailable error that names the cause, and the compiler
does not run.
Run the frontend as a MEP extension
Install the guest as a dotnet tool. It needs the .NET runtime.
dotnet tool install --global Morphir.Frontend.FSharp.Extension
The same guest also has a self-contained folder delivery that carries its own
.NET runtime, for machines without .NET. See
Compile without .NET installed.
Both deliveries compile with the same references (the .NET framework,
FSharp.Core and Morphir.FSharp.Annotations), so at the same .NET runtime
version they give the same IR and diagnostics. The model revision names the
frontend identity, which includes the runtime version, so it is equal only on
the same runtime patch.
The guest speaks only MEP 0.2.0-draft.1, over stdio with Content-Length
framing. It serves morphir.frontend.compileModel and claims:
| Claim | Value |
|---|
| Claims version | 0.1.0-draft.2 |
| Language | fsharp, with the file extension .fs |
| Several documents in one request | yes (multiDocument: true) |
Legacy morphir.frontend.compile | no |
| Incremental compilation | no |
| Model tuples | morphir.frontend.compileModel, profile morphir-ir, codec morphir-json, envelope 0.1.0-draft.1, IR 4.0.0 and 3.0.0 |
Classic IR versions 1 and 2 have no tuple. For them, the host compiles at
3.0.0, reads the classic model, and writes it at the requested version. The
model does not change family. A host that agrees only 4.0.0 with a guest
refuses a classic request before it sends one.
To use the guest from the morphir executable, give its path with
--extension, or set it in the project configuration:
morphir compile --extension "$(which morphir-fsharp-frontend)"
[frontend.fsharp]
extension = "morphir-fsharp-frontend"
The host starts the guest with explicit arguments and no shell, and passes the
guest’s stderr to its own stderr. Before it sends a request, it checks that the
session admits the language, the number of documents and the IR version. It
always ends the session with shutdown and exit, or stops the process after
a deadline. Guest output on stdout that is not a frame fails the compile with
extension.protocol_error.
The model in a successful result is a verified envelope: canonical base64 of
the IR JSON, its exact length and its SHA-256. The host checks them before it
reads the IR. For the same documents, the IR and the diagnostics from the guest
equal the in-process results; a test compares them for every case of the
conformance corpus.
Upstream qualification: the MEP guest kit of finos/morphir
(generic-json-library-v1) still needs a legacy 0.1 stage and a backend, so
the guest is not qualified with it yet. Its conformance comes from the pinned
0.2.0-draft.1 contract, the pinned session transcripts and the
model-transport cases. See deviations.
How the frontend is checked
- The conformance corpus
has cases for IR versions 1 to 4 and three input kinds. Each refused
construct has its own case. The cases do not depend on the implementation.
- Behavior cases call the compiled values with JSON arguments and compare the
results.
- A round trip generates F# from each compiled model with the F# backend,
compiles that F# again, and requires the same model.
Prior art
The frontend uses the design of two F# projects, but it does not depend on
them.
| Project | What the frontend uses | What it does not use, and why |
|---|
| Fable (MIT) | Design patterns: how decision trees appear in the typed tree, a replacement table for core-library calls, how inline values and witnesses appear in the typed tree, and a resolver interface for project cracking. | Fable.Transforms as a library. It carries Fable’s own syntax tree and replacements for JavaScript, Python and Rust targets. |
| Fantomas (Apache-2.0) | The F# backend formats generated code with Fantomas.Core. The round-trip check uses that backend. | Fantomas.FCS in the frontend. Its syntax tree is not binary-compatible with the F# compiler service, and the frontend needs the compiler service for type checking. A second parser would give a second set of ranges. |
If code is adapted from these projects, and not only its design, the source file
names its origin and the NOTICE file gives the attribution.
1 - F# frontend coverage
The F# constructs that the F# frontend refuses, with their diagnostic codes and examples.
The F# frontend compiles a subset of F#. Before it lowers a document, admission checks the
syntax and refuses each construct outside the subset with an error diagnostic. All refusals
of a run are reported together, and the frontend writes no model. The
overview describes the supported subset.
This page is generated from the admission catalog of the frontend, so it lists every code
that admission can report. Each rule has a minimal example that gives only its own code. The
conformance corpus has one refusal case for each code. Codes are stable once released.
Codes
The fallback rules below refuse any expression, pattern, type or declaration outside the
supported subset that no specific rule refused.
Rules
fsharp.unsupported_mutable
Construct: mutable binding or field
Message: Mutable bindings and fields are not supported; Morphir values are immutable.
Suggestion: Use a new let binding, or a record update, instead of changing a value.
Example. The diagnostic is at mutable.
Example.fs:
namespace Acme.Demo
module Example =
let mutable total = 0
fsharp.unsupported_assignment
Construct: assignment
Message: Assignment with ‘<-’ is not supported; Morphir values are immutable.
Suggestion: Return a new value instead of changing an existing one.
Example. The diagnostic is at builder.Length <- 0.
Example.fs:
namespace Acme.Demo
module Example =
let reset (builder: System.Text.StringBuilder) = builder.Length <- 0
fsharp.unsupported_loop
Construct: loop
Message: Loops (‘for’, ‘while’) are not supported; Morphir has no statements.
Suggestion: Use recursion or a List function such as List.map or List.fold.
Example. The diagnostic is at for index in 1 .. 3 do ignore index.
Example.fs:
namespace Acme.Demo
module Example =
let count () = for index in 1 .. 3 do ignore index
fsharp.unsupported_class
Construct: class
Message: Classes are not supported; Morphir types are records, unions and type abbreviations.
Suggestion: Use a record for the data and module functions for the behavior.
Example. The diagnostic is at this text:
Counter() =
member _.Value = 1
Example.fs:
namespace Acme.Demo
module Example =
type Counter() =
member _.Value = 1
fsharp.unsupported_interface
Construct: interface
Message: Interfaces are not supported; Morphir has no subtyping.
Suggestion: Use a record of functions, or a union of the cases.
Example. The diagnostic is at this text:
IShape =
abstract Area: float
Example.fs:
namespace Acme.Demo
module Example =
type IShape =
abstract Area: float
fsharp.unsupported_member
Construct: member
Message: Members and type extensions are not supported; Morphir types have no methods.
Suggestion: Define a module function that takes the value as a parameter.
Example. The diagnostic is at member this.Double = this.Seats * 2.
Example.fs:
namespace Acme.Demo
module Example =
type Car = { Seats: int } with
member this.Double = this.Seats * 2
fsharp.unsupported_object_expression
Construct: object expression
Message: Object expressions are not supported; Morphir has no objects.
Suggestion: Use a record of functions.
Example. The diagnostic is at { new System.Object() with member _.ToString() = "x" }.
Example.fs:
namespace Acme.Demo
module Example =
let text = { new System.Object() with member _.ToString() = "x" }
fsharp.unsupported_sequence_expression
Construct: sequence expression
Message: Sequence expressions (‘seq { }’) are not supported; Morphir has no lazy sequences.
Suggestion: Use a list and the List functions.
Example. The diagnostic is at { yield 1 }.
Example.fs:
namespace Acme.Demo
module Example =
let numbers = seq { yield 1 }
fsharp.unsupported_computation_expression
Construct: computation expression
Message: Computation expressions are not supported; Morphir has no effects or builders.
Suggestion: Write the steps as plain functions, for example with Result.bind.
Example. The diagnostic is at { return 1 }.
Example.fs:
namespace Acme.Demo
module Example =
let work = async { return 1 }
fsharp.unsupported_comprehension
Construct: list comprehension or range
Message: List comprehensions and ranges (’[ for … ]’, ‘[ 1 .. 3 ]’) are not supported.
Suggestion: Use List.map, List.filter or an explicit list.
Example. The diagnostic is at [ for x in [ 1; 2 ] -> x * x ].
Example.fs:
namespace Acme.Demo
module Example =
let squares = [ for x in [ 1; 2 ] -> x * x ]
fsharp.unsupported_array
Construct: array
Message: Arrays are not supported; Morphir collections are immutable lists.
Suggestion: Use a list.
Example. The diagnostic is at [| 1; 2 |].
Example.fs:
namespace Acme.Demo
module Example =
let numbers = [| 1; 2 |]
fsharp.unsupported_exception
Construct: exception
Message: Exceptions (’exception’, ’try … with’, ’try … finally’) are not supported; Morphir has no effects.
Suggestion: Return a Result or an option.
Example. The diagnostic is at exception Failed of string.
Example.fs:
namespace Acme.Demo
module Example =
exception Failed of string
fsharp.unsupported_active_pattern
Construct: active pattern
Message: Active patterns are not supported; Morphir patterns match only constructors and values.
Suggestion: Use a union and a function that returns it.
Example. The diagnostic is at let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd.
Example.fs:
namespace Acme.Demo
module Example =
let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd
fsharp.unsupported_unit_of_measure
Construct: unit of measure
Message: Units of measure are not supported; Morphir numbers carry no unit.
Suggestion: Use a single-case union to name the quantity.
Example. The diagnostic is at [<Measure>] type kg.
Example.fs:
namespace Acme.Demo
module Example =
[<Measure>] type kg
fsharp.unsupported_quotation
Construct: quotation
Message: Quotations are not supported; Morphir models values, not code.
Example. The diagnostic is at <@ 1 + 1 @>.
Example.fs:
namespace Acme.Demo
module Example =
let expression = <@ 1 + 1 @>
fsharp.unsupported_anonymous_record
Construct: anonymous record
Message: Anonymous records are not supported; Morphir record types have names.
Suggestion: Declare a record type.
Example. The diagnostic is at {| X = 1 |}.
Example.fs:
namespace Acme.Demo
module Example =
let point = {| X = 1 |}
fsharp.unsupported_struct
Construct: struct type or struct tuple
Message: Struct types and struct tuples are not supported; Morphir has no value-type layout.
Suggestion: Remove [] or ‘struct’.
Example. The diagnostic is at struct (1, 2).
Example.fs:
namespace Acme.Demo
module Example =
let pair = struct (1, 2)
fsharp.unsupported_inline
Construct: inline function
Message: Inline functions are not supported; Morphir functions are not specialized at their call sites.
Suggestion: Remove ‘inline’ and give the parameters concrete types.
Example. The diagnostic is at inline.
Example.fs:
namespace Acme.Demo
module Example =
let inline double (x: int) = x * 2
fsharp.unsupported_attribute
Construct: attribute with an effect
Message: This attribute changes the meaning of the code and is not supported.
Suggestion: Remove the attribute. Only RequireQualifiedAccess, AutoOpen, CompilationRepresentation, CompiledName and MorphirName are accepted.
Example. The diagnostic is at Literal.
Example.fs:
namespace Acme.Demo
module Example =
[<Literal>]
let Limit = 10
fsharp.unsupported_type_test
Construct: type test
Message: Type tests (’:?’) are not supported; Morphir has no runtime types.
Suggestion: Use a union to tell the cases apart.
Example. The diagnostic is at value :? string.
Example.fs:
namespace Acme.Demo
module Example =
let isText (value: obj) = value :? string
fsharp.unsupported_cast
Construct: cast
Message: Casts (’:>’, ‘:?>’, ‘upcast’, ‘downcast’) are not supported; Morphir has no subtyping.
Example. The diagnostic is at 1 :> obj.
Example.fs:
namespace Acme.Demo
module Example =
let boxed = (1 :> obj)
fsharp.unsupported_null
Construct: null
Message: ’null’ is not supported; Morphir values are never null.
Suggestion: Use an option.
Example. The diagnostic is at null.
Example.fs:
namespace Acme.Demo
module Example =
let nothing: string = null
fsharp.unsupported_use
Construct: use binding
Message: ‘use’ bindings are not supported; Morphir has no resources to dispose.
Suggestion: Use ’let’.
Example. The diagnostic is at this text:
use stream = new System.IO.MemoryStream()
stream.Length
Example.fs:
namespace Acme.Demo
module Example =
let size () =
use stream = new System.IO.MemoryStream()
stream.Length
fsharp.unsupported_sequential
Construct: sequential expression
Message: Sequential expressions (‘a; b’) are not supported; Morphir values have no side effects.
Suggestion: Remove the expression whose value is discarded.
Example. The diagnostic is at this text:
Example.fs:
namespace Acme.Demo
module Example =
let both () =
ignore 1
2
fsharp.unsupported_literal
Construct: literal of an unsupported type
Message: This literal type is not supported; Morphir numbers are Int, Float and Decimal.
Suggestion: Use an int, int64, float or decimal literal.
Example. The diagnostic is at 1uy.
Example.fs:
namespace Acme.Demo
module Example =
let small = 1uy
fsharp.unsupported_enum
Construct: enum
Message: Enums are not supported; Morphir custom types have no numeric values.
Suggestion: Use a union without fields.
Example. The diagnostic is at this text:
Color =
| Red = 0
| Blue = 1
Example.fs:
namespace Acme.Demo
module Example =
type Color =
| Red = 0
| Blue = 1
fsharp.unsupported_delegate
Construct: delegate
Message: Delegates are not supported; Morphir has function types.
Suggestion: Use a function type.
Example. The diagnostic is at Handler = delegate of int -> unit.
Example.fs:
namespace Acme.Demo
module Example =
type Handler = delegate of int -> unit
fsharp.unsupported_pattern_guard
Construct: pattern guard
Message: Pattern guards (‘when’) are not supported; a Morphir pattern has no condition.
Suggestion: Match the case, then use ‘if’ in its body.
Example. The diagnostic is at n > 3.
Example.fs:
namespace Acme.Demo
module Example =
let size n =
match n with
| n when n > 3 -> "big"
| _ -> "small"
fsharp.unsupported_or_pattern
Construct: or pattern
Message: Or patterns (‘A | B’) are not supported; a Morphir case has one pattern.
Suggestion: Write one case for each alternative.
Example. The diagnostic is at 1 | 2.
Example.fs:
namespace Acme.Demo
module Example =
let low n =
match n with
| 1 | 2 -> true
| _ -> false
fsharp.unsupported_record_pattern
Construct: record pattern
Message: Record patterns are not supported.
Suggestion: Bind the record to a variable and read its fields.
Example. The diagnostic is at { Seats = s }.
Example.fs:
namespace Acme.Demo
module Example =
type Car = { Seats: int }
let seats car =
match car with
| { Seats = s } -> s
fsharp.unsupported_list_pattern
Construct: list pattern with elements
Message: List patterns with elements (’[ a; b ]’) are not supported.
Suggestion: Use ‘::’ and ‘[]’ patterns.
Example. The diagnostic is at [ a ].
Example.fs:
namespace Acme.Demo
module Example =
let first xs =
match xs with
| [ a ] -> a
| _ -> 0
fsharp.unsupported_named_field_pattern
Construct: named field pattern
Message: Named field patterns (‘Case(field = p)’) are not supported.
Suggestion: Match the fields by position.
Example. The diagnostic is at Rented(days = d).
Example.fs:
namespace Acme.Demo
module Example =
type Status = Rented of days: int
let days status =
match status with
| Rented(days = d) -> d
fsharp.unsupported_conditional_compilation
Construct: conditional compilation
Message: Conditional compilation (’#if’) is not supported, because this frontend does not choose defines.
Suggestion: Remove the directive, or keep one branch.
Example. The diagnostic is at #if DEBUG.
Example.fs:
namespace Acme.Demo
module Example =
#if DEBUG
let mode = "debug"
#else
let mode = "release"
#endif
fsharp.unsupported_signature_file
Construct: signature file
Message: Signature files (’.fsi’) are not supported; access comes from F# visibility and the exposed modules.
Suggestion: Remove the signature file and use ‘private’ or ‘internal’.
Example. The diagnostic is an empty range at the start of the first document.
Example.fsi:
namespace Acme.Demo
module Example =
val total: int
Example.fs:
namespace Acme.Demo
module Example =
let total = 1
fsharp.unsupported_expression
Construct: expression outside the supported subset
Message: This kind of expression is not supported by the F# frontend.
Example. The diagnostic is at lazy 1.
Example.fs:
namespace Acme.Demo
module Example =
let later = lazy 1
fsharp.unsupported_pattern
Construct: pattern outside the supported subset
Message: This kind of pattern is not supported by the F# frontend.
Example. The diagnostic is at (a, _) & (_, b).
Example.fs:
namespace Acme.Demo
module Example =
let both pair =
match pair with
| (a, _) & (_, b) -> a + b
fsharp.unsupported_type
Construct: type outside the supported subset
Message: This kind of type is not supported by the F# frontend.
Example. The diagnostic is at #seq<int>.
Example.fs:
namespace Acme.Demo
module Example =
let total (values: #seq<int>) = Seq.sum values
fsharp.unsupported_declaration
Construct: declaration outside the supported subset
Message: This kind of declaration is not supported by the F# frontend.
Example. The diagnostic is at do ignore 1.
Example.fs:
namespace Acme.Demo
module Example =
do ignore 1