Skip to content

Watchman query language (Meta) ​

The only production system in the survey whose user-facing model is the architecture this design is building: a query is one or more generators that enumerate candidates, feeding an expression evaluator that filters them. That is the Plan/Predicate split, named and shipped.

LanguageC++ (server), JSON (query surface)
LicenseMIT
Repositoryfacebook/watchman
Surveyed revisiond99639db (all file/line citations pin this commit)
CategoryFile-watching query service
Composition modelSet algebra over an explicitly generated candidate set
GrammarJSON s-expressions — no textual syntax

Overview ​

What it solves ​

Answering "which files under this root satisfy this predicate" against an in-memory index that a watcher keeps current, fast enough that build tools (Buck, Mercurial/Sapling, jest) call it on every invocation.

Design philosophy ​

The query is explicitly two-layer, and the documentation leads with it:

Watchman file queries consist of 1 or more generators that feed files through the expression evaluator.

Generators are analogous to the list of paths that you specify when using the find(1) utility, but are implemented in watchman with a bit of a twist because watchman doesn't need to crawl the filesystem in realtime and instead maintains a couple of indexes over the tree.

— website/docs/file-query.md

Everything that can bound the candidate set is a generator; everything that can only test a candidate is an expression term. The split is not an implementation detail — it is the API.

How it works ​

Five generators:

GeneratorBounds the candidate set by
sincea clockspec — everything modified since a point in time
suffixa suffix or list of suffixes, served from an index
globa list of glob patterns, walked as a tree (below)
patha path plus a depth bound ({"path": "bar", "depth": 0})
allevery known file — the default when no generator is named

and an expression language of allof, anyof, not, true, false, match/imatch, name/iname, dirname/idirname, type, size, empty, exists, since, suffix, pcre — website/docs/expr/.

The glob generator is the one that matters most here:

It does this by building a tree from the glob expression(s) and walking both the expression and the in-memory filesystem tree concurrently.

— website/docs/file-query.md

Analysis ​

1. Composition model ​

Set algebra (allof/anyof/not) over a candidate set that a generator has already bounded. Multiple generators union, with an explicit duplicate hazard:

A query may specify any number of generators; each generator will emit its list of files and this may mean that you see the same file output more than once if you specified the use of multiple generators that all produce the same file. … You may ask Watchman to de-duplicate results for you by enabling the dedup_results boolean.

— website/docs/file-query.md

dedup_results defaults off for backwards compatibility and is implicitly on for the glob generator. This is the start-root deduplication problem: a union of arms whose literal prefixes overlap will enumerate the overlap twice unless the planner merges them by containment.

2. Operator surface & precedence ​

None — the query is JSON, so nesting is explicit and precedence does not exist. That is the point of comparison: the entire O1–O6 design surface of a textual language is the price of not writing ["allof", ["match", "*.d"], ["not", …]].

3. Primary vocabulary & the kind namespace ​

There is no kind: prefix, because every primary is a named JSON term. Two of those terms carry lessons a prefix namespace has to reproduce:

match takes its scope as an argument, not as an inference:

The match expression performs a glob-style match against the basename of the file … You may optionally provide a third argument to change the scope of the match from the basename to the wholename of the file.

json
["match", "*.txt", "basename"]
["match", "dir/*.txt", "wholename"]

— website/docs/expr/match.md

Modifiers are an additive flag dictionary, not a suffix on the term name:

json
["match", "*.txt", "basename", { "includedotfiles": true }]

By default, backslashes in the pattern escape the next character, so \* matches a literal * character. To change this behavior so backslashes are treated literally, set the noescape flag.

— website/docs/expr/match.md

That is git pathspec's :(glob,icase) idea in a different notation, and the second independent occurrence of it in the survey.

4. Anchoring & glob semantics ​

basename by default, wholename on request — a fifth anchoring rule, and the important thing is that it is explicit per term rather than inferred from whether the pattern contains a /. ** works only in wholename scope, and dotfiles are excluded by default.

Prefix closure is a separate, tunable term. dirname takes a relational depth expression:

The following two terms will match any file whose dirname is either exactly a match for foo/bar or is any child directory of foo/bar. The first of these two is a shortcut for the second:

["dirname", "foo/bar"]
["dirname", "foo/bar", ["depth", "ge", 0]]

— website/docs/expr/dirname.md

so ["dirname", "foo/bar", ["depth", "eq", 0]] is Mercurial's rootfilesin: and the default is jj's prefix-glob: — one term, parameterised, instead of two kinds.

Case sensitivity is policy in three layers: match/imatch variants, a query-level case_sensitive field, and automatic equivalence "on systems where the watched root is a case insensitive filesystem". Identity is never touched.

5. Lexis: quoting, escaping, reserved characters ​

JSON's, entirely. There is no token to escape, which is precisely why watchman can afford not and a flag dictionary and five generators without a grammar.

6. Evaluation strategy & pruning ​

The generator is the plan, and the cost trade-off is documented for users to make by hand:

Note that it is more efficient to use the suffix generator together with a dirname expression term for such a broadly scoped query as it results in fewer comparisons. This example is included as an illustration of recursive globbing.

— website/docs/file-query.md

i.e. {"glob": ["**/*.c"]} and {"suffix": "c", "expression": ["dirname", …]} denote the same set, and the second is faster. Watchman asks the user to choose; a compiler over an IR should choose for them. That is the clearest statement in the survey of what the Plan/Predicate split is for.

On emptiness, every generator agrees: "If the suffix generator is given an empty array, it produces no files", and likewise for glob and path — an empty generator is an empty result, not an error and not the universe.

And on complement: not is safe here because the generator bounds the universe first. ["not", ["match", "*.c"]] never means "every file on the disk"; it means "every candidate this generator produced that isn't a .c file". That is the precise condition under which a complement is affordable.

Strengths ​

  • The generator/expression split is explicit, documented, and load-bearing.
  • path with a depth bound is a prunable primary that the pattern languages reach only through kind proliferation.
  • dirname + relational depth parameterises prefix closure instead of multiplying kinds.
  • Modifier flags compose additively.
  • JSON means no lexis, no precedence, no escaping — and therefore no O1–O6.

Weaknesses ​

  • Unwritable by hand: the query surface exists for programs, and the documented ergonomics gap is real enough that watchman also ships watchman-make, watchman-wait and a simple query shorthand.
  • Generator choice is a manual performance decision with no compiler behind it.
  • dedup_results defaults off, so the common multi-generator query is wrong by default for backwards-compatibility reasons.
  • Generators do not compose with the expression algebra — you cannot intersect two generators, only union them and filter afterwards.

Key design decisions and trade-offs ​

DecisionRationaleTrade-off
Generators separate from expressionsOnly a generator can bound the walk; only an expression can testTwo vocabularies to learn; generators cannot intersect
not in the expression layerThe generator has already bounded the universeComplement is meaningless without knowing which generator ran
Scope (basename/wholename) as an argumentNo inference from pattern shape; no surpriseEvery match term is longer than a bare glob
Modifiers as a flag dictionaryAdditive; no combinatorial term namesA fourth positional argument before the flags
dedup_results off by defaultBackwards compatibilityThe natural multi-generator query double-counts
JSON instead of a syntaxNo lexer, no precedence, no escapingNobody types it; a shorthand had to be added anyway

Sources ​