penelope-effects-0.1.0.0: src/Penelope/Effects/Scoped.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.Scoped
Description : Scoped reader and exception semantics with higher-order elaboration
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 scoped effect semantics ('Reader'/'Local' and 'Error'/'Catch') guaranteed never
to leak environment handles or clobber lexical scopes across delimited coroutine
suspensions or non-deterministic branching resumptions.
See Note [Core Architecture] [ref:note-core-architecture],
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades], and
Note [Two-Phase Weaving and Unweaving] [ref:note-two-phase-engine].
-}
module Penelope.Effects.Scoped
( -- * Scoped Reader Effect
AskEff (..)
, Local
, ask
, askReader
, local
, localAt
, localElaborator
, readerHandler
, runReaderPure
-- * Scoped Error Effect
, ThrowEff (..)
, Catch
, throwError
, throw
, catchError
, catchErrorAt
, catchElaborator
, errorHandler
, runErrorPure
) where
import Data.Functor.Identity (Identity (..))
import Data.Kind (Type)
import Data.Primitive.SmallArray (indexSmallArray)
import GHC.Exts (Any, Word (W#), Word#, eqWord#, isTrue#, unsafeCoerce#)
import GHC.TypeLits (Symbol)
import Penelope.Core.Engine.Elaborate (Elaborator (..))
import Penelope.Core.Syntax
( BranchCont (..)
, Penelope (..)
, PolyHEffect
, Region (..)
)
import Penelope.Core.Wiring (preComposeRoute, rmapFused, seqPenelope)
import Penelope.Effects.Unary
( Eff (..)
, EffHandler
, UnaryEffect
, mkEffHandler
, runEffM
, send
)
import Polytropos (Field (..), emptyRecord, insertField)
import Polytropos.Core (ConstructorIndex (..), Pair (..), Rep (..), RowRep (..))
import Polytropos.RowMorph (unsafeMkRowMorph)
{- |
First-order algebraic reader operation querying an environment of type @r@.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data AskEff (r :: Type) (a :: Type) where
Ask :: AskEff r r
{- |
First-order algebraic error operation terminating computation with an error of type @e@.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data ThrowEff (e :: Type) (a :: Type) where
Throw :: e -> ThrowEff e a
{- |
Higher-order scoped effect representing local lexical environment modification.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Two-Phase Weaving and Unweaving] [ref:note-two-phase-engine].
-}
data Local (r :: Type) :: PolyHEffect
{- |
Higher-order scoped effect representing dynamic error boundary interception.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Two-Phase Weaving and Unweaving] [ref:note-two-phase-engine].
-}
data Catch (e :: Type) :: PolyHEffect
{- |
Query the reader environment at a specified effect label.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE ask #-}
ask ::
forall (l :: Symbol) r eh ef.
ConstructorIndex l ef =>
Eff eh ef r
ask = send @l Ask
{- |
Query the reader environment defaulting to effect label @"reader"@.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE askReader #-}
askReader ::
forall r eh ef.
ConstructorIndex "reader" ef =>
Eff eh ef r
askReader = ask @"reader"
{- |
Throw an algebraic error at a specified effect label.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE throwError #-}
throwError ::
forall (l :: Symbol) e eh ef a.
ConstructorIndex l ef =>
e
-> Eff eh ef a
throwError e = send @l (Throw e)
{- |
Throw an algebraic error defaulting to effect label @"error"@.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE throw #-}
throw ::
forall e eh ef a.
ConstructorIndex "error" ef =>
e
-> Eff eh ef a
throw = throwError @"error"
{- |
Walk the 'Penelope' polycategorical graph, transforming every 'Ask' node targeted
by 'targetTag#' so its return continuation receives the locally modified environment.
See Note [Two-Phase Weaving and Unweaving] [ref:note-two-phase-engine].
-}
localWalk ::
forall eh ef r ins outs.
Word#
-> (r -> r)
-> Penelope eh ef ins outs
-> Penelope eh ef ins outs
localWalk !targetTag# f pipe = go pipe
where
walkCont :: forall i o branchOuts. BranchCont (Penelope eh ef i o) branchOuts -> BranchCont (Penelope eh ef i o) branchOuts
walkCont (BranchCont k) = BranchCont (\recFrame -> go (k recFrame))
walkRegion :: forall sig. Region eh ef sig -> Region eh ef sig
walkRegion (Region p) = Region (go p)
go :: forall i o. Penelope eh ef i o -> Penelope eh ef i o
go = \case
Route morph ->
Route morph
LiftPure inMap pureF (BranchCont k) ->
LiftPure inMap pureF (BranchCont (\recFrame -> go (k recFrame)))
OpF tag# inMap opAny conts
| isTrue# (tag# `eqWord#` targetTag#) ->
let wrapAskCont :: forall branchOuts. BranchCont (Penelope eh ef i o) branchOuts -> BranchCont (Penelope eh ef i o) branchOuts
wrapAskCont (BranchCont k) = BranchCont $ \retFrame ->
let stage = LiftPure
(unsafeMkRowMorph [0])
(\recFrame ->
let !(RowRep (Rep _ arr)) = recFrame
!(Identity valAny) = indexSmallArray arr 0
!curEnv = unsafeCoerce# valAny :: r
!newEnv = f curEnv
in insertField (Field @"val") newEnv emptyRecord
)
(BranchCont (\_ -> go (k retFrame)))
in stage
in OpF tag# inMap opAny (rmapFused wrapAskCont conts)
| otherwise ->
OpF tag# inMap opAny (rmapFused walkCont conts)
OpH tag# inMap regions opAny conts ->
OpH tag# inMap (rmapFused walkRegion regions) opAny (rmapFused walkCont conts)
ScopeRow ehMorph efMorph subPipe ->
ScopeRow ehMorph efMorph (unsafeCoerce# (go (unsafeCoerce# subPipe)))
{- |
Modify the reader environment within a scoped sub-computation at a custom effect label.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE localAt #-}
localAt ::
forall (l :: Symbol) r eh ef a.
ConstructorIndex l ef =>
(r -> r)
-> Eff eh ef a
-> Eff eh ef a
localAt f (Eff pipe) =
let !targetTag# = case constructorIndex @_ @l @ef of W# w# -> w#
in Eff (localWalk targetTag# f pipe)
{- |
Modify the reader environment within a scoped sub-computation defaulting to label @"reader"@.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE local #-}
local ::
forall r eh ef a.
ConstructorIndex "reader" ef =>
(r -> r)
-> Eff eh ef a
-> Eff eh ef a
local = localAt @"reader"
{- |
Walk the 'Penelope' polycategorical graph, intercepting every 'Throw' node targeted
by 'targetTag#' and substituting the handler continuation.
See Note [Two-Phase Weaving and Unweaving] [ref:note-two-phase-engine].
-}
catchWalk ::
forall eh ef e a ins outs.
Word#
-> (e -> Eff eh ef a)
-> Penelope eh ef ins outs
-> Penelope eh ef ins outs
catchWalk !targetTag# handler pipe = go pipe
where
walkCont :: forall i o branchOuts. BranchCont (Penelope eh ef i o) branchOuts -> BranchCont (Penelope eh ef i o) branchOuts
walkCont (BranchCont k) = BranchCont (\recFrame -> go (k recFrame))
walkRegion :: forall sig. Region eh ef sig -> Region eh ef sig
walkRegion (Region p) = Region (go p)
go :: forall i o. Penelope eh ef i o -> Penelope eh ef i o
go = \case
Route morph ->
Route morph
LiftPure inMap pureF (BranchCont k) ->
LiftPure inMap pureF (BranchCont (\recFrame -> go (k recFrame)))
OpF tag# _inMap opAny _conts
| isTrue# (tag# `eqWord#` targetTag#) ->
let !(Throw err) = unsafeCoerce# opAny :: ThrowEff e Any
!(Eff handlerPipe) = handler err
in unsafeCoerce# handlerPipe
| otherwise ->
OpF tag# _inMap opAny (rmapFused walkCont _conts)
OpH tag# inMap regions opAny conts ->
OpH tag# inMap (rmapFused walkRegion regions) opAny (rmapFused walkCont conts)
ScopeRow ehMorph efMorph subPipe ->
ScopeRow ehMorph efMorph (unsafeCoerce# (go (unsafeCoerce# subPipe)))
{- |
Catch and recover from algebraic errors at a custom effect label.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE catchErrorAt #-}
catchErrorAt ::
forall (l :: Symbol) e eh ef a.
ConstructorIndex l ef =>
Eff eh ef a
-> (e -> Eff eh ef a)
-> Eff eh ef a
catchErrorAt (Eff pipe) handler =
let !targetTag# = case constructorIndex @_ @l @ef of W# w# -> w#
in Eff (catchWalk targetTag# handler pipe)
{- |
Catch and recover from algebraic errors defaulting to effect label @"error"@.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE catchError #-}
catchError ::
forall e eh ef a.
ConstructorIndex "error" ef =>
Eff eh ef a
-> (e -> Eff eh ef a)
-> Eff eh ef a
catchError = catchErrorAt @"error"
{- |
Elaborator for higher-order 'Local' effect unrolling into pure first-order 'ef' operations.
See Note [Two-Phase Weaving and Unweaving] [ref:note-two-phase-engine].
-}
{-# INLINE localElaborator #-}
localElaborator ::
forall (l :: Symbol) ef r.
ConstructorIndex l ef =>
Elaborator '[] ef (Local r)
localElaborator = Elaborator $ \inMap regs opAny kConts ->
let !(Identity bodyRegAny) = indexSmallArray (let RowRep (Rep _ arr) = regs in arr) 0
!(Region bodyPipe) = unsafeCoerce# bodyRegAny
!f = unsafeCoerce# opAny :: r -> r
!targetTag# = case constructorIndex @_ @l @ef of W# w# -> w#
!transformedBody = localWalk targetTag# f (unsafeCoerce# bodyPipe)
!(Identity retContAny) = indexSmallArray (let RowRep (Rep _ arr) = kConts in arr) 0
!(BranchCont retK) = unsafeCoerce# retContAny
in preComposeRoute inMap (unsafeCoerce# (transformedBody `seqPenelope` retK (unsafeCoerce# emptyRecord)))
{- |
Elaborator for higher-order 'Catch' effect unrolling into pure first-order 'ef' operations.
See Note [Two-Phase Weaving and Unweaving] [ref:note-two-phase-engine].
-}
{-# INLINE catchElaborator #-}
catchElaborator ::
forall (l :: Symbol) ef e.
ConstructorIndex l ef =>
Elaborator '[] ef (Catch e)
catchElaborator = Elaborator $ \inMap regs _opAny kConts ->
let !(Identity bodyRegAny) = indexSmallArray (let RowRep (Rep _ arr) = regs in arr) 0
!(Region bodyPipe) = unsafeCoerce# bodyRegAny
!(Identity handlerRegAny) = indexSmallArray (let RowRep (Rep _ arr) = regs in arr) 1
!(Region handlerPipe) = unsafeCoerce# handlerRegAny
!targetTag# = case constructorIndex @_ @l @ef of W# w# -> w#
!transformedBody = catchWalk targetTag# (\_err -> Eff (unsafeCoerce# handlerPipe)) (unsafeCoerce# bodyPipe)
!(Identity retContAny) = indexSmallArray (let RowRep (Rep _ arr) = kConts in arr) 0
!(BranchCont retK) = unsafeCoerce# retContAny
in preComposeRoute inMap (unsafeCoerce# (transformedBody `seqPenelope` retK (unsafeCoerce# emptyRecord)))
{- |
Construct an 'EffHandler' supplying an environment @r@ for 'AskEff'.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE readerHandler #-}
readerHandler ::
forall r m.
Monad m =>
r
-> EffHandler m (AskEff r)
readerHandler env = mkEffHandler $ \case
Ask -> pure env
{- |
Execute a pure 'Eff' computation supplying an environment @r@.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE runReaderPure #-}
runReaderPure ::
forall r a.
r
-> Eff '[] '["reader" ':= UnaryEffect (AskEff r)] a
-> a
runReaderPure env prog =
let handlers = insertField (Field @"reader") (readerHandler env) emptyRecord
Identity res = runEffM handlers prog
in res
{- |
Construct an 'EffHandler' interpreting 'ThrowEff' under the 'Either e' error monad.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE errorHandler #-}
errorHandler ::
forall e.
EffHandler (Either e) (ThrowEff e)
errorHandler = mkEffHandler $ \(Throw err) -> Left err
{- |
Execute a pure 'Eff' computation catching errors into 'Either e a'.
See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE runErrorPure #-}
runErrorPure ::
forall e a.
Eff '[] '["error" ':= UnaryEffect (ThrowEff e)] a
-> Either e a
runErrorPure prog =
let handlers = insertField (Field @"error") errorHandler emptyRecord
in runEffM handlers prog