packages feed

keiro-dsl 0.7.0.0 → 0.8.0.0

raw patch · 26 files changed

+4162/−2184 lines, 26 filesdep ~keiro-corePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: keiro-core

API changes (from Hackage documentation)

- Keiro.Dsl.Parser: instance GHC.Classes.Eq Keiro.Dsl.Parser.ContextualParseFailure
- Keiro.Dsl.Parser: instance GHC.Classes.Ord Keiro.Dsl.Parser.ContextualParseFailure
- Keiro.Dsl.Parser: instance GHC.Internal.Show.Show Keiro.Dsl.Parser.ContextualParseFailure
- Keiro.Dsl.Parser: instance Text.Megaparsec.Error.ShowErrorComponent Keiro.Dsl.Parser.ContextualParseFailure
+ Keiro.Dsl.Frontend: BodyParsingPhase :: FrontendPhase
+ Keiro.Dsl.Frontend: BodySyntaxError :: FrontendErrorCode
+ Keiro.Dsl.Frontend: FrontendContext :: !FilePath -> !SourceLanguage -> !LanguageDefinition -> FrontendContext
+ Keiro.Dsl.Frontend: FrontendFailure :: !FrontendPhase -> !FrontendErrorCode -> !SourceSpan -> !Text -> ![Text] -> ![LanguageVersion] -> !ParseFailure -> FrontendFailure
+ Keiro.Dsl.Frontend: InvalidSourceSpan :: LoweringFailureCode
+ Keiro.Dsl.Frontend: LoweringError :: !LoweringFailureCode -> FrontendErrorCode
+ Keiro.Dsl.Frontend: LoweringFailure :: !LoweringFailureCode -> !SourceSpan -> !Text -> LoweringFailure
+ Keiro.Dsl.Frontend: LoweringPhase :: FrontendPhase
+ Keiro.Dsl.Frontend: SourceLanguageError :: !SourceLanguageErrorCode -> FrontendErrorCode
+ Keiro.Dsl.Frontend: SourceNameMismatch :: LoweringFailureCode
+ Keiro.Dsl.Frontend: SourceSelectionPhase :: FrontendPhase
+ Keiro.Dsl.Frontend: SourceSelectionSyntaxError :: FrontendErrorCode
+ Keiro.Dsl.Frontend: SurfaceOrderInvalid :: LoweringFailureCode
+ Keiro.Dsl.Frontend: [code] :: LoweringFailure -> !LoweringFailureCode
+ Keiro.Dsl.Frontend: [compatibility] :: FrontendFailure -> !ParseFailure
+ Keiro.Dsl.Frontend: [definition] :: FrontendContext -> !LanguageDefinition
+ Keiro.Dsl.Frontend: [expected] :: FrontendFailure -> ![Text]
+ Keiro.Dsl.Frontend: [language] :: FrontendContext -> !SourceLanguage
+ Keiro.Dsl.Frontend: [message] :: LoweringFailure -> !Text
+ Keiro.Dsl.Frontend: [phase] :: FrontendFailure -> !FrontendPhase
+ Keiro.Dsl.Frontend: [source] :: FrontendContext -> !FilePath
+ Keiro.Dsl.Frontend: [span] :: LoweringFailure -> !SourceSpan
+ Keiro.Dsl.Frontend: [supportedVersions] :: FrontendFailure -> ![LanguageVersion]
+ Keiro.Dsl.Frontend: data FrontendContext
+ Keiro.Dsl.Frontend: data FrontendErrorCode
+ Keiro.Dsl.Frontend: data FrontendFailure
+ Keiro.Dsl.Frontend: data FrontendPhase
+ Keiro.Dsl.Frontend: data LoweringFailure
+ Keiro.Dsl.Frontend: data LoweringFailureCode
+ Keiro.Dsl.Frontend: frontendErrorCodeText :: FrontendErrorCode -> Text
+ Keiro.Dsl.Frontend: frontendFailureFromLowering :: LoweringFailure -> FrontendFailure
+ Keiro.Dsl.Frontend: frontendLanguageVersion :: FrontendContext -> LanguageVersion
+ Keiro.Dsl.Frontend: frontendSupportsFeature :: FrontendContext -> LanguageFeature -> Bool
+ Keiro.Dsl.Frontend: lowerSurfaceSource :: SurfaceSource -> Either LoweringFailure ParsedSource
+ Keiro.Dsl.Frontend: parseSurfaceSource :: FilePath -> Text -> Either FrontendFailure SurfaceSource
+ Keiro.Dsl.Frontend: renderFrontendFailure :: FrontendFailure -> Text
+ Keiro.Dsl.Frontend: renderLoweringFailure :: LoweringFailure -> Text
+ Keiro.Dsl.LanguageVersion: [definitionRuntimeSemantics] :: LanguageDefinition -> !Text
+ Keiro.Dsl.LanguageVersion: [definitionSyntaxProfile] :: LanguageDefinition -> !SyntaxProfile
+ Keiro.Dsl.LanguageVersion: data SyntaxProfile
+ Keiro.Dsl.LanguageVersion: instance GHC.Classes.Eq Keiro.Dsl.LanguageVersion.SyntaxProfile
+ Keiro.Dsl.LanguageVersion: instance GHC.Internal.Generics.Generic Keiro.Dsl.LanguageVersion.LanguageDefinition
+ Keiro.Dsl.LanguageVersion: instance GHC.Internal.Generics.Generic Keiro.Dsl.LanguageVersion.SyntaxProfile
+ Keiro.Dsl.LanguageVersion: instance GHC.Internal.Show.Show Keiro.Dsl.LanguageVersion.SyntaxProfile
+ Keiro.Dsl.LanguageVersion: languageVersionsSupportingFeature :: LanguageFeature -> [LanguageVersion]
+ Keiro.Dsl.LanguageVersion: sourceLanguageDiagnosticMessage :: SourceLanguageDiagnostic -> Text
+ Keiro.Dsl.LanguageVersion: syntaxProfileIdentifier :: SyntaxProfile -> Text
+ Keiro.Dsl.LanguageVersion: syntaxProfileSupportsFeature :: SyntaxProfile -> LanguageFeature -> Bool
+ Keiro.Dsl.Source: Located :: !SourceSpan -> !a -> Located a
+ Keiro.Dsl.Source: SourcePoint :: !Int -> !Int -> !Int -> SourcePoint
+ Keiro.Dsl.Source: SourceSpan :: !FilePath -> !SourcePoint -> !SourcePoint -> SourceSpan
+ Keiro.Dsl.Source: [column] :: SourcePoint -> !Int
+ Keiro.Dsl.Source: [end] :: SourceSpan -> !SourcePoint
+ Keiro.Dsl.Source: [line] :: SourcePoint -> !Int
+ Keiro.Dsl.Source: [offset] :: SourcePoint -> !Int
+ Keiro.Dsl.Source: [source] :: SourceSpan -> !FilePath
+ Keiro.Dsl.Source: [span] :: Located a -> !SourceSpan
+ Keiro.Dsl.Source: [start] :: SourceSpan -> !SourcePoint
+ Keiro.Dsl.Source: [value] :: Located a -> !a
+ Keiro.Dsl.Source: data Located a
+ Keiro.Dsl.Source: data SourcePoint
+ Keiro.Dsl.Source: data SourceSpan
+ Keiro.Dsl.Source: instance GHC.Classes.Eq Keiro.Dsl.Source.SourcePoint
+ Keiro.Dsl.Source: instance GHC.Classes.Eq Keiro.Dsl.Source.SourceSpan
+ Keiro.Dsl.Source: instance GHC.Classes.Eq a => GHC.Classes.Eq (Keiro.Dsl.Source.Located a)
+ Keiro.Dsl.Source: instance GHC.Classes.Ord Keiro.Dsl.Source.SourcePoint
+ Keiro.Dsl.Source: instance GHC.Classes.Ord Keiro.Dsl.Source.SourceSpan
+ Keiro.Dsl.Source: instance GHC.Classes.Ord a => GHC.Classes.Ord (Keiro.Dsl.Source.Located a)
+ Keiro.Dsl.Source: instance GHC.Internal.Base.Functor Keiro.Dsl.Source.Located
+ Keiro.Dsl.Source: instance GHC.Internal.Generics.Generic (Keiro.Dsl.Source.Located a)
+ Keiro.Dsl.Source: instance GHC.Internal.Generics.Generic Keiro.Dsl.Source.SourcePoint
+ Keiro.Dsl.Source: instance GHC.Internal.Generics.Generic Keiro.Dsl.Source.SourceSpan
+ Keiro.Dsl.Source: instance GHC.Internal.Show.Show Keiro.Dsl.Source.SourcePoint
+ Keiro.Dsl.Source: instance GHC.Internal.Show.Show Keiro.Dsl.Source.SourceSpan
+ Keiro.Dsl.Source: instance GHC.Internal.Show.Show a => GHC.Internal.Show.Show (Keiro.Dsl.Source.Located a)
+ Keiro.Dsl.Source: mapLocated :: (a -> b) -> Located a -> Located b
+ Keiro.Dsl.Source: mkSourcePoint :: Int -> Int -> Int -> Maybe SourcePoint
+ Keiro.Dsl.Source: mkSourceSpan :: FilePath -> SourcePoint -> SourcePoint -> Maybe SourceSpan
+ Keiro.Dsl.Source: startLine :: SourceSpan -> Int
+ Keiro.Dsl.Source: validSourceSpan :: SourceSpan -> Bool
+ Keiro.Dsl.Syntax: SurfaceEnum :: !EnumDecl -> SurfaceTopItem
+ Keiro.Dsl.Syntax: SurfaceExpression :: !Expr -> SurfaceElement
+ Keiro.Dsl.Syntax: SurfaceField :: !Text -> SurfaceElement
+ Keiro.Dsl.Syntax: SurfaceId :: !IdDecl -> SurfaceTopItem
+ Keiro.Dsl.Syntax: SurfaceMapped :: !MappedDecl -> SurfaceTopItem
+ Keiro.Dsl.Syntax: SurfaceNode :: !Node -> SurfaceTopItem
+ Keiro.Dsl.Syntax: SurfaceNominalScalar :: !NominalScalarDecl -> SurfaceTopItem
+ Keiro.Dsl.Syntax: SurfaceRule :: !RuleDecl -> SurfaceTopItem
+ Keiro.Dsl.Syntax: SurfaceSource :: !FilePath -> !SourceLanguage -> !Maybe (Located SourceLanguage) -> !Located SurfaceSpec -> SurfaceSource
+ Keiro.Dsl.Syntax: SurfaceSpec :: !Located Text -> !Maybe (Located Text) -> !Maybe (Located Placement) -> ![Located SurfaceTopItem] -> ![Located SurfaceElement] -> SurfaceSpec
+ Keiro.Dsl.Syntax: [context] :: SurfaceSpec -> !Located Text
+ Keiro.Dsl.Syntax: [elements] :: SurfaceSpec -> ![Located SurfaceElement]
+ Keiro.Dsl.Syntax: [items] :: SurfaceSpec -> ![Located SurfaceTopItem]
+ Keiro.Dsl.Syntax: [language] :: SurfaceSource -> !SourceLanguage
+ Keiro.Dsl.Syntax: [layout] :: SurfaceSpec -> !Maybe (Located Placement)
+ Keiro.Dsl.Syntax: [moduleRoot] :: SurfaceSpec -> !Maybe (Located Text)
+ Keiro.Dsl.Syntax: [preamble] :: SurfaceSource -> !Maybe (Located SourceLanguage)
+ Keiro.Dsl.Syntax: [source] :: SurfaceSource -> !FilePath
+ Keiro.Dsl.Syntax: [spec] :: SurfaceSource -> !Located SurfaceSpec
+ Keiro.Dsl.Syntax: data SurfaceElement
+ Keiro.Dsl.Syntax: data SurfaceSource
+ Keiro.Dsl.Syntax: data SurfaceSpec
+ Keiro.Dsl.Syntax: data SurfaceTopItem
+ Keiro.Dsl.Syntax: instance GHC.Classes.Eq Keiro.Dsl.Syntax.SurfaceElement
+ Keiro.Dsl.Syntax: instance GHC.Classes.Eq Keiro.Dsl.Syntax.SurfaceSource
+ Keiro.Dsl.Syntax: instance GHC.Classes.Eq Keiro.Dsl.Syntax.SurfaceSpec
+ Keiro.Dsl.Syntax: instance GHC.Classes.Eq Keiro.Dsl.Syntax.SurfaceTopItem
+ Keiro.Dsl.Syntax: instance GHC.Internal.Generics.Generic Keiro.Dsl.Syntax.SurfaceElement
+ Keiro.Dsl.Syntax: instance GHC.Internal.Generics.Generic Keiro.Dsl.Syntax.SurfaceSource
+ Keiro.Dsl.Syntax: instance GHC.Internal.Generics.Generic Keiro.Dsl.Syntax.SurfaceSpec
+ Keiro.Dsl.Syntax: instance GHC.Internal.Generics.Generic Keiro.Dsl.Syntax.SurfaceTopItem
+ Keiro.Dsl.Syntax: instance GHC.Internal.Show.Show Keiro.Dsl.Syntax.SurfaceElement
+ Keiro.Dsl.Syntax: instance GHC.Internal.Show.Show Keiro.Dsl.Syntax.SurfaceSource
+ Keiro.Dsl.Syntax: instance GHC.Internal.Show.Show Keiro.Dsl.Syntax.SurfaceSpec
+ Keiro.Dsl.Syntax: instance GHC.Internal.Show.Show Keiro.Dsl.Syntax.SurfaceTopItem
- Keiro.Dsl.LanguageVersion: LanguageDefinition :: !LanguageVersion -> !Maybe LanguageVersion -> !LanguageBodyParser -> LanguageDefinition
+ Keiro.Dsl.LanguageVersion: LanguageDefinition :: !LanguageVersion -> !Maybe LanguageVersion -> !LanguageBodyParser -> !SyntaxProfile -> !Text -> LanguageDefinition

Files

