Skip to content

Commit 26d4cbe

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 26d4cbe

8 files changed

Lines changed: 325 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: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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

glean/db/Glean/Query/UserQuery.hs

Lines changed: 13 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
@@ -987,12 +988,24 @@ compileAngleQuery rec ver dbSchema mode source stored debug = do
987988
reordered <- checkBadQuery id $ runExcept $ reorder dbSchema optimised
988989
ifDebug $ "reordered query: " <> show (displayDefault (qiQuery reordered))
989990

991+
when (queryLint debug) $
992+
case lintQuery dbSchema reordered of
993+
Left err -> throwIO $ Thrift.BadQuery $
994+
Text.pack $ show $ "query lint:" <+> err
995+
Right () -> return ()
996+
990997
final <- case mode of
991998
NoExtraSteps -> return reordered
992999
IncrementalDerivation getStats -> do
9931000
vlog 2 "made incremental"
9941001
return $ makeIncremental getStats reordered
9951002

1003+
when (queryLint debug) $
1004+
case lintQuery dbSchema final of
1005+
Left err -> throwIO $ Thrift.BadQuery $
1006+
Text.pack $ show $ "query lint:" <+> err
1007+
Right () -> return ()
1008+
9961009
return (final, qiReturnType typechecked, preds)
9971010
where
9981011
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)