Skip to content

core/refactor: tying up the printers to allow for recursive printing - #54

Merged
vicgeentor merged 5 commits into
mainfrom
make-printing-recursive
Apr 9, 2026
Merged

vicgeentor merged 5 commits into
mainfrom
make-printing-recursive

Conversation

@vicgeentor

Copy link
Copy Markdown
Collaborator

Sorry for the big PR again but I didn't know how I would split this up in different PRs, since it refactors so much of the core of our printing logic that rely on each other. Splitting it up without a failing CI seemed impossible.

We currently use multiple different ways of dealing with things like adding indentation, recursing over a list of elements (like matches). This PR deals with tying all this seperated printer logic together.

Unfortunately, the Hattier.Printer.Expression module has become somewhat large. This is unavoidable due to otherwise cyclic dependencies.

printNames :: [LIdP GhcPs] -> Hattier
printNames [] = pure ()
printNames [(L _ name)] = append $ pprText name
printNames [(L _ name)] = fallback name

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see Utils.hs for the definition of fallback

Comment on lines -30 to -48
case style of
PrimaryAlignment -> do
let clausePatterns = [pats | L _ Match {m_pats = pats} <- matches]
maxWidths = map (maximum . map patWidth) (transpose clausePatterns)
-- splitting up these cases enables us to only put
-- newlines in between declarations and not after the
-- final one.
case matches of
[] -> pure ()
(x : xs) -> do
printClause fname maxWidths x
mapM_ (\clause -> newline >> printClause fname maxWidths clause) xs
NoAlignment -> do
-- See the comment at the above case expression
case matches of
[] -> pure ()
(x : xs) -> do
append $ pprText x
mapM_ (\match -> newline >> (append $ pprText match)) xs

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part contains two patterns we often use:

  1. Checking the alignment style, and doing almost exactly the same with either choice except for some alignment.
  2. Intercalating some monadic actions between a list of things to print (like mapM_ (\clause -> newline >> printClause fname maxWidths clause) xs).

The first pattern is abstracted with computeAlignment and computeColumnAlignments, see their definitions inside Utils.hs

The second pattern is abstracted with withSep, with its definition inside Combinators.hs

append $ pprText pat
append $ T.replicate (maxWidth - patWidth pat) " "
printPatsWithPadding rest
printPats (zip pats maxWidths)

@vicgeentor vicgeentor Apr 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

printPats is now also a function we all use instead of seperate implementations. It has moved to Pattern.hs

printExpr :: HsExpr GhcPs -> Hattier
printExpr (HsCase _ scrut mg) = printCaseExpr scrut mg
printExpr expr = append (pprText expr)
printExpr (HsLet _ binds body) = printLetExpr binds body

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

printing let-expressions has moved to here.

withSep newline $ map (printAlt (indToText indW) maxWidth) matches

-- With NoAlignment, maxWidth should be 0
printAlt :: Text -> Int -> LMatch GhcPs (LHsExpr GhcPs) -> Hattier

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PrintAlt is basically the merged version of PrintAltAlign and PrintAltNoAlign. When not aligning, maxWidth is simply 0.

Comment on lines +60 to +104
printLetExpr :: HsLocalBinds GhcPs -> LHsExpr GhcPs -> Hattier
printLetExpr (HsValBinds _ (ValBinds _ binds _sigs)) body = do
style <- asks (letAlignment . cfg)
indW <- asks (fromIntegral . indentWidth . cfg)
let bindList = bagToList binds
bindInd = indToText (indW + 4)
alignCol = computeAlignment style (map nameLen bindList)
append "let "
printBinds bindInd alignCol bindList
newline >> append (indToText indW) >> append "in " >> printExpr (unLoc body)
printLetExpr localBinds body = do
-- TODO: HsIPBinds and EmptyLocalBinds cases
indW <- asks (fromIntegral . indentWidth . cfg)
fallback localBinds
newline >> append (indToText indW) >> append "in " >> printExpr (unLoc body)

-- | Print a list of bindings multi-line, using @alignCol@ for padding.
-- Pass @alignCol = 0@ for no alignment.
--
-- let x = 1 -- PrimaryAlignment (alignCol = 8)
-- longName = 2
--
-- let x = 1 -- NoAlignment (alignCol = 0)
-- longName = 2
printBinds :: Text -> Int -> [LHsBind GhcPs] -> Hattier
printBinds ind alignCol binds =
withSep (newline >> append ind) $ map (printBind ind alignCol) binds

