├── .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.

3 |

You can use Loogle from within the Lean4 VSCode language extension 4 | using (by default) Ctrl-K Ctrl-S. You can also try the 5 | #loogle command from LeanSearchClient, 7 | the CLI version, the Loogle 9 | VS Code extension, the lean.nvim 11 | integration or the Zulip bot.

13 |

Usage

14 |

Loogle finds definitions and lemmas in various ways:

15 |
    16 |
  1. By constant:
    17 | 🔍 Real.sin
    18 | finds all lemmas whose statement somehow mentions the sine 19 | function.

  2. 20 |
  3. By lemma name substring:
    21 | 🔍 "differ"
    22 | finds all lemmas that have "differ" somewhere in their 23 | lemma name.

  4. 24 |
  5. By subexpression:
    25 | 🔍 _ * (_ ^ _)
    26 | finds all lemmas whose statements somewhere include a product where the 27 | second argument is raised to some power.

    28 |

    The pattern can also be non-linear, as in
    29 | 🔍 Real.sqrt ?a * Real.sqrt ?a

    31 |

    If the pattern has parameters, they are matched in any order. Both of 32 | these will find List.map:
    33 | 🔍 (?a -> ?b) -> List ?a -> List ?b
    35 | 🔍 List ?a -> (?a -> ?b) -> List ?b

  6. 37 |
  7. 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.

  8. 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.

62 |

Source code

63 |

You can find the source code for this service at https://github.com/nomeata/loogle. The https://loogle.lean-lang.org/ service is provided by the 68 | Lean FRO.

