packages feed

keiro-dsl-0.8.0.0: test/Keiro/Dsl/FrontendProfiles.hs

{-# 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