gisel-ab 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. gisel_ab/__init__.py +120 -0
  2. gisel_ab/__main__.py +8 -0
  3. gisel_ab/baseline.py +145 -0
  4. gisel_ab/bugreport.py +113 -0
  5. gisel_ab/cli.py +150 -0
  6. gisel_ab/cohort.py +704 -0
  7. gisel_ab/cpp/aarch64_generated_ctor.cpp +23 -0
  8. gisel_ab/cpp/aarch64_query_runner.cpp +380 -0
  9. gisel_ab/cpp/arm_generated_ctor.cpp +18 -0
  10. gisel_ab/cpp/arm_query_runner.cpp +58 -0
  11. gisel_ab/cpp/m68k_generated_ctor.cpp +21 -0
  12. gisel_ab/cpp/m68k_query_runner.cpp +43 -0
  13. gisel_ab/cpp/mips_generated_ctor.cpp +16 -0
  14. gisel_ab/cpp/mips_query_runner.cpp +52 -0
  15. gisel_ab/cpp/ppc_generated_ctor.cpp +16 -0
  16. gisel_ab/cpp/ppc_query_runner.cpp +46 -0
  17. gisel_ab/cpp/query-recovery.h +229 -0
  18. gisel_ab/cpp/query_runner.cpp +419 -0
  19. gisel_ab/cpp/query_runner_common.h +417 -0
  20. gisel_ab/cpp/riscv_generated_ctor.cpp +25 -0
  21. gisel_ab/cpp/riscv_query_runner.cpp +379 -0
  22. gisel_ab/cpp/spirv_generated_ctor.cpp +24 -0
  23. gisel_ab/cpp/spirv_query_runner.cpp +98 -0
  24. gisel_ab/cpp/wasm_generated_ctor.cpp +17 -0
  25. gisel_ab/cpp/wasm_query_runner.cpp +52 -0
  26. gisel_ab/cpp/x86_generated_ctor.cpp +24 -0
  27. gisel_ab/data/aarch64-contract.json +102 -0
  28. gisel_ab/data/baseline-2026-11-12.json +1684 -0
  29. gisel_ab/data/closure/aarch64-D.json +156 -0
  30. gisel_ab/data/closure/aarch64-G.json +75 -0
  31. gisel_ab/data/closure/arm-D.json +50 -0
  32. gisel_ab/data/closure/arm-G.json +57 -0
  33. gisel_ab/data/closure/m68k-D.json +46 -0
  34. gisel_ab/data/closure/m68k-G.json +54 -0
  35. gisel_ab/data/closure/mips-D.json +50 -0
  36. gisel_ab/data/closure/mips-G.json +87 -0
  37. gisel_ab/data/closure/ppc-D.json +49 -0
  38. gisel_ab/data/closure/ppc-G.json +56 -0
  39. gisel_ab/data/closure/riscv-D.json +59 -0
  40. gisel_ab/data/closure/riscv-G.json +66 -0
  41. gisel_ab/data/closure/spirv-D.json +58 -0
  42. gisel_ab/data/closure/spirv-G.json +58 -0
  43. gisel_ab/data/closure/wasm-D.json +56 -0
  44. gisel_ab/data/closure/wasm-G.json +62 -0
  45. gisel_ab/data/closure/x86-D.json +60 -0
  46. gisel_ab/data/closure/x86-G.json +66 -0
  47. gisel_ab/data/domain.json +139 -0
  48. gisel_ab/data/mir-configs.json +444 -0
  49. gisel_ab/data/mir-shapes.jsonc +1168 -0
  50. gisel_ab/data/riscv-contract.json +102 -0
  51. gisel_ab/data/suite/README.md +81 -0
  52. gisel_ab/data/suite/aarch64/official.answers.jsonl +12559 -0
  53. gisel_ab/data/suite/aarch64/queries.jsonl +12559 -0
  54. gisel_ab/data/suite/aarch64/suite.json +43 -0
  55. gisel_ab/data/suite/m68k/official.answers.jsonl +35 -0
  56. gisel_ab/data/suite/m68k/queries.jsonl +35 -0
  57. gisel_ab/data/suite/m68k/suite.json +24 -0
  58. gisel_ab/data/suite/riscv/official.answers.jsonl +6086 -0
  59. gisel_ab/data/suite/riscv/queries.jsonl +6086 -0
  60. gisel_ab/data/suite/riscv/suite.json +41 -0
  61. gisel_ab/data/suite/x86/official.answers.jsonl +3014 -0
  62. gisel_ab/data/suite/x86/queries.jsonl +3014 -0
  63. gisel_ab/data/suite/x86/suite.json +33 -0
  64. gisel_ab/data/targets/aarch64.json +39 -0
  65. gisel_ab/data/targets/arm.json +39 -0
  66. gisel_ab/data/targets/m68k.json +39 -0
  67. gisel_ab/data/targets/mips.json +39 -0
  68. gisel_ab/data/targets/ppc.json +39 -0
  69. gisel_ab/data/targets/riscv.json +39 -0
  70. gisel_ab/data/targets/schema.json +266 -0
  71. gisel_ab/data/targets/spirv.json +39 -0
  72. gisel_ab/data/targets/wasm.json +39 -0
  73. gisel_ab/data/targets/x86-ctor.json +39 -0
  74. gisel_ab/data/targets/x86.json +39 -0
  75. gisel_ab/data/x86-contract.json +93 -0
  76. gisel_ab/doctor.py +320 -0
  77. gisel_ab/domain_plan.py +130 -0
  78. gisel_ab/evidence.py +892 -0
  79. gisel_ab/l0/__init__.py +0 -0
  80. gisel_ab/l0/attribute.py +286 -0
  81. gisel_ab/l0/closure.py +255 -0
  82. gisel_ab/l0/diff.py +388 -0
  83. gisel_ab/l0/g_attribution.py +93 -0
  84. gisel_ab/l0/inject_opcode.py +123 -0
  85. gisel_ab/l0/inject_predicate.py +150 -0
  86. gisel_ab/l0/queries.py +215 -0
  87. gisel_ab/l0/run.py +688 -0
  88. gisel_ab/l0/swap.py +257 -0
  89. gisel_ab/l0/u_attribution.py +192 -0
  90. gisel_ab/l3.py +271 -0
  91. gisel_ab/mir/__init__.py +0 -0
  92. gisel_ab/mir/config_check.py +144 -0
  93. gisel_ab/mir/from_query.py +717 -0
  94. gisel_ab/mir/libs.py +252 -0
  95. gisel_ab/mir/replay.py +548 -0
  96. gisel_ab/paths.py +151 -0
  97. gisel_ab/refresh_counts.py +80 -0
  98. gisel_ab/replay_frozen.py +154 -0
  99. gisel_ab/setup.py +92 -0
  100. gisel_ab/suite.py +390 -0
  101. gisel_ab/targets.py +287 -0
  102. gisel_ab/vertical.py +654 -0
  103. gisel_ab-0.9.0.dist-info/METADATA +349 -0
  104. gisel_ab-0.9.0.dist-info/RECORD +107 -0
  105. gisel_ab-0.9.0.dist-info/WHEEL +5 -0
  106. gisel_ab-0.9.0.dist-info/entry_points.txt +2 -0
  107. gisel_ab-0.9.0.dist-info/top_level.txt +1 -0
