Skip to content

[WIP] Fable Beam Initial Scaffolding#4340

Draft
dbrattli wants to merge 29 commits intomainfrom
fable-beam
Draft

[WIP] Fable Beam Initial Scaffolding#4340
dbrattli wants to merge 29 commits intomainfrom
fable-beam

Conversation

@dbrattli
Copy link
Collaborator

@dbrattli dbrattli commented Feb 8, 2026

Summary

Add a new Erlang/BEAM compilation target to Fable, enabling F# code to be compiled to Erlang and run on the BEAM virtual machine. This opens the door to leveraging the BEAM's lightweight process model and massive concurrency capabilities from F#, making constructs like MailboxProcessor naturally map to the actor-based runtime they were inspired by.

792 tests passing, 0 failures.

Compiler Infrastructure

  • Beam AST definitions (Beam.AST.fs)
  • F# → Erlang transform (Fable2Beam.fs)
  • Erlang source code printer (ErlangPrinter.fs)
  • Beam-specific BCL replacements (Beam/Replacements.fs)
  • CLI, build system, and CI integration

Language Features

  • Discriminated unions, records, tuples, and pattern matching
  • Classes with constructors, properties, and methods
  • Mutual recursion via named fun dispatch
  • Tail-call optimization (native in Erlang)
  • Try/catch with exception message extraction
  • Mutable variables via process dictionary
  • For/while loops as tail-recursive funs
  • Seq expressions (yield, yield!, for, combine)
  • Basic reflection (TypeInfo as Erlang maps)

F# Standard Library Coverage

  • List — 94 tests: fold, map, filter, sort, find, choose, collect, scan, zip, distinct, pairwise, permute, transpose, and more
  • Array — 80 tests: all List operations plus indexing, copy, partition, windowed, splitInto, updateAt, insertAt, removeAt
  • Map — 41 tests: native Erlang #{} maps with full FSharpMap API
  • Seq — eager list-based implementation with delay, unfold, init, take, skip, distinct
  • String — 48 tests: indexing, split, trim, pad, replace, join, contains, compare, Char module
  • Option — 26 tests: map, bind, defaultValue, filter, flatten, contains, iter, fold
  • Result — 17 functions via fable_result.erl
  • Arithmetic — 95 tests: Int32/Int64/BigInt (native arbitrary-precision), Float64, bitwise ops
  • Conversions — 43 tests: int/float/string conversions, System.Convert
  • Comparison — 34 tests: compare, hash, isNull, Equals, CompareTo, GetHashCode

Runtime Library (src/fable-library-beam/)

Native Erlang modules providing curried-compatible implementations:
fable_list.erl, fable_map.erl, fable_string.erl, fable_option.erl, fable_result.erl, fable_seq.erl, fable_char.erl, fable_comparison.erl, fable_convert.erl

Design Decisions

  • Arrays as lists: Erlang has no mutable arrays; F# arrays are represented as Erlang lists
  • Options as values: None = undefined atom, Some(x) = x (no wrapper tuple)
  • Unions as tagged tuples: Case(a, b) = {tag, A, B}
  • Classes as refs: make_ref() for identity, state stored in process dictionary
  • Curried lambdas: F# multi-arg lambdas compile as nested single-arg funs, matching Fable's standard currying

Not Yet Implemented

  • Interfaces and object expressions
  • Set module
  • Async/Task
  • sprintf/printf formatting beyond basic %s/%d/%f
  • Custom equality/comparison (IEquatable, IComparable)
  • Interop with OTP (gen_server, supervisors, etc.)

dbrattli and others added 26 commits February 7, 2026 07:24
Implements an F# to Erlang compiler target for the BEAM VM. Includes
AST definitions, Fable-to-Beam transform, Erlang printer, build system
integration, and a test suite covering arithmetic, unions, records,
pattern matching, functions, and lists.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…6 tests passing)

Intercept List module imports in Fable2Beam.fs to map head/tail/length/map/filter/fold/rev/append/sum
to Erlang's built-in hd/tl/length and lists:* functions. Handle fold arg order swap (F# acc,item vs
Erlang item,acc) and drop injected IGenericAdder from sum. Add unary negation in Beam Replacements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… tests passing)

Erlang has native arbitrary-precision integers, so Int64/UInt64/BigInt arithmetic
can use direct binary operators instead of library calls. Intercept standard
arithmetic operators for big integer types in Beam Replacements, same approach
as Python where bigint/nativeint go straight to native int.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…91 tests passing)

Uses a single named fun that dispatches on atom tags to resolve
forward references between mutually recursive functions in Erlang.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… tests passing)

Map String.IsNullOrEmpty to (S =:= undefined) orelse (S =:= <<"">>)
and String.Contains (via indexOf) to binary:match with nomatch fallback.
This fixes util.erl compilation failure from unknown_call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ules (91 tests, 0 failures)

