packages feed

tadka-2.0.0.0: test/props/Phase13.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE CPP #-}

-- | Every constructor below needs its own source/span/message shape, so at
-- least one field per constructor (the payload distinguishing that variant)
-- is inherently partial: GHC's auto-generated accessor for e.g. 'tmExpected'
-- can only be total if every constructor has a 'tmExpected' field of the
-- same type, which isn't true of a genuinely heterogeneous sum type by
-- definition. Shared fields ('crSrc', 'crAt') are named and typed
-- identically across all three constructors specifically to avoid this
-- where the shape genuinely allows it; the rest ('peId', 'tmExpected',
-- 'tmActual', 'uvName', 'uvPrior', 'uvRelated', 'uvCause', 'uvId') cannot be
-- shared without losing the type-level distinction the test exists to
-- exercise, so the warning is suppressed here rather than upstream —
-- any real 'deriveDiagnosticSum' consumer with a similarly heterogeneous
-- sum type hits the same tradeoff and makes the same call locally.
{-# OPTIONS_GHC -Wno-partial-fields #-}
#if __GLASGOW_HASKELL__ >= 910
{-# OPTIONS_GHC -Wno-incomplete-record-selectors #-}
#endif


module Phase13 (group) where

import qualified Data.Aeson                 as A
import           Data.Text                  (Text)
import qualified Data.Text.Lazy             as TL
import qualified Data.Text.Lazy.Encoding    as TLE
import           Prettyprinter              (LayoutOptions (..), PageWidth (Unbounded),
                                             layoutPretty, pretty)
import           Prettyprinter.Render.Text  (renderStrict)

import           Hedgehog                   (Group (..), Property, property, withTests, (===))

import           Tadka
import           Tadka.Internal             (buildContext, buildContextWith)

rightOrErr :: Show a => Either a b -> b
rightOrErr = either (error . show) id

-- === Derived via deriveDiagnosticSum =======================================

data CompileError
  = ParseFailure { crSrc :: NamedSource, crAt :: Span, peId :: DiagnosticId }
  | TypeMismatch { crSrc :: NamedSource, crAt :: Span, tmExpected :: Text, tmActual :: Text }
  | UndefinedVar { crSrc :: NamedSource, crAt :: Span, uvName :: Text, uvPrior :: [Span]
                 , uvRelated :: [SomeDiagnostic], uvCause :: Maybe SomeDiagnostic, uvId :: Text }

deriveDiagnosticSum
  [ ( 'ParseFailure
    , defaultSpec
        { specCode        = Just "tadka::E0101"
        , specSourceField = Just 'crSrc
        , specLabelFields = [('crAt, "here")]
        , specId          = Just 'peId
        , specMessage     = Just [| \_ -> pretty ("unexpected token" :: Text) |]
        }
    )
  , ( 'TypeMismatch
    , defaultSpec
        { specSeverity    = SevWarning
        , specHelp        = Just "check the type annotation"
        , specSourceField = Just 'crSrc
        , specLabelFields = [('crAt, "here")]
        , specMessage     = Just [| \e -> pretty ("type mismatch: expected " <> tmExpected e
                                                     <> ", got " <> tmActual e) |]
        }
    )
  , ( 'UndefinedVar
    , defaultSpec
        { specSourceField                    = Just 'crSrc
        , specLabelFields                    = [('crAt, "used here")]
        , specSecondaryLabelCollectionFields  = [('uvPrior, "shadowed here")]
        , specRelated                        = Just 'uvRelated
        , specCause                          = Just 'uvCause
        , specId                             = Just 'uvId
        , specMessage                        = Just [| \e -> pretty ("undefined variable " <> uvName e) |]
        }
    )
  ]
  ''CompileError

-- === Hand-written twin: identical fields, identical bodies =================

data CompileErrorManual
  = ParseFailureM { crmSrc :: NamedSource, crmAt :: Span, pemId :: DiagnosticId }
  | TypeMismatchM { crmSrc :: NamedSource, crmAt :: Span, tmmExpected :: Text, tmmActual :: Text }
  | UndefinedVarM { crmSrc :: NamedSource, crmAt :: Span, uvmName :: Text, uvmPrior :: [Span]
                  , uvmRelated :: [SomeDiagnostic], uvmCause :: Maybe SomeDiagnostic, uvmId :: Text }

instance Diagnostic CompileErrorManual where
  message ParseFailureM{}                        = pretty ("unexpected token" :: Text)
  message (TypeMismatchM { tmmExpected = ex, tmmActual = ac }) =
    pretty ("type mismatch: expected " <> ex <> ", got " <> ac)
  message (UndefinedVarM { uvmName = nm })       = pretty ("undefined variable " <> nm)

  code (ParseFailureM {}) = Just (rightOrErr (mkDiagnosticCode "tadka::E0101"))
  code _                  = Nothing

  severity (TypeMismatchM {}) = SevWarning
  severity _                  = SevError

  help (TypeMismatchM {}) = Just (pretty ("check the type annotation" :: Text))
  help _                  = Nothing

  context (ParseFailureM { crmSrc = s, crmAt = a }) = buildContext s [(a, Just "here")]
  context (TypeMismatchM { crmSrc = s, crmAt = a }) = buildContext s [(a, Just "here")]
  context (UndefinedVarM { crmSrc = s, crmAt = a, uvmPrior = prior }) =
    buildContextWith s
      (  [ (a, Primary, Just "used here") ]
      ++ [ (p, Secondary, Just "shadowed here") | p <- prior ] )

  related (UndefinedVarM { uvmRelated = r }) = r
  related _                                  = []

  diagnosticCause (UndefinedVarM { uvmCause = c }) = c
  diagnosticCause _                                = Nothing

  diagnosticId (ParseFailureM { pemId = i }) = Just i
  diagnosticId (UndefinedVarM { uvmId = i })  = Just (mkDiagnosticId i)
  diagnosticId _                              = Nothing

-- === Fixtures ===============================================================

srcV :: NamedSource
srcV = rightOrErr (mkNamedSource "f.hs" "let x = 1 in y")

spanX, spanY :: Span
spanX = rightOrErr (mkSpan 4 1)    -- "x"
spanY = rightOrErr (mkSpan 14 1)   -- "y"

priorD :: CompileError
priorD = ParseFailure srcV spanX (mkDiagnosticId "prior")

priorM :: CompileErrorManual
priorM = ParseFailureM srcV spanX (mkDiagnosticId "prior")

pfD :: CompileError
pfD = ParseFailure srcV spanX (mkDiagnosticId "pf-1")

pfM :: CompileErrorManual
pfM = ParseFailureM srcV spanX (mkDiagnosticId "pf-1")

tmD :: CompileError
tmD = TypeMismatch srcV spanX "Int" "Bool"

tmM :: CompileErrorManual
tmM = TypeMismatchM srcV spanX "Int" "Bool"

uvD :: CompileError
uvD = UndefinedVar srcV spanY "y" [spanX] [SomeDiagnostic priorD] (Just (SomeDiagnostic priorD)) "uv-1"

uvM :: CompileErrorManual
uvM = UndefinedVarM srcV spanY "y" [spanX] [SomeDiagnostic priorM] (Just (SomeDiagnostic priorM)) "uv-1"

-- === Renderers (identical to Phase 8's) =====================================

gfx :: Diagnostic e => e -> Text
gfx e = case selectRenderer (withColorMode ColorNever (withUnicodeMode UnicodeAlways (withTarget TGraphical defaultConfig))) of
  SomeRenderer r@(Graphical _) -> renderStrict (layoutPretty (LayoutOptions Unbounded) (render r e))
  _                            -> ""

nar :: Diagnostic e => e -> Text
nar e = case selectRenderer (withTarget TNarratable defaultConfig) of
  SomeRenderer r@(Narratable _) -> render r e
  _                             -> ""

jsn :: Diagnostic e => e -> Text
jsn e = case selectRenderer (withTarget TJson defaultConfig) of
  SomeRenderer r@(Json _) -> TL.toStrict (TLE.decodeUtf8 (A.encode (render r e)))
  _                       -> ""

-- === Properties =============================================================

prop_parseFailure :: Property
prop_parseFailure = withTests 1 . property $ do
  gfx pfD === gfx pfM
  nar pfD === nar pfM
  jsn pfD === jsn pfM

prop_typeMismatch :: Property
prop_typeMismatch = withTests 1 . property $ do
  gfx tmD === gfx tmM
  nar tmD === nar tmM
  jsn tmD === jsn tmM

prop_undefinedVar :: Property
prop_undefinedVar = withTests 1 . property $ do
  gfx uvD === gfx uvM
  nar uvD === nar uvM
  jsn uvD === jsn uvM

group :: Group
group = Group "Phase 13 - deriveDiagnosticSum (derived == manual, per constructor)"
  [ ("ParseFailure: full property", prop_parseFailure)
  , ("TypeMismatch: full property", prop_typeMismatch)
  , ("UndefinedVar (secondary collection + related + cause): full property", prop_undefinedVar)
  ]