simdref 0.0.13__tar.gz → 0.0.14__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 (98) hide show
  1. {simdref-0.0.13/src/simdref.egg-info → simdref-0.0.14}/PKG-INFO +3 -2
  2. {simdref-0.0.13 → simdref-0.0.14}/README.md +1 -1
  3. {simdref-0.0.13 → simdref-0.0.14}/pyproject.toml +6 -1
  4. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/annotate.py +50 -15
  5. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/arm_instructions.py +92 -11
  6. simdref-0.0.14/src/simdref/asm_identity.py +928 -0
  7. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/cli.py +20 -1
  8. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/display.py +27 -5
  9. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/export.py +65 -2
  10. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/ingest_catalog.py +110 -18
  11. simdref-0.0.14/src/simdref/lsp.py +1574 -0
  12. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/models.py +18 -4
  13. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/riscv.py +77 -33
  14. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/storage.py +41 -12
  15. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/tui.py +1 -1
  16. {simdref-0.0.13 → simdref-0.0.14/src/simdref.egg-info}/PKG-INFO +3 -2
  17. {simdref-0.0.13 → simdref-0.0.14}/src/simdref.egg-info/SOURCES.txt +15 -0
  18. simdref-0.0.14/src/simdref.egg-info/requires.txt +13 -0
  19. simdref-0.0.14/tests/test_asm_identity.py +1260 -0
  20. simdref-0.0.14/tests/test_asm_wire.py +173 -0
  21. {simdref-0.0.13 → simdref-0.0.14}/tests/test_auto_update_on_version_change.py +5 -0
  22. {simdref-0.0.13 → simdref-0.0.14}/tests/test_display.py +66 -0
  23. {simdref-0.0.13 → simdref-0.0.14}/tests/test_export.py +101 -0
  24. simdref-0.0.14/tests/test_ingest_enc_fields.py +374 -0
  25. simdref-0.0.14/tests/test_leftover_arm_form_pick.py +276 -0
  26. simdref-0.0.14/tests/test_leftover_arm_postindex.py +17 -0
  27. simdref-0.0.14/tests/test_leftover_att_movx.py +74 -0
  28. simdref-0.0.14/tests/test_leftover_autoupdate.py +23 -0
  29. simdref-0.0.14/tests/test_leftover_catalog_gaps.py +277 -0
  30. simdref-0.0.14/tests/test_leftover_ingest_isa_merge.py +168 -0
  31. simdref-0.0.14/tests/test_leftover_intel_size.py +103 -0
  32. simdref-0.0.14/tests/test_leftover_perf_join.py +137 -0
  33. simdref-0.0.14/tests/test_leftover_riscv_shared.py +109 -0
  34. {simdref-0.0.13 → simdref-0.0.14}/tests/test_lsp_stdio.py +107 -22
  35. simdref-0.0.14/tests/test_source_validation.py +417 -0
  36. {simdref-0.0.13 → simdref-0.0.14}/tests/test_storage_payload.py +36 -0
  37. {simdref-0.0.13 → simdref-0.0.14}/tests/test_storage_schema.py +3 -3
  38. simdref-0.0.14/tests/test_vendor_riscv_categories.py +80 -0
  39. simdref-0.0.14/tests/test_vendor_riscv_sources.py +35 -0
  40. simdref-0.0.13/src/simdref/lsp.py +0 -811
  41. simdref-0.0.13/src/simdref.egg-info/requires.txt +0 -10
  42. simdref-0.0.13/tests/test_source_validation.py +0 -48
  43. {simdref-0.0.13 → simdref-0.0.14}/LICENSE +0 -0
  44. {simdref-0.0.13 → simdref-0.0.14}/setup.cfg +0 -0
  45. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/__init__.py +0 -0
  46. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/__main__.py +0 -0
  47. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/filters.py +0 -0
  48. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/ingest.py +0 -0
  49. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/ingest_pdf.py +0 -0
  50. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/ingest_sources.py +0 -0
  51. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/manpages.py +0 -0
  52. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/pdfparse/__init__.py +0 -0
  53. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/pdfparse/base.py +0 -0
  54. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/pdfparse/intel.py +0 -0
  55. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/pdfparse/registry.py +0 -0
  56. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/pdfparse/types.py +0 -0
  57. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/pdfrefs.py +0 -0
  58. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/perf.py +0 -0
  59. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/perf_sources/__init__.py +0 -0
  60. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/perf_sources/cores.py +0 -0
  61. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/perf_sources/llvm_mca.py +0 -0
  62. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/perf_sources/llvm_scheduling.py +0 -0
  63. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/perf_sources/merge.py +0 -0
  64. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/queries.py +0 -0
  65. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/search.py +0 -0
  66. {simdref-0.0.13 → simdref-0.0.14}/src/simdref/ui_labels.py +0 -0
  67. {simdref-0.0.13 → simdref-0.0.14}/src/simdref.egg-info/dependency_links.txt +0 -0
  68. {simdref-0.0.13 → simdref-0.0.14}/src/simdref.egg-info/entry_points.txt +0 -0
  69. {simdref-0.0.13 → simdref-0.0.14}/src/simdref.egg-info/top_level.txt +0 -0
  70. {simdref-0.0.13 → simdref-0.0.14}/tests/test_annotate.py +0 -0
  71. {simdref-0.0.13 → simdref-0.0.14}/tests/test_annotate_positions.py +0 -0
  72. {simdref-0.0.13 → simdref-0.0.14}/tests/test_audit_coverage.py +0 -0
  73. {simdref-0.0.13 → simdref-0.0.14}/tests/test_cli_bare_query.py +0 -0
  74. {simdref-0.0.13 → simdref-0.0.14}/tests/test_cli_bootstrap_progress.py +0 -0
  75. {simdref-0.0.13 → simdref-0.0.14}/tests/test_cli_help.py +0 -0
  76. {simdref-0.0.13 → simdref-0.0.14}/tests/test_cli_llm.py +0 -0
  77. {simdref-0.0.13 → simdref-0.0.14}/tests/test_coverage_live.py +0 -0
  78. {simdref-0.0.13 → simdref-0.0.14}/tests/test_coverage_parity.py +0 -0
  79. {simdref-0.0.13 → simdref-0.0.14}/tests/test_filters.py +0 -0
  80. {simdref-0.0.13 → simdref-0.0.14}/tests/test_ingest_sources.py +0 -0
  81. {simdref-0.0.13 → simdref-0.0.14}/tests/test_intel_operation_and_url.py +0 -0
  82. {simdref-0.0.13 → simdref-0.0.14}/tests/test_issue2_fixes.py +0 -0
  83. {simdref-0.0.13 → simdref-0.0.14}/tests/test_issues_23_24_25.py +0 -0
  84. {simdref-0.0.13 → simdref-0.0.14}/tests/test_lsp_web.py +0 -0
  85. {simdref-0.0.13 → simdref-0.0.14}/tests/test_models.py +0 -0
  86. {simdref-0.0.13 → simdref-0.0.14}/tests/test_pdfparse.py +0 -0
  87. {simdref-0.0.13 → simdref-0.0.14}/tests/test_perf.py +0 -0
  88. {simdref-0.0.13 → simdref-0.0.14}/tests/test_perf_sources.py +0 -0
  89. {simdref-0.0.13 → simdref-0.0.14}/tests/test_presentation.py +0 -0
  90. {simdref-0.0.13 → simdref-0.0.14}/tests/test_preset_persistence.py +0 -0
  91. {simdref-0.0.13 → simdref-0.0.14}/tests/test_riscv_docs_pages.py +0 -0
  92. {simdref-0.0.13 → simdref-0.0.14}/tests/test_search.py +0 -0
  93. {simdref-0.0.13 → simdref-0.0.14}/tests/test_search_pushdown.py +0 -0
  94. {simdref-0.0.13 → simdref-0.0.14}/tests/test_source_kind_filter.py +0 -0
  95. {simdref-0.0.13 → simdref-0.0.14}/tests/test_tui.py +0 -0
  96. {simdref-0.0.13 → simdref-0.0.14}/tests/test_ui_labels_parity.py +0 -0
  97. {simdref-0.0.13 → simdref-0.0.14}/tests/test_version_matches_pyproject.py +0 -0
  98. {simdref-0.0.13 → simdref-0.0.14}/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.13
