simdref 0.0.7__tar.gz → 0.0.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {simdref-0.0.7/src/simdref.egg-info → simdref-0.0.8}/PKG-INFO +3 -3
  2. {simdref-0.0.7 → simdref-0.0.8}/README.md +2 -2
  3. {simdref-0.0.7 → simdref-0.0.8}/pyproject.toml +1 -1
  4. simdref-0.0.8/src/simdref/lsp.py +434 -0
  5. {simdref-0.0.7 → simdref-0.0.8/src/simdref.egg-info}/PKG-INFO +3 -3
  6. {simdref-0.0.7 → simdref-0.0.8}/src/simdref.egg-info/SOURCES.txt +1 -0
  7. simdref-0.0.8/tests/test_lsp_stdio.py +339 -0
  8. simdref-0.0.7/src/simdref/lsp.py +0 -231
  9. {simdref-0.0.7 → simdref-0.0.8}/LICENSE +0 -0
  10. {simdref-0.0.7 → simdref-0.0.8}/setup.cfg +0 -0
  11. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/__init__.py +0 -0
  12. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/__main__.py +0 -0
  13. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/annotate.py +0 -0
  14. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/arm_instructions.py +0 -0
  15. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/cli.py +0 -0
  16. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/display.py +0 -0
  17. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/export.py +0 -0
  18. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/filters.py +0 -0
  19. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/ingest.py +0 -0
  20. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/ingest_catalog.py +0 -0
  21. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/ingest_pdf.py +0 -0
  22. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/ingest_sources.py +0 -0
  23. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/manpages.py +0 -0
  24. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/models.py +0 -0
  25. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/pdfparse/__init__.py +0 -0
  26. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/pdfparse/base.py +0 -0
  27. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/pdfparse/intel.py +0 -0
  28. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/pdfparse/registry.py +0 -0
  29. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/pdfparse/types.py +0 -0
  30. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/pdfrefs.py +0 -0
  31. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/perf.py +0 -0
  32. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/perf_sources/__init__.py +0 -0
  33. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/perf_sources/cores.py +0 -0
  34. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/perf_sources/llvm_mca.py +0 -0
  35. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/perf_sources/llvm_scheduling.py +0 -0
  36. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/perf_sources/merge.py +0 -0
  37. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/queries.py +0 -0
  38. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/riscv.py +0 -0
  39. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/search.py +0 -0
  40. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/storage.py +0 -0
  41. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/tui.py +0 -0
  42. {simdref-0.0.7 → simdref-0.0.8}/src/simdref/ui_labels.py +0 -0
  43. {simdref-0.0.7 → simdref-0.0.8}/src/simdref.egg-info/dependency_links.txt +0 -0
  44. {simdref-0.0.7 → simdref-0.0.8}/src/simdref.egg-info/entry_points.txt +0 -0
  45. {simdref-0.0.7 → simdref-0.0.8}/src/simdref.egg-info/requires.txt +0 -0
  46. {simdref-0.0.7 → simdref-0.0.8}/src/simdref.egg-info/top_level.txt +0 -0
  47. {simdref-0.0.7 → simdref-0.0.8}/tests/test_annotate.py +0 -0
  48. {simdref-0.0.7 → simdref-0.0.8}/tests/test_annotate_positions.py +0 -0
  49. {simdref-0.0.7 → simdref-0.0.8}/tests/test_audit_coverage.py +0 -0
  50. {simdref-0.0.7 → simdref-0.0.8}/tests/test_auto_update_on_version_change.py +0 -0
  51. {simdref-0.0.7 → simdref-0.0.8}/tests/test_cli_bare_query.py +0 -0
  52. {simdref-0.0.7 → simdref-0.0.8}/tests/test_cli_bootstrap_progress.py +0 -0
  53. {simdref-0.0.7 → simdref-0.0.8}/tests/test_cli_help.py +0 -0
  54. {simdref-0.0.7 → simdref-0.0.8}/tests/test_cli_llm.py +0 -0
  55. {simdref-0.0.7 → simdref-0.0.8}/tests/test_coverage_live.py +0 -0
  56. {simdref-0.0.7 → simdref-0.0.8}/tests/test_coverage_parity.py +0 -0
  57. {simdref-0.0.7 → simdref-0.0.8}/tests/test_display.py +0 -0
  58. {simdref-0.0.7 → simdref-0.0.8}/tests/test_export.py +0 -0
  59. {simdref-0.0.7 → simdref-0.0.8}/tests/test_filters.py +0 -0
  60. {simdref-0.0.7 → simdref-0.0.8}/tests/test_ingest_sources.py +0 -0
  61. {simdref-0.0.7 → simdref-0.0.8}/tests/test_intel_operation_and_url.py +0 -0
  62. {simdref-0.0.7 → simdref-0.0.8}/tests/test_issue2_fixes.py +0 -0
  63. {simdref-0.0.7 → simdref-0.0.8}/tests/test_issues_23_24_25.py +0 -0
  64. {simdref-0.0.7 → simdref-0.0.8}/tests/test_lsp_web.py +0 -0
  65. {simdref-0.0.7 → simdref-0.0.8}/tests/test_models.py +0 -0
  66. {simdref-0.0.7 → simdref-0.0.8}/tests/test_pdfparse.py +0 -0
  67. {simdref-0.0.7 → simdref-0.0.8}/tests/test_perf.py +0 -0
  68. {simdref-0.0.7 → simdref-0.0.8}/tests/test_perf_sources.py +0 -0
  69. {simdref-0.0.7 → simdref-0.0.8}/tests/test_presentation.py +0 -0
  70. {simdref-0.0.7 → simdref-0.0.8}/tests/test_preset_persistence.py +0 -0
  71. {simdref-0.0.7 → simdref-0.0.8}/tests/test_riscv_docs_pages.py +0 -0
  72. {simdref-0.0.7 → simdref-0.0.8}/tests/test_search.py +0 -0
  73. {simdref-0.0.7 → simdref-0.0.8}/tests/test_search_pushdown.py +0 -0
  74. {simdref-0.0.7 → simdref-0.0.8}/tests/test_source_kind_filter.py +0 -0
  75. {simdref-0.0.7 → simdref-0.0.8}/tests/test_source_validation.py +0 -0
  76. {simdref-0.0.7 → simdref-0.0.8}/tests/test_storage_payload.py +0 -0
  77. {simdref-0.0.7 → simdref-0.0.8}/tests/test_storage_schema.py +0 -0
  78. {simdref-0.0.7 → simdref-0.0.8}/tests/test_tui.py +0 -0
  79. {simdref-0.0.7 → simdref-0.0.8}/tests/test_ui_labels_parity.py +0 -0
  80. {simdref-0.0.7 → simdref-0.0.8}/tests/test_version_matches_pyproject.py +0 -0
  81. {simdref-0.0.7 → simdref-0.0.8}/tests/test_x86_linking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simdref
