simdref 0.0.8__tar.gz → 0.0.9__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 (80) hide show
  1. {simdref-0.0.8/src/simdref.egg-info → simdref-0.0.9}/PKG-INFO +1 -1
  2. {simdref-0.0.8 → simdref-0.0.9}/pyproject.toml +1 -1
  3. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/display.py +63 -0
  4. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/lsp.py +74 -36
  5. {simdref-0.0.8 → simdref-0.0.9/src/simdref.egg-info}/PKG-INFO +1 -1
  6. {simdref-0.0.8 → simdref-0.0.9}/tests/test_lsp_stdio.py +64 -2
  7. {simdref-0.0.8 → simdref-0.0.9}/tests/test_lsp_web.py +1 -1
  8. {simdref-0.0.8 → simdref-0.0.9}/LICENSE +0 -0
  9. {simdref-0.0.8 → simdref-0.0.9}/README.md +0 -0
  10. {simdref-0.0.8 → simdref-0.0.9}/setup.cfg +0 -0
  11. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/__init__.py +0 -0
  12. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/__main__.py +0 -0
  13. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/annotate.py +0 -0
  14. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/arm_instructions.py +0 -0
  15. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/cli.py +0 -0
  16. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/export.py +0 -0
  17. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/filters.py +0 -0
  18. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/ingest.py +0 -0
  19. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/ingest_catalog.py +0 -0
  20. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/ingest_pdf.py +0 -0
  21. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/ingest_sources.py +0 -0
  22. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/manpages.py +0 -0
  23. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/models.py +0 -0
  24. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/pdfparse/__init__.py +0 -0
  25. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/pdfparse/base.py +0 -0
  26. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/pdfparse/intel.py +0 -0
  27. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/pdfparse/registry.py +0 -0
  28. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/pdfparse/types.py +0 -0
  29. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/pdfrefs.py +0 -0
  30. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/perf.py +0 -0
  31. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/perf_sources/__init__.py +0 -0
  32. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/perf_sources/cores.py +0 -0
  33. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/perf_sources/llvm_mca.py +0 -0
  34. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/perf_sources/llvm_scheduling.py +0 -0
  35. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/perf_sources/merge.py +0 -0
  36. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/queries.py +0 -0
  37. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/riscv.py +0 -0
  38. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/search.py +0 -0
  39. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/storage.py +0 -0
  40. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/tui.py +0 -0
  41. {simdref-0.0.8 → simdref-0.0.9}/src/simdref/ui_labels.py +0 -0
  42. {simdref-0.0.8 → simdref-0.0.9}/src/simdref.egg-info/SOURCES.txt +0 -0
  43. {simdref-0.0.8 → simdref-0.0.9}/src/simdref.egg-info/dependency_links.txt +0 -0
  44. {simdref-0.0.8 → simdref-0.0.9}/src/simdref.egg-info/entry_points.txt +0 -0
  45. {simdref-0.0.8 → simdref-0.0.9}/src/simdref.egg-info/requires.txt +0 -0
  46. {simdref-0.0.8 → simdref-0.0.9}/src/simdref.egg-info/top_level.txt +0 -0
  47. {simdref-0.0.8 → simdref-0.0.9}/tests/test_annotate.py +0 -0
  48. {simdref-0.0.8 → simdref-0.0.9}/tests/test_annotate_positions.py +0 -0
  49. {simdref-0.0.8 → simdref-0.0.9}/tests/test_audit_coverage.py +0 -0
  50. {simdref-0.0.8 → simdref-0.0.9}/tests/test_auto_update_on_version_change.py +0 -0
  51. {simdref-0.0.8 → simdref-0.0.9}/tests/test_cli_bare_query.py +0 -0
  52. {simdref-0.0.8 → simdref-0.0.9}/tests/test_cli_bootstrap_progress.py +0 -0
  53. {simdref-0.0.8 → simdref-0.0.9}/tests/test_cli_help.py +0 -0
  54. {simdref-0.0.8 → simdref-0.0.9}/tests/test_cli_llm.py +0 -0
  55. {simdref-0.0.8 → simdref-0.0.9}/tests/test_coverage_live.py +0 -0
  56. {simdref-0.0.8 → simdref-0.0.9}/tests/test_coverage_parity.py +0 -0
  57. {simdref-0.0.8 → simdref-0.0.9}/tests/test_display.py +0 -0
  58. {simdref-0.0.8 → simdref-0.0.9}/tests/test_export.py +0 -0
  59. {simdref-0.0.8 → simdref-0.0.9}/tests/test_filters.py +0 -0
  60. {simdref-0.0.8 → simdref-0.0.9}/tests/test_ingest_sources.py +0 -0
  61. {simdref-0.0.8 → simdref-0.0.9}/tests/test_intel_operation_and_url.py +0 -0
  62. {simdref-0.0.8 → simdref-0.0.9}/tests/test_issue2_fixes.py +0 -0
  63. {simdref-0.0.8 → simdref-0.0.9}/tests/test_issues_23_24_25.py +0 -0
  64. {simdref-0.0.8 → simdref-0.0.9}/tests/test_models.py +0 -0
  65. {simdref-0.0.8 → simdref-0.0.9}/tests/test_pdfparse.py +0 -0
  66. {simdref-0.0.8 → simdref-0.0.9}/tests/test_perf.py +0 -0
  67. {simdref-0.0.8 → simdref-0.0.9}/tests/test_perf_sources.py +0 -0
  68. {simdref-0.0.8 → simdref-0.0.9}/tests/test_presentation.py +0 -0
  69. {simdref-0.0.8 → simdref-0.0.9}/tests/test_preset_persistence.py +0 -0
  70. {simdref-0.0.8 → simdref-0.0.9}/tests/test_riscv_docs_pages.py +0 -0
  71. {simdref-0.0.8 → simdref-0.0.9}/tests/test_search.py +0 -0
  72. {simdref-0.0.8 → simdref-0.0.9}/tests/test_search_pushdown.py +0 -0
  73. {simdref-0.0.8 → simdref-0.0.9}/tests/test_source_kind_filter.py +0 -0
  74. {simdref-0.0.8 → simdref-0.0.9}/tests/test_source_validation.py +0 -0
  75. {simdref-0.0.8 → simdref-0.0.9}/tests/test_storage_payload.py +0 -0
  76. {simdref-0.0.8 → simdref-0.0.9}/tests/test_storage_schema.py +0 -0
  77. {simdref-0.0.8 → simdref-0.0.9}/tests/test_tui.py +0 -0
  78. {simdref-0.0.8 → simdref-0.0.9}/tests/test_ui_labels_parity.py +0 -0
  79. {simdref-0.0.8 → simdref-0.0.9}/tests/test_version_matches_pyproject.py +0 -0
  80. {simdref-0.0.8 → simdref-0.0.9}/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.8
