Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add real error messages for associativity errors #3315

Merged
merged 4 commits into from Apr 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/failing/MixedAssociativityError.purs
@@ -0,0 +1,6 @@
-- @shouldFailWith MixedAssociativityError
module Main where

import Prelude

feq f x y = f <$> x == f <$> y
Copy link
Member Author

Choose a reason for hiding this comment

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

This case was a little surprising, but GHC has an error for it too:

Prelude> let f x y = f <$> x == f <$> y

<interactive>:10:13:
    Precedence parsing error
        cannot mix ‘<$>’ [infixl 4] and ‘==’ [infix 4] in the same infix expression

<interactive>:10:13:
    Precedence parsing error
        cannot mix ‘==’ [infix 4] and ‘<$>’ [infixl 4] in the same infix expression

8 changes: 8 additions & 0 deletions examples/failing/NonAssociativeError.purs
@@ -0,0 +1,8 @@
-- @shouldFailWith NonAssociativeError
-- @shouldFailWith NonAssociativeError
module Main where

import Prelude

a = true == true == true
Copy link
Member Author

Choose a reason for hiding this comment

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

GHC again:

Prelude> True == True == True

<interactive>:11:1:
    Precedence parsing error
        cannot mix ‘==’ [infix 4] and ‘==’ [infix 4] in the same infix expression

