Support TCO for functions with tail-recursive inner functions (#3958) · purescript/purescript@efbcc47 · GitHub
Skip to content

Commit efbcc47

Browse files
authored
Support TCO for functions with tail-recursive inner functions (#3958)
This commit adds support for optimizing functions that contain local functions which call the outer function in tail position, as long as those functions themselves are only called from tail position, either in the outer function or in other such local functions. This enables hand-written mutually-tail-recursive function groups to be optimized, but more critically, it also means that case guards which desugar to use local functions don't break TCO.
1 parent b7a8616 commit efbcc47

4 files changed

Lines changed: 280 additions & 72 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions

‎src/Language/PureScript/CoreImp/Optimizer/TCO.hs‎

Lines changed: 135 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,87 +3,166 @@ module Language.PureScript.CoreImp.Optimizer.TCO (tco) where
33

44
import Prelude.Compat
55

6-
import Data.Text (Text)
6+
import Control.Applicative (empty, liftA2)
7+
import Control.Monad (guard)
8+
import Control.Monad.State (State, evalState, get, modify)
9+
import Data.Foldable (foldr)
10+
import Data.Functor (($>), (<&>))
11+
import qualified Data.Set as S
12+
import Data.Text (Text, pack)
713
import qualified Language.PureScript.Constants.Prim as C
814
import Language.PureScript.CoreImp.AST
915
import Language.PureScript.AST.SourcePos (SourceSpan)
1016
import Safe (headDef, tailSafe)
1117

1218
-- | Eliminate tail calls
1319
tco :: AST -> AST
14-
tco = everywhere convert where
20+
tco = flip evalState 0 . everywhereTopDownM convert where
1521
tcoVar :: Text -> Text
1622
tcoVar arg = "$tco_var_" <> arg
1723

1824
copyVar :: Text -> Text
1925
copyVar arg = "$copy_" <> arg
2026

21-
tcoDone :: Text
22-
tcoDone = "$tco_done"
27+
tcoDoneM :: State Int Text
28+
tcoDoneM = get <&> \count -> "$tco_done" <>
29+
if count == 0 then "" else pack . show $ count
2330

2431
tcoLoop :: Text
2532
tcoLoop = "$tco_loop"
2633

2734
tcoResult :: Text
2835
tcoResult = "$tco_result"
2936

30-
convert :: AST -> AST
37+
convert :: AST -> State Int AST
3138
convert (VariableIntroduction ss name (Just fn@Function {}))
32-
| isTailRecursive name body'
33-
= VariableIntroduction ss name (Just (replace (toLoop name outerArgs innerArgs body')))
39+
| Just trFns <- findTailRecursiveFns name arity body'
40+
= VariableIntroduction ss name . Just . replace <$> toLoop trFns name arity outerArgs innerArgs body'
3441
where
3542
innerArgs = headDef [] argss
3643
outerArgs = concat . reverse $ tailSafe argss
37-
(argss, body', replace) = collectAllFunctionArgs [] id fn
38-
convert js = js
39-
40-
collectAllFunctionArgs :: [[Text]] -> (AST -> AST) -> AST -> ([[Text]], AST, AST -> AST)
41-
collectAllFunctionArgs allArgs f (Function s1 ident args (Block s2 (body@(Return _ _):_))) =
42-
collectAllFunctionArgs (args : allArgs) (\b -> f (Function s1 ident (map copyVar args) (Block s2 [b]))) body
43-
collectAllFunctionArgs allArgs f (Function ss ident args body@(Block _ _)) =
44-
(args : allArgs, body, f . Function ss ident (map copyVar args))
45-
collectAllFunctionArgs allArgs f (Return s1 (Function s2 ident args (Block s3 [body]))) =
46-
collectAllFunctionArgs (args : allArgs) (\b -> f (Return s1 (Function s2 ident (map copyVar args) (Block s3 [b])))) body
47-
collectAllFunctionArgs allArgs f (Return s1 (Function s2 ident args body@(Block _ _))) =
48-
(args : allArgs, body, f . Return s1 . Function s2 ident (map copyVar args))
49-
collectAllFunctionArgs allArgs f body = (allArgs, body, f)
50-
51-
isTailRecursive :: Text -> AST -> Bool
52-
isTailRecursive ident js = countSelfReferences js > 0 && allInTailPosition js where
53-
countSelfReferences = everything (+) match where
54-
match :: AST -> Int
55-
match (Var _ ident') | ident == ident' = 1
56-
match _ = 0
44+
arity = length argss
45+
-- ^ this is the number of calls, not the number of arguments, if there's
46+
-- ever a practical difference.
47+
(argss, body', replace) = topCollectAllFunctionArgs [] id fn
48+
convert js = pure js
49+
50+
rewriteFunctionsWith :: ([Text] -> [Text]) -> [[Text]] -> (AST -> AST) -> AST -> ([[Text]], AST, AST -> AST)
51+
rewriteFunctionsWith argMapper = collectAllFunctionArgs
52+
where
53+
collectAllFunctionArgs allArgs f (Function s1 ident args (Block s2 (body@(Return _ _):_))) =
54+
collectAllFunctionArgs (args : allArgs) (\b -> f (Function s1 ident (argMapper args) (Block s2 [b]))) body
55+
collectAllFunctionArgs allArgs f (Function ss ident args body@(Block _ _)) =
56+
(args : allArgs, body, f . Function ss ident (argMapper args))
57+
collectAllFunctionArgs allArgs f (Return s1 (Function s2 ident args (Block s3 [body]))) =
58+
collectAllFunctionArgs (args : allArgs) (\b -> f (Return s1 (Function s2 ident (argMapper args) (Block s3 [b])))) body
59+
collectAllFunctionArgs allArgs f (Return s1 (Function s2 ident args body@(Block _ _))) =
60+
(args : allArgs, body, f . Return s1 . Function s2 ident (argMapper args))
61+
collectAllFunctionArgs allArgs f body = (allArgs, body, f)
62+
63+
topCollectAllFunctionArgs :: [[Text]] -> (AST -> AST) -> AST -> ([[Text]], AST, AST -> AST)
64+
topCollectAllFunctionArgs = rewriteFunctionsWith (map copyVar)
65+
66+
innerCollectAllFunctionArgs :: [[Text]] -> (AST -> AST) -> AST -> ([[Text]], AST, AST -> AST)
67+
innerCollectAllFunctionArgs = rewriteFunctionsWith id
68+
69+
countReferences :: Text -> AST -> Int
70+
countReferences ident = everything (+) match where
71+
match :: AST -> Int
72+
match (Var _ ident') | ident == ident' = 1
73+
match _ = 0
74+
75+
-- If `ident` is a tail-recursive function, returns a set of identifiers
76+
-- that are locally bound to functions participating in the tail recursion.
77+
-- Otherwise, returns Nothing.
78+
findTailRecursiveFns :: Text -> Int -> AST -> Maybe (S.Set Text)
79+
findTailRecursiveFns ident arity js = guard (countReferences ident js > 0) *> go (S.empty, S.singleton (ident, arity))
80+
where
81+
82+
go :: (S.Set Text, S.Set (Text, Int)) -> Maybe (S.Set Text)
83+
go (known, required) =
84+
case S.minView required of
85+
Just (r, required') -> do
86+
required'' <- findTailPositionDeps r js
87+
go (S.insert (fst r) known, required' <> (S.filter (not . (`S.member` known) . fst) required''))
88+
Nothing ->
89+
pure known
90+
91+
-- Returns set of identifiers (with their arities) that need to be used
92+
-- exclusively in tail calls using their full arity in order for this
93+
-- identifier to be considered in tail position (or Nothing if this
94+
-- identifier is used somewhere not as a tail call with full arity).
95+
findTailPositionDeps :: (Text, Int) -> AST -> Maybe (S.Set (Text, Int))
96+
findTailPositionDeps (ident, arity) js = allInTailPosition js where
97+
countSelfReferences = countReferences ident
5798

5899
allInTailPosition (Return _ expr)
59-
| isSelfCall ident expr = countSelfReferences expr == 1
60-
| otherwise = countSelfReferences expr == 0
100+
| isSelfCall ident arity expr = guard (countSelfReferences expr == 1) $> S.empty
101+
| otherwise = guard (countSelfReferences expr == 0) $> S.empty
61102
allInTailPosition (While _ js1 body)
62-
= countSelfReferences js1 == 0 && allInTailPosition body
103+
= guard (countSelfReferences js1 == 0) *> allInTailPosition body
63104
allInTailPosition (For _ _ js1 js2 body)
64-
= countSelfReferences js1 == 0 && countSelfReferences js2 == 0 && allInTailPosition body
105+
= guard (countSelfReferences js1 == 0 && countSelfReferences js2 == 0) *> allInTailPosition body
65106
allInTailPosition (ForIn _ _ js1 body)
66-
= countSelfReferences js1 == 0 && allInTailPosition body
107+
= guard (countSelfReferences js1 == 0) *> allInTailPosition body
67108
allInTailPosition (IfElse _ js1 body el)
68-
= countSelfReferences js1 == 0 && allInTailPosition body && all allInTailPosition el
109+
= guard (countSelfReferences js1 == 0) *> liftA2 mappend (allInTailPosition body) (foldMapA allInTailPosition el)
69110
allInTailPosition (Block _ body)
70-
= all allInTailPosition body
111+
= foldMapA allInTailPosition body
71112
allInTailPosition (Throw _ js1)
72-
= countSelfReferences js1 == 0
113+
= guard (countSelfReferences js1 == 0) $> S.empty
73114
allInTailPosition (ReturnNoResult _)
74-
= True
75-
allInTailPosition (VariableIntroduction _ _ js1)
76-
= all ((== 0) . countSelfReferences) js1
115+
= pure S.empty
116+
allInTailPosition (VariableIntroduction _ _ Nothing)
117+
= pure S.empty
118+
allInTailPosition (VariableIntroduction _ ident' (Just js1))
119+
| countSelfReferences js1 == 0 = pure S.empty
120+
| Function _ Nothing _ _ <- js1
121+
, (argss, body, _) <- innerCollectAllFunctionArgs [] id js1
122+
= S.insert (ident', length argss) <$> allInTailPosition body
123+
| otherwise = empty
77124
allInTailPosition (Assignment _ _ js1)
78-
= countSelfReferences js1 == 0
125+
= guard (countSelfReferences js1 == 0) $> S.empty
79126
allInTailPosition (Comment _ _ js1)
80127
= allInTailPosition js1
81128
allInTailPosition _
82-
= False
83-
84-
toLoop :: Text -> [Text] -> [Text] -> AST -> AST
85-
toLoop ident outerArgs innerArgs js =
86-
Block rootSS $
129+
= empty
130+
131+
toLoop :: S.Set Text -> Text -> Int -> [Text] -> [Text] -> AST -> State Int AST
132+
toLoop trFns ident arity outerArgs innerArgs js = do
133+
tcoDone <- tcoDoneM
134+
modify (+ 1)
135+
136+
let
137+
markDone :: Maybe SourceSpan -> AST
138+
markDone ss = Assignment ss (Var ss tcoDone) (BooleanLiteral ss True)
139+
140+
loopify :: AST -> AST
141+
loopify (Return ss ret)
142+
| isSelfCall ident arity ret =
143+
let
144+
allArgumentValues = concat $ collectArgs [] ret
145+
in
146+
Block ss $
147+
zipWith (\val arg ->
148+
Assignment ss (Var ss (tcoVar arg)) val) allArgumentValues outerArgs
149+
++ zipWith (\val arg ->
150+
Assignment ss (Var ss (copyVar arg)) val) (drop (length outerArgs) allArgumentValues) innerArgs
151+
++ [ ReturnNoResult ss ]
152+
| isIndirectSelfCall ret = Return ss ret
153+
| otherwise = Block ss [ markDone ss, Return ss ret ]
154+
loopify (ReturnNoResult ss) = Block ss [ markDone ss, ReturnNoResult ss ]
155+
loopify (While ss cond body) = While ss cond (loopify body)
156+
loopify (For ss i js1 js2 body) = For ss i js1 js2 (loopify body)
157+
loopify (ForIn ss i js1 body) = ForIn ss i js1 (loopify body)
158+
loopify (IfElse ss cond body el) = IfElse ss cond (loopify body) (fmap loopify el)
159+
loopify (Block ss body) = Block ss (map loopify body)
160+
loopify (VariableIntroduction ss f (Just fn@(Function _ Nothing _ _)))
161+
| (_, body, replace) <- innerCollectAllFunctionArgs [] id fn
162+
, f `S.member` trFns = VariableIntroduction ss f (Just (replace (loopify body)))
163+
loopify other = other
164+
165+
pure $ Block rootSS $
87166
map (\arg -> VariableIntroduction rootSS (tcoVar arg) (Just (Var rootSS (copyVar arg)))) outerArgs ++
88167
[ VariableIntroduction rootSS tcoDone (Just (BooleanLiteral rootSS False))
89168
, VariableIntroduction rootSS tcoResult Nothing
@@ -96,38 +175,22 @@ tco = everywhere convert where
96175
where
97176
rootSS = Nothing
98177

99-
loopify :: AST -> AST
100-
loopify (Return ss ret)
101-
| isSelfCall ident ret =
102-
let
103-
allArgumentValues = concat $ collectArgs [] ret
104-
in
105-
Block ss $
106-
zipWith (\val arg ->
107-
Assignment ss (Var ss (tcoVar arg)) val) allArgumentValues outerArgs
108-
++ zipWith (\val arg ->
109-
Assignment ss (Var ss (copyVar arg)) val) (drop (length outerArgs) allArgumentValues) innerArgs
110-
++ [ ReturnNoResult ss ]
111-
| otherwise = Block ss [ markDone ss, Return ss ret ]
112-
loopify (ReturnNoResult ss) = Block ss [ markDone ss, ReturnNoResult ss ]
113-
loopify (While ss cond body) = While ss cond (loopify body)
114-
loopify (For ss i js1 js2 body) = For ss i js1 js2 (loopify body)
115-
loopify (ForIn ss i js1 body) = ForIn ss i js1 (loopify body)
116-
loopify (IfElse ss cond body el) = IfElse ss cond (loopify body) (fmap loopify el)
117-
loopify (Block ss body) = Block ss (map loopify body)
118-
loopify other = other
119-
120-
markDone :: Maybe SourceSpan -> AST
121-
markDone ss = Assignment ss (Var ss tcoDone) (BooleanLiteral ss True)
122-
123178
collectArgs :: [[AST]] -> AST -> [[AST]]
124179
collectArgs acc (App _ fn []) =
125180
-- count 0-argument applications as single-argument so we get the correct number of args
126181
collectArgs ([Var Nothing C.undefined] : acc) fn
127182
collectArgs acc (App _ fn args') = collectArgs (args' : acc) fn
128183
collectArgs acc _ = acc
129184

130-
isSelfCall :: Text -> AST -> Bool
131-
isSelfCall ident (App _ (Var _ ident') _) = ident == ident'
132-
isSelfCall ident (App _ fn _) = isSelfCall ident fn
133-
isSelfCall _ _ = False
185+
isIndirectSelfCall :: AST -> Bool
186+
isIndirectSelfCall (App _ (Var _ ident') _) = ident' `S.member` trFns
187+
isIndirectSelfCall (App _ fn _) = isIndirectSelfCall fn
188+
isIndirectSelfCall _ = False
189+
190+
isSelfCall :: Text -> Int -> AST -> Bool
191+
isSelfCall ident 1 (App _ (Var _ ident') _) = ident == ident'
192+
isSelfCall ident arity (App _ fn _) = isSelfCall ident (arity - 1) fn
193+
isSelfCall _ _ _ = False
194+
195+
foldMapA :: (Applicative f, Monoid w, Foldable t) => (a -> f w) -> t a -> f w
196+
foldMapA f = foldr (liftA2 mappend . f) (pure mempty)

‎tests/purs/passing/3957.purs‎

Lines changed: 39 additions & 0 deletions

0 commit comments

Comments
 (0)