simdref 0.0.3__tar.gz → 0.0.4__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.3/src/simdref.egg-info → simdref-0.0.4}/PKG-INFO +2 -1
  2. {simdref-0.0.3 → simdref-0.0.4}/pyproject.toml +2 -1
  3. simdref-0.0.4/src/simdref/__init__.py +10 -0
  4. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/annotate.py +3 -2
  5. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/cli.py +107 -3
  6. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/ingest_catalog.py +24 -0
  7. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/perf.py +8 -1
  8. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/storage.py +21 -1
  9. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/templates/app.js +155 -28
  10. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/web.py +60 -15
  11. {simdref-0.0.3 → simdref-0.0.4/src/simdref.egg-info}/PKG-INFO +2 -1
  12. {simdref-0.0.3 → simdref-0.0.4}/src/simdref.egg-info/SOURCES.txt +5 -0
  13. {simdref-0.0.3 → simdref-0.0.4}/src/simdref.egg-info/requires.txt +1 -0
  14. {simdref-0.0.3 → simdref-0.0.4}/tests/test_annotate.py +16 -0
  15. simdref-0.0.4/tests/test_auto_update_on_version_change.py +174 -0
  16. {simdref-0.0.3 → simdref-0.0.4}/tests/test_cli_llm.py +19 -0
  17. simdref-0.0.4/tests/test_intel_operation_and_url.py +119 -0
  18. {simdref-0.0.3 → simdref-0.0.4}/tests/test_lsp_web.py +21 -17
  19. {simdref-0.0.3 → simdref-0.0.4}/tests/test_perf.py +21 -0
  20. {simdref-0.0.3 → simdref-0.0.4}/tests/test_presentation.py +21 -8
  21. simdref-0.0.4/tests/test_search_index_js.py +34 -0
  22. {simdref-0.0.3 → simdref-0.0.4}/tests/test_storage_schema.py +3 -3
  23. simdref-0.0.4/tests/test_version_matches_pyproject.py +32 -0
  24. simdref-0.0.4/tests/test_web_e2e.py +201 -0
  25. simdref-0.0.3/src/simdref/__init__.py +0 -5
  26. {simdref-0.0.3 → simdref-0.0.4}/LICENSE +0 -0
  27. {simdref-0.0.3 → simdref-0.0.4}/README.md +0 -0
  28. {simdref-0.0.3 → simdref-0.0.4}/setup.cfg +0 -0
  29. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/__main__.py +0 -0
  30. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/arm_instructions.py +0 -0
  31. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/display.py +0 -0
  32. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/filters.py +0 -0
  33. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/ingest.py +0 -0
  34. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/ingest_pdf.py +0 -0
  35. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/ingest_sources.py +0 -0
  36. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/lsp.py +0 -0
  37. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/manpages.py +0 -0
  38. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/models.py +0 -0
  39. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/pdfparse/__init__.py +0 -0
  40. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/pdfparse/base.py +0 -0
  41. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/pdfparse/intel.py +0 -0
  42. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/pdfparse/registry.py +0 -0
  43. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/pdfparse/types.py +0 -0
  44. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/pdfrefs.py +0 -0
  45. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/perf_sources/__init__.py +0 -0
  46. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/perf_sources/cores.py +0 -0
  47. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/perf_sources/llvm_mca.py +0 -0
  48. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/perf_sources/llvm_scheduling.py +0 -0
  49. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/perf_sources/merge.py +0 -0
  50. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/__init__.py +0 -0
  51. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/adapters/__init__.py +0 -0
  52. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/adapters/exegesis.py +0 -0
  53. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/adapters/mca.py +0 -0
  54. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/adapters/perf.py +0 -0
  55. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/adapters/uprof.py +0 -0
  56. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/adapters/vtune.py +0 -0
  57. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/adapters/xctrace.py +0 -0
  58. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/hotloop.py +0 -0
  59. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/merge.py +0 -0
  60. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/model.py +0 -0
  61. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/orchestrate.py +0 -0
  62. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/profile/registry.py +0 -0
  63. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/queries.py +0 -0
  64. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/riscv.py +0 -0
  65. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/search.py +0 -0
  66. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/templates/__init__.py +0 -0
  67. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/templates/favicon.svg +0 -0
  68. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/templates/index.html +0 -0
  69. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/templates/logo.svg +0 -0
  70. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/templates/style.css +0 -0
  71. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/tui.py +0 -0
  72. {simdref-0.0.3 → simdref-0.0.4}/src/simdref/ui_labels.py +0 -0
  73. {simdref-0.0.3 → simdref-0.0.4}/src/simdref.egg-info/dependency_links.txt +0 -0
  74. {simdref-0.0.3 → simdref-0.0.4}/src/simdref.egg-info/entry_points.txt +0 -0
  75. {simdref-0.0.3 → simdref-0.0.4}/src/simdref.egg-info/top_level.txt +0 -0
  76. {simdref-0.0.3 → simdref-0.0.4}/tests/test_audit_coverage.py +0 -0
  77. {simdref-0.0.3 → simdref-0.0.4}/tests/test_cli_bare_query.py +0 -0
  78. {simdref-0.0.3 → simdref-0.0.4}/tests/test_cli_bootstrap_progress.py +0 -0
  79. {simdref-0.0.3 → simdref-0.0.4}/tests/test_cli_help.py +0 -0
  80. {simdref-0.0.3 → simdref-0.0.4}/tests/test_coverage_live.py +0 -0
  81. {simdref-0.0.3 → simdref-0.0.4}/tests/test_coverage_parity.py +0 -0
  82. {simdref-0.0.3 → simdref-0.0.4}/tests/test_display.py +0 -0
  83. {simdref-0.0.3 → simdref-0.0.4}/tests/test_filters.py +0 -0
  84. {simdref-0.0.3 → simdref-0.0.4}/tests/test_ingest_sources.py +0 -0
  85. {simdref-0.0.3 → simdref-0.0.4}/tests/test_issue2_fixes.py +0 -0
  86. {simdref-0.0.3 → simdref-0.0.4}/tests/test_models.py +0 -0
  87. {simdref-0.0.3 → simdref-0.0.4}/tests/test_pdfparse.py +0 -0
  88. {simdref-0.0.3 → simdref-0.0.4}/tests/test_perf_sources.py +0 -0
  89. {simdref-0.0.3 → simdref-0.0.4}/tests/test_preset_persistence.py +0 -0
  90. {simdref-0.0.3 → simdref-0.0.4}/tests/test_search.py +0 -0
  91. {simdref-0.0.3 → simdref-0.0.4}/tests/test_search_pushdown.py +0 -0
  92. {simdref-0.0.3 → simdref-0.0.4}/tests/test_source_kind_filter.py +0 -0
  93. {simdref-0.0.3 → simdref-0.0.4}/tests/test_source_validation.py +0 -0
  94. {simdref-0.0.3 → simdref-0.0.4}/tests/test_tui.py +0 -0
  95. {simdref-0.0.3 → simdref-0.0.4}/tests/test_ui_labels_parity.py +0 -0
  96. {simdref-0.0.3 → simdref-0.0.4}/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
