packages feed

penelope-effects-0.1.0.0: src/Penelope/Effects/Coroutine.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.Coroutine
Description : Delimited continuations, frame suspension, and non-determinism
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 delimited continuations with frame suspension and resumption:
1. 'Coroutine' effect with 'Yield' and 'Await' primitives
2. Generator execution via 'runGenerator' and 'runGeneratorIO'
3. 'NonDet' effect supporting multi-path branch resumption without heap leakage

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
module Penelope.Effects.Coroutine
  ( -- * Coroutine & Suspension
    Coroutine
  , Suspension (..)
  , YieldEff (..)
  , AwaitEff (..)
  , yield
  , await

    -- * Generator Runners
  , runGenerator
  , runGeneratorIO

    -- * Non-Determinism
  , NonDet
  , NonDetEff (..)
  , branch
  , emptyNonDet
  , nonDetHandler
  , runNonDet
  ) where

import Data.IORef (modifyIORef', newIORef, readIORef)
import Data.Kind (Type)
import GHC.TypeLits (Symbol)
import Penelope.Core.Syntax (PolyEffect)
import Penelope.Effects.Unary
  ( Eff (..)
  , EffHandler
  , UnaryEffect
  , mkEffHandler
  , runEffM
  , send
  )
import Polytropos (Field (..), emptyRecord, insertField)
import Polytropos.Core (ConstructorIndex (..), Pair (..))

{- |
Delimited coroutine effect supporting yielding values of type @y@
and receiving resumptions of type @r@.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data Coroutine (y :: Type) (r :: Type) :: PolyEffect

{- |
Status of a suspended coroutine pipeline.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data Suspension y r a
  = Done a
  | Suspended y (r -> Suspension y r a)

{- |
Unary effect GADT representing a yield operation.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data YieldEff (y :: Type) (a :: Type) where
  Yield :: y -> YieldEff y ()

{- |
Unary effect GADT representing an await operation.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data AwaitEff (y :: Type) (a :: Type) where
  Await :: AwaitEff y y

{- |
Yield a value of type @y@ from within an 'Eff' computation.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE yield #-}
yield ::
  forall (l :: Symbol) y eh ef.
  ConstructorIndex l ef =>
  y
  -> Eff eh ef ()
yield y = send @l (Yield y)

{- |
Await a value of type @y@ from an enclosing producer.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE await #-}
await ::
  forall (l :: Symbol) y eh ef.
  ConstructorIndex l ef =>
  Eff eh ef y
await = send @l Await

-- | Simple internal state monad for pure generator evaluation.
newtype GenState s a = GenState { runGenState :: s -> (a, s) }

instance Functor (GenState s) where
  {-# INLINE fmap #-}
  fmap f (GenState g) = GenState $ \s ->
    let (a, s') = g s in (f a, s')

instance Applicative (GenState s) where
  {-# INLINE pure #-}
  pure a = GenState $ \s -> (a, s)
  {-# INLINE (<*>) #-}
  GenState gf <*> GenState ga = GenState $ \s ->
    let (f, s1) = gf s
        (a, s2) = ga s1
    in (f a, s2)

instance Monad (GenState s) where
  {-# INLINE (>>=) #-}
  GenState g >>= k = GenState $ \s ->
    let (a, s1) = g s
    in runGenState (k a) s1

{- |
Execute a generator 'Eff' computation, returning the list of yielded items
along with the final return value in a pure pair @([y], a)@.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE runGenerator #-}
runGenerator ::
  forall y a.
  Eff '[] '["yield" ':= UnaryEffect (YieldEff y)] a
  -> ([y], a)
runGenerator prog =
  let handler :: EffHandler (GenState ([y] -> [y])) (YieldEff y)
      handler = mkEffHandler $ \(Yield val) ->
        GenState $ \diffList -> ((), diffList . (val :))

      handlers = insertField (Field @"yield") handler emptyRecord
      GenState stAction = runEffM handlers prog
      (finalVal, diffRes) = stAction id
  in (diffRes [], finalVal)

{- |
Execute a generator 'Eff' computation under 'IO' using an 'IORef'.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE runGeneratorIO #-}
runGeneratorIO ::
  forall y a.
  Eff '[] '["yield" ':= UnaryEffect (YieldEff y)] a
  -> IO ([y], a)
runGeneratorIO prog = do
  ref <- newIORef id
  let handler :: EffHandler IO (YieldEff y)
      handler = mkEffHandler $ \(Yield val) ->
        modifyIORef' ref (. (val :))

      handlers = insertField (Field @"yield") handler emptyRecord
  res <- runEffM handlers prog
  diffRes <- readIORef ref
  pure (diffRes [], res)

{- |
Polycategorical non-determinism effect.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data NonDet :: PolyEffect

{- |
Unary effect GADT representing non-deterministic choice points.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data NonDetEff (a :: Type) where
  Branch :: NonDetEff Bool
  Empty  :: NonDetEff a

{- |
Emit a binary choice point in 'Eff'.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE branch #-}
branch ::
  forall (l :: Symbol) eh ef.
  ConstructorIndex l ef =>
  Eff eh ef Bool
branch = send @l Branch

{- |
Abort the current non-deterministic branch.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE emptyNonDet #-}
emptyNonDet ::
  forall (l :: Symbol) a eh ef.
  ConstructorIndex l ef =>
  Eff eh ef a
emptyNonDet = send @l Empty

{- |
Handler for 'NonDetEff' into the list monad '[]'.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE nonDetHandler #-}
nonDetHandler :: EffHandler [] NonDetEff
nonDetHandler = mkEffHandler $ \case
  Branch -> [True, False]
  Empty  -> []

{- |
Execute a non-deterministic 'Eff' computation, returning all valid execution paths.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE runNonDet #-}
runNonDet ::
  Eff '[] '["nondet" ':= UnaryEffect NonDetEff] a
  -> [a]
runNonDet prog =
  let handlers = insertField (Field @"nondet") nonDetHandler emptyRecord
  in runEffM handlers prog