Skip to content

Commit a7b9510

Browse files
authored
[Beam] Fix try/catch warnings and reraise unbound variable bug (#4392)
1 parent e19011f commit a7b9510

8 files changed

Lines changed: 186 additions & 76 deletions

File tree

src/Fable.Cli/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
* [Beam] Fix unused term warning in try/catch when exception variable is not referenced (by @dbrattli)
13+
* [Beam] Fix "no effect" warning for pure BIF calls (`self/0`, `node/0`) in non-final block positions (by @dbrattli)
14+
* [Beam] Fix `reraise()` generating unbound `MatchValue` variable — use raw Erlang reason variable for re-throw (by @dbrattli)
1215
* [Beam] Fix `Erlang.receive<'T>()` resolving to timeout overload due to F# unit argument (by @dbrattli)
1316

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

src/Fable.Compiler/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
* [Beam] Fix unused term warning in try/catch when exception variable is not referenced (by @dbrattli)
13+
* [Beam] Fix "no effect" warning for pure BIF calls (`self/0`, `node/0`) in non-final block positions (by @dbrattli)
14+
* [Beam] Fix `reraise()` generating unbound `MatchValue` variable — use raw Erlang reason variable for re-throw (by @dbrattli)
1215
* [Beam] Fix `Erlang.receive<'T>()` resolving to timeout overload due to F# unit argument (by @dbrattli)
1316

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

src/Fable.Transforms/Beam/ErlangPrinter.fs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,25 @@ module Fable.Transforms.ErlangPrinter
33
open Fable.AST.Beam
44
open Fable.Transforms
55

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

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

2227
module Output =
@@ -258,7 +263,7 @@ module Output =
258263
sb.Append(") ->") |> ignore
259264
sb.AppendLine() |> ignore
260265

261-
let body = stripStrayOk clause.Body
266+
let body = stripNoEffect clause.Body
262267

263268
body
264269
|> List.iteri (fun j bodyExpr ->
@@ -296,7 +301,7 @@ module Output =
296301
sb.Append(") ->") |> ignore
297302
sb.AppendLine() |> ignore
298303

299-
let body = stripStrayOk clause.Body
304+
let body = stripNoEffect clause.Body
300305

301306
body
302307
|> List.iteri (fun j bodyExpr ->
@@ -340,7 +345,7 @@ module Output =
340345
sb.Append(" ->") |> ignore
341346
sb.AppendLine() |> ignore
342347

343-
let caseBody = stripStrayOk clause.Body
348+
let caseBody = stripNoEffect clause.Body
344349

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

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

409-
let tryBody = stripStrayOk body
414+
let tryBody = stripNoEffect body
410415

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

428-
let catchBody' = stripStrayOk catchBody
433+
let catchBody' = stripNoEffect catchBody
429434

430435
catchBody'
431436
|> List.iteri (fun i bodyExpr ->
@@ -498,7 +503,7 @@ module Output =
498503
sb.Append(" ->") |> ignore
499504
sb.AppendLine() |> ignore
500505

501-
let caseBody = stripStrayOk clause.Body
506+
let caseBody = stripNoEffect clause.Body
502507

503508
caseBody
504509
|> List.iteri (fun j bodyExpr ->
@@ -525,7 +530,7 @@ module Output =
525530
sb.Append(" ->") |> ignore
526531
sb.AppendLine() |> ignore
527532

528-
let afterBody = stripStrayOk bodyExprs
533+
let afterBody = stripNoEffect bodyExprs
529534

530535
afterBody
531536
|> List.iteri (fun j bodyExpr ->
@@ -573,7 +578,7 @@ module Output =
573578
sb.Append(" ->") |> ignore
574579
sb.AppendLine() |> ignore
575580

576-
let topBody = stripStrayOk clause.Body
581+
let topBody = stripNoEffect clause.Body
577582

578583
topBody
579584
|> List.iteri (fun i bodyExpr ->

src/Fable.Transforms/Beam/Fable2Beam.Util.fs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ let rec containsIdentRef (name: string) (expr: Expr) : bool =
7474
|| match baseCall with
7575
| Some e -> containsIdentRef name e
7676
| None -> false
77+
| Extended(kind, _) ->
78+
match kind with
79+
| Throw(Some e, _) -> containsIdentRef name e
80+
| Throw(None, _)
81+
| Debugger
82+
| Curry _ -> false
7783
| _ -> false
7884

7985
/// Check if an identifier is captured inside a closure (Lambda/Delegate) within the expression.
@@ -128,6 +134,12 @@ let isCapturedInClosure (name: string) (expr: Expr) : bool =
128134
|| match baseCall with
129135
| Some e -> check inClosure e
130136
| None -> false
137+
| Extended(kind, _) ->
138+
match kind with
139+
| Throw(Some e, _) -> check inClosure e
140+
| Throw(None, _)
141+
| Debugger
142+
| Curry _ -> false
131143
| _ -> false
132144

133145
check false expr

src/Fable.Transforms/Beam/Fable2Beam.fs

Lines changed: 80 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type Context =
5252
CtorFieldExprs: Map<string, Beam.ErlExpr> // field name -> Erlang expr during constructor
5353
ClassFieldPrefix: bool // When true, NewRecord uses "field_" prefix for map keys (for explicit val field class ctors)
5454
CtorParamNames: Set<string> // Constructor parameter names (stored as class fields)
55+
CatchReasonVar: (string * string) option // (catch ident name, Erlang reason var name) for reraise support
5556
}
5657

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

762-
let ctx' = { ctx with LocalVars = ctx.LocalVars.Add(ident.Name) }
763+
let ctx' =
764+
{ ctx with
765+
LocalVars = ctx.LocalVars.Add(ident.Name)
766+
CatchReasonVar = Some(ident.Name, reasonVar)
767+
}
768+
763769
let erlCatchBody = transformExpr com ctx' catchExpr
764770

765771
let catchBodyExprs =
766772
match erlCatchBody with
767773
| Beam.ErlExpr.Block es -> es
768774
| e -> [ e ]
769775

770-
let reasonRef = Beam.ErlExpr.Variable reasonVar
776+
// Only generate the exception wrapping/binding when the catch body
777+
// actually references the exception identifier. This avoids unused
778+
// term warnings from the Erlang compiler.
779+
if containsIdentRef ident.Name catchExpr then
780+
let reasonRef = Beam.ErlExpr.Variable reasonVar
771781

772-
let formatExpr =
773-
Beam.ErlExpr.Call(
774-
None,
775-
"iolist_to_binary",
776-
[
777-
Beam.ErlExpr.Call(
778-
Some "io_lib",
779-
"format",
780-
[
781-
Beam.ErlExpr.Call(
782-
None,
783-
"binary_to_list",
784-
[ Beam.ErlExpr.Literal(Beam.ErlLiteral.StringLit "~p") ]
785-
)
786-
Beam.ErlExpr.List [ reasonRef ]
787-
]
788-
)
789-
]
790-
)
791-
792-
let messageExpr =
793-
Beam.ErlExpr.Case(
794-
reasonRef,
795-
[
796-
{
797-
Pattern = Beam.PWildcard
798-
Guard = [ Beam.ErlExpr.Call(None, "is_binary", [ reasonRef ]) ]
799-
Body = [ reasonRef ]
800-
}
801-
{
802-
Pattern = Beam.PWildcard
803-
Guard = []
804-
Body = [ formatExpr ]
805-
}
806-
]
807-
)
782+
let formatExpr =
783+
Beam.ErlExpr.Call(
784+
None,
785+
"iolist_to_binary",
786+
[
787+
Beam.ErlExpr.Call(
788+
Some "io_lib",
789+
"format",
790+
[
791+
Beam.ErlExpr.Call(
792+
None,
793+
"binary_to_list",
794+
[ Beam.ErlExpr.Literal(Beam.ErlLiteral.StringLit "~p") ]
795+
)
796+
Beam.ErlExpr.List [ reasonRef ]
797+
]
798+
)
799+
]
800+
)
808801

809-
// If reason is already a map (F# exception) or reference (class inheriting exn), use it directly.
810-
// Otherwise wrap in #{message => ...} for .Message access.
811-
let bindIdent =
812-
Beam.ErlExpr.Match(
813-
Beam.PVar identVar,
802+
let messageExpr =
814803
Beam.ErlExpr.Case(
815804
reasonRef,
816805
[
817806
{
818807
Pattern = Beam.PWildcard
819-
Guard = [ Beam.ErlExpr.Call(None, "is_map", [ reasonRef ]) ]
820-
Body = [ reasonRef ]
821-
}
822-
{
823-
Pattern = Beam.PWildcard
824-
Guard = [ Beam.ErlExpr.Call(None, "is_reference", [ reasonRef ]) ]
808+
Guard = [ Beam.ErlExpr.Call(None, "is_binary", [ reasonRef ]) ]
825809
Body = [ reasonRef ]
826810
}
827811
{
828812
Pattern = Beam.PWildcard
829813
Guard = []
830-
Body =
831-
[
832-
Beam.ErlExpr.Map
833-
[
834-
Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "message")),
835-
messageExpr
836-
]
837-
]
814+
Body = [ formatExpr ]
838815
}
839816
]
840817
)
841-
)
842818

843-
Beam.ErlExpr.TryCatch(bodyExprs, reasonVar, [ bindIdent ] @ catchBodyExprs, afterExprs)
819+
// If reason is already a map (F# exception) or reference (class inheriting exn), use it directly.
820+
// Otherwise wrap in #{message => ...} for .Message access.
821+
let bindIdent =
822+
Beam.ErlExpr.Match(
823+
Beam.PVar identVar,
824+
Beam.ErlExpr.Case(
825+
reasonRef,
826+
[
827+
{
828+
Pattern = Beam.PWildcard
829+
Guard = [ Beam.ErlExpr.Call(None, "is_map", [ reasonRef ]) ]
830+
Body = [ reasonRef ]
831+
}
832+
{
833+
Pattern = Beam.PWildcard
834+
Guard = [ Beam.ErlExpr.Call(None, "is_reference", [ reasonRef ]) ]
835+
Body = [ reasonRef ]
836+
}
837+
{
838+
Pattern = Beam.PWildcard
839+
Guard = []
840+
Body =
841+
[
842+
Beam.ErlExpr.Map
843+
[
844+
Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "message")),
845+
messageExpr
846+
]
847+
]
848+
}
849+
]
850+
)
851+
)
852+
853+
Beam.ErlExpr.TryCatch(bodyExprs, reasonVar, [ bindIdent ] @ catchBodyExprs, afterExprs)
854+
else
855+
Beam.ErlExpr.TryCatch(bodyExprs, reasonVar, catchBodyExprs, afterExprs)
844856
| None, [] ->
845857
// No catch handler and no finalizer
846858
erlBody
@@ -1156,8 +1168,14 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er
11561168
| Extended(kind, _range) ->
11571169
match kind with
11581170
| Throw(Some exprArg, _typ) ->
1159-
let erlExpr = transformExpr com ctx exprArg
1160-
Beam.ErlExpr.Call(Some "erlang", "error", [ erlExpr ])
1171+
// For reraise: if the thrown expression references the catch ident,
1172+
// use the raw Erlang reason variable to preserve the original exception.
1173+
match exprArg, ctx.CatchReasonVar with
1174+
| IdentExpr ident, Some(catchIdentName, reasonVar) when ident.Name = catchIdentName ->
1175+
Beam.ErlExpr.Call(Some "erlang", "error", [ Beam.ErlExpr.Variable reasonVar ])
1176+
| _ ->
1177+
let erlExpr = transformExpr com ctx exprArg
1178+
Beam.ErlExpr.Call(Some "erlang", "error", [ erlExpr ])
11611179
| Throw(None, _typ) ->
11621180
// Re-raise (should not normally happen outside catch context)
11631181
Beam.ErlExpr.Call(
@@ -3355,6 +3373,7 @@ let transformFile (com: Fable.Compiler) (file: File) : Beam.ErlModule =
33553373
CtorFieldExprs = Map.empty
33563374
ClassFieldPrefix = false
33573375
CtorParamNames = Set.empty
3376+
CatchReasonVar = None
33583377
}
33593378

33603379
let ctorNameRegistry = System.Collections.Generic.Dictionary<string, string>()

src/Fable.Transforms/Beam/Replacements.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,6 +1887,10 @@ let private seqModule
18871887
=
18881888
match info.CompiledName, args with
18891889
| "Cast", [ arg ] -> Some arg
1890+
| "ToList", [ arg ] ->
1891+
// Use fable_utils:to_list which handles binaries (strings), plain lists,
1892+
// ref-wrapped arrays, enumerator maps, and lazy seqs.
1893+
emitExpr r t [ arg ] "fable_utils:to_list($0)" |> Some
18901894
| "ToArray", [ arg ] ->
18911895
// seq:to_array already returns a ref-wrapped array, don't double-wrap
18921896
Helper.LibCall(com, "seq", "to_array", t, [ arg ], info.SignatureArgTypes, ?loc = r)

tests/Beam/SeqTests.fs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,3 +1034,9 @@ let ``test Seq.except works with various types`` () =
10341034
Seq.except [(1, 2)] [(1, 2)] |> Seq.isEmpty |> equal true
10351035
Seq.except [|49|] [|7; 49|] |> Seq.last |> equal 7
10361036
Seq.except [{ Bar= "test" }] [{ Bar = "test" }] |> Seq.isEmpty |> equal true
1037+
1038+
[<Fact>]
1039+
let ``test Seq.toList on string works`` () =
1040+
let text = "ABC"
1041+
let chars = text |> Seq.toList
1042+
chars |> List.length |> equal 3

0 commit comments

Comments
 (0)