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 +21 -0
- axsent-0.1.0/PKG-INFO +123 -0
- axsent-0.1.0/README.md +104 -0
- axsent-0.1.0/pyproject.toml +47 -0
- axsent-0.1.0/setup.cfg +4 -0
- axsent-0.1.0/src/axsent/__init__.py +70 -0
- axsent-0.1.0/src/axsent/agda.py +127 -0
- axsent-0.1.0/src/axsent/cli.py +59 -0
- axsent-0.1.0/src/axsent/isa_strip.py +62 -0
- axsent-0.1.0/src/axsent/isabelle.py +103 -0
- axsent-0.1.0/src/axsent/rocq.py +150 -0
- axsent-0.1.0/src/axsent.egg-info/PKG-INFO +123 -0
- axsent-0.1.0/src/axsent.egg-info/SOURCES.txt +15 -0
- axsent-0.1.0/src/axsent.egg-info/dependency_links.txt +1 -0
- axsent-0.1.0/src/axsent.egg-info/entry_points.txt +2 -0
- axsent-0.1.0/src/axsent.egg-info/top_level.txt +1 -0
- axsent-0.1.0/tests/test_readers.py +103 -0
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,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 @@
|
|
|
1
|
+
|
|
@@ -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)
|