packages feed

dynamic-pp 0.1.0 → 0.2.0

raw patch · 2 files changed

+280/−222 lines, 2 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Text.Format: instance Eq Offsets
- Text.Format: instance Hashable Offsets
+ Text.Format: (<!>) :: Doc -> Doc -> Doc
+ Text.Format: hardline :: Doc
+ Text.Format: instance Enum LineKind
+ Text.Format: instance Eq Doc
+ Text.Format: instance Eq Graphics
+ Text.Format: instance Eq LineKind
+ Text.Format: instance Hashable Doc
+ Text.Format: instance Hashable Graphics
+ Text.Format: instance Hashable LineKind
+ Text.Format: instance Ord Doc
+ Text.Format: instance Ord Graphics
+ Text.Format: instance Ord LineKind
- Text.Format: flatten :: Doc -> Doc
+ Text.Format: flatten :: Doc -> Maybe Doc

Files

dynamic-pp.cabal view
@@ -1,6 +1,6 @@ Name:                   dynamic-pp Category:               Text-Version:                0.1.0+Version:                0.2.0 License:                BSD3 License-File:           LICENSE Author:                 Eric McCorkle@@ -22,6 +22,8 @@   .   This library also provides two simpler rendering engines for uses where the full optimal layout engine is not   necessary.  These engines are much simpler and consume fewer resources.+  .+  NOTE: This library is still in development.  It may contain bugs, performance issues, and the interface may change. Build-type:             Simple Cabal-version:          >= 1.16 
src/Text/Format.hs view
@@ -53,6 +53,7 @@        empty,        line,        linebreak,+       hardline,        softline,        softbreak, @@ -139,6 +140,7 @@        -- *** Derived        (<>),        (<+>),+       (<!>),        (<$>),        (<$$>),        (</>),@@ -173,10 +175,12 @@  import Blaze.ByteString.Builder import Blaze.ByteString.Builder.Char.Utf8+import Control.Arrow((***)) import Control.Monad import Data.Hashable-import Data.HashMap.Strict(HashMap)-import Data.List(intersperse, minimumBy)+import Data.HashSet(HashSet)+--import Data.HashMap.Strict(HashMap)+import Data.List(intersperse, minimumBy, sort) import Data.Maybe import Data.Monoid hiding ((<>)) import Data.Word@@ -189,10 +193,20 @@ import qualified Data.ByteString.Lazy as Lazy import qualified Data.ByteString.Lazy.Char8 as Lazy.Char8 import qualified Data.ByteString.Lazy.UTF8 as Lazy.UTF8-import qualified Data.HashMap.Strict as HashMap+--import qualified Data.HashMap.Strict as HashMap+import qualified Data.HashSet as HashSet  -- | Datatype representing a formatted document. +data LineKind =+    -- | Unerasable linebreak+    Hard+    -- | Linebreak replaced with nothing+  | Soft+    -- | Linebreak replaced with a space+  | Break+    deriving (Ord, Eq, Enum)+ -- Docs are organized into a tree structure whose nodes dictate the -- formatting of the generated text.  These are rendered by the -- various rendering engines into a Builder (from the blaze-builder@@ -202,16 +216,16 @@     Char { charContent :: !Char }     -- | A raw Builder that constructs a string containing no     -- newlines.  This is used to represent basic text.-  | Builder {+  | Content {       -- | Length of the text that gets built.-      builderLength :: !Int,+      contentLength :: !Int,       -- | A Builder that constructs the text.-      builderContent :: !Builder+      contentString :: !Lazy.ByteString     }-    -- | A newline.+    -- | An erasable newline.   | Line {       -- | Whether to insert a space when undone by a group.-      insertSpace :: !Bool+      lineKind :: !LineKind     }     -- | Concatenated documents.  An empty list here represents an empty @Doc@.   | Cat {@@ -232,7 +246,7 @@     -- | Choose the \"best\" from among a list of options.   | Choose {       -- | The list of options.-      chooseOptions :: [Doc]+      chooseOptions :: HashSet Doc     }     -- | Set graphics mode options when rendering the child @Doc@.   | Graphics {@@ -241,6 +255,7 @@       -- | Document to render with graphic mode.       graphicsDoc :: Doc     }+    deriving (Eq)  -- | Graphics options for ANSI terminals.  All options are wrapped in -- the 'Maybe' datatype, with 'Nothing' meaning \"leave this option@@ -264,7 +279,81 @@     }     -- | Reset the terminal in this mode.   | Default+    deriving (Ord, Eq) +instance Ord Doc where+  compare Char { charContent = c1 } Char { charContent = c2 } = compare c1 c2+  compare Char {} _ = LT+  compare _ Char {} = GT+  compare Content { contentString = str1 } Content { contentString = str2 } =+    compare str1 str2+  compare Content {} _ = LT+  compare _ Content {} = GT+  compare Line { lineKind = kind1 } Line { lineKind = kind2 } =+    compare kind1 kind2+  compare Line {} _ = LT+  compare _ Line {} = GT+  compare Cat { catDocs = docs1 } Cat { catDocs = docs2 } = compare docs1 docs2+  compare Cat {} _ = LT+  compare _ Cat {} = GT+  compare Nest { nestLevel = lvl1, nestAlign = al1,+                 nestDelay = delay1, nestDoc = doc1 }+          Nest { nestLevel = lvl2, nestAlign = al2,+                 nestDelay = delay2, nestDoc = doc2 } =+    case compare lvl1 lvl2 of+      EQ -> case compare al1 al2 of+        EQ -> case compare delay1 delay2 of+          EQ -> compare doc1 doc2+          out -> out+        out -> out+      out -> out+  compare Nest {} _ = LT+  compare _ Nest {} = GT+  compare Choose { chooseOptions = opts1 } Choose { chooseOptions = opts2 } =+    compare (sort (HashSet.toList opts1)) (sort (HashSet.toList opts2))+  compare Choose {} _ = LT+  compare _ Choose {} = GT+  compare Graphics { graphicsSGR = sgr1, graphicsDoc = doc1 }+          Graphics { graphicsSGR = sgr2, graphicsDoc = doc2 } =+    case compare sgr1 sgr2 of+      EQ -> compare doc1 doc2+      out -> out++instance Hashable LineKind where+  hashWithSalt s = hashWithSalt s . fromEnum++instance Hashable Doc where+  hashWithSalt s Char { charContent = c } =+    s `hashWithSalt` (0 :: Int) `hashWithSalt` c+  hashWithSalt s Content { contentLength = len, contentString = str } =+    s `hashWithSalt` (1 :: Int) `hashWithSalt` len `hashWithSalt` str+  hashWithSalt s Line { lineKind = kind } =+    s `hashWithSalt` (2 :: Int) `hashWithSalt` kind+  hashWithSalt s Cat { catDocs = docs } =+    s `hashWithSalt` (3 :: Int) `hashWithSalt` docs+  hashWithSalt s Nest { nestLevel = lvl, nestAlign = al,+                        nestDelay = delay, nestDoc = doc } =+    s `hashWithSalt` (4 :: Int) `hashWithSalt` lvl `hashWithSalt`+    al `hashWithSalt` delay `hashWithSalt` doc+  hashWithSalt s Choose { chooseOptions = opts } =+    s `hashWithSalt` (5 :: Int) `hashWithSalt` sort (HashSet.toList opts)+  hashWithSalt s Graphics { graphicsSGR = sgr, graphicsDoc = doc } =+    s `hashWithSalt` (6 :: Int) `hashWithSalt` sgr `hashWithSalt` doc++instance Hashable Graphics where+  hashWithSalt s Options { consoleIntensity = consIntensity,+                           swapForegroundBackground = swap,+                           underlining = underline,+                           foreground = fore,+                           background = back,+                           blinkSpeed = blink } =+    s `hashWithSalt` (0 :: Int) `hashWithSalt`+    fmap fromEnum consIntensity `hashWithSalt`+    fmap fromEnum swap `hashWithSalt` fmap fromEnum underline `hashWithSalt`+    fmap (fromEnum *** fromEnum) fore `hashWithSalt`+    fmap (fromEnum *** fromEnum) back `hashWithSalt` fmap fromEnum blink+  hashWithSalt s Default = s `hashWithSalt` (1 :: Int)+ -- | Generate a 'Doc' representing a graphics mode switch. switchGraphics :: Graphics -> Graphics -> Builder switchGraphics _ Default = fromString (setSGRCode [Reset])@@ -342,22 +431,26 @@ -- | A 'Doc' consisting of a linebreak, that is not turned into a -- space when erased by a 'group'. line :: Doc-line = Line { insertSpace = False }+line = Line { lineKind = Soft }  -- | A 'Doc' consisting of a linebreak, that is turned into a space -- when erased by a 'group'. linebreak :: Doc-linebreak = Line { insertSpace = True }+linebreak = Line { lineKind = Break } +-- | A 'Doc' consisting of a linebreak that cannot be erased by a 'group'.+hardline :: Doc+hardline = Line { lineKind = Hard }+ -- | A 'Doc' consisting of a space character, that can be turned into -- a linebreak in order to break lines that are too long. softline :: Doc-softline = Choose { chooseOptions = [ char ' ', linebreak ] }+softline = Choose { chooseOptions = HashSet.fromList [ char ' ', linebreak ] }  -- | An empty 'Doc' that can be turned into a linebreak in order to -- break lines that are too long. softbreak :: Doc-softbreak = Choose { chooseOptions = [ empty, line ] }+softbreak = Choose { chooseOptions = HashSet.fromList [ empty, line ] }  -- | A 'Doc' containing a single character. char :: Char -> Doc@@ -366,22 +459,22 @@  -- | Create a 'Doc' containing a string. string :: String -> Doc-string str = Builder { builderContent = fromString str,-                       builderLength = length str }+string str = Content { contentString = Lazy.UTF8.fromString str,+                       contentLength = length str }  -- | Create a 'Doc' containing a bytestring. bytestring :: Strict.ByteString -> Doc bytestring txt   | Strict.null txt = empty-  | otherwise = Builder { builderLength = Strict.UTF8.length txt,-                          builderContent = fromByteString txt }+  | otherwise = Content { contentLength = Strict.UTF8.length txt,+                          contentString = Lazy.fromStrict txt }  -- | Create a 'Doc' containing a lazy bytestring lazyBytestring :: Lazy.ByteString -> Doc lazyBytestring txt   | Lazy.null txt = empty-  | otherwise = Builder { builderLength = Lazy.UTF8.length txt,-                          builderContent = fromLazyByteString txt }+  | otherwise = Content { contentLength = Lazy.UTF8.length txt,+                          contentString = txt }  -- | The character @(@ lparen :: Doc@@ -443,7 +536,7 @@ dot :: Doc dot = char '.' --- | The character @\@+-- | The character @\\@ backslash :: Doc backslash = char '\\' @@ -798,6 +891,10 @@ (<+>) :: Doc -> Doc -> Doc left <+> right = left <> space <> right +-- | Join two 'Doc's with a 'hardline' in between them.+(<!>) :: Doc -> Doc -> Doc+left <!> right = left <> hardline <> right+ -- | Join two 'Doc's with a 'line' in between them. (<$>) :: Doc -> Doc -> Doc left <$> right = left <> line <> right@@ -832,7 +929,7 @@ choose :: [Doc] -> Doc choose [] = empty choose [doc] = doc-choose docs = Choose { chooseOptions = docs }+choose docs = Choose { chooseOptions = HashSet.fromList docs }  -- | Concatenate a list of 'Doc's.  This is generally more efficient -- than repeatedly using 'beside' or '<>'.@@ -856,11 +953,11 @@  -- | Join a list of 'Doc's using either 'hsep' or 'vsep'. sep :: [Doc] -> Doc-sep docs = Choose { chooseOptions = [hsep docs, vsep docs] }+sep docs = Choose { chooseOptions = HashSet.fromList [hsep docs, vsep docs] }  -- | Join a list of 'Doc's using either 'hcat' or 'vcat'. cat :: [Doc] -> Doc-cat docs = Choose { chooseOptions = [hcat docs, vcat docs] }+cat docs = Choose { chooseOptions = HashSet.fromList [hcat docs, vcat docs] }  -- | Join a list of 'Doc's with 'softline's in between each.  This is -- generally more efficient than repeatedly using '</>'.@@ -897,30 +994,42 @@  -- | Erase all linebreaks in a 'Doc' and replace them with either -- spaces or nothing, depending on the kind of linebreak.-flatten :: Doc -> Doc-flatten Line { insertSpace = True } = Char { charContent = ' ' }-flatten Line { insertSpace = False } = empty-flatten Cat { catDocs = docs } = Cat { catDocs = map flatten docs }+flatten :: Doc -> Maybe Doc+flatten Line { lineKind = Hard } = Nothing+flatten Line { lineKind = Break } = Just Char { charContent = ' ' }+flatten Line { lineKind = Soft } = Just empty+flatten Cat { catDocs = docs } =+  case mapMaybe flatten docs of+    [] -> Nothing+    flatinner -> Just Cat { catDocs = flatinner } flatten Choose { chooseOptions = docs } =-  Choose { chooseOptions = map flatten docs }-flatten n @ Nest { nestDoc = inner } = n { nestDoc = flatten inner }-flatten doc = doc+  case mapMaybe flatten (HashSet.toList docs) of+    [] -> Nothing+    flatdocs -> Just Choose { chooseOptions = HashSet.fromList flatdocs }+flatten n @ Nest { nestDoc = inner } =+  do+    flatinner <- flatten inner+    return n { nestDoc = flatinner }+flatten doc = Just doc  -- | A 'Doc' that 'choose's between the unmodified argument, or the -- 'flatten'ed version of the argument. group :: Doc -> Doc-group doc = Choose { chooseOptions = [ doc, flatten doc ] }+group doc = case flatten doc of+  Just flatdoc -> Choose { chooseOptions = HashSet.fromList [ doc, flatdoc ] }+  Nothing -> doc  -- | Produce a 'Builder' that renders the 'Doc' to one line. buildOneLine :: Doc -> Builder buildOneLine Char { charContent = chr } = fromChar chr-buildOneLine Builder { builderContent = builder } = builder-buildOneLine Line { insertSpace = True } = fromChar ' '-buildOneLine Line { insertSpace = False } = mempty+buildOneLine Content { contentString = builder } = fromLazyByteString builder+buildOneLine Line { lineKind = Break } = fromChar ' '+buildOneLine Line { lineKind = Soft } = mempty+buildOneLine Line { lineKind = Hard } = fromChar '\n' buildOneLine Cat { catDocs = docs } = mconcat (map buildOneLine docs) buildOneLine Nest { nestDoc = inner } = buildOneLine inner-buildOneLine Choose { chooseOptions = first : _ } = buildOneLine first-buildOneLine Choose {} = error "Choose with no options"+buildOneLine Choose { chooseOptions = opts } =+  buildOneLine (head (HashSet.toList opts)) buildOneLine Graphics { graphicsDoc = inner } = buildOneLine inner  -- | Render the entire 'Doc' to one line.  Good for output that@@ -937,12 +1046,12 @@ -- | Produce a 'Builder' that renders the 'Doc' quickly. buildFast :: Doc -> Builder buildFast Char { charContent = chr } = fromChar chr-buildFast Builder { builderContent = builder } = builder+buildFast Content { contentString = builder } = fromLazyByteString builder buildFast Line {} = fromChar '\n' buildFast Cat { catDocs = docs } = mconcat (map buildFast docs) buildFast Nest { nestDoc = inner } = buildFast inner-buildFast Choose { chooseOptions = first : _ } = buildFast first-buildFast Choose {} = error "Choose with no options"+buildFast Choose { chooseOptions = opts } =+  buildFast (head (HashSet.toList opts)) buildFast Graphics { graphicsDoc = inner } = buildFast inner  -- | Render the entire 'Doc', preserving newlines, but without any@@ -957,24 +1066,6 @@ putFast handle =   toByteStringIO (Strict.hPut handle) . buildFast --- | A rendering of a document.---- Renderings store three basic things: A notion of the "badness" of--- this particular rendering (represented by the overrun and the--- number of lines), the indentation mode for the next document, and a--- function that actually produces the Builder.-data Render =-  Render {-    -- | The number of lines in the document.-    renderLines :: !Word,-    -- | The largest amount by which we've overrun.-    renderOverrun :: !Column,-    -- | A builder that constructs the document.-    renderBuilder :: !(Int -> Int -> Builder),-    -- | Indentation mode for the next document.-    renderIndent :: !Indent-  }- -- | Column data type.  Represents how rendered documents affect the -- current column. @@ -1094,26 +1185,65 @@         Maximum { maxFixed = fixed2, maxRelative = rel2 } =   Maximum { maxFixed = max fixed2 (fixed1 + rel2), maxRelative = rel1 + rel2 } --- | Offsets structure.  this is used as a key in a hash table.  It is--- also important to how the algorithm works.  Renderings represent--- "chunks", which have an ending column and a maximum column.  The--- algorithm glues these chunks together, then evaluates their--- "badness" using the 'advance' function to recalculate the ending--- column and the maximum starting offset.-data Offsets =-  Offsets {+-- | A rendering of a document.+-- Renderings store three basic things: A notion of the "badness" of+-- this particular rendering (represented by the overrun and the+-- number of lines), the indentation mode for the next document, and a+-- function that actually produces the Builder.+data Render =+  Render {     -- | Upper-bound: Highest starting column for this document     -- without causing overrun.  If this is negative, it means you've     -- overrun by that much.-    offsetUpper :: !Int,+    renderUpper :: !Int,     -- | Ending column.-    offsetCol :: !Column+    renderCol :: !Column,+    -- | The number of lines in the document.+    renderLines :: !Word,+    -- | The largest amount by which we've overrun.+    renderOverrun :: !Column,+    -- | A builder that constructs the document.+    renderBuilder :: !(Int -> Int -> Builder),+    -- | Indentation mode for the next document.+    renderIndent :: !Indent   }-  deriving Eq -instance Hashable Offsets where-  hashWithSalt s Offsets { offsetUpper = upper, offsetCol = col } =-    s `hashWithSalt` upper `hashWithSalt` col+-- | Determine whether the first 'Render' is strictly better than the second.+subsumes :: Render -> Render -> Bool+-- Simple comparisons: if the upper bound is greater, the lines are+-- less, and the column is less, then the render is always better.+subsumes Render { renderUpper = upper1, renderLines = lines1,+                  renderCol = Fixed { fixedOffset = col1 } }+         Render { renderUpper = upper2, renderLines = lines2,+                  renderCol = Fixed { fixedOffset = col2 } } =+  upper1 >= upper2 && lines1 <= lines2 && col1 <= col2+subsumes Render { renderUpper = upper1, renderLines = lines1,+                  renderCol = Relative { relOffset = col1 } }+         Render { renderUpper = upper2, renderLines = lines2,+                  renderCol = Relative { relOffset = col2 } } =+  upper1 >= upper2 && lines1 <= lines2 && col1 <= col2+-- A fixed column offset can subsume a relative offset+subsumes Render { renderUpper = upper1, renderLines = lines1,+                  renderCol = Fixed { fixedOffset = col1 } }+         Render { renderUpper = upper2, renderLines = lines2,+                  renderCol = Relative { relOffset = col2 } } =+  upper1 >= upper2 && lines1 <= lines2 && col1 <= col2+-- For two maximums, it's a straightaway comparison+subsumes Render { renderUpper = upper1, renderLines = lines1,+                  renderCol = Maximum { maxRelative = rel1,+                                        maxFixed = fixed1 } }+         Render { renderUpper = upper2, renderLines = lines2,+                  renderCol = Maximum { maxRelative = rel2,+                                        maxFixed = fixed2 } } =+  upper1 >= upper2 && lines1 <= lines2 && rel1 <= rel2 && fixed1 <= fixed2+-- Fixed can subsume a maximum as above+subsumes Render { renderUpper = upper1, renderLines = lines1,+                  renderCol =  Fixed { fixedOffset = col1 } }+         Render { renderUpper = upper2, renderLines = lines2,+                  renderCol = Maximum { maxRelative = rel2,+                                        maxFixed = fixed2 } } =+  upper1 >= upper2 && lines1 <= lines2 && col1 <= rel2 && col1 <= fixed2+subsumes _ _ = False  -- | A result.  This is split into 'Single' and 'Multi' in order to -- optimize for the common case of a single possible rendering.@@ -1123,12 +1253,7 @@     -- | A single possible rendering.     Single {       -- | The rendered document.-      singleRender :: !Render,-      -- | The first column at which this render causes an overrun.-      -- Same as 'offsetUpper'.-      singleUpper :: !Int,-      -- | The current column at the end of rendering.-      singleCol :: !Column+      singleRender :: !Render     }     -- | Multiple possible renderings.   | Multi {@@ -1136,44 +1261,37 @@       -- upper-bound (meaning the first column at which using any of       -- the contents will cause an overrun).  The second map is       -- indexed by the ending column.-      multiOptions :: !(HashMap Offsets Render)+      multiOptions :: ![Render]     }  -- | Generate n spaces makespaces :: Int -> Builder makespaces n = fromLazyByteString (Lazy.Char8.replicate (fromIntegral n) ' ') --- | Pick the best rendering of two.-bestRender :: Render -> Render -> Render-bestRender r1 @ Render { renderLines = lines1, renderOverrun = overrun1 }-           r2 @ Render { renderLines = lines2, renderOverrun = overrun2 }-    -- If one overruns less than the other, pick that one.-  | overrun1 < overrun2 = r1-  | overrun1 > overrun2 = r2-    -- Otherwise, pick the shortest one.-  | otherwise = if lines1 < lines2 then r1 else r2---- Add a result into the HashMap-insertRender :: Int -> Column -> Render -> HashMap Offsets Render ->-                HashMap Offsets Render-insertRender upper col render =-  let-    offsets = Offsets { offsetUpper = upper, offsetCol = col }-  in-    HashMap.insertWith bestRender offsets render+-- Add a 'Render' into a result set, ensuring that any subsumed+-- renders are dropped.+--+-- TODO: The asymptotic runtime of this could likely be improved by some+-- kind of tree structure; however, the design of this structure is+-- nontrivial, due to the 3+-dimensional nature of subsumption, and+-- the wierd interactions between the various kinds of column offsets.+insertRender :: [Render] -> Render -> [Render]+insertRender renders ins+    -- If the inserted element is subsumed by anything in the+    -- list, then don't insert it at all.+  | any (`subsumes` ins) renders = renders+    -- Otherwise, add the element to the list, and drop everything it subsumes.+  | otherwise = ins : filter (not . subsumes ins) renders  -- If a result only has one possibility, convert it to a Single. -- Otherwise, leave it.-packResult :: HashMap Offsets Render -> Result-packResult opts =-  case HashMap.toList opts of-    [(Offsets { offsetUpper = upper, offsetCol = col }, render)] ->-      Single { singleCol = col, singleUpper = upper, singleRender = render }-    _ -> Multi { multiOptions = opts }+packResult :: [Render] -> Result+packResult [opt] = Single { singleRender = opt }+packResult opts = Multi { multiOptions = opts }  -- Pick the best result out of a set of results.  Used at the end to -- pick the final result.-bestRenderInOpts :: HashMap Offsets Render -> Render+bestRenderInOpts :: [Render] -> Render bestRenderInOpts =   let     -- | Compare two Renders.  Less than means better.@@ -1184,21 +1302,18 @@         EQ -> compare lines1 lines2         out -> out   in-    minimumBy compareRenders . HashMap.elems+    minimumBy compareRenders  -- | Append operation on complete states.  A complete state is the -- contents of an Offset and a Render (ie. the contents of Single, or -- the contents of an entry in a Multi combined with the corresponding -- key).-appendOne :: (Int, Column, Render) -> (Int, Column, Render) ->-             (Int, Column, Render)-appendOne (upper1, col1, Render { renderBuilder = build1,-                                  renderLines = lines1,-                                  renderOverrun = overrun1 })-          (upper2, col2, Render { renderBuilder = build2,-                                  renderLines = lines2,-                                  renderOverrun = overrun2,-                                  renderIndent = ind }) =+appendOne :: Render -> Render -> Render+appendOne Render { renderUpper = upper1, renderLines = lines1, renderCol = col1,+                   renderOverrun = overrun1, renderBuilder = build1 }+          Render { renderUpper = upper2, renderLines = lines2, renderCol = col2,+                   renderOverrun = overrun2, renderBuilder = build2,+                   renderIndent = ind } =   let     -- The new build is completely determined by the first render's     -- starting column.@@ -1243,13 +1358,13 @@         else Fixed { fixedOffset = 0 }    in-    -- For the new column, use advance-    (newupper, col1 `advance` col2,-     Render { renderBuilder = newbuild, renderIndent = ind,-              -- For the new overrun, take the max of the existing-              -- overruns and newoverrun-              renderOverrun = max (max overrun1 overrun2) newoverrun,-              renderLines = lines1 + lines2 })+    Render { renderBuilder = newbuild, renderIndent = ind,+             renderLines = lines1 + lines2, renderUpper = newupper,+             -- For the new overrun, take the max of the existing+             -- overruns and newoverrun+             renderOverrun = max (max overrun1 overrun2) newoverrun,+             -- For the new column, use advance+             renderCol = col1 `advance` col2 }  -- | Combine two results into a Multi.  This achieves the same result -- as HashMap.unionWith bestRender (meaning, union these maps,@@ -1257,46 +1372,19 @@ -- index), but handles Singles as well. mergeResults :: Result -> Result -> Result -- Single is equivalent to a single-entry HashMap-mergeResults s1 @ Single { singleRender =-                              r1 @ Render { renderOverrun = overrun1,-                                            renderLines = lines1 },-                           singleUpper = upper1, singleCol = col1 }-             s2 @ Single { singleRender =-                              r2 @ Render { renderOverrun = overrun2,-                                            renderLines = lines2 },-                           singleUpper = upper2, singleCol = col2 }-  -- If the keys would collide-  | upper1 == upper2 && col1 == col2 =-    -- Duplicate the functionality of bestRender-    if overrun1 < overrun2-      then s1-      else if overrun1 > overrun2-        then s2-        else if lines1 < lines2-          then s1-          else s2-  -- Otherwise, create a Multi-  | otherwise =-    Multi { multiOptions =-               HashMap.fromList [(Offsets { offsetUpper = upper1,-                                            offsetCol = col1 },-                                  r1),-                                 (Offsets { offsetUpper = upper2,-                                            offsetCol = col2 },-                                  r2)] }-mergeResults Single { singleRender = render, singleUpper = upper,-                      singleCol = col }+mergeResults s1 @ Single { singleRender = r1 }+             s2 @ Single { singleRender = r2 }+  | subsumes r1 r2 = s1+  | subsumes r2 r1 = s2+  | otherwise = Multi { multiOptions = [r1, r2] }+mergeResults Single { singleRender = render }              Multi { multiOptions = opts } =-  -- Do an insertWith bestRender-  let-    offsets = Offsets { offsetUpper = upper, offsetCol = col }-  in-   Multi { multiOptions = HashMap.insertWith bestRender offsets render opts }+  packResult (insertRender opts render) -- This operation is commutative mergeResults m @ Multi {} s @ Single {} = mergeResults s m -- Otherwise it's a straightaway HashMap union mergeResults Multi { multiOptions = opts1 } Multi { multiOptions = opts2 } =-  Multi { multiOptions = HashMap.unionWith bestRender opts1 opts2 }+  packResult (foldl insertRender opts1 opts2)  -- Add indentation on to a builder.  Note, this is used to create the -- builder functions used in Render.@@ -1349,25 +1437,22 @@         -- Single characters have a single possibility, a relative         -- ending position one beyond the start, and an upper-bound         -- one shorter than the maximum width.-        Single {-          singleRender =-             Render { renderLines = 0, renderOverrun = overrun,-                      renderBuilder = builder, renderIndent = None },-          singleCol = Relative 1, singleUpper = maxcol - 1-        }-    buildDynamic _ _ ind Builder { builderContent = txt, builderLength = len } =+        Single { singleRender =+                    Render { renderOverrun = overrun, renderIndent = None,+                             renderBuilder = builder, renderCol = Relative 1,+                             renderLines = 0, renderUpper = maxcol - 1 } }+    buildDynamic _ _ ind Content { contentString = txt, contentLength = len } =       let         -- Why would you have maxcol == 0?  Who knows, but check for it.         overrun = if maxcol >= len then Relative 0 else Relative (len - maxcol)-        builder = contentBuilder ind txt+        builder = contentBuilder ind (fromLazyByteString txt)       in         -- Text has a single possibility and a relative ending position         -- equal to its length-       Single {-         singleRender = Render { renderLines = 0, renderOverrun = overrun,-                                 renderBuilder = builder, renderIndent = None },-         singleCol = Relative len, singleUpper = maxcol - len-       }+       Single { singleRender =+                   Render { renderLines = 0, renderUpper = maxcol - len,+                            renderBuilder = builder, renderCol = Relative len,+                            renderIndent = None, renderOverrun = overrun } }     buildDynamic _ nesting _ Line {} =       -- A newline starts at the nesting level, has no overrun, and an       -- upper-bound equal to the maximum column width.@@ -1377,100 +1462,71 @@         singleRender = Render { renderOverrun = Fixed { fixedOffset = 0 },                                 renderIndent = Full, renderLines = 1,                                 renderBuilder = const $! const $!-                                                fromChar '\n' },-        singleCol = nesting, singleUpper = maxcol-      }+                                                fromChar '\n',+                                renderCol = nesting, renderUpper = maxcol } }     -- This is for an empty cat, ie. the empty document     buildDynamic _ _ ind Cat { catDocs = [] } =       -- The empty document has no content, no overrun, a relative end       -- position of 0, and an upper-bound of maxcol.       Single {-        singleRender = Render { renderOverrun = Fixed { fixedOffset = 0 },-                                renderIndent = ind, renderLines = 0,-                                renderBuilder = const mempty },-        singleCol = Relative { relOffset = 0 }, singleUpper = maxcol-      }+        singleRender = Render { renderOverrun = Fixed 0, renderIndent = ind,+                                renderLines = 0, renderBuilder = const mempty,+                                renderCol = Relative 0, renderUpper = maxcol } }     buildDynamic sgr nesting ind Cat { catDocs = first : rest } =       let         -- Glue two Results together.  This gets used in a fold.         appendResults :: Result -> Doc -> Result         -- The accumulated result is a Single.         appendResults Single { singleRender =-                                  render1 @ Render { renderIndent = ind' },-                               singleUpper = upper1, singleCol = col1 } doc' =+                                  render1 @ Render { renderIndent = ind' } }+                      doc' =           -- Render the document.           case buildDynamic sgr nesting ind' doc' of             -- If there's a single result, it's easy; just use appendOne.-            Single { singleUpper = upper2, singleCol = col2,-                     singleRender = render2 } ->+            Single { singleRender = render2  } ->               let-                (newupper, newcol, newrender) =-                  appendOne (upper1, col1, render1) (upper2, col2, render2)+                newrender = appendOne render1 render2               in-                Single { singleUpper = newupper, singleCol = newcol,-                         singleRender = newrender }+                Single { singleRender = newrender }             -- Otherwise, we have to fold over all the options and             -- glue on the accumulated result.             Multi { multiOptions = opts } ->               let                 -- Level 2 fold function: glue the accumulated result                 -- on to an option.-                foldfun :: HashMap Offsets Render -> Offsets -> Render ->-                           HashMap Offsets Render-                foldfun accum Offsets { offsetUpper = upper2,-                                        offsetCol = col2 } render2 =-                  let-                    (newupper, newcol, newrender) =-                      appendOne (upper1, col1, render1) (upper2, col2, render2)-                  in-                    insertRender newupper newcol newrender accum+                foldfun :: [Render] -> Render -> [Render]+                foldfun accum = insertRender accum . appendOne render1               in                 -- Fold it up, then use packResult.-                packResult (HashMap.foldlWithKey' foldfun HashMap.empty opts)+                packResult (foldl foldfun [] opts)         -- If the accumulate result is a multi, then we'll need to         -- glue the next render on to each option.         appendResults Multi { multiOptions = opts } doc' =           let             -- Outer fold, over each option in the accumulated result,             -- gluing on the next render.-            outerfold :: HashMap Offsets Render -> Offsets -> Render ->-                         HashMap Offsets Render-            outerfold accum Offsets { offsetUpper = upper1,-                                      offsetCol = col1 }-                      render1 @ Render { renderIndent = ind' } =+            outerfold :: [Render] -> Render -> [Render]+            outerfold accum render1 @ Render { renderIndent = ind' } =               case buildDynamic sgr nesting ind' doc' of                 -- If the render is a single result, then just glue it                 -- on to the current option.-                Single { singleUpper = upper2, singleCol = col2,-                         singleRender = render2 } ->-                  let-                    (newupper, newcol, newrender) =-                      appendOne (upper1, col1, render1) (upper2, col2, render2)-                  in-                    insertRender newupper newcol newrender accum+                Single { singleRender = render2 } ->+                  insertRender accum (appendOne render1 render2)                 -- If the render result is ALSO a Multi, then we need                 -- to do another level of fold to glue those options                 -- on to the current one.                 Multi { multiOptions = opts2 } ->                   let                     -- Innermost fold, glues two options together-                    innerfold :: HashMap Offsets Render -> Offsets -> Render ->-                                 HashMap Offsets Render-                    innerfold accum' Offsets { offsetUpper = upper2,-                                               offsetCol = col2 } render2 =-                      let-                        (newupper, newcol, newrender) =-                          appendOne (upper1, col1, render1)-                                    (upper2, col2, render2)-                      in-                        insertRender newupper newcol newrender accum'+                    innerfold :: [Render] -> Render -> [Render]+                    innerfold accum' = insertRender accum' . appendOne render1                   in                     -- Don't bother calling packResult here, we'll do                     -- it at the end anyway.-                    HashMap.foldlWithKey' innerfold accum opts2+                    foldl innerfold accum opts2           in             -- Fold it up, then use packResult.-            packResult (HashMap.foldlWithKey' outerfold HashMap.empty opts)+            packResult (foldl outerfold [] opts)          -- Build the first item         firstres = buildDynamic sgr nesting ind first@@ -1519,11 +1575,11 @@         s @ Single { singleRender = r } -> s { singleRender = updateRender r }         -- Update all renders for a Multi.         m @ Multi { multiOptions = opts } ->-          m { multiOptions = HashMap.map updateRender opts }+          m { multiOptions = map updateRender opts }     buildDynamic sgr nesting ind Choose { chooseOptions = options } =       let         -- Build up all the components-        results = map (buildDynamic sgr nesting ind) options+        results = map (buildDynamic sgr nesting ind) (HashSet.toList options)       in         -- Now merge them into an minimal set of options         foldl1 mergeResults results@@ -1542,7 +1598,7 @@           s @ Single { singleRender = render } ->             s { singleRender = wrapBuilder render }           m @ Multi { multiOptions = opts } ->-            m { multiOptions = HashMap.map wrapBuilder opts }+            m { multiOptions = map wrapBuilder opts }       -- Otherwise, skip it entirely       | otherwise = buildDynamic sgr2 nesting ind inner