purescript-0.15.15: src/Language/PureScript/Sugar/Operators/Common.hs
module Language.PureScript.Sugar.Operators.Common where
import Prelude
import Control.Monad.State (guard, join)
import Control.Monad.Except (MonadError(..))
import Data.Either (rights)
import Data.Functor.Identity (Identity)
import Data.List (sortOn)
import Data.Maybe (mapMaybe, fromJust)
import Data.List.NonEmpty qualified as NEL
import Data.Map qualified as M
import Text.Parsec qualified as P
import Text.Parsec.Pos qualified as P
import Text.Parsec.Expr qualified as P
import Language.PureScript.AST (Associativity(..), ErrorMessageHint(..), SourceSpan)
import Language.PureScript.Crash (internalError)
import Language.PureScript.Errors (ErrorMessage(..), MultipleErrors(..), SimpleErrorMessage(..))
import Language.PureScript.Names (OpName, Qualified, eraseOpName)
type Chain a = [Either a a]
type FromOp nameType a = a -> Maybe (SourceSpan, Qualified (OpName nameType))
type Reapply nameType a = SourceSpan -> Qualified (OpName nameType) -> a -> a -> a
toAssoc :: Associativity -> P.Assoc
toAssoc Infixl = P.AssocLeft
toAssoc Infixr = P.AssocRight
toAssoc Infix = P.AssocNone
token :: (P.Stream s Identity t) => (t -> Maybe a) -> P.Parsec s u a
token = P.token (const "") (const (P.initialPos ""))
parseValue :: P.Parsec (Chain a) () a
parseValue = token (either Just (const Nothing)) P.<?> "expression"
parseOp
:: FromOp nameType a
-> P.Parsec (Chain a) () (SourceSpan, Qualified (OpName nameType))
parseOp fromOp = token (either (const Nothing) fromOp) P.<?> "operator"
matchOp
:: FromOp nameType a
-> Qualified (OpName nameType)
-> P.Parsec (Chain a) () SourceSpan
matchOp fromOp op = do
(ss, ident) <- parseOp fromOp
guard $ ident == op
pure ss
opTable
:: [[(Qualified (OpName nameType), Associativity)]]
-> FromOp nameType a
-> Reapply nameType a
-> [[P.Operator (Chain a) () Identity a]]
opTable ops fromOp reapply =
map (map (\(name, a) -> P.Infix (P.try (matchOp fromOp name) >>= \ss -> return (reapply ss name)) (toAssoc a))) ops
matchOperators
:: forall m a nameType
. Show a
=> MonadError MultipleErrors m
=> (a -> Bool)
-> (a -> Maybe (a, a, a))
-> FromOp nameType a
-> Reapply nameType a
-> ([[P.Operator (Chain a) () Identity a]] -> P.OperatorTable (Chain a) () Identity a)
-> [[(Qualified (OpName nameType), Associativity)]]
-> a
-> m a
matchOperators isBinOp extractOp fromOp reapply modOpTable ops = parseChains
where
parseChains :: a -> m a
parseChains ty
| True <- isBinOp ty = bracketChain (extendChain ty)
| otherwise = pure ty
extendChain :: a -> Chain a
extendChain ty
| Just (op, l, r) <- extractOp ty = Left l : Right op : extendChain r
| otherwise = [Left ty]
bracketChain :: Chain a -> m a
bracketChain chain =
case P.parse opParser "operator expression" chain of
Right a -> pure a
Left _ -> throwError . MultipleErrors $ mkErrors chain
opParser :: P.Parsec (Chain a) () a
opParser = P.buildExpressionParser (modOpTable (opTable ops fromOp reapply)) parseValue <* P.eof
-- Generating a good error message involves a bit of work here, as the parser
-- can't provide one for us.
--
-- We examine the expression chain, plucking out the operators and then
-- grouping them by shared precedence, then if any of the following conditions
-- are met, we have something to report:
-- 1. any of the groups have mixed associativity
-- 2. there is more than one occurrence of a non-associative operator in a
-- precedence group
mkErrors :: Chain a -> [ErrorMessage]
mkErrors chain =
let
opInfo :: M.Map (Qualified (OpName nameType)) (Integer, Associativity)
opInfo = M.fromList $ concatMap (\(n, o) -> map (\(name, assoc) -> (name, (n, assoc))) o) (zip [0..] ops)
opPrec :: Qualified (OpName nameType) -> Integer
opPrec = fst . fromJust . flip M.lookup opInfo
opAssoc :: Qualified (OpName nameType) -> Associativity
opAssoc = snd . fromJust . flip M.lookup opInfo
chainOpSpans :: M.Map (Qualified (OpName nameType)) (NEL.NonEmpty SourceSpan)
chainOpSpans = foldr (\(ss, name) -> M.alter (Just . maybe (pure ss) (NEL.cons ss)) name) M.empty . mapMaybe fromOp $ rights chain
opUsages :: Qualified (OpName nameType) -> Int
opUsages = maybe 0 NEL.length . flip M.lookup chainOpSpans
precGrouped :: [NEL.NonEmpty (Qualified (OpName nameType))]
precGrouped = NEL.groupWith opPrec . sortOn opPrec $ M.keys chainOpSpans
assocGrouped :: [NEL.NonEmpty (NEL.NonEmpty (Qualified (OpName nameType)))]
assocGrouped = fmap (NEL.groupWith1 opAssoc . NEL.sortWith opAssoc) precGrouped
mixedAssoc :: [NEL.NonEmpty (Qualified (OpName nameType))]
mixedAssoc = fmap join . filter (\precGroup -> NEL.length precGroup > 1) $ assocGrouped
nonAssoc :: [NEL.NonEmpty (Qualified (OpName nameType))]
nonAssoc = NEL.filter (\assocGroup -> opAssoc (NEL.head assocGroup) == Infix && sum (fmap opUsages assocGroup) > 1) =<< assocGrouped
in
if null (nonAssoc ++ mixedAssoc)
then internalError "matchOperators: cannot reorder operators"
else
map
(\grp ->
mkPositionedError chainOpSpans grp
(MixedAssociativityError (fmap (\name -> (eraseOpName <$> name, opAssoc name)) grp)))
mixedAssoc
++ map
(\grp ->
mkPositionedError chainOpSpans grp
(NonAssociativeError (fmap (fmap eraseOpName) grp)))
nonAssoc
mkPositionedError
:: M.Map (Qualified (OpName nameType)) (NEL.NonEmpty SourceSpan)
-> NEL.NonEmpty (Qualified (OpName nameType))
-> SimpleErrorMessage
-> ErrorMessage
mkPositionedError chainOpSpans grp =
ErrorMessage
[PositionedError (fromJust . flip M.lookup chainOpSpans =<< grp)]