simdref 0.0.0__py3-none-any.whl

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.
simdref/display.py ADDED
@@ -0,0 +1,963 @@
1
+ """Terminal display and formatting for simdref CLI output.
2
+
3
+ This module owns the shared :class:`~rich.console.Console` instance and all
4
+ functions that render intrinsics, instructions, performance tables, and ISA
5
+ metadata to the terminal. The CLI command handlers in :mod:`simdref.cli`
6
+ delegate to these functions for all Rich-based output.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from typing import TYPE_CHECKING
13
+
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.rule import Rule
17
+ from rich.table import Table
18
+
19
+ from simdref.perf import (
20
+ latency_cycle_values,
21
+ variant_perf_summary,
22
+ variant_perf_summary_labeled,
23
+ )
24
+ from simdref.pdfrefs import normalize_pdf_refs, pdf_ref_label
25
+ from simdref.queries import linked_instruction_records
26
+
27
+ if TYPE_CHECKING:
28
+ import sqlite3
29
+
30
+ from simdref.models import Catalog, InstructionRecord, IntrinsicRecord
31
+ from simdref.search import SearchResult
32
+
33
+ console = Console()
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Microarchitecture constants
37
+ # ---------------------------------------------------------------------------
38
+
39
+ UARCH_ORDER = [
40
+ "ARL-P", "ARL-E", "MTL-P", "MTL-E", "EMR",
41
+ "ADL-P", "ADL-E", "RKL", "TGL", "ICL",
42
+ "CLX", "CNL", "SKX", "CFL", "KBL", "SKL",
43
+ "BDW", "HSW", "IVB", "SNB",
44
+ "ZEN5", "ZEN4", "ZEN3", "ZEN2", "ZEN+",
45
+ ]
46
+
47
+ UARCH_LABELS: dict[str, tuple[str, str]] = {
48
+ "ARL-P": ("Arrow Lake-P", "2024"),
49
+ "ARL-E": ("Arrow Lake-E", "2024"),
50
+ "MTL-P": ("Meteor Lake-P", "2023"),
51
+ "MTL-E": ("Meteor Lake-E", "2023"),
52
+ "EMR": ("Emerald Rapids", "2023"),
53
+ "ADL-P": ("Alder Lake-P", "2021"),
54
+ "ADL-E": ("Alder Lake-E", "2021"),
55
+ "RKL": ("Rocket Lake", "2021"),
56
+ "TGL": ("Tiger Lake", "2020"),
57
+ "ICL": ("Ice Lake", "2019"),
58
+ "CLX": ("Cascade Lake", "2019"),
59
+ "CNL": ("Cannon Lake", "2018"),
60
+ "SKX": ("Skylake-X", "2017"),
61
+ "CFL": ("Coffee Lake", "2017"),
62
+ "KBL": ("Kaby Lake", "2016"),
63
+ "SKL": ("Skylake", "2015"),
64
+ "BDW": ("Broadwell", "2014"),
65
+ "HSW": ("Haswell", "2013"),
66
+ "IVB": ("Ivy Bridge", "2012"),
67
+ "SNB": ("Sandy Bridge", "2011"),
68
+ "ZEN5": ("Zen 5", "2024"),
69
+ "ZEN4": ("Zen 4", "2022"),
70
+ "ZEN3": ("Zen 3", "2020"),
71
+ "ZEN2": ("Zen 2", "2019"),
72
+ "ZEN+": ("Zen+", "2018"),
73
+ "AMT": ("Atom", "2012"),
74
+ "BNL": ("Bonnell", "2008"),
75
+ "CON": ("Conroe", "2006"),
76
+ "GLM": ("Goldmont", "2016"),
77
+ "GLP": ("Goldmont Plus", "2017"),
78
+ "NHM": ("Nehalem", "2008"),
79
+ "TRM": ("Tremont", "2019"),
80
+ "WOL": ("Wolfdale", "2007"),
81
+ "WSM": ("Westmere", "2010"),
82
+ }
83
+
84
+ # ISA chronological ordering for sort keys.
85
+ ISA_CHRONOLOGY: dict[str, tuple[int, int]] = {
86
+ "I86": (0, 0), "MMX": (1, 0),
87
+ "SSE": (2, 0), "SSE2": (2, 1), "SSE3": (2, 2), "SSSE3": (2, 3),
88
+ "SSE4A": (2, 4), "SSE4.1": (2, 5), "SSE4.2": (2, 6),
89
+ "AES": (2, 7), "PCLMULQDQ": (2, 8),
90
+ "F16C": (3, 0), "FMA": (3, 1),
91
+ "AVX": (4, 0), "NEON": (4, 1), "AVX2": (5, 0),
92
+ "AVX512F": (6, 0), "AVX512DQ": (6, 1), "AVX512IFMA": (6, 2),
93
+ "AVX512PF": (6, 3), "AVX512ER": (6, 4), "AVX512CD": (6, 5),
94
+ "AVX512BW": (6, 6), "AVX512VL": (6, 7), "AVX512VBMI": (6, 8),
95
+ "AVX512VBMI2": (6, 9), "AVX512VNNI": (6, 10), "AVX512BITALG": (6, 11),
96
+ "AVX512VPOPCNTDQ": (6, 12), "AVX5124VNNIW": (6, 13),
97
+ "AVX5124FMAPS": (6, 14), "AVX512VP2INTERSECT": (6, 15),
98
+ "AVX512BF16": (6, 16), "AVX512FP16": (6, 99),
99
+ "AVX10": (7, 0), "AMX": (7, 0), "SVE": (8, 0), "SVE2": (8, 1), "V": (8, 2), "ZVE": (8, 3), "ZV": (8, 4), "APX": (9, 0),
100
+ }
101
+
102
+ _ISA_PREFIXES_BY_LEN = sorted(ISA_CHRONOLOGY.items(), key=lambda kv: len(kv[0]), reverse=True)
103
+
104
+ # ISA family/sub-ISA constants live in simdref.filters (single source of truth).
105
+ # Re-exported here for back-compat with TUI, web export, and external callers.
106
+ from simdref.filters import ( # noqa: E402 (re-export)
107
+ DEFAULT_ENABLED_ISAS,
108
+ DEFAULT_SUBS,
109
+ FAMILY_SUB_ORDER,
110
+ ISA_FAMILY_ORDER,
111
+ )
112
+
113
+ X86_BASE_ISAS: frozenset[str] = frozenset({
114
+ "I86", "I186", "I286", "I386", "I486", "I586", "X87", "CMOV",
115
+ })
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Compiled patterns (simple helpers avoid regex where possible)
119
+ # ---------------------------------------------------------------------------
120
+
121
+ _NORMALIZE_TEXT_RE = re.compile(r"[^a-z0-9]+")
122
+
123
+ # Matches display-only instruction decorators like ``{evex}``, ``{load}``,
124
+ # ``{disp8}``, etc. at the beginning of a form/mnemonic.
125
+ _LEADING_INSTR_TAG_RE = re.compile(r"^(?:\s*\{[^}]+\}\s*)+")
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Microarchitecture helpers
129
+ # ---------------------------------------------------------------------------
130
+
131
+
132
+ def uarch_sort_key(name: str) -> tuple[int, str]:
133
+ """Sort key that places known architectures in chronological order."""
134
+ try:
135
+ return (UARCH_ORDER.index(name), name)
136
+ except ValueError:
137
+ return (len(UARCH_ORDER), name)
138
+
139
+
140
+ def _uarch_display_mode() -> str:
141
+ width = console.size.width
142
+ if width >= 145:
143
+ return "full"
144
+ if width >= 115:
145
+ return "year"
146
+ return "short"
147
+
148
+
149
+ def display_uarch(name: str, mode: str | None = None) -> str:
150
+ """Format a microarchitecture code for display."""
151
+ if not name:
152
+ return "-"
153
+ mode = mode or _uarch_display_mode()
154
+ label = UARCH_LABELS.get(name)
155
+ if label is None:
156
+ return name
157
+ full_name, year = label
158
+ if mode == "full":
159
+ return f"{full_name} ({year})"
160
+ if mode == "year":
161
+ return f"{name} ({year})"
162
+ return name
163
+
164
+
165
+ def _uarch_display_mode_for_table(rows: list[dict], columns: list[str], label_map: dict[str, str]) -> str:
166
+ if "uarch" not in columns:
167
+ return "short"
168
+ available = console.size.width - 8
169
+ sample_rows = rows[:64]
170
+ for column in columns:
171
+ if column == "uarch":
172
+ continue
173
+ header = label_map.get(column, column)
174
+ max_cell = max((len(str(row.get(column, "-"))) for row in sample_rows), default=1)
175
+ available -= min(max(len(header), max_cell), 22) + 3
176
+ uarch_names = [str(row.get("uarch", "-")) for row in sample_rows]
177
+ full_needed = max((len(display_uarch(name, mode="full")) for name in uarch_names), default=0)
178
+ year_needed = max((len(display_uarch(name, mode="year")) for name in uarch_names), default=0)
179
+ if available >= full_needed + 2:
180
+ return "full"
181
+ if available >= year_needed + 2:
182
+ return "year"
183
+ return "short"
184
+
185
+
186
+ def _column_width_budget(rows: list[dict], columns: list[str], label_map: dict[str, str], uarch_mode: str) -> dict[str, int]:
187
+ """Compute column widths that fit the terminal, shrinking least important first."""
188
+ sample_rows = rows[:64]
189
+ desired: dict[str, int] = {}
190
+ minimum: dict[str, int] = {}
191
+ for column in columns:
192
+ header = label_map.get(column, column)
193
+ if column == "uarch":
194
+ values = [display_uarch(str(row.get("uarch", "-")), mode=uarch_mode) for row in sample_rows]
195
+ desired[column] = max([len(header), *[len(v) for v in values]] or [len(header)])
196
+ minimum[column] = 5
197
+ continue
198
+ values = [str(row.get(column, "-")) for row in sample_rows]
199
+ widest = max([len(header), *[len(v) for v in values]] or [len(header)])
200
+ desired[column] = min(widest, 34 if column == "ports" else 22)
201
+ if column == "ports":
202
+ minimum[column] = 10
203
+ elif column.startswith("TP") or column.startswith("cycle/") or column == "latency":
204
+ minimum[column] = 8
205
+ elif column == "uops":
206
+ minimum[column] = 4
207
+ else:
208
+ minimum[column] = min(max(len(header), 4), 10)
209
+
210
+ separator_cost = max(0, (len(columns) - 1) * 3)
211
+ available = max(40, console.size.width - 8 - separator_cost)
212
+ current = sum(desired.values())
213
+ if current <= available:
214
+ return desired
215
+
216
+ widths = dict(desired)
217
+ shrink_order = ["ports", "TP_unrolled", "TP_loop", "TP_ports", "TP", "latency", "uarch", "uops"]
218
+ shrink_order += [c for c in columns if c not in shrink_order]
219
+ deficit = current - available
220
+ while deficit > 0:
221
+ changed = False
222
+ for column in shrink_order:
223
+ if column not in widths:
224
+ continue
225
+ if widths[column] > minimum[column]:
226
+ widths[column] -= 1
227
+ deficit -= 1
228
+ changed = True
229
+ if deficit <= 0:
230
+ break
231
+ if not changed:
232
+ break
233
+ return widths
234
+
235
+
236
+ # ---------------------------------------------------------------------------
237
+ # ISA display helpers
238
+ # ---------------------------------------------------------------------------
239
+
240
+
241
+ def strip_instruction_decorators(text: str) -> str:
242
+ """Remove display-only leading decorators and EVEX suffix markers."""
243
+ result = text or ""
244
+ while True:
245
+ stripped = _LEADING_INSTR_TAG_RE.sub("", result, count=1)
246
+ if stripped == result:
247
+ break
248
+ result = stripped
249
+ return result.replace("_EVEX", "").strip()
250
+
251
+
252
+ def display_isa(values: list[str]) -> str:
253
+ """Normalize and deduplicate ISA extension names for display."""
254
+ def normalize_token(value: str) -> str:
255
+ # Strip width and scalar suffixes.
256
+ base = value
257
+ for suffix in ("_128", "_256", "_512", "_SCALAR"):
258
+ if base.endswith(suffix):
259
+ base = base[: -len(suffix)]
260
+ upper = base.upper()
261
+ if upper in {"ADV_SIMD", "ADVSIMD", "NEON"}:
262
+ return "NEON"
263
+ if upper.startswith("SVE2"):
264
+ return "SVE2"
265
+ if upper.startswith("SVE"):
266
+ return "SVE"
267
+ if upper == "AARCH64":
268
+ return "AArch64"
269
+ if upper.startswith("AVX10_"):
270
+ parts = base.split("_")
271
+ if len(parts) >= 2:
272
+ version = parts[1]
273
+ suffix_str = " ".join(parts[2:]) if len(parts) > 2 else ""
274
+ return f"AVX10.{version}{(' ' + suffix_str) if suffix_str else ''}"
275
+ if upper.startswith("AVX512"):
276
+ tail = base[len("AVX512"):]
277
+ if tail.startswith("_"):
278
+ return f"AVX512 {tail[1:].replace('_', ' ')}"
279
+ return f"AVX512{tail}"
280
+ if upper.startswith("AMX_"):
281
+ return f"AMX-{base.split('_', 1)[1].replace('_', '-')}"
282
+ return base
283
+
284
+ rendered: list[str] = []
285
+ seen: set[str] = set()
286
+ for value in values:
287
+ normalized = normalize_token(value)
288
+ if normalized not in seen:
289
+ seen.add(normalized)
290
+ rendered.append(normalized)
291
+ return ", ".join(rendered) or "-"
292
+
293
+
294
+ def display_architecture(architecture: str) -> str:
295
+ return {
296
+ "x86": "x86",
297
+ "arm": "Arm",
298
+ "riscv": "RISC-V",
299
+ }.get((architecture or "").lower(), architecture or "-")
300
+
301
+
302
+ def normalize_isa_token(isa: str) -> str:
303
+ """Normalize ISA tokens for family and sub-ISA matching."""
304
+ return isa.upper().replace(" ", "").replace("-", "").replace("_", "").replace(".", "")
305
+
306
+
307
+ def isa_family(isa: str) -> str:
308
+ """Map a single ISA token to its high-level family."""
309
+ d = normalize_isa_token(display_isa([isa]) or isa)
310
+ if not d or d == "-":
311
+ return "Other"
312
+ if d in X86_BASE_ISAS or d.startswith("BMI") or d in ("ADX", "AES", "PCLMULQDQ", "CRC32"):
313
+ return "x86"
314
+ if d == "SVML":
315
+ return "SVML"
316
+ if d in {"AARCH64", "NEON"} or d.startswith("SVE"):
317
+ return "Arm"
318
+ if d == "V" or d.startswith("RV") or d.startswith("ZV") or d.startswith("ZVE"):
319
+ return "RISC-V"
320
+ if d.startswith("APX"):
321
+ return "APX"
322
+ if d.startswith("AMX"):
323
+ return "AMX"
324
+ if d.startswith("AVX10"):
325
+ return "AVX10"
326
+ if d.startswith("AVX512") or d in ("VAES", "VPCLMULQDQ", "GFNI"):
327
+ return "AVX-512"
328
+ if d in ("AVX", "AVX2", "AVX2GATHER") or d.startswith("AVX") or d in ("FMA", "FMA4", "F16C", "XOP"):
329
+ return "AVX"
330
+ if d.startswith("SSE") or d.startswith("SSSE"):
331
+ return "SSE"
332
+ if d.startswith("MMX") or d in ("3DNOW", "PENTIUMMMX"):
333
+ return "MMX"
334
+ return "Other"
335
+
336
+
337
+ def isa_families(values: list[str]) -> list[str]:
338
+ """Deduplicated ISA families for a list of ISA tokens."""
339
+ seen: set[str] = set()
340
+ families: list[str] = []
341
+ for value in values:
342
+ family = isa_family(value)
343
+ if family and family not in seen:
344
+ seen.add(family)
345
+ families.append(family)
346
+ return families
347
+
348
+
349
+ def isa_to_sub_isa(raw_isa: str) -> str | None:
350
+ """Map a raw ISA token to its configured family sub-ISA.
351
+
352
+ Longest matching sub wins so that ``SSE2``/``SSE4.1`` don't collapse
353
+ to ``SSE`` just because SSE appears earlier in FAMILY_SUB_ORDER.
354
+ """
355
+ family = isa_family(raw_isa)
356
+ subs = FAMILY_SUB_ORDER.get(family)
357
+ if not subs:
358
+ return None
359
+ normalized = normalize_isa_token(display_isa([raw_isa]) or raw_isa)
360
+ # Exact match first, then longest-prefix match.
361
+ for sub in subs:
362
+ if normalize_isa_token(sub) == normalized:
363
+ return sub
364
+ best: str | None = None
365
+ best_len = -1
366
+ for sub in subs:
367
+ candidate = normalize_isa_token(sub)
368
+ if normalized.startswith(candidate) and len(candidate) > best_len:
369
+ best = sub
370
+ best_len = len(candidate)
371
+ return best
372
+
373
+
374
+ def isa_sort_key(values: list[str]) -> tuple[int, int, str]:
375
+ """Chronological sort key for a list of ISA extensions."""
376
+ display = display_isa(values)
377
+ normalized_values = [display_isa([v]).upper() for v in values] or [display.upper()]
378
+
379
+ def isa_rank(value: str) -> tuple[int, int]:
380
+ compact = value.replace(" ", "")
381
+ for prefix, rank in _ISA_PREFIXES_BY_LEN:
382
+ if compact.startswith(prefix):
383
+ return rank
384
+ for family in ("AVX10", "AMX", "APX"):
385
+ if compact.startswith(family):
386
+ return ISA_CHRONOLOGY[family]
387
+ return (8, 0)
388
+
389
+ return min((isa_rank(v) for v in normalized_values), default=(8, 0)) + (display,)
390
+
391
+
392
+ def is_apx_isa(values: list[str]) -> bool:
393
+ return display_isa(values).upper().startswith("APX")
394
+
395
+
396
+ def is_fp16_or_bf16_isa(values: list[str]) -> bool:
397
+ display = display_isa(values).upper()
398
+ return "FP16" in display or "BF16" in display
399
+
400
+
401
+ def isa_visible(values: list[str], show_fp16: bool = False) -> bool:
402
+ """Whether an ISA variant should be shown (hides APX and optionally FP16/BF16)."""
403
+ if is_apx_isa(values):
404
+ return False
405
+ if not show_fp16 and is_fp16_or_bf16_isa(values):
406
+ return False
407
+ return True
408
+
409
+
410
+ # ---------------------------------------------------------------------------
411
+ # Instruction text helpers
412
+ # ---------------------------------------------------------------------------
413
+
414
+
415
+ def canonical_url(path: str) -> str:
416
+ if not path:
417
+ return ""
418
+ if path.startswith("http"):
419
+ return path
420
+ return f"https://www.{path.lstrip('/')}"
421
+
422
+
423
+ def instruction_query_text(item) -> str:
424
+ """Build a clean query string from an instruction's form."""
425
+ form = strip_instruction_decorators(item.form or "")
426
+ name = strip_instruction_decorators(item.mnemonic or "")
427
+ if "(" in form:
428
+ form_name = form.split("(", 1)[0].strip()
429
+ if form_name:
430
+ name = form_name
431
+ form = form[len(form_name):].strip()
432
+ elif form.casefold().startswith(item.mnemonic.casefold()):
433
+ form = form[len(item.mnemonic):].strip()
434
+ if form.startswith("(") and form.endswith(")"):
435
+ form = form[1:-1]
436
+ tokens = [t for t in re.split(r"[\s,()]+", form) if t]
437
+ return " ".join([name, *tokens]).strip()
438
+
439
+
440
+ def display_instruction_form(form: str) -> str:
441
+ return strip_instruction_decorators(form or "") or "-"
442
+
443
+
444
+ def display_instruction_title(item) -> str:
445
+ return display_instruction_form(item.key)
446
+
447
+
448
+ def normalize_instruction_query(value: str) -> str:
449
+ """Normalize an instruction query to lowercase alphanumeric tokens."""
450
+ text = value.replace("_", " ").replace(",", " ").replace("{", " ").replace("}", " ")
451
+ tokens = []
452
+ for ch in text:
453
+ if ch.isalnum():
454
+ tokens.append(ch.lower())
455
+ elif tokens and tokens[-1] != " ":
456
+ tokens.append(" ")
457
+ return "".join(tokens).strip()
458
+
459
+
460
+ def natural_query_sort_key(value: str) -> tuple[object, ...]:
461
+ """Sort key that orders numbers numerically within text."""
462
+ parts = re.findall(r"[A-Za-z]+|\d+", value.casefold())
463
+ key: list[object] = []
464
+ for part in parts:
465
+ if part.isdigit():
466
+ key.append((1, int(part)))
467
+ else:
468
+ key.append((0, part))
469
+ return tuple(key)
470
+
471
+
472
+ def instruction_variant_items(items):
473
+ """Sort instruction variants by ISA chronology then natural form order."""
474
+ return sorted(
475
+ items,
476
+ key=lambda item: (
477
+ isa_sort_key(item.isa),
478
+ natural_query_sort_key(instruction_query_text(item)),
479
+ item.key,
480
+ ),
481
+ )
482
+
483
+
484
+ def normalized_sentence(text: str) -> str:
485
+ return _NORMALIZE_TEXT_RE.sub(" ", text.casefold()).strip()
486
+
487
+
488
+ # ---------------------------------------------------------------------------
489
+ # Row extraction helpers
490
+ # ---------------------------------------------------------------------------
491
+
492
+
493
+ def measurement_rows(item) -> list[dict]:
494
+ """Extract per-microarchitecture measurement rows from an instruction.
495
+
496
+ Each row carries a ``source`` column (``measured`` / ``modeled``) so
497
+ downstream renderers can show provenance without re-reading the
498
+ ``arch_details`` entry.
499
+ """
500
+ rows: list[dict] = []
501
+ for arch, details in item.arch_details.items():
502
+ measurement = details.get("measurement") or {}
503
+ if measurement:
504
+ row = {"uarch": arch, **measurement}
505
+ values = latency_cycle_values(details.get("latencies") or [])
506
+ if values:
507
+ row["latency"] = values[0]
508
+ row["source"] = details.get("source_kind") or "measured"
509
+ rows.append(row)
510
+ return rows
511
+
512
+
513
+ def doc_rows(item) -> list[dict]:
514
+ rows: list[dict] = []
515
+ for arch, details in item.arch_details.items():
516
+ doc = details.get("doc") or {}
517
+ if doc:
518
+ rows.append({"uarch": arch, **doc})
519
+ return rows
520
+
521
+
522
+ def iaca_rows(item) -> list[dict]:
523
+ rows: list[dict] = []
524
+ for arch, details in item.arch_details.items():
525
+ for iaca in details.get("iaca") or []:
526
+ rows.append({"uarch": arch, **iaca})
527
+ return rows
528
+
529
+
530
+ def latency_rows(item) -> list[dict]:
531
+ rows: list[dict] = []
532
+ for arch, details in item.arch_details.items():
533
+ values = latency_cycle_values(details.get("latencies") or [])
534
+ if values:
535
+ rows.append({"uarch": arch, "cycles": ", ".join(values)})
536
+ return rows
537
+
538
+
539
+ # ---------------------------------------------------------------------------
540
+ # Rich table printing
541
+ # ---------------------------------------------------------------------------
542
+
543
+ _GENERIC_TABLE_LABEL_MAP = {
544
+ "uarch": "microarch",
545
+ "latency": "LAT",
546
+ "TP_loop": "CPI",
547
+ "TP_unrolled": "CPI unroll",
548
+ "TP_ports": "CPI ports",
549
+ "TP": "CPI",
550
+ "TP_no_interiteration": "cycle/instr (no interiteration)",
551
+ "kind": "op",
552
+ }
553
+
554
+
555
+ _PERF_PANEL_ORDER = ("measured", "modeled")
556
+ _PERF_PANEL_TITLES = {
557
+ "measured": "perf (measured)",
558
+ "modeled": "perf (modeled)",
559
+ }
560
+ _PERF_PANEL_BORDER = {
561
+ "measured": "green",
562
+ "modeled": "yellow",
563
+ }
564
+
565
+
566
+ def split_perf_rows(rows: list[dict]) -> list[tuple[str, list[dict]]]:
567
+ """Group measurement rows by ``source`` kind in a stable order.
568
+
569
+ Returns ``[(kind, rows), ...]`` with measured first, then modeled, so
570
+ callers can render one panel per kind without re-sorting. Rows
571
+ missing a ``source`` field fall under ``"measured"`` for back-compat
572
+ with older arch_details entries.
573
+ """
574
+ buckets: dict[str, list[dict]] = {}
575
+ for row in rows:
576
+ kind = row.get("source") or "measured"
577
+ buckets.setdefault(kind, []).append(row)
578
+ ordered = [(k, buckets[k]) for k in _PERF_PANEL_ORDER if k in buckets]
579
+ # Any unexpected kinds (e.g. a new source kind) appear after the
580
+ # known ones, sorted for determinism.
581
+ for kind in sorted(buckets):
582
+ if kind not in _PERF_PANEL_ORDER:
583
+ ordered.append((kind, buckets[kind]))
584
+ return ordered
585
+
586
+
587
+ def perf_panel_title(kind: str) -> str:
588
+ """Human-readable panel title for a given source kind."""
589
+ return _PERF_PANEL_TITLES.get(kind, f"perf ({kind})")
590
+
591
+
592
+ def perf_panel_border(kind: str) -> str:
593
+ """Rich border style that distinguishes measured from modeled panels."""
594
+ return _PERF_PANEL_BORDER.get(kind, "green")
595
+
596
+
597
+
598
+
599
+ def print_generic_table(
600
+ rows: list[dict],
601
+ title: str,
602
+ preferred_order: list[str] | None = None,
603
+ border_style: str = "green",
604
+ exclude_keys: set[str] | None = None,
605
+ include_extras: bool = True,
606
+ ) -> None:
607
+ """Render a performance data table inside a Rich Panel."""
608
+ if not rows:
609
+ return
610
+ preferred_order = preferred_order or []
611
+ exclude_keys = exclude_keys or set()
612
+ keys = [k for k in preferred_order if any(k in row for row in rows)]
613
+ extras = sorted({k for row in rows for k in row if k not in keys and k not in exclude_keys}) if include_extras else []
614
+ columns = keys + extras
615
+ table = Table(header_style=f"bold {border_style}", expand=True)
616
+ uarch_mode = _uarch_display_mode_for_table(rows, columns, _GENERIC_TABLE_LABEL_MAP)
617
+ widths = _column_width_budget(rows, columns, _GENERIC_TABLE_LABEL_MAP, uarch_mode)
618
+ for column in columns:
619
+ label = _GENERIC_TABLE_LABEL_MAP.get(column, column)
620
+ if column == "uarch":
621
+ table.add_column(label, no_wrap=True, width=widths[column])
622
+ elif column == "ports":
623
+ table.add_column(label, no_wrap=True, width=widths[column], overflow="ellipsis")
624
+ else:
625
+ table.add_column(label, width=widths[column], overflow="fold")
626
+
627
+ def row_sort(row: dict):
628
+ return uarch_sort_key(row.get("uarch", "")) if "uarch" in row else ("",)
629
+
630
+ for row in sorted(rows, key=row_sort):
631
+ rendered = []
632
+ for column in columns:
633
+ value = row.get(column, "-")
634
+ if column == "uarch":
635
+ rendered.append(display_uarch(str(value), mode=uarch_mode))
636
+ else:
637
+ rendered.append(str(value))
638
+ table.add_row(*rendered)
639
+ console.print(Panel(table, title=title, border_style=border_style))
640
+
641
+
642
+ def print_perf_tables(rows: list[dict]) -> None:
643
+ """Render one Rich panel per source kind (measured vs modeled).
644
+
645
+ Splits *rows* on the ``source`` column and prints each group as its
646
+ own table with a distinct title and border — no ``source`` column
647
+ inside the table itself, since the panel header already names it.
648
+ """
649
+ for kind, group in split_perf_rows(rows):
650
+ print_generic_table(
651
+ group,
652
+ perf_panel_title(kind),
653
+ preferred_order=_MEASUREMENT_PREFERRED_ORDER,
654
+ border_style=perf_panel_border(kind),
655
+ exclude_keys=_MEASUREMENT_EXCLUDE_KEYS,
656
+ include_extras=False,
657
+ )
658
+
659
+
660
+ def print_operand_block(item) -> None:
661
+ """Render an operand details table."""
662
+ if not item.operand_details:
663
+ return
664
+ table = Table(header_style="bold blue")
665
+ table.add_column("idx", width=4)
666
+ table.add_column("rw", width=4)
667
+ table.add_column("type", width=8)
668
+ table.add_column("width", width=6)
669
+ table.add_column("xtype", width=8)
670
+ table.add_column("name", width=10)
671
+ for operand in item.operand_details:
672
+ rw = "".join(flag for flag in ("r", "w") if operand.get(flag) == "1")
673
+ table.add_row(
674
+ operand.get("idx", "-"),
675
+ rw or "-",
676
+ operand.get("type", "-"),
677
+ operand.get("width", "-"),
678
+ operand.get("xtype", "-"),
679
+ operand.get("name", "-"),
680
+ )
681
+ console.print(Panel(table, title="operands", border_style="blue"))
682
+
683
+
684
+ def print_instruction_mapping(catalog, intrinsic, conn=None) -> None:
685
+ """Show which instructions implement a given intrinsic."""
686
+ linked = linked_instruction_records(catalog, intrinsic, conn=conn)
687
+ if not linked:
688
+ return
689
+ table = Table(header_style="bold cyan")
690
+ table.add_column("instruction", style="cyan")
691
+ table.add_column("arch", width=6)
692
+ table.add_column("summary")
693
+ table.add_column("isa", width=12)
694
+ for instr in linked:
695
+ table.add_row(instr.key, display_architecture(instr.architecture), instr.summary or "-", ", ".join(instr.isa) or "-")
696
+ console.print(table)
697
+
698
+
699
+ def print_intrinsic_mapping(catalog, item, conn=None, find_intrinsic_fn=None) -> None:
700
+ """Show which intrinsics use a given instruction."""
701
+ if not item.linked_intrinsics:
702
+ return
703
+ from simdref.storage import load_intrinsic_from_db
704
+ from simdref.search import find_intrinsic as _find_intrinsic
705
+
706
+ find_fn = find_intrinsic_fn or _find_intrinsic
707
+ table = Table(header_style="bold cyan")
708
+ table.add_column("intrinsic", style="cyan")
709
+ table.add_column("arch", width=6)
710
+ table.add_column("summary")
711
+ table.add_column("isa", width=12)
712
+ for intrinsic_name in item.linked_intrinsics:
713
+ intrinsic = load_intrinsic_from_db(conn, intrinsic_name) if conn is not None else find_fn(catalog, intrinsic_name)
714
+ if intrinsic is None:
715
+ table.add_row(intrinsic_name, "-", "-", "-")
716
+ continue
717
+ table.add_row(
718
+ intrinsic.signature or intrinsic.name,
719
+ display_architecture(intrinsic.architecture),
720
+ intrinsic.description or "-",
721
+ ", ".join(intrinsic.isa) or "-",
722
+ )
723
+ console.print(table)
724
+
725
+
726
+ _MEASUREMENT_PREFERRED_ORDER = [
727
+ "uarch", "latency", "TP_loop", "TP_ports", "uops", "ports", "kind",
728
+ ]
729
+ _MEASUREMENT_EXCLUDE_KEYS = {
730
+ "uops_retire_slots", "uops_MITE", "uops_MS", "macro_fusible", "source",
731
+ }
732
+
733
+
734
+ def print_instruction_metadata(item) -> None:
735
+ table = Table(show_header=False, box=None)
736
+ for key, value in instruction_metadata_rows(item):
737
+ table.add_row(key, value)
738
+ console.print(Panel(table, title="instruction metadata", border_style="magenta"))
739
+
740
+
741
+ def instruction_metadata_rows(item) -> list[tuple[str, str]]:
742
+ rows: list[tuple[str, str]] = [("summary", item.summary or "-")]
743
+ url = item.metadata.get("url", "")
744
+ if url:
745
+ rows.append(("url", canonical_url(url)))
746
+ if item.metadata.get("url-ref"):
747
+ rows.append(("reference", canonical_url(item.metadata["url-ref"])))
748
+ for ref in normalize_pdf_refs(getattr(item, "pdf_refs", []), item.metadata):
749
+ url = ref.get("url", "")
750
+ label = ref.get("label", "pdf").lower()
751
+ page_label = pdf_ref_label(ref)
752
+ if page_label.casefold() != (ref.get("label", "") or "").casefold():
753
+ rows.append((label, f"{url} [{page_label}]"))
754
+ else:
755
+ rows.append((label, url))
756
+ if item.metadata.get("extension"):
757
+ rows.append(("isa", item.metadata["extension"]))
758
+ if item.metadata.get("category"):
759
+ rows.append(("category", item.metadata["category"]))
760
+ if item.metadata.get("cpl"):
761
+ rows.append(("cpl", item.metadata["cpl"]))
762
+ return rows
763
+
764
+
765
+ _DESCRIPTION_ORDER = [
766
+ "Description", "Operation", "Intrinsic Equivalents",
767
+ "Flags Affected", "FPU Flags Affected",
768
+ "Exceptions", "SIMD Floating-Point Exceptions",
769
+ "Floating-Point Exceptions", "x87 FPU and SIMD Floating-Point Exceptions",
770
+ "Numeric Exceptions", "Other Exceptions", "Other Mode Exceptions",
771
+ "Protected Mode Exceptions", "Real-Address Mode Exceptions",
772
+ "Real Address Mode Exceptions",
773
+ "Virtual-8086 Mode Exceptions", "Virtual-8086 Exceptions",
774
+ "Virtual 8086 Mode Exceptions",
775
+ "Compatibility Mode Exceptions", "64-Bit Mode Exceptions",
776
+ ]
777
+
778
+ # Sections that are always shown expanded.
779
+ _EXPANDED_SECTIONS: set[str] = set()
780
+
781
+ # Syntax language per code section.
782
+ _CODE_SECTION_LANG = {
783
+ "Operation": "asm",
784
+ "ACLE Operation": "asm",
785
+ "Intrinsic Equivalents": "c",
786
+ }
787
+
788
+
789
+ def print_description_sections(
790
+ description: dict[str, str],
791
+ full: bool = False,
792
+ ) -> None:
793
+ """Render instruction description sections as Rich panels.
794
+
795
+ By default, only Description and Flags Affected are expanded.
796
+ Other sections show a collapsed summary line. Pass *full=True*
797
+ to expand everything.
798
+ """
799
+ if not description:
800
+ return
801
+ shown: set[str] = set()
802
+ for key in _DESCRIPTION_ORDER:
803
+ if key in description:
804
+ expand = full or key in _EXPANDED_SECTIONS
805
+ _print_section(key, description[key], expand=expand)
806
+ shown.add(key)
807
+ for key, value in description.items():
808
+ if key not in shown:
809
+ expand = full or key in _EXPANDED_SECTIONS
810
+ _print_section(key, value, expand=expand)
811
+
812
+
813
+ def _print_section(title: str, body: str, expand: bool = True) -> None:
814
+ from rich.syntax import Syntax
815
+ from rich.text import Text
816
+
817
+ if not expand:
818
+ line_count = body.count("\n") + 1
819
+ summary = Text(f" \u25b8 {title} ({line_count} lines)", style="dim")
820
+ console.print(summary)
821
+ return
822
+
823
+ lang = _CODE_SECTION_LANG.get(title)
824
+ if lang:
825
+ syntax = Syntax(body, lang, theme="monokai", word_wrap=True)
826
+ console.print(Panel(syntax, title=title, border_style="dim"))
827
+ else:
828
+ console.print(Panel(body, title=title, border_style="dim"))
829
+
830
+
831
+ # ---------------------------------------------------------------------------
832
+ # High-level render functions
833
+ # ---------------------------------------------------------------------------
834
+
835
+
836
+ def render_intrinsic(catalog, item, conn=None, short: bool = False, full: bool = False) -> None:
837
+ """Render full intrinsic detail view to the terminal."""
838
+ table = Table(show_header=False, box=None)
839
+ table.add_row("signature", item.signature or "-")
840
+ table.add_row("header", item.header or "-")
841
+ if item.url:
842
+ table.add_row("source", item.url)
843
+ table.add_row("architecture", display_architecture(item.architecture))
844
+ table.add_row("isa", ", ".join(item.isa) or "-")
845
+ table.add_row("category", item.category or "-")
846
+ for key in ("reference_url", "argument_preparation", "result", "supported_architectures", "classification_path"):
847
+ if item.metadata.get(key):
848
+ table.add_row(key, item.metadata[key])
849
+ table.add_row("notes", "; ".join(item.notes) or "-")
850
+ linked = linked_instruction_records(catalog, item, conn=conn)
851
+ primary = linked[0] if linked else None
852
+ if primary:
853
+ for key, value in instruction_metadata_rows(primary):
854
+ if key == "summary":
855
+ continue
856
+ table.add_row(key, value)
857
+ console.print(Panel(table, title=f"intrinsic: {item.name}", border_style="cyan"))
858
+ if not short and item.doc_sections:
859
+ print_description_sections(item.doc_sections, full=full)
860
+ if not short and primary and primary.description:
861
+ print_description_sections(primary.description, full=full)
862
+ if linked:
863
+ console.print(Rule("intrinsic to instruction mapping", style="cyan"))
864
+ print_instruction_mapping(catalog, item, conn=conn)
865
+ console.print(Rule(f"instruction details: {display_instruction_title(primary)}", style="magenta"))
866
+ print_operand_block(primary)
867
+ print_perf_tables(measurement_rows(primary))
868
+
869
+
870
+ def render_instruction_sections(catalog, item, include_title: bool = True, conn=None, short: bool = False, full: bool = False) -> None:
871
+ """Render instruction detail with optional title panel."""
872
+ if include_title:
873
+ table = Table(show_header=False, box=None)
874
+ table.add_row("mnemonic", item.mnemonic)
875
+ table.add_row("form", display_instruction_form(item.form))
876
+ table.add_row("architecture", display_architecture(item.architecture))
877
+ table.add_row("isa", display_isa(item.isa))
878
+ for key, value in instruction_metadata_rows(item):
879
+ table.add_row(key, value)
880
+ console.print(Panel(table, title=f"instruction: {display_instruction_title(item)}", border_style="magenta"))
881
+ else:
882
+ print_instruction_metadata(item)
883
+ if not short and item.description:
884
+ print_description_sections(item.description, full=full)
885
+ console.print(Rule("instruction to intrinsic mapping", style="cyan"))
886
+ print_intrinsic_mapping(catalog, item, conn=conn)
887
+ print_operand_block(item)
888
+ print_perf_tables(measurement_rows(item))
889
+
890
+
891
+ def render_instruction(catalog, item, conn=None, short: bool = False, full: bool = False) -> None:
892
+ """Render full instruction detail view."""
893
+ render_instruction_sections(catalog, item, include_title=True, conn=conn, short=short, full=full)
894
+
895
+
896
+ def render_instruction_variants(query: str, items, show_fp16: bool = False) -> None:
897
+ """Render a variant selection table for a mnemonic with multiple forms."""
898
+ all_items = instruction_variant_items(items)
899
+ visible_items = [item for item in all_items if isa_visible(item.isa, show_fp16=show_fp16)]
900
+ items = visible_items or all_items
901
+ table = Table(header_style="bold cyan")
902
+ table.add_column("#", width=3, style="cyan")
903
+ table.add_column("query", style="cyan")
904
+ table.add_column("arch", width=6)
905
+ table.add_column("isa", width=14)
906
+ table.add_column("lat", width=5)
907
+ table.add_column("cpi", width=5)
908
+ table.add_column("summary")
909
+ for index, item in enumerate(items, start=1):
910
+ lat, cpi = variant_perf_summary_labeled(item.arch_details)
911
+ table.add_row(
912
+ str(index),
913
+ instruction_query_text(item),
914
+ display_architecture(item.architecture),
915
+ display_isa(item.isa),
916
+ _label_perf(lat),
917
+ _label_perf(cpi),
918
+ item.summary or "-",
919
+ )
920
+
921
+
922
+ def _label_perf(value) -> str:
923
+ """Render a :class:`PerfValue` as ``"val (measured, core)"`` for tables.
924
+
925
+ Empty / missing values render as ``"-"`` to match the legacy behaviour
926
+ the unit tests depend on.
927
+ """
928
+ raw = getattr(value, "value", value)
929
+ if not raw or raw == "-":
930
+ return "-"
931
+ kind = getattr(value, "source_kind", "")
932
+ core = getattr(value, "core", "")
933
+ if kind and core:
934
+ return f"{raw} ({kind[0]}, {core})"
935
+ return str(raw)
936
+ console.print(Panel(table, title=f"instruction variants: {query}", border_style="magenta"))
937
+ base_query = query.split()[0] if query.split() else query
938
+ if items:
939
+ hidden_count = len(all_items) - len(items)
940
+ hidden_note = f" Hidden {hidden_count} APX/FP16/BF16 forms by default." if hidden_count > 0 else ""
941
+ console.print(
942
+ f"[dim]Open one with `simdref {instruction_query_text(items[0])}` or `simdref {base_query} <index>`. Showing {len(items)} forms.{hidden_note}[/dim]"
943
+ )
944
+
945
+
946
+ def render_search_results(
947
+ results: list[tuple],
948
+ ) -> None:
949
+ """Render search results table from pre-computed rows.
950
+
951
+ Each element in *results* is ``(SearchResult, arch_str, isa_str, lat, cpi)``.
952
+ """
953
+ table = Table(show_header=True, header_style="bold cyan", expand=True)
954
+ table.add_column("#", width=3, style="cyan")
955
+ table.add_column("query", width=30, no_wrap=True, overflow="ellipsis")
956
+ table.add_column("Arch", width=6)
957
+ table.add_column("ISA", width=14)
958
+ table.add_column("lat", width=5)
959
+ table.add_column("cpi", width=5)
960
+ table.add_column("summary", min_width=18, overflow="fold")
961
+ for index, (result, arch, isa, lat, cpi) in enumerate(results, start=1):
962
+ table.add_row(str(index), result.title, arch, isa, lat, cpi, result.subtitle)
963
+ console.print(table)