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
6 changes: 6 additions & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Fixed

* [Beam] Fix unused term warning in try/catch when exception variable is not referenced (by @dbrattli)
* [Beam] Fix "no effect" warning for pure BIF calls (`self/0`, `node/0`) in non-final block positions (by @dbrattli)
* [Beam] Fix `reraise()` generating unbound `MatchValue` variable — use raw Erlang reason variable for re-throw (by @dbrattli)

## 5.0.0-rc.3 - 2026-03-10

### Added
Expand Down
6 changes: 6 additions & 0 deletions src/Fable.Compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Fixed

* [Beam] Fix unused term warning in try/catch when exception variable is not referenced (by @dbrattli)
* [Beam] Fix "no effect" warning for pure BIF calls (`self/0`, `node/0`) in non-final block positions (by @dbrattli)
* [Beam] Fix `reraise()` generating unbound `MatchValue` variable — use raw Erlang reason variable for re-throw (by @dbrattli)

## 5.0.0-rc.10 - 2026-03-10

### Added
Expand Down
35 changes: 20 additions & 15 deletions src/Fable.Transforms/Beam/ErlangPrinter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@ module Fable.Transforms.ErlangPrinter
open Fable.AST.Beam
open Fable.Transforms

/// Strip bare `ok` atoms from non-final positions in expression lists.
/// These are stray unit values (F# unit → Erlang `ok`) that have no side effects.
let private stripStrayOk (exprs: ErlExpr list) =
/// Strip expressions with no side effects from non-final positions in expression lists.
/// This removes stray unit values (F# unit → Erlang `ok`), standalone variables,
/// literals, and known pure BIF calls (e.g. `self()`, `node()`) that would otherwise
/// produce "has no effect" warnings from the Erlang compiler.
let private stripNoEffect (exprs: ErlExpr list) =
match exprs with
| []
| [ _ ] -> exprs
| _ ->
let isOkAtom =
let isNoEffect =
function
| Literal(AtomLit(Atom "ok")) -> true
| Literal _ -> true
| Emit("ok", []) -> true
| Variable _ -> true
| Call(None, ("self" | "node"), []) -> true
| Call(Some "erlang", ("self" | "node"), []) -> true
| _ -> false

let nonFinal = exprs.[.. exprs.Length - 2] |> List.filter (not << isOkAtom)
let nonFinal = exprs.[.. exprs.Length - 2] |> List.filter (not << isNoEffect)
nonFinal @ [ exprs.[exprs.Length - 1] ]

module Output =
Expand Down Expand Up @@ -258,7 +263,7 @@ module Output =
sb.Append(") ->") |> ignore
sb.AppendLine() |> ignore

let body = stripStrayOk clause.Body
let body = stripNoEffect clause.Body

