packages feed

smirk-penelope-0.1.0.0: src/Smirk/Effect/Penelope.hs

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

{- |
Module      : Smirk.Effect.Penelope
Description : Penelope effect system interface for the Smirk PCRE engine
Copyright   : (c) Nadia Yvette Chambers, 2026
License     : Apache-2.0
Maintainer  : Nadia Chambers <nadia.yvette.chambers@ik.me>
Stability   : experimental
Portability : non-portable (GHC extensions)

Exposes Smirk's step-bounded, pure PCRE engine as an algebraic effect ('RegexEngine')
for the @Penelope@ effect system, providing zero-cost polycategorical dispatch over
flat Polytropos row vectors, unlifted pure interpretation, and fuel-bounded execution.

=== Example Usage

@
import Data.Text (Text)
import Penelope.Effects ('t:Eff')
import Smirk (re)
import Smirk.Effect.Penelope

searchUser :: (ConstructorIndex "regex" ef) => Text -> 't:Eff' eh ef (Maybe Text)
searchUser input = do
  mMatch <- matchRegex [re|user:(\\w+)|] input
  pure (fmap matchedSlice mMatch)
@
-}
module Smirk.Effect.Penelope
  ( -- * Effect Definition
    RegexEngine (..)

    -- * Effect Injection Operations
  , matchRegex
  , matchRegexAt
  , matchRegexFuel
  , matchRegexFuelAt
  , findAllRegex
  , findAllRegexAt

    -- * Handlers
  , interpretRegexPure
  , interpretRegexWithFuel

    -- * Pure and Monadic Runners
  , runRegexEnginePure
  , runRegexEngineWithFuel
  , runRegexEngineM
  , runRegexEngineWithFuelM
  ) where

import Data.Functor.Identity (Identity (..))
import Data.Kind (Type)
import Data.Text (Text)
import GHC.TypeLits (Symbol)
import Penelope.Effects
  ( Eff
  , EffHandler
  , UnaryEffect
  , mkEffHandler
  , runEffM
  , send
  )
import Polytropos (Field (..), Pair (..), emptyRecord, insertField)
import Polytropos.Core (ConstructorIndex)
import Smirk.Core.Bytecode (Regex)
import Smirk.Engine.VM
  ( Fuel (..)
  , MatchResult
  , MatchStatus (..)
  , findAllMatches
  , runMatch
  )

-- | First-class algebraic effect representing regular expression operations.
data RegexEngine (a :: Type) where
  -- | Perform an unconstrained search returning the first match if found.
  MatchRegex     :: Regex -> Text -> RegexEngine (Maybe (MatchResult Text))
  -- | Perform a step-budgeted search tracking consumed fuel and yielding continuations on exhaustion.
  MatchRegexFuel :: Regex -> Text -> Fuel -> RegexEngine (MatchStatus Text)
  -- | Find all non-overlapping matches across the input text.
  FindAllRegex   :: Regex -> Text -> RegexEngine [MatchResult Text]

