Skip to content

Commit 347fe33

Browse files
committed
fix(all): eliminate dead bindings left by CE/inline lowering
1 parent fedf367 commit 347fe33

5 files changed

Lines changed: 111 additions & 5 deletions

File tree

src/Fable.Transforms/Babel/BabelPrinter.fs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,25 @@ module PrinterExtensions =
351351
printer.Print(expr)
352352
printer.Print(")")
353353

354+
/// True when the expression's leftmost token would otherwise be parsed as the start of
355+
/// a block/function/class declaration, e.g. `{ A: 1 };` parses as an invalid block.
356+
member printer.NeedsParensAsExpressionStatement(expr: Expression) =
357+
match expr with
358+
| CommentedExpression(_, e) -> printer.NeedsParensAsExpressionStatement(e)
359+
| ObjectExpression _
360+
| ClassExpression _
361+
| FunctionExpression _ -> true
362+
| SequenceExpression(exprs, _) when exprs.Length > 0 -> printer.NeedsParensAsExpressionStatement(exprs[0])
363+
| BinaryExpression(left, _, _, _)
364+
| LogicalExpression(left, _, _, _)
365+
| AssignmentExpression(left, _, _, _) -> printer.NeedsParensAsExpressionStatement(left)
366+
| ConditionalExpression(test, _, _, _) -> printer.NeedsParensAsExpressionStatement(test)
367+
| MemberExpression(object, _, _, _) -> printer.NeedsParensAsExpressionStatement(object)
368+
| CallExpression(callee, _, _, _) -> printer.NeedsParensAsExpressionStatement(callee)
369+
| UpdateExpression(false, argument, _, _) -> printer.NeedsParensAsExpressionStatement(argument)
370+
| AsExpression(e, _) -> printer.NeedsParensAsExpressionStatement(e)
371+
| _ -> false
372+
354373
/// Should the expression be printed with parens when nested?
355374
member printer.IsComplex(expr: Expression) =
356375
match expr with
@@ -642,9 +661,15 @@ module PrinterExtensions =
642661
printer.PrintOptional(label, " ")
643662

644663
| ExpressionStatement(expr) ->
645-
match expr with
646-
| UnaryExpression(argument, "void", false, _loc) -> printer.Print(argument)
647-
| _ -> printer.Print(expr)
664+
let expr =
665+
match expr with
666+
| UnaryExpression(argument, "void", false, _loc) -> argument
667+
| expr -> expr
668+
669+
if printer.NeedsParensAsExpressionStatement(expr) then
670+
printer.WithParens(expr)
671+
else
672+
printer.Print(expr)
648673

649674
member printer.PrintJsDoc(doc: string option) =
650675
match doc with

src/Fable.Transforms/FableTransforms.fs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,18 @@ module private Transforms =
441441
(not com.Options.DebugMode) || ident.IsCompilerGenerated
442442

443443
match e with
444+
| Let(ident, value, letBody) when
445+
(not ident.IsMutable)
446+
&& isErasingCandidate ident
447+
&& countReferencesUntil 1 ident.Name letBody = 0
448+
&& canHaveSideEffects com value
449+
->
450+
// The binding is never read but its value may have side effects (e.g. residue from
451+
// inlining CE builder methods like `Combine`/`Run` that discard their argument).
452+
// Keep evaluating it for its effects, but drop the now-useless named binding.
453+
match letBody with
454+
| Sequential exprs -> Sequential(value :: exprs)
455+
| letBody -> Sequential [ value; letBody ]
444456
| Let(ident, value, letBody) when (not ident.IsMutable) && isErasingCandidate ident ->
445457
match tryInlineBinding com ident value letBody with
446458
| Some(ident, value) ->

src/Fable.Transforms/Rust/Fable2Rust.fs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2793,6 +2793,20 @@ module Util =
27932793
let expr = transformLeaveContext com ctx None e
27942794
mkExprStmt expr
27952795

