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.
Files changed (58) hide show
  1. machinechecked-0.1.0.dist-info/METADATA +108 -0
  2. machinechecked-0.1.0.dist-info/RECORD +58 -0
  3. machinechecked-0.1.0.dist-info/WHEEL +5 -0
  4. machinechecked-0.1.0.dist-info/entry_points.txt +3 -0
  5. machinechecked-0.1.0.dist-info/licenses/LICENSE +201 -0
  6. machinechecked-0.1.0.dist-info/top_level.txt +1 -0
  7. prooflens/__init__.py +1 -0
  8. prooflens/__main__.py +3 -0
  9. prooflens/assess.py +504 -0
  10. prooflens/audit.py +115 -0
  11. prooflens/bundle.py +70 -0
  12. prooflens/cli.py +542 -0
  13. prooflens/config.py +145 -0
  14. prooflens/data/ProofLensProbe.lean +469 -0
  15. prooflens/data/prooflens.sty +75 -0
  16. prooflens/data/schema/evidence.schema.json +24 -0
  17. prooflens/data/schema/inventory.schema.json +55 -0
  18. prooflens/data/schema/manifest-claim.schema.json +126 -0
  19. prooflens/data/schema/manifest-index.schema.json +128 -0
  20. prooflens/evidence.py +60 -0
  21. prooflens/export.py +227 -0
  22. prooflens/formalization.py +106 -0
  23. prooflens/manifest.py +296 -0
  24. prooflens/pdfclaims.py +129 -0
  25. prooflens/pipeline.py +331 -0
  26. prooflens/probe.py +126 -0
  27. prooflens/promote.py +204 -0
  28. prooflens/review_queue.py +138 -0
  29. prooflens/reviews.py +105 -0
  30. prooflens/schemas.py +19 -0
  31. prooflens/server.py +63 -0
  32. prooflens/simple.py +628 -0
  33. prooflens/sources.py +164 -0
  34. prooflens/tex.py +408 -0
  35. prooflens/util.py +66 -0
  36. prooflens/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  37. prooflens/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  38. prooflens/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  39. prooflens/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  40. prooflens/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  41. prooflens/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  42. prooflens/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  43. prooflens/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  44. prooflens/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  45. prooflens/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  46. prooflens/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  47. prooflens/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  48. prooflens/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  49. prooflens/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  50. prooflens/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  51. prooflens/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  52. prooflens/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  53. prooflens/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  54. prooflens/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  55. prooflens/web/assets/index-B2GDDbsW.css +1 -0
  56. prooflens/web/assets/index-DN7uLe7p.js +285 -0
  57. prooflens/web/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  58. 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")])