[commit: ghc] wip/pretty: Resolve foldr-strictness stack overflow bug (cb828c0)

git at git.haskell.org git at git.haskell.org
Sat Aug 15 13:56:07 UTC 2015


Repository : ssh://git@git.haskell.org/ghc

On branch  : wip/pretty
Link       : http://ghc.haskell.org/trac/ghc/changeset/cb828c0d43148dfdfa407a82310279ce98601233/ghc

>---------------------------------------------------------------

commit cb828c0d43148dfdfa407a82310279ce98601233
Author: Eyal Lotem <eyal.lotem at gmail.com>
Date:   Fri Jun 28 23:03:21 2013 +0300

    Resolve foldr-strictness stack overflow bug
    
    This is a backport of 307b8173f41cd776eae8f547267df6d72bff2d68 from
    libraries/pretty.
    
    From https://github.com/haskell/pretty/pull/9:
    
        The foldr applications in vcat, hsep, and hcat use a function which
        is strict on its second argument.
    
        This is a recipe for stack overflows which indeed happen when trying
        to build very large Doc values.
    
        Added a test to reproduce the problem.
    
        The fix is moving the canonization of Empty values into reduceAB,
        and doing it in a maximally-lazy fashion.


>---------------------------------------------------------------

cb828c0d43148dfdfa407a82310279ce98601233
 compiler/utils/Pretty.hs | 43 +++++++++++++++++++++++++++++--------------
 1 file changed, 29 insertions(+), 14 deletions(-)

diff --git a/compiler/utils/Pretty.hs b/compiler/utils/Pretty.hs
index 4aae2c8..49be9ee 100644
--- a/compiler/utils/Pretty.hs
+++ b/compiler/utils/Pretty.hs
@@ -529,15 +529,15 @@ reduceDoc p              = p
 
 -- | List version of '<>'.
 hcat :: [Doc] -> Doc
-hcat = reduceAB . foldr (beside_' False) empty
+hcat = reduceAB . foldr (\p q -> Beside p False q) empty
 
 -- | List version of '<+>'.
 hsep :: [Doc] -> Doc
-hsep = reduceAB . foldr (beside_' True)  empty
+hsep = reduceAB . foldr (\p q -> Beside p True q)  empty
 
 -- | List version of '$$'.
 vcat :: [Doc] -> Doc
-vcat = reduceAB . foldr (above_' False) empty
+vcat = reduceAB . foldr (\p q -> Above p False q) empty
 
 -- | Nest (or indent) a document by a given number of positions
 -- (which may also be negative).  'nest' satisfies the laws:
@@ -584,18 +584,33 @@ mkUnion :: Doc -> Doc -> Doc
 mkUnion Empty _ = Empty
 mkUnion p q     = p `union_` q
 
-beside_' :: Bool -> Doc -> Doc -> Doc
-beside_' _ p Empty = p
-beside_' g p q     = Beside p g q
-
-above_' :: Bool -> Doc -> Doc -> Doc
-above_' _ p Empty = p
-above_' g p q     = Above p g q
-
 reduceAB :: Doc -> Doc
-reduceAB (Above  Empty _ q) = q
-reduceAB (Beside Empty _ q) = q
-reduceAB doc                = doc
+reduceAB = snd . reduceAB'
+
+data IsEmpty = IsEmpty | NotEmpty
+
+reduceAB' :: Doc -> (IsEmpty, Doc)
+reduceAB' (Above  p g q) = eliminateEmpty Above  (reduceAB p) g (reduceAB' q)
+reduceAB' (Beside p g q) = eliminateEmpty Beside (reduceAB p) g (reduceAB' q)
+reduceAB' doc            = (NotEmpty, doc)
+
+-- Left-arg-strict
+eliminateEmpty ::
+  (Doc -> Bool -> Doc -> Doc) ->
+  Doc -> Bool -> (IsEmpty, Doc) -> (IsEmpty, Doc)
+eliminateEmpty _    Empty _ q          = q
+eliminateEmpty cons p     g q          =
+  (NotEmpty,
+   -- We're not empty whether or not q is empty, so for laziness-sake,
+   -- after checking that p isn't empty, we put the NotEmpty result
+   -- outside independent of q. This allows reduceAB to immediately
+   -- return the appropriate constructor (Above or Beside) without
+   -- forcing the entire nested Doc. This allows the foldr in vcat,
+   -- hsep, and hcat to be lazy on its second argument, avoiding a
+   -- stack overflow.
+   case q of
+     (NotEmpty, q') -> cons p g q'
+     (IsEmpty, _) -> p)
 
 nilAbove_ :: RDoc -> RDoc
 nilAbove_ = NilAbove



More information about the ghc-commits mailing list