|
| 1 | +{- |
| 2 | + Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | + All rights reserved. |
| 4 | +
|
| 5 | + This source code is licensed under the BSD-style license found in the |
| 6 | + LICENSE file in the root directory of this source tree. |
| 7 | +-} |
| 8 | + |
| 9 | +module Glean.Query.Lint ( |
| 10 | + lintQuery |
| 11 | + ) where |
| 12 | + |
| 13 | +import Control.Monad |
| 14 | +import Control.Monad.Except |
| 15 | +import Control.Monad.State |
| 16 | +import Data.List (foldl') |
| 17 | +import Data.IntSet (IntSet) |
| 18 | +import qualified Data.IntSet as IntSet |
| 19 | +import Compat.Prettyprinter |
| 20 | + |
| 21 | +import Glean.Angle.Types |
| 22 | + (Type_(..), latestAngleVersion, tempPredicateId) |
| 23 | +import Glean.Database.Schema.Types |
| 24 | + (DbSchema, PredicateDetails(..), lookupPid, lookupPredicateId) |
| 25 | +import Glean.Display |
| 26 | +import Glean.Query.Codegen.Types |
| 27 | +import Glean.RTS.Term as RTS |
| 28 | +import Glean.RTS.Types as RTS |
| 29 | +import Glean.Schema.Util (tupleSchema) |
| 30 | + |
| 31 | +-- | The lint monad: tracks the set of bound variable IDs in state, |
| 32 | +-- and can throw a pretty-printed error. |
| 33 | +type L a = StateT IntSet (Except (Doc ())) a |
| 34 | + |
| 35 | +runL :: L a -> Either (Doc ()) a |
| 36 | +runL m = runExcept (evalStateT m IntSet.empty) |
| 37 | + |
| 38 | +lintError :: Doc () -> L a |
| 39 | +lintError = throwError |
| 40 | + |
| 41 | +-- | Record a variable as bound. |
| 42 | +bindVar :: Var -> L () |
| 43 | +bindVar v = modify (IntSet.insert (varId v)) |
| 44 | + |
| 45 | +-- | Check that a variable is currently bound. |
| 46 | +requireBound :: Var -> L () |
| 47 | +requireBound v = do |
| 48 | + bound <- get |
| 49 | + unless (varId v `IntSet.member` bound) $ |
| 50 | + lintError $ "unbound variable:" <+> displayDefault v |
| 51 | + |
| 52 | +-- | Run an action in a nested scope. The bound set is saved and |
| 53 | +-- restored afterwards, so bindings made inside do not escape. |
| 54 | +scoped :: L a -> L a |
| 55 | +scoped m = do |
| 56 | + saved <- get |
| 57 | + a <- m |
| 58 | + put saved |
| 59 | + return a |
| 60 | + |
| 61 | +{- |
| 62 | + Check the query for internal consistency. lintQuery will check |
| 63 | + the following properties of the query: |
| 64 | +
|
| 65 | + - type-correctness, e.g. |
| 66 | + - in A = B, the types of A and B are equivalent |
| 67 | + - in FactGenerator and DerivedFactGenerator, the types of the key |
| 68 | + and value pattern match those of the predicate in the schema |
| 69 | + - arguments of primitives have the correct type |
| 70 | + - bind-before-use: for all variables, MatchBind occurs before MatchVar |
| 71 | +
|
| 72 | +-} |
| 73 | +lintQuery :: DbSchema -> CodegenQuery -> Either (Doc ()) () |
| 74 | +lintQuery schema QueryWithInfo{..} = runL $ do |
| 75 | + let CgQuery head body = qiQuery |
| 76 | + lintStatements schema body |
| 77 | + lintPat head |
| 78 | + case qiGenerator of |
| 79 | + Nothing -> checkEqType "query head" head qiReturnType |
| 80 | + Just gen -> do |
| 81 | + lintGenerator gen |
| 82 | + lintGeneratorTypes schema head gen |
| 83 | + |
| 84 | +-- --------------------------------------------------------------------------- |
| 85 | +-- Bind-before-use and type checking for statement lists |
| 86 | + |
| 87 | +lintStatements :: DbSchema -> [CgStatement] -> L () |
| 88 | +lintStatements schema = mapM_ (lintStatement schema) |
| 89 | + |
| 90 | +lintStatement :: DbSchema -> CgStatement -> L () |
| 91 | +lintStatement schema = \case |
| 92 | + CgStatement pat gen -> do |
| 93 | + -- The generator is evaluated first; its patterns can bind variables |
| 94 | + lintGenerator gen |
| 95 | + -- Then the LHS pattern matches the generator result, and can use |
| 96 | + -- variables bound by the generator as well as previously bound ones |
| 97 | + lintPat pat |
| 98 | + -- Type check: pattern type should match generator result type |
| 99 | + lintStmtTypes schema pat gen |
| 100 | + |
| 101 | + CgAllStatement var expr stmts -> do |
| 102 | + -- The inner statements are evaluated in a nested scope |
| 103 | + innerBound <- scoped $ do |
| 104 | + lintStatements schema stmts |
| 105 | + get |
| 106 | + -- The expression uses variables bound in the inner scope |
| 107 | + saved <- get |
| 108 | + put innerBound |
| 109 | + lintPat expr |
| 110 | + put saved |
| 111 | + -- The variable bound by 'all' is available to subsequent statements |
| 112 | + bindVar var |
| 113 | + |
| 114 | + CgNegation stmts -> |
| 115 | + -- Negation introduces its own scope; bindings do not escape |
| 116 | + scoped $ lintStatements schema stmts |
| 117 | + |
| 118 | + CgDisjunction stmtss -> do |
| 119 | + -- Each branch starts with the same bound set. Variables bound |
| 120 | + -- in all branches are available afterwards. |
| 121 | + bounds <- forM stmtss $ \stmts -> scoped $ do |
| 122 | + lintStatements schema stmts |
| 123 | + get |
| 124 | + case bounds of |
| 125 | + [] -> return () |
| 126 | + (b:bs) -> put (foldl' IntSet.intersection b bs) |
| 127 | + |
| 128 | + CgConditional{..} -> do |
| 129 | + outer <- get |
| 130 | + lintStatements schema cond |
| 131 | + -- then branch sees cond bindings |
| 132 | + lintStatements schema then_ |
| 133 | + thenBound <- get |
| 134 | + -- else branch only sees pre-condition bindings |
| 135 | + put outer |
| 136 | + lintStatements schema else_ |
| 137 | + elseBound <- get |
| 138 | + -- Variables bound in both branches are available after |
| 139 | + put (IntSet.intersection thenBound elseBound) |
| 140 | + |
| 141 | +-- --------------------------------------------------------------------------- |
| 142 | +-- Check uses and record bindings in a single depth-first left-to-right |
| 143 | +-- traversal, so that a MatchBind makes the variable immediately |
| 144 | +-- available for a later MatchVar in the same pattern. |
| 145 | + |
| 146 | +lintPat :: Pat -> L () |
| 147 | +lintPat = mapM_ lintMatch |
| 148 | + |
| 149 | +lintMatch :: Match () Var -> L () |
| 150 | +lintMatch = \case |
| 151 | + MatchBind v -> bindVar v |
| 152 | + MatchVar v -> requireBound v |
| 153 | + MatchAnd a b -> do |
| 154 | + lintPat a |
| 155 | + lintPat b |
| 156 | + MatchPrefix _ rest -> |
| 157 | + lintPat rest |
| 158 | + MatchArrayPrefix _ pre whole -> do |
| 159 | + mapM_ lintPat pre |
| 160 | + lintPat whole |
| 161 | + _ -> return () |
| 162 | + |
| 163 | +-- | Lint expression-position subterms of a generator, then bind |
| 164 | +-- pattern-position variables. |
| 165 | +lintGenerator :: Generator -> L () |
| 166 | +lintGenerator = \case |
| 167 | + FactGenerator _ kpat vpat _ -> do |
| 168 | + lintPat kpat |
| 169 | + lintPat vpat |
| 170 | + TermGenerator expr -> |
| 171 | + lintPat expr |
| 172 | + DerivedFactGenerator _ key value -> do |
| 173 | + lintPat key |
| 174 | + lintPat value |
| 175 | + ArrayElementGenerator _ arr -> |
| 176 | + lintPat arr |
| 177 | + SetElementGenerator _ set -> |
| 178 | + lintPat set |
| 179 | + PrimCall _ args _ -> |
| 180 | + mapM_ lintPat args |
| 181 | + |
| 182 | +-- | Type-check the result generator (qiGenerator). The head expression |
| 183 | +-- is the fact ID that the generator will look up. |
| 184 | +lintGeneratorTypes :: DbSchema -> Pat -> Generator -> L () |
| 185 | +lintGeneratorTypes schema head gen = case gen of |
| 186 | + FactGenerator pidRef kpat vpat _ -> do |
| 187 | + mdetails <- lookupPredicate schema pidRef |
| 188 | + forM_ mdetails $ \details -> do |
| 189 | + checkEqType |
| 190 | + ("result generator key of" <+> displayDefault pidRef) |
| 191 | + kpat (predicateKeyType details) |
| 192 | + checkEqType |
| 193 | + ("result generator value of" <+> displayDefault pidRef) |
| 194 | + vpat (predicateValueType details) |
| 195 | + checkEqType "result generator fact ID" head (PredicateTy () pidRef) |
| 196 | + _ -> return () |
| 197 | + |
| 198 | +-- --------------------------------------------------------------------------- |
| 199 | +-- Type checking |
| 200 | + |
| 201 | +-- | Check the type consistency of a statement: pattern type vs |
| 202 | +-- generator result type, and predicate key/value types for fact |
| 203 | +-- generators. |
| 204 | +lintStmtTypes :: DbSchema -> Pat -> Generator -> L () |
| 205 | +lintStmtTypes schema pat gen = case gen of |
| 206 | + FactGenerator pidRef kpat vpat _ -> do |
| 207 | + mdetails <- lookupPredicate schema pidRef |
| 208 | + forM_ mdetails $ \details -> do |
| 209 | + checkEqType |
| 210 | + ("key of" <+> displayDefault pidRef) kpat (predicateKeyType details) |
| 211 | + checkEqType |
| 212 | + ("value of" <+> displayDefault pidRef) vpat (predicateValueType details) |
| 213 | + -- The LHS pattern matches the fact ID, which has the predicate type |
| 214 | + checkEqType "fact generator result" pat (PredicateTy () pidRef) |
| 215 | + |
| 216 | + DerivedFactGenerator pidRef key value -> do |
| 217 | + mdetails <- lookupPredicate schema pidRef |
| 218 | + forM_ mdetails $ \details -> do |
| 219 | + checkEqType |
| 220 | + ("key of" <+> displayDefault pidRef) key (predicateKeyType details) |
| 221 | + checkEqType |
| 222 | + ("value of" <+> displayDefault pidRef) value (predicateValueType details) |
| 223 | + checkEqType "derived fact generator result" pat (PredicateTy () pidRef) |
| 224 | + |
| 225 | + TermGenerator expr -> |
| 226 | + case (patType pat, patType expr) of |
| 227 | + (Just pty, Just ety) -> typesShouldMatch "term generator" pty ety |
| 228 | + _ -> return () |
| 229 | + |
| 230 | + ArrayElementGenerator eltTy _arr -> |
| 231 | + checkEqType "array element generator" pat eltTy |
| 232 | + |
| 233 | + SetElementGenerator eltTy _set -> |
| 234 | + checkEqType "set element generator" pat eltTy |
| 235 | + |
| 236 | + PrimCall _op _args retTy -> |
| 237 | + checkEqType "primcall result" pat retTy |
| 238 | + |
| 239 | +lookupPredicate :: DbSchema -> PidRef -> L (Maybe PredicateDetails) |
| 240 | +lookupPredicate schema (PidRef pid predId) |
| 241 | + -- The temporary predicate used for derived fact generators is not |
| 242 | + -- stored in the schema, so skip it. |
| 243 | + | predId == tempPredicateId = return Nothing |
| 244 | + | otherwise = |
| 245 | + case lookupPid pid schema of |
| 246 | + Nothing -> case lookupPredicateId predId schema of |
| 247 | + Nothing -> |
| 248 | + lintError $ "unknown predicate:" <+> displayDefault predId |
| 249 | + Just details -> return (Just details) |
| 250 | + Just details -> return (Just details) |
| 251 | + |
| 252 | +-- | Check that the type of a pattern matches an expected type. |
| 253 | +-- When the pattern's type cannot be determined, the check is skipped. |
| 254 | +checkEqType :: Doc () -> Pat -> Type -> L () |
| 255 | +checkEqType ctx pat expected = |
| 256 | + case patType pat of |
| 257 | + Nothing -> return () |
| 258 | + Just actual -> typesShouldMatch ctx actual expected |
| 259 | + |
| 260 | +typesShouldMatch :: Doc () -> Type -> Type -> L () |
| 261 | +typesShouldMatch ctx actual expected |
| 262 | + | eqType latestAngleVersion actual expected = return () |
| 263 | + | otherwise = throwError $ vcat |
| 264 | + [ "type mismatch in" <+> ctx <> ":" |
| 265 | + , " expected:" <+> displayDefault expected |
| 266 | + , " actual:" <+> displayDefault actual |
| 267 | + ] |
| 268 | + |
| 269 | +-- --------------------------------------------------------------------------- |
| 270 | +-- Inferring the type of a pattern |
| 271 | + |
| 272 | +-- | Attempt to determine the type of a pattern. Returns Nothing when |
| 273 | +-- the type cannot be determined from the pattern structure alone |
| 274 | +-- (e.g. an empty array or an Alt without surrounding context). |
| 275 | +patType :: Pat -> Maybe Type |
| 276 | +patType (Byte _) = Just ByteTy |
| 277 | +patType (Nat _) = Just NatTy |
| 278 | +patType (RTS.String _) = Just StringTy |
| 279 | +patType (ByteArray _) = Just (ArrayTy ByteTy) |
| 280 | +patType (Array []) = Nothing |
| 281 | +patType (Array (t:_)) = ArrayTy <$> patType t |
| 282 | +patType (Tuple ts) = tupleSchema <$> traverse patType ts |
| 283 | +patType (Alt _ _) = Nothing |
| 284 | +patType (Ref m) = matchType m |
| 285 | + |
| 286 | +matchType :: Match () Var -> Maybe Type |
| 287 | +matchType (MatchWild ty) = Just ty |
| 288 | +matchType (MatchNever ty) = Just ty |
| 289 | +matchType (MatchBind v) = Just (varType v) |
| 290 | +matchType (MatchVar v) = Just (varType v) |
| 291 | +matchType (MatchFid _) = Nothing |
| 292 | +matchType (MatchAnd a _) = patType a |
| 293 | +matchType (MatchPrefix _ _) = Just StringTy |
| 294 | +matchType (MatchArrayPrefix ty _ _) = Just (ArrayTy ty) |
| 295 | +matchType (MatchExt _) = Nothing |
0 commit comments