3
- Version: 0.0.7
3
+ Version: 0.0.8
4
4
  Summary: Searchable SIMD intrinsic and instruction reference with CLI, manpages, TUI, and LSP.
5
5
  Author: Marco
6
6
  License: GPL-3.0-or-later
@@ -51,11 +51,11 @@ structured JSON interface for LLM skills.
51
51
  [GitHub](https://github.com/simd-labs/simdref) ·
52
52
  [Contributing](CONTRIBUTING.md)
53
53
 
54
- <!-- Screenshots are hosted on the `docs-assets` branch so the main
54
+ <!-- Screenshots are hosted on the `assets/docs` ref so the main
55
55
  branch stays lightweight to clone. -->
56
56
 
57
57
  <p align="center">
58
- <img alt="simdref TUI" src="https://raw.githubusercontent.com/simd-labs/simdref/docs-assets/img/tui.svg" width="720">
58
+ <img alt="simdref TUI" src="https://raw.githubusercontent.com/simd-labs/simdref/refs/assets/docs/img/tui.svg" width="720">
59
59
  <br><em>Interactive TUI with ISA filters, ranked results, and measured/modeled performance tables.</em>
60
60
  </p>
61
61
 
@@ -17,11 +17,11 @@ structured JSON interface for LLM skills.
17
17
  [GitHub](https://github.com/simd-labs/simdref) ·
18
18
  [Contributing](CONTRIBUTING.md)
19
19
 
20
- <!-- Screenshots are hosted on the `docs-assets` branch so the main
20
+ <!-- Screenshots are hosted on the `assets/docs` ref so the main
21
21
  branch stays lightweight to clone. -->
22
22
 
23
23
  <p align="center">
24
- <img alt="simdref TUI" src="https://raw.githubusercontent.com/simd-labs/simdref/docs-assets/img/tui.svg" width="720">
24
+ <img alt="simdref TUI" src="https://raw.githubusercontent.com/simd-labs/simdref/refs/assets/docs/img/tui.svg" width="720">
25
25
  <br><em>Interactive TUI with ISA filters, ranked results, and measured/modeled performance tables.</em>
26
26
  </p>
27
27
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simdref"
7
- version = "0.0.7"
7
+ version = "0.0.8"
8
8
  description = "Searchable SIMD intrinsic and instruction reference with CLI, manpages, TUI, and LSP."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,434 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import json
5
+ import os
6
+ import re
7
+ import sqlite3
8
+ import sys
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ from simdref.perf import best_cpi, best_latency
13
+ from simdref.queries import linked_instruction_records
14
+ from simdref.search import search_records
15
+ from simdref.storage import (
16
+ SQLITE_PATH,
17
+ load_intrinsic_from_db,
18
+ load_instruction_from_db,
19
+ load_instructions_by_mnemonic_from_db,
20
+ open_db,
21
+ search_intrinsic_candidates_from_db,
22
+ search_instruction_candidates_from_db,
23
+ sqlite_schema_is_current,
24
+ )
25
+
26
+
27
+ WORD_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_.]*")
28
+ MNEMONIC_RE = re.compile(r"^[A-Za-z][A-Za-z0-9.]*$")
29
+ ASM_COMMENT_RE = re.compile(r"#.*$|//.*$|/\*.*?\*/")
30
+ # In a .asm file (NASM/MASM) ";" starts a comment. A .s/.S file in GAS syntax
31
+ # uses ";" as a statement separator, so it stays.
32
+ ASM_SEMICOLON_COMMENT_RE = re.compile(r";.*$")
33
+ # Matches a string literal or a comment. The scanner blanks comments and keeps strings.
34
+ # ponytail: character literals ('"') and raw strings are not handled.
35
+ C_TOKEN_RE = re.compile(r'"(?:[^"\\\n]|\\.)*"|//[^\n]*|/\*.*?\*/', re.DOTALL)
36
+ # asm("..."), __asm__("..."), __asm("..."), with optional qualifiers. Adjacent
37
+ # string literals concatenate. Extended asm (": outputs : inputs") ends at ':'.
38
+ ASM_STRING_RE = re.compile(
39
+ r"(?<![A-Za-z0-9_])(?:__asm__|__asm|asm)"
40
+ r"(?:\s+(?:volatile|__volatile__|goto|inline))*\s*\(\s*"
41
+ r'("(?:[^"\\]|\\.)*"(?:\s*"(?:[^"\\]|\\.)*")*)'
42
+ r"(?=\s*[:)])"
43
+ )
44
+ STRING_PART_RE = re.compile(r'"((?:[^"\\]|\\.)*)"')
45
+ ASM_SPLIT_RE = re.compile(r"\\n|;")
46
+ C_EXTENSIONS = (".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".cu", ".cuh")
47
+ CATALOG_MISSING = "simdref catalog not found. Run: isa update, then restart the editor."
48
+
49
+
50
+ @dataclass
51
+ class Session:
52
+ documents: dict[str, str]
53
+ languages: dict[str, str] = field(default_factory=dict)
54
+
55
+
56
+ def _jsonrpc_write(payload: dict) -> None:
57
+ body = json.dumps(payload).encode("utf-8")
58
+ sys.stdout.buffer.write(f"Content-Length: {len(body)}\r\n\r\n".encode("ascii"))
59
+ sys.stdout.buffer.write(body)
60
+ sys.stdout.buffer.flush()
61
+
62
+
63
+ def _jsonrpc_read() -> dict | None:
64
+ headers = {}
65
+ while True:
66
+ line = sys.stdin.buffer.readline()
67
+ if not line:
68
+ return None
69
+ if line in (b"\r\n", b"\n"):
70
+ break
71
+ key, value = line.decode("ascii").split(":", 1)
72
+ headers[key.strip().lower()] = value.strip()
73
+ length = int(headers.get("content-length", "0"))
74
+ if length <= 0:
75
+ return None
76
+ body = sys.stdin.buffer.read(length)
77
+ return json.loads(body)
78
+
79
+
80
+ def _word_at(text: str, line: int, character: int) -> str | None:
81
+ lines = text.splitlines()
82
+ if line >= len(lines):
83
+ return None
84
+ current = lines[line]
85
+ for match in WORD_RE.finditer(current):
86
+ if match.start() <= character <= match.end():
87
+ return match.group(0)
88
+ return None
89
+
90
+
91
+ def _line_prefix(text: str, line: int, character: int) -> str:
92
+ lines = text.splitlines()
93
+ if line >= len(lines):
94
+ return ""
95
+ current = lines[line][:character]
96
+ match = re.search(WORD_RE.pattern + r"$", current)
97
+ return match.group(0) if match else ""
98
+
99
+
100
+ def _hover_markdown(conn, word: str, allow_instruction: bool = True) -> str | None:
101
+ intrinsic = load_intrinsic_from_db(conn, word)
102
+ if intrinsic is not None:
103
+ lines = [f"```c\n{intrinsic.signature}\n```"]
104
+ if intrinsic.description:
105
+ lines.append(intrinsic.description)
106
+ meta = []
107
+ if intrinsic.header:
108
+ meta.append(f"header `{intrinsic.header}`")
109
+ if intrinsic.isa:
110
+ meta.append(f"ISA {', '.join(intrinsic.isa)}")
111
+ if intrinsic.category:
112
+ meta.append(f"category {intrinsic.category}")
113
+ if intrinsic.url:
114
+ meta.append(f"[source]({intrinsic.url})")
115
+ if meta:
116
+ lines.append(" | ".join(meta))
117
+ if intrinsic.instructions:
118
+ lines.append(f"Instructions: {', '.join(intrinsic.instructions[:6])}")
119
+ linked = linked_instruction_records(None, intrinsic, conn=conn)
120
+ if linked:
121
+ latencies = [
122
+ best_latency(item.arch_details)
123
+ for item in linked
124
+ if best_latency(item.arch_details) != "-"
125
+ ]
126
+ throughputs = [
127
+ best_cpi(item.arch_details) for item in linked if best_cpi(item.arch_details) != "-"
128
+ ]
129
+ perf = []
130
+ if latencies:
131
+ perf.append(f"best latency {min(latencies, key=lambda value: float(value))} cycles")
132
+ if throughputs:
133
+ perf.append(f"best cycle/instr {min(throughputs, key=lambda value: float(value))}")
134
+ if perf:
135
+ lines.append("Performance: " + ", ".join(perf))
136
+ return "\n\n".join(lines)
137
+
138
+ if not allow_instruction:
139
+ return None
140
+ instruction = load_instruction_from_db(conn, word)
141
+ if instruction is None:
142
+ # Catalog keys look like "VADDPS (XMM, XMM, XMM)", so a bare mnemonic needs this lookup.
143
+ matches = load_instructions_by_mnemonic_from_db(conn, word)
144
+ instruction = matches[0] if matches else None
145
+ if instruction is not None:
146
+ lines = [f"```asm\n{instruction.key}\n```"]
147
+ if instruction.summary:
148
+ lines.append(instruction.summary)
149
+ meta = []
150
+ if instruction.isa:
151
+ meta.append(f"ISA {', '.join(instruction.isa)}")
152
+ if instruction.metadata.get("category"):
153
+ meta.append(f"category {instruction.metadata['category']}")
154
+ if meta:
155
+ lines.append(" | ".join(meta))
156
+ if instruction.linked_intrinsics:
157
+ lines.append(f"Intrinsics: {', '.join(instruction.linked_intrinsics[:6])}")
158
+ perf = []
159
+ lat = best_latency(instruction.arch_details)
160
+ cpi = best_cpi(instruction.arch_details)
161
+ if lat != "-":
162
+ perf.append(f"best latency {lat} cycles")
163
+ if cpi != "-":
164
+ perf.append(f"best cycle/instr {cpi}")
165
+ if perf:
166
+ lines.append("Performance: " + ", ".join(perf))
167
+ return "\n\n".join(lines)
168
+ return None
169
+
170
+
171
+ def _completion_candidates(conn, prefix: str, limit: int = 50) -> list[dict]:
172
+ prefix_folded = prefix.casefold()
173
+ emitted: set[tuple[str, str]] = set()
174
+ items: list[dict] = []
175
+ candidate_limit = max(limit * 3, 100)
176
+ intrinsics = search_intrinsic_candidates_from_db(conn, prefix or "_mm", limit=candidate_limit)
177
+ instructions = search_instruction_candidates_from_db(
178
+ conn, prefix or "_mm", limit=candidate_limit
179
+ )
180
+ for result in search_records(intrinsics, instructions, prefix or "_mm", limit=candidate_limit):
181
+ label = result.title
182
+ if prefix_folded and not label.casefold().startswith(prefix_folded):
183
+ continue
184
+ key = (result.kind, label)
185
+ if key in emitted:
186
+ continue
187
+ emitted.add(key)
188
+ kind = 3 if result.kind == "intrinsic" else 14
189
+ items.append({"label": label, "kind": kind, "detail": result.subtitle, "insertText": label})
190
+ if len(items) >= limit:
191
+ return items
192
+ return items
193
+
194
+
195
+ def _is_c_doc(language_id: str, uri: str) -> bool:
196
+ return language_id in ("c", "cpp") or uri.lower().endswith(C_EXTENSIONS)
197
+
198
+
199
+ def _asm_literals(text: str) -> list[tuple[int, str]]:
200
+ """Return (offset, body) for each string literal inside an asm(...) call in C source."""
201
+ masked = C_TOKEN_RE.sub(
202
+ lambda m: m.group(0) if m.group(0)[0] == '"' else re.sub(r"[^\n]", " ", m.group(0)),
203
+ text,
204
+ )
205
+ return [
206
+ (match.start(1) + part.start(1), part.group(1))
207
+ for match in ASM_STRING_RE.finditer(masked)
208
+ for part in STRING_PART_RE.finditer(match.group(1))
209
+ ]
210
+
211
+
212
+ def _in_asm_string(text: str, line: int, character: int) -> bool:
213
+ offset = sum(len(item) + 1 for item in text.split("\n")[:line]) + character
214
+ return any(start <= offset <= start + len(body) for start, body in _asm_literals(text))
215
+
216
+
217
+ def _operand_count(operands: str) -> int:
218
+ """Count top-level commas: "(%rax,%rbx,4)" and "{1to16}" count as one operand."""
219
+ if not operands.strip():
220
+ return 0
221
+ depth, count = 0, 1
222
+ for char in operands:
223
+ if char in "([{":
224
+ depth += 1
225
+ elif char in ")]}":
226
+ depth -= 1
227
+ elif char == "," and depth == 0:
228
+ count += 1
229
+ return count
230
+
231
+
232
+ def _mnemonic_from_asm_line(line: str, semicolon_is_comment: bool = False):
233
+ """Return (mnemonic, operand_count) for an asm line, or None."""
234
+ line = ASM_COMMENT_RE.sub("", line)
235
+ if semicolon_is_comment:
236
+ line = ASM_SEMICOLON_COMMENT_RE.sub("", line)
237
+ line = line.strip()
238
+ if not line or line.startswith("."):
239
+ return None
240
+ parts = line.split(None, 1)
241
+ if parts[0].endswith(":"):
242
+ line = parts[1].strip() if len(parts) > 1 else ""
243
+ if not line or line.startswith("."):
244
+ return None
245
+ parts = line.split(None, 1)
246
+ if not MNEMONIC_RE.match(parts[0]):
247
+ return None
248
+ return parts[0], _operand_count(parts[1] if len(parts) > 1 else "")
249
+
250
+
251
+ def _operands_of_key(key: str) -> list[str]:
252
+ start, end = key.find("("), key.rfind(")")
253
+ inner = key[start + 1 : end].strip() if 0 <= start < end else ""
254
+ return [piece.strip() for piece in inner.split(",")] if inner else []
255
+
256
+
257
+ def _cut_label(text: str) -> str:
258
+ return text[:57] + "..." if len(text) > 60 else text
259
+
260
+
261
+ @functools.lru_cache(maxsize=1024)
262
+ def _mnemonic_matches(db_path: str, mnemonic: str) -> tuple:
263
+ # The inlay-hint loop asks the same mnemonics for every line, so query once
264
+ # per (catalog, mnemonic) and reuse. The key carries the path, not the
265
+ # connection, so a reloaded catalog at the same path would keep stale rows;
266
+ # clear the cache if catalog reload lands.
267
+ conn = open_db(path=Path(db_path))
268
+ try:
269
+ return tuple(load_instructions_by_mnemonic_from_db(conn, mnemonic))
270
+ finally:
271
+ conn.close()
272
+
273
+
274
+ def _hint_label(db_path: str, mnemonic: str, operand_count: int) -> str | None:
275
+ matches = _mnemonic_matches(db_path, mnemonic)
276
+ if not matches:
277
+ return None
278
+ # ponytail: a line has no architecture, so take the first form with the same operand count.
279
+ # The fallback takes the first form with fewest operands, never a form with more
280
+ # operands than the line: a zero-operand "movsd" must not get the MOVSD_XMM hint.
281
+ chosen = next(
282
+ (m for m in matches if len(_operands_of_key(m.key)) == operand_count),
283
+ min(matches, key=lambda m: len(_operands_of_key(m.key))),
284
+ )
285
+ return _cut_label(chosen.summary or "")
286
+
287
+
288
+ def _inlay_hints(conn, text: str, language_id: str, uri: str, start: int, end: int) -> list[dict]:
289
+ db_path = conn.execute("PRAGMA database_list").fetchone()[2]
290
+ source_lines = [line.rstrip("\r") for line in text.split("\n")]
291
+ is_c_doc = _is_c_doc(language_id, uri)
292
+ semicolon_is_comment = not is_c_doc and uri.lower().endswith(".asm")
293
+ if is_c_doc:
294
+ candidates = [
295
+ (text.count("\n", 0, offset), segment)
296
+ for offset, body in _asm_literals(text)
297
+ for segment in ASM_SPLIT_RE.split(body.replace("\\t", " "))
298
+ ]
299
+ else:
300
+ candidates = list(enumerate(source_lines))
301
+ hints: dict[int, list[str]] = {} # every instruction on the source line feeds its hint
302
+ for line, segment in candidates:
303
+ if not start <= line <= end:
304
+ continue
305
+ parsed = _mnemonic_from_asm_line(segment, semicolon_is_comment)
306
+ label = _hint_label(db_path, *parsed) if parsed else None
307
+ if label is not None:
308
+ hints.setdefault(line, []).append(label)
309
+ return [
310
+ {
311
+ # LSP columns count UTF-16 code units.
312
+ "position": {
313
+ "line": line,
314
+ "character": len(source_lines[line].encode("utf-16-le")) // 2,
315
+ },
316
+ "label": _cut_label("; ".join(hints[line])),
317
+ "paddingLeft": True,
318
+ }
319
+ for line in sorted(hints)
320
+ ]
321
+
322
+
323
+ def _open_catalog() -> sqlite3.Connection | None:
324
+ path = Path(os.environ.get("SIMDREF_CATALOG") or SQLITE_PATH)
325
+ try:
326
+ # A stale schema opens fine but fails at the first payload read.
327
+ return open_db(path=path) if sqlite_schema_is_current(path) else None
328
+ except sqlite3.Error:
329
+ return None
330
+
331
+
332
+ def main() -> int:
333
+ conn = _open_catalog()
334
+ session = Session(documents={})
335
+ while True:
336
+ message = _jsonrpc_read()
337
+ if message is None:
338
+ return 0
339
+ method = message.get("method")
340
+ if method == "initialize":
341
+ _jsonrpc_write(
342
+ {
343
+ "jsonrpc": "2.0",
344
+ "id": message["id"],
345
+ "result": {
346
+ "capabilities": {
347
+ "hoverProvider": True,
348
+ "textDocumentSync": 1,
349
+ "inlayHintProvider": True,
350
+ "completionProvider": {
351
+ "resolveProvider": False,
352
+ "triggerCharacters": ["_", ".", "m", "v"],
353
+ },
354
+ }
355
+ },
356
+ }
357
+ )
358
+ elif method == "initialized":
359
+ if conn is None:
360
+ _jsonrpc_write(
361
+ {
362
+ "jsonrpc": "2.0",
363
+ "method": "window/showMessage",
364
+ "params": {"type": 2, "message": CATALOG_MISSING},
365
+ }
366
+ )
367
+ elif method == "shutdown":
368
+ _jsonrpc_write({"jsonrpc": "2.0", "id": message["id"], "result": None})
369
+ elif method == "exit":
370
+ return 0
371
+ elif method == "textDocument/didOpen":
372
+ doc = message["params"]["textDocument"]
373
+ session.documents[doc["uri"]] = doc["text"]
374
+ session.languages[doc["uri"]] = doc.get("languageId", "")
375
+ elif method == "textDocument/didChange":
376
+ params = message["params"]
377
+ session.documents[params["textDocument"]["uri"]] = params["contentChanges"][-1]["text"]
378
+ elif method == "textDocument/didClose":
379
+ uri = message["params"]["textDocument"]["uri"]
380
+ session.documents.pop(uri, None)
381
+ session.languages.pop(uri, None)
382
+ elif method == "textDocument/hover":
383
+ params = message["params"]
384
+ uri = params["textDocument"]["uri"]
385
+ text = session.documents.get(uri, "")
386
+ line, character = params["position"]["line"], params["position"]["character"]
387
+ word = _word_at(text, line, character)
388
+ # In C/C++ only the asm strings hold instructions; intrinsics hover everywhere.
389
+ allow_instruction = not _is_c_doc(session.languages.get(uri, ""), uri) or (
390
+ _in_asm_string(text, line, character)
391
+ )
392
+ body = _hover_markdown(conn, word, allow_instruction) if conn and word else None
393
+ _jsonrpc_write(
394
+ {
395
+ "jsonrpc": "2.0",
396
+ "id": message["id"],
397
+ "result": {"contents": {"kind": "markdown", "value": body}} if body else None,
398
+ }
399
+ )
400
+ elif method == "textDocument/completion":
401
+ params = message["params"]
402
+ uri = params["textDocument"]["uri"]
403
+ text = session.documents.get(uri, "")
404
+ prefix = _line_prefix(text, params["position"]["line"], params["position"]["character"])
405
+ items = _completion_candidates(conn, prefix) if conn else []
406
+ _jsonrpc_write(
407
+ {
408
+ "jsonrpc": "2.0",
409
+ "id": message["id"],
410
+ "result": {"isIncomplete": False, "items": items},
411
+ }
412
+ )
413
+ elif method == "textDocument/inlayHint":
414
+ params = message["params"]
415
+ uri = params["textDocument"]["uri"]
416
+ rng = params["range"]
417
+ hints = (
418
+ _inlay_hints(
419
+ conn,
420
+ session.documents.get(uri, ""),
421
+ session.languages.get(uri, ""),
422
+ uri,
423
+ rng["start"]["line"],
424
+ rng["end"]["line"],
425
+ )
426
+ if conn
427
+ else []
428
+ )
429
+ _jsonrpc_write({"jsonrpc": "2.0", "id": message["id"], "result": hints})
430
+ return 0
431
+
432
+
433
+ if __name__ == "__main__":
434
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simdref
3
- Version: 0.0.7
3
+ Version: 0.0.8
4
4
  Summary: Searchable SIMD intrinsic and instruction reference with CLI, manpages, TUI, and LSP.
5
5
  Author: Marco
6
6
  License: GPL-3.0-or-later
@@ -51,11 +51,11 @@ structured JSON interface for LLM skills.
51
51
  [GitHub](https://github.com/simd-labs/simdref) ·
52
52
  [Contributing](CONTRIBUTING.md)
53
53
 
54
- <!-- Screenshots are hosted on the `docs-assets` branch so the main
54
+ <!-- Screenshots are hosted on the `assets/docs` ref so the main
55
55
  branch stays lightweight to clone. -->
56
56
 
57
57
  <p align="center">
58
- <img alt="simdref TUI" src="https://raw.githubusercontent.com/simd-labs/simdref/docs-assets/img/tui.svg" width="720">
58
+ <img alt="simdref TUI" src="https://raw.githubusercontent.com/simd-labs/simdref/refs/assets/docs/img/tui.svg" width="720">
59
59
  <br><em>Interactive TUI with ISA filters, ranked results, and measured/modeled performance tables.</em>
60
60
  </p>
61
61
 
@@ -57,6 +57,7 @@ tests/test_ingest_sources.py
57
57
  tests/test_intel_operation_and_url.py
58
58
  tests/test_issue2_fixes.py
59
59
  tests/test_issues_23_24_25.py
60
+ tests/test_lsp_stdio.py
60
61
  tests/test_lsp_web.py
61
62
  tests/test_models.py
62
63
  tests/test_pdfparse.py