-- | Print a single binding, padding the name to @alignCol@ columns.
-- When @alignCol = 0@ no padding is added.
printBind :: Text -> Int -> LHsBind GhcPs -> Hattier
printBind bindInd alignCol (L _ (FunBind _ lname mg)) = do
let name = pprText $ unLoc lname
append $ padTo alignCol name
case unLoc (mg_alts mg) of
[L _ Match {m_pats = pats, m_grhss = grhss}] -> do
printPats (zip pats (repeat 0))
printRHS bindInd grhss
-- Empty or multi-clause let bindings are not valid Haskell and
-- cannot be produced by GHC's parser, so these branches are unreachable.
-- still, we need to handle them to satisfy the type checker.
_ -> pure ()
printBind _ _ bind =
-- TODO: PatBind and other binding forms
fallback (unLoc bind)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is basically unchanged. It simply uses the new abstractions, making it more compact. It also uses the same logic for printing patterns as the rest of the code

@LVries LVries left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First look, will look more once I am home

Comment thread test/Integration/Let.hs
Comment on lines -18 to -25
-- funcAlignment drives which code path handles the function body.
-- PrimaryAlignment uses printClause, which calls printExpr on the body.
-- printExpr does not handle HsLet, so it falls through to pprText, placing
-- 'in' at column 0 - invalid Haskell. NoAlignment uses pprText for the whole
-- match clause, which always produces valid output.
-- letAlignment does not affect this routing, so only funcAlignment matters.
[ testCase "#37: funcAlignment=PrimaryAlignment (default) - 'in' incorrectly at col 0" letInFunctionBodyPrimary,
testCase "#37: funcAlignment=NoAlignment - valid output via pprText fallback" letInFunctionBodyNoAlign

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough

Comment thread test/Integration/Let.hs
Comment on lines 78 to -83
"greet 1 = \" INFOAFP\"",
"greet _",
" = let",
" hello = greet 0",
"greet _ =",
" let hello = greet 0",
" course = greet 1",
" in hello ++ course"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change in expected outcome desired?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Since only funcAlignment is disabled here. Not letAlignment

Comment on lines +11 to +16
printModule :: Hattier
printModule = do
printModHeader
printModImports
printModDecls

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3

Comment on lines +22 to +28
computeAlignment :: Alignment -> [Int] -> Int
computeAlignment PrimaryAlignment xs = maximum xs
computeAlignment NoAlignment _ = 0

computeColumnAlignments :: Alignment -> [[Int]] -> [Int]
computeColumnAlignments PrimaryAlignment cols = map maximum cols
computeColumnAlignments NoAlignment cols = map (const 0) cols

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet

printBind :: Text -> Int -> LHsBind GhcPs -> Hattier
printBind bindInd alignCol (L _ (FunBind _ lname mg)) = do
let name = pprText $ unLoc lname
append $ padTo alignCol name

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

padTo is a new abstraction in Utils.hs. Take a look at its definition to understand it. If it doesn't feel logical, please tell me so we'll think of a better abstraction.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I correct in assuming padTo 5 "y" ->yxxxx (where is a space for clarity) so it adds (y is length 1) 5-1 spaces?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's exactly correct. This lets you do stuff like append $ padTo maxWidth patTxt so that your next patTxt will start on the right column.

Also, running your example in ghci gives exactly what you assumed:

ghci> padTo 5 "y"
"y    "

nameLen _ = 0

-- | Print the body of a @let@ expression, recursing into nested @let@s.
printLetBody :: HsExpr GhcPs -> Hattier

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now the same as printExpr now that printLetExpr has moved into that. This function is thus entirely removed.

indW <- asks (fromIntegral . indentWidth . cfg)
let bindList = bagToList binds
bindInd = indToText (indW + 4)
alignCol = computeAlignment style (map nameLen bindList)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like I said earlier, the style seperation between NoAlignment and PrimaryAlignment has moved to computeAlignment. Makes for more simple implementations

GRHSs _ [L _ (GRHS _ [] body)] _ ->
printExpr (unLoc body)
GRHSs _ xs _ ->
printGRHS ind xs

@vicgeentor vicgeentor Apr 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Printing a guarded RHS has moved to printGRHS at the bottom of this file. I actually wanted to abstract this entire case expression to printRHS (also at the bottom of this file) but haven't figured that out yet.

printRHS also calls printGRHS for a guarded expression. Outside of that, this is currently the only place that calls printGRHS. We should figure out how to defer this to printRHS for maintainability.

Comment on lines +84 to +86
printBinds :: Text -> Int -> [LHsBind GhcPs] -> Hattier
printBinds ind alignCol binds =
withSep (newline >> append ind) $ map (printBind ind alignCol) binds

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic unchanged, but again uses the new withSep abstraction.

-- TODO: PatBind and other binding forms
fallback (unLoc bind)

-- * RHS

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New section purely dedicated to printing RHS's

Comment on lines +14 to +17
printPats :: [(LPat GhcPs, Int)] -> Hattier
printPats = mapM_ $ \(pat, maxWidth) -> do
let patTxt = pprText pat
append " " >> append (padTo maxWidth patTxt)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this used to be printPatsWithPadding. With NoAlignment all the second tuple elements should be 0.

Comment on lines +30 to +65
patWidth :: LPat GhcPs -> Int
patWidth (L _ pat) =
case pat of
-- In most cases where a specific integer is added to the width, the
-- comment after the case shows what that integer represents
WildPat _ -> 1
VarPat _ (L _ name) -> T.length (pprText name)
LazyPat _ pat' -> 1 + patWidth pat' -- "~"
AsPat _ (L _ name) pat' -> T.length (pprText name) + 1 + patWidth pat' -- "@"
ParPat _ pat' -> 2 + patWidth pat' -- "(" + ")"
BangPat _ pat' -> 1 + patWidth pat' -- "!"
ListPat _ pats -> 2 + sum (map patWidth pats) + max 0 (length pats - 1) * 2 -- "[" + "]" + commas: ", "
TuplePat _ pats Boxed ->
2 + sum (map patWidth pats) + max 0 (length pats - 1) * 2 -- "(" + ")" + commas: ", "
TuplePat _ pats Unboxed ->
4 + sum (map patWidth pats) + max 0 (length pats - 1) * 2 -- "(#" + "#)" + commas: ", "
ConPat _ (L _ name) (InfixCon l r) ->
patWidth l + 1 + T.length (pprText name) + 1 + patWidth r -- " con "
ConPat _ (L _ name) (PrefixCon _ args) ->
T.length (pprText name) + sum (map (\a -> 1 + patWidth a) args)
ConPat _ (L _ name) (RecCon _) -> T.length $ pprText name -- TODO: records
LitPat _ lit -> T.length $ pprText lit
NPat _ (L _ lit) (Just _) _ -> 1 + (T.length $ pprText lit) -- "-"
NPat _ (L _ lit) Nothing _ -> T.length $ pprText lit
SigPat _ pat' sig -> 2 + patWidth pat' + 4 + (T.length $ pprText sig) -- "(" + " :: " + ")"
-- TODO: the following require certain language extensions
SumPat _ _ _ _ -> undefined -- UnboxedSums extension
ViewPat _ _ _ -> undefined -- ViewPatterns extension
SplicePat _ _ -> undefined -- TemplateHaskell extension
NPlusKPat _ _ _ _ _ _ -> undefined -- NPlusKPatterns extension
EmbTyPat _ _ -> undefined -- ExplicitNamespaces and RequiredTypeArguments extensions
InvisPat _ _ -> undefined -- TypeAbstractions extension

matchWidth :: LMatch GhcPs (LHsExpr GhcPs) -> Int
matchWidth (L _ Match {m_pats = [pat]}) = patWidth pat
matchWidth _ = 0

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchanged code that moved

Comment on lines +12 to +13
pprText :: (Outputable a) => a -> Text
pprText = T.pack . showSDocUnsafe . ppr

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchanged code that moved

Comment on lines +67 to +69
nameLen :: LHsBind GhcPs -> Int
nameLen (L _ (FunBind _ lname _)) = T.length (pprText $ unLoc lname)
nameLen _ = 0

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New function currently only used inside printLetExpr

Comment thread test/Integration/Let.hs
Comment on lines 78 to -83
"greet 1 = \" INFOAFP\"",
"greet _",
" = let",
" hello = greet 0",
"greet _ =",
" let hello = greet 0",
" course = greet 1",
" in hello ++ course"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Since only funcAlignment is disabled here. Not letAlignment

@@ -0,0 +1,150 @@
-- Some disabled warnings to delete after recursive printing with indentation is implemented

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See this comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I correct in seeing this test as a check for recursive indentation printing?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right. It is basically a test for nested let AND case expressions, that's why I called it an integration test

Comment thread test/Unit/Format/Let.hs
nestedNoAlignmentTest = expected @=? runLetPrinter NoAlignment nestedLetSrc
where
expected = "let x = 1\n longName = 2\n in let result = x\n in result"
expected = "let x = 1\n longName = 2\n in let result = x\n in result"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even when not aligning equals signs, I think the in should still always have an extra space. If you don't agree, I'll remove it

@nomadalgia nomadalgia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mostly did a proper check that the changes are sound for Expression.hs, since I was most familiar with it.
For the rest, it seems like it is mostly moving code around.
Very good! Highly needed change.
It merge new functionality with refactoring, but that's fine since

  1. they are both correct so its not blocking
  2. we need this merge now now now now
    Once merged, I will adapt my case-cons-align branch appropriately as mentioned there.

@vicgeentor
vicgeentor merged commit ac9b90f into main Apr 9, 2026
2 checks passed
@vicgeentor
vicgeentor deleted the make-printing-recursive branch April 9, 2026 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ticket (core/refactor): tying up the printers to allow for recursive printing

3 participants