packages feed

penelope-effects-0.1.0.0: src/Penelope/Effects/Control.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.Control
Description : Native multi-region and multi-exit control effect battery
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 standard native \((m,n,k,r)\)-ary control effects:
1. Multi-region 'While' featuring scoped sub-regions (@"cond"@ and @"body"@)
2. Dynamic disjunctive \(k\)-way exit branching via 'Switch'
3. Transactional workflows with compensation handlers via 'Saga'

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
module Penelope.Effects.Control
  ( -- * Multi-Region While
    While (..)
  , whileElaborator
  , whileEff

    -- * Multi-Exit Switch
  , Switch (..)
  , switchHandler
  , sendSwitch

    -- * Transactional Saga
  , Saga (..)
  , sagaElaborator
  , runSagaEff
  ) where

import Data.Functor.Identity (Identity (..))
import Data.Kind (Type)
import Data.Primitive.SmallArray (indexSmallArray)
import GHC.Exts (Word (W#), unsafeCoerce#)
import GHC.TypeLits (Symbol)
import Penelope.Core.Engine.Elaborate (Elaborator (..))
import Penelope.Core.Engine.Interpret
  ( FrameKleisli (..)
  , Handler (..)
  , runPenelopePure
  )
import Penelope.Core.Syntax
  ( BranchCont (..)
  , Penelope (..)
  , PortSig (..)
  , Region (..)
  )
import Penelope.Effects.Unary (Eff (..))
import Polytropos (Record, RowOf)
import Polytropos.Core (ConstructorIndex (..), Pair (..), Rep (..), RowRep (..))
import Polytropos.RowMorph (gatherRep, unsafeMkRowMorph)

{- |
Higher-order scoped 'While' loop with two explicit sub-regions:
1. @"cond"@: Evaluates loop termination condition over state @st@.
2. @"body"@: Transforms current state into successor state.

Exits with final state @st@ via @"exit"@ branch.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data While (r :: PortSig -> Type) (regs :: RowOf PortSig) (opIns :: RowOf Type) (exits :: RowOf (RowOf Type)) where
  WhileOp :: While r
    '[ "cond" ':= 'PortSig st '["cond" ':= Bool]
     , "body" ':= 'PortSig st st
     ]
    st
    '[ "exit" ':= st ]

{- |
Elaborator for 'While' lowering multi-region loop execution into pure iterative steps.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE whileElaborator #-}
whileElaborator ::
  forall ef.
  Elaborator '[] ef While
whileElaborator = Elaborator $ \inMap regs _ kConts ->
  LiftPure inMap
    (\stRec ->
      let !(Identity condRegAny) = indexSmallArray (let RowRep (Rep _ arr) = regs in arr) 0
          !(Region condPipe) = unsafeCoerce# condRegAny
          !(Identity bodyRegAny) = indexSmallArray (let RowRep (Rep _ arr) = regs in arr) 1
          !(Region bodyPipe) = unsafeCoerce# bodyRegAny

          loop curSt =
            let condRec = runPenelopePure condPipe (unsafeCoerce# curSt)
                !(Identity condVal) = indexSmallArray (let RowRep (Rep _ sArr) = condRec in sArr) 0
                !cond = unsafeCoerce# condVal :: Bool
            in if cond
               then let nextSt = runPenelopePure bodyPipe (unsafeCoerce# curSt)
                    in loop (unsafeCoerce# nextSt)
               else curSt
      in loop stRec
    )
    (BranchCont $ \finalSt ->
      let !(Identity exitContAny) = indexSmallArray (let RowRep (Rep _ arr) = kConts in arr) 0
          !(BranchCont exitK) = unsafeCoerce# exitContAny
      in exitK (unsafeCoerce# finalSt)
    )

{- |
Convenient monadic 'While' combinator for 'Eff'.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE whileEff #-}
whileEff ::
  forall st eh ef.
  (st -> Eff eh ef Bool)
  -> (st -> Eff eh ef st)
  -> st
  -> Eff eh ef st
whileEff cond body = go
  where
    go !s = do
      b <- cond s
      if b
        then do
          s' <- body s
          go s'
        else pure s

{- |
First-order 'Switch' effect with dynamic disjunctive \(k\)-way exits.
Dispatches directly using a zero-allocation branch jump table.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data Switch (ins :: RowOf Type) (exits :: RowOf (RowOf Type)) where
  SwitchOp :: Switch '["tag" ':= Int] exits

{- |
Handler for 'Switch' dispatching multi-exit branch continuations in \(O(1)\).

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE switchHandler #-}
switchHandler ::
  forall ef m.
  Handler ef (FrameKleisli m) Switch
switchHandler = Handler $ \inMap _ exits -> FrameKleisli $ \inFrame -> do
  let opInFrame = RowRep (gatherRep inMap (let RowRep inRep = inFrame in inRep))
      !(Identity tagVal) = indexSmallArray (let RowRep (Rep _ sArr) = opInFrame in sArr) 0
      !tag = unsafeCoerce# tagVal :: Int
      !(RowRep (Rep _ contArr)) = exits
      !(Identity contAny) = indexSmallArray contArr tag
      !(BranchCont k) = unsafeCoerce# contAny
  runFrameKleisli (k (unsafeCoerce# opInFrame)) inFrame

{- |
Smart constructor injecting a 'Switch' operation with \(k\) exit branches.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE sendSwitch #-}
sendSwitch ::
  forall (l :: Symbol) exits eh ef outs.
  (ConstructorIndex l ef) =>
  Record exits
  -> Penelope eh ef '["tag" ':= Int] outs
sendSwitch conts =
  let !tag# = case constructorIndex @_ @l @ef of W# w# -> w#
  in OpF tag# (unsafeMkRowMorph [0]) (unsafeCoerce# (SwitchOp @'[])) (unsafeCoerce# conts)

{- |
Transactional 'Saga' higher-order effect with three scoped sub-regions:
1. @"try"@: Primary forward action
2. @"compensate"@: Rollback compensation handler invoked on failure
3. @"catch"@: Recovery handler producing alternative outcome

Exits into either @"committed"@ or @"aborted"@.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
data Saga (r :: PortSig -> Type) (regs :: RowOf PortSig) (opIns :: RowOf Type) (exits :: RowOf (RowOf Type)) where
  SagaOp :: Saga r
    '[ "try"        ':= 'PortSig ins mid
     , "compensate" ':= 'PortSig mid errRow
     , "catch"      ':= 'PortSig errRow outRow
     ]
    ins
    '[ "committed"  ':= mid
     , "aborted"    ':= outRow
     ]

{- |
Elaborator for 'Saga' wiring forward actions and compensation paths.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE sagaElaborator #-}
sagaElaborator ::
  forall ef.
  Elaborator '[] ef Saga
sagaElaborator = Elaborator $ \inMap regs _ kConts ->
  LiftPure inMap
    (\inRec ->
      let !(Identity tryRegAny) = indexSmallArray (let RowRep (Rep _ arr) = regs in arr) 0
          !(Region tryPipe) = unsafeCoerce# tryRegAny
      in runPenelopePure tryPipe (unsafeCoerce# inRec)
    )
    (BranchCont $ \midRec ->
      let !(Identity commitContAny) = indexSmallArray (let RowRep (Rep _ arr) = kConts in arr) 0
          !(BranchCont commitK) = unsafeCoerce# commitContAny
      in commitK (unsafeCoerce# midRec)
    )

{- |
Execute a transactional saga in 'Eff': runs action, and if an exception occurs,
executes the compensation handler before returning the failure.

See Note [Core Architecture] [ref:note-core-architecture] and
Note [Zero-Cost Surface Specializations] [ref:note-surface-facades].
-}
{-# INLINE runSagaEff #-}
runSagaEff ::
  forall a b err eh ef.
  Eff eh ef a
  -> (a -> Eff eh ef ())
  -> (a -> Eff eh ef (Either err b))
  -> Eff eh ef (Either err b)
runSagaEff action compensate body = do
  a <- action
  res <- body a
  case res of
    Left _ -> do
      compensate a
      pure res
    Right b ->
      pure (Right b)