packages feed

polytropos-core-0.1.0.3: src/Polytropos/RowMorph.hs

{-|
Module      : Polytropos.RowMorph
Description : Reified unboxed row morphisms and primitive gather/scatter operations
Copyright   : (c) 2026 The Polytropos Authors
License     : EUPL-1.2 OR Apache-2.0
Maintainer  : nadia.yvette.chambers@ik.me
Stability   : experimental
Portability : non-portable (GHC extensions)

Reified unboxed row morphisms and primitive gather/scatter operations for @polytropos@.

= Architectural Overview & Rationale

In conventional polymorphic record and pipeline architectures, wire routing and subrow
projection typically incur either:
1. Opaque runtime closures (@arr (\\(a, (b, c)) -> (b, a))@) that prevent static inspection,
   optimizer inlining, and peephole fusion.
2. Inductive type-level searches (@KnownNat@ / inductive dictionary trees) that generate
   \(O(N)\) or \(O(N^2)\) pointer-chasing calls at runtime.

'RowMorph' collapses row routing and projection into a single unboxed 'PrimArray' 'Int'
(backed by a contiguous 'GHC.Exts.ByteArray#' primitive). Each index @i@ in @[0 .. |r1| - 1]@
contains the literal 0-based slot offset in the target row @r2@.

== Key Features & Operations

* __Unboxed Reification:__ 'RowMorph' has zero heap allocation overhead per element, storing
  pure integer indices densely in cache lines.
* __\(O(|r1|)\) Category Composition:__ 'composeRowMorph' composes two morphisms in a single
  branchless pass, computing @m23(m12(i))@ into a newly allocated 'PrimArray' without intermediate lists.
* __Contravariant Reduct Gather:__ 'gatherRep' extracts an arbitrary subrow or permutation from
  an existing 'Polytropos.Core.Rep' in \(O(|sub|)\) time with a single 'Data.Primitive.SmallArray.SmallArray' allocation.
* __In-Place Scatter:__ 'scatterRep' writes back updated subrow slots into an existing 'Rep'
  in \(O(|full|)\) time via 'Data.Primitive.SmallArray.thawSmallArray'.
* __Compile-Time Synthesis:__ The evidence class 'KnownRowMorph' is solved directly by
  @Polytropos.Plugin@, emitting literal index vectors into Core AST with zero runtime dictionary search.
* __Frame-Rule Auto-Wiring:__ 'FramePlan' and 'FrameWire' generalize binary port routing
  to stateful execution frames with automatic output row synthesis (see Note [Frame-Rule Automatic Wire Synthesis] [ref:note-frame-rule-wiring]).

= Note [Reified Row Morphisms and Gather-Scatter] [tag:note-row-morph-routing]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A row morphism @RowMorph r1 r2@ models a structural embedding / projection map from @r1@ into @r2@.
Rather than representing morphisms as functions @Record r2 -> Record r1@ or type-level inductive
witnesses, 'RowMorph' reifies the mapping as a flat primitive array of integers.

1. Category Structure:
   - Identity: @idMorph = RowMorph [0, 1, .. n-1]@.
   - Composition: @composeRowMorph m12 m23@ computes @dstIdx = m23[m12[i]]@ for all @i@.
     Associativity @(m1 >>> m2) >>> m3 == m1 >>> (m2 >>> m3)@ holds strictly by array index equality.

2. Contravariant Reduct / Port Gather:
   Given a wider register frame or handler record @full@ and a required sub-signature @sub@,
   @gatherRep morph full@ acts as the contravariant reduct \(\mathrm{Mod}(\sigma)(M')\), pulling
   exactly the active ports or handler methods into a compact, contiguous memory buffer.

3. Frame-Rule Auto-Wiring:
   'FramePlan' and 'FrameWire' generalize binary port routing to stateful execution frames:
   an operation @(opIn -> opOut)@ inside @(frameIn -> frameOut)@ executes by gathering @opIn@
   from @frameIn@, copying surviving @frameIn@ slots to @frameOut@, and scattering @opOut@ into @frameOut@
   in two tight branchless loops.
   See Note [Frame-Rule Automatic Wire Synthesis] [ref:note-frame-rule-wiring].

= Note [Frame-Rule Automatic Wire Synthesis] [tag:note-frame-rule-wiring]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In polycategorical and symmetric monoidal effect systems (such as Penelope),
computation is organized into multi-input, multi-output arrows or pipeline stages
executed against an ambient state/register frame:

    stage :: Arrow p => p (Record opIn) (Record opOut)

When executing @stage@ inside a wider frame @frameIn@, the categorical frame rule
states that the ambient context survives unchanged except where explicitly modified:

    frame :: FrameWire frameIn opIn opOut frameOut
          => p (Record opIn) (Record opOut)
          -> p (Record frameIn) (Record frameOut)

1. Dual Gather-Scatter Lifecycle:
   Execution proceeds in two phases coordinated by 'FramePlan':
   a. Contravariant Port Gather:
      Before the operation runs, its input ports @opIn@ are gathered from @frameIn@
      via @fpGatherIn :: RowMorph opIn frameIn@.
   b. Frame Merge & Output Scatter ('executeFrameScatter'):
      After the operation produces @opOut@, the new frame @frameOut@ is synthesized:
      - Surviving @frameIn@ slots are written to @frameOut@ via @fpBaseScatter@.
      - Output ports @opOut@ are written to @frameOut@ via @fpOpScatter@, overwriting
        matching input ports and appending any fresh ports in two tight loops.

2. Compile-Time Synthesis by Polytropos.Plugin:
   Rather than performing associative map lookups or recursive type-level list
   rewriting at runtime, the evidence class 'FrameWire' is solved directly by
   the typechecker plugin:
   - Sub-Row & Type Verification: Asserts that every port in @opIn@ exists in @frameIn@.
     If polymorphic port type variables are present, nominal equality constraints
     (@inTy ~# fTy@) are emitted, driving bidirectional port inference across pipeline stages.
   - Canonical Row Overwrite: Computes @RowOverwrite frameIn opOut@ where matching labels
     in @frameIn@ receive updated types from @opOut@, untouched labels are preserved in
     their original order, and new labels from @opOut@ are appended.
   - Automatic Output Row Unification: Emits a wanted nominal equality constraint
     @frameOut ~# synthOutTy@. When chaining stages @stage1 >&> stage2@, downstream
     frame outputs are automatically inferred without manual type signatures.
   - Literal Core AST Synthesis: Synthesizes a call to 'unsafeMkFramePlan' with
     pre-computed integer lists for @gatherIdxs@, @baseIdxs@, and @opIdxs@,
     which bake directly into unboxed 'PrimArray' arrays in the binary with zero
     heap traversal.
-}

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RoleAnnotations #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TypeApplications #-}

module Polytropos.RowMorph
  ( -- * Reified Row Morphisms
    RowMorph(..)
  , KnownRowMorph(..)
  , unsafeMkRowMorph
  , composeRowMorph
  , indexRowMorph
    -- * Record Gather & Scatter over Rep
  , gatherRep
  , scatterRep
    -- * Frame-Rule Auto-Wiring Plan
  , FramePlan(..)
  , FrameWire(..)
  , unsafeMkFramePlan
  , executeFrameScatter
  ) where

import Control.Monad.ST
  ( runST
  )
import Data.Kind
  ( Constraint
  , Type
  )
import Data.Primitive.PrimArray
  ( PrimArray
  , indexPrimArray
  , newPrimArray
  , primArrayFromList
  , sizeofPrimArray
  , unsafeFreezePrimArray
  , writePrimArray
  )
import Data.Primitive.SmallArray
  ( indexSmallArray
  , newSmallArray
  , runSmallArray
  , sizeofSmallArray
  , thawSmallArray
  , writeSmallArray
  )
import Polytropos.Core
  ( Rep(..)
  )

{-------------------------------------------------------------------------------
  Reified Row Morphisms
-------------------------------------------------------------------------------}

-- | A compile-time synthesized slot injection map from @r1@ into @r2@.
--
-- Each index @i@ in @[0 .. |r1| - 1]@ maps to its target slot in @[0 .. |r2| - 1]@.
-- Backed by an unboxed 'PrimArray' 'Int' with nominal type role parameters,
-- preventing illicit coercions across distinct row types.
--
-- See Note [Reified Row Morphisms and Gather-Scatter] [ref:note-row-morph-routing]
type RowMorph :: forall {k1} {k2}. k1 -> k2 -> Type
type role RowMorph nominal nominal
newtype RowMorph (r1 :: k1) (r2 :: k2) = RowMorph
  { unRowMorph :: PrimArray Int }
  deriving stock (Eq, Show)

-- | Solved by @Polytropos.Plugin@ into a static 'RowMorph' CAF constant.
--
-- When @r1@ is a subrow or permutation of @r2@, the compiler plugin synthesizes
-- the literal permutation array and emits the instance dictionary directly into Core AST.
type KnownRowMorph :: forall {k1} {k2}. k1 -> k2 -> Constraint
class KnownRowMorph (r1 :: k1) (r2 :: k2) where
  rowMorph :: RowMorph r1 r2

-- | Entry point called by Core expressions synthesized by @Polytropos.Plugin@.
--
-- Must be marked @NOINLINE@ to establish an optimization barrier for the Core simplifier.
unsafeMkRowMorph :: forall {k1} {k2} (r1 :: k1) (r2 :: k2). [Int] -> RowMorph r1 r2
{-# NOINLINE unsafeMkRowMorph #-}
unsafeMkRowMorph idxs = RowMorph (primArrayFromList idxs)

-- | Reads the target slot index for the @i@-th entry of @r1@.
indexRowMorph :: RowMorph r1 r2 -> Int -> Int
{-# INLINE indexRowMorph #-}
indexRowMorph (RowMorph arr) !i = indexPrimArray arr i

-- | Category composition for 'RowMorph': \(\mathcal{O}(|r1|)\) primitive array indexing.
--
-- Given morphisms @m12 : r1 -> r2@ and @m23 : r2 -> r3@, computes @m13 : r1 -> r3@
-- where each slot @i@ in @r1@ maps to @m23(m12(i))@ in @r3@.
--
-- See Note [Reified Row Morphisms and Gather-Scatter] [ref:note-row-morph-routing]
composeRowMorph :: RowMorph r1 r2 -> RowMorph r2 r3 -> RowMorph r1 r3
{-# INLINE composeRowMorph #-}
composeRowMorph (RowMorph m12) (RowMorph m23) = RowMorph $ runST $ do
  let !n = sizeofPrimArray m12
  out <- newPrimArray n
  let go !i
        | i >= n    = unsafeFreezePrimArray out
        | otherwise = do
            let !midIdx = indexPrimArray m12 i
                !dstIdx = indexPrimArray m23 midIdx
            writePrimArray out i dstIdx
            go (i + 1)
  go 0

{-------------------------------------------------------------------------------
  Record Gather & Scatter over Rep
-------------------------------------------------------------------------------}

-- | Contravariant reduct / port gather: extracts @sub@ slots from @full@ in \(\mathcal{O}(|sub|)\).
--
-- Directly implements \(\mathrm{Mod}(\sigma)(M')\) for handler records, input port frames,
-- and structural subrow extraction with a single 'SmallArray' allocation.
--
-- See Note [Reified Row Morphisms and Gather-Scatter] [ref:note-row-morph-routing]
gatherRep :: RowMorph sub full -> Rep f a -> Rep f b
{-# INLINE gatherRep #-}
gatherRep (RowMorph morph) (Rep _ srcArr) = Rep 0 $ runSmallArray $ do
  let !n = sizeofPrimArray morph
  dstArr <- newSmallArray n (error "gatherRep: uninitialized slot")
  let go !i
        | i >= n    = pure dstArr
        | otherwise = do
            let !srcIdx = indexPrimArray morph i
            writeSmallArray dstArr i (indexSmallArray srcArr srcIdx)
            go (i + 1)
  go 0

-- | Overwrites @sub@ slots inside an existing @full@ record in \(\mathcal{O}(|full|)\).
--
-- Preserves the runtime constructor tag of the target representation while updating
-- matching slots in place via array thawing and non-overlapping writes.
scatterRep :: RowMorph sub full -> Rep f a -> Rep f b -> Rep f b
{-# INLINE scatterRep #-}
scatterRep (RowMorph morph) (Rep _ subArr) (Rep t fullArr) = Rep t $ runSmallArray $ do
  let !fullLen = sizeofSmallArray fullArr
      !subLen  = sizeofPrimArray morph
  dstArr <- thawSmallArray fullArr 0 fullLen
  let go !i
        | i >= subLen = pure dstArr
        | otherwise   = do
            let !targetIdx = indexPrimArray morph i
            writeSmallArray dstArr targetIdx (indexSmallArray subArr i)
            go (i + 1)
  go 0

{-------------------------------------------------------------------------------
  Frame-Rule Auto-Wiring Plan
-------------------------------------------------------------------------------}

-- | Concrete execution plan for wiring an operation @(opIn -> opOut)@
-- inside a live register frame @(frameIn -> frameOut)@.
--
-- See Note [Reified Row Morphisms and Gather-Scatter] [ref:note-row-morph-routing]
-- See Note [Frame-Rule Automatic Wire Synthesis] [ref:note-frame-rule-wiring]
type FramePlan :: forall {k1} {k2} {k3} {k4}. k1 -> k2 -> k3 -> k4 -> Type
data FramePlan frameIn opIn opOut frameOut = FramePlan
  { fpOutSize     :: {-# UNPACK #-} !Int
  , fpGatherIn    :: !(RowMorph opIn frameIn)
    -- ^ Gathers the operation's input ports from @frameIn@.
  , fpBaseScatter :: !(RowMorph frameIn frameOut)
    -- ^ Places surviving @frameIn@ ports into @frameOut@.
  , fpOpScatter   :: !(RowMorph opOut frameOut)
    -- ^ Places @opOut@ ports into @frameOut@ (overwriting collided @frameIn@ ports).
  }
  deriving stock (Eq, Show)

-- | Solved by @Polytropos.Plugin@:
-- 1. Verifies @opIn@ is a subrow of @frameIn@ (unifying port types).
-- 2. Synthesizes @frameOut ~ RowOverwrite frameIn opOut@.
-- 3. Emits the 'FramePlan' dictionary.
--
-- See Note [Frame-Rule Automatic Wire Synthesis] [ref:note-frame-rule-wiring]
type FrameWire :: forall {k1} {k2} {k3} {k4}. k1 -> k2 -> k3 -> k4 -> Constraint
class FrameWire frameIn opIn opOut frameOut where
  framePlan :: FramePlan frameIn opIn opOut frameOut

-- | Entry point called by Core expressions synthesized by @Polytropos.Plugin@ for 'FrameWire'.
--
-- See Note [Frame-Rule Automatic Wire Synthesis] [ref:note-frame-rule-wiring]
unsafeMkFramePlan
  :: forall {k1} {k2} {k3} {k4} (frameIn :: k1) (opIn :: k2) (opOut :: k3) (frameOut :: k4).
     Int
  -> [Int]
  -> [Int]
  -> [Int]
  -> FramePlan frameIn opIn opOut frameOut
{-# NOINLINE unsafeMkFramePlan #-}
unsafeMkFramePlan outSz gatherIdxs baseIdxs opIdxs = FramePlan
  { fpOutSize     = outSz
  , fpGatherIn    = unsafeMkRowMorph gatherIdxs
  , fpBaseScatter = unsafeMkRowMorph baseIdxs
  , fpOpScatter   = unsafeMkRowMorph opIdxs
  }

-- | Merges the previous frame @frameIn@ and the operation output @opOut@
-- into the next frame @frameOut@ in two tight, branchless loops.
--
-- See Note [Frame-Rule Automatic Wire Synthesis] [ref:note-frame-rule-wiring]
executeFrameScatter
  :: FramePlan frameIn opIn opOut frameOut
  -> Rep f a
  -> Rep f b
  -> Rep f c
{-# INLINE executeFrameScatter #-}
executeFrameScatter (FramePlan !outSz _ (RowMorph baseMap) (RowMorph opMap)) (Rep _ baseArr) (Rep _ opArr) =
  Rep 0 $ runSmallArray $ do
    let !baseLen = sizeofPrimArray baseMap
        !opLen   = sizeofPrimArray opMap
    dst <- newSmallArray outSz (error "executeFrameScatter: uninitialized slot")
    let copyBase !i
          | i >= baseLen = pure ()
          | otherwise    = do
              let !dstIdx = indexPrimArray baseMap i
              writeSmallArray dst dstIdx (indexSmallArray baseArr i)
              copyBase (i + 1)
        copyOp !j
          | j >= opLen   = pure dst
          | otherwise    = do
              let !dstIdx = indexPrimArray opMap j
              writeSmallArray dst dstIdx (indexSmallArray opArr j)
              copyOp (j + 1)
    copyBase 0
    copyOp 0