on master help sign in
Implement pretty printing combinators
commitfcd35a1faa7155009803cc2f4bf4733df8685ab4 authormrkkrp <markkarpov92@gmail.com> dateSat 8 Dec 2018, 21:39 (8 years ago) parentbeaa521
19 files changed+608−28
app/Main.hs+3 data/printer/list0-multi.hs+4 data/printer/list0-single.hs+1 data/printer/list1-multi.hs+8 data/printer/list1-single.hs+1 data/printer/mhdr-multi.hs+7 data/printer/mhdr-single.hs+3 data/printer/ssig-multi.hs+5 data/printer/ssig-single.hs+1 ormolu.cabal+18−7 src/Ormolu/Parser.hs+4−1 src/Ormolu/Printer.hs+33−1 src/Ormolu/Printer/Base.hs−3 src/Ormolu/Printer/Combinators.hs+160 src/Ormolu/Printer/Internal.hs+234 src/Ormolu/Type.hs+13−12 tests/Main.hs−4 tests/Ormolu/Printer/CombinatorsSpec.hs+112 tests/Spec.hs+1
app/Main.hs+3
@@ -2,6 +2,7 @@ module Main (main) where
22
33import Control.Monad
44import Ormolu.Parser
5+import Ormolu.Printer
56import System.Environment (getArgs)
67import qualified Outputable as GHC
78
@@ -23,5 +24,7 @@ main = do
2324 putStrLn "\nparsed module:\n"
2425 putStrLn (showOutputable parsedModule)
2526
27+ printModule anns parsedModule
28+
2629showOutputable :: GHC.Outputable o => o -> String
2730showOutputable = GHC.showSDocUnsafe . GHC.ppr
data/printer/list0-multi.hsnew file+4
@@ -0,0 +1,4 @@
1+[ foo
2+, bar
3+, baz
4+]
data/printer/list0-single.hsnew file+1
@@ -0,0 +1 @@
1+[foo, bar, baz]
data/printer/list1-multi.hsnew file+8
@@ -0,0 +1,8 @@
1+[ foo
2+, bar
3+, foo
4+ :: ( Int
5+ -> Int
6+ )
7+ -> Bool
8+]
data/printer/list1-single.hsnew file+1
@@ -0,0 +1 @@
1+[foo, bar, foo :: (Int -> Int) -> Bool]
data/printer/mhdr-multi.hsnew file+7
@@ -0,0 +1,7 @@
1+module MyModule
2+ ( R
3+ , runR
4+ , txt
5+ , blah
6+ )
7+where
data/printer/mhdr-single.hsnew file+3
@@ -0,0 +1,3 @@
1+module MyModule
2+ (R, runR, txt, blah)
3+where
data/printer/ssig-multi.hsnew file+5
@@ -0,0 +1,5 @@
1+foo
2+ :: ( Int
3+ -> Int
4+ )
5+ -> Bool
data/printer/ssig-single.hsnew file+1
@@ -0,0 +1 @@
1+foo :: (Int -> Int) -> Bool
ormolu.cabal+18−7
@@ -13,6 +13,7 @@ build-type: Simple
1313description: A formatter for Haskell source code.
1414extra-doc-files: CHANGELOG.md
1515 , README.md
16+data-files: data/printer/*.hs
1617
1718source-repository head
1819 type: git
@@ -25,15 +26,18 @@ flag dev
2526
2627library
2728 hs-source-dirs: src
28− build-depends: base >= 4.8 && < 5.0
29− , data-default-class
30− , ghc >= 8.4.3
31− , ghc-boot-th >= 8.4.3
29+ build-depends: base >= 4.8 && < 5.0
30+ , containers >= 0.5 && < 0.7
31+ , ghc >= 8.4.3
32+ , ghc-boot-th >= 8.4.3
3233 , ghc-exactprint >= 0.5.6
34+ , mtl >= 2.0 && < 3.0
35+ , text >= 0.2 && < 1.3
3336 exposed-modules: Ormolu
3437 , Ormolu.Parser
3538 , Ormolu.Printer
36− , Ormolu.Printer.Base
39+ , Ormolu.Printer.Combinators
40+ , Ormolu.Printer.Internal
3741 , Ormolu.Type
3842 if flag(dev)
3943 ghc-options: -Wall -Werror -Wcompat
@@ -46,11 +50,18 @@ library
4650 default-language: Haskell2010
4751
4852test-suite tests
49− main-is: Main.hs
53+ main-is: Spec.hs
5054 hs-source-dirs: tests
5155 type: exitcode-stdio-1.0
52− build-depends: base >= 4.8 && < 5.0
56+ build-depends: base >= 4.8 && < 5.0
57+ , containers >= 0.5 && < 0.7
58+ , hspec >= 2.0 && < 3.0
5359 , ormolu
60+ , text >= 0.2 && < 1.3
61+ build-tools: hspec-discover >= 2.0 && < 3.0
62+ other-modules:
63+ Ormolu.Printer.CombinatorsSpec
64+
5465 if flag(dev)
5566 ghc-options: -Wall -Werror
5667 else
src/Ormolu/Parser.hs+4−1
@@ -1,3 +1,5 @@
1+-- | Parser for Haskell source code.
2+
13module Ormolu.Parser
24 ( parseModule )
35where
@@ -18,5 +20,6 @@ parseModule
1820 -> IO ([GHC.Warn], Either (GHC.SrcSpan, String) (Anns, GHC.ParsedSource))
1921parseModule dynOpts path input = ghcWrapper $ do
2022 dynFlags0 <- initDynFlagsPure path input
21− (dynFlags1, _, ws) <- GHC.parseDynamicFilePragma dynFlags0 (dynOption <$> dynOpts)
23+ (dynFlags1, _, ws) <-
24+ GHC.parseDynamicFilePragma dynFlags0 (dynOption <$> dynOpts)
2225 return (ws, parseModuleFromStringInternal dynFlags1 path input)
src/Ormolu/Printer.hs+33−1
@@ -1,3 +1,35 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE RecordWildCards #-}
3+
4+-- | Pretty-printer for Haskell AST.
5+
16module Ormolu.Printer
2− ( )
7+ ( printModule )
38where
9+
10+import GHC
11+import Language.Haskell.GHC.ExactPrint.Types
12+import Ormolu.Printer.Combinators
13+import qualified Data.Text.IO as TIO
14+
15+-- | Render a module.
16+
17+printModule
18+ :: Anns
19+ -> ParsedSource
20+ -> IO ()
21+printModule anns src = TIO.putStr $
22+ runR True (p_HsModule src) anns
23+
24+p_HsModule :: ParsedSource -> R ()
25+p_HsModule l =
26+ located l $ \HsModule {..} ->
27+ p_hsmodName hsmodName
28+
29+p_hsmodName :: Maybe (Located ModuleName) -> R ()
30+p_hsmodName Nothing = return ()
31+p_hsmodName (Just l) =
32+ located l $ \moduleName -> do
33+ line $ do
34+ txt "module "
35+ atom moduleName
src/Ormolu/Printer/Base.hsdeleted−3
@@ -1,3 +0,0 @@
1−module Ormolu.Printer.Base
2− ( )
3−where
src/Ormolu/Printer/Combinators.hsnew file+160
@@ -0,0 +1,160 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE RecordWildCards #-}
3+
4+-- | Printing combinators.
5+
6+module Ormolu.Printer.Combinators
7+ ( -- * The 'R' monad
8+ R
9+ , runR
10+ -- * Combinators
11+ -- ** Basic
12+ , txt
13+ , atom
14+ , newline
15+ , inci
16+ , located
17+ , velt
18+ , velt'
19+ -- ** Wrapping
20+ , line
21+ , braces
22+ , brackets
23+ , parens
24+ -- ** Literals
25+ , comma
26+ , ofType
27+ , sarrow
28+ , darrow
29+ )
30+where
31+
32+import Data.Data (Data)
33+import Data.List (intersperse)
34+import Data.Text (Text)
35+import Debug.Trace
36+import Language.Haskell.GHC.ExactPrint.Types
37+import Ormolu.Printer.Internal
38+import Outputable (Outputable (..), showSDocUnsafe)
39+import SrcLoc
40+import qualified Data.Text as T
41+
42+----------------------------------------------------------------------------
43+-- Basic
44+
45+-- | Output a fixed 'Text' fragment. The argument may not contain any line
46+-- breaks or tab characters.
47+
48+txt :: Text -> R ()
49+txt t = ensureIndent >> spit t
50+
51+-- | Output 'Outputable' fragment of AST.
52+
53+atom :: Outputable a => a -> R ()
54+atom = txt . T.pack . showSDocUnsafe . ppr
55+
56+-- | Enter a 'Located' entity. This primitive handles outputting comments
57+-- that may be associated with the primitive and sets corresponding layout
58+-- for the inner computation.
59+
60+located :: Data a => Located a -> (a -> R ()) -> R ()
61+located loc@(L l a) f = do
62+ -- TODO implement handling of comments properly
63+ mann <- lookupAnn loc
64+ let m = enterLayout
65+ (if isOneLineSpan l
66+ then SingleLine
67+ else MultiLine)
68+ (f a)
69+ case mann of
70+ Nothing -> m
71+ Just Ann {..} ->
72+ enterLayout MultiLine $ do
73+ traceShow annPriorComments $ mapM_ atom annPriorComments
74+ m
75+ traceShow annFollowingComments $ mapM_ atom annFollowingComments
76+
77+-- | Element of variable layout. This means that the sub-components may be
78+-- rendered either on single line or each on its own line depending on
79+-- current layout.
80+--
81+-- This version does not make subsequent element (second and later) align
82+-- with the first automatically and does not insert spaces between elements
83+-- when layout is single line.
84+
85+velt :: [R ()] -> R ()
86+velt xs = sequence_ (intersperse sep (sitcc <$> xs))
87+ where
88+ sep = vlayout (pure ()) newline
89+
90+-- | Like 'velt', but all sub-elements start at the same indentation level
91+-- as first element, additionally spaces are inserted when layout is single
92+-- line.
93+
94+velt' :: [R ()] -> R ()
95+velt' xs = sitcc $ sequence_ (intersperse sep (sitcc <$> xs))
96+ where
97+ sep = vlayout (spit " ") newline
98+
99+----------------------------------------------------------------------------
100+-- Wrapping
101+
102+-- | Finish given entity by a 'newline'.
103+
104+line :: R () -> R ()
105+line m = do
106+ m
107+ newline
108+
109+-- | Surround given entity by curly braces.
110+
111+braces :: R () -> R ()
112+braces m = sitcc $ do
113+ txt "{"
114+ ospaces m
115+ txt "}"
116+
117+-- | Surround given entity by square brackets.
118+
119+brackets :: R () -> R ()
120+brackets m = sitcc $ do
121+ txt "["
122+ ospaces m
123+ txt "]"
124+
125+-- | Surround given entity by parentheses.
126+
127+parens :: R () -> R ()
128+parens m = sitcc $ do
129+ txt "("
130+ ospaces m
131+ txt ")"
132+
133+-- | Surround given entity by optional space before and a newline after, iff
134+-- current layout is multiline.
135+
136+ospaces :: R () -> R ()
137+ospaces m = vlayout m (txt " " >> m >> newline)
138+
139+----------------------------------------------------------------------------
140+-- Literals
141+
142+-- | Print @,@ followed by a space.
143+
144+comma :: R ()
145+comma = txt ", "
146+
147+-- | Print @::@ followed by a space.
148+
149+ofType :: R ()
150+ofType = txt ":: "
151+
152+-- | Print @->@ followed by a space.
153+
154+sarrow :: R ()
155+sarrow = txt "-> "
156+
157+-- | Print @=>@ followed by a space.
158+
159+darrow :: R ()
160+darrow = txt "=> "
src/Ormolu/Printer/Internal.hsnew file+234
@@ -0,0 +1,234 @@
1+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
4+-- | In most cases import "Ormolu.Printer.Combinators" instead, these
5+-- functions are the low-level building blocks and should not be used on
6+-- their own. The 'R' monad is re-exported from "Ormolu.Printer.Combinators"
7+-- as well.
8+
9+module Ormolu.Printer.Internal
10+ ( -- * The 'R' monad
11+ R
12+ , runR
13+ -- * Internal functions
14+ , spit
15+ , newline
16+ , ensureIndent
17+ , inci
18+ , sitcc
19+ , Layout (..)
20+ , enterLayout
21+ , vlayout
22+ , lookupAnn
23+ )
24+where
25+
26+import Control.Monad.Reader
27+import Control.Monad.State.Strict
28+import Data.Data (Data)
29+import Data.Text (Text)
30+import Data.Text.Lazy.Builder
31+import Debug.Trace
32+import Language.Haskell.GHC.ExactPrint.Types
33+import SrcLoc
34+import qualified Data.Text.Lazy as TL
35+import qualified Data.Map as M
36+import qualified Data.Text as T
37+
38+----------------------------------------------------------------------------
39+-- The 'R' monad
40+
41+-- | The 'R' monad hosts combinators that allow us to describe how to render
42+-- AST.
43+
44+newtype R a = R (ReaderT RC (State SC) a)
45+ deriving (Functor, Applicative, Monad)
46+
47+-- | Reader context of 'R'.
48+
49+data RC = RC
50+ { rcIndent :: !Int
51+ -- ^ Indentation level, as the column index we need to start from after a
52+ -- newline if we break lines
53+ , rcLayout :: Layout
54+ -- ^ Current layout
55+ , rcAnns :: Anns
56+ -- ^ The collection of annotations obtained after parsing
57+ , rcDebug :: Bool
58+ -- ^ Whether to print debugging info as we go
59+ }
60+
61+-- | State context of 'R'.
62+
63+data SC = SC
64+ { scColumn :: !Int
65+ -- ^ Index of the next column to render
66+ , scBuilder :: Builder
67+ -- ^ Rendered source code so far
68+ }
69+
70+-- | 'Layout' options.
71+
72+data Layout
73+ = SingleLine -- ^ Put everything on single line
74+ | MultiLine -- ^ Use multiple lines
75+ deriving (Eq, Show)
76+
77+-- | Run an 'R' monad.
78+
79+runR
80+ :: Bool -- ^ Whether to print debugging info
81+ -> R () -- ^ Monad to run
82+ -> Anns -- ^ Annotations to use
83+ -> Text -- ^ Resulting rendition
84+runR debug (R m) anns =
85+ TL.toStrict . toLazyText . scBuilder $ execState (runReaderT m rc) sc
86+ where
87+ rc = RC
88+ { rcIndent = 0
89+ , rcLayout = MultiLine
90+ , rcAnns = anns
91+ , rcDebug = debug
92+ }
93+ sc = SC
94+ { scColumn = 0
95+ , scBuilder = mempty
96+ }
97+
98+----------------------------------------------------------------------------
99+-- Internal functions
100+
101+-- | Grow current output by appending given 'Text' to it, which may not
102+-- contain newlines.
103+
104+spit :: Text -> R ()
105+spit x = do
106+ traceR "spit_before" (Just x)
107+ R (modify modSC)
108+ traceR "spit_after" Nothing
109+ where
110+ modSC sc = sc
111+ { scBuilder = scBuilder sc <> fromText x
112+ , scColumn = scColumn sc + T.length x
113+ }
114+
115+-- | Output a newline.
116+
117+newline :: R ()
118+newline = do
119+ traceR "newline_before" (Just "\n")
120+ R (modify modSC)
121+ traceR "newline_after" Nothing
122+ where
123+ modSC sc = sc
124+ { scBuilder = scBuilder sc <> "\n"
125+ , scColumn = 0
126+ }
127+
128+-- | Ensure that indentation level is satisfied. Insert correct number of
129+-- spaces if it isn't.
130+
131+ensureIndent :: R ()
132+ensureIndent = do
133+ traceR "ensure_indent" Nothing
134+ i <- R (asks rcIndent)
135+ c <- R (gets scColumn)
136+ when (c < i) $
137+ spit (T.replicate (i - c) " ")
138+
139+-- | Increase indentation level by one indentation step for the inner
140+-- computation.
141+
142+inci :: R () -> R ()
143+inci m' = do
144+ traceR "inci_before" Nothing
145+ let R m = traceR "inci_inside" Nothing >> m'
146+ R (local modRC m)
147+ traceR "inci_ended" Nothing
148+ where
149+ modRC x = x
150+ { rcIndent = rcIndent x + indentStep
151+ }
152+
153+-- | Set indentation level for the inner computation equal to current
154+-- column. This makes sure that the entire inner block is uniformly
155+-- \"shifted\" to the right. Only works (and makes sense) when enclosing
156+-- layout is multilined.
157+
158+sitcc :: R () -> R ()
159+sitcc m' = do
160+ traceR "sitcc_before" Nothing
161+ i <- R (asks rcIndent)
162+ c <- R (gets scColumn)
163+ let modRC x = x
164+ { rcIndent = max i c
165+ }
166+ R m = traceR "sitcc_inside" Nothing >> m'
167+ vlayout m' (R (local modRC m))
168+ traceR "sitcc_ended" Nothing
169+
170+-- | Set 'Layout' for internal computation.
171+
172+enterLayout :: Layout -> R () -> R ()
173+enterLayout l (R m) = do
174+ let label =
175+ case l of
176+ SingleLine -> "single_line"
177+ MultiLine -> "multi_line"
178+ traceR ("lstart_" ++ label) Nothing
179+ let modRC x = x
180+ { rcLayout = l
181+ }
182+ R (local modRC m)
183+ traceR ("lend_" ++ label) Nothing
184+
185+-- | Do one or another thing depending on current 'Layout'.
186+
187+vlayout
188+ :: R () -- ^ Single line
189+ -> R () -- ^ Multi line
190+ -> R ()
191+vlayout sline mline = do
192+ l <- R (asks rcLayout)
193+ case l of
194+ SingleLine -> sline
195+ MultiLine -> mline
196+
197+-- | Lookup an annotation.
198+
199+lookupAnn :: Data a => Located a -> R (Maybe Annotation)
200+lookupAnn (L s d) = do
201+ anns <- R (asks rcAnns)
202+ let k = AnnKey s (annGetConstr d)
203+ traceShow k $ return (M.lookup k anns)
204+
205+----------------------------------------------------------------------------
206+-- Debug helpers
207+
208+-- | Tracing helper.
209+
210+traceR
211+ :: String -- ^ Label
212+ -> Maybe Text -- ^ Output, if any
213+ -> R ()
214+traceR label moutput = R $ do
215+ debug <- asks rcDebug
216+ i <- asks rcIndent
217+ c <- gets scColumn
218+ when debug . traceM . concat $
219+ [ replicate i ' '
220+ , unwords [ label ++ ":"
221+ , "i=" ++ show i
222+ , "c=" ++ show c
223+ ]
224+ ] ++ case moutput of
225+ Nothing -> []
226+ Just output -> [" out=" ++ show output]
227+
228+----------------------------------------------------------------------------
229+-- Constants
230+
231+-- | Indentation step.
232+
233+indentStep :: Int
234+indentStep = 2
src/Ormolu/Type.hs+13−12
@@ -1,27 +1,28 @@
1+-- | Types used in the library.
2+
13module Ormolu.Type
24 ( Config (..)
5+ , defaultConfig
36 , DynOption (..)
47 , dynOption
58 )
69where
710
8−import Data.Default.Class
911import qualified SrcLoc as GHC
1012
1113-- | Ormolu configuration.
1214
13−data Config = Config
14− { cfgLineWidth :: Int
15− , cfgIndentStep :: Int
16− , cfgDynOptions :: [DynOption]
17− }
15+newtype Config = Config
16+ { cfgDynOptions :: [DynOption]
17+ -- ^ Dynamic options to pass to GHC parser
18+ } deriving (Eq, Show)
1819
19−instance Default Config where
20− def = Config
21− { cfgLineWidth = 80
22− , cfgIndentStep = 2
23− , cfgDynOptions = []
24− }
20+-- | Default 'Config'.
21+
22+defaultConfig :: Config
23+defaultConfig = Config
24+ { cfgDynOptions = []
25+ }
2526
2627-- | A wrapper for dynamic options.
2728
tests/Main.hsdeleted−4
@@ -1,4 +0,0 @@
1−module Main (main) where
2−
3−main :: IO ()
4−main = return ()
tests/Ormolu/Printer/CombinatorsSpec.hsnew file+112
@@ -0,0 +1,112 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Ormolu.Printer.CombinatorsSpec (spec) where
4+
5+import Ormolu.Printer.Combinators
6+import Ormolu.Printer.Internal
7+import Test.Hspec
8+import qualified Data.Map.Strict as M
9+import qualified Data.Text.IO as T
10+
11+-- NOTE Testing combinators in separation is easy, but it's not very useful
12+-- either because interesting things happen only when combinators are used
13+-- together. However, there are too many combinations to test, so we test
14+-- just a few things here and expect new tests to be added on demand when
15+-- particularly tricky or problematic use cases are discovered.
16+--
17+-- The printing logic is indirectly tested in the main bulk of reformatting
18+-- tests.
19+
20+spec :: Spec
21+spec = do
22+ stdTest "simple signature" rSimpleSig "ssig"
23+ stdTest "list" rList0 "list0"
24+ stdTest "list with annotated name in it" rList1 "list1"
25+ stdTest "module header" rModuleHeader "mhdr"
26+
27+-- | Do a standard test prodedure testing both single-line and multi-line
28+-- layouts.
29+
30+stdTest
31+ :: String -- ^ Test name
32+ -> R () -- ^ Computation to test
33+ -> FilePath -- ^ Base file name where expected result is stored
34+ -> Spec
35+stdTest name m path = describe name $ do
36+ let spath = "data/printer/" ++ path ++ "-single.hs"
37+ mpath = "data/printer/" ++ path ++ "-multi.hs"
38+ context "single-line" $
39+ it "works as expected" $
40+ singleLine m `shouldRender` spath
41+ context "multi-line" $
42+ it "works as expected" $
43+ multiLine m `shouldRender` mpath
44+
45+----------------------------------------------------------------------------
46+-- Test computations
47+
48+rSimpleSig :: R ()
49+rSimpleSig = line rFn
50+
51+rList0 :: R ()
52+rList0 = line . brackets $
53+ velt
54+ [ txt "foo"
55+ , comma >> txt "bar"
56+ , comma >> txt "baz"
57+ ]
58+
59+rList1 :: R ()
60+rList1 = line . brackets $
61+ velt
62+ [ txt "foo"
63+ , comma >> txt "bar"
64+ , comma >> rFn
65+ ]
66+
67+rFn :: R ()
68+rFn = velt'
69+ [ txt "foo"
70+ , inci $ velt'
71+ [ do ofType
72+ parens $ velt'
73+ [ txt "Int"
74+ , sarrow >> txt "Int"
75+ ]
76+ , sarrow >> txt "Bool"
77+ ]
78+ ]
79+
80+rModuleHeader :: R ()
81+rModuleHeader = do
82+ line $ do
83+ txt "module "
84+ txt "MyModule"
85+ line . inci . parens . velt $
86+ [ txt "R"
87+ , comma >> txt "runR"
88+ , comma >> txt "txt"
89+ , comma >> txt "blah"
90+ ]
91+ line (txt "where")
92+
93+----------------------------------------------------------------------------
94+-- Helpers
95+
96+-- | Run given computation in @'R' ()@ monad and compare result of rendering
97+-- with contents of a given file.
98+
99+shouldRender :: R () -> FilePath -> Expectation
100+shouldRender m path = do
101+ expected <- T.readFile path
102+ runR False m M.empty `shouldBe` expected
103+
104+-- | Render using single-line layout.
105+
106+singleLine :: R () -> R ()
107+singleLine = enterLayout SingleLine
108+
109+-- | Render using multi-line layout.
110+
111+multiLine :: R () -> R ()
112+multiLine = enterLayout MultiLine
tests/Spec.hsnew file+1
@@ -0,0 +1 @@
1+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}