Skip to content

Commit 215ce84

Browse files
dbrattliMangelMaxime
authored andcommitted
feat: add Fable/BEAM (Erlang) as a test target
Add `beam` as a fourth Fable runtime (alongside dotnet/js/ts/python) for the core pilot libraries: Ink, Nib, Parchment, Quill. Nib.Browser stays JS-only; Hedgehog and Nib.Snapshot are deferred. - build/Commands/Test.fs: Runtime.Beam (beam/erlang alias) + a runBeam step (fable --lang beam -> rebar3 compile -> erl main:main([]) via ERL_LIBS). - Quill: TargetPlatform.Beam, Compiler.isBeam detection, skipIfBeam, and halt(exitCode) exit propagation in universalRunTests. - Idiomatic FABLE_COMPILER_BEAM branches for platform primitives (cwd, isCI, stopwatch, writeRaw, Ink NO_COLOR, Parchment stderr); the JS-only browser sink is excluded from beam. - CI: erlef/setup-beam (OTP + rebar3); .gitignore beam-build/. Verified against local Fable: Ink 51/51, Nib 68/68, Quill 29 + 1 skip, Parchment 27/27. Requires an unreleased Fable (3 Beam runtime fixes: Async.StartChild, DU ordered comparison, function PhysicalEquality); bump the `fable` tool once released so the tool-based build picks them up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> chore: bump Fable chore: simplify some comments
1 parent 812e97a commit 215ce84

14 files changed

Lines changed: 186 additions & 112 deletions

File tree

.github/workflows/test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ jobs:
2929
node-version: 24
3030
cache: npm
3131

32+
- name: Setup Erlang/OTP
33+
uses: erlef/setup-beam@v1
34+
with:
35+
otp-version: "27"
36+
rebar3-version: "3"
37+
3238
- name: Install Playwright browsers
3339
run: |
3440
npm install

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ docs/src/content/docs/.gitignore
4242
# Fable TS/Python test output (kept out of obj/)
4343
ts-build/
4444
py-build/
45+
46+
# Fable BEAM/Erlang test output (transpiled .erl + rebar3 build)
47+
beam-build/

build/Commands/Test.fs

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module EasyBuild.Commands.Test
22

3+
open System.IO
34
open Spectre.Console.Cli
45
open SimpleExec
56
open BlackFox.CommandLine
@@ -12,6 +13,7 @@ type Runtime =
1213
| JavaScript
1314
| TypeScript
1415
| Python
16+
| Beam
1517
| AllRuntime
1618

1719
static member All =
@@ -26,6 +28,7 @@ type Runtime =
2628
| JavaScript -> "js"
2729
| TypeScript -> "ts"
2830
| Python -> "py"
31+
| Beam -> "beam"
2932

3033
static member fromString(value: string) =
3134
match value.ToLowerInvariant() with
@@ -37,18 +40,19 @@ type Runtime =
3740
| "ts" -> TypeScript
3841
| "python"
3942
| "py" -> Python
43+
| "beam"
44+
| "erlang" -> Beam
4045
| _ ->
4146
failwith
4247
$"""Unknown runtime value: '%s{value}'
4348
4449
It should be one of the following:
4550
- dotnet
46-
- javascript
47-
- js
48-
- typescript
49-
- ts
50-
- python
51-
- py
51+
- javascript (or js)
52+
- typescript (or ts)
53+
- python (or py)
54+
- beam (or erlang)
55+
- all
5256
"""
5357

