gargamelle-effect-0.1.0.0: src/Gargamelle/Effect/Router.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-|
Module : Gargamelle.Effect.Router
Description : O(1) variant jump table routing for Penelope and Gargamelle ropes
Copyright : (c) 2026 The Gargamelle Authors
License : EUPL-1.2
Maintainer : nadia.yvette.chambers@ik.me
Provides constant-time \(O(1)\) jump table dispatch and extensible multi-branch choice
for effect arrow computations over 'Polytropos.Variant' open sums, powered by
@polytropos@ and @penelope@.
-}
module Gargamelle.Effect.Router
( -- * O(1) Variant Router Jump Table
VariantRouter(..)
, makeRouter
, routeVariant
, routeVariantRope
, routerSize
-- * Type-Safe Variant Routing & Multi-Way Branching
, routeVariantWith
, branchRope
-- * Payload Extraction Helpers
, getSlot
, getSlotValue
-- * Re-exports from Penelope & Polytropos
, deriveRouter
) where
import Data.Functor.Identity (Identity(..))
import Data.Primitive.SmallArray
( SmallArray
, sizeofSmallArray
, indexSmallArray
, smallArrayFromList
)
import Unsafe.Coerce (unsafeCoerce)
import Effect.Rope (Rope(..))
import Penelope.Effects.Binary (deriveRouter)
import Polytropos
( Variant
, Record
, HandlerRecordRow
, eliminateUniform
)
import Polytropos.Core (Rep, RowRep(..), repTag, repVals)
{- Note [O(1) Variant Jump-Table Dispatch Physics]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[tag:note-variant-jump-table-physics]
Traditional extensible effect systems (e.g., Freer, Polysemy, Eff) dispatch dynamic
effects by traversing a type-level type list using 'Member eff r' evidence. At runtime,
this induces two substantial overheads:
1. O(N) linear search through the effect mantle, where N is the number of active effects.
2. Recursive dictionary unpackings and intermediate constructor allocations on the heap.
Gargamelle leverages the structural open ADT representation of @polytropos@ to achieve
true hardware-level O(1) jump-table dispatch:
1. A Polytropos 'Variant' is represented internally by 'Polytropos.Core.Rep':
a machine-word constructor tag ('tag :: Int') plus a flat 'SmallArray' of payload slots.
2. The 'VariantRouter' stores a flat 'SmallArray' of handler closures indexed directly
by integer constructor tag (0 .. N-1).
3. Dispatch via 'routeVariant' or 'routeVariantRope' reads the unboxed tag from the
incoming variant and performs an immediate O(1) array index:
handler = indexSmallArray table tag
handler (payload rep)
4. The CPU executes a single indirect jump with zero allocation on the hot path,
guaranteeing constant-time effect resolution regardless of how many strands are woven
into the mantle.
-}
-- | A constant-time O(1) jump table routing open variant effect requests
-- directly to core arrow handlers indexed by their unboxed constructor tags.
newtype VariantRouter core a b = VariantRouter
{ routerTable :: SmallArray (Rep Identity () -> core a b)
}
-- | Construct a 'VariantRouter' from an ordered list of branch handlers corresponding
-- to consecutive constructor tags (0, 1, ..., n-1) of the sum row.
makeRouter :: [Rep Identity () -> core a b] -> VariantRouter core a b
makeRouter handlers = VariantRouter (smallArrayFromList handlers)
{-# INLINE makeRouter #-}
-- | Dispatch an open variant effect request through the jump table in O(1) time.
routeVariant :: VariantRouter core a b -> Variant s -> core a b
routeVariant (VariantRouter table) (RowRep rep) =
let idx = fromIntegral (repTag rep)
in if idx >= 0 && idx < sizeofSmallArray table
then indexSmallArray table idx rep
else error ("routeVariant: unhandled variant tag " ++ show idx)
{-# INLINE routeVariant #-}
-- | Route an open variant effect into a Gargamelle 'Effect.Rope.Rope' arrow computation.
--
-- See Note [O(1) Variant Jump-Table Dispatch Physics] [ref:note-variant-jump-table-physics].
routeVariantRope :: VariantRouter core a b -> Variant s -> Rope core mantle a b
routeVariantRope router var = Rope \_ -> routeVariant router var
{-# INLINE routeVariantRope #-}
-- | Route an open variant effect into a 'Rope' computation using a statically-typed
-- 'Polytropos.Record' of core arrow handlers. The compiler verifies full row exhaustiveness.
routeVariantWith :: Record (HandlerRecordRow s (core a b)) -> Variant s -> Rope core mantle a b
routeVariantWith handlers var = Rope \_ -> eliminateUniform handlers var
{-# INLINE routeVariantWith #-}
-- | Multi-way branch combinator for 'Rope' arrows over an open variant in O(1) time.
-- Eliminates the need for binary 'Control.Arrow.ArrowChoice' cascades over nested 'Either's.
branchRope :: Record (HandlerRecordRow s (Rope core mantle a b)) -> Variant s -> Rope core mantle a b
branchRope handlers var = eliminateUniform handlers var
{-# INLINE branchRope #-}
-- | Number of registered branches in the jump table.
routerSize :: VariantRouter core a b -> Int
routerSize (VariantRouter table) = sizeofSmallArray table
{-# INLINE routerSize #-}
-- | Safely extract a typed value from the i-th payload slot of a variant's active constructor.
getSlot :: forall x. Int -> Rep Identity () -> x
getSlot idx rep =
let vals = repVals rep
in if idx >= 0 && idx < sizeofSmallArray vals
then unsafeCoerce (runIdentity (indexSmallArray vals idx))
else error ("getSlot: index out of bounds " ++ show idx)
{-# INLINE getSlot #-}
-- | Convenience helper for extracting a payload slot.
getSlotValue :: forall x. Int -> Rep Identity () -> x
getSlotValue = getSlot
{-# INLINE getSlotValue #-}