gisel_ab/__init__.py ADDED
@@ -0,0 +1,120 @@
1
+ """gisel-ab: an A/B workbench around LLVM GlobalISel legalizer rules.
2
+
3
+ The package has one job: answer "does the legalizer this repository replaced
4
+ behave like the one it came from?" Everything a caller needs to locate lives
5
+ here, so no module has to guess its depth in the tree.
6
+
7
+ PKG this package (code, data, cpp)
8
+ AB_ROOT the checkout that contains ``src/`` and ``scripts/``
9
+ DATA target registry, contracts, suite, domain (package data)
10
+ CPP query-runner C++ sources (package data)
11
+ SCRIPTS the shell entry points (checkout only)
12
+ EVIDENCE the frozen evidence tree (checkout only)
13
+
14
+ ``DATA`` and ``CPP`` travel with an installed wheel; ``SCRIPTS`` and
15
+ ``EVIDENCE`` do not, and the commands that need them say so instead of
16
+ raising a FileNotFoundError three levels down.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import importlib.util
22
+ import os
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ PKG = Path(__file__).resolve().parent
27
+ SRC = PKG.parent
28
+ AB_ROOT = SRC.parent
29
+ DATA = PKG / "data"
30
+ CPP = PKG / "cpp"
31
+ SCRIPTS = AB_ROOT / "scripts"
32
+ EVIDENCE = AB_ROOT / "evidence"
33
+
34
+
35
+ def _gisel_package_dir() -> Path | None:
36
+ """The installed ``gisel`` package directory, when it is importable."""
37
+ try:
38
+ spec = importlib.util.find_spec("gisel")
39
+ except (ImportError, ValueError):
40
+ return None
41
+ if spec is None or not spec.submodule_search_locations:
42
+ return None
43
+ return Path(next(iter(spec.submodule_search_locations))).resolve()
44
+
45
+
46
+ def gisel_src(explicit=None) -> Path:
47
+ """A directory that makes ``import gisel`` work.
48
+
49
+ ``GISEL_SRC`` wins; then a sibling checkout; then the installed package,
50
+ whose parent is the directory that belongs on ``sys.path``. A path is
51
+ returned even when nothing is found, so the caller's error names the
52
+ place it looked.
53
+ """
54
+ value = explicit or os.environ.get("GISEL_SRC")
55
+ if value:
56
+ return Path(value).expanduser().resolve()
57
+ sibling = (AB_ROOT.parent / "gisel").resolve()
58
+ if (sibling / "gisel" / "__init__.py").is_file():
59
+ return sibling
60
+ package = _gisel_package_dir()
61
+ if package is not None:
62
+ return package.parent
63
+ return sibling
64
+
65
+
66
+ def gisel_package() -> Path | None:
67
+ """The ``gisel`` package directory, from a checkout or an install."""
68
+ root = gisel_src()
69
+ for candidate in (root / "gisel", root):
70
+ if (candidate / "__init__.py").is_file():
71
+ return candidate
72
+ return _gisel_package_dir()
73
+
74
+
75
+ def gisel_data(name: str) -> Path | None:
76
+ """The ``corpus``/``cases`` directory, checkout first, wheel second.
77
+
78
+ The wheel ships byte-identical copies under ``gisel/data``; the checkout
79
+ keeps the canonical copies under ``tests/``.
80
+ """
81
+ package = gisel_package()
82
+ roots = [package.parent, package] if package is not None else [gisel_src()]
83
+ for root in roots:
84
+ for candidate in (root / "tests" / name,
85
+ root / "gisel" / "data" / name,
86
+ root / "data" / name):
87
+ if candidate.is_dir():
88
+ return candidate.resolve()
89
+ return None
90
+
91
+
92
+ def gisel_path_entry() -> str:
93
+ """The ``sys.path`` entry that makes ``import gisel`` work."""
94
+ return str(gisel_src())
95
+
96
+
97
+ def ensure_gisel_on_path() -> Path:
98
+ path = gisel_src()
99
+ entry = str(path)
100
+ if entry not in sys.path:
101
+ sys.path.insert(0, entry)
102
+ return path
103
+
104
+
105
+ def child_env(env=None) -> dict:
106
+ """A copy of ``env`` (default: os.environ) that children can import from.
107
+
108
+ Subprocesses are started as ``python3 -m gisel_ab.<verb>``; they need
109
+ ``SRC`` on ``PYTHONPATH`` and gisel on it too unless it is installed.
110
+ """
111
+ out = dict(os.environ if env is None else env)
112
+ parts = [str(SRC)]
113
+ gisel = gisel_path_entry()
114
+ if Path(gisel).is_dir():
115
+ parts.append(gisel)
116
+ current = out.get("PYTHONPATH")
117
+ if current:
118
+ parts.extend(part for part in current.split(os.pathsep) if part)
119
+ out["PYTHONPATH"] = os.pathsep.join(dict.fromkeys(parts))
120
+ return out
gisel_ab/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """``python3 -m gisel_ab`` is the installed entry point (needs ``src`` on PYTHONPATH)."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
gisel_ab/baseline.py ADDED
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """Record the facts a remediation run is measured against.
3
+
4
+ baseline.py [--out data/baseline-YYYY-MM-DD.json]
5
+
6
+ The file is deliberately boring: repository heads, corpus/artifact/feature
7
+ hashes, the target registry, and the numbers already pinned by EVIDENCE.json.
8
+ It does not run a build and it does not change behavior. Later waves compare
9
+ their output with this snapshot instead of trusting a document's memory.
10
+
11
+ Exit codes: 0 = written; 2 = a required input is missing or unreadable.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import hashlib
18
+ import json
19
+ import subprocess
20
+ import sys
21
+ from datetime import date
22
+ from pathlib import Path
23
+
24
+ from gisel_ab import paths
25
+ from gisel_ab import targets as target_specs
26
+
27
+ PKG = Path(__file__).resolve().parent
28
+ AB = PKG.parents[1]
29
+ DATA = PKG / "data"
30
+ GISEL = Path(__import__("os").environ.get("GISEL_SRC", AB.parent / "gisel"))
31
+ UPSTREAM = paths.sibling(AB.parent, "UPSTREAM_SOURCE",
32
+ "gisel-anchor", "upstream-source")
33
+
34
+
35
+ def sha256(path: Path) -> str:
36
+ return hashlib.sha256(path.read_bytes()).hexdigest()
37
+
38
+
39
+ def repo_state(path: Path) -> dict:
40
+ def git(*args: str) -> str:
41
+ proc = subprocess.run(
42
+ ["git", "-C", str(path), *args],
43
+ capture_output=True, text=True)
44
+ if proc.returncode != 0:
45
+ raise SystemExit("git %s failed in %s: %s"
46
+ % (" ".join(args), path, proc.stderr.strip()))
47
+ return proc.stdout.strip()
48
+
49
+ return {
50
+ "head": git("rev-parse", "HEAD"),
51
+ "branch": git("rev-parse", "--abbrev-ref", "HEAD"),
52
+ "dirty": bool(git("status", "--porcelain", "--untracked-files=no")),
53
+ }
54
+
55
+
56
+ def hashes(root: Path, pattern: str) -> dict:
57
+ return {path.relative_to(root).as_posix(): sha256(path)
58
+ for path in sorted(root.glob(pattern)) if path.is_file()}
59
+
60
+
61
+ def evidence_summary(path: Path) -> dict:
62
+ manifest = json.loads(path.read_text())
63
+ entries = {}
64
+ for entry in manifest.get("entries", []):
65
+ expect = entry.get("expect", {})
66
+ entries[entry["id"]] = {
67
+ "direction": entry.get("direction"),
68
+ "target": entry.get("target"),
69
+ "expect": {key: expect[key] for key in sorted(expect)},
70
+ "snapshot": entry.get("snapshot"),
71
+ }
72
+ return {
73
+ "sha256": sha256(path),
74
+ "schema": manifest.get("schema"),
75
+ "generated": manifest.get("generated"),
76
+ "entries": entries,
77
+ }
78
+
79
+
80
+ def target_registry(root: Path) -> dict:
81
+ out = {}
82
+ for path in sorted(target_specs.TARGET_DIR.glob("*.json")):
83
+ data = json.loads(path.read_text())
84
+ out[path.stem] = {
85
+ "sha256": sha256(path),
86
+ "strategy": data.get("strategy"),
87
+ "query_count": data.get("query_count"),
88
+ }
89
+ return out
90
+
91
+
92
+ def build() -> dict:
93
+ for path in (GISEL, UPSTREAM, AB):
94
+ if not path.is_dir():
95
+ raise SystemExit("missing checkout: %s" % path)
96
+ return {
97
+ "snapshot": "pre-remediation",
98
+ "repos": {
99
+ "gisel": repo_state(GISEL),
100
+ "ab": repo_state(AB),
101
+ "upstream-source": repo_state(UPSTREAM),
102
+ },
103
+ "gisel": {
104
+ "corpora": hashes(GISEL, "tests/corpus/*.gisel"),
105
+ "artifacts": hashes(GISEL, "tests/artifacts/*.inc"),
106
+ "features": hashes(GISEL, "tests/cases/*features*.json"),
107
+ "domain_queries": hashes(GISEL, "tests/cases/*domain*.jsonl"),
108
+ },
109
+ "ab": {
110
+ "evidence": evidence_summary(AB / "EVIDENCE.json"),
111
+ "targets": target_registry(AB),
112
+ "contracts": hashes(DATA, "*-contract.json"),
113
+ "gate_scripts": {
114
+ name: sha256(AB / name)
115
+ for name in (
116
+ "src/gisel_ab/l0/diff.py",
117
+ "src/gisel_ab/l0/queries.py",
118
+ "scripts/verify-evidence.sh",
119
+ "scripts/e2e/run-u-end-to-end.sh",
120
+ "scripts/negative/run-g-negative.sh",
121
+ "scripts/negative/run-predicate-negative.sh",
122
+ )
123
+ },
124
+ },
125
+ "upstream_source": {
126
+ "pin_manifest": hashes(UPSTREAM, "pin/*.json"),
127
+ "oracle_queries": hashes(UPSTREAM, "oracle/*/*/queries.jsonl*"),
128
+ },
129
+ }
130
+
131
+
132
+ def main(argv=None) -> int:
133
+ parser = argparse.ArgumentParser(prog="baseline")
134
+ parser.add_argument("--out", type=Path, default=None)
135
+ args = parser.parse_args(argv)
136
+ out = args.out or (DATA / ("baseline-%s.json" % date.today()))
137
+ data = build()
138
+ out.parent.mkdir(parents=True, exist_ok=True)
139
+ out.write_text(json.dumps(data, indent=1, sort_keys=True) + "\n")
140
+ print(str(out))
141
+ return 0
142
+
143
+
144
+ if __name__ == "__main__":
145
+ sys.exit(main())
gisel_ab/bugreport.py ADDED
@@ -0,0 +1,113 @@
1
+ """`gisel-ab bug-report`: one JSON file with the facts a reader would ask for.
2
+
3
+ gisel-ab bug-report # stdout
4
+ gisel-ab bug-report --out bug.json # write a file
5
+
6
+ It collects versions, the cohort record, the doctor verdict and the last
7
+ run's ledger line. It deliberately does not collect the evidence tree: a bug
8
+ report is small enough to paste into an issue, and the evidence is fetchable
9
+ from the cohort release.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import platform
17
+ import sys
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+
21
+ from gisel_ab import doctor
22
+
23
+ SCHEMA = 1
24
+
25
+
26
+ def _installed(name: str):
27
+ try:
28
+ import importlib.metadata as metadata
29
+ return metadata.version(name)
30
+ except Exception:
31
+ return None
32
+
33
+
34
+ def _module_version(name: str):
35
+ try:
36
+ module = __import__(name)
37
+ except Exception:
38
+ return None
39
+ return getattr(module, "__version__", None)
40
+
41
+
42
+ def _release() -> dict:
43
+ path = doctor.AB_ROOT / "RELEASE.json"
44
+ try:
45
+ doc = json.loads(path.read_text())
46
+ except (OSError, ValueError):
47
+ return {}
48
+ keep = ("cohort", "version", "repos", "generated_at")
49
+ return {key: doc[key] for key in keep if key in doc}
50
+
51
+
52
+ def _last_run() -> dict | None:
53
+ ledgers = sorted((doctor.AB_ROOT / ".ab").glob("*/ledger.jsonl"))
54
+ if not ledgers:
55
+ return None
56
+ path = ledgers[-1]
57
+ lines = path.read_text().strip().splitlines()
58
+ if not lines:
59
+ return None
60
+ try:
61
+ record = json.loads(lines[-1])
62
+ except ValueError:
63
+ return {"target": path.parent.name, "ledger": str(path),
64
+ "error": "the last line is not JSON"}
65
+ return {"target": path.parent.name, "ledger": str(path), "record": record}
66
+
67
+
68
+ def report(tree=None, build=None) -> dict:
69
+ from gisel_ab.cli import _version
70
+ checks = doctor.checks(tree, build)
71
+ return {
72
+ "schema": SCHEMA,
73
+ "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
74
+ "python": sys.version.split()[0],
75
+ "platform": platform.platform(),
76
+ "versions": {
77
+ "gisel-ab": _version(),
78
+ "gisel-anchor": _module_version("gisel_anchor"),
79
+ "gisel": _module_version("gisel"),
80
+ "installed.gisel-ab": _installed("gisel-ab"),
81
+ "installed.gisel-anchor": _installed("gisel-anchor"),
82
+ "installed.gisel": _installed("gisel"),
83
+ },
84
+ "cohort": _release(),
85
+ "doctor": {"ok": not any(c.status == doctor.FAIL for c in checks),
86
+ "checks": [c.as_dict() for c in checks]},
87
+ "last_run": _last_run(),
88
+ }
89
+
90
+
91
+ def main(argv=None) -> int:
92
+ ap = argparse.ArgumentParser(
93
+ prog="gisel-ab bug-report",
94
+ description="collect versions, the cohort record and the doctor verdict "
95
+ "into one JSON file (no evidence tree)")
96
+ ap.add_argument("--out", help="write here instead of stdout")
97
+ ap.add_argument("--tree", help="LLVM checkout (default: configured)")
98
+ ap.add_argument("--build", help="ninja build (default: configured)")
99
+ args = ap.parse_args(argv)
100
+ text = json.dumps(report(args.tree, args.build), indent=2,
101
+ sort_keys=True) + "\n"
102
+ if args.out:
103
+ path = Path(args.out).expanduser()
104
+ path.parent.mkdir(parents=True, exist_ok=True)
105
+ path.write_text(text)
106
+ print("wrote %s" % path)
107
+ return 0
108
+ print(text, end="")
109
+ return 0
110
+
111
+
112
+ if __name__ == "__main__":
113
+ sys.exit(main())
gisel_ab/cli.py ADDED
@@ -0,0 +1,150 @@
1
+ """One verb table for the workbench: ``ab.py <verb> ...``.
2
+
3
+ Before this module each tool was its own script with its own parser, and the
4
+ documentation had to name six different entry points. There is still one
5
+ implementation per verb; this module only routes. Imports are lazy so
6
+ ``--help`` works in an environment without LLVM, gisel, or the evidence tree.
7
+
8
+ Verbs that need a checkout rather than an install say so in their own error
9
+ message; nothing here reads the tree.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ # verb -> (module, callable, prepend the verb to argv)
20
+ #
21
+ # ``run``/``mir``/``verify`` share one parser in ``l0/run.py``: the harness
22
+ # builds, answers and diffs in one process, so they must stay in one module.
23
+ VERBS = {
24
+ "doctor": (("gisel_ab.doctor", "main", False),
25
+ "check tree/build/gisel/state before a run (read-only)"),
26
+ "bug-report": (("gisel_ab.bugreport", "main", False),
27
+ "collect versions, cohort and doctor into one JSON"),
28
+ "setup": (("gisel_ab.setup", "main", False),
29
+ "record the LLVM tree/build/evidence paths (no export)"),
30
+ "run": (("gisel_ab.l0.run", "main", True),
31
+ "build both sides, answer, diff, restore"),
32
+ "mir": (("gisel_ab.l0.run", "main", True),
33
+ "L2: answerless queries -> MIR -> replay both libraries"),
34
+ "verify": (("gisel_ab.l0.run", "main", True),
35
+ "check the build is back on the baseline library"),
36
+ "diff": (("gisel_ab.l0.diff", "main", False),
37
+ "compare two answer files (action or full)"),
38
+ "l3": (("gisel_ab.l3", "main", False),
39
+ "run check-llvm twice and compare it test by test"),
40
+ "suite": (("gisel_ab.suite", "main", False),
41
+ "check/refresh the per-target answer snapshots"),
42
+ "vertical": (("gisel_ab.vertical", "main", False),
43
+ "run or verify one target's whole slice"),
44
+ "cohort": (("gisel_ab.cohort", "main", False),
45
+ "plan, check, tag and verify a frozen cohort"),
46
+ "evidence": (("gisel_ab.evidence", "main", False),
47
+ "verify EVIDENCE.json against live or frozen artifacts"),
48
+ "plan": (("gisel_ab.domain_plan", "main", True),
49
+ "print one target's domain plan (no build)"),
50
+ }
51
+
52
+ # Not in the front page but still reachable: they are the internals the ten
53
+ # verbs call, and keeping them addressable is how a failure gets reproduced.
54
+ INTERNALS = {
55
+ "attribute": ("gisel_ab.l0.attribute", "main"),
56
+ "closure": ("gisel_ab.l0.closure", "main"),
57
+ "queries": ("gisel_ab.l0.queries", "main"),
58
+ "swap": ("gisel_ab.l0.swap", "main"),
59
+ "inject": ("gisel_ab.l0.inject_predicate", "main"),
60
+ "baseline": ("gisel_ab.baseline", "main"),
61
+ "replay-frozen": ("gisel_ab.replay_frozen", "main"),
62
+ "refresh-counts": ("gisel_ab.refresh_counts", "main"),
63
+ "mir-from-query": ("gisel_ab.mir.from_query", "main"),
64
+ "mir-replay": ("gisel_ab.mir.replay", "main"),
65
+ "mir-config-check": ("gisel_ab.mir.config_check", "main"),
66
+ }
67
+
68
+
69
+ def _version() -> str:
70
+ """The checkout's version when there is one, else the installed metadata.
71
+
72
+ The source wins so `python3 ab.py --version` reflects the tree being
73
+ edited, even when a stale editable install is still on the path.
74
+ """
75
+ pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
76
+ try:
77
+ text = pyproject.read_text()
78
+ except OSError:
79
+ text = ""
80
+ match = re.search(r'^version\s*=\s*"([^"]+)"', text, re.M)
81
+ if match:
82
+ return match.group(1)
83
+ try:
84
+ from importlib.metadata import PackageNotFoundError, version
85
+ try:
86
+ return version("gisel-ab")
87
+ except PackageNotFoundError:
88
+ pass
89
+ except ImportError:
90
+ pass
91
+ return "unknown"
92
+
93
+
94
+ # What a verb needs before it can run; the help groups by this so a reader
95
+ # can start with the offline half.
96
+ NEEDS_BUILD = {"run", "mir", "verify", "l3", "vertical"}
97
+ NEEDS_TREE = {"cohort"}
98
+
99
+
100
+ def _usage() -> str:
101
+ width = max(len(name) for name in VERBS)
102
+ groups = (
103
+ ("without LLVM", [name for name in VERBS
104
+ if name not in NEEDS_BUILD | NEEDS_TREE]),
105
+ ("with an LLVM tree and build", [name for name in VERBS
106
+ if name in NEEDS_BUILD]),
107
+ ("with a git tree (no build)", [name for name in VERBS
108
+ if name in NEEDS_TREE]),
109
+ )
110
+ chunks = []
111
+ for title, names in groups:
112
+ chunks.append(" %s:" % title)
113
+ for name in names:
114
+ chunks.append(" %-*s %s" % (width, name, VERBS[name][1]))
115
+ return (
116
+ "usage: gisel-ab <verb> [options]\n"
117
+ " (checkout: python3 ab.py <verb>)\n"
118
+ "\n"
119
+ "verbs (each takes --help; gisel-ab --version):\n"
120
+ + "\n".join(chunks)
121
+ + "\n\n"
122
+ "Internals stay callable for reproduction: %s\n"
123
+ % ", ".join(sorted(INTERNALS))
124
+ )
125
+
126
+
127
+ def main(argv=None) -> int:
128
+ argv = list(sys.argv[1:] if argv is None else argv)
129
+ if argv and argv[0] in ("-V", "--version"):
130
+ print("gisel-ab %s" % _version())
131
+ return 0
132
+ if not argv or argv[0] in ("-h", "--help", "help"):
133
+ print(_usage(), end="")
134
+ return 0 if argv else 2
135
+ verb, rest = argv[0], argv[1:]
136
+ if verb in INTERNALS:
137
+ module, name = INTERNALS[verb]
138
+ return getattr(importlib.import_module(module), name)(rest) or 0
139
+ entry = VERBS.get(verb)
140
+ if entry is None:
141
+ print("ab: unknown verb %r\n\n%s" % (verb, _usage()), file=sys.stderr,
142
+ end="")
143
+ return 2
144
+ (module, name, prepend), _ = entry
145
+ args = [verb, *rest] if prepend else rest
146
+ return getattr(importlib.import_module(module), name)(args) or 0
147
+
148
+
149
+ if __name__ == "__main__":
150
+ sys.exit(main())