simdref 0.0.14__tar.gz → 0.0.15__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 (96) hide show
  1. {simdref-0.0.14/src/simdref.egg-info → simdref-0.0.15}/PKG-INFO +1 -1
  2. {simdref-0.0.14 → simdref-0.0.15}/pyproject.toml +1 -1
  3. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/annotate.py +22 -2
  4. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/cli.py +13 -1
  5. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/lsp.py +42 -8
  6. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/perf.py +22 -2
  7. {simdref-0.0.14 → simdref-0.0.15/src/simdref.egg-info}/PKG-INFO +1 -1
  8. {simdref-0.0.14 → simdref-0.0.15}/src/simdref.egg-info/SOURCES.txt +1 -0
  9. simdref-0.0.15/tests/test_no_catalog_row.py +208 -0
  10. {simdref-0.0.14 → simdref-0.0.15}/tests/test_perf.py +31 -0
  11. {simdref-0.0.14 → simdref-0.0.15}/LICENSE +0 -0
  12. {simdref-0.0.14 → simdref-0.0.15}/README.md +0 -0
  13. {simdref-0.0.14 → simdref-0.0.15}/setup.cfg +0 -0
  14. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/__init__.py +0 -0
  15. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/__main__.py +0 -0
  16. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/arm_instructions.py +0 -0
  17. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/asm_identity.py +0 -0
  18. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/display.py +0 -0
  19. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/export.py +0 -0
  20. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/filters.py +0 -0
  21. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/ingest.py +0 -0
  22. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/ingest_catalog.py +0 -0
  23. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/ingest_pdf.py +0 -0
  24. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/ingest_sources.py +0 -0
  25. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/manpages.py +0 -0
  26. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/models.py +0 -0
  27. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/pdfparse/__init__.py +0 -0
  28. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/pdfparse/base.py +0 -0
  29. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/pdfparse/intel.py +0 -0
  30. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/pdfparse/registry.py +0 -0
  31. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/pdfparse/types.py +0 -0
  32. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/pdfrefs.py +0 -0
  33. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/perf_sources/__init__.py +0 -0
  34. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/perf_sources/cores.py +0 -0
  35. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/perf_sources/llvm_mca.py +0 -0
  36. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/perf_sources/llvm_scheduling.py +0 -0
  37. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/perf_sources/merge.py +0 -0
  38. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/queries.py +0 -0
  39. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/riscv.py +0 -0
  40. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/search.py +0 -0
  41. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/storage.py +0 -0
  42. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/tui.py +0 -0
  43. {simdref-0.0.14 → simdref-0.0.15}/src/simdref/ui_labels.py +0 -0
  44. {simdref-0.0.14 → simdref-0.0.15}/src/simdref.egg-info/dependency_links.txt +0 -0
  45. {simdref-0.0.14 → simdref-0.0.15}/src/simdref.egg-info/entry_points.txt +0 -0
  46. {simdref-0.0.14 → simdref-0.0.15}/src/simdref.egg-info/requires.txt +0 -0
  47. {simdref-0.0.14 → simdref-0.0.15}/src/simdref.egg-info/top_level.txt +0 -0
  48. {simdref-0.0.14 → simdref-0.0.15}/tests/test_annotate.py +0 -0
  49. {simdref-0.0.14 → simdref-0.0.15}/tests/test_annotate_positions.py +0 -0
  50. {simdref-0.0.14 → simdref-0.0.15}/tests/test_asm_identity.py +0 -0
  51. {simdref-0.0.14 → simdref-0.0.15}/tests/test_asm_wire.py +0 -0
  52. {simdref-0.0.14 → simdref-0.0.15}/tests/test_audit_coverage.py +0 -0
  53. {simdref-0.0.14 → simdref-0.0.15}/tests/test_auto_update_on_version_change.py +0 -0
  54. {simdref-0.0.14 → simdref-0.0.15}/tests/test_cli_bare_query.py +0 -0
  55. {simdref-0.0.14 → simdref-0.0.15}/tests/test_cli_bootstrap_progress.py +0 -0
  56. {simdref-0.0.14 → simdref-0.0.15}/tests/test_cli_help.py +0 -0
  57. {simdref-0.0.14 → simdref-0.0.15}/tests/test_cli_llm.py +0 -0
  58. {simdref-0.0.14 → simdref-0.0.15}/tests/test_coverage_live.py +0 -0
  59. {simdref-0.0.14 → simdref-0.0.15}/tests/test_coverage_parity.py +0 -0
  60. {simdref-0.0.14 → simdref-0.0.15}/tests/test_display.py +0 -0
  61. {simdref-0.0.14 → simdref-0.0.15}/tests/test_export.py +0 -0
  62. {simdref-0.0.14 → simdref-0.0.15}/tests/test_filters.py +0 -0
  63. {simdref-0.0.14 → simdref-0.0.15}/tests/test_ingest_enc_fields.py +0 -0
  64. {simdref-0.0.14 → simdref-0.0.15}/tests/test_ingest_sources.py +0 -0
  65. {simdref-0.0.14 → simdref-0.0.15}/tests/test_intel_operation_and_url.py +0 -0
  66. {simdref-0.0.14 → simdref-0.0.15}/tests/test_issue2_fixes.py +0 -0
  67. {simdref-0.0.14 → simdref-0.0.15}/tests/test_issues_23_24_25.py +0 -0
  68. {simdref-0.0.14 → simdref-0.0.15}/tests/test_leftover_arm_form_pick.py +0 -0
  69. {simdref-0.0.14 → simdref-0.0.15}/tests/test_leftover_arm_postindex.py +0 -0
  70. {simdref-0.0.14 → simdref-0.0.15}/tests/test_leftover_att_movx.py +0 -0
  71. {simdref-0.0.14 → simdref-0.0.15}/tests/test_leftover_autoupdate.py +0 -0
  72. {simdref-0.0.14 → simdref-0.0.15}/tests/test_leftover_catalog_gaps.py +0 -0
  73. {simdref-0.0.14 → simdref-0.0.15}/tests/test_leftover_ingest_isa_merge.py +0 -0
  74. {simdref-0.0.14 → simdref-0.0.15}/tests/test_leftover_intel_size.py +0 -0
  75. {simdref-0.0.14 → simdref-0.0.15}/tests/test_leftover_perf_join.py +0 -0
  76. {simdref-0.0.14 → simdref-0.0.15}/tests/test_leftover_riscv_shared.py +0 -0
  77. {simdref-0.0.14 → simdref-0.0.15}/tests/test_lsp_stdio.py +0 -0
  78. {simdref-0.0.14 → simdref-0.0.15}/tests/test_lsp_web.py +0 -0
  79. {simdref-0.0.14 → simdref-0.0.15}/tests/test_models.py +0 -0
  80. {simdref-0.0.14 → simdref-0.0.15}/tests/test_pdfparse.py +0 -0
  81. {simdref-0.0.14 → simdref-0.0.15}/tests/test_perf_sources.py +0 -0
  82. {simdref-0.0.14 → simdref-0.0.15}/tests/test_presentation.py +0 -0
  83. {simdref-0.0.14 → simdref-0.0.15}/tests/test_preset_persistence.py +0 -0
  84. {simdref-0.0.14 → simdref-0.0.15}/tests/test_riscv_docs_pages.py +0 -0
  85. {simdref-0.0.14 → simdref-0.0.15}/tests/test_search.py +0 -0
  86. {simdref-0.0.14 → simdref-0.0.15}/tests/test_search_pushdown.py +0 -0
  87. {simdref-0.0.14 → simdref-0.0.15}/tests/test_source_kind_filter.py +0 -0
  88. {simdref-0.0.14 → simdref-0.0.15}/tests/test_source_validation.py +0 -0
  89. {simdref-0.0.14 → simdref-0.0.15}/tests/test_storage_payload.py +0 -0
  90. {simdref-0.0.14 → simdref-0.0.15}/tests/test_storage_schema.py +0 -0
  91. {simdref-0.0.14 → simdref-0.0.15}/tests/test_tui.py +0 -0
  92. {simdref-0.0.14 → simdref-0.0.15}/tests/test_ui_labels_parity.py +0 -0
  93. {simdref-0.0.14 → simdref-0.0.15}/tests/test_vendor_riscv_categories.py +0 -0
  94. {simdref-0.0.14 → simdref-0.0.15}/tests/test_vendor_riscv_sources.py +0 -0
  95. {simdref-0.0.14 → simdref-0.0.15}/tests/test_version_matches_pyproject.py +0 -0
  96. {simdref-0.0.14 → simdref-0.0.15}/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.14
