simdref 0.0.11__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.
Files changed (81) hide show
  1. {simdref-0.0.11/src/simdref.egg-info → simdref-0.0.13}/PKG-INFO +6 -6
  2. {simdref-0.0.11 → simdref-0.0.13}/README.md +5 -5
  3. {simdref-0.0.11 → simdref-0.0.13}/pyproject.toml +1 -1
  4. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/cli.py +1 -3
  5. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/ingest_catalog.py +6 -3
  6. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/lsp.py +322 -41
  7. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/tui.py +11 -0
  8. {simdref-0.0.11 → simdref-0.0.13/src/simdref.egg-info}/PKG-INFO +6 -6
  9. {simdref-0.0.11 → simdref-0.0.13}/tests/test_auto_update_on_version_change.py +5 -4
  10. simdref-0.0.13/tests/test_lsp_stdio.py +872 -0
  11. {simdref-0.0.11 → simdref-0.0.13}/tests/test_search.py +17 -1
  12. {simdref-0.0.11 → simdref-0.0.13}/tests/test_tui.py +47 -0
  13. simdref-0.0.11/tests/test_lsp_stdio.py +0 -450
  14. {simdref-0.0.11 → simdref-0.0.13}/LICENSE +0 -0
  15. {simdref-0.0.11 → simdref-0.0.13}/setup.cfg +0 -0
  16. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/__init__.py +0 -0
  17. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/__main__.py +0 -0
  18. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/annotate.py +0 -0
  19. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/arm_instructions.py +0 -0
  20. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/display.py +0 -0
  21. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/export.py +0 -0
  22. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/filters.py +0 -0
  23. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/ingest.py +0 -0
  24. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/ingest_pdf.py +0 -0
  25. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/ingest_sources.py +0 -0
  26. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/manpages.py +0 -0
  27. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/models.py +0 -0
  28. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/pdfparse/__init__.py +0 -0
  29. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/pdfparse/base.py +0 -0
  30. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/pdfparse/intel.py +0 -0
  31. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/pdfparse/registry.py +0 -0
  32. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/pdfparse/types.py +0 -0
  33. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/pdfrefs.py +0 -0
  34. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/perf.py +0 -0
  35. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/perf_sources/__init__.py +0 -0
  36. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/perf_sources/cores.py +0 -0
  37. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/perf_sources/llvm_mca.py +0 -0
  38. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/perf_sources/llvm_scheduling.py +0 -0
  39. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/perf_sources/merge.py +0 -0
  40. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/queries.py +0 -0
  41. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/riscv.py +0 -0
  42. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/search.py +0 -0
  43. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/storage.py +0 -0
  44. {simdref-0.0.11 → simdref-0.0.13}/src/simdref/ui_labels.py +0 -0
  45. {simdref-0.0.11 → simdref-0.0.13}/src/simdref.egg-info/SOURCES.txt +0 -0
  46. {simdref-0.0.11 → simdref-0.0.13}/src/simdref.egg-info/dependency_links.txt +0 -0
  47. {simdref-0.0.11 → simdref-0.0.13}/src/simdref.egg-info/entry_points.txt +0 -0
  48. {simdref-0.0.11 → simdref-0.0.13}/src/simdref.egg-info/requires.txt +0 -0
  49. {simdref-0.0.11 → simdref-0.0.13}/src/simdref.egg-info/top_level.txt +0 -0
  50. {simdref-0.0.11 → simdref-0.0.13}/tests/test_annotate.py +0 -0
  51. {simdref-0.0.11 → simdref-0.0.13}/tests/test_annotate_positions.py +0 -0
  52. {simdref-0.0.11 → simdref-0.0.13}/tests/test_audit_coverage.py +0 -0
  53. {simdref-0.0.11 → simdref-0.0.13}/tests/test_cli_bare_query.py +0 -0
  54. {simdref-0.0.11 → simdref-0.0.13}/tests/test_cli_bootstrap_progress.py +0 -0
  55. {simdref-0.0.11 → simdref-0.0.13}/tests/test_cli_help.py +0 -0
  56. {simdref-0.0.11 → simdref-0.0.13}/tests/test_cli_llm.py +0 -0
  57. {simdref-0.0.11 → simdref-0.0.13}/tests/test_coverage_live.py +0 -0
  58. {simdref-0.0.11 → simdref-0.0.13}/tests/test_coverage_parity.py +0 -0
  59. {simdref-0.0.11 → simdref-0.0.13}/tests/test_display.py +0 -0
  60. {simdref-0.0.11 → simdref-0.0.13}/tests/test_export.py +0 -0
  61. {simdref-0.0.11 → simdref-0.0.13}/tests/test_filters.py +0 -0
  62. {simdref-0.0.11 → simdref-0.0.13}/tests/test_ingest_sources.py +0 -0
  63. {simdref-0.0.11 → simdref-0.0.13}/tests/test_intel_operation_and_url.py +0 -0
  64. {simdref-0.0.11 → simdref-0.0.13}/tests/test_issue2_fixes.py +0 -0
  65. {simdref-0.0.11 → simdref-0.0.13}/tests/test_issues_23_24_25.py +0 -0
  66. {simdref-0.0.11 → simdref-0.0.13}/tests/test_lsp_web.py +0 -0
  67. {simdref-0.0.11 → simdref-0.0.13}/tests/test_models.py +0 -0
  68. {simdref-0.0.11 → simdref-0.0.13}/tests/test_pdfparse.py +0 -0
  69. {simdref-0.0.11 → simdref-0.0.13}/tests/test_perf.py +0 -0
  70. {simdref-0.0.11 → simdref-0.0.13}/tests/test_perf_sources.py +0 -0
  71. {simdref-0.0.11 → simdref-0.0.13}/tests/test_presentation.py +0 -0
  72. {simdref-0.0.11 → simdref-0.0.13}/tests/test_preset_persistence.py +0 -0
  73. {simdref-0.0.11 → simdref-0.0.13}/tests/test_riscv_docs_pages.py +0 -0
  74. {simdref-0.0.11 → simdref-0.0.13}/tests/test_search_pushdown.py +0 -0
  75. {simdref-0.0.11 → simdref-0.0.13}/tests/test_source_kind_filter.py +0 -0
  76. {simdref-0.0.11 → simdref-0.0.13}/tests/test_source_validation.py +0 -0
  77. {simdref-0.0.11 → simdref-0.0.13}/tests/test_storage_payload.py +0 -0
  78. {simdref-0.0.11 → simdref-0.0.13}/tests/test_storage_schema.py +0 -0
  79. {simdref-0.0.11 → simdref-0.0.13}/tests/test_ui_labels_parity.py +0 -0
  80. {simdref-0.0.11 → simdref-0.0.13}/tests/test_version_matches_pyproject.py +0 -0
  81. {simdref-0.0.11 → 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.11
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 need `llvm-mca`. Only `isa build` needs
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 over stdio:
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 emits JSON and NDJSON with exit codes 0 (match), 1 (bad
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, rendered on demand |
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` | Emit the FilterSpec or stream matching catalog entries |
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 need `llvm-mca`. Only `isa build` needs
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 over stdio:
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 emits JSON and NDJSON with exit codes 0 (match), 1 (bad
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, rendered on demand |
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` | Emit the FilterSpec or stream matching catalog entries |
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.11"
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
- # Stamp anyway so we don't retry on every invocation; the user has
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
- return "i32"
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(child.attrib.get("xtype", "").strip())
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())
@@ -99,6 +99,11 @@ def _code_point_index(line: str, character: int) -> int:
99
99
  return index
100
100
 
101
101
 
102
+ def _utf16_column(line: str, index: int) -> int:
103
+ # The reverse of _code_point_index: a code-point index to a UTF-16 column.
104
+ return sum(2 if ord(c) > 0xFFFF else 1 for c in line[:index])
105
+
106
+
102
107
  def _line_prefix(text: str, line: int, character: int) -> str:
103
108
  lines = text.splitlines()
104
109
  if line >= len(lines):
@@ -108,31 +113,286 @@ def _line_prefix(text: str, line: int, character: int) -> str:
108
113
  return match.group(0) if match else ""
109
114
 
110
115
 
111
- def _match_form(matches, operand_count: int | None = None, operand_text: str = ""):
112
- """Pick the same form the line names: exact operand count, then width, then smallest."""
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]
113
357
  if operand_count is None:
