tilia-0.0.1.0: src/Tilia/Render/Operator.hs
{-# LANGUAGE LambdaCase #-}
-- | Regrouping a chain of infix operators by precedence.
module Tilia.Render.Operator
( -- * Chains
OpChain (..),
flatten,
flattenAround,
associate,
-- * Asking about a chain
chainSpan,
lastOperand,
isSeparator,
)
where
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Maybe (isNothing, mapMaybe)
import Tilia.Fixity (Direction (..), Fixity (..))
import Tilia.Span
-- | A chain of operator applications.
--
-- A branch holds @n + 1@ operands and @n@ operators, all of which bind
-- equally tightly. This is the shape layout wants: the operators of one
-- level are siblings, so the printer can decide once how that level breaks
-- rather than rediscovering it at every binary node.
data OpChain a op
= Operand a
| Chain (NonEmpty (OpChain a op)) [op]
deriving (Eq, Show)
-- | Take a binary application tree apart into one flat run.
--
-- The decomposition function returns the two operands and the operator of a
-- node that is an application of an infix operator, and nothing for a node
-- that is a leaf.
flatten ::
-- | Take one node apart, if it comes apart
(a -> Maybe (a, op, a)) ->
a ->
(NonEmpty a, [op])
flatten split = go
where
go x = case split x of
Nothing -> (x :| [], [])
Just (l, op, r) ->
let (ls, lops) = go l
(rs, rops) = go r
in (ls <> rs, lops <> [op] <> rops)
-- | 'flatten' for a node the caller has already taken apart.
--
-- The printers match on the operator application in order to reach its
-- parts, so by the time a chain is being built the outermost node has
-- already been destructured and there is nothing left to hand to 'flatten'.
flattenAround ::
(a -> Maybe (a, op, a)) ->
a ->
op ->
a ->
(NonEmpty a, [op])
flattenAround split l op r =
let (ls, lops) = flatten split l
(rs, rops) = flatten split r
in (ls <> rs, lops <> (op : rops))
-- | Regroup a flat run by precedence.
--
-- The loosest-binding operators of the run become the operators of the top
-- branch, and everything between two of them becomes a subtree, regrouped
-- the same way. When any operator in the run has no known precedence the run
-- is left as one flat branch: nothing is asserted about how it associates,
-- so nothing is rearranged.
associate ::
-- | The fixity of an operator, if it was established
(op -> Maybe Fixity) ->
NonEmpty a ->
[op] ->
OpChain a op
associate fixityOf = build
where
build (x :| []) _ = Operand x
build operands ops
| any (isNothing . precedenceOf) ops = flatBranch operands ops
| otherwise =
case splitOn ((== Just loosest) . precedenceOf) operands ops of
(groups, splitters) -> Chain (fmap (uncurry build) groups) splitters
where
loosest = minimum (mapMaybe precedenceOf ops)
flatBranch operands ops = Chain (Operand <$> operands) ops
precedenceOf = fmap fixityPrecedence . fixityOf
-- | Cut a run wherever the operator satisfies the predicate.
splitOn ::
(op -> Bool) ->
NonEmpty a ->
[op] ->
(NonEmpty (NonEmpty a, [op]), [op])
splitOn cuts (x0 :| xs) ops = go (x0 :| []) [] (zip ops xs)
where
go current currentOps [] = ((NE.reverse current, reverse currentOps) :| [], [])
go current currentOps ((op, y) : rest)
| cuts op =
let (groups, splitters) = go (y :| []) [] rest
in (NE.cons (NE.reverse current, reverse currentOps) groups, op : splitters)
| otherwise = go (NE.cons y current) (op : currentOps) rest
----------------------------------------------------------------------------
-- Asking about a chain
-- | The region of the input a chain came from.
chainSpan :: (a -> Maybe Span) -> OpChain a op -> Maybe Span
chainSpan spanOfOperand = \case
Operand x -> spanOfOperand x
Chain xs _ -> foldr1 join' (chainSpan spanOfOperand <$> xs)
where
join' (Just a) (Just b) = Just (a <> b)
join' a b = maybe b Just a
-- | The rightmost operand of a chain.
lastOperand :: OpChain a op -> a
lastOperand = \case
Operand x -> x
Chain xs _ -> lastOperand (NE.last xs)
-- | Is this operator one of the ones that exist to separate rather than to
-- combine?
isSeparator :: Maybe Fixity -> Bool
isSeparator = \case
Just (Fixity RightAssoc 0) -> True
_ -> False