polysemy-plugin 0.2.2.0 → 0.2.3.0
raw patch · 11 files changed
+497/−237 lines, 11 filesdep ~polysemy
Dependency ranges changed: polysemy
Files
- ChangeLog.md +10/−1
- polysemy-plugin.cabal +7/−3
- src/Polysemy/Plugin/Fundep.hs +146/−219
- src/Polysemy/Plugin/Fundep/Stuff.hs +83/−0
- src/Polysemy/Plugin/Fundep/Unification.hs +136/−0
- src/Polysemy/Plugin/Fundep/Utils.hs +33/−0
- test/ExampleSpec.hs +11/−5
- test/LegitimateTypeErrorSpec.hs +4/−4
- test/MultipleVarsSpec.hs +62/−0
- test/PluginSpec.hs +4/−4
- test/TypeErrors.hs +1/−1
ChangeLog.md view
@@ -1,5 +1,14 @@ # Changelog for polysemy-plugin ++## 0.2.3.0 (2019-09-04)+- The plugin will now choose between given effects based on the ability to unify them.+ This makes it possible for disambiguation to kick in even when using multiple+ instances of the same effect with different type variables,+ as long as type annotations/applications are used to+ target a specific instance.+- Updated the test suite to test against `polysemy-1.2.0.0`.+ ## 0.2.2.0 (2019-07-04) - The plugin will now prevent some false-positives in `polysemy`'s error@@ -50,5 +59,5 @@ ## Unreleased changes -- Added `runErrorAsAnother`+- Added `mapError`
polysemy-plugin.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 63b03550f0fe1e13941505a11116fac8e6eef8a871dff51b30d2bca2263b2a75+-- hash: 01634ce3c7ac101e60c1a02f8ccad7ec499c02a04b66e5d9dd5993f314318097 name: polysemy-plugin-version: 0.2.2.0+version: 0.2.3.0 synopsis: Disambiguate obvious uses of effects. description: Please see the README on GitHub at <https://github.com/isovector/polysemy/tree/master/polysemy-plugin#readme> category: Polysemy@@ -31,6 +31,9 @@ exposed-modules: Polysemy.Plugin Polysemy.Plugin.Fundep+ Polysemy.Plugin.Fundep.Stuff+ Polysemy.Plugin.Fundep.Unification+ Polysemy.Plugin.Fundep.Utils Polysemy.Plugin.Phases other-modules: Paths_polysemy_plugin@@ -55,6 +58,7 @@ DoctestSpec ExampleSpec LegitimateTypeErrorSpec+ MultipleVarsSpec PluginSpec TypeErrors VDQSpec@@ -73,7 +77,7 @@ , ghc-tcplugins-extra >=0.3 && <0.4 , hspec >=2.6.0 && <3 , inspection-testing >=0.4.2 && <0.5- , polysemy >=0.6.0.0+ , polysemy >=1.2.0.0 , polysemy-plugin , should-not-typecheck >=2.1.0 && <3 , syb >=0.7 && <0.8
src/Polysemy/Plugin/Fundep.hs view
@@ -4,7 +4,7 @@ ------------------------------------------------------------------------------ -- The MIT License (MIT) ----- Copyright (c) 2017 Luka Horvat+-- Copyright (c) 2017 Luka Horvat, 2019 Sandy Maguire -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to@@ -26,218 +26,119 @@ -- ------------------------------------------------------------------------------ ----- This module is heavily based on 'Control.Effects.Plugin' from the--- 'simple-effects' package, originally by Luka Horvat.+-- This module was originally based on 'Control.Effects.Plugin' from the+-- 'simple-effects' package, by Luka Horvat. -- -- https://gitlab.com/LukaHorvat/simple-effects/commit/966ce80b8b5777a4bd8f87ffd443f5fa80cc8845#f51c1641c95dfaa4827f641013f8017e8cd02aab module Polysemy.Plugin.Fundep (fundepPlugin) where -import Class-import CoAxiom-import Control.Applicative import Control.Monad import Data.Bifunctor-import Data.Bool import Data.Coerce-import Data.Function (on) import Data.IORef-import qualified Data.Kind as K-import Data.List+import qualified Data.Map as M import Data.Maybe import qualified Data.Set as S-import FastString (fsLit)-import GHC (TyCon, Name)-import GHC.TcPluginM.Extra (lookupModule, lookupName)-import Module (mkModuleName)-import OccName (mkTcOcc)+import Polysemy.Plugin.Fundep.Stuff+import Polysemy.Plugin.Fundep.Unification+import Polysemy.Plugin.Fundep.Utils import TcEvidence-import TcPluginM (TcPluginM, tcLookupClass, tcLookupTyCon, tcPluginIO)+import TcPluginM (TcPluginM, tcPluginIO) import TcRnTypes import TcSMonad hiding (tcLookupClass)-import TyCoRep (Type (..)) import Type -data LookupState- = Locations- | Things --type family ThingOf (l :: LookupState) (a :: K.Type) :: K.Type where- ThingOf 'Locations _ = (String, String)- ThingOf 'Things a = a---data PolysemyStuff (l :: LookupState) = PolysemyStuff- { findClass :: ThingOf l Class- , semTyCon :: ThingOf l TyCon- , ifStuckTyCon :: ThingOf l TyCon- , indexOfTyCon :: ThingOf l TyCon+fundepPlugin :: TcPlugin+fundepPlugin = TcPlugin+ { tcPluginInit =+ (,) <$> tcPluginIO (newIORef S.empty)+ <*> polysemyStuff+ , tcPluginSolve = solveFundep+ , tcPluginStop = const $ pure () } -class CanLookup a where- lookupStrategy :: Name -> TcPluginM a--instance CanLookup Class where- lookupStrategy = tcLookupClass--instance CanLookup TyCon where- lookupStrategy = tcLookupTyCon---doLookup :: CanLookup a => ThingOf 'Locations a -> TcPluginM (ThingOf 'Things a)-doLookup (mdname, name) = do- md <- lookupModule (mkModuleName mdname) $ fsLit "polysemy"- nm <- lookupName md $ mkTcOcc name- lookupStrategy nm---lookupEverything :: PolysemyStuff 'Locations -> TcPluginM (PolysemyStuff 'Things)-lookupEverything (PolysemyStuff a b c d) =- PolysemyStuff <$> doLookup a- <*> doLookup b- <*> doLookup c- <*> doLookup d---polysemyStuffLocations :: PolysemyStuff 'Locations-polysemyStuffLocations = PolysemyStuff- { findClass = ("Polysemy.Internal.Union", "Find")- , semTyCon = ("Polysemy.Internal", "Sem")- , ifStuckTyCon = ("Polysemy.Internal.CustomErrors.Redefined", "IfStuck")- , indexOfTyCon = ("Polysemy.Internal.Union", "IndexOf")+------------------------------------------------------------------------------+-- | Corresponds to a 'Polysemy.Internal.Union.Find' constraint. For example,+-- given @Member (State s) r@, we would get:+data FindConstraint = FindConstraint+ { fcLoc :: CtLoc+ , fcEffectName :: Type -- ^ @State@+ , fcEffect :: Type -- ^ @State s@+ , fcRow :: Type -- ^ @r@ } -fundepPlugin :: TcPlugin-fundepPlugin = TcPlugin- { tcPluginInit =- (,) <$> tcPluginIO (newIORef S.empty)- <*> lookupEverything polysemyStuffLocations- , tcPluginSolve = solveFundep- , tcPluginStop = const (return ()) }+------------------------------------------------------------------------------+-- | Given a list of constraints, filter out the 'FindConstraint's.+getFindConstraints :: PolysemyStuff 'Things -> [Ct] -> [FindConstraint]+getFindConstraints (findClass -> cls) cts = do+ cd@CDictCan{cc_class = cls', cc_tyargs = [_, r, eff]} <- cts+ guard $ cls == cls'+ pure $ FindConstraint+ { fcLoc = ctLoc cd+ , fcEffectName = getEffName eff+ , fcEffect = eff+ , fcRow = r+ } -allMonadEffectConstraints :: PolysemyStuff 'Things -> [Ct] -> [(CtLoc, (Type, Type, Type))]-allMonadEffectConstraints (findClass -> cls) cts =- [ (ctLoc cd, (effName, eff, r))- | cd@CDictCan{cc_class = cls', cc_tyargs = [_, r, eff]} <- cts- , cls == cls'- , let effName = getEffName eff- ] -singleListToJust :: [a] -> Maybe a-singleListToJust [a] = Just a-singleListToJust _ = Nothing+------------------------------------------------------------------------------+-- | If there's only a single @Member@ in the same @r@ whose effect name+-- matches and could possibly unify, return its effect (including tyvars.)+findMatchingEffectIfSingular+ :: FindConstraint+ -> [FindConstraint]+ -> Maybe Type+findMatchingEffectIfSingular (FindConstraint _ eff_name wanted r) ts =+ singleListToJust $ do+ FindConstraint _ eff_name' eff' r' <- ts+ guard $ eqType eff_name eff_name'+ guard $ eqType r r'+ guard $ canUnifyRecursive FunctionDef wanted eff'+ pure eff' -findMatchingEffectIfSingular :: (Type, Type, Type) -> [(Type, Type, Type)] -> Maybe Type-findMatchingEffectIfSingular (effName, _, mon) ts = singleListToJust- [ eff'- | (effName', eff', mon') <- ts- , eqType effName effName'- , eqType mon mon' ] +------------------------------------------------------------------------------+-- | Given an effect, compute its effect name. getEffName :: Type -> Type getEffName t = fst $ splitAppTys t -canUnifyRecursive :: SolveContext -> Type -> Type -> Bool-canUnifyRecursive solve_ctx = go True+------------------------------------------------------------------------------+-- | Generate a wanted unification for the effect described by the+-- 'FindConstraint' and the given effect.+mkWantedForce+ :: FindConstraint+ -> Type+ -> TcPluginM (Unification, Ct)+mkWantedForce fc given = do+ (ev, _) <- unsafeTcPluginTcM+ . runTcSDeriveds+ $ newWantedEq (fcLoc fc) Nominal wanted given+ pure ( Unification (OrdType wanted) (OrdType given)+ , CNonCanonical ev+ ) where- -- It's only OK to solve a polymorphic "given" if we're in the context of- -- an interpreter, because it's not really a given!- poly_given_ok :: Bool- poly_given_ok =- case solve_ctx of- InterpreterUse _ -> True- FunctionDef -> False-- -- On the first go around, we don't want to unify effects with tyvars, but- -- we _do_ want to unify their arguments, thus 'is_first'.- go :: Bool -> Type -> Type -> Bool- go is_first wanted given =- let (w, ws) = splitAppTys wanted- (g, gs) = splitAppTys given- in (&& bool (canUnify poly_given_ok) eqType is_first w g)- . flip all (zip ws gs)- $ \(wt, gt) -> canUnify poly_given_ok wt gt || go False wt gt---canUnify :: Bool -> Type -> Type -> Bool-canUnify poly_given_ok wt gt =- or [ isTyVarTy wt- , isTyVarTy gt && poly_given_ok- , eqType wt gt- ]-+ wanted = fcEffect fc --------------------------------------------------------------------------------- | Like 'Control.Monad.when', but in the context of an 'Alternative'.-whenA- :: (Monad m, Alternative z)- => Bool- -> m a- -> m (z a)-whenA False _ = pure empty-whenA True ma = fmap pure ma--+-- | Generate a wanted unification for the effect described by the+-- 'FindConstraint' and the given effect --- if they can be unified in this+-- context. mkWanted- :: SolveContext- -> CtLoc- -> Type- -> Type- -> TcPluginM (Maybe ( (OrdType, OrdType) -- the types we want to unify- , Ct -- the constraint- ))-mkWanted solve_ctx loc wanted given =- whenA (not (mustUnify solve_ctx) || canUnifyRecursive solve_ctx wanted given) $ do- (ev, _) <- unsafeTcPluginTcM- . runTcSDeriveds- $ newWantedEq loc Nominal wanted given- pure ( (OrdType wanted, OrdType given)- , CNonCanonical ev- )--thd :: (a, b, c) -> c-thd (_, _, c) = c--countLength :: (a -> a -> Bool) -> [a] -> [(a, Int)]-countLength eq as =- let grouped = groupBy eq as- in zipWith (curry $ bimap head length) grouped grouped------------------------------------------------------------------------------------ | 'Type's don't have 'Eq' or 'Ord' instances by default, even though there--- are functions in GHC that implement these operations. This newtype gives us--- those instances.-newtype OrdType = OrdType- { getOrdType :: Type- }--instance Eq OrdType where- (==) = eqType `on` getOrdType--instance Ord OrdType where- compare = nonDetCmpType `on` getOrdType------------------------------------------------------------------------------------ | The context in which we're attempting to solve a constraint.-data SolveContext- = -- | In the context of a function definition.- FunctionDef- -- | In the context of running an interpreter. The 'Bool' corresponds to- -- whether we are only trying to solve a single 'Member' constraint right- -- now. If so, we *must* produce a unification wanted.- | InterpreterUse Bool- deriving (Eq, Ord, Show)--mustUnify :: SolveContext -> Bool-mustUnify FunctionDef = True-mustUnify (InterpreterUse b) = b+ :: FindConstraint+ -> SolveContext+ -> Type -- ^ The given effect.+ -> TcPluginM (Maybe (Unification, Ct))+mkWanted fc solve_ctx given =+ whenA (not (mustUnify solve_ctx) || canUnifyRecursive solve_ctx wanted given) $+ mkWantedForce fc given+ where+ wanted = fcEffect fc ------------------------------------------------------------------------------@@ -246,68 +147,94 @@ getBogusRs :: PolysemyStuff 'Things -> [Ct] -> [Type] getBogusRs stuff wanteds = do CIrredCan ct _ <- wanteds- case splitAppTys $ ctev_pred ct of- (_, [_, _, a, b]) ->- maybeToList (getRIfSem stuff a) ++ maybeToList (getRIfSem stuff b)- (_, _) -> []+ (_, [_, _, a, b]) <- pure . splitAppTys $ ctev_pred ct+ maybeToList (extractRowFromSem stuff a)+ ++ maybeToList (extractRowFromSem stuff b) ------------------------------------------------------------------------------ -- | Take the @r@ out of @Sem r a@.-getRIfSem :: PolysemyStuff 'Things -> Type -> Maybe Type-getRIfSem (semTyCon -> sem) ty =- case splitTyConApp_maybe ty of- Just (tycon, [r, _]) | tycon == sem -> pure r- _ -> Nothing+extractRowFromSem :: PolysemyStuff 'Things -> Type -> Maybe Type+extractRowFromSem (semTyCon -> sem) ty = do+ (tycon, [r, _]) <- splitTyConApp_maybe ty+ guard $ tycon == sem+ pure r ------------------------------------------------------------------------------ -- | Given a list of bogus @r@s, and the wanted constraints, produce bogus -- evidence terms that will prevent @IfStuck (IndexOf r _) _ _@ error messsages.-solveBogusError :: PolysemyStuff 'Things -> [Type] -> [Ct] -> [(EvTerm, Ct)]-solveBogusError stuff bogus wanteds = do+solveBogusError :: PolysemyStuff 'Things -> [Ct] -> [(EvTerm, Ct)]+solveBogusError stuff wanteds = do+ let splitTyConApp_list = maybeToList . splitTyConApp_maybe++ let bogus = getBogusRs stuff wanteds ct@(CIrredCan ce _) <- wanteds- case splitTyConApp_maybe $ ctev_pred ce of- Just (stuck, [_, _, expr, _, _]) | stuck == ifStuckTyCon stuff -> do- case splitTyConApp_maybe expr of- Just (idx, [_, r, _]) | idx == indexOfTyCon stuff -> do- case elem @[] (OrdType r) $ coerce bogus of- True -> pure (error "bogus proof for stuck type family", ct)- False -> []- _ -> []- _ -> []+ (stuck, [_, _, expr, _, _]) <- splitTyConApp_list $ ctev_pred ce+ guard $ stuck == ifStuckTyCon stuff+ (idx, [_, r, _]) <- splitTyConApp_list expr+ guard $ idx == indexOfTyCon stuff+ guard $ elem @[] (OrdType r) $ coerce bogus+ pure (error "bogus proof for stuck type family", ct) +------------------------------------------------------------------------------+-- | Determine if there is exactly one wanted find for the @r@ in question.+exactlyOneWantedForR+ :: [FindConstraint] -- ^ Wanted finds+ -> Type -- ^ Effect row+ -> Bool+exactlyOneWantedForR wanteds+ = fromMaybe False+ . flip M.lookup singular_r+ . OrdType+ where+ singular_r = M.fromList+ -- TODO(sandy): Nothing fails if this is just @second (const+ -- True)@. Why not? Incomplete test suite, or doing too much+ -- work?+ . fmap (second (/= 1))+ . countLength+ $ fmap (OrdType . fcRow) wanteds++ solveFundep- :: (IORef (S.Set (OrdType, OrdType)), PolysemyStuff 'Things)+ :: ( IORef (S.Set Unification)+ , PolysemyStuff 'Things+ ) -> [Ct] -> [Ct] -> [Ct] -> TcPluginM TcPluginResult solveFundep _ _ _ [] = pure $ TcPluginOk [] []-solveFundep (ref, stuff) giv _ want = do- let bogus = getBogusRs stuff want- solved_bogus = solveBogusError stuff bogus want+solveFundep (ref, stuff) given _ wanted = do+ let wanted_finds = getFindConstraints stuff wanted+ given_finds = getFindConstraints stuff given - let wantedEffs = allMonadEffectConstraints stuff want- givenEffs = snd <$> allMonadEffectConstraints stuff giv- num_wanteds_by_r = countLength eqType $ fmap (thd . snd) wantedEffs- must_unify r =- let Just num_wanted = find (eqType r . fst) num_wanteds_by_r- in snd num_wanted /= 1+ eqs <- forM wanted_finds $ \fc -> do+ let r = fcRow fc+ case findMatchingEffectIfSingular fc given_finds of+ -- We found a real given, therefore we are in the context of a function+ -- with an explicit @Member e r@ constraint. We also know it can+ -- be unified (although it may generate unsatisfiable constraints).+ Just eff' -> Just <$> mkWantedForce fc eff' - eqs <- forM wantedEffs $ \(loc, e@(_, eff, r)) -> do- case findMatchingEffectIfSingular e givenEffs of- Nothing -> do- case splitAppTys r of- (_, [_, eff', _]) -> mkWanted (InterpreterUse $ must_unify r) loc eff eff'- _ -> pure Nothing- Just eff' -> mkWanted FunctionDef loc eff eff'+ -- Otherwise, check to see if @r ~ (e ': r')@. If so, pretend we're+ -- trying to solve a given @Member e r@. But this can only happen in the+ -- context of an interpreter!+ Nothing ->+ case splitAppTys r of+ (_, [_, eff', _]) ->+ mkWanted fc+ (InterpreterUse $ exactlyOneWantedForR wanted_finds r)+ eff'+ _ -> pure Nothing - already_emitted <- tcPluginIO $ readIORef ref- let new_wanteds = filter (not . flip S.member already_emitted . fst)- $ catMaybes eqs+ -- We only want to emit a unification wanted once, otherwise a type error can+ -- force the type checker to loop forever.+ already_emitted <- tcPluginIO $ readIORef ref+ let (unifications, new_wanteds) = unzipNewWanteds already_emitted $ catMaybes eqs+ tcPluginIO $ modifyIORef ref $ S.union $ S.fromList unifications - tcPluginIO $ modifyIORef ref $ S.union $ S.fromList $ fmap fst new_wanteds- pure . TcPluginOk solved_bogus $ fmap snd new_wanteds+ pure $ TcPluginOk (solveBogusError stuff wanted) new_wanteds
+ src/Polysemy/Plugin/Fundep/Stuff.hs view
@@ -0,0 +1,83 @@+module Polysemy.Plugin.Fundep.Stuff+ ( PolysemyStuff (..)+ , LookupState (..)+ , polysemyStuff+ ) where++import Data.Kind (Type)+import FastString (fsLit)+import GHC (Name, Class, TyCon, mkModuleName)+import GHC.TcPluginM.Extra (lookupModule, lookupName)+import OccName (mkTcOcc)+import TcPluginM (TcPluginM, tcLookupClass, tcLookupTyCon)++++------------------------------------------------------------------------------+-- | All of the things from "polysemy" that we need access to in the plugin.+-- When @l ~ 'Locations@, each of these is just a pair of strings. When @l+-- ~ 'Things@, it's actually references to the stuff.+data PolysemyStuff (l :: LookupState) = PolysemyStuff+ { findClass :: ThingOf l Class+ , semTyCon :: ThingOf l TyCon+ , ifStuckTyCon :: ThingOf l TyCon+ , indexOfTyCon :: ThingOf l TyCon+ }+++------------------------------------------------------------------------------+-- | All of the things we need to lookup.+polysemyStuffLocations :: PolysemyStuff 'Locations+polysemyStuffLocations = PolysemyStuff+ { findClass = ("Polysemy.Internal.Union", "Find")+ , semTyCon = ("Polysemy.Internal", "Sem")+ , ifStuckTyCon = ("Polysemy.Internal.CustomErrors.Redefined", "IfStuck")+ , indexOfTyCon = ("Polysemy.Internal.Union", "IndexOf")+ }+++------------------------------------------------------------------------------+-- | Lookup all of the 'PolysemyStuff'.+polysemyStuff :: TcPluginM (PolysemyStuff 'Things)+polysemyStuff =+ let PolysemyStuff a b c d = polysemyStuffLocations+ in PolysemyStuff <$> doLookup a+ <*> doLookup b+ <*> doLookup c+ <*> doLookup d+++------------------------------------------------------------------------------+-- | Data kind for 'ThingOf'.+data LookupState+ = Locations+ | Things+++------------------------------------------------------------------------------+-- | HKD indexed by the 'LookupState'; used by 'PolysemyStuff'.+type family ThingOf (l :: LookupState) (a :: Type) :: Type where+ ThingOf 'Locations _ = (String, String)+ ThingOf 'Things a = a+++------------------------------------------------------------------------------+-- | Things that can be found in a 'TcPluginM' environment.+class CanLookup a where+ lookupStrategy :: Name -> TcPluginM a++instance CanLookup Class where+ lookupStrategy = tcLookupClass++instance CanLookup TyCon where+ lookupStrategy = tcLookupTyCon+++------------------------------------------------------------------------------+-- | Transform a @'ThingOf' 'Locations@ into a @'ThingOf' 'Things@.+doLookup :: CanLookup a => ThingOf 'Locations a -> TcPluginM (ThingOf 'Things a)+doLookup (mdname, name) = do+ md <- lookupModule (mkModuleName mdname) $ fsLit "polysemy"+ nm <- lookupName md $ mkTcOcc name+ lookupStrategy nm+
+ src/Polysemy/Plugin/Fundep/Unification.hs view
@@ -0,0 +1,136 @@+module Polysemy.Plugin.Fundep.Unification where++import Data.Bool+import Data.Function (on)+import qualified Data.Set as S+import TcRnTypes+import Type+++------------------------------------------------------------------------------+-- | The context in which we're attempting to solve a constraint.+data SolveContext+ = -- | In the context of a function definition.+ FunctionDef+ -- | In the context of running an interpreter. The 'Bool' corresponds to+ -- whether we are only trying to solve a single 'Member' constraint right+ -- now. If so, we *must* produce a unification wanted.+ | InterpreterUse Bool+ deriving (Eq, Ord, Show)+++------------------------------------------------------------------------------+-- | Depending on the context in which we're solving a constraint, we may or+-- may not want to force a unification of effects. For example, when defining+-- user code whose type is @Member (State Int) r => ...@, if we see @get :: Sem+-- r s@, we should unify @s ~ Int@.+mustUnify :: SolveContext -> Bool+mustUnify FunctionDef = True+mustUnify (InterpreterUse b) = b+++------------------------------------------------------------------------------+-- | Determine whether or not two effects are unifiable. This is nuanced.+--+-- There are several cases:+--+-- 1. [W] ∀ e1. e1 [G] ∀ e2. e2+-- Always fails, because we never want to unify two effects if effect names+-- are polymorphic.+--+-- 2. [W] State s [G] State Int+-- Always succeeds. It's safe to take our given as a fundep annotation.+--+-- 3. [W] State Int [G] State s+-- (when the [G] is a given that comes from a type signature)+--+-- This should fail, because it means we wrote the type signature @Member+-- (State s) r => ...@, but are trying to use @s@ as an @Int@. Clearly+-- bogus!+--+-- 4. [W] State Int [G] State s+-- (when the [G] was generated by running an interpreter)+--+-- Sometimes OK, but only if the [G] is the only thing we're trying to solve+-- right now. Consider the case:+--+-- runState 5 $ pure @(Sem (State Int ': r)) ()+--+-- Here we have [G] forall a. Num a => State a and [W] State Int. Clearly+-- the typechecking should flow "backwards" here, out of the row and into+-- the type of 'runState'.+--+-- What happens if there are multiple [G]s in scope for the same @r@? Then+-- we'd emit multiple unification constraints for the same effect but with+-- different polymorphic variables, which would unify a bunch of effects+-- that shouldn't be!+canUnifyRecursive+ :: SolveContext+ -> Type -- ^ wanted+ -> Type -- ^ given+ -> Bool+canUnifyRecursive solve_ctx = go True+ where+ -- It's only OK to solve a polymorphic "given" if we're in the context of+ -- an interpreter, because it's not really a given!+ poly_given_ok :: Bool+ poly_given_ok =+ case solve_ctx of+ InterpreterUse _ -> True+ FunctionDef -> False++ -- On the first go around, we don't want to unify effects with tyvars, but+ -- we _do_ want to unify their arguments, thus 'is_first'.+ go :: Bool -> Type -> Type -> Bool+ go is_first wanted given =+ let (w, ws) = splitAppTys wanted+ (g, gs) = splitAppTys given+ in (&& bool (canUnify poly_given_ok) eqType is_first w g)+ . flip all (zip ws gs)+ $ \(wt, gt) -> canUnify poly_given_ok wt gt || go False wt gt+++------------------------------------------------------------------------------+-- | A non-recursive version of 'canUnifyRecursive'.+canUnify :: Bool -> Type -> Type -> Bool+canUnify poly_given_ok wt gt =+ or [ isTyVarTy wt+ , isTyVarTy gt && poly_given_ok+ , eqType wt gt+ ]+++------------------------------------------------------------------------------+-- | A wrapper for two types that we want to say have been unified.+data Unification = Unification+ { _unifyLHS :: OrdType+ , _unifyRHS :: OrdType+ }+ deriving (Eq, Ord)+++------------------------------------------------------------------------------+-- | 'Type's don't have 'Eq' or 'Ord' instances by default, even though there+-- are functions in GHC that implement these operations. This newtype gives us+-- those instances.+newtype OrdType = OrdType+ { getOrdType :: Type+ }++instance Eq OrdType where+ (==) = eqType `on` getOrdType++instance Ord OrdType where+ compare = nonDetCmpType `on` getOrdType+++------------------------------------------------------------------------------+-- | Filter out the unifications we've already emitted, and then give back the+-- things we should put into the @S.Set Unification@, and the new constraints+-- we should emit.+unzipNewWanteds+ :: S.Set Unification+ -> [(Unification, Ct)]+ -> ([Unification], [Ct])+unzipNewWanteds old = unzip . filter (not . flip S.member old . fst)+
+ src/Polysemy/Plugin/Fundep/Utils.hs view
@@ -0,0 +1,33 @@+module Polysemy.Plugin.Fundep.Utils where++import Control.Applicative+import Data.Bifunctor+import Data.List++++------------------------------------------------------------------------------+-- | Returns the head of the list iff there is exactly one element.+singleListToJust :: [a] -> Maybe a+singleListToJust [a] = Just a+singleListToJust _ = Nothing+++------------------------------------------------------------------------------+-- | Like 'Control.Monad.when', but in the context of an 'Alternative'.+whenA+ :: (Monad m, Alternative z)+ => Bool+ -> m a+ -> m (z a)+whenA False _ = pure empty+whenA True ma = fmap pure ma+++------------------------------------------------------------------------------+-- | Count the number of times 'a' is present in the list.+countLength :: Eq a => [a] -> [(a, Int)]+countLength as =+ let grouped = group as+ in zipWith (curry $ bimap head length) grouped grouped+
test/ExampleSpec.hs view
@@ -16,10 +16,10 @@ makeSem ''Teletype -runTeletypeIO :: Member (Lift IO) r => Sem (Teletype ': r) a -> Sem r a-runTeletypeIO = interpret $ \case- ReadTTY -> sendM getLine- WriteTTY msg -> sendM $ putStrLn msg+teletypeToIO :: Member (Embed IO) r => Sem (Teletype ': r) a -> Sem r a+teletypeToIO = interpret $ \case+ ReadTTY -> embed getLine+ WriteTTY msg -> embed $ putStrLn msg data CustomException = ThisException | ThatException deriving Show @@ -33,7 +33,13 @@ _ -> writeTTY i >> writeTTY "no exceptions" foo :: IO (Either CustomException ())-foo = (runM .@ runResourceInIO .@@ runErrorInIO @CustomException) $ runTeletypeIO program+foo =+ runFinal+ . embedToFinal @IO+ . resourceToIOFinal+ . errorToIOFinal @CustomException+ . teletypeToIO+ $ program spec :: Spec spec = describe "example" $ do
test/LegitimateTypeErrorSpec.hs view
@@ -6,8 +6,8 @@ import Test.Hspec import Test.ShouldNotTypecheck -wrongLift :: Member (Lift IO) r => Sem r ()-wrongLift = sendM putStrLn+wrongEmbed :: Member (Embed IO) r => Sem r ()+wrongEmbed = embed putStrLn wrongReturn :: Sem (e ': r) () -> Sem r () wrongReturn = reinterpret undefined@@ -17,8 +17,8 @@ spec :: Spec spec = do describe "Legitimate type errors" $ do- it "should be caused by `sendM`ing an unsaturated function" $- shouldNotTypecheck wrongLift+ it "should be caused by `embed`ing an unsaturated function" $+ shouldNotTypecheck wrongEmbed it "should be caused by giving a bad type to reinterpret" $ shouldNotTypecheck wrongReturn
+ test/MultipleVarsSpec.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-}++module MultipleVarsSpec where++import Polysemy+import Polysemy.State++import Test.Hspec++data TaggedState k s m a where+ TaggedGet :: forall k s m. TaggedState k s m s+ TaggedPut :: forall k s m. s -> TaggedState k s m ()++makeSem ''TaggedState++runTaggedState :: forall k s r a+ . s+ -> Sem (TaggedState k s ': r) a+ -> Sem r (s, a)+runTaggedState s =+ (runState s .)+ $ reinterpret+ $ \case+ TaggedGet -> get+ TaggedPut s -> put s++test :: Members '[+ TaggedState Char Int+ , TaggedState Bool Int+ ] r+ => Sem r ()+test = do+ taggedPut @Bool 10+ taggedPut @Char (-10)++spec :: Spec+spec = describe "Using multiple, but ununifiable instances\+ \ of the same effect" $ do+ it "should get disambiguated and compile, \+ \and actions should target the right effects." $ do+ let+ res1 =+ run+ . runTaggedState @Char 0+ . runTaggedState @Bool 7+ $ test+ res2 =+ run+ . runTaggedState @Bool 0+ . runTaggedState @Char 7+ $ test+ res3 =+ run+ . runTaggedState @Bool 0+ . runTaggedState @Char 7+ $ do+ taggedPut @Bool 10+ taggedPut @Char (-10)+ res1 `shouldBe` (-10, (10, ()))+ res2 `shouldBe` (10, (-10, ()))+ res3 `shouldBe` (10, (-10, ()))
test/PluginSpec.hs view
@@ -46,8 +46,8 @@ err -lifted :: Monad m => Member (Lift m) r => Sem r ()-lifted = sendM $ pure ()+lifted :: Monad m => Member (Embed m) r => Sem r ()+lifted = embed $ pure () newtype MyString = MyString String@@ -131,12 +131,12 @@ describe "Output effect" $ do it "should unify recursively with tyvars" $ do- flipShouldBe 11 . sum . fst . run . runFoldMapOutput id $ do+ flipShouldBe 11 . sum . fst . run . runOutputMonoid id $ do output [1] output $ replicate 2 5 - describe "Lift effect" $ do+ describe "Embed effect" $ do it "should interpret against IO" $ do res <- runM lifted res `shouldBe` ()
test/TypeErrors.hs view
@@ -22,7 +22,7 @@ -- ... Couldn't match expected type ...Sem r Bool... with actual type ...Bool... -- ... -- ... Couldn't match expected type ...Maybe a0...--- ... with actual type ...Sem r0 a1...+-- ... with actual type ...Sem r0 s0... -- ... missingFmap = ()