packages feed

keiro-dsl-0.8.0.0: src/Keiro/Dsl/Parser/Aggregate.hs

{-# LANGUAGE ImportQualifiedPost #-}

-- | 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 Transition [Located SurfaceElement]

pAggregate :: FrontendContext -> P (Aggregate, [Located SurfaceElement])
pAggregate context = do
  loc <- getLoc
  keyword "aggregate"
  name <- ident
  regs <- pRegsBlock context
  states <- pStatesLine
  positionedItems <- many ((,) <$> getOffset <*> pBodyItem context)
  let items = map snd positionedItems
      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 = states,
          aggCommands = [c | BICommand c _ <- items],
          aggEvents = [e | BIEvent e _ <- items],
          aggTransitions = [t | BITransition t _ <- items],
          aggWire = listToMaybe [w | BIWire w <- items],
          aggProjection = listToMaybe [p | BIProjection p <- items],
          aggSnapshot = listToMaybe [s | BISnapshot s <- items],
          aggLoc = loc
        },
      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 _ -> []

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 [StateDecl]
pStatesLine = do
  keyword "states"
  many 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,
      BISnapshot <$> pSnapshot,
      uncurry BITransition <$> pTransition context
    ]

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
  mty <- optional (symbol ":" *> pMappedTypeExpr context)
  pure AggregateField {aggregateFieldName = n, 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 :: P ProjectionSpec
pProjection = do
  loc <- getLoc
  keyword "projection"
  table <- ident
  cons <- 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
  | 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]
      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],
          tGoto = gt,
          tMode = mode,
          tLoc = loc
        },
      elements
    )

pClause :: FrontendContext -> P (Clause, [Located SurfaceElement])
pClause context =
  choice
    [ 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)
    ]

--------------------------------------------------------------------------------