Add TryCatch to Beam AST and Erlang printer. Caught exceptions are
wrapped in a map with "message" field so e.Message field access works.
Fix test runner to only discover modules ending in _tests, avoiding
false failures from calling util helper functions as tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… 0 failures)

Handle Extended/Throw AST node by generating erlang:error(msg). Intercept
FailWith/InvalidOp/InvalidArg/Raise in Beam Replacements to pass the message
string directly instead of wrapping in a non-existent Exception constructor.
Fix TryCatch message extraction to pass binary reasons through directly using
is_binary guard, avoiding ~p formatting that wraps binaries in <<>> delimiters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tests, 0 failures)

Add Beam-specific replacements for String instance methods (Length, ToUpper, ToLower,
Trim, StartsWith, EndsWith, Substring, Replace) and Option module functions (defaultValue,
map, bind, isSome, isNone). Implement Emit AST node for inline Erlang code generation
with $0/$1 substitution, and fix guard printing in Case clauses.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… tests, 0 failures)

Implement ForLoop/WhileLoop as tail-recursive named funs, mutable variables
via process dictionary (put/get), type conversions (int/float/string), and
fix string + operator to use iolist_to_binary instead of arithmetic +.
Also fix float literal printing to always include decimal point and Let
value hoisting to prevent incorrect nested match expressions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement arrays as Erlang lists with full Array module support (map, filter,
fold, sort, reduce, etc.), array indexing via lists:nth with 0-to-1 base
conversion, TypeTest using Erlang is_* guards, and NewArray value handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rators (229 tests, 0 failures)

- Add F# Map → Erlang native #{} maps (ofList, add, find, tryFind, containsKey,
  remove, isEmpty, count, toList, map, filter, fold, exists, forall, empty,
  instance methods)
- Add 22 new List module operations (contains, exists, forall, find, tryFind,
  choose, collect, sort, sortBy, sortDescending, partition, zip, unzip, min,
  max, reduce, concat, singleton, foldBack, indexed)
- Add String.Split, String.Join, String.concat, String.replicate, and more
- Add math operators (abs, sqrt, sin, cos, tan, exp, log, floor, ceil, round,
  pow, sign, min, max) to prevent JS math module fallthrough
- Fix emitExpr variable scoping by wrapping case patterns in (fun() -> end)()
- Add map module import handler in Fable2Beam for JS fallthrough

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migrate ~46 complex emitExpr patterns (IIFEs, case expressions, arg-reordering
wrappers) from Beam/Replacements.fs to native Erlang modules in
src/fable-library-beam/. Simple 1-to-1 BIF mappings remain as emitExpr.

New library modules:
- fable_list.erl: fold, fold_back, reduce, map_indexed, sort_by/with, find,
  try_find, choose, collect, sum_by, min_by, max_by, indexed, zip
- fable_map.erl: try_find, fold, fold_back, map, filter, exists, forall,
  iterate, find_key, try_find_key, partition, try_get_value
- fable_string.erl: insert, remove, starts_with, ends_with, pad_left/right,
  replace, join, concat, replicate, is_null_or_empty, is_null_or_white_space
- fable_option.erl: default_value, default_with, map, bind

Library functions use curried application (Fn(A))(B) to match Fable's curried
lambda compilation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ailures)

Implement comprehensive Seq module for the Beam target using eager Erlang lists.
Add fable_seq.erl runtime library for Seq-specific operations (delay, unfold, init,
take, skip, distinct, etc.) and seqModule handler in Beam Replacements dispatching
SeqModule + RuntimeHelpers. Also fix map_indexed using element(2,...) instead of
element(1,...) to extract result list from lists:mapfoldl.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…xing (273 tests, 0 failures)

Add range expression support (op_Range/op_RangeStep -> lists:seq), array indexing
(IntrinsicFunctions.GetArray -> lists:nth), and Array.mapi. Add Sudoku solver as
integration test exercising Seq, Array, ranges, and array comprehensions. Update
test runner to filter by test_ prefix and rename RecordTests accordingly. Update
FABLE-BEAM.md to reflect current status (Phase 4 Collections complete, 273 tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ts, 0 failures)

Add fable_result.erl runtime library with 17 Result module functions
(map, mapError, bind, isOk, isError, contains, count, defaultValue,
defaultWith, exists, fold, foldBack, forall, iter, toArray, toList,
toOption). Result values use Erlang tagged tuples: Ok x = {0, X},
Error e = {1, E}.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…hods (302 tests, 0 failures)

Implement classes as unique refs with state stored in process dictionary.
Constructors create a ref via make_ref() and store field values in a map.
Instance members receive the ref as first arg and access state via get/put.