69 | -------------------------------------------------------------------------------- /blurb.md: -------------------------------------------------------------------------------- 1 | ## About 2 | Loogle searches Lean and Mathlib definitions and theorems. 3 | 4 | You can use Loogle from within the Lean4 VSCode language extension using (by default) Ctrl-K Ctrl-S. You can also try the `#loogle` command from [LeanSearchClient](https://github.com/leanprover-community/LeanSearchClient), the [CLI version](https://github.com/nomeata/loogle), the [Loogle VS Code extension](https://marketplace.visualstudio.com/items?itemName=ShreyasSrinivas.loogle-lean), the [`lean.nvim` integration](https://github.com/Julian/lean.nvim#features) or the [Zulip bot](https://github.com/nomeata/loogle#zulip-bot). 5 | 6 | ## Usage 7 | 8 | Loogle finds definitions and lemmas in various ways: 9 | 10 | 1. By constant:\ 11 | 🔍 [`Real.sin`](?q=Real.sin)\ 12 | finds all lemmas whose statement somehow mentions the sine function. 13 | 14 | 2. By lemma name substring:\ 15 | 🔍 [`"differ"`](?q="differ")\ 16 | finds all lemmas that have `"differ"` somewhere in their lemma _name_. 17 | 18 | 3. By subexpression:\ 19 | 🔍 [`_ * (_ ^ _)`](?q=_+*+(_+^+_))\ 20 | finds all lemmas whose statements somewhere include a product where the second argument is 21 | raised to some power. 22 | 23 | The pattern can also be non-linear, as in\ 24 | 🔍 [`Real.sqrt ?a * Real.sqrt ?a`](?q=Real.sqrt+%3Fa+*+Real.sqrt+%3Fa) 25 | 26 | If the pattern has parameters, they are matched in any order. Both of these will find `List.map`:\ 27 | 🔍 [`(?a -> ?b) -> List ?a -> List ?b`](?q=(?a -> ?b) -> List ?a -> List ?b)\ 28 | 🔍 [`List ?a -> (?a -> ?b) -> List ?b`](?q=List ?a -> (?a -> ?b) -> List ?b) 29 | 30 | 4. By main conclusion:\ 31 | 🔍 [`|- tsum _ = _ * tsum _`](?q=|- tsum _ = _ * tsum _)\ 32 | finds all lemmas where the conclusion (the subexpression to the right of all `→` and `∀`) has the 33 | given shape. 34 | 35 | As before, if the pattern has parameters, they are matched against the hypotheses of 36 | the lemma in any order; for example,\ 37 | 🔍 [`|- _ < _ → tsum _ < tsum _`](?q=|- _ < _ → tsum _ < tsum _)\ 38 | will find `tsum_lt_tsum` even though the hypothesis `f i < g i` is not the last. 39 | 40 | 41 | If you pass more than one such search filter, separated by commas Loogle will return lemmas which match _all_ of them. 42 | The search\ 43 | 🔍 [`Real.sin, "two", tsum, _ * _, _ ^ _, |- _ < _ → _`](?q=Real.sin,+"two",+tsum,+_+*+_,+_+^+_,+|-+_+<+_+→+_)\ 44 | would find all lemmas which mention the constants `Real.sin` and `tsum`, have `"two"` as a 45 | substring of the lemma name, include a product and a power somewhere in the type, *and* have a 46 | hypothesis of the form `_ < _` (if there were any such lemmas). Metavariables (`?a`) are assigned independently in each filter. 47 | 48 | The `#lucky` button will directly send you to the documentation of the first hit. 49 | 50 | ## Source code 51 | 52 | You can find the source code for this service at . 53 | The service is provided by the Lean FRO. 54 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1736096028, 6 | "narHash": "sha256-LZlcIxrfPxK0XSH3R17Ue7RbJ9Vv1jHlFRTK9q1iCqQ=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "63f67a56387685aa2af3773f81f766c17aed2746", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "master", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = github:NixOS/nixpkgs/master; 3 | 4 | outputs = { self, nixpkgs, ...}@inputs: 5 | let 6 | system = "x86_64-linux"; 7 | pkgs = nixpkgs.legacyPackages.${system}; 8 | my_python = pkgs.python3.withPackages(p: with p; [prometheus-client]); 9 | in 10 | { 11 | devShells.${system}.default = (pkgs.mkShell.override { stdenv = pkgs.llvmPackages_15.stdenv; }) { 12 | packages = with pkgs; 13 | [ elan 14 | pkgsStatic.libseccomp 15 | gdb 16 | my_python 17 | ]; 18 | }; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /lake-manifest.json: -------------------------------------------------------------------------------- 1 | {"version": "1.1.0", 2 | "packagesDir": ".lake/packages", 3 | "packages": 4 | [{"url": "https://github.com/leanprover-community/mathlib4", 5 | "type": "git", 6 | "subDir": null, 7 | "scope": "", 8 | "rev": "e33299d64d0cf96dba1a31e50f299a63f91f6654", 9 | "name": "mathlib", 10 | "manifestFile": "lake-manifest.json", 11 | "inputRev": "master", 12 | "inherited": false, 13 | "configFile": "lakefile.lean"}, 14 | {"url": "https://github.com/leanprover-community/plausible", 15 | "type": "git", 16 | "subDir": null, 17 | "scope": "leanprover-community", 18 | "rev": "304c5e2f490d546134c06bf8919e13b175272084", 19 | "name": "plausible", 20 | "manifestFile": "lake-manifest.json", 21 | "inputRev": "main", 22 | "inherited": true, 23 | "configFile": "lakefile.toml"}, 24 | {"url": "https://github.com/leanprover-community/LeanSearchClient", 25 | "type": "git", 26 | "subDir": null, 27 | "scope": "leanprover-community", 28 | "rev": "25078369972d295301f5a1e53c3e5850cf6d9d4c", 29 | "name": "LeanSearchClient", 30 | "manifestFile": "lake-manifest.json", 31 | "inputRev": "main", 32 | "inherited": true, 33 | "configFile": "lakefile.toml"}, 34 | {"url": "https://github.com/leanprover-community/import-graph", 35 | "type": "git", 36 | "subDir": null, 37 | "scope": "leanprover-community", 38 | "rev": "f5e58ef1f58fc0cbd92296d18951f45216309e48", 39 | "name": "importGraph", 40 | "manifestFile": "lake-manifest.json", 41 | "inputRev": "main", 42 | "inherited": true, 43 | "configFile": "lakefile.toml"}, 44 | {"url": "https://github.com/leanprover-community/ProofWidgets4", 45 | "type": "git", 46 | "subDir": null, 47 | "scope": "leanprover-community", 48 | "rev": "632ca63a94f47dbd5694cac3fd991354b82b8f7a", 49 | "name": "proofwidgets", 50 | "manifestFile": "lake-manifest.json", 51 | "inputRev": "v0.0.59", 52 | "inherited": true, 53 | "configFile": "lakefile.lean"}, 54 | {"url": "https://github.com/leanprover-community/aesop", 55 | "type": "git", 56 | "subDir": null, 57 | "scope": "leanprover-community", 58 | "rev": "9264d548cf1ccf0ba454b82f931f44c37c299fc1", 59 | "name": "aesop", 60 | "manifestFile": "lake-manifest.json", 61 | "inputRev": "master", 62 | "inherited": true, 63 | "configFile": "lakefile.toml"}, 64 | {"url": "https://github.com/leanprover-community/quote4", 65 | "type": "git", 66 | "subDir": null, 67 | "scope": "leanprover-community", 68 | "rev": "36ce5e17d6ab3c881e0cb1bb727982507e708130", 69 | "name": "Qq", 70 | "manifestFile": "lake-manifest.json", 71 | "inputRev": "master", 72 | "inherited": true, 73 | "configFile": "lakefile.toml"}, 74 | {"url": "https://github.com/leanprover-community/batteries", 75 | "type": "git", 76 | "subDir": null, 77 | "scope": "leanprover-community", 78 | "rev": "78e1181c4752c7e10874d2ed5a6a15063f4a35b6", 79 | "name": "batteries", 80 | "manifestFile": "lake-manifest.json", 81 | "inputRev": "main", 82 | "inherited": true, 83 | "configFile": "lakefile.toml"}, 84 | {"url": "https://github.com/leanprover/lean4-cli", 85 | "type": "git", 86 | "subDir": null, 87 | "scope": "leanprover", 88 | "rev": "4f22c09e7ded721e6ecd3cf59221c4647ca49664", 89 | "name": "Cli", 90 | "manifestFile": "lake-manifest.json", 91 | "inputRev": "main", 92 | "inherited": true, 93 | "configFile": "lakefile.toml"}], 94 | "name": "loogle", 95 | "lakeDir": ".lake"} 96 | -------------------------------------------------------------------------------- /lakefile.lean: -------------------------------------------------------------------------------- 1 | import Lake 2 | open Lake DSL 3 | 4 | package «loogle» { 5 | moreLinkArgs := 6 | if run_io Option.isSome <$> IO.getEnv "LOOGLE_SECCOMP" 7 | then #[ "-lseccomp" ] 8 | else #[] 9 | testDriver := "Tests" 10 | } 11 | 12 | -- require std from git "https://github.com/leanprover/std4" @ "main" 13 | require mathlib from git "https://github.com/leanprover-community/mathlib4" @ "master" 14 | 15 | meta if run_io Option.isSome <$> IO.getEnv "LOOGLE_SECCOMP" then do 16 | target loogle_seccomp.o pkg : System.FilePath := do 17 | let oFile := pkg.buildDir / "loogle_seccomp.o" 18 | let srcJob ← inputTextFile <| pkg.dir / "loogle_seccomp.c" 19 | let flags := #["-I", (← getLeanIncludeDir).toString, "-fPIC"] 20 | buildO oFile srcJob flags #[] "cc" 21 | 22 | extern_lib libloogle_seccomp pkg := do 23 | let name := nameToStaticLib "loogle_seccomp" 24 | let ffiO ← fetch <| pkg.target ``loogle_seccomp.o 25 | buildStaticLib (pkg.nativeLibDir / name) #[ffiO] 26 | 27 | lean_lib Seccomp where 28 | roots := #[`Seccomp] 29 | precompileModules := true 30 | 31 | lean_lib Loogle where 32 | roots := #[`Loogle] 33 | globs := #[.andSubmodules `Loogle] 34 | 35 | lean_lib LoogleMathlibCache where 36 | roots := #[`LoogleMathlibCache] 37 | 38 | lean_lib Tests where 39 | roots := #[`Tests] 40 | 41 | @[default_target] 42 | lean_exe loogle where 43 | root := `Loogle 44 | supportInterpreter := true 45 | -------------------------------------------------------------------------------- /lean-toolchain: -------------------------------------------------------------------------------- 1 | leanprover/lean4:v4.20.0-rc5 2 | -------------------------------------------------------------------------------- /loogle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomeata/loogle/19971e9e513c648628fc733844b818d6816534c5/loogle.png -------------------------------------------------------------------------------- /loogle_seccomp.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | LEAN_EXPORT lean_obj_res loogle_seccomp ( lean_obj_arg _0 ) { 8 | scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL_PROCESS); 9 | seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0); 10 | seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0); 11 | seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0); 12 | seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0); 13 | seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0); 14 | 15 | seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0); 16 | seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(madvise), 0); 17 | seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0); 18 | seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(munmap), 0); 19 | 20 | // Part of a clean shutdown it seems 21 | seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(futex), 0); 22 | 23 | // Depending on when seccomp is enabled, lean wants to fork something aftewards 24 | // If so, the following can help 25 | // seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigaction), 0); 26 | // seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigprocmask), 0); 27 | // seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mprotect), 0); 28 | // seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(clone3), 0); 29 | // seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(futex), 0); 30 | // seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rseq), 0); 31 | // seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(set_robust_list), 0); 32 | // seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(sigaltstack), 0); 33 | // seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(madvise), 0); 34 | 35 | // lean seems to use newfsstatat on handle 0 and 1 upon first use 36 | // of stdin and stdout. If they are used before Seccomp.enable 37 | // then the following is not needed 38 | seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(newfstatat), 0); 39 | seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat), 0); 40 | seccomp_load(ctx); 41 | return lean_io_result_mk_ok(lean_box(0)); 42 | } 43 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from http.server import BaseHTTPRequestHandler, HTTPServer 4 | import urllib 5 | import subprocess 6 | import json 7 | import html 8 | import sys 9 | import time 10 | import re 11 | import os 12 | import select 13 | 14 | 15 | hostName = "localhost" 16 | serverPort = 8088 17 | 18 | blurb = open("./blurb.html","rb").read() 19 | icon = open("./loogle.png","rb").read() 20 | 21 | rev1 = "UNKNOWN" 22 | try: 23 | rev1 = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('ascii').strip() 24 | except _: 25 | pass 26 | 27 | rev2 = "UNKNOWN" 28 | try: 29 | manifest = json.load(open('lake-manifest.json')) 30 | for package in manifest['packages']: 31 | if package['name'] == "mathlib": 32 | rev2 = package['rev'] 33 | except _: 34 | pass 35 | 36 | # Prometheus setup 37 | import prometheus_client 38 | m_info = prometheus_client.Info('versions', 'Lean and mathlib versions') 39 | m_info.info({'loogle': rev1, 'mathlib': rev2}) 40 | m_queries = prometheus_client.Counter('queries', 'Total number of queries') 41 | m_errors = prometheus_client.Counter('errors', 'Total number of failing queries') 42 | m_results = prometheus_client.Histogram('results', 'Results per query', buckets=(0,1,2,5,10,50,100,200,500,1000)) 43 | m_heartbeats = prometheus_client.Histogram('heartbeats', 'Heartbeats per query', buckets=(0,2e0,2e1,2e2,2e3,2e4)) 44 | m_client = prometheus_client.Counter('clients', 'Clients used', ["client"]) 45 | for l in ("web", "zulip", "json", "nvim", "vscode-lean4", "vscode-loogle", "LeanSearchClient"): m_client.labels(l) 46 | 47 | examples = [ 48 | "Real.sin", 49 | "Real.sin, tsum", 50 | "Real.sin (_ + 2*Real.pi)", 51 | "List.replicate (_ + _) _", 52 | "Real.sqrt ?a * Real.sqrt ?a", 53 | ] 54 | 55 | class Loogle(): 56 | def __init__(self): 57 | self.start() 58 | 59 | def start(self): 60 | self.starting = True 61 | self.loogle = subprocess.Popen( 62 | #[".lake/build/bin/loogle","--json", "--interactive", "--module","Init.Data.List.Basic"], 63 | [".lake/build/bin/loogle","--json", "--interactive"], 64 | stdin=subprocess.PIPE, 65 | stdout=subprocess.PIPE, 66 | ) 67 | 68 | def do_query(self, query): 69 | if self.starting: 70 | r, w, e = select.select([ self.loogle.stdout ], [], [], 0) 71 | if self.loogle.stdout in r: 72 | greeting = self.loogle.stdout.readline() 73 | if greeting != b"Loogle is ready.\n": 74 | self.loogle.kill() # just to be sure 75 | self.start() 76 | return {"error": "The backend process did not send greeting, killing and restarting..."} 77 | else: 78 | self.starting = False 79 | else: 80 | return {"error": "The backend process is starting up, please try again later..."} 81 | try: 82 | self.loogle.stdin.write(bytes(query, "utf8")); 83 | self.loogle.stdin.write(b"\n"); 84 | self.loogle.stdin.flush(); 85 | output_json = self.loogle.stdout.readline() 86 | output = json.loads(output_json) 87 | return output 88 | except (IOError, json.JSONDecodeError) as e: 89 | time.sleep(5) # to allow the process to die 90 | code = self.loogle.poll() 91 | if code == -31: 92 | sys.stderr.write(f"Backend died trying to escape the sandbox.\n") 93 | self.start() 94 | return {"error": 95 | f"Backend died trying to escape the sandbox. Restarting..." 96 | } 97 | if code is not None: 98 | sys.stderr.write(f"Backend died with code {code}.\n") 99 | self.start() 100 | return {"error": 101 | f"The backend process died with code {code}. Restarting..." 102 | } 103 | else: 104 | sys.stderr.write(f"Backend did not respond ({e}).\n") 105 | self.loogle.kill() # just to be sure 106 | self.start() 107 | return {"error": "The backend process did not respond, killing and restarting..."} 108 | 109 | def query(self, query): 110 | m_queries.inc() 111 | print(f"Query: {json.dumps(query)}", flush=True) 112 | output = self.do_query(query) 113 | # Update metrics 114 | if "error" in output: 115 | m_errors.inc() 116 | if "count" in output: 117 | m_results.observe(output["count"]) 118 | if "heartbeats" in output: 119 | m_heartbeats.observe(output["heartbeats"]) 120 | return output 121 | 122 | 123 | 124 | loogle = Loogle() 125 | 126 | # link formatting 127 | def locallink(query): 128 | return f"?q={urllib.parse.quote(query)}" 129 | 130 | def querylink(query): 131 | return f"https://loogle.lean-lang.org/?q={urllib.parse.quote(query)}" 132 | 133 | def doclink(hit): 134 | name = hit["name"] 135 | modpath = hit["module"].replace(".","/") 136 | return f"https://leanprover-community.github.io/mathlib4_docs/{urllib.parse.quote(modpath)}.html#{urllib.parse.quote(name)}" 137 | 138 | def zulHit(hit): 139 | return f"[{hit['name']}]({doclink(hit)})" 140 | 141 | def zulQuery(sugg): 142 | return f"[`{sugg}`]({querylink(sugg)})" 143 | 144 | class MyHandler(prometheus_client.MetricsHandler): 145 | 146 | def return404(self): 147 | self.send_response(404) 148 | self.send_header("Content-type", "text/plain") 149 | self.send_header("Access-Control-Allow-Origin", "*") 150 | self.send_header("Access-Control-Allow-Headers", "User-Agent, X-Loogle-Client") 151 | self.end_headers() 152 | self.wfile.write(b"Not found.\n") 153 | 154 | def return400(self): 155 | self.send_response(400) 156 | self.send_header("Content-type", "text/plain") 157 | self.send_header("Access-Control-Allow-Origin", "*") 158 | self.send_header("Access-Control-Allow-Headers", "User-Agent, X-Loogle-Client") 159 | self.end_headers() 160 | try: 161 | self.wfile.write(b"Invalid request.\n") 162 | except BrokenPipeError: 163 | # browsers seem to like to close this early 164 | pass 165 | 166 | def returnRedirect(self, url): 167 | self.send_response(302) 168 | self.send_header("Location", url) 169 | self.send_header("Access-Control-Allow-Origin", "*") 170 | self.send_header("Access-Control-Allow-Headers", "User-Agent, X-Loogle-Client") 171 | self.end_headers() 172 | 173 | def returnJSON(self, data): 174 | self.send_response(200) 175 | self.send_header("Content-type", "application/json") 176 | self.send_header("Access-Control-Allow-Origin", "*") 177 | self.send_header("Access-Control-Allow-Headers", "User-Agent, X-Loogle-Client") 178 | self.end_headers() 179 | try: 180 | self.wfile.write(bytes(json.dumps(data), "utf8")) 181 | except BrokenPipeError: 182 | pass 183 | 184 | def returnIcon(self): 185 | self.send_response(200) 186 | self.send_header("Content-type", "image/png") 187 | self.send_header("Access-Control-Allow-Origin", "*") 188 | self.send_header("Access-Control-Allow-Headers", "User-Agent, X-Loogle-Client") 189 | self.end_headers() 190 | try: 191 | self.wfile.write(icon) 192 | except BrokenPipeError: 193 | pass 194 | 195 | def do_OPTIONS(self): 196 | url = urllib.parse.urlparse(self.path) 197 | if url.path == "/json": 198 | self.send_response(200) 199 | self.send_header("Content-type", "application/json") 200 | self.send_header("Access-Control-Allow-Origin", "*") 201 | self.send_header("Access-Control-Allow-Methods", "GET") 202 | self.send_header("Access-Control-Allow-Headers", "User-Agent, X-Loogle-Client") 203 | self.end_headers() 204 | else: 205 | self.return404() 206 | 207 | def do_POST(self): 208 | try: 209 | url = urllib.parse.urlparse(self.path) 210 | if url.path != "/zulipbot": 211 | self.return404() 212 | return 213 | 214 | if self.headers.get_content_type() != 'application/json': 215 | self.send_response(400) 216 | self.end_headers() 217 | return 218 | m_client.labels("zulip").inc() 219 | 220 | length = int(self.headers.get('content-length')) 221 | message = json.loads(self.rfile.read(length)) 222 | 223 | m = re.search('@\*\*loogle\*\*[:,\?]?\s*(.*)$', message['data'], flags = re.MULTILINE) 224 | if m: 225 | query = m.group(1) 226 | else: 227 | query = message['data'].split('\n', 1)[0] 228 | 229 | result = loogle.query(query) 230 | 231 | if "error" in result: 232 | if "\n" in result['error']: 233 | reply = f"❗\n```\n{result['error']}\n```" 234 | else: 235 | reply = f"❗ {result['error']}" 236 | if "suggestions" in result: 237 | suggs = result["suggestions"] 238 | reply += "\n" 239 | 240 | if len(suggs) == 1: 241 | reply += f"Did you mean {zulQuery(suggs[0])}?" 242 | elif len(suggs) == 2: 243 | reply += f"Did you mean {zulQuery(suggs[0])} or {zulQuery(suggs[1])}?" 244 | else: 245 | reply += f"Did you mean {zulQuery(suggs[0])}, {zulQuery(suggs[1])}, or [something else]({querylink(query)})?" 246 | 247 | else: 248 | hits = result["hits"] 249 | if len(hits) == 0: 250 | reply = f"🤷 nothing found" 251 | elif len(hits) == 1: 252 | reply = f"🔍 {zulHit(hits[0])}" 253 | elif len(hits) == 2: 254 | reply = f"🔍 {zulHit(hits[0])}, {zulHit(hits[1])}" 255 | else: 256 | n = result["count"] - 2 257 | reply = f"🔍 {zulHit(hits[0])}, {zulHit(hits[1])}, and [{n} more]({querylink(query)})" 258 | self.returnJSON({ "content": reply }) 259 | except BrokenPipeError: 260 | # browsers seem to like to close this early 261 | pass 262 | 263 | def do_GET(self): 264 | try: 265 | query = "" 266 | result = {} 267 | url = urllib.parse.urlparse(self.path) 268 | want_json = False 269 | 270 | if url.path == "/loogle.png": 271 | self.returnIcon() 272 | return 273 | if url.path == "/json": 274 | want_json = True 275 | elif url.path == "/metrics": 276 | return super(MyHandler, self).do_GET() 277 | elif url.path != "/": 278 | self.return404() 279 | return 280 | 281 | url_query = url.query 282 | params = urllib.parse.parse_qs(url_query) 283 | if "q" in params and len(params["q"]) == 1: 284 | if want_json: 285 | if "lean4/" in self.headers.get("x-loogle-client", ""): 286 | m_client.labels("vscode-lean4").inc() 287 | elif "LeanSearchClient" in self.headers["user-agent"]: 288 | m_client.labels("LeanSearchClient").inc() 289 | elif "vscode" in self.headers["user-agent"]: 290 | m_client.labels("vscode-loogle").inc() 291 | elif "lean.nvim" in self.headers["user-agent"]: 292 | m_client.labels("nvim").inc() 293 | elif "lean+nvim" in self.headers["user-agent"]: 294 | m_client.labels("nvim").inc() 295 | else: 296 | m_client.labels("json").inc() 297 | else: 298 | m_client.labels("web").inc() 299 | 300 | query = params["q"][0].strip().removeprefix("#find ").strip() 301 | if query: 302 | if "\n" in query: 303 | self.return400() 304 | return 305 | result = loogle.query(query) 306 | 307 | if "lucky" in params: 308 | if "hits" in result and len(result["hits"]) >= 1: 309 | self.returnRedirect(doclink(result["hits"][0])) 310 | return 311 | 312 | 313 | if want_json: 314 | self.returnJSON(result) 315 | return 316 | 317 | self.send_response(200) 318 | self.send_header("Content-type", "text/html") 319 | self.end_headers() 320 | self.wfile.write(bytes(""" 321 | 322 | 323 | 324 | 325 | 326 | 330 | 334 | 338 | 342 | 372 | 373 | Loogle! 374 | 375 |
376 | 377 |
378 |

