leanback 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,221 @@
1
+ """
2
+ Simple Mathlib search using Python regex on source files.
3
+
4
+ No complex parsing, no indexes, just fast pattern matching.
5
+ """
6
+
7
+ import re
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ from .common.errors import LeanBackError
12
+
13
+
14
+ @dataclass
15
+ class SimpleSearchResult:
16
+ """A search result from Mathlib."""
17
+
18
+ name: str
19
+ file: str
20
+ line: int
21
+ text: str
22
+ kind: str | None = None # theorem, def, lemma, etc.
23
+
24
+
25
+ class SimpleMathlibSearch:
26
+ """Fast Mathlib search using regex on source files."""
27
+
28
+ def __init__(self, project_path: Path | None = None):
29
+ self.project_path = project_path or Path.cwd()
30
+ self.mathlib_dir = self._find_mathlib_in_project()
31
+
32
+ if not self.mathlib_dir:
33
+ raise LeanBackError(
34
+ "Mathlib not found in project",
35
+ "Ensure your project has Mathlib as a dependency",
36
+ )
37
+
38
+ def _find_mathlib_in_project(self) -> Path | None:
39
+ """Find Mathlib in the project's lake packages."""
40
+ lake_packages = self.project_path / ".lake" / "packages"
41
+ if not lake_packages.exists():
42
+ return None
43
+
44
+ # Check common names first, then fall back to scanning
45
+ for name in ("mathlib", "mathlib4"):
46
+ candidate = lake_packages / name
47
+ if candidate.exists() and (candidate / "Mathlib").exists():
48
+ return candidate
49
+
50
+ # Scan all packages for one containing a Mathlib/ directory
51
+ for item in lake_packages.iterdir():
52
+ if item.is_dir() and (item / "Mathlib").exists():
53
+ return item
54
+
55
+ return None
56
+
57
+ def _get_namespaces_at_line(self, content: str, target_line: int) -> list[str]:
58
+ """Get the active namespace stack at a given line number."""
59
+ namespaces = []
60
+ for i, line in enumerate(content.splitlines(), 1):
61
+ if i >= target_line:
62
+ break
63
+ stripped = line.strip()
64
+ if stripped.startswith("namespace "):
65
+ ns = stripped.split("namespace ", 1)[1].split()[0].strip()
66
+ namespaces.append(ns)
67
+ elif stripped.startswith("end ") and namespaces:
68
+ end_ns = stripped.split("end ", 1)[1].split()[0].strip()
69
+ # Pop matching namespace
70
+ if namespaces and namespaces[-1] == end_ns:
71
+ namespaces.pop()
72
+ return namespaces
73
+
74
+ def _fully_qualify_name(self, name: str, namespaces: list[str]) -> str:
75
+ """Prepend active namespaces to a declaration name."""
76
+ if not namespaces or "." in name:
77
+ return name
78
+ return ".".join(namespaces) + "." + name
79
+
80
+ def search(self, query: str, max_results: int = 50) -> list[SimpleSearchResult]:
81
+ """
82
+ Search for declarations using Python's built-in regex.
83
+
84
+ Args:
85
+ query: Search query (can be name, pattern, or regex)
86
+ max_results: Maximum results to return
87
+
88
+ Returns:
89
+ List of search results
90
+ """
91
+ if not query.strip():
92
+ return []
93
+
94
+ results = []
95
+
96
+ # Always search for declarations (theorem/def/lemma/etc.) matching the query.
97
+ # This avoids matching usages like `rw [add_comm]`.
98
+ decl_keywords = r"(?:protected\s+)?(?:noncomputable\s+)?(?:private\s+)?(theorem|def|lemma|instance|structure|class|inductive|abbrev|axiom)"
99
+
100
+ # For dotted names like "SimpleGraph.neighborFinset", we need to search
101
+ # for the last component and then verify the namespace matches
102
+ if self._looks_like_exact_name(query):
103
+ # Extract the last component for the regex search
104
+ parts = query.rsplit(".", 1)
105
+ last_component = re.escape(parts[-1])
106
+ namespace_prefix = parts[0] if len(parts) > 1 else None
107
+ pattern = re.compile(
108
+ rf"^\s*{decl_keywords}\s+\S*{last_component}\b",
109
+ re.MULTILINE,
110
+ )
111
+ else:
112
+ namespace_prefix = None
113
+ escaped_query = re.escape(query)
114
+ # Non-dotted query: match declarations whose name contains the query
115
+ pattern = re.compile(
116
+ rf"^\s*{decl_keywords}\s+\S*{escaped_query}\S*",
117
+ re.MULTILINE,
118
+ )
119
+
120
+ # Search through all Lean files — max_results only truncates output,
121
+ # it does not limit the scan scope.
122
+ mathlib_path = self.mathlib_dir / "Mathlib"
123
+ total_found = 0
124
+
125
+ for lean_file in mathlib_path.rglob("*.lean"):
126
+ try:
127
+ # Read file content
128
+ with open(lean_file, encoding="utf-8") as f:
129
+ content = f.read()
130
+
131
+ # Search for matches
132
+ for match in pattern.finditer(content):
133
+ # Get line number and full line
134
+ line_no = content[: match.start()].count("\n") + 1
135
+
136
+ # Get the full line containing the match
137
+ line_start = content.rfind("\n", 0, match.start()) + 1
138
+ line_end = content.find("\n", match.end())
139
+ if line_end == -1:
140
+ line_end = len(content)
141
+ line_text = content[line_start:line_end].strip()
142
+
143
+ # Extract declaration kind and name from the full line
144
+ kind = None
145
+ name = "unknown"
146
+
147
+ # Strip modifiers to find the keyword
148
+ clean_line = line_text
149
+ for modifier in ("protected ", "noncomputable ", "private "):
150
+ if clean_line.startswith(modifier):
151
+ clean_line = clean_line[len(modifier) :]
152
+
153
+ for k in [
154
+ "theorem",
155
+ "def",
156
+ "lemma",
157
+ "instance",
158
+ "structure",
159
+ "class",
160
+ "inductive",
161
+ "abbrev",
162
+ "axiom",
163
+ ]:
164
+ if clean_line.startswith(k + " "):
165
+ kind = k
166
+ # Extract name after kind
167
+ after_kind = clean_line.split(k, 1)[1].strip()
168
+ name_part = (
169
+ after_kind.split()[0] if after_kind else "unknown"
170
+ )
171
+ name = (
172
+ name_part.split(":")[0]
173
+ .split("(")[0]
174
+ .split("{")[0]
175
+ .strip()
176
+ )
177
+ break
178
+
179
+ if name == "unknown":
180
+ continue
181
+
182
+ # Fully qualify the name using namespace context
183
+ namespaces = self._get_namespaces_at_line(content, line_no)
184
+ fq_name = self._fully_qualify_name(name, namespaces)
185
+
186
+ # If searching for a dotted name, verify the namespace matches
187
+ if namespace_prefix:
188
+ if not fq_name.startswith(
189
+ namespace_prefix + "."
190
+ ) and not fq_name.endswith("." + query.rsplit(".", 1)[-1]):
191
+ # Also check if the query appears as-is in the fq_name
192
+ if query not in fq_name:
193
+ continue
194
+
195
+ total_found += 1
196
+
197
+ # Only store up to max_results
198
+ if len(results) < max_results:
199
+ rel_path = lean_file.relative_to(self.mathlib_dir)
200
+ results.append(
201
+ SimpleSearchResult(
202
+ name=fq_name,
203
+ file=str(rel_path),
204
+ line=line_no,
205
+ text=line_text,
206
+ kind=kind,
207
+ )
208
+ )
209
+
210
+ except Exception:
211
+ # Skip files that can't be read
212
+ continue
213
+
214
+ # Store total_found for callers to access
215
+ self._last_total_found = total_found
216
+ return results
217
+
218
+ def _looks_like_exact_name(self, query: str) -> bool:
219
+ """Check if query looks like an exact declaration name."""
220
+ # Names typically have dots and use camelCase or snake_case
221
+ return "." in query and all(c.isalnum() or c in "._" for c in query)