axsent 0.1.0__tar.gz

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.
axsent-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vincent Gonzalez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
axsent-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: axsent
3
+ Version: 0.1.0
4
+ Summary: Measure what a formal library assumes: Rocq, Agda and Isabelle, from source, with nothing built
5
+ Author-email: Vincent Gonzalez <vincegonzalez@me.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/vince-gonzalez/axsent
8
+ Project-URL: Paper, https://doi.org/10.5281/zenodo.22148932
9
+ Keywords: axiom provenance,formal verification,proof assistants,Rocq,Agda,Isabelle,dependency analysis
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
14
+ Classifier: Topic :: Software Development :: Quality Assurance
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ ```
21
+ ╔════════════════════════════════════════════════════════════════════════════════════════════╗
22
+ ║ ║
23
+ ║ █████╗ ██╗ ██╗███████╗███████╗███╗ ██╗████████╗ ║
24
+ ║ ██╔══██╗╚██╗██╔╝██╔════╝██╔════╝████╗ ██║╚══██╔══╝ ║
25
+ ║ ███████║ ╚███╔╝ ███████╗█████╗ ██╔██╗ ██║ ██║ ║
26
+ ║ ██╔══██║ ██╔██╗ ╚════██║██╔══╝ ██║╚██╗██║ ██║ ║
27
+ ║ ██║ ██║██╔╝ ██╗███████║███████╗██║ ╚████║ ██║ ║
28
+ ║ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ║
29
+ ║ ║
30
+ ║ what a formal library assumes ║
31
+ ║ ║
32
+ ╚════════════════════════════════════════════════════════════════════════════════════════════╝
33
+ ```
34
+
35
+ An accent is a dependence on something local that travels with whoever carries
36
+ it. An inherited axiom is the same thing: a library speaks in the assumptions of
37
+ wherever it came from, and most of what it carries it never chose.
38
+
39
+ Rocq, Agda and Isabelle each let a development assert something without proving
40
+ it, and each records the dependency edges that carry the assertion outward.
41
+ `axsent` reads those declarations and those edges from source text, with no
42
+ proof assistant installed and nothing built.
43
+
44
+ ```bash
45
+ pip install axsent
46
+ ```
47
+
48
+ ```bash
49
+ axsent rocq path/to/stdlib/theories
50
+ axsent agda path/to/agda-stdlib/src
51
+ axsent isabelle path/to/afp/thys
52
+ ```
53
+
54
+ ## Why counts should not be totalled
55
+
56
+ A library declares two kinds of assumption and both use the same keyword.
57
+
58
+ **Mathematical** — excluded middle, choice, functional extensionality, proof
59
+ irrelevance. Claims about mathematics. A result depending on one is classical
60
+ rather than constructive.
61
+
62
+ **Interface** — that native 63-bit arithmetic behaves as specified, that a
63
+ mutable array returns what was written to it, that a foreign function returns
64
+ what its type says. Claims about the machine and the runtime. A result
65
+ depending on one is not thereby classical.
66
+
67
+ Rocq's Corelib declares **five times** as many axioms as the Rocq standard
68
+ library and makes no mathematical assumption at all: all 73 specify primitive
69
+ machine arithmetic, native arrays, floats and strings. Every one of the 16
70
+ postulating modules in the Agda standard library is a foreign-function or
71
+ operating-system interface. A single figure hides which of the two you are
72
+ looking at.
73
+
74
+ ## What it is not
75
+
76
+ It does not read Lean or Metamath. [gonzalgo](https://pypi.org/project/gonzalgo/)
77
+ and [mmforge](https://pypi.org/project/mmforge/) do that. This is a separate
78
+ instrument for separate systems, not an extension of either.
79
+
80
+ It measures at file or module granularity, not per theorem. A file requiring a
81
+ module that declares an axiom inherits it whether or not any theorem in that
82
+ file uses it, so every figure is an **upper bound on inheritance**, not a
83
+ statement about use. `Print Assumptions` and `#print axioms` answer the
84
+ per-theorem question and need a built library. This needs only the source,
85
+ which is what makes a cross-system census tractable.
86
+
87
+ ## Reading the source correctly
88
+
89
+ Three exclusions do real work, and each was derived from a failure rather than
90
+ anticipated.
91
+
92
+ **Rocq `Parameter` and `Hypothesis` are not axioms** inside a `Module Type` or
93
+ `Section` — they are discharged. The Rocq standard library has 389 `Parameter`
94
+ and 125 `Hypothesis` against 14 top-level `Axiom`; counting the former reports
95
+ a library assuming five hundred things it does not.
96
+
97
+ **Agda `postulate` must open a block.** `postulate[_|->_]` is a legal function
98
+ *name*, and a word-boundary match counts it as an assumption.
99
+
100
+ **Isabelle comments nest.** `(* outer (* inner *) still outer *)` is one
101
+ comment; a non-greedy match closes it at the inner terminator and returns the
102
+ rest as live code. On the Archive of Formal Proofs that single difference is 33
103
+ reported `sorry` occurrences against 3.
104
+
105
+ ## The check that makes it trustworthy
106
+
107
+ Agda's `{-# OPTIONS --safe #-}` is compiler-enforced and transitive: a safe
108
+ module may not postulate and may not import a module that is not itself safe.
109
+ The compiler has already computed the transitive answer, so `axsent agda`
110
+ ends by checking itself against it. Any module reported as both `--safe` and
111
+ reaching a postulate is a bug in this tool, not a finding about Agda.
112
+
113
+ Nothing in Rocq, Isabelle, Lean or Metamath offers a comparable ground truth.
114
+ Run the Agda census first for that reason alone.
115
+
116
+ ## Citation
117
+
118
+ The method and the five-library census it produced:
119
+
120
+ > Gonzalez, V. 2026. *Interface Assumptions Are Not Mathematical Assumptions:
121
+ > An Axiom Census of Five Libraries Across Four Proof Systems.* Zenodo.
122
+ > https://doi.org/10.5281/zenodo.22148932
123
+
axsent-0.1.0/README.md ADDED
@@ -0,0 +1,104 @@
1
+ ```
2
+ ╔════════════════════════════════════════════════════════════════════════════════════════════╗
3
+ ║ ║
4
+ ║ █████╗ ██╗ ██╗███████╗███████╗███╗ ██╗████████╗ ║
5
+ ║ ██╔══██╗╚██╗██╔╝██╔════╝██╔════╝████╗ ██║╚══██╔══╝ ║
6
+ ║ ███████║ ╚███╔╝ ███████╗█████╗ ██╔██╗ ██║ ██║ ║
7
+ ║ ██╔══██║ ██╔██╗ ╚════██║██╔══╝ ██║╚██╗██║ ██║ ║
8
+ ║ ██║ ██║██╔╝ ██╗███████║███████╗██║ ╚████║ ██║ ║
9
+ ║ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ║
10
+ ║ ║
11
+ ║ what a formal library assumes ║
12
+ ║ ║
13
+ ╚════════════════════════════════════════════════════════════════════════════════════════════╝
14
+ ```
15
+
16
+ An accent is a dependence on something local that travels with whoever carries
17
+ it. An inherited axiom is the same thing: a library speaks in the assumptions of
18
+ wherever it came from, and most of what it carries it never chose.
19
+
20
+ Rocq, Agda and Isabelle each let a development assert something without proving
21
+ it, and each records the dependency edges that carry the assertion outward.
22
+ `axsent` reads those declarations and those edges from source text, with no
23
+ proof assistant installed and nothing built.
24
+
25
+ ```bash
26
+ pip install axsent
27
+ ```
28
+
29
+ ```bash
30
+ axsent rocq path/to/stdlib/theories
31
+ axsent agda path/to/agda-stdlib/src
32
+ axsent isabelle path/to/afp/thys
33
+ ```
34
+
35
+ ## Why counts should not be totalled
36
+
37
+ A library declares two kinds of assumption and both use the same keyword.
38
+
39
+ **Mathematical** — excluded middle, choice, functional extensionality, proof
40
+ irrelevance. Claims about mathematics. A result depending on one is classical
41
+ rather than constructive.
42
+
43
+ **Interface** — that native 63-bit arithmetic behaves as specified, that a
44
+ mutable array returns what was written to it, that a foreign function returns
45
+ what its type says. Claims about the machine and the runtime. A result
46
+ depending on one is not thereby classical.
47
+
48
+ Rocq's Corelib declares **five times** as many axioms as the Rocq standard
49
+ library and makes no mathematical assumption at all: all 73 specify primitive
50
+ machine arithmetic, native arrays, floats and strings. Every one of the 16
51
+ postulating modules in the Agda standard library is a foreign-function or
52
+ operating-system interface. A single figure hides which of the two you are
53
+ looking at.
54
+
55
+ ## What it is not
56
+
57
+ It does not read Lean or Metamath. [gonzalgo](https://pypi.org/project/gonzalgo/)
58
+ and [mmforge](https://pypi.org/project/mmforge/) do that. This is a separate
59
+ instrument for separate systems, not an extension of either.
60
+
61
+ It measures at file or module granularity, not per theorem. A file requiring a
62
+ module that declares an axiom inherits it whether or not any theorem in that
63
+ file uses it, so every figure is an **upper bound on inheritance**, not a
64
+ statement about use. `Print Assumptions` and `#print axioms` answer the
65
+ per-theorem question and need a built library. This needs only the source,
66
+ which is what makes a cross-system census tractable.
67
+
68
+ ## Reading the source correctly
69
+
70
+ Three exclusions do real work, and each was derived from a failure rather than
71
+ anticipated.
72
+
73
+ **Rocq `Parameter` and `Hypothesis` are not axioms** inside a `Module Type` or
74
+ `Section` — they are discharged. The Rocq standard library has 389 `Parameter`
75
+ and 125 `Hypothesis` against 14 top-level `Axiom`; counting the former reports
76
+ a library assuming five hundred things it does not.
77
+
78
+ **Agda `postulate` must open a block.** `postulate[_|->_]` is a legal function
79
+ *name*, and a word-boundary match counts it as an assumption.
80
+
81
+ **Isabelle comments nest.** `(* outer (* inner *) still outer *)` is one
82
+ comment; a non-greedy match closes it at the inner terminator and returns the
83
+ rest as live code. On the Archive of Formal Proofs that single difference is 33
84
+ reported `sorry` occurrences against 3.
85
+
86
+ ## The check that makes it trustworthy
87
+
88
+ Agda's `{-# OPTIONS --safe #-}` is compiler-enforced and transitive: a safe
89
+ module may not postulate and may not import a module that is not itself safe.
90
+ The compiler has already computed the transitive answer, so `axsent agda`
91
+ ends by checking itself against it. Any module reported as both `--safe` and
92
+ reaching a postulate is a bug in this tool, not a finding about Agda.
93
+
94
+ Nothing in Rocq, Isabelle, Lean or Metamath offers a comparable ground truth.
95
+ Run the Agda census first for that reason alone.
96
+
97
+ ## Citation
98
+
99
+ The method and the five-library census it produced:
100
+
101
+ > Gonzalez, V. 2026. *Interface Assumptions Are Not Mathematical Assumptions:
102
+ > An Axiom Census of Five Libraries Across Four Proof Systems.* Zenodo.
103
+ > https://doi.org/10.5281/zenodo.22148932
104
+
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "axsent"
7
+ version = "0.1.0"
8
+ description = "Measure what a formal library assumes: Rocq, Agda and Isabelle, from source, with nothing built"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "Vincent Gonzalez", email = "vincegonzalez@me.com" }]
14
+ keywords = [
15
+ "axiom provenance",
16
+ "formal verification",
17
+ "proof assistants",
18
+ "Rocq",
19
+ "Agda",
20
+ "Isabelle",
21
+ "dependency analysis",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 3 - Alpha",
25
+ "Intended Audience :: Science/Research",
26
+ "Programming Language :: Python :: 3",
27
+ "Topic :: Scientific/Engineering :: Mathematics",
28
+ "Topic :: Software Development :: Quality Assurance",
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/vince-gonzalez/axsent"
34
+ Paper = "https://doi.org/10.5281/zenodo.22148932"
35
+
36
+ [project.scripts]
37
+ axsent = "axsent.cli:main"
38
+
39
+ [tool.setuptools]
40
+ package-dir = { "" = "src" }
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
44
+
45
+ [tool.pytest.ini_options]
46
+ pythonpath = ["src"]
47
+ testpaths = ["tests"]
axsent-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,70 @@
1
+ """Measure what a formal library assumes, in systems that are not Lean or Metamath.
2
+
3
+ Rocq, Agda and Isabelle each let a development assert something without proving
4
+ it, and each records the dependency edges that carry the assertion outward.
5
+ This reads those declarations and those edges from source text, with no proof
6
+ assistant installed and nothing built.
7
+
8
+ axsent rocq <path> top-level Axiom, via Require Import/Export
9
+ axsent agda <path> postulate blocks, via open import
10
+ axsent isabelle <path> axiomatization and sorry, via theory imports
11
+
12
+ WHAT IT IS NOT
13
+
14
+ It does not read Lean or Metamath; gonzalgo and mmforge do that, and this is a
15
+ separate instrument for separate systems rather than an extension of either.
16
+
17
+ It measures at file or module granularity, not per theorem. A file that
18
+ requires a module declaring an axiom inherits it whether or not any theorem in
19
+ that file uses it, so every figure is an upper bound on inheritance rather than
20
+ a statement about use. `Print Assumptions` and `#print axioms` answer the
21
+ per-theorem question and need a built library; this answers the per-file one
22
+ and needs only the source, which is what makes a cross-system census possible
23
+ at all.
24
+
25
+ TWO POPULATIONS
26
+
27
+ Counts produced here should not be totalled without separating them. A library
28
+ declares assumptions about mathematics -- excluded middle, choice, functional
29
+ extensionality -- and assumptions about its interfaces -- that native 63-bit
30
+ arithmetic behaves as specified, that a foreign function returns what its type
31
+ says. Both use the same keyword and both are counted by the same tools. Rocq's
32
+ Corelib declares five times as many axioms as the Rocq standard library and
33
+ makes no mathematical assumption at all.
34
+
35
+ Reference: Gonzalez, V. 2026. "Interface Assumptions Are Not Mathematical
36
+ Assumptions: An Axiom Census of Five Libraries Across Four Proof Systems."
37
+ https://doi.org/10.5281/zenodo.22148932
38
+ """
39
+ from __future__ import annotations
40
+
41
+ from pathlib import Path
42
+
43
+ __version__ = "0.1.0"
44
+
45
+ PAPER_DOI = "10.5281/zenodo.22148932"
46
+
47
+
48
+ def open_source_tree(path) -> Path:
49
+ """Resolve a corpus path, or exit with a message rather than a traceback.
50
+
51
+ A wrong path is the most likely thing to go wrong when one of these runs
52
+ unattended, and a stack trace ending in FileNotFoundError buries the only
53
+ fact the operator needs.
54
+ """
55
+ p = Path(path)
56
+ if not p.exists():
57
+ raise SystemExit(f"no such path: {p}")
58
+ if not p.is_dir():
59
+ raise SystemExit(f"not a directory: {p}")
60
+ return p
61
+
62
+
63
+ def pct(n, d, places=1):
64
+ """A percentage, or "n/a" when there is nothing to divide by.
65
+
66
+ An empty or partial corpus is a legitimate input -- a stub checkout, a
67
+ subdirectory, a library whose sources have not been fetched. Reporting that
68
+ nothing was found is a result, not a reason to divide by zero.
69
+ """
70
+ return f"{n / d:.{places}%}" if d else "n/a"
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ r"""What does the Agda standard library assume, and where?
3
+
4
+ python agda_axioms.py /path/to/agda-stdlib/src
5
+
6
+ Agda's assumption is the `postulate`: a name given a type and no definition.
7
+ Its unsafe surface is wider than that -- `primTrustMe`, `unsafeCoerce`, and the
8
+ `TERMINATING` pragma each let something through the checker that the checker did
9
+ not establish.
10
+
11
+ WHY THIS LIBRARY IS THE ONE WORTH MEASURING
12
+
13
+ Agda has `{-# OPTIONS --safe #-}`, and the compiler enforces it: a module
14
+ declared safe may not postulate, may not use the unsafe primitives, and may not
15
+ import a module that is not itself safe. That last clause is the useful part.
16
+ It means the compiler has already computed the transitive answer, so a textual
17
+ analysis can be checked against ground truth rather than argued about.
18
+
19
+ Any file this reports as both `--safe` and reaching a postulate is a bug in
20
+ this script, not a finding about Agda. That check runs at the end and is the
21
+ only reason to trust the rest of the output.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import re
26
+ import sys
27
+ from pathlib import Path
28
+ from . import open_source_tree
29
+
30
+ BLOCK = re.compile(r"\{-(?!#).*?-\}", re.S) # {- ... -} but not pragmas
31
+ LINE = re.compile(r"(?<!-)--(?!\})[^\n]*") # -- to end of line
32
+ IMPORT = re.compile(r"^\s*(?:open\s+)?import\s+([\w.']+)", re.M)
33
+ # "postulate" opens a block only when nothing follows it on the line.
34
+ # `postulate[_|->_]` is a legal function NAME in Agda, and \b matches between
35
+ # the "e" and the "[", so \b counts a definition as an assumption.
36
+ POSTULATE = re.compile(r"^[ \t]*postulate[ \t]*(?:$|--)", re.M)
37
+ OPTIONS = re.compile(r"\{-#\s*OPTIONS([^#]*)#-\}")
38
+ UNSAFE_TOKENS = ("primTrustMe", "unsafeCoerce", "TERMINATING")
39
+
40
+
41
+ def strip(text):
42
+ """Remove comments so a word in prose is not read as code."""
43
+ return LINE.sub(" ", BLOCK.sub(" ", text))
44
+
45
+
46
+ def load(root: Path):
47
+ mods, raw, code = {}, {}, {}
48
+ for p in sorted(root.rglob("*.agda")):
49
+ mod = ".".join(p.relative_to(root).with_suffix("").parts)
50
+ mods[mod] = p
51
+ t = p.read_text(encoding="utf-8", errors="replace")
52
+ raw[mod] = t
53
+ code[mod] = strip(t)
54
+ return mods, raw, code
55
+
56
+
57
+ def main() -> None:
58
+ root = open_source_tree(sys.argv[1])
59
+ mods, raw, code = load(root)
60
+ print(f" {len(mods)} modules under {root}")
61
+
62
+ safe, opts = {}, {}
63
+ for m, t in raw.items():
64
+ o = " ".join(x.group(1) for x in OPTIONS.finditer(t))
65
+ opts[m] = o
66
+ safe[m] = "--safe" in o
67
+ print(f" {sum(safe.values())} declare --safe "
68
+ f"({sum(safe.values())/len(mods):.1%})")
69
+
70
+ posts = {m for m, c in code.items() if POSTULATE.search(c)}
71
+ unsafe_prim = {m for m, c in code.items()
72
+ if any(tok in c for tok in UNSAFE_TOKENS)}
73
+ print(f" {len(posts)} modules postulate; "
74
+ f"{len(unsafe_prim)} use primTrustMe / unsafeCoerce / TERMINATING")
75
+ print()
76
+
77
+ known = set(mods)
78
+ deps = {}
79
+ for m, c in code.items():
80
+ deps[m] = {i for i in IMPORT.findall(c) if i in known}
81
+
82
+ sys.setrecursionlimit(30000)
83
+ reach = {}
84
+
85
+ def visit(m, stack=()):
86
+ if m in reach:
87
+ return reach[m]
88
+ if m in stack:
89
+ return set()
90
+ got = {m} if m in posts else set()
91
+ for d in deps.get(m, ()):
92
+ got |= visit(d, stack + (m,))
93
+ reach[m] = got
94
+ return got
95
+
96
+ for m in mods:
97
+ visit(m)
98
+
99
+ touched = [m for m in mods if reach[m]]
100
+ print(f" {len(touched)} of {len(mods)} modules ({len(touched)/len(mods):.1%}) "
101
+ f"transitively import a postulate")
102
+ print()
103
+
104
+ below = {}
105
+ for m in mods:
106
+ for r in reach[m]:
107
+ below.setdefault(r, set()).add(m)
108
+ print(f" {'module that postulates':<52}{'safe?':>7}{'below':>7}")
109
+ print(" " + "-" * 68)
110
+ for r, users in sorted(below.items(), key=lambda kv: -len(kv[1])):
111
+ print(f" {r:<52}{('yes' if safe[r] else 'no'):>7}{len(users):>7}")
112
+
113
+ # ---- the check that makes the rest trustworthy --------------------
114
+ print()
115
+ contradictions = [m for m in mods if safe[m] and reach[m]]
116
+ if contradictions:
117
+ print(f" INSTRUMENT FAILS: {len(contradictions)} modules are --safe "
118
+ f"yet reach a postulate. Agda forbids that, so this is my bug.")
119
+ for m in contradictions[:10]:
120
+ print(f" {m} -> {', '.join(sorted(reach[m]))[:70]}")
121
+ else:
122
+ print(" CHECK PASSES: no --safe module reaches a postulate, which is "
123
+ "what the Agda compiler guarantees.")
124
+
125
+
126
+ if __name__ == "__main__":
127
+ main()
@@ -0,0 +1,59 @@
1
+ """axsent command line.
2
+
3
+ axsent <system> <path> [options]
4
+ axsent <system> --help
5
+
6
+ Each subcommand is a reader for one proof assistant. They share a shape --
7
+ find what is asserted without proof, follow the dependency edges, report how
8
+ far each assertion reaches -- but nothing else, because the three systems have
9
+ no syntax in common.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+
15
+ from . import __doc__ as PACKAGE_DOC
16
+ from . import __version__, PAPER_DOI
17
+
18
+ COMMANDS = {
19
+ "rocq": ("rocq", "top-level Axiom, followed through Require Import/Export"),
20
+ "agda": ("agda", "postulate blocks, checked against the --safe flag"),
21
+ "isabelle": ("isabelle", "axiomatization and sorry, across theory imports"),
22
+ }
23
+
24
+
25
+ def usage(code: int = 0) -> None:
26
+ print(PACKAGE_DOC.strip().split("\n\n")[0])
27
+ print()
28
+ print("usage: axsent <system> <path>")
29
+ print(" axsent <system> --help")
30
+ print()
31
+ width = max(len(c) for c in COMMANDS)
32
+ for name, (_mod, summary) in COMMANDS.items():
33
+ print(f" {name:<{width}} {summary}")
34
+ print()
35
+ print("Counts of mathematical and interface assumptions should not be")
36
+ print(f"totalled together. See doi:{PAPER_DOI}")
37
+ sys.exit(code)
38
+
39
+
40
+ def main() -> None:
41
+ if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help", "help"):
42
+ usage(0)
43
+ name = sys.argv[1]
44
+ if name in ("-V", "--version"):
45
+ print(f"axsent {__version__}")
46
+ sys.exit(0)
47
+ if name not in COMMANDS:
48
+ print(f"axsent: unknown system {name!r}\n", file=sys.stderr)
49
+ usage(2)
50
+
51
+ module_name, _ = COMMANDS[name]
52
+ import importlib
53
+ mod = importlib.import_module(f".{module_name}", package="axsent")
54
+ sys.argv = [f"axsent {name}"] + sys.argv[2:]
55
+ mod.main()
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env python3
2
+ r"""Strip Isabelle comments, documentation and string literals properly.
3
+
4
+ Isabelle comments NEST: (* outer (* inner *) still outer *)
5
+
6
+ A non-greedy regex closes the outer comment at the inner comment's terminator
7
+ and hands back the remainder as live code. That is how a commented-out block
8
+ containing `sorry` gets reported as an admitted proof in a refereed archive.
9
+ Depth counting is the only thing that reads this correctly.
10
+
11
+ Also removed:
12
+ \<open> ... \<close> text cartouches -- Isabelle's documentation syntax, and
13
+ where most prose discussing `sorry` actually lives
14
+ "..." string literals -- one AFP entry searches HOL.thy for
15
+ the literal word "sorry" as a test of its string matcher
16
+ """
17
+ from __future__ import annotations
18
+
19
+ OPEN, CLOSE = "(*", "*)"
20
+ COPEN, CCLOSE = "\\<open>", "\\<close>"
21
+
22
+
23
+ def strip(text: str) -> str:
24
+ out = []
25
+ i, n = 0, len(text)
26
+ depth = 0 # nested (* *)
27
+ cart = 0 # nested \<open> \<close>
28
+ in_str = False
29
+ while i < n:
30
+ if not in_str and text.startswith(OPEN, i):
31
+ depth += 1
32
+ i += 2
33
+ continue
34
+ if depth and text.startswith(CLOSE, i):
35
+ depth -= 1
36
+ i += 2
37
+ out.append(" ")
38
+ continue
39
+ if depth:
40
+ i += 1
41
+ continue
42
+ if text.startswith(COPEN, i):
43
+ cart += 1
44
+ i += len(COPEN)
45
+ continue
46
+ if cart and text.startswith(CCLOSE, i):
47
+ cart -= 1
48
+ i += len(CCLOSE)
49
+ out.append(" ")
50
+ continue
51
+ if cart:
52
+ i += 1
53
+ continue
54
+ c = text[i]
55
+ if c == '"' and (i == 0 or text[i - 1] != "\\"):
56
+ in_str = not in_str
57
+ out.append(" ")
58
+ i += 1
59
+ continue
60
+ out.append(" " if in_str else c)
61
+ i += 1
62
+ return "".join(out)
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ r"""What does the Archive of Formal Proofs assume, and what is unfinished in it?
3
+
4
+ python afp_axioms.py /path/to/afp/thys
5
+
6
+ The AFP is a curated, refereed archive: every entry is built and checked by the
7
+ Isabelle release process. That makes two questions worth asking of it that are
8
+ not worth asking of an ordinary library.
9
+
10
+ `axiomatization` Isabelle's way of asserting something without proof. An AFP
11
+ entry that axiomatizes is adding to the trusted base of
12
+ anything that imports it. Isabelle/HOL's own axioms are not
13
+ in scope here; this counts what the ARCHIVE adds on top.
14
+
15
+ `sorry` an admitted proof. Isabelle accepts it, marks the result,
16
+ and carries on. A `sorry` anywhere upstream means a theorem
17
+ that looks proved is not, which is exactly the failure mode
18
+ a provenance tool exists to catch, and exactly the one that
19
+ a green build does not reveal.
20
+
21
+ Comments are stripped before counting, because "sorry" and "axiomatization"
22
+ both appear in prose discussing them, and counting those would inflate every
23
+ number here. Occurrences inside string literals are left, which biases the
24
+ counts slightly upward -- the report says which files so they can be read.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ import sys
30
+ from collections import Counter
31
+ from pathlib import Path
32
+
33
+ from .isa_strip import strip as isa_strip
34
+ from . import open_source_tree
35
+
36
+ COMMENT = re.compile(r"\(\*.*?\*\)", re.S)
37
+ # a theory header: theory NAME imports A B C begin
38
+ IMPORTS = re.compile(r"\btheory\s+([\w.']+)\s+imports\s+(.*?)\bbegin\b", re.S)
39
+ AXIOMATIZATION = re.compile(r"(?<![\w'])axiomatization(?![\w'])")
40
+ SORRY = re.compile(r"(?<![\w'])sorry(?![\w'])")
41
+ OOPS = re.compile(r"(?<![\w'])oops(?![\w'])")
42
+ CONSTS = re.compile(r"^\s*consts(?![\w'])", re.M)
43
+
44
+
45
+ def main() -> None:
46
+ root = open_source_tree(sys.argv[1])
47
+ files = sorted(root.rglob("*.thy"))
48
+ print(f" {len(files):,} theory files under {root}")
49
+
50
+ ax_files, sorry_files, oops_files, consts_files = {}, {}, {}, {}
51
+ entries = Counter()
52
+ total = Counter()
53
+
54
+ for p in files:
55
+ raw = p.read_text(encoding="utf-8", errors="replace")
56
+ # only pay for the depth-counting strip when a keyword is present
57
+ if any(k in raw for k in ("sorry", "axiomatization", "oops", "consts")):
58
+ code = isa_strip(raw)
59
+ else:
60
+ code = raw
61
+ rel = p.relative_to(root)
62
+ entry = rel.parts[0] if len(rel.parts) > 1 else "(top)"
63
+ entries[entry] += 1
64
+
65
+ n_ax = len(AXIOMATIZATION.findall(code))
66
+ n_sorry = len(SORRY.findall(code))
67
+ n_oops = len(OOPS.findall(code))
68
+ n_consts = len(CONSTS.findall(code))
69
+ total["axiomatization"] += n_ax
70
+ total["sorry"] += n_sorry
71
+ total["oops"] += n_oops
72
+ total["consts"] += n_consts
73
+ if n_ax:
74
+ ax_files[str(rel)] = n_ax
75
+ if n_sorry:
76
+ sorry_files[str(rel)] = n_sorry
77
+ if n_oops:
78
+ oops_files[str(rel)] = n_oops
79
+ if n_consts:
80
+ consts_files[str(rel)] = n_consts
81
+
82
+ print(f" {len(entries):,} entries")
83
+ print()
84
+ print(f" {'construct':<18}{'occurrences':>13}{'files':>8}{'entries':>9}")
85
+ print(" " + "-" * 50)
86
+ for name, d in (("axiomatization", ax_files), ("sorry", sorry_files),
87
+ ("oops", oops_files), ("consts", consts_files)):
88
+ ents = {f.split("/")[0].split("\\")[0] for f in d}
89
+ print(f" {name:<18}{total[name]:>13,}{len(d):>8,}{len(ents):>9,}")
90
+
91
+ for title, d in (("entries that axiomatize", ax_files),
92
+ ("files containing sorry", sorry_files)):
93
+ print()
94
+ print(f" {title}")
95
+ print(" " + "-" * 66)
96
+ for f, n in sorted(d.items(), key=lambda kv: -kv[1])[:18]:
97
+ print(f" {n:>4} {f}")
98
+ if len(d) > 18:
99
+ print(f" ... and {len(d)-18} more")
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env python3
2
+ r"""Where does the Rocq standard library spend its axioms?
3
+
4
+ python rocq_axioms.py /path/to/stdlib/theories
5
+
6
+ Rocq is constructive by default. Excluded middle, choice, functional
7
+ extensionality and proof irrelevance are not available unless a file asks for
8
+ them, and asking is a visible act: `Require Import` of a module that declares
9
+ them. That makes the question "what does this result assume" answerable from
10
+ the source, without building anything.
11
+
12
+ WHAT COUNTS AS AN AXIOM
13
+
14
+ Only a top-level `Axiom`. `Parameter` and `Hypothesis` inside a `Module Type`
15
+ or a `Section` are discharged when the module is instantiated or the section
16
+ closes; they are interface obligations, not assumptions the library makes. A
17
+ census that counts them reports a library assuming several hundred things it
18
+ does not assume. Depth is tracked so they can be excluded rather than guessed
19
+ at.
20
+
21
+ WHAT THIS DOES NOT DO
22
+
23
+ This is module-level, not theorem-level. A file that requires
24
+ `Logic.Classical` inherits the axiom whether or not any theorem in it uses
25
+ excluded middle, so the counts below are an upper bound on what actually rests
26
+ on the axiom. `Print Assumptions` answers the per-theorem question and needs a
27
+ built library; this answers the per-file one and needs only the source.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import re
32
+ import sys
33
+ from collections import defaultdict, deque
34
+ from pathlib import Path
35
+ from . import open_source_tree
36
+
37
+ AXIOM = re.compile(r"^\s*Axiom\s+([A-Za-z_][A-Za-z0-9_']*)", re.M)
38
+ OPEN = re.compile(r"^\s*(Module\s+Type\b|Module\b|Section\b)", re.M)
39
+ CLOSE = re.compile(r"^\s*End\b", re.M)
40
+ # "Require Import A.B C." / "Require Export A." / "From X Require Import Y."
41
+ REQUIRE = re.compile(
42
+ r"^\s*(?:From\s+([\w.]+)\s+)?Require\s+(?:Import|Export)?\s*([^.]*)\.",
43
+ re.M)
44
+ COMMENT = re.compile(r"\(\*.*?\*\)", re.S)
45
+
46
+
47
+ def toplevel_axioms(text):
48
+ """Axioms declared at depth 0, i.e. not inside a Module Type or Section."""
49
+ depth = 0
50
+ found = []
51
+ events = []
52
+ for m in OPEN.finditer(text):
53
+ events.append((m.start(), +1, None))
54
+ for m in CLOSE.finditer(text):
55
+ events.append((m.start(), -1, None))
56
+ for m in AXIOM.finditer(text):
57
+ events.append((m.start(), 0, m.group(1)))
58
+ for _pos, kind, name in sorted(events):
59
+ if kind == 0:
60
+ if depth == 0:
61
+ found.append(name)
62
+ else:
63
+ depth = max(0, depth + kind)
64
+ return found
65
+
66
+
67
+ def load(root: Path):
68
+ files, text = {}, {}
69
+ for p in sorted(root.rglob("*.v")):
70
+ rel = p.relative_to(root)
71
+ mod = ".".join(rel.with_suffix("").parts)
72
+ files[mod] = p
73
+ text[mod] = COMMENT.sub(" ", p.read_text(encoding="utf-8", errors="replace"))
74
+ return files, text
75
+
76
+
77
+ def requires(text, known):
78
+ """Modules this file requires, resolved against what exists."""
79
+ out = set()
80
+ for m in REQUIRE.finditer(text):
81
+ prefix, body = m.group(1), m.group(2)
82
+ for tok in body.replace("\n", " ").split():
83
+ if not re.match(r"^[\w.]+$", tok):
84
+ continue
85
+ cands = [tok]
86
+ if prefix:
87
+ cands.append(prefix + "." + tok)
88
+ # Rocq module paths drop the library root; match on any suffix
89
+ for c in cands:
90
+ tail = c.split(".")
91
+ for k in known:
92
+ if k.split(".")[-len(tail):] == tail:
93
+ out.add(k)
94
+ return out
95
+
96
+
97
+ def main() -> None:
98
+ root = open_source_tree(sys.argv[1])
99
+ files, text = load(root)
100
+ known = set(files)
101
+ print(f" {len(files)} files under {root}")
102
+
103
+ declares = {m: toplevel_axioms(t) for m, t in text.items()}
104
+ roots = {m: a for m, a in declares.items() if a}
105
+ total_axioms = sum(len(a) for a in roots.values())
106
+ print(f" {total_axioms} top-level Axiom declarations in {len(roots)} files")
107
+
108
+ deps = {m: requires(t, known) for m, t in text.items()}
109
+
110
+ # who reaches an axiom-declaring file
111
+ reach = {}
112
+
113
+ def visit(m, stack=()):
114
+ if m in reach:
115
+ return reach[m]
116
+ if m in stack:
117
+ return set()
118
+ got = set(roots.get(m, ()) and [m] or [])
119
+ for d in deps.get(m, ()):
120
+ got |= visit(d, stack + (m,))
121
+ reach[m] = got
122
+ return got
123
+
124
+ sys.setrecursionlimit(20000)
125
+ for m in files:
126
+ visit(m)
127
+
128
+ touched = [m for m in files if reach[m]]
129
+ print(f" {len(touched)} of {len(files)} files "
130
+ f"({len(touched)/len(files):.1%}) transitively require an axiom file")
131
+ print()
132
+
133
+ # which axiom source is inherited most widely
134
+ below = defaultdict(set)
135
+ for m in files:
136
+ for r in reach[m]:
137
+ below[r].add(m)
138
+ print(f" {'axiom-declaring file':<44}{'axioms':>7}{'files below':>13}")
139
+ print(" " + "-" * 66)
140
+ for r, users in sorted(below.items(), key=lambda kv: -len(kv[1])):
141
+ print(f" {r:<44}{len(roots[r]):>7}{len(users):>13}")
142
+ print()
143
+ print(" the axioms themselves, by file")
144
+ for r in sorted(roots, key=lambda r: -len(below[r])):
145
+ print(f" {r}")
146
+ print(f" {', '.join(sorted(roots[r]))[:200]}")
147
+
148
+
149
+ if __name__ == "__main__":
150
+ main()
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: axsent
3
+ Version: 0.1.0
4
+ Summary: Measure what a formal library assumes: Rocq, Agda and Isabelle, from source, with nothing built
5
+ Author-email: Vincent Gonzalez <vincegonzalez@me.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/vince-gonzalez/axsent
8
+ Project-URL: Paper, https://doi.org/10.5281/zenodo.22148932
9
+ Keywords: axiom provenance,formal verification,proof assistants,Rocq,Agda,Isabelle,dependency analysis
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
14
+ Classifier: Topic :: Software Development :: Quality Assurance
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ ```
21
+ ╔════════════════════════════════════════════════════════════════════════════════════════════╗
22
+ ║ ║
23
+ ║ █████╗ ██╗ ██╗███████╗███████╗███╗ ██╗████████╗ ║
24
+ ║ ██╔══██╗╚██╗██╔╝██╔════╝██╔════╝████╗ ██║╚══██╔══╝ ║
25
+ ║ ███████║ ╚███╔╝ ███████╗█████╗ ██╔██╗ ██║ ██║ ║
26
+ ║ ██╔══██║ ██╔██╗ ╚════██║██╔══╝ ██║╚██╗██║ ██║ ║
27
+ ║ ██║ ██║██╔╝ ██╗███████║███████╗██║ ╚████║ ██║ ║
28
+ ║ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ║
29
+ ║ ║
30
+ ║ what a formal library assumes ║
31
+ ║ ║
32
+ ╚════════════════════════════════════════════════════════════════════════════════════════════╝
33
+ ```
34
+
35
+ An accent is a dependence on something local that travels with whoever carries
36
+ it. An inherited axiom is the same thing: a library speaks in the assumptions of
37
+ wherever it came from, and most of what it carries it never chose.
38
+
39
+ Rocq, Agda and Isabelle each let a development assert something without proving
40
+ it, and each records the dependency edges that carry the assertion outward.
41
+ `axsent` reads those declarations and those edges from source text, with no
42
+ proof assistant installed and nothing built.
43
+
44
+ ```bash
45
+ pip install axsent
46
+ ```
47
+
48
+ ```bash
49
+ axsent rocq path/to/stdlib/theories
50
+ axsent agda path/to/agda-stdlib/src
51
+ axsent isabelle path/to/afp/thys
52
+ ```
53
+
54
+ ## Why counts should not be totalled
55
+
56
+ A library declares two kinds of assumption and both use the same keyword.
57
+
58
+ **Mathematical** — excluded middle, choice, functional extensionality, proof
59
+ irrelevance. Claims about mathematics. A result depending on one is classical
60
+ rather than constructive.
61
+
62
+ **Interface** — that native 63-bit arithmetic behaves as specified, that a
63
+ mutable array returns what was written to it, that a foreign function returns
64
+ what its type says. Claims about the machine and the runtime. A result
65
+ depending on one is not thereby classical.
66
+
67
+ Rocq's Corelib declares **five times** as many axioms as the Rocq standard
68
+ library and makes no mathematical assumption at all: all 73 specify primitive
69
+ machine arithmetic, native arrays, floats and strings. Every one of the 16
70
+ postulating modules in the Agda standard library is a foreign-function or
71
+ operating-system interface. A single figure hides which of the two you are
72
+ looking at.
73
+
74
+ ## What it is not
75
+
76
+ It does not read Lean or Metamath. [gonzalgo](https://pypi.org/project/gonzalgo/)
77
+ and [mmforge](https://pypi.org/project/mmforge/) do that. This is a separate
78
+ instrument for separate systems, not an extension of either.
79
+
80
+ It measures at file or module granularity, not per theorem. A file requiring a
81
+ module that declares an axiom inherits it whether or not any theorem in that
82
+ file uses it, so every figure is an **upper bound on inheritance**, not a
83
+ statement about use. `Print Assumptions` and `#print axioms` answer the
84
+ per-theorem question and need a built library. This needs only the source,
85
+ which is what makes a cross-system census tractable.
86
+
87
+ ## Reading the source correctly
88
+
89
+ Three exclusions do real work, and each was derived from a failure rather than
90
+ anticipated.
91
+
92
+ **Rocq `Parameter` and `Hypothesis` are not axioms** inside a `Module Type` or
93
+ `Section` — they are discharged. The Rocq standard library has 389 `Parameter`
94
+ and 125 `Hypothesis` against 14 top-level `Axiom`; counting the former reports
95
+ a library assuming five hundred things it does not.
96
+
97
+ **Agda `postulate` must open a block.** `postulate[_|->_]` is a legal function
98
+ *name*, and a word-boundary match counts it as an assumption.
99
+
100
+ **Isabelle comments nest.** `(* outer (* inner *) still outer *)` is one
101
+ comment; a non-greedy match closes it at the inner terminator and returns the
102
+ rest as live code. On the Archive of Formal Proofs that single difference is 33
103
+ reported `sorry` occurrences against 3.
104
+
105
+ ## The check that makes it trustworthy
106
+
107
+ Agda's `{-# OPTIONS --safe #-}` is compiler-enforced and transitive: a safe
108
+ module may not postulate and may not import a module that is not itself safe.
109
+ The compiler has already computed the transitive answer, so `axsent agda`
110
+ ends by checking itself against it. Any module reported as both `--safe` and
111
+ reaching a postulate is a bug in this tool, not a finding about Agda.
112
+
113
+ Nothing in Rocq, Isabelle, Lean or Metamath offers a comparable ground truth.
114
+ Run the Agda census first for that reason alone.
115
+
116
+ ## Citation
117
+
118
+ The method and the five-library census it produced:
119
+
120
+ > Gonzalez, V. 2026. *Interface Assumptions Are Not Mathematical Assumptions:
121
+ > An Axiom Census of Five Libraries Across Four Proof Systems.* Zenodo.
122
+ > https://doi.org/10.5281/zenodo.22148932
123
+
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/axsent/__init__.py
5
+ src/axsent/agda.py
6
+ src/axsent/cli.py
7
+ src/axsent/isa_strip.py
8
+ src/axsent/isabelle.py
9
+ src/axsent/rocq.py
10
+ src/axsent.egg-info/PKG-INFO
11
+ src/axsent.egg-info/SOURCES.txt
12
+ src/axsent.egg-info/dependency_links.txt
13
+ src/axsent.egg-info/entry_points.txt
14
+ src/axsent.egg-info/top_level.txt
15
+ tests/test_readers.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ axsent = axsent.cli:main
@@ -0,0 +1 @@
1
+ axsent
@@ -0,0 +1,103 @@
1
+ """The three exclusions that do real work, and the degenerate inputs.
2
+
3
+ Every case here was a wrong answer this tool gave before it was corrected, so
4
+ each test is a regression rather than a hypothetical. Two of them come from
5
+ reading a refereed archive and reporting defects that were not there.
6
+ """
7
+ import pytest
8
+
9
+ from axsent import open_source_tree, pct
10
+ from axsent.isa_strip import strip as isa_strip
11
+ from axsent.rocq import toplevel_axioms
12
+ from axsent.agda import POSTULATE
13
+
14
+
15
+ # --- Rocq: Module Type and Section obligations are not assumptions ---------
16
+
17
+ def test_toplevel_axiom_is_counted():
18
+ assert toplevel_axioms("Axiom classic : forall P:Prop, P \\/ ~ P.") == ["classic"]
19
+
20
+
21
+ def test_axiom_inside_module_type_is_not_counted():
22
+ src = """
23
+ Module Type Specification.
24
+ Axiom archimedean : something.
25
+ Axiom algebraic : something_else.
26
+ End Specification.
27
+ Module Implementation : Specification.
28
+ End Implementation.
29
+ """
30
+ assert toplevel_axioms(src) == []
31
+
32
+
33
+ def test_axiom_after_a_closed_section_is_counted_again():
34
+ src = """
35
+ Section S.
36
+ Axiom hidden : thing.
37
+ End S.
38
+ Axiom visible : thing.
39
+ """
40
+ assert toplevel_axioms(src) == ["visible"]
41
+
42
+
43
+ # --- Agda: postulate must open a block ------------------------------------
44
+
45
+ def test_postulate_block_is_matched():
46
+ assert POSTULATE.search("postulate\n pure : A -> IO A\n")
47
+
48
+
49
+ def test_postulate_in_a_function_name_is_not_matched():
50
+ """`postulate[_|->_]` is a legal Agda name; \\b matches inside it."""
51
+ src = "postulate[_↦_] : forall {a b} -> A -> B\n"
52
+ assert not POSTULATE.search(src)
53
+
54
+
55
+ # --- Isabelle: comments nest ----------------------------------------------
56
+
57
+ def test_nested_comment_is_fully_removed():
58
+ """A non-greedy match closes the outer comment at the inner terminator
59
+ and hands back `sorry` as live code. On the AFP that was 33 against 3."""
60
+ src = "(* outer (* FIXME inner *) lemma x: y sorry *) real_code"
61
+ out = isa_strip(src)
62
+ assert "sorry" not in out
63
+ assert "real_code" in out
64
+
65
+
66
+ def test_documentation_cartouche_is_removed():
67
+ src = "text \\<open>we did not want a sorry in this file\\<close> real_code"
68
+ out = isa_strip(src)
69
+ assert "sorry" not in out
70
+ assert "real_code" in out
71
+
72
+
73
+ def test_string_literal_is_removed():
74
+ """One AFP entry searches HOL.thy for the literal word as a test."""
75
+ src = 'val test = kmp "sorry" (File.read x) real_code'
76
+ assert "sorry" not in isa_strip(src)
77
+
78
+
79
+ def test_live_code_survives_stripping():
80
+ src = "lemma a: b\n sorry\n"
81
+ assert "sorry" in isa_strip(src)
82
+
83
+
84
+ # --- degenerate inputs -----------------------------------------------------
85
+
86
+ def test_pct_does_not_divide_by_zero():
87
+ assert pct(0, 0) == "n/a"
88
+ assert pct(3, 0) == "n/a"
89
+ assert pct(1, 4) == "25.0%"
90
+
91
+
92
+ def test_missing_path_is_a_message_not_a_traceback(tmp_path):
93
+ with pytest.raises(SystemExit) as e:
94
+ open_source_tree(tmp_path / "nope")
95
+ assert "no such path" in str(e.value)
96
+
97
+
98
+ def test_file_instead_of_directory_is_a_message(tmp_path):
99
+ f = tmp_path / "a.v"
100
+ f.write_text("Axiom x : y.", encoding="utf-8")
101
+ with pytest.raises(SystemExit) as e:
102
+ open_source_tree(f)
103
+ assert "not a directory" in str(e.value)