3
+ Version: 0.0.4
4
4
  Summary: Searchable SIMD intrinsic and instruction reference with CLI, manpages, TUI, LSP, and static web export.
5
5
  Author: Marco
6
6
  License: GPL-3.0-or-later
@@ -18,6 +18,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
+ Requires-Dist: click<10,>=8.1
21
22
  Requires-Dist: httpx<1,>=0.28
22
23
  Requires-Dist: msgpack<2,>=1.0
23
24
  Requires-Dist: pdfplumber<1,>=0.11
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simdref"
7
- version = "0.0.3"
7
+ version = "0.0.4"
8
8
  description = "Searchable SIMD intrinsic and instruction reference with CLI, manpages, TUI, LSP, and static web export."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -26,6 +26,7 @@ classifiers = [
26
26
  "Topic :: Software Development :: Libraries :: Python Modules",
27
27
  ]
28
28
  dependencies = [
29
+ "click>=8.1,<10",
29
30
  "httpx>=0.28,<1",
30
31
  "msgpack>=1.0,<2",
31
32
  "pdfplumber>=0.11,<1",
@@ -0,0 +1,10 @@
1
+ """simdref package."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ __all__ = ["__version__"]
6
+
7
+ try:
8
+ __version__ = version("simdref")
9
+ except PackageNotFoundError: # uninstalled source checkout
10
+ __version__ = "0.0.0+source"
@@ -68,8 +68,6 @@ def parse_asm_line(line: str, *, track_positions: bool = False) -> AsmLine:
68
68
  return AsmLine(LineKind.COMMENT, stripped)
69
69
  if bare.startswith("."):
70
70
  return AsmLine(LineKind.DIRECTIVE, stripped)
71
- if _LABEL_RE.match(stripped):
72
- return AsmLine(LineKind.LABEL, stripped)
73
71
 
74
72
  address: int | None = None
75
73
  if track_positions:
@@ -97,6 +95,9 @@ def parse_asm_line(line: str, *, track_positions: bool = False) -> AsmLine:
97
95
  # source keywords ("return", "if", "for", ...) as mnemonics.
98
96
  return AsmLine(LineKind.COMMENT, stripped)
99
97
 
98
+ if _LABEL_RE.match(stripped):
99
+ return AsmLine(LineKind.LABEL, stripped)
100
+
100
101
  m = _INSTR_RE.match(stripped)
101
102
  if not m:
102
103
  return AsmLine(LineKind.COMMENT, stripped)
@@ -70,6 +70,7 @@ from simdref.ingest_sources import (
70
70
  refresh_local_arm_intrinsics_bundle,
71
71
  )
72
72
  from simdref.manpages import open_manpage, write_manpages
73
+ from simdref import perf
73
74
  from simdref.perf import variant_perf_summary
74
75
  from simdref.queries import intrinsic_perf_summary_runtime, instruction_rows_for_intrinsic
75
76
  from simdref.search import (
@@ -92,10 +93,12 @@ from simdref.storage import (
92
93
  load_instructions_by_mnemonic_from_db,
93
94
  load_instructions_by_mnemonic_prefix_from_db,
94
95
  open_db,
96
+ read_installed_version_stamp,
95
97
  save_catalog,
96
98
  search_instruction_candidates_from_db,
97
99
  search_intrinsic_candidates_from_db,
98
100
  sqlite_schema_is_current,
101
+ write_installed_version_stamp,
99
102
  )
100
103
  from simdref.web import export_web
101
104
 
@@ -287,6 +290,18 @@ def _download_from_release() -> None:
287
290
  f"failed to download {asset}: {exc.response.status_code}", style="red"
288
291
  )
289
292
  raise typer.Exit(code=1) from exc
293
+ except (
294
+ httpx.ConnectError,
295
+ httpx.ConnectTimeout,
296
+ httpx.ReadTimeout,
297
+ httpx.NetworkError,
298
+ ) as exc:
299
+ err_console.print(
300
+ "[bold yellow]warning:[/bold yellow] no internet connectivity — "
301
+ f"could not reach the release server ({exc.__class__.__name__}). "
302
+ "Some features may not work correctly until the catalog is refreshed.",
303
+ )
304
+ raise typer.Exit(code=1) from exc
290
305
  return False
291
306
 
292
307
  for asset in ("catalog.msgpack", "catalog.db"):
@@ -444,6 +459,7 @@ def _build_runtime_locally(*, man_dir: Path, include_sdm: bool = False) -> None:
444
459
  f"updated catalog with {len(catalog.intrinsics)} intrinsics and {len(catalog.instructions)} instructions",
445
460
  style="green",
446
461
  )
462
+ write_installed_version_stamp(__version__)
447
463
 
448
464
 
449
465
  def _refresh_runtime_from_existing_catalog(*, man_dir: Path) -> None:
@@ -463,6 +479,7 @@ def _refresh_runtime_from_existing_catalog(*, man_dir: Path) -> None:
463
479
  f"refreshed runtime from existing catalog with {len(catalog.intrinsics)} intrinsics and {len(catalog.instructions)} instructions",
464
480
  style="green",
465
481
  )
482
+ write_installed_version_stamp(__version__)
466
483
 
467
484
 
468
485
  def _finalize_runtime_from_download(*, man_dir: Path) -> None:
@@ -476,6 +493,7 @@ def _finalize_runtime_from_download(*, man_dir: Path) -> None:
476
493
  f"refreshed local web/man assets from downloaded catalog with {len(catalog.intrinsics)} intrinsics and {len(catalog.instructions)} instructions",
477
494
  style="green",
478
495
  )
496
+ write_installed_version_stamp(__version__)
479
497
 
480
498
 
481
499
  def _download_release_or_fallback(*, man_dir: Path) -> None:
@@ -559,9 +577,16 @@ def ensure_catalog():
559
577
 
560
578
 
561
579
  def ensure_runtime() -> None:
562
- """Ensure catalog + SQLite are present and current."""
580
+ """Ensure catalog + SQLite are present and current.
581
+
582
+ If the package version recorded in the data dir does not match the
583
+ currently installed version, transparently re-run the release-download
584
+ flow so users don't need to invoke ``simdref update`` manually after
585
+ a ``pip``/``uv`` install or upgrade. Honors ``SIMDREF_SKIP_AUTOUPDATE``.
586
+ """
563
587
  if not CATALOG_PATH.exists():
564
588
  _bootstrap_interactive()
589
+ _maybe_auto_update_for_version_change()
565
590
  return
566
591
  if not sqlite_schema_is_current():
567
592
  err_console.print(
@@ -569,6 +594,37 @@ def ensure_runtime() -> None:
569
594
  style="yellow",
570
595
  )
571
596
  _refresh_runtime_from_existing_catalog(man_dir=DEFAULT_MAN_DIR)
597
+ _maybe_auto_update_for_version_change()
598
+
599
+
600
+ def _maybe_auto_update_for_version_change() -> None:
601
+ """Refresh data when the package version differs from the stamped one."""
602
+ if os.environ.get("SIMDREF_SKIP_AUTOUPDATE"):
603
+ return
604
+ stamped = read_installed_version_stamp()
605
+ if stamped == __version__:
606
+ return
607
+ if stamped is None:
608
+ # First run after install: stamp without re-downloading. The catalog
609
+ # we just bootstrapped (or that already exists locally) is what the
610
+ # user expects to see.
611
+ write_installed_version_stamp(__version__)
612
+ return
613
+ err_console.print(
614
+ f"simdref upgraded from {stamped} to {__version__}; refreshing catalog "
615
+ "(set SIMDREF_SKIP_AUTOUPDATE=1 to disable)",
616
+ style="yellow",
617
+ )
618
+ try:
619
+ _download_release_or_fallback(man_dir=DEFAULT_MAN_DIR)
620
+ except typer.Exit:
621
+ err_console.print(
622
+ "[bold yellow]warning:[/bold yellow] auto-update failed; continuing with the existing catalog. "
623
+ "Some features may not work correctly until `simdref update` succeeds.",
624
+ )
625
+ # Stamp anyway so we don't retry on every invocation; the user has
626
+ # been warned and can re-run `simdref update` once back online.
627
+ write_installed_version_stamp(__version__)
572
628
 
573
629
 
574
630
  def _catalog_meta(catalog) -> dict:
@@ -700,10 +756,11 @@ def _llm_result_payload(
700
756
  intrinsic_map[result.key] = item
701
757
  if item is not None:
702
758
  lat, cpi = intrinsic_perf_summary_runtime(conn, item, instruction_map)
703
- return {
759
+ payload: dict = {
704
760
  "query": item.name,
705
761
  "intrinsic": item.name,
706
762
  "signature": item.signature,
763
+ "url": getattr(item, "url", "") or "",
707
764
  "instructions": item.instructions,
708
765
  "instruction_refs": item.instruction_refs,
709
766
  "summary": item.description,
@@ -711,6 +768,10 @@ def _llm_result_payload(
711
768
  "lat": lat,
712
769
  "cpi": cpi,
713
770
  }
771
+ operation = _intrinsic_operation_text(item)
772
+ if operation:
773
+ payload["operation"] = operation
774
+ return payload
714
775
  item = instruction_map.get(result.key)
715
776
  if item is None:
716
777
  item = load_instruction_from_db(conn, result.key)
@@ -739,7 +800,7 @@ def _llm_result_payload(
739
800
  def _llm_intrinsic_payload(conn, intrinsic) -> dict:
740
801
  instruction_map: dict[str, object] = {}
741
802
  lat, cpi = intrinsic_perf_summary_runtime(conn, intrinsic, instruction_map)
742
- return {
803
+ payload: dict = {
743
804
  "query": intrinsic.name,
744
805
  "intrinsic": intrinsic.name,
745
806
  "signature": intrinsic.signature,
@@ -751,6 +812,23 @@ def _llm_intrinsic_payload(conn, intrinsic) -> dict:
751
812
  "cpi": cpi,
752
813
  "summary": intrinsic.description,
753
814
  }
815
+ operation = _intrinsic_operation_text(intrinsic)
816
+ if operation:
817
+ # Encodes algorithmic quirks (e.g. the bit-1 selector in
818
+ # ``_mm_permutevar_pd``) that the one-line summary cannot convey.
819
+ payload["operation"] = operation
820
+ return payload
821
+
822
+
823
+ def _intrinsic_operation_text(intrinsic) -> str:
824
+ """Return the SDM-style ``Operation`` pseudocode for *intrinsic*, or ``""``.
825
+
826
+ Intel ingest stores it under ``doc_sections["Operation"]``; ARM ACLE under
827
+ ``doc_sections["ACLE Operation"]``. Both are pseudocode the LLM payload
828
+ needs to surface so callers can reason about behavior beyond the summary.
829
+ """
830
+ sections = getattr(intrinsic, "doc_sections", None) or {}
831
+ return (sections.get("Operation") or sections.get("ACLE Operation") or "").strip()
754
832
 
755
833
 
756
834
  def _llm_instruction_payload(item) -> dict:
@@ -762,9 +840,24 @@ def _llm_instruction_payload(item) -> dict:
762
840
  "lat": lat,
763
841
  "cpi": cpi,
764
842
  "summary": item.summary,
843
+ "source_kinds": _payload_source_kinds(item.arch_details),
765
844
  }
766
845
 
767
846
 
847
+ def _payload_source_kinds(arch_details) -> list[str]:
848
+ """Return the distinct provenance kinds present in an instruction's arch_details."""
849
+ if not isinstance(arch_details, dict):
850
+ return []
851
+ kinds: list[str] = []
852
+ for details in arch_details.values():
853
+ if not isinstance(details, dict):
854
+ continue
855
+ kind = perf._source_kind(details)
856
+ if kind not in kinds:
857
+ kinds.append(kind)
858
+ return kinds
859
+
860
+
768
861
  # ---------------------------------------------------------------------------
769
862
  # Search results
770
863
  # ---------------------------------------------------------------------------
@@ -1184,6 +1277,9 @@ def _llm_filter_records(
1184
1277
 
1185
1278
  def _record_has_source_kind(rec: dict, wanted: str) -> bool:
1186
1279
  """Check whether an llm payload dict carries at least one entry with *wanted* provenance."""
1280
+ slim = rec.get("source_kinds")
1281
+ if isinstance(slim, list) and any(k == wanted for k in slim):
1282
+ return True
1187
1283
  arch_details = rec.get("arch_details") or {}
1188
1284
  if isinstance(arch_details, dict):
1189
1285
  for details in arch_details.values():
@@ -1323,6 +1419,14 @@ def _llm_schema_payload() -> dict:
1323
1419
  "lat": {"type": "string"},
1324
1420
  "cpi": {"type": "string"},
1325
1421
  "summary": {"type": "string"},
1422
+ "url": {
1423
+ "type": "string",
1424
+ "description": "Vendor documentation URL (Intel Intrinsics Guide / ARM ACLE).",
1425
+ },
1426
+ "operation": {
1427
+ "type": "string",
1428
+ "description": "SDM-style pseudocode describing the intrinsic's behavior, when available.",
1429
+ },
1326
1430
  },
1327
1431
  },