3
+ Version: 0.0.9
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simdref"
7
- version = "0.0.8"
7
+ version = "0.0.9"
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"
@@ -9,6 +9,7 @@ delegate to these functions for all Rich-based output.
9
9
  from __future__ import annotations
10
10
 
11
11
  import re
12
+ import urllib.parse
12
13
  from typing import TYPE_CHECKING
13
14
 
14
15
  from rich.console import Console
@@ -480,6 +481,68 @@ def isa_visible(values: list[str], show_fp16: bool = False) -> bool:
480
481
  # ---------------------------------------------------------------------------
481
482
 
482
483
 
484
+ _WEB_APP_URL = "https://simdref.diamondinoia.com"
485
+
486
+
487
+ def web_deep_link(kind: str, key: str) -> str:
488
+ """Deep link into the simdref web app for a record key."""
489
+ # Match the web app's encodeURIComponent(entry.key): parens stay literal.
490
+ return f"{_WEB_APP_URL}/#{urllib.parse.quote(str(key), safe='()')}"
491
+
492
+
493
+ def instruction_markdown(item, conn=None) -> str:
494
+ """Render the full instruction page as Markdown (hover docs, exports).
495
+
496
+ Carries the same data as :func:`render_instruction` prints to the
497
+ terminal: metadata, every description section, the intrinsic mapping
498
+ and the per-microarchitecture performance table, plus reference URLs.
499
+ """
500
+ lines = [f"```asm\n{item.key}\n```"]
501
+ lines.append(f"**isa.** {display_isa(item.isa)}")
502
+ for key, value in instruction_metadata_rows(item):
503
+ lines.append(f"**{key}.** {value}")
504
+ if item.description:
505
+ shown: set[str] = set()
506
+ for section in _DESCRIPTION_ORDER:
507
+ if section in item.description:
508
+ lines.append(f"## {section}\n\n{item.description[section].strip()}")
509
+ shown.add(section)
510
+ for section, body in item.description.items():
511
+ if section not in shown:
512
+ lines.append(f"## {section}\n\n{body.strip()}")
513
+ if item.linked_intrinsics:
514
+ lines.append(
515
+ "## instruction to intrinsic mapping\n\n"
516
+ + "\n".join(f"- `{name}`" for name in item.linked_intrinsics)
517
+ )
518
+ lines.extend(_perf_markdown_lines(measurement_rows(item)))
519
+ return "\n\n".join(lines)
520
+
521
+
522
+ def _perf_markdown_lines(rows: list[dict]) -> list[str]:
523
+ """Markdown table for measurement rows, one section per source kind."""
524
+ sections: list[str] = []
525
+ for kind, group in split_perf_rows(rows):
526
+ group = [row for row in group if row]
527
+ columns = [
528
+ column
529
+ for column in _MEASUREMENT_PREFERRED_ORDER
530
+ if column not in _MEASUREMENT_EXCLUDE_KEYS and any(column in row for row in group)
531
+ ]
532
+ if not columns:
533
+ continue
534
+ header = [_GENERIC_TABLE_LABEL_MAP.get(column, column) for column in columns]
535
+ table = ["| " + " | ".join(header) + " |", "|" + "---|" * len(columns)]
536
+ for row in sorted(group, key=lambda r: uarch_sort_key(r.get("uarch", ""))):
537
+ cells = []
538
+ for column in columns:
539
+ value = row.get(column, "-")
540
+ cells.append(display_uarch(str(value)) if column == "uarch" else str(value))
541
+ table.append("| " + " | ".join(cells) + " |")
542
+ sections.append(f"## {perf_panel_title(kind)}\n\n" + "\n".join(table))
543
+ return sections
544
+
545
+
483
546
  def canonical_url(path: str) -> str:
