packages feed

penelope-static-0.1.0.0: src/Penelope/Static/Analysis.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UnboxedTuples #-}

{- |
Module      : Penelope.Static.Analysis
Description : Static pre-execution Cayley analysis, capability extraction, and liveness analysis
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 compile-time and pre-execution static inspection of Penelope pipelines without
evaluating runtime payloads.
See Note [Static Reflection and Cayley Analysis] [ref:note-static-cayley].
-}
module Penelope.Static.Analysis
  ( -- * Static Accumulator
    StaticPenelope (..)
  , annotateStatic
  , seqStatic
  , frameWireStatic

    -- * Phantom Port Frame Construction
  , phantomFrame

    -- * Liveness and Capability Telemetry
  , LivenessReport (..)
  , morphReadSet
  , analyzeLivenessAndEffects
  ) where

import Data.Functor.Identity (Identity (..))
import Data.IntSet (IntSet)
import qualified Data.IntSet as IntSet
import Data.Kind (Type)
import Data.Primitive.PrimArray (sizeofPrimArray)
import Data.Primitive.SmallArray (indexSmallArray, newSmallArray, runSmallArray, sizeofSmallArray)
import GHC.Exts (Int (..), unsafeCoerce#, word2Int#)
import Penelope.Core
  ( BranchCont (..)
  , Penelope (..)
  , PolyEffect
  , PolyHEffect
  , PortSig (..)
  , Region (..)
  , (>&>)
  , seqPenelope
  )
import Polytropos (Record, RowOf)
import Polytropos.Core (Rep (..), RowRep (..))
import Polytropos.RowMorph
  ( FrameWire (..)
  , RowMorph (..)
  , indexRowMorph
  )

{- |
Cayley static wrapper generalizing static effect accumulators to (m, n, k, r)-ary graphs.
Pairs an Applicative metadata accumulator @f ()@ with an executable 'Penelope' pipeline.

See Note [Static Reflection and Cayley Analysis] [ref:note-static-cayley].
-}
data StaticPenelope
       (f    :: Type -> Type)
       (eh   :: RowOf PolyHEffect)
       (ef   :: RowOf PolyEffect)
       (ins  :: RowOf Type)
       (outs :: RowOf Type)
  = StaticPenelope
  { spStatic   :: !(f ())
  , spPipeline :: !(Penelope eh ef ins outs)
  }

-- | Attach a static Applicative metadata requirement to any Penelope computation.
{-# INLINE annotateStatic #-}
annotateStatic ::
  f ()
  -> Penelope eh ef ins outs
  -> StaticPenelope f eh ef ins outs
annotateStatic !meta !pipe = StaticPenelope meta pipe

-- | Sequential composition accumulates static metadata via '*>' in O(1).
{-# INLINE seqStatic #-}
seqStatic ::
  Applicative f =>
  StaticPenelope f eh ef a b
  -> StaticPenelope f eh ef b c
  -> StaticPenelope f eh ef a c
seqStatic (StaticPenelope s1 p1) (StaticPenelope s2 p2) =
  StaticPenelope (s1 *> s2) (p1 `seqPenelope` p2)

-- | Frame-rule auto-wiring preserving and combining static metadata.
{-# INLINE frameWireStatic #-}
frameWireStatic ::
  forall (frameIn :: RowOf Type) (ins1 :: RowOf Type) (outs1 :: RowOf Type)
         (frameMid :: RowOf Type) (ins2 :: RowOf Type) (outs2 :: RowOf Type)
         (frameOut :: RowOf Type)
         (eh :: RowOf PolyHEffect) (ef :: RowOf PolyEffect)
         f.
  ( Applicative f
  , FrameWire frameIn ins1 outs1 frameMid
  , FrameWire frameMid ins2 outs2 frameOut
  ) =>
  StaticPenelope f eh ef ins1 outs1
  -> StaticPenelope f eh ef ins2 outs2
  -> StaticPenelope f eh ef frameIn frameOut
frameWireStatic (StaticPenelope s1 p1) (StaticPenelope s2 p2) =
  StaticPenelope (s1 *> s2) ((>&>) @_ @_ @_ @frameMid p1 p2)

{- |
Construct a phantom port frame for traversing downstream continuations without evaluating payloads.
Uses uninitialized/dummy identity slots that do not throw exceptions.

See Note [Static Reflection and Cayley Analysis] [ref:note-static-cayley].
-}
{-# INLINE phantomFrame #-}
phantomFrame :: Int -> Record r
phantomFrame !numPorts = RowRep $ Rep 0 $ runSmallArray $ do
  m <- newSmallArray numPorts (Identity (unsafeCoerce# ()))
  pure m

{- |
Static telemetry collected across the hypergraph without executing a single effect handler.

See Note [Static Reflection and Cayley Analysis] [ref:note-static-cayley].
-}
data LivenessReport = LivenessReport
  { lrReadFrameSlots     :: !IntSet
    -- ^ Indices of input ports in @ins@ that are actually read by at least one operation.
  , lrActiveFirstOrder   :: !IntSet
    -- ^ Slot indices in @ef@ actually invoked in the graph (for dead-handler pruning).
  , lrActiveHigherOrder  :: !IntSet
    -- ^ Slot indices in @eh@ actually invoked in the graph.
  , lrTotalRegions       :: !Int
    -- ^ Total number of nested higher-order regions ('OpH') in the graph.
  , lrMaxBranchingFactor :: !Int
    -- ^ Maximum exit arity encountered across all operations.
  } deriving stock (Show, Eq)

instance Semigroup LivenessReport where
  LivenessReport r1 f1 h1 reg1 b1 <> LivenessReport r2 f2 h2 reg2 b2 =
    LivenessReport
      (IntSet.union r1 r2)
      (IntSet.union f1 f2)
      (IntSet.union h1 h2)
      (reg1 + reg2)
      (max b1 b2)

instance Monoid LivenessReport where
  mempty = LivenessReport IntSet.empty IntSet.empty IntSet.empty 0 1

-- | Extract the set of frame slot indices read by a 'RowMorph'.
{-# INLINE morphReadSet #-}
morphReadSet :: RowMorph a b -> IntSet
morphReadSet (RowMorph primArr) =
  let !n = sizeofPrimArray primArr
      collect !i !acc
        | i >= n    = acc
        | otherwise = collect (i + 1) (IntSet.insert (indexRowMorph (RowMorph primArr) i) acc)
  in collect 0 IntSet.empty

{- |
Universal structural liveness and effect footprint analysis pass.
Directly inspects unpacked constructor tags, 'RowMorph' primitive arrays, and 'Record' lengths.

See Note [Static Reflection and Cayley Analysis] [ref:note-static-cayley].
-}
analyzeLivenessAndEffects ::
  forall eh ef ins outs.
  Penelope eh ef ins outs
  -> LivenessReport
analyzeLivenessAndEffects = go
  where
    analyzeRegionArr :: Record regions -> (Int, LivenessReport)
    analyzeRegionArr (RowRep (Rep _ arr)) =
      let !rCount = sizeofSmallArray arr
          step !i !acc
            | i >= rCount = acc
            | otherwise   =
                case (unsafeCoerce# (indexSmallArray arr i) :: Region eh ef ('PortSig ins outs)) of
                  Region sub -> step (i + 1) (acc <> analyzeLivenessAndEffects sub)
      in (rCount, step 0 mempty)

    analyzeContArr :: Record exits -> (Int, LivenessReport)
    analyzeContArr (RowRep (Rep _ arr)) =
      let !kCount = sizeofSmallArray arr
          step !i !acc
            | i >= kCount = acc
            | otherwise   =
                let !(BranchCont kFn) = unsafeCoerce# (indexSmallArray arr i)
                in step (i + 1) (acc <> go (kFn (phantomFrame 64)))
      in (kCount, step 0 mempty)

    go :: Penelope eh ef ins outs -> LivenessReport
    go = \case
      Route morph ->
        mempty { lrReadFrameSlots = morphReadSet morph }

      LiftPure inMap _ (BranchCont kFn) ->
        mempty { lrReadFrameSlots = morphReadSet inMap }
          <> go (kFn (phantomFrame 64))

      OpF tag# inMap _ conts ->
        let !slotIdx         = I# (word2Int# tag#)
            (!k, contReport) = analyzeContArr conts
        in LivenessReport
             { lrReadFrameSlots     = morphReadSet inMap
             , lrActiveFirstOrder   = IntSet.singleton slotIdx
             , lrActiveHigherOrder  = IntSet.empty
             , lrTotalRegions       = 0
             , lrMaxBranchingFactor = k
             } <> contReport

      OpH tag# inMap regions _ conts ->
        let !slotIdx         = I# (word2Int# tag#)
            (!r, regReport)  = analyzeRegionArr regions
            (!k, contReport) = analyzeContArr conts
        in LivenessReport
             { lrReadFrameSlots     = morphReadSet inMap
             , lrActiveFirstOrder   = IntSet.empty
             , lrActiveHigherOrder  = IntSet.singleton slotIdx
             , lrTotalRegions       = r
             , lrMaxBranchingFactor = k
             } <> regReport <> contReport

      ScopeRow (RowMorph ehMap) (RowMorph efMap) subPipe ->
        let inner = analyzeLivenessAndEffects subPipe
            remapSet primArr s =
              IntSet.fromList [ indexRowMorph (RowMorph primArr) i | i <- IntSet.toList s ]
        in inner
             { lrActiveHigherOrder = remapSet ehMap (lrActiveHigherOrder inner)
             , lrActiveFirstOrder  = remapSet efMap (lrActiveFirstOrder inner)
             }