3
+ Version: 0.0.14
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
@@ -28,6 +28,7 @@ Requires-Dist: PyMuPDF<2,>=1.28
28
28
  Requires-Dist: PyYAML<7,>=6.0.3
29
29
  Requires-Dist: rapidfuzz<4,>=3.14
30
30
  Requires-Dist: rich<16,>=15
31
+ Requires-Dist: simdref-llvm-mc==23.1.4; (sys_platform == "linux" and (platform_machine == "x86_64" or platform_machine == "aarch64")) or (sys_platform == "darwin" and (platform_machine == "arm64" or platform_machine == "x86_64")) or (sys_platform == "win32" and platform_machine == "AMD64")
31
32
  Requires-Dist: textual<9,>=8.2
32
33
  Requires-Dist: typer<1,>=0.27
33
34
  Dynamic: license-file
@@ -106,7 +107,7 @@ simdref-lsp
106
107
 
107
108
  Editor clients, one for each editor:
108
109
 
109
- - Zed: [zed-simdref](https://github.com/simd-labs/zed-simdref)
110
+ - Zed: [zed-asm-intrinsics](https://github.com/simd-labs/zed-asm-intrinsics)
110
111
  - VS Code: [vscode-simdref](https://github.com/simd-labs/vscode-simdref)
111
112
  - JetBrains: [jetbrains-simdref](https://github.com/simd-labs/jetbrains-simdref)
112
113
  - Neovim: [nvim-simdref](https://github.com/simd-labs/nvim-simdref)
@@ -72,7 +72,7 @@ simdref-lsp
72
72
 
73
73
  Editor clients, one for each editor:
74
74
 
75
- - Zed: [zed-simdref](https://github.com/simd-labs/zed-simdref)
75
+ - Zed: [zed-asm-intrinsics](https://github.com/simd-labs/zed-asm-intrinsics)
76
76
  - VS Code: [vscode-simdref](https://github.com/simd-labs/vscode-simdref)
77
77
  - JetBrains: [jetbrains-simdref](https://github.com/simd-labs/jetbrains-simdref)
78
78
  - Neovim: [nvim-simdref](https://github.com/simd-labs/nvim-simdref)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simdref"
7
- version = "0.0.13"
7
+ version = "0.0.14"
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"
@@ -41,6 +41,11 @@ dependencies = [
41
41
  "PyYAML>=6.0.3,<7",
42
42
  "rapidfuzz>=3.14,<4",
43
43
  "rich>=15,<16",
44
+ # Pinned to the LLVM the catalog build used. Wheels exist only for
45
+ # manylinux x86_64 aarch64, macOS arm64 x86_64 and win_amd64, so the
46
+ # marker keeps the dependency off every other platform (musl included),
47
+ # where pip must see no candidate instead of failing to resolve.
48
+ 'simdref-llvm-mc==23.1.4; (sys_platform == "linux" and (platform_machine == "x86_64" or platform_machine == "aarch64")) or (sys_platform == "darwin" and (platform_machine == "arm64" or platform_machine == "x86_64")) or (sys_platform == "win32" and platform_machine == "AMD64")',
44
49
  "textual>=8.2,<9",
45
50
  "typer>=0.27,<1",
46
51
  ]
@@ -17,8 +17,9 @@ 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
21
  from simdref.models import InstructionRecord
21
- from simdref.storage import load_instructions_by_mnemonic_from_db
22
+ from simdref.storage import load_instruction_from_db, load_instructions_by_mnemonic_from_db
22
23
 
23
24
 
24
25
  class LineKind(str, Enum):
@@ -410,6 +411,9 @@ def _operand_match_score(
410
411
  return score
411
412
 
412
413
 
414
+ # shortcut: the form pick answers only when byte identity has no row
415
+ # (schema-13 catalogs or missing tools); remove it at the schema-14 data
416
+ # release with simdref-llvm-mc on PyPI as a required dependency
413
417
  def pick_record(
414
418
  records: list[InstructionRecord],
415
419
  *,
@@ -679,22 +683,30 @@ def _annotate_instruction(
679
683
  conn: sqlite3.Connection,
680
684
  lookup_cache: dict[str, list[InstructionRecord]] | None = None,
681
685
  stats: dict[str, int] | None = None,
686
+ identity_key: str | None = None,
682
687
  ) -> tuple[str, dict[str, Any] | None]:
683
688
  """Return the rendered output line and an optional JSON record."""
684
- if lookup_cache is not None:
685
- records = lookup_cache.get(parsed.mnemonic)
686
- if records is None:
687
- # Module-level call (not a captured reference) so tests can
688
- # keep monkeypatching ``annotate.lookup`` with a 2-arg fake.
689
- records = lookup(parsed.mnemonic, conn)
690
- lookup_cache[parsed.mnemonic] = records
691
- else:
692
- records = lookup(parsed.mnemonic, conn)
693
689
  if stats is not None:
694
690
  stats["parsed"] = stats.get("parsed", 0) + 1
695
- if records:
691
+ # Byte identity wins over the form pick: the record is what the line
692
+ # assembles to, and an empty row keeps the old pick unchanged.
693
+ record = load_instruction_from_db(conn, identity_key) if identity_key else None
694
+ if record is not None:
695
+ if stats is not None:
696
+ stats["recognized"] = stats.get("recognized", 0) + 1
697
+ else:
698
+ if lookup_cache is not None:
699
+ records = lookup_cache.get(parsed.mnemonic)
700
+ if records is None:
701
+ # Module-level call (not a captured reference) so tests can
702
+ # keep monkeypatching ``annotate.lookup`` with a 2-arg fake.
703
+ records = lookup(parsed.mnemonic, conn)
704
+ lookup_cache[parsed.mnemonic] = records
705
+ else:
706
+ records = lookup(parsed.mnemonic, conn)
707
+ if stats is not None and records:
696
708
  stats["recognized"] = stats.get("recognized", 0) + 1
697
- record = pick_record(records, arch=opts.arch, operands=parsed.operands)
709
+ record = pick_record(records, arch=opts.arch, operands=parsed.operands)
698
710
 
699
711
  if record is None:
700
712
  if opts.unknown == "drop":
@@ -784,8 +796,29 @@ def annotate_stream(
784
796
  pending_src_line: int | None = None
785
797
  lookup_cache: dict[str, list[InstructionRecord]] = {}
786
798
 
787
- for line in lines:
788
- parsed = parse_asm_line(line, track_positions=opts.track_positions)
799
+ parsed_lines = [parse_asm_line(line, track_positions=opts.track_positions) for line in lines]
800
+ # One llvm-mc run for the whole input: byte identity per instruction
801
+ # line. An objdump line assembles as its mnemonic and operands only, so
802
+ # the address and byte columns never reach the assembler.
803
+ items = [
804
+ (
805
+ index,
806
+ f"{parsed.mnemonic} {parsed.operands}".strip()
807
+ if parsed.address is not None
808
+ else parsed.raw,
809
+ )
810
+ for index, parsed in enumerate(parsed_lines)
811
+ if parsed.kind == LineKind.INSTRUCTION
812
+ ]
813
+ identity = {
814
+ index: key
815
+ for (index, _text), key in zip(
816
+ items, identity_keys([text for _index, text in items], None, conn)
817
+ )
818
+ if key
819
+ }
820
+
821
+ for index, parsed in enumerate(parsed_lines):
789
822
  if opts.track_positions:
790
823
  if parsed.kind == LineKind.INSTRUCTION:
791
824
  parsed.source_file = pending_src_file
@@ -815,7 +848,9 @@ def annotate_stream(
815
848
  if not collecting_json:
816
849
  yield parsed.raw + "\n"
817
850
  continue
818
- out_line, record = _annotate_instruction(parsed, opts, conn, lookup_cache, stats)
851
+ out_line, record = _annotate_instruction(
852
+ parsed, opts, conn, lookup_cache, stats, identity_key=identity.get(index)
853
+ )
819
854
  if collecting_json:
820
855
  if record is not None:
821
856
  json_records.append(record)
@@ -256,6 +256,18 @@ def _candidate_instruction_lists(payload: Any) -> list[list[dict[str, Any]]]:
256
256
  return lists
257
257
 
258
258
 
259
+ def _is_aarchmrs_tree(payload: Any) -> bool:
260
+ top = payload.get("instructions") if isinstance(payload, dict) else None
261
+ return (
262
+ isinstance(top, list)
263
+ and bool(top)
264
+ and all(
265
+ isinstance(node, dict) and str(node.get("_type", "")).endswith("InstructionSet")
266
+ for node in top
267
+ )
268
+ )
269
+
270
+
259
271
  def _records_from_payload(payload: Any) -> list[InstructionRecord]:
260
272
  if isinstance(payload, dict) and payload.get("format") == "arm-aarchmrs-instructions-v1":
261
273
  instructions_json = str(payload.get("instructions_json") or "[]")
@@ -264,9 +276,8 @@ def _records_from_payload(payload: Any) -> list[InstructionRecord]:
264
276
  if isinstance(payload, dict) and payload.get("format") == "arm-instructions-fixture-v1":
265
277
  return _records_from_payload(payload.get("instructions") or [])
266
278
 
267
- aarchmrs = _records_from_aarchmrs_tree(payload)
268
- if aarchmrs:
269
- return aarchmrs
279
+ if _is_aarchmrs_tree(payload):
280
+ return _records_from_aarchmrs_tree(payload)
270
281
 
271
282
  for candidates in _candidate_instruction_lists(payload):
272
283
  records = [_normalize_instruction_item(item) for item in candidates]
@@ -280,6 +291,44 @@ def _records_from_payload(payload: Any) -> list[InstructionRecord]:
280
291
  # AARCHMRS (official Arm machine-readable spec) tree walker
281
292
  # ---------------------------------------------------------------------------
282
293
 
294
+
295
+ def _encodeset_mask_match(encoding: Any) -> tuple[int, int, int] | None:
296
+ """Return (bits, mask, match) for an AARCHMRS encodeset, or None.
297
+
298
+ Every encodeset entry value with a fixed '0'/'1' pattern adds mask and
299
+ match bits; 'x' entries (operand fields) add nothing. An entry with a
300
+ non-binary value or a length that disagrees with its range makes the
301
+ whole encodeset unusable as a key.
302
+ """
303
+ if not isinstance(encoding, dict):
304
+ return None
305
+ width = encoding.get("width")
306
+ values = encoding.get("values")
307
+ if width != 32 or not isinstance(values, list):
308
+ return None
309
+ mask = match = 0
310
+ for entry in values:
311
+ if not isinstance(entry, dict):
312
+ return None
313
+ value = str(((entry.get("value") or {}).get("value")) or "").strip("'")
314
+ rng = entry.get("range") or {}
315
+ start, field_width = rng.get("start"), rng.get("width")
316
+ if not isinstance(start, int) or not isinstance(field_width, int):
317
+ return None
318
+ if len(value) != field_width:
319
+ return None
320
+ for i, char in enumerate(value):
321
+ bit = start + field_width - 1 - i
322
+ if char == "x":
323
+ continue
324
+ if char not in "01":
325
+ return None
326
+ mask |= 1 << bit
327
+ if char == "1":
328
+ match |= 1 << bit
329
+ return 32, mask, match
330
+
331
+
283
332
  _RULE_PLACEHOLDER: dict[str, str] = {
284
333
  # Scalar
285
334
  "Xd": "x0",
@@ -311,6 +360,13 @@ _RULE_PLACEHOLDER: dict[str, str] = {
311
360
  "Dn": "d1",
312
361
  "Dm": "d2",
313
362
  "Da": "d3",
363
+ # FP accumulator register operands; t/s letters follow the Xt/Xs numbering.
364
+ "fpfar_dt": "d0",
365
+ "fpfar_ds": "d1",
366
+ "fpfar_st": "s0",
367
+ "fpfar_ss": "s1",
368
+ "fpfar_ht": "h0",
369
+ "fpfar_hs": "h1",
314
370
  "Sd": "s0",
315
371
  "Sn": "s1",
316
372
  "Sm": "s2",
@@ -451,11 +507,16 @@ def _records_from_aarchmrs_tree(payload: Any) -> list[InstructionRecord]:
451
507
  return []
452
508
 
453
509
  records: list[InstructionRecord] = []
454
- seen_keys: set[tuple[str, str]] = set()
510
+ seen_keys: dict[tuple[str, str], tuple[int, int]] = {}
455
511
 
456
512
  def walk(node: dict[str, Any], group_path: list[str]) -> None:
457
513
  node_type = str(node.get("_type", ""))
458
514
  if node_type.endswith("Instruction"):
515
+ enc = _encodeset_mask_match(node.get("encoding"))
516
+ if enc is None:
517
+ enc_bits = enc_mask = enc_match = 0
518
+ else:
519
+ enc_bits, enc_mask, enc_match = enc
459
520
  assembly = node.get("assembly")
460
521
  if not isinstance(assembly, dict):
461
522
  return
@@ -463,15 +524,25 @@ def _records_from_aarchmrs_tree(payload: Any) -> list[InstructionRecord]:
463
524
  if not rendered:
464
525
  return
465
526
  head, _, tail = rendered.partition(" ")
466
- mnemonic = head.strip().upper()
467
- if not mnemonic or not re.match(r"^[A-Z][A-Z0-9]*$", mnemonic):
527
+ # Head tokens can glue a literal or rule placeholder onto the
528
+ # mnemonic (RET<Rn_option>, DCPS1#0, B.<cond_option>): take the
529
+ # leading alphanumeric run instead of dropping the whole node.
530
+ mnemonic_match = re.match(r"[A-Z][A-Z0-9]*", head.strip().upper())
531
+ if not mnemonic_match:
468
532
  return
533
+ mnemonic = mnemonic_match.group(0)
469
534
  operand_form = tail.strip()
470
535
  form = _canonical_instruction_key(mnemonic, operand_form) if operand_form else mnemonic
471
536
  key = (mnemonic, form.casefold())
472
- if key in seen_keys:
473
- return
474
- seen_keys.add(key)
537
+ seen_enc = seen_keys.get(key)
538
+ if seen_enc is not None:
539
+ if seen_enc == (enc_mask, enc_match):
540
+ return
541
+ # Sibling encodings of one rendered form stay separate rows;
542
+ # their keys are never merged (spec K4).
543
+ form = f"{form} [{str(node.get('name') or '').strip()}]"
544
+ key = (mnemonic, form.casefold())
545
+ seen_keys[key] = (enc_mask, enc_match)
475
546
  summary = _generated_summary(mnemonic)
476
547
  if not summary.endswith("."):
477
548
  summary += "."
@@ -490,6 +561,9 @@ def _records_from_aarchmrs_tree(payload: Any) -> list[InstructionRecord]:
490
561
  isa=_infer_aarchmrs_isa(group_path),
491
562
  metadata=metadata,
492
563
  source="arm-a64",
564
+ enc_bits=enc_bits,
565
+ enc_mask=enc_mask,
566
+ enc_match=enc_match,
493
567
  )
494
568
  )
495
569
  return
@@ -503,8 +577,15 @@ def _records_from_aarchmrs_tree(payload: Any) -> list[InstructionRecord]:
503
577
  walk(child, next_path)
504
578
 
505
579
  for top_node in top:
506
- if isinstance(top_node, dict):
507
- walk(top_node, [str(top_node.get("name") or "")])
580
+ if not isinstance(top_node, dict):
581
+ continue
582
+ set_name = str(top_node.get("name") or "")
583
+ # A32/T32 encodings cannot be scoped from a bare word (their patterns
584
+ # overlap A32 condition space), and the byte key is defined on the A64
585
+ # instruction set only (spec K2/K4).
586
+ if set_name != "A64":
587
+ continue
588
+ walk(top_node, [set_name])
508
589
  return records
509
590
 
510
591