diff --git a/docs/docs/beam/build-and-run.md b/docs/docs/beam/build-and-run.md new file mode 100644 index 0000000..3d48432 --- /dev/null +++ b/docs/docs/beam/build-and-run.md @@ -0,0 +1,106 @@ +--- +title: Build and Run +layout: standard +--- + +## Erlang/OTP Version + +Fable targets Erlang/OTP 25 or higher. + +## Architecture + +Fable compiles F# to Erlang source files (`.erl`), following the same pipeline as all Fable targets: + +```text +F# Source + | FSharp2Fable +Fable AST + | FableTransforms +Fable AST (optimized) + | Fable2Beam +Erlang AST + | ErlangPrinter +.erl source files +``` + +The generated `.erl` files are standard Erlang that can be compiled with `erlc` and run on the BEAM VM. + +## Compiling to Erlang + +```bash +dotnet fable --lang beam +``` + +By default, output goes to the project directory (where the `.fsproj` is). You can change this with `--outDir`: + +```bash +dotnet fable --lang beam --outDir /path/to/output +``` + +## Output Structure + +The output directory will contain: + +```text +output/ + program.erl # Your compiled F# modules + fable_modules/ + fable-library-beam/ + fable_list.erl # F# List runtime + fable_map.erl # F# Map runtime + fable_string.erl # String utilities + fable_seq.erl # Seq/IEnumerable support + ... # Other runtime modules +``` + +## Compiling Erlang + +After Fable generates `.erl` files, compile them with `erlc`: + +```bash +# Compile the runtime library +erlc -o output/fable_modules/fable-library-beam output/fable_modules/fable-library-beam/*.erl + +# Compile your project files +erlc -pa output/fable_modules/fable-library-beam -o output output/*.erl +``` + +## Running Erlang Code + +Run your compiled module using the Erlang shell: + +```bash +erl -pa output -pa output/fable_modules/fable-library-beam \ + -noshell -eval 'program:main(), halt().' +``` + +The `-pa` flag adds directories to the code path so Erlang can find both your modules and the Fable runtime library. + +## Module Naming + +Fable converts F# filenames to snake_case to match Erlang conventions: + +- `Program.fs` becomes `program.erl` with `-module(program).` +- `MyModule.fs` becomes `my_module.erl` with `-module(my_module).` + +Erlang requires the module name to match the filename, so this conversion is automatic. + +## Program vs Library + +The Beam backend currently does not differentiate between `Exe` and `Library` output types. All top-level code is compiled into a `main/0` Erlang function regardless of the `OutputType` setting. The `[]` attribute is not used. + +## Watch Mode + +For development, use watch mode to recompile on changes: + +```bash +dotnet fable watch --lang beam +``` + +## Custom fable-library Path + +If you need a custom version of the runtime library (e.g., for development), use the `--fableLib` option: + +```bash +dotnet fable --lang beam --fableLib /path/to/custom/fable-library-beam +``` diff --git a/docs/docs/beam/compatibility.md b/docs/docs/beam/compatibility.md new file mode 100644 index 0000000..55dbfc5 --- /dev/null +++ b/docs/docs/beam/compatibility.md @@ -0,0 +1,208 @@ +--- +title: .NET and F# compatibility +layout: standard +toc: + to: 4 +--- + +:::warning +Beam target is in alpha meaning that breaking changes can happen between minor versions. +::: + +Fable provides support for some classes of .NET BCL (Base Class Library) and most of FSharp.Core library. When possible, Fable translates .NET types and methods to native Erlang types for minimum overhead. + +## Common Types and Objects + +Many F#/.NET types have natural counterparts in Erlang. Fable takes advantage of this to compile to native types that are more performant and idiomatic: + +- **Strings** compile to Erlang binaries (`<<"hello">>`). +- **Booleans** compile to Erlang atoms (`true` / `false`). +- **Chars** are compiled as strings of length 1. +- **Integers** use Erlang's native arbitrary-precision integers. Unlike Python and JavaScript, no wrapper types are needed for `int`, `int64`, or `bigint`. +- **Floats** use Erlang's native `float()` type. +- **Tuples** compile directly to Erlang tuples (`{A, B, C}`). +- **Lists** (`list`) compile to Erlang linked lists — both languages use cons cells, making this a perfect fit. +- **Arrays** compile to process dictionary references wrapping Erlang lists (for mutability). Byte arrays use `atomics` for O(1) read/write. +- **ResizeArray** compiles to process dictionary references with list mutation helpers. +- Any **IEnumerable** (or `seq`) can be traversed using Erlang's iterator patterns. +- **Maps** (`Map`) compile to Erlang native maps (`#{}`). +- **Sets** (`Set`) compile to Erlang `ordsets` (sorted lists). +- Mutable **dictionaries** compile to process dictionary references wrapping Erlang maps. +- **Unit** compiles to the `ok` atom. + +## .NET Base Class Library + +The following classes are translated to Erlang and most of their methods (static and instance) should be available in Fable. + +.NET | Erlang +---------------------------------------|---------------------------- +Numeric Types | Native integers and floats +Arrays | Process dict refs (lists / atomics for byte[]) +System.Boolean | `true` / `false` atoms +System.Char | Binary (string of length 1) +System.String | Binary (`<<"...">>`) +System.Decimal | Custom fixed-scale integer +System.DateTime | `{Ticks, Kind}` tuple +System.DateTimeOffset | `{Ticks, OffsetTicks, Kind}` tuple +System.TimeSpan | Ticks-based integer +System.Guid | UUID v4 binary +System.Uri | URI binary with parsing +System.Text.RegularExpressions.Regex | Erlang `re` module (PCRE) +System.Text.StringBuilder | Mutable binary builder +System.Collections.Generic.List | Process dict ref (list) +System.Collections.Generic.Dictionary | Process dict ref (map) +System.Collections.Generic.HashSet | Process dict ref (map-based set) +System.Collections.Generic.Queue | Process dict ref (Erlang queue) +System.Collections.Generic.Stack | Process dict ref (list) +System.Diagnostics.Stopwatch | `erlang:monotonic_time` +Records | Erlang maps (`#{}`) +Anonymous Records | Erlang maps (`#{}`) +Tuples | Erlang tuples (`{}`) + +## FSharp.Core + +Most of FSharp.Core operators are supported, as well as formatting with `sprintf`, `printfn`, or `failwithf`. +The following types and/or corresponding modules from FSharp.Core lib will translate to Erlang: + +.NET | Erlang +------------------|---------------------------------------------------------- +Tuples | Erlang tuples +Option | (erased) — `Some(x)` = `x`, `None` = `undefined` +String | Binary +List | Erlang linked list (cons cells) +Map | Erlang native map (`#{}`) +Set | `ordsets` (sorted list) +ResizeArray | Process dict ref (list) +Record | Erlang map (`#{}`) +Anonymous Record | Erlang map (`#{}`) +Result | `{ok, Value}` / `{error, Error}` +Async | CPS function `fun(Ctx) -> ... end` +Task | Alias for Async on Beam + +### Caveats + +- Options are **erased** in Erlang (`Some 5` becomes just `5` and `None` becomes `undefined`). Nested options use wrapped representation (`{some, x}`) to avoid ambiguity. +- **Records** compile to Erlang maps with snake_case atom keys. +- **Anonymous Records** also compile to Erlang maps. +- **Result** maps to Erlang's idiomatic `{ok, V}` / `{error, E}` convention. +- **Unit** is represented as the `ok` atom, which is distinct from `undefined` (None) — unlike JS/Python where both map to similar concepts. + +## Interfaces and Protocols + +F# interfaces compile to Erlang maps of closures (dispatch maps): + +.NET | Erlang | Comment +--------------|------------------------------|------------------------------------ +`IEquatable` | Native `=:=` | Deep structural equality on all types +`IEnumerator` | Iterator pattern | `next()` style iteration +`IEnumerable` | List iteration | `for` loop / `lists:foreach` +`IComparable` | Native `<`, `>`, `=<`, `>=` | Works on all Erlang terms +`IDisposable` | Manual cleanup | No `with` statement equivalent +`ToString` | `fable_string:to_string/1` | String formatting + +## Object Oriented Programming + +F# OOP features like interfaces, abstract classes, inheritance, and overloading are supported. Object expressions compile to Erlang maps of closures. + +## Numeric Types + +Erlang has native arbitrary-precision integers, making integer support much simpler than on JavaScript or Python targets. No wrapper types or Rust NIFs are needed: + +F# | .NET | Erlang | Notes +:----------------|:-----------|:-------------|---------------------------------- +bool | Boolean | `true`/`false` atoms | Native +int | Int32 | `integer()` | Native arbitrary-precision +byte | Byte | `integer()` | Native +sbyte | SByte | `integer()` | Native +int16 | Int16 | `integer()` | Native +int64 | Int64 | `integer()` | Native (no BigInt library needed) +uint16 | UInt16 | `integer()` | Native +uint32 | UInt32 | `integer()` | Native +uint64 | UInt64 | `integer()` | Native +float / double | Double | `float()` | Native IEEE 754 +float32 / single | Single | `float()` | Native IEEE 754 +decimal | Decimal | Custom | Fixed-scale integer implementation +bigint | BigInteger | `integer()` | Native (Erlang integers ARE arbitrary-precision) + +### Sized Integer Overflow + +Like Python, Erlang has native arbitrary-precision integers. Sized integer wrapping (for overflow semantics of `int32`, `int64`, etc.) uses Erlang's bit syntax: + +```erlang +%% Wrapping int32 arithmetic +wrap32(N) -> + <> = <>, + V. +``` + +## Reflection + +Full `FSharp.Reflection` support is available via `fable_reflection.erl`: + +- `FSharpType.IsTuple`, `IsRecord`, `IsUnion`, `IsFunction` +- `FSharpType.GetTupleElements`, `GetRecordFields`, `GetUnionCases` +- `FSharpValue.GetRecordFields`, `MakeRecord` +- `FSharpValue.GetTupleFields`, `MakeTuple`, `GetTupleField` +- `FSharpValue.GetUnionFields`, `MakeUnion` +- `PropertyInfo.GetValue` + +## Async and Concurrency + +F# async workflows use CPS (Continuation-Passing Style) where `Async` = `fun(Ctx) -> ok end` with a context map containing `on_success`, `on_error`, `on_cancel`, and `cancel_token`. + +F# | Erlang +----------------------------|--------------------------------------- +`async { return x }` | CPS function with `on_success` callback +`let! x = comp` | `bind(Comp, fun(X) -> ... end)` +`Async.RunSynchronously` | CPS invocation in same process +`Async.Parallel` | `spawn` per computation, `receive` to collect +`Async.Sleep` | `timer:sleep(Ms)` +`task { return x }` | Same as async (alias on Beam) + +:::info +`task { }` and `async { }` compile to the same CPS representation on Beam. The hot-start semantics of .NET `Task` are not preserved. Downcasting from `obj` to `Task` or `Async` is not supported. +::: + +### CancellationToken + +Full cancellation support via `fable_cancellation.erl`: + +- `CancellationTokenSource` — create, cancel, cancel after timeout +- `Register` / `Unregister` — listener management +- `IsCancellationRequested` — poll cancellation state +- Timer-based auto-cancel via `cancel_after` + +## Observable + +Full `Observable` module support: + +- `subscribe`, `add`, `choose`, `filter`, `map` +- `merge`, `pairwise`, `partition`, `scan`, `split` + +## Sequence Expressions + +Sequence expressions are supported and compile to lazy evaluation: + +```fs +let numbers = seq { + yield 1 + yield 2 + yield! [3; 4; 5] +} +``` + +## String Formatting + +Full F# format string support (`%d`, `%s`, `%.2f`, `%g`, `%x`, etc.) via `fable_string.erl` runtime: + +- `sprintf` — format to string +- `printfn` — format to stdout +- `eprintfn` — format to stderr +- `failwithf` — format and throw +- `String.Format` — .NET-style positional format strings + +String interpolation (`$"Hello, {name}!"`) compiles to `iolist_to_binary` with appropriate type conversions. + +## Tail Call Optimization + +Erlang has native tail call optimization, so recursive F# functions compile to efficient tail-recursive Erlang functions without needing a trampoline. diff --git a/docs/docs/beam/features.md b/docs/docs/beam/features.md new file mode 100644 index 0000000..707e803 --- /dev/null +++ b/docs/docs/beam/features.md @@ -0,0 +1,458 @@ +--- +title: Features +layout: standard +toc: + to: 4 +--- + +In this section, we will cover specific features of Fable when targeting the BEAM (Erlang). + +:::warning +Beam target is in alpha meaning that breaking changes can happen between minor versions. +::: + +## Utilities + +### `nativeOnly` + +`nativeOnly` provides a dummy implementation used when writing bindings to Erlang libraries. + +```fs +[] +let reverse : 'a list -> 'a list = nativeOnly +``` + +The thrown exception should never be seen as `nativeOnly` calls are replaced by actual Erlang module calls. + +### Automatic case conversion + +When targeting Erlang, Fable automatically converts F# camelCase names to Erlang snake_case names. + +```fs +let addTwoNumbers x y = + x + y +``` + +generates: + +```erlang +add_two_numbers(X, Y) -> + X + Y. +``` + +Record fields are also converted to snake_case atoms: + +```fs +type User = { FirstName: string; Age: int } +``` + +generates: + +```erlang +#{first_name => <<"Alice">>, age => 30} +``` + +### Erlang keyword escaping + +F# identifiers that conflict with Erlang reserved words are automatically escaped with a `_` suffix: + +```fs +let maybe x = x + 1 +let receive x = x * 2 +``` + +generates: + +```erlang +maybe_(X) -> X + 1. +receive_(X) -> X * 2. +``` + +## Imports + +Fable provides attributes to import Erlang modules and functions. + +### `[]` + +Import a function from an Erlang module: + +```fs +[] +let reverse (xs: 'a list) : 'a list = nativeOnly +``` + +generates: + +```erlang +lists:reverse(Xs) +``` + +### `[]` + +Use the `Emit` attribute to inline Erlang code directly: + +```fs +[] +let reverse (xs: 'a list) : 'a list = nativeOnly +``` + +## Emit, when F# is not enough + +Emit allows you to write Erlang code directly in F#. + +:::danger +Content of emit snippets is not validated by the F# compiler, so you should use this feature sparingly. +::: + +### `[]` + +Decorate functions with `Emit` to inline Erlang expressions. Use `$0`, `$1`, etc. to reference arguments: + +```fs +[] +let add (x: int) (y: int) : int = nativeOnly + +let result = add 1 2 +``` + +generates: + +```erlang +Result = 1 + 2. +``` + +### `emitExpr` + +Destructure a tuple of arguments and apply to literal Erlang code: + +```fs +open Fable.Core.ErlInterop + +let two : int = + emitExpr (1, 1) "$0 + $1" +``` + +generates: + +```erlang +Two = 1 + 1. +``` + +## Discriminated Unions + +F# discriminated unions compile to atom-tagged tuples in Erlang, which is the idiomatic Erlang convention: + +```fs +type Shape = + | Circle of radius: float + | Rectangle of width: float * height: float + | Point +``` + +generates: + +```erlang +%% Circle(5.0) becomes: +{circle, 5.0} + +%% Rectangle(3.0, 4.0) becomes: +{rectangle, 3.0, 4.0} + +%% Point becomes: +point +``` + +Fieldless cases compile to bare atoms for efficiency. + +### Pattern Matching + +Pattern matching on DUs uses Erlang's native pattern matching: + +```fs +let area shape = + match shape with + | Circle r -> 3.14159 * r * r + | Rectangle(w, h) -> w * h + | Point -> 0.0 +``` + +generates: + +```erlang +area(Shape) -> + case Shape of + {circle, R} -> 3.14159 * R * R; + {rectangle, W, H} -> W * H; + point -> 0.0 + end. +``` + +## Records + +F# records compile to Erlang maps: + +```fs +type User = { Name: string; Age: int } + +let user = { Name = "Alice"; Age = 30 } +let name = user.Name +``` + +generates: + +```erlang +User = #{name => <<"Alice">>, age => 30}. +Name = maps:get(name, User). +``` + +Record update syntax works naturally: + +```fs +let older = { user with Age = user.Age + 1 } +``` + +## Option Type + +Options use an erased representation for efficiency: + +- `None` compiles to the `undefined` atom +- `Some(x)` is erased to just `x` for simple cases +- Nested options (`Option>`) use wrapped representation: `{some, x}` + +```fs +let greet name = + match name with + | Some n -> printfn "Hello, %s!" n + | None -> printfn "Hello, stranger!" +``` + +generates: + +```erlang +greet(Name) -> + case Name of + undefined -> io:format("Hello, stranger!~n"); + N -> io:format("Hello, ~s!~n", [N]) + end. +``` + +## Result Type + +F# `Result` maps to Erlang's idiomatic `{ok, Value}` / `{error, Error}` convention: + +```fs +let divide x y = + if y = 0 then Error "Division by zero" + else Ok (x / y) +``` + +generates: + +```erlang +divide(X, Y) -> + case Y =:= 0 of + true -> {error, <<"Division by zero">>}; + false -> {ok, X div Y} + end. +``` + +## Structural Equality and Comparison + +Erlang's native `=:=` operator performs deep structural comparison on all types (tuples, maps, lists, atoms, numbers, binaries), which matches F#'s structural equality semantics perfectly. No runtime library is needed: + +```fs +let a = { Name = "Alice"; Age = 30 } +let b = { Name = "Alice"; Age = 30 } +a = b // true +``` + +generates: + +```erlang +A =:= B. %% Deep comparison, returns true +``` + +Structural comparison uses Erlang's native ordering operators (`<`, `>`, `=<`, `>=`), which work on all Erlang terms. + +## Async and Task + +F# `async` and `task` computation expressions are supported using CPS (Continuation-Passing Style): + +```fs +let fetchData () = async { + do! Async.Sleep 1000 + return "Hello from BEAM!" +} + +Async.RunSynchronously (fetchData ()) +``` + +- `Async.Parallel` spawns one Erlang process per computation and collects results via message passing +- `Async.Sleep` uses `timer:sleep` +- `task { }` is an alias for `async { }` on the Beam target — both compile to the same CPS representation. The hot-start semantics of .NET `Task` are not preserved. Downcasting from `obj` to `Task` or `Async` is not supported + +## MailboxProcessor + +F#'s `MailboxProcessor` is supported using an in-process CPS continuation model: + +```fs +let agent = MailboxProcessor.Start(fun inbox -> + let rec loop count = async { + let! msg = inbox.Receive() + printfn "Received: %s (count: %d)" msg count + return! loop (count + 1) + } + loop 0 +) + +agent.Post "Hello" +agent.Post "World" +``` + +## `[]` + +### Erased unions + +Decorate a union type with `[]` to tell Fable not to emit code for that type. The union cases are replaced with their underlying values: + +```fs +[] +type ValueType = + | Number of int + | Text of string + +[] +let processList (value: ValueType) : unit = nativeOnly + +processList (Number 42) +processList (Text "hello") +``` + +generates: + +```erlang +lists:process(42). +lists:process(<<"hello">>). +``` + +### `U2`, `U3`, ..., `U9` + +Fable provides built-in erased union types that you can use without defining custom erased unions: + +```fs +open Fable.Core + +[] +let handle (arg: U2) : unit = nativeOnly + +handle (U2.Case1 "hello") +handle (U2.Case2 42) +``` + +## `[]` + +:::info +These union types must not have any data fields as they will be compiled to a string matching the name of the union case. +::: + +```fs +open Fable.Core + +[] +type LogLevel = + | Debug + | Info + | Warning + +[] +let log (level: LogLevel) (msg: string) : unit = nativeOnly + +log Info "Application started" +``` + +generates: + +```erlang +logger:log(<<"info">>, <<"Application started">>). +``` + +## Name Mangling + +Because Erlang doesn't support function overloading, Fable mangles names when necessary: + +```fs +module A.Long.Namespace.RootModule + +// Root module functions keep their names +let add (x: int) (y: int) = x + y + +module Nested = + // Nested functions get prefixed + let add (x: int) (y: int) = x * y +``` + +generates: + +```erlang +add(X, Y) -> X + Y. + +nested_add(X, Y) -> X * Y. +``` + +Fable will never change the names of: + +- Record fields +- Interface and abstract members +- Functions and values in the **root module** + +### `[]` + +Use `AttachMembers` to keep all members as standard, non-mangled Erlang functions. Be aware that overloads won't work in this case. + +## Automatic Uncurrying + +Fable automatically uncurries functions when passed to and from Erlang, so in most cases you can use them as if they were uncurried: + +```fs +let execute (f: int -> int -> int) x y = + f x y +``` + +## Exceptions + +Exceptions use Erlang's `throw`/`catch` mechanism: + +```fs +try + failwith "Something went wrong" +with +| ex -> printfn "Error: %s" ex.Message +``` + +Custom F# exceptions compile to maps with type tags for discrimination: + +```fs +exception MyError of message: string +exception MyError2 of code: int * message: string + +try + raise (MyError "oops") +with +| :? MyError as e -> printfn "MyError: %s" e.Message +| :? MyError2 as e -> printfn "MyError2: code=%d" e.code +``` + +## Type Testing + +Runtime type checks use Erlang guard functions: + +```fs +let describe (x: obj) = + match x with + | :? int as i -> sprintf "Integer: %d" i + | :? string as s -> sprintf "String: %s" s + | :? float as f -> sprintf "Float: %f" f + | _ -> "Unknown" +``` + +generates guards like `is_integer(X)`, `is_binary(X)`, `is_float(X)`, etc. diff --git a/docs/docs/getting-started/beam.md b/docs/docs/getting-started/beam.md new file mode 100644 index 0000000..c760d07 --- /dev/null +++ b/docs/docs/getting-started/beam.md @@ -0,0 +1,71 @@ +--- +title: Beam (Erlang) +layout: standard +--- + +:::warning +Beam target is in alpha meaning that breaking changes can happen between minor versions. +::: + +This section is specific to Beam (Erlang) targeting. It will guide you through the process of setting up your project and using Fable to compile F# to Erlang. + +:::info +Please make sure you followed the [Fable setup guide](/docs/2-steps/your-first-fable-project) before continuing. +::: + +## Prerequisites + +You need [Erlang/OTP](https://www.erlang.org/downloads) 24 or higher installed. Verify with: + +```bash +erl -eval 'erlang:display(erlang:system_info(otp_release)), halt().' -noshell +``` + +
    + +
  • + +Compile your project to Erlang: + +```bash +dotnet fable --lang beam +``` + +This generates `.erl` files in the project directory (where the `.fsproj` is) by default. + +
  • + +
  • + +Compile the generated Erlang files: + +```bash +erlc -o output/fable_modules/fable-library-beam output/fable_modules/fable-library-beam/*.erl +erlc -pa output/fable_modules/fable-library-beam -o output output/*.erl +``` + +
  • + +
  • + +Run your code: + +```bash +erl -pa output -pa output/fable_modules/fable-library-beam -noshell -eval 'program:main(), halt().' +``` + +
  • + +
  • + +Run Fable in watch mode for development: + +```bash +dotnet fable watch --lang beam +``` + +
  • + +
+ +For more details, see [Build and Run](/docs/beam/build-and-run). diff --git a/docs/docs/index.md b/docs/docs/index.md index cbdaaa6..be77e8b 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -69,6 +69,10 @@ F# is already used on the server for web and cloud apps, and it's also used quit PHP Experimental + + Beam (Erlang) + Experimental + diff --git a/docs/docs/menu.json b/docs/docs/menu.json index 5d81a14..0b98d56 100644 --- a/docs/docs/menu.json +++ b/docs/docs/menu.json @@ -17,6 +17,7 @@ "docs/getting-started/javascript", "docs/getting-started/typescript", "docs/getting-started/python", + "docs/getting-started/beam", "docs/getting-started/rust", "docs/getting-started/cli" ] @@ -66,6 +67,15 @@ "docs/python/compatibility" ] }, + { + "type": "section", + "label": "Beam (Erlang)", + "items": [ + "docs/beam/build-and-run", + "docs/beam/features", + "docs/beam/compatibility" + ] + }, { "type": "section", "label": "Miscellaneous",