packages feed

keiro-dsl-0.15.0.0: internal/Keiro/Dsl/GeneratedHaskellLanguage.hs

-- | The Haskell language contract for overwriteable generated modules.
--
-- The manifest and conformance build profile publish the shared baseline.
-- Syntax outside that baseline must be requested through the closed extension
-- type and rendered as a module-local pragma.
module Keiro.Dsl.GeneratedHaskellLanguage
  ( GeneratedHaskellExtension (..),
    generatedHaskellDefaultLanguage,
    generatedHaskellDefaultExtensions,
    idiomaticV2LabelMigrations,
    modernizeGeneratedHaskellSource,
    modernizeGeneratedHaskellSourceWithState,
    RewriteState (..),
    renderGeneratedLanguagePragmas,
  )
where

import Data.Char (isAlphaNum)
import Data.List (nub, sort)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T

data GeneratedHaskellExtension
  = ExtBlockArguments
  | ExtDeriveAnyClass
  | ExtDuplicateRecordFields
  | ExtOverloadedLabels
  | ExtOverloadedRecordDot
  | ExtQualifiedDo
  | ExtTemplateHaskell
  | ExtTypeFamilies
  deriving (Eq, Ord, Show)

generatedHaskellDefaultLanguage :: Text
generatedHaskellDefaultLanguage = "GHC2024"

generatedHaskellDefaultExtensions :: [Text]
generatedHaskellDefaultExtensions =
  [ "DuplicateRecordFields",
    "NoFieldSelectors",
    "OverloadedRecordDot",
    "OverloadedStrings"
  ]

renderGeneratedLanguagePragmas :: [GeneratedHaskellExtension] -> [Text]
renderGeneratedLanguagePragmas =
  map renderPragma
    . filter (`notElem` generatedHaskellDefaultExtensions)
    . sort
    . nub
    . map extensionName
  where
    renderPragma name = "{-# LANGUAGE " <> name <> " #-}"

extensionName :: GeneratedHaskellExtension -> Text
extensionName extension = case extension of
  ExtBlockArguments -> "BlockArguments"
  ExtDeriveAnyClass -> "DeriveAnyClass"
  ExtDuplicateRecordFields -> "DuplicateRecordFields"
  ExtOverloadedLabels -> "OverloadedLabels"
  ExtOverloadedRecordDot -> "OverloadedRecordDot"
  ExtQualifiedDo -> "QualifiedDo"
  ExtTemplateHaskell -> "TemplateHaskell"
  ExtTypeFamilies -> "TypeFamilies"

-- | Apply the idiomatic-v2 product-label mapping to Haskell identifiers while
-- preserving comments and literals byte for byte. The emitter templates still
-- use the released idiomatic-v1 labels as their semantic vocabulary; keeping
-- the presentation rewrite here makes the edition boundary complete and keeps
-- wire strings, diagnostics, and generated comments outside that boundary.
modernizeGeneratedHaskellSource :: Text -> Text
modernizeGeneratedHaskellSource = fst . modernizeGeneratedHaskellSourceWithState

