FLPQ: Formal-Language-Constrained Path Querying#
- Release:
6.1.1
- Date:
Oct 15, 2026
Formal-language-constrained path querying (FLPQ) studies reachability in labeled directed graphs where a path from \(u\) to \(v\) counts only if its edge-label string belongs to a given formal language. FLPQ_Data currently provides data for context-free path queries (CFPQ); it is being extended to all classes of FLPQ: regular path queries (RPQ) and multiple context-free path queries (MCFPQ). This page records the design decisions — the query-class taxonomy, the grammar formalisms and file formats, and the target structure of the site, the package, and the dataset. Implementation happens in follow-up tasks; until then the existing CFPQ data and interfaces remain unchanged.
Query classes#
Query class |
Language class |
Formalism |
Status in the dataset |
|---|---|---|---|
RPQ |
Regular |
Regular expressions, NFA/DFA |
designed, no data |
CFPQ |
Context-free |
CFG/CNF (incl. indexed grammars) |
current |
MCFPQ |
Multiple context-free |
MCFG, \(d\)-MCFG(\(r\)) |
planned |
All three classes use the same query pattern: a language \(L\) constrains the paths of a labeled graph, and a pair \((u, v)\) is reachable if some string of \(L\) labels a path from \(u\) to \(v\). The classes differ only in the language class used for the constraint and in the formalism that specifies it. The existing data — the graphs catalog, the query templates, and the reachable pair counts — is CFPQ data; the shared graph catalog is reused by all classes.
Regular path queries#
RPQ constraints are regular languages over the graph’s edge labels. The design decision for this class:
Format. An RPQ query is a regular expression in the pyformlang
Regextext syntax, stored as a.refile and read byregex_from_text.Templates. As with CFG templates, an RPQ template is a named, parameterized regular expression instantiated per graph from its stored labels. The designed seed set consists of two canonical queries applicable to every graph:
reachability— \((L_1 \,|\, \dots \,|\, L_n)^*\) over all stored labels \(L_1, \dots, L_n\): unrestricted reachability;label_star— \(L^*\) for each stored label \(L\): the per-label transitive closure.
Additional templates may be added later together with real-world data; the stub pages in the RPQ section document both templates.
Placement. Query files live inside the self-contained graph archive under
queries/rpq/(see File structure); there are no standalone query or example archives. An RPQ may also be represented by a regular RSM (.rsm, see below).Data status. No real-world RPQ data exists yet — the templates are designed stubs. Real-world data will be provided later and enters the dataset via the graph archives (migration, task 48).
Recursive state machines#
A recursive state machine (RSM) [1] is a way to specify context-free
languages that resembles finite automata: it is a set of boxes, each box
being a deterministic finite automaton without ε-transitions over the union
alphabet of terminals and nonterminals, with a start state and final states.
There is no stack in the representation — recursion happens during
computation, when a transition labelled by a nonterminal invokes the
respective box. A grammar in Extended Backus-Naur Form (EBNF) — productions
N -> E where E is a regular expression over terminals and
nonterminals — maps to an RSM with one box per production.
RSMs are the query formalism of the GLL-based CFPQ algorithm [2], which evaluates EBNF queries natively and was evaluated on this dataset.
Every RSM defines a context-free language, and
cfg_from_rsa converts
it to a CFG (each box state becomes a nonterminal, each transition a
production). An RSM is therefore a valid alternative representation of a
CFPQ query — and of an RPQ when it is regular: no box transition is labelled
by a nonterminal.
Format#
A .rsm file uses one of two description styles, selected automatically by
rsa_from_text:
EBNF style — one production per line; each production becomes one box:
start: S S -> a* b S c B -> (x | y)+
Transition-system style — explicit boxes as labelled graphs with start and final states (a
[box <name>]section header selects this style):start: S [box S] start: 0 final: 2, 3 0 --a--> 1 1 --b--> 2 1 --c--> 3 [box B] start: 0 final: 1 0 --x--> 1
In both styles an optional start: <N> line names the start box (S by
default). Boxes are deterministic — a repeated (state, label) transition
is an error. rsa_to_text emits the EBNF style as the
canonical form, so a transition-system file round-trips to EBNF-style text.
Placement#
.rsm representation files are allowed in the queries/cfpq/ and
queries/rpq/ query directories of a graph archive (see
File structure). In rpq/ the RSM must be regular — no box
transition may be labelled by a nonterminal.
Multiple context-free languages#
MCFPQ constraints are multiple context-free languages (MCFLs), generated by multiple context-free grammars (MCFGs) (Seki et al., 1991). An MCFL is a class of languages strictly wider than the context-free languages: an MCFG performs simultaneous context-free parsing on several substrings of a word and can thus express bounded context-sensitivity between them. MCFLs are mildly context-sensitive: membership is decidable, and the classes form an infinite hierarchy parameterized by two integers.
A multiple context-free grammar of dimension \(d\) and rank \(r\), written \(d\)-MCFG(\(r\)), is a tuple \(\mathcal{G} = (\mathcal{N}, \Sigma, \mathcal{R}, \mathcal{S})\):
\(\mathcal{N}\) — nonterminals, each denoted as a predicate \(A(x_1, \dots, x_k)\) of arity \(k \leq d\);
\(\Sigma\) — terminals;
\(\mathcal{R}\) — rules. A basic rule is a nonterminal \(A(s_1, \dots, s_k)\) with each \(s_i\) a string of terminals (possibly empty). A production rule has the form
\[A_0(s_1, \dots, s_{k_0}) \leftarrow A_1(x_1^1, \dots, x_{k_1}^1), \dots, A_\ell(x_1^\ell, \dots, x_{k_\ell}^\ell)\]with \(\ell \leq r\), each head template \(s_i \in (\Sigma \cup X)^*\) over the rule’s variables \(X = \{x_j^i\}\), and each variable appearing at most once in \(s_1 \cdots s_{k_0}\);
\(\mathcal{S}\) — the start nonterminal, of arity 1.
A nonterminal of arity \(k\) derives a tuple of \(k\) strings simultaneously; the language of the grammar is \(\mathcal{L}(\mathcal{G}) = \{w \in \Sigma^* : \mathcal{S} \Rightarrow w\}\).
The hierarchy contains the familiar classes as special levels: context-free languages are exactly the 1-MCFLs, and tree adjoining languages (and head languages) fall in dimension 2. Membership of a word of length \(n\) is decidable in \(O(n^{d(r+1)})\) time.
In the path-querying setting, MCFL reachability generalizes CFPQ directly (Conrado et al., POPL 2025, ACM). On a labeled graph, an arity-\(k\) nonterminal denotes \(k\) independent paths: \(A[(u_1, v_1), \dots, (u_k, v_k)]\) holds iff there exist paths \(P_i : u_i \leadsto v_i\) such that \(A \Rightarrow (\lambda(P_1), \dots, \lambda(P_k))\). Because the start symbol always has arity 1, the reachable pairs \(\{(u, v) : \mathcal{S}[(u, v)]\}\) are defined exactly as for CFPQ, so the reference-answer data of this dataset extends to MCFPQ without changing its meaning. All-pairs reachability is computable in \(O(n^{2d+1})\) time for \(r = 1\) and \(O(n^{d(r+1)})\) for \(r > 1\). The paper’s motivating application is static program analysis: the MCFLs underapproximate interleaved Dyck reachability — the undecidable language behind combined call-context and field sensitivity — with tunable precision as the dimension grows.
MCFG grammar format (.mcfg)#
MCFPQ grammars are stored in text files with the .mcfg extension, one
rule per line, in a Datalog-like syntax that follows the predicate notation
of Conrado et al.. The syntax was
chosen deliberately: it reads like logic-programming rules, it subsumes CFGs
as the dimension-1 special case, and it matches the formalism used by the
reachability literature.
Lexical conventions#
Blank lines and lines starting with
#are ignored.Nonterminals — identifiers starting with an uppercase letter (
S,A,P1), consistent with the CFG convention of this dataset.Terminals — edge labels verbatim (
0,#,subClassOf,load_5).Variables — a lowercase letter followed by one or more digits (
x1,y2, …), mirroring the \(x^i\), \(y^j\) notation of the literature. A terminal must never match this pattern; the reader rejects such grammars.Empty string — the token
eps.Arrow —
<-between the head and the body of a production rule.
Rule forms#
A basic rule names a nonterminal with terminal-string arguments, each
argument a space-separated sequence of terminals or eps:
A(eps, eps)
A production rule has a head — a nonterminal whose arguments are templates mixing terminals and variables — and a body of comma-separated atoms carrying variables only:
S(x1 y1 # y2 x2) <- A(x1, x2), A(y1, y2)
Validation constraints#
The reader enforces, beyond parsing:
Arity consistency — every occurrence of a nonterminal carries the same number of arguments.
Variable uniqueness — all body variables of a rule are pairwise distinct.
No dangling variables — each body variable appears exactly once across the head templates (stricter than the minimal definition, which allows at most one appearance; every published example satisfies the stricter form).
Start symbol — defaults to
S, must have arity 1, and is overridable via an API parameter, consistent with the existingcnf/cfg/rsareaders.
The dimension \(d\) (maximum nonterminal arity) and the rank \(r\) (maximum number of body atoms) are computed from the rules and reported by the reader; the file carries no header to keep in sync.
Examples#
The 2-MCFG(2) for \(\mathcal{L} = \{w_1 w_2 \# w_2 w_1 \mid
w_1, w_2 \in \{0, 1\}^*\}\) from the literature (A parses equal pairs of
binary strings; S interleaves two such pairs around #):
A(eps, eps)
A(x1 0, x2 0) <- A(x1, x2)
A(x1 1, x2 1) <- A(x1, x2)
S(x1 y1 # y2 x2) <- A(x1, x2), A(y1, y2)
The same format subsumes CFGs: the dimension-1 grammar for \(\{0^n 1^n 1^m 0^m \mid n, m \geq 0\}\):
A(eps)
B(eps)
A(0 x1 1) <- A(x1)
B(1 x2 0) <- B(x2)
S(x1 x2) <- A(x1), B(x2)
Parser and relationship to existing formats#
No off-the-shelf library parses this syntax: the PyPI datalog package is
a hardware data-logger driver, pyDatalog is a Datalog engine without a
documented text parser (and standard Datalog heads take variables and
constants only, so MCFG head templates are outside its grammar), and no
MCFG/MCFL library exists on PyPI. The reader therefore uses lark (MIT license, actively maintained,
no required dependencies); its EBNF grammar doubles as executable
documentation of the format, and the semantic constraints above are checked
on the parse tree.
Existing formats are kept: CFPQ queries stay .cnf (the pyformlang
ecosystem), RPQ queries are regular-expression .re text files, and
MCFPQ uses .mcfg; all of them live inside the self-contained graph
archives (see “Dataset layout and migration” below). A .cnf <-> .mcfg
converter is a follow-up task.
Site structure#
The Dataset section has one sub-section per query class next to the shared graph catalog: each class reuses the existing layout — an index page with its templates and its “Applicable graphs” list, and one page per template — while the graph catalog stays shared.
Dataset
├── Graphs shared catalog (8 categories, 113 pages) — unchanged
├── CFPQ grammar templates (4 + indexed grammars) | applicable graphs
├── RPQ query templates (regular expressions) | applicable graphs
├── MCFPQ grammar templates (MCFG) | applicable graphs
└── Reachable pairs per-category tables; flat CSV download
There is no separate benchmarks section: benchmarking data and results are
not part of the dataset site. The per-class “applicable graphs” lists are
rendered from the reachable-pairs registry (reachable_pairs.csv) by a
utils/ generator and cross-link the shared per-graph pages — no graph page
is duplicated. The sidebar navigation depth (navigation_depth = 3 in
docs/conf.py) accommodates the section -> class -> template pages without
change.
Reachable pair counts#
The single flat table currently rendered on the
reachable pairs page is removed; it is too large to
scan. The page renders one table per graph category instead (columns:
Graph, Grammar, Reachable pairs), so every table stays small. The flat
table survives as a downloadable CSV — the single source of truth for
automatic processing — which gains a category and a query_class
column so it is self-describing. A generator script in utils/ (following the pattern of
utils/archive_sizes.py) renders both the per-category tables and the
count columns of the category pages from that CSV, so no count is
hand-maintained in two places.
Package structure#
The package is renamed from cfpq_data to flpq_data (PyPI
distribution flpq-data) at version 6.0.0, and the grammar module gains
the same query-class level as the site:
flpq_data/
├── config.py version
├── dataset/ registry + metadata API, machine-global cache with lazy access (graph_dir), reachable pairs
├── graphs/ class-agnostic I/O (mtx/rdf/txt), streaming format conversion, generators, utils
└── queries/ renamed from grammars/
├── cfpq/ existing generators/, readwrite/{cfg,cnf,cnf_template}, converters/, utils/
├── rpq/ readwrite/{regex,rsa} moved here; new regex-template generators
└── mcfpq/ new lark-based mcfg readwrite; generators
API changes#
download(name)becomesgraph_dir(name)and returns the self-contained directory — the graph plus itsqueries/tree — from the machine-global cache, downloading only on a miss (see “Core API: metadata, lazy access, and cache”);download_graph(name)anddownload(name)stay as deprecated aliases;download_grammars(template, graph_name=None)is deprecated: queries come with the graph archive (the example archives it downloaded forgraph_name=Noneare dropped);the old names stay in
flpq_dataas deprecated aliases;the graph registry list is replaced by the per-graph
registry.jsonand thegraph_names()function (see “Core API: metadata, lazy access, and cache”), andGRAMMAR_TEMPLATESbecomes the per-classCFPQ_TEMPLATES/RPQ_TEMPLATES/MCFPQ_TEMPLATES— they name the query files inside the archives, not downloadable archives;reachable_pairs()gains the category and query_class fields.
Distribution#
The new PyPI project flpq-data carries version 6.0.0. The existing
cfpq-data distribution is published at 6.0.0 as a thin shim that
depends on flpq-data and re-exports the old names with
DeprecationWarning, so scripts importing cfpq_data keep working
through one more release.
Dataset layout and migration#
The dataset on object storage moves to the 6.0.0 key prefix:
6.0.0/
└── graph/<name>.tar.gz self-contained — repackaged from 5.0.0
Graphs are large and class-agnostic, so they live under one shared prefix.
Queries no longer have their own archives: every graph archive is
self-contained and carries all queries that apply to the graph — the layout
is documented once in File structure (README.md, graph/*.mtx,
and queries/{cfpq,rpq,mcfpq}/ with one directory per query — its
representations plus a results.mtx — and a common description document).
Migration path:
Repackage every graph archive into the self-contained structure: move the per-graph grammars from the legacy
4.0.0/grammar/archives into onequeries/cfpq/<query>/directory each, add the per-queryresults.mtx(the constrained reachability facts), write the mandatory-questionREADME.md, and validate withflpq-check-archive(Archive structure checker).The example query archives (
4.0.0/grammar/example/) are dropped — every query file lives inside a graph archive.Re-point
DATASET_URLat the new prefix as part of the version bump (the rename task);GRAMMARS_URLdisappears with the separate grammar archives.The upload tool validates the structure before uploading (Upload to Yandex S3, Archive structure checker).
Core API: metadata, lazy access, and cache#
The package core is for end users — dataset utilisation for benchmarks and
statistical analysis. Dataset maintenance (archive validation, upload, merge,
registry generation) lives in the repo-only utils/ scripts and is never
installed with the wheel; nothing in flpq_data/ performs maintenance
work, and the maintenance scripts consume the package as a library. This
design responds to #140 (the graph archive was re-downloaded on every call)
and #147 (no way to get the category of a graph from the package); it is
tracked in hub issue #148.
Graph registry#
flpq_data/dataset/registry.json is the machine-readable per-graph
registry, bundled with the wheel for the current dataset version and
published to object storage as <version>/registry.json:
{ "version": "6.0.0",
"graphs": { "skos": { "category": "rdf", "num_nodes": 144,
"num_edges": 252, "size_mb": 0.003,
"sha256": "...",
"queries": [ {"class": "cfpq",
"name": "nested_parentheses_subClassOf",
"representations": ["cnf", "rsm"]} ] } } }
Field sources: the graph list and category from
reachable_pairs.csv; num_nodes/num_edges from the MTX headers of
the archive’s graph/ dir; size_mb from the stored object size;
sha256 of the .tar.gz; queries from the archive’s queries/
tree. The repo-only utils/generate_registry.py (pattern of
utils/archive_sizes.py) regenerates the file; it runs locally, never in
CI (no-network policy).
The metadata API never touches the network: graph_names() ->
list[str] (named to avoid clashing with the flpq_data.graphs
subpackage),
graph_info(name) -> GraphInfo (a frozen dataclass mirroring one registry
record), and categories() -> dict[str, list[str]] — the category-to-
graphs mapping of #147. The never-released GRAPHS/DATASET constants
are dropped; the future cfpq-data shim maps DATASET ->
graph_names().
Machine-global cache#
Downloaded data lives outside the package in a machine-global, per-user
cache shared across projects: the FLPQ_DATA_CACHE environment variable,
or the OS default from platformdirs.user_cache_dir("flpq-data") when it
is unset (~/.cache/flpq-data on Linux, ~/Library/Caches/flpq-data on
macOS, %LOCALAPPDATA%\flpq-data\Cache on Windows). The in-package data
dir (the old config.DATA/GRAPHS_DIR) is removed.
The cache mirrors the dataset layout; the dataset version is the root, so several versions coexist:
<root>/6.0.0/
├── reachable_pairs.csv
└── graph/<name>/<name>.tar.gz plus the unpacked contents in the same
folder (README.md, graph/*.mtx, queries/)
Lazy access#
graph_dir(name) is the single data accessor and is lazy: it returns the
local graph folder and downloads only when the cache is missing or invalid.
The name is validated against the bundled registry.
Hit:
<root>/<v>/graph/<name>/exists and the archive’s sha256 (sidecar.sha256written at install) matches the registry — return the path, zero network.verify=Truere-hashes the archive instead of trusting the sidecar.Miss: download to a temp file (
raise_for_status), stream-verify the sha256 against the registry, extract, check the single top-level dir, and move the folder into the cache atomically; any failure leaves the cache untouched.
download_graph/download remain as deprecated aliases of
graph_dir. reachable_pairs.csv resolves to the per-version cache copy
when present, else the bundled file; download_reachable_pairs() writes
into the cache.
Cache manipulation#
cache_root() -> Path— the effective cache root (env var or OS default).cached_versions() -> list[str]— dataset versions present in the cache.clear_cache(keep: str | None = None)— remove version directories under the root exceptkeep(Noneremoves all); returns the removed paths.