machinechecked 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- machinechecked-0.1.0.dist-info/METADATA +108 -0
- machinechecked-0.1.0.dist-info/RECORD +58 -0
- machinechecked-0.1.0.dist-info/WHEEL +5 -0
- machinechecked-0.1.0.dist-info/entry_points.txt +3 -0
- machinechecked-0.1.0.dist-info/licenses/LICENSE +201 -0
- machinechecked-0.1.0.dist-info/top_level.txt +1 -0
- prooflens/__init__.py +1 -0
- prooflens/__main__.py +3 -0
- prooflens/assess.py +504 -0
- prooflens/audit.py +115 -0
- prooflens/bundle.py +70 -0
- prooflens/cli.py +542 -0
- prooflens/config.py +145 -0
- prooflens/data/ProofLensProbe.lean +469 -0
- prooflens/data/prooflens.sty +75 -0
- prooflens/data/schema/evidence.schema.json +24 -0
- prooflens/data/schema/inventory.schema.json +55 -0
- prooflens/data/schema/manifest-claim.schema.json +126 -0
- prooflens/data/schema/manifest-index.schema.json +128 -0
- prooflens/evidence.py +60 -0
- prooflens/export.py +227 -0
- prooflens/formalization.py +106 -0
- prooflens/manifest.py +296 -0
- prooflens/pdfclaims.py +129 -0
- prooflens/pipeline.py +331 -0
- prooflens/probe.py +126 -0
- prooflens/promote.py +204 -0
- prooflens/review_queue.py +138 -0
- prooflens/reviews.py +105 -0
- prooflens/schemas.py +19 -0
- prooflens/server.py +63 -0
- prooflens/simple.py +628 -0
- prooflens/sources.py +164 -0
- prooflens/tex.py +408 -0
- prooflens/util.py +66 -0
- prooflens/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- prooflens/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- prooflens/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- prooflens/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- prooflens/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- prooflens/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- prooflens/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- prooflens/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- prooflens/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- prooflens/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- prooflens/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- prooflens/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- prooflens/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- prooflens/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- prooflens/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- prooflens/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- prooflens/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- prooflens/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- prooflens/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- prooflens/web/assets/index-B2GDDbsW.css +1 -0
- prooflens/web/assets/index-DN7uLe7p.js +285 -0
- prooflens/web/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- prooflens/web/index.html +14 -0
prooflens/config.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""The project configuration, `prooflens.toml`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import tomllib
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
STANDARD_AXIOMS = ["propext", "Classical.choice", "Quot.sound"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigError(ValueError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Publish:
|
|
19
|
+
"""What of a paper may appear in a public export. Everything defaults to private."""
|
|
20
|
+
|
|
21
|
+
pdf: bool = False
|
|
22
|
+
source_text: bool = False
|
|
23
|
+
lean: bool = False
|
|
24
|
+
reviews: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Paper:
|
|
29
|
+
id: str
|
|
30
|
+
title: str
|
|
31
|
+
source: dict
|
|
32
|
+
root: str | None = None
|
|
33
|
+
pdf: str | None = None
|
|
34
|
+
commit: str | None = None
|
|
35
|
+
envs: list[str] | None = None
|
|
36
|
+
publish: Publish = field(default_factory=Publish)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Config:
|
|
41
|
+
path: Path
|
|
42
|
+
name: str
|
|
43
|
+
lean_root: Path
|
|
44
|
+
modules: list[str]
|
|
45
|
+
scope_roots: list[str]
|
|
46
|
+
spec_prefix: str
|
|
47
|
+
accept_prefix: str
|
|
48
|
+
allowed_axioms: list[str]
|
|
49
|
+
inventory: Path
|
|
50
|
+
reviews: list[Path]
|
|
51
|
+
statement_authors: list[str]
|
|
52
|
+
ai_reviewers: list[str]
|
|
53
|
+
dismissals: Path | None
|
|
54
|
+
comparator_log: Path | None
|
|
55
|
+
papers: list[Paper]
|
|
56
|
+
out_dir: Path
|
|
57
|
+
review_check: str | None = None
|
|
58
|
+
display: Path | None = None
|
|
59
|
+
issues: str | None = None # "owner/repo" where the reader's "Raise issue" opens issues
|
|
60
|
+
site_url: str | None = None # the public address of the published site, for permalinks
|
|
61
|
+
author: str | None = None # the name the audit is published under (audit.json)
|
|
62
|
+
repository: str | None = None # "owner/repo" of the Lean source, if not the git remote
|
|
63
|
+
repository_private: bool = False # the reader does not show the repository's name
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def root(self) -> Path:
|
|
67
|
+
return self.path.parent
|
|
68
|
+
|
|
69
|
+
def paper(self, pid: str) -> Paper:
|
|
70
|
+
for p in self.papers:
|
|
71
|
+
if p.id == pid:
|
|
72
|
+
return p
|
|
73
|
+
raise ConfigError(f"no paper {pid!r} in {self.path}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
CONFIG_NAMES = ("machinechecked.toml", "prooflens.toml")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def work_dir_name(base: Path) -> str:
|
|
80
|
+
"""The working directory's name: `.machinechecked`, or `.prooflens` where one exists."""
|
|
81
|
+
return ".prooflens" if (base / ".prooflens").is_dir() and \
|
|
82
|
+
not (base / ".machinechecked").is_dir() else ".machinechecked"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def issue_repo(value: str | None, where: str) -> str | None:
|
|
86
|
+
"""A GitHub repository given as `owner/repo` (or its URL), checked."""
|
|
87
|
+
if not value:
|
|
88
|
+
return None
|
|
89
|
+
v = re.sub(r"^(https?://)?(www\.)?github\.com/", "", value.strip()).rstrip("/")
|
|
90
|
+
v = re.sub(r"\.git$", "", v)
|
|
91
|
+
if not re.fullmatch(r"[A-Za-z0-9][\w.-]*/[\w.-]+", v):
|
|
92
|
+
raise ConfigError(f"{where}: issues must be a GitHub repository, owner/repo: {value!r}")
|
|
93
|
+
return v
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _req(d: dict, key: str, where: str):
|
|
97
|
+
if key not in d:
|
|
98
|
+
raise ConfigError(f"{where}: missing `{key}`")
|
|
99
|
+
return d[key]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def load(path: str | Path) -> Config:
|
|
103
|
+
path = Path(path).resolve()
|
|
104
|
+
with open(path, "rb") as f:
|
|
105
|
+
raw = tomllib.load(f)
|
|
106
|
+
base = path.parent
|
|
107
|
+
proj = _req(raw, "project", str(path))
|
|
108
|
+
papers = []
|
|
109
|
+
for p in raw.get("paper", []):
|
|
110
|
+
src = _req(p, "source", "paper")
|
|
111
|
+
if not any(k in src for k in ("path", "git", "arxiv")):
|
|
112
|
+
raise ConfigError(f"paper {p.get('id')}: source needs one of path, git, arxiv")
|
|
113
|
+
papers.append(Paper(
|
|
114
|
+
id=_req(p, "id", "paper"), title=p.get("title", p["id"]), source=src,
|
|
115
|
+
root=p.get("root"), pdf=p.get("pdf"), commit=src.get("rev"), envs=p.get("envs"),
|
|
116
|
+
publish=Publish(**p.get("publish", {}))))
|
|
117
|
+
return Config(
|
|
118
|
+
path=path,
|
|
119
|
+
name=_req(proj, "name", "project"),
|
|
120
|
+
lean_root=(base / proj.get("lean_root", ".")).resolve(),
|
|
121
|
+
modules=_req(proj, "modules", "project"),
|
|
122
|
+
scope_roots=_req(proj, "scope_roots", "project"),
|
|
123
|
+
spec_prefix=_req(proj, "spec_prefix", "project"),
|
|
124
|
+
accept_prefix=_req(proj, "accept_prefix", "project"),
|
|
125
|
+
allowed_axioms=proj.get("allowed_axioms", STANDARD_AXIOMS),
|
|
126
|
+
inventory=(base / _req(proj, "inventory", "project")).resolve(),
|
|
127
|
+
reviews=[(base / r).resolve() for r in proj.get("reviews", [])],
|
|
128
|
+
statement_authors=proj.get("statement_authors", []),
|
|
129
|
+
ai_reviewers=proj.get("ai_reviewers", []),
|
|
130
|
+
dismissals=(base / proj["dismissals"]).resolve() if "dismissals" in proj else None,
|
|
131
|
+
comparator_log=(base / proj["comparator_log"]).resolve()
|
|
132
|
+
if "comparator_log" in proj else None,
|
|
133
|
+
papers=papers,
|
|
134
|
+
out_dir=(base / proj.get("out_dir", work_dir_name(base))).resolve(),
|
|
135
|
+
# A command run after promoting reviews (from the configuration's directory): the
|
|
136
|
+
# project's own acceptance checker, if it has one.
|
|
137
|
+
review_check=raw.get("review", {}).get("check"),
|
|
138
|
+
# Display corrections (TeX descriptions, printed numbers), outside the inventory.
|
|
139
|
+
display=(base / proj["display"]).resolve() if "display" in proj else None,
|
|
140
|
+
issues=issue_repo(proj.get("issues"), str(path)),
|
|
141
|
+
site_url=proj.get("site_url"),
|
|
142
|
+
author=proj.get("author"),
|
|
143
|
+
repository=issue_repo(proj.get("repository"), str(path)),
|
|
144
|
+
repository_private=bool(proj.get("repository_private", False)),
|
|
145
|
+
)
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/-!
|
|
2
|
+
# The ProofLens probe
|
|
3
|
+
|
|
4
|
+
Body of the probe that `prooflens probe` runs inside a Lean project. The CLI prepends
|
|
5
|
+
`import Lean` and the project's modules and runs the result with `lake env lean`, so this file
|
|
6
|
+
has no imports of its own and is not built as part of any library.
|
|
7
|
+
|
|
8
|
+
Input: the JSON query named by `PROOFLENS_QUERY`. Output: JSON lines written to the file named
|
|
9
|
+
by `PROOFLENS_OUT`, one record per request and one per declaration of the definition closure.
|
|
10
|
+
|
|
11
|
+
For a specification the record holds two representations of the proposition:
|
|
12
|
+
|
|
13
|
+
* the verification representation: the declaration, its axioms, the constants it uses, its
|
|
14
|
+
structural hashes (`legacy_hash`, a 128-bit structural hash kept for compatibility with
|
|
15
|
+
existing review markers, and `canon`, a canonical serialisation the CLI hashes with SHA-256);
|
|
16
|
+
* the inspection representation: the body of the definition split into a quantifier prefix
|
|
17
|
+
(universal variables, hypotheses, instances, existential witnesses) and a conclusion, each
|
|
18
|
+
pretty-printed with the constants it mentions marked for navigation.
|
|
19
|
+
-/
|
|
20
|
+
|
|
21
|
+
open Lean Meta Elab
|
|
22
|
+
|
|
23
|
+
namespace ProofLensProbe
|
|
24
|
+
|
|
25
|
+
/-! ## The query -/
|
|
26
|
+
|
|
27
|
+
structure Request where
|
|
28
|
+
kind : String
|
|
29
|
+
name : String
|
|
30
|
+
spec : Option String := none
|
|
31
|
+
deriving FromJson, Inhabited
|
|
32
|
+
|
|
33
|
+
structure Query where
|
|
34
|
+
scope_roots : Array String
|
|
35
|
+
spec_prefix : String
|
|
36
|
+
accept_prefix : String
|
|
37
|
+
allowed_axioms : Array String
|
|
38
|
+
requests : Array Request
|
|
39
|
+
deriving FromJson, Inhabited
|
|
40
|
+
|
|
41
|
+
/-! ## Environment helpers -/
|
|
42
|
+
|
|
43
|
+
def moduleOf (env : Environment) (n : Name) : Name :=
|
|
44
|
+
match env.getModuleIdxFor? n with
|
|
45
|
+
| some idx => env.header.moduleNames[idx.toNat]!
|
|
46
|
+
| none => `«this file»
|
|
47
|
+
|
|
48
|
+
def inScope (roots : Array Name) (env : Environment) (n : Name) : Bool :=
|
|
49
|
+
roots.contains (moduleOf env n).getRoot
|
|
50
|
+
|
|
51
|
+
def kindOf : ConstantInfo → String
|
|
52
|
+
| .thmInfo _ => "theorem" | .defnInfo _ => "def" | .inductInfo _ => "inductive"
|
|
53
|
+
| .ctorInfo _ => "ctor" | .opaqueInfo _ => "opaque" | .recInfo _ => "rec"
|
|
54
|
+
| .axiomInfo _ => "axiom" | .quotInfo _ => "quot"
|
|
55
|
+
|
|
56
|
+
def rangeJson (n : Name) : MetaM Json := do
|
|
57
|
+
match ← findDeclarationRanges? n with
|
|
58
|
+
| some r => return Json.mkObj [("line", r.range.pos.line), ("end_line", r.range.endPos.line)]
|
|
59
|
+
| none => return Json.null
|
|
60
|
+
|
|
61
|
+
/-! ## Proof erasure and fingerprints -/
|
|
62
|
+
|
|
63
|
+
/-- The expression with every proof replaced by one constant, so that hashes see statements and
|
|
64
|
+
data only. -/
|
|
65
|
+
def erase (e : Expr) : MetaM Expr := do
|
|
66
|
+
let e ← instantiateMVars e
|
|
67
|
+
transform e (pre := fun x => do
|
|
68
|
+
if x.isFVar || x.isBVar || x.isSort then return .continue
|
|
69
|
+
let p ← try isProof x catch _ => pure false
|
|
70
|
+
if p then return .done (mkConst `«erased proof»)
|
|
71
|
+
return .continue)
|
|
72
|
+
|
|
73
|
+
/-- A 64-bit structural hash, blind to binder
|
|
74
|
+
names and metadata; its form is fixed so that existing review markers keep matching. -/
|
|
75
|
+
partial def deepHash (e : Expr) : StateM (Std.HashMap Expr UInt64) UInt64 := do
|
|
76
|
+
if let some h := (← get)[e]? then return h
|
|
77
|
+
let h ← match e with
|
|
78
|
+
| .bvar i => pure (mixHash 11 (hash i))
|
|
79
|
+
| .fvar i => pure (mixHash 13 (hash i.name.toString))
|
|
80
|
+
| .mvar i => pure (mixHash 17 (hash i.name.toString))
|
|
81
|
+
| .sort l => pure (mixHash 19 (hash (toString l)))
|
|
82
|
+
| .const n ls => pure (mixHash 23 (mixHash (hash n.toString) (hash (toString ls))))
|
|
83
|
+
| .lit (.natVal k) => pure (mixHash 29 (hash k))
|
|
84
|
+
| .lit (.strVal t) => pure (mixHash 31 (hash t))
|
|
85
|
+
| .mdata _ b => deepHash b
|
|
86
|
+
| .app f a => do pure (mixHash 37 (mixHash (← deepHash f) (← deepHash a)))
|
|
87
|
+
| .lam _ t b bi => do
|
|
88
|
+
pure (mixHash 41 (mixHash (hash (toString (repr bi)))
|
|
89
|
+
(mixHash (← deepHash t) (← deepHash b))))
|
|
90
|
+
| .forallE _ t b bi => do
|
|
91
|
+
pure (mixHash 43 (mixHash (hash (toString (repr bi)))
|
|
92
|
+
(mixHash (← deepHash t) (← deepHash b))))
|
|
93
|
+
| .letE _ t v b _ => do
|
|
94
|
+
pure (mixHash 47 (mixHash (← deepHash t) (mixHash (← deepHash v) (← deepHash b))))
|
|
95
|
+
| .proj s i b => do
|
|
96
|
+
pure (mixHash 53 (mixHash (hash s.toString) (mixHash (hash i) (← deepHash b))))
|
|
97
|
+
modify (·.insert e h)
|
|
98
|
+
return h
|
|
99
|
+
|
|
100
|
+
def hex (h : UInt64) : String :=
|
|
101
|
+
let s := String.ofList (Nat.toDigits 16 h.toNat)
|
|
102
|
+
"".pushn '0' (16 - s.length) ++ s
|
|
103
|
+
|
|
104
|
+
/-- State of the canonical serialisation: the index of every node already emitted. -/
|
|
105
|
+
structure CanonState where
|
|
106
|
+
ids : Std.HashMap Expr Nat := {}
|
|
107
|
+
lines : Array String := #[]
|
|
108
|
+
|
|
109
|
+
/-- Canonical serialisation of an expression as a table of shared nodes, in post-order. Binder
|
|
110
|
+
names and metadata are omitted; binder kinds, constants, universe levels and literals are kept.
|
|
111
|
+
The table form keeps the size linear in the number of distinct subterms. -/
|
|
112
|
+
partial def canon (e : Expr) : StateM CanonState Nat := do
|
|
113
|
+
if let some i := (← get).ids[e]? then return i
|
|
114
|
+
let line ← match e with
|
|
115
|
+
| .bvar i => pure s!"bvar {i}"
|
|
116
|
+
| .fvar i => pure s!"fvar {i.name}"
|
|
117
|
+
| .mvar i => pure s!"mvar {i.name}"
|
|
118
|
+
| .sort l => pure s!"sort {l}"
|
|
119
|
+
| .const n ls => pure s!"const {n} {ls}"
|
|
120
|
+
| .lit (.natVal k) => pure s!"nat {k}"
|
|
121
|
+
| .lit (.strVal t) => pure s!"str {(toJson t).compress}"
|
|
122
|
+
| .mdata _ b => return ← canon b
|
|
123
|
+
| .app f a => do pure s!"app {← canon f} {← canon a}"
|
|
124
|
+
| .lam _ t b bi => do pure s!"lam {repr bi} {← canon t} {← canon b}"
|
|
125
|
+
| .forallE _ t b bi => do pure s!"pi {repr bi} {← canon t} {← canon b}"
|
|
126
|
+
| .letE _ t v b _ => do pure s!"let {← canon t} {← canon v} {← canon b}"
|
|
127
|
+
| .proj s i b => do pure s!"proj {s} {i} {← canon b}"
|
|
128
|
+
let s ← get
|
|
129
|
+
let i := s.lines.size
|
|
130
|
+
set ({ ids := s.ids.insert e i, lines := s.lines.push line } : CanonState)
|
|
131
|
+
return i
|
|
132
|
+
|
|
133
|
+
def canonString (ty : Expr) (val : Option Expr) : String :=
|
|
134
|
+
let act : StateM CanonState (Nat × Option Nat) := do
|
|
135
|
+
let t ← canon ty
|
|
136
|
+
let v ← match val with | some v => some <$> canon v | none => pure none
|
|
137
|
+
return (t, v)
|
|
138
|
+
let ((t, v), s) := act.run {}
|
|
139
|
+
let root := match v with | some v => s!"root {t} {v}" | none => s!"root {t}"
|
|
140
|
+
"\n".intercalate (s.lines.push root).toList
|
|
141
|
+
|
|
142
|
+
/-- Fingerprint data of a constant with its proofs erased, and the constants it uses. -/
|
|
143
|
+
def constPrint (ci : ConstantInfo) : MetaM (String × String × Array Name) := do
|
|
144
|
+
let ty ← erase ci.type
|
|
145
|
+
let mut used := ty.getUsedConstants
|
|
146
|
+
let mut vh : UInt64 := 0
|
|
147
|
+
let mut val : Option Expr := none
|
|
148
|
+
match ci with
|
|
149
|
+
| .defnInfo d =>
|
|
150
|
+
let v ← erase d.value
|
|
151
|
+
vh := (deepHash v).run' {}
|
|
152
|
+
val := some v
|
|
153
|
+
used := used ++ v.getUsedConstants
|
|
154
|
+
| .inductInfo i => used := used ++ i.ctors.toArray
|
|
155
|
+
| .ctorInfo c => used := used.push c.induct
|
|
156
|
+
| _ => pure ()
|
|
157
|
+
let lvls := " ".intercalate (ci.levelParams.map toString)
|
|
158
|
+
let c := s!"levels {lvls}\n" ++ canonString ty val
|
|
159
|
+
return (hex ((deepHash ty).run' {}) ++ hex vh, c, used)
|
|
160
|
+
|
|
161
|
+
/-! ## Pretty-printing with navigation -/
|
|
162
|
+
|
|
163
|
+
/-- Flatten tagged code into segments; a segment carries the constant it names when the
|
|
164
|
+
innermost subexpression around it is a constant. -/
|
|
165
|
+
partial def segments (t : Widget.CodeWithInfos) (ref : Option Name) (acc : Array Json) : Array Json :=
|
|
166
|
+
match t with
|
|
167
|
+
| Widget.TaggedText.text s =>
|
|
168
|
+
match ref with
|
|
169
|
+
| some n => acc.push (Json.mkObj [("t", s), ("ref", n.toString)])
|
|
170
|
+
| none => acc.push (Json.mkObj [("t", s)])
|
|
171
|
+
| Widget.TaggedText.append xs => xs.foldl (fun a x => segments x ref a) acc
|
|
172
|
+
| Widget.TaggedText.tag info sub =>
|
|
173
|
+
let r := match info.info.val.info with
|
|
174
|
+
| .ofTermInfo ti => if let .const n _ := ti.expr then some n else none
|
|
175
|
+
| _ => none
|
|
176
|
+
segments sub (r <|> none) acc
|
|
177
|
+
|
|
178
|
+
/-- The pretty-printed expression with navigable constants; plain text if tagging fails, and a
|
|
179
|
+
marker if printing fails, so that one large term cannot stop the probe. -/
|
|
180
|
+
def ppJson (e : Expr) : MetaM Json := do
|
|
181
|
+
try
|
|
182
|
+
let code ← Widget.ppExprTagged e
|
|
183
|
+
let text := toString (← ppExpr e)
|
|
184
|
+
return Json.mkObj [("text", text), ("segments", Json.arr (segments code none #[]))]
|
|
185
|
+
catch _ =>
|
|
186
|
+
try
|
|
187
|
+
return Json.mkObj [("text", toString (← ppExpr e))]
|
|
188
|
+
catch err =>
|
|
189
|
+
return Json.mkObj [("text", "(not printable)"), ("error", toString (← err.toMessageData.toString))]
|
|
190
|
+
|
|
191
|
+
/-! ## The inspection representation -/
|
|
192
|
+
|
|
193
|
+
def isExistsApp (e : Expr) : Option (Name × Expr) :=
|
|
194
|
+
if e.isAppOfArity ``Exists 2 then
|
|
195
|
+
match e.appArg! with
|
|
196
|
+
| .lam n _ _ _ => some (n, e.appArg!)
|
|
197
|
+
| _ => none
|
|
198
|
+
else none
|
|
199
|
+
|
|
200
|
+
/-- Split a proposition into its quantifier prefix and its conclusion. Universal binders are
|
|
201
|
+
`forall` (data), `hypothesis` (a proposition) or `instance`; existential binders are `exists`.
|
|
202
|
+
Definitions are not unfolded: the prefix is the one written in the specification. -/
|
|
203
|
+
partial def telescope (e : Expr) (acc : Array Json) : MetaM (Array Json × Json) := do
|
|
204
|
+
match e with
|
|
205
|
+
| .forallE n t body bi =>
|
|
206
|
+
let isHyp ← isProp t
|
|
207
|
+
let role :=
|
|
208
|
+
if bi.isInstImplicit then "instance" else if isHyp then "hypothesis" else "forall"
|
|
209
|
+
let tj ← ppJson t
|
|
210
|
+
withLocalDecl n bi t fun x => do
|
|
211
|
+
let hyps := (acc.filter fun j => j.getObjValD "role" == Json.str "hypothesis").size
|
|
212
|
+
let nm := if n.hasMacroScopes || n.isAnonymous then s!"h{hyps + 1}"
|
|
213
|
+
else n.toString
|
|
214
|
+
let j := Json.mkObj [("role", role), ("name", nm),
|
|
215
|
+
("type", tj), ("binder", binderStr bi)]
|
|
216
|
+
telescope (body.instantiate1 x) (acc.push j)
|
|
217
|
+
| _ =>
|
|
218
|
+
match isExistsApp e with
|
|
219
|
+
| some (_, .lam n t body bi) =>
|
|
220
|
+
let tj ← ppJson t
|
|
221
|
+
withLocalDecl n bi t fun x => do
|
|
222
|
+
let j := Json.mkObj [("role", "exists"), ("name", n.eraseMacroScopes.toString),
|
|
223
|
+
("type", tj), ("binder", binderStr bi)]
|
|
224
|
+
telescope (body.instantiate1 x) (acc.push j)
|
|
225
|
+
| _ => return (acc, ← ppJson e)
|
|
226
|
+
where
|
|
227
|
+
binderStr : BinderInfo → String
|
|
228
|
+
| .default => "explicit" | .implicit => "implicit"
|
|
229
|
+
| .strictImplicit => "strict-implicit" | .instImplicit => "instance"
|
|
230
|
+
|
|
231
|
+
/-- Constants of the project that `e` uses directly. -/
|
|
232
|
+
def projectConsts (roots : Array Name) (e : Expr) : MetaM (Array String) := do
|
|
233
|
+
let env ← getEnv
|
|
234
|
+
return e.getUsedConstants.filter (inScope roots env) |>.map toString
|
|
235
|
+
|
|
236
|
+
/-! ## Records -/
|
|
237
|
+
|
|
238
|
+
def emit (h : IO.FS.Handle) (j : Json) : IO Unit := h.putStrLn j.compress
|
|
239
|
+
|
|
240
|
+
def declRecord (roots : Array Name) (n : Name) (ci : ConstantInfo) : MetaM (Json × Array Name) := do
|
|
241
|
+
let env ← getEnv
|
|
242
|
+
let (lh, c, used) ← constPrint ci
|
|
243
|
+
let sig ← ppJson ci.type
|
|
244
|
+
let value ← match ci with
|
|
245
|
+
| .defnInfo d =>
|
|
246
|
+
if (← isProp ci.type) then pure Json.null
|
|
247
|
+
else do
|
|
248
|
+
let v ← ppJson d.value
|
|
249
|
+
pure (if v.compress.length > 20000 then Json.null else v)
|
|
250
|
+
| _ => pure Json.null
|
|
251
|
+
let doc ← findDocString? env n
|
|
252
|
+
let j := Json.mkObj [
|
|
253
|
+
("record", "decl"), ("name", n.toString), ("kind", kindOf ci),
|
|
254
|
+
("module", (moduleOf env n).toString), ("range", ← rangeJson n),
|
|
255
|
+
("doc", match doc with | some d => Json.str d | none => Json.null),
|
|
256
|
+
("legacy_hash", lh), ("canon", c), ("signature", sig), ("value", value),
|
|
257
|
+
("uses", Json.arr ((used.filter fun u => inScope roots env u && !u.isInternal).map
|
|
258
|
+
(Json.str ·.toString)))]
|
|
259
|
+
return (j, used)
|
|
260
|
+
|
|
261
|
+
def emitClosure (h : IO.FS.Handle) (roots : Array Name) (root : Name) (used : Array Name)
|
|
262
|
+
(seen : IO.Ref NameSet) : MetaM (Array String) := do
|
|
263
|
+
let env ← getEnv
|
|
264
|
+
let mut stack := used
|
|
265
|
+
let mut members : Array String := #[]
|
|
266
|
+
let mut visited : NameSet := {}
|
|
267
|
+
while !stack.isEmpty do
|
|
268
|
+
let n := stack.back!
|
|
269
|
+
stack := stack.pop
|
|
270
|
+
if visited.contains n || n == root then continue
|
|
271
|
+
visited := visited.insert n
|
|
272
|
+
unless inScope roots env n do continue
|
|
273
|
+
let some c := env.find? n | continue
|
|
274
|
+
members := members.push n.toString
|
|
275
|
+
let (j, more) ← declRecord roots n c
|
|
276
|
+
unless (← seen.get).contains n do
|
|
277
|
+
seen.modify (·.insert n)
|
|
278
|
+
emit h j
|
|
279
|
+
stack := stack ++ more
|
|
280
|
+
return members.qsort (· < ·)
|
|
281
|
+
|
|
282
|
+
def disallowed (allowed : Array Name) (axs : Array Name) : Array Name :=
|
|
283
|
+
axs.filter (!allowed.contains ·)
|
|
284
|
+
|
|
285
|
+
def specRecord (h : IO.FS.Handle) (q : Query) (roots : Array Name) (seen : IO.Ref NameSet)
|
|
286
|
+
(spec : Name) : MetaM Unit := do
|
|
287
|
+
let env ← getEnv
|
|
288
|
+
let some ci := env.find? spec
|
|
289
|
+
| emit h (Json.mkObj [("record", "spec"), ("name", spec.toString), ("errors", toJson #["missing"])])
|
|
290
|
+
let mut errs : Array String := #[]
|
|
291
|
+
unless ci matches .defnInfo _ do errs := errs.push "not-a-def"
|
|
292
|
+
unless ci.type == .sort .zero do errs := errs.push "type-not-Prop"
|
|
293
|
+
unless q.spec_prefix.toName.isPrefixOf (moduleOf env spec) do errs := errs.push "outside-spec-layer"
|
|
294
|
+
let axs ← collectAxioms spec
|
|
295
|
+
let bad := disallowed (q.allowed_axioms.map (·.toName)) axs
|
|
296
|
+
unless bad.isEmpty do errs := errs.push s!"axioms={bad.toList}"
|
|
297
|
+
let (j, used) ← declRecord roots spec ci
|
|
298
|
+
let (binders, concl) ← match ci with
|
|
299
|
+
| .defnInfo d => telescope d.value #[]
|
|
300
|
+
| _ => pure (#[], Json.null)
|
|
301
|
+
let closure ← emitClosure h roots spec used seen
|
|
302
|
+
let direct ← match ci with
|
|
303
|
+
| .defnInfo d => projectConsts roots d.value
|
|
304
|
+
| _ => pure #[]
|
|
305
|
+
let consts := match ci with
|
|
306
|
+
| .defnInfo d => d.value.getUsedConstants.map toString
|
|
307
|
+
| _ => #[]
|
|
308
|
+
emit h (Json.mkObj [("record", "spec"), ("name", spec.toString), ("errors", toJson errs),
|
|
309
|
+
("axioms", toJson (axs.map toString)), ("decl", j),
|
|
310
|
+
("binders", Json.arr binders), ("conclusion", concl),
|
|
311
|
+
("direct", toJson direct), ("consts", toJson consts), ("closure", toJson closure)])
|
|
312
|
+
|
|
313
|
+
def levelsMatch (ls : List Level) (params : List Name) : Bool :=
|
|
314
|
+
let ps := ls.filterMap fun l => match l with | .param p => some p | _ => none
|
|
315
|
+
ps.length == ls.length && ps.eraseDups.length == ps.length && ps.length == params.length
|
|
316
|
+
&& params.all ps.contains
|
|
317
|
+
|
|
318
|
+
/-- `accept`: the type is exactly the specification; `refute`: exactly its negation; `witness`:
|
|
319
|
+
any theorem of the acceptance layer. -/
|
|
320
|
+
def proofRecord (h : IO.FS.Handle) (q : Query) (kind : String) (thm : Name)
|
|
321
|
+
(spec : Option Name) : MetaM Unit := do
|
|
322
|
+
let env ← getEnv
|
|
323
|
+
let some ci := env.find? thm
|
|
324
|
+
| emit h (Json.mkObj [("record", kind), ("name", thm.toString), ("errors", toJson #["missing"])])
|
|
325
|
+
let mut errs : Array String := #[]
|
|
326
|
+
unless ci matches .thmInfo _ do errs := errs.push "not-a-theorem"
|
|
327
|
+
unless q.accept_prefix.toName.isPrefixOf (moduleOf env thm) do
|
|
328
|
+
errs := errs.push "outside-accept-layer"
|
|
329
|
+
match kind, spec with
|
|
330
|
+
| "accept", some s =>
|
|
331
|
+
match ci.type with
|
|
332
|
+
| .const c ls => unless c == s && levelsMatch ls ci.levelParams do errs := errs.push "type-not-spec"
|
|
333
|
+
| _ => errs := errs.push "type-not-spec"
|
|
334
|
+
| "refute", some s =>
|
|
335
|
+
match ci.type with
|
|
336
|
+
| .app (.const ``Not []) (.const c _) => unless c == s do errs := errs.push "type-not-negated-spec"
|
|
337
|
+
| .forallE _ (.const c _) (.const ``False []) _ =>
|
|
338
|
+
unless c == s do errs := errs.push "type-not-negated-spec"
|
|
339
|
+
| _ => errs := errs.push "type-not-negated-spec"
|
|
340
|
+
| _, _ => pure ()
|
|
341
|
+
let axs ← collectAxioms thm
|
|
342
|
+
let bad := disallowed (q.allowed_axioms.map (·.toName)) axs
|
|
343
|
+
unless bad.isEmpty do errs := errs.push s!"axioms={bad.toList}"
|
|
344
|
+
let ty ← ppJson ci.type
|
|
345
|
+
emit h (Json.mkObj [("record", kind), ("name", thm.toString),
|
|
346
|
+
("spec", match spec with | some s => Json.str s.toString | none => Json.null),
|
|
347
|
+
("errors", toJson errs), ("axioms", toJson (axs.map toString)),
|
|
348
|
+
("type", ty), ("module", (moduleOf env thm).toString), ("range", ← rangeJson thm),
|
|
349
|
+
("sorry", toJson (axs.contains ``sorryAx))])
|
|
350
|
+
|
|
351
|
+
/-- Whether a name is one a reader looks for: not generated by the compiler or elaborator. -/
|
|
352
|
+
def userFacing (n : Name) : Bool :=
|
|
353
|
+
!n.isInternal && !(n.components.any fun c =>
|
|
354
|
+
let s := c.toString
|
|
355
|
+
s.startsWith "_" || s.startsWith "match_" || s.startsWith "proof_" || s == "eq_def"
|
|
356
|
+
|| s.startsWith "eq_" && (s.drop 3).all Char.isDigit)
|
|
357
|
+
|
|
358
|
+
/-- Every user-facing theorem, definition and type declared in the modules of `roots`: name,
|
|
359
|
+
kind, module, line, docstring and statement as text. -/
|
|
360
|
+
def indexRecords (h : IO.FS.Handle) (roots : Array Name) : MetaM Unit := do
|
|
361
|
+
let env ← getEnv
|
|
362
|
+
let names := env.header.moduleNames
|
|
363
|
+
for i in [:names.size] do
|
|
364
|
+
let mod := names[i]!
|
|
365
|
+
unless roots.contains mod.getRoot do continue
|
|
366
|
+
for n in env.header.moduleData[i]!.constNames do
|
|
367
|
+
unless userFacing n do continue
|
|
368
|
+
let some ci := env.find? n | continue
|
|
369
|
+
let kind := match ci with
|
|
370
|
+
| .thmInfo _ => some "theorem" | .defnInfo _ => some "def"
|
|
371
|
+
| .inductInfo _ => some "inductive" | .opaqueInfo _ => some "opaque"
|
|
372
|
+
| .axiomInfo _ => some "axiom" | _ => none
|
|
373
|
+
let some k := kind | continue
|
|
374
|
+
if k == "def" && (← isInstance n) then continue
|
|
375
|
+
let ty ← try pure (toString (← ppExpr ci.type)) catch _ => pure "(not printable)"
|
|
376
|
+
let doc ← findDocString? env n
|
|
377
|
+
emit h (Json.mkObj [("record", "index"), ("name", n.toString), ("kind", k),
|
|
378
|
+
("module", mod.toString), ("range", ← rangeJson n),
|
|
379
|
+
("doc", match doc with | some d => Json.str d | none => Json.null),
|
|
380
|
+
("type", if ty.length > 4000 then (ty.take 4000).toString ++ " …" else ty)])
|
|
381
|
+
|
|
382
|
+
/-- A theorem of a link: its statement split like a specification, and its axioms. -/
|
|
383
|
+
def linkRecord (h : IO.FS.Handle) (q : Query) (roots : Array Name) (seen : IO.Ref NameSet)
|
|
384
|
+
(n : Name) : MetaM Unit := do
|
|
385
|
+
let env ← getEnv
|
|
386
|
+
let some ci := env.find? n
|
|
387
|
+
| emit h (Json.mkObj [("record", "link"), ("name", n.toString), ("errors", toJson #["missing"])])
|
|
388
|
+
-- A theorem whose type is a closed proposition (a locked statement) shows that proposition.
|
|
389
|
+
let stmt ← match ci.type with
|
|
390
|
+
| .const c _ => match env.find? c with
|
|
391
|
+
| some (.defnInfo d) => if d.type == .sort .zero then pure d.value else pure ci.type
|
|
392
|
+
| _ => pure ci.type
|
|
393
|
+
| _ => pure ci.type
|
|
394
|
+
let (binders, concl) ← telescope stmt #[]
|
|
395
|
+
let axs ← collectAxioms n
|
|
396
|
+
let bad := disallowed (q.allowed_axioms.map (·.toName)) axs
|
|
397
|
+
let (j, used) ← declRecord roots n ci
|
|
398
|
+
let closure ← emitClosure h roots n used seen
|
|
399
|
+
emit h (Json.mkObj [("record", "link"), ("name", n.toString), ("kind", kindOf ci),
|
|
400
|
+
("errors", toJson (if bad.isEmpty then #[] else #[s!"axioms={bad.toList}"])),
|
|
401
|
+
("axioms", toJson (axs.map toString)), ("sorry", toJson (axs.contains ``sorryAx)),
|
|
402
|
+
("decl", j), ("binders", Json.arr binders), ("conclusion", concl),
|
|
403
|
+
("closure", toJson closure)])
|
|
404
|
+
|
|
405
|
+
/-- Every module of the specification layer that imports a module of the acceptance layer. -/
|
|
406
|
+
def layering (h : IO.FS.Handle) (q : Query) : CoreM Unit := do
|
|
407
|
+
let env ← getEnv
|
|
408
|
+
let names := env.header.moduleNames
|
|
409
|
+
let mut violations : Array Json := #[]
|
|
410
|
+
for i in [:names.size] do
|
|
411
|
+
let mod := names[i]!
|
|
412
|
+
unless q.spec_prefix.toName.isPrefixOf mod do continue
|
|
413
|
+
let mut seen : NameSet := {}
|
|
414
|
+
let mut stack : Array Name := #[mod]
|
|
415
|
+
while !stack.isEmpty do
|
|
416
|
+
let m := stack.back!
|
|
417
|
+
stack := stack.pop
|
|
418
|
+
if seen.contains m then continue
|
|
419
|
+
seen := seen.insert m
|
|
420
|
+
if q.accept_prefix.toName.isPrefixOf m then
|
|
421
|
+
violations := violations.push (Json.mkObj [("module", mod.toString), ("imports", m.toString)])
|
|
422
|
+
let some idx := env.getModuleIdx? m | continue
|
|
423
|
+
for imp in env.header.moduleData[idx.toNat]!.imports do
|
|
424
|
+
stack := stack.push imp.module
|
|
425
|
+
emit h (Json.mkObj [("record", "layering"), ("violations", Json.arr violations)])
|
|
426
|
+
|
|
427
|
+
end ProofLensProbe
|
|
428
|
+
|
|
429
|
+
open ProofLensProbe in
|
|
430
|
+
#eval show MetaM Unit from do
|
|
431
|
+
let some qpath ← IO.getEnv "PROOFLENS_QUERY" | throwError "PROOFLENS_QUERY unset"
|
|
432
|
+
let some opath ← IO.getEnv "PROOFLENS_OUT" | throwError "PROOFLENS_OUT unset"
|
|
433
|
+
let q ← match (Json.parse (← IO.FS.readFile qpath)) >>= fromJson? (α := Query) with
|
|
434
|
+
| .ok q => pure q
|
|
435
|
+
| .error e => throwError s!"bad query: {e}"
|
|
436
|
+
let roots := q.scope_roots.map (·.toName)
|
|
437
|
+
let seen ← IO.mkRef ({} : NameSet)
|
|
438
|
+
let h ← IO.FS.Handle.mk opath .write
|
|
439
|
+
do
|
|
440
|
+
emit h (Json.mkObj [("record", "env"), ("lean", Lean.versionString),
|
|
441
|
+
("githash", Lean.githash)])
|
|
442
|
+
layering h q
|
|
443
|
+
for r in q.requests do
|
|
444
|
+
-- Each request has its own heartbeat budget.
|
|
445
|
+
try withCurrHeartbeats <|
|
|
446
|
+
match r.kind with
|
|
447
|
+
| "spec" => specRecord h q roots seen r.name.toName
|
|
448
|
+
| "accept" | "refute" => proofRecord h q r.kind r.name.toName (r.spec.map (·.toName))
|
|
449
|
+
| "witness" => proofRecord h q "witness" r.name.toName none
|
|
450
|
+
| "index" => indexRecords h roots
|
|
451
|
+
| "link" => linkRecord h q roots seen r.name.toName
|
|
452
|
+
| "decl" => do
|
|
453
|
+
let env ← getEnv
|
|
454
|
+
if let some ci := env.find? r.name.toName then
|
|
455
|
+
let (j, used) ← declRecord roots r.name.toName ci
|
|
456
|
+
seen.modify (·.insert r.name.toName)
|
|
457
|
+
emit h j
|
|
458
|
+
let members ← emitClosure h roots r.name.toName used seen
|
|
459
|
+
emit h (Json.mkObj [("record", "defclosure"), ("name", r.name),
|
|
460
|
+
("closure", toJson members)])
|
|
461
|
+
else
|
|
462
|
+
emit h (Json.mkObj [("record", "defclosure"), ("name", r.name),
|
|
463
|
+
("errors", toJson #["missing"])])
|
|
464
|
+
| _ => pure ()
|
|
465
|
+
catch err =>
|
|
466
|
+
emit h (Json.mkObj [("record", if r.kind == "decl" then "defclosure" else r.kind),
|
|
467
|
+
("name", r.name), ("spec", match r.spec with | some x => Json.str x | none => Json.null),
|
|
468
|
+
("errors", toJson #[s!"probe-error: {← err.toMessageData.toString}"])])
|
|
469
|
+
emit h (Json.mkObj [("record", "done")])
|