b = true == false /= true
2 changes: 2 additions & 0 deletions src/Language/PureScript/AST/Declarations.hs
Expand Up @@ -174,6 +174,8 @@ data SimpleErrorMessage
-- | a declaration couldn't be used because it contained free variables
| UnusableDeclaration Ident [[Text]]
| CannotDefinePrimModules ModuleName
| MixedAssociativityError (NEL.NonEmpty (Qualified (OpName 'AnyOpName), Associativity))
| NonAssociativeError (NEL.NonEmpty (Qualified (OpName 'AnyOpName)))
deriving (Show)

-- | Error message hints, providing more detailed information about failure.
Expand Down
23 changes: 23 additions & 0 deletions src/Language/PureScript/Errors.hs
Expand Up @@ -180,6 +180,8 @@ errorCode em = case unwrapErrorMessage em of
UserDefinedWarning{} -> "UserDefinedWarning"
UnusableDeclaration{} -> "UnusableDeclaration"
CannotDefinePrimModules{} -> "CannotDefinePrimModules"
MixedAssociativityError{} -> "MixedAssociativityError"
NonAssociativeError{} -> "NonAssociativeError"

-- | A stack trace for an error
newtype MultipleErrors = MultipleErrors
Expand Down Expand Up @@ -997,6 +999,27 @@ prettyPrintSingleError (PPEOptions codeColor full level showDocs relPath) e = fl
, line $ "The Prim namespace is reserved for compiler-defined terms."
]

renderSimpleErrorMessage (MixedAssociativityError opsWithAssoc) =
paras
[ line "Cannot parse an expression that uses operators of the same precedence but mixed associativity:"
, indent $ paras $ map (\(name, assoc) -> line $ markCode (showQualified showOp name) <> " is " <> markCode (T.pack (showAssoc assoc))) (NEL.toList opsWithAssoc)
, line "Use parentheses to resolve this ambiguity."
]

renderSimpleErrorMessage (NonAssociativeError ops) =
if NEL.length ops == 1
then
paras
[ line $ "Cannot parse an expression that uses multiple instances of the non-associative operator " <> markCode (showQualified showOp (NEL.head ops)) <> "."
, line "Use parentheses to resolve this ambiguity."
]
else
paras
[ line "Cannot parse an expression that uses multiple non-associative operators of the same precedence:"
, indent $ paras $ map (line . markCode . showQualified showOp) (NEL.toList ops)
, line "Use parentheses to resolve this ambiguity."
]

renderHint :: ErrorMessageHint -> Box.Box -> Box.Box
renderHint (ErrorUnifyingTypes t1 t2) detail =
paras [ detail
Expand Down
5 changes: 4 additions & 1 deletion src/Language/PureScript/Names.hs
Expand Up @@ -121,7 +121,10 @@ showOp op = "(" <> runOpName op <> ")"
-- |
-- The closed set of operator alias types.
--
data OpNameType = ValueOpName | TypeOpName
data OpNameType = ValueOpName | TypeOpName | AnyOpName

eraseOpName :: OpName a -> OpName 'AnyOpName
eraseOpName = OpName . runOpName

-- |
-- Proper names, i.e. capitalized names for e.g. module names, type//data constructors.
Expand Down
12 changes: 3 additions & 9 deletions src/Language/PureScript/Sugar/Operators.hs
Expand Up @@ -197,19 +197,13 @@ rebracketModule pred_ valueOpTable typeOpTable (Module ss coms mn ds exts) =
(f, _, _) =
everywhereOnValuesTopDownM
goDecl
(goExpr <=< decontextify goExpr')
(goBinder <=< decontextify goBinder')
(matchExprOperators valueOpTable <=< decontextify goExpr')
(matchBinderOperators valueOpTable <=< decontextify goBinder')

(goDecl, goExpr', goBinder') = updateTypes (const goType)

goExpr :: Expr -> m Expr
goExpr = return . matchExprOperators valueOpTable

goBinder :: Binder -> m Binder
goBinder = return . matchBinderOperators valueOpTable

goType :: Type -> m Type
goType = return . matchTypeOperators typeOpTable
goType = matchTypeOperators typeOpTable

decontextify :: (Maybe SourceSpan -> a -> m (Maybe SourceSpan, a)) -> a -> m a
decontextify ctxf = fmap snd . ctxf Nothing
Expand Down
9 changes: 8 additions & 1 deletion src/Language/PureScript/Sugar/Operators/Binders.hs
Expand Up @@ -2,11 +2,18 @@ module Language.PureScript.Sugar.Operators.Binders where

import Prelude.Compat

import Control.Monad.Except

import Language.PureScript.AST
import Language.PureScript.Errors
import Language.PureScript.Names
import Language.PureScript.Sugar.Operators.Common

matchBinderOperators :: [[(Qualified (OpName 'ValueOpName), Associativity)]] -> Binder -> Binder
matchBinderOperators
:: MonadError MultipleErrors m
=> [[(Qualified (OpName 'ValueOpName), Associativity)]]
-> Binder
-> m Binder
matchBinderOperators = matchOperators isBinOp extractOp fromOp reapply id
where

Expand Down
82 changes: 72 additions & 10 deletions src/Language/PureScript/Sugar/Operators/Common.hs
Expand Up @@ -3,15 +3,22 @@ module Language.PureScript.Sugar.Operators.Common where
import Prelude.Compat

import Control.Monad.State
import Control.Monad.Except

import Data.Either (rights)
import Data.Functor.Identity
import Data.List (sortOn)
import Data.Maybe (mapMaybe, fromJust)
import qualified Data.List.NonEmpty as NEL
import qualified Data.Map as M

import qualified Text.Parsec as P
import qualified Text.Parsec.Pos as P
import qualified Text.Parsec.Expr as P

import Language.PureScript.AST
import Language.PureScript.Crash
import Language.PureScript.Errors
import Language.PureScript.Names

type Chain a = [Either a a]
Expand Down Expand Up @@ -53,30 +60,85 @@ 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 a nameType
:: 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
-> a
-> m a
matchOperators isBinOp extractOp fromOp reapply modOpTable ops = parseChains
where
parseChains :: a -> a
parseChains :: a -> m a
parseChains ty
| True <- isBinOp ty = bracketChain (extendChain ty)
| otherwise = 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 -> a
bracketChain =
either
(\_ -> internalError "matchTypeOperators: cannot reorder operators")
id
. P.parse opParser "operator expression"
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 occurance 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 = fromJust . fmap fst . flip M.lookup opInfo
opAssoc :: Qualified (OpName nameType) -> Associativity
opAssoc = fromJust . fmap snd . 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 = join $ fmap (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 (join . fmap (fromJust . flip M.lookup chainOpSpans) $ grp)]
8 changes: 7 additions & 1 deletion src/Language/PureScript/Sugar/Operators/Expr.hs
Expand Up @@ -2,6 +2,7 @@ module Language.PureScript.Sugar.Operators.Expr where

import Prelude.Compat

import Control.Monad.Except
import Data.Functor.Identity

import qualified Text.Parsec as P
Expand All @@ -10,8 +11,13 @@ import qualified Text.Parsec.Expr as P
import Language.PureScript.AST
import Language.PureScript.Names
import Language.PureScript.Sugar.Operators.Common
import Language.PureScript.Errors

matchExprOperators :: [[(Qualified (OpName 'ValueOpName), Associativity)]] -> Expr -> Expr
matchExprOperators
:: MonadError MultipleErrors m
=> [[(Qualified (OpName 'ValueOpName), Associativity)]]
-> Expr
-> m Expr
matchExprOperators = matchOperators isBinOp extractOp fromOp reapply modOpTable
where

Expand Down
8 changes: 7 additions & 1 deletion src/Language/PureScript/Sugar/Operators/Types.hs
Expand Up @@ -2,13 +2,19 @@ module Language.PureScript.Sugar.Operators.Types where

import Prelude.Compat

import Control.Monad.Except
import Language.PureScript.AST
import Language.PureScript.Crash
import Language.PureScript.Errors
import Language.PureScript.Names
import Language.PureScript.Sugar.Operators.Common
import Language.PureScript.Types

matchTypeOperators :: [[(Qualified (OpName 'TypeOpName), Associativity)]] -> Type -> Type
matchTypeOperators
:: MonadError MultipleErrors m
=> [[(Qualified (OpName 'TypeOpName), Associativity)]]
-> Type
-> m Type
matchTypeOperators = matchOperators isBinOp extractOp fromOp reapply id
where

Expand Down