114
358
  return matches[0] if matches else None
115
- # ponytail: a line has no architecture, so take the first form with the same operand count.
116
- # The fallback takes the first form with fewest operands, never a form with more
117
- # operands than the line: a zero-operand "movsd" must not get the MOVSD_XMM hint.
118
- same_count = [m for m in matches if len(_operands_of_key(m.key)) == operand_count]
119
- if not same_count:
120
- return min(matches, key=lambda m: len(_operands_of_key(m.key)), default=None)
121
- # Score candidates by how many operands agree with the register width the
122
- # line names ("ymm0" vs "YMM, YMM, M128"). Skip the tiebreak on a tie.
123
- tokens = re.findall(r"\b(zmm|ymm|xmm|m512|m256|m128|m64|m32)\d*\b", operand_text.casefold())
124
- if tokens:
125
- width = tokens[0].rstrip("0123456789")
126
- if width.startswith(("x", "y", "z")):
127
-
128
- def _score(item):
129
- operands = [op.casefold().rstrip("0123456789") for op in _operands_of_key(item.key)]
130
- return sum(1 for op in operands if op == width)
131
-
132
- scored = sorted(same_count, key=lambda m: (-_score(m), m.key))
133
- if _score(scored[0]) > _score(scored[-1]):
134
- return scored[0]
135
- return same_count[0]
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)
136
396
 
