keiro-dsl-0.8.0.0: src/Keiro/Dsl/Frontend/Internal.hs
{-# 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