git.iko.soy/typeable/compaREST
on master help sign in
Formulas (#19)

* Formulas * Memoization * Fix anyOf

commit84e87f6ad130c5b838463124197bae0bf884927e authormniip <mniip@users.noreply.github.com> dateFri 12 Mar 2021, 10:49 (6 years ago) parentc951e3e
5 files changed+212−88
openapi-diff.cabal+1 src/OpenAPI/Checker/Formula.hs+75 src/OpenAPI/Checker/Memo.hs+89−34 src/OpenAPI/Checker/Subtree.hs+44−51 src/OpenAPI/Checker/TracePrefixTree.hs+3−3
openapi-diff.cabal+1
@@ -86,6 +86,7 @@ library
8686 import: common-options
8787 hs-source-dirs: src
8888 exposed-modules: OpenAPI.Checker.Aux
89+ , OpenAPI.Checker.Formula
8990 , OpenAPI.Checker.Memo
9091 , OpenAPI.Checker.Report
9192 , OpenAPI.Checker.Run
src/OpenAPI/Checker/Formula.hsnew file+75
@@ -0,0 +1,75 @@
1+module OpenAPI.Checker.Formula
2+ ( FormulaF
3+ , VarRef
4+ , anyOf
5+ , anError
6+ , calculate
7+ ) where
8+
9+import Data.Kind
10+import Data.Monoid
11+import qualified Data.List.NonEmpty as NE
12+import OpenAPI.Checker.Trace
13+import qualified OpenAPI.Checker.TracePrefixTree as T
14+
15+type VarRef = Int
16+
17+-- | The type @FormulaF f r ()@ describes (modulo contents of errors) boolean
18+-- formulas involving variables, conjunctions, and disjunctions. These
19+-- operations (and the generated algebra) are monotonous. This ensures that
20+-- there always exist fixpoints, i.e. that @x = f x@ has at least one solution.
21+data FormulaF (f :: k -> Type) (r :: k) (a :: Type) where
22+ Result :: a -> FormulaF f r a
23+ Errors :: !(T.TracePrefixTree f r) -> FormulaF f r a
24+ -- ^ invariant: never empty
25+ Apply :: FormulaF f r (b -> c) -> FormulaF f r b -> (c -> a) -> FormulaF f r a
26+ -- ^ invariant: LHS and RHS are never 'Result' nor 'Errors'
27+ SelectFirst :: NE.NonEmpty (FormulaF f r b)
28+ -> !(AnItem f r) -> (b -> a) -> FormulaF f r a
29+ -- ^ invariant: the list doesn't contain any 'Result's or 'Errors'.
30+ Variable :: !VarRef -> FormulaF f r a
31+
32+anError :: AnItem f r -> FormulaF f r a
33+anError e = Errors $ T.singleton e
34+
35+instance Functor (FormulaF f r) where
36+ fmap f (Result x) = Result (f x)
37+ fmap _ (Errors e) = Errors e
38+ fmap f (Apply g x h) = Apply g x (f . h)
39+ fmap f (SelectFirst xs e h) = SelectFirst xs e (f . h)
40+ fmap _ (Variable r) = Variable r
41+
42+instance Applicative (FormulaF f r) where
43+ pure = Result
44+ Result f <*> x = f <$> x
45+ Errors e <*> Result _ = Errors e
46+ Errors e1 <*> Errors e2 = Errors (e1 <> e2)
47+ f <*> x = Apply f x id
48+
49+anyOf :: [FormulaF f r a] -> AnItem f r -> FormulaF f r a
50+anyOf fs allE = case foldMap check fs of
51+ (First (Just x), _) -> Result x
52+ (First Nothing, (x:xs)) -> SelectFirst (x NE.:| xs) allE id
53+ (First Nothing, []) -> Errors $ T.singleton allE
54+ where
55+ check (Result x) = (First (Just x), mempty)
56+ check (Errors _) = (mempty, mempty)
57+ check (SelectFirst xs _ h) = (mempty, NE.toList (fmap (fmap h) xs))
58+ check x = (mempty, [x])
59+
60+calculate :: FormulaF f r a -> Either (T.TracePrefixTree f r) a
61+calculate (Result x) = Right x
62+calculate (Errors e) = Left e
63+calculate (Apply f x h) = case calculate f of
64+ Left e1 -> case calculate x of
65+ Left e2 -> Left (e1 <> e2)
66+ Right _ -> Left e1
67+ Right f' -> case calculate x of
68+ Left e2 -> Left e2
69+ Right x' -> Right (h (f' x'))
70+calculate (SelectFirst xs e h) = go (calculate <$> NE.toList xs)
71+ where
72+ go (Left _ : rs) = go rs
73+ go (Right x : _) = Right (h x)
74+ go [] = Left $ T.singleton e
75+calculate (Variable i) = error $ "Unknown variable " <> show i
src/OpenAPI/Checker/Memo.hs+89−34
@@ -1,70 +1,125 @@
11{-# LANGUAGE AllowAmbiguousTypes #-}
22-- | Utilities for effectfully memoizing other, more effectful functions.
33module OpenAPI.Checker.Memo
4− ( Progress(..)
5− , MonadMemo
4+ ( MonadMemo
65 , MemoState
76 , runMemo
8− , tryMemo
9− , tryMemoWithTag
7+ , modifyMemoNonce
8+ , KnotTier(..)
9+ , unknot
10+ , memoWithKnot
11+ , memoTaggedWithKnot
1012 ) where
1113
1214import Control.Monad.State
15+import Data.Dynamic
1316import qualified Data.Map as M
1417import Data.Tagged
18+import Data.Void
1519import qualified Data.TypeRepMap as T
1620import Type.Reflection
1721
18−-- | A computation has either 'Finished', or it is 'InProgress'.
19−data Progress a = Finished a | InProgress
20− deriving (Eq, Ord, Show)
22+data Progress a = Finished a | Started | TyingKnot Dynamic
2123
2224data MemoMap a where
2325 MemoMap :: !(M.Map k (Progress v)) -> MemoMap (k, v)
2426
25−newtype MemoState = MemoState (T.TypeRepMap MemoMap)
27+data MemoState s = MemoState s (T.TypeRepMap MemoMap)
2628
2729-- | An effectful memoization monad.
28−type MonadMemo m = MonadState MemoState m
30+type MonadMemo s m = MonadState (MemoState s) m
2931
3032memoStateLookup
31− :: forall k v. (Typeable k, Typeable v, Ord k)
32− => k -> MemoState -> Maybe (Progress v)
33−memoStateLookup k (MemoState t) = case T.lookup @(k, v) t of
33+ :: forall k v s. (Typeable k, Typeable v, Ord k)
34+ => k -> MemoState s -> Maybe (Progress v)
35+memoStateLookup k (MemoState _ t) = case T.lookup @(k, v) t of
3436 Just (MemoMap m) -> M.lookup k m
3537 Nothing -> Nothing
3638
3739memoStateInsert
38− :: forall k v. (Typeable k, Typeable v, Ord k)
39− => k -> Progress v -> MemoState -> MemoState
40−memoStateInsert k x (MemoState t) = MemoState $ T.insert (MemoMap m'') t
40+ :: forall k v s. (Typeable k, Typeable v, Ord k)
41+ => k -> Progress v -> MemoState s -> MemoState s
42+memoStateInsert k x (MemoState s t) = MemoState s $ T.insert (MemoMap m'') t
4143 where
4244 m'' = M.insert k x m'
4345 m' = case T.lookup @(k, v) t of
4446 Just (MemoMap m) -> m
4547 Nothing -> M.empty
4648
49+modifyMemoNonce :: MonadMemo s m => (s -> s) -> m s
50+modifyMemoNonce f = do
51+ MemoState s t <- get
52+ put $ MemoState (f s) t
53+ pure s
54+
4755-- | Run a memoized computation.
48−runMemo :: Monad m => StateT MemoState m a -> m a
49−runMemo = (`evalStateT` MemoState T.empty)
56+runMemo :: Monad m => s -> StateT (MemoState s) m a -> m a
57+runMemo s = (`evalStateT` MemoState s T.empty)
58+
59+-- | A description of how to effectfully tie knots in type @v@, using the @m@
60+-- monad, and by sharing some @d@ data among the recursive instances.
61+data KnotTier v d m = KnotTier
62+ { onKnotFound :: m d -- ^ Create some data that will be connected to this knot
63+ , onKnotUsed :: d -> m v -- ^ This is what the knot will look like as a value
64+ -- to the inner computations
65+ , tieKnot :: d -> v -> m v -- ^ Once we're done and we're outside, tie the
66+ -- knot using the datum
67+ }
68+
69+unknot :: KnotTier v Void m
70+unknot = KnotTier
71+ { onKnotFound = error "Recursion detected"
72+ , onKnotUsed = absurd
73+ , tieKnot = absurd
74+ }
5075
51−-- | Try to call a function whilst memoizing its result. If we are already
52−-- inside an evaluation of some function with these arguments, this returns
53−-- @InProgress@. Beware of different functions that have the same type.
54−tryMemo
55− :: forall k v m. (Typeable k, Typeable v, Ord k, MonadMemo m)
56− => (k -> m v) -> k -> m (Progress v)
57−tryMemo f k = memoStateLookup k <$> get >>= \case
58− Just x -> pure x
76+-- | Run a potentially recursive computation. The provided key will be used to
77+-- refer to the result of this computation. If during the computation, another
78+-- attempt to run the computation with the same key is made, we run a
79+-- tying-the-knot procedure.
80+--
81+-- If another attempt to run the computation with the same key is made
82+-- *after we're done*, we will return the memoized value.
83+memoWithKnot
84+ :: forall k v d m s.
85+ (Typeable k, Typeable v, Typeable d, Ord k, MonadMemo s m)
86+ => KnotTier v d m
87+ -> m v -- ^ the computation to memoize
88+ -> k -- ^ key for memoization
89+ -> m v
90+memoWithKnot tier f k = memoStateLookup @k @v k <$> get >>= \case
91+ Just (Finished v) -> pure v
92+ Just Started -> do
93+ d <- onKnotFound tier
94+ modify $ memoStateInsert @k @v k (TyingKnot $ toDyn d)
95+ onKnotUsed tier d
96+ Just (TyingKnot dyn) -> case fromDynamic dyn of
97+ Just d -> onKnotUsed tier d
98+ Nothing -> error $ "Type mismatch when examining the knot of "
99+ <> show (typeRep @(k -> v)) <> ": expected " <> show (typeRep @d)
100+ <> ", got " <> show (dynTypeRep dyn)
59101 Nothing -> do
60− modify $ memoStateInsert k (InProgress @v)
61− result <- Finished <$> f k
62− modify $ memoStateInsert k result
63− pure result
102+ modify $ memoStateInsert @k @v k Started
103+ v <- f
104+ v' <- memoStateLookup @k @v k <$> get >>= \case
105+ Just Started -> pure v
106+ Just (TyingKnot dyn) -> case fromDynamic dyn of
107+ Just d -> tieKnot tier d v
108+ Nothing -> error $ "Type mismatch when tying the knot of "
109+ <> show (typeRep @(k -> v)) <> ": expected " <> show (typeRep @d)
110+ <> ", got " <> show (dynTypeRep dyn)
111+ Just (Finished _) -> error $ "Unexpected Finished when memoizing "
112+ <> show (typeRep @(k -> v))
113+ Nothing -> error $ "No key found when memoizing "
114+ <> show (typeRep @(k -> v))
115+ modify $ memoStateInsert @k @v k (Finished v')
116+ pure v'
64117
65118-- | Disambiguate memoized computations with an arbitrary tag.
66−tryMemoWithTag
67− :: forall t k v m. (Typeable t, Typeable k, Typeable v, Ord k, MonadMemo m)
68− => (k -> m v) -> k -> m (Progress v)
69−tryMemoWithTag f k = withTypeable (typeRepKind $ typeRep @t) $
70− tryMemo (f . unTagged) (Tagged @t k)
119+memoTaggedWithKnot
120+ :: forall t k v d m s.
121+ ( Typeable t, Typeable k, Typeable v, Typeable d
122+ , Ord k, MonadMemo s m )
123+ => KnotTier v d m -> m v -> k -> m v
124+memoTaggedWithKnot tier f k = withTypeable (typeRepKind $ typeRep @t) $
125+ memoWithKnot tier f (Tagged @t k)
src/OpenAPI/Checker/Subtree.hs+44−51
@@ -2,27 +2,24 @@ module OpenAPI.Checker.Subtree
22 ( APIStep(..)
33 , Subtree(..)
44 , CompatM(..)
5− , runCompatM
5+ , CompatFormula
6+ , runCompatFormula
7+ , localM
68 , local'
7− , warnIssue
8− , throwIssue
9− , warnIssueAt
10− , throwIssueAt
11− , throwWarnings
12− , warnIssues
9+ , issueAtTrace
10+ , issueAt
11+ , memo
1312 ) where
1413
15−import Control.Monad.Error.Class
1614import Control.Monad.Identity
17−import Control.Monad.Reader.Class
18−import Control.Monad.Trans.Except (ExceptT, runExceptT)
19−import Control.Monad.Trans.Reader (ReaderT(..))
20−import Control.Monad.Trans.State (StateT)
21−import Control.Monad.Trans.Writer.Strict (WriterT, runWriterT)
22−import Control.Monad.Writer.Class
15+import Control.Monad.Reader
16+import Control.Monad.State
17+import Data.Functor.Compose
2318import Data.Kind
2419import Data.OpenApi
2520import Data.Text
21+import Data.Typeable
22+import OpenAPI.Checker.Formula
2623import OpenAPI.Checker.Memo
2724import OpenAPI.Checker.Trace
2825import qualified OpenAPI.Checker.TracePrefixTree as T
@@ -48,64 +45,60 @@ data TracedEnv t = TracedEnv
4845newtype CompatM t a = CompatM
4946 { unCompatM ::
5047 ReaderT (ProdCons (TracedEnv t))
51− (ExceptT (T.TracePrefixTree CheckIssue OpenApi)
52− (WriterT (T.TracePrefixTree CheckIssue OpenApi)
53− (StateT MemoState Identity))) a
48+ (StateT (MemoState VarRef) Identity) a
5449 } deriving newtype
5550 ( Functor, Applicative, Monad
5651 , MonadReader (ProdCons (TracedEnv t))
57− , MonadError (T.TracePrefixTree CheckIssue OpenApi)
58− , MonadWriter (T.TracePrefixTree CheckIssue OpenApi)
52+ , MonadState (MemoState VarRef)
5953 )
6054
61−class (Ord t, Ord (CheckIssue t)) => Subtree (t :: Type) where
55+type CompatFormula t = Compose (CompatM t) (FormulaF CheckIssue OpenApi)
56+
57+class (Typeable t, Ord t, Ord (CheckIssue t)) => Subtree (t :: Type) where
6258 type family CheckEnv t :: Type
6359 data family CheckIssue t :: Type
64− checkCompatibility :: ProdCons t -> CompatM t a
60+ -- | If we ever followed a reference, reroute the path through "components"
61+ normalizeTrace :: Trace OpenApi t -> Trace OpenApi t
62+ checkCompatibility :: ProdCons t -> CompatFormula t ()
6563
66−runCompatM :: ProdCons (TracedEnv t) -> CompatM t a ->
67− ( Either (T.TracePrefixTree CheckIssue OpenApi) a -- unrecovarable errors
68− , T.TracePrefixTree CheckIssue OpenApi ) -- warnings
69−runCompatM env = runIdentity . runMemo . runWriterT
70− . runExceptT . (`runReaderT` env) . unCompatM
64+runCompatFormula
65+ :: ProdCons (TracedEnv t)
66+ -> Compose (CompatM t) (FormulaF f r) a
67+ -> Either (T.TracePrefixTree f r) a
68+runCompatFormula env (Compose f)
69+ = calculate . runIdentity . runMemo 0 . (`runReaderT` env) . unCompatM $ f
7170
72−local'
71+localM
7372 :: ProdCons (Trace a b)
7473 -> (ProdCons (CheckEnv a) -> ProdCons (CheckEnv b))
7574 -> CompatM b x
7675 -> CompatM a x
77−local' xs wrapEnv (CompatM k) = CompatM $ ReaderT $ \env ->
76+localM xs wrapEnv (CompatM k) = CompatM $ ReaderT $ \env ->
7877 runReaderT k $ TracedEnv
7978 <$> (catTrace <$> (getTrace <$> env) <*> xs)
8079 <*> wrapEnv (getEnv <$> env)
8180
82−warnIssue :: Subtree t => Trace OpenApi t -> CheckIssue t -> CompatM t ()
83−warnIssue xs issue = tell $ T.singleton $ AnItem xs issue
81+local'
82+ :: ProdCons (Trace a b)
83+ -> (ProdCons (CheckEnv a) -> ProdCons (CheckEnv b))
84+ -> Compose (CompatM b) (FormulaF f r) x
85+ -> Compose (CompatM a) (FormulaF f r) x
86+local' xs wrapEnv (Compose h) = Compose (localM xs wrapEnv h)
8487
85−throwIssue :: Subtree t => Trace OpenApi t -> CheckIssue t -> CompatM t a
86−throwIssue xs issue = throwError $ T.singleton $ AnItem xs issue
88+issueAtTrace
89+ :: Subtree t => Trace OpenApi t -> CheckIssue t -> CompatFormula t a
90+issueAtTrace xs issue = Compose $ pure $ anError $ AnItem xs issue
8791
88−warnIssueAt
92+issueAt
8993 :: Subtree t
9094 => (forall x. ProdCons x -> x)
9195 -> CheckIssue t
92− -> CompatM t ()
93−warnIssueAt f issue = do
96+ -> CompatFormula t a
97+issueAt f issue = Compose $ do
9498 xs <- asks $ getTrace . f
95− warnIssue xs issue
96−
97−throwIssueAt
98− :: Subtree t
99− => (forall x. ProdCons x -> x)
100− -> CheckIssue t
101− -> CompatM t a
102−throwIssueAt f issue = do
103− xs <- asks $ getTrace . f
104− throwIssue xs issue
105−
106−throwWarnings :: CompatM t a -> CompatM t a
107−throwWarnings k = listen k >>= \case
108− (x, issues) -> if T.null issues then pure x else throwError issues
99+ pure $ anError $ AnItem xs issue
109100
110−warnIssues :: CompatM t a -> CompatM t (Maybe a)
111−warnIssues k = catchError (Just <$> k) (\issues -> Nothing <$ tell issues)
101+memo :: (Subtree t, Typeable a) => CompatFormula t a -> CompatFormula t a
102+memo (Compose f) = Compose $ do
103+ pxs <- asks (normalizeTrace . getTrace <$>)
104+ memoWithKnot unknot f pxs
src/OpenAPI/Checker/TracePrefixTree.hs+3−3
@@ -24,9 +24,9 @@ import Data.Type.Equality
2424
2525-- | A list of @AnItem r f@, but optimized into a prefix tree.
2626data TracePrefixTree (f :: k -> Type) (r :: k) = TracePrefixTree
27− { rootItems :: !(ASet (f r))
28− , snocItems :: !(T.TypeRepMap (AStep f r))
29− }
27+ { rootItems :: !(ASet (f r))
28+ , snocItems :: !(T.TypeRepMap (AStep f r))
29+ }
3030
3131deriving instance Eq (TracePrefixTree f a)
3232