137
397
 
138
398
  def _hover_markdown(
@@ -141,6 +401,7 @@ def _hover_markdown(
141
401
  allow_instruction: bool = True,
142
402
  operand_count: int | None = None,
143
403
  operand_text: str = "",
404
+ intel: bool = False,
144
405
  ) -> str | None:
145
406
  intrinsic = load_intrinsic_from_db(conn, word)
146
407
  if intrinsic is not None:
@@ -181,12 +442,22 @@ def _hover_markdown(
181
442
 
182
443
  if not allow_instruction:
183
444
  return None
445
+ line = _classify_line(operand_text, intel)
184
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
185
456
  if instruction is None:
186
- matches = load_instructions_by_mnemonic_from_db(conn, word)
187
457
  # Match the form on the source line like _hint_label does, so
188
458
  # "vaddps ymm0, ymm1, ymm2" gets the YMM form, not the first row.
189
- instruction = _match_form(matches, operand_count, operand_text)
459
+ matches, width = _lookup_mnemonic(conn, word, line[0])
460
+ instruction = _match_form(matches, operand_count, line, width)
190
461
  if instruction is not None:
191
462
  body = instruction_markdown(instruction, conn=conn)
192
463
  links = [f"Full page: {web_deep_link('instruction', instruction.key)}"]
@@ -299,7 +570,7 @@ def _operand_count(operands: str) -> int:
299
570
 
300
571
 
301
572
  def _mnemonic_from_asm_line(line: str, semicolon_is_comment: bool = False):
302
- """Return (mnemonic, operand_count) for an asm line, or None."""
573
+ """Return (mnemonic, operand_count, operands) for an asm line, or None."""
303
574
  line = ASM_COMMENT_RE.sub("", line)
304
575
  if semicolon_is_comment:
305
576
  line = ASM_SEMICOLON_COMMENT_RE.sub("", line)
@@ -314,7 +585,8 @@ def _mnemonic_from_asm_line(line: str, semicolon_is_comment: bool = False):
314
585
  parts = line.split(None, 1)
315
586
  if not MNEMONIC_RE.match(parts[0]):
316
587
  return None
317
- return parts[0], _operand_count(parts[1] if len(parts) > 1 else "")
588
+ operands = parts[1] if len(parts) > 1 else ""
589
+ return parts[0], _operand_count(operands), operands
318
590
 
319
591
 
320
592
  def _operands_of_key(key: str) -> list[str]:
@@ -328,24 +600,32 @@ def _cut_label(text: str) -> str:
328
600
 
329
601
 
330
602
  @functools.lru_cache(maxsize=1024)
331
- def _mnemonic_matches(db_path: str, mnemonic: str) -> tuple:
603
+ def _mnemonic_matches(db_path: str, mnemonic: str, arch: frozenset = frozenset()) -> tuple:
332
604
  # The inlay-hint loop asks the same mnemonics for every line, so query once
333
- # per (catalog, mnemonic) and reuse. The key carries the path, not the
334
- # connection, so a reloaded catalog at the same path would keep stale rows;
335
- # 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.
336
608
  conn = open_db(path=Path(db_path))
337
609
  try:
338
- return tuple(load_instructions_by_mnemonic_from_db(conn, mnemonic))
610
+ forms, width = _lookup_mnemonic(conn, mnemonic, arch)
611
+ return tuple(forms), width
339
612
  finally:
340
613
  conn.close()
341
614
 
342
615
 
343
- def _hint_label(db_path: str, mnemonic: str, operand_count: int) -> str | None:
344
- matches = _mnemonic_matches(db_path, mnemonic)
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])
345
625
  if not matches:
