keiro-dsl-0.6.0.0: src/Keiro/Dsl/AggregateType.hs
{-# OPTIONS_GHC -Werror=incomplete-patterns #-}
-- | Canonical aggregate type resolution and capability policy.
--
-- Parsing a 'TypeExpr' only establishes syntax. This module decides whether that
-- type is legal at an aggregate use site, canonicalizes aliases, validates
-- register initials, and supplies total Haskell lowering for admitted values.
module Keiro.Dsl.AggregateType
( AggregateUseSite (..),
AggregateCapability (..),
ResolvedAggregateType (..),
AggregateSymbols,
aggregateSymbols,
AggregateTypeErrorReason (..),
AggregateTypeError (..),
resolveAggregateType,
inferAggregateFieldType,
aggregateCapability,
aggregateCanonicalName,
typeExprCanonicalName,
aggregateHaskellType,
aggregateImports,
aggregatePackages,
aggregateSampleHaskell,
ResolvedRegisterInitial (..),
resolveRegisterInitial,
renderRegisterInitial,
registerInitialCanonicalName,
)
where
import Data.Char (toUpper)
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Data.Time.Calendar (toGregorian)
import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds)
import Data.Time.Format.ISO8601 (iso8601ParseM)
import Keiro.Dsl.Grammar
import Keiro.Dsl.NominalType
import Keiro.Dsl.TypeGraph
import Numeric.Natural (Natural)
import Text.Read (readMaybe)
data AggregateUseSite
= CommandFieldUse
| EventFieldUse
| RegisterUse
| EqualityGuardUse
| OrderingGuardUse
| WholeValueWriteUse
| CodecUse
| SnapshotUse
| HarnessSampleUse
| HaskellLoweringUse
deriving stock (Eq, Ord, Show, Enum, Bounded)
-- | Whether a use is solver-visible, legal but opaque, or unsupported.
data AggregateCapability = SolverVisible | OpaqueOnly | Unsupported
deriving stock (Eq, Ord, Show, Enum, Bounded)
data ResolvedAggregateType
= AggregateText
| AggregateInt
| AggregateInteger
| AggregateBool
| AggregateTime
| AggregateNatural
| AggregateNominal !ResolvedNominalType
| AggregateVertex !Name
| AggregateMapped !MappedKey
deriving stock (Eq, Ord, Show)
data AggregateSymbols = AggregateSymbols
{ symbolNominals :: !(Map Name ResolvedNominalType),
symbolVertices :: !(Map Name [Name]),
symbolMapped :: !(Map MappedKey ResolvedMappedDecl)
}
aggregateSymbols :: Spec -> AggregateSymbols
aggregateSymbols spec =
AggregateSymbols
{ symbolNominals = either (const Map.empty) nominalTypes (resolveNominalTypes spec),
symbolVertices =
Map.fromList
[ (aggName aggregate <> "Vertex", map stName (aggStates aggregate))
| NAggregate aggregate <- specNodes spec
],
symbolMapped = either (const Map.empty) tgDeclarations (resolveTypeGraph spec)
}
data AggregateTypeErrorReason
= UnknownAggregateType !Name
| UnsupportedAggregateShape !TypeExpr
| UnsupportedAggregateCapability !ResolvedAggregateType
| InvalidRegisterInitial !ResolvedAggregateType !Text
deriving stock (Eq, Show)
data AggregateTypeError = AggregateTypeError
{ aggregateTypeErrorLoc :: !Loc,
aggregateTypeErrorUseSite :: !AggregateUseSite,
aggregateTypeErrorReason :: !AggregateTypeErrorReason
}
deriving stock (Eq, Show)
resolveAggregateType :: AggregateSymbols -> Loc -> AggregateUseSite -> TypeExpr -> Either AggregateTypeError ResolvedAggregateType
resolveAggregateType symbols loc useSite expression = do
resolved <- case expression of
TText -> pure AggregateText
TInt -> pure AggregateInt
TInteger -> pure AggregateInteger
TBool -> pure AggregateBool
TNatural -> pure AggregateNatural
TTime -> pure AggregateTime
TJson -> unsupportedShape
TOptional {} -> unsupportedShape
TList {} -> unsupportedShape
TMap {} -> unsupportedShape
TRef name
| Just nominal <- Map.lookup name (symbolNominals symbols) -> pure (AggregateNominal nominal)
| Map.member name (symbolVertices symbols) -> pure (AggregateVertex name)
| Map.member (MappedKey name) (symbolMapped symbols) -> pure (AggregateMapped (MappedKey name))
| otherwise -> Left (AggregateTypeError loc useSite (UnknownAggregateType name))
case aggregateCapability useSite resolved of
Unsupported -> Left (AggregateTypeError loc useSite (UnsupportedAggregateCapability resolved))
SolverVisible -> pure resolved
OpaqueOnly -> pure resolved
where
unsupportedShape = Left (AggregateTypeError loc useSite (UnsupportedAggregateShape expression))
inferAggregateFieldType :: AggregateSymbols -> Aggregate -> AggregateUseSite -> AggregateField -> Either AggregateTypeError ResolvedAggregateType
inferAggregateFieldType symbols aggregate useSite field =
resolveAggregateType symbols (aggregateFieldLoc field) useSite inferred
where
inferred = case aggregateFieldType field of
Just expression -> expression
Nothing -> case [regType register | register <- aggRegs aggregate, regName register == aggregateFieldName field] of
expression : _ -> expression
[] ->
let candidate = pascal (aggregateFieldName field)
in if Map.member candidate (symbolNominals symbols)
|| Map.member candidate (symbolVertices symbols)
|| Map.member (MappedKey candidate) (symbolMapped symbols)
then TRef candidate
else TText
aggregateCapability :: AggregateUseSite -> ResolvedAggregateType -> AggregateCapability
aggregateCapability useSite resolved = case useSite of
EqualityGuardUse -> case resolved of
AggregateMapped {} -> Unsupported
_ -> solverVisibility resolved
OrderingGuardUse -> case resolved of
AggregateInt -> SolverVisible
AggregateInteger -> SolverVisible
AggregateTime -> SolverVisible
AggregateNatural -> SolverVisible
AggregateNominal nominal -> nominalOrderingCapability nominal
AggregateText -> Unsupported
AggregateBool -> Unsupported
AggregateVertex {} -> Unsupported
AggregateMapped {} -> Unsupported
CommandFieldUse -> solverVisibility resolved
EventFieldUse -> solverVisibility resolved
RegisterUse -> solverVisibility resolved
WholeValueWriteUse -> solverVisibility resolved
CodecUse -> solverVisibility resolved
SnapshotUse -> solverVisibility resolved
HarnessSampleUse -> solverVisibility resolved
HaskellLoweringUse -> solverVisibility resolved
solverVisibility :: ResolvedAggregateType -> AggregateCapability
solverVisibility resolved = case resolved of
AggregateText -> SolverVisible
AggregateInt -> SolverVisible
AggregateInteger -> SolverVisible
AggregateBool -> SolverVisible
AggregateTime -> SolverVisible
AggregateNatural -> SolverVisible
AggregateNominal nominal -> nominalSolverVisibility nominal
AggregateVertex {} -> OpaqueOnly
AggregateMapped {} -> OpaqueOnly
nominalSolverVisibility :: ResolvedNominalType -> AggregateCapability
nominalSolverVisibility nominal = case resolvedNominalRepresentation nominal of
ScalarRepresentation {} -> SolverVisible
IdRepresentation {} -> OpaqueOnly
EnumRepresentation {} -> OpaqueOnly
nominalOrderingCapability :: ResolvedNominalType -> AggregateCapability
nominalOrderingCapability nominal = case resolvedNominalRepresentation nominal of
ScalarRepresentation NominalInt -> SolverVisible
ScalarRepresentation NominalNatural -> SolverVisible
ScalarRepresentation NominalTime -> SolverVisible
ScalarRepresentation NominalText -> Unsupported
ScalarRepresentation NominalBool -> Unsupported
IdRepresentation {} -> Unsupported
EnumRepresentation {} -> Unsupported
aggregateCanonicalName :: ResolvedAggregateType -> Text
aggregateCanonicalName resolved = case resolved of
AggregateText -> "Text"
AggregateInt -> "Int"
AggregateInteger -> "Integer"
AggregateBool -> "Bool"
AggregateTime -> "Time"
AggregateNatural -> "Natural"
AggregateNominal nominal -> resolvedNominalName nominal
AggregateVertex name -> name
AggregateMapped key -> unMappedKey key
typeExprCanonicalName :: TypeExpr -> Text
typeExprCanonicalName expression = case expression of
TText -> "Text"
TInt -> "Int"
TInteger -> "Integer"
TBool -> "Bool"
TNatural -> "Natural"
TTime -> "Time"
TJson -> "Json"
TOptional value -> "Optional(" <> typeExprCanonicalName value <> ")"
TList value -> "List(" <> typeExprCanonicalName value <> ")"
TMap value -> "Map(" <> typeExprCanonicalName value <> ")"
TRef name -> name
aggregateHaskellType :: AggregateSymbols -> ResolvedAggregateType -> Text
aggregateHaskellType symbols resolved = case resolved of
AggregateTime -> "UTCTime"
AggregateNominal nominal -> case resolvedNominalOwnership nominal of
GeneratedNominal -> resolvedNominalName nominal
ConsumerNominal binding -> renderHaskellSource (consumerNominalHaskell binding)
AggregateMapped key -> case Map.lookup key (symbolMapped symbols) of
Just declaration -> renderHaskellSource (mappedHaskell declaration)
Nothing -> unMappedKey key
_ -> aggregateCanonicalName resolved
where
mappedHaskell (ResolvedStructural declaration _) = sdHaskell declaration
mappedHaskell (ResolvedOpaque declaration) = odHaskell declaration
renderHaskellSource source = hsModule source <> "." <> hsType source
aggregateImports :: AggregateSymbols -> ResolvedAggregateType -> Set Text
aggregateImports symbols resolved = case resolved of
AggregateTime ->
Set.fromList
[ "Data.Time.Calendar (fromGregorian)",
"Data.Time.Clock (UTCTime(..), picosecondsToDiffTime)"
]
AggregateNatural -> Set.singleton "Numeric.Natural (Natural)"
AggregateNominal nominal -> case resolvedNominalOwnership nominal of
GeneratedNominal -> Set.empty
ConsumerNominal binding -> Set.singleton (hsModule (consumerNominalHaskell binding) <> " qualified")
AggregateMapped key -> case Map.lookup key (symbolMapped symbols) of
Just declaration -> Set.singleton (hsModule (mappedHaskell declaration) <> " qualified")
Nothing -> Set.empty
_ -> Set.empty
where
mappedHaskell (ResolvedStructural declaration _) = sdHaskell declaration
mappedHaskell (ResolvedOpaque declaration) = odHaskell declaration
aggregatePackages :: AggregateSymbols -> ResolvedAggregateType -> Set Text
aggregatePackages symbols resolved = case resolved of
AggregateTime -> Set.singleton "time"
AggregateNominal nominal -> case resolvedNominalOwnership nominal of
GeneratedNominal -> Set.empty
ConsumerNominal binding -> Set.singleton (hsPackage (consumerNominalHaskell binding))
AggregateMapped key -> case Map.lookup key (symbolMapped symbols) of
Just declaration -> Set.singleton (hsPackage (mappedHaskell declaration))
Nothing -> Set.empty
_ -> Set.empty
where
mappedHaskell (ResolvedStructural declaration _) = sdHaskell declaration
mappedHaskell (ResolvedOpaque declaration) = odHaskell declaration
aggregateSampleHaskell :: AggregateSymbols -> Text -> ResolvedAggregateType -> Text
aggregateSampleHaskell symbols fieldName resolved = case resolved of
AggregateText -> tshow ("sample-" <> fieldName)
AggregateInt -> "0"
AggregateInteger -> "0"
AggregateBool -> "False"
AggregateTime -> "(UTCTime (fromGregorian 2026 1 2) (picosecondsToDiffTime 11045123456789012))"
AggregateNatural -> "0"
AggregateNominal nominal -> nominalSample nominal
AggregateVertex name -> firstConstructor name
AggregateMapped key -> case Map.lookup key (symbolMapped symbols) of
Just declaration -> "(snd (NonEmpty.head (fixtureCases " <> unQualifiedValueName (mappedFixtures declaration) <> ")))"
Nothing -> unMappedKey key <> ".sample"
where
firstConstructor name = case Map.lookup name (symbolVertices symbols) of
Just (constructor : _) -> constructor
_ -> name
nominalSample nominal = case resolvedNominalOwnership nominal of
ConsumerNominal binding ->
"(nominalFixtureDomain (NonEmpty.head (nominalFixtureCases "
<> unQualifiedValueName (consumerNominalFixtures binding)
<> ")))"
GeneratedNominal -> case resolvedNominalRepresentation nominal of
IdRepresentation {} -> "(" <> resolvedNominalName nominal <> " \"sample\")"
EnumRepresentation constructors -> fst (NE.head constructors)
ScalarRepresentation {} -> resolvedNominalName nominal <> ".sample"
mappedFixtures (ResolvedStructural declaration _) = sdFixtures declaration
mappedFixtures (ResolvedOpaque declaration) = odFixtures declaration
data ResolvedRegisterInitial
= InitialText !Text
| InitialInt !Int
| InitialInteger !Integer
| InitialBool !Bool
| InitialTime !UTCTime
| InitialNatural !Natural
| InitialId !Name
| InitialNamed !ResolvedAggregateType !Name
| InitialNominal !Name !QualifiedValueName
| InitialMapped !MappedKey !QualifiedValueName
deriving stock (Eq, Show)
resolveRegisterInitial :: AggregateSymbols -> Loc -> ResolvedAggregateType -> RegInitial -> Either AggregateTypeError ResolvedRegisterInitial
resolveRegisterInitial symbols loc resolved syntax = case resolved of
AggregateText -> case syntax of
RegInitText value -> pure (InitialText value)
RegInitBare _ -> invalid "Text initials must be quoted"
AggregateInt -> case syntax of
RegInitBare value -> maybe (invalid "Int initials must be integral literals in the Haskell Int range") (pure . InitialInt) (readMaybe (T.unpack value))
RegInitText _ -> invalid "Int initials must be unquoted integral literals"
AggregateInteger -> case syntax of
RegInitBare value -> maybe (invalid "Integer initials must be integral literals") (pure . InitialInteger) (readMaybe (T.unpack value))
RegInitText _ -> invalid "Integer initials must be unquoted integral literals"
AggregateBool -> case syntax of
RegInitBare "True" -> pure (InitialBool True)
RegInitBare "False" -> pure (InitialBool False)
_ -> invalid "Bool initials must be True or False"
AggregateTime -> case syntax of
RegInitText value -> maybe (invalid "Time initials must be valid quoted ISO-8601 UTC timestamps") (pure . InitialTime) (iso8601ParseM (T.unpack value))
RegInitBare _ -> invalid "Time initials must be quoted ISO-8601 UTC timestamps"
AggregateNatural -> case syntax of
RegInitBare value -> case readMaybe (T.unpack value) :: Maybe Integer of
Just number | number >= 0 -> pure (InitialNatural (fromInteger number))
_ -> invalid "Natural initials must be non-negative integral literals"
RegInitText _ -> invalid "Natural initials must be unquoted non-negative integral literals"
AggregateNominal nominal -> case resolvedNominalOwnership nominal of
ConsumerNominal binding -> case syntax of
RegInitBare "initial" -> case consumerNominalInitial binding of
Just value -> pure (InitialNominal (resolvedNominalName nominal) value)
Nothing -> invalid "consumer-owned nominal register type must declare an initial symbol"
_ -> invalid "consumer-owned nominal register initials must use the bare initial token"
GeneratedNominal -> case resolvedNominalRepresentation nominal of
IdRepresentation {} -> case syntax of
RegInitBare "placeholder" -> pure (InitialId (resolvedNominalName nominal))
_ -> invalid "ID initials must use placeholder"
EnumRepresentation constructors -> namedInitial (resolvedNominalName nominal) (Just (map fst (NE.toList constructors)))
ScalarRepresentation {} -> invalid "generated nominal scalars are unsupported"
AggregateVertex name -> namedInitial name (Map.lookup name (symbolVertices symbols))
AggregateMapped key -> case syntax of
RegInitBare "initial" -> case Map.lookup key (symbolMapped symbols) >>= mappedInitial of
Just value -> pure (InitialMapped key value)
Nothing -> invalid "mapped register type must declare an initial symbol"
_ -> invalid "mapped register initials must use the bare initial token"
where
invalid detail = Left (AggregateTypeError loc RegisterUse (InvalidRegisterInitial resolved detail))
namedInitial name constructors = case syntax of
RegInitBare constructor | maybe False (constructor `elem`) constructors -> pure (InitialNamed resolved constructor)
_ -> invalid ("initial must name a constructor of " <> name)
mappedInitial (ResolvedStructural declaration _) = sdInitial declaration
mappedInitial (ResolvedOpaque declaration) = odInitial declaration
renderRegisterInitial :: ResolvedRegisterInitial -> Text
renderRegisterInitial initial = case initial of
InitialText value -> tshow value
InitialInt value -> T.pack (show value)
InitialInteger value -> T.pack (show value)
InitialBool value -> if value then "True" else "False"
InitialTime value ->
let (year, month, day) = toGregorian (utctDay value)
picoseconds = diffTimeToPicoseconds (utctDayTime value)
in "(UTCTime (fromGregorian "
<> T.pack (show year)
<> " "
<> T.pack (show month)
<> " "
<> T.pack (show day)
<> ") (picosecondsToDiffTime "
<> T.pack (show picoseconds)
<> "))"
InitialNatural value -> T.pack (show value)
InitialId name -> "(" <> name <> " \"\")"
InitialNamed resolved constructor -> case resolved of
AggregateVertex vertexType -> T.dropEnd (T.length ("Vertex" :: Text)) vertexType <> constructor
_ -> constructor
InitialNominal _ value -> unQualifiedValueName value
InitialMapped _ value -> unQualifiedValueName value
registerInitialCanonicalName :: ResolvedRegisterInitial -> Text
registerInitialCanonicalName initial = case initial of
InitialText value -> tshow value
InitialInt value -> T.pack (show value)
InitialInteger value -> T.pack (show value)
InitialBool value -> if value then "True" else "False"
InitialTime {} -> renderRegisterInitial initial
InitialNatural value -> T.pack (show value)
InitialId {} -> "placeholder"
InitialNamed _ constructor -> constructor
InitialNominal {} -> "initial"
InitialMapped {} -> "initial"
pascal :: Text -> Text
pascal value = case T.uncons value of
Just (first, rest) -> T.cons (toUpper first) rest
Nothing -> value
tshow :: Text -> Text
tshow = T.pack . show