Key changes:
- Add transformClassDeclaration for constructor generation from FieldSet IR
- Handle ThisValue/ThisArg/ThisIdentNames for proper this-reference mapping
- Modify FieldGet/FieldSet to use process dict for class instances
- Add isClassType helper excluding BCL/exception types to avoid badmap errors
- Sanitize $ and @ in function/variable names (sanitizeErlangVar)
- Prepend ThisArg to args in transformCall for instance method dispatch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… tests, 0 failures)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ve instance methods (347 tests, 0 failures)

Add fable_comparison.erl library with compare/2 returning -1/0/1. Handle comparison
operators (< > <= >=), compare, hash, isNull, nullArg, Object.ReferenceEquals,
PhysicalEquality, and .Equals/.CompareTo/.GetHashCode instance methods on strings,
numerics, booleans, chars, System.Object, and System.ValueType. Route System.Math
through operators for Max/Min/Abs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…, 0 failures)

Add fable_char.erl library with character classification (IsLetter, IsDigit, IsUpper,
IsLower, IsWhiteSpace, IsControl, IsPunctuation, IsSeparator, IsSymbol, IsNumber,
IsLetterOrDigit), case conversion (ToUpper, ToLower), ToString, Parse, and
GetUnicodeCategory. Support both single-char and (string, index) overloads.

Fix Char.ToString to emit <<C/utf8>> instead of integer_to_binary for char types.
Expand comparison tests with exception equality, map/array option equality, string
array comparison, hash on tuples/lists/records, and more primitive type coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… and parameterless constructors (413 tests, 0 failures)

- Add 14 tail-call tests (factorial, nested recursion, class methods, IIFE, tree search, state preservation)
- Add 9 seq expression tests (yield, yield!, for, combine, multiple yields, recursive traverse, array/list expressions)
- Fix LetRec single-binding to generate Erlang named funs (was only handled for mutual recursion)
- Fix containsIdentRef to handle Emit expressions (seq compilation uses Emit for Node branches)
- Fix parameterless class constructors to accept unit arg (_UnitVar) matching call-site arity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tests, 0 failures)

- Option: Add 26 tests (orElse, defaultWith, iter, map2/3, contains, filter, fold/foldBack, toArray/toList, flatten, count, forAll, exists, side-effects, defaultArg)
- PatternMatch: Add 17 tests (or-patterns, union matching, guard expressions, nested matching, result/list/char matching, tuple-bool-guard patterns)
- Record: Add 8 tests (recursive records, reserved word fields, mutating records, optional field equality, camel/pascal casing, anonymous record functions/equality)
- UnionType: Add 8 tests (many-arg cases, common targets, Tag field, active patterns, equality in filter)
- Add defaultArg handling in Beam Replacements operators
- Expand fable_option.erl with 14 new functions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sts, 0 failures)

- Add 50+ new List functions to fable_list.erl: init, replicate, scan, scanBack,
  tryHead, tryLast, tryItem, exactlyOne, tryExactlyOne, distinct, distinctBy,
  pairwise, exists2, forall2, map2, map3, mapi2, iter2, iteri, iteri2, average,
  averageBy, countBy, groupBy, unfold, splitAt, chunkBySize, windowed, splitInto,
  except, allPairs, permute, mapFold, mapFoldBack, pick, tryPick, reduceBack,
  findIndex, tryFindIndex, findBack, tryFindBack, zip3
- Add corresponding Replacements entries for all new List functions
- Expand ListTests.fs from 35 to 94 tests covering all new operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ic handling (664 tests, 0 failures)

- Add native bitwise operators (band/bor/bxor/bsl/bsr/bnot) instead of JS BigInt fallback
- Fix BigInt literal handling (FromZero/FromOne/FromString) for Erlang native integers
- Add all missing NumberConstant variants (Int8/UInt8/Int16/UInt16/UInt32/UInt64/Float32/etc.)
- Fix float literal precision (%.17g for full double precision)
- Expand System.Convert support for all numeric types
- Expand fable_map.erl with pick/try_pick/min_key_value/max_key_value/change
- Add 95 arithmetic tests, 43 conversion tests, 41 map tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…principles

- Create fable_convert.erl with robust to_float/1 that handles edge cases
  like "1." that Erlang's binary_to_float/1 rejects
- Replace all raw binary_to_float calls with fable_convert:to_float library calls
- Restore original F# test (don't modify tests to accommodate Erlang quirks)
- Update FABLE-BEAM.md: add "Design Principles" section emphasizing Beam as
  an independent target, not inheriting JS/Python patterns unnecessarily
- Update runtime library table and test counts (664 tests, 0 failures)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ilures)