484
547
  if not path:
485
548
  return ""
@@ -9,6 +9,7 @@ import sys
9
9
  from dataclasses import dataclass, field
10
10
  from pathlib import Path
11
11
 
12
+ from simdref.display import canonical_url, instruction_markdown, web_deep_link
12
13
  from simdref.perf import best_cpi, best_latency
13
14
  from simdref.queries import linked_instruction_records
14
15
  from simdref.search import search_records
@@ -97,7 +98,40 @@ def _line_prefix(text: str, line: int, character: int) -> str:
97
98
  return match.group(0) if match else ""
98
99
 
99
100
 
100
- def _hover_markdown(conn, word: str, allow_instruction: bool = True) -> str | None:
101
+ def _match_form(matches, operand_count: int | None = None, operand_text: str = ""):
102
+ """Pick the same form the line names: exact operand count, then width, then smallest."""
103
+ if operand_count is None:
104
+ return matches[0] if matches else None
105
+ # ponytail: a line has no architecture, so take the first form with the same operand count.
106
+ # The fallback takes the first form with fewest operands, never a form with more
107
+ # operands than the line: a zero-operand "movsd" must not get the MOVSD_XMM hint.
108
+ same_count = [m for m in matches if len(_operands_of_key(m.key)) == operand_count]
109
+ if not same_count:
110
+ return min(matches, key=lambda m: len(_operands_of_key(m.key)), default=None)
111
+ # Score candidates by how many operands agree with the register width the
112
+ # line names ("ymm0" vs "YMM, YMM, M128"). Skip the tiebreak on a tie.
113
+ tokens = re.findall(r"\b(zmm|ymm|xmm|m512|m256|m128|m64|m32)\d*\b", operand_text.casefold())
114
+ if tokens:
115
+ width = tokens[0].rstrip("0123456789")
116
+ if width.startswith(("x", "y", "z")):
117
+
118
+ def _score(item):
119
+ operands = [op.casefold().rstrip("0123456789") for op in _operands_of_key(item.key)]
120
+ return sum(1 for op in operands if op == width)
121
+
122
+ scored = sorted(same_count, key=lambda m: (-_score(m), m.key))
123
+ if _score(scored[0]) > _score(scored[-1]):
124
+ return scored[0]
125
+ return same_count[0]
126
+
127
+
128
+ def _hover_markdown(
129
+ conn,
130
+ word: str,
131
+ allow_instruction: bool = True,
132
+ operand_count: int | None = None,
133
+ operand_text: str = "",
134
+ ) -> str | None:
101
135
  intrinsic = load_intrinsic_from_db(conn, word)