2796+
// Only the block's true tail can omit the trailing `;`. A block-like expression (`if`,
2797+
// `match`, nested `{ }`) in a non-tail statement needs it forced via `Semi`, or a
2798+
// non-`()`-typed value there is a Rust type error, not just a style choice.
2799+
let transformExprsAsStmts (com: IRustCompiler) ctx (exprs: Fable.Expr list) : Rust.Stmt list =
2800+
match List.rev exprs with
2801+
| [] -> []
2802+
| last :: revRest ->
2803+
let nonTailStmts =
2804+
revRest
2805+
|> List.rev
2806+
|> List.map (fun e -> transformLeaveContext com ctx None e |> mkSemiStmt)
2807+
2808+
nonTailStmts @ [ transformAsStmt com ctx last ]
2809+
27962810
// flatten nested Let binding expressions
27972811
let rec flattenLet acc (expr: Fable.Expr) =
27982812
match expr with
@@ -2954,13 +2968,13 @@ module Util =
29542968
match body with
29552969
| Fable.Sequential exprs ->
29562970
let exprs = flattenSequential body
2957-
List.map (transformAsStmt com ctx) exprs
2971+
transformExprsAsStmts com ctx exprs
29582972
| _ -> [ transformAsStmt com ctx body ]
29592973

29602974
letStmts @ bodyStmts |> mkStmtBlockExpr
29612975

29622976
let transformSequential (com: IRustCompiler) ctx exprs =
2963-
exprs |> List.map (transformAsStmt com ctx) |> mkStmtBlockExpr
2977+
exprs |> transformExprsAsStmts com ctx |> mkStmtBlockExpr
29642978

29652979
let transformIfThenElse (com: IRustCompiler) ctx range guard thenBody elseBody =
29662980
// transform null checks for nullable value types

tests/Js/Main/InlineIfLambdaTests.fs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ type TwiceBuilder() =
4242

4343
let twice = TwiceBuilder()
4444

45+
// ── CE builder whose members discard their argument ──────────────────────────
46+
//
47+
// All members are `inline` and return `()` regardless of their argument, so the
48+
// inlined argument bindings are never read. `bindingBetaReduction` must turn
49+
// those into plain sequential evaluation instead of leaving a dead `let` behind.
50+
type DiscardBuilder() =
51+
member inline _.Yield(_: unit) = ()
52+
member inline _.Combine(_: unit, _: unit) = ()
53+
member inline _.Delay([<InlineIfLambda>] f: unit -> unit) = f ()
54+
member inline _.Zero() = ()
55+
member inline _.Run(_: unit) = ()
56+
57+
let discard = DiscardBuilder()
58+
4559
// ── tests ────────────────────────────────────────────────────────────────────
4660

4761
let tests =
@@ -115,4 +129,13 @@ let tests =
115129
callCount |> equal 2
116130
result |> equal 2
117131

132+
testCase "Inline CE builder whose members discard their argument still runs body once, in order" <| fun () ->
133+
let mutable log = []
134+
let addLog x = log <- x :: log
135+
discard {
136+
addLog "a"
137+
addLog "b"
138+
}
139+
log |> List.rev |> equal [ "a"; "b" ]
140+
118141
]

tests/Rust/tests/src/InlineIfLambdaTests.fs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@ type TwiceBuilder() =
3434

3535
let twice = TwiceBuilder()
3636

37+
type DiscardBuilder() =
38+
member inline _.Yield(_: unit) = ()
39+
member inline _.Combine(_: unit, _: unit) = ()
40+
member inline _.Delay([<InlineIfLambda>] f: unit -> unit) = f ()
41+
member inline _.Zero() = ()
42+
member inline _.Run(_: unit) = ()
43+
44+
let discard = DiscardBuilder()
45+
46+
// Inlining this at a discarded call site keeps `r` as a real `let` (referenced
47+
// twice), so the discarded value compiles to a Rust block ending in a non-`()`
48+
// tail expression - the shape that needs a forced `;` to stay valid Rust.
49+
let inline makeAndFill () =
50+
let r = ResizeArray()
51+
r.Add(1)
52+
r
53+
3754
// ── tests ──────────────────────────────────────────────────────────────────────
3855

3956
[<Fact>]
@@ -102,3 +119,18 @@ let ``InlineIfLambda on CE builder Delay inlines body at each call site`` () =
102119
}
103120
callCount |> equal 2
104121
result |> equal 2
122+
123+
[<Fact>]
124+
let ``Inline CE builder whose members discard their argument still runs body once, in order`` () =
125+
let mutable log = []
126+
let addLog x = log <- x :: log
127+
discard {
128+
addLog "a"
129+
addLog "b"
130+
}
131+
log |> List.rev |> equal [ "a"; "b" ]
132+
133+
[<Fact>]
134+
let ``Discarding a value that compiles to a Rust block does not corrupt codegen`` () =
135+
makeAndFill () |> ignore
136+
true |> equal true

0 commit comments

Comments
 (0)