descript-lang-0.2.0.0: src/Descript/BasicInj/Process/Validate.hs
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE TypeFamilies #-}
-- | Checks that Descript source is well-formed before it's interpreted.
-- If the source isn't well-formed, generates user-friendly problems.
module Descript.BasicInj.Process.Validate
( validateForRefactor
, validate
, validate_
, validate'
, validateForRefactorIn
, validateIn
, validateIn_
, validateIn'
) where
import qualified Descript.BasicInj.Traverse.Term as T
import Descript.BasicInj.Traverse
import qualified Descript.BasicInj.Data.Value.In as In
import qualified Descript.BasicInj.Data.Value.Out as Out
import qualified Descript.BasicInj.Data.Type as RecordType (head)
import Descript.BasicInj.Data
import qualified Descript.Misc.Build.Process.Validate.Term as Term
import Descript.Misc
import Data.Semigroup as S
import Data.Monoid as M
import Data.List
import Core.Data.List
import Core.Data.List.Assoc
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import Core.Control.Monad.Trans
import Prelude hiding (head, mod)
newtype ValidateAll an = ValidateAll (AModule ())
newtype ValidateRecords an = ValidateRecords (RecordCtx ())
newtype ValidatePropPaths an = ValidatePropPaths (In.Value ())
instance (Semigroup an) => Fold (ValidateAll an) where
type Res (ValidateAll an) = [Problem an]
type FAnn (ValidateAll an) = an
fonTerm T.Program (ValidateAll extra) prog
= foldTerm T.Query (ValidateRecords recordCtx') (query prog)
where recordCtx' = recordCtx_ M.<> recordCtx extra
recordCtx_ = remAnns $ recordCtx $ amodule $ module' prog
fonTerm T.AModule (ValidateAll extra) mod
= foldTerm T.ReduceCtx (ValidateRecords recordCtx') (reduceCtx mod)
where recordCtx' = recordCtx_ M.<> recordCtx extra
recordCtx_ = remAnns $ recordCtx mod
fonTerm T.ImportCtx (ValidateAll _) (ImportCtx _ _ idecls)
= validateImportDeclsNoDups idecls
fonTerm T.RecordCtx (ValidateAll extra) (RecordCtx _ decls)
= validateRecordDeclsNoDups extraDecls decls
where extraDecls = recordCtxDecls $ recordCtx extra
fonTerm T.Reducer (ValidateAll _) (Reducer _ input' output')
= foldTerm T.Output (ValidatePropPaths input_) output'
where input_ = remAnns input'
fonTerm T.GenRecord (ValidateAll _) (Record _ _ props)
= validatePropsNoDups props
fonTerm T.InjApp (ValidateAll _) injApp = validateInjAppExists injApp
fonTerm _ _ _ = mempty
instance (Semigroup an) => Fold (ValidateRecords an) where
type Res (ValidateRecords an) = [Problem an]
type FAnn (ValidateRecords an) = an
fonTerm T.GenRecord (ValidateRecords recordCtx') record
= validateRecordConforms recordCtx' record
fonTerm _ _ _ = mempty
instance (Semigroup an) => Fold (ValidatePropPaths an) where
type Res (ValidatePropPaths an) = [Problem an]
type FAnn (ValidatePropPaths an) = an
fonTerm T.PropPath (ValidatePropPaths input') path
= validatePathExists input' path
fonTerm _ _ _ = mempty
-- | If the node is valid, returns an empty success.
-- Otherwise returns a refactor failure.
validateForRefactor :: (Monad u)
=> DirtyDepd Source SrcAnn
-> RefactorResultT u ()
validateForRefactor = hoist . mapError RefactorValidateError . validate_
-- | If the source is valid, returns a success containing it.
-- Otherwise returns a failure containing all the problems.
validate :: (Ord an, Semigroup an)
=> DirtyDepd Source an
-> Result [Problem an] (Depd Source an)
validate dx
| null problems = Success $ mapDep dirtyVal dx
| otherwise = Failure problems
where problems = validate' dx
-- | If the source is valid, returns an empty success.
-- Otherwise returns a failure containing all the problems.
-- Useful in do notation.
validate_ :: (Ord an, Semigroup an)
=> DirtyDepd Source an
-> Result [Problem an] ()
validate_ dx
| null problems = Success ()
| otherwise = Failure problems
where problems = validate' dx
-- | Finds all problems within the source. If the source has no
-- problems, it's well formed and can be interpreted.
validate' :: (Ord an, Semigroup an)
=> DirtyDepd Source an
-> [Problem an]
validate' (Depd (Dirty derrs extra) x) = dprobs ++ rprobs
where dprobs = map ProblemDepFail derrs
rprobs = validateIn' T.Source extra x
-- | If the node is valid, returns an empty success.
-- Otherwise returns a refactor failure.
validateForRefactorIn :: (Monad u)
=> TTerm t
-> AModule ()
-> t SrcAnn
-> RefactorResultT u ()
validateForRefactorIn term extra
= hoist . mapError RefactorValidateError . validateIn_ term extra
-- | If the node is valid, returns a success containing it.
-- Otherwise returns a failure containing all the problems.
-- Uses context (e.g. declared records) from the module.
validateIn :: (Ord an, Semigroup an)
=> TTerm t
-> AModule ()
-> t an
-> Result [Problem an] (t an)
validateIn term extra x
| null problems = Success x
| otherwise = Failure problems
where problems = validateIn' term extra x
-- | If the node is valid, returns an empty success.
-- Otherwise returns a failure containing all the problems.
-- Useful in do notation.
-- Uses context (e.g. declared records) from the module.
validateIn_ :: (Ord an, Semigroup an)
=> TTerm t
-> AModule ()
-> t an
-> Result [Problem an] ()
validateIn_ term extra x
| null problems = Success ()
| otherwise = Failure problems
where problems = validateIn' term extra x
-- | Finds all problems within the node. If the node has no
-- problems, it's well formed and can be interpreted.
-- Uses context (e.g. declared records) from the module.
validateIn' :: (Ord an, Semigroup an) => TTerm t -> AModule () -> t an -> [Problem an]
validateIn' term extra = sortOn getAnn . foldTerm term (ValidateAll extra)
validateImportDeclsNoDups :: (Semigroup an) => [ImportDecl an] -> [Problem an]
validateImportDeclsNoDups decls
= concat $ zipWith validateImportDeclNoConflict otherDeclss decls
where otherDeclss = inits $ map remAnns decls
validateRecordDeclsNoDups :: (Semigroup an) => [RecordDecl ()] -> [RecordDecl an] -> [Problem an]
validateRecordDeclsNoDups extraDecls decls
= concat $ zipWith validateRecordDeclNoDup otherDeclss decls
where otherDeclss = map (extraDecls ++) $ inits $ map remAnns decls
validatePropsNoDups :: (Semigroup an, FwdPrintable v, GenPropVal v)
=> [GenProperty v an]
-> [Problem an]
validatePropsNoDups props
= concat $ zipWith validatePropNoDup otherPropss props
where otherPropss = inits $ map remAnns props
validateImportDeclNoConflict :: (Semigroup an) => [ImportDecl ()] -> ImportDecl an -> [Problem an]
validateImportDeclNoConflict prevDecls decl
| any (`importsConflict` decl) prevDecls
= [Conflict (getAnn decl) Term.Import $ pprintStr decl]
| otherwise = []
validateRecordDeclNoDup :: (Semigroup an) => [RecordDecl ()] -> RecordDecl an -> [Problem an]
validateRecordDeclNoDup prevDecls decl
| any (`declsConflict` decl) prevDecls
= [Duplicate (getAnn decl) Term.RecordDecl $ pprintStr decl]
| otherwise = []
validatePropNoDup :: (Semigroup an, FwdPrintable v, GenPropVal v)
=> [GenProperty v ()]
-> GenProperty v an
-> [Problem an]
validatePropNoDup prevProps prop
| any (`propsConflict` prop) prevProps = [Duplicate (getAnn prop) Term.Property $ pprintStr prop]
| otherwise = []
-- | Whether both imports are redundant and/or could break each other.
importsConflict :: ImportDecl an1 -> ImportDecl an2 -> Bool
ImportDecl _ xPath xISrcs xIDsts `importsConflict` ImportDecl _ yPath yISrcs yIDsts
= xPath =@= yPath
&& (isPermBy (=@=) xISrcs yISrcs || overlapsBy (=@=) xIDsts yIDsts)
-- | Whether both declarations' types conflict.
declsConflict :: RecordDecl an1 -> RecordDecl an2 -> Bool
RecordDecl _ x `declsConflict` RecordDecl _ y = x `typesConflict` y
-- | Whether both types have the same head - whether both types can't
-- exist in the same context.
typesConflict :: RecordType an1 -> RecordType an2 -> Bool
x `typesConflict` y = xHead =@= yHead && xHead /@= undefinedFSym
where xHead = RecordType.head x
yHead = RecordType.head y
-- | Whether both properties conflict - whether they can't be in the
-- same record.
propsConflict :: GenProperty v1 an1 -> GenProperty v2 an2 -> Bool
x `propsConflict` y = xKey =@= yKey && xKey /@= undefinedSym
where xKey = propertyKey x
yKey = propertyKey y
-- | Validates that the record's type was defined, and it conforms.
validateRecordConforms :: (Semigroup an) => RecordCtx () -> GenRecord v an -> [Problem an]
validateRecordConforms ctx (Record ann head' properties')
= case recordTypeFor ctx head_ of
Nothing -> [UndeclaredRecord (getAnn head') $ pprintStr head']
Just (RecordType _ _ typeProps)
-> validatePropsComplete typeProps ann properties'
++ validatePropsFit typeProps properties'
where head_ = remAnns head'
validatePropsComplete :: (Semigroup an) => [Symbol ()] -> an -> [GenProperty v an] -> [Problem an]
validatePropsComplete typeProps ann properties'
= case filter (not . (`assocMember` properties')) typeProps_ of
[] -> []
missingProps -> [IncompleteRecord ann $ map pprintStr missingProps]
where typeProps_ = map remAnns typeProps
validatePropsFit :: (Semigroup an) => [Symbol ()] -> [GenProperty v an] -> [Problem an]
validatePropsFit declProps properties'
= case deleteFirstsBy' (=@=) declProps propKeys of
[] -> []
extraKeys@(x : xs) -> [OvercompleteRecord extraKeysAnn $ extraKeyPrs]
where extraKeyPrs = map pprintStr extraKeys
extraKeysAnn = sconcat $ NonEmpty.map getAnn extraKeys'
extraKeys' = x :| xs
where propKeys = map propertyKey properties'
validatePathExists :: (Semigroup an) => In.Value () -> PropPath an -> [Problem an]
validatePathExists input' (PropPath _ elems)
= validateSubpathExists (Just input') $ NonEmpty.toList elems
validateSubpathExists :: (Semigroup an) => Maybe (In.Value ()) -> [PathElem an] -> [Problem an]
validateSubpathExists _ [] = []
validateSubpathExists input' ((PathElem _ keyRef' headRef') : elems)
= case recWithHead headRef_ =<< input' of
Nothing -> [UndeclaredPathElemHead (getAnn headRef') $ pprintStr headRef']
Just inRec
-> case lookupProp keyRef_ inRec of
Nothing -> [UndeclaredPathElemKey (getAnn keyRef') $ pprintStr keyRef']
Just subInput -> validateSubpathExists subInput' elems
where subInput' = In.optValToMaybeVal subInput
where headRef_ = remAnns headRef'
keyRef_ = remAnns keyRef'
validateInjAppExists :: (Semigroup an) => Out.InjApp an -> [Problem an]
validateInjAppExists (Out.InjApp _ funcId' _)
= case lookupFunc funcId' of
Nothing -> [UndefinedInjFunc (getAnn funcId') $ pprintStr funcId']
Just _ -> [] -- TODO Check params. Requires refactor move into `ValidatePropPaths`
recordTypeFor :: (Semigroup an) => RecordCtx an -> FSymbol an -> Maybe (RecordType an)
recordTypeFor (RecordCtx _ decls) head'
= find (recordTypeMatches head') $ map recordDeclType decls
recordTypeMatches :: (Semigroup an) => FSymbol an -> RecordType an -> Bool
recordTypeMatches head' decl = head' =@= RecordType.head decl