fourmolu-0.0.6.0: src/Ormolu/Printer/Operators.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | This module helps handle operator chains composed of different
-- operators that may have different precedence and fixities.
module Ormolu.Printer.Operators
( OpTree (..),
opTreeLoc,
reassociateOpTree,
)
where
import Data.Function (on)
import qualified Data.List as L
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Ord (Down (Down), comparing)
import GHC
import OccName (mkVarOcc)
import Ormolu.Utils (unSrcSpan)
-- | Intermediate representation of operator trees. It has two type
-- parameters: @ty@ is the type of sub-expressions, while @op@ is the type
-- of operators.
data OpTree ty op
= OpNode ty
| OpBranch
(OpTree ty op)
op
(OpTree ty op)
-- | Return combined 'SrcSpan's of all elements in this 'OpTree'.
opTreeLoc :: OpTree (Located a) b -> SrcSpan
opTreeLoc (OpNode (L l _)) = l
opTreeLoc (OpBranch l _ r) = combineSrcSpans (opTreeLoc l) (opTreeLoc r)
-- | Re-associate an 'OpTree' taking into account automagically inferred
-- relative precedence of operators. Users are expected to first construct
-- an initial 'OpTree', then re-associate it using this function before
-- printing.
reassociateOpTree ::
-- | How to get name of an operator
(op -> Maybe RdrName) ->
-- | Original 'OpTree'
OpTree (Located ty) (Located op) ->
-- | Re-associated 'OpTree'
OpTree (Located ty) (Located op)
reassociateOpTree getOpName opTree =
reassociateOpTreeWith
(buildFixityMap getOpName normOpTree)
(getOpName . unLoc)
normOpTree
where
normOpTree = normalizeOpTree opTree
-- | Re-associate an 'OpTree' given the map with operator fixities.
reassociateOpTreeWith ::
forall ty op.
-- | Fixity map for operators
[(RdrName, Fixity)] ->
-- | How to get the name of an operator
(op -> Maybe RdrName) ->
-- | Original 'OpTree'
OpTree ty op ->
-- | Re-associated 'OpTree'
OpTree ty op
reassociateOpTreeWith fixityMap getOpName = go
where
fixityOf :: op -> Fixity
fixityOf op = fromMaybe defaultFixity $ do
opName <- getOpName op
lookup opName fixityMap
-- Here, left branch is already associated and the root alongside with
-- the right branch is right-associated. This function picks up one item
-- from the right and inserts it correctly to the left.
--
-- Also, we are using the 'compareFixity' function which returns if the
-- expression should associate to right.
go :: OpTree ty op -> OpTree ty op
-- base cases
go t@(OpNode _) = t
go t@(OpBranch (OpNode _) _ (OpNode _)) = t
-- shift one operator to the left at the beginning
go (OpBranch l@(OpNode _) op (OpBranch l' op' r')) =
go (OpBranch (OpBranch l op l') op' r')
-- at the last operator, place the operator and don't recurse
go (OpBranch (OpBranch l op r) op' r'@(OpNode _)) =
if snd $ compareFixity (fixityOf op) (fixityOf op')
then OpBranch l op (go $ OpBranch r op' r')
else OpBranch (OpBranch l op r) op' r'
-- else, shift one operator to left and recurse.
go (OpBranch (OpBranch l op r) op' (OpBranch l' op'' r')) =
if snd $ compareFixity (fixityOf op) (fixityOf op')
then go $ OpBranch (OpBranch l op (go $ OpBranch r op' l')) op'' r'
else go $ OpBranch (OpBranch (OpBranch l op r) op' l') op'' r'
-- | Build a map of inferred 'Fixity's from an 'OpTree'.
buildFixityMap ::
forall ty op.
-- | How to get the name of an operator
(op -> Maybe RdrName) ->
-- | Operator tree
OpTree (Located ty) (Located op) ->
-- | Fixity map
[(RdrName, Fixity)]
buildFixityMap getOpName opTree =
concatMap (\(i, ns) -> map (\(n, _) -> (n, fixity i InfixL)) ns)
. zip [0 ..]
. L.groupBy (doubleWithinEps 0.00001 `on` snd)
. (overrides ++)
. modeScores
$ score opTree
where
-- Add a special case for ($), since it is pretty unlikely for someone
-- to override it.
overrides :: [(RdrName, Double)]
overrides =
[ (mkRdrUnqual $ mkVarOcc "$", -1)
]
-- Assign scores to operators based on their location in the source.
score :: OpTree (Located ty) (Located op) -> [(RdrName, Double)]
score (OpNode _) = []
score (OpBranch l o r) = fromMaybe (score r) $ do
-- If we fail to get any of these, 'defaultFixity' will be used by
-- 'reassociateOpTreeWith'.
le <- srcSpanEndLine <$> unSrcSpan (opTreeLoc l) -- left end
ob <- srcSpanStartLine <$> unSrcSpan (getLoc o) -- operator begin
oe <- srcSpanEndLine <$> unSrcSpan (getLoc o) -- operator end
rb <- srcSpanStartLine <$> unSrcSpan (opTreeLoc r) -- right begin
oc <- srcSpanStartCol <$> unSrcSpan (getLoc o) -- operator column
opName <- getOpName (unLoc o)
let s
| le < ob =
-- if the operator is in the beginning of a line, assign
-- a score relative to its column within range [0, 1).
fromIntegral oc / fromIntegral (maxCol + 1)
| oe < rb =
-- if the operator is in the end of the line, assign the
-- score 1.
1
| otherwise =
2 -- otherwise, assign a high score.
return $ (opName, s) : score r
-- Pick the most common score per 'RdrName'.
modeScores :: [(RdrName, Double)] -> [(RdrName, Double)]
modeScores =
L.sortOn snd
. mapMaybe
( \case
[] -> Nothing
xs@((n, _) : _) -> Just (n, mode $ map snd xs)
)
. L.groupBy ((==) `on` fst)
. L.sort
-- Return the most common number, leaning to the smaller
-- one in case of a tie.
mode :: [Double] -> Double
mode =
head
. L.minimumBy (comparing (Down . length))
. L.groupBy (doubleWithinEps 0.0001)
. L.sort
-- The start column of the rightmost operator.
maxCol = go opTree
where
go (OpNode (L _ _)) = 0
go (OpBranch l (L o _) r) =
maximum
[ go l,
maybe 0 srcSpanStartCol (unSrcSpan o),
go r
]
----------------------------------------------------------------------------
-- Helpers
-- | Convert an 'OpTree' to with all operators having the same fixity and
-- associativity (left infix).
normalizeOpTree :: OpTree ty op -> OpTree ty op
normalizeOpTree (OpNode n) =
OpNode n
normalizeOpTree (OpBranch (OpNode l) lop r) =
OpBranch (OpNode l) lop (normalizeOpTree r)
normalizeOpTree (OpBranch (OpBranch l' lop' r') lop r) =
normalizeOpTree (OpBranch l' lop' (OpBranch r' lop r))
fixity :: Int -> FixityDirection -> Fixity
fixity = Fixity NoSourceText
doubleWithinEps :: Double -> Double -> Double -> Bool
doubleWithinEps eps a b = abs (a - b) < eps