modernizeGeneratedHaskellSourceWithState :: Text -> (Text, RewriteState)
modernizeGeneratedHaskellSourceWithState source =
  let (output, finalState) = go Code (T.unpack source)
   in (T.pack output, finalState)
  where
    go Code [] = ([], Code)
    go LineComment [] = ([], Code)
    go state [] = ([], state)
    go Code input@('-' : '-' : _) =
      let (dashes, remaining) = span (== '-') input
          startsComment = case remaining of
            [] -> True
            character : _ -> not (isHaskellSymbol character)
       in emit dashes (go (if startsComment then LineComment else Code) remaining)
    go Code ('{' : '-' : rest) = emit "{-" (go (BlockComment 1) rest)
    go Code ('"' : rest) = emit "\"" (go StringLiteral rest)
    go Code ('\'' : rest)
      | startsCharacterLiteral rest = emit "'" (go CharacterLiteral rest)
      | otherwise = emit "'" (go Code rest)
    go Code sourceText@(character : rest)
      | identifierStart character =
          let (token, remaining) = span identifierCharacter sourceText
              replacement = Map.findWithDefault (T.pack token) (T.pack token) idiomaticV2Labels
           in emit (T.unpack replacement) (go Code remaining)
      | otherwise = emit [character] (go Code rest)
    go LineComment ('\n' : rest) = emit "\n" (go Code rest)
    go LineComment (character : rest) = emit [character] (go LineComment rest)
    go (BlockComment depth) ('{' : '-' : rest) = emit "{-" (go (BlockComment (depth + 1)) rest)
    go (BlockComment 1) ('-' : '}' : rest) = emit "-}" (go Code rest)
    go (BlockComment depth) ('-' : '}' : rest) = emit "-}" (go (BlockComment (depth - 1)) rest)
    go (BlockComment depth) (character : rest) = emit [character] (go (BlockComment depth) rest)
    go StringLiteral ('\\' : escaped : rest) = emit ['\\', escaped] (go StringLiteral rest)
    go StringLiteral ('"' : rest) = emit "\"" (go Code rest)
    go StringLiteral (character : rest) = emit [character] (go StringLiteral rest)
    go CharacterLiteral ('\\' : escaped : rest) = emit ['\\', escaped] (go CharacterLiteral rest)
    go CharacterLiteral ('\'' : rest) = emit "'" (go Code rest)
    go CharacterLiteral (character : rest) = emit [character] (go CharacterLiteral rest)

    emit prefix (output, state) = (prefix <> output, state)
    startsCharacterLiteral ('\\' : _) = True
    startsCharacterLiteral (_ : '\'' : _) = True
    startsCharacterLiteral _ = False
    isHaskellSymbol character = character `elem` ("!#$%&*+./<=>?@\\^|-~:" :: String)
    identifierStart character = character == '_' || character >= 'A' && character <= 'Z' || character >= 'a' && character <= 'z'
    identifierCharacter character = identifierStart character || isAlphaNum character || character == '\''

data RewriteState
  = Code
  | LineComment
  | BlockComment !Int
  | StringLiteral
  | CharacterLiteral
  deriving stock (Eq, Show)

idiomaticV2Labels :: Map.Map Text Text
idiomaticV2Labels = Map.fromList idiomaticV2LabelMigrations

idiomaticV2LabelMigrations :: [(Text, Text)]
idiomaticV2LabelMigrations =
  [ ("failureCode", "code"),
    ("failureDetail", "detail"),
    ("failureKey", "key"),
    ("failureSubject", "subject"),
    ("inboxFailureAttempt", "attempt"),
    ("inboxFailureReason", "reason"),
    ("reportDuplicate", "duplicate"),
    ("reportFailed", "failed"),
    ("reportFilled", "filled"),
    ("reportMissing", "missing"),
    ("reportPending", "pending"),
    ("reportRequired", "required"),
    ("reportStale", "stale"),
    ("reportUnverified", "unverified"),
    ("reportVerified", "verified"),
    ("requirementCommandName", "commandName"),
    ("requirementEventKinds", "eventKinds"),
    ("requirementEvidence", "evidence"),
    ("requirementExpectedEdge", "expectedEdge"),
    ("requirementGuardCoverage", "guardCoverage"),
    ("requirementKey", "key"),
    ("requirementKind", "kind"),
    ("requirementLine", "line"),
    ("requirementSource", "source"),
    ("requirementTarget", "target"),
    ("sourceColumn", "column"),
    ("sourceFile", "file"),
    ("sourceLine", "line"),
    ("witnessCommand", "command"),
    ("witnessExpected", "expected"),
    ("witnessHistory", "history"),
    ("witnessHistoryPrefix", "historyPrefix"),
    ("witnessKey", "key"),
    ("witnessObservedChunk", "observedChunk"),
    ("workflowFactAwaitLabels", "awaitLabels"),
    ("workflowFactBody", "body"),
    ("workflowFactIdField", "idField"),
    ("workflowFactIdVia", "idVia"),
    ("workflowFactName", "name"),
    ("workflowFactPatchIds", "patchIds")
  ]