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/__init__.py +6 -0
- simdref/__main__.py +6 -0
- simdref/annotate.py +448 -0
- simdref/arm_instructions.py +417 -0
- simdref/cli.py +1598 -0
- simdref/display.py +963 -0
- simdref/filters.py +318 -0
- simdref/ingest.py +113 -0
- simdref/ingest_catalog.py +1172 -0
- simdref/ingest_pdf.py +188 -0
- simdref/ingest_sources.py +580 -0
- simdref/lsp.py +208 -0
- simdref/manpages.py +139 -0
- simdref/models.py +225 -0
- simdref/pdfparse/__init__.py +13 -0
- simdref/pdfparse/base.py +116 -0
- simdref/pdfparse/intel.py +614 -0
- simdref/pdfparse/registry.py +19 -0
- simdref/pdfparse/types.py +77 -0
- simdref/pdfrefs.py +95 -0
- simdref/perf.py +220 -0
- simdref/perf_sources/__init__.py +51 -0
- simdref/perf_sources/cores.py +101 -0
- simdref/perf_sources/llvm_mca.py +176 -0
- simdref/perf_sources/llvm_scheduling.py +625 -0
- simdref/perf_sources/merge.py +121 -0
- simdref/queries.py +207 -0
- simdref/riscv.py +446 -0
- simdref/search.py +288 -0
- simdref/storage.py +504 -0
- simdref/templates/__init__.py +0 -0
- simdref/templates/app.js +1590 -0
- simdref/templates/favicon.svg +5 -0
- simdref/templates/index.html +112 -0
- simdref/templates/logo.svg +12 -0
- simdref/templates/style.css +680 -0
- simdref/tui.py +2366 -0
- simdref/web.py +403 -0
- simdref-0.0.0.dist-info/METADATA +240 -0
- simdref-0.0.0.dist-info/RECORD +44 -0
- simdref-0.0.0.dist-info/WHEEL +5 -0
- simdref-0.0.0.dist-info/entry_points.txt +4 -0
- simdref-0.0.0.dist-info/licenses/LICENSE +674 -0
- simdref-0.0.0.dist-info/top_level.txt +1 -0
simdref/templates/app.js
ADDED
|
@@ -0,0 +1,1590 @@
|
|
|
1
|
+
/* simdref – client-side SPA logic */
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/* ── DOM refs ─────────────────────────────────────────────────────── */
|
|
5
|
+
const $ = (id) => document.getElementById(id);
|
|
6
|
+
const queryInput = $("query");
|
|
7
|
+
const resultsNode = $("results");
|
|
8
|
+
const detailNode = $("detail");
|
|
9
|
+
const detailEmpty = $("detail-empty");
|
|
10
|
+
const metaNode = $("meta");
|
|
11
|
+
const resultsCount = $("results-count");
|
|
12
|
+
const isaSummary = $("isa-summary");
|
|
13
|
+
const isaPanel = $("isa-panel");
|
|
14
|
+
const isaFamiliesNode = $("isa-families");
|
|
15
|
+
const isaSubgroupsNode = $("isa-subgroups");
|
|
16
|
+
const categoryPanel = $("category-panel");
|
|
17
|
+
const categoryChips = $("category-chips");
|
|
18
|
+
const categorySummary = $("category-summary");
|
|
19
|
+
const themeToggle = $("theme-toggle");
|
|
20
|
+
const themeIconLight = $("theme-icon-light");
|
|
21
|
+
const themeIconDark = $("theme-icon-dark");
|
|
22
|
+
const shortcutsOverlay = $("shortcuts-overlay");
|
|
23
|
+
|
|
24
|
+
/* ── State ────────────────────────────────────────────────────────── */
|
|
25
|
+
let catalog = null; // search-index.json payload
|
|
26
|
+
let searchEntries = [];
|
|
27
|
+
let searchTokenIndex = new Map();
|
|
28
|
+
let searchPrefixIndex = new Map();
|
|
29
|
+
let resultPool = [];
|
|
30
|
+
let activeKey = null;
|
|
31
|
+
let focusedIndex = -1;
|
|
32
|
+
let renderTimer = null;
|
|
33
|
+
let visibleSet = null;
|
|
34
|
+
let filterRenderScheduled = false;
|
|
35
|
+
let renderedCount = 0;
|
|
36
|
+
let loadMoreScheduled = false;
|
|
37
|
+
const INITIAL_RENDER_BATCH = 50;
|
|
38
|
+
const RENDER_BATCH_SIZE = 10;
|
|
39
|
+
const LOAD_MORE_THRESHOLD_PX = 600;
|
|
40
|
+
|
|
41
|
+
/* Viewport virtualisation — keep only rows inside the visible window
|
|
42
|
+
* (plus a small buffer) in the DOM. Row height must match .result in
|
|
43
|
+
* style.css. */
|
|
44
|
+
const ROW_HEIGHT_PX = 88;
|
|
45
|
+
const VIEWPORT_BUFFER_ROWS = 30;
|
|
46
|
+
let virtualWrap = null;
|
|
47
|
+
let virtualRange = { start: -1, end: -1 };
|
|
48
|
+
|
|
49
|
+
/* Detail chunk caches: prefix -> Promise<data>. Separate maps for
|
|
50
|
+
* instructions (detail-chunks/) and intrinsics (intrinsic-chunks/) so
|
|
51
|
+
* colliding prefixes (e.g. "ADD") don't shadow each other. */
|
|
52
|
+
const chunkCache = new Map();
|
|
53
|
+
const intrinsicChunkCache = new Map();
|
|
54
|
+
|
|
55
|
+
/* ISA state */
|
|
56
|
+
let defaultEnabledIsas = new Set(["SSE", "AVX", "AVX-512"]);
|
|
57
|
+
let availableIsas = [];
|
|
58
|
+
let enabledIsas = new Set();
|
|
59
|
+
let enableAllIsas = false;
|
|
60
|
+
let enabledSubIsas = new Map();
|
|
61
|
+
|
|
62
|
+
/* Category state — null means "all enabled" (no filter) */
|
|
63
|
+
let availableCategories = []; // [{family, category, subcategory, count}]
|
|
64
|
+
let enabledCategories = null;
|
|
65
|
+
|
|
66
|
+
/* Kind (intrinsic vs instruction/asm) filter — both enabled by default. */
|
|
67
|
+
const enabledKinds = new Set(["intrinsic", "instruction"]);
|
|
68
|
+
|
|
69
|
+
/* Perf source-kind filter — toggle modeled vs measured perf tables.
|
|
70
|
+
Persisted to localStorage like the theme so the choice survives reloads. */
|
|
71
|
+
const _storedPerfKinds = (() => {
|
|
72
|
+
try {
|
|
73
|
+
const raw = localStorage.getItem("simdref-perf-kinds");
|
|
74
|
+
if (raw) return new Set(JSON.parse(raw));
|
|
75
|
+
} catch (_) { /* ignore */ }
|
|
76
|
+
return new Set(["measured", "modeled"]);
|
|
77
|
+
})();
|
|
78
|
+
const enabledPerfKinds = _storedPerfKinds;
|
|
79
|
+
function persistPerfKinds() {
|
|
80
|
+
try {
|
|
81
|
+
localStorage.setItem("simdref-perf-kinds", JSON.stringify([...enabledPerfKinds]));
|
|
82
|
+
} catch (_) { /* ignore */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let FAMILY_SUB_ORDER = {};
|
|
86
|
+
let DEFAULT_SUBS = {};
|
|
87
|
+
let isaFamilyOrder = {};
|
|
88
|
+
let ARCH_PRESETS = {};
|
|
89
|
+
let enabledArmArch = null; // null = no filter; Set of "A32"|"A64"|"BOTH" when active
|
|
90
|
+
|
|
91
|
+
/* ── Theme ────────────────────────────────────────────────────────── */
|
|
92
|
+
function getEffectiveTheme() {
|
|
93
|
+
const stored = localStorage.getItem("simdref-theme");
|
|
94
|
+
if (stored) return stored;
|
|
95
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function applyTheme(theme) {
|
|
99
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
100
|
+
localStorage.setItem("simdref-theme", theme);
|
|
101
|
+
themeIconLight.style.display = theme === "dark" ? "none" : "";
|
|
102
|
+
themeIconDark.style.display = theme === "dark" ? "" : "none";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toggleTheme() {
|
|
106
|
+
applyTheme(getEffectiveTheme() === "dark" ? "light" : "dark");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
applyTheme(getEffectiveTheme());
|
|
110
|
+
|
|
111
|
+
/* ── Helpers ──────────────────────────────────────────────────────── */
|
|
112
|
+
function esc(v) {
|
|
113
|
+
return String(v ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function canonUrl(v) {
|
|
117
|
+
if (!v) return "";
|
|
118
|
+
if (String(v).startsWith("http")) return v;
|
|
119
|
+
return "https://www." + String(v).replace(/^\/+/, "");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function displayInstr(v) {
|
|
123
|
+
return String(v || "").trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function tokens(v) {
|
|
127
|
+
return String(v || "").replaceAll("_", " ").replaceAll(",", " ").replaceAll("{", " ").replaceAll("}", " ")
|
|
128
|
+
.toLowerCase().match(/[a-z0-9]+/g) || [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function displayIsa(values) {
|
|
132
|
+
return Array.isArray(values) ? (values.join(", ") || "-") : String(values || "-");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function defaultSubsForFamily(family) {
|
|
136
|
+
const order = FAMILY_SUB_ORDER[family] || [];
|
|
137
|
+
const defaults = DEFAULT_SUBS[family] || new Set(order);
|
|
138
|
+
return new Set(order.filter(sub => defaults.has(sub)));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function initEnabledSubIsas() {
|
|
142
|
+
enabledSubIsas = new Map();
|
|
143
|
+
for (const family of availableIsas) {
|
|
144
|
+
enabledSubIsas.set(family, FAMILY_SUB_ORDER[family] ? defaultSubsForFamily(family) : new Set());
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ensureManualIsaState() {
|
|
149
|
+
if (!enableAllIsas) return;
|
|
150
|
+
enableAllIsas = false;
|
|
151
|
+
enabledIsas = new Set(availableIsas);
|
|
152
|
+
for (const family of availableIsas) {
|
|
153
|
+
if (FAMILY_SUB_ORDER[family]) enabledSubIsas.set(family, new Set(FAMILY_SUB_ORDER[family]));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isaVisible(item) {
|
|
158
|
+
if (enableAllIsas) return true;
|
|
159
|
+
for (const family of (item.isa_families || [])) {
|
|
160
|
+
if (!enabledIsas.has(family)) continue;
|
|
161
|
+
if (!FAMILY_SUB_ORDER[family]) return true;
|
|
162
|
+
const enabledSubs = enabledSubIsas.get(family);
|
|
163
|
+
const entrySubs = item.isa_subs || [];
|
|
164
|
+
if (entrySubs.some(subIsa => enabledSubs && enabledSubs.has(subIsa))) return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function categoryVisible(item) {
|
|
170
|
+
if (enabledCategories === null) return true;
|
|
171
|
+
const cat = item.category || (item.metadata && item.metadata.category) || "";
|
|
172
|
+
if (!cat) return enabledCategories.has("");
|
|
173
|
+
return enabledCategories.has(cat);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function armArchVisible(item) {
|
|
177
|
+
if (!enabledArmArch) return true;
|
|
178
|
+
// Only intrinsics carry arm_arch classification; instructions are not filtered.
|
|
179
|
+
if (!(item.isa_families || []).includes("Arm")) return true;
|
|
180
|
+
const bucket = item.arm_arch || null;
|
|
181
|
+
return bucket !== null && enabledArmArch.has(bucket);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ── Bucket indexes (built once after load) ─────────────────────────
|
|
185
|
+
* Filter toggles intersect small buckets instead of scanning ~140k
|
|
186
|
+
* entries. Buckets:
|
|
187
|
+
* byKind: "intrinsic" | "instruction" -> entry indexes
|
|
188
|
+
* byFamily: family name -> entry indexes
|
|
189
|
+
* byCategory: category name (or "") -> entry indexes
|
|
190
|
+
* byArmArch: "A32" | "A64" | "BOTH" | "__none" -> entry indexes
|
|
191
|
+
*/
|
|
192
|
+
let bucketsBuilt = false;
|
|
193
|
+
const byKind = new Map();
|
|
194
|
+
const byFamily = new Map();
|
|
195
|
+
const byCategory = new Map();
|
|
196
|
+
const byArmArch = new Map();
|
|
197
|
+
|
|
198
|
+
function _pushBucket(map, key, i) {
|
|
199
|
+
let arr = map.get(key);
|
|
200
|
+
if (!arr) { arr = []; map.set(key, arr); }
|
|
201
|
+
arr.push(i);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function buildBuckets() {
|
|
205
|
+
byKind.clear(); byFamily.clear(); byCategory.clear(); byArmArch.clear();
|
|
206
|
+
for (let i = 0; i < searchEntries.length; i++) {
|
|
207
|
+
const e = searchEntries[i];
|
|
208
|
+
_pushBucket(byKind, e.kind, i);
|
|
209
|
+
const fams = e.item.isa_families || [];
|
|
210
|
+
if (fams.length === 0) _pushBucket(byFamily, "__none", i);
|
|
211
|
+
for (const f of fams) _pushBucket(byFamily, f, i);
|
|
212
|
+
const cat = e.item.category || "";
|
|
213
|
+
_pushBucket(byCategory, cat, i);
|
|
214
|
+
const arch = e.item.arm_arch || "__none";
|
|
215
|
+
_pushBucket(byArmArch, arch, i);
|
|
216
|
+
}
|
|
217
|
+
bucketsBuilt = true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _unionKeys(map, keys) {
|
|
221
|
+
const out = new Set();
|
|
222
|
+
for (const k of keys) {
|
|
223
|
+
const list = map.get(k);
|
|
224
|
+
if (!list) continue;
|
|
225
|
+
for (const i of list) out.add(i);
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function rebuildVisibleSet() {
|
|
231
|
+
if (!bucketsBuilt) buildBuckets();
|
|
232
|
+
// Kind filter: union of enabled-kind buckets.
|
|
233
|
+
let cand = _unionKeys(byKind, [...enabledKinds]);
|
|
234
|
+
|
|
235
|
+
// ISA family/sub-family filter — keep the existing predicate for the
|
|
236
|
+
// sub-ISA case (FAMILY_SUB_ORDER), but short-circuit at the family
|
|
237
|
+
// bucket first to avoid scanning every row.
|
|
238
|
+
if (!enableAllIsas) {
|
|
239
|
+
const famCand = _unionKeys(byFamily, [...enabledIsas]);
|
|
240
|
+
cand = _intersect(cand, famCand);
|
|
241
|
+
// Sub-ISA filtering — may still require a per-entry check when
|
|
242
|
+
// enabledSubIsas doesn't match the family default.
|
|
243
|
+
const needsSubCheck = [...enabledIsas].some(f => FAMILY_SUB_ORDER[f]);
|
|
244
|
+
if (needsSubCheck) {
|
|
245
|
+
cand = _filterSet(cand, (i) => isaVisible(searchEntries[i].item));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (enabledCategories !== null) {
|
|
250
|
+
const catCand = _unionKeys(byCategory, [...enabledCategories]);
|
|
251
|
+
cand = _intersect(cand, catCand);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (enabledArmArch) {
|
|
255
|
+
// Non-Arm entries are never filtered out. Union of:
|
|
256
|
+
// * arm_arch bucket entries (Arm, bucketed by A32/A64/BOTH)
|
|
257
|
+
// * entries whose families don't include "Arm"
|
|
258
|
+
const armCand = _unionKeys(byArmArch, [...enabledArmArch]);
|
|
259
|
+
const nonArm = _filterSet(cand, (i) => !(searchEntries[i].item.isa_families || []).includes("Arm"));
|
|
260
|
+
// armCand ∩ cand, ∪ nonArm
|
|
261
|
+
const intersect = new Set();
|
|
262
|
+
for (const i of armCand) if (cand.has(i)) intersect.add(i);
|
|
263
|
+
for (const i of nonArm) intersect.add(i);
|
|
264
|
+
cand = intersect;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
visibleSet = cand;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function _intersect(a, b) {
|
|
271
|
+
const [small, big] = a.size <= b.size ? [a, b] : [b, a];
|
|
272
|
+
const out = new Set();
|
|
273
|
+
for (const i of small) if (big.has(i)) out.add(i);
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function _filterSet(s, pred) {
|
|
278
|
+
const out = new Set();
|
|
279
|
+
for (const i of s) if (pred(i)) out.add(i);
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function resultMarkup(entries, offset = 0) {
|
|
284
|
+
return entries.map((e, i) => `
|
|
285
|
+
<article class="result ${e.kind}-kind ${e.key === activeKey ? "active" : ""} ${offset + i === focusedIndex ? "focused" : ""}" data-key="${esc(e.key)}" data-index="${offset + i}">
|
|
286
|
+
<div class="result-top">
|
|
287
|
+
<span class="result-kind ${e.kind}">${esc(e.kind)}</span>
|
|
288
|
+
<span class="result-isa">${esc(e.item.display_architecture || e.item.architecture || "-")}</span>
|
|
289
|
+
<span class="result-isa">${esc(e.item.display_isa || displayIsa(e.item.isa))}</span>
|
|
290
|
+
</div>
|
|
291
|
+
<div class="result-title">${esc(e.title)}</div>
|
|
292
|
+
<div class="result-meta">
|
|
293
|
+
${e.item.lat && e.item.lat !== "-" ? `<span class="result-perf">LAT ${esc(e.item.lat)}</span>` : ""}
|
|
294
|
+
${e.item.cpi && e.item.cpi !== "-" ? `<span class="result-perf">CPI ${esc(e.item.cpi)}</span>` : ""}
|
|
295
|
+
</div>
|
|
296
|
+
<div class="result-summary">${esc(e.subtitle || "")}</div>
|
|
297
|
+
</article>
|
|
298
|
+
`).join("");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function syncResultsCount(query) {
|
|
302
|
+
const shown = Math.min(renderedCount, resultPool.length);
|
|
303
|
+
if (query) {
|
|
304
|
+
resultsCount.textContent = shown < resultPool.length
|
|
305
|
+
? `Showing ${shown} of ${resultPool.length} results for "${query}"`
|
|
306
|
+
: `${resultPool.length} result${resultPool.length !== 1 ? "s" : ""} for "${query}"`;
|
|
307
|
+
} else if (catalog && Array.isArray(catalog.intrinsics) && Array.isArray(catalog.instructions)) {
|
|
308
|
+
// Empty-query hint: show the catalog size so visitors know the index loaded.
|
|
309
|
+
const nI = catalog.intrinsics.length;
|
|
310
|
+
const nA = catalog.instructions.length;
|
|
311
|
+
resultsCount.textContent = shown < resultPool.length
|
|
312
|
+
? `Showing ${shown} of ${resultPool.length} (${nI} intrinsics · ${nA} instructions)`
|
|
313
|
+
: `Type to search ${nI} intrinsics · ${nA} instructions`;
|
|
314
|
+
} else {
|
|
315
|
+
resultsCount.textContent = shown < resultPool.length
|
|
316
|
+
? `Showing ${shown} of ${resultPool.length} results`
|
|
317
|
+
: `${resultPool.length} results`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function _ensureVirtualWrap() {
|
|
322
|
+
if (virtualWrap && virtualWrap.isConnected) return virtualWrap;
|
|
323
|
+
resultsNode.innerHTML = '<div class="results-virtual"></div>';
|
|
324
|
+
virtualWrap = resultsNode.firstElementChild;
|
|
325
|
+
return virtualWrap;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function _visibleRowRange() {
|
|
329
|
+
const top = resultsNode.scrollTop;
|
|
330
|
+
const h = resultsNode.clientHeight;
|
|
331
|
+
const first = Math.max(0, Math.floor(top / ROW_HEIGHT_PX) - VIEWPORT_BUFFER_ROWS);
|
|
332
|
+
const last = Math.min(resultPool.length, Math.ceil((top + h) / ROW_HEIGHT_PX) + VIEWPORT_BUFFER_ROWS);
|
|
333
|
+
return [first, last];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function _rowMarkup(entry, index) {
|
|
337
|
+
return `<article class="result ${entry.kind}-kind ${entry.key === activeKey ? "active" : ""} ${index === focusedIndex ? "focused" : ""}" data-key="${esc(entry.key)}" data-index="${index}" style="top:${index * ROW_HEIGHT_PX}px">
|
|
338
|
+
<div class="result-top">
|
|
339
|
+
<span class="result-kind ${entry.kind}">${esc(entry.kind)}</span>
|
|
340
|
+
<span class="result-isa">${esc(entry.item.display_architecture || entry.item.architecture || "-")}</span>
|
|
341
|
+
<span class="result-isa">${esc(entry.item.display_isa || displayIsa(entry.item.isa))}</span>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="result-title">${esc(entry.title)}</div>
|
|
344
|
+
<div class="result-meta">
|
|
345
|
+
${entry.item.lat && entry.item.lat !== "-" ? `<span class="result-perf">LAT ${esc(entry.item.lat)}</span>` : ""}
|
|
346
|
+
${entry.item.cpi && entry.item.cpi !== "-" ? `<span class="result-perf">CPI ${esc(entry.item.cpi)}</span>` : ""}
|
|
347
|
+
</div>
|
|
348
|
+
<div class="result-summary">${esc(entry.subtitle || "")}</div>
|
|
349
|
+
</article>`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function renderVisibleResults(reset = false) {
|
|
353
|
+
const wrap = _ensureVirtualWrap();
|
|
354
|
+
wrap.style.height = (resultPool.length * ROW_HEIGHT_PX) + "px";
|
|
355
|
+
const [first, last] = _visibleRowRange();
|
|
356
|
+
if (reset || first !== virtualRange.start || last !== virtualRange.end) {
|
|
357
|
+
let html = "";
|
|
358
|
+
for (let i = first; i < last; i++) html += _rowMarkup(resultPool[i], i);
|
|
359
|
+
wrap.innerHTML = html;
|
|
360
|
+
virtualRange = { start: first, end: last };
|
|
361
|
+
}
|
|
362
|
+
renderedCount = last;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function loadMoreResults() {
|
|
366
|
+
// With virtualisation there's no incremental "load more" — the
|
|
367
|
+
// viewport already shows what's needed. Kept for API parity.
|
|
368
|
+
renderVisibleResults(false);
|
|
369
|
+
syncResultsCount(queryInput.value.trim());
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function maybeLoadMoreResults() {
|
|
373
|
+
renderVisibleResults(false);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* ── ISA sort key (chronological) ─────────────────────────────────── */
|
|
377
|
+
const chronology = {
|
|
378
|
+
"I86": [0,0], "MMX": [1,0],
|
|
379
|
+
"SSE": [2,0], "SSE2": [2,1], "SSE3": [2,2], "SSSE3": [2,3], "SSE4A": [2,4], "SSE4.1": [2,5], "SSE4.2": [2,6], "AES": [2,7], "PCLMULQDQ": [2,8],
|
|
380
|
+
"F16C": [3,0], "FMA": [3,1],
|
|
381
|
+
"AVX": [4,0], "NEON": [4,1], "AVX2": [5,0],
|
|
382
|
+
"AVX512F": [6,0], "AVX512DQ": [6,1], "AVX512IFMA": [6,2], "AVX512PF": [6,3], "AVX512ER": [6,4], "AVX512CD": [6,5], "AVX512BW": [6,6], "AVX512VL": [6,7],
|
|
383
|
+
"AVX512VBMI": [6,8], "AVX512VBMI2": [6,9], "AVX512VNNI": [6,10], "AVX512BITALG": [6,11], "AVX512VPOPCNTDQ": [6,12],
|
|
384
|
+
"AVX512FP16": [6,99], "AVX10": [7,0], "AMX": [7,0], "SVE": [8,0], "SVE2": [8,1], "APX": [9,0],
|
|
385
|
+
};
|
|
386
|
+
const chronologyEntries = Object.entries(chronology).sort((a, b) => b[0].length - a[0].length);
|
|
387
|
+
|
|
388
|
+
function isaSortKey(values) {
|
|
389
|
+
const norms = (values && values.length ? values : ["-"]).map(v => String(Array.isArray(v) ? displayIsa(v) : v || "-").toUpperCase());
|
|
390
|
+
function rank(v) {
|
|
391
|
+
const c = v.replaceAll(" ", "");
|
|
392
|
+
for (const [pfx, bucket] of chronologyEntries) { if (c.startsWith(pfx)) return bucket; }
|
|
393
|
+
if (c.startsWith("AVX10")) return chronology["AVX10"];
|
|
394
|
+
if (c.startsWith("AMX")) return chronology["AMX"];
|
|
395
|
+
if (c.startsWith("APX")) return chronology["APX"];
|
|
396
|
+
return [8, 0];
|
|
397
|
+
}
|
|
398
|
+
return norms.reduce((best, v) => {
|
|
399
|
+
const cur = rank(v);
|
|
400
|
+
if (!best) return cur;
|
|
401
|
+
return cur[0] < best[0] || (cur[0] === best[0] && cur[1] < best[1]) ? cur : best;
|
|
402
|
+
}, null) || [8, 0];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* ── Search ranking ───────────────────────────────────────────────── */
|
|
406
|
+
function classifyQuery(q) {
|
|
407
|
+
const lo = q.trim().toLowerCase();
|
|
408
|
+
const norm = tokens(q).join(" ");
|
|
409
|
+
if (lo.startsWith("_mm") || lo.startsWith("__m") || norm === "mm" || norm.startsWith("mm ")) return "intrinsic";
|
|
410
|
+
if (lo.includes("_mm")) return "intrinsic";
|
|
411
|
+
const parts = norm.split(" ").filter(Boolean);
|
|
412
|
+
if (parts.length) {
|
|
413
|
+
const f = parts[0];
|
|
414
|
+
if (["add","sub","mul","div","mov","cmp","and","or","xor"].includes(f) || f.startsWith("v")) return "instruction";
|
|
415
|
+
}
|
|
416
|
+
return "neutral";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function tokenOverlapCount(qt, ct) {
|
|
420
|
+
let n = 0;
|
|
421
|
+
for (const t of qt) { if (ct.some(c => c === t || c.startsWith(t) || t.startsWith(c))) n++; }
|
|
422
|
+
return n;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function meaningfulTokens(t) { return t.filter(x => !/^(mm|mm\d+|xmm|ymm|zmm)$/.test(x)); }
|
|
426
|
+
|
|
427
|
+
function hasOverlap(query, text) {
|
|
428
|
+
const q = (query || "").trim().toLowerCase(), t = (text || "").trim().toLowerCase();
|
|
429
|
+
if (!q || !t) return false;
|
|
430
|
+
if (q === t || t.startsWith(q) || t.includes(q)) return true;
|
|
431
|
+
const qt = tokens(query), ct = tokens(text);
|
|
432
|
+
const m = meaningfulTokens(qt);
|
|
433
|
+
return m.length ? tokenOverlapCount(m, ct) > 0 : tokenOverlapCount(qt, ct) > 0;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function scoreText(query, text) {
|
|
437
|
+
const q = query.trim().toLowerCase(), t = (text || "").trim().toLowerCase();
|
|
438
|
+
if (!q || !t) return 0;
|
|
439
|
+
if (q === t) return 220;
|
|
440
|
+
if (t.startsWith(q)) return 175;
|
|
441
|
+
if (t.includes(q)) return 135;
|
|
442
|
+
const qt = tokens(q), tt = tokens(t);
|
|
443
|
+
if (!qt.length || !tt.length) return 0;
|
|
444
|
+
const ov = tokenOverlapCount(qt, tt);
|
|
445
|
+
return ov ? 100 * ov / qt.length + 40 : 0;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function rankEntry(query, entry) {
|
|
449
|
+
const kind = classifyQuery(query);
|
|
450
|
+
if (kind === "intrinsic" && !hasOverlap(query, entry.kind === "intrinsic" ? entry.item.name : entry.item.key)) return -Infinity;
|
|
451
|
+
if (kind === "instruction" && !hasOverlap(query, entry.kind === "instruction" ? entry.item.key : entry.item.name)) return -Infinity;
|
|
452
|
+
let score = Math.max(...entry.fields.map(f => scoreText(query, f)));
|
|
453
|
+
if (entry.kind === "intrinsic") {
|
|
454
|
+
const wq = new Set(tokens(query).filter(t => /^mm\d+$|^xmm$|^ymm$|^zmm$/.test(t)));
|
|
455
|
+
const wt = new Set(tokens(entry.title).filter(t => /^mm\d+$|^xmm$|^ymm$|^zmm$/.test(t)));
|
|
456
|
+
if (wq.size && wt.size) score += [...wq].some(w => wt.has(w)) ? 22 : -22;
|
|
457
|
+
}
|
|
458
|
+
if (kind === "intrinsic") score += entry.kind === "intrinsic" ? 45 : -25;
|
|
459
|
+
if (kind === "instruction") score += entry.kind === "instruction" ? 35 : -10;
|
|
460
|
+
return score;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/* ── Search index ─────────────────────────────────────────────────── */
|
|
464
|
+
function buildSearchIndexes(entries) {
|
|
465
|
+
searchTokenIndex = new Map();
|
|
466
|
+
searchPrefixIndex = new Map();
|
|
467
|
+
entries.forEach((entry, i) => {
|
|
468
|
+
entry.searchTokens = [...new Set(entry.fields.flatMap(f => tokens(f)))];
|
|
469
|
+
for (const t of entry.searchTokens) {
|
|
470
|
+
if (!searchTokenIndex.has(t)) searchTokenIndex.set(t, []);
|
|
471
|
+
searchTokenIndex.get(t).push(i);
|
|
472
|
+
for (let sz = 1; sz <= Math.min(t.length, 6); sz++) {
|
|
473
|
+
const pfx = t.slice(0, sz);
|
|
474
|
+
if (!searchPrefixIndex.has(pfx)) searchPrefixIndex.set(pfx, []);
|
|
475
|
+
searchPrefixIndex.get(pfx).push(i);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function candidateIndexes(query) {
|
|
482
|
+
const qt = tokens(query);
|
|
483
|
+
if (!qt.length) return null;
|
|
484
|
+
const lists = qt.map(t => searchTokenIndex.get(t) || searchPrefixIndex.get(t) || []);
|
|
485
|
+
if (lists.some(l => !l.length)) return [];
|
|
486
|
+
lists.sort((a, b) => a.length - b.length);
|
|
487
|
+
let cur = new Set(lists[0]);
|
|
488
|
+
for (const l of lists.slice(1)) {
|
|
489
|
+
const allowed = new Set(l);
|
|
490
|
+
const next = new Set();
|
|
491
|
+
for (const i of cur) if (allowed.has(i)) next.add(i);
|
|
492
|
+
cur = next;
|
|
493
|
+
if (!cur.size) break;
|
|
494
|
+
}
|
|
495
|
+
return [...cur];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/* ── Natural sort ─────────────────────────────────────────────────── */
|
|
499
|
+
function naturalKey(v) { return (v || "").toLowerCase().match(/[a-z]+|\d+/g) || []; }
|
|
500
|
+
function naturalCmp(a, b) {
|
|
501
|
+
const ak = naturalKey(a), bk = naturalKey(b);
|
|
502
|
+
for (let i = 0; i < Math.max(ak.length, bk.length); i++) {
|
|
503
|
+
if (ak[i] == null) return -1;
|
|
504
|
+
if (bk[i] == null) return 1;
|
|
505
|
+
const an = /^\d+$/.test(ak[i]), bn = /^\d+$/.test(bk[i]);
|
|
506
|
+
if (an && bn) { const d = Number(ak[i]) - Number(bk[i]); if (d) return d; }
|
|
507
|
+
else if (an !== bn) return an ? 1 : -1;
|
|
508
|
+
else { const d = ak[i].localeCompare(bk[i]); if (d) return d; }
|
|
509
|
+
}
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/* ── gzip-aware fetch ─────────────────────────────────────────────────
|
|
514
|
+
* Prefer a pre-compressed *.gz sidecar (so GitHub Pages & bare static
|
|
515
|
+
* hosts don't need Content-Encoding: gzip), fall back to the raw .json.
|
|
516
|
+
*/
|
|
517
|
+
async function fetchJson(path) {
|
|
518
|
+
const base = path.replace(/\.json$/, "");
|
|
519
|
+
if (typeof DecompressionStream !== "undefined") {
|
|
520
|
+
try {
|
|
521
|
+
const r = await fetch(`${base}.json.gz`);
|
|
522
|
+
if (r.ok) {
|
|
523
|
+
const ds = r.body.pipeThrough(new DecompressionStream("gzip"));
|
|
524
|
+
return await new Response(ds).json();
|
|
525
|
+
}
|
|
526
|
+
} catch (_) { /* fall through */ }
|
|
527
|
+
}
|
|
528
|
+
const r = await fetch(`${base}.json`);
|
|
529
|
+
return r.ok ? r.json() : null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* ── Lazy detail loading ──────────────────────────────────────────── */
|
|
533
|
+
function chunkPrefix(mnemonic) {
|
|
534
|
+
const c = (mnemonic || "").trim().toUpperCase();
|
|
535
|
+
return c.length >= 3 ? c.slice(0, 3) : c;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/* Keep in sync with simdref.web._intrinsic_chunk_prefix. */
|
|
539
|
+
function intrinsicChunkPrefix(name) {
|
|
540
|
+
let s = String(name || "").replace(/^_+/, "");
|
|
541
|
+
if (s.slice(0, 6).toLowerCase() === "riscv_") s = s.slice(6);
|
|
542
|
+
const m = s.match(/^(?:mm\d*|sv|vq?)_?/i);
|
|
543
|
+
if (m && m[0].length < s.length) s = s.slice(m[0].length);
|
|
544
|
+
s = s.replace(/^_+/, "");
|
|
545
|
+
const clean = (s.match(/[a-z0-9]/gi) || []).join("").toLowerCase();
|
|
546
|
+
if (clean.length < 2) return "misc";
|
|
547
|
+
return clean.slice(0, 3);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function loadChunk(prefix) {
|
|
551
|
+
if (chunkCache.has(prefix)) return chunkCache.get(prefix);
|
|
552
|
+
const p = fetchJson(`detail-chunks/${encodeURIComponent(prefix)}.json`)
|
|
553
|
+
.then(data => data || {})
|
|
554
|
+
.catch(() => ({}));
|
|
555
|
+
chunkCache.set(prefix, p);
|
|
556
|
+
return p;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function loadIntrinsicChunk(prefix) {
|
|
560
|
+
if (intrinsicChunkCache.has(prefix)) return intrinsicChunkCache.get(prefix);
|
|
561
|
+
const p = fetchJson(`intrinsic-chunks/${encodeURIComponent(prefix)}.json`)
|
|
562
|
+
.then(data => data || {})
|
|
563
|
+
.catch(() => ({}));
|
|
564
|
+
intrinsicChunkCache.set(prefix, p);
|
|
565
|
+
return p;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/* Fetch one intrinsic's full details (lazy, per-bucket). Returns the
|
|
569
|
+
* detail entry or null if not found in its bucket (build skew). */
|
|
570
|
+
async function loadIntrinsic(name) {
|
|
571
|
+
const chunk = await loadIntrinsicChunk(intrinsicChunkPrefix(name));
|
|
572
|
+
return chunk[name] || null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/* Warm intrinsic-chunks for the top N visible hits during browser
|
|
576
|
+
* idle time so clicking a result doesn't pay a network round-trip. */
|
|
577
|
+
function prefetchIntrinsicChunks(entries) {
|
|
578
|
+
const schedule = window.requestIdleCallback || ((cb) => setTimeout(cb, 0));
|
|
579
|
+
const prefixes = new Set();
|
|
580
|
+
for (const e of entries) {
|
|
581
|
+
if (!e || e.kind !== "intrinsic") continue;
|
|
582
|
+
prefixes.add(intrinsicChunkPrefix(e.item.name));
|
|
583
|
+
if (prefixes.size >= 4) break;
|
|
584
|
+
}
|
|
585
|
+
for (const prefix of prefixes) {
|
|
586
|
+
if (intrinsicChunkCache.has(prefix)) continue;
|
|
587
|
+
schedule(() => { loadIntrinsicChunk(prefix); });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/* ── Render helpers ───────────────────────────────────────────────── */
|
|
592
|
+
function renderTable(headers, rows) {
|
|
593
|
+
if (!rows.length) return `<div style="color:var(--text-muted);font-size:0.82rem">No data.</div>`;
|
|
594
|
+
return `<table><thead><tr>${headers.map(h => `<th>${esc(h.label)}</th>`).join("")}</tr></thead><tbody>${
|
|
595
|
+
rows.map(r => `<tr>${headers.map(h => {
|
|
596
|
+
const cls = h.cls ? ` class="${h.cls}"` : "";
|
|
597
|
+
return `<td${cls}>${h.render ? h.render(r) : esc(r[h.key] ?? "-")}</td>`;
|
|
598
|
+
}).join("")}</tr>`).join("")
|
|
599
|
+
}</tbody></table>`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/* ── Measurement grouping ─────────────────────────────────────────── */
|
|
603
|
+
const uarchOrder = ["ARL-P","ARL-E","MTL-P","MTL-E","EMR","ADL-P","ADL-E","RKL","TGL","ICL","CLX","CNL","SKX","CFL","KBL","SKL","BDW","HSW","IVB","SNB","ZEN5","ZEN4","ZEN3","ZEN2","ZEN+"];
|
|
604
|
+
|
|
605
|
+
function uarchFamily(u) {
|
|
606
|
+
if (["ZEN5","ZEN4","ZEN3","ZEN2","ZEN+"].includes(u)) return "AMD";
|
|
607
|
+
if (["EMR","CLX","SKX","CNL"].includes(u)) return "Intel Server";
|
|
608
|
+
return "Intel Client";
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function groupMeasurements(rows) {
|
|
612
|
+
const sorted = [...rows].sort((a, b) => {
|
|
613
|
+
const ai = uarchOrder.indexOf(a.uarch), bi = uarchOrder.indexOf(b.uarch);
|
|
614
|
+
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi) || a.uarch.localeCompare(b.uarch);
|
|
615
|
+
});
|
|
616
|
+
const groups = new Map();
|
|
617
|
+
for (const r of sorted) {
|
|
618
|
+
const fam = uarchFamily(r.uarch);
|
|
619
|
+
if (!groups.has(fam)) groups.set(fam, []);
|
|
620
|
+
groups.get(fam).push(r);
|
|
621
|
+
}
|
|
622
|
+
return groups;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const uarchLabels = {
|
|
626
|
+
"ARL-P": "Arrow Lake-P (2024)", "ARL-E": "Arrow Lake-E (2024)",
|
|
627
|
+
"MTL-P": "Meteor Lake-P (2023)", "MTL-E": "Meteor Lake-E (2023)",
|
|
628
|
+
"EMR": "Emerald Rapids (2023)",
|
|
629
|
+
"ADL-P": "Alder Lake-P (2021)", "ADL-E": "Alder Lake-E (2021)",
|
|
630
|
+
"RKL": "Rocket Lake (2021)", "TGL": "Tiger Lake (2020)", "ICL": "Ice Lake (2019)",
|
|
631
|
+
"CLX": "Cascade Lake (2019)", "CNL": "Cannon Lake (2018)", "SKX": "Skylake-X (2017)",
|
|
632
|
+
"CFL": "Coffee Lake (2017)", "KBL": "Kaby Lake (2016)", "SKL": "Skylake (2015)",
|
|
633
|
+
"BDW": "Broadwell (2014)", "HSW": "Haswell (2013)", "IVB": "Ivy Bridge (2012)", "SNB": "Sandy Bridge (2011)",
|
|
634
|
+
"ZEN5": "Zen 5 (2024)", "ZEN4": "Zen 4 (2022)", "ZEN3": "Zen 3 (2020)", "ZEN2": "Zen 2 (2019)", "ZEN+": "Zen+ (2018)",
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const measHeaders = [
|
|
638
|
+
{key: "uarch", label: "microarch", render: r => esc(uarchLabels[r.uarch] || r.uarch)},
|
|
639
|
+
{key: "latency", label: "LAT", cls: "highlight"},
|
|
640
|
+
{key: "tpLoop", label: "CPI", cls: "highlight"},
|
|
641
|
+
{key: "tpPorts", label: "CPI ports"},
|
|
642
|
+
{key: "uops", label: "uops"},
|
|
643
|
+
{key: "ports", label: "ports"},
|
|
644
|
+
];
|
|
645
|
+
|
|
646
|
+
function splitMeasurements(measurements) {
|
|
647
|
+
/* Bucket rows by sourceKind in a stable order (measured first).
|
|
648
|
+
Mirrors split_perf_rows in src/simdref/display.py. */
|
|
649
|
+
const buckets = new Map();
|
|
650
|
+
for (const row of measurements) {
|
|
651
|
+
const kind = row.sourceKind || "measured";
|
|
652
|
+
if (!buckets.has(kind)) buckets.set(kind, []);
|
|
653
|
+
buckets.get(kind).push(row);
|
|
654
|
+
}
|
|
655
|
+
const order = ["measured", "modeled"];
|
|
656
|
+
const out = [];
|
|
657
|
+
for (const kind of order) {
|
|
658
|
+
if (buckets.has(kind)) out.push([kind, buckets.get(kind)]);
|
|
659
|
+
}
|
|
660
|
+
for (const [kind, rows] of buckets) {
|
|
661
|
+
if (!order.includes(kind)) out.push([kind, rows]);
|
|
662
|
+
}
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const perfKindLabels = {measured: "perf (measured)", modeled: "perf (modeled)"};
|
|
667
|
+
const perfKindBorders = {measured: "var(--accent-green, #2ea043)", modeled: "var(--accent-yellow, #bf8700)"};
|
|
668
|
+
|
|
669
|
+
function renderMeasurements(measurements) {
|
|
670
|
+
if (!measurements || !measurements.length) return `<div style="color:var(--text-muted);font-size:0.82rem">No performance data.</div>`;
|
|
671
|
+
const splits = splitMeasurements(measurements).filter(([kind]) => enabledPerfKinds.has(kind));
|
|
672
|
+
if (!splits.length) return `<div style="color:var(--text-muted);font-size:0.82rem">All perf sources disabled.</div>`;
|
|
673
|
+
let html = "";
|
|
674
|
+
for (const [kind, rows] of splits) {
|
|
675
|
+
const label = perfKindLabels[kind] || `perf (${kind})`;
|
|
676
|
+
const color = perfKindBorders[kind] || "var(--accent-green, #2ea043)";
|
|
677
|
+
const groups = groupMeasurements(rows);
|
|
678
|
+
let inner = "";
|
|
679
|
+
for (const [family, frows] of groups) {
|
|
680
|
+
inner += `<details class="meas-group" open><summary>${esc(family)} (${frows.length})</summary>${renderTable(measHeaders, frows)}</details>`;
|
|
681
|
+
}
|
|
682
|
+
html += `<section class="perf-panel" style="border-left:3px solid ${color};padding-left:0.6rem;margin-bottom:0.8rem">
|
|
683
|
+
<h4 style="margin:0 0 0.3rem 0;color:${color};font-size:0.85rem">${esc(label)} (${rows.length})</h4>
|
|
684
|
+
${inner}
|
|
685
|
+
</section>`;
|
|
686
|
+
}
|
|
687
|
+
return html;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* ── Description sections ────────────────────────────────────────── */
|
|
691
|
+
const descSectionOrder = [
|
|
692
|
+
"Description", "Operation", "Intrinsic Equivalents",
|
|
693
|
+
"Flags Affected", "Exceptions", "SIMD Floating-Point Exceptions",
|
|
694
|
+
"Numeric Exceptions", "Other Exceptions",
|
|
695
|
+
"Protected Mode Exceptions", "Real-Address Mode Exceptions",
|
|
696
|
+
"Virtual-8086 Mode Exceptions", "Compatibility Mode Exceptions",
|
|
697
|
+
"64-Bit Mode Exceptions",
|
|
698
|
+
];
|
|
699
|
+
const codeSections = new Set(["Operation", "Intrinsic Equivalents"]);
|
|
700
|
+
|
|
701
|
+
function renderDescriptionSections(description) {
|
|
702
|
+
if (!description || !Object.keys(description).length) return "";
|
|
703
|
+
const shown = new Set();
|
|
704
|
+
let html = "";
|
|
705
|
+
for (const key of descSectionOrder) {
|
|
706
|
+
if (description[key]) {
|
|
707
|
+
const isCode = codeSections.has(key);
|
|
708
|
+
const isFirst = key === "Description";
|
|
709
|
+
html += `<details class="desc-section"${isFirst ? " open" : ""}>
|
|
710
|
+
<summary>${esc(key)}</summary>
|
|
711
|
+
${isCode ? `<pre class="desc-code">${esc(description[key])}</pre>` : `<div class="desc-body">${esc(description[key])}</div>`}
|
|
712
|
+
</details>`;
|
|
713
|
+
shown.add(key);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
for (const [key, value] of Object.entries(description)) {
|
|
717
|
+
if (!shown.has(key)) {
|
|
718
|
+
html += `<details class="desc-section">
|
|
719
|
+
<summary>${esc(key)}</summary>
|
|
720
|
+
<div class="desc-body">${esc(value)}</div>
|
|
721
|
+
</details>`;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return html;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/* ── Detail rendering ─────────────────────────────────────────────── */
|
|
728
|
+
const operandHeaders = [
|
|
729
|
+
{key: "idx", label: "Idx"},
|
|
730
|
+
{key: "rw", label: "R/W", render: r => esc(((r.r === "1" ? "r" : "") + (r.w === "1" ? "w" : "")) || "-")},
|
|
731
|
+
{key: "type", label: "Type"},
|
|
732
|
+
{key: "width", label: "Width"},
|
|
733
|
+
{key: "xtype", label: "XType"},
|
|
734
|
+
{key: "name", label: "Name", cls: "mono"},
|
|
735
|
+
];
|
|
736
|
+
|
|
737
|
+
function renderIntrinsicDetail(item, detail) {
|
|
738
|
+
const linkedRefs = detail ? (detail.instruction_refs || item.instruction_refs || []) : (item.instruction_refs || []);
|
|
739
|
+
const linked = (linkedRefs.length
|
|
740
|
+
? linkedRefs.map(ref => catalog.instrByKey[ref.key] || catalog.instrByDisplayKey[ref.display_key] || catalog.instrByMnem[ref.name])
|
|
741
|
+
: (item.instructions || []).map(k => catalog.instrByDisplayKey[k] || catalog.instrByKey[k] || catalog.instrByMnem[k]))
|
|
742
|
+
.filter(Boolean).filter(r => isaVisible(r.isa));
|
|
743
|
+
|
|
744
|
+
const hasMeasurements = detail && detail._measurements && detail._measurements.length;
|
|
745
|
+
const hasOperands = detail && detail._operands && detail._operands.length;
|
|
746
|
+
const meta = detail ? (detail.metadata || {}) : (item.metadata || {});
|
|
747
|
+
const instrMeta = detail ? (detail._instructionMeta || {}) : {};
|
|
748
|
+
const instrPdfRefs = detail ? (detail._pdfRefs || []) : [];
|
|
749
|
+
|
|
750
|
+
return `
|
|
751
|
+
<div class="detail-head">
|
|
752
|
+
<span class="result-kind intrinsic">intrinsic</span>
|
|
753
|
+
<h2>${esc(item.name)} <button class="copy-btn" data-copy="${esc(item.name)}" title="Copy name">⎘</button></h2>
|
|
754
|
+
<div class="detail-sub">${esc(detail ? detail.signature : item.signature)}</div>
|
|
755
|
+
<div class="chips">
|
|
756
|
+
${((item.display_isa_tokens || item.isa || [])).map(v => `<span class="chip">${esc(Array.isArray(v) ? displayIsa(v) : v)}</span>`).join("")}
|
|
757
|
+
${item.header ? `<span class="chip">${esc(item.header)}</span>` : ""}
|
|
758
|
+
</div>
|
|
759
|
+
</div>
|
|
760
|
+
<section class="section">
|
|
761
|
+
<h3>Description</h3>
|
|
762
|
+
<div>${esc(detail ? detail.description : item.description)}</div>
|
|
763
|
+
</section>
|
|
764
|
+
${detail && detail.doc_sections && Object.keys(detail.doc_sections).length ? `<section class="section">
|
|
765
|
+
<h3>ACLE Documentation</h3>
|
|
766
|
+
${renderDescriptionSections(detail.doc_sections)}
|
|
767
|
+
</section>` : ""}
|
|
768
|
+
${detail && detail._instrDescription && Object.keys(detail._instrDescription).length ? `<section class="section">
|
|
769
|
+
<h3>Instruction Semantics</h3>
|
|
770
|
+
${renderDescriptionSections(detail._instrDescription)}
|
|
771
|
+
</section>` : ""}
|
|
772
|
+
<section class="section">
|
|
773
|
+
<h3>Metadata</h3>
|
|
774
|
+
<dl class="kv">
|
|
775
|
+
<dt>Header</dt><dd>${esc(item.header || "-")}</dd>
|
|
776
|
+
<dt>Architecture</dt><dd>${esc(item.display_architecture || item.architecture || "-")}</dd>
|
|
777
|
+
<dt>ISA</dt><dd>${esc(item.display_isa || displayIsa(item.isa))}</dd>
|
|
778
|
+
${item.category ? `<dt>Category</dt><dd>${esc(item.subcategory ? `${item.subcategory} / ${item.category}` : item.category)}</dd>` : ""}
|
|
779
|
+
${detail && detail.url ? `<dt>Source</dt><dd><a href="${esc(detail.url)}" target="_blank" rel="noreferrer">${esc(detail.url)}</a></dd>` : ""}
|
|
780
|
+
${meta.reference_url ? `<dt>Reference</dt><dd><a href="${esc(meta.reference_url)}" target="_blank" rel="noreferrer">${esc(meta.reference_url)}</a></dd>` : ""}
|
|
781
|
+
${meta.argument_preparation ? `<dt>Argument Prep</dt><dd>${esc(meta.argument_preparation)}</dd>` : ""}
|
|
782
|
+
${meta.result ? `<dt>Result</dt><dd>${esc(meta.result)}</dd>` : ""}
|
|
783
|
+
${meta.supported_architectures ? `<dt>Supported</dt><dd>${esc(meta.supported_architectures)}</dd>` : ""}
|
|
784
|
+
${meta.classification_path ? `<dt>Section</dt><dd>${esc(meta.classification_path)}</dd>` : ""}
|
|
785
|
+
${detail && detail.notes && detail.notes.length ? `<dt>Notes</dt><dd>${esc(detail.notes.join("; "))}</dd>` : ""}
|
|
786
|
+
${detail && detail._linkedInstruction ? `<dt>Instruction</dt><dd>${esc(detail._linkedInstruction)}</dd>` : ""}
|
|
787
|
+
${instrMeta.category ? `<dt>Instruction Category</dt><dd>${esc(instrMeta.category)}</dd>` : ""}
|
|
788
|
+
${instrMeta.cpl ? `<dt>CPL</dt><dd>${esc(instrMeta.cpl)}</dd>` : ""}
|
|
789
|
+
${instrMeta.url ? `<dt>Instruction Source</dt><dd><a href="${esc(canonUrl(instrMeta.url))}" target="_blank" rel="noreferrer">${esc(canonUrl(instrMeta.url))}</a></dd>` : ""}
|
|
790
|
+
${instrMeta["url-ref"] ? `<dt>Instruction Reference</dt><dd><a href="${esc(canonUrl(instrMeta["url-ref"]))}" target="_blank" rel="noreferrer">${esc(canonUrl(instrMeta["url-ref"]))}</a></dd>` : ""}
|
|
791
|
+
${instrPdfRefs.map(ref => `<dt>${esc(ref.label || ref.source_id || "PDF")}</dt><dd><a href="${esc(ref.url || "")}" target="_blank" rel="noreferrer">Open PDF${ref.page_start ? ` (page ${esc(ref.page_start)})` : ""}</a></dd>`).join("")}
|
|
792
|
+
</dl>
|
|
793
|
+
</section>
|
|
794
|
+
<section class="section">
|
|
795
|
+
<h3>Instructions</h3>
|
|
796
|
+
${linked.length ? renderTable(
|
|
797
|
+
[
|
|
798
|
+
{key: "key", label: "Instruction", render: r => `<a class="xref" href="#${encodeURIComponent(r.key)}" data-kind="instruction" data-key="${esc(r.key)}">${esc(r.display_key || r.key)}</a>`},
|
|
799
|
+
{key: "summary", label: "Summary"},
|
|
800
|
+
{key: "isa", label: "ISA", render: r => esc(r.display_isa || displayIsa(r.isa))},
|
|
801
|
+
],
|
|
802
|
+
linked
|
|
803
|
+
) : `<div style="color:var(--text-muted);font-size:0.82rem">No linked instructions.</div>`}
|
|
804
|
+
</section>
|
|
805
|
+
${hasOperands ? `<section class="section">
|
|
806
|
+
<h3>Operands</h3>
|
|
807
|
+
${renderTable(operandHeaders, detail._operands)}
|
|
808
|
+
</section>` : ""}
|
|
809
|
+
${hasMeasurements ? `<section class="section">
|
|
810
|
+
<h3>Performance</h3>
|
|
811
|
+
${renderMeasurements(detail._measurements)}
|
|
812
|
+
</section>` : ""}
|
|
813
|
+
`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function renderInstructionDetail(item, detail) {
|
|
817
|
+
const d = detail || {};
|
|
818
|
+
const linked = (item.linked_intrinsics || [])
|
|
819
|
+
.map(n => catalog.intrByName[n])
|
|
820
|
+
.filter(Boolean).filter(r => isaVisible(r.isa));
|
|
821
|
+
|
|
822
|
+
const measurements = d.measurements || [];
|
|
823
|
+
const operands = d.operand_details || [];
|
|
824
|
+
const meta = d.metadata || {};
|
|
825
|
+
const pdfRefs = Array.isArray(d.pdf_refs) ? d.pdf_refs : (meta["intel-sdm-url"] ? [{
|
|
826
|
+
source_id: "intel-sdm",
|
|
827
|
+
label: "Intel SDM",
|
|
828
|
+
url: meta["intel-sdm-url"],
|
|
829
|
+
page_start: meta["intel-sdm-page-start"] || "",
|
|
830
|
+
page_end: meta["intel-sdm-page-end"] || "",
|
|
831
|
+
}] : []);
|
|
832
|
+
|
|
833
|
+
return `
|
|
834
|
+
<div class="detail-head">
|
|
835
|
+
<span class="result-kind instruction">instruction</span>
|
|
836
|
+
<h2>${esc(item.display_key || item.key || item.mnemonic)} <button class="copy-btn" data-copy="${esc(item.display_key || item.key || item.mnemonic)}" title="Copy name">⎘</button></h2>
|
|
837
|
+
<div class="detail-sub">${esc(d.display_form || item.display_form || d.form || item.form || item.display_mnemonic || item.mnemonic)}</div>
|
|
838
|
+
<div class="chips">
|
|
839
|
+
${((item.display_isa_tokens || item.isa || [])).map(v => `<span class="chip">${esc(Array.isArray(v) ? displayIsa(v) : v)}</span>`).join("")}
|
|
840
|
+
${meta.cpl ? `<span class="chip">CPL ${esc(meta.cpl)}</span>` : ""}
|
|
841
|
+
</div>
|
|
842
|
+
</div>
|
|
843
|
+
<section class="section">
|
|
844
|
+
<h3>Summary</h3>
|
|
845
|
+
<div>${esc(d.summary || item.summary || "-")}</div>
|
|
846
|
+
</section>
|
|
847
|
+
${d.description && Object.keys(d.description).length ? `<section class="section">
|
|
848
|
+
<h3>Instruction Semantics</h3>
|
|
849
|
+
${renderDescriptionSections(d.description)}
|
|
850
|
+
</section>` : ""}
|
|
851
|
+
<section class="section">
|
|
852
|
+
<h3>Metadata</h3>
|
|
853
|
+
<dl class="kv">
|
|
854
|
+
<dt>Mnemonic</dt><dd class="mono">${esc(item.display_mnemonic || item.mnemonic)}</dd>
|
|
855
|
+
<dt>Form</dt><dd class="mono">${esc(d.display_form || item.display_form || d.form || item.form || "-")}</dd>
|
|
856
|
+
<dt>Architecture</dt><dd>${esc(item.display_architecture || item.architecture || "-")}</dd>
|
|
857
|
+
<dt>ISA</dt><dd>${esc(item.display_isa || displayIsa(item.isa))}</dd>
|
|
858
|
+
${meta.url ? `<dt>uops.info</dt><dd><a href="${esc(canonUrl(meta.url))}" target="_blank" rel="noreferrer">${esc(canonUrl(meta.url))}</a></dd>` : ""}
|
|
859
|
+
${meta["url-ref"] ? `<dt>Reference</dt><dd><a href="${esc(canonUrl(meta["url-ref"]))}" target="_blank" rel="noreferrer">${esc(canonUrl(meta["url-ref"]))}</a></dd>` : ""}
|
|
860
|
+
${pdfRefs.map(ref => `<dt>${esc(ref.label || ref.source_id || "PDF")}</dt><dd><a href="${esc(ref.url || "")}" target="_blank" rel="noreferrer">Open PDF${ref.page_start ? ` (page ${esc(ref.page_start)})` : ""}</a></dd>`).join("")}
|
|
861
|
+
</dl>
|
|
862
|
+
</section>
|
|
863
|
+
<section class="section">
|
|
864
|
+
<h3>Intrinsics</h3>
|
|
865
|
+
${linked.length ? renderTable(
|
|
866
|
+
[
|
|
867
|
+
{key: "name", label: "Intrinsic", render: r => `<a class="xref" href="#${encodeURIComponent(r.name)}" data-kind="intrinsic" data-key="${esc(r.name)}">${esc(r.name)}</a>`},
|
|
868
|
+
{key: "description", label: "Description"},
|
|
869
|
+
{key: "isa", label: "ISA", render: r => esc(r.display_isa || displayIsa(r.isa))},
|
|
870
|
+
],
|
|
871
|
+
linked
|
|
872
|
+
) : `<div style="color:var(--text-muted);font-size:0.82rem">No linked intrinsics.</div>`}
|
|
873
|
+
</section>
|
|
874
|
+
${operands.length ? `<section class="section">
|
|
875
|
+
<h3>Operands</h3>
|
|
876
|
+
${renderTable(operandHeaders, operands)}
|
|
877
|
+
</section>` : ""}
|
|
878
|
+
${measurements.length ? `<section class="section">
|
|
879
|
+
<h3>Performance</h3>
|
|
880
|
+
${renderMeasurements(measurements)}
|
|
881
|
+
</section>` : ""}
|
|
882
|
+
`;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/* ── Detail view ──────────────────────────────────────────────────── */
|
|
886
|
+
async function renderDetail(entry) {
|
|
887
|
+
activeKey = entry.key;
|
|
888
|
+
detailEmpty.style.display = "none";
|
|
889
|
+
|
|
890
|
+
// Highlight in results
|
|
891
|
+
for (const n of resultsNode.querySelectorAll(".result")) {
|
|
892
|
+
n.classList.toggle("active", n.dataset.key === entry.key);
|
|
893
|
+
}
|
|
894
|
+
location.hash = encodeURIComponent(entry.key);
|
|
895
|
+
|
|
896
|
+
if (entry.kind === "intrinsic") {
|
|
897
|
+
// Load primary instruction detail for operands/measurements
|
|
898
|
+
const primaryKey = entry.item.primary_instr || (entry.item.instruction_refs || [])[0]?.key || (entry.item.instructions || [])[0];
|
|
899
|
+
let detail = null;
|
|
900
|
+
if (primaryKey) {
|
|
901
|
+
const instr = catalog.instrByDisplayKey[primaryKey] || catalog.instrByKey[primaryKey] || catalog.instrByMnem[primaryKey];
|
|
902
|
+
if (instr) {
|
|
903
|
+
const prefix = chunkPrefix(instr.mnemonic);
|
|
904
|
+
const chunk = await loadChunk(prefix);
|
|
905
|
+
const instrDetail = chunk[instr.key] || chunk[instr.display_key] || chunk[primaryKey] || {};
|
|
906
|
+
// Load this intrinsic's chunk lazily; fall back to the
|
|
907
|
+
// search-index entry if the chunk doesn't carry it.
|
|
908
|
+
const full = await loadIntrinsic(entry.item.name);
|
|
909
|
+
detail = full || entry.item;
|
|
910
|
+
detail._measurements = instrDetail.measurements || [];
|
|
911
|
+
detail._operands = instrDetail.operand_details || [];
|
|
912
|
+
detail._instrDescription = instrDetail.description || {};
|
|
913
|
+
detail._instructionMeta = instrDetail.metadata || {};
|
|
914
|
+
detail._linkedInstruction = instrDetail.display_form || instr.display_key || instr.key || "";
|
|
915
|
+
detail._pdfRefs = instrDetail.pdf_refs || [];
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (!detail) {
|
|
919
|
+
const full = await loadIntrinsic(entry.item.name);
|
|
920
|
+
detail = full || entry.item;
|
|
921
|
+
detail._measurements = [];
|
|
922
|
+
detail._operands = [];
|
|
923
|
+
detail._pdfRefs = [];
|
|
924
|
+
}
|
|
925
|
+
detailNode.innerHTML = renderIntrinsicDetail(entry.item, detail);
|
|
926
|
+
} else {
|
|
927
|
+
// Load instruction detail chunk
|
|
928
|
+
const prefix = chunkPrefix(entry.item.mnemonic);
|
|
929
|
+
const chunk = await loadChunk(prefix);
|
|
930
|
+
const detail = chunk[entry.item.key] || null;
|
|
931
|
+
detailNode.innerHTML = renderInstructionDetail(entry.item, detail);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/* ── Category Filters ─────────────────────────────────────────────── */
|
|
937
|
+
|
|
938
|
+
function updateCategorySummary() {
|
|
939
|
+
if (!categorySummary) return;
|
|
940
|
+
if (enabledCategories === null) {
|
|
941
|
+
categorySummary.textContent = "Category: All";
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const n = enabledCategories.size;
|
|
945
|
+
if (n === 0) { categorySummary.textContent = "Category: None"; return; }
|
|
946
|
+
if (n <= 2) {
|
|
947
|
+
categorySummary.textContent = "Category: " + [...enabledCategories].map(c => c || "(none)").join(", ");
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
categorySummary.textContent = `Category: ${n} selected`;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function renderCategoryFilters() {
|
|
954
|
+
if (!categoryChips) return;
|
|
955
|
+
// De-duplicate categories across families (same name, aggregate count).
|
|
956
|
+
const bucket = new Map();
|
|
957
|
+
for (const spec of availableCategories) {
|
|
958
|
+
const cat = spec.category || "";
|
|
959
|
+
const prev = bucket.get(cat) || { category: cat, count: 0, families: new Set() };
|
|
960
|
+
prev.count += spec.count || 0;
|
|
961
|
+
if (spec.family) prev.families.add(spec.family);
|
|
962
|
+
bucket.set(cat, prev);
|
|
963
|
+
}
|
|
964
|
+
const entries = [...bucket.values()].sort((a, b) => b.count - a.count);
|
|
965
|
+
categoryChips.innerHTML = entries.map(entry => {
|
|
966
|
+
const active = enabledCategories === null || enabledCategories.has(entry.category);
|
|
967
|
+
const label = entry.category || "(uncategorised)";
|
|
968
|
+
return `<label class="isa-chip ${active ? "active" : ""}" title="${esc([...entry.families].join(", "))}">
|
|
969
|
+
<input type="checkbox" data-category="${esc(entry.category)}" ${active ? "checked" : ""}>
|
|
970
|
+
${esc(label)}
|
|
971
|
+
<span class="chip-count">${entry.count}</span>
|
|
972
|
+
</label>`;
|
|
973
|
+
}).join("");
|
|
974
|
+
|
|
975
|
+
for (const cb of categoryChips.querySelectorAll("input[data-category]")) {
|
|
976
|
+
cb.addEventListener("change", () => {
|
|
977
|
+
const cat = cb.dataset.category;
|
|
978
|
+
if (enabledCategories === null) {
|
|
979
|
+
// First interaction: switch from "all" to an explicit set.
|
|
980
|
+
enabledCategories = new Set(entries.map(e => e.category));
|
|
981
|
+
}
|
|
982
|
+
if (cb.checked) enabledCategories.add(cat); else enabledCategories.delete(cat);
|
|
983
|
+
visibleSet = null;
|
|
984
|
+
scheduleFilterRender();
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/* ── ISA Filters ──────────────────────────────────────────────────── */
|
|
990
|
+
|
|
991
|
+
function updateIsaSummary() {
|
|
992
|
+
if (enableAllIsas) { isaSummary.textContent = "ISA: All"; return; }
|
|
993
|
+
const active = availableIsas.filter(v => enabledIsas.has(v));
|
|
994
|
+
if (!active.length) { isaSummary.textContent = "ISA: None"; return; }
|
|
995
|
+
if (active.length <= 3) { isaSummary.textContent = "ISA: " + active.join(", "); return; }
|
|
996
|
+
isaSummary.textContent = `ISA: ${active.slice(0, 2).join(", ")} +${active.length - 2}`;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function renderIsaFilters() {
|
|
1000
|
+
isaFamiliesNode.innerHTML = availableIsas.map(family => {
|
|
1001
|
+
const familyActive = enableAllIsas || enabledIsas.has(family);
|
|
1002
|
+
return `<label class="isa-chip ${familyActive ? "active" : ""}">
|
|
1003
|
+
<input type="checkbox" data-family="${esc(family)}" ${familyActive ? "checked" : ""}>
|
|
1004
|
+
${esc(family)}
|
|
1005
|
+
</label>`;
|
|
1006
|
+
}).join("");
|
|
1007
|
+
|
|
1008
|
+
isaSubgroupsNode.innerHTML = availableIsas.flatMap(family => {
|
|
1009
|
+
const subs = FAMILY_SUB_ORDER[family];
|
|
1010
|
+
const familyActive = enableAllIsas || enabledIsas.has(family);
|
|
1011
|
+
if (!subs || !familyActive) return [];
|
|
1012
|
+
const enabledSubs = enabledSubIsas.get(family) || new Set();
|
|
1013
|
+
return [`<div class="isa-group">
|
|
1014
|
+
<button class="isa-group-label family-active" type="button" data-group-family="${esc(family)}">${esc(family)}</button>
|
|
1015
|
+
<div class="isa-group-items">
|
|
1016
|
+
${subs.map(sub => `<label class="isa-chip ${(enableAllIsas || enabledSubs.has(sub)) ? "active" : ""}">
|
|
1017
|
+
<input type="checkbox" data-sub-isa="${esc(sub)}" data-family="${esc(family)}" ${(enableAllIsas || enabledSubs.has(sub)) ? "checked" : ""}>
|
|
1018
|
+
${esc(sub)}
|
|
1019
|
+
</label>`).join("")}
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>`];
|
|
1022
|
+
}).join("");
|
|
1023
|
+
|
|
1024
|
+
for (const cb of isaFamiliesNode.querySelectorAll("input[data-family]")) {
|
|
1025
|
+
cb.addEventListener("change", () => {
|
|
1026
|
+
const family = cb.dataset.family;
|
|
1027
|
+
ensureManualIsaState();
|
|
1028
|
+
if (cb.checked) enabledIsas.add(family); else enabledIsas.delete(family);
|
|
1029
|
+
visibleSet = null;
|
|
1030
|
+
scheduleFilterRender();
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
for (const btn of isaSubgroupsNode.querySelectorAll("button[data-group-family]")) {
|
|
1035
|
+
btn.addEventListener("click", () => {
|
|
1036
|
+
const family = btn.dataset.groupFamily;
|
|
1037
|
+
ensureManualIsaState();
|
|
1038
|
+
const familyActive = enabledIsas.has(family);
|
|
1039
|
+
if (familyActive) {
|
|
1040
|
+
enabledIsas.delete(family);
|
|
1041
|
+
enabledSubIsas.set(family, new Set());
|
|
1042
|
+
} else {
|
|
1043
|
+
enabledIsas.add(family);
|
|
1044
|
+
enabledSubIsas.set(family, defaultSubsForFamily(family));
|
|
1045
|
+
}
|
|
1046
|
+
visibleSet = null;
|
|
1047
|
+
scheduleFilterRender();
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
for (const cb of isaSubgroupsNode.querySelectorAll("input[data-sub-isa]")) {
|
|
1052
|
+
cb.addEventListener("change", () => {
|
|
1053
|
+
const family = cb.dataset.family;
|
|
1054
|
+
const subIsa = cb.dataset.subIsa;
|
|
1055
|
+
ensureManualIsaState();
|
|
1056
|
+
enabledIsas.add(family);
|
|
1057
|
+
const familySubs = new Set(enabledSubIsas.get(family) || []);
|
|
1058
|
+
if (cb.checked) familySubs.add(subIsa); else familySubs.delete(subIsa);
|
|
1059
|
+
enabledSubIsas.set(family, familySubs);
|
|
1060
|
+
if (!familySubs.size) enabledIsas.delete(family);
|
|
1061
|
+
visibleSet = null;
|
|
1062
|
+
scheduleFilterRender();
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
updateIsaSummary();
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function applyIsaPreset(mode) {
|
|
1069
|
+
const preset = ARCH_PRESETS[mode];
|
|
1070
|
+
if (!preset) return;
|
|
1071
|
+
|
|
1072
|
+
if (mode === "all") {
|
|
1073
|
+
enableAllIsas = true;
|
|
1074
|
+
enabledIsas = new Set(availableIsas);
|
|
1075
|
+
initEnabledSubIsas();
|
|
1076
|
+
} else {
|
|
1077
|
+
enableAllIsas = false;
|
|
1078
|
+
enabledIsas = new Set((preset.families || []).filter(v => availableIsas.includes(v)));
|
|
1079
|
+
// Apply preset subs per family (intersect with FAMILY_SUB_ORDER).
|
|
1080
|
+
enabledSubIsas = new Map();
|
|
1081
|
+
const presetSubs = new Set(preset.subs || []);
|
|
1082
|
+
for (const family of availableIsas) {
|
|
1083
|
+
const subs = FAMILY_SUB_ORDER[family];
|
|
1084
|
+
if (!subs) continue;
|
|
1085
|
+
const keep = new Set(subs.filter(s => presetSubs.has(s)));
|
|
1086
|
+
enabledSubIsas.set(family, keep);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// arm_arch facet
|
|
1091
|
+
enabledArmArch = preset.arm_arch ? new Set(preset.arm_arch) : null;
|
|
1092
|
+
|
|
1093
|
+
// kind facet
|
|
1094
|
+
enabledKinds.clear();
|
|
1095
|
+
for (const k of (preset.kind || ["intrinsic", "instruction"])) enabledKinds.add(k);
|
|
1096
|
+
// Reflect kind state in the checkboxes.
|
|
1097
|
+
for (const cb of document.querySelectorAll('#kind-bar input[data-kind]')) {
|
|
1098
|
+
cb.checked = enabledKinds.has(cb.dataset.kind);
|
|
1099
|
+
const chip = cb.closest(".isa-chip");
|
|
1100
|
+
if (chip) chip.classList.toggle("active", cb.checked);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
visibleSet = null;
|
|
1104
|
+
scheduleFilterRender();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/* ── Results rendering ────────────────────────────────────────────── */
|
|
1108
|
+
function scheduleFilterRender() {
|
|
1109
|
+
if (filterRenderScheduled) return;
|
|
1110
|
+
filterRenderScheduled = true;
|
|
1111
|
+
requestAnimationFrame(() => {
|
|
1112
|
+
filterRenderScheduled = false;
|
|
1113
|
+
rebuildVisibleSet();
|
|
1114
|
+
if (typeof updateIsaSummary === "function") updateIsaSummary();
|
|
1115
|
+
if (typeof updateCategorySummary === "function") updateCategorySummary();
|
|
1116
|
+
renderIsaFilters();
|
|
1117
|
+
if (typeof renderCategoryFilters === "function") renderCategoryFilters();
|
|
1118
|
+
renderResults();
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function renderResults() {
|
|
1123
|
+
const query = queryInput.value.trim();
|
|
1124
|
+
if (!catalog) return;
|
|
1125
|
+
if (visibleSet === null) rebuildVisibleSet();
|
|
1126
|
+
const visible = searchEntries.filter((_, i) => visibleSet.has(i));
|
|
1127
|
+
|
|
1128
|
+
if (!query) {
|
|
1129
|
+
resultPool = visible;
|
|
1130
|
+
} else {
|
|
1131
|
+
const cids = candidateIndexes(query);
|
|
1132
|
+
const pool = cids == null ? visible : cids.filter(i => visibleSet.has(i)).map(i => searchEntries[i]);
|
|
1133
|
+
resultPool = pool
|
|
1134
|
+
.map(e => ({...e, score: rankEntry(query, e)}))
|
|
1135
|
+
.filter(e => Number.isFinite(e.score) && e.score >= 35)
|
|
1136
|
+
.sort((a, b) =>
|
|
1137
|
+
b.score - a.score
|
|
1138
|
+
|| (a.kind !== b.kind ? (a.kind === "instruction" ? -1 : 1) : 0)
|
|
1139
|
+
|| (() => { const [aa,ab] = isaSortKey(a.item.display_isa_tokens || a.item.isa); const [ba,bb] = isaSortKey(b.item.display_isa_tokens || b.item.isa); return aa-ba||ab-bb; })()
|
|
1140
|
+
|| naturalCmp(a.title, b.title)
|
|
1141
|
+
|| a.title.length - b.title.length
|
|
1142
|
+
)
|
|
1143
|
+
.slice(0, 5000);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
renderedCount = 0;
|
|
1147
|
+
virtualRange = { start: -1, end: -1 };
|
|
1148
|
+
resultsNode.scrollTop = 0;
|
|
1149
|
+
syncResultsCount(query);
|
|
1150
|
+
renderVisibleResults(true);
|
|
1151
|
+
prefetchIntrinsicChunks(resultPool.slice(0, 16));
|
|
1152
|
+
|
|
1153
|
+
// Auto-select
|
|
1154
|
+
const fromHash = decodeURIComponent(location.hash.replace(/^#/, ""));
|
|
1155
|
+
const selected = resultPool.find(e => e.key === activeKey)
|
|
1156
|
+
|| resultPool.find(e => e.key === fromHash)
|
|
1157
|
+
|| resultPool[0];
|
|
1158
|
+
if (selected) renderDetail(selected);
|
|
1159
|
+
else {
|
|
1160
|
+
detailNode.innerHTML = "";
|
|
1161
|
+
detailEmpty.style.display = "";
|
|
1162
|
+
detailEmpty.textContent = query ? `No results for "${query}".` : "Select a result or search for an intrinsic / instruction.";
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function scheduleRender() {
|
|
1167
|
+
if (renderTimer) clearTimeout(renderTimer);
|
|
1168
|
+
renderTimer = setTimeout(() => { renderTimer = null; renderResults(); }, 90);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/* ── Keyboard navigation ──────────────────────────────────────────── */
|
|
1172
|
+
function focusResult(idx) {
|
|
1173
|
+
if (idx < 0 || idx >= resultPool.length) return;
|
|
1174
|
+
focusedIndex = idx;
|
|
1175
|
+
// Scroll target row into the viewport, leaving a row's worth of margin
|
|
1176
|
+
// on either edge so it's not clipped.
|
|
1177
|
+
const rowTop = idx * ROW_HEIGHT_PX;
|
|
1178
|
+
const viewTop = resultsNode.scrollTop;
|
|
1179
|
+
const viewBottom = viewTop + resultsNode.clientHeight;
|
|
1180
|
+
if (rowTop < viewTop) resultsNode.scrollTop = rowTop;
|
|
1181
|
+
else if (rowTop + ROW_HEIGHT_PX > viewBottom) resultsNode.scrollTop = rowTop - resultsNode.clientHeight + ROW_HEIGHT_PX;
|
|
1182
|
+
renderVisibleResults(false);
|
|
1183
|
+
for (const n of resultsNode.querySelectorAll(".result")) {
|
|
1184
|
+
n.classList.toggle("focused", parseInt(n.dataset.index) === idx);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function selectFocused() {
|
|
1189
|
+
if (focusedIndex >= 0 && focusedIndex < resultPool.length) {
|
|
1190
|
+
renderDetail(resultPool[focusedIndex]);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
resultsNode.addEventListener("click", (e) => {
|
|
1195
|
+
const node = e.target.closest(".result");
|
|
1196
|
+
if (!node) return;
|
|
1197
|
+
const idx = parseInt(node.dataset.index);
|
|
1198
|
+
focusedIndex = idx;
|
|
1199
|
+
renderDetail(resultPool[idx]);
|
|
1200
|
+
focusResult(idx);
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
resultsNode.addEventListener("scroll", () => {
|
|
1204
|
+
if (loadMoreScheduled) return;
|
|
1205
|
+
loadMoreScheduled = true;
|
|
1206
|
+
requestAnimationFrame(() => {
|
|
1207
|
+
loadMoreScheduled = false;
|
|
1208
|
+
maybeLoadMoreResults();
|
|
1209
|
+
});
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
function refineSearchFromResultsKey(key) {
|
|
1213
|
+
if (key === "Backspace") {
|
|
1214
|
+
if (!queryInput.value) return false;
|
|
1215
|
+
queryInput.focus();
|
|
1216
|
+
queryInput.value = queryInput.value.slice(0, -1);
|
|
1217
|
+
scheduleRender();
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
if (key.length !== 1) return false;
|
|
1221
|
+
queryInput.focus();
|
|
1222
|
+
queryInput.value += key;
|
|
1223
|
+
scheduleRender();
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
document.addEventListener("keydown", (e) => {
|
|
1228
|
+
const tag = document.activeElement?.tagName;
|
|
1229
|
+
const inInput = tag === "INPUT" || tag === "TEXTAREA";
|
|
1230
|
+
|
|
1231
|
+
// Global: ? for shortcuts
|
|
1232
|
+
if (e.key === "?" && !inInput) {
|
|
1233
|
+
e.preventDefault();
|
|
1234
|
+
shortcutsOverlay.classList.toggle("hidden");
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Escape: close overlays, blur
|
|
1239
|
+
if (e.key === "Escape") {
|
|
1240
|
+
if (!shortcutsOverlay.classList.contains("hidden")) {
|
|
1241
|
+
shortcutsOverlay.classList.add("hidden");
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (!isaPanel.classList.contains("hidden")) {
|
|
1245
|
+
isaPanel.classList.add("hidden");
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (inInput) { queryInput.blur(); return; }
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Ctrl+K: focus search (works everywhere)
|
|
1253
|
+
if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
|
|
1254
|
+
e.preventDefault();
|
|
1255
|
+
queryInput.focus();
|
|
1256
|
+
queryInput.select();
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// /: focus search (when not in input)
|
|
1261
|
+
if (e.key === "/" && !inInput) {
|
|
1262
|
+
e.preventDefault();
|
|
1263
|
+
queryInput.focus();
|
|
1264
|
+
queryInput.select();
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// In search input: arrow keys navigate results directly
|
|
1269
|
+
if (inInput && document.activeElement === queryInput) {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if (
|
|
1274
|
+
(document.activeElement === resultsNode || document.activeElement === detailNode) &&
|
|
1275
|
+
!e.ctrlKey && !e.metaKey && !e.altKey &&
|
|
1276
|
+
refineSearchFromResultsKey(e.key)
|
|
1277
|
+
) {
|
|
1278
|
+
e.preventDefault();
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if (inInput) return;
|
|
1283
|
+
|
|
1284
|
+
// d: toggle dark mode
|
|
1285
|
+
if (e.key === "d") { toggleTheme(); return; }
|
|
1286
|
+
|
|
1287
|
+
// f: toggle ISA filter
|
|
1288
|
+
if (e.key === "f") { isaPanel.classList.toggle("hidden"); return; }
|
|
1289
|
+
if (e.key === "g") { categoryPanel.classList.toggle("hidden"); return; }
|
|
1290
|
+
|
|
1291
|
+
if (e.shiftKey && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
1292
|
+
e.preventDefault();
|
|
1293
|
+
resultsNode.scrollBy({ top: e.key === "ArrowDown" ? 120 : -120, behavior: "smooth" });
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (e.shiftKey && (e.key === "ArrowRight" || e.key === "ArrowLeft")) {
|
|
1297
|
+
e.preventDefault();
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// j/k or arrows: navigate results
|
|
1302
|
+
if (e.key === "j" || e.key === "ArrowDown") {
|
|
1303
|
+
e.preventDefault();
|
|
1304
|
+
focusResult(Math.min(focusedIndex + 1, resultPool.length - 1));
|
|
1305
|
+
selectFocused();
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
if (e.key === "k" || e.key === "ArrowUp") {
|
|
1309
|
+
e.preventDefault();
|
|
1310
|
+
focusResult(Math.max(focusedIndex - 1, 0));
|
|
1311
|
+
selectFocused();
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Enter/l: select
|
|
1316
|
+
if (e.key === "Enter" || e.key === "l") { selectFocused(); return; }
|
|
1317
|
+
|
|
1318
|
+
// [ / ]: previous/next result (alternate to j/k)
|
|
1319
|
+
if (e.key === "]") {
|
|
1320
|
+
e.preventDefault();
|
|
1321
|
+
focusResult(Math.min(focusedIndex + 1, resultPool.length - 1));
|
|
1322
|
+
selectFocused();
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (e.key === "[") {
|
|
1326
|
+
e.preventDefault();
|
|
1327
|
+
focusResult(Math.max(focusedIndex - 1, 0));
|
|
1328
|
+
selectFocused();
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Home/End: jump to first/last result
|
|
1333
|
+
if (e.key === "Home") {
|
|
1334
|
+
e.preventDefault();
|
|
1335
|
+
focusResult(0);
|
|
1336
|
+
selectFocused();
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
if (e.key === "End") {
|
|
1340
|
+
e.preventDefault();
|
|
1341
|
+
focusResult(resultPool.length - 1);
|
|
1342
|
+
selectFocused();
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Tab/Shift+Tab: switch focus between results and detail panels
|
|
1347
|
+
if (e.key === "Tab") {
|
|
1348
|
+
e.preventDefault();
|
|
1349
|
+
if (e.shiftKey) resultsNode.focus();
|
|
1350
|
+
else detailNode.focus();
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// c: copy current intrinsic/instruction name to clipboard
|
|
1355
|
+
if (e.key === "c") {
|
|
1356
|
+
const copyBtn = detailNode.querySelector(".copy-btn");
|
|
1357
|
+
if (copyBtn) copyBtn.click();
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// s: toggle sidebar collapse (mobile)
|
|
1362
|
+
if (e.key === "s") {
|
|
1363
|
+
const panel = document.getElementById("results-panel");
|
|
1364
|
+
panel.classList.toggle("collapsed");
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// h: back to search
|
|
1369
|
+
if (e.key === "h") { queryInput.focus(); queryInput.select(); return; }
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
/* ── Copy button clicks ──────────────────────────────────────────── */
|
|
1373
|
+
detailNode.addEventListener("click", (e) => {
|
|
1374
|
+
const btn = e.target.closest(".copy-btn");
|
|
1375
|
+
if (!btn) return;
|
|
1376
|
+
e.preventDefault();
|
|
1377
|
+
const text = btn.dataset.copy;
|
|
1378
|
+
if (text) navigator.clipboard.writeText(text).then(() => {
|
|
1379
|
+
btn.textContent = "\u2713";
|
|
1380
|
+
setTimeout(() => { btn.textContent = "\u2398"; }, 1200);
|
|
1381
|
+
});
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
/* ── Cross-reference clicks ───────────────────────────────────────── */
|
|
1385
|
+
detailNode.addEventListener("click", (e) => {
|
|
1386
|
+
const link = e.target.closest("a.xref");
|
|
1387
|
+
if (!link) return;
|
|
1388
|
+
e.preventDefault();
|
|
1389
|
+
const key = link.dataset.key;
|
|
1390
|
+
const kind = link.dataset.kind;
|
|
1391
|
+
if (!key || !catalog) return;
|
|
1392
|
+
|
|
1393
|
+
let entry = null;
|
|
1394
|
+
if (kind === "intrinsic") {
|
|
1395
|
+
const item = catalog.intrByName[key];
|
|
1396
|
+
if (item) entry = { kind: "intrinsic", key: item.name, title: item.name, subtitle: item.subtitle || "", item, fields: item.search_fields || [] };
|
|
1397
|
+
} else {
|
|
1398
|
+
const item = catalog.instrByKey[key];
|
|
1399
|
+
if (item) entry = { kind: "instruction", key: item.key, title: item.display_key || item.key, subtitle: item.summary || "", item, fields: item.search_fields || [] };
|
|
1400
|
+
}
|
|
1401
|
+
if (entry) renderDetail(entry);
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
/* ── ISA panel toggle ─────────────────────────────────────────────── */
|
|
1405
|
+
$("isa-toggle").addEventListener("click", () => isaPanel.classList.toggle("hidden"));
|
|
1406
|
+
$("isa-default").addEventListener("click", () => applyIsaPreset("default"));
|
|
1407
|
+
if ($("isa-intel")) $("isa-intel").addEventListener("click", () => applyIsaPreset("intel"));
|
|
1408
|
+
if ($("isa-arm32")) $("isa-arm32").addEventListener("click", () => applyIsaPreset("arm32"));
|
|
1409
|
+
if ($("isa-arm64")) $("isa-arm64").addEventListener("click", () => applyIsaPreset("arm64"));
|
|
1410
|
+
if ($("isa-riscv")) $("isa-riscv").addEventListener("click", () => applyIsaPreset("riscv"));
|
|
1411
|
+
$("isa-none").addEventListener("click", () => applyIsaPreset("none"));
|
|
1412
|
+
$("isa-all").addEventListener("click", () => applyIsaPreset("all"));
|
|
1413
|
+
|
|
1414
|
+
/* Kind filter — intrinsics vs instructions/asm */
|
|
1415
|
+
for (const cb of document.querySelectorAll('#kind-bar input[data-kind]')) {
|
|
1416
|
+
cb.addEventListener("change", () => {
|
|
1417
|
+
const kind = cb.dataset.kind;
|
|
1418
|
+
if (cb.checked) enabledKinds.add(kind); else enabledKinds.delete(kind);
|
|
1419
|
+
cb.parentElement.classList.toggle("active", cb.checked);
|
|
1420
|
+
visibleSet = null;
|
|
1421
|
+
scheduleFilterRender();
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/* Perf-kind filter — measured vs modeled perf tables (persisted). */
|
|
1426
|
+
for (const cb of document.querySelectorAll('#kind-bar input[data-perf-kind]')) {
|
|
1427
|
+
cb.checked = enabledPerfKinds.has(cb.dataset.perfKind);
|
|
1428
|
+
cb.parentElement.classList.toggle("active", cb.checked);
|
|
1429
|
+
cb.addEventListener("change", () => {
|
|
1430
|
+
const kind = cb.dataset.perfKind;
|
|
1431
|
+
if (cb.checked) enabledPerfKinds.add(kind); else enabledPerfKinds.delete(kind);
|
|
1432
|
+
cb.parentElement.classList.toggle("active", cb.checked);
|
|
1433
|
+
persistPerfKinds();
|
|
1434
|
+
const selected = resultPool.find(e => e.key === activeKey);
|
|
1435
|
+
if (selected) renderDetail(selected);
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/* ── Category panel toggle + presets ──────────────────────────────── */
|
|
1440
|
+
if ($("category-toggle")) {
|
|
1441
|
+
$("category-toggle").addEventListener("click", () => categoryPanel.classList.toggle("hidden"));
|
|
1442
|
+
}
|
|
1443
|
+
if ($("category-all")) {
|
|
1444
|
+
$("category-all").addEventListener("click", () => {
|
|
1445
|
+
enabledCategories = null;
|
|
1446
|
+
visibleSet = null;
|
|
1447
|
+
scheduleFilterRender();
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
if ($("category-none")) {
|
|
1451
|
+
$("category-none").addEventListener("click", () => {
|
|
1452
|
+
enabledCategories = new Set();
|
|
1453
|
+
visibleSet = null;
|
|
1454
|
+
scheduleFilterRender();
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
$("close-shortcuts").addEventListener("click", () => shortcutsOverlay.classList.add("hidden"));
|
|
1458
|
+
themeToggle.addEventListener("click", toggleTheme);
|
|
1459
|
+
queryInput.addEventListener("input", scheduleRender);
|
|
1460
|
+
queryInput.addEventListener("keydown", (e) => {
|
|
1461
|
+
if (e.shiftKey && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
1462
|
+
e.preventDefault();
|
|
1463
|
+
e.stopPropagation();
|
|
1464
|
+
resultsNode.scrollBy({ top: e.key === "ArrowDown" ? 120 : -120, behavior: "smooth" });
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
if (e.shiftKey && (e.key === "ArrowRight" || e.key === "ArrowLeft")) {
|
|
1468
|
+
e.preventDefault();
|
|
1469
|
+
e.stopPropagation();
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
if (e.key === "ArrowDown") {
|
|
1473
|
+
e.preventDefault();
|
|
1474
|
+
e.stopPropagation();
|
|
1475
|
+
focusResult(Math.min(focusedIndex + 1, resultPool.length - 1));
|
|
1476
|
+
selectFocused();
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (e.key === "ArrowUp") {
|
|
1480
|
+
e.preventDefault();
|
|
1481
|
+
e.stopPropagation();
|
|
1482
|
+
focusResult(Math.max(focusedIndex - 1, 0));
|
|
1483
|
+
selectFocused();
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (e.key === "Enter") {
|
|
1487
|
+
e.preventDefault();
|
|
1488
|
+
e.stopPropagation();
|
|
1489
|
+
if (focusedIndex >= 0) selectFocused();
|
|
1490
|
+
else if (resultPool.length) {
|
|
1491
|
+
focusResult(0);
|
|
1492
|
+
selectFocused();
|
|
1493
|
+
} else {
|
|
1494
|
+
renderResults();
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
/* ── Hash navigation ──────────────────────────────────────────────── */
|
|
1500
|
+
window.addEventListener("hashchange", () => {
|
|
1501
|
+
const key = decodeURIComponent(location.hash.replace(/^#/, ""));
|
|
1502
|
+
if (!catalog || !key) return;
|
|
1503
|
+
const entry = resultPool.find(e => e.key === key)
|
|
1504
|
+
|| (catalog.intrByName[key] ? {kind: "intrinsic", key, title: key, subtitle: catalog.intrByName[key].subtitle || "", item: catalog.intrByName[key], fields: catalog.intrByName[key].search_fields || []} : null)
|
|
1505
|
+
|| (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);
|
|
1506
|
+
if (entry) renderDetail(entry);
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
/* ── Bootstrap ────────────────────────────────────────────────────── */
|
|
1510
|
+
Promise.all([
|
|
1511
|
+
fetchJson("search-index.json"),
|
|
1512
|
+
fetchJson("filter_spec.json").catch(() => null),
|
|
1513
|
+
fetchJson("build_stamp.json").catch(() => null),
|
|
1514
|
+
])
|
|
1515
|
+
.then(([data, spec, stamp]) => {
|
|
1516
|
+
catalog = data;
|
|
1517
|
+
// Prefer filter_spec.json (single source of truth) over embedded isa_config.
|
|
1518
|
+
const config = spec || data.isa_config || {};
|
|
1519
|
+
defaultEnabledIsas = new Set(config.default_enabled || [...defaultEnabledIsas]);
|
|
1520
|
+
FAMILY_SUB_ORDER = config.family_sub_order || {};
|
|
1521
|
+
DEFAULT_SUBS = Object.fromEntries(Object.entries(config.default_subs || {}).map(([family, values]) => [family, new Set(values)]));
|
|
1522
|
+
isaFamilyOrder = config.family_order || {};
|
|
1523
|
+
availableCategories = Array.isArray(config.categories) ? config.categories : [];
|
|
1524
|
+
ARCH_PRESETS = config.presets && typeof config.presets === "object" ? config.presets : {};
|
|
1525
|
+
if (stamp && metaNode) {
|
|
1526
|
+
const stale = stamp.catalog_generated_at && data.generated_at && stamp.catalog_generated_at !== data.generated_at;
|
|
1527
|
+
const label = stamp.git_sha ? `build ${stamp.git_sha}` : `v${stamp.version || ""}`;
|
|
1528
|
+
metaNode.textContent = label;
|
|
1529
|
+
metaNode.title = `Catalog ${stamp.catalog_generated_at || "?"}${stale ? " (stale)" : ""}`;
|
|
1530
|
+
metaNode.dataset.stamp = label;
|
|
1531
|
+
if (stale) metaNode.classList.add("stale");
|
|
1532
|
+
}
|
|
1533
|
+
catalog.intrByName = Object.fromEntries(data.intrinsics.map(i => [i.name, i]));
|
|
1534
|
+
catalog.instrByKey = Object.fromEntries(data.instructions.map(i => [i.key, i]));
|
|
1535
|
+
catalog.instrByDisplayKey = Object.fromEntries(data.instructions.map(i => [i.display_key || i.key, i]));
|
|
1536
|
+
catalog.instrByMnem = Object.fromEntries(data.instructions.map(i => [i.mnemonic, i]));
|
|
1537
|
+
|
|
1538
|
+
searchEntries = [
|
|
1539
|
+
...data.intrinsics.map(i => ({
|
|
1540
|
+
kind: "intrinsic", key: i.name, title: i.name, subtitle: i.subtitle || i.description || "", item: i,
|
|
1541
|
+
fields: i.search_fields || [i.name, i.description || "", i.display_isa || displayIsa(i.isa), (i.instructions || []).join(" ")],
|
|
1542
|
+
})),
|
|
1543
|
+
...data.instructions.map(i => ({
|
|
1544
|
+
kind: "instruction", key: i.key, title: i.display_key || i.key, subtitle: i.summary || "", item: i,
|
|
1545
|
+
fields: i.search_fields || [i.display_mnemonic || i.mnemonic || "", i.display_form || i.form || "", i.summary || "", i.display_isa || displayIsa(i.isa)],
|
|
1546
|
+
})),
|
|
1547
|
+
];
|
|
1548
|
+
|
|
1549
|
+
buildSearchIndexes(searchEntries);
|
|
1550
|
+
|
|
1551
|
+
availableIsas = [...new Set(searchEntries.flatMap(e => e.item.isa_families || []))]
|
|
1552
|
+
.sort((a, b) => (isaFamilyOrder[a] ?? 99) - (isaFamilyOrder[b] ?? 99) || a.localeCompare(b));
|
|
1553
|
+
enabledIsas = new Set([...defaultEnabledIsas].filter(v => availableIsas.includes(v)));
|
|
1554
|
+
initEnabledSubIsas();
|
|
1555
|
+
|
|
1556
|
+
renderIsaFilters();
|
|
1557
|
+
renderCategoryFilters();
|
|
1558
|
+
updateCategorySummary();
|
|
1559
|
+
|
|
1560
|
+
// Apply ?preset=NAME from URL, if provided and recognised.
|
|
1561
|
+
try {
|
|
1562
|
+
const params = new URLSearchParams(location.search);
|
|
1563
|
+
const presetName = params.get("preset");
|
|
1564
|
+
if (presetName && ARCH_PRESETS[presetName]) {
|
|
1565
|
+
applyIsaPreset(presetName);
|
|
1566
|
+
}
|
|
1567
|
+
} catch (_) { /* ignore malformed URL */ }
|
|
1568
|
+
|
|
1569
|
+
rebuildVisibleSet();
|
|
1570
|
+
|
|
1571
|
+
// Build-stamp badge text is set above if a stamp is present; only fall
|
|
1572
|
+
// back to the catalog-size summary when no stamp was emitted.
|
|
1573
|
+
if (!metaNode.dataset.stamp) {
|
|
1574
|
+
metaNode.textContent = `${data.intrinsics.length} intrinsics \u00b7 ${data.instructions.length} instructions`;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const fromHash = decodeURIComponent(location.hash.replace(/^#/, ""));
|
|
1578
|
+
if (fromHash) queryInput.value = fromHash;
|
|
1579
|
+
renderResults();
|
|
1580
|
+
})
|
|
1581
|
+
.catch((err) => {
|
|
1582
|
+
// Fetch failure (CORS, 404, gzip misconfig) should not leave "Loading..." up forever.
|
|
1583
|
+
console.error("simdref: failed to load catalog", err);
|
|
1584
|
+
if (metaNode) metaNode.textContent = "catalog load failed";
|
|
1585
|
+
if (resultsCount) resultsCount.textContent = "Failed to load search index";
|
|
1586
|
+
if (detailEmpty) {
|
|
1587
|
+
detailEmpty.textContent = "Could not load the search index. Open the browser console for details.";
|
|
1588
|
+
detailEmpty.style.display = "";
|
|
1589
|
+
}
|
|
1590
|
+
});
|