-- | Perform an unconstrained search at effect label @"regex"@ returning the first match if found.
{-# INLINE matchRegex #-}
matchRegex ::
  forall eh ef.
  ConstructorIndex "regex" ef =>
  Regex
  -> Text
  -> Eff eh ef (Maybe (MatchResult Text))
matchRegex = matchRegexAt @"regex"

-- | Perform an unconstrained search at an explicit effect label returning the first match if found.
{-# INLINE matchRegexAt #-}
matchRegexAt ::
  forall (l :: Symbol) eh ef.
  ConstructorIndex l ef =>
  Regex
  -> Text
  -> Eff eh ef (Maybe (MatchResult Text))
matchRegexAt reg txt = send @l (MatchRegex reg txt)

-- | Perform a step-budgeted search at effect label @"regex"@.
{-# INLINE matchRegexFuel #-}
matchRegexFuel ::
  forall eh ef.
  ConstructorIndex "regex" ef =>
  Regex
  -> Text
  -> Fuel
  -> Eff eh ef (MatchStatus Text)
matchRegexFuel = matchRegexFuelAt @"regex"

-- | Perform a step-budgeted search at an explicit effect label.
{-# INLINE matchRegexFuelAt #-}
matchRegexFuelAt ::
  forall (l :: Symbol) eh ef.
  ConstructorIndex l ef =>
  Regex
  -> Text
  -> Fuel
  -> Eff eh ef (MatchStatus Text)
matchRegexFuelAt reg txt fuel = send @l (MatchRegexFuel reg txt fuel)

-- | Find all non-overlapping matches at effect label @"regex"@ across the input text.
{-# INLINE findAllRegex #-}
findAllRegex ::
  forall eh ef.
  ConstructorIndex "regex" ef =>
  Regex
  -> Text
  -> Eff eh ef [MatchResult Text]
findAllRegex = findAllRegexAt @"regex"

-- | Find all non-overlapping matches at an explicit effect label across the input text.
{-# INLINE findAllRegexAt #-}
findAllRegexAt ::
  forall (l :: Symbol) eh ef.
  ConstructorIndex l ef =>
  Regex
  -> Text
  -> Eff eh ef [MatchResult Text]
findAllRegexAt reg txt = send @l (FindAllRegex reg txt)

-- | Construct an 'EffHandler' interpreting 'RegexEngine' with unlimited execution fuel.
-- Suitable for trusted regular expressions and inputs where ReDoS attacks are not a concern.
{-# INLINE interpretRegexPure #-}
interpretRegexPure ::
  Monad m =>
  EffHandler m RegexEngine
interpretRegexPure = mkEffHandler $ \case
  MatchRegex reg txt -> case runMatch reg txt Unlimited of
    Matched res -> pure (Just res)
    _           -> pure Nothing
  MatchRegexFuel reg txt fuel -> pure (runMatch reg txt fuel)
  FindAllRegex reg txt -> pure (findAllMatches reg txt)

-- | Construct an 'EffHandler' interpreting 'RegexEngine' enforcing a specific default step budget limit.
-- Guarantees ReDoS immunity by bounding execution iterations across untrusted inputs.
{-# INLINE interpretRegexWithFuel #-}
interpretRegexWithFuel ::
  Monad m =>
  Fuel
  -> EffHandler m RegexEngine
interpretRegexWithFuel defaultFuel = mkEffHandler $ \case
  MatchRegex reg txt -> case runMatch reg txt defaultFuel of
    Matched res -> pure (Just res)
    _           -> pure Nothing
  MatchRegexFuel reg txt fuel -> pure (runMatch reg txt fuel)
  FindAllRegex reg txt -> pure (findAllMatches reg txt)

-- | Execute a pure 't:Eff' computation with unlimited fuel using the @"regex"@ label.
{-# INLINE runRegexEnginePure #-}
runRegexEnginePure ::
  Eff '[] '["regex" ':= UnaryEffect RegexEngine] a
  -> a
runRegexEnginePure prog =
  let handlers = insertField (Field @"regex") interpretRegexPure emptyRecord
      Identity res = runEffM handlers prog
  in res

-- | Execute a pure 't:Eff' computation enforcing a step budget using the @"regex"@ label.
{-# INLINE runRegexEngineWithFuel #-}
runRegexEngineWithFuel ::
  Fuel
  -> Eff '[] '["regex" ':= UnaryEffect RegexEngine] a
  -> a
runRegexEngineWithFuel fuel prog =
  let handlers = insertField (Field @"regex") (interpretRegexWithFuel fuel) emptyRecord
      Identity res = runEffM handlers prog
  in res

-- | Execute an effectful 't:Eff' computation under a target 'Monad' using pure unlimited regex matching.
{-# INLINE runRegexEngineM #-}
runRegexEngineM ::
  Monad m =>
  Eff '[] '["regex" ':= UnaryEffect RegexEngine] a
  -> m a
runRegexEngineM prog =
  let handlers = insertField (Field @"regex") interpretRegexPure emptyRecord
  in runEffM handlers prog

-- | Execute an effectful 't:Eff' computation under a target 'Monad' enforcing a step budget.
{-# INLINE runRegexEngineWithFuelM #-}
runRegexEngineWithFuelM ::
  Monad m =>
  Fuel
  -> Eff '[] '["regex" ':= UnaryEffect RegexEngine] a
  -> m a
runRegexEngineWithFuelM fuel prog =
  let handlers = insertField (Field @"regex") (interpretRegexWithFuel fuel) emptyRecord
  in runEffM handlers prog