├── .envrc
├── .gitignore
├── LICENSE
├── Loogle.lean
├── Loogle
├── BaseIOThunk.lean
├── BlackListed.lean
├── Cache.lean
├── Find.lean
├── NameRel.lean
├── RBTree.lean
└── Trie.lean
├── LoogleMathlibCache.lean
├── README.md
├── Seccomp.lean
├── Tests.lean
├── blurb.html
├── blurb.md
├── flake.lock
├── flake.nix
├── lake-manifest.json
├── lakefile.lean
├── lean-toolchain
├── loogle.png
├── loogle_seccomp.c
└── server.py
/.envrc:
--------------------------------------------------------------------------------
1 | use flake
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | .lake
3 | /lake-packages/*
4 | result
5 | lakefile.olean
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License 2.0 (Apache)
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
13 |
14 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
15 |
16 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
17 |
18 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
19 |
20 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
21 |
22 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
23 |
24 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
25 |
26 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
27 |
28 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
29 |
30 | 2. Grant of Copyright License.
31 |
32 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
33 |
34 | 3. Grant of Patent License.
35 |
36 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
37 |
38 | 4. Redistribution.
39 |
40 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
41 |
42 | 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and
43 |
44 | 2. You must cause any modified files to carry prominent notices stating that You changed the files; and
45 |
46 | 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
47 |
48 | 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
49 |
50 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
51 |
52 | 5. Submission of Contributions.
53 |
54 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
55 |
56 | 6. Trademarks.
57 |
58 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
59 |
60 | 7. Disclaimer of Warranty.
61 |
62 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
63 |
64 | 8. Limitation of Liability.
65 |
66 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
67 |
68 | 9. Accepting Warranty or Additional Liability.
69 |
70 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
71 |
72 |
--------------------------------------------------------------------------------
/Loogle.lean:
--------------------------------------------------------------------------------
1 | import Lean.Meta
2 | import Lake.CLI.Error
3 | import Lake.Util.Cli
4 |
5 | import Loogle.Find
6 |
7 | import Seccomp
8 |
9 | set_option autoImplicit false
10 |
11 |
12 | section RunParser
13 | open Lean Parser
14 |
15 | --- like `Parser.runParserCategory`, but does not need a parser category. H'T Kyle Miller
16 | def Parser.runParser (env : Environment) (declName : Name) (input : String)
17 | (fileName := "") :
18 | Except String Syntax :=
19 | let p := andthenFn whitespace (evalParserConst declName)
20 | let ictx := mkInputContext input fileName
21 | let s := p.run ictx { env, options := {} } (getTokenTable env) (mkParserState input)
22 | if s.hasError then
23 | Except.error (s.toErrorMsg ictx)
24 | else if input.atEnd s.pos then
25 | Except.ok s.stxStack.back
26 | else
27 | Except.error ((s.mkError "end of input").toErrorMsg ictx)
28 |
29 | end RunParser
30 |
31 | open Lean Core Meta Elab Term Command
32 | open Loogle
33 |
34 | instance : ToExpr System.FilePath where
35 | toTypeExpr := Lean.mkConst ``System.FilePath
36 | toExpr path := mkApp (Lean.mkConst ``System.FilePath.mk) (toExpr path.1)
37 |
38 | elab "#compileTimeSearchPath" : term => do
39 | let path ← searchPathRef.get
40 | let path' :=
41 | -- A little hack to not embed the searchpath when building under nix
42 | if path.any (fun p => p.toString.startsWith "/nix/store")
43 | then [] else path
44 | return toExpr path'
45 | def compileTimeSearchPath : SearchPath := #compileTimeSearchPath
46 |
47 | /-- Like `Find.Failure`, but without syntax references and with syntax pretty-printed -/
48 | abbrev Failure := String
49 |
50 | def Result := (Except Failure Find.Result × Array String × Nat)
51 | def Printer := Result → CoreM Unit
52 |
53 | def runQuery (index : Find.Index) (query : String) : CoreM Result :=
54 | withCurrHeartbeats do
55 | let (r, suggs) ← tryCatchRuntimeEx
56 | (handler := fun e => do return (.error (← e.toMessageData.toString), #[])) do
57 | match Parser.runParser (← getEnv) `Loogle.Find.find_filters query with
58 | | .error err => pure $ (.error err, #[])
59 | | .ok s => do
60 | MetaM.run' do
61 | match ← TermElabM.run' $ Loogle.Find.find index (.mk s) with
62 | | .ok result => do
63 | let suggs ← result.suggestions.mapM fun sugg => do
64 | return (← PrettyPrinter.ppCategory ``Find.find_filters sugg).pretty
65 | pure $ (.ok result, suggs)
66 | | .error err => do
67 | let suggs ← err.suggestions.mapM fun sugg => do
68 | return (← PrettyPrinter.ppCategory ``Find.find_filters sugg).pretty
69 | return (.error (← err.message.toString), suggs)
70 | let heartbeats := ((← IO.getNumHeartbeats) - (← getInitHeartbeats )) / 1000
71 | return (r, suggs, heartbeats)
72 |
73 | def printPlain : Printer
74 | | (.error err, suggs, _) => do
75 | IO.println err
76 | unless suggs.isEmpty do
77 | IO.println "Maybe you meant:"
78 | suggs.forM (fun s => IO.println ("* " ++ s))
79 | | (.ok result, _) => do
80 | IO.println (← result.header.toString)
81 | result.hits.forM fun (ci, mod) => match mod with
82 | | none => IO.println s!"{ci.name}" -- unlikely to happen in CLI usage
83 | | some mod => IO.println s!"{ci.name} (from {mod})"
84 |
85 | open PrettyPrinter in
86 | /-- Like PrettyPrinter.ppSignature, but omits the id -/
87 | def ppSignature (name : Name) : MetaM Format := withCurrHeartbeats do
88 | tryCatchRuntimeEx do
89 | let e ← mkConstWithLevelParams name
90 | let (stx, _infos) ← delabCore e (delab := Delaborator.delabConstWithSignature)
91 | let stx : Syntax := stx
92 | -- stx[1] picks out the signature
93 | ppTerm ⟨stx[1]⟩
94 | fun e =>
95 | return f!"[Failed to pretty-print signature: {← e.toMessageData.format}]"
96 |
97 |
98 | def toJson : Result → CoreM Json -- only in IO for MessageData.toString
99 | | (.error err, suggs, heartbeats) => do
100 | if suggs.isEmpty then
101 | pure $ .mkObj [ ("error", .str err), ("heartbeats", heartbeats) ]
102 | else
103 | pure $ .mkObj [ ("error", .str err), ("suggestions", .arr (suggs.map .str)), ("heartbeats", heartbeats)]
104 | | (.ok result, suggs, heartbeats) => do
105 | pure $ .mkObj $ [
106 | ("header", .str (← result.header.toString)),
107 | ("heartbeats", .num heartbeats),
108 | ("count", .num result.count),
109 | ("hits", .arr $ ← result.hits.mapM fun (ci, mod) => do
110 | let ty := (← (ppSignature ci.name).run').pretty
111 | let ds := match ← findDocString? (← getEnv) ci.name false with
112 | | some s => .str s
113 | | none => .null
114 | let mod := match mod with | none => .null | some name => .str name.toString
115 | return .mkObj [
116 | ("name", .str ci.name.toString),
117 | ("type", .str ty),
118 | ("module", mod ),
119 | ("doc", ds )
120 | ]
121 | )
122 | ] ++
123 | (if suggs.isEmpty then [] else [
124 | ("suggestions", .arr (suggs.map .str))
125 | ])
126 |
127 | def printJson : Printer := fun r => do
128 | IO.println (← toJson r).compress
129 | (← IO.getStdout).flush
130 |
131 | def single (index : Find.Index) (print : Printer) (query : String) : CoreM Unit := do
132 | let r ← runQuery index query
133 | print r
134 |
135 | def interactive (index : Find.Index) (print : Printer) : CoreM Unit := do
136 | IO.println "Loogle is ready."
137 | (← IO.getStdout).flush
138 | while true do
139 | let query := (← (← IO.getStdin).getLine).trim
140 | if query.isEmpty then break
141 | single index print query
142 |
143 | structure LoogleOptions where
144 | interactive : Bool := false
145 | json : Bool := false
146 | query : Option String := none
147 | module : String := "Mathlib"
148 | searchPath : List System.FilePath := []
149 | writeIndex : Option String := none
150 | readIndex : Option String := none
151 | wantsHelp : Bool := false
152 |
153 | unsafe def work (opts : LoogleOptions) (act : Find.Index → CoreM Unit) : IO Unit := do
154 | if opts.searchPath.isEmpty
155 | then searchPathRef.set compileTimeSearchPath
156 | else searchPathRef.set opts.searchPath
157 |
158 | Lean.enableInitializersExecution
159 | let imports := #[{module := opts.module.toName}, {module := `Loogle.Find}]
160 | let env ← importModules (loadExts := true) imports {}
161 | let ctx := {fileName := "/", fileMap := Inhabited.default}
162 | let state := {env}
163 | Prod.fst <$> act'.toIO ctx state
164 | where act' : CoreM Unit := do
165 | let index ← match opts.readIndex with
166 | | some path => do
167 | let (index, _) ← unpickle _ path
168 | Find.Index.mkFromCache index
169 | | none =>
170 | -- Special-case Mathlib and use the cached index if present
171 | if opts.writeIndex.isNone && opts.module.toName = `Mathlib
172 | then Find.cachedIndex.get
173 | else Find.Index.mk
174 | -- warm up cache eagerly
175 | let _ ← index.1.cache.get
176 | let _ ← index.2.cache.get
177 | if let some path := opts.writeIndex then pickle path (← index.getCache)
178 | Seccomp.enable
179 | act index
180 |
181 | abbrev CliMainM := ExceptT Lake.CliError IO
182 | abbrev CliStateM := StateT LoogleOptions CliMainM
183 | abbrev CliM := Lake.ArgsT CliStateM
184 |
185 | def takeArg (arg : String) : CliM String := do
186 | match (← Lake.takeArg?) with
187 | | none => throw <| Lake.CliError.missingArg arg
188 | | some arg => pure arg
189 |
190 | def lakeShortOption : (opt : Char) → CliM PUnit
191 | | 'i' => modifyThe LoogleOptions ({· with interactive := true})
192 | | 'j' => modifyThe LoogleOptions ({· with json := true})
193 | | 'h' => modifyThe LoogleOptions ({· with wantsHelp := true})
194 | | opt => throw <| Lake.CliError.unknownShortOption opt
195 |
196 | def lakeLongOption : (opt : String) → CliM PUnit
197 | | "--help" => lakeShortOption 'h'
198 | | "--interactive" => lakeShortOption 'i'
199 | | "--json" => lakeShortOption 'j'
200 | | "--path" => do
201 | let path : System.FilePath ← takeArg "--path"
202 | modifyThe LoogleOptions fun opts => {opts with searchPath := opts.searchPath ++ [path]}
203 | | "--module" => do modifyThe LoogleOptions ({· with module := ← takeArg "--module"})
204 | | "--write-index" => do modifyThe LoogleOptions ({· with writeIndex := ← takeArg "--write-index"})
205 | | "--read-index" => do modifyThe LoogleOptions ({· with readIndex := ← takeArg "--read-index"})
206 | | opt => throw <| Lake.CliError.unknownLongOption opt
207 |
208 | def lakeOption :=
209 | Lake.option {
210 | short := lakeShortOption
211 | long := lakeLongOption
212 | longShort := Lake.shortOptionWithArg lakeShortOption
213 | }
214 |
215 | def usage := "
216 | USAGE:
217 | loogle [OPTIONS] [QUERY]
218 |
219 | OPTIONS:
220 | --help
221 | --interactive, -i read querys from stdin
222 | --json, -j print result in JSON format
223 | --module mod import this module (default: Mathlib)
224 | --path path search for .olean files here (default: the build time path)
225 | --write-index file persists the search index to a file
226 | --read-index file read the search index from a file. This file is blindly trusted!
227 | " ++
228 | if compileTimeSearchPath.isEmpty then "" else "
229 | Default search path
230 | " ++ String.join (compileTimeSearchPath.map (fun p => s!" * {p}\n"))
231 |
232 | unsafe def loogleCli : CliM PUnit := do
233 | match (← Lake.getArgs) with
234 | | [] => IO.println usage
235 | | _ => -- normal CLI
236 | Lake.processOptions lakeOption
237 | let opts ← getThe LoogleOptions
238 | let queries ← Lake.takeArgs
239 | let print := if opts.json then printJson else printPlain
240 | if opts.wantsHelp ||
241 | queries.isEmpty && not opts.interactive && opts.writeIndex.isNone
242 | then IO.println usage
243 | else work opts fun index => do
244 | queries.forM (single index print)
245 | if opts.interactive
246 | then interactive index print
247 |
248 | unsafe def main (args : List String) : IO Unit := do
249 | match (← (loogleCli.run args |>.run' {}).run) with
250 | | .ok _ => pure ()
251 | | .error e => IO.println e.toString
252 |
--------------------------------------------------------------------------------
/Loogle/BaseIOThunk.lean:
--------------------------------------------------------------------------------
1 | import Std.Sync.Mutex
2 |
3 | /-!
4 | # Monadic version of `Thunk`
5 |
6 | This file defines `BaseIO.Thunk` and `IO.Thunk`.
7 |
8 | It makes the choice that errors are cached just like values,
9 | as opposed to declaring them uncacheable as Python's `functools` caching operations do.
10 | -/
11 |
12 | /-- A version of `Thunk` that runs in `BaseIO`.
13 |
14 | Note that unlike `Thunk`, this does not have optimized C-side support. -/
15 | structure BaseIO.Thunk (α : Type) : Type where
16 | private ref : IO.Ref (Option α)
17 | private mutex : Std.BaseMutex
18 | init : BaseIO α
19 | deriving Nonempty
20 |
21 | /-- Construct a new lazily initialized reference, used typically as
22 | ```lean
23 | initialize foo : BaseIO.Thunk Foo ← BaseIO.Thunk.new mkFoo
24 | ```
25 | in place of
26 | ```lean
27 | initialize foo : Foo ← mkFoo
28 | ```
29 | -/
30 | def BaseIO.Thunk.new {α} (init : BaseIO α) : BaseIO (BaseIO.Thunk α) := do
31 | return { ref := ← IO.mkRef none, mutex := ← Std.BaseMutex.new, init := init}
32 |
33 | /-- Obtain the value, constructing it in a thread-safe way if necessary. -/
34 | def BaseIO.Thunk.get {α} (l : BaseIO.Thunk α) : BaseIO α := do
35 | if let .some a ← l.ref.get then
36 | return a
37 | try
38 | l.mutex.lock
39 | if let .some a ← l.ref.get then
40 | return a
41 | let a ← l.init
42 | l.ref.set (some a)
43 | return a
44 | finally
45 | l.mutex.unlock
46 |
47 | /-- A wrapper for `BaseIO.Thunk` to also cache `IO.Error`s.-/
48 | abbrev IO.Thunk (α : Type) : Type := BaseIO.Thunk (Except IO.Error α)
49 |
50 | /-- Construct a new lazily initialized reference, used typically as
51 | ```lean
52 | initialize foo : IO.Thunk Foo ← IO.Thunk.new mkFoo
53 | ```
54 | in place of
55 | ```lean
56 | initialize foo : Foo ← mkFoo
57 | ```
58 | -/
59 | def IO.Thunk.new {α} (init : IO α) : BaseIO (IO.Thunk α) := BaseIO.Thunk.new <| init.toBaseIO
60 |
61 | /-- Obtain the value, constructing it in a thread-safe way if necessary.
62 |
63 | If the initializer fails, the error is also cached.
64 | Note this diverges from the behavior of Python's `@functools.lru_cache` and related helpers. -/
65 | def IO.Thunk.get {α} (l : IO.Thunk α) : IO α := do
66 | match ← BaseIO.Thunk.get l with
67 | | .ok a => return a
68 | | .error e => throw e
69 |
--------------------------------------------------------------------------------
/Loogle/BlackListed.lean:
--------------------------------------------------------------------------------
1 | /-
2 | Copyright (c) 2019 Robert Y. Lewis. All rights reserved.
3 | Released under Apache 2.0 license as described in the file LICENSE.
4 | Authors: Mario Carneiro, Simon Hudon, Scott Morrison, Keeley Hoek, Robert Y. Lewis,
5 | Floris van Doorn, E.W.Ayers, Arthur Paulino
6 | -/
7 | import Lean
8 | import Batteries.Lean.Expr
9 | import Batteries.Data.List.Basic
10 |
11 | set_option autoImplicit true
12 |
13 | namespace Lean
14 |
15 | namespace Name
16 |
17 | /-! ### Declarations about `name` -/
18 |
19 |
20 | /-- `isPrefixOf? pre nm` returns `some post` if `nm = pre ++ post`.
21 | Note that this includes the case where `nm` has multiple more namespaces.
22 | If `pre` is not a prefix of `nm`, it returns `none`. -/
23 | private def isPrefixOf? (pre nm : Name) : Option Name :=
24 | if pre == nm then
25 | some anonymous
26 | else match nm with
27 | | anonymous => none
28 | | num p' a => (isPrefixOf? pre p').map (·.num a)
29 | | str p' s => (isPrefixOf? pre p').map (·.str s)
30 |
31 | /-- Lean 4 makes declarations which are technically not internal
32 | (that is, head string does not start with `_`) but which sometimes should
33 | be treated as such. For example, the `to_additive` attribute needs to
34 | transform `proof_1` constants generated by `Lean.Meta.mkAuxDefinitionFor`.
35 | This might be better fixed in core, but until then, this method can act
36 | as a polyfill. This method only looks at the name to decide whether it is probably internal.
37 | Note: this declaration also occurs as `shouldIgnore` in the Lean 4 file `test/lean/run/printDecls`.
38 | -/
39 | private def isInternal' (declName : Name) : Bool :=
40 | declName.isInternal ||
41 | match declName with
42 | | .str _ s => "match_".isPrefixOf s || "proof_".isPrefixOf s
43 | | _ => true
44 |
45 | end Lean.Name
46 |
47 | namespace Loogle
48 |
49 | open Lean Meta
50 |
51 | -- from Lean.Server.Completion
52 | -- This definition is copied all over the place; let's hope we eventually
53 | -- get an authoritive version in std, or an explicit attribute in core
54 |
55 | def isBlackListed {m} [Monad m] [MonadEnv m] (declName : Name) : m Bool := do
56 | if declName == ``sorryAx then return true
57 | if declName matches .str _ "inj" then return true
58 | if declName matches .str _ "noConfusionType" then return true
59 | let env ← getEnv
60 | pure $ declName.isInternal'
61 | || isAuxRecursor env declName
62 | || isNoConfusion env declName
63 | <||> isRec declName <||> isMatcher declName
64 |
65 | end Loogle
66 |
--------------------------------------------------------------------------------
/Loogle/Cache.lean:
--------------------------------------------------------------------------------
1 | /-
2 | Copyright (c) 2023 Joachim Breitner. All rights reserved.
3 | Released under Apache 2.0 license as described in the file LICENSE.
4 | Authors: Joachim Breitner
5 | -/
6 | import Batteries.Util.Cache
7 |
8 | open Lean Meta
9 |
10 | -- Odd to have that namespace in std4
11 | namespace Batteries.Tactic
12 |
13 | /--
14 | A type synonym for a `DeclCache` containing a pair of elements.
15 | The first will store declarations in the current file,
16 | the second will store declarations from imports (and will hopefully be "read-only" after creation).
17 | -/
18 | def DeclCache2 (α : Type) : Type := DeclCache (α × α)
19 |
20 | instance (α : Type) [h : Nonempty α] : Nonempty (DeclCache2 α) :=
21 | -- inferInstanceAs (Nonempty (DeclCache (α × α)))
22 | -- work around lack of Prod.nonempty in std/core:
23 | h.elim fun x => @instNonemptyDeclCache _ ⟨x,x⟩
24 |
25 | /--
26 | Creates a `DeclCache`.
27 | The cached structure `α` is initialized with `empty`,
28 | and then `addLibraryDecl` is called for every imported constant, and the result is cached.
29 | After all imported constants have been added, we run `post`.
30 | When `get` is called, `addDecl` is also called for every constant in the current file.
31 | -/
32 | def DeclCache2.mk (profilingName : String) (empty : α)
33 | (addDecl : Name → ConstantInfo → α → MetaM α)
34 | (post : α → MetaM α := fun a => pure a) : IO (DeclCache2 α) :=
35 | DeclCache.mk profilingName
36 | (empty := (empty, empty))
37 | (addDecl := fun n c (m₁, m₂) => do pure (← addDecl n c m₁, m₂))
38 | (addLibraryDecl := fun n c (m₁, m₂) => do pure (m₁, ← addDecl n c m₂))
39 | (post := fun (m₁, m₂) => return (m₁, ← post m₂))
40 |
41 | /--
42 | Access the cache.
43 | Calling this function for the first time
44 | will initialize the cache with the function
45 | provided in the constructor.
46 | -/
47 | def DeclCache2.get (cache : DeclCache2 α) : MetaM (α × α) := DeclCache.get cache
48 |
49 | /--
50 | Access the cache (imports only).
51 | Suitable to get a value to be pickled and fed to `mkFromCache` later.
52 | -/
53 | def DeclCache2.getImported (cache : DeclCache2 α) : CoreM α := do
54 | let (_, m₂) ← cache.cache.get
55 | pure m₂
56 |
57 | /--
58 | Creates a `DeclCache2` from a pre-computed index, typically obtained via `DeclCache2.getImports`.
59 | The cached structure `α` is initialized with the given value.
60 | When `get` is called, `addDecl` is additionally called for every constant in the current file.
61 | -/
62 | def DeclCache2.mkFromCache (empty : α) (addDecl : Name → ConstantInfo → α → MetaM α) (cached : α) :
63 | IO (DeclCache2 α) := do
64 | let cache ← Cache.mk (pure (empty, cached))
65 | pure { cache, addDecl := fun n c (m₁, m₂) => do pure (← addDecl n c m₁, m₂) }
66 |
--------------------------------------------------------------------------------
/Loogle/Find.lean:
--------------------------------------------------------------------------------
1 | /-
2 | Copyright (c) 2023 Joachim Breitner. All rights reserved.
3 | Released under Apache 2.0 license as described in the file LICENSE.
4 | Authors: Joachim Breitner
5 | -/
6 | import Lean
7 | import Batteries.Data.String.Matcher
8 | import Batteries.Util.Pickle
9 |
10 | import Loogle.Cache
11 | import Loogle.NameRel
12 | import Loogle.RBTree
13 | import Loogle.BlackListed
14 | import Loogle.Trie
15 | import Loogle.BaseIOThunk
16 |
17 | /-!
18 | # The `#find` command and tactic.
19 | -/
20 |
21 | open Lean Meta Elab
22 |
23 | /-!
24 | ## Formatting utilities
25 | -/
26 |
27 | /-- Puts `MessageData` into a bulleted list -/
28 | def MessageData.bulletList (xs : Array MessageData) : MessageData :=
29 | MessageData.joinSep (xs.toList.map (m!"• " ++ ·)) Format.line
30 |
31 | /-- Formats a list of names and types, as you would expect from a lemma-searching command. -/
32 | def MessageData.bulletListOfConstsAndTypes {m} [Monad m] [MonadEnv m] [MonadError m]
33 | (names : Array (Name × Expr)) (showTypes : Bool := false) : m MessageData := do
34 | let ms ← names.mapM fun (n,t) => do
35 | let n := .ofConst (← mkConstWithLevelParams n)
36 | if showTypes then
37 | return m!"{n} : {t}"
38 | else
39 | return n
40 | pure (MessageData.bulletList ms)
41 | /-!
42 | ## Name sorting
43 | -/
44 |
45 | namespace Loogle.Find
46 |
47 | /-- Sorts a thing with a name so that names defied in modules that come earlier (as per `ModuleIdx`)
48 | come earlier. Within one module, sort according to given function, and finally by Name.
49 | -/
50 | def sortByModule' {m} [Monad m] [MonadEnv m] {α} (name : α → Name) (f : α → Nat)
51 | (ns : Array α) : m (Array (α × Option ModuleIdx)) := do
52 | let env ← getEnv
53 | return ns
54 | |>.map (fun x => (env.getModuleIdxFor? (name x), f x, x))
55 | -- Sort by modindex and then by the given predicate
56 | -- A ModIdx of none means locally defined, we put them first.
57 | -- The modindex corresponds to a topological sort of the modules, so basic lemmas come earlier.
58 | |>.qsort (fun (mi₁, k₁, x₁) (mi₂, k₂, x₂) =>
59 | match mi₁, mi₂ with
60 | | none, none => Nat.blt k₁ k₂ || Nat.beq k₁ k₂ && Name.lt (name x₁) (name x₂)
61 | | none, some _ => true
62 | | some _, none => false
63 | | some mi₁, some mi₂ =>
64 | Nat.blt mi₁ mi₂ ||
65 | Nat.beq mi₁ mi₂ && (Nat.blt k₁ k₂ || Nat.beq k₁ k₂ && Name.lt (name x₁) (name x₂)))
66 | |>.map fun (mi, _k, x) => (x, mi)
67 |
68 | /-- See `sortByModule'` --/
69 | def sortByModule {m} [Monad m] [MonadEnv m] {α} (name : α → Name) (f : α → Nat)
70 | (ns : Array α) : m (Array α) := do
71 | return (← sortByModule' name f ns).map (·.1)
72 |
73 | /-- In lieu of an real `Lean.Expr.size` function, explicitly fold for now -/
74 | def exprSize (e : Expr ) : Nat := go e 0
75 | where go : Expr → Nat → Nat
76 | | Expr.forallE _ d b _, c => go b (go d (c + 1))
77 | | Expr.lam _ d b _, c => go b (go d (c + 1))
78 | | Expr.letE _ t v b _, c => go b (go v (go t (c + 1)))
79 | | Expr.app f a, c => go a (go f (c + 1))
80 | | Expr.mdata _ b, c => go b (c + 1)
81 | | Expr.proj _ _ b, c => go b (c + 1)
82 | | _, c => c + 1
83 |
84 | /-!
85 | ## Matching term patterns against conclusions or any subexpression
86 | -/
87 |
88 | /-- Comparing `HeadIndex` for whether the terms could be equal; for `HeadIndex.mvar` we have no
89 | information. -/
90 | def canMatch : HeadIndex → HeadIndex → Bool
91 | | .mvar _, _ => true
92 | | _, .mvar _ => true
93 | | hi₁, hi₂ => hi₁ == hi₂
94 |
95 | /-- A predicate on `ConstantInfo` -/
96 | def ConstMatcher := ConstantInfo → MetaM Bool
97 |
98 | private partial def matchHyps : List Expr → List Expr → List Expr → MetaM Bool
99 | | p::ps, oldHyps, h::newHyps => do
100 | let pt ← inferType p
101 | let t ← inferType h
102 | if (← isDefEq pt t) then
103 | matchHyps ps [] (oldHyps ++ newHyps)
104 | else
105 | matchHyps (p::ps) (h::oldHyps) newHyps
106 | | [], _, _ => pure true
107 | | _::_, _, [] => pure false
108 |
109 | /-- Like defEq, but the pattern is a function type, matches parameters up to reordering -/
110 | def matchUpToHyps (pat: AbstractMVarsResult) (head : HeadIndex) (e : Expr) : MetaM Bool := do
111 | forallTelescope e fun e_params e_concl ↦ do
112 | if canMatch head e_concl.toHeadIndex then
113 | let (_, _, pat_e) ← openAbstractMVarsResult pat
114 | let (pat_params, _, pat_concl) ← forallMetaTelescope pat_e
115 | isDefEq e_concl pat_concl <&&> matchHyps pat_params.toList [] e_params.toList
116 | else
117 | pure false
118 |
119 | /-- Takes a pattern (of type `Expr`), and returns a matcher that succeeds if the conclusion of the
120 | lemma matches the pattern. If the pattern is a function type, it matches up to parameter
121 | reordering. -/
122 | def matchConclusion (t : Expr) : MetaM ConstMatcher := withReducible do
123 | let head := (← forallMetaTelescope t).2.2.toHeadIndex
124 | let pat ← abstractMVars (← instantiateMVars t)
125 | pure fun (c : ConstantInfo) => withReducible do
126 | let cTy := c.instantiateTypeLevelParams (← mkFreshLevelMVars c.numLevelParams)
127 | matchUpToHyps pat head cTy
128 |
129 | /--
130 | A wrapper around `Lean.Meta.forEachExpr'` that checks if any subexpression matches the
131 | predicate. The `pre` predicate is used to prune subexpressions eagerly.
132 | -/
133 | def Lean.Meta.anyExpr (input : Expr) (pre : Expr → MetaM Bool) (pred : Expr → MetaM Bool) : MetaM Bool := do
134 | let found ← IO.mkRef false
135 | Lean.Meta.forEachExpr' input fun sub_e => do
136 | unless ← pre sub_e do return false
137 | if ← pred sub_e then found.set true
138 | -- keep searching if we haven't found it yet
139 | not <$> found.get
140 | found.get
141 |
142 | /--
143 | Used to prune the search early: Checks that all consts mentioned in `consts` appear
144 | in `e`.
145 | -/
146 | def checkUsedConsts (consts : NameSet) (e : Expr) : MetaM Bool := do
147 | return consts.subset e.getUsedConstantsAsSet
148 |
149 | /-- Takes a pattern (of type `Expr`), and returns a matcher that succeeds if _any_ subexpression
150 | matches that patttern. If the pattern is a function type, it matches up to parameter reordering. -/
151 | def matchAnywhere (t : Expr) : MetaM ConstMatcher := withReducible do
152 | let patConsts := t.getUsedConstantsAsSet
153 | let head := (← forallMetaTelescope t).2.2.toHeadIndex
154 | let pat ← abstractMVars (← instantiateMVars t)
155 | pure fun (c : ConstantInfo) => withReducible do
156 | let cTy := c.instantiateTypeLevelParams (← mkFreshLevelMVars c.numLevelParams)
157 | -- NB: Lean.Meta.forEachExpr`' handles nested foralls in one go, so
158 | -- in `(a → b → c)`, it will never vist `b → c`.
159 | -- But since we use `matchUpToHyps` instead of `isDefEq` directly, this is ok.
160 | Lean.Meta.anyExpr cTy (checkUsedConsts patConsts) (matchUpToHyps pat head)
161 |
162 | /-!
163 | ## The two indexes used
164 |
165 | `#find` uses two indexes: One mapping names to names of constants that mention
166 | that name anywhere, which is the primary index that we use.
167 |
168 | Additionaly, for queries that involve _only_ lemma name fragments, we maintain a suffix trie
169 | of lemma names.
170 | -/
171 |
172 | /-- For all names `n` mentioned in the type of the constant `c`, add a mapping from
173 | `n` to `c.name` to the relation. -/
174 | private def addDecl (name : Lean.Name) (c : ConstantInfo) (m : NameRel) : MetaM NameRel := do
175 | if ← Loogle.isBlackListed name then
176 | return m
177 | let consts := c.type.getUsedConstantsAsSet
178 | return consts.fold (init := m) fun m n => m.insert n name
179 |
180 |
181 | /-- A suffix trie for `Name`s -/
182 | def SuffixTrie := Loogle.Trie (Array Name)
183 | deriving Nonempty
184 |
185 | /-- Insert a `Name` into a `SuffixTrie` -/
186 | def SuffixTrie.insert (t : SuffixTrie) (n : Lean.Name) : SuffixTrie := Id.run $ do
187 | let mut t := t
188 | let s := n.toString.toLower
189 | for i in List.range (s.length - 1) do -- -1 to not consider the empty suffix
190 | let suf := s.drop i
191 | t := t.upsert suf fun ns? => ns? |>.getD #[] |>.push n
192 | pure t
193 |
194 | /-- Insert a declaration into a `SuffixTrie`, if the name isn't blacklisted. -/
195 | def SuffixTrie.addDecl (name : Lean.Name) (_ : ConstantInfo) (t : SuffixTrie) :
196 | MetaM SuffixTrie := do
197 | if ← Loogle.isBlackListed name then
198 | return t
199 | return t.insert name
200 |
201 | -- /-- Search the suffix trie, returning an empty array if nothing matches -/
202 | def SuffixTrie.find (t : SuffixTrie) (s : String) : NameSet :=
203 | t.findPrefix s.toLower |>.map (RBTree.fromArray (cmp := _)) |>.foldl (init := {}) NameSet.append
204 |
205 | /-- Search the suffix trie, returning an empty array if nothing matches -/
206 | def SuffixTrie.findSuffix (t : SuffixTrie) (s : String) : Array Name :=
207 | (Loogle.Trie.find? t s.toLower).getD #[]
208 |
209 | open Batteries.Tactic
210 |
211 | /-- The index used by `#find`: A declaration cache with a `NameRel` mapping names to the name
212 | of constants they are mentinend in, and a declaration cache storing a suffix trie. -/
213 | structure Index where mk'' ::
214 | /-- Maps names to the names of constants they are mentioned in. -/
215 | nameRelCache : DeclCache2 NameRel
216 | /-- Suffix trie of lemma names -/
217 | trieCache: DeclCache2 SuffixTrie
218 | deriving Nonempty
219 |
220 | -- NB: In large files it may be slightly wasteful to calculate a full NameSet for the local
221 | -- definition upon every invocation of `#find`, and a linear scan might work better. For now,
222 | -- this keeps the code below more uniform.
223 |
224 | /-- Create a fresh index. -/
225 | def Index.mk : IO Index := do
226 | let c1 ← DeclCache2.mk "#find: init cache" .empty addDecl
227 | let c2 ← DeclCache2.mk "#find: init trie" .empty SuffixTrie.addDecl
228 | pure ⟨c1, c2⟩
229 |
230 | /-- The part of the index that includes the imported definitions, for example to be persisted and
231 | distributed by `MathlibExtras.Find`. -/
232 | def Index.getCache (i : Index) : CoreM (NameRel × SuffixTrie) := do
233 | let r ← i.nameRelCache.getImported
234 | let t ← i.trieCache.getImported
235 | pure (r, t)
236 |
237 | /-- Create an index from a cached value -/
238 | def Index.mkFromCache (init : NameRel × SuffixTrie) : IO Index := do
239 | let c1 ← DeclCache2.mkFromCache .empty addDecl init.1
240 | let c2 ← DeclCache2.mkFromCache .empty SuffixTrie.addDecl init.2
241 | pure ⟨c1, c2⟩
242 |
243 | /-!
244 | ## The #find syntax and elaboration helpers
245 | -/
246 |
247 | open Parser
248 |
249 | /-- The turnstyle uesd bin `#find`, unicode or ascii allowed -/
250 | syntax turnstyle := patternIgnore("⊢ " <|> "|- ")
251 | /-- a single `#find` filter. The `term` can also be an ident or a strlit,
252 | these are distinguished in `parseFindFilters` -/
253 | syntax find_filter := (turnstyle term) <|> term
254 |
255 | /-- The argument to `#find`, a list of filters -/
256 | syntax find_filters := find_filter,*
257 |
258 | /-- A variant of `Lean.Elab.Term.elabTerm` that does not complain for example
259 | when a type class constraint has no instances. -/
260 | def elabTerm' (t : Term) (expectedType? : Option Expr) : TermElabM Expr := do
261 | withTheReader Term.Context ({ · with ignoreTCFailures := true, errToSorry := false }) do
262 | let t ← Term.elabTerm t expectedType?
263 | -- So far all tests work without the following line. Lets expand the test
264 | -- suite once someone complains
265 | -- Term.synthesizeSyntheticMVars (mayPostpone := true) (ignoreStuckTC := true)
266 | instantiateMVars t
267 |
268 | /-!
269 | ## Generating suggestions for unresolvable names
270 |
271 | For `#find` it is really helpful if, when the user entered a term with a name that cannot
272 | be resolved, we use the name trie to see if it exists maybe in some qualified form. For
273 | simple `ident` patterns, this is straight forward, but for expressions it is harder: The term
274 | elaborator simply throws an unstructured expression.
275 |
276 | So we’ll use the source reference from that exception, check if there is an `.ident` at tht position
277 | in the source, check if the name there does not resolve in the environments, generate
278 | possible suggestions, and re-assemble the syntax.
279 |
280 | The following functions are rather hackish, maybe there can be a more principled approach.
281 | -/
282 |
283 | /-- Equality on `SourceInfo` -/
284 | private def SourceInfo.beq : SourceInfo → SourceInfo → Bool
285 | | .original _ p1 _ p2, .original _ p3 _ p4
286 | | .original _ p1 _ p2, .synthetic p3 p4 _
287 | | .synthetic p1 p2 _, .synthetic p3 p4 _
288 | | .synthetic p1 p2 _, .original _ p3 _ p4
289 | => p1 == p3 && p2 == p4
290 | | _, _ => false
291 |
292 |
293 | /-- Within the given `Syntax`, see if the `SourceInfo` points to an `.ident` -/
294 | partial def findNameAt (needle : SourceInfo) : Syntax → Option Name
295 | | .node _ _ cs => cs.findSome? (findNameAt needle)
296 | | .ident si _ n _ =>
297 | if SourceInfo.beq needle si then
298 | .some n
299 | else
300 | .none
301 | | _ => .none
302 |
303 | /-- Within the given `Syntax`, if the `SourceInfo` points to an `.ident`, replace the `Name` -/
304 | partial def replaceIdentAt' (needle : SourceInfo) (new_name : Name) : Syntax → Syntax
305 | | .node si kind cs => .node si kind (cs.map (replaceIdentAt' needle new_name))
306 | | .ident si str n prs =>
307 | if SourceInfo.beq needle si then
308 | .ident si (new_name.toString) new_name []
309 | else
310 | .ident si str n prs
311 | | s => s
312 |
313 | /-- Within the given `TSyntax`, if the `SourceInfo` points to an `.ident`, replace the `Name` -/
314 | def replaceIdentAt {kind} (si : SourceInfo) (n : Name) : TSyntax kind → TSyntax kind
315 | | .mk s => .mk (replaceIdentAt' si n s)
316 |
317 | /-- When a name cannot be resolved, see if we can find it in the trie under some namepace. -/
318 | def resolveUnqualifiedName (index : Index) (n : Name) : MetaM (Array Name) := do
319 | let s := "." ++ n.toString
320 | let (t₁, t₂) ← index.trieCache.get
321 | let names := t₁.findSuffix s ++ t₂.findSuffix s
322 | let names := names.filter ((← getEnv).contains ·)
323 | sortByModule id (fun _ => 0) names
324 |
325 | /-- If the `s` at `si` is an identifier not found in he environment, produce a list
326 | of possible suggestions in place of `s`. -/
327 | def suggestQualifiedNames {kind} (index : Index) (s : TSyntax kind) (si : SourceInfo) :
328 | MetaM (Array (TSyntax kind)) := do
329 | let .some n := findNameAt si s | pure #[]
330 | let .none := (← getEnv).find? n | pure #[]
331 | let suggestedNames ← resolveUnqualifiedName index n
332 | return suggestedNames.map fun sugg => replaceIdentAt si sugg s
333 |
334 | /-- If the `s` at the subexpression `needle` is an identifier `find_filter`, suggest replacing it
335 | with a name string filter. -/
336 | partial def suggestQuoted' (needle : SourceInfo) (s : Syntax) : Syntax :=
337 | match s with
338 | | .node si₁ ``find_filter #[.ident si₂ str _n _prs] =>
339 | if SourceInfo.beq needle si₂ then
340 | .node si₁ ``find_filter #[Syntax.mkStrLit str.toString]
341 | else
342 | s
343 | | .node si kind cs =>
344 | .node si kind <| cs.map (suggestQuoted' needle)
345 | | _ => s
346 |
347 | /-- If the `s` at the subexpression `needle` is an identifier `find_filter`, suggest replacing it
348 | with a name string filter. -/
349 | partial def suggestQuoted {kind} (needle : SourceInfo) : TSyntax kind → Array (TSyntax kind)
350 | | .mk s =>
351 | let s' := suggestQuoted' needle s
352 | if s == s' then #[] else #[.mk s']
353 |
354 |
355 | /-!
356 | ## The core find tactic engine
357 | -/
358 |
359 | /-- The parsed and elaborated arguments to `#find` -/
360 | structure Arguments where
361 | /-- Identifiers to search for -/
362 | idents : Array Name
363 | /-- Lemma name substrings to search for -/
364 | namePats : Array String
365 | /-- Term patterns to search for. The boolean indicates conclusion patterns. -/
366 | terms : Array (Bool × Expr)
367 | /-- Extra messages arising from parsing -/
368 | parserMessage : MessageData
369 |
370 | /-- Result of `find` -/
371 | structure Result where
372 | /-- Statistical summary -/
373 | header : MessageData
374 | /-- Total number of matches -/
375 | count : Nat
376 | /-- Matching definition (with defining module, if imported) -/
377 | hits : Array (ConstantInfo × Option Name)
378 | /-- Alternative suggestions -/
379 | suggestions : Array (TSyntax ``find_filters)
380 |
381 | /-- Negative result of `find` -/
382 | structure Failure where
383 | /-- Position of error message -/
384 | ref : Syntax
385 | /-- Error message -/
386 | message : MessageData
387 | /-- Alternative suggestions -/
388 | suggestions : Array (TSyntax ``find_filters)
389 |
390 | /-- The core of the `#find` tactic with all parsing/printing factored out, for
391 | programmatic use (e.g. the loogle CLI).
392 | It also does not use the implicit global Index, but rather expects one as an argument. -/
393 | def find (index : Index) (args : TSyntax ``find_filters) (maxShown := 200) :
394 | TermElabM (Except Failure Result) := do
395 | profileitM Exception "#find" (← getOptions) do
396 | let mut message := MessageData.nil
397 | let mut idents := #[]
398 | let mut namePats := #[]
399 | let mut terms := #[]
400 | try
401 | match args with
402 | | `(find_filters| $args':find_filter,*) =>
403 | for argi in List.range args'.getElems.size do
404 | let arg := args'.getElems[argi]!
405 | match arg with
406 | | `(find_filter| $_:turnstyle $s:term) => do
407 | let e ← elabTerm' s (some (mkSort (← mkFreshLevelMVar)))
408 | let t := ← inferType e
409 | unless t.isSort do
410 | throwErrorAt s m!"Conclusion pattern is of type {t}, should be of type `Sort`"
411 | terms := terms.push (true, e)
412 | | `(find_filter| $ss:str) => do
413 | let str := Lean.TSyntax.getString ss
414 | if str = "" || str = "." then
415 | throwErrorAt ss "Name pattern is too general"
416 | namePats := namePats.push str
417 | | `(find_filter| $i:ident) => do
418 | let n := Lean.TSyntax.getId i
419 | if (← getEnv).contains n then
420 | idents := idents.push n
421 | else
422 | throwErrorAt i m!"unknown identifier '{n}'"
423 | | `(find_filter| _) => do
424 | throwErrorAt arg ("Cannot search for _. " ++
425 | "Did you forget to put a term pattern in parentheses?")
426 | | `(find_filter| $s:term) => do
427 | let e ← elabTerm' s none
428 | terms := terms.push (false, e)
429 | | _ => throwErrorAt args "unexpected argument to #find"
430 | | _ => throwErrorAt args "unexpected argument to #find"
431 | catch e => do
432 | let .error ref msg := e | throw e
433 | let suggestions1 := suggestQuoted (.fromRef ref) args
434 | let suggestions2 ← suggestQualifiedNames index args (.fromRef ref)
435 | return .error ⟨ref, msg, suggestions1 ++ suggestions2⟩
436 |
437 | -- Collect set of names to query the index by
438 | let needles : NameSet :=
439 | {} |> idents.foldl NameSet.insert
440 | |> terms.foldl (fun s (_,t) => t.foldConsts s (flip NameSet.insert))
441 | let (indexHits, remainingNamePats) ← do
442 | if needles.isEmpty then do
443 | if namePats.isEmpty then
444 | return .error ⟨args, m!"Cannot search: No constants or name fragments in search pattern.",
445 | #[]⟩
446 | -- No constants in patterns, use trie
447 | let (t₁, t₂) ← index.trieCache.get
448 | let hitArrays := namePats.map (fun s => (s, t₁.find s ++ t₂.find s))
449 | -- If we have more than one name fragment pattern, use the one that returns the smallest
450 | -- array of names
451 | let hitArrays := hitArrays.qsort fun (_, a₁) (_, a₂) => a₁.size > a₂.size
452 | let (needle, hits) := hitArrays.back!
453 | if hits.size == 1 then
454 | message := message ++ m!"Found one declaration whose name contains \"{needle}\".\n"
455 | else
456 | message := message ++ m!"Found {hits.size} declarations whose name contains \"{needle}\".\n"
457 | let remainingNamePats := hitArrays.pop.map (·.1)
458 | pure (hits, remainingNamePats)
459 | else do
460 | -- Query the declaration cache
461 | let (m₁, m₂) ← index.nameRelCache.get
462 | let hits := RBTree.intersects <| needles.toArray.map <| fun needle =>
463 | ((m₁.find needle).union (m₂.find needle)).insert needle
464 |
465 | let needlesList := .andList (needles.toList.map .ofConstName)
466 | if hits.size == 1 then
467 | message := message ++ m!"Found one declaration mentioning {needlesList}.\n"
468 | else
469 | message := message ++ m!"Found {hits.size} declarations mentioning {needlesList}.\n"
470 | pure (hits, namePats)
471 |
472 | -- Filter by name patterns
473 | let nameMatchers := remainingNamePats.map (String.Matcher.ofString ·.toLower)
474 | let hits2 := indexHits.toArray.filter fun n => nameMatchers.all fun m =>
475 | m.find? n.toString.toLower |>.isSome
476 | unless (remainingNamePats.isEmpty) do
477 | let nameList := .andList <| namePats.toList.map fun s => m!"\"{s}\""
478 | if hits2.size == 1 then
479 | message := message ++ m!"Of these, one has a name containing {nameList}.\n"
480 | else
481 | message := message ++ m!"Of these, {hits2.size} have a name containing {nameList}.\n"
482 |
483 | -- Obtain ConstantInfos
484 | let env ← getEnv
485 | let hits3 := hits2.filterMap env.find?
486 |
487 | -- Filter by term patterns
488 | let pats ← liftM <| terms.mapM fun (isConclusionPattern, t) =>
489 | if isConclusionPattern then
490 | matchConclusion t
491 | else
492 | matchAnywhere t
493 | let hits4 ← hits3.filterM fun ci => pats.allM (· ci)
494 | unless (pats.isEmpty) do
495 | if hits4.size == 1 then
496 | message := message ++ m!"Of these, one matches your pattern(s).\n"
497 | else
498 | message := message ++ m!"Of these, {hits4.size} match your pattern(s).\n"
499 |
500 | -- Sort by modindex and then by theorem size.
501 | let hits5 ← sortByModule' (·.name) (exprSize ·.type) hits4
502 |
503 | -- Apply maxShown limit
504 | unless (hits5.size ≤ maxShown) do
505 | message := message ++ m!"Of these, only the first {maxShown} are shown.\n"
506 | let hits6 := hits5.extract 0 maxShown
507 |
508 | -- Resolve ModIndex to module name
509 | let hits7 := hits6.map fun (ci, mi) =>
510 | let modName : Option Name := do env.header.moduleNames[(← mi).toNat]!
511 | (ci, modName)
512 |
513 | -- If searching by names had hits, but searching by patterns does not,
514 | -- suggest search just by names
515 | let mut suggestions := #[]
516 | if !needles.isEmpty && !indexHits.isEmpty && hits7.isEmpty then
517 | let is := needles.toArray.map Lean.mkIdent
518 | let sugg ← `(find_filters| $[$is:ident],*)
519 | suggestions := suggestions.push sugg
520 |
521 | return .ok ⟨message, hits4.size, hits7, suggestions⟩
522 |
523 | /-
524 | ### The per-module cache used by the `#find` command
525 | -/
526 |
527 | open System (FilePath)
528 |
529 | /-- Where to search for the cached index -/
530 | def cachePath : IO FilePath :=
531 | try
532 | return (← findOLean `LoogleMathlibCache).withExtension "extra"
533 | catch _ =>
534 | return ".lake" / "build" / "lib" / "LoogleMathlibCache.extra"
535 |
536 | /--
537 | The `DeclCache` used by `#find`.
538 |
539 | This is lazily initialized, so that the cost is only paid when Loogle is used, not when it is
540 | imported. Among other things, this means we do not bother loading the database when _compiling_
541 | `Loogle.lean` into a binary!
542 | -/
543 | initialize cachedIndex : IO.Thunk Index ← IO.Thunk.new <| unsafe do
544 | let path ← cachePath
545 | if (← path.pathExists) then
546 | let (d, _) ← unpickle _ path
547 | Index.mkFromCache d
548 | else
549 | Index.mk
550 |
551 | open Command
552 |
553 | /--
554 | Option to control whether `find` should print types of found lemmas
555 | -/
556 | register_option find.showTypes : Bool := {
557 | defValue := true
558 | descr := "showing types in #f"
559 | }
560 |
561 | def elabFind (args : TSyntax `Loogle.Find.find_filters) : TermElabM Unit := do
562 | profileitM Exception "find" (← getOptions) do
563 | match ← find (← cachedIndex.get) args with
564 | | .error ⟨s, warn, suggestions⟩ => do
565 | Lean.logErrorAt s warn
566 | unless suggestions.isEmpty do
567 | Lean.Meta.Tactic.TryThis.addSuggestions args <| suggestions.map fun sugg =>
568 | { suggestion := .tsyntax sugg }
569 | | .ok result =>
570 | let showTypes := (<- getOptions).get find.showTypes.name find.showTypes.defValue
571 | let names := result.hits.map $ fun x=> (x.1.name, x.1.type)
572 | Lean.logInfo $ result.header ++ (← MessageData.bulletListOfConstsAndTypes names showTypes)
573 |
574 |
575 | /--
576 | The `#find` command finds definitions and lemmas in various ways. One can search by: the constants
577 | involved in the type; a substring of the name; a subexpression of the type; or a subexpression
578 | located in the return type or a hypothesis specifically. All of these search methods can be
579 | combined in a single query, comma-separated.
580 |
581 | 1. By constant:
582 | ```lean
583 | #find Real.sin
584 | ```
585 | finds all lemmas whose statement somehow mentions the sine function.
586 |
587 | 2. By lemma name substring:
588 | ```lean
589 | #find "differ"
590 | ```
591 | finds all lemmas that have `"differ"` somewhere in their lemma _name_.
592 | This check is case-insensitive.
593 |
594 | 3. By subexpression:
595 | ```lean
596 | #find _ * (_ ^ _)
597 | ```
598 | finds all lemmas whose statements somewhere include a product where the second argument is
599 | raised to some power.
600 |
601 | The pattern can also be non-linear, as in
602 | ```lean
603 | #find Real.sqrt ?a * Real.sqrt ?a
604 | ```
605 |
606 | If the pattern has parameters, they are matched in any order. Both of these will find `List.map`:
607 | ```
608 | #find (?a -> ?b) -> List ?a -> List ?b
609 | #find List ?a -> (?a -> ?b) -> List ?b
610 | ```
611 |
612 | 4. By main conclusion:
613 | ```lean
614 | #find ⊢ tsum _ = _ * tsum _
615 | ```
616 | finds all lemmas where the conclusion (the subexpression to the right of all `→` and `∀`) has the
617 | given shape.
618 |
619 | As before, if the pattern has parameters, they are matched against the hypotheses of
620 | the lemma in any order; for example,
621 | ```lean
622 | #find ⊢ _ < _ → tsum _ < tsum _
623 | ```
624 | will find `tsum_lt_tsum` even though the hypothesis `f i < g i` is not the last.
625 |
626 |
627 | If you pass more than one such search filter, `#find` will return lemmas which match _all_ of them.
628 | The search
629 | ```lean
630 | #find Real.sin, "two", tsum, _ * _, _ ^ _, ⊢ _ < _ → _
631 | ```
632 | will find all lemmas which mention the constants `Real.sin` and `tsum`, have `"two"` as a
633 | substring of the lemma name, include a product and a power somewhere in the type, *and* have a
634 | hypothesis of the form `_ < _`.
635 |
636 | `#find` maintains an index of which lemmas mention which other constants and name fragments.
637 | If you have downloaded the olean cache for mathlib, the index will be precomputed. If not,
638 | the _first_ use of `#find` in a module will be slow (in the order of minutes). If you cannot use
639 | the distributed cache, it may be useful to open a scratch file, `import Mathlib`, and use `#find`
640 | there, this way you will find lemmas that you have not yet imported, and the
641 | cache will stay up-to-date.
642 |
643 | By default `#find` prints names and types of found definitions and lemmas. You can also make it print
644 | names only by setting `find.showType` to `false`:
645 |
646 | ```lean
647 | set_option find.showTypes false
648 | ```
649 | -/
650 | elab(name := findSyntax) "#find " args:find_filters : command =>
651 | liftTermElabM $ elabFind args
652 |
653 | @[inherit_doc findSyntax]
654 | elab(name := findSyntaxTac) "#find " args:find_filters : tactic =>
655 | elabFind args
656 |
--------------------------------------------------------------------------------
/Loogle/NameRel.lean:
--------------------------------------------------------------------------------
1 | /-
2 | Copyright (c) 2023 Joachim Breitner. All rights reserved.
3 | Released under Apache 2.0 license as described in the file LICENSE.
4 | Authors: Joachim Breitner
5 | -/
6 |
7 | import Lean.Data.NameMap
8 | import Lean.Declaration
9 |
10 | /-!
11 | ## A data structure for a relation on names
12 | -/
13 |
14 | open Lean Meta
15 |
16 | namespace Lean
17 |
18 | /-- A `NameRel` maps names to sets of names -/
19 | def NameRel := NameMap NameSet
20 |
21 | instance : EmptyCollection NameRel :=
22 | inferInstanceAs $ EmptyCollection (NameMap NameSet)
23 |
24 | instance : Inhabited NameRel :=
25 | inferInstanceAs $ Inhabited (NameMap NameSet)
26 |
27 | /-- Finds the set of names associated with the given one. -/
28 | def NameRel.find (m : NameRel) (n : Name) : NameSet := m.findD n {}
29 |
30 | /-- Inserts one entry to to the relation. -/
31 | def NameRel.insert (m : NameRel) (n₁ n₂ : Name) : NameRel :=
32 | m.find n₁ |>.insert n₂ |> NameMap.insert m n₁
33 |
34 | end Lean
35 |
--------------------------------------------------------------------------------
/Loogle/RBTree.lean:
--------------------------------------------------------------------------------
1 | /-
2 | Copyright (c) 2023 Joachim Breitner. All rights reserved.
3 | Released under Apache 2.0 license as described in the file LICENSE.
4 | Authors: Joachim Breitner
5 | -/
6 | import Lean.Data.RBTree
7 |
8 | /-!
9 | # Additional functions on `Lean.RBTree`.
10 | -/
11 |
12 | universe u
13 | variable {α : Type u} {cmp : α → α → Ordering}
14 |
15 | /-- The intersection of a (non-empty) array of `RBTree`s. If
16 | the input is empty, the empty tree is returned. -/
17 | def Lean.RBTree.intersects (ts : Array (RBTree α cmp)) : RBTree α cmp :=
18 | if ts.isEmpty then {} else
19 | -- sorts smallest set to the back, and iterate over that one
20 | -- TODO: An `RBTree` admits bulk operations, which could replace this implementation
21 | let ts := ts.qsort (·.size > ·.size)
22 | ts.back!.fold (init := {}) fun s m =>
23 | if ts.pop.all (·.contains m) then s.insert m else s
24 |
--------------------------------------------------------------------------------
/Loogle/Trie.lean:
--------------------------------------------------------------------------------
1 | /-
2 | Copyright (c) 2018 Microsoft Corporation. All rights reserved.
3 | Released under Apache 2.0 license as described in the file LICENSE.
4 | Author: Sebastian Ullrich, Leonardo de Moura, Joachim Breitner
5 |
6 | A string trie data structure, used for tokenizing the Lean language.
7 |
8 | Adapted from Lean.Data.Trie to use path compression.
9 |
10 | -/
11 | import Lean.Data.Format
12 | import Batteries.Data.Array
13 |
14 | namespace Loogle
15 |
16 |
17 | set_option autoImplicit false
18 |
19 | /-- A Trie is a key-value store where the keys are of type `String`,
20 | and the internal structure is a tree that branches on the bytes of the string. -/
21 | inductive Trie (α : Type) where
22 | | leaf : Option α → Trie α
23 | | path : Option α → (ps : ByteArray) → 0 < ps.size → Trie α → Trie α
24 | | node : Option α → ByteArray → Array (Trie α) → Trie α
25 |
26 | namespace Trie
27 | variable {α : Type}
28 |
29 | /-- The empty `Trie` -/
30 | def empty : Trie α := leaf none
31 |
32 | instance : EmptyCollection (Trie α) :=
33 | ⟨empty⟩
34 |
35 | instance : Inhabited (Trie α) where
36 | default := empty
37 |
38 | def commonPrefix (s₁ : String) (s₂ : ByteArray) (offset1 : Nat) : Nat :=
39 | let rec loop (i : Nat) : Nat :=
40 | if h : offset1 + i < s₁.utf8ByteSize then
41 | if h' : i < s₂.size then
42 | if s₁.getUtf8Byte (offset1 + i) h == s₂[i] then
43 | loop (i + 1)
44 | else
45 | i
46 | else
47 | i
48 | else
49 | i
50 | termination_by s₂.size - i
51 | loop 0
52 |
53 | def hasPrefix (s₁ : String) (s₂ : ByteArray) (offset1 : Nat) : Bool :=
54 | let rec loop (i : Nat) : Bool :=
55 | if h' : i < s₂.size then
56 | if h : offset1 + i < s₁.utf8ByteSize then
57 | if s₁.getUtf8Byte (offset1 + i) h == s₂[i] then
58 | loop (i + 1)
59 | else
60 | false
61 | else
62 | false
63 | else
64 | true
65 | termination_by s₂.size - i
66 | loop 0
67 |
68 | def mkPath (v : Option α) (ps : ByteArray) (t : Trie α) : Trie α :=
69 | if h : 0 < ps.size then
70 | path v ps h t
71 | else
72 | t
73 |
74 | /-- Insert or update the value at a the given key `s`. -/
75 | def upsert (t : Trie α) (s : String) (f : Option α → α) : Trie α :=
76 | let rec insertEmpty (i : Nat) : Trie α :=
77 | mkPath none (s.toUTF8.extract i s.utf8ByteSize) (leaf (f .none))
78 | let rec loop
79 | | i, leaf v =>
80 | if i < s.utf8ByteSize then
81 | mkPath v (s.toUTF8.extract i s.utf8ByteSize) (leaf (f .none))
82 | else
83 | leaf (f v)
84 | | i, path v ps hps t' =>
85 | if h : i < s.utf8ByteSize then
86 | let j := commonPrefix s ps i
87 | if hj : 0 < j then
88 | -- split common prefix, continue
89 | mkPath v (ps.extract 0 j) <| loop (i + j) <|
90 | mkPath none (ps.extract j ps.size) t'
91 | else
92 | -- no common prefix, split off first character
93 | let c := s.getUtf8Byte i h
94 | let c' := ps.get! 0
95 | let t := insertEmpty (i + 1)
96 | let t'' := mkPath none (ps.extract 1 ps.size) t'
97 | node v (.mk #[c, c']) #[t, t'']
98 | else
99 | path (f v) ps hps t'
100 | | i, node v cs ts =>
101 | if h : i < s.utf8ByteSize then
102 | let c := s.getUtf8Byte i h
103 | match cs.findIdx? (· == c) with
104 | | none =>
105 | let t := insertEmpty (i + 1)
106 | node v (cs.push c) (ts.push t)
107 | | some idx =>
108 | node v cs (ts.modify idx (loop (i + 1)))
109 | else
110 | node (f v) cs ts
111 | termination_by i _ => s.utf8ByteSize - i
112 | decreasing_by all_goals { simp_wf; omega }
113 | loop 0 t
114 |
115 | /-- Inserts a value at a the given key `s`, overriding an existing value if present. -/
116 | def insert (t : Trie α) (s : String) (val : α) : Trie α :=
117 | upsert t s (fun _ => val)
118 |
119 | /-- Looks up a value at the given key `s`. -/
120 | def find? (t : Trie α) (s : String) : Option α :=
121 | let rec loop
122 | | i, leaf val =>
123 | if i < s.utf8ByteSize then
124 | none
125 | else
126 | val
127 | | i, path val ps _ t' =>
128 | if i < s.utf8ByteSize then
129 | if hasPrefix s ps i then
130 | loop (i + ps.size) t'
131 | else
132 | none
133 | else
134 | val
135 | | i, node val cs ts =>
136 | if h : i < s.utf8ByteSize then
137 | let c := s.getUtf8Byte i h
138 | match cs.findIdx? (· == c) with
139 | | none => none
140 | | some idx => loop (i + 1) ts[idx]!
141 | else
142 | val
143 | termination_by i _ => s.utf8ByteSize - i
144 | decreasing_by all_goals { simp_wf; omega }
145 | loop 0 t
146 |
147 | /-- Returns an `Array` of all values in the trie, in no particular order. -/
148 | def values (t : Trie α) : Array α := go t |>.run #[] |>.2
149 | where
150 | go : Trie α → StateM (Array α) Unit
151 | | leaf a? => do
152 | if let some a := a? then
153 | modify (·.push a)
154 | | path a? _ _ t' => do
155 | if let some a := a? then
156 | modify (·.push a)
157 | go t'
158 | | node a? _ ts => do
159 | if let some a := a? then
160 | modify (·.push a)
161 | ts.attach.forM fun ⟨t',_⟩ => go t'
162 |
163 | /-- Returns all values whose key have the given string `pre` as a prefix, in no particular order. -/
164 | def findPrefix (t : Trie α) (pre : String) : Array α := go t 0
165 | where
166 | go (t : Trie α) (i : Nat) : Array α :=
167 | if h : i < pre.utf8ByteSize then
168 | let c := pre.getUtf8Byte i h
169 | match t with
170 | | leaf _val => .empty
171 | | path _val ps _ t' =>
172 | let j := commonPrefix pre ps i
173 | if j == ps.size then
174 | go t' (i + ps.size)
175 | else if i + j == pre.utf8ByteSize then
176 | t'.values
177 | else
178 | .empty
179 | | node _val cs ts =>
180 | match cs.findIdx? (· == c) with
181 | | none => .empty
182 | | some idx =>
183 | if let some ⟨t',_⟩ := ts.attach[idx]? then
184 | go t' (i + 1)
185 | else .empty -- should be unreachable
186 | else
187 | t.values
188 |
189 |
190 | open Lean in
191 | private partial def toStringAux {α : Type} : Trie α → List Format
192 | | leaf _ => []
193 | | path _ ps _ t =>
194 | [ format (repr ps.data), Format.group $ Format.nest 4 $ flip Format.joinSep Format.line $ toStringAux t ]
195 | | node _ cs ts =>
196 | List.flatten $ List.zipWith (fun c t =>
197 | [ format (repr c), (Format.group $ Format.nest 4 $ flip Format.joinSep Format.line $ toStringAux t) ]
198 | ) cs.toList ts.toList
199 |
200 | open Lean in
201 | instance {α : Type} : ToString (Trie α) :=
202 | ⟨fun t => (flip Format.joinSep Format.line $ toStringAux t).pretty⟩
203 |
204 | end Trie
205 |
206 |
207 | end Loogle
208 |
--------------------------------------------------------------------------------
/LoogleMathlibCache.lean:
--------------------------------------------------------------------------------
1 | /-
2 | Copyright (c) 2023 Joachim Breitner. All rights reserved.
3 | Released under Apache 2.0 license as described in the file LICENSE.
4 | Authors: Joachim Breitner
5 | -/
6 | import Mathlib
7 | import Loogle.Find
8 |
9 | /-!
10 | # Saving the `#find` cache.
11 |
12 | After importing of mathlib, we build a `#find` cache and pickle it to disk.
13 | This file will be distributed via our Azure storage.
14 | -/
15 |
16 | open Lean.Elab.Command
17 | open Loogle.Find
18 |
19 | run_meta do
20 | let path ← cachePath
21 | _ ← path.parent.mapM fun p => IO.FS.createDirAll p
22 | pickle path (← (← Index.mk).getCache) `Find
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | loogle
2 | ======
3 |
4 | Loogle is a search tool for [Lean]/[Mathlib], and can be used on the web, via a
5 | zulip bot, via APIs, from VSCode or nvim extensions as a Lean command or a command line tool.
6 |
7 | Try it at !
8 |
9 |
10 | $ loogle '(List.replicate (_ + _) _ = _)'
11 | Found 5 declarations mentioning List.replicate, HAdd.hAdd and Eq.
12 | Of these, 3 match your patterns.
13 |
14 | List.replicate_add
15 | List.replicate_succ
16 | List.replicate_succ'
17 |
18 | [lean]: https://leanprover.github.io/
19 | [mathlib]: https://github.com/leanprover-community/mathlib4
20 |
21 | Running locally
22 | ---------------
23 |
24 | To use `loogle` locally:
25 |
26 | 1. check out this repository
27 | 2. install [elan](https://github.com/leanprover/elan)
28 | 3. run `lake exe cache get`
29 | 4. run `lake exe loogle --help` (or other options)
30 |
31 | If you use `loogle` on a large repository like Mathlib, the startup-time will
32 | be rather large. Run `lake build LoogleMathlibCache` if you want to pre-compute
33 | the index for Mathlib.
34 |
35 | [elan]: https://github.com/leanprover/elan
36 |
37 | CLI Usage
38 | ---------
39 |
40 | USAGE:
41 | loogle [OPTIONS] [QUERY]
42 |
43 | OPTIONS:
44 | --help
45 | --interactive, -i read querys from stdin
46 | --json, -j print result in JSON format
47 | --module mod import this module (default: Mathlib)
48 | --path path search for .olean files here (default: the build time path)
49 | --write-index file persists the search index to a file
50 | --read-index file read the search index from a file. This file is blindly trusted!
51 |
52 | By default, it will create an internal index upon starting, which takes a bit.
53 | You can use `--write-index` and `--read-index` to cache that, but it is your
54 | responsibility to pass the right index for the given module and search path. In
55 | the nix setup, the index is built as part of the build process.
56 |
57 | Web service
58 | -----------
59 |
60 | This tool is the backend of . This is currently
61 | running on a virtual host with a nixos system with a ngingx reverse proxy (for
62 | SSL) in front of a small python HTTP server (see `./server.py`) that uses
63 | `loogle`. The query processing is locked down using SECCOMP (see
64 | `./loogle_seccomp.c`). It automatically tries to upgrade to the latest
65 | mathlib every 6 hours.
66 |
67 | You can run this server locally as well, either using `./server.py` after you
68 | built `loogle` via `lake`.
69 |
70 | At the path `/json?q=…` (instead of `/?q=…`), the result is returned in JSON
71 | format. No stability of the format is guaranteed at this point.
72 |
73 | Zulip bot
74 | ---------
75 |
76 | The [leanprover Zulip chat](https://leanprover.zulipchat.com/) has a bot called
77 | `loogle` that will respond to messages with the first two hits from loogle.
78 | Just write `@**loogle** query` in a public stream.
79 |
80 | It is implemented via an outgoing webhook to the above web service.
81 |
82 | Editor integration
83 | ------------------
84 |
85 | These are created by their respective maintainers; reach out to them if you have questions
86 |
87 | * [VS Code extension “Loogle Lean”](https://marketplace.visualstudio.com/items?itemName=ShreyasSrinivas.loogle-lean)
88 | * [`lean.nvim`](https://github.com/Julian/lean.nvim#features) has built-in support for loogle.
89 |
90 | Contact
91 | -------
92 |
93 | This tool was created by Joachim Breitner <>. Feel free
94 | to use this repository to report issues or (even better) submit PRs that
95 | resolves such issues.
96 |
--------------------------------------------------------------------------------
/Seccomp.lean:
--------------------------------------------------------------------------------
1 | import Lean
2 |
3 | syntax (name := ifEnv)
4 | "#ifEnv " str " then " ppLine command " else " command : command
5 |
6 | elab_rules : command
7 | | `(command| #ifEnv $s then $cmd1 else $cmd2) => do
8 | let s := s.getString
9 | if (← IO.getEnv s).isSome then do
10 | Lean.logInfo s!"[INFO] {s} is set"
11 | Lean.Elab.Command.elabCommandTopLevel cmd1
12 | else
13 | Lean.logInfo s!"[INFO] {s} is not set"
14 | Lean.Elab.Command.elabCommandTopLevel cmd2
15 |
16 |
17 |
18 | #ifEnv "LOOGLE_SECCOMP" then
19 | @[extern "loogle_seccomp"]
20 | def Seccomp.enable : BaseIO Unit := return ()
21 | else
22 | def Seccomp.enable : BaseIO Unit := return ()
23 |
--------------------------------------------------------------------------------
/Tests.lean:
--------------------------------------------------------------------------------
1 | import Loogle.Find
2 |
3 | -- We want the following tests to be self-contained.
4 | -- Therefore we erase all knowledge about imported definitios from find:
5 |
6 | elab (name := erase_loogle_cache) "erase_loogle_cache " : command => do
7 | (← Loogle.Find.cachedIndex.get).1.cache.set (.inr (.pure (pure (.empty, .empty))))
8 | (← Loogle.Find.cachedIndex.get).2.cache.set (.inr (.pure (pure (.empty, .empty))))
9 |
10 | erase_loogle_cache
11 |
12 | /-- error: Cannot search: No constants or name fragments in search pattern. -/
13 | #guard_msgs in
14 | #find
15 |
16 | /-- info: Found 0 declarations whose name contains "namefragmentsearch". -/
17 | #guard_msgs in
18 | #find "namefragmentsearch"
19 |
20 | /-- We use this definition in all tests below to get reproducible results,
21 | including the statistics about how many lemas were found in the index. -/
22 | def my_true : Bool := true
23 |
24 | theorem my_true_eq_true : my_true = true := rfl
25 |
26 | theorem my_true_eq_True : my_true = true := rfl -- intentionally capitalized
27 |
28 | /--
29 | info: Found 3 declarations mentioning my_true.
30 | • my_true : Bool
31 | • my_true_eq_True : my_true = true
32 | • my_true_eq_true : my_true = true
33 | -/
34 | #guard_msgs in
35 | #find my_true
36 |
37 | /--
38 | info: Found 3 declarations whose name contains "my_true".
39 | • my_true : Bool
40 | • my_true_eq_True : my_true = true
41 | • my_true_eq_true : my_true = true
42 | -/
43 | #guard_msgs in
44 | #find "my_true"
45 |
46 | /--
47 | info: Found 3 declarations whose name contains "y_tru".
48 | • my_true : Bool
49 | • my_true_eq_True : my_true = true
50 | • my_true_eq_true : my_true = true
51 | -/
52 | #guard_msgs in
53 | #find "y_tru"
54 |
55 | /--
56 | info: Found 3 declarations mentioning my_true.
57 | Of these, 2 have a name containing "eq".
58 | • my_true_eq_True : my_true = true
59 | • my_true_eq_true : my_true = true
60 | -/
61 | #guard_msgs in
62 | #find my_true, "eq"
63 |
64 | /--
65 | info: Found 2 declarations mentioning Bool, my_true and Eq.
66 | Of these, 2 match your pattern(s).
67 | • my_true_eq_True : my_true = true
68 | • my_true_eq_true : my_true = true
69 | -/
70 | #guard_msgs in
71 | #find my_true = _
72 |
73 | /--
74 | info: Found 2 declarations mentioning Bool, my_true and Eq.
75 | Of these, 0 match your pattern(s).
76 | -/
77 | #guard_msgs in
78 | #find (_ = my_true)
79 |
80 | /--
81 | error: unknown identifier 'doesn'texist'
82 | ---
83 | info: Try these:
84 | • "doesn'texist"
85 | -/
86 | #guard_msgs in
87 | #find doesn'texist
88 |
89 |
90 | /-- error: unknown identifier 'doesn'texist' -/
91 | #guard_msgs in
92 | #find (doesn'texist = _)
93 |
94 | /-- error: Cannot search for _. Did you forget to put a term pattern in parentheses? -/
95 | #guard_msgs in
96 | #find my_true, _
97 |
98 | /-- warning: declaration uses 'sorry' -/
99 | #guard_msgs in
100 | theorem non_linear_pattern_test1 {n m : Nat} :
101 | List.replicate (2 * n) () = List.replicate n () ++ List.replicate n () := by
102 | sorry
103 |
104 | /-- warning: declaration uses 'sorry' -/
105 | #guard_msgs in
106 | theorem non_linear_pattern_test2 {n m : Nat} :
107 | List.replicate n () ++ List.replicate m () = List.replicate (n + m) () := by
108 | sorry
109 |
110 | /--
111 | info: Found 2 declarations mentioning List.replicate, List and HAppend.hAppend.
112 | Of these, one matches your pattern(s).
113 | • non_linear_pattern_test1 : ∀ {n : Nat} {m : Nat},
114 | List.replicate (2 * n) () = List.replicate n () ++ List.replicate n ()
115 | -/
116 | #guard_msgs in
117 | #find List.replicate ?n _ ++ List.replicate ?n _
118 |
119 | /--
120 | info: Found 2 declarations mentioning List.replicate, List and HAppend.hAppend.
121 | Of these, 2 match your pattern(s).
122 | • non_linear_pattern_test2 : ∀ {n m : Nat}, List.replicate n () ++ List.replicate m () = List.replicate (n + m) ()
123 | • non_linear_pattern_test1 : ∀ {n : Nat} {m : Nat},
124 | List.replicate (2 * n) () = List.replicate n () ++ List.replicate n ()
125 | -/
126 | #guard_msgs in
127 | #find List.replicate ?n _ ++ List.replicate ?m _
128 |
129 | /--
130 | info: Found 2 declarations mentioning List.replicate, List, Eq and HAppend.hAppend.
131 | Of these, one matches your pattern(s).
132 | • non_linear_pattern_test1 : ∀ {n : Nat} {m : Nat},
133 | List.replicate (2 * n) () = List.replicate n () ++ List.replicate n ()
134 | -/
135 | #guard_msgs in
136 | #find |- _ = List.replicate ?n _ ++ List.replicate ?m _
137 |
138 | /--
139 | info: Found 2 declarations mentioning List.replicate, List, Eq and HAppend.hAppend.
140 | Of these, one matches your pattern(s).
141 | • non_linear_pattern_test2 : ∀ {n m : Nat}, List.replicate n () ++ List.replicate m () = List.replicate (n + m) ()
142 | -/
143 | #guard_msgs in
144 | #find |- List.replicate ?n _ ++ List.replicate ?m _ = _
145 |
146 | theorem hyp_ordering_test1 {n : Nat} (h : 0 < n) (_ : n + n = 6 * n): 0 ≤ n := Nat.le_of_lt h
147 | theorem hyp_ordering_test2 {n : Nat} (_ : n + n = 6 * n) (h : 0 < n) : 0 ≤ n := Nat.le_of_lt h
148 |
149 | /--
150 | info: Found 2 declarations mentioning LE.le, LT.lt and OfNat.ofNat.
151 | Of these, 2 match your pattern(s).
152 | • hyp_ordering_test1 : ∀ {n : Nat}, 0 < n → n + n = 6 * n → 0 ≤ n
153 | • hyp_ordering_test2 : ∀ {n : Nat}, n + n = 6 * n → 0 < n → 0 ≤ n
154 | -/
155 | #guard_msgs in
156 | #find ⊢ 0 < ?n → _ ≤ ?n
157 |
158 |
159 | -- Regression test
160 |
161 | section LinearPatternTest
162 | namespace LinearPatternTest
163 |
164 | class Star (R : Type _) where star : R → R
165 | export Star(star)
166 |
167 | /-- warning: declaration uses 'sorry' -/
168 | #guard_msgs in
169 | theorem star_comm_self' {R} [Mul R] [Star R] (x : R) : star x * x = x * star x := sorry
170 |
171 | /--
172 | info: Found 2 declarations mentioning star.
173 | Of these, one matches your pattern(s).
174 | • star_comm_self' : ∀ {R : Type u_1} [inst : Mul R] [inst_1 : Star R] (x : R), star x * x = x * star x
175 | -/
176 | #guard_msgs in
177 | #find star _
178 |
179 | /--
180 | info: Found one declaration mentioning HMul.hMul, star and Eq.
181 | Of these, one matches your pattern(s).
182 | • star_comm_self' : ∀ {R : Type u_1} [inst : Mul R] [inst_1 : Star R] (x : R), star x * x = x * star x
183 | -/
184 | #guard_msgs in
185 | #find star ?a * ?a = ?a * star ?_
186 |
187 | /--
188 | info: Found one declaration mentioning HMul.hMul, star and Eq.
189 | Of these, one matches your pattern(s).
190 | • star_comm_self' : ∀ {R : Type u_1} [inst : Mul R] [inst_1 : Star R] (x : R), star x * x = x * star x
191 | -/
192 | #guard_msgs in
193 | #find star ?a * ?a = ?b * star ?b
194 |
195 |
196 | end LinearPatternTest
197 |
198 | section ListMapTest
199 |
200 | open Loogle.Find
201 | open Lean Elab Command
202 |
203 | elab s:"#assert_match " name_s:ident concl:(turnstyle)? query:term : command => liftTermElabM do
204 | let pat ← Loogle.Find.elabTerm' query none
205 | let name := Lean.TSyntax.getId name_s
206 | let matcher ←
207 | if concl.isSome
208 | then matchConclusion pat
209 | else matchAnywhere pat
210 | let c := (← getEnv).constants.find! name
211 | unless ← matcher c do
212 | logErrorAt s "Pattern does not match when it should!"
213 |
214 | #assert_match List.map (?a -> ?b) -> List ?a -> List ?b
215 | #assert_match List.map List ?a → (?a -> ?b) -> List ?b
216 | #assert_match List.map |- (?a -> ?b) -> List ?a -> List ?b
217 | #assert_match List.get |- List ?a -> ?a
218 |
219 | end ListMapTest
220 |
221 | section DefaultingTest
222 |
223 | set_option autoImplicit true
224 |
225 | /-- warning: declaration uses 'sorry' -/
226 | #guard_msgs in
227 | theorem test_with_zero {α} [Zero α] [HMul α α α] [LE α] {a : α}: 0 ≤ a * a := sorry
228 |
229 | -- Tests the defaulting does not kick in below
230 |
231 | #assert_match test_with_zero |- 0 ≤ ?a * ?a
232 |
233 | end DefaultingTest
234 |
235 |
236 | /-- error: Name pattern is too general -/
237 | #guard_msgs in
238 | #find ""
239 |
240 | /-- error: Name pattern is too general -/
241 | #guard_msgs in
242 | #find "."
243 |
244 | /--
245 | info: Found 2 declarations whose name contains "my_true_eq_True".
246 | • my_true_eq_True : my_true = true
247 | • my_true_eq_true : my_true = true
248 | -/
249 | #guard_msgs in
250 | #find "my_true_eq_True" -- checks for case-insensitivity
251 |
252 |
253 | -- Check that |- only allows Sort-typed things
254 |
255 | /--
256 | info: Found 0 declarations mentioning And and True.
257 | Of these, 0 match your pattern(s).
258 | -/
259 | #guard_msgs in
260 | #find And True
261 |
262 | /-- error: Conclusion pattern is of type Bool, should be of type `Sort` -/
263 | #guard_msgs in
264 | #find |- true
265 |
266 | /-- error: Conclusion pattern is of type Prop → Prop, should be of type `Sort` -/
267 | #guard_msgs in
268 | #find |- And True
269 |
270 | /--
271 | info: Found 0 declarations mentioning And, True and my_true.
272 | Of these, 0 match your pattern(s).
273 | -/
274 | #guard_msgs in
275 | #find |- And True True, my_true
276 |
277 |
278 | -- Searching for qualified names
279 |
280 | def Namespaced.TestDefinition := true
281 |
282 | theorem TestDefinition_eq_true:
283 | Namespaced.TestDefinition = true := rfl
284 |
285 | /--
286 | error: unknown identifier 'TestDefinition'
287 | ---
288 | info: Try these:
289 | • "TestDefinition"
290 | • Namespaced.TestDefinition
291 | -/
292 | #guard_msgs in
293 | #find TestDefinition
294 |
295 |
296 | -- Handlig duplcats
297 |
298 | def NamespacedA.AnotherTestDefinition := true
299 | def NamespacedB.AnotherTestDefinition := true
300 | def NamespacedC.AnotherTestDefinition := true
301 | theorem NamespcedA.AnotherTestDefinition_eq_true:
302 | NamespacedA.AnotherTestDefinition = true := rfl
303 | theorem NamespcedB.AnotherTestDefinition_eq_true:
304 | NamespacedB.AnotherTestDefinition = true := rfl
305 |
306 | /--
307 | error: unknown identifier 'AnotherTestDefinition'
308 | ---
309 | info: Try these:
310 | • "AnotherTestDefinition"
311 | • NamespacedA.AnotherTestDefinition
312 | • NamespacedB.AnotherTestDefinition
313 | • NamespacedC.AnotherTestDefinition
314 | -/
315 | #guard_msgs in
316 | #find AnotherTestDefinition
317 |
318 | /--
319 | error: unknown identifier 'AnotherTestDefinition'
320 | ---
321 | info: Try these:
322 | • "some string before", "AnotherTestDefinition", some (Expr after)
323 | • "some string before", NamespacedA.AnotherTestDefinition, some (Expr after)
324 | • "some string before", NamespacedB.AnotherTestDefinition, some (Expr after)
325 | • "some string before", NamespacedC.AnotherTestDefinition, some (Expr after)
326 | -/
327 | #guard_msgs in
328 | #find "some string before", AnotherTestDefinition, some (Expr after)
329 |
330 | -- doesn't give suggestions (yet)
331 |
332 | /--
333 | error: unknown identifier 'AnotherTestDefinition'
334 | ---
335 | info: Try these:
336 | • "some string before", NamespacedA.AnotherTestDefinition = _, some (Expr after)
337 | • "some string before", NamespacedB.AnotherTestDefinition = _, some (Expr after)
338 | • "some string before", NamespacedC.AnotherTestDefinition = _, some (Expr after)
339 | -/
340 | #guard_msgs in
341 | #find "some string before", AnotherTestDefinition = _, some (Expr after)
342 |
343 | /--
344 | error: unknown identifier 'AnotherTestDefinition'
345 | ---
346 | info: Try these:
347 | • "some string before", |- NamespacedA.AnotherTestDefinition = _, some (Expr after)
348 | • "some string before", |- NamespacedB.AnotherTestDefinition = _, some (Expr after)
349 | • "some string before", |- NamespacedC.AnotherTestDefinition = _, some (Expr after)
350 | -/
351 | #guard_msgs in
352 | #find "some string before", |- AnotherTestDefinition = _, some (Expr after)
353 |
354 | -- The following check checks that a type error (or other error) at an identifier
355 | -- that can be resolved doesn't make #find look for possible candidates
356 |
357 | /--
358 | error: application type mismatch
359 | Nat.add 0 my_true
360 | argument
361 | my_true
362 | has type
363 | Bool : Type
364 | but is expected to have type
365 | Nat : Type
366 | -/
367 | #guard_msgs in
368 | #find Nat.add 0 my_true
369 |
370 |
371 | -- A pattern with a coercion
372 |
373 | inductive A where | A1 : A | A2 : A
374 | inductive B where | mk : B
375 | def B.ofA : A → B | A.A1 => B.mk | A.A2 => B.mk
376 | instance : Coe A B where coe := B.ofA
377 |
378 | def findThisLemma : A.A1 = B.mk := rfl
379 | def doNotFindThisLemma : ∀ a, a = B.mk := fun _a => rfl
380 |
381 | /--
382 | info: Found one declaration mentioning B, A.A1, B.ofA, B.mk and Eq.
383 | Of these, one matches your pattern(s).
384 | • findThisLemma : B.ofA A.A1 = B.mk
385 | -/
386 | #guard_msgs in
387 | #find A.A1 = B.mk
388 |
389 |
390 | /--
391 | info: Found one declaration mentioning B, A.A1, B.ofA, B.mk and Eq.
392 | Of these, one matches your pattern(s).
393 | • findThisLemma : B.ofA A.A1 = B.mk
394 | -/
395 | #guard_msgs in
396 | #find Eq B.mk A.A1
397 |
398 |
399 | set_option pp.raw true
400 |
401 | /--
402 | info: Found one declaration mentioning B, A.A1, B.ofA, B.mk and Eq.
403 | Of these, one matches your pattern(s).
404 | • findThisLemma : Eq.{1} B (B.ofA A.A1) B.mk
405 | -/
406 | #guard_msgs in
407 | #find ↑A.A1 = B.mk
408 |
409 |
410 | def this_peculiar_name_repeats_a_peculiar_substring := true
411 | def this_other_peculiar_name_repeats_a_peculiar_substring := true
412 | /--
413 | info: Found 2 declarations whose name contains "peculiar".
414 | • this_other_peculiar_name_repeats_a_peculiar_substring : Bool
415 | • this_peculiar_name_repeats_a_peculiar_substring : Bool
416 | -/
417 | #guard_msgs in
418 | #find "peculiar"
419 |
420 |
421 | /--
422 | info: Found 2 declarations whose name contains "peculiar".
423 | Of these, one has a name containing "peculiar" and "the".
424 | • this_other_peculiar_name_repeats_a_peculiar_substring : Bool
425 | -/
426 | #guard_msgs in
427 | #find "peculiar", "the"
428 |
429 | -- To make find not to print types of found definitions and lemmas use `find.showTypes` option
430 |
431 | set_option find.showTypes false
432 |
433 | /--
434 | info: Found 3 declarations mentioning my_true.
435 | Of these, 2 have a name containing "eq".
436 | • my_true_eq_True
437 | • my_true_eq_true
438 | -/
439 | #guard_msgs in
440 | #find my_true, "eq"
441 |
--------------------------------------------------------------------------------
/blurb.html:
--------------------------------------------------------------------------------
1 |
About
2 |
Loogle searches Lean and Mathlib definitions and theorems.
Loogle finds definitions and lemmas in various ways:
15 |
16 |
By constant:
17 | 🔍 Real.sin
18 | finds all lemmas whose statement somehow mentions the sine
19 | function.
20 |
By lemma name substring:
21 | 🔍 "differ"
22 | finds all lemmas that have "differ" somewhere in their
23 | lemma name.
24 |
By subexpression:
25 | 🔍 _ * (_ ^ _)
26 | finds all lemmas whose statements somewhere include a product where the
27 | second argument is raised to some power.
By main conclusion:
38 | 🔍 |- tsum _ = _ * tsum _
40 | finds all lemmas where the conclusion (the subexpression to the right of
41 | all → and ∀) has the given shape.
42 |
As before, if the pattern has parameters, they are matched against
43 | the hypotheses of the lemma in any order; for example,
44 | 🔍 |- _ < _ → tsum _ < tsum _
46 | will find tsum_lt_tsum even though the hypothesis
47 | f i < g i is not the last.
48 |
49 |
If you pass more than one such search filter, separated by commas
50 | Loogle will return lemmas which match all of them. The
51 | search
52 | 🔍 Real.sin, "two", tsum, _ * _, _ ^ _, |- _ < _ → _
54 | would find all lemmas which mention the constants Real.sin
55 | and tsum, have "two" as a substring of the
56 | lemma name, include a product and a power somewhere in the type,
57 | and have a hypothesis of the form _ < _ (if
58 | there were any such lemmas). Metavariables (?a) are
59 | assigned independently in each filter.
60 |
The #lucky button will directly send you to the
61 | documentation of the first hit.
402 | """, "utf-8"))
403 | if "hits" in result:
404 | self.wfile.write(bytes(f"""
405 |
406 | """, "utf-8"))
407 | for hit in result["hits"]:
408 | name = hit["name"]
409 | mod = hit["module"]
410 | type = hit["type"]
411 | self.wfile.write(bytes(f"""
412 |