body
|> List.iteri (fun j bodyExpr ->
Expand Down Expand Up @@ -296,7 +301,7 @@ module Output =
sb.Append(") ->") |> ignore
sb.AppendLine() |> ignore

let body = stripStrayOk clause.Body
let body = stripNoEffect clause.Body

body
|> List.iteri (fun j bodyExpr ->
Expand Down Expand Up @@ -340,7 +345,7 @@ module Output =
sb.Append(" ->") |> ignore
sb.AppendLine() |> ignore

let caseBody = stripStrayOk clause.Body
let caseBody = stripNoEffect clause.Body

caseBody
|> List.iteri (fun j bodyExpr ->
Expand Down Expand Up @@ -371,7 +376,7 @@ module Output =
// Wrap multi-expression blocks in begin...end to avoid
// comma-separated expressions being misinterpreted as
// separate function call arguments
let filtered = stripStrayOk exprs
let filtered = stripNoEffect exprs
let needsBeginEnd = filtered.Length > 1

if needsBeginEnd then
Expand Down Expand Up @@ -406,7 +411,7 @@ module Output =
| TryCatch(body, catchVar, catchBody, after) ->
sb.AppendLine("try") |> ignore

let tryBody = stripStrayOk body
let tryBody = stripNoEffect body

tryBody
|> List.iteri (fun i bodyExpr ->
Expand All @@ -425,7 +430,7 @@ module Output =
writeIndent ()
sb.AppendLine($" _:%s{catchVar} ->") |> ignore

let catchBody' = stripStrayOk catchBody
let catchBody' = stripNoEffect catchBody

catchBody'
|> List.iteri (fun i bodyExpr ->
Expand Down Expand Up @@ -498,7 +503,7 @@ module Output =
sb.Append(" ->") |> ignore
sb.AppendLine() |> ignore

let caseBody = stripStrayOk clause.Body
let caseBody = stripNoEffect clause.Body

caseBody
|> List.iteri (fun j bodyExpr ->
Expand All @@ -525,7 +530,7 @@ module Output =
sb.Append(" ->") |> ignore
sb.AppendLine() |> ignore

let afterBody = stripStrayOk bodyExprs
let afterBody = stripNoEffect bodyExprs

afterBody
|> List.iteri (fun j bodyExpr ->
Expand Down Expand Up @@ -573,7 +578,7 @@ module Output =
sb.Append(" ->") |> ignore
sb.AppendLine() |> ignore

let topBody = stripStrayOk clause.Body
let topBody = stripNoEffect clause.Body

topBody
|> List.iteri (fun i bodyExpr ->
Expand Down
12 changes: 12 additions & 0 deletions src/Fable.Transforms/Beam/Fable2Beam.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ let rec containsIdentRef (name: string) (expr: Expr) : bool =
|| match baseCall with
| Some e -> containsIdentRef name e
| None -> false
| Extended(kind, _) ->
match kind with
| Throw(Some e, _) -> containsIdentRef name e
| Throw(None, _)
| Debugger
| Curry _ -> false
| _ -> false

/// Check if an identifier is captured inside a closure (Lambda/Delegate) within the expression.
Expand Down Expand Up @@ -128,6 +134,12 @@ let isCapturedInClosure (name: string) (expr: Expr) : bool =
|| match baseCall with
| Some e -> check inClosure e
| None -> false
| Extended(kind, _) ->
match kind with
| Throw(Some e, _) -> check inClosure e
| Throw(None, _)
| Debugger
| Curry _ -> false
| _ -> false

check false expr
Expand Down
141 changes: 80 additions & 61 deletions src/Fable.Transforms/Beam/Fable2Beam.fs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type Context =
CtorFieldExprs: Map<string, Beam.ErlExpr> // field name -> Erlang expr during constructor
ClassFieldPrefix: bool // When true, NewRecord uses "field_" prefix for map keys (for explicit val field class ctors)
CtorParamNames: Set<string> // Constructor parameter names (stored as class fields)
CatchReasonVar: (string * string) option // (catch ident name, Erlang reason var name) for reraise support
}

/// Check if an entity ref refers to an interface type
Expand Down Expand Up @@ -759,88 +760,99 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er
let reasonVar = $"Exn_reason_%d{ctr}"
let identVar = capitalizeFirst ident.Name

let ctx' = { ctx with LocalVars = ctx.LocalVars.Add(ident.Name) }
let ctx' =
{ ctx with
LocalVars = ctx.LocalVars.Add(ident.Name)
CatchReasonVar = Some(ident.Name, reasonVar)
}

let erlCatchBody = transformExpr com ctx' catchExpr

let catchBodyExprs =
match erlCatchBody with
| Beam.ErlExpr.Block es -> es
| e -> [ e ]

let reasonRef = Beam.ErlExpr.Variable reasonVar
// Only generate the exception wrapping/binding when the catch body
// actually references the exception identifier. This avoids unused
// term warnings from the Erlang compiler.
if containsIdentRef ident.Name catchExpr then
let reasonRef = Beam.ErlExpr.Variable reasonVar

let formatExpr =
Beam.ErlExpr.Call(
None,
"iolist_to_binary",
[
Beam.ErlExpr.Call(
Some "io_lib",
"format",
[
Beam.ErlExpr.Call(
None,
"binary_to_list",
[ Beam.ErlExpr.Literal(Beam.ErlLiteral.StringLit "~p") ]
)
Beam.ErlExpr.List [ reasonRef ]
]
)
]
)

let messageExpr =
Beam.ErlExpr.Case(
reasonRef,
[
{
Pattern = Beam.PWildcard
Guard = [ Beam.ErlExpr.Call(None, "is_binary", [ reasonRef ]) ]
Body = [ reasonRef ]
}
{
Pattern = Beam.PWildcard
Guard = []
Body = [ formatExpr ]
}
]
)
let formatExpr =
Beam.ErlExpr.Call(
None,
"iolist_to_binary",
[
Beam.ErlExpr.Call(
Some "io_lib",
"format",
[
Beam.ErlExpr.Call(
None,
"binary_to_list",
[ Beam.ErlExpr.Literal(Beam.ErlLiteral.StringLit "~p") ]
)
Beam.ErlExpr.List [ reasonRef ]
]
)
]
)

// If reason is already a map (F# exception) or reference (class inheriting exn), use it directly.
// Otherwise wrap in #{message => ...} for .Message access.
let bindIdent =
Beam.ErlExpr.Match(
Beam.PVar identVar,
let messageExpr =
Beam.ErlExpr.Case(
reasonRef,
[
{
Pattern = Beam.PWildcard
Guard = [ Beam.ErlExpr.Call(None, "is_map", [ reasonRef ]) ]
Body = [ reasonRef ]
}
{
Pattern = Beam.PWildcard
Guard = [ Beam.ErlExpr.Call(None, "is_reference", [ reasonRef ]) ]
Guard = [ Beam.ErlExpr.Call(None, "is_binary", [ reasonRef ]) ]
Body = [ reasonRef ]
}
{
Pattern = Beam.PWildcard
Guard = []
Body =
[
Beam.ErlExpr.Map
[
Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "message")),
messageExpr
]
]
Body = [ formatExpr ]
}
]
)
)

Beam.ErlExpr.TryCatch(bodyExprs, reasonVar, [ bindIdent ] @ catchBodyExprs, afterExprs)
// If reason is already a map (F# exception) or reference (class inheriting exn), use it directly.
// Otherwise wrap in #{message => ...} for .Message access.
let bindIdent =
Beam.ErlExpr.Match(
Beam.PVar identVar,
Beam.ErlExpr.Case(
reasonRef,
[
{
Pattern = Beam.PWildcard
Guard = [ Beam.ErlExpr.Call(None, "is_map", [ reasonRef ]) ]
Body = [ reasonRef ]
}
{
Pattern = Beam.PWildcard
Guard = [ Beam.ErlExpr.Call(None, "is_reference", [ reasonRef ]) ]
Body = [ reasonRef ]
}
{
Pattern = Beam.PWildcard
Guard = []
Body =
[
Beam.ErlExpr.Map
[
Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "message")),
messageExpr
]
]
}
]
)
)

Beam.ErlExpr.TryCatch(bodyExprs, reasonVar, [ bindIdent ] @ catchBodyExprs, afterExprs)
else
Beam.ErlExpr.TryCatch(bodyExprs, reasonVar, catchBodyExprs, afterExprs)
| None, [] ->
// No catch handler and no finalizer
erlBody
Expand Down Expand Up @@ -1156,8 +1168,14 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er
| Extended(kind, _range) ->
match kind with
| Throw(Some exprArg, _typ) ->
let erlExpr = transformExpr com ctx exprArg
Beam.ErlExpr.Call(Some "erlang", "error", [ erlExpr ])
// For reraise: if the thrown expression references the catch ident,
// use the raw Erlang reason variable to preserve the original exception.
match exprArg, ctx.CatchReasonVar with
| IdentExpr ident, Some(catchIdentName, reasonVar) when ident.Name = catchIdentName ->
Beam.ErlExpr.Call(Some "erlang", "error", [ Beam.ErlExpr.Variable reasonVar ])
| _ ->
let erlExpr = transformExpr com ctx exprArg
Beam.ErlExpr.Call(Some "erlang", "error", [ erlExpr ])
| Throw(None, _typ) ->
// Re-raise (should not normally happen outside catch context)
Beam.ErlExpr.Call(
Expand Down Expand Up @@ -3355,6 +3373,7 @@ let transformFile (com: Fable.Compiler) (file: File) : Beam.ErlModule =
CtorFieldExprs = Map.empty
ClassFieldPrefix = false
CtorParamNames = Set.empty
CatchReasonVar = None
}

let ctorNameRegistry = System.Collections.Generic.Dictionary<string, string>()
Expand Down
4 changes: 4 additions & 0 deletions src/Fable.Transforms/Beam/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1887,6 +1887,10 @@ let private seqModule
=
match info.CompiledName, args with
| "Cast", [ arg ] -> Some arg
| "ToList", [ arg ] ->
// Use fable_utils:to_list which handles binaries (strings), plain lists,
// ref-wrapped arrays, enumerator maps, and lazy seqs.
emitExpr r t [ arg ] "fable_utils:to_list($0)" |> Some
| "ToArray", [ arg ] ->
// seq:to_array already returns a ref-wrapped array, don't double-wrap
Helper.LibCall(com, "seq", "to_array", t, [ arg ], info.SignatureArgTypes, ?loc = r)
Expand Down
6 changes: 6 additions & 0 deletions tests/Beam/SeqTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1034,3 +1034,9 @@ let ``test Seq.except works with various types`` () =
Seq.except [(1, 2)] [(1, 2)] |> Seq.isEmpty |> equal true
Seq.except [|49|] [|7; 49|] |> Seq.last |> equal 7
Seq.except [{ Bar= "test" }] [{ Bar = "test" }] |> Seq.isEmpty |> equal true

[<Fact>]
let ``test Seq.toList on string works`` () =
let text = "ABC"
let chars = text |> Seq.toList
chars |> List.length |> equal 3
Loading
Loading