simdref 0.0.12__tar.gz → 0.0.13__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.12/src/simdref.egg-info → simdref-0.0.13}/PKG-INFO +6 -6
- {simdref-0.0.12 → simdref-0.0.13}/README.md +5 -5
- {simdref-0.0.12 → simdref-0.0.13}/pyproject.toml +1 -1
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/cli.py +1 -3
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/ingest_catalog.py +6 -3
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/lsp.py +316 -40
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/tui.py +11 -0
- {simdref-0.0.12 → simdref-0.0.13/src/simdref.egg-info}/PKG-INFO +6 -6
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_auto_update_on_version_change.py +5 -4
- simdref-0.0.13/tests/test_lsp_stdio.py +872 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_search.py +17 -1
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_tui.py +47 -0
- simdref-0.0.12/tests/test_lsp_stdio.py +0 -472
- {simdref-0.0.12 → simdref-0.0.13}/LICENSE +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/setup.cfg +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/__init__.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/__main__.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/annotate.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/arm_instructions.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/display.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/export.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/filters.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/ingest.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/ingest_pdf.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/ingest_sources.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/manpages.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/models.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/pdfparse/__init__.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/pdfparse/base.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/pdfparse/intel.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/pdfparse/registry.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/pdfparse/types.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/pdfrefs.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/perf.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/perf_sources/__init__.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/perf_sources/cores.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/perf_sources/llvm_mca.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/perf_sources/llvm_scheduling.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/perf_sources/merge.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/queries.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/riscv.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/search.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/storage.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref/ui_labels.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref.egg-info/SOURCES.txt +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref.egg-info/dependency_links.txt +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref.egg-info/entry_points.txt +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref.egg-info/requires.txt +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/src/simdref.egg-info/top_level.txt +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_annotate.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_annotate_positions.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_audit_coverage.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_cli_bare_query.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_cli_bootstrap_progress.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_cli_help.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_cli_llm.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_coverage_live.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_coverage_parity.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_display.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_export.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_filters.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_ingest_sources.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_intel_operation_and_url.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_issue2_fixes.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_issues_23_24_25.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_lsp_web.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_models.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_pdfparse.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_perf.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_perf_sources.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_presentation.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_preset_persistence.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_riscv_docs_pages.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_search_pushdown.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_source_kind_filter.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_source_validation.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_storage_payload.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_storage_schema.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_ui_labels_parity.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/tests/test_version_matches_pyproject.py +0 -0
- {simdref-0.0.12 → simdref-0.0.13}/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.13
|
|
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
|
|
@@ -67,7 +67,7 @@ isa # open the TUI
|
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
The package installs two equivalent executables, `isa` and `simdref`. The
|
|
70
|
-
catalog download does not
|
|
70
|
+
catalog download does not use `llvm-mca`. Only `isa build` uses
|
|
71
71
|
`llvm-mca` 18+ on `PATH`.
|
|
72
72
|
|
|
73
73
|
Pre-release builds live on TestPyPI:
|
|
@@ -98,7 +98,7 @@ reads the JSON site data this repo exports:
|
|
|
98
98
|
isa export --out-dir ./site-data
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
The LSP server speaks JSON-RPC
|
|
101
|
+
The LSP server speaks JSON-RPC on stdio:
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
104
|
simdref-lsp
|
|
@@ -114,7 +114,7 @@ Editor clients, one for each editor:
|
|
|
114
114
|
Each client shows a one-line brief at the end of each instruction line and
|
|
115
115
|
the full manpage on hover, from the local catalog.
|
|
116
116
|
|
|
117
|
-
The LLM interface
|
|
117
|
+
The LLM interface writes JSON and NDJSON with exit codes 0 (match), 1 (bad
|
|
118
118
|
flag), 2 (no match), 3 (ambiguous):
|
|
119
119
|
|
|
120
120
|
```bash
|
|
@@ -148,11 +148,11 @@ Commands
|
|
|
148
148
|
| `isa doctor` | Check the install, non-zero exit on failure |
|
|
149
149
|
| `isa update` | Download the pre-built catalog, `--from-release` for the GitHub Release |
|
|
150
150
|
| `isa annotate <file.s>` | Annotate a `.s` file with summaries and latency/CPI, writes `<file>.sa` |
|
|
151
|
-
| `isa man <name>` | Show a manpage,
|
|
151
|
+
| `isa man <name>` | Show a manpage, made on demand |
|
|
152
152
|
| `isa install-manpages` | Pre-generate man7 pages so plain `man vpaddd` works |
|
|
153
153
|
| `isa llm query <q>` | Strict lookup to JSON, NDJSON, or Markdown |
|
|
154
154
|
| `isa llm batch` | Resolve many queries from stdin in one invocation |
|
|
155
|
-
| `isa llm list` |
|
|
155
|
+
| `isa llm list` | Write the FilterSpec or stream matching catalog entries |
|
|
156
156
|
| `isa llm schema` | Print the JSON schema for `llm` payloads |
|
|
157
157
|
|
|
158
158
|
Dev commands
|
|
@@ -33,7 +33,7 @@ isa # open the TUI
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
The package installs two equivalent executables, `isa` and `simdref`. The
|
|
36
|
-
catalog download does not
|
|
36
|
+
catalog download does not use `llvm-mca`. Only `isa build` uses
|
|
37
37
|
`llvm-mca` 18+ on `PATH`.
|
|
38
38
|
|
|
39
39
|
Pre-release builds live on TestPyPI:
|
|
@@ -64,7 +64,7 @@ reads the JSON site data this repo exports:
|
|
|
64
64
|
isa export --out-dir ./site-data
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
The LSP server speaks JSON-RPC
|
|
67
|
+
The LSP server speaks JSON-RPC on stdio:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
simdref-lsp
|
|
@@ -80,7 +80,7 @@ Editor clients, one for each editor:
|
|
|
80
80
|
Each client shows a one-line brief at the end of each instruction line and
|
|
81
81
|
the full manpage on hover, from the local catalog.
|
|
82
82
|
|
|
83
|
-
The LLM interface
|
|
83
|
+
The LLM interface writes JSON and NDJSON with exit codes 0 (match), 1 (bad
|
|
84
84
|
flag), 2 (no match), 3 (ambiguous):
|
|
85
85
|
|
|
86
86
|
```bash
|
|
@@ -114,11 +114,11 @@ Commands
|
|
|
114
114
|
| `isa doctor` | Check the install, non-zero exit on failure |
|
|
115
115
|
| `isa update` | Download the pre-built catalog, `--from-release` for the GitHub Release |
|
|
116
116
|
| `isa annotate <file.s>` | Annotate a `.s` file with summaries and latency/CPI, writes `<file>.sa` |
|
|
117
|
-
| `isa man <name>` | Show a manpage,
|
|
117
|
+
| `isa man <name>` | Show a manpage, made on demand |
|
|
118
118
|
| `isa install-manpages` | Pre-generate man7 pages so plain `man vpaddd` works |
|
|
119
119
|
| `isa llm query <q>` | Strict lookup to JSON, NDJSON, or Markdown |
|
|
120
120
|
| `isa llm batch` | Resolve many queries from stdin in one invocation |
|
|
121
|
-
| `isa llm list` |
|
|
121
|
+
| `isa llm list` | Write the FilterSpec or stream matching catalog entries |
|
|
122
122
|
| `isa llm schema` | Print the JSON schema for `llm` payloads |
|
|
123
123
|
|
|
124
124
|
Dev commands
|
|
@@ -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.13"
|
|
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"
|
|
@@ -659,9 +659,7 @@ def _maybe_auto_update_for_version_change() -> None:
|
|
|
659
659
|
"[bold yellow]warning:[/bold yellow] auto-update failed; continuing with the existing catalog. "
|
|
660
660
|
"Some features may not work correctly until `simdref update` succeeds.",
|
|
661
661
|
)
|
|
662
|
-
#
|
|
663
|
-
# been warned and can re-run `simdref update` once back online.
|
|
664
|
-
write_installed_version_stamp(__version__)
|
|
662
|
+
# Keep the old stamp so the next invocation retries the download.
|
|
665
663
|
|
|
666
664
|
|
|
667
665
|
def _catalog_meta(catalog) -> dict:
|
|
@@ -85,12 +85,13 @@ def _canonical_instruction_key(name: str, form: str) -> str:
|
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
@lru_cache(maxsize=128)
|
|
88
|
-
def _normalize_operand_xtype(value: str) -> str:
|
|
88
|
+
def _normalize_operand_xtype(value: str, width: str = "") -> str:
|
|
89
89
|
xtype = value.strip()
|
|
90
90
|
if not xtype:
|
|
91
91
|
return xtype
|
|
92
92
|
if xtype == "int":
|
|
93
|
-
|
|
93
|
+
# uops.info marks every scalar integer operand xtype=int; the width holds the size
|
|
94
|
+
return f"i{width}" if width in {"8", "16", "32", "64"} else "i32"
|
|
94
95
|
match = re.fullmatch(r"\d+([iu]\d+)", xtype)
|
|
95
96
|
if match:
|
|
96
97
|
return match.group(1)
|
|
@@ -1020,7 +1021,9 @@ def parse_uops_xml(source: str | Path) -> list[InstructionRecord]:
|
|
|
1020
1021
|
arch_details: dict[str, dict[str, Any]] = {}
|
|
1021
1022
|
for child in node:
|
|
1022
1023
|
if child.tag == "operand":
|
|
1023
|
-
xtype = _normalize_operand_xtype(
|
|
1024
|
+
xtype = _normalize_operand_xtype(
|
|
1025
|
+
child.attrib.get("xtype", "").strip(), child.attrib.get("width", "").strip()
|
|
1026
|
+
)
|
|
1024
1027
|
operand_payload = {
|
|
1025
1028
|
key: (
|
|
1026
1029
|
_intern_small(value.strip())
|
|
@@ -113,31 +113,286 @@ def _line_prefix(text: str, line: int, character: int) -> str:
|
|
|
113
113
|
return match.group(0) if match else ""
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
_ATT_SUFFIX_WIDTH = {"b": 8, "w": 16, "l": 32, "q": 64}
|
|
117
|
+
_ACCUMULATOR = {"al": "r8", "ax": "r16", "eax": "r32", "rax": "r64"}
|
|
118
|
+
# x86 numbers only r8-r15; r0-r7 are ARM registers.
|
|
119
|
+
_GPR_PATTERNS = (
|
|
120
|
+
(64, r"r(ax|bx|cx|dx|si|di|bp|sp|8|9|1[0-5])"),
|
|
121
|
+
(32, r"e(ax|bx|cx|dx|si|di|bp|sp)|r(8|9|1[0-5])d"),
|
|
122
|
+
(16, r"(ax|bx|cx|dx|si|di|bp|sp)|r(8|9|1[0-5])w"),
|
|
123
|
+
(8, r"[a-d][lh]|(si|di|bp|sp)l|r(8|9|1[0-5])b"),
|
|
124
|
+
)
|
|
125
|
+
_STRING_OP_RE = re.compile(r"(movs|cmps|scas|lods|stos|ins|outs)[bwdq]", re.IGNORECASE)
|
|
126
|
+
_X86_VECTOR_WIDTHS = {"xmm": "v128", "ymm": "v256", "zmm": "v512"}
|
|
127
|
+
_X86_VECTOR_RE = re.compile(r"(?:[xyz]mm(?:[12]?[0-9]|3[01])|k[0-7]|mm[0-7])\Z")
|
|
128
|
+
_A64_ARRANGEMENT_RE = re.compile(r"[bdhqsvz](?:[0-9]|[12][0-9]|3[01])\.[0-9]*[bhsdq]\Z")
|
|
129
|
+
_A64_SCALAR_RE = re.compile(r"(?:x(?:[0-9]|[12][0-9]|30)|w(?:[0-9]|[12][0-9]|30)|xzr|wzr)\Z")
|
|
130
|
+
_A64_SCALAR_FP_RE = re.compile(r"[bhsdq](?:[0-9]|[12][0-9]|3[01])\Z")
|
|
131
|
+
# A bare v register is a vector spelling ARM and RISC-V share.
|
|
132
|
+
_A64_VECTOR_BARE_RE = re.compile(r"v(?:[0-9]|[12][0-9]|3[01])\Z")
|
|
133
|
+
_ARM32_SCALAR_RE = re.compile(r"(?:r(?:[0-9]|1[0-5])|ip|pc)\Z")
|
|
134
|
+
# ARM has "sp" and "fp"; RISC-V shares both spellings and adds these scalar aliases.
|
|
135
|
+
_RISCV_SCALARS = frozenset(
|
|
136
|
+
{f"a{i}" for i in range(8)}
|
|
137
|
+
| {f"s{i}" for i in range(12)}
|
|
138
|
+
| {f"t{i}" for i in range(7)}
|
|
139
|
+
| {"zero", "ra", "gp", "tp"}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _lookup_mnemonic(conn, mnemonic: str, arch: frozenset = frozenset()):
|
|
144
|
+
"""Return (forms, width). The exact mnemonic wins, else drop an AT&T size suffix (addq -> add).
|
|
145
|
+
|
|
146
|
+
The suffix strip runs on an x86 or undecided line only: "addw" on a RISC-V
|
|
147
|
+
or ARM32 line must not land on x86 "add". Rows of other architectures stay
|
|
148
|
+
in the result; _match_form filters them by the line's candidates.
|
|
149
|
+
"""
|
|
150
|
+
forms = load_instructions_by_mnemonic_from_db(conn, mnemonic)
|
|
151
|
+
width = _ATT_SUFFIX_WIDTH.get(mnemonic[-1:].casefold())
|
|
152
|
+
# movsbl is a sign-extend, not the string op movsb.
|
|
153
|
+
if forms or not width or _STRING_OP_RE.fullmatch(mnemonic[:-1]):
|
|
154
|
+
return forms, None
|
|
155
|
+
if arch and "x86" not in arch:
|
|
156
|
+
return [], None
|
|
157
|
+
stripped = load_instructions_by_mnemonic_from_db(conn, mnemonic[:-1])
|
|
158
|
+
return [form for form in stripped if form.architecture == "x86"], width
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _classify_line(operands: str, intel: bool) -> tuple[frozenset, list[str | None], bool]:
|
|
162
|
+
"""One pass over the line: (candidate architecture buckets, one class per operand, reverse).
|
|
163
|
+
|
|
164
|
+
A strong vote (a %- or Intel-style register, an x86 vector, an AArch64
|
|
165
|
+
arrangement, an AArch64/ARM32/RISC-V-only scalar) names one bucket. A
|
|
166
|
+
spelling two architectures share ("sp", "fp", s0-s11, a bare v) votes for
|
|
167
|
+
every bucket it can mean and only counts when no strong vote fired. Empty
|
|
168
|
+
candidates mean undecided; callers then keep today's unfiltered behaviour.
|
|
169
|
+
"reverse" is True when the line's operand order flips into key order
|
|
170
|
+
(AT&T x86). A register of a non-candidate architecture reads as a symbol.
|
|
171
|
+
"""
|
|
172
|
+
parts = re.split(r",(?![^(\[{]*[)\]}])", operands)
|
|
173
|
+
strong, weak = set(), set()
|
|
174
|
+
for raw in parts:
|
|
175
|
+
op = raw.strip()
|
|
176
|
+
name = op.lstrip("*").lstrip("%").casefold()
|
|
177
|
+
prefixed = op.lstrip("*").startswith("%")
|
|
178
|
+
reg = next((w for w, pattern in _GPR_PATTERNS if re.fullmatch(pattern, name)), None)
|
|
179
|
+
if op.startswith("$"):
|
|
180
|
+
strong.add("x86")
|
|
181
|
+
elif _X86_VECTOR_RE.fullmatch(name):
|
|
182
|
+
strong.add("x86")
|
|
183
|
+
elif prefixed and reg:
|
|
184
|
+
strong.add("x86")
|
|
185
|
+
elif not name or "(" in op or "[" in op or "{" in op:
|
|
186
|
+
continue
|
|
187
|
+
elif _A64_ARRANGEMENT_RE.fullmatch(name) or _A64_SCALAR_RE.fullmatch(name) or name == "wsp":
|
|
188
|
+
strong.add("arm")
|
|
189
|
+
elif _ARM32_SCALAR_RE.fullmatch(name):
|
|
190
|
+
strong.add("arm")
|
|
191
|
+
elif _A64_SCALAR_FP_RE.fullmatch(name):
|
|
192
|
+
# s0-s11 are also RISC-V saved registers: a weak vote there, and
|
|
193
|
+
# the mnemonic's catalog rows arbitrate between arm and riscv.
|
|
194
|
+
if name in _RISCV_SCALARS:
|
|
195
|
+
weak.update(("arm", "riscv"))
|
|
196
|
+
else:
|
|
197
|
+
strong.add("arm")
|
|
198
|
+
elif name in _RISCV_SCALARS:
|
|
199
|
+
strong.add("riscv")
|
|
200
|
+
elif _A64_VECTOR_BARE_RE.fullmatch(name):
|
|
201
|
+
weak.update(("arm", "riscv"))
|
|
202
|
+
elif name in ("sp", "fp"):
|
|
203
|
+
weak.update(("arm", "riscv"))
|
|
204
|
+
elif name == "lr":
|
|
205
|
+
weak.add("arm")
|
|
206
|
+
arch = frozenset({"x86"}) if intel or "x86" in strong else frozenset(strong or weak)
|
|
207
|
+
classes = [_operand_class(op.strip(), intel, arch) for op in parts]
|
|
208
|
+
reverse = not intel and ("x86" in arch or not arch)
|
|
209
|
+
return arch, classes, reverse
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _operand_class(op: str, intel: bool, arch: frozenset) -> str | None:
|
|
213
|
+
"""Classify one operand on a line whose candidates are arch, or None.
|
|
214
|
+
|
|
215
|
+
Classes: r8..r64, v128/v256/v512/k/mm, av (AArch64 vector with an
|
|
216
|
+
arrangement), x/w/xzr/wzr/sp/wsp (AArch64 scalars; fp and lr read as x),
|
|
217
|
+
b/h/s/d/q (AArch64 scalar FP by width), ar (ARM32), rv (RISC-V), and
|
|
218
|
+
i, lit0/lit1, m, rel. A bare symbol is a memory reference in AT&T and a
|
|
219
|
+
rel target in Intel syntax.
|
|
220
|
+
"""
|
|
221
|
+
if not op:
|
|
222
|
+
return None
|
|
223
|
+
name = op.lstrip("*").lstrip("%").casefold()
|
|
224
|
+
if re.fullmatch(r"\$?0+", op):
|
|
225
|
+
return "lit0"
|
|
226
|
+
if re.fullmatch(r"\$?0*1", op):
|
|
227
|
+
return "lit1"
|
|
228
|
+
if op.startswith("$") or re.fullmatch(r"-?(0x[0-9a-f]+|\d+)", op, re.IGNORECASE):
|
|
229
|
+
return "i"
|
|
230
|
+
if "{1to" in op:
|
|
231
|
+
return "mbc"
|
|
232
|
+
if "{" in op:
|
|
233
|
+
op = re.sub(r"\{[^}]*\}", "", op).strip()
|
|
234
|
+
name = op.lstrip("*").lstrip("%").casefold()
|
|
235
|
+
if "(" in op or "[" in op:
|
|
236
|
+
return "m"
|
|
237
|
+
reg = next((w for w, pattern in _GPR_PATTERNS if re.fullmatch(pattern, name)), None)
|
|
238
|
+
# In AT&T a bare "sp" is the ARM stack pointer or a symbol, never the r16.
|
|
239
|
+
if reg and (intel or op.lstrip("*").startswith("%") or name not in ("sp", "bp", "si", "di")):
|
|
240
|
+
if intel or arch.isdisjoint({"arm", "riscv"}):
|
|
241
|
+
return f"r{reg}"
|
|
242
|
+
if _X86_VECTOR_RE.fullmatch(name):
|
|
243
|
+
if name[0] == "k":
|
|
244
|
+
return "k"
|
|
245
|
+
if name[0] == "m":
|
|
246
|
+
return "mm"
|
|
247
|
+
return _X86_VECTOR_WIDTHS[name[:3]]
|
|
248
|
+
if not intel and op.startswith("*"):
|
|
249
|
+
return "m"
|
|
250
|
+
if "arm" in arch:
|
|
251
|
+
if _A64_ARRANGEMENT_RE.fullmatch(name) or _A64_VECTOR_BARE_RE.fullmatch(name):
|
|
252
|
+
return "av"
|
|
253
|
+
if _A64_SCALAR_FP_RE.fullmatch(name):
|
|
254
|
+
return name[0]
|
|
255
|
+
if _A64_SCALAR_RE.fullmatch(name):
|
|
256
|
+
return {"xzr": "xzr", "wzr": "wzr"}.get(name, name[0])
|
|
257
|
+
if name in ("sp", "wsp", "fp", "lr"):
|
|
258
|
+
return {"sp": "sp", "wsp": "wsp"}.get(name, "x")
|
|
259
|
+
if _ARM32_SCALAR_RE.fullmatch(name):
|
|
260
|
+
return "ar"
|
|
261
|
+
if "riscv" in arch and (name in _RISCV_SCALARS or _A64_VECTOR_BARE_RE.fullmatch(name)):
|
|
262
|
+
return "rv"
|
|
263
|
+
if "x86" in arch or not arch:
|
|
264
|
+
return "rel" if intel else "m"
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
_IMMEDIATE_ACCEPT = frozenset({"i", "lit0", "lit1"})
|
|
269
|
+
_LITERAL_ACCEPT = {frozenset({"lit0"}), frozenset({"lit1"})}
|
|
270
|
+
_CLASS_KIND = dict(
|
|
271
|
+
[(c, "register") for c in ("r8", "r16", "r32", "r64")]
|
|
272
|
+
+ [(c, "vector") for c in ("v128", "v256", "v512", "k", "mm", "av", "dfp", "pred")]
|
|
273
|
+
+ [(c, "memory") for c in ("m", "moffs", "mbc")]
|
|
274
|
+
+ [(c, "immediate") for c in ("i", "lit0", "lit1")]
|
|
275
|
+
+ [
|
|
276
|
+
(c, "scalar")
|
|
277
|
+
for c in ("x", "w", "xzr", "wzr", "sp", "wsp", "b", "h", "s", "d", "q", "ar", "rv")
|
|
278
|
+
]
|
|
279
|
+
+ [("rel", "rel")]
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _form_accept(op: str) -> frozenset:
|
|
284
|
+
"""The line classes a catalog-key operand accepts, as one frozenset.
|
|
285
|
+
|
|
286
|
+
An immediate-width key (i8..i64) takes every immediate class. A literal 0/1
|
|
287
|
+
takes only its own value: a fixed-1 shift is not the line's "2". An AArch64
|
|
288
|
+
placeholder maps to the register family it encodes (<XNSP> = x or sp).
|
|
289
|
+
"""
|
|
290
|
+
o = op.casefold()
|
|
291
|
+
if o in ("0", "1"):
|
|
292
|
+
return frozenset({"lit" + o})
|
|
293
|
+
if re.fullmatch(r"i\d+", o):
|
|
294
|
+
return _IMMEDIATE_ACCEPT
|
|
295
|
+
if o in _ACCUMULATOR or o == "cl":
|
|
296
|
+
return frozenset({_ACCUMULATOR.get(o, "r8")})
|
|
297
|
+
m = re.fullmatch(r"r(8|16|32|64)[hl]?", o)
|
|
298
|
+
if m:
|
|
299
|
+
return frozenset({"r" + m.group(1)})
|
|
300
|
+
if o in _X86_VECTOR_WIDTHS or o in ("k", "mm"):
|
|
301
|
+
return frozenset({_X86_VECTOR_WIDTHS.get(o, o)})
|
|
302
|
+
if re.fullmatch(r"m\d+_\d+to\d+", o):
|
|
303
|
+
return frozenset({"mbc"})
|
|
304
|
+
if re.fullmatch(r"m\d+", o):
|
|
305
|
+
return frozenset({"m"})
|
|
306
|
+
if re.fullmatch(r"moffs\d+", o):
|
|
307
|
+
return frozenset({"moffs"})
|
|
308
|
+
if re.fullmatch(r"rel\d*", o):
|
|
309
|
+
return frozenset({"rel"})
|
|
310
|
+
if op.startswith("["):
|
|
311
|
+
return frozenset({"m"})
|
|
312
|
+
name = re.sub(r"(#\d+)+$", "", op.upper())
|
|
313
|
+
if "<HASH>" in name:
|
|
314
|
+
return _IMMEDIATE_ACCEPT
|
|
315
|
+
if re.match(r"V\d+<|^[ZV]\d+\.|^\{|^<ZA>", name):
|
|
316
|
+
return frozenset({"av"})
|
|
317
|
+
# Numbered scalar FP placeholders: D0 or S1 style numbers, <BT> or <QT1> style aliases.
|
|
318
|
+
if re.fullmatch(r"[BHSDQ]\d+", name):
|
|
319
|
+
return frozenset({name[0].lower()})
|
|
320
|
+
if re.fullmatch(r"<[BHSDQ]T\d*>", name):
|
|
321
|
+
return frozenset({name[1].lower()})
|
|
322
|
+
if re.match(r"^D<|^<FPFAR_DT>$", name):
|
|
323
|
+
return frozenset({"dfp"})
|
|
324
|
+
if re.match(r"^P\d", name):
|
|
325
|
+
return frozenset({"pred"})
|
|
326
|
+
if "WORX" in name or "R_OPTION" in name:
|
|
327
|
+
return frozenset({"w", "x"})
|
|
328
|
+
if "WZR" in name:
|
|
329
|
+
return frozenset({"w", "wzr"})
|
|
330
|
+
if "XZR" in name:
|
|
331
|
+
return frozenset({"x", "xzr"})
|
|
332
|
+
if "WSP" in name:
|
|
333
|
+
return frozenset({"w", "wsp"})
|
|
334
|
+
if "SP" in name:
|
|
335
|
+
return frozenset({"x", "sp"})
|
|
336
|
+
return frozenset()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _match_form(
|
|
340
|
+
matches,
|
|
341
|
+
operand_count: int | None,
|
|
342
|
+
line: tuple,
|
|
343
|
+
suffix_bits: int | None,
|
|
344
|
+
):
|
|
345
|
+
"""Pick the form the line names, under one total order.
|
|
346
|
+
|
|
347
|
+
line is _classify_line's (arch candidates, ordered classes, reverse). Rows
|
|
348
|
+
of a non-candidate architecture drop out before ranking; an undecided line
|
|
349
|
+
keeps every row. Rank: class-set score (accepted +1 / same kind 0 / else
|
|
350
|
+
-1, a literal scores only on its own value), position score, iN over a
|
|
351
|
+
literal 0/1, match with the AT&T suffix width, fewer operands, key name.
|
|
352
|
+
The no-same-count fallback uses the same key.
|
|
353
|
+
"""
|
|
354
|
+
arch, classes, reverse = line
|
|
355
|
+
if arch:
|
|
356
|
+
matches = [m for m in matches if m.architecture in arch]
|
|
118
357
|
if operand_count is None:
|
|
119
358
|
return matches[0] if matches else None
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
width =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
359
|
+
same = [m for m in matches if len(_operands_of_key(m.key)) == operand_count]
|
|
360
|
+
pool = same or matches
|
|
361
|
+
if not pool:
|
|
362
|
+
return None
|
|
363
|
+
ordered = list(reversed(classes)) if reverse else classes
|
|
364
|
+
line_set = {c for c in classes if c}
|
|
365
|
+
line_kinds = {_CLASS_KIND[c] for c in line_set if c in _CLASS_KIND}
|
|
366
|
+
|
|
367
|
+
def _rank(form):
|
|
368
|
+
accepts = [_form_accept(op) for op in _operands_of_key(form.key)]
|
|
369
|
+
set_score = position = in_over_lit = width = 0
|
|
370
|
+
for acc in accepts:
|
|
371
|
+
if acc in _LITERAL_ACCEPT:
|
|
372
|
+
set_score += 0 if acc & line_set else -1
|
|
373
|
+
elif acc & line_set:
|
|
374
|
+
set_score += 1
|
|
375
|
+
elif {_CLASS_KIND[a] for a in acc} & line_kinds:
|
|
376
|
+
pass
|
|
377
|
+
else:
|
|
378
|
+
set_score -= 1
|
|
379
|
+
if any(
|
|
380
|
+
c in line_set and int(c[1:]) == suffix_bits
|
|
381
|
+
for c in acc
|
|
382
|
+
if re.fullmatch(r"[rv]\d+", c)
|
|
383
|
+
):
|
|
384
|
+
width += 1
|
|
385
|
+
for cls, acc in zip(ordered, accepts):
|
|
386
|
+
if cls in acc:
|
|
387
|
+
position += 1
|
|
388
|
+
if cls in ("i", "lit0", "lit1"):
|
|
389
|
+
if acc == _IMMEDIATE_ACCEPT:
|
|
390
|
+
in_over_lit += 1
|
|
391
|
+
elif acc in _LITERAL_ACCEPT:
|
|
392
|
+
in_over_lit -= 1
|
|
393
|
+
return (-set_score, -position, -in_over_lit, -width, len(accepts), form.key)
|
|
394
|
+
|
|
395
|
+
return min(pool, key=_rank)
|
|
141
396
|
|
|
142
397
|
|
|
143
398
|
def _hover_markdown(
|
|
@@ -146,6 +401,7 @@ def _hover_markdown(
|
|
|
146
401
|
allow_instruction: bool = True,
|
|
147
402
|
operand_count: int | None = None,
|
|
148
403
|
operand_text: str = "",
|
|
404
|
+
intel: bool = False,
|
|
149
405
|
) -> str | None:
|
|
150
406
|
intrinsic = load_intrinsic_from_db(conn, word)
|
|
151
407
|
if intrinsic is not None:
|
|
@@ -186,12 +442,22 @@ def _hover_markdown(
|
|
|
186
442
|
|
|
187
443
|
if not allow_instruction:
|
|
188
444
|
return None
|
|
445
|
+
line = _classify_line(operand_text, intel)
|
|
189
446
|
instruction = load_instruction_from_db(conn, word)
|
|
447
|
+
# An exact name can name a row with a different operand count (a bare
|
|
448
|
+
# "movsd" is the string move), or a row from the wrong architecture
|
|
449
|
+
# (a bare "nop" picks AArch64 NOP over x86 in an Intel file).
|
|
450
|
+
# In both cases, fall back to _match_form so hover and hint agree.
|
|
451
|
+
if instruction is not None:
|
|
452
|
+
if line[0] and instruction.architecture not in line[0]:
|
|
453
|
+
instruction = None
|
|
454
|
+
elif operand_count is not None and len(_operands_of_key(instruction.key)) != operand_count:
|
|
455
|
+
instruction = None
|
|
190
456
|
if instruction is None:
|
|
191
|
-
matches = load_instructions_by_mnemonic_from_db(conn, word)
|
|
192
457
|
# Match the form on the source line like _hint_label does, so
|
|
193
458
|
# "vaddps ymm0, ymm1, ymm2" gets the YMM form, not the first row.
|
|
194
|
-
|
|
459
|
+
matches, width = _lookup_mnemonic(conn, word, line[0])
|
|
460
|
+
instruction = _match_form(matches, operand_count, line, width)
|
|
195
461
|
if instruction is not None:
|
|
196
462
|
body = instruction_markdown(instruction, conn=conn)
|
|
197
463
|
links = [f"Full page: {web_deep_link('instruction', instruction.key)}"]
|
|
@@ -304,7 +570,7 @@ def _operand_count(operands: str) -> int:
|
|
|
304
570
|
|
|
305
571
|
|
|
306
572
|
def _mnemonic_from_asm_line(line: str, semicolon_is_comment: bool = False):
|
|
307
|
-
"""Return (mnemonic, operand_count) for an asm line, or None."""
|
|
573
|
+
"""Return (mnemonic, operand_count, operands) for an asm line, or None."""
|
|
308
574
|
line = ASM_COMMENT_RE.sub("", line)
|
|
309
575
|
if semicolon_is_comment:
|
|
310
576
|
line = ASM_SEMICOLON_COMMENT_RE.sub("", line)
|
|
@@ -319,7 +585,8 @@ def _mnemonic_from_asm_line(line: str, semicolon_is_comment: bool = False):
|
|
|
319
585
|
parts = line.split(None, 1)
|
|
320
586
|
if not MNEMONIC_RE.match(parts[0]):
|
|
321
587
|
return None
|
|
322
|
-
|
|
588
|
+
operands = parts[1] if len(parts) > 1 else ""
|
|
589
|
+
return parts[0], _operand_count(operands), operands
|
|
323
590
|
|
|
324
591
|
|
|
325
592
|
def _operands_of_key(key: str) -> list[str]:
|
|
@@ -333,24 +600,32 @@ def _cut_label(text: str) -> str:
|
|
|
333
600
|
|
|
334
601
|
|
|
335
602
|
@functools.lru_cache(maxsize=1024)
|
|
336
|
-
def _mnemonic_matches(db_path: str, mnemonic: str) -> tuple:
|
|
603
|
+
def _mnemonic_matches(db_path: str, mnemonic: str, arch: frozenset = frozenset()) -> tuple:
|
|
337
604
|
# The inlay-hint loop asks the same mnemonics for every line, so query once
|
|
338
|
-
# per (catalog, mnemonic) and reuse. The key carries the
|
|
339
|
-
# connection, so a reloaded catalog at the same path would
|
|
340
|
-
# clear the cache if catalog reload lands.
|
|
605
|
+
# per (catalog, mnemonic, arch candidates) and reuse. The key carries the
|
|
606
|
+
# path, not the connection, so a reloaded catalog at the same path would
|
|
607
|
+
# keep stale rows; clear the cache if catalog reload lands.
|
|
341
608
|
conn = open_db(path=Path(db_path))
|
|
342
609
|
try:
|
|
343
|
-
|
|
610
|
+
forms, width = _lookup_mnemonic(conn, mnemonic, arch)
|
|
611
|
+
return tuple(forms), width
|
|
344
612
|
finally:
|
|
345
613
|
conn.close()
|
|
346
614
|
|
|
347
615
|
|
|
348
|
-
def _hint_label(
|
|
349
|
-
|
|
616
|
+
def _hint_label(
|
|
617
|
+
db_path: str,
|
|
618
|
+
mnemonic: str,
|
|
619
|
+
operand_count: int,
|
|
620
|
+
operands: str = "",
|
|
621
|
+
intel: bool = False,
|
|
622
|
+
) -> str | None:
|
|
623
|
+
line = _classify_line(operands, intel)
|
|
624
|
+
matches, width = _mnemonic_matches(db_path, mnemonic, line[0])
|
|
350
625
|
if not matches:
|
|
351
626
|
return None
|
|
352
|
-
chosen = _match_form(matches, operand_count)
|
|
353
|
-
return _cut_label(chosen.summary or "")
|
|
627
|
+
chosen = _match_form(matches, operand_count, line, width)
|
|
628
|
+
return _cut_label(chosen.summary or "") if chosen else None
|
|
354
629
|
|
|
355
630
|
|
|
356
631
|
def _inlay_hints(conn, text: str, language_id: str, uri: str, start: int, end: int) -> list[dict]:
|
|
@@ -371,7 +646,7 @@ def _inlay_hints(conn, text: str, language_id: str, uri: str, start: int, end: i
|
|
|
371
646
|
if not start <= line <= end:
|
|
372
647
|
continue
|
|
373
648
|
parsed = _mnemonic_from_asm_line(segment, semicolon_is_comment)
|
|
374
|
-
label = _hint_label(db_path, *parsed) if parsed else None
|
|
649
|
+
label = _hint_label(db_path, *parsed, intel=semicolon_is_comment) if parsed else None
|
|
375
650
|
if label is not None:
|
|
376
651
|
hints.setdefault(line, []).append(label)
|
|
377
652
|
return [
|
|
@@ -454,7 +729,8 @@ def main() -> int:
|
|
|
454
729
|
line, character = params["position"]["line"], params["position"]["character"]
|
|
455
730
|
word = _word_at(text, line, character)
|
|
456
731
|
# In C/C++ only the asm strings hold instructions; intrinsics hover everywhere.
|
|
457
|
-
|
|
732
|
+
language_id = session.languages.get(uri, "")
|
|
733
|
+
is_c_doc = _is_c_doc(language_id, uri)
|
|
458
734
|
allow_instruction = not is_c_doc or (_in_asm_string(text, line, character))
|
|
459
735
|
operand_count = None
|
|
460
736
|
operand_text = ""
|
|
@@ -462,16 +738,15 @@ def main() -> int:
|
|
|
462
738
|
offset = sum(len(item) + 1 for item in text.split("\n")[:line]) + _code_point_index(
|
|
463
739
|
text.split("\n")[line], character
|
|
464
740
|
)
|
|
465
|
-
language_id = session.languages.get(uri, "")
|
|
466
741
|
segment = _asm_line_at(text, language_id, uri, offset)
|
|
467
742
|
if segment:
|
|
468
|
-
operand_text = segment
|
|
469
743
|
# The C-escape pair "\t" starts a segment in asm("...\n\t...").
|
|
470
744
|
# Strip the backslash so the mnemonic survives.
|
|
471
745
|
parsed = _mnemonic_from_asm_line(
|
|
472
746
|
segment.lstrip("\\"), _semicolon_is_comment(language_id, uri)
|
|
473
747
|
)
|
|
474
748
|
if parsed:
|
|
749
|
+
operand_text = parsed[2]
|
|
475
750
|
if parsed[0].casefold() == word.casefold():
|
|
476
751
|
operand_count = parsed[1]
|
|
477
752
|
elif word.casefold().endswith(parsed[0].casefold()):
|
|
@@ -487,6 +762,7 @@ def main() -> int:
|
|
|
487
762
|
allow_instruction,
|
|
488
763
|
operand_count=operand_count,
|
|
489
764
|
operand_text=operand_text,
|
|
765
|
+
intel=_semicolon_is_comment(language_id, uri),
|
|
490
766
|
)
|
|
491
767
|
if conn and word
|
|
492
768
|
else None
|
|
@@ -150,6 +150,12 @@ _INITIAL_RESULT_BATCH = 50
|
|
|
150
150
|
_RESULT_BATCH_SIZE = 10
|
|
151
151
|
_RESULT_PREFETCH_THRESHOLD = 5
|
|
152
152
|
|
|
153
|
+
|
|
154
|
+
def _NO_DATA(text: str) -> tuple[str, str]:
|
|
155
|
+
"""Detail line that marks a section with no data, instead of omitting it."""
|
|
156
|
+
return ("static_markup", f" [dim]{text}[/]")
|
|
157
|
+
|
|
158
|
+
|
|
153
159
|
_ISA_FAMILIES: list[str] = [
|
|
154
160
|
name for name, _ in sorted(ISA_FAMILY_ORDER.items(), key=lambda item: item[1])
|
|
155
161
|
]
|
|
@@ -1781,6 +1787,8 @@ class SimdrefApp(App):
|
|
|
1781
1787
|
if linked:
|
|
1782
1788
|
self._append_operand_section(payload, linked)
|
|
1783
1789
|
self._append_perf_sections(payload, linked)
|
|
1790
|
+
else:
|
|
1791
|
+
payload.append(_NO_DATA("no linked instruction: no operands or performance data"))
|
|
1784
1792
|
if intrinsic.doc_sections:
|
|
1785
1793
|
self._append_description_sections(payload, intrinsic.doc_sections)
|
|
1786
1794
|
if linked and linked.description:
|
|
@@ -1816,6 +1824,7 @@ class SimdrefApp(App):
|
|
|
1816
1824
|
|
|
1817
1825
|
def _append_operand_section(self, payload: list[tuple], item: InstructionRecord) -> None:
|
|
1818
1826
|
if not item.operand_details:
|
|
1827
|
+
payload.append(_NO_DATA("operands: no data"))
|
|
1819
1828
|
return
|
|
1820
1829
|
table = Table(header_style="bold blue")
|
|
1821
1830
|
table.add_column("idx", width=4)
|
|
@@ -1844,6 +1853,8 @@ class SimdrefApp(App):
|
|
|
1844
1853
|
payload.append(
|
|
1845
1854
|
("static_markup", f" [green]latency:[/] {lat} [green]CPI:[/] {cpi}")
|
|
1846
1855
|
)
|
|
1856
|
+
else:
|
|
1857
|
+
payload.append(_NO_DATA("performance: no data"))
|
|
1847
1858
|
return
|
|
1848
1859
|
label_map = {
|
|
1849
1860
|
"uarch": "microarch",
|