Skip to content

Commit 3350127

Browse files
authored
Fix [<tailcall>] false positive with yield! (#16933)
* add test case showing a false positive with yield! * add missing case in IsAppInLambdaBody that can happen with yield! * add release notes entry * update PR number * only bind to what we are interested in * add test expecting a warning for yield! in a list comprehension * add test case using yield! in a custom CE that overflows the stack
1 parent 306bebc commit 3350127

File tree

3 files changed

+169
-0
lines changed

3 files changed

+169
-0
lines changed

docs/release-notes/.FSharp.Compiler.Service/8.0.300.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
### Fixed
22

3+
* Fix a false positive of the `[<TailCall>]` analysis in combination with `yield!`. ([PR #16933](https://github.com/dotnet/fsharp/pull/16933))
34
* Don't blow the stack when traversing deeply nested sequential expressions. ([PR #16882](https://github.com/dotnet/fsharp/pull/16882))
45
* Fix wrong range start of INTERP_STRING_END. ([PR #16774](https://github.com/dotnet/fsharp/pull/16774), [PR #16785](https://github.com/dotnet/fsharp/pull/16785))
56
* Fix missing warning for recursive calls in list comprehensions. ([PR #16652](https://github.com/dotnet/fsharp/pull/16652))

src/Compiler/Checking/TailCallChecks.fs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,13 @@ and CheckCall cenv args ctxts (tailCall: TailCall) =
222222
| Expr.App _ -> Some(TailCall.YesFromExpr cenv.g e)
223223
| IsAppInLambdaBody t -> Some t
224224
| _ -> None
225+
| Expr.App(args = args) ->
226+
args
227+
|> List.tryPick (fun a ->
228+
match a with
229+
| IsAppInLambdaBody t -> Some t
230+
| _ -> None)
231+
225232
| _ -> None
226233

227234
// if we haven't already decided this is no tail call, try to detect CPS-like expressions

tests/FSharp.Compiler.ComponentTests/ErrorMessages/TailCallAttribute.fs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,3 +1522,164 @@ namespace N
15221522
Message =
15231523
"The member or function 'reverse' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way." }
15241524
]
1525+
1526+
[<FSharp.Test.FactForNETCOREAPP>]
1527+
let ``Don't warn for yield! call of rec func in seq`` () =
1528+
"""
1529+
namespace N
1530+
1531+
module M =
1532+
1533+
type SynExpr =
1534+
| Sequential of expr1 : SynExpr * expr2 : SynExpr
1535+
| NotSequential
1536+
member _.Range = 99
1537+
1538+
type SyntaxNode = SynExpr of SynExpr
1539+
1540+
type SyntaxVisitor () = member _.VisitExpr _ = None
1541+
1542+
let visitor = SyntaxVisitor ()
1543+
let dive expr range f = range, fun () -> Some expr
1544+
let traverseSynExpr _ expr = Some expr
1545+
1546+
[<TailCall>]
1547+
let rec traverseSequentials path expr =
1548+
seq {
1549+
match expr with
1550+
| SynExpr.Sequential(expr1 = expr1; expr2 = SynExpr.Sequential _ as expr2) ->
1551+
yield dive expr expr.Range (fun expr -> visitor.VisitExpr(path, traverseSynExpr path, (fun _ -> None), expr))
1552+
let path = SyntaxNode.SynExpr expr :: path
1553+
yield dive expr1 expr1.Range (traverseSynExpr path)
1554+
yield! traverseSequentials path expr2 // should not warn
1555+
1556+
| _ ->
1557+
yield dive expr expr.Range (traverseSynExpr path)
1558+
}
1559+
"""
1560+
|> FSharp
1561+
|> withLangVersion80
1562+
|> compile
1563+
|> shouldSucceed
1564+
1565+
[<FSharp.Test.FactForNETCOREAPP>]
1566+
let ``Warn for yield! call of rec func in list comprehension`` () =
1567+
"""
1568+
namespace N
1569+
1570+
module M =
1571+
1572+
type SynExpr =
1573+
| Sequential of expr1 : SynExpr * expr2 : SynExpr
1574+
| NotSequential
1575+
member _.Range = 99
1576+
1577+
type SyntaxNode = SynExpr of SynExpr
1578+
1579+
type SyntaxVisitor () = member _.VisitExpr _ = None
1580+
1581+
let visitor = SyntaxVisitor ()
1582+
let dive expr range f = range, fun () -> Some expr
1583+
let traverseSynExpr _ expr = Some expr
1584+
1585+
[<TailCall>]
1586+
let rec traverseSequentials path expr =
1587+
[
1588+
match expr with
1589+
| SynExpr.Sequential(expr1 = expr1; expr2 = SynExpr.Sequential _ as expr2) ->
1590+
// It's a nested sequential expression.
1591+
// Visit it, but make defaultTraverse do nothing,
1592+
// since we're going to traverse its descendants ourselves.
1593+
yield dive expr expr.Range (fun expr -> visitor.VisitExpr(path, traverseSynExpr path, (fun _ -> None), expr))
1594+
1595+
// Now traverse its descendants.
1596+
let path = SyntaxNode.SynExpr expr :: path
1597+
yield dive expr1 expr1.Range (traverseSynExpr path)
1598+
yield! traverseSequentials path expr2 // should warn
1599+
1600+
| _ ->
1601+
// It's not a nested sequential expression.
1602+
// Traverse it normally.
1603+
yield dive expr expr.Range (traverseSynExpr path)
1604+
]
1605+
"""
1606+
|> FSharp
1607+
|> withLangVersion80
1608+
|> compile
1609+
|> shouldFail
1610+
|> withResults [
1611+
{ Error = Warning 3569
1612+
Range = { StartLine = 32
1613+
StartColumn = 24
1614+
EndLine = 32
1615+
EndColumn = 54 }
1616+
Message =
1617+
"The member or function 'traverseSequentials' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way." }
1618+
]
1619+
1620+
[<FSharp.Test.FactForNETCOREAPP>]
1621+
let ``Warn for yield! call of rec func in custom CE`` () =
1622+
"""
1623+
namespace N
1624+
1625+
module M =
1626+
1627+
type SynExpr =
1628+
| Sequential of expr1 : SynExpr * expr2 : SynExpr
1629+
| NotSequential
1630+
member _.Range = 99
1631+
1632+
type SyntaxNode = SynExpr of SynExpr
1633+
1634+
type SyntaxVisitor () = member _.VisitExpr _ = None
1635+
1636+
let visitor = SyntaxVisitor ()
1637+
let dive expr range f = range, fun () -> Some expr
1638+
let traverseSynExpr _ expr = Some expr
1639+
1640+
type ThingsBuilder() =
1641+
1642+
member _.Yield(x) = [ x ]
1643+
1644+
member _.Combine(currentThings, newThings) = currentThings @ newThings
1645+
1646+
member _.Delay(f) = f ()
1647+
1648+
member _.YieldFrom(x) = x
1649+
1650+
let things = ThingsBuilder()
1651+
1652+
[<TailCall>]
1653+
let rec traverseSequentials path expr =
1654+
things {
1655+
match expr with
1656+
| SynExpr.Sequential(expr1 = expr1; expr2 = SynExpr.Sequential _ as expr2) ->
1657+
// It's a nested sequential expression.
1658+
// Visit it, but make defaultTraverse do nothing,
1659+
// since we're going to traverse its descendants ourselves.
1660+
yield dive expr expr.Range (fun expr -> visitor.VisitExpr(path, traverseSynExpr path, (fun _ -> None), expr))
1661+
1662+
// Now traverse its descendants.
1663+
let path = SyntaxNode.SynExpr expr :: path
1664+
yield dive expr1 expr1.Range (traverseSynExpr path)
1665+
yield! traverseSequentials path expr2 // should warn
1666+
1667+
| _ ->
1668+
// It's not a nested sequential expression.
1669+
// Traverse it normally.
1670+
yield dive expr expr.Range (traverseSynExpr path)
1671+
}
1672+
"""
1673+
|> FSharp
1674+
|> withLangVersion80
1675+
|> compile
1676+
|> shouldFail
1677+
|> withResults [
1678+
{ Error = Warning 3569
1679+
Range = { StartLine = 43
1680+
StartColumn = 17
1681+
EndLine = 43
1682+
EndColumn = 68 }
1683+
Message =
1684+
"The member or function 'traverseSequentials' has the 'TailCallAttribute' attribute, but is not being used in a tail recursive way." }
1685+
]

0 commit comments

Comments
 (0)