Loogle!

379 | """, "utf-8")) 380 | self.wfile.write(bytes(f""" 381 |
382 |
383 | 384 |
{html.escape(query)}
385 | 386 | 387 |
388 |
389 |
390 | """, "utf-8")) 391 | if "error" in result: 392 | self.wfile.write(bytes(f""" 393 |

Error

394 |
{html.escape(result['error'])}
395 | """, "utf-8")) 396 | if "header" in result: 397 | self.wfile.write(b""" 398 |

Result

399 | """) 400 | self.wfile.write(bytes(f""" 401 |

{html.escape(result['header'])}

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 |
  • {html.escape(name)} 📋 {html.escape(mod)}
    {html.escape(type)}
  • 413 | """, "utf-8")) 414 | self.wfile.write(b""" 415 |
416 | """) 417 | if "suggestions" in result: 418 | self.wfile.write(b'

Did you maybe mean

    ') 419 | for sugg in result["suggestions"]: 420 | link = locallink(sugg) 421 | self.wfile.write(bytes(f'
  • 🔍 {html.escape(sugg)}
  • ', "utf-8")) 422 | self.wfile.write(b'
') 423 | 424 | self.wfile.write(blurb) 425 | 426 | self.wfile.write(bytes(f""" 427 |

This is Loogle revision {rev1[:7]} serving mathlib revision {rev2[:7]}

428 | """, "utf-8")) 429 | 430 | self.wfile.write(b""" 431 |
432 | 461 | 462 | 463 | """) 464 | except BrokenPipeError: 465 | # browsers seem to like to close this early 466 | pass 467 | 468 | if __name__ == "__main__": 469 | webServer = HTTPServer((hostName, serverPort), MyHandler) 470 | print("Server started http://%s:%s" % (hostName, serverPort), flush=True) 471 | 472 | try: 473 | webServer.serve_forever() 474 | except KeyboardInterrupt: 475 | pass 476 | 477 | webServer.server_close() 478 | print("Server stopped.") 479 | --------------------------------------------------------------------------------