packages feed

keiki-0.7.0.0: src/Keiki/Internal/ProjectionDomain.hs

{-# LANGUAGE GADTs #-}

-- | Internal representation shared by the backend-neutral public API and the
-- SBV compiler. The constructors stay hidden from package consumers so every
-- textual pattern is validated before it can be used as exact evidence.
module Keiki.Internal.ProjectionDomain
  ( ProjectionDomain (..),
    TextPattern (..),
    DomainConstructionError (..),
    maximumSmtCodePoint,
    finiteProjectionDomain,
    wholeProjectionDomain,
    textProjectionDomain,
    textLiteral,
    textCharSet,
    textCharRanges,
    textConcat,
    textAlternation,
    textRepeatBetween,
    memberProjectionDomain,
    matchesTextPattern,
  )
where

import Data.Foldable (traverse_)
import Data.List (stripPrefix)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Text (Text)
import Data.Text qualified as T
import Numeric.Natural (Natural)

-- | The exact image of a field projection. The text constructor is indexed so
-- a text pattern cannot accidentally be attached to another result carrier.
data ProjectionDomain a where
  ProjectionWhole :: ProjectionDomain a
  ProjectionFinite :: (Eq a) => NonEmpty a -> ProjectionDomain a
  ProjectionText :: TextPattern -> ProjectionDomain Text

-- | A deliberately small full-string pattern language. Every constructor has
-- both a pure interpreter and an exact SBV regular-expression translation.
data TextPattern
  = TextLiteral Text
  | TextRanges (NonEmpty (Char, Char))
  | TextConcat (NonEmpty TextPattern)
  | TextAlternation (NonEmpty TextPattern)
  | TextRepeatBetween Natural Natural TextPattern
  deriving stock (Eq, Show)

-- | Why a textual exact-domain declaration could not be constructed.
data DomainConstructionError
  = CodePointAboveSmtMaximum Char
  | ReversedCharacterRange Char Char
  | InvalidRepetitionInterval Natural Natural
  | RepetitionBoundTooLarge Natural
  deriving stock (Eq, Show)

-- | SMT-LIB strings contain code points U+0000 through U+2FFFF.
maximumSmtCodePoint :: Char
maximumSmtCodePoint = '\x2FFFF'

-- | Construct a non-empty finite domain, removing duplicates while retaining
-- the first occurrence of every value. The symbolic compiler separately
-- verifies that each retained literal round-trips through its representation
-- and satisfies backend bounds; failure omits the entire constraint and makes
-- the containing translation conservative.
finiteProjectionDomain :: (Eq a) => NonEmpty a -> ProjectionDomain a
finiteProjectionDomain = ProjectionFinite . stableNub
  where
    stableNub (x :| xs) = x :| go [x] xs
    go _ [] = []
    go seen (x : xs)
      | x `elem` seen = go seen xs
      | otherwise = x : go (x : seen) xs

-- | Declare that every value of the result carrier belongs to the image.
-- Exact witnesses may use this only when Keiki classifies the symbolic
-- representation as a whole-carrier isomorphism.
wholeProjectionDomain :: ProjectionDomain a
wholeProjectionDomain = ProjectionWhole

-- | Lift a validated full-string pattern into a 'Text' projection domain.
textProjectionDomain :: TextPattern -> ProjectionDomain Text
textProjectionDomain = ProjectionText

-- | Match one literal text value. Code points outside SMT-LIB's string domain
-- are rejected instead of making the pure and symbolic meanings disagree.
textLiteral :: Text -> Either DomainConstructionError TextPattern
textLiteral value = TextLiteral value <$ validateCharacters (T.unpack value)

-- | Match exactly one member of a non-empty character set.
textCharSet :: NonEmpty Char -> Either DomainConstructionError TextPattern
textCharSet chars = do
  validateCharacters (NE.toList chars)
  pure (TextRanges ((\c -> (c, c)) <$> stableNub chars))
  where
    stableNub (x :| xs) = x :| go [x] xs
    go _ [] = []
    go seen (x : xs)
      | x `elem` seen = go seen xs
      | otherwise = x : go (x : seen) xs

-- | Match exactly one character from one of the supplied inclusive ranges.
textCharRanges ::
  NonEmpty (Char, Char) ->
  Either DomainConstructionError TextPattern
textCharRanges ranges = do
  traverse_ validateRange ranges
  pure (TextRanges ranges)
  where
    validateRange (lower, upper)
      | lower > upper = Left (ReversedCharacterRange lower upper)
      | otherwise = validateCharacters [lower, upper]

-- | Concatenate a non-empty sequence of full-string patterns.
textConcat :: NonEmpty TextPattern -> TextPattern
textConcat = TextConcat

-- | Match any one of a non-empty sequence of full-string patterns.
textAlternation :: NonEmpty TextPattern -> TextPattern
textAlternation = TextAlternation

-- | Match a pattern between the inclusive lower and upper bounds. SBV's
-- regular-expression node stores machine 'Int' bounds, so larger naturals are
-- rejected at construction rather than truncated.
textRepeatBetween ::
  Natural ->
  Natural ->
  TextPattern ->
  Either DomainConstructionError TextPattern
textRepeatBetween lower upper textPattern
  | lower > upper = Left (InvalidRepetitionInterval lower upper)
  | lower > maxInt = Left (RepetitionBoundTooLarge lower)
  | upper > maxInt = Left (RepetitionBoundTooLarge upper)
  | otherwise = Right (TextRepeatBetween lower upper textPattern)
  where
    maxInt = fromIntegral (maxBound :: Int)

-- | Decide concrete membership in the exact domain.
memberProjectionDomain :: (Eq a) => ProjectionDomain a -> a -> Bool
memberProjectionDomain ProjectionWhole _ = True
memberProjectionDomain (ProjectionFinite values) value = value `elem` values
memberProjectionDomain (ProjectionText textPattern) value =
  matchesTextPattern textPattern value

-- | Interpret a 'TextPattern' as a complete-string matcher.
matchesTextPattern :: TextPattern -> Text -> Bool
matchesTextPattern textPattern value = any null (match textPattern (T.unpack value))
  where
    match :: TextPattern -> String -> [String]
    match (TextLiteral literal) input =
      maybe [] pure (T.unpack literal `stripPrefix` input)
    match (TextRanges ranges) input = case input of
      [] -> []
      c : rest
        | any (\(lower, upper) -> lower <= c && c <= upper) ranges -> [rest]
        | otherwise -> []
    match (TextConcat patterns) input =
      foldl (\remainders next -> remainders >>= match next) [input] patterns
    match (TextAlternation patterns) input =
      concatMap (`match` input) patterns
    match (TextRepeatBetween lower upper repeated) input =
      concatMap (\count -> applyCount count input) [lower .. upper]
      where
        applyCount 0 current = [current]
        applyCount count current =
          match repeated current >>= applyCount (count - 1)

validateCharacters :: [Char] -> Either DomainConstructionError ()
validateCharacters = traverse_ validateCharacter
  where
    validateCharacter c
      | c <= maximumSmtCodePoint = Right ()
      | otherwise = Left (CodePointAboveSmtMaximum c)