penelope-effects-0.1.0.0: src/Penelope/Effects/UnliftIO.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{- |
Module : Penelope.Effects.UnliftIO
Description : Sound scoped resource management, masking, and dependency injection
Copyright : (c) 2026 The Penelope Authors
License : EUPL-1.2 OR Apache-2.0
Maintainer : nadia.yvette.chambers@ik.me
Stability : experimental
Portability : non-portable (GHC extensions)
Provides sound scoped resource management guaranteed never to leak handles
across dynamic evaluation contexts:
1. 'Bracket' higher-order scoped resource acquisition and cleanup
2. 'Mask' asynchronous exception safety
3. 'Provider' dependency injection
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
module Penelope.Effects.UnliftIO
( -- * Scoped Bracket
Bracket (..)
, bracketElaborator
, bracketEff
, bracketEitherEff
, bracketIO
-- * Masking
, Mask
, maskIO
-- * Provider Dependency Injection
, Provider
, ProviderEff (..)
, askProvider
, providerHandler
, runProviderPure
) where
import qualified Control.Exception as E
import Data.Functor.Identity (Identity (..))
import Data.Kind (Type)
import Data.Primitive.SmallArray (indexSmallArray)
import GHC.Exts (unsafeCoerce#)
import GHC.TypeLits (Symbol)
import Penelope.Core.Engine.Elaborate (Elaborator (..))
import Penelope.Core.Engine.Interpret (runPenelopePure)
import Penelope.Core.Syntax
( BranchCont (..)
, Penelope (..)
, PolyEffect
, PolyHEffect
, PortSig (..)
, Region (..)
)
import Penelope.Effects.Unary
( Eff (..)
, EffHandler
, UnaryEffect
, mkEffHandler
, runEffM
, send
)
import Polytropos (Field (..), RowOf, emptyRecord, insertField)
import Polytropos.Core (ConstructorIndex (..), Pair (..), Rep (..), RowRep (..))
{- |
Higher-order scoped 'Bracket' effect with three sub-regions:
1. @"acquire"@: Allocates the resource handle.
2. @"action"@: Operates with the acquired resource.
3. @"release"@: Guarantees release of the resource on all paths.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data Bracket (r :: PortSig -> Type) (regs :: RowOf PortSig) (opIns :: RowOf Type) (exits :: RowOf (RowOf Type)) where
BracketOp :: Bracket r
'[ "acquire" ':= 'PortSig ins '["res" ':= res]
, "action" ':= 'PortSig '["res" ':= res] '["out" ':= a]
, "release" ':= 'PortSig '["res" ':= res] '[]
]
ins
'[ "ret" ':= '["out" ':= a] ]
{- |
Elaborator for 'Bracket' lowering scoped resource handling into guaranteed cleanup.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE bracketElaborator #-}
bracketElaborator ::
forall ef.
Elaborator '[] ef Bracket
bracketElaborator = Elaborator $ \inMap regs _ kConts ->
LiftPure inMap
(\inRec ->
let !(Identity acqRegAny) = indexSmallArray (let RowRep (Rep _ arr) = regs in arr) 0
!(Region acqPipe) = unsafeCoerce# acqRegAny
!(Identity actRegAny) = indexSmallArray (let RowRep (Rep _ arr) = regs in arr) 1
!(Region actPipe) = unsafeCoerce# actRegAny
!(Identity relRegAny) = indexSmallArray (let RowRep (Rep _ arr) = regs in arr) 2
!(Region relPipe) = unsafeCoerce# relRegAny
!resRec = runPenelopePure acqPipe (unsafeCoerce# inRec)
!actRec = runPenelopePure actPipe (unsafeCoerce# resRec)
!relRec = runPenelopePure relPipe (unsafeCoerce# resRec)
in seq relRec actRec
)
(BranchCont $ \finalActRec ->
let !(Identity retContAny) = indexSmallArray (let RowRep (Rep _ arr) = kConts in arr) 0
!(BranchCont retK) = unsafeCoerce# retContAny
in retK (unsafeCoerce# finalActRec)
)
{- |
Bracket pattern for 'Eff' ensuring resource cleanup.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE bracketEff #-}
bracketEff ::
forall res a eh ef.
Eff eh ef res
-> (res -> Eff eh ef ())
-> (res -> Eff eh ef a)
-> Eff eh ef a
bracketEff acquire release action = do
r <- acquire
val <- action r
release r
pure val
{- |
Bracket pattern for 'Eff' with error exit propagation ensuring resource cleanup
on both successful and failed branches.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE bracketEitherEff #-}
bracketEitherEff ::
forall res err a eh ef.
Eff eh ef res
-> (res -> Eff eh ef ())
-> (res -> Eff eh ef (Either err a))
-> Eff eh ef (Either err a)
bracketEitherEff acquire release action = do
r <- acquire
resOrErr <- action r
release r
pure resOrErr
{- |
Bracket pattern for 'IO' utilizing underlying exception safety guarantees.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE bracketIO #-}
bracketIO ::
forall res a.
IO res
-> (res -> IO ())
-> (res -> IO a)
-> IO a
bracketIO = E.bracket
{- |
Higher-order scoped 'Mask' effect guaranteeing asynchronous exception masking.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data Mask :: PolyHEffect
{- |
Mask asynchronous exceptions within an 'IO' action.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE maskIO #-}
maskIO ::
forall a.
((forall b. IO b -> IO b) -> IO a)
-> IO a
maskIO = E.mask
{- |
Polycategorical provider dependency injection effect.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data Provider (r :: Type) :: PolyEffect
{- |
Unary effect GADT representing an environment query.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data ProviderEff (r :: Type) (a :: Type) where
AskProvider :: ProviderEff r r
{- |
Query the provided environment from within 'Eff'.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE askProvider #-}
askProvider ::
forall (l :: Symbol) r eh ef.
ConstructorIndex l ef =>
Eff eh ef r
askProvider = send @l AskProvider
{- |
Construct an 'EffHandler' supplying an environment handle @r@.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE providerHandler #-}
providerHandler ::
forall r m.
Monad m =>
r
-> EffHandler m (ProviderEff r)
providerHandler env = mkEffHandler $ \case
AskProvider -> pure env
{- |
Run an 'Eff' computation with a provided environment handle @r@ purely.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE runProviderPure #-}
runProviderPure ::
forall r a.
r
-> Eff '[] '["provider" ':= UnaryEffect (ProviderEff r)] a
-> a
runProviderPure env prog =
let handlers = insertField (Field @"provider") (providerHandler env) emptyRecord
Identity res = runEffM handlers prog
in res