CHANGELOG.md view
@@ -6,6 +6,43 @@  ## [Unreleased] +## 0.8.0.0 — 2026-08-01++### Breaking Changes++- `LanguageDefinition` gains two fields — `definitionSyntaxProfile` and+  `definitionRuntimeSemantics`. The type is exported as `LanguageDefinition (..)` from+  `Keiro.Dsl.LanguageVersion`, so positional construction and non-wildcard record patterns no longer+  compile. `definitionBodyParser` is retained as a compatibility projection but is no longer+  consulted for parser dispatch.++### New Features++- Adds the public `Keiro.Dsl.Source`, `Keiro.Dsl.Syntax`, and `Keiro.Dsl.Frontend` advanced API.+  Source-aware tooling can parse an ordered, located `SurfaceSource`, inspect exact half-open spans,+  and lower explicitly to the existing `ParsedSource`/`Spec` semantic boundary.+- Adds structured frontend failures with source-selection, body-parsing, and lowering phases;+  stable codes; exact primary spans; messages; expected items; and supported-version metadata.+  Megaparsec types remain internal.+- Adds `syntaxProfileIdentifier`, `syntaxProfileSupportsFeature`,+  `languageVersionsSupportingFeature`, and `sourceLanguageDiagnosticMessage` to+  `Keiro.Dsl.LanguageVersion`.++### Other Changes++- Organizes the `.keiro` grammar into internal concern modules behind the stable+  `Keiro.Dsl.Parser` compatibility facade. CLI and workspace members still parse once through that+  facade; semantic checking, scaffolding, diffing, fingerprints, and replay consume only lowered+  semantic values.+- Makes every released registry entry explicitly select an immutable syntax profile and runtime-+  semantics identity. Version 3 deliberately reuses version 2's syntax profile; adding a future+  version no longer inherits syntax or runtime behavior from numeric ordering.+- Keeps `parseSource`, `parseSpec`, `parseSpecText`, their rendered diagnostics, the complete 0.7+  acceptance matrix, and generated Haskell bytes unchanged. Canonical pretty printing remains+  non-lossless: the located surface layer does not retain comments or whitespace.+- Derives `languageFeatureMinimumVersion` and `languageSupportsFeature` from the registry's syntax+  profiles instead of numeric version ordering.+ ## 0.7.0.0 — 2026-08-01  ### Breaking Changes
keiro-dsl.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.0 name:            keiro-dsl-version:         0.7.0.0+version:         0.8.0.0 synopsis:        Typed specification toolchain for keiro services description:   keiro-dsl is the toolchain over a typed `.keiro` specification of a keiro@@ -42,6 +42,7 @@     Keiro.Dsl.ExplainBindings     Keiro.Dsl.Expression     Keiro.Dsl.FoldFingerprint+    Keiro.Dsl.Frontend     Keiro.Dsl.Goldens     Keiro.Dsl.Grammar     Keiro.Dsl.Harness@@ -60,6 +61,8 @@     Keiro.Dsl.ScaffoldRun     Keiro.Dsl.SemanticContract     Keiro.Dsl.Skeleton+    Keiro.Dsl.Source+    Keiro.Dsl.Syntax     Keiro.Dsl.TypeGraph     Keiro.Dsl.Validate     Keiro.Dsl.Workspace@@ -68,6 +71,21 @@     Keiro.Dsl.WorkspaceRecord     Keiro.Dsl.WorkspaceScaffold +  other-modules:+    Keiro.Dsl.Frontend.Internal+    Keiro.Dsl.Parser.Aggregate+    Keiro.Dsl.Parser.Coordination+    Keiro.Dsl.Parser.Core+    Keiro.Dsl.Parser.Declaration+    Keiro.Dsl.Parser.Document+    Keiro.Dsl.Parser.Expression+    Keiro.Dsl.Parser.Integration+    Keiro.Dsl.Parser.Mapped+    Keiro.Dsl.Parser.Preamble+    Keiro.Dsl.Parser.Queue+    Keiro.Dsl.Parser.ReadModel+    Keiro.Dsl.Parser.Workflow+   build-depends:     , aeson               >=2.2.1    && <2.3     , base                >=4.21     && <5@@ -76,7 +94,7 @@     , directory           >=1.3      && <1.4     , filepath            >=1.4      && <1.6     , keiki               >=0.7      && <0.8-    , keiro-core          ^>=0.7.0.0+    , keiro-core          ^>=0.8.0.0     , megaparsec          >=9.6      && <9.9     , mmzk-typeid         >=0.7      && <0.8     , parser-combinators  >=1.3      && <1.4@@ -103,6 +121,12 @@   type:           exitcode-stdio-1.0   hs-source-dirs: test   main-is:        Main.hs+  other-modules:+    Keiro.Dsl.FrontendCompatibility+    Keiro.Dsl.FrontendProfiles+    Keiro.Dsl.FrontendPublicApiProbe+    Keiro.Dsl.FrontendSurface+   build-depends:     , aeson       >=2.2  && <2.3     , base        >=4.21 && <5
+ src/Keiro/Dsl/Frontend.hs view
@@ -0,0 +1,42 @@+-- | Advanced source-aware entry points for the Keiro DSL frontend.+--+-- Ordinary callers can continue to use "Keiro.Dsl.Parser". This module owns+-- the located surface/lowering seam without exposing Megaparsec.+--+-- A source-aware caller parses once, inspects exact half-open spans, and then+-- lowers into the same 'Keiro.Dsl.LanguageVersion.ParsedSource' returned by+-- the compatibility facade:+--+-- @+-- case parseSurfaceSource "orders.keiro" input of+--   Left failure -> renderFrontendFailure failure+--   Right surface ->+--     case lowerSurfaceSource surface of+--       Left loweringFailure -> renderLoweringFailure loweringFailure+--       Right _parsed -> "ready for semantic checking"+-- @+--+-- Pattern matching on 'Keiro.Dsl.Source.SourceSpan' exposes source name,+-- token offsets, and one-based line/column points. The surface tree does not+-- retain comments or whitespace; canonical pretty printing is therefore not+-- a lossless source formatter.+module Keiro.Dsl.Frontend+  ( FrontendContext (..),+    frontendLanguageVersion,+    frontendSupportsFeature,+    FrontendPhase (..),+    FrontendErrorCode (..),+    frontendErrorCodeText,+    FrontendFailure (..),+    renderFrontendFailure,+    LoweringFailureCode (..),+    LoweringFailure (..),+    renderLoweringFailure,+    frontendFailureFromLowering,+    parseSurfaceSource,+    lowerSurfaceSource,+  )+where++import Keiro.Dsl.Frontend.Internal+import Keiro.Dsl.Parser.Document (parseSurfaceSource)
+ src/Keiro/Dsl/Frontend/Internal.hs view
@@ -0,0 +1,307 @@+{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE NoFieldSelectors #-}++-- | Shared implementation behind the public frontend and parser compatibility+-- facade. This module is intentionally not exposed by the package.+module Keiro.Dsl.Frontend.Internal+  ( FrontendContext (..),+    frontendLanguageVersion,+    frontendSupportsFeature,+    FrontendPhase (..),+    FrontendErrorCode (..),+    frontendErrorCodeText,+    FrontendFailure (..),+    frontendCompatibilityFailure,+    frontendFailureFromSourceDiagnostic,+    frontendFailureFromBody,+    frontendFailureFromLowering,+    renderFrontendFailure,+    LoweringFailureCode (..),+    LoweringFailure (..),+    renderLoweringFailure,+    lowerSurfaceSource,+  )+where++import Data.Foldable (traverse_)+import Data.List (find)+import Data.List.NonEmpty qualified as NE+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import Data.Text qualified as T+import GHC.Generics (Generic)+import Keiro.Dsl.Grammar+import Keiro.Dsl.LanguageVersion+  ( LanguageDefinition,+    LanguageFeature,+    LanguageVersion,+    ParseFailure (..),+    ParsedSource (..),+    SourceLanguage,+    SourceLanguageDiagnostic (..),+    SourceLanguageErrorCode,+    definitionVersion,+    languageSupportsFeature,+    renderParseFailure,+    sourceLanguageDiagnosticMessage,+    sourceLanguageErrorCodeText,+  )+import Keiro.Dsl.Source+import Keiro.Dsl.Syntax+import Prelude hiding (span)++-- | The single released-language selection threaded through the modular+-- grammar. Grammar productions ask this context about exact profile+-- membership rather than comparing version numbers.+data FrontendContext = FrontendContext+  { source :: !FilePath,+    language :: !SourceLanguage,+    definition :: !LanguageDefinition+  }+  deriving stock (Eq, Show, Generic)++frontendLanguageVersion :: FrontendContext -> LanguageVersion+frontendLanguageVersion FrontendContext {definition} = definitionVersion definition++frontendSupportsFeature :: FrontendContext -> LanguageFeature -> Bool+frontendSupportsFeature context feature = languageSupportsFeature (frontendLanguageVersion context) feature++data FrontendPhase+  = SourceSelectionPhase+  | BodyParsingPhase+  | LoweringPhase+  deriving stock (Eq, Ord, Show, Generic)++data FrontendErrorCode+  = SourceLanguageError !SourceLanguageErrorCode+  | SourceSelectionSyntaxError+  | BodySyntaxError+  | LoweringError !LoweringFailureCode+  deriving stock (Eq, Ord, Show, Generic)++frontendErrorCodeText :: FrontendErrorCode -> Text+frontendErrorCodeText = \case+  SourceLanguageError code -> sourceLanguageErrorCodeText code+  SourceSelectionSyntaxError -> "SourceSelectionSyntaxError"+  BodySyntaxError -> "BodySyntaxError"+  LoweringError code -> T.pack (show code)++-- | A source-aware frontend failure. The compatibility projection is retained+-- as data so the released parser facade can render byte-identical diagnostics+-- without exposing Megaparsec types.+data FrontendFailure = FrontendFailure+  { phase :: !FrontendPhase,+    code :: !FrontendErrorCode,+    span :: !SourceSpan,+    message :: !Text,+    expected :: ![Text],+    supportedVersions :: ![LanguageVersion],+    compatibility :: !ParseFailure+  }+  deriving stock (Eq, Show, Generic)++renderFrontendFailure :: FrontendFailure -> Text+renderFrontendFailure FrontendFailure {compatibility} = renderParseFailure compatibility++frontendCompatibilityFailure :: FrontendFailure -> ParseFailure+frontendCompatibilityFailure FrontendFailure {compatibility} = compatibility++frontendFailureFromSourceDiagnostic :: FrontendPhase -> SourceSpan -> Maybe [LanguageVersion] -> SourceLanguageDiagnostic -> FrontendFailure+frontendFailureFromSourceDiagnostic phase span supportedOverride diagnostic =+  FrontendFailure+    { phase,+      code = SourceLanguageError (sourceLanguageErrorCode diagnostic),+      span,+      message = sourceLanguageDiagnosticMessage diagnostic,+      expected = [],+      supportedVersions = fromMaybe (NE.toList (sourceLanguageSupportedVersions diagnostic)) supportedOverride,+      compatibility = SourceLanguageFailure diagnostic+    }++frontendFailureFromBody :: FrontendPhase -> SourceSpan -> Text -> [Text] -> ParseFailure -> FrontendFailure+frontendFailureFromBody phase span message expected compatibility =+  FrontendFailure+    { phase,+      code = case phase of+        SourceSelectionPhase -> SourceSelectionSyntaxError+        BodyParsingPhase -> BodySyntaxError+        LoweringPhase -> BodySyntaxError,+      span,+      message,+      expected,+      supportedVersions = [],+      compatibility+    }++data LoweringFailureCode+  = InvalidSourceSpan+  | SourceNameMismatch+  | SurfaceOrderInvalid+  deriving stock (Eq, Ord, Show, Generic)++-- | A failure found while converting surface evidence to the semantic graph.+data LoweringFailure = LoweringFailure+  { code :: !LoweringFailureCode,+    span :: !SourceSpan,+    message :: !Text+  }+  deriving stock (Eq, Show, Generic)++renderLoweringFailure :: LoweringFailure -> Text+renderLoweringFailure+  LoweringFailure+    { code,+      span = SourceSpan {source, start = SourcePoint {line, column}},+      message+    } =+    T.pack source+      <> ":"+      <> T.pack (show line)+      <> ":"+      <> T.pack (show column)+      <> ": error ["+      <> T.pack (show code)+      <> "]: "+      <> message++frontendFailureFromLowering :: LoweringFailure -> FrontendFailure+frontendFailureFromLowering failure@LoweringFailure {code, span, message} =+  FrontendFailure+    { phase = LoweringPhase,+      code = LoweringError code,+      span,+      message,+      expected = [],+      supportedVersions = [],+      compatibility = BodyGrammarFailure (renderLoweringFailure failure)+    }++-- | Remove document order and exact locations while projecting each top-level+-- span's starting line into the compatibility 'Loc'.+lowerSurfaceSource :: SurfaceSource -> Either LoweringFailure ParsedSource+lowerSurfaceSource surfaceSource@SurfaceSource {language, spec = locatedSpec} = do+  validateSurfaceSource surfaceSource+  pure+    ParsedSource+      { parsedSourceLanguage = language,+        parsedSpec = lowerSpec locatedSpec+      }++lowerSpec :: Located SurfaceSpec -> Spec+lowerSpec+  Located+    { value =+        SurfaceSpec+          { context = Located {value = contextName},+            moduleRoot,+            layout,+            items+          }+    } =+    Spec+      { specContext = contextName,+        specModuleRoot = locatedValue <$> moduleRoot,+        specLayout = locatedValue <$> layout,+        specIds = [declaration | Located {span, value = SurfaceId value} <- items, let declaration = value {idLoc = spanLoc span}],+        specEnums = [declaration | Located {span, value = SurfaceEnum value} <- items, let declaration = value {enumLoc = spanLoc span}],+        specRules = [declaration | Located {span, value = SurfaceRule value} <- items, let declaration = value {ruleLoc = spanLoc span}],+        specNominalScalars = [declaration | Located {span, value = SurfaceNominalScalar value} <- items, let declaration = value {nominalScalarLoc = spanLoc span}],+        specMapped = [declaration | Located {span, value = SurfaceMapped value} <- items, let declaration = setMappedLoc (spanLoc span) value],+        specNodes = [node | Located {span, value = SurfaceNode value} <- items, let node = setNodeLoc (spanLoc span) value]+      }++locatedValue :: Located a -> a+locatedValue Located {value} = value++spanLoc :: SourceSpan -> Loc+spanLoc sourceSpan = Loc (startLine sourceSpan)++setMappedLoc :: Loc -> MappedDecl -> MappedDecl+setMappedLoc loc value@MappedStructural {} = value {msLoc = loc}+setMappedLoc loc value@MappedOpaque {} = value {moLoc = loc}++setNodeLoc :: Loc -> Node -> Node+setNodeLoc loc = \case+  NAggregate value -> NAggregate value {aggLoc = loc}+  NProcess value -> NProcess value {procLoc = loc}+  NRouter value -> NRouter value {rtLoc = loc}+  NContract value -> NContract value {ctrLoc = loc}+  NIntake value -> NIntake value {inkLoc = loc}+  NEmit value -> NEmit value {emLoc = loc}+  NPublisher value -> NPublisher value {pubLoc = loc}+  NWorkqueue value -> NWorkqueue value {wqLoc = loc}+  NPgmqDispatch value -> NPgmqDispatch value {pdLoc = loc}+  NReadModel value -> NReadModel value {rmLoc = loc}+  NWorkflow+    WorkflowNode+      { wfId,+        wfStable,+        wfInput,+        wfInputFields,+        wfOutput,+        wfIdField,+        wfIdVia,+        wfBody+      } ->+      NWorkflow+        WorkflowNode+          { wfId,+            wfStable,+            wfInput,+            wfInputFields,+            wfOutput,+            wfIdField,+            wfIdVia,+            wfBody,+            wfLoc = loc+          }+  NOperation value -> NOperation value {opLoc = loc}++validateSurfaceSource :: SurfaceSource -> Either LoweringFailure ()+validateSurfaceSource+  SurfaceSource+    { source = sourceName,+      preamble,+      spec = Located {span = specSpan, value = surfaceSpec}+    } = do+    traverse_ (validateOwnedSpan sourceName) allSpans+    traverse_ (validateContained specSpan) bodySpans+    validateOrder (surfaceItemSpans surfaceSpec)+    where+      bodySpans = surfaceSpecSpans surfaceSpec+      allSpans = specSpan : maybe [] (\Located {span} -> [span]) preamble <> bodySpans+      validateOwnedSpan expected sourceSpan@SourceSpan {source}+        | not (validSourceSpan sourceSpan) =+            Left LoweringFailure {code = InvalidSourceSpan, span = sourceSpan, message = "source span end precedes its start"}+        | source /= expected =+            Left LoweringFailure {code = SourceNameMismatch, span = sourceSpan, message = "surface span belongs to a different source"}+        | otherwise = Right ()+      validateContained outer inner+        | contains outer inner = Right ()+        | otherwise = Left LoweringFailure {code = InvalidSourceSpan, span = inner, message = "surface element lies outside the document body span"}++surfaceSpecSpans :: SurfaceSpec -> [SourceSpan]+surfaceSpecSpans SurfaceSpec {context = Located {span = contextSpan}, moduleRoot, layout, items, elements} =+  contextSpan+    : maybe [] (\Located {span} -> [span]) moduleRoot+      <> maybe [] (\Located {span} -> [span]) layout+      <> map (\Located {span} -> span) items+      <> map (\Located {span} -> span) elements++surfaceItemSpans :: SurfaceSpec -> [SourceSpan]+surfaceItemSpans SurfaceSpec {items} = map (\Located {span} -> span) items++contains :: SourceSpan -> SourceSpan -> Bool+contains+  SourceSpan {start = outerStart, end = outerEnd}+  SourceSpan {start = innerStart, end = innerEnd} =+    outerStart <= innerStart && innerEnd <= outerEnd++validateOrder :: [SourceSpan] -> Either LoweringFailure ()+validateOrder spans =+  case find outOfOrder (zip spans (drop 1 spans)) of+    Nothing -> Right ()+    Just (_, offendingSpan) ->+      Left LoweringFailure {code = SurfaceOrderInvalid, span = offendingSpan, message = "top-level surface items are not in source order"}+  where+    outOfOrder (SourceSpan {start = previousStart}, SourceSpan {start = nextStart}) = previousStart > nextStart
src/Keiro/Dsl/LanguageVersion.hs view
@@ -14,16 +14,21 @@     declaredLanguageVersionMaybe,     effectiveLanguageVersion,     LanguageBodyParser (..),+    SyntaxProfile,+    syntaxProfileIdentifier,+    syntaxProfileSupportsFeature,     LanguageDefinition (..),     languageRegistry,     supportedLanguageVersions,     lookupLanguageDefinition,     LanguageFeature (..),     languageFeatureMinimumVersion,+    languageVersionsSupportingFeature,     languageSupportsFeature,     SourceLanguageErrorCode (..),     sourceLanguageErrorCodeText,     SourceLanguageDiagnostic (..),+    sourceLanguageDiagnosticMessage,     renderSourceLanguageDiagnostic,     ParsedSource (..),     ParseFailure (..),@@ -35,8 +40,11 @@ import Data.List (find) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NE+import Data.Set (Set)+import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T+import GHC.Generics (Generic) import Keiro.Dsl.Grammar (Loc (..), Spec, noLoc) import Numeric.Natural (Natural) @@ -90,13 +98,32 @@   | LanguageBodyParserV2   deriving stock (Eq, Show) +-- | An immutable, explicitly named set of source-language capabilities.+-- Constructors stay private so a released profile can only be selected from+-- the authoritative registry rather than widened ad hoc by parser callers.+data SyntaxProfile = SyntaxProfile+  { profileIdentifier :: !Text,+    profileFeatures :: !(Set LanguageFeature)+  }+  deriving stock (Eq, Show, Generic)++syntaxProfileIdentifier :: SyntaxProfile -> Text+syntaxProfileIdentifier SyntaxProfile {profileIdentifier} = profileIdentifier++syntaxProfileSupportsFeature :: SyntaxProfile -> LanguageFeature -> Bool+syntaxProfileSupportsFeature SyntaxProfile {profileFeatures} feature = Set.member feature profileFeatures+ -- | One append-only released-language registry entry. data LanguageDefinition = LanguageDefinition   { definitionVersion :: !LanguageVersion,     definitionPredecessor :: !(Maybe LanguageVersion),-    definitionBodyParser :: !LanguageBodyParser+    -- | Compatibility projection retained for the 0.7 public API. Parser+    -- dispatch uses 'definitionSyntaxProfile', never this historical tag.+    definitionBodyParser :: !LanguageBodyParser,+    definitionSyntaxProfile :: !SyntaxProfile,+    definitionRuntimeSemantics :: !Text   }-  deriving stock (Eq, Show)+  deriving stock (Eq, Show, Generic)  version1 :: LanguageVersion version1 = LanguageVersion 1@@ -110,11 +137,26 @@ -- | The authoritative, append-only registry of released language contracts. languageRegistry :: NonEmpty LanguageDefinition languageRegistry =-  LanguageDefinition version1 Nothing LanguageBodyParserV1-    :| [ LanguageDefinition version2 (Just version1) LanguageBodyParserV2,-         LanguageDefinition version3 (Just version2) LanguageBodyParserV2+  LanguageDefinition version1 Nothing LanguageBodyParserV1 profileV1 "keiro-dsl/runtime-semantics/1"+    :| [ LanguageDefinition version2 (Just version1) LanguageBodyParserV2 profileV2 "keiro-dsl/runtime-semantics/1",+         LanguageDefinition version3 (Just version2) LanguageBodyParserV2 profileV2 "keiro-dsl/runtime-semantics/2"        ] +profileV1 :: SyntaxProfile+profileV1 = SyntaxProfile "keiro-dsl/syntax-profile/1" Set.empty++profileV2 :: SyntaxProfile+profileV2 =+  SyntaxProfile+    "keiro-dsl/syntax-profile/2"+    ( Set.fromList+        [ NominalBindingSyntax,+          IntegerScalarSyntax,+          TypedAggregateExpressionSyntax,+          ExplicitTransitionImplementationSyntax+        ]+    )+ -- | Supported versions, derived from 'languageRegistry'. supportedLanguageVersions :: NonEmpty LanguageVersion supportedLanguageVersions = definitionVersion <$> languageRegistry@@ -135,14 +177,21 @@  -- | The first released contract that owns each grammar feature. languageFeatureMinimumVersion :: LanguageFeature -> LanguageVersion-languageFeatureMinimumVersion = \case-  NominalBindingSyntax -> version2-  IntegerScalarSyntax -> version2-  TypedAggregateExpressionSyntax -> version2-  ExplicitTransitionImplementationSyntax -> version2+languageFeatureMinimumVersion feature =+  case find (\definition -> syntaxProfileSupportsFeature (definitionSyntaxProfile definition) feature) (NE.toList languageRegistry) of+    Just definition -> definitionVersion definition+    Nothing -> error "keiro-dsl internal invariant: a released language feature has no owning profile" +languageVersionsSupportingFeature :: LanguageFeature -> [LanguageVersion]+languageVersionsSupportingFeature feature =+  [ definitionVersion definition+  | definition <- NE.toList languageRegistry,+    syntaxProfileSupportsFeature (definitionSyntaxProfile definition) feature+  ]+ languageSupportsFeature :: LanguageVersion -> LanguageFeature -> Bool-languageSupportsFeature version feature = version >= languageFeatureMinimumVersion feature+languageSupportsFeature version feature =+  maybe False (\definition -> syntaxProfileSupportsFeature (definitionSyntaxProfile definition) feature) (lookupLanguageDefinition version)  effectiveLanguageVersion :: SourceLanguage -> LanguageVersion effectiveLanguageVersion LegacyUnversioned = version1@@ -203,9 +252,14 @@     <> ":1: error ["     <> sourceLanguageErrorCodeText code     <> "]: "-    <> detail+    <> sourceLanguageDiagnosticMessage diagnostic   where     Loc line = sourceLanguageLoc diagnostic+    code = sourceLanguageErrorCode diagnostic++sourceLanguageDiagnosticMessage :: SourceLanguageDiagnostic -> Text+sourceLanguageDiagnosticMessage diagnostic = detail+  where     code = sourceLanguageErrorCode diagnostic     supported = T.intercalate ", " (map languageVersionText (NE.toList (sourceLanguageSupportedVersions diagnostic)))     token = maybe "<missing>" id (sourceLanguageToken diagnostic)
src/Keiro/Dsl/Parser.hs view
@@ -1,2165 +1,39 @@--- | The megaparsec parser for the keiro DSL. Turns @.keiro@ text into the typed--- 'Spec' AST. The notation is keyword-driven: newlines and @#@-comments are--- whitespace, structure comes from keywords (@aggregate@, @regs@, @states@,--- @command@, @event@, @wire@, @projection@) and the transition arrow--- @Src -- Command --> clauses@. Guards and write right-hand sides are parsed as--- a typed 'Expr' (never an opaque string) so the validator can scope-check them.-module Keiro.Dsl.Parser-  ( ParseError,-    ParseFailure (..),-    ParsedSource (..),-    parseSource,-    parseSpec,-    parseSpecText,-    renderParseFailure,-  )-where--import Control.Monad.Combinators.Expr (Operator (..), makeExprParser)-import Data.Bifunctor (first)-import Data.Char (isAlpha, isAlphaNum, isAscii, isDigit, isUpper)-import Data.List.NonEmpty qualified as NE-import Data.Maybe (mapMaybe)-import Data.Set qualified as Set-import Data.Text (Text)-import Data.Text qualified as T-import Data.Text.Read qualified as TR-import Keiro.Dsl.Grammar-import Keiro.Dsl.LanguageVersion-import Numeric.Natural (Natural)-import Text.Megaparsec hiding (ParseError)-import Text.Megaparsec.Char (char, digitChar, letterChar, space1)-import Text.Megaparsec.Char.Lexer qualified as L---- | A rendered, line-numbered parse error, ready to print to the user.-type ParseError = Text--data ContextualParseFailure = ContextualParseFailure-  { contextualFailureCode :: !SourceLanguageErrorCode,-    contextualFailureLine :: !Int-  }-  deriving stock (Eq, Ord, Show)--instance ShowErrorComponent ContextualParseFailure where-  showErrorComponent contextual = T.unpack (sourceLanguageErrorCodeText (contextualFailureCode contextual))--type P = Parsec ContextualParseFailure Text---- | Parse a @.keiro@ source. The 'FilePath' is used only as the source name in--- diagnostics (megaparsec's line/column reporting); it need not exist on disk.--- This is the canonical signature shared across all keiro-dsl plans.-parseSpec :: FilePath -> Text -> Either ParseError Spec-parseSpec src input = parsedSpec <$> first renderParseFailure (parseSource src input)---- | Convenience wrapper for callers without a source name (tests, stdin).-parseSpecText :: Text -> Either ParseError Spec-parseSpecText = parseSpec "<input>"---- | Parse a source without discarding the language contract selected for it.--- Source selection completes before the selected body grammar is run.-parseSource :: FilePath -> Text -> Either ParseFailure ParsedSource-parseSource src input = do-  sourceLanguage <- selectSourceLanguage src input-  definition <- case lookupLanguageDefinition (effectiveLanguageVersion sourceLanguage) of-    Just value -> Right value-    Nothing -> Left (unsupportedDiagnostic src sourceLanguage)-  spec <- parseSelectedBody definition sourceLanguage-  pure ParsedSource {parsedSourceLanguage = sourceLanguage, parsedSpec = spec}-  where-    parseSelectedBody definition sourceLanguage =-      let version = definitionVersion definition-          laterPreambleCode = case sourceLanguage of-            LegacyUnversioned -> MisplacedLanguagePreamble-            DeclaredLanguage {} -> DuplicateLanguagePreamble-          parser = case definitionBodyParser definition of-            LanguageBodyParserV1 ->-              sc-                *> case sourceLanguage of-                  LegacyUnversioned -> pSpec version laterPreambleCode <* eof-                  DeclaredLanguage {} -> pDeclaredPreamble *> pSpec version laterPreambleCode <* eof-            LanguageBodyParserV2 ->-              sc *> pDeclaredPreamble *> pSpec version laterPreambleCode <* eof-       in case runParser parser src input of-            Left bundle -> case firstContextualFailure bundle of-              Just contextual -> Left (SourceLanguageFailure (contextualDiagnostic src sourceLanguage contextual))-              Nothing -> Left (BodyGrammarFailure (T.pack (errorBundlePretty bundle)))-            Right spec -> Right spec--firstContextualFailure :: ParseErrorBundle Text ContextualParseFailure -> Maybe ContextualParseFailure-firstContextualFailure bundle =-  case [ contextual-       | FancyError _ fancy <- NE.toList (bundleErrors bundle),-         ErrorCustom contextual <- Set.toList fancy-       ] of-    contextual : _ -> Just contextual-    [] -> Nothing--contextualDiagnostic :: FilePath -> SourceLanguage -> ContextualParseFailure -> SourceLanguageDiagnostic-contextualDiagnostic src sourceLanguage contextual =-  SourceLanguageDiagnostic-    { sourceLanguageErrorCode = code,-      sourceLanguageSource = src,-      sourceLanguageLoc = Loc (contextualFailureLine contextual),-      sourceLanguageToken = case code of-        LanguageFeatureRequiresVersion -> Just (languageVersionText effectiveVersion)-        _ -> Nothing,-      sourceLanguageDeclaredVersion = case code of-        LanguageFeatureRequiresVersion -> Just effectiveVersion-        _ -> Nothing,-      sourceLanguageSupportedVersions = supportedLanguageVersions-    }-  where-    code = contextualFailureCode contextual-    effectiveVersion = effectiveLanguageVersion sourceLanguage--contextualFailureAt :: Loc -> SourceLanguageErrorCode -> P a-contextualFailureAt (Loc line) code = customFailure ContextualParseFailure {contextualFailureCode = code, contextualFailureLine = line}--requireLanguageFeatureAt :: LanguageVersion -> LanguageFeature -> Loc -> P ()-requireLanguageFeatureAt version feature loc-  | languageSupportsFeature version feature = pure ()-  | otherwise = contextualFailureAt loc LanguageFeatureRequiresVersion---- | Consume a preamble already validated by 'selectSourceLanguage'. This--- parser is also reused at grammar boundaries to recognize only complete--- preamble syntax, never a nested identifier whose spelling is @language@.-pDeclaredPreamble :: P ()-pDeclaredPreamble = do-  keyword "language"-  keyword "keiro-dsl"-  _ <- lexeme (some asciiDigit)-  pure ()---- | The source-selection pass inspects only the first grammar clause after--- leading whitespace and comments. Body lines are left entirely to 'pSpec'.-data InitialLanguageClause = InitialLanguageClause-  { initialLanguageLine :: !Int,-    initialLanguageText :: !Text-  }--selectSourceLanguage :: FilePath -> Text -> Either ParseFailure SourceLanguage-selectSourceLanguage src input = do-  initialClause <- case runParser pInitialLanguageClause src input of-    Left bundle -> Left (BodyGrammarFailure (T.pack (errorBundlePretty bundle)))-    Right value -> Right value-  case initialClause of-    Nothing -> Right LegacyUnversioned-    Just languageClause -> do-      version <- parsePreamble languageClause-      case lookupLanguageDefinition version of-        Nothing -> Left (sourceFailure UnsupportedLanguageVersion languageClause (Just (languageVersionText version)) (Just version))-        Just _ -> Right (DeclaredLanguage version (Loc (initialLanguageLine languageClause)))-  where-    sourceFailure code line tokenText declared =-      SourceLanguageFailure-        SourceLanguageDiagnostic-          { sourceLanguageErrorCode = code,-            sourceLanguageSource = src,-            sourceLanguageLoc = Loc (initialLanguageLine line),-            sourceLanguageToken = tokenText,-            sourceLanguageDeclaredVersion = declared,-            sourceLanguageSupportedVersions = supportedLanguageVersions-          }--    parsePreamble line = case T.words (initialLanguageText line) of-      ["language", "keiro-dsl", tokenText]-        | T.all (\c -> isAscii c && isDigit c) tokenText && not (T.null tokenText) ->-            case TR.decimal tokenText :: Either String (Natural, Text) of-              Right (value, "") -> case languageVersion value of-                Just version -> Right version-                Nothing -> invalid line tokenText-              _ -> invalid line tokenText-      wordsFound -> invalid line (T.unwords wordsFound)--    invalid line tokenText =-      Left (sourceFailure InvalidLanguageVersion line (Just tokenText) Nothing)--pInitialLanguageClause :: P (Maybe InitialLanguageClause)-pInitialLanguageClause = sc *> optional pLanguageClause-  where-    pLanguageClause = do-      position <- getSourcePos-      _ <- lookAhead (chunk "language" *> notFollowedBy (identChar <|> (char '-' *> identChar)))-      rawLine <- takeWhileP (Just "language preamble") (\c -> c /= '\n' && c /= '\r')-      let content = T.strip (T.takeWhile (/= '#') rawLine)-      pure InitialLanguageClause {initialLanguageLine = unPos (sourceLine position), initialLanguageText = content}--unsupportedDiagnostic :: FilePath -> SourceLanguage -> ParseFailure-unsupportedDiagnostic src sourceLanguage =-  SourceLanguageFailure-    SourceLanguageDiagnostic-      { sourceLanguageErrorCode = UnsupportedLanguageVersion,-        sourceLanguageSource = src,-        sourceLanguageLoc = case sourceLanguage of-          LegacyUnversioned -> Loc 1-          DeclaredLanguage {languageVersionLoc = loc} -> loc,-        sourceLanguageToken = Just (languageVersionText (effectiveLanguageVersion sourceLanguage)),-        sourceLanguageDeclaredVersion = Just (effectiveLanguageVersion sourceLanguage),-        sourceLanguageSupportedVersions = supportedLanguageVersions-      }------------------------------------------------------------------------------------- Lexer------------------------------------------------------------------------------------- | Space consumer: spaces, newlines, and @#@ line comments are all whitespace.-sc :: P ()-sc = L.space space1 (L.skipLineComment "#") empty--lexeme :: P a -> P a-lexeme = L.lexeme sc--symbol :: Text -> P Text-symbol = L.symbol sc---- | A reserved keyword: the literal word not followed by an identifier--- character (so @goto@ matches @goto@ but not @gotoX@).-keyword :: Text -> P ()-keyword w = (lexeme . try) (string' w *> notFollowedBy (identChar <|> (char '-' *> identChar)))-  where-    string' = chunk--identChar :: P Char-identChar = asciiAlphaNum <|> char '_'--asciiLetter :: P Char-asciiLetter = satisfy (\c -> isAscii c && isAlpha c)--asciiUpper :: P Char-asciiUpper = satisfy (\c -> isAscii c && isUpper c)--asciiDigit :: P Char-asciiDigit = satisfy (\c -> isAscii c && isDigit c)--asciiAlphaNum :: P Char-asciiAlphaNum = satisfy (\c -> isAscii c && isAlphaNum c)---- | Fail with the diagnostic caret placed at a previously captured offset.-failAt :: Int -> String -> P a-failAt offset message = region (setErrorOffset offset) (fail message)---- | Parse a decimal as an unbounded Integer, then reject values that cannot be--- represented as Int. Parsing L.decimal directly at Int silently wraps.-boundedDecimal :: P Int-boundedDecimal = do-  offset <- getOffset-  value <- lexeme (L.decimal :: P Integer)-  checkedDecimal offset value--checkedDecimal :: Int -> Integer -> P Int-checkedDecimal offset value-  | value > fromIntegral (maxBound :: Int) =-      failAt-        offset-        ( "decimal literal "-            <> show value-            <> " is out of range (maximum "-            <> show (maxBound :: Int)-            <> ")"-        )-  | otherwise = pure (fromIntegral value)---- | Words that may not be used as bare identifiers, because they introduce a--- different construct and would otherwise be swallowed (e.g. @aggregate@ ending--- one node and beginning the next).-reservedWords :: [Text]-reservedWords =-  [ "context",-    "module",-    "layout",-    "prefixed",-    "collocated",-    "id",-    "enum",-    "rule",-    "mapped",-    "ex",-    "aggregate",-    "regs",-    "states",-    "command",-    "event",-    "wire",-    "projection",-    "snapshot",-    "category",-    "guard",-    "write",-    "emit",-    "goto",-    "fields",-    "status-map",-    "true",-    "false",-    "retiring",-    "deprecated",-    "upcast",-    "from",-    "HOLE",-    "process",-    "router",-    "dispatch-each",-    "resolve",-    "read-model",-    "dispatch",-    -- EP-4 integration: structural keywords never used as identifiers, so a-    -- list like @accept A B C@ stops at the next block keyword.-    "intake",-    "contract",-    "topic",-    "accept",-    "bind",-    "dedupe",-    "persist",-    "decode",-    "disposition",-    "publisher",-    "map",-    -- EP-5 pgmq structural keywords.-    "workqueue",-    "queue",-    "payload",-    "retry",-    "fanout",-    "dedup",-    "enqueue",-    "seenIn",-    -- EP-6 workflow/operation: reserved so the multi-word result-type parse and-    -- node boundaries don't swallow the next block keyword.-    "workflow",-    "operation",-    "consistency",-    "body",-    "step",-    "await",-    "sleep",-    "child",-    "patch",-    "continueAsNew",-    -- EP-107 read-model structural words. Clause labels such as table and-    -- schema remain usable identifiers because their block parser consumes-    -- them with symbol-style matching.-    "readmodel",-    "columns",-    "feed",-    "scope",-    "shape"-  ]---- | A CamelCase / snake_case identifier (no dashes): type names, register--- names, command\/event\/state names, enum constructors, projection keys.-ident :: P Name-ident = (lexeme . try) $ do-  c <- asciiLetter <|> char '_'-  cs <- many identChar-  let w = T.pack (c : cs)-  if w `elem` reservedWords-    then fail ("unexpected reserved word " <> T.unpack w)-    else pure w---- | A wire-spelling token, which may contain dashes (@partial-divert@,--- @hospital-capacity@). Used for the context name, id prefixes, enum wire--- spellings, and status-map values.-wireWord :: P Text-wireWord = lexeme $ do-  c <- asciiLetter <|> asciiDigit-  cs <- many (identChar <|> char '-')-  pure (T.pack (c : cs))---- | Patch ids use wire-word spelling, but admit @:@ so the validator can emit--- the domain-specific 'WorkflowPatchIdInvalid' diagnostic at the owning item.-patchIdWord :: P Text-patchIdWord = lexeme $ do-  c <- asciiLetter <|> asciiDigit-  cs <- many (identChar <|> char '-' <|> char ':')-  pure (T.pack (c : cs))--getLoc :: P Loc-getLoc = (Loc . unPos . sourceLine) <$> getSourcePos------------------------------------------------------------------------------------- Top level-----------------------------------------------------------------------------------data TopItem-  = TIId IdDecl-  | TIEnum EnumDecl-  | TIRule RuleDecl-  | TINominalScalar NominalScalarDecl-  | TIMapped MappedDecl-  | TINode Node--pContextualPreamble :: SourceLanguageErrorCode -> P a-pContextualPreamble code = do-  loc <- getLoc-  _ <- try pDeclaredPreamble-  contextualFailureAt loc code--pSpec :: LanguageVersion -> SourceLanguageErrorCode -> P Spec-pSpec version laterPreambleCode = do-  pContextualPreamble laterPreambleCode <|> pure ()-  keyword "context"-  ctx <- wireWord-  mroot <- optional pModuleClause-  mlayout <- optional pLayoutClause-  items <- many (pTopItem version laterPreambleCode)-  pure-    Spec-      { specContext = ctx,-        specModuleRoot = mroot,-        specLayout = mlayout,-        specIds = [d | TIId d <- items],-        specEnums = [d | TIEnum d <- items],-        specRules = [d | TIRule d <- items],-        specNominalScalars = [d | TINominalScalar d <- items],-        specMapped = [d | TIMapped d <- items],-        specNodes = [n | TINode n <- items]-      }---- | @module Acme.Services@ — the optional namespace-prefix clause.-pModuleClause :: P Text-pModuleClause = keyword "module" *> pModulePrefix---- | @layout (prefixed|collocated)@ — the optional placement-style clause.-pLayoutClause :: P Placement-pLayoutClause =-  keyword "layout"-    *> choice-      [ GeneratedPrefix <$ keyword "prefixed",-        CollocatedLeaf <$ keyword "collocated"-      ]---- | A dotted module prefix: one-or-more PascalCase segments joined by dots,--- e.g. @Acme@ or @Acme.Services@.-pModulePrefix :: P Text-pModulePrefix = lexeme $ do-  seg0 <- pSeg-  segs <- many (char '.' *> pSeg)-  pure (T.intercalate "." (seg0 : segs))-  where-    pSeg = do-      c <- asciiUpper-      cs <- many identChar-      pure (T.pack (c : cs))--pTopItem :: LanguageVersion -> SourceLanguageErrorCode -> P TopItem-pTopItem version laterPreambleCode =-  choice-    ( [ pContextualPreamble laterPreambleCode,-        TIId <$> pIdDecl version,-        TIEnum <$> pEnumDecl version,-        TIRule <$> pRuleDecl version,-        pMappedTopItem version-      ]-        ++ [ TINode . NRouter <$> pRouter,-             TINode . NProcess <$> pProcess,-             TINode . NContract <$> pContract,-             TINode . NIntake <$> pIntake,-             TINode . NEmit <$> pEmit,-             TINode . NPublisher <$> pPublisher,-             TINode . NWorkqueue <$> pWorkqueue,-             TINode . NPgmqDispatch <$> pPgmqDispatch,-             TINode . NReadModel <$> pReadModel,-             TINode . NWorkflow <$> pWorkflow,-             TINode . NOperation <$> pOperation,-             TINode . NAggregate <$> pAggregate version-           ]-    )--pIdDecl :: LanguageVersion -> P IdDecl-pIdDecl version = do-  loc <- getLoc-  keyword "id"-  name <- ident-  _ <- symbol "prefix"-  _ <- symbol "="-  pfx <- wireWord-  binding <- optionalLanguageFeature version NominalBindingSyntax "using" pUsingNominalBinding-  pure IdDecl {idName = name, idPrefix = pfx, idBinding = binding, idLoc = loc}--pEnumDecl :: LanguageVersion -> P EnumDecl-pEnumDecl version = do-  loc <- getLoc-  keyword "enum"-  name <- ident-  ctors <- braces (many pEnumCtor)-  binding <- optionalLanguageFeature version NominalBindingSyntax "using" pUsingNominalBinding-  pure EnumDecl {enumName = name, enumCtors = ctors, enumBinding = binding, enumLoc = loc}-  where-    pEnumCtor = do-      c <- ident-      _ <- symbol "="-      w <- wireWord-      pure (c, w)--pRuleDecl :: LanguageVersion -> P RuleDecl-pRuleDecl version = do-  loc <- getLoc-  keyword "rule"-  name <- ident-  _ <- symbol ":"-  dom <- ident-  _ <- symbol "->"-  cod <- ident-  keyword "ex"-  cases <- sepBy1 pCase (symbol ";")-  pure-    RuleDecl-      { ruleName = name,-        ruleDomain = dom,-        ruleCodomain = cod,-        ruleCases = cases,-        ruleLoc = loc-      }-  where-    pCase = do-      c <- ident-      _ <- symbol "=>"-      e <- pExpr version-      pure (c, e)--optionalLanguageFeature :: LanguageVersion -> LanguageFeature -> Text -> P a -> P (Maybe a)-optionalLanguageFeature version feature marker parser-  | languageSupportsFeature version feature = optional parser-  | otherwise = reject <|> pure Nothing-  where-    reject = do-      loc <- getLoc-      _ <- try (keyword marker)-      contextualFailureAt loc LanguageFeatureRequiresVersion------------------------------------------------------------------------------------- Consumer-owned mapped and nominal types-----------------------------------------------------------------------------------data MappedKind = MappedRecord | MappedEnum | MappedUnion--data MappedClause-  = MCHaskell HaskellSource-  | MCBinding Text-  | MCBindingVersion Text-  | MCCanonical Text-  | MCFixtures Text-  | MCInitial Text-  | MCCodec Text-  | MCCodecVersion Text-  | MCShape MappedShape--pMappedTopItem :: LanguageVersion -> P TopItem-pMappedTopItem version = do-  loc <- getLoc-  keyword "mapped"-  choice-    [ if languageSupportsFeature version NominalBindingSyntax-        then TINominalScalar <$> pNominalScalarAfterMapped loc-        else do-          _ <- try (keyword "nominal")-          contextualFailureAt loc LanguageFeatureRequiresVersion,-      TIMapped <$> pMappedStructural version loc,-      TIMapped <$> pMappedOpaque loc-    ]--pMappedStructural :: LanguageVersion -> Loc -> P MappedDecl-pMappedStructural version loc = do-  keyword "structural"-  kind <--    choice-      [ MappedRecord <$ keyword "record",-        MappedEnum <$ keyword "enum",-        MappedUnion <$ keyword "union"-      ]-  name <- ident-  clauses <- braces (many (pStructuralClause version kind))-  hs <- oneClause "haskell" (\case MCHaskell value -> Just value; _ -> Nothing) clauses-  binding <- oneClause "binding" (\case MCBinding value -> Just value; _ -> Nothing) clauses-  bindingVersion <- oneClause "binding-version" (\case MCBindingVersion value -> Just value; _ -> Nothing) clauses-  canonical <- oneClause "canonical-type" (\case MCCanonical value -> Just value; _ -> Nothing) clauses-  fixtures <- oneClause "fixtures" (\case MCFixtures value -> Just value; _ -> Nothing) clauses-  initial <- oneClause "initial" (\case MCInitial value -> Just value; _ -> Nothing) clauses-  shape <- requiredClause "wire" (\case MCShape value -> Just value; _ -> Nothing) clauses-  pure-    MappedStructural-      { msName = name,-        msHaskell = hs,-        msBinding = binding,-        msBindingVersion = bindingVersion,-        msCanonical = canonical,-        msFixtures = fixtures,-        msInitial = initial,-        msShape = shape,-        msLoc = loc-      }--pMappedOpaque :: Loc -> P MappedDecl-pMappedOpaque loc = do-  keyword "opaque"-  name <- ident-  clauses <- braces (many pOpaqueClause)-  hs <- oneClause "haskell" (\case MCHaskell value -> Just value; _ -> Nothing) clauses-  codec <- oneClause "codec" (\case MCCodec value -> Just value; _ -> Nothing) clauses-  version <- oneClause "version" (\case MCCodecVersion value -> Just value; _ -> Nothing) clauses-  fixtures <- oneClause "fixtures" (\case MCFixtures value -> Just value; _ -> Nothing) clauses-  initial <- oneClause "initial" (\case MCInitial value -> Just value; _ -> Nothing) clauses-  pure-    MappedOpaque-      { moName = name,-        moHaskell = hs,-        moCodecId = codec,-        moCodecVersion = version,-        moFixtures = fixtures,-        moInitial = initial,-        moLoc = loc-      }--pNominalScalarAfterMapped :: Loc -> P NominalScalarDecl-pNominalScalarAfterMapped loc = do-  keyword "nominal"-  name <- ident-  _ <- symbol ":"-  representation <- ident-  binding <- pNominalBindingBlock loc-  pure-    NominalScalarDecl-      { nominalScalarName = name,-        nominalScalarRepresentation = representation,-        nominalScalarBinding = binding,-        nominalScalarLoc = loc-      }--pUsingNominalBinding :: P NominalBindingDecl-pUsingNominalBinding = do-  keyword "using"-  loc <- getLoc-  pNominalBindingBlock loc--pNominalBindingBlock :: Loc -> P NominalBindingDecl-pNominalBindingBlock loc = do-  clauses <- braces (many pNominalClause)-  hs <- oneClause "haskell" (\case MCHaskell value -> Just value; _ -> Nothing) clauses-  binding <- oneClause "binding" (\case MCBinding value -> Just value; _ -> Nothing) clauses-  bindingVersion <- oneClause "binding-version" (\case MCBindingVersion value -> Just value; _ -> Nothing) clauses-  canonical <- oneClause "canonical-type" (\case MCCanonical value -> Just value; _ -> Nothing) clauses-  fixtures <- oneClause "fixtures" (\case MCFixtures value -> Just value; _ -> Nothing) clauses-  initial <- oneClause "initial" (\case MCInitial value -> Just value; _ -> Nothing) clauses-  pure-    NominalBindingDecl-      { nominalHaskell = hs,-        nominalBinding = binding,-        nominalBindingVersion = bindingVersion,-        nominalCanonicalType = canonical,-        nominalFixtures = fixtures,-        nominalInitial = initial,-        nominalLoc = loc-      }--pNominalClause :: P MappedClause-pNominalClause =-  choice-    [ MCHaskell <$> pHaskellSource,-      MCBindingVersion <$> pQuotedFact "binding-version",-      MCBinding <$> pQuotedFact "binding",-      MCCanonical <$> pQuotedFact "canonical-type",-      MCFixtures <$> pQuotedFact "fixtures",-      MCInitial <$> pQuotedFact "initial"-    ]--pStructuralClause :: LanguageVersion -> MappedKind -> P MappedClause-pStructuralClause version kind =-  choice-    [ MCHaskell <$> pHaskellSource,-      MCBindingVersion <$> pQuotedFact "binding-version",-      MCBinding <$> pQuotedFact "binding",-      MCCanonical <$> pQuotedFact "canonical-type",-      MCFixtures <$> pQuotedFact "fixtures",-      MCInitial <$> pQuotedFact "initial",-      MCShape <$> pMappedShape version kind-    ]--pOpaqueClause :: P MappedClause-pOpaqueClause =-  choice-    [ MCHaskell <$> pHaskellSource,-      MCCodec <$> pQuotedFact "codec",-      MCCodecVersion <$> pQuotedFact "version",-      MCFixtures <$> pQuotedFact "fixtures",-      MCInitial <$> pQuotedFact "initial"-    ]--pHaskellSource :: P HaskellSource-pHaskellSource = do-  keyword "haskell"-  keyword "package"-  _ <- symbol "="-  packageName <- wireWord-  keyword "module"-  _ <- symbol "="-  moduleName <- pModulePrefix-  keyword "type"-  _ <- symbol "="-  typeName <- ident-  pure HaskellSource {hsPackage = packageName, hsModule = moduleName, hsType = typeName}--pQuotedFact :: Text -> P Text-pQuotedFact factName = keyword factName *> symbol "=" *> stringLit--pMappedShape :: LanguageVersion -> MappedKind -> P MappedShape-pMappedShape version kind = do-  keyword "wire"-  case kind of-    MappedRecord -> do-      keyword "object"-      keyword "constructor"-      _ <- symbol "="-      constructor <- ident-      unknownFields <- pUnknownFieldsFact-      fields <- braces (many (pWireField version))-      pure (ShapeRecord constructor unknownFields fields)-    MappedEnum -> do-      keyword "string"-      ShapeEnum <$> braces (many pWireEnum)-    MappedUnion -> do-      keyword "tagged-object"-      keyword "tag"-      _ <- symbol "="-      tagField <- stringLit-      keyword "contents"-      _ <- symbol "="-      contentsField <- stringLit-      unknownFields <- pUnknownFieldsFact-      arms <- braces (many (pWireArm version))-      pure (ShapeUnion (TaggedObject tagField contentsField unknownFields) arms)--pUnknownFieldsFact :: P UnknownFields-pUnknownFieldsFact = do-  keyword "unknown-fields"-  _ <- symbol "="-  choice [RejectUnknown <$ keyword "reject", IgnoreUnknown <$ keyword "ignore"]--pWireField :: LanguageVersion -> P WireField-pWireField version = do-  loc <- getLoc-  haskellName <- ident-  keyword "as"-  wireKey <- stringLit-  _ <- symbol ":"-  fieldType <- pMappedTypeExpr version-  presence <- choice [PRequired <$ keyword "required", POptional <$ keyword "optional"]-  onMissing <- optional (keyword "on-missing" *> symbol "=" *> pOnMissing)-  pure-    WireField-      { wfHaskell = haskellName,-        wfKey = wireKey,-        wfType = fieldType,-        wfPresence = presence,-        wfOnMissing = onMissing,-        wfLoc = loc-      }--pWireEnum :: P WireEnum-pWireEnum = do-  loc <- getLoc-  constructor <- ident-  keyword "as"-  wireTag <- stringLit-  pure WireEnum {weCtor = constructor, weTag = wireTag, weLoc = loc}--pWireArm :: LanguageVersion -> P WireArm-pWireArm version = do-  loc <- getLoc-  constructor <- ident-  keyword "as"-  wireTag <- stringLit-  payload <- optional (symbol ":" *> pMappedTypeExpr version)-  pure WireArm {waCtor = constructor, waTag = wireTag, waPayload = payload, waLoc = loc}--pMappedTypeExpr :: LanguageVersion -> P TypeExpr-pMappedTypeExpr version =-  choice-    [ TOptional <$> (keyword "Optional" *> pTypeArgument),-      TList <$> (keyword "List" *> pTypeArgument),-      TMap <$> (keyword "Map" *> pTypeArgument),-      TText <$ keyword "Text",-      TInt <$ keyword "Int",-      TInteger <$ languageFeatureKeyword version IntegerScalarSyntax "Integer",-      TBool <$ keyword "Bool",-      TNatural <$ keyword "Natural",-      TTime <$ (keyword "Time" <|> keyword "UTCTime"),-      TJson <$ keyword "Json",-      TRef <$> ident-    ]-  where-    pTypeArgument = parens (pMappedTypeExpr version) <|> pTypeAtom-    pTypeAtom =-      choice-        [ TText <$ keyword "Text",-          TInt <$ keyword "Int",-          TInteger <$ languageFeatureKeyword version IntegerScalarSyntax "Integer",-          TBool <$ keyword "Bool",-          TNatural <$ keyword "Natural",-          TTime <$ (keyword "Time" <|> keyword "UTCTime"),-          TJson <$ keyword "Json",-          TRef <$> ident-        ]--languageFeatureKeyword :: LanguageVersion -> LanguageFeature -> Text -> P ()-languageFeatureKeyword version feature spelling = do-  loc <- getLoc-  keyword spelling-  requireLanguageFeatureAt version feature loc--pOnMissing :: P OnMissing-pOnMissing =-  choice-    [ OmNull <$ keyword "null",-      OmEmptyList <$ (symbol "[" *> symbol "]"),-      OmEmptyMap <$ (symbol "{" *> symbol "}"),-      OmBool True <$ keyword "true",-      OmBool False <$ keyword "false",-      OmText <$> stringLit,-      OmInt <$> integerLiteral,-      OmCtor <$> ident-    ]--integerLiteral :: P Integer-integerLiteral = lexeme (L.signed (pure ()) L.decimal)--oneClause :: String -> (MappedClause -> Maybe a) -> [MappedClause] -> P (Maybe a)-oneClause clauseName select clauses =-  case mapMaybe select clauses of-    [] -> pure Nothing-    [value] -> pure (Just value)-    _ -> fail ("duplicate " <> clauseName <> " clause in mapped declaration")--requiredClause :: String -> (MappedClause -> Maybe a) -> [MappedClause] -> P a-requiredClause clauseName select clauses = do-  found <- oneClause clauseName select clauses-  maybe (fail ("missing " <> clauseName <> " clause in mapped structural declaration")) pure found------------------------------------------------------------------------------------- Aggregate node-----------------------------------------------------------------------------------data BodyItem-  = BICommand Command-  | BIEvent Event-  | BIWire WireSpec-  | BIProjection ProjectionSpec-  | BISnapshot SnapshotSpec-  | BITransition Transition--pAggregate :: LanguageVersion -> P Aggregate-pAggregate version = do-  loc <- getLoc-  keyword "aggregate"-  name <- ident-  regs <- pRegsBlock version-  states <- pStatesLine-  positionedItems <- many ((,) <$> getOffset <*> pBodyItem version)-  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-      }-  where-    listToMaybe xs = case xs of (x : _) -> Just x; [] -> Nothing--pRegsBlock :: LanguageVersion -> P [RegDecl]-pRegsBlock version = do-  keyword "regs"-  many (pRegDecl version)--pRegDecl :: LanguageVersion -> P RegDecl-pRegDecl version = do-  loc <- getLoc-  name <- ident-  ty <- pMappedTypeExpr version-  _ <- 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 :: LanguageVersion -> P BodyItem-pBodyItem version =-  choice-    [ BICommand <$> pCommand version,-      BIEvent <$> pEvent version,-      BIWire <$> pWire,-      BIProjection <$> pProjection,-      BISnapshot <$> pSnapshot,-      BITransition <$> pTransition version-    ]--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 :: LanguageVersion -> P Command-pCommand version = do-  loc <- getLoc-  keyword "command"-  name <- ident-  fs <- braces (many (pAggregateField version))-  pure Command {cmdName = name, cmdFields = fs, cmdLoc = loc}--pAggregateField :: LanguageVersion -> P AggregateField-pAggregateField version = do-  loc <- getLoc-  n <- ident-  mty <- optional (symbol ":" *> pMappedTypeExpr version)-  pure AggregateField {aggregateFieldName = n, aggregateFieldType = mty, aggregateFieldLoc = loc}--pField :: P Field-pField = do-  n <- ident-  mty <- optional (symbol ":" *> ident)-  pure Field {fieldName = n, fieldType = mty}--pEvent :: LanguageVersion -> P Event-pEvent version = 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 <--    choice-      [ EventFromCommand <$> (symbol "=" *> keyword "fields" *> parens ident),-        EventFields <$> braces (many (pAggregateField version))-      ]-  up <- optional pUpcast-  pure-    Event-      { evName = name,-        evBody = body,-        evVersion = ver,-        evUpcastFrom = up,-        evRetiring = retiring,-        evDeprecated = deprecated,-        evLoc = loc-      }-  where-    pUpcast = do-      keyword "upcast"-      keyword "from"-      m <- pVersion-      _ <- symbol "="-      keyword "HOLE"-      pure (m, Hole)---- | A @vN@ schema-version token (e.g. @v2@). Fails (backtracking) on anything--- that is not @v@ immediately followed by digits.-pVersion :: P Int-pVersion = do-  offset <- getOffset-  value <- lexeme (try (char 'v' *> (L.decimal :: P Integer) <* notFollowedBy identChar))-  checkedDecimal offset value--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)------------------------------------------------------------------------------------- Integration contract (EP-4)-----------------------------------------------------------------------------------pContract :: P ContractNode-pContract = do-  loc <- getLoc-  keyword "contract"-  nm <- ident-  _ <- symbol "{"-  keyword "schemaVersion"-  sv <- boundedDecimal-  keyword "discriminator"-  disc <- ident-  topics <- many pTopic-  events <- many pContractEvent-  _ <- symbol "}"-  pure-    ContractNode-      { ctrName = nm,-        ctrSchemaVersion = sv,-        ctrDiscriminator = disc,-        ctrTopics = topics,-        ctrEvents = events,-        ctrLoc = loc-      }-  where-    pTopic = do-      keyword "topic"-      alias <- ident-      t <- stringLit-      pure (alias, t)-    pContractEvent = do-      keyword "event"-      nm <- ident-      keyword "on"-      topicAlias <- ident-      fs <- braces (many pContractField)-      pure ContractEvent {ceName = nm, ceTopic = topicAlias, ceFields = fs}-    pContractField = do-      n <- ident-      _ <- symbol ":"-      ty <- pContractType-      _ <- optional (symbol ";")-      pure ContractField {cfName = n, cfType = ty}-    pContractType =-      choice-        [ CTypeId <$> (keyword "typeid" *> stringLit),-          CText <$ keyword "text",-          CInt <$ keyword "int"-        ]--pIntake :: P IntakeNode-pIntake = do-  loc <- getLoc-  keyword "intake"-  nm <- ident-  _ <- symbol "{"-  keyword "contract"-  ctr <- ident-  keyword "topic"-  tp <- ident-  keyword "accept"-  acc <- some ident-  binds <- many pBindRow-  keyword "dedupe"-  keyword "key"-  dk <- ident-  keyword "policy"-  dp <- ident-  persistence <--    option InkPersistFull $-      keyword "persist"-        *> symbol "="-        *> choice-          [ InkPersistFull <$ keyword "full-envelope",-            InkPersistDedupeOnly <$ keyword "dedupe-only"-          ]-  dec <- pDecode-  disp <- pDisposition-  _ <- symbol "}"-  pure-    IntakeNode-      { inkName = nm,-        inkContract = ctr,-        inkTopic = tp,-        inkAccept = acc,-        inkBinds = binds,-        inkDedupeKey = dk,-        inkDedupePolicy = dp,-        inkPersist = persistence,-        inkDecode = dec,-        inkDisposition = disp,-        inkLoc = loc-      }-  where-    pBindRow = do-      keyword "bind"-      f <- ident-      keyword "from"-      src <- pWireSource-      req <- option False (True <$ keyword "required")-      xc <- option False (True <$ (keyword "cross-check" *> keyword "body"))-      pure BindRow {brField = f, brSource = src, brRequired = req, brCrossCheck = xc}-    pWireSource =-      choice-        [ SrcHeader <$> (keyword "header" *> stringLit),-          SrcKafkaKey <$ keyword "kafka-key",-          SrcKafkaCursor <$ keyword "kafka-cursor",-          SrcBody <$ keyword "body"-        ]-    pDecode = do-      keyword "decode"-      _ <- symbol "{"-      keyword "envelope"-      env <- pEnvelopePolicy-      keyword "body"-      strict <- (True <$ keyword "strict") <|> (False <$ keyword "lenient")-      keyword "schemaVersion"-      _ <- symbol "=="-      v <- boundedDecimal-      _ <- symbol "}"-      pure DecodeSpec {decEnvelope = env, decBodyStrict = strict, decBodySchemaVersion = v}-    pEnvelopePolicy = do-      a <- wireWord-      b <- wireWord-      pure (a <> " " <> b)-    pDisposition = do-      keyword "disposition"-      rows <- braces (many pDispositionRow)-      pure rows-    pDispositionRow = do-      loc <- getLoc-      o <- ident-      _ <- symbol "=>"-      act <- pInboxAction-      pure DispositionRow {drOutcome = o, drAction = act, drLoc = loc}-    pInboxAction =-      choice-        [ IAckOk <$ keyword "ackOk",-          IRetry <$> (keyword "retry" *> pWindow),-          IDeadLetter <$> (keyword "deadLetter" *> optional stringLit)-        ]--pEmit :: P EmitNode-pEmit = do-  loc <- getLoc-  keyword "emit"-  nm <- ident-  _ <- symbol "{"-  keyword "contract"-  ctr <- ident-  keyword "topic"-  tp <- ident-  keyword "source"-  src <- stringLit-  keyword "key"-  k <- ident-  keyword "map"-  disc <- ident-  (rows, skip) <- braces pMapRows-  keyword "messageId"-  mid <- pDerive-  keyword "idempotencyKey"-  idk <- pDerive-  _ <- symbol "}"-  pure-    EmitNode-      { emName = nm,-        emContract = ctr,-        emTopic = tp,-        emSource = src,-        emKey = k,-        emDiscriminant = disc,-        emMap = rows,-        emSkip = skip,-        emMessageId = mid,-        emIdempotencyKey = idk,-        emLoc = loc-      }-  where-    pMapRows = do-      rows <- many pMapRow-      skip <- option False (True <$ try (symbol "_" *> symbol "=>" *> keyword "skip"))-      pure (rows, skip)-    pMapRow = try $ do-      loc <- getLoc-      v <- stringLit-      _ <- symbol "=>"-      ev <- ident-      pure EmitMapRow {emrValue = v, emrEvent = ev, emrLoc = loc}-    pDerive = do-      keyword "derive"-      pfx <- optional stringLit-      keyword "hole"-      pure DeriveSpec {dsPrefix = pfx}--pPublisher :: P PublisherNode-pPublisher = do-  loc <- getLoc-  keyword "publisher"-  nm <- ident-  _ <- symbol "{"-  keyword "emit"-  em <- ident-  keyword "ordering"-  ord <- ident-  keyword "maxAttempts"-  ma <- boundedDecimal-  keyword "backoff"-  bk <- ident-  bw <- pWindow-  bm <- optional (keyword "max" *> symbol "=" *> pWindow)-  multiplier <- optional (keyword "multiplier" *> symbol "=" *> decimalText)-  keyword "outboxId"-  keyword "stable"-  keyword "from"-  obf <- ident-  _ <- symbol "}"-  pure-    PublisherNode-      { pubName = nm,-        pubEmit = em,-        pubOrdering = ord,-        pubMaxAttempts = ma,-        pubBackoff = BackoffSpec {boKind = bk, boWindow = bw, boMax = bm, boMultiplier = multiplier},-        pubOutboxField = obf,-        pubLoc = loc-      }--pWorkqueue :: P WorkqueueNode-pWorkqueue = do-  loc <- getLoc-  keyword "workqueue"-  nm <- ident-  _ <- symbol "{"-  keyword "queue"-  _ <- symbol "logical" *> symbol "="-  logical <- stringLit-  keyword "derive"-  _ <- symbol "physical" *> symbol "="-  phys <- stringLit-  _ <- symbol "dlq" *> symbol "="-  dlqName <- stringLit-  _ <- symbol "table" *> symbol "="-  tbl <- stringLit-  ordering <- option WqUnordered pOrdering-  groupKey <- optional pGroupKey-  provision <- option WqStandard pProvision-  keyword "payload"-  pn <- ident-  fields <- braces (many pWqField)-  keyword "retry"-  _ <- symbol "maxRetries" *> symbol "="-  mr <- boundedDecimal-  _ <- symbol "delay" *> symbol "="-  dl <- pWindow-  _ <- symbol "dlq" *> symbol "="-  dlqOn <- (True <$ keyword "on") <|> (False <$ keyword "off")-  keyword "disposition"-  disp <- braces (many pWqDispRow)-  _ <- symbol "}"-  pure-    WorkqueueNode-      { wqName = nm,-        wqLogical = logical,-        wqPhysical = phys,-        wqDlq = dlqName,-        wqTable = tbl,-        wqOrdering = ordering,-        wqGroupKey = groupKey,-        wqProvision = provision,-        wqPayloadName = pn,-        wqPayload = fields,-        wqMaxRetries = mr,-        wqDelay = dl,-        wqDlqOn = dlqOn,-        wqDisposition = disp,-        wqLoc = loc-      }-  where-    pOrdering = do-      _ <- symbol "ordering"-      choice-        [ WqUnordered <$ symbol "unordered",-          WqFifoThroughput <$ symbol "fifo-throughput",-          WqFifoRoundRobin <$ symbol "fifo-roundrobin"-        ]-    pGroupKey = do-      _ <- symbol "group" *> symbol "key" *> symbol "from"-      field <- ident-      _ <- symbol "via"-      via <- ident-      fixture <- optional (symbol "fixture" *> stringLit)-      pure WqGroupKey {gkField = field, gkVia = via, gkFixture = fixture}-    pProvision = do-      _ <- symbol "provision"-      choice-        [ WqStandard <$ symbol "standard",-          WqUnlogged <$ symbol "unlogged",-          do-            _ <- symbol "partitioned" *> symbol "("-            _ <- symbol "interval" *> symbol "="-            interval <- stringLit-            _ <- symbol "," *> symbol "retention" *> symbol "="-            retention <- stringLit-            _ <- symbol ")"-            pure (WqPartitioned interval retention)-        ]-    pWqField = do-      n <- ident-      _ <- symbol "->"-      w <- stringLit-      ty <- ident-      req <- option False (True <$ keyword "required")-      pure WqField {wqfName = n, wqfWire = w, wqfType = ty, wqfRequired = req}-    pWqDispRow = do-      loc <- getLoc-      o <- ident-      _ <- symbol "->"-      act <- choice [IAckOk <$ keyword "ackOk", IRetry <$> (keyword "retry" *> pWindow), IDeadLetter <$> (keyword "deadLetter" *> optional stringLit)]-      pure WqDispRow {wqdOutcome = o, wqdAction = act, wqdLoc = loc}--pReadModel :: P ReadModelNode-pReadModel = do-  loc <- getLoc-  keyword "readmodel"-  name <- ident-  _ <- symbol "{"-  _ <- symbol "table" *> symbol "="-  table <- stringLit-  _ <- symbol "schema" *> symbol "="-  schema <- stringLit-  _ <- symbol "columns"-  columns <- braces (many pColumn)-  _ <- symbol "version" *> symbol "="-  version <- boundedDecimal-  _ <- symbol "shape" *> symbol "="-  shape <- stringLit-  _ <- symbol "consistency" *> symbol "="-  consistency <- pConsistency-  scope <- optional (symbol "scope" *> symbol "=" *> pScope)-  _ <- symbol "feed" *> symbol "="-  feed <- pFeed-  subscription <- optional (symbol "subscription" *> symbol "=" *> stringLit)-  _ <- symbol "}"-  pure-    ReadModelNode-      { rmName = name,-        rmTable = table,-        rmSchema = schema,-        rmColumns = columns,-        rmVersion = version,-        rmShape = shape,-        rmConsistency = consistency,-        rmScope = scope,-        rmFeed = feed,-        rmSubscription = subscription,-        rmLoc = loc-      }-  where-    pColumn =-      RmColumn-        <$> wireWord-        <*> ident-        <*> option False (True <$ keyword "required")-    pConsistency = choice [Strong <$ keyword "Strong", Eventual <$ keyword "Eventual"]-    pScope =-      choice-        [ RmEntireLog <$ keyword "entire-log",-          RmCategory <$> (keyword "category" *> stringLit)-        ]-    pFeed = choice [RmInline <$ keyword "inline", RmSubscription <$ keyword "subscription"]--pPgmqDispatch :: P PgmqDispatchNode-pPgmqDispatch = do-  loc <- getLoc-  keyword "dispatch"-  nm <- ident-  _ <- symbol "{"-  keyword "source"-  _ <- symbol "readModel" *> symbol "="-  srm <- ident-  _ <- symbol "key" *> symbol "="-  sk <- ident-  keyword "fanout"-  _ <- symbol "body" *> symbol "="-  fb <- ident-  keyword "dedup"-  _ <- symbol "key" *> symbol "="-  dk <- ident-  _ <- keyword "seenIn" *> symbol "readModel" *> symbol "="-  drm <- ident-  _ <- symbol "field" *> symbol "="-  drmf <- ident-  _ <- keyword "seenIn" *> symbol "queue" *> symbol "="-  dq <- ident-  _ <- symbol "field" *> symbol "="-  dqf <- ident-  keyword "enqueue"-  _ <- symbol "to" *> symbol "="-  enq <- ident-  _ <- symbol "}"-  pure-    PgmqDispatchNode-      { pdName = nm,-        pdSourceReadModel = srm,-        pdSourceKey = sk,-        pdFanoutBody = fb,-        pdDedupKey = dk,-        pdDedupReadModel = drm,-        pdDedupReadModelField = drmf,-        pdDedupQueue = dq,-        pdDedupQueueField = dqf,-        pdEnqueueTo = enq,-        pdLoc = loc-      }--pWorkflow :: P WorkflowNode-pWorkflow = do-  loc <- getLoc-  keyword "workflow"-  wid <- ident-  keyword "name"-  nm <- stringLit-  keyword "in"-  inTy <- ident-  inFields <- option [] (braces (many pField))-  keyword "out"-  outTy <- ident-  keyword "id"-  keyword "from"-  keyword "input"-  idField <- optional (symbol "." *> ident)-  keyword "via"-  idVia <- ident-  keyword "body"-  body <- many pWfBodyItem-  pure-    WorkflowNode-      { wfId = wid,-        wfStable = nm,-        wfInput = inTy,-        wfInputFields = inFields,-        wfOutput = outTy,-        wfIdField = idField,-        wfIdVia = idVia,-        wfBody = body,-        wfLoc = loc-      }-  where-    pWfBodyItem =-      choice-        [ do-            loc <- getLoc-            WfStep <$> (keyword "step" *> wireWord) <*> (symbol "->" *> ident) <*> pure loc,-          do-            loc <- getLoc-            WfAwait <$> (keyword "await" *> wireWord) <*> (symbol "->" *> ident) <*> pure loc,-          do-            loc <- getLoc-            WfSleep <$> (keyword "sleep" *> wireWord) <*> (keyword "after" *> ident) <*> pure loc,-          do-            loc <- getLoc-            WfChild-              <$> (keyword "child" *> wireWord)-              <*> (keyword "id" *> keyword "input" *> keyword "via" *> ident)-              <*> (symbol "->" *> ident)-              <*> pure loc,-          do-            loc <- getLoc-            WfPatch-              <$> (keyword "patch" *> patchIdWord)-              <*> braces (many pWfBodyItem)-              <*> pure loc,-          do-            loc <- getLoc-            WfContinueAsNew <$> (keyword "continueAsNew" *> ident) <*> pure loc-        ]--pOperation :: P OperationNode-pOperation = do-  loc <- getLoc-  keyword "operation"-  nm <- ident-  shape <--    choice-      [ pCommandOp,-        pQueryOp,-        pSignalOp,-        pRunOp-      ]-  pure OperationNode {opName = nm, opShape = shape, opLoc = loc}-  where-    pCommandOp = do-      keyword "command"-      keyword "on"-      agg <- ident-      _ <- keyword "stream" *> keyword "from"-      sf <- ident-      keyword "via"-      sv <- ident-      proj <- option [] (keyword "project" *> brackets (many ident))-      pure (CommandOp agg sf sv proj)-    pQueryOp = do-      keyword "query"-      rm <- ident-      keyword "input"-      inp <- ident-      keyword "result"-      res <- pTypeExpr-      cons <- option "Strong" (keyword "consistency" *> ident)-      pure (QueryOp rm inp res cons)-    pSignalOp = do-      keyword "signal"-      lbl <- wireWord-      keyword "of"-      wf <- ident-      _ <- keyword "key" *> keyword "from"-      kf <- ident-      keyword "via"-      kv <- ident-      keyword "value"-      val <- ident-      pure (SignalOp lbl wf kf kv val)-    pRunOp = do-      keyword "run"-      wf <- ident-      keyword "input"-      inp <- ident-      _ <- keyword "outcome" *> symbol "->"-      oc <- ident-      pure (RunOp wf inp oc)-    -- A result type expression, possibly multi-word like @Maybe TransferDecision@.-    pTypeExpr = do-      ws <- some ident-      pure (T.unwords ws)------------------------------------------------------------------------------------- Process manager + durable timer (EP-3)-----------------------------------------------------------------------------------pProcess :: P ProcessNode-pProcess = do-  loc <- getLoc-  keyword "process"-  pid <- ident-  keyword "name"-  nm <- stringLit-  inp <- pInputDecl-  corr <- pCorrelate-  saga <- pSaga-  keyword "target"-  tgt <- ident-  projs <- keyword "projections" *> brackets (many ident)-  handle <- pHandle-  _ <- optional pDispatchIdLine-  rejected <- pPolicyLine "rejected"-  poison <- pPolicyLine "poison"-  timer <- pTimerNode-  pure-    ProcessNode-      { procId = pid,-        procName = nm,-        procInput = inp,-        procCorrelate = corr,-        procSaga = saga,-        procTarget = tgt,-        procProjections = projs,-        procHandle = handle,-        procRejected = rejected,-        procPoison = poison,-        procTimer = timer,-        procLoc = loc-      }--pRouter :: P RouterNode-pRouter = do-  loc <- getLoc-  keyword "router"-  rid <- ident-  keyword "name"-  nm <- stringLit-  inp <- pInputDecl-  key <- pRouterKey-  resolved <- pResolveDecl-  keyword "target"-  target <- ident-  projections <- keyword "projections" *> brackets (many ident)-  dispatch <- pRouterDispatch-  pRouterDispatchIdLine-  rejected <- pPolicyLine "rejected"-  poison <- pPolicyLine "poison"-  pure-    RouterNode-      { rtId = rid,-        rtName = nm,-        rtInput = inp,-        rtKey = key,-        rtResolve = resolved,-        rtTarget = target,-        rtProjections = projections,-        rtDispatch = dispatch,-        rtRejected = rejected,-        rtPoison = poison,-        rtLoc = loc-      }--pRouterKey :: P CorrelateDecl-pRouterKey = do-  keyword "key"-  _ <- keyword "input" *> symbol "."-  field <- ident-  keyword "via"-  via <- ident-  pure CorrelateDecl {corrField = field, corrVia = via}--pResolveDecl :: P ResolveDecl-pResolveDecl = do-  loc <- getLoc-  keyword "resolve"-  keyword "stable"-  keyword "via"-  source <- choice [ResolveReadModel <$> (keyword "read-model" *> ident), ResolveHole <$ keyword "hole"]-  keyword "row"-  row <- braces (many ident)-  pure ResolveDecl {rvSource = source, rvRow = row, rvLoc = loc}--pRouterDispatch :: P RouterDispatchNode-pRouterDispatch = do-  loc <- getLoc-  keyword "dispatch-each"-  command <- ident-  fields <- braces (many pFieldBinding)-  disposition <--    DispatchDisposition-      <$> (keyword "on-appended" *> pDisp)-      <*> (symbol ";" *> keyword "on-duplicate" *> pDisp)-      <*> (symbol ";" *> keyword "on-failed" *> pDisp)-  pure RouterDispatchNode {rdCommand = command, rdFields = fields, rdDisposition = disposition, rdLoc = loc}--pRouterDispatchIdLine :: P ()-pRouterDispatchIdLine = do-  keyword "dispatch-id"-  _ <- symbol "strategy" *> symbol "=" *> keyword "uuidv5"-  _ <- symbol "from" *> symbol "=" *> parens fixedInputs-  pure ()-  where-    fixedInputs = do-      keyword "name"-      _ <- symbol ","-      keyword "key"-      _ <- symbol ","-      keyword "sourceEventId"-      _ <- symbol ","-      keyword "targetStreamName"-      _ <- symbol ","-      keyword "occurrence"--pPolicyLine :: Text -> P PolicyChoice-pPolicyLine clause = keyword clause *> symbol "=>" *> pPolicyChoice--pPolicyChoice :: P PolicyChoice-pPolicyChoice =-  choice-    [ PolHalt <$ keyword "halt",-      PolDeadLetter <$ keyword "deadLetter",-      PolSkip <$ keyword "skip"-    ]--pInputDecl :: P InputDecl-pInputDecl = do-  keyword "input"-  nm <- ident-  fs <- braces (many pField)-  pure InputDecl {inName = nm, inFields = fs}--pCorrelate :: P CorrelateDecl-pCorrelate = do-  keyword "correlate"-  _ <- keyword "input" *> symbol "."-  f <- ident-  keyword "via"-  v <- ident-  pure CorrelateDecl {corrField = f, corrVia = v}--pSaga :: P SagaRef-pSaga = do-  keyword "saga"-  agg <- ident-  keyword "category"-  categoryName <- stringLit-  pure SagaRef {sagaAgg = agg, sagaCategory = categoryName}--pHandle :: P HandleNode-pHandle = do-  keyword "on"-  onName <- ident-  adv <- pAdvance-  disps <- many pDispatch-  keyword "schedule"-  sched <- ident-  pure HandleNode {hOn = onName, hAdvance = adv, hDispatch = disps, hSchedule = sched}--pAdvance :: P AdvanceNode-pAdvance = do-  keyword "advance"-  cmd <- ident-  fs <- braces (many pFieldBinding)-  pure AdvanceNode {advCommand = cmd, advFields = fs}--pDispatch :: P DispatchNode-pDispatch = do-  loc <- getLoc-  keyword "dispatch"-  tgt <- ident-  _ <- symbol "@"-  key <- dottedRef-  cmd <- ident-  fs <- braces (many pFieldBinding)-  disp <--    DispatchDisposition-      <$> (keyword "on-appended" *> pDisp)-      <*> (symbol ";" *> keyword "on-duplicate" *> pDisp)-      <*> (symbol ";" *> keyword "on-failed" *> pDisp)-  pure DispatchNode {dispTarget = tgt, dispKey = key, dispCommand = cmd, dispFields = fs, dispDisposition = disp, dispLoc = loc}--pDisp :: P Disp-pDisp =-  choice-    [ DAckOk <$ keyword "AckOk",-      DRetry <$ keyword "Retry",-      DDeadLetter <$> (keyword "DeadLetter" *> stringLit)-    ]---- The dispatch-id line is a fixed, runtime-owned strategy; parse and discard.-pDispatchIdLine :: P ()-pDispatchIdLine = do-  keyword "dispatch-id"-  _ <- symbol "strategy" *> symbol "=" *> ident-  _ <- symbol "from" *> symbol "=" *> parens (sepBy dottedRef (symbol ","))-  pure ()--pTimerNode :: P TimerNode-pTimerNode = do-  loc <- getLoc-  keyword "timer"-  nm <- ident-  tid <- keyword "id" *> pIdExpr-  fat <- keyword "fireAt" *> pFireAt-  pay <- keyword "payload" *> braces (many pFieldBinding)-  fire <- pFire-  _ <- keyword "decode" *> keyword "unknown-status" *> symbol "=>"-  unk <- ident-  keyword "max-attempts"-  ma <- boundedDecimal-  keyword "dead-letter"-  dl <- stringLit-  pure-    TimerNode-      { tmName = nm,-        tmId = tid,-        tmFireAt = fat,-        tmPayload = pay,-        tmFire = fire,-        tmDecodeUnknown = unk,-        tmMaxAttempts = ma,-        tmDeadLetter = dl,-        tmLoc = loc-      }--pIdExpr :: P IdExpr-pIdExpr = do-  keyword "uuidv5"-  pfx <- stringLit-  _ <- symbol "<>"-  _ <- ident -- correlationId (fixed)-  pure IdExpr {ideStrategy = UuidV5Id, idePrefix = pfx}--pFireAt :: P FireAtExpr-pFireAt = do-  _ <- keyword "input" *> symbol "."-  f <- ident-  _ <- symbol "+"-  w <- pWindow-  pure FireAtExpr {faField = f, faWindow = w}--pWindow :: P Text-pWindow = lexeme $ do-  ds <- some digitChar-  u <- choice [char 's', char 'm', char 'h'] <?> "time unit: s, m, or h"-  notFollowedBy letterChar <?> "time unit: s, m, or h"-  pure (T.pack (ds <> [u]))--decimalText :: P Text-decimalText = lexeme $ do-  whole <- some digitChar-  fractional <- optional (char '.' *> some digitChar)-  pure (T.pack (whole <> maybe "" ('.' :) fractional))--signedDecimalText :: P Text-signedDecimalText = lexeme $ do-  sign <- optional (char '-')-  digits <- some digitChar-  fractional <- optional (char '.' *> some digitChar)-  pure (T.pack (maybe "" pure sign <> digits <> maybe "" ('.' :) fractional))--pFire :: P FireNode-pFire = do-  keyword "fire"-  keyword "dispatch"-  tgt <- ident-  _ <- symbol "@"-  key <- dottedRef-  cmd <- ident-  fs <- braces (many pFieldBinding)-  fid <- keyword "fired-event-id" *> pIdExpr-  disp <--    FireDisposition-      <$> (keyword "on-ok" *> pFireOutcome)-      <*> (symbol ";" *> keyword "on-reject" *> pFireOutcome)-      <*> (symbol ";" *> keyword "on-ambiguous" *> pFireOutcome)-      <*> (symbol ";" *> keyword "on-error" *> pFireOutcome)-      <*> (symbol ";" *> keyword "not-mine" *> pFireOutcome)-  pure FireNode {fireTarget = tgt, fireKey = key, fireCommand = cmd, fireFields = fs, fireFiredEventId = fid, fireDisposition = disp}--pFireOutcome :: P FireOutcome-pFireOutcome = choice [OFired <$ keyword "Fired", ORetry <$ keyword "Retry"]--pFieldBinding :: P FieldBinding-pFieldBinding = do-  n <- ident-  v <- optional (symbol "=" *> pBindingValue)-  pure FieldBinding {fbName = n, fbValue = v}---- | A binding value: a quoted string (kept quoted) or a dotted reference.-pBindingValue :: P Text-pBindingValue = choice [quoted, dottedRef]-  where-    quoted = do-      s <- stringLit-      pure ("\"" <> s <> "\"")---- | A dotted/plain reference token like @input.hospitalId@, @timer.id@,--- @correlationId@.-dottedRef :: P Text-dottedRef = lexeme $ do-  c <- asciiLetter-  cs <- many (asciiAlphaNum <|> char '_' <|> char '.')-  pure (T.pack (c : cs))---- | A double-quoted string literal, returning raw (unescaped) inner text.--- The surface syntax supports a closed escape set so unknown escapes remain--- available for backward-compatible extensions.-stringLit :: P Text-stringLit = lexeme $ do-  _ <- char '"'-  s <- many strChar-  _ <- char '"'-  pure (T.pack s)-  where-    strChar =-      choice-        [ char '\\' *> escapeCode,-          char '\n' *> fail "unescaped newline in string literal (write \\n)",-          anySingleBut '"'-        ]-    escapeCode =-      choice-        [ '"' <$ char '"',-          '\\' <$ char '\\',-          '\n' <$ char 'n',-          '\t' <$ char 't',-          '\r' <$ char 'r',-          anySingle >>= \c -> fail ("unknown escape sequence \\" <> [c] <> " in string literal")-        ]--brackets :: P a -> P a-brackets = between (symbol "[") (symbol "]")------------------------------------------------------------------------------------- Transitions-----------------------------------------------------------------------------------data Clause-  = CGuard Expr-  | CWrite Name Expr-  | CEmit Name-  | CGoto Name-  | CImplementationHole--pTransition :: LanguageVersion -> P Transition-pTransition version = 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 version <* optional (symbol ";")))-  let clauses = map 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-          [] | languageSupportsFeature version 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-      }--pClause :: LanguageVersion -> P Clause-pClause version =-  choice-    [ do-        loc <- getLoc-        keyword "implementation"-        keyword "hole"-        requireLanguageFeatureAt version ExplicitTransitionImplementationSyntax loc-        pure CImplementationHole,-      CGuard <$> (keyword "guard" *> pExpr version),-      (\r e -> CWrite r e) <$> (keyword "write" *> ident) <*> (symbol ":=" *> pExpr version),-      try $ do-        keyword "emit"-        eventName <- ident-        notFollowedBy (symbol "{")-        pure (CEmit eventName),-      CGoto <$> (keyword "goto" *> ident)-    ]------------------------------------------------------------------------------------- Expr sublanguage-----------------------------------------------------------------------------------pExpr :: LanguageVersion -> P Expr-pExpr version-  | languageSupportsFeature version TypedAggregateExpressionSyntax = makeExprParser (pScalarTerm version) scalarOperatorTable-  | otherwise = makeExprParser (pLegacyTerm version) legacyOperatorTable--pLegacyTerm :: LanguageVersion -> P Expr-pLegacyTerm version =-  choice-    [ parens (pExpr version),-      pUnsupportedScalarTerm version,-      EAtom . ABool <$> (True <$ keyword "true" <|> False <$ keyword "false"),-      EAtom . AName <$> ident-    ]--pUnsupportedScalarTerm :: LanguageVersion -> P Expr-pUnsupportedScalarTerm version = do-  loc <- getLoc-  _ <--    try ((keyword "reg" <|> keyword "cmd") *> symbol ".")-  requireLanguageFeatureAt version TypedAggregateExpressionSyntax loc-  fail "unreachable supported scalar term in predecessor grammar"---- | Highest precedence first: relational comparisons bind tighter than @&&@,--- which binds tighter than @||@.-legacyOperatorTable :: [[Operator P Expr]]-legacyOperatorTable =-  [ [InfixL arithmeticUnsupported],-    [ InfixN (ECmp OpLe <$ op "<="),-      InfixN (ECmp OpGe <$ op ">="),-      InfixN (ECmp OpEq <$ op "=="),-      InfixN (ECmp OpNeq <$ op "!="),-      InfixN (ECmp OpLt <$ op "<"),-      InfixN (ECmp OpGt <$ op ">")-    ],-    [InfixL (EAnd <$ op "&&")],-    [InfixL (EOr <$ op "||")]-  ]-  where-    op s = symbol s-    arithmeticUnsupported = do-      offset <- getOffset-      operator <- lexeme (oneOf ['+', '-', '*', '/'])-      failAt offset ("aggregate arithmetic operator '" <> [operator] <> "' is unsupported; compare or copy whole values instead")--pScalarTerm :: LanguageVersion -> P Expr-pScalarTerm version =-  choice-    [ parens (pExpr version),-      collectionTermUnsupported,-      try pIdLiteral,-      do-        loc <- getLoc-        ELiteral loc . LiteralBool <$> (True <$ keyword "true" <|> False <$ keyword "false"),-      do-        loc <- getLoc-        ELiteral loc . LiteralText <$> stringLit,-      try $ do-        loc <- getLoc-        ELiteral loc . LiteralIntegral <$> integerLiteral,-      pScalarPath-    ]--pIdLiteral :: P Expr-pIdLiteral = do-  loc <- getLoc-  constructor <- ident-  value <- parens stringLit-  pure (ELiteral loc (LiteralId constructor value))--pScalarPath :: P Expr-pScalarPath = do-  loc <- getLoc-  firstName <- ident-  rest <- many (symbol "." *> ident)-  pure $ case (firstName, rest) of-    ("reg", name : path) -> EPath loc RegisterRoot (name : path)-    ("cmd", name : path) -> EPath loc CommandRoot (name : path)-    (_, [constructor]) | startsUpper firstName -> ELiteral loc (LiteralQualified firstName constructor)-    _ -> EPath loc UnqualifiedRoot (firstName : rest)-  where-    startsUpper value = maybe False (isUpper . fst) (T.uncons value)--collectionTermUnsupported :: P Expr-collectionTermUnsupported = do-  offset <- getOffset-  choice-    [ () <$ symbol "[",-      () <$ symbol "{",-      () <$ keyword "keys",-      () <$ keyword "values",-      () <$ keyword "any",-      () <$ keyword "all"-    ]-  failAt offset collectionExpressionMessage--collectionExpressionMessage :: String-collectionExpressionMessage = "CollectionExpressionUnsupported: collection expressions are reserved for plan 166"--scalarOperatorTable :: [[Operator P Expr]]-scalarOperatorTable =-  [ [InfixL (op "*" *> located EMultiply)],-    [ InfixL (op "+" *> located EAdd),-      InfixL (op "-" *> located ESubtract),-      InfixL scalarArithmeticUnsupported-    ],-    [ InfixN (ECmp OpLe <$ op "<="),-      InfixN (ECmp OpGe <$ op ">="),-      InfixN (ECmp OpEq <$ op "=="),-      InfixN (ECmp OpNeq <$ op "!="),-      InfixN (ECmp OpLt <$ op "<"),-      InfixN (ECmp OpGt <$ op ">"),-      InfixN collectionOperatorUnsupported-    ],-    [InfixL (EAnd <$ op "&&")],-    [InfixL (EOr <$ op "||")]-  ]-  where-    op value = symbol value-    located constructor = do-      loc <- getLoc-      pure (constructor loc)-    scalarArithmeticUnsupported = do-      offset <- getOffset-      operator <- lexeme (oneOf ['/', '%'])-      failAt offset ("aggregate arithmetic operator '" <> [operator] <> "' is unsupported")-    collectionOperatorUnsupported = do-      offset <- getOffset-      _ <- try (keyword "not" *> keyword "in") <|> keyword "in"-      failAt offset collectionExpressionMessage------------------------------------------------------------------------------------- Helpers-----------------------------------------------------------------------------------braces :: P a -> P a-braces = between (symbol "{") (symbol "}")--parens :: P a -> P a-parens = between (symbol "(") (symbol ")")+-- | Stable compatibility facade for the Keiro DSL language frontend.+module Keiro.Dsl.Parser+  ( ParseError,+    ParseFailure (..),+    ParsedSource (..),+    parseSource,+    parseSpec,+    parseSpecText,+    renderParseFailure,+  )+where++import Data.Bifunctor (first)+import Data.Text (Text)+import Keiro.Dsl.Frontend (lowerSurfaceSource, parseSurfaceSource)+import Keiro.Dsl.Frontend.Internal+  ( frontendCompatibilityFailure,+    frontendFailureFromLowering,+  )+import Keiro.Dsl.Grammar (Spec)+import Keiro.Dsl.LanguageVersion++-- | A rendered, line-numbered parse error, ready to print to the user.+type ParseError = Text++-- | Parse a @.keiro@ source. The 'FilePath' is used only as the source name in+-- diagnostics; it need not exist on disk.+parseSpec :: FilePath -> Text -> Either ParseError Spec+parseSpec sourceName input = parsedSpec <$> first renderParseFailure (parseSource sourceName input)++-- | Convenience wrapper for callers without a source name (tests, stdin).+parseSpecText :: Text -> Either ParseError Spec+parseSpecText = parseSpec "<input>"++-- | Parse a source without discarding its selected language contract.+parseSource :: FilePath -> Text -> Either ParseFailure ParsedSource+parseSource sourceName input = do+  surface <- first frontendCompatibilityFailure (parseSurfaceSource sourceName input)+  first (frontendCompatibilityFailure . frontendFailureFromLowering) (lowerSurfaceSource surface)
+ src/Keiro/Dsl/Parser/Aggregate.hs view
@@ -0,0 +1,337 @@+{-# 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)+    ]++--------------------------------------------------------------------------------
+ src/Keiro/Dsl/Parser/Coordination.hs view
@@ -0,0 +1,311 @@+{-# LANGUAGE ImportQualifiedPost #-}++-- | Process-manager, router, timer, dispatch, and correlation syntax.+module Keiro.Dsl.Parser.Coordination+  ( pProcess,+    pRouter,+  )+where++import Data.Text (Text)+import Data.Text qualified as T+import Keiro.Dsl.Grammar+import Keiro.Dsl.Parser.Core+import Text.Megaparsec+import Text.Megaparsec.Char (char)++-- Process manager + durable timer (EP-3)+--------------------------------------------------------------------------------++pProcess :: P ProcessNode+pProcess = do+  loc <- getLoc+  keyword "process"+  pid <- ident+  keyword "name"+  nm <- stringLit+  inp <- pInputDecl+  corr <- pCorrelate+  saga <- pSaga+  keyword "target"+  tgt <- ident+  projs <- keyword "projections" *> brackets (many ident)+  handle <- pHandle+  _ <- optional pDispatchIdLine+  rejected <- pPolicyLine "rejected"+  poison <- pPolicyLine "poison"+  timer <- pTimerNode+  pure+    ProcessNode+      { procId = pid,+        procName = nm,+        procInput = inp,+        procCorrelate = corr,+        procSaga = saga,+        procTarget = tgt,+        procProjections = projs,+        procHandle = handle,+        procRejected = rejected,+        procPoison = poison,+        procTimer = timer,+        procLoc = loc+      }++pRouter :: P RouterNode+pRouter = do+  loc <- getLoc+  keyword "router"+  rid <- ident+  keyword "name"+  nm <- stringLit+  inp <- pInputDecl+  key <- pRouterKey+  resolved <- pResolveDecl+  keyword "target"+  target <- ident+  projections <- keyword "projections" *> brackets (many ident)+  dispatch <- pRouterDispatch+  pRouterDispatchIdLine+  rejected <- pPolicyLine "rejected"+  poison <- pPolicyLine "poison"+  pure+    RouterNode+      { rtId = rid,+        rtName = nm,+        rtInput = inp,+        rtKey = key,+        rtResolve = resolved,+        rtTarget = target,+        rtProjections = projections,+        rtDispatch = dispatch,+        rtRejected = rejected,+        rtPoison = poison,+        rtLoc = loc+      }++pRouterKey :: P CorrelateDecl+pRouterKey = do+  keyword "key"+  _ <- keyword "input" *> symbol "."+  field <- ident+  keyword "via"+  via <- ident+  pure CorrelateDecl {corrField = field, corrVia = via}++pResolveDecl :: P ResolveDecl+pResolveDecl = do+  loc <- getLoc+  keyword "resolve"+  keyword "stable"+  keyword "via"+  source <- choice [ResolveReadModel <$> (keyword "read-model" *> ident), ResolveHole <$ keyword "hole"]+  keyword "row"+  row <- braces (many ident)+  pure ResolveDecl {rvSource = source, rvRow = row, rvLoc = loc}++pRouterDispatch :: P RouterDispatchNode+pRouterDispatch = do+  loc <- getLoc+  keyword "dispatch-each"+  command <- ident+  fields <- braces (many pFieldBinding)+  disposition <-+    DispatchDisposition+      <$> (keyword "on-appended" *> pDisp)+      <*> (symbol ";" *> keyword "on-duplicate" *> pDisp)+      <*> (symbol ";" *> keyword "on-failed" *> pDisp)+  pure RouterDispatchNode {rdCommand = command, rdFields = fields, rdDisposition = disposition, rdLoc = loc}++pRouterDispatchIdLine :: P ()+pRouterDispatchIdLine = do+  keyword "dispatch-id"+  _ <- symbol "strategy" *> symbol "=" *> keyword "uuidv5"+  _ <- symbol "from" *> symbol "=" *> parens fixedInputs+  pure ()+  where+    fixedInputs = do+      keyword "name"+      _ <- symbol ","+      keyword "key"+      _ <- symbol ","+      keyword "sourceEventId"+      _ <- symbol ","+      keyword "targetStreamName"+      _ <- symbol ","+      keyword "occurrence"++pPolicyLine :: Text -> P PolicyChoice+pPolicyLine clause = keyword clause *> symbol "=>" *> pPolicyChoice++pPolicyChoice :: P PolicyChoice+pPolicyChoice =+  choice+    [ PolHalt <$ keyword "halt",+      PolDeadLetter <$ keyword "deadLetter",+      PolSkip <$ keyword "skip"+    ]++pInputDecl :: P InputDecl+pInputDecl = do+  keyword "input"+  nm <- ident+  fs <- braces (many pField)+  pure InputDecl {inName = nm, inFields = fs}++pCorrelate :: P CorrelateDecl+pCorrelate = do+  keyword "correlate"+  _ <- keyword "input" *> symbol "."+  f <- ident+  keyword "via"+  v <- ident+  pure CorrelateDecl {corrField = f, corrVia = v}++pSaga :: P SagaRef+pSaga = do+  keyword "saga"+  agg <- ident+  keyword "category"+  categoryName <- stringLit+  pure SagaRef {sagaAgg = agg, sagaCategory = categoryName}++pHandle :: P HandleNode+pHandle = do+  keyword "on"+  onName <- ident+  adv <- pAdvance+  disps <- many pDispatch+  keyword "schedule"+  sched <- ident+  pure HandleNode {hOn = onName, hAdvance = adv, hDispatch = disps, hSchedule = sched}++pAdvance :: P AdvanceNode+pAdvance = do+  keyword "advance"+  cmd <- ident+  fs <- braces (many pFieldBinding)+  pure AdvanceNode {advCommand = cmd, advFields = fs}++pDispatch :: P DispatchNode+pDispatch = do+  loc <- getLoc+  keyword "dispatch"+  tgt <- ident+  _ <- symbol "@"+  key <- dottedRef+  cmd <- ident+  fs <- braces (many pFieldBinding)+  disp <-+    DispatchDisposition+      <$> (keyword "on-appended" *> pDisp)+      <*> (symbol ";" *> keyword "on-duplicate" *> pDisp)+      <*> (symbol ";" *> keyword "on-failed" *> pDisp)+  pure DispatchNode {dispTarget = tgt, dispKey = key, dispCommand = cmd, dispFields = fs, dispDisposition = disp, dispLoc = loc}++pDisp :: P Disp+pDisp =+  choice+    [ DAckOk <$ keyword "AckOk",+      DRetry <$ keyword "Retry",+      DDeadLetter <$> (keyword "DeadLetter" *> stringLit)+    ]++-- The dispatch-id line is a fixed, runtime-owned strategy; parse and discard.+pDispatchIdLine :: P ()+pDispatchIdLine = do+  keyword "dispatch-id"+  _ <- symbol "strategy" *> symbol "=" *> ident+  _ <- symbol "from" *> symbol "=" *> parens (sepBy dottedRef (symbol ","))+  pure ()++pTimerNode :: P TimerNode+pTimerNode = do+  loc <- getLoc+  keyword "timer"+  nm <- ident+  tid <- keyword "id" *> pIdExpr+  fat <- keyword "fireAt" *> pFireAt+  pay <- keyword "payload" *> braces (many pFieldBinding)+  fire <- pFire+  _ <- keyword "decode" *> keyword "unknown-status" *> symbol "=>"+  unk <- ident+  keyword "max-attempts"+  ma <- boundedDecimal+  keyword "dead-letter"+  dl <- stringLit+  pure+    TimerNode+      { tmName = nm,+        tmId = tid,+        tmFireAt = fat,+        tmPayload = pay,+        tmFire = fire,+        tmDecodeUnknown = unk,+        tmMaxAttempts = ma,+        tmDeadLetter = dl,+        tmLoc = loc+      }++pIdExpr :: P IdExpr+pIdExpr = do+  keyword "uuidv5"+  pfx <- stringLit+  _ <- symbol "<>"+  _ <- ident -- correlationId (fixed)+  pure IdExpr {ideStrategy = UuidV5Id, idePrefix = pfx}++pFireAt :: P FireAtExpr+pFireAt = do+  _ <- keyword "input" *> symbol "."+  f <- ident+  _ <- symbol "+"+  w <- pWindow+  pure FireAtExpr {faField = f, faWindow = w}++pFire :: P FireNode+pFire = do+  keyword "fire"+  keyword "dispatch"+  tgt <- ident+  _ <- symbol "@"+  key <- dottedRef+  cmd <- ident+  fs <- braces (many pFieldBinding)+  fid <- keyword "fired-event-id" *> pIdExpr+  disp <-+    FireDisposition+      <$> (keyword "on-ok" *> pFireOutcome)+      <*> (symbol ";" *> keyword "on-reject" *> pFireOutcome)+      <*> (symbol ";" *> keyword "on-ambiguous" *> pFireOutcome)+      <*> (symbol ";" *> keyword "on-error" *> pFireOutcome)+      <*> (symbol ";" *> keyword "not-mine" *> pFireOutcome)+  pure FireNode {fireTarget = tgt, fireKey = key, fireCommand = cmd, fireFields = fs, fireFiredEventId = fid, fireDisposition = disp}++pFireOutcome :: P FireOutcome+pFireOutcome = choice [OFired <$ keyword "Fired", ORetry <$ keyword "Retry"]++pFieldBinding :: P FieldBinding+pFieldBinding = do+  n <- ident+  v <- optional (symbol "=" *> pBindingValue)+  pure FieldBinding {fbName = n, fbValue = v}++-- | A binding value: a quoted string (kept quoted) or a dotted reference.+pBindingValue :: P Text+pBindingValue = choice [quoted, dottedRef]+  where+    quoted = do+      s <- stringLit+      pure ("\"" <> s <> "\"")++-- | A dotted/plain reference token like @input.hospitalId@, @timer.id@,+-- @correlationId@.+dottedRef :: P Text+dottedRef = lexeme $ do+  c <- asciiLetter+  cs <- many (asciiAlphaNum <|> char '_' <|> char '.')+  pure (T.pack (c : cs))++-- | A double-quoted string literal, returning raw (unescaped) inner text.+-- The surface syntax supports a closed escape set so unknown escapes remain+-- available for backward-compatible extensions.
+ src/Keiro/Dsl/Parser/Core.hs view
@@ -0,0 +1,450 @@+{-# LANGUAGE ImportQualifiedPost #-}+-- EP-4 integration: structural keywords never used as identifiers, so a+-- list like @accept A B C@ stops at the next block keyword.+-- EP-5 pgmq structural keywords.+-- EP-6 workflow/operation: reserved so the multi-word result-type parse and+-- node boundaries don't swallow the next block keyword.+-- EP-107 read-model structural words. Clause labels such as table and+-- schema remain usable identifiers because their block parser consumes+-- them with symbol-style matching.+--------------------------------------------------------------------------------+--------------------------------------------------------------------------------+--------------------------------------------------------------------------------+-- Helpers+--------------------------------------------------------------------------------+{-# LANGUAGE ImportQualifiedPost #-}++-- | Parser primitives shared by every internal grammar concern.+module Keiro.Dsl.Parser.Core+  ( ContextualParseFailure (..),+    P,+    firstContextualFailure,+    bundleFailureSpan,+    bundleExpected,+    bundleMessage,+    contextualFailureAt,+    requireLanguageFeatureAt,+    sc,+    lexeme,+    symbol,+    keyword,+    identChar,+    asciiLetter,+    asciiUpper,+    asciiDigit,+    asciiAlphaNum,+    failAt,+    boundedDecimal,+    checkedDecimal,+    reservedWords,+    ident,+    wireWord,+    patchIdWord,+    getLoc,+    withOwnedSpan,+    spanOf,+    locatedValue,+    optionalLanguageFeature,+    pField,+    pVersion,+    pWindow,+    decimalText,+    signedDecimalText,+    integerLiteral,+    stringLit,+    brackets,+    braces,+    parens,+    pModulePrefix,+  )+where++import Data.Char (isAlpha, isAlphaNum, isAscii, isDigit, isSpace, isUpper)+import Data.List.NonEmpty qualified as NE+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as T+import Keiro.Dsl.Frontend.Internal+import Keiro.Dsl.Grammar+import Keiro.Dsl.LanguageVersion+import Keiro.Dsl.Source+import Text.Megaparsec+import Text.Megaparsec.Char (char, digitChar, letterChar, space1)+import Text.Megaparsec.Char.Lexer qualified as L+import Prelude hiding (span)++data ContextualParseFailure = ContextualParseFailure+  { contextualFailureCode :: !SourceLanguageErrorCode,+    contextualFailureFeature :: !(Maybe LanguageFeature),+    contextualFailureSpan :: !SourceSpan+  }+  deriving stock (Eq, Ord, Show)++instance ShowErrorComponent ContextualParseFailure where+  showErrorComponent contextual = T.unpack (sourceLanguageErrorCodeText (contextualFailureCode contextual))++type P = Parsec ContextualParseFailure Text++firstContextualFailure :: ParseErrorBundle Text ContextualParseFailure -> Maybe ContextualParseFailure+firstContextualFailure bundle =+  case [ contextual+       | FancyError _ fancy <- NE.toList (bundleErrors bundle),+         ErrorCustom contextual <- Set.toList fancy+       ] of+    contextual : _ -> Just contextual+    [] -> Nothing++bundleFailureSpan :: ParseErrorBundle Text e -> SourceSpan+bundleFailureSpan bundle =+  SourceSpan+    { source = sourceName position,+      start = point,+      end = point+    }+  where+    offset = errorOffset (NE.head (bundleErrors bundle))+    reachedState = reachOffsetNoLine offset (bundlePosState bundle)+    position = pstateSourcePos reachedState+    point = sourcePointAt offset position++bundleExpected :: ParseErrorBundle Text e -> [Text]+bundleExpected bundle =+  case NE.head (bundleErrors bundle) of+    TrivialError _ _ expected -> map (T.pack . show) (Set.toList expected)+    FancyError {} -> []++bundleMessage :: ParseErrorBundle Text ContextualParseFailure -> Text+bundleMessage = T.pack . parseErrorTextPretty . NE.head . bundleErrors++contextualFailureAt :: SourceSpan -> SourceLanguageErrorCode -> P a+contextualFailureAt span code =+  customFailure ContextualParseFailure {contextualFailureCode = code, contextualFailureFeature = Nothing, contextualFailureSpan = span}++requireLanguageFeatureAt :: FrontendContext -> LanguageFeature -> SourceSpan -> P ()+requireLanguageFeatureAt context feature span+  | frontendSupportsFeature context feature = pure ()+  | otherwise =+      customFailure+        ContextualParseFailure+          { contextualFailureCode = LanguageFeatureRequiresVersion,+            contextualFailureFeature = Just feature,+            contextualFailureSpan = span+          }++-- | Space consumer: spaces, newlines, and @#@ line comments are all whitespace.+sc :: P ()+sc = L.space space1 (L.skipLineComment "#") empty++lexeme :: P a -> P a+lexeme = L.lexeme sc++symbol :: Text -> P Text+symbol = L.symbol sc++-- | A reserved keyword: the literal word not followed by an identifier+-- character (so @goto@ matches @goto@ but not @gotoX@).+keyword :: Text -> P ()+keyword w = (lexeme . try) (string' w *> notFollowedBy (identChar <|> (char '-' *> identChar)))+  where+    string' = chunk++identChar :: P Char+identChar = asciiAlphaNum <|> char '_'++asciiLetter :: P Char+asciiLetter = satisfy (\c -> isAscii c && isAlpha c)++asciiUpper :: P Char+asciiUpper = satisfy (\c -> isAscii c && isUpper c)++asciiDigit :: P Char+asciiDigit = satisfy (\c -> isAscii c && isDigit c)++asciiAlphaNum :: P Char+asciiAlphaNum = satisfy (\c -> isAscii c && isAlphaNum c)++-- | Fail with the diagnostic caret placed at a previously captured offset.+failAt :: Int -> String -> P a+failAt offset message = region (setErrorOffset offset) (fail message)++-- | Parse a decimal as an unbounded Integer, then reject values that cannot be+-- represented as Int. Parsing L.decimal directly at Int silently wraps.+boundedDecimal :: P Int+boundedDecimal = do+  offset <- getOffset+  value <- lexeme (L.decimal :: P Integer)+  checkedDecimal offset value++checkedDecimal :: Int -> Integer -> P Int+checkedDecimal offset value+  | value > fromIntegral (maxBound :: Int) =+      failAt+        offset+        ( "decimal literal "+            <> show value+            <> " is out of range (maximum "+            <> show (maxBound :: Int)+            <> ")"+        )+  | otherwise = pure (fromIntegral value)++-- | Words that may not be used as bare identifiers, because they introduce a+-- different construct and would otherwise be swallowed (e.g. @aggregate@ ending+-- one node and beginning the next).+reservedWords :: [Text]+reservedWords =+  [ "context",+    "module",+    "layout",+    "prefixed",+    "collocated",+    "id",+    "enum",+    "rule",+    "mapped",+    "ex",+    "aggregate",+    "regs",+    "states",+    "command",+    "event",+    "wire",+    "projection",+    "snapshot",+    "category",+    "guard",+    "write",+    "emit",+    "goto",+    "fields",+    "status-map",+    "true",+    "false",+    "retiring",+    "deprecated",+    "upcast",+    "from",+    "HOLE",+    "process",+    "router",+    "dispatch-each",+    "resolve",+    "read-model",+    "dispatch",+    "intake",+    "contract",+    "topic",+    "accept",+    "bind",+    "dedupe",+    "persist",+    "decode",+    "disposition",+    "publisher",+    "map",+    "workqueue",+    "queue",+    "payload",+    "retry",+    "fanout",+    "dedup",+    "enqueue",+    "seenIn",+    "workflow",+    "operation",+    "consistency",+    "body",+    "step",+    "await",+    "sleep",+    "child",+    "patch",+    "continueAsNew",+    "readmodel",+    "columns",+    "feed",+    "scope",+    "shape"+  ]++-- | A CamelCase / snake_case identifier (no dashes): type names, register+-- names, command\/event\/state names, enum constructors, projection keys.+ident :: P Name+ident = (lexeme . try) $ do+  c <- asciiLetter <|> char '_'+  cs <- many identChar+  let w = T.pack (c : cs)+  if w `elem` reservedWords+    then fail ("unexpected reserved word " <> T.unpack w)+    else pure w++-- | A wire-spelling token, which may contain dashes (@partial-divert@,+-- @hospital-capacity@). Used for the context name, id prefixes, enum wire+-- spellings, and status-map values.+wireWord :: P Text+wireWord = lexeme $ do+  c <- asciiLetter <|> asciiDigit+  cs <- many (identChar <|> char '-')+  pure (T.pack (c : cs))++-- | Patch ids use wire-word spelling, but admit @:@ so the validator can emit+-- the domain-specific 'WorkflowPatchIdInvalid' diagnostic at the owning item.+patchIdWord :: P Text+patchIdWord = lexeme $ do+  c <- asciiLetter <|> asciiDigit+  cs <- many (identChar <|> char '-' <|> char ':')+  pure (T.pack (c : cs))++getLoc :: P Loc+getLoc = (Loc . unPos . sourceLine) <$> getSourcePos++-- | Attach the exact syntax owned by a production. Existing token parsers+-- consume following trivia, so the consumed slice is scanned to exclude final+-- whitespace and @#@ comments from the half-open end point.+withOwnedSpan :: P a -> P (Located a)+withOwnedSpan parser = do+  startOffset <- getOffset+  startPosition <- getSourcePos+  startState <- getParserState+  inputBefore <- getInput+  value <- parser+  inputAfter <- getInput+  let consumedLength = T.length inputBefore - T.length inputAfter+      ownedLength = ownedSyntaxLength (T.take consumedLength inputBefore)+      endOffset = startOffset + ownedLength+      endPosition = pstateSourcePos (reachOffsetNoLine endOffset (statePosState startState))+      span =+        SourceSpan+          { source = sourceName startPosition,+            start = sourcePointAt startOffset startPosition,+            end = sourcePointAt endOffset endPosition+          }+  pure Located {span, value}++sourcePointAt :: Int -> SourcePos -> SourcePoint+sourcePointAt offset position =+  SourcePoint+    { offset,+      line = unPos (sourceLine position),+      column = unPos (sourceColumn position)+    }++spanOf :: Located a -> SourceSpan+spanOf Located {span} = span++locatedValue :: Located a -> a+locatedValue Located {value} = value++data TriviaScan+  = InSyntax+  | InString !Bool+  | InComment++ownedSyntaxLength :: Text -> Int+ownedSyntaxLength = go 0 InSyntax 0 . T.unpack+  where+    go _ _ lastOwned [] = lastOwned+    go index mode lastOwned (character : rest) =+      case mode of+        InComment+          | character == '\n' || character == '\r' -> go (index + 1) InSyntax lastOwned rest+          | otherwise -> go (index + 1) InComment lastOwned rest+        InString escaped+          | escaped -> go (index + 1) (InString False) (index + 1) rest+          | character == '\\' -> go (index + 1) (InString True) (index + 1) rest+          | character == '"' -> go (index + 1) InSyntax (index + 1) rest+          | otherwise -> go (index + 1) (InString False) (index + 1) rest+        InSyntax+          | character == '#' -> go (index + 1) InComment lastOwned rest+          | character == '"' -> go (index + 1) (InString False) (index + 1) rest+          | isSpace character -> go (index + 1) InSyntax lastOwned rest+          | otherwise -> go (index + 1) InSyntax (index + 1) rest++optionalLanguageFeature :: FrontendContext -> LanguageFeature -> Text -> P a -> P (Maybe a)+optionalLanguageFeature context feature marker parser+  | frontendSupportsFeature context feature = optional parser+  | otherwise = reject <|> pure Nothing+  where+    reject = do+      locatedMarker <- withOwnedSpan (try (keyword marker))+      requireLanguageFeatureAt context feature (spanOf locatedMarker)+      fail "unreachable enabled feature in predecessor grammar"++pField :: P Field+pField = do+  n <- ident+  mty <- optional (symbol ":" *> ident)+  pure Field {fieldName = n, fieldType = mty}++pVersion :: P Int+pVersion = do+  offset <- getOffset+  value <- lexeme (try (char 'v' *> (L.decimal :: P Integer) <* notFollowedBy identChar))+  checkedDecimal offset value++pWindow :: P Text+pWindow = lexeme $ do+  ds <- some digitChar+  u <- choice [char 's', char 'm', char 'h'] <?> "time unit: s, m, or h"+  notFollowedBy letterChar <?> "time unit: s, m, or h"+  pure (T.pack (ds <> [u]))++decimalText :: P Text+decimalText = lexeme $ do+  whole <- some digitChar+  fractional <- optional (char '.' *> some digitChar)+  pure (T.pack (whole <> maybe "" ('.' :) fractional))++signedDecimalText :: P Text+signedDecimalText = lexeme $ do+  sign <- optional (char '-')+  digits <- some digitChar+  fractional <- optional (char '.' *> some digitChar)+  pure (T.pack (maybe "" pure sign <> digits <> maybe "" ('.' :) fractional))++integerLiteral :: P Integer+integerLiteral = lexeme (L.signed (pure ()) L.decimal)++stringLit :: P Text+stringLit = lexeme $ do+  _ <- char '"'+  s <- many strChar+  _ <- char '"'+  pure (T.pack s)+  where+    strChar =+      choice+        [ char '\\' *> escapeCode,+          char '\n' *> fail "unescaped newline in string literal (write \\n)",+          anySingleBut '"'+        ]+    escapeCode =+      choice+        [ '"' <$ char '"',+          '\\' <$ char '\\',+          '\n' <$ char 'n',+          '\t' <$ char 't',+          '\r' <$ char 'r',+          anySingle >>= \c -> fail ("unknown escape sequence \\" <> [c] <> " in string literal")+        ]++brackets :: P a -> P a+brackets = between (symbol "[") (symbol "]")++braces :: P a -> P a+braces = between (symbol "{") (symbol "}")++parens :: P a -> P a+parens = between (symbol "(") (symbol ")")++-- | A dotted module prefix with one or more PascalCase segments.+pModulePrefix :: P Text+pModulePrefix = lexeme $ do+  seg0 <- pSeg+  segs <- many (char '.' *> pSeg)+  pure (T.intercalate "." (seg0 : segs))+  where+    pSeg = do+      c <- asciiUpper+      cs <- many identChar+      pure (T.pack (c : cs))
+ src/Keiro/Dsl/Parser/Declaration.hs view
@@ -0,0 +1,73 @@+-- | Shared ID, enum, and rule declarations.+module Keiro.Dsl.Parser.Declaration+  ( pIdDecl,+    pEnumDecl,+    pRuleDecl,+  )+where++import Keiro.Dsl.Frontend.Internal (FrontendContext)+import Keiro.Dsl.Grammar+import Keiro.Dsl.LanguageVersion+import Keiro.Dsl.Parser.Core+import Keiro.Dsl.Parser.Expression (pExpr)+import Keiro.Dsl.Parser.Mapped (pUsingNominalBinding)+import Keiro.Dsl.Source (Located, mapLocated)+import Keiro.Dsl.Syntax (SurfaceElement (..))+import Text.Megaparsec (many, sepBy1)++pIdDecl :: FrontendContext -> P IdDecl+pIdDecl context = do+  loc <- getLoc+  keyword "id"+  name <- ident+  _ <- symbol "prefix"+  _ <- symbol "="+  pfx <- wireWord+  binding <- optionalLanguageFeature context NominalBindingSyntax "using" pUsingNominalBinding+  pure IdDecl {idName = name, idPrefix = pfx, idBinding = binding, idLoc = loc}++pEnumDecl :: FrontendContext -> P EnumDecl+pEnumDecl context = do+  loc <- getLoc+  keyword "enum"+  name <- ident+  ctors <- braces (many pEnumCtor)+  binding <- optionalLanguageFeature context NominalBindingSyntax "using" pUsingNominalBinding+  pure EnumDecl {enumName = name, enumCtors = ctors, enumBinding = binding, enumLoc = loc}+  where+    pEnumCtor = do+      c <- ident+      _ <- symbol "="+      w <- wireWord+      pure (c, w)++pRuleDecl :: FrontendContext -> P (RuleDecl, [Located SurfaceElement])+pRuleDecl context = do+  loc <- getLoc+  keyword "rule"+  name <- ident+  _ <- symbol ":"+  dom <- ident+  _ <- symbol "->"+  cod <- ident+  keyword "ex"+  parsedCases <- sepBy1 pCase (symbol ";")+  let cases = map fst parsedCases+      elements = map snd parsedCases+  pure+    ( RuleDecl+        { ruleName = name,+          ruleDomain = dom,+          ruleCodomain = cod,+          ruleCases = cases,+          ruleLoc = loc+        },+      elements+    )+  where+    pCase = do+      c <- ident+      _ <- symbol "=>"+      expression <- withOwnedSpan (pExpr context)+      pure ((c, locatedValue expression), mapLocated SurfaceExpression expression)
+ src/Keiro/Dsl/Parser/Document.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE ImportQualifiedPost #-}++-- | Released-language dispatch and complete document composition.+module Keiro.Dsl.Parser.Document+  ( parseSurfaceSource,+  )+where++import Data.Text (Text)+import Data.Text qualified as T+import Keiro.Dsl.Frontend.Internal+import Keiro.Dsl.Grammar+import Keiro.Dsl.LanguageVersion+import Keiro.Dsl.Parser.Aggregate (pAggregate)+import Keiro.Dsl.Parser.Coordination (pProcess, pRouter)+import Keiro.Dsl.Parser.Core+import Keiro.Dsl.Parser.Declaration (pEnumDecl, pIdDecl, pRuleDecl)+import Keiro.Dsl.Parser.Integration (pContract, pEmit, pIntake, pPublisher)+import Keiro.Dsl.Parser.Mapped (pMappedTopItem)+import Keiro.Dsl.Parser.Preamble+import Keiro.Dsl.Parser.Queue (pPgmqDispatch, pWorkqueue)+import Keiro.Dsl.Parser.ReadModel (pReadModel)+import Keiro.Dsl.Parser.Workflow (pOperation, pWorkflow)+import Keiro.Dsl.Source+import Keiro.Dsl.Syntax+import Text.Megaparsec+import Prelude hiding (span)++parseSurfaceSource :: FilePath -> Text -> Either FrontendFailure SurfaceSource+parseSurfaceSource src input = do+  sourceLanguage <- selectSourceLanguage src input+  definition <- case lookupLanguageDefinition (effectiveLanguageVersion sourceLanguage) of+    Just value -> Right value+    Nothing -> error "keiro-dsl internal invariant: source selection returned an unregistered language"+  parseSelectedBody FrontendContext {source = src, language = sourceLanguage, definition}+  where+    parseSelectedBody context@FrontendContext {language = sourceLanguage} =+      let laterPreambleCode = case sourceLanguage of+            LegacyUnversioned -> MisplacedLanguagePreamble+            DeclaredLanguage {} -> DuplicateLanguagePreamble+          parser =+            sc+              *> case sourceLanguage of+                LegacyUnversioned -> pSurfaceDocument context Nothing laterPreambleCode <* eof+                DeclaredLanguage {} -> do+                  locatedPreamble <- withOwnedSpan (sourceLanguage <$ pDeclaredPreamble)+                  pSurfaceDocument context (Just locatedPreamble) laterPreambleCode <* eof+       in case runParser parser src input of+            Left bundle -> case firstContextualFailure bundle of+              Just contextual ->+                let diagnostic = contextualDiagnostic src sourceLanguage contextual+                    supported = languageVersionsSupportingFeature <$> contextualFailureFeature contextual+                 in Left (frontendFailureFromSourceDiagnostic BodyParsingPhase (contextualFailureSpan contextual) supported diagnostic)+              Nothing ->+                Left+                  ( frontendFailureFromBody+                      BodyParsingPhase+                      (bundleFailureSpan bundle)+                      (bundleMessage bundle)+                      (bundleExpected bundle)+                      (BodyGrammarFailure (T.pack (errorBundlePretty bundle)))+                  )+            Right surface -> Right surface++-- Top level+--------------------------------------------------------------------------------++pContextualPreamble :: SourceLanguageErrorCode -> P a+pContextualPreamble code = do+  locatedPreamble <- withOwnedSpan (try pDeclaredPreamble)+  contextualFailureAt (spanOf locatedPreamble) code++pSurfaceDocument :: FrontendContext -> Maybe (Located SourceLanguage) -> SourceLanguageErrorCode -> P SurfaceSource+pSurfaceDocument context@FrontendContext {source = sourceName, language = sourceLanguage} preamble laterPreambleCode = do+  spec <- pSurfaceSpec context laterPreambleCode+  pure SurfaceSource {source = sourceName, language = sourceLanguage, preamble, spec}++pSurfaceSpec :: FrontendContext -> SourceLanguageErrorCode -> P (Located SurfaceSpec)+pSurfaceSpec context laterPreambleCode = do+  pContextualPreamble laterPreambleCode <|> pure ()+  locatedContext <- withOwnedSpan (keyword "context" *> wireWord)+  moduleRoot <- optional (withOwnedSpan pModuleClause)+  layout <- optional (withOwnedSpan pLayoutClause)+  parsedItems <- many (withOwnedSpan (pTopItem context laterPreambleCode))+  let items = map surfaceItem parsedItems+      elements = concatMap surfaceElements parsedItems+  let contextSpan@SourceSpan {source, start} = spanOf locatedContext+      finalSpan = case reverse items of+        item : _ -> spanOf item+        [] -> case layout of+          Just value -> spanOf value+          Nothing -> maybe contextSpan spanOf moduleRoot+      SourceSpan {end} = finalSpan+  pure+    Located+      { span = SourceSpan {source, start, end},+        value = SurfaceSpec {context = locatedContext, moduleRoot, layout, items, elements}+      }+  where+    surfaceItem Located {span, value = ParsedTopItem item _} = Located {span, value = item}+    surfaceElements Located {value = ParsedTopItem _ elements} = elements++-- | @module Acme.Services@ — the optional namespace-prefix clause.+pModuleClause :: P Text+pModuleClause = keyword "module" *> pModulePrefix++-- | @layout (prefixed|collocated)@ — the optional placement-style clause.+pLayoutClause :: P Placement+pLayoutClause =+  keyword "layout"+    *> choice+      [ GeneratedPrefix <$ keyword "prefixed",+        CollocatedLeaf <$ keyword "collocated"+      ]++-- | A parsed top-level value plus any nested syntax evidence it owns.+data ParsedTopItem = ParsedTopItem !SurfaceTopItem ![Located SurfaceElement]++pTopItem :: FrontendContext -> SourceLanguageErrorCode -> P ParsedTopItem+pTopItem context laterPreambleCode =+  choice+    ( [ pContextualPreamble laterPreambleCode,+        plain (SurfaceId <$> pIdDecl context),+        plain (SurfaceEnum <$> pEnumDecl context),+        do+          (rule, elements) <- pRuleDecl context+          pure (ParsedTopItem (SurfaceRule rule) elements),+        plain (pMappedTopItem context)+      ]+        ++ [ plain (SurfaceNode . NRouter <$> pRouter),+             plain (SurfaceNode . NProcess <$> pProcess),+             plain (SurfaceNode . NContract <$> pContract),+             plain (SurfaceNode . NIntake <$> pIntake),+             plain (SurfaceNode . NEmit <$> pEmit),+             plain (SurfaceNode . NPublisher <$> pPublisher),+             plain (SurfaceNode . NWorkqueue <$> pWorkqueue),+             plain (SurfaceNode . NPgmqDispatch <$> pPgmqDispatch),+             plain (SurfaceNode . NReadModel <$> pReadModel),+             plain (SurfaceNode . NWorkflow <$> pWorkflow),+             plain (SurfaceNode . NOperation <$> pOperation),+             do+               (aggregate, elements) <- pAggregate context+               pure (ParsedTopItem (SurfaceNode (NAggregate aggregate)) elements)+           ]+    )+  where+    plain parser = (`ParsedTopItem` []) <$> parser
+ src/Keiro/Dsl/Parser/Expression.hs view
@@ -0,0 +1,149 @@+{-# LANGUAGE ImportQualifiedPost #-}++-- | Legacy and typed scalar expression syntax.+module Keiro.Dsl.Parser.Expression+  ( pExpr,+  )+where++import Control.Monad.Combinators.Expr (Operator (..), makeExprParser)+import Data.Char (isUpper)+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 Text.Megaparsec++-- Expr sublanguage+--------------------------------------------------------------------------------++pExpr :: FrontendContext -> P Expr+pExpr context+  | frontendSupportsFeature context TypedAggregateExpressionSyntax = makeExprParser (pScalarTerm context) scalarOperatorTable+  | otherwise = makeExprParser (pLegacyTerm context) legacyOperatorTable++pLegacyTerm :: FrontendContext -> P Expr+pLegacyTerm context =+  choice+    [ parens (pExpr context),+      pUnsupportedScalarTerm context,+      EAtom . ABool <$> (True <$ keyword "true" <|> False <$ keyword "false"),+      EAtom . AName <$> ident+    ]++pUnsupportedScalarTerm :: FrontendContext -> P Expr+pUnsupportedScalarTerm context = do+  locatedRoot <- withOwnedSpan (try ((keyword "reg" <|> keyword "cmd") *> symbol "."))+  requireLanguageFeatureAt context TypedAggregateExpressionSyntax (spanOf locatedRoot)+  fail "unreachable supported scalar term in predecessor grammar"++-- | Highest precedence first: relational comparisons bind tighter than @&&@,+-- which binds tighter than @||@.+legacyOperatorTable :: [[Operator P Expr]]+legacyOperatorTable =+  [ [InfixL arithmeticUnsupported],+    [ InfixN (ECmp OpLe <$ op "<="),+      InfixN (ECmp OpGe <$ op ">="),+      InfixN (ECmp OpEq <$ op "=="),+      InfixN (ECmp OpNeq <$ op "!="),+      InfixN (ECmp OpLt <$ op "<"),+      InfixN (ECmp OpGt <$ op ">")+    ],+    [InfixL (EAnd <$ op "&&")],+    [InfixL (EOr <$ op "||")]+  ]+  where+    op s = symbol s+    arithmeticUnsupported = do+      offset <- getOffset+      operator <- lexeme (oneOf ['+', '-', '*', '/'])+      failAt offset ("aggregate arithmetic operator '" <> [operator] <> "' is unsupported; compare or copy whole values instead")++pScalarTerm :: FrontendContext -> P Expr+pScalarTerm context =+  choice+    [ parens (pExpr context),+      collectionTermUnsupported,+      try pIdLiteral,+      do+        loc <- getLoc+        ELiteral loc . LiteralBool <$> (True <$ keyword "true" <|> False <$ keyword "false"),+      do+        loc <- getLoc+        ELiteral loc . LiteralText <$> stringLit,+      try $ do+        loc <- getLoc+        ELiteral loc . LiteralIntegral <$> integerLiteral,+      pScalarPath+    ]++pIdLiteral :: P Expr+pIdLiteral = do+  loc <- getLoc+  constructor <- ident+  value <- parens stringLit+  pure (ELiteral loc (LiteralId constructor value))++pScalarPath :: P Expr+pScalarPath = do+  loc <- getLoc+  firstName <- ident+  rest <- many (symbol "." *> ident)+  pure $ case (firstName, rest) of+    ("reg", name : path) -> EPath loc RegisterRoot (name : path)+    ("cmd", name : path) -> EPath loc CommandRoot (name : path)+    (_, [constructor]) | startsUpper firstName -> ELiteral loc (LiteralQualified firstName constructor)+    _ -> EPath loc UnqualifiedRoot (firstName : rest)+  where+    startsUpper value = maybe False (isUpper . fst) (T.uncons value)++collectionTermUnsupported :: P Expr+collectionTermUnsupported = do+  offset <- getOffset+  choice+    [ () <$ symbol "[",+      () <$ symbol "{",+      () <$ keyword "keys",+      () <$ keyword "values",+      () <$ keyword "any",+      () <$ keyword "all"+    ]+  failAt offset collectionExpressionMessage++collectionExpressionMessage :: String+collectionExpressionMessage = "CollectionExpressionUnsupported: collection expressions are reserved for plan 166"++scalarOperatorTable :: [[Operator P Expr]]+scalarOperatorTable =+  [ [InfixL (op "*" *> located EMultiply)],+    [ InfixL (op "+" *> located EAdd),+      InfixL (op "-" *> located ESubtract),+      InfixL scalarArithmeticUnsupported+    ],+    [ InfixN (ECmp OpLe <$ op "<="),+      InfixN (ECmp OpGe <$ op ">="),+      InfixN (ECmp OpEq <$ op "=="),+      InfixN (ECmp OpNeq <$ op "!="),+      InfixN (ECmp OpLt <$ op "<"),+      InfixN (ECmp OpGt <$ op ">"),+      InfixN collectionOperatorUnsupported+    ],+    [InfixL (EAnd <$ op "&&")],+    [InfixL (EOr <$ op "||")]+  ]+  where+    op value = symbol value+    located constructor = do+      loc <- getLoc+      pure (constructor loc)+    scalarArithmeticUnsupported = do+      offset <- getOffset+      operator <- lexeme (oneOf ['/', '%'])+      failAt offset ("aggregate arithmetic operator '" <> [operator] <> "' is unsupported")+    collectionOperatorUnsupported = do+      offset <- getOffset+      _ <- try (keyword "not" *> keyword "in") <|> keyword "in"+      failAt offset collectionExpressionMessage++--------------------------------------------------------------------------------
+ src/Keiro/Dsl/Parser/Integration.hs view
@@ -0,0 +1,241 @@+-- | Contract, intake, emit, and publisher syntax.+module Keiro.Dsl.Parser.Integration+  ( pContract,+    pIntake,+    pEmit,+    pPublisher,+  )+where++import Keiro.Dsl.Grammar+import Keiro.Dsl.Parser.Core+import Text.Megaparsec++-- Integration contract (EP-4)+--------------------------------------------------------------------------------++pContract :: P ContractNode+pContract = do+  loc <- getLoc+  keyword "contract"+  nm <- ident+  _ <- symbol "{"+  keyword "schemaVersion"+  sv <- boundedDecimal+  keyword "discriminator"+  disc <- ident+  topics <- many pTopic+  events <- many pContractEvent+  _ <- symbol "}"+  pure+    ContractNode+      { ctrName = nm,+        ctrSchemaVersion = sv,+        ctrDiscriminator = disc,+        ctrTopics = topics,+        ctrEvents = events,+        ctrLoc = loc+      }+  where+    pTopic = do+      keyword "topic"+      alias <- ident+      t <- stringLit+      pure (alias, t)+    pContractEvent = do+      keyword "event"+      nm <- ident+      keyword "on"+      topicAlias <- ident+      fs <- braces (many pContractField)+      pure ContractEvent {ceName = nm, ceTopic = topicAlias, ceFields = fs}+    pContractField = do+      n <- ident+      _ <- symbol ":"+      ty <- pContractType+      _ <- optional (symbol ";")+      pure ContractField {cfName = n, cfType = ty}+    pContractType =+      choice+        [ CTypeId <$> (keyword "typeid" *> stringLit),+          CText <$ keyword "text",+          CInt <$ keyword "int"+        ]++pIntake :: P IntakeNode+pIntake = do+  loc <- getLoc+  keyword "intake"+  nm <- ident+  _ <- symbol "{"+  keyword "contract"+  ctr <- ident+  keyword "topic"+  tp <- ident+  keyword "accept"+  acc <- some ident+  binds <- many pBindRow+  keyword "dedupe"+  keyword "key"+  dk <- ident+  keyword "policy"+  dp <- ident+  persistence <-+    option InkPersistFull $+      keyword "persist"+        *> symbol "="+        *> choice+          [ InkPersistFull <$ keyword "full-envelope",+            InkPersistDedupeOnly <$ keyword "dedupe-only"+          ]+  dec <- pDecode+  disp <- pDisposition+  _ <- symbol "}"+  pure+    IntakeNode+      { inkName = nm,+        inkContract = ctr,+        inkTopic = tp,+        inkAccept = acc,+        inkBinds = binds,+        inkDedupeKey = dk,+        inkDedupePolicy = dp,+        inkPersist = persistence,+        inkDecode = dec,+        inkDisposition = disp,+        inkLoc = loc+      }+  where+    pBindRow = do+      keyword "bind"+      f <- ident+      keyword "from"+      src <- pWireSource+      req <- option False (True <$ keyword "required")+      xc <- option False (True <$ (keyword "cross-check" *> keyword "body"))+      pure BindRow {brField = f, brSource = src, brRequired = req, brCrossCheck = xc}+    pWireSource =+      choice+        [ SrcHeader <$> (keyword "header" *> stringLit),+          SrcKafkaKey <$ keyword "kafka-key",+          SrcKafkaCursor <$ keyword "kafka-cursor",+          SrcBody <$ keyword "body"+        ]+    pDecode = do+      keyword "decode"+      _ <- symbol "{"+      keyword "envelope"+      env <- pEnvelopePolicy+      keyword "body"+      strict <- (True <$ keyword "strict") <|> (False <$ keyword "lenient")+      keyword "schemaVersion"+      _ <- symbol "=="+      v <- boundedDecimal+      _ <- symbol "}"+      pure DecodeSpec {decEnvelope = env, decBodyStrict = strict, decBodySchemaVersion = v}+    pEnvelopePolicy = do+      a <- wireWord+      b <- wireWord+      pure (a <> " " <> b)+    pDisposition = do+      keyword "disposition"+      rows <- braces (many pDispositionRow)+      pure rows+    pDispositionRow = do+      loc <- getLoc+      o <- ident+      _ <- symbol "=>"+      act <- pInboxAction+      pure DispositionRow {drOutcome = o, drAction = act, drLoc = loc}+    pInboxAction =+      choice+        [ IAckOk <$ keyword "ackOk",+          IRetry <$> (keyword "retry" *> pWindow),+          IDeadLetter <$> (keyword "deadLetter" *> optional stringLit)+        ]++pEmit :: P EmitNode+pEmit = do+  loc <- getLoc+  keyword "emit"+  nm <- ident+  _ <- symbol "{"+  keyword "contract"+  ctr <- ident+  keyword "topic"+  tp <- ident+  keyword "source"+  src <- stringLit+  keyword "key"+  k <- ident+  keyword "map"+  disc <- ident+  (rows, skip) <- braces pMapRows+  keyword "messageId"+  mid <- pDerive+  keyword "idempotencyKey"+  idk <- pDerive+  _ <- symbol "}"+  pure+    EmitNode+      { emName = nm,+        emContract = ctr,+        emTopic = tp,+        emSource = src,+        emKey = k,+        emDiscriminant = disc,+        emMap = rows,+        emSkip = skip,+        emMessageId = mid,+        emIdempotencyKey = idk,+        emLoc = loc+      }+  where+    pMapRows = do+      rows <- many pMapRow+      skip <- option False (True <$ try (symbol "_" *> symbol "=>" *> keyword "skip"))+      pure (rows, skip)+    pMapRow = try $ do+      loc <- getLoc+      v <- stringLit+      _ <- symbol "=>"+      ev <- ident+      pure EmitMapRow {emrValue = v, emrEvent = ev, emrLoc = loc}+    pDerive = do+      keyword "derive"+      pfx <- optional stringLit+      keyword "hole"+      pure DeriveSpec {dsPrefix = pfx}++pPublisher :: P PublisherNode+pPublisher = do+  loc <- getLoc+  keyword "publisher"+  nm <- ident+  _ <- symbol "{"+  keyword "emit"+  em <- ident+  keyword "ordering"+  ord <- ident+  keyword "maxAttempts"+  ma <- boundedDecimal+  keyword "backoff"+  bk <- ident+  bw <- pWindow+  bm <- optional (keyword "max" *> symbol "=" *> pWindow)+  multiplier <- optional (keyword "multiplier" *> symbol "=" *> decimalText)+  keyword "outboxId"+  keyword "stable"+  keyword "from"+  obf <- ident+  _ <- symbol "}"+  pure+    PublisherNode+      { pubName = nm,+        pubEmit = em,+        pubOrdering = ord,+        pubMaxAttempts = ma,+        pubBackoff = BackoffSpec {boKind = bk, boWindow = bw, boMax = bm, boMultiplier = multiplier},+        pubOutboxField = obf,+        pubLoc = loc+      }
+ src/Keiro/Dsl/Parser/Mapped.hs view
@@ -0,0 +1,323 @@+{-# LANGUAGE ImportQualifiedPost #-}++-- | Consumer-owned mapped and nominal type syntax.+module Keiro.Dsl.Parser.Mapped+  ( pMappedTopItem,+    pMappedTypeExpr,+    pUsingNominalBinding,+  )+where++import Data.Maybe (mapMaybe)+import Data.Text (Text)+import Keiro.Dsl.Frontend.Internal (FrontendContext, frontendSupportsFeature)+import Keiro.Dsl.Grammar+import Keiro.Dsl.LanguageVersion+import Keiro.Dsl.Parser.Core+import Keiro.Dsl.Syntax (SurfaceTopItem (..))+import Text.Megaparsec++-- Consumer-owned mapped and nominal types+--------------------------------------------------------------------------------++data MappedKind = MappedRecord | MappedEnum | MappedUnion++data MappedClause+  = MCHaskell HaskellSource+  | MCBinding Text+  | MCBindingVersion Text+  | MCCanonical Text+  | MCFixtures Text+  | MCInitial Text+  | MCCodec Text+  | MCCodecVersion Text+  | MCShape MappedShape++pMappedTopItem :: FrontendContext -> P SurfaceTopItem+pMappedTopItem context = do+  loc <- getLoc+  keyword "mapped"+  choice+    [ if frontendSupportsFeature context NominalBindingSyntax+        then SurfaceNominalScalar <$> pNominalScalarAfterMapped loc+        else do+          marker <- withOwnedSpan (try (keyword "nominal"))+          requireLanguageFeatureAt context NominalBindingSyntax (spanOf marker)+          fail "unreachable enabled nominal syntax in predecessor grammar",+      SurfaceMapped <$> pMappedStructural context loc,+      SurfaceMapped <$> pMappedOpaque loc+    ]++pMappedStructural :: FrontendContext -> Loc -> P MappedDecl+pMappedStructural context loc = do+  keyword "structural"+  kind <-+    choice+      [ MappedRecord <$ keyword "record",+        MappedEnum <$ keyword "enum",+        MappedUnion <$ keyword "union"+      ]+  name <- ident+  clauses <- braces (many (pStructuralClause context kind))+  hs <- oneClause "haskell" (\case MCHaskell value -> Just value; _ -> Nothing) clauses+  binding <- oneClause "binding" (\case MCBinding value -> Just value; _ -> Nothing) clauses+  bindingVersion <- oneClause "binding-version" (\case MCBindingVersion value -> Just value; _ -> Nothing) clauses+  canonical <- oneClause "canonical-type" (\case MCCanonical value -> Just value; _ -> Nothing) clauses+  fixtures <- oneClause "fixtures" (\case MCFixtures value -> Just value; _ -> Nothing) clauses+  initial <- oneClause "initial" (\case MCInitial value -> Just value; _ -> Nothing) clauses+  shape <- requiredClause "wire" (\case MCShape value -> Just value; _ -> Nothing) clauses+  pure+    MappedStructural+      { msName = name,+        msHaskell = hs,+        msBinding = binding,+        msBindingVersion = bindingVersion,+        msCanonical = canonical,+        msFixtures = fixtures,+        msInitial = initial,+        msShape = shape,+        msLoc = loc+      }++pMappedOpaque :: Loc -> P MappedDecl+pMappedOpaque loc = do+  keyword "opaque"+  name <- ident+  clauses <- braces (many pOpaqueClause)+  hs <- oneClause "haskell" (\case MCHaskell value -> Just value; _ -> Nothing) clauses+  codec <- oneClause "codec" (\case MCCodec value -> Just value; _ -> Nothing) clauses+  version <- oneClause "version" (\case MCCodecVersion value -> Just value; _ -> Nothing) clauses+  fixtures <- oneClause "fixtures" (\case MCFixtures value -> Just value; _ -> Nothing) clauses+  initial <- oneClause "initial" (\case MCInitial value -> Just value; _ -> Nothing) clauses+  pure+    MappedOpaque+      { moName = name,+        moHaskell = hs,+        moCodecId = codec,+        moCodecVersion = version,+        moFixtures = fixtures,+        moInitial = initial,+        moLoc = loc+      }++pNominalScalarAfterMapped :: Loc -> P NominalScalarDecl+pNominalScalarAfterMapped loc = do+  keyword "nominal"+  name <- ident+  _ <- symbol ":"+  representation <- ident+  binding <- pNominalBindingBlock loc+  pure+    NominalScalarDecl+      { nominalScalarName = name,+        nominalScalarRepresentation = representation,+        nominalScalarBinding = binding,+        nominalScalarLoc = loc+      }++pUsingNominalBinding :: P NominalBindingDecl+pUsingNominalBinding = do+  keyword "using"+  loc <- getLoc+  pNominalBindingBlock loc++pNominalBindingBlock :: Loc -> P NominalBindingDecl+pNominalBindingBlock loc = do+  clauses <- braces (many pNominalClause)+  hs <- oneClause "haskell" (\case MCHaskell value -> Just value; _ -> Nothing) clauses+  binding <- oneClause "binding" (\case MCBinding value -> Just value; _ -> Nothing) clauses+  bindingVersion <- oneClause "binding-version" (\case MCBindingVersion value -> Just value; _ -> Nothing) clauses+  canonical <- oneClause "canonical-type" (\case MCCanonical value -> Just value; _ -> Nothing) clauses+  fixtures <- oneClause "fixtures" (\case MCFixtures value -> Just value; _ -> Nothing) clauses+  initial <- oneClause "initial" (\case MCInitial value -> Just value; _ -> Nothing) clauses+  pure+    NominalBindingDecl+      { nominalHaskell = hs,+        nominalBinding = binding,+        nominalBindingVersion = bindingVersion,+        nominalCanonicalType = canonical,+        nominalFixtures = fixtures,+        nominalInitial = initial,+        nominalLoc = loc+      }++pNominalClause :: P MappedClause+pNominalClause =+  choice+    [ MCHaskell <$> pHaskellSource,+      MCBindingVersion <$> pQuotedFact "binding-version",+      MCBinding <$> pQuotedFact "binding",+      MCCanonical <$> pQuotedFact "canonical-type",+      MCFixtures <$> pQuotedFact "fixtures",+      MCInitial <$> pQuotedFact "initial"+    ]++pStructuralClause :: FrontendContext -> MappedKind -> P MappedClause+pStructuralClause context kind =+  choice+    [ MCHaskell <$> pHaskellSource,+      MCBindingVersion <$> pQuotedFact "binding-version",+      MCBinding <$> pQuotedFact "binding",+      MCCanonical <$> pQuotedFact "canonical-type",+      MCFixtures <$> pQuotedFact "fixtures",+      MCInitial <$> pQuotedFact "initial",+      MCShape <$> pMappedShape context kind+    ]++pOpaqueClause :: P MappedClause+pOpaqueClause =+  choice+    [ MCHaskell <$> pHaskellSource,+      MCCodec <$> pQuotedFact "codec",+      MCCodecVersion <$> pQuotedFact "version",+      MCFixtures <$> pQuotedFact "fixtures",+      MCInitial <$> pQuotedFact "initial"+    ]++pHaskellSource :: P HaskellSource+pHaskellSource = do+  keyword "haskell"+  keyword "package"+  _ <- symbol "="+  packageName <- wireWord+  keyword "module"+  _ <- symbol "="+  moduleName <- pModulePrefix+  keyword "type"+  _ <- symbol "="+  typeName <- ident+  pure HaskellSource {hsPackage = packageName, hsModule = moduleName, hsType = typeName}++pQuotedFact :: Text -> P Text+pQuotedFact factName = keyword factName *> symbol "=" *> stringLit++pMappedShape :: FrontendContext -> MappedKind -> P MappedShape+pMappedShape context kind = do+  keyword "wire"+  case kind of+    MappedRecord -> do+      keyword "object"+      keyword "constructor"+      _ <- symbol "="+      constructor <- ident+      unknownFields <- pUnknownFieldsFact+      fields <- braces (many (pWireField context))+      pure (ShapeRecord constructor unknownFields fields)+    MappedEnum -> do+      keyword "string"+      ShapeEnum <$> braces (many pWireEnum)+    MappedUnion -> do+      keyword "tagged-object"+      keyword "tag"+      _ <- symbol "="+      tagField <- stringLit+      keyword "contents"+      _ <- symbol "="+      contentsField <- stringLit+      unknownFields <- pUnknownFieldsFact+      arms <- braces (many (pWireArm context))+      pure (ShapeUnion (TaggedObject tagField contentsField unknownFields) arms)++pUnknownFieldsFact :: P UnknownFields+pUnknownFieldsFact = do+  keyword "unknown-fields"+  _ <- symbol "="+  choice [RejectUnknown <$ keyword "reject", IgnoreUnknown <$ keyword "ignore"]++pWireField :: FrontendContext -> P WireField+pWireField context = do+  loc <- getLoc+  haskellName <- ident+  keyword "as"+  wireKey <- stringLit+  _ <- symbol ":"+  fieldType <- pMappedTypeExpr context+  presence <- choice [PRequired <$ keyword "required", POptional <$ keyword "optional"]+  onMissing <- optional (keyword "on-missing" *> symbol "=" *> pOnMissing)+  pure+    WireField+      { wfHaskell = haskellName,+        wfKey = wireKey,+        wfType = fieldType,+        wfPresence = presence,+        wfOnMissing = onMissing,+        wfLoc = loc+      }++pWireEnum :: P WireEnum+pWireEnum = do+  loc <- getLoc+  constructor <- ident+  keyword "as"+  wireTag <- stringLit+  pure WireEnum {weCtor = constructor, weTag = wireTag, weLoc = loc}++pWireArm :: FrontendContext -> P WireArm+pWireArm context = do+  loc <- getLoc+  constructor <- ident+  keyword "as"+  wireTag <- stringLit+  payload <- optional (symbol ":" *> pMappedTypeExpr context)+  pure WireArm {waCtor = constructor, waTag = wireTag, waPayload = payload, waLoc = loc}++pMappedTypeExpr :: FrontendContext -> P TypeExpr+pMappedTypeExpr context =+  choice+    [ TOptional <$> (keyword "Optional" *> pTypeArgument),+      TList <$> (keyword "List" *> pTypeArgument),+      TMap <$> (keyword "Map" *> pTypeArgument),+      TText <$ keyword "Text",+      TInt <$ keyword "Int",+      TInteger <$ languageFeatureKeyword context IntegerScalarSyntax "Integer",+      TBool <$ keyword "Bool",+      TNatural <$ keyword "Natural",+      TTime <$ (keyword "Time" <|> keyword "UTCTime"),+      TJson <$ keyword "Json",+      TRef <$> ident+    ]+  where+    pTypeArgument = parens (pMappedTypeExpr context) <|> pTypeAtom+    pTypeAtom =+      choice+        [ TText <$ keyword "Text",+          TInt <$ keyword "Int",+          TInteger <$ languageFeatureKeyword context IntegerScalarSyntax "Integer",+          TBool <$ keyword "Bool",+          TNatural <$ keyword "Natural",+          TTime <$ (keyword "Time" <|> keyword "UTCTime"),+          TJson <$ keyword "Json",+          TRef <$> ident+        ]++languageFeatureKeyword :: FrontendContext -> LanguageFeature -> Text -> P ()+languageFeatureKeyword context feature spelling = do+  marker <- withOwnedSpan (keyword spelling)+  requireLanguageFeatureAt context feature (spanOf marker)++pOnMissing :: P OnMissing+pOnMissing =+  choice+    [ OmNull <$ keyword "null",+      OmEmptyList <$ (symbol "[" *> symbol "]"),+      OmEmptyMap <$ (symbol "{" *> symbol "}"),+      OmBool True <$ keyword "true",+      OmBool False <$ keyword "false",+      OmText <$> stringLit,+      OmInt <$> integerLiteral,+      OmCtor <$> ident+    ]++oneClause :: String -> (MappedClause -> Maybe a) -> [MappedClause] -> P (Maybe a)+oneClause clauseName select clauses =+  case mapMaybe select clauses of+    [] -> pure Nothing+    [value] -> pure (Just value)+    _ -> fail ("duplicate " <> clauseName <> " clause in mapped declaration")++requiredClause :: String -> (MappedClause -> Maybe a) -> [MappedClause] -> P a+requiredClause clauseName select clauses = do+  found <- oneClause clauseName select clauses+  maybe (fail ("missing " <> clauseName <> " clause in mapped structural declaration")) pure found++--------------------------------------------------------------------------------
+ src/Keiro/Dsl/Parser/Preamble.hs view
@@ -0,0 +1,117 @@+{-# LANGUAGE ImportQualifiedPost #-}++-- | Released-language preamble recognition and source selection.+module Keiro.Dsl.Parser.Preamble+  ( contextualDiagnostic,+    pDeclaredPreamble,+    selectSourceLanguage,+  )+where++import Data.Char (isAscii, isDigit)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Read qualified as TR+import Keiro.Dsl.Frontend.Internal+import Keiro.Dsl.Grammar (Loc (..))+import Keiro.Dsl.LanguageVersion+import Keiro.Dsl.Parser.Core+import Keiro.Dsl.Source+import Numeric.Natural (Natural)+import Text.Megaparsec+import Text.Megaparsec.Char (char)++contextualDiagnostic :: FilePath -> SourceLanguage -> ContextualParseFailure -> SourceLanguageDiagnostic+contextualDiagnostic src sourceLanguage contextual =+  SourceLanguageDiagnostic+    { sourceLanguageErrorCode = code,+      sourceLanguageSource = src,+      sourceLanguageLoc = Loc line,+      sourceLanguageToken = case code of+        LanguageFeatureRequiresVersion -> Just (languageVersionText effectiveVersion)+        _ -> Nothing,+      sourceLanguageDeclaredVersion = case code of+        LanguageFeatureRequiresVersion -> Just effectiveVersion+        _ -> Nothing,+      sourceLanguageSupportedVersions = supportedLanguageVersions+    }+  where+    code = contextualFailureCode contextual+    SourceSpan {start = SourcePoint {line}} = contextualFailureSpan contextual+    effectiveVersion = effectiveLanguageVersion sourceLanguage++-- | Consume a preamble already validated by 'selectSourceLanguage'. This+-- parser is also reused at grammar boundaries to recognize only complete+-- preamble syntax, never a nested identifier whose spelling is @language@.+pDeclaredPreamble :: P ()+pDeclaredPreamble = do+  keyword "language"+  keyword "keiro-dsl"+  _ <- lexeme (some asciiDigit)+  pure ()++-- | The source-selection pass inspects only the first grammar clause after+-- leading whitespace and comments. Body lines are left entirely to+-- 'pSurfaceSpec'.+data InitialLanguageClause = InitialLanguageClause+  { initialLanguageSpan :: !SourceSpan,+    initialLanguageText :: !Text+  }++selectSourceLanguage :: FilePath -> Text -> Either FrontendFailure SourceLanguage+selectSourceLanguage src input = do+  initialClause <- case runParser pInitialLanguageClause src input of+    Left bundle ->+      Left+        ( frontendFailureFromBody+            SourceSelectionPhase+            (bundleFailureSpan bundle)+            (bundleMessage bundle)+            (bundleExpected bundle)+            (BodyGrammarFailure (T.pack (errorBundlePretty bundle)))+        )+    Right value -> Right value+  case initialClause of+    Nothing -> Right LegacyUnversioned+    Just languageClause -> do+      version <- parsePreamble languageClause+      case lookupLanguageDefinition version of+        Nothing -> Left (sourceFailure UnsupportedLanguageVersion languageClause (Just (languageVersionText version)) (Just version))+        Just _ -> Right (DeclaredLanguage version (Loc (startLine (initialLanguageSpan languageClause))))+  where+    sourceFailure code line tokenText declared =+      frontendFailureFromSourceDiagnostic+        SourceSelectionPhase+        (initialLanguageSpan line)+        Nothing+        SourceLanguageDiagnostic+          { sourceLanguageErrorCode = code,+            sourceLanguageSource = src,+            sourceLanguageLoc = Loc (startLine (initialLanguageSpan line)),+            sourceLanguageToken = tokenText,+            sourceLanguageDeclaredVersion = declared,+            sourceLanguageSupportedVersions = supportedLanguageVersions+          }++    parsePreamble line = case T.words (initialLanguageText line) of+      ["language", "keiro-dsl", tokenText]+        | T.all (\c -> isAscii c && isDigit c) tokenText && not (T.null tokenText) ->+            case TR.decimal tokenText :: Either String (Natural, Text) of+              Right (value, "") -> case languageVersion value of+                Just version -> Right version+                Nothing -> invalid line tokenText+              _ -> invalid line tokenText+      wordsFound -> invalid line (T.unwords wordsFound)++    invalid line tokenText =+      Left (sourceFailure InvalidLanguageVersion line (Just tokenText) Nothing)++pInitialLanguageClause :: P (Maybe InitialLanguageClause)+pInitialLanguageClause = sc *> optional pLanguageClause+  where+    pLanguageClause = do+      _ <- lookAhead (chunk "language" *> notFollowedBy (identChar <|> (char '-' *> identChar)))+      locatedLine <- withOwnedSpan (takeWhileP (Just "language preamble") (\c -> c /= '\n' && c /= '\r'))+      let rawLine = locatedValue locatedLine+      let content = T.strip (T.takeWhile (/= '#') rawLine)+      pure InitialLanguageClause {initialLanguageSpan = spanOf locatedLine, initialLanguageText = content}
+ src/Keiro/Dsl/Parser/Queue.hs view
@@ -0,0 +1,147 @@+-- | Workqueue and pgmq-dispatch syntax.+module Keiro.Dsl.Parser.Queue+  ( pWorkqueue,+    pPgmqDispatch,+  )+where++import Keiro.Dsl.Grammar+import Keiro.Dsl.Parser.Core+import Text.Megaparsec++pWorkqueue :: P WorkqueueNode+pWorkqueue = do+  loc <- getLoc+  keyword "workqueue"+  nm <- ident+  _ <- symbol "{"+  keyword "queue"+  _ <- symbol "logical" *> symbol "="+  logical <- stringLit+  keyword "derive"+  _ <- symbol "physical" *> symbol "="+  phys <- stringLit+  _ <- symbol "dlq" *> symbol "="+  dlqName <- stringLit+  _ <- symbol "table" *> symbol "="+  tbl <- stringLit+  ordering <- option WqUnordered pOrdering+  groupKey <- optional pGroupKey+  provision <- option WqStandard pProvision+  keyword "payload"+  pn <- ident+  fields <- braces (many pWqField)+  keyword "retry"+  _ <- symbol "maxRetries" *> symbol "="+  mr <- boundedDecimal+  _ <- symbol "delay" *> symbol "="+  dl <- pWindow+  _ <- symbol "dlq" *> symbol "="+  dlqOn <- (True <$ keyword "on") <|> (False <$ keyword "off")+  keyword "disposition"+  disp <- braces (many pWqDispRow)+  _ <- symbol "}"+  pure+    WorkqueueNode+      { wqName = nm,+        wqLogical = logical,+        wqPhysical = phys,+        wqDlq = dlqName,+        wqTable = tbl,+        wqOrdering = ordering,+        wqGroupKey = groupKey,+        wqProvision = provision,+        wqPayloadName = pn,+        wqPayload = fields,+        wqMaxRetries = mr,+        wqDelay = dl,+        wqDlqOn = dlqOn,+        wqDisposition = disp,+        wqLoc = loc+      }+  where+    pOrdering = do+      _ <- symbol "ordering"+      choice+        [ WqUnordered <$ symbol "unordered",+          WqFifoThroughput <$ symbol "fifo-throughput",+          WqFifoRoundRobin <$ symbol "fifo-roundrobin"+        ]+    pGroupKey = do+      _ <- symbol "group" *> symbol "key" *> symbol "from"+      field <- ident+      _ <- symbol "via"+      via <- ident+      fixture <- optional (symbol "fixture" *> stringLit)+      pure WqGroupKey {gkField = field, gkVia = via, gkFixture = fixture}+    pProvision = do+      _ <- symbol "provision"+      choice+        [ WqStandard <$ symbol "standard",+          WqUnlogged <$ symbol "unlogged",+          do+            _ <- symbol "partitioned" *> symbol "("+            _ <- symbol "interval" *> symbol "="+            interval <- stringLit+            _ <- symbol "," *> symbol "retention" *> symbol "="+            retention <- stringLit+            _ <- symbol ")"+            pure (WqPartitioned interval retention)+        ]+    pWqField = do+      n <- ident+      _ <- symbol "->"+      w <- stringLit+      ty <- ident+      req <- option False (True <$ keyword "required")+      pure WqField {wqfName = n, wqfWire = w, wqfType = ty, wqfRequired = req}+    pWqDispRow = do+      loc <- getLoc+      o <- ident+      _ <- symbol "->"+      act <- choice [IAckOk <$ keyword "ackOk", IRetry <$> (keyword "retry" *> pWindow), IDeadLetter <$> (keyword "deadLetter" *> optional stringLit)]+      pure WqDispRow {wqdOutcome = o, wqdAction = act, wqdLoc = loc}++pPgmqDispatch :: P PgmqDispatchNode+pPgmqDispatch = do+  loc <- getLoc+  keyword "dispatch"+  nm <- ident+  _ <- symbol "{"+  keyword "source"+  _ <- symbol "readModel" *> symbol "="+  srm <- ident+  _ <- symbol "key" *> symbol "="+  sk <- ident+  keyword "fanout"+  _ <- symbol "body" *> symbol "="+  fb <- ident+  keyword "dedup"+  _ <- symbol "key" *> symbol "="+  dk <- ident+  _ <- keyword "seenIn" *> symbol "readModel" *> symbol "="+  drm <- ident+  _ <- symbol "field" *> symbol "="+  drmf <- ident+  _ <- keyword "seenIn" *> symbol "queue" *> symbol "="+  dq <- ident+  _ <- symbol "field" *> symbol "="+  dqf <- ident+  keyword "enqueue"+  _ <- symbol "to" *> symbol "="+  enq <- ident+  _ <- symbol "}"+  pure+    PgmqDispatchNode+      { pdName = nm,+        pdSourceReadModel = srm,+        pdSourceKey = sk,+        pdFanoutBody = fb,+        pdDedupKey = dk,+        pdDedupReadModel = drm,+        pdDedupReadModelField = drmf,+        pdDedupQueue = dq,+        pdDedupQueueField = dqf,+        pdEnqueueTo = enq,+        pdLoc = loc+      }
+ src/Keiro/Dsl/Parser/ReadModel.hs view
@@ -0,0 +1,60 @@+-- | Read-model syntax.+module Keiro.Dsl.Parser.ReadModel+  ( pReadModel,+  )+where++import Keiro.Dsl.Grammar+import Keiro.Dsl.Parser.Core+import Text.Megaparsec++pReadModel :: P ReadModelNode+pReadModel = do+  loc <- getLoc+  keyword "readmodel"+  name <- ident+  _ <- symbol "{"+  _ <- symbol "table" *> symbol "="+  table <- stringLit+  _ <- symbol "schema" *> symbol "="+  schema <- stringLit+  _ <- symbol "columns"+  columns <- braces (many pColumn)+  _ <- symbol "version" *> symbol "="+  version <- boundedDecimal+  _ <- symbol "shape" *> symbol "="+  shape <- stringLit+  _ <- symbol "consistency" *> symbol "="+  consistency <- pConsistency+  scope <- optional (symbol "scope" *> symbol "=" *> pScope)+  _ <- symbol "feed" *> symbol "="+  feed <- pFeed+  subscription <- optional (symbol "subscription" *> symbol "=" *> stringLit)+  _ <- symbol "}"+  pure+    ReadModelNode+      { rmName = name,+        rmTable = table,+        rmSchema = schema,+        rmColumns = columns,+        rmVersion = version,+        rmShape = shape,+        rmConsistency = consistency,+        rmScope = scope,+        rmFeed = feed,+        rmSubscription = subscription,+        rmLoc = loc+      }+  where+    pColumn =+      RmColumn+        <$> wireWord+        <*> ident+        <*> option False (True <$ keyword "required")+    pConsistency = choice [Strong <$ keyword "Strong", Eventual <$ keyword "Eventual"]+    pScope =+      choice+        [ RmEntireLog <$ keyword "entire-log",+          RmCategory <$> (keyword "category" *> stringLit)+        ]+    pFeed = choice [RmInline <$ keyword "inline", RmSubscription <$ keyword "subscription"]
+ src/Keiro/Dsl/Parser/Workflow.hs view
@@ -0,0 +1,135 @@+{-# LANGUAGE ImportQualifiedPost #-}++-- | Durable workflow and operation syntax.+module Keiro.Dsl.Parser.Workflow+  ( pWorkflow,+    pOperation,+  )+where++import Data.Text qualified as T+import Keiro.Dsl.Grammar+import Keiro.Dsl.Parser.Core+import Text.Megaparsec++pWorkflow :: P WorkflowNode+pWorkflow = do+  loc <- getLoc+  keyword "workflow"+  wid <- ident+  keyword "name"+  nm <- stringLit+  keyword "in"+  inTy <- ident+  inFields <- option [] (braces (many pField))+  keyword "out"+  outTy <- ident+  keyword "id"+  keyword "from"+  keyword "input"+  idField <- optional (symbol "." *> ident)+  keyword "via"+  idVia <- ident+  keyword "body"+  body <- many pWfBodyItem+  pure+    WorkflowNode+      { wfId = wid,+        wfStable = nm,+        wfInput = inTy,+        wfInputFields = inFields,+        wfOutput = outTy,+        wfIdField = idField,+        wfIdVia = idVia,+        wfBody = body,+        wfLoc = loc+      }+  where+    pWfBodyItem =+      choice+        [ do+            loc <- getLoc+            WfStep <$> (keyword "step" *> wireWord) <*> (symbol "->" *> ident) <*> pure loc,+          do+            loc <- getLoc+            WfAwait <$> (keyword "await" *> wireWord) <*> (symbol "->" *> ident) <*> pure loc,+          do+            loc <- getLoc+            WfSleep <$> (keyword "sleep" *> wireWord) <*> (keyword "after" *> ident) <*> pure loc,+          do+            loc <- getLoc+            WfChild+              <$> (keyword "child" *> wireWord)+              <*> (keyword "id" *> keyword "input" *> keyword "via" *> ident)+              <*> (symbol "->" *> ident)+              <*> pure loc,+          do+            loc <- getLoc+            WfPatch+              <$> (keyword "patch" *> patchIdWord)+              <*> braces (many pWfBodyItem)+              <*> pure loc,+          do+            loc <- getLoc+            WfContinueAsNew <$> (keyword "continueAsNew" *> ident) <*> pure loc+        ]++pOperation :: P OperationNode+pOperation = do+  loc <- getLoc+  keyword "operation"+  nm <- ident+  shape <-+    choice+      [ pCommandOp,+        pQueryOp,+        pSignalOp,+        pRunOp+      ]+  pure OperationNode {opName = nm, opShape = shape, opLoc = loc}+  where+    pCommandOp = do+      keyword "command"+      keyword "on"+      agg <- ident+      _ <- keyword "stream" *> keyword "from"+      sf <- ident+      keyword "via"+      sv <- ident+      proj <- option [] (keyword "project" *> brackets (many ident))+      pure (CommandOp agg sf sv proj)+    pQueryOp = do+      keyword "query"+      rm <- ident+      keyword "input"+      inp <- ident+      keyword "result"+      res <- pTypeExpr+      cons <- option "Strong" (keyword "consistency" *> ident)+      pure (QueryOp rm inp res cons)+    pSignalOp = do+      keyword "signal"+      lbl <- wireWord+      keyword "of"+      wf <- ident+      _ <- keyword "key" *> keyword "from"+      kf <- ident+      keyword "via"+      kv <- ident+      keyword "value"+      val <- ident+      pure (SignalOp lbl wf kf kv val)+    pRunOp = do+      keyword "run"+      wf <- ident+      keyword "input"+      inp <- ident+      _ <- keyword "outcome" *> symbol "->"+      oc <- ident+      pure (RunOp wf inp oc)+    -- A result type expression, possibly multi-word like @Maybe TransferDecision@.+    pTypeExpr = do+      ws <- some ident+      pure (T.unwords ws)++--------------------------------------------------------------------------------
src/Keiro/Dsl/SemanticContract.hs view
@@ -28,6 +28,7 @@   ( LanguageVersion,     ParsedSource (..),     SourceLanguage (..),+    definitionRuntimeSemantics,     effectiveLanguageVersion,     languageVersion,     languageVersionNumber,@@ -76,14 +77,11 @@ -- semantics a compile-visible registry change. effectiveLanguageContractForVersion :: LanguageVersion -> Maybe EffectiveLanguageContract effectiveLanguageContractForVersion version = do-  _ <- lookupLanguageDefinition version+  definition <- lookupLanguageDefinition version   pure     EffectiveLanguageContract       { effectiveContractLanguageVersion = version,-        effectiveRuntimeSemantics =-          if languageVersionNumber version >= 3-            then "keiro-dsl/runtime-semantics/2"-            else "keiro-dsl/runtime-semantics/1"+        effectiveRuntimeSemantics = definitionRuntimeSemantics definition       }  -- | Fold/replay discriminator for runtime semantics newer than the historical
+ src/Keiro/Dsl/Source.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE NoFieldSelectors #-}++-- | Source locations used by the Keiro language frontend.+--+-- Offsets count tokens in the parsed 'Text' stream. Spans are half-open: the+-- end point is immediately after the final token owned by a construct.+module Keiro.Dsl.Source+  ( SourcePoint (..),+    mkSourcePoint,+    SourceSpan (..),+    mkSourceSpan,+    validSourceSpan,+    startLine,+    Located (..),+    mapLocated,+  )+where++import GHC.Generics (Generic)+import Prelude hiding (span)++-- | An exact position in a parsed source.+data SourcePoint = SourcePoint+  { offset :: !Int,+    line :: !Int,+    column :: !Int+  }+  deriving stock (Eq, Ord, Show, Generic)++-- | Construct a non-negative offset with one-based line and column values.+mkSourcePoint :: Int -> Int -> Int -> Maybe SourcePoint+mkSourcePoint offset line column+  | offset < 0 = Nothing+  | line < 1 = Nothing+  | column < 1 = Nothing+  | otherwise = Just SourcePoint {offset, line, column}++-- | A half-open source interval.+data SourceSpan = SourceSpan+  { source :: !FilePath,+    start :: !SourcePoint,+    end :: !SourcePoint+  }+  deriving stock (Eq, Ord, Show, Generic)++-- | Construct a span whose end does not precede its start.+mkSourceSpan :: FilePath -> SourcePoint -> SourcePoint -> Maybe SourceSpan+mkSourceSpan source start end+  | validPoints start end = Just SourceSpan {source, start, end}+  | otherwise = Nothing++-- | Check the ordering invariant even for a value built with the public+-- constructor.+validSourceSpan :: SourceSpan -> Bool+validSourceSpan SourceSpan {start, end} = validPoints start end++-- | Project the one-based starting line used by the legacy semantic 'Loc'.+startLine :: SourceSpan -> Int+startLine SourceSpan {start = SourcePoint {line}} = line++-- | A value together with the exact source syntax that owns it.+data Located a = Located+  { span :: !SourceSpan,+    value :: !a+  }+  deriving stock (Eq, Ord, Show, Functor, Generic)++mapLocated :: (a -> b) -> Located a -> Located b+mapLocated f Located {span, value} = Located {span, value = f value}++validPoints :: SourcePoint -> SourcePoint -> Bool+validPoints+  SourcePoint {offset = startOffset, line = startLineNumber, column = startColumn}+  SourcePoint {offset = endOffset, line = endLineNumber, column = endColumn} =+    startOffset <= endOffset+      && (startLineNumber, startColumn) <= (endLineNumber, endColumn)
+ src/Keiro/Dsl/Syntax.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE NoFieldSelectors #-}++-- | Located, non-lossless surface syntax for a Keiro source document.+--+-- The surface layer preserves document order and source ownership while+-- deliberately omitting comments and whitespace. Semantic consumers should+-- use 'Keiro.Dsl.Frontend.lowerSurfaceSource' rather than depending on these+-- types directly.+module Keiro.Dsl.Syntax+  ( SurfaceSource (..),+    SurfaceSpec (..),+    SurfaceTopItem (..),+    SurfaceElement (..),+  )+where++import Data.Text (Text)+import GHC.Generics (Generic)+import Keiro.Dsl.Grammar+  ( EnumDecl,+    Expr,+    IdDecl,+    MappedDecl,+    Node,+    NominalScalarDecl,+    Placement,+    RuleDecl,+  )+import Keiro.Dsl.LanguageVersion (SourceLanguage)+import Keiro.Dsl.Source (Located)++-- | One parsed source with its selected released language contract.+data SurfaceSource = SurfaceSource+  { source :: !FilePath,+    language :: !SourceLanguage,+    preamble :: !(Maybe (Located SourceLanguage)),+    spec :: !(Located SurfaceSpec)+  }+  deriving stock (Eq, Show, Generic)++-- | Document-level syntax before declarations are normalized into grouped+-- semantic lists.+data SurfaceSpec = SurfaceSpec+  { context :: !(Located Text),+    moduleRoot :: !(Maybe (Located Text)),+    layout :: !(Maybe (Located Placement)),+    items :: ![Located SurfaceTopItem],+    elements :: ![Located SurfaceElement]+  }+  deriving stock (Eq, Show, Generic)++-- | A source-ordered top-level declaration or runtime node. Existing grammar+-- values are reused where parsing does not erase a syntax distinction.+data SurfaceTopItem+  = SurfaceId !IdDecl+  | SurfaceEnum !EnumDecl+  | SurfaceRule !RuleDecl+  | SurfaceNominalScalar !NominalScalarDecl+  | SurfaceMapped !MappedDecl+  | SurfaceNode !Node+  deriving stock (Eq, Show, Generic)++-- | Nested syntax whose exact ownership is required before semantic lowering.+-- The set grows with grammar-module extraction; semantic leaf values are+-- reused when parsing does not erase a distinction.+data SurfaceElement+  = SurfaceField !Text+  | SurfaceExpression !Expr+  deriving stock (Eq, Show, Generic)
+ test/Keiro/Dsl/FrontendCompatibility.hs view
@@ -0,0 +1,468 @@+{-# LANGUAGE ImportQualifiedPost #-}++-- | The executable compatibility oracle for the released keiro-dsl-0.7.0.0+-- language frontend. The checked JSON is deliberately data, not an update mode:+-- a behavior change must produce a reviewed manifest diff.+module Keiro.Dsl.FrontendCompatibility+  ( SourceExpectation (..),+    WorkspaceExpectation (..),+    CompatibilityManifest (..),+    diagnosticGoldens,+    frontendCompatibilitySpec,+    observeSource,+    observeWorkspace,+    readCompatibilityManifest,+    readRepoText,+    releasedFrontendEntryPoints,+    sourceFixturePaths,+    workspaceFixturePaths,+  )+where++import Control.Monad (filterM, forM, forM_)+import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.:?), (.=))+import Data.Aeson qualified as Aeson+import Data.List (sort)+import Data.List.NonEmpty qualified as NE+import Data.Maybe (listToMaybe)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.IO qualified as TIO+import Keiro.Dsl.FrontendPublicApiProbe (apiProbe)+import Keiro.Dsl.LanguageVersion+import Keiro.Dsl.Parser (parseSource)+import Keiro.Dsl.PrettyPrint (renderSource)+import Keiro.Dsl.SemanticContract (checkedSource)+import Keiro.Dsl.Validate (Diagnostic (..), Severity (..), renderDiagnostic, validateService)+import Keiro.Dsl.Workspace+import Numeric.Natural (Natural)+import System.Directory (doesDirectoryExist, doesFileExist, listDirectory)+import System.Environment (lookupEnv)+import System.FilePath (takeDirectory, takeExtension, (</>))+import Test.Hspec+import Text.Read (readMaybe)++data SourceExpectation = SourceExpectation+  { sourcePath :: !FilePath,+    sourceForm :: !Text,+    sourceDeclaredVersion :: !(Maybe Natural),+    sourceEffectiveVersion :: !(Maybe Natural),+    sourceResult :: !Text,+    sourceFailureClass :: !(Maybe Text),+    sourceDiagnosticCode :: !(Maybe Text)+  }+  deriving stock (Eq, Show)++instance FromJSON SourceExpectation where+  parseJSON = withObject "SourceExpectation" $ \fields ->+    SourceExpectation+      <$> fields .: "path"+      <*> fields .: "sourceForm"+      <*> fields .:? "declaredVersion"+      <*> fields .:? "effectiveVersion"+      <*> fields .: "result"+      <*> fields .:? "failureClass"+      <*> fields .:? "diagnosticCode"++instance ToJSON SourceExpectation where+  toJSON row =+    object+      [ "path" .= sourcePath row,+        "sourceForm" .= sourceForm row,+        "declaredVersion" .= sourceDeclaredVersion row,+        "effectiveVersion" .= sourceEffectiveVersion row,+        "result" .= sourceResult row,+        "failureClass" .= sourceFailureClass row,+        "diagnosticCode" .= sourceDiagnosticCode row+      ]++data WorkspaceExpectation = WorkspaceExpectation+  { workspacePath :: !FilePath,+    workspaceResult :: !Text,+    workspaceFailureClass :: !(Maybe Text),+    workspaceDiagnosticCode :: !(Maybe Text)+  }+  deriving stock (Eq, Show)++instance FromJSON WorkspaceExpectation where+  parseJSON = withObject "WorkspaceExpectation" $ \fields ->+    WorkspaceExpectation+      <$> fields .: "path"+      <*> fields .: "result"+      <*> fields .:? "failureClass"+      <*> fields .:? "diagnosticCode"++instance ToJSON WorkspaceExpectation where+  toJSON row =+    object+      [ "path" .= workspacePath row,+        "result" .= workspaceResult row,+        "failureClass" .= workspaceFailureClass row,+        "diagnosticCode" .= workspaceDiagnosticCode row+      ]++data CompatibilityManifest = CompatibilityManifest+  { manifestSchema :: !Text,+    manifestRelease :: !Text,+    manifestEntryPoints :: ![Text],+    manifestSources :: ![SourceExpectation],+    manifestWorkspaces :: ![WorkspaceExpectation]+  }+  deriving stock (Eq, Show)++instance FromJSON CompatibilityManifest where+  parseJSON = withObject "CompatibilityManifest" $ \fields ->+    CompatibilityManifest+      <$> fields .: "schema"+      <*> fields .: "release"+      <*> fields .: "entryPoints"+      <*> fields .: "sources"+      <*> fields .: "workspaces"++instance ToJSON CompatibilityManifest where+  toJSON manifest =+    object+      [ "schema" .= manifestSchema manifest,+        "release" .= manifestRelease manifest,+        "entryPoints" .= manifestEntryPoints manifest,+        "sources" .= manifestSources manifest,+        "workspaces" .= manifestWorkspaces manifest+      ]++frontendCompatibilitySpec :: SpecWith ()+frontendCompatibilitySpec = describe "frontend 0.7 compatibility" $ do+  it "decodes the released manifest and classifies every checked-in frontend fixture" $ do+    manifest <- readManifest+    manifestSchema manifest `shouldBe` "keiro-dsl/frontend-compatibility/1"+    manifestRelease manifest `shouldBe` "0.7.0.0"+    manifestEntryPoints manifest `shouldBe` releasedFrontendEntryPoints+    sources <- sourceFixturePaths+    workspaces <- workspaceFixturePaths+    map sourcePath (manifestSources manifest) `shouldBe` sources+    map workspacePath (manifestWorkspaces manifest) `shouldBe` workspaces++  it "preserves every source outcome, released contract, and accepted canonical round trip" $ do+    manifest <- readManifest+    forM_ (manifestSources manifest) $ \expected -> do+      actual <- observeSource (sourcePath expected)+      actual `shouldBe` expected+      whenAccepted expected $ do+        source <- readRepoText (sourcePath expected)+        case parseSource (sourcePath expected) source of+          Left failure -> expectationFailure (show failure)+          Right parsed -> case parseSource (sourcePath expected) (renderSource parsed) of+            Left failure -> expectationFailure (show failure)+            Right reparsed -> do+              parsedSpec reparsed `shouldBe` parsedSpec parsed+              effectiveLanguageVersion (parsedSourceLanguage reparsed)+                `shouldBe` effectiveLanguageVersion (parsedSourceLanguage parsed)+              sourceFormText (parsedSourceLanguage reparsed)+                `shouldBe` sourceFormText (parsedSourceLanguage parsed)++  it "preserves every workspace composition outcome and member-attribution code" $ do+    manifest <- readManifest+    forM_ (manifestWorkspaces manifest) $ \expected ->+      observeWorkspace (workspacePath expected) `shouldReturn` expected++  it "keeps direct parsing and representative one-member workspaces semantically identical" $ do+    let examples =+          [ ("legacy.keiro", "context parity\n"),+            ("v1.keiro", "language keiro-dsl 1\ncontext parity\n"),+            ("v2.keiro", "language keiro-dsl 2\ncontext parity\n"),+            ("v3.keiro", "language keiro-dsl 3\ncontext parity\n")+          ]+    forM_ examples $ \(path, source) -> case parseSource path source of+      Left failure -> expectationFailure (show failure)+      Right parsed -> do+        let workspace = oneMemberParsedWorkspace path parsed+        wsMergedSpec workspace `shouldBe` parsedSpec parsed+        checkWorkspace workspace `shouldBe` []++  it "renders curated source, grammar, semantic, and workspace failures byte-for-byte" $+    forM_ diagnosticGoldens $ \(name, renderActual) -> do+      expected <- readRepoText (diagnosticRoot </> name)+      actual <- renderActual+      actual `shouldBe` expected++  it "keeps the released parser, renderer, and representative selector signatures compiling" $+    apiProbe `shouldBe` ()++whenAccepted :: SourceExpectation -> IO () -> IO ()+whenAccepted row action+  | sourceResult row == "accept" = action+  | otherwise = pure ()++observeSource :: FilePath -> IO SourceExpectation+observeSource path = do+  source <- readRepoText path+  let (headerForm, headerVersion) = sourceHeader source+      base result failureClass diagnostic effective =+        SourceExpectation+          { sourcePath = path,+            sourceForm = headerForm,+            sourceDeclaredVersion = headerVersion,+            sourceEffectiveVersion = effective,+            sourceResult = result,+            sourceFailureClass = failureClass,+            sourceDiagnosticCode = diagnostic+          }+  pure $ case parseSource path source of+    Left (SourceLanguageFailure diagnostic) ->+      base+        "reject"+        (Just "source-language")+        (Just (sourceLanguageErrorCodeText (sourceLanguageErrorCode diagnostic)))+        (languageVersionNumber <$> sourceLanguageDeclaredVersion diagnostic)+    Left (BodyGrammarFailure _) ->+      base "reject" (Just "body-grammar") Nothing (supportedHeaderVersion headerVersion)+    Right parsed ->+      let sourceLanguage = parsedSourceLanguage parsed+          effective = Just (languageVersionNumber (effectiveLanguageVersion sourceLanguage))+          errors = filter ((== Error) . severity) (validateService (checkedSource parsed))+       in case errors of+            diagnostic : _ -> base "reject" (Just "semantic") (Just (T.pack (show (code diagnostic)))) effective+            [] -> base "accept" Nothing Nothing effective++observeWorkspace :: FilePath -> IO WorkspaceExpectation+observeWorkspace path = do+  resolved <- resolveRepoPath path+  loaded <- loadWorkspace (fileContentSource (takeDirectory resolved)) path+  pure $ case loaded of+    Left (WorkspaceManifestUnreadable _) -> rejected "manifest-unreadable" Nothing+    Left (WorkspaceManifestUnparseable _) -> rejected "manifest-grammar" Nothing+    Left (WorkspaceRefused diagnostics) ->+      rejected "composition" (Just (T.pack (show (wdCode (NE.head diagnostics)))))+    Right workspace -> case filter ((== Error) . wdSeverity) (checkWorkspace workspace) of+      diagnostic : _ -> rejected "semantic" (Just (T.pack (show (wdCode diagnostic))))+      [] -> WorkspaceExpectation path "accept" Nothing Nothing+  where+    rejected failureClass diagnostic = WorkspaceExpectation path "reject" (Just failureClass) diagnostic++sourceHeader :: Text -> (Text, Maybe Natural)+sourceHeader source = case firstSignificantLine source of+  Just lineText+    | Just token <- T.stripPrefix "language keiro-dsl " lineText ->+        ("declared", readMaybe (T.unpack (T.strip token)))+  _ -> ("legacy-unversioned", Nothing)++firstSignificantLine :: Text -> Maybe Text+firstSignificantLine =+  listToMaybe+    . filter (\lineText -> not (T.null lineText) && not ("#" `T.isPrefixOf` lineText))+    . map T.strip+    . T.lines++supportedHeaderVersion :: Maybe Natural -> Maybe Natural+supportedHeaderVersion raw = do+  number <- raw+  version <- languageVersion number+  _ <- lookupLanguageDefinition version+  pure number++readManifest :: IO CompatibilityManifest+readManifest = do+  path <- resolveRepoPath manifestPath+  decoded <- Aeson.eitherDecodeFileStrict' path+  case decoded of+    Left problem -> expectationFailure problem >> fail "invalid frontend compatibility manifest"+    Right manifest -> pure manifest++readCompatibilityManifest :: IO CompatibilityManifest+readCompatibilityManifest = readManifest++sourceFixturePaths :: IO [FilePath]+sourceFixturePaths = fixturePaths ".keiro"++workspaceFixturePaths :: IO [FilePath]+workspaceFixturePaths = fixturePaths ".keiro-workspace"++fixturePaths :: String -> IO [FilePath]+fixturePaths extension = do+  fixtureRoot <- resolveRepoDirectory "keiro-dsl/test/fixtures"+  relative <- walk fixtureRoot ""+  pure . sort $ ["keiro-dsl/test/fixtures" </> path | path <- relative, takeExtension path == extension]+  where+    walk root relative = do+      entries <- sort <$> listDirectory (root </> relative)+      fmap concat . forM entries $ \entry -> do+        let child = if null relative then entry else relative </> entry+        isDirectory <- doesDirectoryExist (root </> child)+        if isDirectory then walk root child else pure [child]++readRepoText :: FilePath -> IO Text+readRepoText path = resolveRepoPath path >>= TIO.readFile++resolveRepoPath :: FilePath -> IO FilePath+resolveRepoPath path = do+  override <- lookupEnv "KEIRO_DSL_TEST_ROOT"+  let packageRelative = maybe path id (T.unpack <$> T.stripPrefix "keiro-dsl/" (T.pack path))+      candidates = [path, packageRelative] <> maybe [] (\root -> [root </> path, root </> packageRelative]) override+  existing <- filterM doesFileExist candidates+  case existing of+    candidate : _ -> pure candidate+    [] -> fail ("unable to locate repository file " <> show path <> "; tried " <> show candidates)++resolveRepoDirectory :: FilePath -> IO FilePath+resolveRepoDirectory path = do+  override <- lookupEnv "KEIRO_DSL_TEST_ROOT"+  let packageRelative = maybe path id (T.unpack <$> T.stripPrefix "keiro-dsl/" (T.pack path))+      candidates = [path, packageRelative] <> maybe [] (\root -> [root </> path, root </> packageRelative]) override+  existing <- filterM doesDirectoryExist candidates+  case existing of+    candidate : _ -> pure candidate+    [] -> fail ("unable to locate repository directory " <> show path <> "; tried " <> show candidates)++manifestPath :: FilePath+manifestPath = "keiro-dsl/test/frontend-0.7/manifest.json"++diagnosticRoot :: FilePath+diagnosticRoot = "keiro-dsl/test/frontend-0.7/diagnostics"++releasedFrontendEntryPoints :: [Text]+releasedFrontendEntryPoints =+  [ "library.parseSource",+    "library.parseSpec",+    "library.parseSpecText",+    "workspace.parseWorkspaceManifest",+    "workspace.loadWorkspace.members",+    "cli.parse.single",+    "cli.parse.workspace-manifest",+    "cli.check.single",+    "cli.check.workspace",+    "cli.inspect.single",+    "cli.inspect.workspace",+    "cli.behavior-obligations.single",+    "cli.behavior-obligations.workspace",+    "cli.scaffold.single",+    "cli.scaffold.workspace",+    "cli.diff.single-working-tree-and-git-baseline",+    "cli.diff.workspace-working-tree-and-git-baseline"+  ]++diagnosticGoldens :: [(FilePath, IO Text)]+diagnosticGoldens =+  [ ("invalid-preamble.txt", renderParse "invalid-preamble.keiro" "language keiro-dsl nope\ncontext source-fixture\n"),+    ("misplaced-preamble.txt", renderParse "misplaced-preamble.keiro" "context source-fixture\nlanguage keiro-dsl 1\n"),+    ("duplicate-preamble.txt", renderParse "duplicate-preamble.keiro" "language keiro-dsl 1\nlanguage keiro-dsl 1\ncontext source-fixture\n"),+    ("feature-nominal-binding-v1.txt", renderParse "feature-nominal-binding-v1.keiro" "context feature\nid OrderId prefix=ord using {}\n"),+    ("feature-integer-v1.txt", renderParse "feature-integer-v1.keiro" integerFeatureSource),+    ("feature-typed-expression-v1.txt", renderParse "feature-typed-expression-v1.keiro" typedExpressionFeatureSource),+    ("feature-explicit-implementation-v1.txt", renderParse "feature-explicit-implementation-v1.keiro" explicitImplementationFeatureSource),+    ("escaped-string.txt", renderParse "escaped-string.keiro" escapedStringSource),+    ("numeric-overflow.txt", renderParse "numeric-overflow.keiro" numericOverflowSource),+    ("duplicate-clause.txt", renderParse "duplicate-clause.keiro" duplicateClauseSource),+    ("expression-error.txt", renderParse "expression-error.keiro" expressionErrorSource),+    ("semantic-error.txt", renderSemantic "semantic-error.keiro" semanticErrorSource),+    ("workspace-member-parse-failure.txt", renderWorkspaceMemberFailure)+  ]++renderParse :: FilePath -> Text -> IO Text+renderParse path source = pure $ case parseSource path source of+  Left failure -> T.stripEnd (renderParseFailure failure) <> "\n"+  Right _ -> error ("diagnostic source unexpectedly parsed: " <> path)++renderSemantic :: FilePath -> Text -> IO Text+renderSemantic path source = pure $ case parseSource path source of+  Left failure -> error ("semantic diagnostic source failed to parse: " <> show failure)+  Right parsed -> case filter ((== Error) . severity) (validateService (checkedSource parsed)) of+    diagnostic : _ -> renderDiagnostic path diagnostic <> "\n"+    [] -> error ("semantic diagnostic source unexpectedly validated: " <> path)++renderWorkspaceMemberFailure :: IO Text+renderWorkspaceMemberFailure = do+  let path = "keiro-dsl/test/fixtures/workspace-member-parse-failed/service.keiro-workspace"+  resolved <- resolveRepoPath path+  loaded <- loadWorkspace (fileContentSource (takeDirectory resolved)) path+  pure $ case loaded of+    Left failure -> T.stripEnd (T.unlines (renderWorkspaceFailure path failure)) <> "\n"+    Right _ -> error "workspace-member-parse-failed unexpectedly composed"++integerFeatureSource :: Text+integerFeatureSource =+  T.unlines+    [ "context feature",+      "aggregate Counter",+      "  regs",+      "    value Integer = 0",+      "  states Open"+    ]++typedExpressionFeatureSource :: Text+typedExpressionFeatureSource =+  T.unlines+    [ "context feature",+      "aggregate Counter",+      "  regs",+      "    value Int = 0",+      "  states Open",+      "  command Tick { amount:Int }",+      "  Open -- Tick -->",+      "    guard reg.value == cmd.amount",+      "    goto Open"+    ]++explicitImplementationFeatureSource :: Text+explicitImplementationFeatureSource =+  T.unlines+    [ "context feature",+      "aggregate Counter",+      "  regs",+      "  states Open",+      "  command Tick { }",+      "  Open -- Tick -->",+      "    implementation hole",+      "    goto Open"+    ]++escapedStringSource :: Text+escapedStringSource =+  "context svc\n\ncontract c {\n  schemaVersion 1\n  discriminator kind\n  topic events \"bad\\q\"\n}\n"++numericOverflowSource :: Text+numericOverflowSource =+  T.unlines+    [ "context svc",+      "aggregate Thing",+      "  regs",+      "  states Open",+      "  event Changed v18446744073709551617 { }"+    ]++duplicateClauseSource :: Text+duplicateClauseSource =+  T.unlines+    [ "context svc",+      "aggregate Thing",+      "  regs",+      "  states Open Closed",+      "  command Move { }",+      "  Open -- Move -->",+      "    goto Closed",+      "    goto Open"+    ]++expressionErrorSource :: Text+expressionErrorSource =+  T.unlines+    [ "language keiro-dsl 2",+      "context svc",+      "aggregate Thing",+      "  regs",+      "    value Int = 0",+      "  states Open",+      "  command Move { amount:Int }",+      "  Open -- Move -->",+      "    guard cmd.amount +",+      "    goto Open"+    ]++semanticErrorSource :: Text+semanticErrorSource =+  T.unlines+    [ "context svc",+      "aggregate Thing",+      "  regs",+      "  states Open",+      "  Open -- MissingCommand -->",+      "    goto Open"+    ]
+ test/Keiro/Dsl/FrontendProfiles.hs view
@@ -0,0 +1,191 @@+{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE NoFieldSelectors #-}++module Keiro.Dsl.FrontendProfiles (frontendProfilesSpec) where++import Control.Monad (forM_)+import Data.List.NonEmpty qualified as NE+import Data.Text (Text)+import Data.Text qualified as T+import Keiro.Dsl.Frontend+import Keiro.Dsl.FrontendCompatibility (readRepoText)+import Keiro.Dsl.LanguageVersion+import Keiro.Dsl.Parser (parseSource)+import Keiro.Dsl.SemanticContract+import Keiro.Dsl.Source+import Keiro.Dsl.Syntax+import Test.Hspec+import Prelude hiding (span)++frontendProfilesSpec :: SpecWith ()+frontendProfilesSpec = do+  describe "released language profiles" $ do+    it "pins each released syntax profile, predecessor, and runtime contract explicitly" $ do+      map definitionRow (NE.toList languageRegistry)+        `shouldBe` [ (1, Nothing, "keiro-dsl/syntax-profile/1", "keiro-dsl/runtime-semantics/1"),+                     (2, Just 1, "keiro-dsl/syntax-profile/2", "keiro-dsl/runtime-semantics/1"),+                     (3, Just 2, "keiro-dsl/syntax-profile/2", "keiro-dsl/runtime-semantics/2")+                   ]+      forM_ allFeatures $ \feature -> do+        languageFeatureMinimumVersion feature `shouldBe` version 2+        languageSupportsFeature (version 1) feature `shouldBe` False+        languageSupportsFeature (version 2) feature `shouldBe` True+        languageSupportsFeature (version 3) feature `shouldBe` True++    it "does not infer a hypothetical successor profile or runtime contract" $ do+      lookupLanguageDefinition (version 4) `shouldBe` Nothing+      effectiveLanguageContractForVersion (version 4) `shouldBe` Nothing+      forM_ allFeatures $ \feature -> languageSupportsFeature (version 4) feature `shouldBe` False+      languageVersionPolicy <- readRepoText "keiro-dsl/src/Keiro/Dsl/LanguageVersion.hs"+      semanticPolicy <- readRepoText "keiro-dsl/src/Keiro/Dsl/SemanticContract.hs"+      languageVersionPolicy `shouldNotSatisfy` T.isInfixOf "version >="+      semanticPolicy `shouldNotSatisfy` T.isInfixOf "version >="++    it "checks every real feature marker against the exact selected profile" $ do+      forM_ featureCases $ \FeatureCase {feature, marker, body} ->+        forM_ [1, 2, 3] $ \versionNumber -> do+          let sourceName = "profile-" <> show versionNumber <> ".keiro"+              source = preamble versionNumber <> body+          case (versionNumber, parseSurfaceSource sourceName source) of+            (1, Left FrontendFailure {phase, code, span, supportedVersions}) -> do+              phase `shouldBe` BodyParsingPhase+              code `shouldBe` SourceLanguageError LanguageFeatureRequiresVersion+              spanText source span `shouldBe` marker+              supportedVersions `shouldBe` [version 2, version 3]+              languageSupportsFeature (version versionNumber) feature `shouldBe` False+            (1, result) -> expectationFailure ("expected v1 feature refusal, got " <> show result)+            (_, Right _) -> languageSupportsFeature (version versionNumber) feature `shouldBe` True+            (_, Left failure) -> expectationFailure (T.unpack (renderFrontendFailure failure))++    it "keeps feature spellings inert in comments, strings, wire keys, and identifiers" $ do+      inertBody <- readRepoText "keiro-dsl/test/fixtures/language-identifier-v1.keiro"+      forM_ [1, 2, 3] $ \versionNumber ->+        parseSurfaceSource ("inert-" <> show versionNumber <> ".keiro") (preamble versionNumber <> inertBody)+          `shouldSatisfy` isRight++  describe "frontend diagnostics" $ do+    it "classifies malformed and unsupported preambles at source selection with exact spans" $ do+      assertSourceSelection InvalidLanguageVersion "language keiro-dsl nope\ncontext malformed\n" "language keiro-dsl nope"+      assertSourceSelection UnsupportedLanguageVersion "language keiro-dsl 4\ncontext future\n" "language keiro-dsl 4"++    it "reports ordinary body syntax with expected items and a point span" $ do+      let source = "language keiro-dsl 1\ncontext body\nlayout\n"+      case parseSurfaceSource "body.keiro" source of+        Left FrontendFailure {phase, code, span = SourceSpan {start, end}, message, expected} -> do+          phase `shouldBe` BodyParsingPhase+          code `shouldBe` BodySyntaxError+          start `shouldBe` end+          message `shouldSatisfy` T.isInfixOf "unexpected end of input"+          expected `shouldSatisfy` (not . null)+        other -> expectationFailure ("expected structured body failure, got " <> show other)++    it "converts lowering refusals into the common phase/code/span model" $ do+      surface <- parseRight "owned.keiro" "context owned\n"+      let corrupted = case surface of+            SurfaceSource {source, language, preamble = sourcePreamble, spec = Located {span = SourceSpan {start, end}, value}} ->+              SurfaceSource+                { source,+                  language,+                  preamble = sourcePreamble,+                  spec = Located {span = SourceSpan {source = "other.keiro", start, end}, value}+                }+      case lowerSurfaceSource corrupted of+        Left lowering@LoweringFailure {span = loweringSpan} ->+          case frontendFailureFromLowering lowering of+            FrontendFailure {phase, code, span} -> do+              phase `shouldBe` LoweringPhase+              code `shouldBe` LoweringError SourceNameMismatch+              span `shouldBe` loweringSpan+        Right _ -> expectationFailure "expected lowering refusal"++    it "keeps the released compatibility renderer byte-identical" $ do+      forM_+        [ "language keiro-dsl nope\ncontext malformed\n",+          "language keiro-dsl 4\ncontext future\n",+          preamble 1 <> featureBody TypedAggregateExpressionSyntax,+          "language keiro-dsl 1\ncontext\n"+        ]+        $ \source -> case (parseSurfaceSource "compat.keiro" source, parseSource "compat.keiro" source) of+          (Left frontendFailure, Left compatibilityFailure) ->+            renderFrontendFailure frontendFailure `shouldBe` renderParseFailure compatibilityFailure+          other -> expectationFailure ("expected matching failures, got " <> show other)++data FeatureCase = FeatureCase+  { feature :: !LanguageFeature,+    marker :: !Text,+    body :: !Text+  }++featureCases :: [FeatureCase]+featureCases =+  [ FeatureCase NominalBindingSyntax "using" (featureBody NominalBindingSyntax),+    FeatureCase IntegerScalarSyntax "Integer" (featureBody IntegerScalarSyntax),+    FeatureCase TypedAggregateExpressionSyntax "cmd." (featureBody TypedAggregateExpressionSyntax),+    FeatureCase ExplicitTransitionImplementationSyntax "implementation hole" (featureBody ExplicitTransitionImplementationSyntax)+  ]++featureBody :: LanguageFeature -> Text+featureBody = \case+  NominalBindingSyntax -> T.unlines ["context profile", "id ProfileId prefix=profile using {}"]+  IntegerScalarSyntax -> T.unlines ["context profile", "aggregate Counter", "  regs", "    count Integer = 0", "  states Open"]+  TypedAggregateExpressionSyntax ->+    T.unlines+      [ "context profile",+        "aggregate Counter",+        "  regs",+        "    count Int = 0",+        "  states Open",+        "  command Add { amount:Int }",+        "  event Added = fields(Add)",+        "  Open -- Add --> guard cmd.amount == reg.count ; emit Added ; goto Open"+      ]+  ExplicitTransitionImplementationSyntax ->+    T.unlines+      [ "context profile",+        "aggregate Counter",+        "  regs",+        "  states Open",+        "  command Tick {}",+        "  event Ticked = fields(Tick)",+        "  Open -- Tick --> implementation hole ; emit Ticked ; goto Open"+      ]++allFeatures :: [LanguageFeature]+allFeatures = [NominalBindingSyntax, IntegerScalarSyntax, TypedAggregateExpressionSyntax, ExplicitTransitionImplementationSyntax]++preamble :: Int -> Text+preamble versionNumber = "language keiro-dsl " <> T.pack (show versionNumber) <> "\n"++version :: Int -> LanguageVersion+version number = maybe (error "invalid test language version") id (languageVersion (fromIntegral number))++definitionRow :: LanguageDefinition -> (Integer, Maybe Integer, Text, Text)+definitionRow definition =+  ( fromIntegral (languageVersionNumber (definitionVersion definition)),+    fromIntegral . languageVersionNumber <$> definitionPredecessor definition,+    syntaxProfileIdentifier (definitionSyntaxProfile definition),+    definitionRuntimeSemantics definition+  )++assertSourceSelection :: SourceLanguageErrorCode -> Text -> Text -> Expectation+assertSourceSelection expectedCode source expectedSpan =+  case parseSurfaceSource "selection.keiro" source of+    Left FrontendFailure {phase, code, span} -> do+      phase `shouldBe` SourceSelectionPhase+      code `shouldBe` SourceLanguageError expectedCode+      spanText source span `shouldBe` expectedSpan+    other -> expectationFailure ("expected structured source-selection failure, got " <> show other)++parseRight :: FilePath -> Text -> IO SurfaceSource+parseRight sourceName source =+  case parseSurfaceSource sourceName source of+    Left failure -> expectationFailure (T.unpack (renderFrontendFailure failure)) >> fail "unreachable"+    Right value -> pure value++spanText :: Text -> SourceSpan -> Text+spanText source SourceSpan {start = SourcePoint {offset = startOffset}, end = SourcePoint {offset = endOffset}} =+  T.take (endOffset - startOffset) (T.drop startOffset source)++isRight :: Either a b -> Bool+isRight (Right _) = True+isRight (Left _) = False
+ test/Keiro/Dsl/FrontendPublicApiProbe.hs view
@@ -0,0 +1,77 @@+-- | Compile-time evidence for the public parser and renderer surface released+-- by keiro-dsl-0.7.0.0. Later frontend refactors may change implementations,+-- but these assignments must keep compiling.+module Keiro.Dsl.FrontendPublicApiProbe+  ( apiProbe,+  )+where++import Data.Text (Text)+import Keiro.Dsl.Frontend (FrontendFailure, LoweringFailure, lowerSurfaceSource, parseSurfaceSource)+import Keiro.Dsl.Grammar (Name, Node, Placement, Spec, specContext, specLayout, specModuleRoot, specNodes)+import Keiro.Dsl.LanguageVersion (ParseFailure, ParsedSource, SourceLanguage, SourceLanguageDiagnostic, SourceLanguageErrorCode, parsedSourceLanguage, parsedSpec, sourceLanguageErrorCode)+import Keiro.Dsl.Parser (ParseError, parseSource, parseSpec, parseSpecText)+import Keiro.Dsl.PrettyPrint (renderSource, renderSpec)+import Keiro.Dsl.Syntax (SurfaceSource)++parseSurfaceSourceProbe :: FilePath -> Text -> Either FrontendFailure SurfaceSource+parseSurfaceSourceProbe = parseSurfaceSource++lowerSurfaceSourceProbe :: SurfaceSource -> Either LoweringFailure ParsedSource+lowerSurfaceSourceProbe = lowerSurfaceSource++parseSourceProbe :: FilePath -> Text -> Either ParseFailure ParsedSource+parseSourceProbe = parseSource++parseSpecProbe :: FilePath -> Text -> Either ParseError Spec+parseSpecProbe = parseSpec++parseSpecTextProbe :: Text -> Either ParseError Spec+parseSpecTextProbe = parseSpecText++renderSourceProbe :: ParsedSource -> Text+renderSourceProbe = renderSource++renderSpecProbe :: Spec -> Text+renderSpecProbe = renderSpec++parsedSourceLanguageProbe :: ParsedSource -> SourceLanguage+parsedSourceLanguageProbe = parsedSourceLanguage++parsedSpecProbe :: ParsedSource -> Spec+parsedSpecProbe = parsedSpec++sourceLanguageErrorCodeProbe :: SourceLanguageDiagnostic -> SourceLanguageErrorCode+sourceLanguageErrorCodeProbe = sourceLanguageErrorCode++specContextProbe :: Spec -> Name+specContextProbe = specContext++specModuleRootProbe :: Spec -> Maybe Text+specModuleRootProbe = specModuleRoot++specLayoutProbe :: Spec -> Maybe Placement+specLayoutProbe = specLayout++specNodesProbe :: Spec -> [Node]+specNodesProbe = specNodes++-- | Referencing every assignment keeps @-Wall@ useful and makes the module a+-- real compile probe instead of passive documentation.+apiProbe :: ()+apiProbe =+  parseSurfaceSourceProbe `seq`+    lowerSurfaceSourceProbe `seq`+      parseSourceProbe `seq`+        parseSpecProbe `seq`+          parseSpecTextProbe `seq`+            renderSourceProbe `seq`+              renderSpecProbe `seq`+                parsedSourceLanguageProbe `seq`+                  parsedSpecProbe `seq`+                    sourceLanguageErrorCodeProbe `seq`+                      specContextProbe `seq`+                        specModuleRootProbe `seq`+                          specLayoutProbe `seq`+                            specNodesProbe `seq`+                              ()
+ test/Keiro/Dsl/FrontendSurface.hs view
@@ -0,0 +1,264 @@+{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE NoFieldSelectors #-}++module Keiro.Dsl.FrontendSurface (frontendSurfaceSpec) where++import Control.Monad (forM_)+import Data.Text (Text)+import Data.Text qualified as T+import Keiro.Dsl.Frontend+import Keiro.Dsl.FrontendCompatibility+  ( CompatibilityManifest (..),+    SourceExpectation (..),+    readCompatibilityManifest,+    readRepoText,+  )+import Keiro.Dsl.Grammar+import Keiro.Dsl.LanguageVersion+import Keiro.Dsl.Parser (parseSource)+import Keiro.Dsl.Source+import Keiro.Dsl.Syntax+import Test.Hspec hiding (Spec)+import Prelude hiding (span)++frontendSurfaceSpec :: SpecWith ()+frontendSurfaceSpec = do+  describe "source spans" $ do+    it "rejects invalid points and backwards spans" $ do+      mkSourcePoint (-1) 1 1 `shouldBe` Nothing+      mkSourcePoint 0 0 1 `shouldBe` Nothing+      mkSourcePoint 0 1 0 `shouldBe` Nothing+      let start = SourcePoint {offset = 3, line = 2, column = 2}+          end = SourcePoint {offset = 2, line = 2, column = 1}+      mkSourceSpan "invalid.keiro" start end `shouldBe` Nothing++    it "locates Unicode-prefixed syntax and excludes trailing comments" $ do+      let leading = "# λ🙂\n"+          preambleText = "language keiro-dsl 2"+          contextText = "context spans"+          idText = "id ThingId prefix=thing"+          source = leading <> preambleText <> "\n" <> contextText <> "\n" <> idText <> "   # trailing\n\n"+      parsed <- parseSurfaceRight "unicode.keiro" source+      case parsed of+        SurfaceSource+          { preamble = Just (Located {span = preambleSpan}),+            spec =+              Located+                { span = bodySpan,+                  value =+                    SurfaceSpec+                      { context = Located {span = contextSpan},+                        items = [Located {span = idSpan, value = SurfaceId _}]+                      }+                }+          } -> do+            spanPoints preambleSpan `shouldBe` (pointAfter leading, pointAfter (leading <> preambleText))+            spanPoints contextSpan+              `shouldBe` ( pointAfter (leading <> preambleText <> "\n"),+                           pointAfter (leading <> preambleText <> "\n" <> contextText)+                         )+            spanPoints idSpan+              `shouldBe` ( pointAfter (leading <> preambleText <> "\n" <> contextText <> "\n"),+                           pointAfter (leading <> preambleText <> "\n" <> contextText <> "\n" <> idText)+                         )+            spanPoints bodySpan `shouldBe` (fst (spanPoints contextSpan), snd (spanPoints idSpan))+        other -> expectationFailure ("unexpected surface shape: " <> show other)++    it "covers a complete multi-line aggregate and an empty document body" $ do+      aggregateSource <- readRepoText "keiro-dsl/test/fixtures/aggregate-scalars.keiro"+      aggregateSurface <- parseSurfaceRight "aggregate-scalars.keiro" aggregateSource+      let aggregateStart = T.length (fst (T.breakOn "aggregate ScalarLedger" aggregateSource))+          aggregateEnd = T.length (T.stripEnd aggregateSource)+      case aggregateSurface of+        SurfaceSource+          { spec =+              Located+                { value =+                    SurfaceSpec+                      { items = [Located {span = aggregateSpan, value = SurfaceNode (NAggregate _)}],+                        elements+                      }+                }+          } -> do+            spanOffsets aggregateSpan `shouldBe` (aggregateStart, aggregateEnd)+            let fields = [(name, spanText aggregateSource fieldSpan) | Located {span = fieldSpan, value = SurfaceField name} <- elements]+                expressions = [spanText aggregateSource expressionSpan | Located {span = expressionSpan, value = SurfaceExpression _} <- elements]+            fields `shouldContain` [("observedAt", "observedAt:Time")]+            expressions `shouldContain` ["observedAt >= observedAt && revision >= revision"]+        other -> expectationFailure ("unexpected aggregate surface shape: " <> show other)+      emptySurface <- parseSurfaceRight "empty.keiro" "context empty   # trailing\n"+      case emptySurface of+        SurfaceSource+          { spec =+              Located+                { span = bodySpan,+                  value = SurfaceSpec {context = Located {span = contextSpan}, items = []}+                }+          } -> bodySpan `shouldBe` contextSpan+        other -> expectationFailure ("unexpected empty surface shape: " <> show other)++  describe "surface lowering" $ do+    it "preserves top-level source order before grouping the semantic graph" $ do+      let source = T.unlines ["context ordering", "id FirstId prefix=first", "enum Mode { On=on Off=off }", "id SecondId prefix=second"]+      surface <- parseSurfaceRight "ordering.keiro" source+      case surface of+        SurfaceSource {spec = Located {value = SurfaceSpec {items}}} ->+          map topItemKind items `shouldBe` ["id", "enum", "id"]+      lowered <- lowerRight surface+      map idName (specIds (parsedSpec lowered)) `shouldBe` ["FirstId", "SecondId"]+      map enumName (specEnums (parsedSpec lowered)) `shouldBe` ["Mode"]++    it "refuses surface evidence attributed to another source" $ do+      surface <- parseSurfaceRight "owned.keiro" "context owned\n"+      let corrupted = case surface of+            SurfaceSource {source, language, preamble, spec = Located {span = SourceSpan {start, end}, value}} ->+              SurfaceSource+                { source,+                  language,+                  preamble,+                  spec = Located {span = SourceSpan {source = "other.keiro", start, end}, value}+                }+      case lowerSurfaceSource corrupted of+        Left LoweringFailure {code = SourceNameMismatch} -> pure ()+        other -> expectationFailure ("expected source ownership failure, got " <> show other)++    it "lowers every accepted 0.7 fixture to the compatibility result" $ do+      manifest <- readCompatibilityManifest+      forM_ [row | row <- manifestSources manifest, sourceResult row == "accept"] $ \row -> do+        source <- readRepoText (sourcePath row)+        surface <- parseSurfaceRight (sourcePath row) source+        lowered <- lowerRight surface+        parseSource (sourcePath row) source `shouldBe` Right lowered++  describe "parser module boundaries" $ do+    it "keeps the public parser facade free of Megaparsec and grammar productions" $ do+      facade <- readRepoText "keiro-dsl/src/Keiro/Dsl/Parser.hs"+      facade `shouldNotSatisfy` T.isInfixOf "Text.Megaparsec"+      facade `shouldNotSatisfy` T.isInfixOf "keyword ::"+      facade `shouldNotSatisfy` T.isInfixOf "pAggregate ::"+      facade `shouldNotSatisfy` T.isInfixOf "    parseSurfaceSource,"++    it "keeps internal grammar modules from importing the public facade" $+      forM_ parserModulePaths $ \path -> do+        source <- readRepoText path+        filter isFacadeImport (T.lines source) `shouldBe` []++    it "keeps source, syntax, and semantic graph modules Megaparsec-free" $+      forM_ semanticModulePaths $ \path -> do+        source <- readRepoText path+        source `shouldNotSatisfy` T.isInfixOf "Text.Megaparsec"++    it "keeps semantic consumers independent of surface syntax and internal parsers" $+      forM_ semanticConsumerPaths $ \path -> do+        source <- readRepoText path+        source `shouldNotSatisfy` T.isInfixOf "import Keiro.Dsl.Syntax"+        source `shouldNotSatisfy` T.isInfixOf "import Keiro.Dsl.Parser."++    it "keeps workspace loading and parser internals on opposite sides of the facade" $ do+      workspace <- readRepoText "keiro-dsl/src/Keiro/Dsl/Workspace.hs"+      workspace `shouldNotSatisfy` T.isInfixOf "import Keiro.Dsl.Parser."+      forM_ parserModulePaths $ \path -> do+        source <- readRepoText path+        forM_ forbiddenParserDependencies $ \dependency ->+          source `shouldNotSatisfy` T.isInfixOf ("import Keiro.Dsl." <> dependency)++parseSurfaceRight :: FilePath -> Text -> IO SurfaceSource+parseSurfaceRight sourceName source =+  case parseSurfaceSource sourceName source of+    Left failure -> expectationFailure (T.unpack (renderFrontendFailure failure)) >> fail "unreachable"+    Right value -> pure value++lowerRight :: SurfaceSource -> IO ParsedSource+lowerRight source =+  case lowerSurfaceSource source of+    Left failure -> expectationFailure (T.unpack (renderLoweringFailure failure)) >> fail "unreachable"+    Right value -> pure value++spanOffsets :: SourceSpan -> (Int, Int)+spanOffsets SourceSpan {start = SourcePoint {offset = startOffset}, end = SourcePoint {offset = endOffset}} =+  (startOffset, endOffset)++spanPoints :: SourceSpan -> ((Int, Int, Int), (Int, Int, Int))+spanPoints SourceSpan {start, end} = (pointTuple start, pointTuple end)++pointTuple :: SourcePoint -> (Int, Int, Int)+pointTuple SourcePoint {offset, line, column} = (offset, line, column)++pointAfter :: Text -> (Int, Int, Int)+pointAfter prefix =+  ( T.length prefix,+    length linesFound,+    T.length (last linesFound) + 1+  )+  where+    linesFound = T.splitOn "\n" prefix++topItemKind :: Located SurfaceTopItem -> Text+topItemKind Located {value} = case value of+  SurfaceId _ -> "id"+  SurfaceEnum _ -> "enum"+  SurfaceRule _ -> "rule"+  SurfaceNominalScalar _ -> "nominal"+  SurfaceMapped _ -> "mapped"+  SurfaceNode _ -> "node"++spanText :: Text -> SourceSpan -> Text+spanText source SourceSpan {start = SourcePoint {offset = startOffset}, end = SourcePoint {offset = endOffset}} =+  T.take (endOffset - startOffset) (T.drop startOffset source)++isFacadeImport :: Text -> Bool+isFacadeImport line =+  let stripped = T.strip line+   in stripped == "import Keiro.Dsl.Parser"+        || "import Keiro.Dsl.Parser " `T.isPrefixOf` stripped++parserModulePaths :: [FilePath]+parserModulePaths =+  map+    ("keiro-dsl/src/Keiro/Dsl/Parser/" <>)+    [ "Aggregate.hs",+      "Coordination.hs",+      "Core.hs",+      "Declaration.hs",+      "Document.hs",+      "Expression.hs",+      "Integration.hs",+      "Mapped.hs",+      "Preamble.hs",+      "Queue.hs",+      "ReadModel.hs",+      "Workflow.hs"+    ]++semanticModulePaths :: [FilePath]+semanticModulePaths =+  map+    ("keiro-dsl/src/Keiro/Dsl/" <>)+    [ "Source.hs",+      "Syntax.hs",+      "Grammar.hs",+      "Validate.hs"+    ]++semanticConsumerPaths :: [FilePath]+semanticConsumerPaths =+  map+    ("keiro-dsl/src/Keiro/Dsl/" <>)+    [ "Grammar.hs",+      "Validate.hs",+      "SemanticContract.hs",+      "Scaffold.hs",+      "Diff.hs",+      "FoldFingerprint.hs",+      "ReplayImpact.hs"+    ]++forbiddenParserDependencies :: [Text]+forbiddenParserDependencies =+  [ "Workspace",+    "Validate",+    "Scaffold",+    "Diff",+    "FoldFingerprint",+    "ReplayImpact"+  ]
test/Main.hs view
@@ -32,6 +32,9 @@ import Keiro.Dsl.ExplainBindings (BindingHole (..), BindingObligation (..), BindingObligationKind (..), bindingHoles, bindingObligations, bindingObligationsForService, renderBindingObligations) import Keiro.Dsl.Expression import Keiro.Dsl.FoldFingerprint (aggregateFoldFingerprint, aggregateFoldFingerprintForService, aggregateFoldSurface, aggregateFoldSurfaceForService)+import Keiro.Dsl.FrontendCompatibility (frontendCompatibilitySpec)+import Keiro.Dsl.FrontendProfiles (frontendProfilesSpec)+import Keiro.Dsl.FrontendSurface (frontendSurfaceSpec) import Keiro.Dsl.Goldens (GoldenEvidence (..), GoldenPayload (..), emitGoldenPayloads, goldenRelativePath, goldensForDiff) import Keiro.Dsl.Grammar import Keiro.Dsl.Harness (harnessFor, harnessForWithGoldens, harnessReadModel, harnessRouter, harnessWorkflow)@@ -68,6 +71,10 @@  main :: IO () main = hspec $ do+  frontendCompatibilitySpec+  frontendSurfaceSpec+  frontendProfilesSpec+   describe "source language version" $ do     let legacy = "context hospital-capacity\n"         declared = "# leading comment\n\nlanguage keiro-dsl 1\ncontext hospital-capacity\n"