102
136
  if intrinsic is not None:
103
137
  lines = [f"```c\n{intrinsic.signature}\n```"]
@@ -139,32 +173,18 @@ def _hover_markdown(conn, word: str, allow_instruction: bool = True) -> str | No
139
173
  return None
140
174
  instruction = load_instruction_from_db(conn, word)
141
175
  if instruction is None:
142
- # Catalog keys look like "VADDPS (XMM, XMM, XMM)", so a bare mnemonic needs this lookup.
143
176
  matches = load_instructions_by_mnemonic_from_db(conn, word)
144
- instruction = matches[0] if matches else None
177
+ # Match the form on the source line like _hint_label does, so
178
+ # "vaddps ymm0, ymm1, ymm2" gets the YMM form, not the first row.
179
+ instruction = _match_form(matches, operand_count, operand_text)
145
180
  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)
181
+ body = instruction_markdown(instruction, conn=conn)
182
+ links = [f"Full page: {web_deep_link('instruction', instruction.key)}"]
183
+ if instruction.metadata.get("url"):
184
+ links.append(canonical_url(instruction.metadata["url"]))
185
+ if instruction.metadata.get("url-ref"):
186
+ links.append(canonical_url(instruction.metadata["url-ref"]))
187
+ return body + "\n\n" + "\n".join(links)
168
188
  return None
169
189
 
170
190
 
@@ -275,13 +295,7 @@ def _hint_label(db_path: str, mnemonic: str, operand_count: int) -> str | None:
275
295
  matches = _mnemonic_matches(db_path, mnemonic)
276
296
  if not matches:
277
297
  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
- )
298
+ chosen = _match_form(matches, operand_count)
285
299
  return _cut_label(chosen.summary or "")
286
300
 
287
301
 
