keiro-dsl-0.12.0.0: src/Keiro/Dsl/Parser/Aggregate.hs
-- | Aggregate declarations, clauses, transitions, and expression ownership.
module Keiro.Dsl.Parser.Aggregate
( pAggregate,
)
where
import Data.Text qualified as T
import Keiro.Dsl.Frontend.Internal (FrontendContext, frontendSupportsFeature)
import Keiro.Dsl.Grammar
import Keiro.Dsl.LanguageVersion
import Keiro.Dsl.Parser.Core
import Keiro.Dsl.Parser.Expression (pExpr)
import Keiro.Dsl.Parser.Mapped (pMappedTypeExpr)
import Keiro.Dsl.Source (Located, mapLocated)
import Keiro.Dsl.Syntax (SurfaceElement (..))
import Text.Megaparsec
-- Aggregate node
--------------------------------------------------------------------------------
data BodyItem
= BICommand Command [Located SurfaceElement]
| BIEvent Event [Located SurfaceElement]
| BIWire WireSpec
| BIProjection ProjectionSpec
| BISnapshot SnapshotSpec
| BITransition (Located Transition) [Located SurfaceElement]
pAggregate :: FrontendContext -> P (Aggregate, [Located SurfaceElement])
pAggregate context = do
loc <- getLoc
keyword "aggregate"
name <- ident
outcomeDeclarations <- many (pDomainOutcomeTypes context)
regs <- pRegsBlock context
locatedStates <- pStatesLine
positionedItems <- many ((,) <$> getOffset <*> pBodyItem context)
let items = map snd positionedItems
transitions = [transition | BITransition transition _ <- items]
stateElements =
[ mapLocated (SurfaceAggregateState name . stName) locatedState
| locatedState <- locatedStates
]
transitionElements =
[ mapLocated (const (SurfaceAggregateTransition name ordinal)) transition
| (ordinal, transition) <- zip [0 ..] transitions
]
wireOffsets = [offset | (offset, BIWire _) <- positionedItems]
projectionOffsets = [offset | (offset, BIProjection _) <- positionedItems]
snapshotOffsets = [offset | (offset, BISnapshot _) <- positionedItems]
case wireOffsets of
_ : duplicateOffset : _ ->
failAt duplicateOffset ("duplicate wire block in aggregate " <> T.unpack name <> " (only one is allowed)")
_ -> pure ()
case projectionOffsets of
_ : duplicateOffset : _ ->
failAt duplicateOffset ("duplicate projection block in aggregate " <> T.unpack name <> " (only one is allowed)")
_ -> pure ()
case snapshotOffsets of
_ : duplicateOffset : _ ->
failAt duplicateOffset ("duplicate snapshot block in aggregate " <> T.unpack name <> " (only one is allowed)")
_ -> pure ()
pure
( Aggregate
{ aggName = name,
aggRegs = regs,
aggStates = map locatedValue locatedStates,
aggCommands = [c | BICommand c _ <- items],
aggEvents = [e | BIEvent e _ <- items],
aggTransitions = map locatedValue transitions,
aggDomainOutcomeTypes = case outcomeDeclarations of
declaration : _ -> Just declaration
[] -> Nothing,
aggDomainOutcomeDuplicateLocs = map outcomeTypesLoc (drop 1 outcomeDeclarations),
aggWire = listToMaybe [w | BIWire w <- items],
aggProjection = listToMaybe [p | BIProjection p <- items],
aggSnapshot = listToMaybe [s | BISnapshot s <- items],
aggLoc = loc
},
stateElements <> transitionElements <> concatMap bodyElements items
)
where
listToMaybe xs = case xs of (x : _) -> Just x; [] -> Nothing
bodyElements = \case
BICommand _ elements -> elements
BIEvent _ elements -> elements
BITransition _ elements -> elements
BIWire _ -> []
BIProjection _ -> []
BISnapshot _ -> []
pDomainOutcomeTypes :: FrontendContext -> P DomainOutcomeTypes
pDomainOutcomeTypes context = do
loc <- getLoc
marker <- withOwnedSpan (keyword "domain-outcomes")
requireLanguageFeatureAt context DomainCommandOutcomeSyntax (spanOf marker)
_ <- symbol "rejection" *> symbol "="
rejection <- ident
_ <- symbol "no-op" *> symbol "="
noOp <- ident
pure
DomainOutcomeTypes
{ rejectionType = rejection,
noOpType = noOp,
outcomeTypesLoc = loc
}
pRegsBlock :: FrontendContext -> P [RegDecl]
pRegsBlock context = do
keyword "regs"
many (pRegDecl context)
pRegDecl :: FrontendContext -> P RegDecl
pRegDecl context = do
loc <- getLoc
name <- ident
ty <- pMappedTypeExpr context
_ <- symbol "="
initial <- (RegInitText <$> stringLit) <|> (RegInitBare <$> (ident <|> signedDecimalText))
pure RegDecl {regName = name, regType = ty, regInitial = initial, regLoc = loc}
pStatesLine :: P [Located StateDecl]
pStatesLine = do
keyword "states"
many (withOwnedSpan pStateDecl)
where
-- A state decl is an identifier with an optional terminal @!@. The
-- @notFollowedBy@ lookahead stops the list before a transition whose source
-- state would otherwise be swallowed as an extra state, e.g. when a
-- transition directly follows the @states@ line with no command\/event
-- between them. The @try@ backtracks so the identifier is left for
-- 'pTransition'.
pStateDecl = try $ do
loc <- getLoc
-- A @replay-only@ transition marker directly after the states line
-- must not be swallowed: 'ident' would take @replay@ (hyphens are
-- not identifier characters) and strand @-only@.
notFollowedBy (keyword "replay-only")
n <- ident
term <- option False (True <$ symbol "!")
notFollowedBy (symbol "--")
pure StateDecl {stName = n, stTerminal = term, stLoc = loc}
pBodyItem :: FrontendContext -> P BodyItem
pBodyItem context =
choice
[ uncurry BICommand <$> pCommand context,
uncurry BIEvent <$> pEvent context,
BIWire <$> pWire,
BIProjection <$> pProjection context,
BISnapshot <$> pSnapshot,
do
located <- withOwnedSpan (pTransition context)
let (_, elements) = locatedValue located
pure (BITransition (mapLocated fst located) elements)
]
pSnapshot :: P SnapshotSpec
pSnapshot = do
loc <- getLoc
keyword "snapshot"
policy <-
choice
[ SnapEvery <$> (keyword "every" *> boundedDecimal),
SnapOnTerminal <$ symbol "on-terminal"
]
_ <- symbol "state-codec"
_ <- symbol "version" *> symbol "="
version <- boundedDecimal
_ <- symbol "shape-hash" *> symbol "="
hash <- stringLit
pure SnapshotSpec {snapPolicy = policy, snapCodecVersion = version, snapShapeHash = hash, snapLoc = loc}
pCommand :: FrontendContext -> P (Command, [Located SurfaceElement])
pCommand context = do
loc <- getLoc
keyword "command"
name <- ident
fields <- braces (many (withOwnedSpan (pAggregateField context)))
pure
( Command {cmdName = name, cmdFields = map locatedValue fields, cmdLoc = loc},
map (mapLocated (SurfaceField . aggregateFieldName)) fields
)
pAggregateField :: FrontendContext -> P AggregateField
pAggregateField context = do
loc <- getLoc
n <- ident
selector <- optionalLanguageFeature context FieldAliasSyntax "haskell" (try (keyword "haskell" *> ident))
wireKey <- optionalLanguageFeature context FieldAliasSyntax "as" (try (keyword "as" *> stringLit))
mty <- optional (symbol ":" *> pMappedTypeExpr context)
pure
AggregateField
{ aggregateFieldName = n,
aggregateFieldSelector = selector,
aggregateFieldWireKey = wireKey,
aggregateFieldType = mty,
aggregateFieldLoc = loc
}
pEvent :: FrontendContext -> P (Event, [Located SurfaceElement])
pEvent context = do
loc <- getLoc
(retiring, deprecated) <-
option
(False, False)
( choice
[ (True, False) <$ keyword "retiring",
(False, True) <$ keyword "deprecated"
]
)
keyword "event"
name <- ident
ver <- option 1 pVersion
(body, elements) <-
choice
[ (\commandName -> (EventFromCommand commandName, [])) <$> (symbol "=" *> keyword "fields" *> parens ident),
do
fields <- braces (many (withOwnedSpan (pAggregateField context)))
pure
( EventFields (map locatedValue fields),
map (mapLocated (SurfaceField . aggregateFieldName)) fields
)
]
up <- optional pUpcast
pure
( Event
{ evName = name,
evBody = body,
evVersion = ver,
evUpcastFrom = up,
evRetiring = retiring,
evDeprecated = deprecated,
evLoc = loc
},
elements
)
where
pUpcast = do
keyword "upcast"
keyword "from"
m <- pVersion
_ <- symbol "="
keyword "HOLE"
pure (m, Hole)
pWire :: P WireSpec
pWire = do
keyword "wire"
_ <- symbol "kind"
_ <- symbol "="
k <- wireWord
_ <- symbol "fields"
_ <- symbol "="
f <- wireWord
_ <- symbol "schemaVersion"
_ <- symbol "="
v <- boundedDecimal
pure WireSpec {wireKind = k, wireFields = f, wireSchemaVersion = v}
pProjection :: FrontendContext -> P ProjectionSpec
pProjection context = do
loc <- getLoc
keyword "projection"
table <- ident
cons <-
if frontendSupportsFeature context SeparatedProjectionQueryPolicySyntax
then do
startOffset <- getOffset
legacyConsistency <- optional (lookAhead (keyword "consistency"))
case legacyConsistency of
Just _ -> failAt startOffset "Language 5 aggregate projections do not declare consistency; put `freshness` on the referenced readmodel"
Nothing -> pure Nothing
else optional (symbol "consistency" *> symbol "=" *> pConsistency)
_ <- symbol "key"
_ <- symbol "="
k <- ident
sm <- optional pStatusMap
pure
ProjectionSpec
{ projTable = table,
projConsistency = cons,
projKey = k,
projStatusMap = sm,
projLoc = loc
}
where
pConsistency =
choice [Strong <$ keyword "Strong", Eventual <$ keyword "Eventual"]
pStatusMap :: P Mapping
pStatusMap = do
keyword "status-map"
partial <- option False (True <$ keyword "partial")
pairs <- braces (many pPair)
pure Mapping {mapPairs = pairs, mapPartial = partial}
where
pPair = do
l <- ident
_ <- symbol "=>"
r <- wireWord
pure (l, r)
--------------------------------------------------------------------------------
-- Transitions
--------------------------------------------------------------------------------
data Clause
= CGuard Expr
| CWrite Name Expr
| CEmit Name
| COutcome TransitionOutcome
| CGoto Name
| CImplementationHole
pTransition :: FrontendContext -> P (Transition, [Located SurfaceElement])
pTransition context = do
startOffset <- getOffset
loc <- getLoc
-- Plan 143: a @replay-only@ prefix marks the transition as serving
-- inversion only; it lowers to a keiki 'ReplayOnly' edge.
mode <- option TmLive (TmReplayOnly <$ keyword "replay-only")
src <- ident
_ <- symbol "--"
cmd <- ident
_ <- symbol "-->"
positionedClauses <- many ((,) <$> getOffset <*> (pClause context <* optional (symbol ";")))
let clauses = map (fst . snd) positionedClauses
elements = concatMap (snd . snd) positionedClauses
gotos = [(offset, target) | (offset, (CGoto target, _)) <- positionedClauses]
holeOffsets = [offset | (offset, (CImplementationHole, _)) <- positionedClauses]
outcomes = [(offset, outcome) | (offset, (COutcome outcome, _)) <- positionedClauses]
transitionName = T.unpack src <> " -- " <> T.unpack cmd
gt <- case gotos of
[] -> failAt startOffset ("transition " <> transitionName <> " is missing a goto clause")
[(_, target)] -> pure target
(_, firstTarget) : (duplicateOffset, _) : _ ->
failAt
duplicateOffset
("duplicate goto clause (transition " <> transitionName <> " already declared goto " <> T.unpack firstTarget <> ")")
case holeOffsets of
_ : duplicateOffset : _ -> failAt duplicateOffset ("duplicate implementation hole clause in transition " <> transitionName)
_ -> pure ()
let guards = [e | CGuard e <- clauses]
pure
( Transition
{ tSource = src,
tCommand = cmd,
tImplementation = case holeOffsets of
_ : _ -> HoleImplementation
[] | frontendSupportsFeature context TypedAggregateExpressionSyntax -> GeneratedImplementation
[] -> LegacyHoleImplementation,
tGuard = case guards of [] -> Nothing; es -> Just (foldr1 EAnd es),
tWrites = [(r, e) | CWrite r e <- clauses],
tEmits = [n | CEmit n <- clauses],
tOutcome = case outcomes of
(_, outcome) : _ -> Just outcome
[] -> Nothing,
tOutcomeDuplicateLocs = map (transitionOutcomeLoc . snd) (drop 1 outcomes),
tGoto = gt,
tMode = mode,
tLoc = loc
},
elements
)
pClause :: FrontendContext -> P (Clause, [Located SurfaceElement])
pClause context =
choice
[ do
loc <- getLoc
let outcomeSelectors :: [(T.Text, P (Clause, [Located SurfaceElement]))]
outcomeSelectors =
[ ("accepted", pure (COutcome (OutcomeAccepted loc), [])),
( "rejected",
do
expression <- withOwnedSpan (pExpr context)
pure (COutcome (OutcomeRejected (locatedValue expression) loc), [mapLocated SurfaceExpression expression])
),
( "no-op",
do
expression <- withOwnedSpan (pExpr context)
pure (COutcome (OutcomeNoOp (locatedValue expression) loc), [mapLocated SurfaceExpression expression])
)
]
-- @outcome@ is a contextual keyword, never reserved (plan 233, completing
-- plan 232's decision for accepted/rejected/no-op): claim it only when one
-- of the three selectors follows, so @outcome@ stays a legal identifier in
-- every language -- including a transition source named @outcome@, whose
-- next token is @--@.
marker <-
withOwnedSpan
( try
( keyword "outcome"
<* lookAhead (choice [keyword name | (name, _) <- outcomeSelectors])
)
)
requireLanguageFeatureAt context DomainCommandOutcomeSyntax (spanOf marker)
choice [keyword name *> handler | (name, handler) <- outcomeSelectors],
do
marker <- withOwnedSpan (keyword "implementation" *> keyword "hole")
requireLanguageFeatureAt context ExplicitTransitionImplementationSyntax (spanOf marker)
pure (CImplementationHole, []),
do
keyword "guard"
expression <- withOwnedSpan (pExpr context)
pure (CGuard (locatedValue expression), [mapLocated SurfaceExpression expression]),
do
register <- keyword "write" *> ident
_ <- symbol ":="
expression <- withOwnedSpan (pExpr context)
pure (CWrite register (locatedValue expression), [mapLocated SurfaceExpression expression]),
try $ do
keyword "emit"
eventName <- ident
notFollowedBy (symbol "{")
pure (CEmit eventName, []),
(\target -> (CGoto target, [])) <$> (keyword "goto" *> ident)
]
--------------------------------------------------------------------------------