5458
type Project =
@@ -98,6 +102,7 @@ It should be one of the following:
98102
- nib-snapshot
99103
- parchment
100104
- quill
105+
- hedgehog
101106
"""
102107

103108
member this.Dir =
@@ -126,13 +131,15 @@ It should be one of the following:
126131
JavaScript
127132
TypeScript
128133
Python
134+
Beam
129135
]
130136
| Scriptorium_Nib ->
131137
[
132138
DotNet
133139
JavaScript
134140
TypeScript
135141
Python
142+
Beam
136143
]
137144
| Scriptorium_Nib_Browser -> [ JavaScript ]
138145
| Scriptorium_Nib_Snapshot ->
@@ -148,13 +155,15 @@ It should be one of the following:
148155
JavaScript
149156
TypeScript
150157
Python
158+
Beam
151159
]
152160
| Scriptorium_Quill ->
153161
[
154162
DotNet
155163
JavaScript
156164
TypeScript
157165
Python
166+
Beam
158167
]
159168
| AllProject -> failwith "All is not a real project, it should be captured before"
160169

@@ -218,15 +227,46 @@ let private testProject (project: Project) (runtime: Runtime) (isWatch: bool) =
218227
let dotnetRun (args: string) =
219228
Command.Run("dotnet", args, workingDirectory = project.Dir)
220229

230+
// BEAM has no single-step `--run`: Fable emits `.erl`, which must be compiled
231+
// with rebar3 and executed on the Erlang VM.
232+
let runBeam () =
233+
let beamBuildDir = Path.Combine(project.Dir, "beam-build")
234+
235+
let erlLibs =
236+
Path.GetFullPath(Path.Combine(beamBuildDir, "_build", "default", "lib"))
237+
238+
// 1. Transpile F# to Erlang into beam-build/ (also emits fable-library-beam).
239+
Command.Run(
240+
"dotnet",
241+
"fable --lang beam -o beam-build",
242+
workingDirectory = project.Dir
243+
)
244+
// 2. Compile the generated Erlang with rebar3.
245+
Command.Run("rebar3", "compile", workingDirectory = beamBuildDir)
246+
// 3. Run the suite on the BEAM. ERL_LIBS makes every compiled app's ebin
247+
// (incl. fable-library-beam) loadable. Fable emits the [<EntryPoint>] as
248+
// main:main/1 (argv); it runs the suite and halts with the exit code.
249+
Command.Run(
250+
"erl",
251+
"-noshell -eval main:main([]) -s init stop",
252+
workingDirectory = beamBuildDir,
253+
configureEnvironment =
254+
(fun (env: System.Collections.Generic.IDictionary<string, string>) ->
255+
env.["ERL_LIBS"] <- erlLibs
256+
)
257+
)
258+
221259
match runtime with
222260
| DotNet -> dotnetRun dotnet
223261
| JavaScript -> dotnetRun javascript
224262
| TypeScript -> dotnetRun typescript
225263
| Python -> dotnetRun python
264+
| Beam -> runBeam ()
226265
| AllRuntime ->
227266
dotnetRun javascript
228267
dotnetRun typescript
229268
dotnetRun python
269+
runBeam ()
230270
dotnetRun dotnet
231271
with :? ExitCodeException ->
232272
printfn
@@ -252,6 +292,7 @@ Accepted values:
252292
- nib-snapshot
253293
- parchment
254294
- quill
295+
- hedgehog
255296
- all
256297
257298
Default: all""")>]
@@ -265,6 +306,7 @@ Accepted values:
265306
- javascript (or js)
266307
- typescript (or ts)
267308
- python (or py)
309+
- beam (or erlang)
268310
- all
269311
270312
Default: all""")>]

dotnet-tools.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"rollForward": false
1111
},
1212
"fable": {
13-
"version": "5.7.0",
13+
"version": "5.8.0",
1414
"commands": [
1515
"fable"
1616
],

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
name = "scriptorium-python-tests"
44
version = "0"
55
requires-python = ">=3.12,<4.0"
6-
dependencies = ["fable-library==5.7.0"]
6+
dependencies = ["fable-library==5.8.0"]
77

88
# Not a package: install the dependencies only, never build/install this project itself.
99
[tool.uv]

src/Scriptorium.Ink/Ink.fs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ let private colorsEnabled: bool =
2020
Fable.Core.PyInterop.emitPyExpr () "__import__('os').environ.get('NO_COLOR') is None"
2121
#endif
2222

23-
#if !(FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT || FABLE_COMPILER_PYTHON)
23+
#if FABLE_COMPILER_BEAM
24+
// os:getenv/1 returns the atom false when NO_COLOR is unset (i.e. colors enabled).
25+
Fable.Core.BeamInterop.emitErlExpr () "os:getenv(\"NO_COLOR\") =:= false"
26+
#endif
27+
28+
#if !(FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT || FABLE_COMPILER_PYTHON || FABLE_COMPILER_BEAM)
2429
System.Environment.GetEnvironmentVariable("NO_COLOR") |> isNull
2530
#endif
2631

src/Scriptorium.Parchment/Sinks/JavaScript.fs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ open Fable.Core.JS
44
open Fable.Core.JsInterop
55
open Scriptorium.Parchment
66

7-
// This module emits raw JavaScript (console API), which the Python target cannot compile, so it is
8-
// excluded there. It stays available on .NET for source compatibility (as before).
9-
#if !FABLE_COMPILER_PYTHON
7+
// This module emits raw JavaScript (console API), which the Python and BEAM targets cannot compile,
8+
// so it is excluded there. It stays available on .NET for source compatibility (as before).
9+
#if !(FABLE_COMPILER_PYTHON || FABLE_COMPILER_BEAM)
1010
/// <summary>
1111
/// Sinks that rely on JavaScript-specific APIs.
1212
/// </summary>

src/Scriptorium.Parchment/Sinks/Universal.fs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ module Universal =
2424
#if FABLE_COMPILER_PYTHON
2525
Fable.Core.PyInterop.emitPyStatement msg "print($0, file=__import__('sys').stderr)"
2626
#endif
27-
#if !(FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT || FABLE_COMPILER_PYTHON)
27+
#if FABLE_COMPILER_BEAM
28+
// ~ts treats the binary as data (not a format string); standard_error is stderr.
29+
Fable.Core.BeamInterop.emitErlStatement msg "io:format(standard_error, \"~ts~n\", [$0])"
30+
#endif
31+
#if !(FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT || FABLE_COMPILER_PYTHON || FABLE_COMPILER_BEAM)
2832
System.Console.Error.WriteLine(msg)
2933
#endif
3034

src/Scriptorium.Quill/DSL.fs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ module Configurers =
3939
else
4040
config
4141

42+
let skipIfBeam (config: TestConfig) : TestConfig =
43+
if currentPlatform = Beam then
44+
{ config with
45+
Skip = true
46+
}
47+
else
48+
config
49+
4250
let timeout (ms: int) (config: TestConfig) : TestConfig =
4351
{ config with
4452
TimeoutMs = Some ms

src/Scriptorium.Quill/Prelude.fs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ module Prelude =
1818
Fable.Core.PyInterop.emitPyExpr () "__import__('os').getcwd()"
1919
#endif
2020

21-
#if !(FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT || FABLE_COMPILER_PYTHON)
21+
#if FABLE_COMPILER_BEAM
22+
// file:get_cwd() returns {ok, Dir} where Dir is a charlist; F# strings are Erlang binaries.
23+
Fable.Core.BeamInterop.emitErlExpr () "list_to_binary(element(2, file:get_cwd()))"
24+
#endif
25+
26+
#if !(FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT || FABLE_COMPILER_PYTHON || FABLE_COMPILER_BEAM)
2227
System.Environment.CurrentDirectory
2328
#endif
2429

@@ -43,7 +48,16 @@ module Prelude =
4348
member _.ElapsedMs() : int = int ((now () - startTime) * 1000.0)
4449
#endif
4550

46-
#if !(FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT || FABLE_COMPILER_PYTHON)
51+
#if FABLE_COMPILER_BEAM
52+
type UniversalStopwatch() =
53+
let now () : int =
54+
Fable.Core.BeamInterop.emitErlExpr () "erlang:monotonic_time(millisecond)"
55+
56+
let startTime = now ()
57+
member _.ElapsedMs() : int = now () - startTime
58+
#endif
59+
60+
#if !(FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT || FABLE_COMPILER_PYTHON || FABLE_COMPILER_BEAM)
4761
type UniversalStopwatch() =
4862
let sw = System.Diagnostics.Stopwatch.StartNew()
4963
member _.ElapsedMs() : int = int sw.ElapsedMilliseconds
@@ -55,6 +69,8 @@ module Prelude =
5569
DotNet
5670
elif Compiler.isPython then
5771
Python
72+
elif Compiler.isBeam then
73+
Beam
5874
else
5975
JavaScript
6076

@@ -67,6 +83,11 @@ module Prelude =
6783
Fable.Core.PyInterop.emitPyExpr () "__import__('os').environ.get('CI') is not None"
6884
#endif
6985

70-
#if !(FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT || FABLE_COMPILER_PYTHON)
86+
#if FABLE_COMPILER_BEAM
87+
// os:getenv/1 returns the value (charlist) or the atom false when unset.
88+
Fable.Core.BeamInterop.emitErlExpr () "os:getenv(\"CI\") =/= false"
89+
#endif
90+
91+
#if !(FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT || FABLE_COMPILER_PYTHON || FABLE_COMPILER_BEAM)
7192
System.Environment.GetEnvironmentVariable("CI") |> isNull |> not
7293
#endif

0 commit comments

Comments
 (0)