346
626
  return None
347
- chosen = _match_form(matches, operand_count)
348
- 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
349
629
 
350
630
 
351
631
  def _inlay_hints(conn, text: str, language_id: str, uri: str, start: int, end: int) -> list[dict]:
@@ -366,7 +646,7 @@ def _inlay_hints(conn, text: str, language_id: str, uri: str, start: int, end: i
366
646
  if not start <= line <= end:
367
647
  continue
368
648
  parsed = _mnemonic_from_asm_line(segment, semicolon_is_comment)
369
- 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
370
650
  if label is not None:
371
651
  hints.setdefault(line, []).append(label)
372
652
  return [
@@ -374,7 +654,7 @@ def _inlay_hints(conn, text: str, language_id: str, uri: str, start: int, end: i
374
654
  # LSP columns count UTF-16 code units.
375
655
  "position": {
376
656
  "line": line,
377
- "character": len(source_lines[line].encode("utf-16-le")) // 2,
657
+ "character": _utf16_column(source_lines[line], len(source_lines[line])),
378
658
  },
379
659
  "label": _cut_label("; ".join(hints[line])),
380
660
  "paddingLeft": True,
@@ -449,7 +729,8 @@ def main() -> int:
449
729
  line, character = params["position"]["line"], params["position"]["character"]
450
730
  word = _word_at(text, line, character)
451
731
  # In C/C++ only the asm strings hold instructions; intrinsics hover everywhere.
452
- is_c_doc = _is_c_doc(session.languages.get(uri, ""), uri)
732
+ language_id = session.languages.get(uri, "")
733
+ is_c_doc = _is_c_doc(language_id, uri)
453
734
  allow_instruction = not is_c_doc or (_in_asm_string(text, line, character))
454
735
  operand_count = None
455
736
  operand_text = ""
@@ -457,16 +738,15 @@ def main() -> int:
457
738
  offset = sum(len(item) + 1 for item in text.split("\n")[:line]) + _code_point_index(
458
739
  text.split("\n")[line], character
459
740
  )
460
- language_id = session.languages.get(uri, "")
461
741
  segment = _asm_line_at(text, language_id, uri, offset)
462
742
  if segment:
463
- operand_text = segment
464
743
  # The C-escape pair "\t" starts a segment in asm("...\n\t...").
465
744
  # Strip the backslash so the mnemonic survives.
466
745
  parsed = _mnemonic_from_asm_line(
467
746
  segment.lstrip("\\"), _semicolon_is_comment(language_id, uri)
468
747
  )
469
748
  if parsed:
749
+ operand_text = parsed[2]
470
750
  if parsed[0].casefold() == word.casefold():
471
751
  operand_count = parsed[1]
472
752
  elif word.casefold().endswith(parsed[0].casefold()):
@@ -482,6 +762,7 @@ def main() -> int:
482
762
  allow_instruction,
483
763
  operand_count=operand_count,
484
764
  operand_text=operand_text,
765
+ intel=_semicolon_is_comment(language_id, uri),
485
766
  )
486
767
  if conn and word
487
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",