@@ -289,7 +303,13 @@ def _inlay_hints(conn, text: str, language_id: str, uri: str, start: int, end: i
289
303
  db_path = conn.execute("PRAGMA database_list").fetchone()[2]
290
304
  source_lines = [line.rstrip("\r") for line in text.split("\n")]
291
305
  is_c_doc = _is_c_doc(language_id, uri)
292
- semicolon_is_comment = not is_c_doc and uri.lower().endswith(".asm")
306
+ # A .s/.S extension means GAS syntax: ";" stays a statement separator there
307
+ # even if the client tags the buffer with an assembly languageId.
308
+ semicolon_is_comment = (
309
+ not is_c_doc
310
+ and not uri.lower().endswith(".s")
311
+ and (language_id in ("asm", "nasm", "masm") or uri.lower().endswith((".asm", ".nasm")))
312
+ )
293
313
  if is_c_doc:
294
314
  candidates = [
295
315
  (text.count("\n", 0, offset), segment)
@@ -386,10 +406,28 @@ def main() -> int:
386
406
  line, character = params["position"]["line"], params["position"]["character"]
387
407
  word = _word_at(text, line, character)
388
408
  # 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)
409
+ is_c_doc = _is_c_doc(session.languages.get(uri, ""), uri)
410
+ allow_instruction = not is_c_doc or (_in_asm_string(text, line, character))
411
+ operand_count = None
412
+ operand_text = ""
413
+ if word and allow_instruction:
414
+ source_lines = text.split("\n")
415
+ source_line = source_lines[line] if line < len(source_lines) else ""
416
+ operand_text = source_line
417
+ parsed = _mnemonic_from_asm_line(source_line)
418
+ if parsed and parsed[0].casefold() == word.casefold():
419
+ operand_count = parsed[1]
420
+ body = (
421
+ _hover_markdown(
422
+ conn,
423
+ word,
424
+ allow_instruction,
425
+ operand_count=operand_count,
426
+ operand_text=operand_text,
427
+ )
428
+ if conn and word
429
+ else None
391
430
  )
392
- body = _hover_markdown(conn, word, allow_instruction) if conn and word else None
393
431
  _jsonrpc_write(
394
432
  {
395
433
  "jsonrpc": "2.0",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simdref
3
- Version: 0.0.8
3
+ Version: 0.0.9
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
@@ -79,14 +79,14 @@ def _read_msg(proc, timeout_s=10):
79
79
 
80
80
 
81
81
  class _Server:
82
- def __init__(self, catalog):
82
+ def __init__(self, catalog, extra_env=None):
83
83
  self.proc = subprocess.Popen(
84
84
  [sys.executable, "-m", "simdref.lsp"],
85
85
  stdin=subprocess.PIPE,
86
86
  stdout=subprocess.PIPE,
87
87
  stderr=subprocess.PIPE,
88
88
  bufsize=0,
89
- env={**os.environ, "SIMDREF_CATALOG": str(catalog)},
89
+ env={**os.environ, "SIMDREF_CATALOG": str(catalog), **(extra_env or {})},
90
90
  )
91
91
  self.next_id = 0
92
92
  self.notifications = []
@@ -198,6 +198,49 @@ class LspEndToEndTests(unittest.TestCase):
198
198
  self.assertIsNotNone(result, "hover on vfmadd231ps returned null")
199
199
  self.assertIn("VFMADD231PS", result["contents"]["value"])
200
200
 
201
+ def test_hover_instruction_full_page_operand_matched(self):
202
+ self.server.open(ASM_URI, "asm", ASM_TEXT)
203
+ result = self.server.hover(ASM_URI, 5, 6)
204
+ self.assertIsNotNone(result, "hover on vaddps returned null")
205
+ value = result["contents"]["value"]
206
+ # The 3-operand YMM form, not the first masked (XMM, K, ...) row.
207
+ self.assertIn("VADDPS (YMM, YMM, YMM)", value)
208
+ self.assertNotIn("VADDPS (XMM, K, XMM, M128)", value)
209
+ self.assertIn("## Description", value)
210
+ self.assertIn("Add Packed Single Precision Floating-Point Values", value)
211
+ self.assertIn("## SIMD Floating-Point Exceptions", value)
212
+ self.assertIn("## instruction to intrinsic mapping", value)
213
+ self.assertIn("_mm256_add_ps", value)
214
+ # The per-uarch perf table as a Markdown table.
215
+ self.assertIn("| microarch |", value)
216
+ self.assertIn("| ZEN5 |", value)
217
+ # Links at the end: web deep link, uops.info, felixcloutier.
218
+ self.assertIn("https://simdref.diamondinoia.com/#VADDPS%20(YMM%2C%20YMM%2C%20YMM)", value)
219
+ self.assertIn("https://www.uops.info/html-instr/VADDPS_YMM_YMM_YMM.html", value)
220
+ self.assertIn("https://www.felixcloutier.com/x86/addps", value)
221
+
222
+ def test_hover_makes_no_network_call(self):
223
+ # The server is a subprocess, so block sockets there via sitecustomize.
224
+ import tempfile
225
+
226
+ with tempfile.TemporaryDirectory() as sitepkg:
227
+ Path(sitepkg, "sitecustomize.py").write_text(
228
+ "import socket\n"
229
+ "def _blocked(*a, **k): raise OSError('network blocked in test')\n"
230
+ "socket.socket = _blocked\n"
231
+ "socket.create_connection = _blocked\n"
232
+ )
233
+ server = _Server(
234
+ CATALOG,
235
+ {"PYTHONPATH": sitepkg + os.pathsep + os.environ.get("PYTHONPATH", "")},
236
+ )
237
+ self.addCleanup(server.stop)
238
+ server.start()
239
+ server.open(ASM_URI, "asm", ASM_TEXT)
240
+ result = server.hover(ASM_URI, 5, 6)
241
+ self.assertIsNotNone(result)
242
+ self.assertIn("VADDPS (YMM, YMM, YMM)", result["contents"]["value"])
243
+
201
244
  def test_cpp_two_instructions_on_one_source_line_get_one_joined_hint(self):
202
245
  text = 'asm volatile("vaddps %ymm1, %ymm2, %ymm0\\n\\tvmulps %ymm0, %ymm0, %ymm3");\n'
203
246
  self.server.open(CPP_URI, "cpp", text)
@@ -240,6 +283,25 @@ class LspEndToEndTests(unittest.TestCase):
240
283
  [h["label"] for h in hints],
241
284
  ["Add Packed Single Precision Floating-Point Values."],
242
285
  )
286
+ # "adc eax, ebx ; j, k" parses as 4 operands in a .s file even when the client
287
+ # sends languageId "nasm": the 4-operand form label differs from the 2-operand one.
288
+ self.server.open(uri, "nasm", "adc eax, ebx ; j, k\n")
289
+ self.assertEqual(self.server.hints(uri)[0]["label"], "Adc instruction.")
290
+
291
+ def test_asm_semicolon_comment_in_dot_nasm_file(self):
292
+ uri = "file:///tmp/simdref_lsp_test.nasm"
293
+ self.server.open(uri, "asm", "adc eax, ebx ; j, k\n")
294
+ hints = self.server.hints(uri)
295
+ self.assertEqual([h["position"]["line"] for h in hints], [0])
296
+ # With ";" a comment the line is the 2-operand form, not the 4-operand parse.
297
+ self.assertEqual(hints[0]["label"], "Add With Carry.")
298
+
299
+ def test_asm_semicolon_comment_from_language_id_masm(self):
300
+ uri = "file:///tmp/simdref_lsp_test.txt"
301
+ self.server.open(uri, "masm", "adc eax, ebx ; j, k\n")
302
+ hints = self.server.hints(uri)
303
+ self.assertEqual([h["position"]["line"] for h in hints], [0])
304
+ self.assertEqual(hints[0]["label"], "Add With Carry.")
243
305
 
244
306
  def test_hint_avx512_mask_and_broadcast_operands(self):
245
307
  uri = "file:///tmp/simdref_lsp_test_zmm.s"
@@ -36,7 +36,7 @@ class LspWebTests(unittest.TestCase):
36
36
  markdown = _hover_markdown(self._conn, "vadd.vv")
37
37
  self.assertIsNotNone(markdown)
38
38
  self.assertIn("vadd.vv", markdown)
39
- self.assertIn("ISA V", markdown)
39
+ self.assertIn("**isa.** V", markdown)
40
40
 
41
41
  def test_sqlite_runtime_preserves_riscv_counts_sections_and_policy_metadata(self):
42
42
  intrinsic_count = self._conn.execute(
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes