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.
- {simdref-0.0.7/src/simdref.egg-info → simdref-0.0.8}/PKG-INFO +3 -3
- {simdref-0.0.7 → simdref-0.0.8}/README.md +2 -2
- {simdref-0.0.7 → simdref-0.0.8}/pyproject.toml +1 -1
- simdref-0.0.8/src/simdref/lsp.py +434 -0
- {simdref-0.0.7 → simdref-0.0.8/src/simdref.egg-info}/PKG-INFO +3 -3
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref.egg-info/SOURCES.txt +1 -0
- simdref-0.0.8/tests/test_lsp_stdio.py +339 -0
- simdref-0.0.7/src/simdref/lsp.py +0 -231
- {simdref-0.0.7 → simdref-0.0.8}/LICENSE +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/setup.cfg +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/__init__.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/__main__.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/annotate.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/arm_instructions.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/cli.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/display.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/export.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/filters.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/ingest.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/ingest_catalog.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/ingest_pdf.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/ingest_sources.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/manpages.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/models.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/pdfparse/__init__.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/pdfparse/base.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/pdfparse/intel.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/pdfparse/registry.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/pdfparse/types.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/pdfrefs.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/perf.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/perf_sources/__init__.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/perf_sources/cores.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/perf_sources/llvm_mca.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/perf_sources/llvm_scheduling.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/perf_sources/merge.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/queries.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/riscv.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/search.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/storage.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/tui.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref/ui_labels.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref.egg-info/dependency_links.txt +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref.egg-info/entry_points.txt +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref.egg-info/requires.txt +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/src/simdref.egg-info/top_level.txt +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_annotate.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_annotate_positions.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_audit_coverage.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_auto_update_on_version_change.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_cli_bare_query.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_cli_bootstrap_progress.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_cli_help.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_cli_llm.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_coverage_live.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_coverage_parity.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_display.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_export.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_filters.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_ingest_sources.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_intel_operation_and_url.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_issue2_fixes.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_issues_23_24_25.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_lsp_web.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_models.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_pdfparse.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_perf.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_perf_sources.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_presentation.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_preset_persistence.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_riscv_docs_pages.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_search.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_search_pushdown.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_source_kind_filter.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_source_validation.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_storage_payload.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_storage_schema.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_tui.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_ui_labels_parity.py +0 -0
- {simdref-0.0.7 → simdref-0.0.8}/tests/test_version_matches_pyproject.py +0 -0
- {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.
|
|
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
|
|
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/
|
|
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
|
|
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/
|
|
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
|
+
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.
|
|
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
|
|
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/
|
|
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
|
|