Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Fable.Transforms/Beam/FABLE-BEAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,17 @@ alone eliminates the single hardest piece of the Fable.Python runtime.
`fable_utils:new_ref([...])`. `derefArr`/`wrapArr` helpers in Replacements convert
between refs and plain lists for bulk operations. Binary comparison operators on arrays
use `fable_comparison:compare(A, B) op 0`. TypeTest for arrays uses `is_reference`.
When an array *literal* flows directly into an FFI/Emit binding that derefs its
argument (e.g. `maps:from_list(erlang:get($0))`), the naive output is
`...erlang:get(fable_utils:new_ref([...]))` — a pointless process-dict round-trip
on an immutable literal (it also leaks an un-erased process-dict entry).
`simplifyArrayRefDerefs` in `Fable2Beam.fs` cancels it on the Beam AST when
building an `Emit` node: if an argument is an inline `new_ref(list)` and *every*
occurrence of its `$N` placeholder in the macro template is deref-wrapped
(`erlang:get($N)`/`get($N)`), the deref is dropped from the template and the
underlying list is passed directly. Gating on the argument's AST shape (rather than
parsing rendered Erlang) keeps it robust; a ref-typed *variable* (a bound/mutable
array) keeps its deref.
- **Byte arrays via atomics**: Byte arrays (`Array<byte>`) use Erlang's `atomics` module
for true O(1) mutable read/write. Represented as `{byte_array, Size, AtomicsRef}` tuples.
Runtime helpers in `fable_utils.erl` handle both direct tuples and process-dict
Expand Down
41 changes: 40 additions & 1 deletion src/Fable.Transforms/Beam/Fable2Beam.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,44 @@ let rec mustWrapOption =
| Option _ -> true
| _ -> false

/// Simplify the process-dict ref round-trip that arises when an immutable array
/// *literal* is passed to an FFI/Emit binding that immediately derefs it.
///
/// Fable-BEAM materialises an array literal as a fresh, inline process-dict ref
/// (`fable_utils:new_ref([...])`). A binding that reads it back with `erlang:get($N)`
/// (or bare `get($N)`) therefore produces `erlang:get(fable_utils:new_ref([...]))` —
/// a round-trip that yields the literal straight back while leaking a never-erased
/// process-dict entry.
///
/// This is recognised on the Beam AST: when argument N is an inline `new_ref(list)`
/// and *every* occurrence of `$N` in the (short, authored) macro template is wrapped
/// by a deref, the deref is dropped from the template and the underlying list is
/// passed directly. Gating on the argument's AST shape — instead of parsing rendered
/// Erlang — keeps this robust; a ref-typed *variable* (a bound/mutable array) is left
/// untouched, so its deref is preserved.
let private simplifyArrayRefDerefs (macro: string) (args: Beam.ErlExpr list) : string * Beam.ErlExpr list =
let folder (macroAcc: string, argsAcc) (i, arg) =
match arg with
| Beam.ErlExpr.Call(Some "fable_utils", "new_ref", [ inner ]) ->
let ph = $"$%d{i}"
// A control-char sentinel (never present in a macro template) marks the
// deref-wrapped placeholders. `erlang:get` is replaced first so its inner
// `get(` isn't consumed by the bare form below.
let sentinel = "\u0000"

let stripped =
macroAcc.Replace($"erlang:get(%s{ph})", sentinel).Replace($"get(%s{ph})", sentinel)

// Collapse only when the placeholder was present and every occurrence was
// deref-wrapped — a surviving bare `$N` means some use site still needs the ref.
if stripped.Contains(sentinel) && not (stripped.Contains(ph)) then
stripped.Replace(sentinel, ph), argsAcc @ [ inner ]
else
macroAcc, argsAcc @ [ arg ]
| _ -> macroAcc, argsAcc @ [ arg ]

args |> List.indexed |> List.fold folder (macro, [])

let unwrapOptionalArg (arg: Fable.Expr) =
match arg with
| Fable.Value(Fable.NewOption(Some inner, _, _), _) -> inner
Expand Down Expand Up @@ -741,7 +779,8 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er
transformReceive com ctx emitInfo typ
else
let args = emitInfo.CallInfo.Args |> List.map (transformExpr com ctx)
Beam.ErlExpr.Emit(emitInfo.Macro, args)
let macro, args = simplifyArrayRefDerefs emitInfo.Macro args
Beam.ErlExpr.Emit(macro, args)

| TryCatch(body, catch_, finalizer, _range) ->
let erlBody = transformExpr com ctx body
Expand Down
21 changes: 21 additions & 0 deletions tests/Beam/InteropTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ let boolToString (x: bool) : string = nativeOnly
[<Emit("case $0 of undefined -> <<\"none\">>; Value -> Value end")>]
let emitWithValueCaseVar (x: string option) : string = nativeOnly

// FFI binding that derefs its array argument. Fable-BEAM represents arrays as
// process-dict refs, so an array literal passed here would otherwise generate a
// `erlang:get(fable_utils:new_ref([...]))` round-trip; the compiler must collapse
// that to a plain list so the BIF receives `[{...}]` directly.
[<Emit("maps:from_list(erlang:get($0))")>]
let mapFromPairs (pairs: (string * int) array) : obj = nativeOnly

[<Emit("maps:get($0, $1)")>]
let mapGet (key: string) (m: obj) : int = nativeOnly

#endif

// ============================================================
Expand Down Expand Up @@ -180,6 +190,17 @@ let ``test emitErlExpr can call Erlang functions`` () =
()
#endif

[<Fact>]
let ``test array literal passed to deref FFI binding round-trips correctly`` () =
#if FABLE_COMPILER
// The literal flows straight into maps:from_list without a process-dict ref.
let m = mapFromPairs [| ("a", 1); ("b", 2) |]
equal 1 (mapGet "a" m)
equal 2 (mapGet "b" m)
#else
()
#endif

[<Fact>]
let ``test Emit with case expression called twice does not leak variables`` () =
#if FABLE_COMPILER
Expand Down
Loading