#!/usr/bin/env python3 """check-wai-quantum-names.py: every `wai.quantum.*` string in the tree is accounted for. A string of the quantum namespace is one of: - a string the registry registers (a capability or a record), or one of its named identifiers, unregistered labels or wire identifiers. The registry is one table, wai/wai-rs/src/codecs/registry.rs, exported by `wai registry export` as wai/registry/capabilities.json (CI holds the export to the table with `wai registry export --check`). This reads the export. A label there says what it names: a module of the quantum crates, a receipt kind, a kernel, a document with no schema file, or a string a test gives a reader to see it refused. - the name of a document a quantum crate writes, `wai.quantum./`, kept as a JSON Schema at `schemas/.v.json` beside the crate's manifest, whose `title` is the name (wai/wai-quantum/VERSIONING.md, *The schemas*). A name such a file gives as the `const` of a member (a document carried inside another) is accounted for the same way. The schema files are the list: a new document adds its file, and no line anywhere else. A string is `wai.quantum.` followed by dot-separated segments of letters, digits, `_` and `-`, and optionally `/` and a revision (`wai.quantum.circuit2`, `wai.quantum.cal.noise_learn`, `wai.quantum.job-result/1`, `wai.quantum.sv/run-wide`). A pattern `wai.quantum..*` is accounted for when some accounted string starts with `wai.quantum..`. A placeholder (`wai.quantum.`) or a prefix (`"wai.quantum."`) is not a string and is not read. Every text file git tracks or would track is read, except the changelog (wai/CHANGELOG.md and the entry files of wai/changes/): it records names as they were when each change was made. Binary files are not read. Findings, one line each: - a string accounted for by none of the above, with its file and line; - a quantum schema file whose `title` is not the name its file name gives; - a `wai.quantum.*` label or wire identifier that nothing outside the registry's own files (registry.rs, its export, REGISTRY.md) uses, so the list stays the set of strings in use; - a `wai.quantum.*` string the registry holds twice. check-wai-quantum-names.py [--root DIR] Exit 0 when every string is accounted for, 1 with one line a finding otherwise, 2 when the tree cannot be read. Standard library only. """ import json import os import re import subprocess import sys PREFIX = "wai.quantum." # A string of the namespace, with its revision and a trailing `.*` (a pattern). TOKEN = re.compile( r"wai\.quantum\.([A-Za-z0-9_][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_][A-Za-z0-9_-]*)*)" r"(/[A-Za-z0-9_-]+)?(\.\*)?" ) EXPORT = "wai/registry/capabilities.json" # The registry's own files: the table, its export and the page that prints it. REGISTRY_FILES = {"wai/wai-rs/src/codecs/registry.rs", EXPORT, "wai/REGISTRY.md"} # The record of past changes, read by nothing here. HISTORY = ("wai/CHANGELOG.md", "wai/changes/") SCHEMA_FILE = re.compile(r"(?:^|/)schemas/([a-z0-9_-]+(?:\.[a-z0-9_-]+)*)\.v([0-9]+)\.json$") def fail(msg): print(f"check-wai-quantum-names: {msg}", file=sys.stderr) sys.exit(2) def git(root, *args): r = subprocess.run(["git", "-C", root, *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE) return r.returncode, r.stdout.decode("utf-8", "replace"), r.stderr.decode("utf-8", "replace") def occurrences(root): """Every string of the namespace in the text files git tracks or would track: [(path, line, string, is_pattern)].""" rc, out, err = git(root, "grep", "-I", "-n", "-z", "--untracked", "-F", "-e", PREFIX) if rc == 1: return [] if rc != 0: fail(f"git grep failed ({rc}): {err.strip()}") found = [] for rec in out.split("\n"): if not rec: continue parts = rec.split("\0", 2) if len(parts) != 3: fail(f"unreadable git grep line: {rec[:120]!r}") path, line, text = parts if path == HISTORY[0] or path.startswith(HISTORY[1]): continue for m in TOKEN.finditer(text): name = PREFIX + m.group(1) + (m.group(2) or "") found.append((path, int(line), name, bool(m.group(3)))) return found def registry(root): """The strings the export holds, by table, and the quantum strings it holds twice.""" path = os.path.join(root, EXPORT) try: with open(path, encoding="utf-8") as fh: doc = json.load(fh) except (OSError, ValueError) as e: fail(f"cannot read {EXPORT}: {e}") tables = { "registered": [c["capability"] for c in doc.get("capabilities", [])], "identifier": [i["identifier"] for i in doc.get("identifiers", [])], "label": [l["label"] for l in doc.get("unregistered_labels", [])], "wire identifier": [w["name"] for w in doc.get("wire_identifiers", [])], } if not tables["registered"]: fail(f"{EXPORT} registers nothing") held = {} twice = [] for table, names in tables.items(): for n in names: if n.startswith(PREFIX): if n in held: twice.append(f"{EXPORT}: {n} is held twice ({held[n]}, {table})") held.setdefault(n, table) return tables, held, twice def schema_names(root): """The document names the quantum schema files give, and the findings about them.""" rc, out, err = git(root, "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "wai/") if rc != 0: fail(f"git ls-files failed ({rc}): {err.strip()}") names, findings = {}, [] def consts(v, acc): if isinstance(v, dict): for k, x in v.items(): if k == "const" and isinstance(x, str): acc.append(x) else: consts(x, acc) elif isinstance(v, list): for x in v: consts(x, acc) for path in sorted(p for p in out.split("\0") if p): m = SCHEMA_FILE.search(path) if not m or not os.path.isfile(os.path.join(root, path)): continue try: with open(os.path.join(root, path), encoding="utf-8") as fh: doc = json.load(fh) except (OSError, ValueError) as e: findings.append(f"{path}: not a JSON document ({e})") continue title = doc.get("title") if isinstance(doc, dict) else None if not (isinstance(title, str) and title.startswith(PREFIX)): continue want = f"{PREFIX}{m.group(1)}/{m.group(2)}" if title != want: findings.append(f"{path}: its title is {title}, but a schema of that file is named {want}") continue names.setdefault(title, path) acc = [] consts(doc, acc) for c in acc: t = TOKEN.fullmatch(c) if t and not t.group(3): names.setdefault(c, path) return names, findings def main(argv): root = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) if len(argv) == 2 and argv[0] == "--root": root = os.path.abspath(argv[1]) elif argv: fail("usage: check-wai-quantum-names.py [--root DIR]") tables, held, findings = registry(root) accounted = {n for names in tables.values() for n in names} schemas, schema_findings = schema_names(root) findings += schema_findings accounted |= set(schemas) used_outside = set() seen = 0 for path, line, name, pattern in occurrences(root): seen += 1 if path not in REGISTRY_FILES: used_outside.add(name) if pattern: stem = name + "." if not any(a.startswith(stem) for a in accounted): findings.append(f"{path}:{line}: {name}.* matches no registered string, label or schema's name") continue if name not in accounted: findings.append(f"{path}:{line}: {name} is not registered, listed as a label or wire identifier, " f"or the name of a quantum schema file") for n, table in sorted(held.items()): if table in ("label", "wire identifier") and n not in used_outside: findings.append(f"{EXPORT}: the {table} {n} is used nowhere outside the registry's own files") if findings: for f in findings: print(f) sys.stdout.flush() print(f"\n{len(findings)} finding(s). A new `wai.quantum.*` string is registered, or listed with what it " "names in UNREGISTERED_LABELS (wai/wai-rs/src/codecs/registry.rs, beside its group), then " "`wai registry export` is run; a document's name is its schema file " "(`schemas/.v.json`, its `title` the name).", file=sys.stderr) return 1 print(f"check-wai-quantum-names: every wai.quantum string is accounted for " f"({seen} occurrences read; {len(schemas)} named by schema files)") return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))