├── .gitignore ├── lean-toolchain ├── LeanSearchClientTest.lean ├── lake-manifest.json ├── lakefile.toml ├── .github └── workflows │ └── lean_action_ci.yml ├── LeanSearchClient.lean ├── LeanSearchClient ├── Basic.lean ├── LoogleSyntax.lean └── Syntax.lean ├── LeanSearchClientTest ├── LoogleExamples.lean ├── Examples.lean └── StateSearchExamples.lean ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /.lake 2 | -------------------------------------------------------------------------------- /lean-toolchain: -------------------------------------------------------------------------------- 1 | leanprover/lean4:v4.27.0-rc1 2 | -------------------------------------------------------------------------------- /LeanSearchClientTest.lean: -------------------------------------------------------------------------------- 1 | module 2 | 3 | public meta import LeanSearchClientTest.Examples 4 | -------------------------------------------------------------------------------- /lake-manifest.json: -------------------------------------------------------------------------------- 1 | {"version": "1.1.0", 2 | "packagesDir": ".lake/packages", 3 | "packages": [], 4 | "name": "LeanSearchClient", 5 | "lakeDir": ".lake"} 6 | -------------------------------------------------------------------------------- /lakefile.toml: -------------------------------------------------------------------------------- 1 | name = "LeanSearchClient" 2 | testDriver = "LeanSearchClientTest" 3 | defaultTargets = ["LeanSearchClient"] 4 | leanOptions = { experimental.module = true } 5 | 6 | [[lean_lib]] 7 | name = "LeanSearchClient" 8 | 9 | [[lean_lib]] 10 | name = "LeanSearchClientTest" 11 | -------------------------------------------------------------------------------- /.github/workflows/lean_action_ci.yml: -------------------------------------------------------------------------------- 1 | name: Lean Action CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: leanprover/lean-action@v1 15 | -------------------------------------------------------------------------------- /LeanSearchClient.lean: -------------------------------------------------------------------------------- 1 | -- This module serves as the root of the `LeanSearchClient` library. 2 | -- Import modules here that should be built as part of the library. 3 | module 4 | 5 | public meta import LeanSearchClient.Basic 6 | public meta import LeanSearchClient.Syntax 7 | public meta import LeanSearchClient.LoogleSyntax 8 | -------------------------------------------------------------------------------- /LeanSearchClient/Basic.lean: -------------------------------------------------------------------------------- 1 | module 2 | 3 | public meta import Lean.Data.Options 4 | 5 | public meta section 6 | 7 | register_option leansearch.queries : Nat := 8 | { defValue := 6 9 | descr := "Number of results requested from leansearch (default 6)" } 10 | 11 | register_option loogle.queries : Nat := 12 | { defValue := 6 13 | descr := "Number of results requested from loogle (default 6)" } 14 | 15 | register_option statesearch.queries : Nat := 16 | { defValue := 6 17 | descr := "Number of results requested from statesearch (default 6)" } 18 | 19 | register_option statesearch.revision : String := 20 | { defValue := s!"v{Lean.versionString}" 21 | descr := "Revision of LeanStateSearch to use" } 22 | 23 | register_option leansearchclient.useragent : String := 24 | { defValue := "LeanSearchClient" 25 | descr := "Username for leansearchclient" } 26 | 27 | register_option leansearchclient.backend : String := 28 | { defValue := "leansearch" 29 | descr := "The backend to use by default, currently only leansearch" } 30 | -------------------------------------------------------------------------------- /LeanSearchClientTest/LoogleExamples.lean: -------------------------------------------------------------------------------- 1 | module 2 | 3 | public meta import LeanSearchClient.LoogleSyntax 4 | 5 | public meta section 6 | 7 | /-! 8 | # Loogle Examples 9 | 10 | Examples of using the Loogle API. The search is triggered by the word at the end of the query. 11 | -/ 12 | 13 | -- #loogle List ?a → ?a 14 | 15 | -- example := #loogle List ?a → ?a 16 | 17 | 18 | -- set_option loogle.queries 1 19 | 20 | -- example : 3 ≤ 5 := by 21 | -- #loogle Nat.succ_le_succ 22 | -- sorry 23 | 24 | example : 3 ≤ 5 := by 25 | #loogle 26 | decide 27 | 28 | #loogle Dist.dist, edist 29 | -- example : 3 ≤ 5 := by 30 | -- #loogle 31 | -- decide 32 | 33 | -- /-! 34 | -- More examples to test comments do not interfere with the search or caching. 35 | -- -/ 36 | 37 | -- #loogle ?a * _ < ?a * _ ↔ _ 38 | -- #loogle ?a * _ < ?a * _ ↔ _ /- foo -/ 39 | -- #loogle ?a * _ < ?a * _ ↔ _ 40 | 41 | 42 | -- comment 43 | #loogle ?a * _ < ?a * _ ↔ _ 44 | 45 | /-- 46 | info: Loogle Search Results 47 | • #check Option.get! -- {α : Type u} [Inhabited α] : Option α → α 48 | Extracts the value from an `Option`, panicking on `none`. 49 | -/ 50 | #guard_msgs in 51 | #loogle Option ?a → ?a, "get!" 52 | 53 | /- hello -/ 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LeanSearchClient 2 | 3 | LeanSearchClient provides syntax for search using the [leansearch API](https://leansearch.net/) and the [LeanStateSearch](https://premise-search.com) API from within Lean. It allows you to search for Lean tactics and theorems using natural language. It also allows searches on [Loogle](https://loogle.lean-lang.org/json) from within Lean. 4 | 5 | We provide syntax to make a query and generate `TryThis` options to click or use a code action to use the results. The queries are of four forms: 6 | 7 | * `Command` syntax: `#search "search query"` as a command. 8 | * `Term` syntax: `#search "search query"` as a term. 9 | * `Tactic` syntax: `#search "search query"` as a tactic. 10 | * `Tactic` syntax based on state: `#search`. 11 | 12 | In all cases results are displayed in the Lean Infoview and clicking these replaces the query text. In the cases of a query for tactics only valid tactics are displayed. 13 | 14 | Which backend is used is determined by the `leansearchclient.backend` option. 15 | 16 | ## Examples 17 | 18 | The following are examples of using the leansearch API. The search is triggered when the sentence ends with a full stop (period) or a question mark. 19 | 20 | ### Query Command 21 | 22 | The common command for all backends: 23 | 24 | ```lean 25 | #search "If a natural number n is less than m, then the successor of n is less than the successor of m." 26 | ``` 27 | 28 | We also have commands for specific backend: 29 | 30 | ```lean 31 | #leansearch "If a natural number n is less than m, then the successor of n is less than the successor of m." 32 | ``` 33 | 34 | ### Query Term 35 | 36 | The general command: 37 | 38 | ```lean 39 | example := #search "If a natural number n is less than m, then the successor of n is less than the successor of m." 40 | ``` 41 | 42 | 43 | For `leansearch`: 44 | 45 | ```lean 46 | example := #leansearch "If a natural number n is less than m, then the successor of n is less than the successor of m." 47 | ``` 48 | 49 | ### Query Tactic 50 | 51 | Note that only valid tactics are displayed. 52 | 53 | The general command has two variants. With a string, calling LeanSearch: 54 | 55 | ```lean 56 | example : 3 ≤ 5 := by 57 | #search "If a natural number n is less than m, then the successor of n is less than the successor of m." 58 | sorry 59 | ``` 60 | 61 | Without a string, calling LeanStateSearch 62 | 63 | ```lean 64 | example : 3 ≤ 5 := by 65 | #search 66 | sorry 67 | ``` 68 | 69 | There are also specific commands for the different backends. 70 | 71 | For `leansearch`: 72 | 73 | ```lean 74 | example : 3 ≤ 5 := by 75 | #leansearch "If a natural number n is less than m, then the successor of n is less than the successor of m." 76 | sorry 77 | ``` 78 | 79 | For LeanStateSearch: 80 | 81 | ```lean 82 | example : 3 ≤ 5 := by 83 | #statesearch 84 | sorry 85 | ``` 86 | 87 | 88 | ## Loogle Search 89 | 90 | The `#loogle` command can also be used in all three modes. The syntax in this case is `#loogle ` as in the following examples. 91 | 92 | ```lean 93 | #loogle List ?a → ?a 94 | 95 | example := #loogle List ?a → ?a 96 | 97 | example : 3 ≤ 5 := by 98 | #loogle Nat.succ_le_succ 99 | sorry 100 | ``` -------------------------------------------------------------------------------- /LeanSearchClientTest/Examples.lean: -------------------------------------------------------------------------------- 1 | module 2 | 3 | public meta import LeanSearchClient.Syntax 4 | 5 | public meta section 6 | 7 | /-! 8 | # Lean Search Examples 9 | 10 | Examples of using the leansearch API. The search is triggered when the sentence ends with a full stop (period) or a question mark. 11 | -/ 12 | 13 | /-- 14 | warning: #leansearch query should be a string that ends with a `.` or `?`. 15 | Note this command sends your query to an external service at https://leansearch.net/. 16 | -/ 17 | #guard_msgs in 18 | #leansearch "If a natural number n is less than m, then the successor of n is less than the successor of m" 19 | 20 | /-- 21 | warning: #leansearch query should be a string that ends with a `.` or `?`. 22 | Note this command sends your query to an external service at https://leansearch.net/. 23 | -/ 24 | #guard_msgs in 25 | example := #leansearch "If a natural number n is less than m, then the successor of n is less than the successor of m" 26 | 27 | -- Sleep to avoid rate limiting (1 per 1 second) 28 | #eval IO.sleep 1500 29 | 30 | set_option leansearch.queries 1 31 | 32 | /-- 33 | info: From: Nat.le_of_succ_le_succ (type: ∀ {n m : Nat}, LE.le n.succ m.succ → LE.le n m) 34 | [apply] apply Nat.le_of_succ_le_succ 35 | [apply] have : ∀ {n m : Nat}, LE.le n.succ m.succ → LE.le n m := Nat.le_of_succ_le_succ 36 | --- 37 | warning: declaration uses 'sorry' 38 | -/ 39 | #guard_msgs in 40 | example : 3 ≤ 5 := by 41 | #leansearch "If a natural number n is less than m, then the successor of n is less than the successor of m." 42 | sorry 43 | 44 | -- Sleep to avoid rate limiting 45 | #eval IO.sleep 1500 46 | 47 | /-- 48 | warning: #leansearch query should be a string that ends with a `.` or `?`. 49 | Note this command sends your query to an external service at https://leansearch.net/. 50 | -/ 51 | #guard_msgs in 52 | example : 3 ≤ 5 := by 53 | #leansearch 54 | decide 55 | 56 | -- Sleep to avoid rate limiting 57 | #eval IO.sleep 1500 58 | 59 | /-! 60 | # Lean Search Examples using `#search` 61 | -/ 62 | set_option leansearchclient.backend "leansearch" 63 | 64 | /-- 65 | warning: #leansearch query should be a string that ends with a `.` or `?`. 66 | Note this command sends your query to an external service at https://leansearch.net/. 67 | -/ 68 | #guard_msgs in 69 | #search "If a natural number n is less than m, then the successor of n is less than the successor of m" 70 | 71 | /-- 72 | warning: #leansearch query should be a string that ends with a `.` or `?`. 73 | Note this command sends your query to an external service at https://leansearch.net/. 74 | -/ 75 | #guard_msgs in 76 | example := #search "If a natural number n is less than m, then the successor of n is less than the successor of m" 77 | 78 | -- Sleep to avoid rate limiting 79 | #eval IO.sleep 1500 80 | 81 | set_option leansearch.queries 1 82 | 83 | /-- 84 | info: From: Nat.le_of_succ_le_succ (type: ∀ {n m : Nat}, LE.le n.succ m.succ → LE.le n m) 85 | [apply] apply Nat.le_of_succ_le_succ 86 | [apply] have : ∀ {n m : Nat}, LE.le n.succ m.succ → LE.le n m := Nat.le_of_succ_le_succ 87 | --- 88 | warning: declaration uses 'sorry' 89 | -/ 90 | #guard_msgs in 91 | example : 3 ≤ 5 := by 92 | #search "If a natural number n is less than m, then the successor of n is less than the successor of m." 93 | sorry 94 | 95 | -- Sleep to avoid rate limiting 96 | #eval IO.sleep 1500 97 | 98 | /-- 99 | warning: #leansearch query should be a string that ends with a `.` or `?`. 100 | Note this command sends your query to an external service at https://leansearch.net/. 101 | --- 102 | warning: declaration uses 'sorry' 103 | -/ 104 | #guard_msgs in 105 | example : 3 ≤ 5 := #search 106 | 107 | -- Sleep to avoid rate limiting 108 | #eval IO.sleep 1500 109 | 110 | set_option leansearchclient.backend "magic" 111 | 112 | /-- error: Invalid backend magic, must be leansearch -/ 113 | #guard_msgs in 114 | #search "Every slice knot is ribbon." 115 | -------------------------------------------------------------------------------- /LeanSearchClientTest/StateSearchExamples.lean: -------------------------------------------------------------------------------- 1 | module 2 | 3 | public meta import LeanSearchClient.Syntax 4 | 5 | public meta section 6 | 7 | /-! 8 | # LeanStateSearch Examples 9 | 10 | Examples of using LeanStateSearch API. The search is triggered by the 11 | tactic `#statesearch`. 12 | -/ 13 | 14 | set_option statesearch.queries 1 -- set the number of results to 6 15 | set_option statesearch.revision "v4.22.0" -- set the revision to v4.16.0 16 | 17 | /-- 18 | info: Try these: 19 | • #check zero_lt_one -- ∀ {α : Type u_1} [inst : Zero α] [inst_1 : One α] [inst_2 : PartialOrder α] [ZeroLEOneClass α] [NeZero (1 : α)], (0 : α) < (1 : α) 20 | ⏎ 21 | --- 22 | warning: declaration uses 'sorry' 23 | -/ 24 | #guard_msgs in 25 | example : 0 < 1 := by 26 | #statesearch 27 | sorry 28 | 29 | set_option statesearch.queries 6 30 | 31 | /-- 32 | info: Try these: 33 | • #check zero_lt_one -- ∀ {α : Type u_1} [inst : Zero α] [inst_1 : One α] [inst_2 : PartialOrder α] [ZeroLEOneClass α] [NeZero (1 : α)], (0 : α) < (1 : α) 34 | ⏎ 35 | • #check one_pos -- ∀ {α : Type u_1} [inst : Zero α] [inst_1 : One α] [inst_2 : PartialOrder α] [ZeroLEOneClass α] [NeZero (1 : α)], (0 : α) < (1 : α) 36 | ⏎ 37 | • #check zero_lt_one' -- ∀ (α : Type u_1) [inst : Zero α] [inst_1 : One α] [inst_2 : PartialOrder α] [ZeroLEOneClass α] [NeZero (1 : α)], (0 : α) < (1 : α) 38 | ⏎ 39 | • #check ONote.zero_lt_one -- (0 : ONote) < (1 : ONote) 40 | ⏎ 41 | • #check ONote.lt_def -- ∀ {x y : ONote}, x < y ↔ x.repr < y.repr 42 | ⏎ 43 | • #check inv_lt_one_of_one_lt₀ -- ∀ {G₀ : Type u_3} [inst : GroupWithZero G₀] [inst_1 : PartialOrder G₀] [PosMulReflectLT G₀] {a : G₀} [ZeroLEOneClass G₀], (1 : G₀) < a → a⁻¹ < (1 : G₀) 44 | ⏎ 45 | --- 46 | warning: declaration uses 'sorry' 47 | -/ 48 | #guard_msgs in 49 | example : 0 < 1 := by 50 | #statesearch 51 | sorry 52 | 53 | set_option statesearch.revision "v4.15.0" 54 | 55 | /-- error: error: "Invalid parameter value" 56 | description: "Lean State Search does not support the specified revision" 57 | -/ 58 | #guard_msgs in 59 | example : 0 ≤ 1 := by 60 | #statesearch 61 | 62 | /-! 63 | Tests using `search` with `statesearch` as the backend. 64 | -/ 65 | set_option statesearch.queries 1 -- set the number of results to 6 66 | set_option statesearch.revision "v4.22.0" -- set the revision to v4.16.0 67 | 68 | 69 | /-- 70 | info: Try these: 71 | • #check zero_lt_one -- ∀ {α : Type u_1} [inst : Zero α] [inst_1 : One α] [inst_2 : PartialOrder α] [ZeroLEOneClass α] [NeZero (1 : α)], (0 : α) < (1 : α) 72 | ⏎ 73 | --- 74 | warning: declaration uses 'sorry' 75 | -/ 76 | #guard_msgs in 77 | example : 0 < 1 := by 78 | #search 79 | sorry 80 | 81 | set_option statesearch.queries 6 82 | 83 | /-- 84 | info: Try these: 85 | • #check zero_lt_one -- ∀ {α : Type u_1} [inst : Zero α] [inst_1 : One α] [inst_2 : PartialOrder α] [ZeroLEOneClass α] [NeZero (1 : α)], (0 : α) < (1 : α) 86 | ⏎ 87 | • #check one_pos -- ∀ {α : Type u_1} [inst : Zero α] [inst_1 : One α] [inst_2 : PartialOrder α] [ZeroLEOneClass α] [NeZero (1 : α)], (0 : α) < (1 : α) 88 | ⏎ 89 | • #check zero_lt_one' -- ∀ (α : Type u_1) [inst : Zero α] [inst_1 : One α] [inst_2 : PartialOrder α] [ZeroLEOneClass α] [NeZero (1 : α)], (0 : α) < (1 : α) 90 | ⏎ 91 | • #check ONote.zero_lt_one -- (0 : ONote) < (1 : ONote) 92 | ⏎ 93 | • #check ONote.lt_def -- ∀ {x y : ONote}, x < y ↔ x.repr < y.repr 94 | ⏎ 95 | • #check inv_lt_one_of_one_lt₀ -- ∀ {G₀ : Type u_3} [inst : GroupWithZero G₀] [inst_1 : PartialOrder G₀] [PosMulReflectLT G₀] {a : G₀} [ZeroLEOneClass G₀], (1 : G₀) < a → a⁻¹ < (1 : G₀) 96 | ⏎ 97 | --- 98 | warning: declaration uses 'sorry' 99 | -/ 100 | #guard_msgs in 101 | example : 0 < 1 := by 102 | #search 103 | sorry 104 | 105 | set_option statesearch.revision "v4.15.0" 106 | 107 | /-- error: error: "Invalid parameter value" 108 | description: "Lean State Search does not support the specified revision" 109 | -/ 110 | #guard_msgs in 111 | example : 0 ≤ 1 := by 112 | #search 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LeanSearchClient/LoogleSyntax.lean: -------------------------------------------------------------------------------- 1 | /- 2 | Copyright (c) 2024 Siddhartha Gadgil. All rights reserved. 3 | Released under Apache 2.0 license as described in the file LICENSE. 4 | Authors: Siddhartha Gadgil 5 | -/ 6 | module 7 | 8 | public meta import Lean.Elab.Tactic.Meta 9 | public meta import Lean.Parser.Basic 10 | public meta import Lean.Meta.Tactic.TryThis 11 | public meta import LeanSearchClient.Basic 12 | public meta import LeanSearchClient.Syntax 13 | 14 | public meta section 15 | 16 | /-! 17 | # LeanSearchClient 18 | 19 | In this file, we provide syntax for search using the [leansearch API](https://leansearch.net/). 20 | from within Lean. It allows you to search for Lean tactics and theorems using natural language. 21 | 22 | We provide syntax to make a query and generate `TryThis` options to click or 23 | use a code action to use the results. 24 | 25 | The queries are of three forms. For leansearch these are: 26 | 27 | * `Command` syntax: `#leansearch "search query"` as a command. 28 | * `Term` syntax: `#leansearch "search query"` as a term. 29 | * `Tactic` syntax: `#leansearch "search query"` as a tactic. 30 | 31 | In all cases results are displayed in the Lean Infoview and clicking these replaces the query text. 32 | In the cases of a query for tactics only valid tactics are displayed. 33 | -/ 34 | 35 | namespace LeanSearchClient 36 | 37 | open Lean Meta Elab Tactic Parser Term 38 | 39 | structure LoogleMatch where 40 | name : String 41 | type : String 42 | doc? : Option String 43 | deriving Inhabited, Repr 44 | 45 | inductive LoogleResult where 46 | | empty : LoogleResult 47 | | success : Array SearchResult → LoogleResult 48 | | failure (error : String) (suggestions: Option <| List String) : LoogleResult 49 | deriving Inhabited, Repr 50 | 51 | initialize loogleCache : 52 | IO.Ref (Std.HashMap (String × Nat) LoogleResult) ← IO.mkRef {} 53 | 54 | def getLoogleQueryJson (s : String) (num_results : Nat := 6) : 55 | CoreM <| LoogleResult:= do 56 | let s := s.splitOn "/-" |>.getD 0 s |>.trimAscii.toString 57 | let s := s.replace "\n" " " 58 | let cache ← loogleCache.get 59 | match cache.get? (s, num_results) with 60 | | some r => return r 61 | | none => do 62 | let apiUrl := (← IO.getEnv "LEANSEARCHCLIENT_LOOGLE_API_URL").getD "https://loogle.lean-lang.org/json" 63 | let s' := System.Uri.escapeUri s 64 | if s.trimAscii.toString == "" then 65 | return LoogleResult.empty 66 | let q := apiUrl ++ s!"?q={s'}" 67 | let out ← IO.Process.output {cmd := "curl", args := #["-X", "GET", "--user-agent", ← useragent, q]} 68 | match Json.parse out.stdout with 69 | | Except.error _ => 70 | IO.throwServerError s!"Could not contact Loogle server" 71 | | Except.ok js => 72 | let result? := js.getObjValAs? Json "hits" |>.toOption 73 | let result? := result?.filter fun js => js != Json.null 74 | match result? with 75 | | some result => do 76 | match result.getArr? with 77 | | Except.ok arr => 78 | let arr := arr[0:num_results] |>.toArray 79 | let xs : Array SearchResult ← 80 | arr.mapM fun js => do 81 | let doc? := js.getObjValAs? String "doc" |>.toOption 82 | let name? := js.getObjValAs? String "name" 83 | let type? := js.getObjValAs? String "type" 84 | match name?, type? with 85 | | Except.ok name, Except.ok type => 86 | pure <| {name := name, type? := some type, docString? := doc?, doc_url? := none, kind? := none} 87 | | _, _ => 88 | IO.throwServerError s!"Could not obtain name and type from {js}" 89 | loogleCache.modify fun m => m.insert (s, num_results) (LoogleResult.success xs) 90 | return LoogleResult.success xs 91 | | Except.error e => IO.throwServerError s!"Could not obtain array from {js}; error: {e}, query :{s'}, hits: {result}" 92 | | _ => 93 | let error? := js.getObjValAs? String "error" 94 | match error? with 95 | | Except.ok error => 96 | let suggestions? := 97 | js.getObjValAs? (List String) "suggestions" |>.toOption 98 | loogleCache.modify fun m => m.insert (s, num_results) (LoogleResult.failure error suggestions?) 99 | pure <| LoogleResult.failure error suggestions? 100 | | _ => 101 | IO.throwServerError s!"Could not obtain hits or error from {js}" 102 | 103 | -- #eval getLoogleQueryJson "List" 104 | 105 | def loogleUsage : String := 106 | "Loogle Usage 107 | 108 | Loogle finds definitions and lemmas in various ways: 109 | 110 | By constant: 111 | 🔍 Real.sin 112 | finds all lemmas whose statement somehow mentions the sine function. 113 | 114 | By lemma name substring: 115 | 🔍 \"differ\" 116 | finds all lemmas that have \"differ\" somewhere in their lemma name. 117 | 118 | By subexpression: 119 | 🔍 _ * (_ ^ _) 120 | finds all lemmas whose statements somewhere include a product where the second argument is raised to some power. 121 | 122 | The pattern can also be non-linear, as in 123 | 🔍 Real.sqrt ?a * Real.sqrt ?a 124 | 125 | If the pattern has parameters, they are matched in any order. Both of these will find List.map: 126 | 🔍 (?a -> ?b) -> List ?a -> List ?b 127 | 🔍 List ?a -> (?a -> ?b) -> List ?b 128 | 129 | By main conclusion: 130 | 🔍 |- tsum _ = _ * tsum _ 131 | finds all lemmas where the conclusion (the subexpression to the right of all → and ∀) has the given shape. 132 | 133 | As before, if the pattern has parameters, they are matched against the hypotheses of the lemma in any order; for example, 134 | 🔍 |- _ < _ → tsum _ < tsum _ 135 | will find tsum_lt_tsum even though the hypothesis f i < g i is not the last. 136 | 137 | If you pass more than one such search filter, separated by commas Loogle will return lemmas which match all of them. The search 138 | 🔍 Real.sin, \"two\", tsum, _ * _, _ ^ _, |- _ < _ → _ 139 | woould find all lemmas which mention the constants Real.sin and tsum, have \"two\" as a substring of the lemma name, include a product and a power somewhere in the type, and have a hypothesis of the form _ < _ (if there were any such lemmas). Metavariables (?a) are assigned independently in each filter." 140 | 141 | open Lean.Parser 142 | def unicode_turnstile := nonReservedSymbol "⊢ " 143 | def ascii_turnstile := nonReservedSymbol "|- " 144 | 145 | /-- The turnstyle uesd bin `#find`, unicode or ascii allowed -/ 146 | syntax turnstyle := patternIgnore(unicode_turnstile <|> ascii_turnstile) 147 | 148 | /-- a single `#find` filter. The `term` can also be an ident or a strlit, 149 | these are distinguished in `parseFindFilters` -/ 150 | syntax loogle_filter := (turnstyle term) <|> term 151 | 152 | /-- The argument to `#find`, a list of filters -/ 153 | syntax loogle_filters := loogle_filter,* 154 | 155 | open Command 156 | /-- 157 | Search [Loogle](https://loogle.lean-lang.org/json) from within Lean. This can be used as a command, term or tactic as in the following examples. In the case of a tactic, only valid tactics are displayed. 158 | 159 | 160 | ```lean 161 | #loogle List ?a → ?a 162 | 163 | example := #loogle List ?a → ?a 164 | 165 | example : 3 ≤ 5 := by 166 | #loogle Nat.succ_le_succ 167 | sorry 168 | 169 | ``` 170 | 171 | ## Loogle Usage 172 | 173 | Loogle finds definitions and lemmas in various ways: 174 | 175 | By constant: 176 | 🔍 Real.sin 177 | finds all lemmas whose statement somehow mentions the sine function. 178 | 179 | By lemma name substring: 180 | 🔍 \"differ\" 181 | finds all lemmas that have \"differ\" somewhere in their lemma name. 182 | 183 | By subexpression: 184 | 🔍 _ * (_ ^ _) 185 | finds all lemmas whose statements somewhere include a product where the second argument is raised to some power. 186 | 187 | The pattern can also be non-linear, as in 188 | 🔍 Real.sqrt ?a * Real.sqrt ?a 189 | 190 | If the pattern has parameters, they are matched in any order. Both of these will find List.map: 191 | 🔍 (?a -> ?b) -> List ?a -> List ?b 192 | 🔍 List ?a -> (?a -> ?b) -> List ?b 193 | 194 | By main conclusion: 195 | 🔍 |- tsum _ = _ * tsum _ 196 | finds all lemmas where the conclusion (the subexpression to the right of all → and ∀) has the given shape. 197 | 198 | As before, if the pattern has parameters, they are matched against the hypotheses of the lemma in any order; for example, 199 | 🔍 |- _ < _ → tsum _ < tsum _ 200 | will find tsum_lt_tsum even though the hypothesis f i < g i is not the last. 201 | 202 | If you pass more than one such search filter, separated by commas Loogle will return lemmas which match all of them. The search 203 | 🔍 Real.sin, \"two\", tsum, _ * _, _ ^ _, |- _ < _ → _ 204 | woould find all lemmas which mention the constants Real.sin and tsum, have \"two\" as a substring of the lemma name, include a product and a power somewhere in the type, and have a hypothesis of the form _ < _ (if there were any such lemmas). Metavariables (?a) are assigned independently in each filter. 205 | 206 | You can modify the Loogle server URL by setting the `LEANSEARCHCLIENT_LOOGLE_API_URL` environment variable. 207 | -/ 208 | syntax (name := loogle_cmd) "#loogle" loogle_filters : command 209 | @[command_elab loogle_cmd] def loogleCmdImpl : CommandElab := fun stx => 210 | Command.liftTermElabM do 211 | match stx with 212 | | `(command| #loogle $args:loogle_filters) => 213 | let s := (← PrettyPrinter.ppCategory ``loogle_filters args).pretty 214 | let result ← getLoogleQueryJson s 215 | match result with 216 | | LoogleResult.empty => 217 | logInfo loogleUsage 218 | | LoogleResult.success xs => 219 | let suggestions := xs.map SearchResult.toCommandSuggestion 220 | if suggestions.isEmpty then 221 | logWarning "Loogle search returned no results" 222 | logInfo loogleUsage 223 | else 224 | TryThis.addSuggestions stx suggestions (header := s!"Loogle Search Results") 225 | | LoogleResult.failure error suggestions? => 226 | logWarning s!"Loogle search failed with error: {error}" 227 | logInfo loogleUsage 228 | match suggestions? with 229 | | some suggestions => 230 | let suggestions : List TryThis.Suggestion := 231 | suggestions.map fun s => 232 | {suggestion := .string s!"#loogle {s}"} 233 | unless suggestions.isEmpty do 234 | TryThis.addSuggestions stx suggestions.toArray (header := s!"Did you maybe mean") 235 | | none => pure () 236 | | _ => throwUnsupportedSyntax 237 | 238 | @[inherit_doc loogle_cmd] 239 | syntax (name := just_loogle_cmd)(priority := low) "#loogle" loogle_filters : command 240 | @[command_elab just_loogle_cmd] def justLoogleCmdImpl : CommandElab := fun _ => return 241 | 242 | 243 | @[inherit_doc loogle_cmd] 244 | syntax (name := loogle_term) "#loogle" loogle_filters : term 245 | @[term_elab loogle_term] def loogleTermImpl : TermElab := 246 | fun stx expectedType? => do 247 | match stx with 248 | | `(#loogle $args) => 249 | let s := (← PrettyPrinter.ppCategory ``loogle_filters args).pretty 250 | let result ← getLoogleQueryJson s 251 | match result with 252 | | LoogleResult.empty => 253 | logInfo loogleUsage 254 | | LoogleResult.success xs => 255 | let suggestions := xs.map SearchResult.toTermSuggestion 256 | if suggestions.isEmpty then 257 | logWarning "Loogle search returned no results" 258 | logInfo loogleUsage 259 | else 260 | TryThis.addSuggestions stx suggestions (header := s!"Loogle Search Results") 261 | 262 | | LoogleResult.failure error suggestions? => 263 | logWarning s!"Loogle search failed with error: {error}" 264 | logInfo loogleUsage 265 | match suggestions? with 266 | | some suggestions => 267 | let suggestions : List TryThis.Suggestion := 268 | suggestions.map fun s => 269 | let s := s.replace "\"" "\\\"" 270 | {suggestion := .string s!"#loogle \"{s}\""} 271 | unless suggestions.isEmpty do 272 | TryThis.addSuggestions stx suggestions.toArray (header := s!"Did you maybe mean") 273 | | none => pure () 274 | defaultTerm expectedType? 275 | | _ => throwUnsupportedSyntax 276 | 277 | @[inherit_doc loogle_cmd] 278 | syntax (name := loogle_tactic) 279 | withPosition("#loogle" (ppSpace colGt (loogle_filters))) : tactic 280 | @[tactic loogle_tactic] def loogleTacticImpl : Tactic := 281 | fun stx => do 282 | match stx with 283 | | `(tactic|#loogle $args) => 284 | let s := (← PrettyPrinter.ppCategory ``loogle_filters args).pretty 285 | let result ← getLoogleQueryJson s 286 | match result with 287 | | LoogleResult.empty => 288 | logInfo loogleUsage 289 | | LoogleResult.success xs => do 290 | let suggestionGroups := xs.map fun sr => 291 | (sr.name, sr.toTacticSuggestions) 292 | for (name, sg) in suggestionGroups do 293 | let sg ← sg.filterM fun s => 294 | let sugTxt := s.suggestion 295 | match sugTxt with 296 | | .string s => do 297 | let stx? := runParserCategory (← getEnv) `tactic s 298 | match stx? with 299 | | Except.ok stx => 300 | let n? ← checkTactic (← getMainTarget) stx 301 | return n?.isSome 302 | | Except.error _ => 303 | pure false 304 | | _ => pure false 305 | unless sg.isEmpty do 306 | TryThis.addSuggestions stx sg (header := s!"From: {name}") 307 | | LoogleResult.failure error suggestions? => 308 | logWarning s!"Loogle search failed with error: {error}" 309 | logInfo loogleUsage 310 | match suggestions? with 311 | | some suggestions => 312 | let suggestions : List TryThis.Suggestion := 313 | suggestions.map fun s => 314 | {suggestion := .string s!"#loogle \"{s}\""} 315 | unless suggestions.isEmpty do 316 | TryThis.addSuggestions stx suggestions.toArray (header := s!"Did you maybe mean") 317 | | none => pure () 318 | | _ => throwUnsupportedSyntax 319 | 320 | syntax (name := just_loogle_tactic)(priority := low) "#loogle" : tactic 321 | @[tactic just_loogle_tactic] def justLoogleTacticImpl : Tactic := 322 | fun _ => do 323 | logWarning loogleUsage 324 | 325 | example : 3 ≤ 5 := by 326 | -- #loogle Nat.succ_le_succ 327 | decide 328 | 329 | -- example := #loogle List ?a → ?b 330 | 331 | end LeanSearchClient 332 | 333 | -- #loogle "sin", Real → Real, |- Real 334 | -------------------------------------------------------------------------------- /LeanSearchClient/Syntax.lean: -------------------------------------------------------------------------------- 1 | /- 2 | Copyright (c) 2024 Siddhartha Gadgil. All rights reserved. 3 | Released under Apache 2.0 license as described in the file LICENSE. 4 | Authors: Siddhartha Gadgil 5 | -/ 6 | module 7 | 8 | public meta import Lean.Elab.Tactic.Meta 9 | public meta import Lean.Meta.Tactic.TryThis 10 | public meta import LeanSearchClient.Basic 11 | public meta import Lean.Server.Utils 12 | public meta import Lean.Elab.Command 13 | 14 | public meta section 15 | 16 | /-! 17 | # LeanSearchClient 18 | 19 | In this file, we provide syntax for search using the [leansearch API](https://leansearch.net/). 20 | from within Lean. It allows you to search for Lean tactics and theorems using natural language. 21 | 22 | We provide syntax to make a query and generate `TryThis` options to click or 23 | use a code action to use the results. 24 | 25 | The queries are of three forms. For leansearch these are: 26 | 27 | * `Command` syntax: `#leansearch "search query"` as a command. 28 | * `Term` syntax: `#leansearch "search query"` as a term. 29 | * `Tactic` syntax: `#leansearch "search query"` as a tactic. 30 | 31 | In all cases results are displayed in the Lean Infoview and clicking these replaces the query text. 32 | In the cases of a query for tactics only valid tactics are displayed. 33 | -/ 34 | 35 | namespace LeanSearchClient 36 | 37 | open Lean Meta Elab Tactic Parser Term 38 | 39 | def useragent : CoreM String := 40 | return leansearchclient.useragent.get (← getOptions) 41 | 42 | initialize leanSearchCache : 43 | IO.Ref (Std.HashMap (String × Nat) (Array Json)) ← IO.mkRef {} 44 | 45 | initialize stateSearchCache : 46 | IO.Ref (Std.HashMap (String × Nat × String) (Array Json)) ← IO.mkRef {} 47 | 48 | def getLeanSearchQueryJson (s : String) (num_results : Nat := 6) : CoreM <| Array Json := do 49 | let cache ← leanSearchCache.get 50 | match cache.get? (s, num_results) with 51 | | some jsArr => return jsArr 52 | | none => do 53 | let apiUrl := (← IO.getEnv "LEANSEARCHCLIENT_LEANSEARCH_API_URL").getD "https://leansearch.net/search" 54 | -- let q := apiUrl ++ s!"?query={s'}&num_results={num_results}" 55 | let js := Json.mkObj [("query", Json.arr #[toJson s]), ("num_results", num_results)] 56 | let out ← IO.Process.output {cmd := "curl", args := #["-X", "POST", apiUrl, "--user-agent", ← useragent, "-H", "accept: application/json", "-H", "Content-Type: application/json", "--data", js.pretty]} 57 | let js ← match Json.parse out.stdout with 58 | | Except.ok js => pure js 59 | | Except.error e => IO.throwServerError s!"Could not parse response from LeanSearch server, error: {e}" 60 | match js.getArr? with 61 | | Except.ok jsArr => do 62 | match jsArr[0]!.getArr? with 63 | | Except.ok jsArr => 64 | leanSearchCache.modify fun m => m.insert (s, num_results) jsArr 65 | return jsArr 66 | | Except.error e => IO.throwServerError s!"Could not obtain inner array from {js}; error: {e}" 67 | | Except.error e => 68 | IO.throwServerError s!"Could not obtain outer array from {js}; error: {e}" 69 | 70 | def getStateSearchQueryJson (s : String) (num_results : Nat := 6) (rev : String) : CoreM <| Array Json := do 71 | let cache ← stateSearchCache.get 72 | match cache.get? (s, num_results, rev) with 73 | | .some jsArr => return jsArr 74 | | none => do 75 | let apiUrl := (← IO.getEnv "LEANSEARCHCLIENT_LEANSTATESEARCH_API_URL").getD "https://premise-search.com/api/search" 76 | let s' := System.Uri.escapeUri s 77 | let q := apiUrl ++ s!"?query={s'}&results={num_results}&rev={rev}" 78 | let out ← IO.Process.output {cmd := "curl", args := #["-X", "GET", "--user-agent", ← useragent, q]} 79 | let js ← match Json.parse out.stdout |>.toOption with 80 | | some js => pure js 81 | | none => IO.throwServerError s!"Could not contact LeanStateSearch server" 82 | match js.getArr? with 83 | | Except.ok jsArr => do 84 | stateSearchCache.modify fun m => m.insert (s, num_results, rev) jsArr 85 | return jsArr 86 | | Except.error e => 87 | let .ok err := js.getObjVal? "error" 88 | | IO.throwServerError s!"{e}" 89 | let .ok schema := js.getObjVal? "schema" 90 | | IO.throwServerError s!"{e}" 91 | let .ok desc := schema.getObjVal? "description" 92 | | IO.throwServerError s!"{e}" 93 | IO.throwServerError s!"error: {err}\ndescription: {desc}" 94 | 95 | structure SearchResult where 96 | name : String 97 | type? : Option String 98 | docString? : Option String 99 | doc_url? : Option String 100 | kind? : Option String 101 | deriving Repr 102 | 103 | namespace SearchResult 104 | 105 | def ofLeanSearchJson? (js : Json) : Option SearchResult := 106 | match js.getObjVal? "result" with 107 | | Except.ok js => 108 | match js.getObjValAs? (List String) "name" with 109 | | Except.ok nameList => 110 | let name := nameList.foldl (init := "") fun acc s => 111 | if acc == "" then s else acc ++ "." ++ s 112 | let type? := js.getObjValAs? String "type" |>.toOption 113 | let doc? := js.getObjValAs? String "docstring" |>.toOption 114 | let doc? := doc?.filter fun s => s != "" 115 | let docurl? := js.getObjValAs? String "doc_url" |>.toOption 116 | let kind? := js.getObjValAs? String "kind" |>.toOption 117 | some {name := name, type? := type?, docString? := doc?, doc_url? := docurl?, kind? := kind?} 118 | | _ => 119 | none 120 | | _ => 121 | none 122 | 123 | def ofLoogleJson? (js : Json) : Option SearchResult := 124 | match js.getObjValAs? String "name" with 125 | | Except.ok name => 126 | let type? := js.getObjValAs? String "type" |>.toOption 127 | let doc? := js.getObjValAs? String "doc" |>.toOption 128 | let doc? := doc?.filter fun s => s != "" 129 | some {name := name, type? := type?, docString? := doc?, doc_url? := none, kind? := none} 130 | | _ => none 131 | 132 | def ofStateSearchJson? (js : Json) : Option SearchResult := 133 | match js.getObjValAs? String "name" with 134 | | Except.ok name => 135 | let type? := js.getObjValAs? String "formal_type" |>.toOption 136 | let doc? := js.getObjValAs? String "doc" |>.toOption 137 | let doc? := doc?.filter fun s => s != "" 138 | let kind? := js.getObjValAs? String "kind" |>.toOption 139 | some {name := name, type? := type?, docString? := doc?, doc_url? := none, kind? := kind?} 140 | | _ => none 141 | 142 | def toCommandSuggestion (sr : SearchResult) : TryThis.Suggestion := 143 | let data := match sr.docString? with 144 | | some doc => s!"{doc}\n" 145 | | none => "" 146 | {suggestion := s!"#check {sr.name}", postInfo? := sr.type?.map fun s => s!" -- {s}" ++ s!"\n{data}"} 147 | 148 | def toTermSuggestion (sr : SearchResult) : TryThis.Suggestion := 149 | match sr.type? with 150 | | some type => {suggestion := sr.name, postInfo? := some s!" (type: {type})"} 151 | | none => {suggestion := sr.name} 152 | 153 | def toTacticSuggestions (sr : SearchResult) : Array TryThis.Suggestion := 154 | match sr.type? with 155 | | some type => #[{suggestion := s!"apply {sr.name}"}, 156 | {suggestion := s!"have : {type} := {sr.name}"}, 157 | {suggestion := s!"rw [{sr.name}]"}, 158 | {suggestion := s!"rw [← {sr.name}]" }] 159 | | none => #[] 160 | 161 | end SearchResult 162 | 163 | 164 | def queryLeanSearch (s : String) (num_results : Nat) : 165 | MetaM <| Array SearchResult := do 166 | let jsArr ← getLeanSearchQueryJson s num_results 167 | return jsArr.filterMap SearchResult.ofLeanSearchJson? 168 | 169 | def queryStateSearch (s : String) (num_results : Nat) (rev : String): 170 | MetaM <| Array SearchResult := do 171 | let jsArr ← getStateSearchQueryJson s num_results rev 172 | return jsArr.filterMap SearchResult.ofStateSearchJson? 173 | 174 | def defaultTerm (expectedType? : Option Expr) : MetaM Expr := do 175 | match expectedType? with 176 | | some type => 177 | if !type.hasExprMVar then 178 | mkAppM ``sorryAx #[type, mkConst ``false] 179 | else 180 | return mkConst ``True.intro 181 | | none => return mkConst ``True.intro 182 | 183 | def checkTactic (target : Expr) (tac : Syntax) : 184 | TermElabM (Option Nat) := 185 | withoutModifyingState do 186 | try 187 | let goal ← mkFreshExprMVar target 188 | let (goals, _) ← 189 | withoutErrToSorry do 190 | Elab.runTactic goal.mvarId! tac 191 | (← read) (← get) 192 | return some goals.length 193 | catch _ => 194 | return none 195 | 196 | structure SearchServer where 197 | name : String 198 | url : String 199 | cmd: String 200 | query : String → Nat → MetaM (Array SearchResult) 201 | queryNum : CoreM Nat 202 | 203 | def leanSearchServer : SearchServer := 204 | {name := "LeanSearch", cmd := "#leansearch", url := "https://leansearch.net/", 205 | query := queryLeanSearch, queryNum := return leansearch.queries.get (← getOptions)} 206 | 207 | instance : Inhabited SearchServer := ⟨leanSearchServer⟩ 208 | 209 | namespace SearchServer 210 | 211 | def getCommandSuggestions (ss : SearchServer) (s : String) (num_results : Nat) : 212 | MetaM (Array TryThis.Suggestion) := do 213 | let suggestions ← ss.query s num_results 214 | return suggestions.map SearchResult.toCommandSuggestion 215 | 216 | def getTermSuggestions (ss : SearchServer) (s : String) (num_results : Nat) : 217 | MetaM (Array TryThis.Suggestion) := do 218 | let suggestions ← ss.query s num_results 219 | return suggestions.map SearchResult.toTermSuggestion 220 | 221 | def getTacticSuggestionGroups (ss : SearchServer) (s : String) (num_results : Nat) : 222 | MetaM (Array (String × Array TryThis.Suggestion)) := do 223 | let suggestions ← ss.query s num_results 224 | return suggestions.map fun sr => 225 | let fullName := match sr.type? with 226 | | some type => s!"{sr.name} (type: {type})" 227 | | none => sr.name 228 | (fullName, sr.toTacticSuggestions) 229 | 230 | def incompleteSearchQuery (ss : SearchServer) : String := 231 | s!"{ss.cmd} query should be a string that ends with a `.` or `?`.\n\ 232 | Note this command sends your query to an external service at {ss.url}." 233 | 234 | open Command 235 | def searchCommandSuggestions (ss: SearchServer) (stx: Syntax) (s: TSyntax `str) : CommandElabM Unit := Command.liftTermElabM do 236 | let s := s.getString 237 | if s.endsWith "." || s.endsWith "?" then 238 | let suggestions ← ss.getCommandSuggestions s (← ss.queryNum) 239 | TryThis.addSuggestions stx suggestions (header := s!"{ss.name} Search Results") 240 | else 241 | logWarning <| ss.incompleteSearchQuery 242 | 243 | def searchTermSuggestions (ss: SearchServer) (stx: Syntax) 244 | (s: TSyntax `str) : TermElabM Unit := do 245 | let s := s.getString 246 | if s.endsWith "." || s.endsWith "?" then 247 | let suggestions ← ss.getTermSuggestions s (← ss.queryNum) 248 | TryThis.addSuggestions stx suggestions (header := s!"{ss.name} Search Results") 249 | else 250 | logWarning <| ss.incompleteSearchQuery 251 | 252 | def searchTacticSuggestions (ss : SearchServer) (stx : Syntax) (s : TSyntax `str) : TacticM Unit := do 253 | let s := s.getString 254 | if s.endsWith "." || s.endsWith "?" then 255 | let target ← getMainTarget 256 | let suggestionGroups ← 257 | ss.getTacticSuggestionGroups s (← ss.queryNum) 258 | for (name, sg) in suggestionGroups do 259 | let sg ← sg.filterM fun s => 260 | let sugTxt := s.suggestion 261 | match sugTxt with 262 | | .string s => do 263 | let stx? := runParserCategory (← getEnv) `tactic s 264 | match stx? with 265 | | Except.ok stx => 266 | let n? ← checkTactic target stx 267 | return n?.isSome 268 | | Except.error _ => 269 | pure false 270 | | _ => pure false 271 | unless sg.isEmpty do 272 | TryThis.addSuggestions stx sg (header := s!"From: {name}") 273 | else 274 | logWarning <| ss.incompleteSearchQuery 275 | 276 | end SearchServer 277 | 278 | open Command 279 | /-- Search [LeanSearch](https://leansearch.net/) from within Lean. 280 | Queries should be a string that ends with a `.` or `?`. This works as a command, as a term 281 | and as a tactic as in the following examples. In tactic mode, only valid tactics are displayed. 282 | 283 | ```lean 284 | #leansearch "If a natural number n is less than m, then the successor of n is less than the successor of m." 285 | 286 | example := #leansearch "If a natural number n is less than m, then the successor of n is less than the successor of m." 287 | 288 | example : 3 ≤ 5 := by 289 | #leansearch "If a natural number n is less than m, then the successor of n is less than the successor of m." 290 | sorry 291 | ``` 292 | 293 | You can modify the LeanSearch URL by setting the `LEANSEARCHCLIENT_LEANSEARCH_API_URL` environment variable. 294 | -/ 295 | syntax (name := leansearch_search_cmd) "#leansearch" (str)? : command 296 | 297 | @[command_elab leansearch_search_cmd] def leanSearchCommandImpl : CommandElab := 298 | fun stx => 299 | match stx with 300 | | `(command| #leansearch $s) => do 301 | leanSearchServer.searchCommandSuggestions stx s 302 | | `(command| #leansearch) => do 303 | logWarning leanSearchServer.incompleteSearchQuery 304 | | _ => throwUnsupportedSyntax 305 | 306 | /-- Search from within Lean, depending on the option `leansearchclient.backend` (currently only leansearch). 307 | Queries should be a string that ends with a `.` or `?`. This works as a command, as a term 308 | and as a tactic as in the following examples. In tactic mode, only valid tactics are displayed. 309 | 310 | ```lean 311 | #search "If a natural number n is less than m, then the successor of n is less than the successor of m." 312 | 313 | example := #search "If a natural number n is less than m, then the successor of n is less than the successor of m." 314 | 315 | example : 3 ≤ 5 := by 316 | #search "If a natural number n is less than m, then the successor of n is less than the successor of m." 317 | sorry 318 | 319 | In tactic mode, if the query string is not supplied, then [LeanStateSearch](https://premise-search.com) is queried based on the goal state. 320 | ``` 321 | -/ 322 | syntax (name := search_cmd) "#search" (str)? : command 323 | @[command_elab search_cmd] def searchCommandImpl : CommandElab := 324 | fun stx => do 325 | let server ← match leansearchclient.backend.get (← getOptions) with 326 | | "leansearch" => pure leanSearchServer 327 | | s => throwError s!"Invalid backend {s}, must be leansearch" 328 | match stx with 329 | | `(command| #search $s) => do 330 | server.searchCommandSuggestions stx s 331 | | `(command| #search) => do 332 | logWarning server.incompleteSearchQuery 333 | | _ => throwUnsupportedSyntax 334 | 335 | 336 | @[inherit_doc leansearch_search_cmd] 337 | syntax (name := leansearch_search_term) "#leansearch" (str)? : term 338 | 339 | @[term_elab leansearch_search_term] def leanSearchTermImpl : TermElab := 340 | fun stx expectedType? => do 341 | match stx with 342 | | `(#leansearch $s) => 343 | leanSearchServer.searchTermSuggestions stx s 344 | defaultTerm expectedType? 345 | | `(#leansearch) => do 346 | logWarning leanSearchServer.incompleteSearchQuery 347 | defaultTerm expectedType? 348 | | _ => throwUnsupportedSyntax 349 | 350 | @[inherit_doc search_cmd] 351 | syntax (name := search_term) "#search" (str)? : term 352 | 353 | @[term_elab search_term] def searchTermImpl : TermElab := 354 | fun stx expectedType? => do 355 | let server ← match leansearchclient.backend.get (← getOptions) with 356 | | "leansearch" => pure leanSearchServer 357 | | s => throwError s!"Invalid backend {s}, should be leansearch" 358 | match stx with 359 | | `(#search $s) => 360 | server.searchTermSuggestions stx s 361 | defaultTerm expectedType? 362 | | `(#search) => do 363 | logWarning server.incompleteSearchQuery 364 | defaultTerm expectedType? 365 | | _ => throwUnsupportedSyntax 366 | 367 | @[inherit_doc leansearch_search_cmd] 368 | syntax (name := leansearch_search_tactic) 369 | withPosition("#leansearch" (colGt str)?) : tactic 370 | 371 | @[tactic leansearch_search_tactic] def leanSearchTacticImpl : Tactic := 372 | fun stx => withMainContext do 373 | match stx with 374 | | `(tactic|#leansearch $s) => 375 | leanSearchServer.searchTacticSuggestions stx s 376 | | `(tactic|#leansearch) => do 377 | logWarning leanSearchServer.incompleteSearchQuery 378 | | _ => throwUnsupportedSyntax 379 | 380 | /-- Search [LeanStateSearch](https://premise-search.com) from within Lean. 381 | Your current main goal is sent as query. The revision to search can be set 382 | using the `statesearch.revision` option. The number of results can be set 383 | using the `statesearch.queries` option. 384 | 385 | Hint: If you want to modify the query, you need to use the web interface. 386 | 387 | ```lean 388 | set_option statesearch.queries 1 389 | set_option statesearch.revision "v4.16.0" 390 | 391 | example : 0 ≤ 1 := by 392 | #statesearch 393 | sorry 394 | ``` 395 | 396 | You can modify the LeanStateSearch URL by setting the `LEANSEARCHCLIENT_LEANSTATESEARCH_API_URL` environment variable. 397 | -/ 398 | syntax (name := statesearch_search_tactic) 399 | withPosition("#statesearch") : tactic 400 | 401 | @[tactic statesearch_search_tactic] def stateSearchTacticImpl : Tactic := 402 | fun stx => withMainContext do 403 | let goal ← getMainGoal 404 | let target ← getMainTarget 405 | let state := (← Meta.ppGoal goal).pretty 406 | let num_results := (statesearch.queries.get (← getOptions)) 407 | let rev := (statesearch.revision.get (← getOptions)) 408 | match stx with 409 | | `(tactic|#statesearch) => 410 | let results ← queryStateSearch state num_results rev 411 | let suggestionGroups := results.map fun sr => 412 | let fullName := match sr.type? with 413 | | some type => s!"{sr.name} (type: {type})" 414 | | none => sr.name 415 | (fullName, sr.toTacticSuggestions) 416 | let mut foundValidTactic := false 417 | for (name, sg) in suggestionGroups do 418 | let sg ← sg.filterM fun s => 419 | let sugTxt := s.suggestion 420 | match sugTxt with 421 | | .string s => do 422 | let stx? := runParserCategory (← getEnv) `tactic s 423 | match stx? with 424 | | Except.ok stx => 425 | let n? ← checkTactic target stx 426 | return n?.isSome 427 | | Except.error _ => 428 | pure false 429 | | _ => pure false 430 | unless sg.isEmpty do 431 | foundValidTactic := true 432 | TryThis.addSuggestions stx sg (header := s!"From: {name}") 433 | unless foundValidTactic do 434 | TryThis.addSuggestions stx (results.map SearchResult.toCommandSuggestion) 435 | | _ => throwUnsupportedSyntax 436 | 437 | @[inherit_doc search_cmd] 438 | syntax (name := search_tactic) "#search" (str)? : tactic 439 | 440 | @[tactic search_tactic] def searchTacticImpl : Tactic := 441 | fun stx => withMainContext do 442 | match stx with 443 | | `(tactic|#search $s) => 444 | let server ← match leansearchclient.backend.get (← getOptions) with 445 | | "leansearch" => pure leanSearchServer 446 | | s => throwError s!"Invalid backend {s}, should be leansearch" 447 | server.searchTacticSuggestions stx s 448 | | `(tactic|#search) => do 449 | evalTactic (← `(tactic|#statesearch)) 450 | | _ => throwUnsupportedSyntax 451 | 452 | end LeanSearchClient 453 | --------------------------------------------------------------------------------