Add comprehensive string support: String module functions (forall, exists, init,
collect, iter, iteri, map, mapi, filter), string constructors, IndexOf/LastIndexOf
with offsets, Trim with chars, Split with options, Contains, Compare, and string
indexing via binary:at.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ionide.Analyzers.Cli found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

dbrattli and others added 2 commits February 8, 2026 06:14
…ests, 0 failures)

Add 50+ new Array operations: init, copy, map2/map3, mapi/mapi2, mapFold/mapFoldBack,
scan/scanBack, reduceBack, findIndex, findBack/findIndexBack, tryFindBack/tryFindIndexBack,
pick/tryPick, partition, permute, distinct, skip/skipWhile, take/takeWhile, truncate,
countBy, groupBy, windowed, pairwise, splitInto, transpose, compareWith, updateAt,
insertAt/insertManyAt, removeAt/removeManyAt, average/averageBy, sortDescending, indexed.

Also fix fable_string split functions to handle char separators and options,
fix iteri argument order, and fix trim functions to handle single char arguments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add build-beam job to CI workflow with Erlang/OTP 27 setup
- Add Beam.AST.fs and Beam/Replacements.fs to Fable.Standalone.fsproj
  so Replacements.Api.fs can resolve Beam.Replacements references
- Add Beam case to standalone Main.fs pattern match (with error message
  since standalone doesn't include the Beam code generator)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dbrattli dbrattli marked this pull request as draft February 8, 2026 05:49
- Add StringComparison.Ordinal to all StartsWith/EndsWith calls
- Add %s/%d format specifiers to interpolated string holes
- Replace `string` function with .ToString() for type safety
- All 792 tests still pass, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@@ -0,0 +1,74 @@
namespace rec Fable.AST.Beam

type Atom = | Atom of string

Check notice

Code scanning / Ionide.Analyzers.Cli

Short description about StructDiscriminatedUnionAnalyzer Note

Consider adding [] to Discriminated Union
| Apply of func: ErlExpr * args: ErlExpr list
| Fun of clauses: ErlFunClause list
| NamedFun of name: string * clauses: ErlFunClause list
| Case of ErlExpr * ErlCaseClause list

Check notice

Code scanning / Ionide.Analyzers.Cli

Verifies each field in a union case is named. Note

Field inside union case is not named!
| Apply of func: ErlExpr * args: ErlExpr list
| Fun of clauses: ErlFunClause list
| NamedFun of name: string * clauses: ErlFunClause list
| Case of ErlExpr * ErlCaseClause list

Check notice

Code scanning / Ionide.Analyzers.Cli

Verifies each field in a union case is named. Note

Field inside union case is not named!
| Fun of clauses: ErlFunClause list
| NamedFun of name: string * clauses: ErlFunClause list
| Case of ErlExpr * ErlCaseClause list
| Match of ErlPattern * ErlExpr

Check notice

Code scanning / Ionide.Analyzers.Cli

Verifies each field in a union case is named. Note

Field inside union case is not named!
| Fun of clauses: ErlFunClause list
| NamedFun of name: string * clauses: ErlFunClause list
| Case of ErlExpr * ErlCaseClause list
| Match of ErlPattern * ErlExpr

Check notice

Code scanning / Ionide.Analyzers.Cli

Verifies each field in a union case is named. Note

Field inside union case is not named!
| Case of ErlExpr * ErlCaseClause list
| Match of ErlPattern * ErlExpr
| Block of ErlExpr list
| BinOp of op: string * ErlExpr * ErlExpr

Check notice

Code scanning / Ionide.Analyzers.Cli

Verifies each field in a union case is named. Note

Field inside union case is not named!
| Case of ErlExpr * ErlCaseClause list
| Match of ErlPattern * ErlExpr
| Block of ErlExpr list
| BinOp of op: string * ErlExpr * ErlExpr

Check notice

Code scanning / Ionide.Analyzers.Cli

Verifies each field in a union case is named. Note

Field inside union case is not named!
| Match of ErlPattern * ErlExpr
| Block of ErlExpr list
| BinOp of op: string * ErlExpr * ErlExpr
| UnaryOp of op: string * ErlExpr

Check notice

Code scanning / Ionide.Analyzers.Cli

Verifies each field in a union case is named. Note

Field inside union case is not named!
type ErlAttribute =
| ModuleAttr of Atom
| ExportAttr of (Atom * int) list
| CustomAttr of Atom * string

Check notice

Code scanning / Ionide.Analyzers.Cli

Verifies each field in a union case is named. Note

Field inside union case is not named!
type ErlAttribute =
| ModuleAttr of Atom
| ExportAttr of (Atom * int) list
| CustomAttr of Atom * string

Check notice

Code scanning / Ionide.Analyzers.Cli

Verifies each field in a union case is named. Note

Field inside union case is not named!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant