Skip to content

Commit 621a59f

Browse files
committed
Add internal query linter
When working on the compiler it's easy to accidentally break something that results in a segfault. Most of these errors can be detected by checks on the Codegen IR, in particular correct type-safety and variable-scoping. - Add Glean.Query.Lint to implement query consistency checks - Turn on query linting with `GLEAN_DEBUG=lint` or `--debug-query-lint` - Linting is automatically enabled for all tests via `withTestEnv`
1 parent 3972375 commit 621a59f

8 files changed

Lines changed: 318 additions & 3 deletions

File tree

glean.cabal.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,7 @@ library db
651651
Glean.Query.Expand
652652
Glean.Query.Flatten
653653
Glean.Query.Flatten.Types
654+
Glean.Query.Lint
654655
Glean.Query.Opt
655656
Glean.Query.Prune
656657
Glean.Query.Reorder

glean/db/Glean/Database/Config.hs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,15 +204,17 @@ data Config = Config
204204
data DebugFlags = DebugFlags
205205
{ tcDebug :: !Bool
206206
, queryDebug :: !Bool
207+
, queryLint :: !Bool
207208
}
208209

209210
instance Default DebugFlags where
210-
def = DebugFlags { tcDebug = False, queryDebug = False }
211+
def = DebugFlags { tcDebug = False, queryDebug = False, queryLint = False }
211212

212213
instance Semigroup DebugFlags where
213214
a <> b = DebugFlags
214215
{ tcDebug = tcDebug a || tcDebug b
215216
, queryDebug = queryDebug a || queryDebug b
217+
, queryLint = queryLint a || queryLint b
216218
}
217219

218220
instance Monoid DebugFlags where
@@ -561,6 +563,7 @@ options = do
561563
debugParser = do
562564
tcDebug <- switch (long "debug-tc")
563565
queryDebug <- switch (long "debug-query")
566+
queryLint <- switch (long "debug-query-lint")
564567
return DebugFlags{..}
565568

566569
serverConfigThriftSource = option (eitherReader ThriftSource.parse)

glean/db/Glean/Database/Env.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ getDebugEnv = do
161161
where
162162
add "tc" = return def { tcDebug = True }
163163
add "query" = return def { queryDebug = True }
164+
add "lint" = return def { queryLint = True }
164165
add other = do
165166
logWarning $ "Unkonwn GLEAN_DEBUG class: " <> other
166167
return def

glean/db/Glean/Query/Lint.hs

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

glean/db/Glean/Query/UserQuery.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import Glean.Query.Flatten
7474
import Glean.Query.Opt
7575
import Glean.Query.Reorder
7676
import Glean.Query.Incremental (makeIncremental)
77+
import Glean.Query.Lint (lintQuery)
7778
import Glean.RTS as RTS
7879
import Glean.RTS.Bytecode.Disassemble
7980
import qualified Glean.RTS.Bytecode.Gen.Version as Bytecode
@@ -993,6 +994,12 @@ compileAngleQuery rec ver dbSchema mode source stored debug = do
993994
vlog 2 "made incremental"
994995
return $ makeIncremental getStats reordered
995996

997+
when (queryLint debug) $
998+
case lintQuery dbSchema final of
999+
Left err -> throwIO $ Thrift.BadQuery $
1000+
Text.pack $ show $ "query lint:" <+> err
1001+
Right () -> return ()
1002+
9961003
return (final, qiReturnType typechecked, preds)
9971004
where
9981005
ifDebug = when (queryDebug debug) . hPutStrLn stderr

glean/hs/Glean/RTS/Types.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ eqType version a b = case (a,b) of
143143
(NatTy, NatTy) -> True
144144
(StringTy, StringTy) -> True
145145
(ArrayTy a, ArrayTy b) -> eqType version a b
146+
(SetTy a, SetTy b) -> eqType version a b
146147
(RecordTy as, RecordTy bs) ->
147148
let isTuple = all (Text.isInfixOf "tuplefield" . fieldDefName)
148149
-- previous to version 7 records were always compared structurally

0 commit comments

Comments
 (0)