keiro-dsl-0.15.0.0: src/Keiro/Dsl/Parser/Preamble.hs
-- | Released-language preamble recognition and source selection.
module Keiro.Dsl.Parser.Preamble
( contextualDiagnostic,
pDeclaredPreamble,
selectSourceLanguage,
)
where
import Data.Char (isAscii, isDigit)
import Data.List.NonEmpty qualified as NE
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
{ errorCode = code,
source = src,
loc = Loc line,
token = case code of
LanguageFeatureRequiresVersion -> Just (languageVersionText effectiveVersion)
_ -> Nothing,
declaredVersion = case code of
LanguageFeatureRequiresVersion -> Just effectiveVersion
_ -> Nothing,
supportedVersions = case (.feature) contextual of
Nothing -> supportedLanguageVersions
Just feature -> case NE.nonEmpty (languageVersionsSupportingFeature feature) of
Just versions -> versions
Nothing -> supportedLanguageVersions
}
where
code = (.code) contextual
SourceSpan {start = SourcePoint {line}} = (.span) 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
{ span :: !SourceSpan,
text :: !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 ((.span) languageClause))))
where
sourceFailure code line tokenText declared =
frontendFailureFromSourceDiagnostic
SourceSelectionPhase
((.span) line)
Nothing
SourceLanguageDiagnostic
{ errorCode = code,
source = src,
loc = Loc (startLine ((.span) line)),
token = tokenText,
declaredVersion = declared,
supportedVersions = supportedLanguageVersions
}
parsePreamble line = case T.words ((.text) 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 {span = spanOf locatedLine, text = content}