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/storage.py ADDED
@@ -0,0 +1,504 @@
1
+ """Msgpack and SQLite persistence for the simdref catalog.
2
+
3
+ Stores a compact msgpack snapshot for reuse and a SQLite database with
4
+ FTS5 virtual tables for fast full-text search with BM25 ranking.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ import sqlite3
12
+ import sys
13
+ from itertools import islice
14
+ from pathlib import Path
15
+
16
+ import msgpack
17
+
18
+ from simdref.models import Catalog, InstructionRecord, IntrinsicRecord, SourceVersion
19
+
20
+
21
+ def derive_arm_arch(isa: list[str] | None, metadata: dict[str, str] | None) -> str | None:
22
+ """Classify an Arm intrinsic as A32/A64/BOTH from its supported_architectures.
23
+
24
+ Rules (matching the Part A preset design):
25
+ * contains A64 and (A32 or v7 or MVE) -> "BOTH"
26
+ * contains A64 only -> "A64"
27
+ * contains A32 or v7 or MVE only -> "A32"
28
+ * non-Arm rows or missing metadata -> None
29
+ """
30
+ supported = str((metadata or {}).get("supported_architectures") or "").strip()
31
+ if not supported:
32
+ # MVE-only Arm intrinsics may not carry supported_architectures;
33
+ # infer A32 from the ISA list in that case.
34
+ if isa and any(tok.upper() == "MVE" for tok in isa):
35
+ return "A32"
36
+ return None
37
+ upper = supported.upper()
38
+ has_a64 = "A64" in upper
39
+ has_a32 = ("A32" in upper) or ("V7" in upper) or ("MVE" in upper)
40
+ if has_a64 and has_a32:
41
+ return "BOTH"
42
+ if has_a64:
43
+ return "A64"
44
+ if has_a32:
45
+ return "A32"
46
+ return None
47
+
48
+
49
+ PACKAGE_ROOT = Path(__file__).resolve().parent
50
+ REPO_ROOT = PACKAGE_ROOT.parents[1]
51
+
52
+
53
+ def _default_data_dir() -> Path:
54
+ """Return the platform-appropriate data directory for simdref.
55
+
56
+ Uses repo-relative paths for editable/dev installs, and a
57
+ platform-appropriate user data directory for wheel installs.
58
+ """
59
+ # Dev install: pyproject.toml next to src/simdref/
60
+ if (REPO_ROOT / "pyproject.toml").exists() and (REPO_ROOT / "src" / "simdref").is_dir():
61
+ return REPO_ROOT / "data" / "derived"
62
+
63
+ # Installed: use platform-appropriate data dir
64
+ if sys.platform == "win32":
65
+ base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
66
+ elif sys.platform == "darwin":
67
+ base = Path.home() / "Library" / "Application Support"
68
+ else:
69
+ base = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
70
+ return base / "simdref"
71
+
72
+
73
+ DATA_DIR = _default_data_dir()
74
+ _is_dev_install = DATA_DIR == REPO_ROOT / "data" / "derived"
75
+
76
+ if _is_dev_install:
77
+ WEB_DIR = REPO_ROOT / "web"
78
+ DEFAULT_MAN_DIR = REPO_ROOT / "share" / "man"
79
+ else:
80
+ WEB_DIR = DATA_DIR / "web"
81
+ DEFAULT_MAN_DIR = DATA_DIR / "man"
82
+
83
+ CATALOG_PATH = DATA_DIR / "catalog.msgpack"
84
+ SQLITE_PATH = DATA_DIR / "catalog.db"
85
+ SQLITE_SCHEMA_VERSION = "11"
86
+ FTS_TOKEN_RE = re.compile(r"[A-Za-z0-9]+")
87
+ SQLITE_INSERT_BATCH_SIZE = 512
88
+
89
+
90
+ def ensure_dir(path: Path) -> None:
91
+ path.mkdir(parents=True, exist_ok=True)
92
+
93
+
94
+ def load_catalog(path: Path = CATALOG_PATH) -> Catalog:
95
+ payload = msgpack.unpackb(path.read_bytes(), raw=False)
96
+ return Catalog.from_dict(payload)
97
+
98
+
99
+ def save_catalog(catalog: Catalog, path: Path = CATALOG_PATH) -> None:
100
+ ensure_dir(path.parent)
101
+ packer = msgpack.Packer(use_bin_type=True)
102
+ with path.open("wb") as fh:
103
+ fh.write(packer.pack_map_header(4))
104
+ fh.write(packer.pack("intrinsics"))
105
+ fh.write(packer.pack_array_header(len(catalog.intrinsics)))
106
+ for record in catalog.intrinsics:
107
+ fh.write(packer.pack(record.to_dict()))
108
+ fh.write(packer.pack("instructions"))
109
+ fh.write(packer.pack_array_header(len(catalog.instructions)))
110
+ for record in catalog.instructions:
111
+ fh.write(packer.pack(record.to_dict()))
112
+ fh.write(packer.pack("sources"))
113
+ fh.write(packer.pack_array_header(len(catalog.sources)))
114
+ for source in catalog.sources:
115
+ fh.write(packer.pack(source.to_dict()))
116
+ fh.write(packer.pack("generated_at"))
117
+ fh.write(packer.pack(catalog.generated_at))
118
+
119
+
120
+ def open_db(path: Path = SQLITE_PATH) -> sqlite3.Connection:
121
+ conn = sqlite3.connect(path)
122
+ conn.row_factory = sqlite3.Row
123
+ return conn
124
+
125
+
126
+ def sqlite_schema_is_current(path: Path = SQLITE_PATH) -> bool:
127
+ if not path.exists():
128
+ return False
129
+ conn = sqlite3.connect(path)
130
+ try:
131
+ meta = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='meta'").fetchone()
132
+ if meta is None:
133
+ return False
134
+ row = conn.execute("SELECT value FROM meta WHERE key = 'schema_version'").fetchone()
135
+ if row is None or row[0] != SQLITE_SCHEMA_VERSION:
136
+ return False
137
+ expected_columns = {"id", "name", "architecture", "signature", "description", "header", "isa", "category", "subcategory", "arm_arch", "payload"}
138
+ actual_columns = {item[1] for item in conn.execute("PRAGMA table_info(intrinsics_data)").fetchall()}
139
+ if expected_columns != actual_columns:
140
+ return False
141
+ expected_instruction_columns = {"db_key", "key", "architecture", "mnemonic", "form", "summary", "isa", "category", "payload"}
142
+ actual_instruction_columns = {item[1] for item in conn.execute("PRAGMA table_info(instructions_data)").fetchall()}
143
+ if expected_instruction_columns != actual_instruction_columns:
144
+ return False
145
+ instr_indexes = {item[1] for item in conn.execute("PRAGMA index_list(instructions_data)").fetchall()}
146
+ if "idx_instruction_category" not in instr_indexes:
147
+ return False
148
+ intr_indexes = {item[1] for item in conn.execute("PRAGMA index_list(intrinsics_data)").fetchall()}
149
+ return "idx_intrinsic_arm_arch" in intr_indexes
150
+ except sqlite3.Error:
151
+ return False
152
+ finally:
153
+ conn.close()
154
+
155
+
156
+ _ALPHA_NUM_SPLIT = re.compile(r"[a-zA-Z]+|[0-9]+")
157
+
158
+
159
+ def _tokenize_name(name: str) -> str:
160
+ """Split alpha/numeric boundaries for better FTS matching.
161
+
162
+ _mm256_add_epi32 → mm 256 add epi 32
163
+ VADDPS (YMM, YMM, YMM) → vaddps ymm ymm ymm
164
+ """
165
+ return " ".join(_ALPHA_NUM_SPLIT.findall(name)).lower()
166
+
167
+
168
+ def _batched(items, size: int = SQLITE_INSERT_BATCH_SIZE):
169
+ iterator = iter(items)
170
+ while True:
171
+ batch = list(islice(iterator, size))
172
+ if not batch:
173
+ break
174
+ yield batch
175
+
176
+
177
+ def build_sqlite(catalog: Catalog, path: Path = SQLITE_PATH) -> None:
178
+ ensure_dir(path.parent)
179
+ if path.exists():
180
+ path.unlink()
181
+ conn = sqlite3.connect(path)
182
+ cur = conn.cursor()
183
+ cur.executescript(
184
+ """
185
+ PRAGMA journal_mode=WAL;
186
+ CREATE TABLE meta (
187
+ key TEXT PRIMARY KEY,
188
+ value TEXT NOT NULL
189
+ );
190
+ CREATE TABLE sources (
191
+ source TEXT PRIMARY KEY,
192
+ payload BLOB NOT NULL
193
+ );
194
+ CREATE TABLE intrinsics_data (
195
+ id INTEGER PRIMARY KEY,
196
+ name TEXT NOT NULL COLLATE NOCASE,
197
+ architecture TEXT NOT NULL,
198
+ signature TEXT NOT NULL,
199
+ description TEXT NOT NULL,
200
+ header TEXT NOT NULL,
201
+ isa TEXT NOT NULL,
202
+ category TEXT NOT NULL,
203
+ subcategory TEXT NOT NULL DEFAULT '',
204
+ arm_arch TEXT,
205
+ payload BLOB NOT NULL
206
+ );
207
+ CREATE INDEX idx_intrinsic_name ON intrinsics_data (name);
208
+ CREATE INDEX idx_intrinsic_arm_arch ON intrinsics_data (arm_arch);
209
+ CREATE TABLE instructions_data (
210
+ db_key TEXT PRIMARY KEY COLLATE NOCASE,
211
+ key TEXT NOT NULL COLLATE NOCASE,
212
+ architecture TEXT NOT NULL,
213
+ mnemonic TEXT NOT NULL COLLATE NOCASE,
214
+ form TEXT NOT NULL,
215
+ summary TEXT NOT NULL,
216
+ isa TEXT NOT NULL,
217
+ category TEXT NOT NULL DEFAULT '',
218
+ payload BLOB NOT NULL
219
+ );
220
+ CREATE INDEX idx_instruction_key ON instructions_data (key);
221
+ CREATE INDEX idx_instruction_mnemonic ON instructions_data (mnemonic);
222
+ CREATE INDEX idx_instruction_category ON instructions_data (category);
223
+ CREATE VIRTUAL TABLE intrinsics_fts USING fts5(name, signature, description, header, isa, category, instructions, notes, aliases, summary, name_tokens);
224
+ CREATE VIRTUAL TABLE instructions_fts USING fts5(key, mnemonic, form, summary, isa, linked_intrinsics, aliases, key_tokens);
225
+ """
226
+ )
227
+ cur.execute("INSERT INTO meta VALUES (?, ?)", ("schema_version", SQLITE_SCHEMA_VERSION))
228
+ cur.execute("INSERT INTO meta VALUES (?, ?)", ("generated_at", catalog.generated_at))
229
+
230
+ # Sources
231
+ source_rows = (
232
+ (source.source, msgpack.packb(source.to_dict(), use_bin_type=True))
233
+ for source in catalog.sources
234
+ )
235
+ for batch in _batched(source_rows):
236
+ cur.executemany("INSERT INTO sources VALUES (?, ?)", batch)
237
+
238
+ # Build a mnemonic -> summary lookup from instructions for fast access
239
+ _instr_summary: dict[str, str] = {}
240
+ for irec in catalog.instructions:
241
+ if irec.mnemonic and irec.summary and irec.mnemonic not in _instr_summary:
242
+ _instr_summary[irec.mnemonic] = irec.summary
243
+
244
+ # Intrinsics data + FTS
245
+ intrinsics_data_batch = []
246
+ intrinsics_fts_batch = []
247
+ for record in catalog.intrinsics:
248
+ payload = msgpack.packb(record.to_dict(), use_bin_type=True)
249
+ intrinsics_data_batch.append((
250
+ record.name,
251
+ record.architecture,
252
+ record.signature,
253
+ record.description,
254
+ record.header,
255
+ " ".join(record.isa),
256
+ record.category,
257
+ record.subcategory,
258
+ derive_arm_arch(record.isa, record.metadata),
259
+ payload,
260
+ ))
261
+ instr_summary = ""
262
+ if record.instructions:
263
+ mnemonic = record.instructions[0].split("(")[0].split()[0].strip()
264
+ instr_summary = _instr_summary.get(mnemonic, "")
265
+ if not instr_summary and record.description:
266
+ instr_summary = record.description.split(".")[0] + "."
267
+ intrinsics_fts_batch.append((
268
+ record.name,
269
+ record.signature,
270
+ record.description,
271
+ record.header,
272
+ " ".join(record.isa),
273
+ record.category,
274
+ " ".join(record.instructions),
275
+ " ".join(record.notes),
276
+ " ".join(record.aliases),
277
+ instr_summary,
278
+ _tokenize_name(record.name),
279
+ ))
280
+ if len(intrinsics_data_batch) >= SQLITE_INSERT_BATCH_SIZE:
281
+ cur.executemany(
282
+ "INSERT INTO intrinsics_data (name, architecture, signature, description, header, isa, category, subcategory, arm_arch, payload) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
283
+ intrinsics_data_batch,
284
+ )
285
+ cur.executemany(
286
+ "INSERT INTO intrinsics_fts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
287
+ intrinsics_fts_batch,
288
+ )
289
+ intrinsics_data_batch.clear()
290
+ intrinsics_fts_batch.clear()
291
+ if intrinsics_data_batch:
292
+ cur.executemany(
293
+ "INSERT INTO intrinsics_data (name, architecture, signature, description, header, isa, category, subcategory, arm_arch, payload) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
294
+ intrinsics_data_batch,
295
+ )
296
+ cur.executemany(
297
+ "INSERT INTO intrinsics_fts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
298
+ intrinsics_fts_batch,
299
+ )
300
+
301
+ # Instructions data + FTS
302
+ instructions_data_batch = []
303
+ instructions_fts_batch = []
304
+ for record in catalog.instructions:
305
+ payload = msgpack.packb(record.to_dict(), use_bin_type=True)
306
+ instructions_data_batch.append((
307
+ record.db_key,
308
+ record.key,
309
+ record.architecture,
310
+ record.mnemonic,
311
+ record.form,
312
+ record.summary,
313
+ " ".join(record.isa),
314
+ record.metadata.get("category", "") if isinstance(record.metadata, dict) else "",
315
+ payload,
316
+ ))
317
+ instructions_fts_batch.append((
318
+ record.key,
319
+ record.mnemonic,
320
+ record.form,
321
+ record.summary,
322
+ " ".join(record.isa),
323
+ " ".join(record.linked_intrinsics),
324
+ " ".join(record.aliases),
325
+ _tokenize_name(record.key),
326
+ ))
327
+ if len(instructions_data_batch) >= SQLITE_INSERT_BATCH_SIZE:
328
+ cur.executemany(
329
+ "INSERT INTO instructions_data VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
330
+ instructions_data_batch,
331
+ )
332
+ cur.executemany(
333
+ "INSERT INTO instructions_fts VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
334
+ instructions_fts_batch,
335
+ )
336
+ instructions_data_batch.clear()
337
+ instructions_fts_batch.clear()
338
+ if instructions_data_batch:
339
+ cur.executemany(
340
+ "INSERT INTO instructions_data VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
341
+ instructions_data_batch,
342
+ )
343
+ cur.executemany(
344
+ "INSERT INTO instructions_fts VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
345
+ instructions_fts_batch,
346
+ )
347
+
348
+ conn.commit()
349
+ conn.close()
350
+
351
+
352
+ def load_sources_from_db(conn: sqlite3.Connection) -> list[SourceVersion]:
353
+ rows = conn.execute("SELECT payload FROM sources ORDER BY source").fetchall()
354
+ return [SourceVersion(**msgpack.unpackb(row["payload"], raw=False)) for row in rows]
355
+
356
+
357
+ def generated_at_from_db(conn: sqlite3.Connection) -> str:
358
+ row = conn.execute("SELECT value FROM meta WHERE key = 'generated_at'").fetchone()
359
+ return row["value"] if row else ""
360
+
361
+
362
+ def load_intrinsic_from_db(conn: sqlite3.Connection, name: str) -> IntrinsicRecord | None:
363
+ row = conn.execute(
364
+ "SELECT payload FROM intrinsics_data WHERE name = ? ORDER BY id LIMIT 1",
365
+ (name,),
366
+ ).fetchone()
367
+ if not row:
368
+ return None
369
+ return IntrinsicRecord(**msgpack.unpackb(row["payload"], raw=False))
370
+
371
+
372
+ def load_instruction_from_db(conn: sqlite3.Connection, key: str) -> InstructionRecord | None:
373
+ row = conn.execute(
374
+ """
375
+ SELECT payload
376
+ FROM instructions_data
377
+ WHERE db_key = ? OR key = ?
378
+ ORDER BY CASE WHEN db_key = ? THEN 0 ELSE 1 END, architecture, key
379
+ LIMIT 1
380
+ """,
381
+ (key, key, key),
382
+ ).fetchone()
383
+ if not row:
384
+ return None
385
+ return InstructionRecord(**msgpack.unpackb(row["payload"], raw=False))
386
+
387
+
388
+ def load_instructions_by_mnemonic_from_db(conn: sqlite3.Connection, mnemonic: str) -> list[InstructionRecord]:
389
+ rows = conn.execute("SELECT payload FROM instructions_data WHERE mnemonic = ? ORDER BY architecture, key", (mnemonic,)).fetchall()
390
+ return [InstructionRecord(**msgpack.unpackb(row["payload"], raw=False)) for row in rows]
391
+
392
+
393
+ def load_instructions_by_mnemonic_prefix_from_db(conn: sqlite3.Connection, prefix: str, limit: int = 400) -> list[InstructionRecord]:
394
+ rows = conn.execute(
395
+ """
396
+ SELECT payload
397
+ FROM instructions_data
398
+ WHERE mnemonic LIKE ? || '%'
399
+ ORDER BY mnemonic, architecture, key
400
+ LIMIT ?
401
+ """,
402
+ (prefix, limit),
403
+ ).fetchall()
404
+ return [InstructionRecord(**msgpack.unpackb(row["payload"], raw=False)) for row in rows]
405
+
406
+
407
+ def _fts_match_query(query: str) -> str:
408
+ tokens = [token.casefold() for token in FTS_TOKEN_RE.findall(query)]
409
+ return " AND ".join(f'"{token}"*' for token in tokens if token)
410
+
411
+
412
+ def _append_filter_clause(
413
+ base_sql: str,
414
+ table: str,
415
+ filter_spec,
416
+ enabled_families,
417
+ enabled_categories,
418
+ binds: list,
419
+ match_marker: str,
420
+ enabled_arm_arch=None,
421
+ ) -> str:
422
+ """Splice a FilterSpec WHERE fragment into the SQL query after the
423
+ FTS MATCH placeholder, so bind ordering stays correct.
424
+
425
+ ``match_marker`` must be the exact string that appears immediately after
426
+ the MATCH ``?`` placeholder in ``base_sql`` (e.g. a newline-preserving
427
+ pattern) — the helper inserts ``AND <clause>`` just after it.
428
+ """
429
+ if filter_spec is None:
430
+ return base_sql
431
+ clause, extra_binds = filter_spec.sql_predicate(
432
+ table,
433
+ enabled_families=enabled_families,
434
+ enabled_categories=enabled_categories,
435
+ enabled_arm_arch=enabled_arm_arch,
436
+ )
437
+ if not clause:
438
+ return base_sql
439
+ binds.extend(extra_binds)
440
+ if match_marker not in base_sql:
441
+ return base_sql
442
+ return base_sql.replace(match_marker, f"{match_marker} AND {clause} ", 1)
443
+
444
+
445
+ def search_intrinsic_candidates_from_db(
446
+ conn: sqlite3.Connection,
447
+ query: str,
448
+ limit: int = 200,
449
+ *,
450
+ filter_spec=None,
451
+ enabled_families=None,
452
+ enabled_categories=None,
453
+ enabled_arm_arch=None,
454
+ ) -> list[IntrinsicRecord]:
455
+ match_query = _fts_match_query(query)
456
+ if not match_query:
457
+ return []
458
+ binds: list = [match_query]
459
+ sql = """
460
+ SELECT intrinsics_data.payload
461
+ FROM intrinsics_fts
462
+ JOIN intrinsics_data ON intrinsics_data.id = intrinsics_fts.rowid
463
+ WHERE intrinsics_fts MATCH ?
464
+ ORDER BY bm25(intrinsics_fts), length(intrinsics_data.name), intrinsics_data.name
465
+ LIMIT ?
466
+ """
467
+ sql = _append_filter_clause(
468
+ sql, "intrinsics_data", filter_spec, enabled_families, enabled_categories, binds,
469
+ match_marker="intrinsics_fts MATCH ?",
470
+ enabled_arm_arch=enabled_arm_arch,
471
+ )
472
+ binds.append(limit)
473
+ rows = conn.execute(sql, binds).fetchall()
474
+ return [IntrinsicRecord(**msgpack.unpackb(row["payload"], raw=False)) for row in rows]
475
+
476
+
477
+ def search_instruction_candidates_from_db(
478
+ conn: sqlite3.Connection,
479
+ query: str,
480
+ limit: int = 200,
481
+ *,
482
+ filter_spec=None,
483
+ enabled_families=None,
484
+ enabled_categories=None,
485
+ ) -> list[InstructionRecord]:
486
+ match_query = _fts_match_query(query)
487
+ if not match_query:
488
+ return []
489
+ binds: list = [match_query]
490
+ sql = """
491
+ SELECT instructions_data.payload
492
+ FROM instructions_fts
493
+ JOIN instructions_data ON instructions_data.rowid = instructions_fts.rowid
494
+ WHERE instructions_fts MATCH ?
495
+ ORDER BY bm25(instructions_fts), length(instructions_data.key), instructions_data.key
496
+ LIMIT ?
497
+ """
498
+ sql = _append_filter_clause(
499
+ sql, "instructions_data", filter_spec, enabled_families, enabled_categories, binds,
500
+ match_marker="instructions_fts MATCH ?",
501
+ )
502
+ binds.append(limit)
503
+ rows = conn.execute(sql, binds).fetchall()
504
+ return [InstructionRecord(**msgpack.unpackb(row["payload"], raw=False)) for row in rows]
File without changes