1328
1432
  "results": {"type": "array", "items": {"$ref": "#/properties/result"}},
@@ -275,6 +275,13 @@ def parse_intel_payload(text: str) -> list[IntrinsicRecord]:
275
275
  name = node.attrib.get("name", "").strip()
276
276
  if not name:
277
277
  continue
278
+ operation_text = ""
279
+ op_node = node.find("./operation")
280
+ if op_node is not None:
281
+ # ``<operation>`` carries SDM-style pseudocode; preserve internal
282
+ # whitespace (it encodes the algorithm) but trim outer padding.
283
+ raw = "".join(op_node.itertext())
284
+ operation_text = raw.strip("\n").rstrip() if raw else ""
278
285
  return_node = node.find("./return")
279
286
  ret = (
280
287
  node.attrib.get("rettype", "").strip()
@@ -321,6 +328,7 @@ def parse_intel_payload(text: str) -> list[IntrinsicRecord]:
321
328
  header=(
322
329
  (node.findtext("./header") or "").strip() or node.attrib.get("header", "")
323
330
  ),
331
+ url=_intel_intrinsic_url(name),
324
332
  architecture="x86",
325
333
  isa=_normalize_isa(
326
334
  cpuid or node.attrib.get("isa", "") or node.attrib.get("tech", "")
@@ -332,6 +340,7 @@ def parse_intel_payload(text: str) -> list[IntrinsicRecord]:
332
340
  subcategory=node.attrib.get("tech", "").strip(),
333
341
  instructions=instructions,
334
342
  instruction_refs=[ref | {"architecture": "x86"} for ref in instruction_refs],
343
+ doc_sections=({"Operation": operation_text} if operation_text else {}),
335
344
  notes=notes,
336
345
  aliases=[],
337
346
  )
@@ -384,12 +393,16 @@ def parse_intel_payload(text: str) -> list[IntrinsicRecord]:
384
393
  description = str(
385
394
  item.get("description") or item.get("summary") or item.get("technology", "")
386
395
  ).strip()
396
+ operation_text = (
397
+ str(item.get("operation") or item.get("Operation") or "").strip("\n").rstrip()
398
+ )
387
399
  records.append(
388
400
  IntrinsicRecord(
389
401
  name=name,
390
402
  signature=signature,
391
403
  description=description,
392
404
  header=str(item.get("header") or item.get("include") or "").strip(),
405
+ url=str(item.get("url") or "").strip() or _intel_intrinsic_url(name),
393
406
  architecture="x86",
394
407
  isa=_normalize_isa(
395
408
  item.get("isa") or item.get("tech") or item.get("instructionSet") or []
@@ -402,6 +415,7 @@ def parse_intel_payload(text: str) -> list[IntrinsicRecord]:
402
415
  for value in instructions
403
416
  if str(value).strip()
404
417
  ],
418
+ doc_sections=({"Operation": operation_text} if operation_text else {}),
405
419
  notes=[str(value).strip() for value in notes if str(value).strip()],
406
420
  aliases=[str(value).strip() for value in aliases if str(value).strip()],
407
421
  )
@@ -476,6 +490,16 @@ def _arm_intrinsic_url(name: str) -> str:
476
490
  return urljoin(_ARM_ACLE_INTRINSIC_BASE_URL, quote(name))
477
491
 
478
492
 
493
+ _INTEL_INTRINSIC_BASE_URL = (
494
+ "https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html"
495
+ )
496
+
497
+
498
+ def _intel_intrinsic_url(name: str) -> str:
499
+ """Stable Intel Intrinsics Guide deep link for *name* (uses ``#text=`` fragment)."""
500
+ return f"{_INTEL_INTRINSIC_BASE_URL}#text={quote(name)}"
501
+
502
+
479
503
  def _arm_slug(text: str) -> str:
480
504
  slug = re.sub(r"[^a-z0-9]+", "-", text.casefold()).strip("-")
481
505
  return slug or "intrinsics"
@@ -70,9 +70,16 @@ def latency_cycle_values(latencies: list[dict[str, Any]]) -> list[str]:
70
70
  """
71
71
  values: list[str] = []
72
72
  for latency in latencies:
73
+ mem_is_upper = latency.get("cycles_mem_is_upper_bound") == "1"
73
74
  for key in ("cycles", "cycles_mem", "cycles_addr", "cycles_addr_index"):
74
75
  value = latency.get(key)
75
- if value and value not in values:
76
+ if not value:
77
+ continue
78
+ if key == "cycles_mem" and mem_is_upper and _is_numeric(value) and float(value) == 0:
79
+ # uops.info convention: cycles_mem=0 with the upper-bound flag
80
+ # is a "not measured" marker, not a real zero-cycle latency.
81
+ continue
82
+ if value not in values:
76
83
  values.append(value)
77
84
  return values
78
85
 
@@ -82,7 +82,27 @@ else:
82
82
 
83
83
  CATALOG_PATH = DATA_DIR / "catalog.msgpack"
84
84
  SQLITE_PATH = DATA_DIR / "catalog.db"
85
- SQLITE_SCHEMA_VERSION = "11"
85
+ INSTALLED_VERSION_STAMP = DATA_DIR / "installed_version"
86
+ SQLITE_SCHEMA_VERSION = "12"
87
+
88
+
89
+ def read_installed_version_stamp() -> str | None:
90
+ """Return the package version that last refreshed the data dir, or None."""
91
+ try:
92
+ return INSTALLED_VERSION_STAMP.read_text(encoding="utf-8").strip() or None
93
+ except OSError:
94
+ return None
95
+
96
+
97
+ def write_installed_version_stamp(version: str) -> None:
98
+ """Record the package version that just refreshed the data dir."""
99
+ try:
100
+ INSTALLED_VERSION_STAMP.parent.mkdir(parents=True, exist_ok=True)
101
+ INSTALLED_VERSION_STAMP.write_text(version.strip() + "\n", encoding="utf-8")
102
+ except OSError:
103
+ pass # best-effort — stamp persistence must not break commands
104
+
105
+
86
106
  FTS_TOKEN_RE = re.compile(r"[A-Za-z0-9]+")
87
107
  SQLITE_INSERT_BATCH_SIZE = 512
88
108
 
@@ -21,7 +21,7 @@ const themeIconDark = $("theme-icon-dark");
21
21
  const shortcutsOverlay = $("shortcuts-overlay");
22
22
 
23
23
  /* ── State ────────────────────────────────────────────────────────── */
24
- let catalog = null; // search-index.json payload
24
+ let catalog = null; // merged search-index-* shard payload
25
25
  let searchEntries = [];
26
26
  let searchTokenIndex = new Map();
27
27
  let searchPrefixIndex = new Map();
@@ -207,8 +207,18 @@ function _pushBucket(map, key, i) {
207
207
 
208
208
  function buildBuckets() {
209
209
  byKind.clear(); byFamily.clear(); byCategory.clear(); byArmArch.clear();
210
- for (let i = 0; i < searchEntries.length; i++) {
211
- const e = searchEntries[i];
210
+ extendBuckets(searchEntries, 0);
211
+ bucketsBuilt = true;
212
+ }
213
+
214
+ /* Append ``entries`` to the kind/family/category/arm-arch buckets at
215
+ * absolute positions ``baseIndex + offset``. Used by Phase-2 intrinsic
216
+ * ingest so newly-arrived entries are reachable via the kind filter
217
+ * without rebuilding from scratch. */
218
+ function extendBuckets(entries, baseIndex) {
219
+ for (let k = 0; k < entries.length; k++) {
220
+ const e = entries[k];
221
+ const i = baseIndex + k;
212
222
  _pushBucket(byKind, e.kind, i);
213
223
  const fams = e.item.isa_families || [];
214
224
  if (fams.length === 0) _pushBucket(byFamily, "__none", i);
@@ -218,7 +228,6 @@ function buildBuckets() {
218
228
  const arch = e.item.arm_arch || "__none";
219
229
  _pushBucket(byArmArch, arch, i);
220
230
  }
221
- bucketsBuilt = true;
222
231
  }
223
232
 
224
233
  function _unionKeys(map, keys) {
@@ -468,18 +477,27 @@ function rankEntry(query, entry) {
468
477
  function buildSearchIndexes(entries) {
469
478
  searchTokenIndex = new Map();
470
479
  searchPrefixIndex = new Map();
471
- entries.forEach((entry, i) => {
480
+ extendSearchIndexes(entries, 0);
481
+ }
482
+
483
+ /* Append ``entries`` to the existing token/prefix maps, treating their
484
+ * absolute position in ``searchEntries`` as ``baseIndex + offset``. Used
485
+ * by the Phase-2 batched intrinsic ingest to avoid the full-rebuild stall. */
486
+ function extendSearchIndexes(entries, baseIndex) {
487
+ for (let k = 0; k < entries.length; k++) {
488
+ const entry = entries[k];
489
+ const idx = baseIndex + k;
472
490
  entry.searchTokens = [...new Set(entry.fields.flatMap(f => tokens(f)))];
473
491
  for (const t of entry.searchTokens) {
474
492
  if (!searchTokenIndex.has(t)) searchTokenIndex.set(t, []);
475
- searchTokenIndex.get(t).push(i);
493
+ searchTokenIndex.get(t).push(idx);
476
494
  for (let sz = 1; sz <= Math.min(t.length, 6); sz++) {
477
495
  const pfx = t.slice(0, sz);
478
496
  if (!searchPrefixIndex.has(pfx)) searchPrefixIndex.set(pfx, []);
479
- searchPrefixIndex.get(pfx).push(i);
497
+ searchPrefixIndex.get(pfx).push(idx);
480
498
  }
481
499
  }
482
- });
500
+ }
483
501
  }
484
502
 
485
503
  function candidateIndexes(query) {
@@ -1508,22 +1526,54 @@ queryInput.addEventListener("keydown", (e) => {
1508
1526
  });
1509
1527
 
1510
1528
  /* ── Hash navigation ──────────────────────────────────────────────── */
1529
+ let intrinsicsReady = null; // resolved by Phase-2 bootstrap
1530
+
1531
+ function _hashEntry(key) {
1532
+ if (!catalog || !key) return null;
1533
+ return resultPool.find(e => e.key === key)
1534
+ || (catalog.intrByName && catalog.intrByName[key] ? {kind: "intrinsic", key, title: key, subtitle: catalog.intrByName[key].subtitle || "", item: catalog.intrByName[key], fields: catalog.intrByName[key].search_fields || []} : null)
1535
+ || (catalog.instrByKey && catalog.instrByKey[key] ? {kind: "instruction", key, title: catalog.instrByKey[key].display_key || key, subtitle: catalog.instrByKey[key].summary || "", item: catalog.instrByKey[key], fields: catalog.instrByKey[key].search_fields || []} : null);
1536
+ }
1537
+
1511
1538
  window.addEventListener("hashchange", () => {
1512
1539
  const key = decodeURIComponent(location.hash.replace(/^#/, ""));
1513
1540
  if (!catalog || !key) return;
1514
- const entry = resultPool.find(e => e.key === key)
1515
- || (catalog.intrByName[key] ? {kind: "intrinsic", key, title: key, subtitle: catalog.intrByName[key].subtitle || "", item: catalog.intrByName[key], fields: catalog.intrByName[key].search_fields || []} : null)
1516
- || (catalog.instrByKey[key] ? {kind: "instruction", key, title: catalog.instrByKey[key].display_key || key, subtitle: catalog.instrByKey[key].summary || "", item: catalog.instrByKey[key], fields: catalog.instrByKey[key].search_fields || []} : null);
1517
- if (entry) renderDetail(entry);
1541
+ let entry = _hashEntry(key);
1542
+ if (entry) { renderDetail(entry); return; }
1543
+ // Hash may name an intrinsic that Phase 2 hasn't ingested yet.
1544
+ if (intrinsicsReady) {
1545
+ intrinsicsReady.then(() => {
1546
+ const retry = _hashEntry(key);
1547
+ if (retry) renderDetail(retry);
1548
+ });
1549
+ }
1550
+ });
1551
+
1552
+ /* ── Bootstrap ──────────────────────────────────────────────────────
1553
+ *
1554
+ * Two-phase load:
1555
+ * Phase 1 — meta + filter_spec + build_stamp + instructions. These
1556
+ * are small (~1 MB gz total) and let us paint the UI with
1557
+ * the instruction pool searchable immediately.
1558
+ * Phase 2 — intrinsics (~1.7 MB gz). Fetched in parallel from boot,
1559
+ * joined into the search index in idle-time batches so the
1560
+ * page stays responsive while ~93 k entries hydrate.
1561
+ */
1562
+ const _intrinsicsFetch = fetchJson("search-index-intrinsics.json").catch((err) => {
1563
+ console.error("simdref: failed to load intrinsic search shard", err);
1564
+ return null;
1518
1565
  });
1519
1566
 
1520
- /* ── Bootstrap ────────────────────────────────────────────────────── */
1521
1567
  Promise.all([
1522
- fetchJson("search-index.json"),
1568
+ fetchJson("search-index-meta.json"),
1569
+ fetchJson("search-index-instructions.json"),
1523
1570
  fetchJson("filter_spec.json").catch(() => null),
1524
1571
  fetchJson("build_stamp.json").catch(() => null),
1525
1572
  ])
1526
- .then(([data, spec, stamp]) => {
1573
+ .then(([meta, instructions, spec, stamp]) => {
1574
+ const data = meta || {};
1575
+ data.instructions = instructions || [];
1576
+ data.intrinsics = []; // populated in Phase 2
1527
1577
  catalog = data;
1528
1578
  // Prefer filter_spec.json (single source of truth) over embedded isa_config.
1529
1579
  const config = spec || data.isa_config || {};
@@ -1541,25 +1591,24 @@ Promise.all([
1541
1591
  metaNode.dataset.stamp = label;
1542
1592
  if (stale) metaNode.classList.add("stale");
1543
1593
  }
1544
- catalog.intrByName = Object.fromEntries(data.intrinsics.map(i => [i.name, i]));
1594
+ catalog.intrByName = Object.create(null);
1545
1595
  catalog.instrByKey = Object.fromEntries(data.instructions.map(i => [i.key, i]));
1546
1596
  catalog.instrByDisplayKey = Object.fromEntries(data.instructions.map(i => [i.display_key || i.key, i]));
1547
1597
  catalog.instrByMnem = Object.fromEntries(data.instructions.map(i => [i.mnemonic, i]));
1548
1598
 
1549
- searchEntries = [
1550
- ...data.intrinsics.map(i => ({
1551
- kind: "intrinsic", key: i.name, title: i.name, subtitle: i.subtitle || i.description || "", item: i,
1552
- fields: i.search_fields || [i.name, i.description || "", i.display_isa || displayIsa(i.isa), (i.instructions || []).join(" ")],
1553
- })),
1554
- ...data.instructions.map(i => ({
1555
- kind: "instruction", key: i.key, title: i.display_key || i.key, subtitle: i.summary || "", item: i,
1556
- fields: i.search_fields || [i.display_mnemonic || i.mnemonic || "", i.display_form || i.form || "", i.summary || "", i.display_isa || displayIsa(i.isa)],
1557
- })),
1558
- ];
1599
+ searchEntries = data.instructions.map(i => ({
1600
+ kind: "instruction", key: i.key, title: i.display_key || i.key, subtitle: i.summary || "", item: i,
1601
+ fields: i.search_fields || [i.display_mnemonic || i.mnemonic || "", i.display_form || i.form || "", i.summary || "", i.display_isa || displayIsa(i.isa)],
1602
+ }));
1559
1603
 
1560
1604
  buildSearchIndexes(searchEntries);
1561
1605
 
1562
- availableIsas = [...new Set(searchEntries.flatMap(e => e.item.isa_families || []))]
1606
+ // Seed the filter chips from the meta shard so the union of ISA
1607
+ // families is correct from first paint, not just instruction families.
1608
+ const seededIsas = Array.isArray(data.available_isas) && data.available_isas.length
1609
+ ? data.available_isas
1610
+ : [...new Set(searchEntries.flatMap(e => e.item.isa_families || []))];
1611
+ availableIsas = [...new Set(seededIsas)]
1563
1612
  .sort((a, b) => (isaFamilyOrder[a] ?? 99) - (isaFamilyOrder[b] ?? 99) || a.localeCompare(b));
1564
1613
  enabledIsas = new Set([...defaultEnabledIsas].filter(v => availableIsas.includes(v)));
1565
1614
  initEnabledSubIsas();
@@ -1588,12 +1637,16 @@ Promise.all([
1588
1637
  // Build-stamp badge text is set above if a stamp is present; only fall
1589
1638
  // back to the catalog-size summary when no stamp was emitted.
1590
1639
  if (!metaNode.dataset.stamp) {
1591
- metaNode.textContent = `${data.intrinsics.length} intrinsics \u00b7 ${data.instructions.length} instructions`;
1640
+ // Intrinsic count is filled in by Phase 2 once the shard arrives.
1641
+ metaNode.textContent = `${data.instructions.length} instructions`;
1592
1642
  }
1593
1643
 
1594
1644
  const fromHash = decodeURIComponent(location.hash.replace(/^#/, ""));
1595
1645
  if (fromHash) queryInput.value = fromHash;
1596
1646
  renderResults();
1647
+
1648
+ // Phase 2: fold intrinsics into the search index without blocking.
1649
+ intrinsicsReady = _ingestIntrinsics(metaNode);
1597
1650
  })
1598
1651
  .catch((err) => {
1599
1652
  // Fetch failure (CORS, 404, gzip misconfig) should not leave "Loading..." up forever.
@@ -1606,6 +1659,80 @@ Promise.all([
1606
1659
  }
1607
1660
  });
1608
1661
 
1662
+ /* Phase 2 — fold the intrinsic shard into searchEntries / search index in
1663
+ * idle-time batches so the main thread keeps responding to scrolls and
1664
+ * keystrokes. Resolves once every intrinsic is searchable. */
1665
+ function _ingestIntrinsics(metaNode) {
1666
+ const BATCH = 5000;
1667
+ const schedule = window.requestIdleCallback
1668
+ ? (cb) => window.requestIdleCallback(cb, {timeout: 250})
1669
+ : (cb) => setTimeout(cb, 0);
1670
+
1671
+ // Badge: surface to the user that intrinsics are still streaming in.
1672
+ let badge = null;
1673
+ const savedStamp = metaNode ? metaNode.dataset.stamp || "" : "";
1674
+ if (metaNode) {
1675
+ badge = document.createElement("span");
1676
+ badge.className = "meta-loading";
1677
+ badge.style.marginLeft = "0.5rem";
1678
+ badge.style.color = "var(--text-muted, #888)";
1679
+ badge.textContent = "loading intrinsics…";
1680
+ metaNode.appendChild(badge);
1681
+ }
1682
+
1683
+ return _intrinsicsFetch.then(intrinsics => new Promise(resolve => {
1684
+ if (!Array.isArray(intrinsics) || !intrinsics.length) {
1685
+ if (badge && badge.parentNode) badge.parentNode.removeChild(badge);
1686
+ resolve();
1687
+ return;
1688
+ }
1689
+ catalog.intrinsics = intrinsics;
1690
+ for (const item of intrinsics) catalog.intrByName[item.name] = item;
1691
+
1692
+ let cursor = 0;
1693
+ const total = intrinsics.length;
1694
+ const pump = () => {
1695
+ const stop = Math.min(cursor + BATCH, total);
1696
+ const batch = [];
1697
+ for (let i = cursor; i < stop; i++) {
1698
+ const item = intrinsics[i];
1699
+ batch.push({
1700
+ kind: "intrinsic",
1701
+ key: item.name,
1702
+ title: item.name,
1703
+ subtitle: item.subtitle || item.description || "",
1704
+ item,
1705
+ fields: item.search_fields || [item.name, item.description || "", item.display_isa || displayIsa(item.isa), (item.instructions || []).join(" ")],
1706
+ });
1707
+ }
1708
+ const base = searchEntries.length;
1709
+ for (const e of batch) searchEntries.push(e);
1710
+ extendSearchIndexes(batch, base);
1711
+ if (bucketsBuilt) extendBuckets(batch, base);
1712
+ cursor = stop;
1713
+
1714
+ // Re-render if the user is already typing or the visible list is
1715
+ // short enough that new hits would actually show up.
1716
+ visibleSet = null;
1717
+ if ((queryInput && queryInput.value) || resultPool.length < 100) {
1718
+ renderResults();
1719
+ }
1720
+
1721
+ if (cursor < total) {
1722
+ schedule(pump);
1723
+ } else {
1724
+ if (badge && badge.parentNode) badge.parentNode.removeChild(badge);
1725
+ if (metaNode && !metaNode.dataset.stamp) {
1726
+ metaNode.textContent = `${total} intrinsics · ${(catalog.instructions || []).length} instructions`;
1727
+ }
1728
+ renderResults();
1729
+ resolve();
1730
+ }
1731
+ };
1732
+ schedule(pump);
1733
+ }));
1734
+ }
1735
+
1609
1736
  /* ── Annotate tab ────────────────────────────────────────────────────
1610
1737
  * Client-side port of src/simdref/annotate.py. Reuses the existing
1611
1738
  * fetchJson, chunkPrefix, loadChunk helpers. Zero backend.