penelope-core-0.1.0.0: src/Penelope/Core/Engine/Partial.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 #-}
{-# LANGUAGE UndecidableInstances #-}
{- |
Module : Penelope.Core.Engine.Partial
Description : Layered effect peeling and partial interpretation
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 partial effect peeling ('interpretSub'), lowering a subset of first-order
operations ('efSub') while leaving residual unhandled effects ('efRem') intact in the syntax tree.
See Note [Two-Phase Weaving and Unweaving] [ref:note-two-phase-engine].
-}
module Penelope.Core.Engine.Partial
( -- * Compile-Time Row Partitioning
RowPartition (..)
, PartitionPlan (..)
, mkPartitionPlan
-- * Partial Interpretation Engine
, interpretSub
) where
import Control.Monad.ST (runST)
import Data.Functor.Identity (Identity (..))
import Data.Kind (Type)
import Data.Primitive.PrimArray
( PrimArray
, indexPrimArray
, newPrimArray
, sizeofPrimArray
, unsafeFreezePrimArray
, writePrimArray
)
import Data.Primitive.SmallArray (indexSmallArray)
import GHC.Exts (Int (..), int2Word#, unsafeCoerce#, word2Int#)
import Penelope.Core.Engine.Elaborate (MapRow)
import Penelope.Core.Engine.Interpret (Handler (..))
import Penelope.Core.Syntax
( BranchCont (..)
, Penelope (..)
, PolyEffect
)
import Penelope.Core.Wiring (rmapFused)
import Polytropos (Record, RowOf)
import Polytropos.Core (Rep (..), RowRep (..))
import Polytropos.RowMorph (KnownRowMorph (..), RowMorph (..), composeRowMorph, indexRowMorph)
{- |
Runtime partition plan capturing the static decomposition of row @full@ into
disjoint sub-rows @sub@ and @rem@.
The 'ppLookup' unboxed array maps each constructor tag @fullIdx@ to:
* @val >= 0@: 0-based index in @sub@.
* @val < 0@: 0-based index in @rem@ (encoded as @-1 - val@).
See Note [Two-Phase Weaving and Unweaving] [ref:note-two-phase-engine].
-}
data PartitionPlan (sub :: RowOf k) (rem :: RowOf k) (full :: RowOf k) = PartitionPlan
{ ppSubMorph :: !(RowMorph sub full)
, ppRemMorph :: !(RowMorph rem full)
, ppLookup :: !(PrimArray Int)
}
-- | Synthesizes an unboxed lookup array from compile-time row morphisms.
{-# INLINE mkPartitionPlan #-}
mkPartitionPlan ::
RowMorph sub full
-> RowMorph rem full
-> PartitionPlan sub rem full
mkPartitionPlan mSub@(RowMorph arrSub) mRem@(RowMorph arrRem) =
let !nSub = sizeofPrimArray arrSub
!nRem = sizeofPrimArray arrRem
!totalSz = nSub + nRem
!lookupArr = runST $ do
m <- newPrimArray totalSz
let initLoop !i
| i >= totalSz = pure ()
| otherwise = writePrimArray m i (-1) >> initLoop (i + 1)
initLoop 0
let fillSub !k
| k >= nSub = pure ()
| otherwise = do
let !fullIdx = indexPrimArray arrSub k
writePrimArray m fullIdx k
fillSub (k + 1)
fillSub 0
let fillRem !j
| j >= nRem = pure ()
| otherwise = do
let !fullIdx = indexPrimArray arrRem j
writePrimArray m fullIdx (-1 - j)
fillRem (j + 1)
fillRem 0
unsafeFreezePrimArray m
in PartitionPlan mSub mRem lookupArr
{- |
Evidence that row @full@ is partitioned into disjoint sub-rows @sub@ and @rem@.
See Note [Two-Phase Weaving and Unweaving] [ref:note-two-phase-engine].
-}
class (KnownRowMorph sub full, KnownRowMorph rem full) => RowPartition (sub :: RowOf k) (rem :: RowOf k) (full :: RowOf k) where
rowPartition :: PartitionPlan sub rem full
instance {-# OVERLAPPABLE #-} (KnownRowMorph sub full, KnownRowMorph rem full) => RowPartition sub rem full where
{-# INLINE rowPartition #-}
rowPartition = mkPartitionPlan (rowMorph @sub @full) (rowMorph @rem @full)
{- |
Partially peel and interpret a subset of first-order effects @efSub@ from @efFull@,
leaving residual effects @efRem@ intact in the syntax tree.
Dispatches each target effect via \(O(1)\) unboxed lookup in the partition table,
invoking the provided handler, while re-tagging unhandled residual operations to @efRem@.
See Note [Two-Phase Weaving and Unweaving] [ref:note-two-phase-engine].
-}
{-# INLINE interpretSub #-}
interpretSub ::
forall (efSub :: RowOf PolyEffect) (efRem :: RowOf PolyEffect) (efFull :: RowOf PolyEffect)
(ins :: RowOf Type) (outs :: RowOf Type).
RowPartition efSub efRem efFull =>
Record (MapRow (Handler efRem (Penelope '[] efRem)) efSub)
-> Penelope '[] efFull ins outs
-> Penelope '[] efRem ins outs
interpretSub !handlers = go
where
plan = rowPartition @_ @efSub @efRem @efFull
arrLookup = ppLookup plan
interpCont ::
BranchCont (Penelope '[] efFull i o) branchOuts
-> BranchCont (Penelope '[] efRem i o) branchOuts
interpCont (BranchCont k) = BranchCont (\branchFrame -> go (k branchFrame))
go :: Penelope '[] efFull i o -> Penelope '[] efRem i o
go = \case
Route morph ->
Route morph
LiftPure inMap f cont ->
LiftPure inMap f (interpCont cont)
OpF tag# inMap opAny conts ->
let !idx = I# (word2Int# tag#)
!entry = indexPrimArray arrLookup idx
!interpretedConts = rmapFused interpCont conts
in if entry >= 0
then
let !(RowRep (Rep _ arr)) = handlers
!(Identity handlerAny) = indexSmallArray arr entry
!(Handler handleFn) = unsafeCoerce# handlerAny
in handleFn (unsafeCoerce# inMap) opAny interpretedConts
else
let !(I# i#) = -1 - entry
!remIdx# = int2Word# i#
in OpF remIdx# inMap opAny interpretedConts
ScopeRow _ehMorph efMorph subPipe ->
goInScope (unsafeCoerce# efMorph) (unsafeCoerce# subPipe)
OpH {} ->
errorWithoutStackTrace "Penelope.Core.Engine.Partial: impossible OpH in empty '[] higher-order row"
goInScope :: RowMorph efInner efFull -> Penelope '[] efInner i o -> Penelope '[] efRem i o
goInScope !efM = \case
Route morph ->
Route morph
LiftPure inMap f (BranchCont k) ->
LiftPure inMap f (BranchCont (\branchFrame -> goInScope efM (k branchFrame)))
OpF tag# inMap opAny conts ->
let !idx = indexRowMorph efM (I# (word2Int# tag#))
!entry = indexPrimArray arrLookup idx
!interpretedConts = rmapFused (\(BranchCont k) -> BranchCont (\branchFrame -> goInScope efM (k branchFrame))) conts
in if entry >= 0
then
let !(RowRep (Rep _ arr)) = handlers
!(Identity handlerAny) = indexSmallArray arr entry
!(Handler handleFn) = unsafeCoerce# handlerAny
in handleFn (unsafeCoerce# inMap) opAny interpretedConts
else
let !(I# i#) = -1 - entry
!remIdx# = int2Word# i#
in OpF remIdx# inMap opAny interpretedConts
ScopeRow _ehInner efMorph2 inner ->
goInScope (composeRowMorph (unsafeCoerce# efMorph2) efM) (unsafeCoerce# inner)
OpH {} ->
errorWithoutStackTrace "Penelope.Core.Engine.Partial: impossible OpH in empty '[] higher-order row"