3
+ Version: 0.0.15
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.14"
7
+ version = "0.0.15"
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"
@@ -17,7 +17,7 @@ from enum import Enum
17
17
  from typing import Any, Iterable, Iterator
18
18
 
19
19
  from simdref import perf
20
- from simdref.lsp import identity_keys
20
+ from simdref.lsp import NO_CATALOG_ROW, identity_keys
21
21
  from simdref.models import InstructionRecord
22
22
  from simdref.storage import load_instruction_from_db, load_instructions_by_mnemonic_from_db
23
23
 
@@ -460,7 +460,9 @@ class PerfSummary:
460
460
  def _per_arch_value(details: dict[str, Any], value_fn) -> float | None:
461
461
  for v in value_fn(details):
462
462
  try:
463
- return float(v)
463
+ # Latency ranges render as "min-max" strings (simdref#50); the
464
+ # float view takes the min so aggregates stay numeric.
465
+ return float(str(v).split("-", 1)[0])
464
466
  except (TypeError, ValueError):
465
467
  continue
466
468
  return None
@@ -688,6 +690,24 @@ def _annotate_instruction(
688
690
  """Return the rendered output line and an optional JSON record."""
689
691
  if stats is not None:
690
692
  stats["parsed"] = stats.get("parsed", 0) + 1
693
+ if identity_key == NO_CATALOG_ROW:
694
+ # Byte identity identified the form and the catalog has no row for
695
+ # it (issue #49: uops.info carries no AVX reg-reg VMOVAPD/VMOVUPS):
696
+ # report the miss by name, never guess a neighbour form below.
697
+ if stats is not None:
698
+ stats["no_row"] = stats.get("no_row", 0) + 1
699
+ no_row: dict[str, Any] = {
700
+ "mnemonic": parsed.mnemonic,
701
+ "known": False,
702
+ "no_measured_row": True,
703
+ }
704
+ if parsed.address is not None:
705
+ no_row["address"] = f"0x{parsed.address:x}"
706
+ if parsed.source_file:
707
+ no_row["source_file"] = parsed.source_file
708
+ if parsed.source_line is not None:
709
+ no_row["source_line"] = parsed.source_line
710
+ return f"{parsed.raw} # {parsed.mnemonic}: no measured row", no_row
691
711
  # Byte identity wins over the form pick: the record is what the line
692
712
  # assembles to, and an empty row keeps the old pick unchanged.
693
713
  record = load_instruction_from_db(conn, identity_key) if identity_key else None
@@ -1897,9 +1897,21 @@ def _build_llm_payload(
1897
1897
  }
1898
1898
  # Byte identity wins over the name lookups below: a full assembly line
1899
1899
  # resolves to the one row its bytes name.
1900
- from simdref.lsp import identity_keys
1900
+ from simdref.lsp import NO_CATALOG_ROW, identity_keys
1901
1901
 
1902
1902
  keys = identity_keys([query_str], None, conn)
1903
+ if keys[0] == NO_CATALOG_ROW:
1904
+ # Identified form the catalog lacks (issue #49): report the miss by
1905
+ # name, never join the line to a neighbour form's row.
1906
+ mnemonic = query_str.split(None, 1)[0]
1907
+ return {
1908
+ "query": query_str,
1909
+ "mode": "exact",
1910
+ "match_kind": "instruction",
1911
+ "detail": f"{mnemonic}: no measured row",
1912
+ "results": [],
1913
+ **meta,
1914
+ }
1903
1915
  if keys[0]:
1904
1916
  item = load_instruction_from_db(conn, keys[0])
1905
1917
  if item is not None:
@@ -963,6 +963,14 @@ def _document_arch(
963
963
  return name, ("intel" if syntax else "att"), keep
964
964
 
965
965
 
966
+ # identity_keys slot value when byte identity ran on a schema-14 catalog and
967
+ # identified the instruction, but the catalog has no row for that form. Not a
968
+ # db_key: callers show the line as having no measured row and never fall back
969
+ # to the legacy pick. The legacy pick stays only where byte identity could
970
+ # not run (missing tools, a schema-13 catalog, a declined document).
971
+ NO_CATALOG_ROW = "?:no-catalog-row"
972
+
973
+
966
974
  @functools.lru_cache(maxsize=32)
967
975
  def _identity_batch(
968
976
  texts: tuple[str, ...], intel: bool, syntax: bool, catalog: str, mtime_ns: int
@@ -989,22 +997,36 @@ def _identity_batch(
989
997
  for index, held, result in zip(probed, keep, results):
990
998
  # The text check keeps a row on its own line even if a text carries
991
999
  # its own line break.
992
- if held and result.row is not None and result.text == texts[index]:
1000
+ if not held or result.text != texts[index]:
1001
+ continue
1002
+ if result.row is not None:
993
1003
  slots[index] = result.row.db_key
1004
+ elif result.reason == "no catalog row" or result.reason.startswith(
1005
+ ("no catalog row (iform ", "no catalog row (splits emptied ")
1006
+ ):
1007
+ # Byte identity identified the form and the catalog lacks its row
1008
+ # (e.g. uops.info carries no AVX reg-reg VMOVAPD): mark the miss.
1009
+ # A fixup placeholder or a foreign decode mode keeps the line on
1010
+ # the old matcher: the row can exist, it is just not byte-proven.
1011
+ slots[index] = NO_CATALOG_ROW
994
1012
  return tuple(slots)
995
1013
 
996
1014
 
997
1015
  def identity_keys(
998
1016
  texts: Sequence[str], intel: bool | None, conn: sqlite3.Connection
999
1017
  ) -> list[str | None]:
1000
- """Map each text to the catalog db_key its encoded bytes name, or None.
1018
+ """Map each text to the catalog db_key its encoded bytes name, or a mark.
1001
1019
 
1002
1020
  One shared entry for hover, inlay hints, annotate and the llm query: one
1003
1021
  llvm-mc run per document batch, cached per document content. A None means
1004
- byte identity has no row there and the caller's old matcher answers that
1005
- line unchanged. ``intel`` is the file-type syntax flag; None infers the
1006
- x86 syntax from the text, and keeps the operand votes neutral so a bare
1007
- AArch64 or RISC-V line never reads as forced x86.
1022
+ byte identity could not run for the line (tools missing, a schema-13
1023
+ catalog, a document byte identity declined, or a line that votes another
1024
+ arch) and the caller's old matcher answers that line unchanged.
1025
+ NO_CATALOG_ROW means byte identity identified the instruction and the
1026
+ catalog has no row for that form: the caller reports no measured row and
1027
+ never guesses a neighbour form. ``intel`` is the file-type syntax flag;
1028
+ None infers the x86 syntax from the text, and keeps the operand votes
1029
+ neutral so a bare AArch64 or RISC-V line never reads as forced x86.
1008
1030
  """
1009
1031
  if not texts:
1010
1032
  return []
@@ -1079,6 +1101,10 @@ def _hover_markdown(
1079
1101
 
1080
1102
  if not allow_instruction:
1081
1103
  return None
1104
+ if identity_key == NO_CATALOG_ROW:
1105
+ # Byte identity identified the form and the catalog has no row for
1106
+ # it: say so by name, never guess a neighbour form below.
1107
+ return f"`{word}`: no measured row"
1082
1108
  # Byte identity wins over every matcher pick below: the row is what the
1083
1109
  # line assembles to. None keeps the old matcher on its exact-name path.
1084
1110
  instruction = load_instruction_from_db(conn, identity_key) if identity_key else None
@@ -1375,14 +1401,22 @@ def _inlay_hints(conn, text: str, language_id: str, uri: str, start: int, end: i
1375
1401
  semicolon_is_comment = _semicolon_is_comment(language_id, uri)
1376
1402
  candidates = _asm_candidates(text, language_id, uri)
1377
1403
  keys = identity_keys([segment for _line, segment in candidates], semicolon_is_comment, conn)
1378
- records = {key: load_instruction_from_db(conn, key) for key in set(keys) if key is not None}
1404
+ records = {
1405
+ key: load_instruction_from_db(conn, key)
1406
+ for key in set(keys)
1407
+ if key is not None and key != NO_CATALOG_ROW
1408
+ }
1379
1409
  hints: dict[int, list[str]] = {} # every instruction on the source line feeds its hint
1380
1410
  labels: dict[str, str | None] = {} # one matcher run per distinct segment, not per line
1381
1411
  for (line, segment), key in zip(candidates, keys):
1382
1412
  if not start <= line <= end:
1383
1413
  continue
1384
1414
  record = records.get(key) if key else None
1385
- if record is not None:
1415
+ if key == NO_CATALOG_ROW:
1416
+ # Identified form the catalog lacks: name it, never the old pick.
1417
+ parsed = _mnemonic_from_asm_line(segment, semicolon_is_comment)
1418
+ label = f"{parsed[0]}: no measured row" if parsed else None
1419
+ elif record is not None:
1386
1420
  label = _cut_label(record.summary or "")
1387
1421
  elif segment in labels:
1388
1422
  label = labels[segment]
@@ -62,8 +62,11 @@ def latency_cycle_values(latencies: list[dict[str, Any]]) -> list[str]:
62
62
  """Collect unique cycle-count strings from a list of latency dicts.
63
63
 
64
64
  Each *latency* dict may contain keys like ``cycles``, ``cycles_mem``,
65
- ``cycles_addr``, or ``cycles_addr_index``. Values are collected in
66
- order, skipping duplicates.
65
+ ``cycles_addr``, or ``cycles_addr_index``. uops.info divide/sqrt
66
+ measurements carry a ``min_cycles`` / ``max_cycles`` pair instead (and
67
+ ``_mem`` / ``_addr`` / ``_addr_index`` variants); a pair renders as one
68
+ range string (``"13"`` when equal, ``"13-15"`` otherwise). Values are
69
+ collected in order, skipping duplicates.
67
70
 
68
71
  Returns:
69
72
  De-duplicated list of cycle-count strings (may be empty).
@@ -81,6 +84,23 @@ def latency_cycle_values(latencies: list[dict[str, Any]]) -> list[str]:
81
84
  continue
82
85
  if value not in values:
83
86
  values.append(value)
87
+ for suffix in ("", "_mem", "_addr", "_addr_index"):
88
+ low = latency.get("min_cycles" + suffix, "")
89
+ high = latency.get("max_cycles" + suffix, "")
90
+ if not low and not high:
91
+ continue
92
+ low_only = low or high
93
+ if (
94
+ suffix == "_mem"
95
+ and latency.get("min_cycles_mem_is_upper_bound") == "1"
96
+ and _is_numeric(low_only)
97
+ and float(low_only) == 0
98
+ ):
99
+ # Same not-measured convention as cycles_mem=0 above.
100
+ continue
101
+ value = f"{low}-{high}" if low and high and low != high else low_only
102
+ if value not in values:
103
+ values.append(value)
84
104
  return values
85
105
 
86
106
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simdref
3
- Version: 0.0.14
3
+ Version: 0.0.15
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
@@ -73,6 +73,7 @@ tests/test_leftover_riscv_shared.py
73
73
  tests/test_lsp_stdio.py
74
74
  tests/test_lsp_web.py
75
75
  tests/test_models.py
76
+ tests/test_no_catalog_row.py
76
77
  tests/test_pdfparse.py
77
78
  tests/test_perf.py
78
79
  tests/test_perf_sources.py
@@ -0,0 +1,208 @@
1
+ """Issue #49: an identified form the catalog lacks reports no row.
2
+
3
+ uops.info has no AVX register-register VMOVAPD/VMOVUPS rows, so byte identity
4
+ correctly finds no row for ``vmovapd %xmm1, %xmm0``. Annotate, hover, hints
5
+ and the llm query must say "no measured row" by name and never fall back to
6
+ the legacy nearest-form pick (which joined the line to the store row, lat
7
+ 12c). The legacy pick stays only where byte identity could not run.
8
+
9
+ Tool-dependent tests skip cleanly when llvm-mc or the XED decoder is absent;
10
+ nothing fakes a pass.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+
17
+ import pytest
18
+ from test_asm_wire import _fail
19
+
20
+ from simdref import annotate as annotate_mod
21
+ from simdref import cli as cli_mod
22
+ from simdref import lsp
23
+ from simdref.asm_identity import _tool_path
24
+ from simdref.lsp import NO_CATALOG_ROW, identity_keys
25
+ from simdref.models import Catalog, InstructionRecord, SourceVersion
26
+ from simdref.storage import build_sqlite, open_db
27
+
28
+ LLVM_MC = _tool_path("SIMDREF_LLVM_MC", "llvm-mc") or ""
29
+ XED = _tool_path("SIMDREF_XED", "xed") or ""
30
+ needs_xed = pytest.mark.skipif(not (LLVM_MC and XED), reason="llvm-mc or xed absent")
31
+
32
+
33
+ # A fixture catalog with only the store rows: the reg-reg iforms
34
+ # (VMOVAPD_XMMdq_XMMdq_28, VMOVUPS_XMMdq_XMMdq_10) are absent, like the real
35
+ # catalog. Measured SKL latency 12c mirrors the issue's wrong join.
36
+ def _store_row(mnemonic: str, iform: str) -> InstructionRecord:
37
+ return InstructionRecord(
38
+ mnemonic=mnemonic,
39
+ form=f"{mnemonic} (M128, XMM)",
40
+ summary=f"Store via {mnemonic}.",
41
+ architecture="x86",
42
+ arch_details={
43
+ "SKL": {
44
+ "latencies": [{"cycles": "12"}],
45
+ "measurement": {"TP": "1.0"},
46
+ "source_kind": "measured",
47
+ }
48
+ },
49
+ metadata={"iform": iform, "split_attrs": "{}"},
50
+ )
51
+
52
+
53
+ def _store_db(tmp_path):
54
+ rows = [
55
+ _store_row("VMOVAPD", "VMOVAPD_MEMdq_XMMdq"),
56
+ _store_row("VMOVUPS", "VMOVUPS_MEMdq_XMMdq"),
57
+ ]
58
+ db = tmp_path / "store-only.db"
59
+ build_sqlite(
60
+ Catalog(
61
+ intrinsics=[],
62
+ instructions=rows,
63
+ sources=[
64
+ SourceVersion(source="t", version="t", fetched_at="2025-01-01", url="test://")
65
+ ],
66
+ generated_at="2025-01-01",
67
+ ),
68
+ db,
69
+ )
70
+ return db
71
+
72
+
73
+ REG_REG = ["vmovapd %xmm1, %xmm0", "vmovups %xmm3, %xmm2"]
74
+ STORE = ["vmovapd %xmm0, (%rax)", "vmovups %xmm2, (%rsi)"]
75
+
76
+
77
+ @needs_xed
78
+ class TestNoCatalogRow:
79
+ def test_identity_marks_the_miss(self, tmp_path):
80
+ conn = open_db(path=_store_db(tmp_path))
81
+ try:
82
+ keys = identity_keys(REG_REG + STORE, False, conn)
83
+ store = [open_key for open_key in keys[2:]]
84
+ finally:
85
+ conn.close()
86
+ assert keys[:2] == [NO_CATALOG_ROW, NO_CATALOG_ROW]
87
+ assert all(key and key != NO_CATALOG_ROW for key in store)
88
+
89
+ def test_annotate_reports_no_row(self, tmp_path, monkeypatch):
90
+ conn = open_db(path=_store_db(tmp_path))
91
+ monkeypatch.setattr(annotate_mod, "lookup", _fail("the old lookup ran"))
92
+ try:
93
+ out = "".join(
94
+ annotate_mod.annotate_stream(
95
+ [line + "\n" for line in REG_REG + STORE],
96
+ opts=annotate_mod.AnnotateOptions(),
97
+ conn=conn,
98
+ )
99
+ ).splitlines()
100
+ finally:
101
+ conn.close()
102
+ assert "# vmovapd: no measured row" in out[0]
103
+ assert "# vmovups: no measured row" in out[1]
104
+ for line in (out[0], out[1]):
105
+ assert "Store via" not in line
106
+ assert "lat=" not in line
107
+ # A store line still gets the store row.
108
+ assert "Store via VMOVAPD." in out[2]
109
+ assert "lat=12.0c" in out[2]
110
+ assert "Store via VMOVUPS." in out[3]
111
+
112
+ def test_annotate_json_marks_no_row(self, tmp_path, monkeypatch):
113
+ conn = open_db(path=_store_db(tmp_path))
114
+ monkeypatch.setattr(annotate_mod, "lookup", _fail("the old lookup ran"))
115
+ try:
116
+ out = "".join(
117
+ annotate_mod.annotate_stream(
118
+ [REG_REG[0] + "\n", STORE[0] + "\n"],
119
+ opts=annotate_mod.AnnotateOptions(fmt="json"),
120
+ conn=conn,
121
+ )
122
+ )
123
+ finally:
124
+ conn.close()
125
+ records = json.loads(out)
126
+ assert records[0] == {
127
+ "mnemonic": "vmovapd",
128
+ "known": False,
129
+ "no_measured_row": True,
130
+ }
131
+ assert records[1]["known"] is True
132
+
133
+ def test_hover_reports_no_row(self, tmp_path, monkeypatch):
134
+ conn = open_db(path=_store_db(tmp_path))
135
+ monkeypatch.setattr(lsp, "_match_form", _fail("the old form matcher ran"))
136
+ monkeypatch.setattr(lsp, "_lookup_mnemonic", _fail("the old matcher ran"))
137
+ try:
138
+ key = identity_keys(REG_REG, False, conn)[0]
139
+ body = lsp._hover_markdown(conn, "vmovapd", identity_key=key)
140
+ finally:
141
+ conn.close()
142
+ assert body == "`vmovapd`: no measured row"
143
+
144
+ def test_hint_reports_no_row(self, tmp_path, monkeypatch):
145
+ conn = open_db(path=_store_db(tmp_path))
146
+ monkeypatch.setattr(lsp, "_hint_label", _fail("the old hint matcher ran"))
147
+ try:
148
+ hints = lsp._inlay_hints(
149
+ conn, REG_REG[0] + "\n" + STORE[0] + "\n", "asm", "file:///x.s", 0, 1
150
+ )
151
+ finally:
152
+ conn.close()
153
+ assert [hint["label"] for hint in hints] == [
154
+ "vmovapd: no measured row",
155
+ "Store via VMOVAPD.",
156
+ ]
157
+
158
+ def test_llm_reports_no_row(self, tmp_path, monkeypatch):
159
+ conn = open_db(path=_store_db(tmp_path))
160
+ monkeypatch.setattr(cli_mod, "_find_instructions_fast", _fail("the old name lookup ran"))
161
+ monkeypatch.setattr(cli_mod, "_search_runtime", _fail("the old search ran"))
162
+ try:
163
+ payload = cli_mod._build_llm_payload(conn, REG_REG[0], 8, None, None, None)
164
+ finally:
165
+ conn.close()
166
+ assert payload["mode"] == "exact"
167
+ assert payload["match_kind"] == "instruction"
168
+ assert payload["detail"] == "vmovapd: no measured row"
169
+ assert payload["results"] == []
170
+
171
+
172
+ class TestMarkerPathNeedsNoTools:
173
+ def test_marker_never_calls_the_legacy_pick(self, tmp_path, monkeypatch):
174
+ # The marker reaches annotate even with tools absent; the old lookup
175
+ # must stay silent either way.
176
+ conn = open_db(path=_store_db(tmp_path))
177
+ monkeypatch.setattr(lsp, "_identity_batch", lambda *a, **k: (NO_CATALOG_ROW,))
178
+ monkeypatch.setattr(annotate_mod, "lookup", _fail("the old lookup ran"))
179
+ try:
180
+ out = "".join(
181
+ annotate_mod.annotate_stream(
182
+ [REG_REG[0] + "\n"],
183
+ opts=annotate_mod.AnnotateOptions(),
184
+ conn=conn,
185
+ stats={},
186
+ )
187
+ )
188
+ finally:
189
+ conn.close()
190
+ assert "# vmovapd: no measured row" in out
191
+
192
+ def test_no_marker_keeps_the_legacy_pick(self, tmp_path, monkeypatch):
193
+ # Byte identity could not run (None): the old matcher answers exactly
194
+ # as before the fix — here the issue's line with only the store row.
195
+ conn = open_db(path=_store_db(tmp_path))
196
+ monkeypatch.setattr(lsp, "_identity_batch", lambda *a, **k: (None,))
197
+ try:
198
+ out = "".join(
199
+ annotate_mod.annotate_stream(
200
+ [REG_REG[0] + "\n"],
201
+ opts=annotate_mod.AnnotateOptions(),
202
+ conn=conn,
203
+ )
204
+ )
205
+ finally:
206
+ conn.close()
207
+ assert "no measured row" not in out
208
+ assert out.strip().startswith("vmovapd")
@@ -68,6 +68,37 @@ class PerfTests(unittest.TestCase):
68
68
  latencies = [{"cycles_mem": "0"}]
69
69
  self.assertEqual(latency_cycle_values(latencies), ["0"])
70
70
 
71
+ def test_latency_cycle_values_reads_min_max_pair(self):
72
+ # simdref#50: uops.info divide/sqrt rows carry only a min/max range.
73
+ latencies = [
74
+ {"min_cycles": "13", "max_cycles": "15", "min_cycles_is_upper_bound": "1"},
75
+ {"min_cycles": "13", "max_cycles": "15", "min_cycles_is_upper_bound": "1"},
76
+ ]
77
+ self.assertEqual(latency_cycle_values(latencies), ["13-15"])
78
+
79
+ def test_latency_cycle_values_min_max_equal(self):
80
+ self.assertEqual(latency_cycle_values([{"min_cycles": "7", "max_cycles": "7"}]), ["7"])
81
+
82
+ def test_latency_cycle_values_min_only(self):
83
+ self.assertEqual(latency_cycle_values([{"min_cycles": "7"}]), ["7"])
84
+
85
+ def test_latency_cycle_values_drops_upper_bound_zero_mem_range(self):
86
+ latencies = [{"min_cycles_mem": "0", "min_cycles_mem_is_upper_bound": "1"}]
87
+ self.assertEqual(latency_cycle_values(latencies), [])
88
+
89
+ def test_min_max_pair_flows_to_every_output(self):
90
+ # simdref#50: the perf-root fix must reach every caller output: the
91
+ # string picks, the variant summary and the float annotate view.
92
+ from simdref.annotate import _latency_for, _per_arch_value
93
+
94
+ details = {"latencies": [{"min_cycles": "13", "max_cycles": "15"}]}
95
+ arch_details = {"MTL-P": details}
96
+ self.assertEqual(best_latency(arch_details), "13-15")
97
+ self.assertEqual(best_latency_labeled(arch_details).value, "13-15")
98
+ lat, _ = variant_perf_summary(arch_details)
99
+ self.assertEqual(lat, "13-15")
100
+ self.assertEqual(_per_arch_value(details, _latency_for), 13.0)
101
+
71
102
  def test_best_numeric_picks_smallest(self):
72
103
  self.assertEqual(best_numeric(["5", "3", "7"]), "3")
73
104
  self.assertEqual(best_numeric(["1.5", "2.0"]), "1.5")
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