1
0

update-keysyms-case-mappings.py 68 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070
  1. #!/usr/bin/env python3
  2. # This script creates the keysym case mappings in `src/keysym-case-mappings.c`.
  3. #
  4. # Inspired from: https://github.com/apankrat/notes/blob/3c551cb028595fd34046c5761fd12d1692576003/fast-case-conversion/README.md
  5. # NOTE: the following docstring is also used to document the resulting C file.
  6. """
  7. There are two kinds of keysyms to consider:
  8. • Legacy keysyms: their case mappings is located at `data/keysyms/list.yaml`.
  9. • Unicode keysyms: their case mappings come from the ICU library.
  10. These mappings would create huge lookup tables if done naively. Fortunately,
  11. we can observe that if we compute only the *difference* between a keysym and
  12. its corresponding case mapping, there are a lot of repetitions that can be
  13. efficiently compressed.
  14. The idea for the compression is, for each kind of keysyms:
  15. 1. Compute the deltas between the keysyms and their case mappings.
  16. 2. Split the delta array in chunks of a given size.
  17. 3. Rearrange the order of the chunks in order to optimize consecutive
  18. chunks overlap.
  19. 4. Create a data table with the reordered chunks and an index table that
  20. maps the original chunk index to its offset in the data table.
  21. Trivial example (chunk size: 4, from step 2):
  22. [1, 2, 3, 4, 2, 3, 4, 5, 0, 1, 2, 3] # source data
  23. -> [[1, 2, 3, 4], [2, 3, 4, 5], [0, 1, 2, 3]] # make chunks
  24. -> [[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]] # rearrange to have best overlaps
  25. -> {data: [0, 1, 2, 3, 4, 5], offsets: [1, 2, 0]} # overlap chunks & compute
  26. # their offsets
  27. Then we can retrieve the data from the original array at index i with the
  28. following formula:
  29. mask = (1 << chunk_size) - 1;
  30. original[i] = data[offsets[i >> chunk_size] + (i & mask)];
  31. Since the index array is itself quite repetitive with the real data, we apply
  32. the compression a second time to the offsets table.
  33. The complete algorithm optimizes the chunk sizes for both arrays in order to
  34. get the lowest total data size.
  35. There are 6 resulting arrays, 3 for each kind of keysyms:
  36. 1. The data array. Each item is either:
  37. • 0, if the keysym is not cased.
  38. • A delta to lower case.
  39. • A delta to upper case.
  40. • For some special cases, there are both a lower *and* an upper case
  41. mapping. The delta is identical in both cases.
  42. 2. The 1st offsets array, that provides offsets into the data array.
  43. 3. The 2nd offsets array, that provides offsets into the 1st index array.
  44. Finally, given the chunks sizes `cs_data` and `cs_offsets`:
  45. 1. We compute the corresponding masks:
  46. • `mask_data = (1 << cs_data) - 1` and
  47. • `mask_offsets = (1 << cs_offsets) - 1`.
  48. 2. We can retrieve the case mapping of a keysyms `ks` with the following
  49. formula:
  50. data[
  51. offsets1[
  52. offsets2[ks >> (cs_data + cs_offsets)] +
  53. ((ks >> cs_data) & mask_offsets)
  54. ] +
  55. (ks & mask_data)
  56. ];
  57. """
  58. from __future__ import annotations
  59. import argparse
  60. import ctypes
  61. import importlib.util
  62. import itertools
  63. import math
  64. import os
  65. import re
  66. import sys
  67. import textwrap
  68. import unicodedata
  69. from abc import ABCMeta, abstractmethod
  70. from collections import defaultdict
  71. from collections.abc import Callable
  72. from ctypes.util import find_library
  73. from dataclasses import dataclass
  74. from enum import Enum, unique
  75. from functools import cache, reduce
  76. from pathlib import Path
  77. from typing import (
  78. Any,
  79. ClassVar,
  80. Generator,
  81. Generic,
  82. Iterable,
  83. NewType,
  84. Protocol,
  85. Self,
  86. Sequence,
  87. TypeAlias,
  88. TypeVar,
  89. cast,
  90. )
  91. import jinja2
  92. import yaml
  93. assert sys.version_info >= (3, 12)
  94. try:
  95. import icu
  96. c = icu.Locale.createFromName("C")
  97. icu.Locale.setDefault(c)
  98. except ImportError:
  99. icu = None
  100. SCRIPT = Path(__file__)
  101. CodePoint = NewType("CodePoint", int)
  102. Keysym = NewType("Keysym", int)
  103. KeysymName = NewType("KeysymName", str)
  104. T = TypeVar("T")
  105. X = TypeVar("X", int, CodePoint, Keysym)
  106. ################################################################################
  107. # Configuration
  108. ################################################################################
  109. @dataclass
  110. class Config:
  111. check_error: bool
  112. verbose: bool
  113. ################################################################################
  114. # XKBCOMMON
  115. ################################################################################
  116. class XKBCOMMON:
  117. XKB_KEYSYM_MIN = Keysym(0)
  118. XKB_KEYSYM_MAX = Keysym(0x1FFFFFFF)
  119. XKB_KEYSYM_MIN_EXPLICIT = Keysym(0x00000000)
  120. XKB_KEYSYM_MAX_EXPLICIT = Keysym(0x1008FFB8)
  121. XKB_KEYSYM_UNICODE_OFFSET = Keysym(0x01000000)
  122. XKB_KEYSYM_UNICODE_MIN = Keysym(0x01000100)
  123. XKB_KEYSYM_UNICODE_MAX = Keysym(0x0110FFFF)
  124. XKB_KEY_NoSymbol = Keysym(0)
  125. xkb_keysym_t = ctypes.c_uint32
  126. XKB_KEYSYM_NO_FLAGS = 0
  127. def __init__(self) -> None:
  128. self._xkbcommon_path = os.environ.get("XKBCOMMON_LIB_PATH")
  129. if self._xkbcommon_path:
  130. self._xkbcommon_path = str(Path(self._xkbcommon_path).resolve())
  131. self._lib = ctypes.cdll.LoadLibrary(self._xkbcommon_path)
  132. else:
  133. self._xkbcommon_path = find_library("xkbcommon")
  134. if self._xkbcommon_path:
  135. self._lib = ctypes.cdll.LoadLibrary(self._xkbcommon_path)
  136. else:
  137. raise OSError("Cannot load libxbcommon")
  138. self._lib.xkb_keysym_to_lower.argtypes = [self.xkb_keysym_t]
  139. self._lib.xkb_keysym_to_lower.restype = self.xkb_keysym_t
  140. self._lib.xkb_keysym_to_upper.argtypes = [self.xkb_keysym_t]
  141. self._lib.xkb_keysym_to_upper.restype = self.xkb_keysym_t
  142. self._lib.xkb_keysym_to_utf8.argtypes = [
  143. self.xkb_keysym_t,
  144. ctypes.c_char_p,
  145. ctypes.c_size_t,
  146. ]
  147. self._lib.xkb_keysym_to_utf8.restype = ctypes.c_int
  148. self._lib.xkb_keysym_to_utf32.argtypes = [self.xkb_keysym_t]
  149. self._lib.xkb_keysym_to_utf32.restype = ctypes.c_uint32
  150. self._lib.xkb_keysym_from_name.argtypes = [ctypes.c_char_p, ctypes.c_int]
  151. self._lib.xkb_keysym_from_name.restype = self.xkb_keysym_t
  152. self._lib.xkb_utf32_to_keysym.argtypes = [ctypes.c_uint32]
  153. self._lib.xkb_utf32_to_keysym.restype = self.xkb_keysym_t
  154. def keysym_from_name(self, keysym_name: KeysymName) -> Keysym:
  155. return self._lib.xkb_keysym_from_name(
  156. keysym_name.encode("utf-8"), self.XKB_KEYSYM_NO_FLAGS
  157. )
  158. def keysym_from_cp(self, cp: CodePoint) -> Keysym:
  159. return self._lib.xkb_utf32_to_keysym(cp)
  160. def keysym_from_char(self, char: str) -> Keysym:
  161. return self._lib.xkb_utf32_to_keysym(ord(char))
  162. def keysym_to_lower(self, keysym: Keysym) -> Keysym:
  163. return self._lib.xkb_keysym_to_lower(keysym)
  164. def keysym_to_upper(self, keysym: Keysym) -> Keysym:
  165. return self._lib.xkb_keysym_to_upper(keysym)
  166. def keysym_to_str(self, keysym: Keysym) -> str:
  167. buf_len = 7
  168. buf = ctypes.create_string_buffer(buf_len)
  169. n = self._lib.xkb_keysym_to_utf8(keysym, buf, ctypes.c_size_t(buf_len))
  170. if n < 0:
  171. raise ValueError(f"Unsupported keysym: 0x{keysym:0>4X})")
  172. elif n >= buf_len:
  173. raise ValueError(f"Buffer is not big enough: expected at least {n}.")
  174. else:
  175. return buf.value.decode("utf-8")
  176. def keysym_code_point(self, keysym: Keysym) -> int:
  177. return self._lib.xkb_keysym_to_utf32(keysym)
  178. def iter_case_mappings(self, unicode: bool) -> Iterable[tuple[Keysym, Entry]]:
  179. for keysym in map(
  180. Keysym,
  181. range(self.XKB_KEYSYM_MIN_EXPLICIT, self.XKB_KEYSYM_MAX_EXPLICIT + 1),
  182. ):
  183. if (
  184. not unicode
  185. and self.XKB_KEYSYM_UNICODE_OFFSET
  186. <= keysym
  187. <= self.XKB_KEYSYM_UNICODE_MAX
  188. ) or not self.keysym_code_point(keysym):
  189. continue
  190. cp = self.keysym_code_point(keysym)
  191. yield (
  192. keysym,
  193. Entry(
  194. lower=self.keysym_to_lower(keysym) - keysym,
  195. upper=keysym - self.keysym_to_upper(keysym),
  196. # FIXME: should use xkbcommon API, but it’s not exposed
  197. is_lower=icu.Char.isULowercase(cp),
  198. is_upper=icu.Char.isUUppercase(cp) or icu.istitle(cp),
  199. ),
  200. )
  201. def case_mappings(self, unicode: bool) -> tuple[Entry, ...]:
  202. mappings = dict(self.iter_case_mappings(unicode))
  203. keysym_max = max(mappings)
  204. return tuple(
  205. mappings.get(cast(Keysym, ks), Entry.zeros())
  206. for ks in range(0, keysym_max + 1)
  207. )
  208. xkbcommon = XKBCOMMON()
  209. def load_keysyms(path: Path, unicode: bool) -> tuple[Entry, ...]:
  210. with path.open("rt", encoding="utf-8") as fd:
  211. keysyms = {
  212. ks: entry
  213. for ks, data in yaml.safe_load(fd).items()
  214. if unicode
  215. or (
  216. ks < XKBCOMMON.XKB_KEYSYM_UNICODE_MIN
  217. or ks > XKBCOMMON.XKB_KEYSYM_UNICODE_MAX
  218. )
  219. if data.get("code point")
  220. if (
  221. entry := Entry(
  222. data.get("lower", ks) - ks,
  223. ks - data.get("upper", ks),
  224. is_lower=icu.Char.isULowercase(data.get("code point")),
  225. is_upper=icu.Char.isUUppercase(data.get("code point"))
  226. or icu.Char.istitle(data.get("code point")),
  227. )
  228. )
  229. }
  230. # Check either non-cased, upper or lower
  231. errors = []
  232. for ks, e in keysyms.items():
  233. if e.lower and e.upper and e.lower != e.upper:
  234. errors.append((ks, e))
  235. if errors:
  236. raise ValueError(errors)
  237. keysym_max = max(keysyms)
  238. return tuple(keysyms.get(ks, Entry.zeros()) for ks in range(0, keysym_max + 1))
  239. ################################################################################
  240. # Case mapping
  241. ################################################################################
  242. @dataclass(frozen=True, order=True)
  243. class Entry:
  244. """
  245. Case mapping deltas for a character or a keysym.
  246. """
  247. lower: int
  248. upper: int
  249. is_lower: bool
  250. is_upper: bool
  251. # [NOTE] Exceptions must be documented in `xkbcommon.h`.
  252. to_upper_exceptions: ClassVar[dict[str, str]] = {"ß": "ẞ"}
  253. "Upper mappings exceptions"
  254. @classmethod
  255. def zeros(cls) -> Self:
  256. return cls(lower=0, upper=0, is_lower=False, is_upper=False)
  257. def __bool__(self) -> bool:
  258. return any(self) or self.is_lower or self.is_upper
  259. def __str__(self) -> str:
  260. return str(tuple(self))
  261. def __iter__(self) -> Generator[int, None, None]:
  262. yield self.lower
  263. yield self.upper
  264. @classmethod
  265. def from_code_point(cls, cp: CodePoint) -> Self:
  266. return cls(
  267. lower=cls.lower_delta(cp),
  268. upper=cls.upper_delta(cp),
  269. is_lower=icu.Char.isULowercase(cp),
  270. is_upper=icu.Char.isUUppercase(cp) or icu.Char.istitle(cp),
  271. )
  272. @classmethod
  273. def lower_delta(cls, cp: CodePoint) -> int:
  274. return cls.to_lower_cp(cp) - cp
  275. @classmethod
  276. def upper_delta(cls, cp: CodePoint) -> int:
  277. return cp - cls.to_upper_cp(cp)
  278. @classmethod
  279. def to_upper_cp(cls, cp: CodePoint) -> CodePoint:
  280. if upper := cls.to_upper_exceptions.get(chr(cp)):
  281. return ord(upper)
  282. return icu.Char.toupper(cp)
  283. @staticmethod
  284. def to_lower_cp(cp: CodePoint) -> CodePoint:
  285. return icu.Char.tolower(cp)
  286. @classmethod
  287. def to_upper_char(cls, char: str) -> str:
  288. if upper := cls.to_upper_exceptions.get(char):
  289. return upper
  290. return icu.Char.toupper(char)
  291. @staticmethod
  292. def to_lower_char(char: str) -> str:
  293. return icu.Char.tolower(char)
  294. def to_lower(self, x: X) -> X:
  295. return x.__class__(x + self.lower)
  296. def to_upper(self, x: X) -> X:
  297. return x.__class__(x - self.upper)
  298. @dataclass
  299. class Deltas(Generic[T]):
  300. """
  301. Sequences of case mappings deltas
  302. """
  303. keysyms: tuple[T, ...]
  304. keysym_max: Keysym
  305. "Maximum keysym with a case mapping"
  306. unicode: tuple[T, ...]
  307. unicode_max: CodePoint
  308. "Maximum Unicode code point with a case mapping"
  309. class Entries(Deltas[Entry]):
  310. @classmethod
  311. def compute(
  312. cls,
  313. config: Config,
  314. path: Path | None = None,
  315. ) -> Self:
  316. # Keysyms
  317. if path:
  318. keysyms_deltas = load_keysyms(path, unicode=False)
  319. else:
  320. keysyms_deltas = xkbcommon.case_mappings(True)
  321. max_keysym = max(Keysym(ks) for ks, e in enumerate(keysyms_deltas) if e)
  322. assert max_keysym
  323. keysyms_deltas = keysyms_deltas[: max_keysym + 1]
  324. # Unicode
  325. unicode_deltas = tuple(
  326. Entry.from_code_point(CodePoint(cp)) for cp in range(0, sys.maxunicode + 1)
  327. )
  328. max_unicode = max((CodePoint(cp) for cp, d in enumerate(unicode_deltas) if d))
  329. assert max_unicode
  330. unicode_deltas = unicode_deltas[: max_unicode + 1]
  331. errors = []
  332. for n, e1 in enumerate(unicode_deltas):
  333. cp = CodePoint(n)
  334. # Check with legacy keysyms
  335. keysym = xkbcommon.keysym_from_cp(cp)
  336. if keysym <= max_keysym:
  337. e2 = keysyms_deltas[keysym]
  338. if e1 or e2:
  339. cls.check(
  340. config,
  341. "lower",
  342. cp,
  343. e1.to_lower(cp),
  344. keysym,
  345. e2.to_lower(keysym),
  346. )
  347. cls.check(
  348. config,
  349. "upper",
  350. cp,
  351. e1.to_upper(cp),
  352. keysym,
  353. e2.to_upper(keysym),
  354. )
  355. # Check character has either
  356. # • No case mapping
  357. # • Lower or an upper mapping
  358. # • Both, with the same delta
  359. if e1.lower and e1.upper and e1.lower != e1.upper:
  360. errors.append((cp, e1))
  361. if errors:
  362. raise ValueError(errors)
  363. return cls(
  364. keysyms=keysyms_deltas,
  365. keysym_max=max_keysym,
  366. unicode=unicode_deltas,
  367. unicode_max=max_unicode,
  368. )
  369. @classmethod
  370. def check(
  371. cls,
  372. config: Config,
  373. casing: str,
  374. cp: CodePoint,
  375. cpʹ: CodePoint,
  376. keysym: Keysym,
  377. keysymʹ: Keysym,
  378. ) -> None:
  379. char = chr(cp)
  380. expected = chr(cpʹ)
  381. got = xkbcommon.keysym_to_str(keysymʹ)
  382. data = (
  383. hex(keysym),
  384. hex(cp),
  385. char,
  386. hex(keysymʹ),
  387. got,
  388. expected,
  389. )
  390. if config.check_error:
  391. assert got == expected, data
  392. elif got != expected:
  393. print(
  394. f"Error: legacy keysym 0x{keysym:4>x} has incorrect {casing} mapping:",
  395. data,
  396. )
  397. @property
  398. def lower(self) -> Deltas[int]:
  399. return Deltas(
  400. keysyms=tuple(e.lower for e in self.keysyms),
  401. keysym_max=self.keysym_max,
  402. unicode=tuple(e.lower for e in self.unicode),
  403. unicode_max=self.unicode_max,
  404. )
  405. @property
  406. def upper(self) -> Deltas[int]:
  407. return Deltas(
  408. keysyms=tuple(e.upper for e in self.keysyms),
  409. keysym_max=self.keysym_max,
  410. unicode=tuple(e.upper for e in self.unicode),
  411. unicode_max=self.unicode_max,
  412. )
  413. @dataclass(frozen=True)
  414. class DeltasPair(Generic[T]):
  415. d1: tuple[T, ...]
  416. d2: tuple[T, ...]
  417. overlap: int
  418. def __contains__(self, x: tuple[T, ...]) -> bool:
  419. return (x == self.d1) or (x == self.d2)
  420. @classmethod
  421. def compute_pairs_overlaps(
  422. cls, deltas: Iterable[tuple[T, ...]]
  423. ) -> Generator[DeltasPair[T], None, None]:
  424. for d1, d2 in itertools.combinations(deltas, 2):
  425. if overlap := Overlap.compute_simple(d1, d2):
  426. yield DeltasPair(d1, d2, overlap)
  427. if overlap := Overlap.compute_simple(d2, d1):
  428. yield DeltasPair(d2, d1, overlap)
  429. def remove_from_pairs(
  430. self, pairs: list[DeltasPair[T]]
  431. ) -> Generator[DeltasPair[T], None, None]:
  432. for p in pairs:
  433. if p.d1 not in self and p.d2 not in self:
  434. yield p
  435. @classmethod
  436. def remove_chunk_from_pairs(
  437. cls, pairs: Iterable[DeltasPair[T]], chunk: tuple[T, ...]
  438. ) -> Generator[DeltasPair[T], None, None]:
  439. for p in pairs:
  440. if chunk not in p:
  441. yield p
  442. @classmethod
  443. def compute_best_pair(cls, pairs: Iterable[DeltasPair[T]]) -> DeltasPair[T]:
  444. return max(pairs, key=lambda p: p.overlap)
  445. ################################################################################
  446. # Overlap
  447. ################################################################################
  448. @dataclass
  449. class Overlap:
  450. "Overlap of two sequences"
  451. offset: int
  452. overlap: int
  453. @staticmethod
  454. @cache
  455. def compute_simple(s1: Sequence[T], s2: Sequence[T]) -> int:
  456. return max((n for n in range(0, len(s1) + 1) if s1[-n:] == s2[:n]), default=0)
  457. @classmethod
  458. @cache
  459. def compute(cls, s1: Sequence[T], s2: Sequence[T]) -> Self:
  460. l1 = len(s1)
  461. l2 = len(s2)
  462. return max(
  463. (
  464. cls(offset=start, overlap=overlap)
  465. for start in range(0, l1)
  466. if (end := min(start + l2, l1))
  467. and (overlap := end - start)
  468. and s1[start:end] == s2[:overlap]
  469. ),
  470. key=lambda x: x.overlap,
  471. default=cls(offset=l1, overlap=0),
  472. )
  473. @classmethod
  474. def test(cls) -> None:
  475. c1 = (1, 2, 3, 4)
  476. c2 = (2, 3)
  477. c3 = (3, 2, 1)
  478. c4 = (3, 4, 5)
  479. c5 = (1, 2, 4)
  480. overlap = cls.compute(c1, c2)
  481. assert overlap == cls(1, 2)
  482. overlap = cls.compute(c3, c1)
  483. assert overlap == cls(2, 1)
  484. overlap = cls.compute(c1, c4)
  485. assert overlap == cls(2, 2)
  486. overlap = cls.compute(c1, c5)
  487. assert overlap == cls(4, 0), overlap
  488. ################################################################################
  489. # Groups
  490. ################################################################################
  491. Groups: TypeAlias = dict[tuple[T, ...], list[int]]
  492. def generate_groups(block_size: int, data: Iterable[T]) -> Groups[T]:
  493. groups: defaultdict[tuple[T, ...], list[int]] = defaultdict(list)
  494. for n, d in enumerate(itertools.batched(data, block_size)):
  495. groups[d].append(n)
  496. return groups
  497. ################################################################################
  498. # Overlapped Sequences
  499. ################################################################################
  500. @dataclass
  501. class OverlappedSequences(Generic[T]):
  502. data: tuple[T, ...]
  503. offsets: dict[tuple[T, ...], int]
  504. def __bool__(self) -> bool:
  505. return bool(self.data)
  506. def __hash__(self) -> int:
  507. return hash((self.data, tuple(self.offsets)))
  508. def __add__(self, x: Any) -> Self:
  509. if isinstance(x, self.__class__):
  510. return self.extend(x)
  511. elif isinstance(x, tuple):
  512. return self.add(x)
  513. else:
  514. return NotImplemented
  515. def __iadd__(self, x: Any) -> Self:
  516. if isinstance(x, self.__class__):
  517. overlap = Overlap.compute(self.data, x.data)
  518. self.data = (
  519. self.data
  520. if overlap.offset <= len(self.data) - len(x.data)
  521. else self.data + x.data[overlap.overlap :]
  522. )
  523. for c, o in x.offsets.items():
  524. self.offsets[c] = overlap.offset + o
  525. return self
  526. elif isinstance(x, tuple):
  527. overlap = Overlap.compute(self.data, x)
  528. self.data = (
  529. self.data
  530. if overlap.offset <= len(self.data) - len(x)
  531. else self.data + x[overlap.overlap :]
  532. )
  533. self.offsets[x] = overlap.offset
  534. return self
  535. else:
  536. return NotImplemented
  537. @classmethod
  538. def from_singleton(cls, chunk: tuple[T, ...]) -> Self:
  539. return cls(data=chunk, offsets={chunk: 0})
  540. @classmethod
  541. def from_pair(cls, pair: DeltasPair[T]) -> Self:
  542. return cls(
  543. data=pair.d1 + pair.d2[pair.overlap :],
  544. offsets={
  545. pair.d1: 0,
  546. pair.d2: len(pair.d1) - pair.overlap,
  547. },
  548. )
  549. @classmethod
  550. def from_iterable(cls, ts: Iterable[tuple[T, ...]]) -> Self:
  551. return reduce(lambda s, t: s.add(t), ts, cls((), {}))
  552. @classmethod
  553. def from_ordered_iterable(cls, ts: Iterable[tuple[T, ...]]) -> Self:
  554. return reduce(lambda s, t: s.append(t), ts, cls((), {}))
  555. def extend(self, s: Self) -> Self:
  556. overlap: Overlap
  557. s1: Self
  558. s2: Self
  559. overlap, s1, s2 = max(
  560. (
  561. (Overlap.compute(self.data, s.data), self, s),
  562. (Overlap.compute(s.data, self.data), s, self),
  563. ),
  564. key=lambda x: x[0].overlap,
  565. )
  566. data = (
  567. s1.data
  568. if overlap.offset <= len(s1.data) - len(s2.data)
  569. else s1.data + s2.data[overlap.overlap :]
  570. )
  571. offsets = dict(s1.offsets)
  572. for c, o in s2.offsets.items():
  573. offsets[c] = overlap.offset + o
  574. return self.__class__(data=data, offsets=offsets)
  575. def add(self, chunk: tuple[T, ...]) -> Self:
  576. return self.extend(self.from_singleton(chunk))
  577. def append(self, chunk: tuple[T, ...]) -> Self:
  578. overlap = Overlap.compute(self.data, chunk)
  579. self.data = (
  580. self.data
  581. if overlap.offset <= len(self.data) - len(chunk)
  582. else self.data + chunk[overlap.overlap :]
  583. )
  584. self.offsets[chunk] = overlap.offset
  585. return self
  586. def insert(self, offset: int, overlap: int, chunk: tuple[T, ...]) -> None:
  587. chunk_length = len(chunk)
  588. if offset < 0:
  589. self.data = chunk[:-overlap] + self.data
  590. assert chunk == self.data[:chunk_length]
  591. for c in self.offsets:
  592. self.offsets[c] += -offset
  593. self.offsets[chunk] = 0
  594. elif offset <= len(self.data) - len(chunk):
  595. assert self.data[offset : offset + chunk_length] == chunk
  596. self.offsets[chunk] = offset
  597. else:
  598. self.data += chunk[overlap:]
  599. assert self.data[offset:] == chunk
  600. self.offsets[chunk] = offset
  601. def merge(self, offset: int, overlap: int, s: OverlappedSequences[T]) -> None:
  602. s_length = len(s.data)
  603. if offset < 0:
  604. self.data = s.data[:-overlap] + self.data
  605. assert s.data == self.data[:s_length]
  606. for c in self.offsets:
  607. self.offsets[c] += -offset
  608. self.offsets.update(s.offsets)
  609. elif offset <= len(self.data) - len(s.data):
  610. assert self.data[offset : offset + s_length] == s.data
  611. for c, offset2 in s.offsets.items():
  612. self.offsets[c] = offset2 + offset
  613. else:
  614. self.data += s.data[overlap:]
  615. assert self.data[offset:] == s.data
  616. for c, offset2 in s.offsets.items():
  617. self.offsets[c] = offset2 + offset
  618. def flatten_iter(self) -> Generator[tuple[T, ...], None, None]:
  619. for t, _ in sorted(self.offsets.items(), key=lambda x: x[1]):
  620. yield t
  621. def flatten(self) -> list[tuple[T, ...]]:
  622. return list(self.flatten_iter())
  623. def flatten_groups_iter(self) -> Generator[list[tuple[T, ...]], None, None]:
  624. i = self.flatten_iter()
  625. g = [next(i)]
  626. for t in i:
  627. if Overlap.compute(g[-1], t).overlap:
  628. g.append(t)
  629. else:
  630. yield g
  631. g = [t]
  632. yield g
  633. def split(self) -> list[OverlappedSequences[T]]:
  634. offsets = sorted(self.offsets.items(), key=lambda x: x[1])
  635. start = 0
  636. pending: dict[tuple[T, ...], int] = {}
  637. end = 0
  638. new: list[OverlappedSequences[T]] = []
  639. for d, i in offsets:
  640. # print(start, end, d, i)
  641. if end and i >= end:
  642. assert start < end
  643. new.append(
  644. OverlappedSequences(data=self.data[start:end], offsets=pending)
  645. )
  646. pending = {d: 0}
  647. start = i
  648. else:
  649. pending[d] = i - start
  650. end = max(end, i + len(d))
  651. assert start < end
  652. new.append(OverlappedSequences(data=self.data[start:end], offsets=pending))
  653. return new
  654. def total_overlap(self) -> int:
  655. return sum(len(d) for d in self.offsets) - len(self.data)
  656. class _OverlappedSequences(OverlappedSequences[int]):
  657. @classmethod
  658. def test(cls) -> None:
  659. c1: tuple[int, ...] = (1, 2, 3, 4)
  660. c2: tuple[int, ...] = (2, 3)
  661. c3: tuple[int, ...] = (3, 4, 5)
  662. c4: tuple[int, ...] = (1, 3)
  663. assert cls(c1, {}).add(c2) == cls(c1, {c2: 1})
  664. assert cls(c1, {}).add(c3) == cls(c1 + c3[2:], {c3: 2})
  665. assert cls(c1, {}).add(c4) == cls(c1 + c4, {c4: len(c1)})
  666. assert cls(c1, {}) + cls(c2, {c2: 0}) == cls(c1, {c2: 1})
  667. assert cls(c1, {}) + cls(c3, {c3: 0}) == cls(c1 + c3[2:], {c3: 2})
  668. assert cls(c1, {}) + cls(c4, {c4: 0}) == cls(c1 + c4, {c4: len(c1)})
  669. assert cls(c2, {c2: 0}) + cls(c1, {}) == cls(c1, {c2: 1})
  670. assert cls(c3, {c3: 0}) + cls(c1, {}) == cls(c1 + c3[2:], {c3: 2})
  671. assert cls(c4, {c4: 0}) + cls(c1, {}) == cls(c4 + c1, {c4: 0})
  672. c5 = (1, 2, 3, 4, 5, 6)
  673. c6 = (1,)
  674. c7 = (6,)
  675. s = cls(c5, {c6: 0, c2: 1, c3: 2, c7: 5})
  676. assert s.total_overlap() == 1, s.total_overlap()
  677. assert s.split() == [
  678. cls(c6, {c6: 0}),
  679. cls(c5[1:-1], {c2: 0, c3: 1}),
  680. cls(c7, {c7: 0}),
  681. ], s.split()
  682. cs = s.flatten()
  683. assert cs == [c6, c2, c3, c7], cs
  684. assert cls.from_iterable(cs) == s
  685. ################################################################################
  686. # Chunks compressor
  687. ################################################################################
  688. class Move(Protocol, Generic[T]):
  689. def __call__(
  690. self,
  691. pairs: list[DeltasPair[T]],
  692. remaining_chunks: set[tuple[T, ...]],
  693. overlapped_sequences: list[OverlappedSequences[T]],
  694. ) -> None: ...
  695. @dataclass
  696. class ChunksCompressor:
  697. verbose: bool
  698. @classmethod
  699. def _insert(
  700. cls, chunk: tuple[T, ...], index: int, offset: int, overlap: int
  701. ) -> Move[T]:
  702. def action(
  703. pairs: list[DeltasPair[T]],
  704. remaining_chunks: set[tuple[T, ...]],
  705. overlapped_sequences: list[OverlappedSequences[T]],
  706. ) -> None:
  707. remaining_chunks.remove(chunk)
  708. pairs[:] = DeltasPair.remove_chunk_from_pairs(pairs, chunk)
  709. s = overlapped_sequences[index]
  710. s.insert(offset, overlap, chunk)
  711. return action
  712. @classmethod
  713. def _merge(cls, index1: int, index2: int, offset: int, overlap: int) -> Move[T]:
  714. def action(
  715. pairs: list[DeltasPair[T]],
  716. remaining_chunks: set[tuple[T, ...]],
  717. overlapped_sequences: list[OverlappedSequences[T]],
  718. ) -> None:
  719. s1 = overlapped_sequences[index1]
  720. s2 = overlapped_sequences.pop(index2)
  721. s1.merge(offset, overlap, s2)
  722. return action
  723. @classmethod
  724. def _add_new_singleton(cls, chunk: tuple[T, ...]) -> Move[T]:
  725. def action(
  726. pairs: list[DeltasPair[T]],
  727. remaining_chunks: set[tuple[T, ...]],
  728. overlapped_sequences: list[OverlappedSequences[T]],
  729. ) -> None:
  730. remaining_chunks.remove(chunk)
  731. pairs[:] = DeltasPair.remove_chunk_from_pairs(pairs, chunk)
  732. overlapped_sequences.append(OverlappedSequences.from_singleton(chunk))
  733. return action
  734. @classmethod
  735. def _add_new_sequence(cls, pair: DeltasPair[T]) -> Move[T]:
  736. def action(
  737. pairs: list[DeltasPair[T]],
  738. remaining_chunks: set[tuple[T, ...]],
  739. overlapped_sequences: list[OverlappedSequences[T]],
  740. ) -> None:
  741. remaining_chunks.remove(pair.d1)
  742. remaining_chunks.remove(pair.d2)
  743. pairs[:] = pair.remove_from_pairs(pairs)
  744. assert pair not in pairs
  745. overlapped_sequences.append(OverlappedSequences.from_pair(pair))
  746. return action
  747. def compress(self, chunks: Iterable[tuple[T, ...]]) -> OverlappedSequences[T]:
  748. # Prepare data and offsets
  749. pairs = list(DeltasPair.compute_pairs_overlaps(chunks))
  750. remaining_chunks = set(chunks)
  751. if self.verbose:
  752. print(
  753. "Count of chunks:",
  754. len(remaining_chunks),
  755. )
  756. overlapped_sequences: list[OverlappedSequences[T]] = []
  757. best_move: Move[T] | None = None
  758. total_chunks = len(remaining_chunks)
  759. while remaining_chunks or (len(overlapped_sequences) > 1 and best_move):
  760. if self.verbose:
  761. print(
  762. f"# Remaining: {len(remaining_chunks)}/{total_chunks}",
  763. "Current pairs:",
  764. len(overlapped_sequences),
  765. )
  766. best_move = None
  767. best_overlap: int = 0
  768. # Try inserting
  769. for n, s in enumerate(overlapped_sequences):
  770. for d in remaining_chunks:
  771. # Try prepend
  772. overlap = Overlap.compute_simple(d, s.data)
  773. if overlap > best_overlap:
  774. best_overlap = overlap
  775. best_move = self._insert(d, n, overlap - len(d), overlap)
  776. if self.verbose:
  777. print(
  778. "Insert chunk (prepend)",
  779. best_overlap,
  780. len(overlapped_sequences),
  781. )
  782. # Try insert
  783. overlap_max = Overlap.compute(s.data, d)
  784. if overlap_max.overlap > best_overlap:
  785. best_overlap = overlap_max.overlap
  786. best_move = self._insert(
  787. d, n, overlap_max.offset, overlap_max.overlap
  788. )
  789. if self.verbose:
  790. print(
  791. "Insert chunk (append)",
  792. overlap_max,
  793. len(overlapped_sequences),
  794. )
  795. # Try merging
  796. for n1, n2 in itertools.permutations(range(len(overlapped_sequences)), 2):
  797. s1 = overlapped_sequences[n1]
  798. s2 = overlapped_sequences[n2]
  799. # Try insert
  800. overlap_max = Overlap.compute(s1.data, s2.data)
  801. if overlap_max.overlap > best_overlap:
  802. best_overlap = overlap_max.overlap
  803. best_move = self._merge(
  804. n1, n2, overlap_max.offset, overlap_max.overlap
  805. )
  806. if self.verbose:
  807. print(
  808. "Merge 2 sequences",
  809. overlap_max,
  810. len(overlapped_sequences),
  811. )
  812. # Take next best pair
  813. if (
  814. pairs
  815. and (best_pair := DeltasPair.compute_best_pair(pairs))
  816. and best_pair.overlap > best_overlap
  817. ):
  818. best_overlap = best_pair.overlap
  819. best_move = self._add_new_sequence(best_pair)
  820. if self.verbose:
  821. print("Add new sequence", best_overlap, len(overlapped_sequences))
  822. if not best_move:
  823. if self.verbose:
  824. print("No best move", len(overlapped_sequences))
  825. if remaining_chunks:
  826. best_move = self._add_new_singleton(min(remaining_chunks))
  827. if best_move:
  828. best_move(
  829. pairs=pairs,
  830. remaining_chunks=remaining_chunks,
  831. overlapped_sequences=overlapped_sequences,
  832. )
  833. assert len(overlapped_sequences) >= 1
  834. assert not remaining_chunks
  835. if (l := len(overlapped_sequences)) > 1:
  836. if self.verbose:
  837. print("Force merging remaining sequences", l)
  838. for n1, n2 in itertools.permutations(range(len(overlapped_sequences)), 2):
  839. s1 = overlapped_sequences[n1]
  840. s2 = overlapped_sequences[n2]
  841. overlap_max = Overlap.compute(s1.data, s2.data)
  842. assert overlap_max.overlap == 0, overlap_max
  843. for _ in range(1, len(overlapped_sequences)):
  844. move: Move[T] = self._merge(0, 1, len(overlapped_sequences[0].data), 0)
  845. move(
  846. pairs=pairs,
  847. remaining_chunks=remaining_chunks,
  848. overlapped_sequences=overlapped_sequences,
  849. )
  850. assert len(overlapped_sequences) == 1, overlapped_sequences
  851. s = overlapped_sequences[0]
  852. return s
  853. @classmethod
  854. def test_moves(cls) -> None:
  855. c1 = (1, 2, 3, 4, 5)
  856. c2 = (6, 1)
  857. c3 = (7, 6, 1)
  858. c4 = (3, 4)
  859. c5 = (5, 6, 7)
  860. c6 = (6, 1, 2)
  861. c7 = (8, 7, 6)
  862. chunks0 = {c1, c2, c3, c4, c5, c6, c7}
  863. chunks = set(chunks0)
  864. sequences: list[OverlappedSequences[int]] = []
  865. offsets: dict[tuple[int, ...], int]
  866. overlap_max = Overlap.compute((1, 2, 3), c1)
  867. assert overlap_max == Overlap(offset=0, overlap=3)
  868. pairs: list[DeltasPair[int]] = []
  869. move = ChunksCompressor._add_new_singleton(c1)
  870. move(pairs, chunks, sequences)
  871. offsets = {c1: 0}
  872. data: tuple[int, ...] = c1
  873. assert len(sequences) == 1
  874. s = sequences[0]
  875. assert s.data == data
  876. assert s.offsets == offsets
  877. overlap = Overlap.compute_simple(c2, s.data)
  878. assert overlap == 1
  879. overlap_max = Overlap.compute(c2, s.data)
  880. assert overlap_max.overlap == overlap
  881. move = ChunksCompressor._insert(
  882. chunk=c2, index=0, offset=-overlap, overlap=overlap
  883. )
  884. move(pairs, chunks, sequences)
  885. offsets[c1] = 1
  886. offsets[c2] = 0
  887. data = c2[:-overlap] + data
  888. assert len(sequences) == 1
  889. assert s.data == data
  890. assert s.offsets == offsets
  891. overlap = Overlap.compute_simple(c3, s.data)
  892. assert overlap == 2
  893. overlap_max = Overlap.compute(c3, s.data)
  894. assert overlap_max.overlap == overlap
  895. move = ChunksCompressor._insert(
  896. chunk=c3, index=0, offset=overlap - len(c3), overlap=overlap
  897. )
  898. move(pairs, chunks, sequences)
  899. offsets[c1] += 1
  900. offsets[c2] += 1
  901. offsets[c3] = 0
  902. data = c3[:-overlap] + data
  903. assert len(sequences) == 1
  904. assert s.data == data
  905. assert s.offsets == offsets
  906. overlap_max = Overlap.compute(s.data, c4)
  907. assert overlap_max.overlap == 2, (s.data, c4, overlap_max)
  908. assert overlap_max.offset == 4, (s.data, c4, overlap_max)
  909. move = ChunksCompressor._insert(
  910. chunk=c4, index=0, offset=overlap_max.offset, overlap=overlap_max.overlap
  911. )
  912. move(pairs, chunks, sequences)
  913. offsets[c4] = overlap_max.offset
  914. assert len(sequences) == 1
  915. assert s.data == data
  916. assert s.offsets == offsets
  917. overlap_max = Overlap.compute(s.data, c5)
  918. assert overlap_max.overlap == 1, (s.data, c5, overlap_max)
  919. assert overlap_max.offset == 6, (s.data, c5, overlap_max)
  920. move = ChunksCompressor._insert(
  921. chunk=c5, index=0, offset=overlap_max.offset, overlap=overlap_max.overlap
  922. )
  923. move(pairs, chunks, sequences)
  924. offsets[c5] = overlap_max.offset
  925. data += c5[overlap_max.overlap :]
  926. assert len(sequences) == 1
  927. assert s.data == data, (s.data, data)
  928. assert s.offsets == offsets
  929. overlap_max = Overlap.compute(s.data, c6)
  930. assert overlap_max.overlap == 3, (s.data, c6, overlap_max)
  931. assert overlap_max.offset == 1, (s.data, c6, overlap_max)
  932. move = ChunksCompressor._insert(
  933. chunk=c6, index=0, offset=overlap_max.offset, overlap=overlap_max.overlap
  934. )
  935. move(pairs, chunks, sequences)
  936. offsets[c6] = overlap_max.offset
  937. assert len(sequences) == 1
  938. assert s.data == data, (s.data, data)
  939. assert s.offsets == offsets
  940. overlap = Overlap.compute_simple(c7, s.data)
  941. assert overlap == 2
  942. overlap_max = Overlap.compute(c7, s.data)
  943. assert overlap_max.overlap == overlap
  944. move = ChunksCompressor._insert(
  945. chunk=c7, index=0, offset=overlap - len(c7), overlap=overlap
  946. )
  947. move(pairs, chunks, sequences)
  948. for c in offsets:
  949. offsets[c] += len(c7) - overlap
  950. offsets[c7] = 0
  951. data = c7[:-overlap] + data
  952. assert len(sequences) == 1
  953. assert s.data == data
  954. assert s.offsets == offsets
  955. for c in chunks0:
  956. offset = s.offsets[c]
  957. assert c == s.data[offset : offset + len(c)], (
  958. c,
  959. s.data[offset : offset + len(c)],
  960. )
  961. @classmethod
  962. def test_compression(cls) -> None:
  963. c1 = (1, 2, 3)
  964. c2 = (-1, 0, 1)
  965. c3 = (-2, -1, 0)
  966. c4 = (3, 4, 5)
  967. c5 = (0, 1, 2)
  968. c6 = (2, 3, 5)
  969. chunks = {c1, c2, c3, c4, c5, c6}
  970. compressor = cls(verbose=True)
  971. r = compressor.compress(chunks)
  972. assert set(r.offsets) == {c1, c2, c3, c4, c5, c6}
  973. for c in chunks:
  974. offset = r.offsets[c]
  975. assert c == r.data[offset : offset + len(c)], (
  976. c,
  977. r.data[offset : offset + len(c)],
  978. )
  979. ################################################################################
  980. # Stats
  981. ################################################################################
  982. @dataclass
  983. class Stats:
  984. data_length: int
  985. data_int_size: int
  986. data_overlap: int
  987. offsets1_length: int
  988. offsets1_int_size: int
  989. offsets2_length: int
  990. offsets2_int_size: int
  991. @property
  992. def data_size(self) -> int:
  993. return self.data_length * self.data_int_size
  994. @property
  995. def offsets1_size(self) -> int:
  996. return self.offsets1_length * self.offsets1_int_size
  997. @property
  998. def offsets2_size(self) -> int:
  999. return self.offsets2_length * self.offsets2_int_size
  1000. @property
  1001. def total(self) -> int:
  1002. return self.data_size + self.offsets1_size + self.offsets2_size
  1003. def _int_size(ts: Sequence[Iterable[int] | int], offset: int = 0) -> int:
  1004. assert ts
  1005. if isinstance(ts[0], int):
  1006. ts = cast(Sequence[int], ts)
  1007. min_delta: int = min(ts)
  1008. max_delta: int = max(ts)
  1009. else:
  1010. ts = cast(Sequence[Iterable[int]], ts)
  1011. min_delta = min(min(t) for t in ts)
  1012. max_delta = max(max(t) for t in ts)
  1013. if offset:
  1014. min_delta <<= offset
  1015. max_delta <<= offset
  1016. for n in range(3, 7):
  1017. size: int = 2**n
  1018. min_int = -(1 << (size - 1))
  1019. max_int = -min_int - 1
  1020. if min_int <= min_delta and max_delta <= max_int:
  1021. return size
  1022. else:
  1023. raise ValueError((min_delta, max_delta))
  1024. def uint_size(ts: Sequence[int]) -> int:
  1025. min_delta = min(ts)
  1026. if min_delta < 0:
  1027. raise ValueError(min_delta)
  1028. max_delta = max(ts)
  1029. for n in range(3, 7):
  1030. size: int = 2**n
  1031. max_int = (1 << size) - 1
  1032. if max_delta <= max_int:
  1033. return size
  1034. else:
  1035. raise ValueError((min_delta, max_delta))
  1036. ################################################################################
  1037. # Compressed array
  1038. ################################################################################
  1039. I = TypeVar("I", Entry, int)
  1040. @dataclass
  1041. class CompressedArray(Generic[I]):
  1042. data: tuple[I, ...]
  1043. offsets: tuple[int, ...]
  1044. chunk_offsets: dict[tuple[I, ...], int]
  1045. @classmethod
  1046. def from_overlapped_sequences(
  1047. cls, s: OverlappedSequences[I], groups: Groups[I]
  1048. ) -> Self:
  1049. offsets = tuple(
  1050. s.offsets[d]
  1051. for _, d in sorted(
  1052. ((g, d0) for d0, gs in groups.items() for g in gs),
  1053. key=lambda x: x[0],
  1054. )
  1055. )
  1056. return cls(data=s.data, offsets=offsets, chunk_offsets=s.offsets)
  1057. def total_overlap(self) -> int:
  1058. return sum(len(d) for d in self.chunk_offsets) - len(self.data)
  1059. def stats(self, int_size: Callable[[Sequence[Iterable[int] | int]], int]) -> Stats:
  1060. return Stats(
  1061. data_length=len(self.data),
  1062. data_int_size=int_size(self.data),
  1063. data_overlap=self.total_overlap(),
  1064. offsets1_length=len(self.offsets),
  1065. offsets1_int_size=_int_size(self.offsets),
  1066. offsets2_length=0,
  1067. offsets2_int_size=0,
  1068. )
  1069. @staticmethod
  1070. def test() -> None:
  1071. c1 = (1, 2, 3, 4)
  1072. c2 = (2, 3)
  1073. c3 = (3, 4, 5)
  1074. c4 = (1, 3)
  1075. s = OverlappedSequences.from_singleton(c1)
  1076. s += c2
  1077. s += c3
  1078. s += c4
  1079. groups: Groups[int] = {c1: [0, 3], c2: [4], c3: [1, 2]}
  1080. a = CompressedArray.from_overlapped_sequences(s, groups)
  1081. assert a == CompressedArray(
  1082. data=s.data, offsets=(0, 2, 2, 0, 1), chunk_offsets=s.offsets
  1083. ), a
  1084. @dataclass
  1085. class ArrayCompressor:
  1086. compressor: ChunksCompressor
  1087. verbose: bool
  1088. def run(self, groups: Groups[I]) -> CompressedArray[I]:
  1089. s = self.compressor.compress(groups)
  1090. return CompressedArray.from_overlapped_sequences(s, groups)
  1091. @classmethod
  1092. def compress(
  1093. cls,
  1094. compressor: ChunksCompressor,
  1095. block_size: int,
  1096. data: Iterable[I],
  1097. default: I,
  1098. verbose: bool = False,
  1099. ) -> CompressedArray[I]:
  1100. groups = generate_groups(block_size=block_size, data=data)
  1101. array_compressor = cls(compressor=compressor, verbose=verbose)
  1102. return array_compressor.run(groups)
  1103. @classmethod
  1104. def test_compression(cls) -> None:
  1105. c1 = (1, 2, 3)
  1106. c2 = (-1, 0, 1)
  1107. c3 = (-2, -1, 0)
  1108. c4 = (3, 4, 5)
  1109. c5 = (0, 1, 2)
  1110. c6 = (2, 3, 5)
  1111. groups: Groups[int] = {
  1112. c1: [0],
  1113. c2: [1],
  1114. c3: [2],
  1115. c4: [3],
  1116. c5: [4],
  1117. c6: [5],
  1118. }
  1119. compressor = cls(
  1120. compressor=ChunksCompressor(verbose=True),
  1121. verbose=True,
  1122. )
  1123. r = compressor.run(groups)
  1124. assert set(r.chunk_offsets) == {c1, c2, c3, c4, c5, c6}
  1125. for c, gs in groups.items():
  1126. for g in gs:
  1127. offset = r.offsets[g]
  1128. assert c == r.data[offset : offset + len(c)], (
  1129. c,
  1130. r.data[offset : offset + len(c)],
  1131. )
  1132. ################################################################################
  1133. # Solutions
  1134. ################################################################################
  1135. @dataclass
  1136. class SimpleSolution(Generic[T]):
  1137. data_block_size_log2: int
  1138. data_int_size: int
  1139. data_overlap: int
  1140. data: tuple[T, ...]
  1141. offsets1_block_size_log2: int
  1142. offsets1_int_size: int
  1143. offsets1: tuple[int, ...]
  1144. offsets2_int_size: int
  1145. offsets2: tuple[int, ...]
  1146. max: int
  1147. total: int
  1148. @classmethod
  1149. def zeros(cls) -> Self:
  1150. return cls(
  1151. data_block_size_log2=0,
  1152. data_int_size=0,
  1153. data_overlap=0,
  1154. data=(),
  1155. offsets1_block_size_log2=0,
  1156. offsets1_int_size=0,
  1157. offsets1=(),
  1158. offsets2_int_size=0,
  1159. offsets2=(),
  1160. max=0,
  1161. total=0,
  1162. )
  1163. @property
  1164. def case(self) -> str:
  1165. return "both"
  1166. @property
  1167. def data_size(self) -> int:
  1168. return len(self.data) * self.data_int_size
  1169. def _convert(self, op: Callable[[X, T], X], x: X) -> X:
  1170. mask1 = (1 << self.data_block_size_log2) - 1
  1171. mask2 = (1 << self.offsets1_block_size_log2) - 1
  1172. cpʹ = x >> self.data_block_size_log2
  1173. offset = self.offsets2[cpʹ >> self.offsets1_block_size_log2] + (cpʹ & mask2)
  1174. return op(x, self.data[self.offsets1[offset] + (x & mask1)])
  1175. class SimpleSolutionCombinedMappings(SimpleSolution[Entry], metaclass=ABCMeta):
  1176. @property
  1177. @abstractmethod
  1178. def type(self) -> str: ...
  1179. def to_lower(self, x: X) -> X:
  1180. return self._convert(lambda a, e: e.to_lower(a), x)
  1181. def to_upper(self, x: X) -> X:
  1182. return self._convert(lambda a, e: e.to_upper(a), x)
  1183. class SimpleSolutionKeysymsCombinedMappings(SimpleSolutionCombinedMappings):
  1184. @property
  1185. def type(self) -> str:
  1186. return "legacy_keysyms"
  1187. def test(self, config: Config) -> bool:
  1188. # Check legacy keysyms
  1189. r = True
  1190. for ks in map(Keysym, range(0, self.max + 1)):
  1191. if not (char := xkbcommon.keysym_to_str(ks)):
  1192. continue
  1193. cp = ord(char)
  1194. # Lower
  1195. if ks <= self.max:
  1196. expected = Entry.to_lower_char(char)
  1197. ksʹ = xkbcommon.keysym_from_char(expected)
  1198. ksʹʹ = self.to_lower(ks)
  1199. got = xkbcommon.keysym_to_str(ksʹʹ)
  1200. ok = got == expected
  1201. data = (
  1202. hex(ks),
  1203. hex(cp),
  1204. char,
  1205. expected,
  1206. char.lower(),
  1207. hex(ksʹ),
  1208. hex(ksʹʹ),
  1209. got,
  1210. )
  1211. if config.check_error:
  1212. assert ok, data
  1213. elif not ok:
  1214. print("Error:", data)
  1215. r = False
  1216. # Upper
  1217. if ks <= self.max:
  1218. expected = Entry.to_upper_char(char)
  1219. ksʹ = xkbcommon.keysym_from_char(expected)
  1220. ksʹʹ = self.to_upper(ks)
  1221. got = xkbcommon.keysym_to_str(ksʹʹ)
  1222. ok = got == expected
  1223. data = (
  1224. hex(ks),
  1225. hex(cp),
  1226. char,
  1227. expected,
  1228. char.upper(),
  1229. hex(ksʹ),
  1230. hex(ksʹʹ),
  1231. got,
  1232. )
  1233. if config.check_error:
  1234. assert ok, data
  1235. elif not ok:
  1236. print("Error:", data)
  1237. r = False
  1238. return r
  1239. class SimpleSolutionUnicodeCombinedMappings(SimpleSolutionCombinedMappings):
  1240. @property
  1241. def type(self) -> str:
  1242. return "unicode"
  1243. def test(self, config: Config) -> bool:
  1244. # Check Unicode keysyms
  1245. unicode_min = (
  1246. xkbcommon.XKB_KEYSYM_UNICODE_MIN - xkbcommon.XKB_KEYSYM_UNICODE_OFFSET
  1247. )
  1248. r = True
  1249. for cp in map(CodePoint, range(unicode_min, self.max + 1)):
  1250. char = chr(cp)
  1251. # Lower
  1252. if cp <= self.max:
  1253. expected = Entry.to_lower_char(char)
  1254. got = chr(self.to_lower(cp))
  1255. ok = got == expected
  1256. dataʹ = (hex(cp), char, expected, char.lower(), got)
  1257. if config.check_error:
  1258. assert ok, dataʹ
  1259. elif not ok:
  1260. print("Error:", dataʹ)
  1261. r = False
  1262. # Upper
  1263. if cp <= self.max:
  1264. expected = Entry.to_upper_char(char)
  1265. got = chr(self.to_upper(cp))
  1266. ok = got == expected
  1267. dataʹ = (hex(cp), char, expected, char.upper(), got)
  1268. if config.check_error:
  1269. assert ok, dataʹ
  1270. elif not ok:
  1271. print("Error:", dataʹ)
  1272. r = False
  1273. return r
  1274. S = TypeVar("S", bound=SimpleSolutionCombinedMappings)
  1275. @dataclass
  1276. class SeparateLegacyKeysymsAndUnicodeCombinedCaseMappings:
  1277. legacy_keysyms: SimpleSolutionKeysymsCombinedMappings
  1278. unicode: SimpleSolutionUnicodeCombinedMappings
  1279. def __iter__(self) -> Generator[SimpleSolutionCombinedMappings, None, None]:
  1280. yield self.legacy_keysyms
  1281. yield self.unicode
  1282. @property
  1283. def total(self) -> int:
  1284. return sum(s.total for s in self)
  1285. def test(self, config: Config) -> bool:
  1286. r = True
  1287. if self.legacy_keysyms.data:
  1288. r &= self.legacy_keysyms.test(config)
  1289. if self.unicode.data:
  1290. r &= self.unicode.test(config)
  1291. return r
  1292. @classmethod
  1293. def optimize_groups(
  1294. cls,
  1295. data: tuple[Entry, ...],
  1296. data_max: int,
  1297. total0: int,
  1298. config: Config,
  1299. cls_: type[S],
  1300. ) -> S:
  1301. chunks_compressor = ChunksCompressor(verbose=config.verbose)
  1302. total = total0
  1303. max_int_size = sys.maxsize
  1304. solution: S | None = None
  1305. for k1 in range(1, 9):
  1306. block_size1 = 2**k1
  1307. if config.verbose:
  1308. print("".center(80, "-"))
  1309. print(f"{block_size1=} Step 1: Generating 1st compression level…")
  1310. ca1 = ArrayCompressor.compress(
  1311. compressor=chunks_compressor,
  1312. block_size=block_size1,
  1313. data=data,
  1314. default=Entry.zeros(),
  1315. verbose=config.verbose,
  1316. )
  1317. stats1 = ca1.stats(int_size=lambda x: _int_size(x, offset=2))
  1318. if config.verbose:
  1319. print("Step 1 done.")
  1320. print(
  1321. f"{block_size1=} Step 1:",
  1322. f"data_overlap={stats1.data_overlap} data_length={stats1.data_length} data_size={stats1.data_size}",
  1323. f"total={stats1.total}",
  1324. )
  1325. current_total = stats1.total
  1326. if config.verbose:
  1327. print(f"{current_total=}")
  1328. if (data_size := stats1.data_size) >= total:
  1329. if config.verbose:
  1330. print(f"We cannot beat the current solution {data_size=} {total=}")
  1331. continue
  1332. current_int_size = max(stats1.data_int_size, stats1.offsets1_int_size)
  1333. if current_total <= total and (
  1334. current_total != total or current_int_size < max_int_size
  1335. ):
  1336. total = current_total
  1337. max_int_size = current_int_size
  1338. if config.verbose:
  1339. print(f"✨ New best solution: {block_size1=} {total=}")
  1340. # TODO: 1st level solution
  1341. for k2 in range(7, 1, -1):
  1342. block_size2 = 2**k2
  1343. if config.verbose:
  1344. print(
  1345. f"{block_size1=} {block_size2=} Step 2: 2nd level compression… ",
  1346. flush=True,
  1347. )
  1348. ca2 = ArrayCompressor.compress(
  1349. compressor=chunks_compressor,
  1350. block_size=block_size2,
  1351. data=ca1.offsets,
  1352. default=0,
  1353. verbose=config.verbose,
  1354. )
  1355. stats2 = ca2.stats(int_size=_int_size)
  1356. stats = Stats(
  1357. data_length=stats1.data_length,
  1358. data_int_size=stats1.data_int_size,
  1359. data_overlap=stats1.data_overlap,
  1360. offsets1_length=stats2.data_length,
  1361. offsets1_int_size=stats2.data_int_size,
  1362. offsets2_length=stats2.offsets1_length,
  1363. offsets2_int_size=stats2.offsets1_int_size,
  1364. )
  1365. current_total = stats.total
  1366. if config.verbose:
  1367. print(
  1368. f"{block_size1=} {block_size2=} Step 2:",
  1369. f"data_overlap={stats.data_overlap} data_length={stats.data_length} data_size={stats.data_size}",
  1370. f"total={stats.total}",
  1371. )
  1372. print("Step 2 done.", flush=True)
  1373. current_int_size = max(
  1374. stats.data_int_size,
  1375. stats.offsets1_int_size,
  1376. stats.offsets2_int_size,
  1377. )
  1378. if current_total < total or (
  1379. current_total == total and current_int_size < max_int_size
  1380. ):
  1381. total = current_total
  1382. max_int_size = current_int_size
  1383. if config.verbose:
  1384. print(
  1385. f"✨ New best solution: {block_size1=} {block_size2=} {total=}"
  1386. )
  1387. solution = cls_(
  1388. data_block_size_log2=k1,
  1389. data_int_size=stats.data_int_size,
  1390. data=ca1.data,
  1391. data_overlap=stats.data_overlap,
  1392. offsets1_block_size_log2=k2,
  1393. offsets1=ca2.data,
  1394. offsets1_int_size=stats.offsets1_int_size,
  1395. offsets2=ca2.offsets,
  1396. offsets2_int_size=stats.offsets2_int_size,
  1397. max=data_max,
  1398. total=stats.total,
  1399. )
  1400. assert solution
  1401. if config.verbose:
  1402. print(" Finished ".center(80, "*"))
  1403. print(
  1404. f"Best solution ({solution.case}):",
  1405. f"data_block_size={2**solution.data_block_size_log2}",
  1406. f"data_overlap={solution.data_overlap}",
  1407. f"offsets1_block_size={2**solution.offsets1_block_size_log2}",
  1408. f"total={solution.total}",
  1409. )
  1410. print("Total:", solution.total)
  1411. print("".center(80, "*"))
  1412. return solution
  1413. @classmethod
  1414. def optimize(
  1415. cls,
  1416. config: Config,
  1417. path: Path | None = None,
  1418. ) -> Self:
  1419. print("Computing deltas… ", end="", flush=True)
  1420. entries = Entries.compute(config=config, path=path)
  1421. print("Done.", flush=True)
  1422. keysyms = cls.optimize_groups(
  1423. config=config,
  1424. data=entries.keysyms,
  1425. data_max=entries.keysym_max,
  1426. total0=32 * len(entries.keysyms),
  1427. cls_=SimpleSolutionKeysymsCombinedMappings,
  1428. )
  1429. unicode = cls.optimize_groups(
  1430. config=config,
  1431. data=entries.unicode,
  1432. data_max=entries.unicode_max,
  1433. total0=32 * len(entries.unicode),
  1434. cls_=SimpleSolutionUnicodeCombinedMappings,
  1435. )
  1436. result = cls(legacy_keysyms=keysyms, unicode=unicode)
  1437. print(" Finished ".center(90, "*"))
  1438. for s in result:
  1439. if s.data:
  1440. print(
  1441. f"✨ {s.type}:",
  1442. f"max: 0x{s.max:0>4x}",
  1443. f"data_block_size={2**s.data_block_size_log2}",
  1444. f"offsets_block_size={2**s.offsets1_block_size_log2}",
  1445. f"data={len(s.data)} (int size: {s.data_int_size})",
  1446. f"data_overlap={s.data_overlap}",
  1447. f"offsets1={len(s.offsets1)} (uint size: {s.offsets1_int_size})",
  1448. f"offsets2={len(s.offsets2)} (uint size: {s.offsets2_int_size})",
  1449. f"total={s.total} ({s.total // 8})",
  1450. )
  1451. print(f"Total: {result.total} ({result.total // 8})")
  1452. print("".center(90, "*"))
  1453. return result
  1454. @classmethod
  1455. def generate(cls, root: Path, write: bool, config: Config) -> None:
  1456. s = cls.optimize(
  1457. config=config,
  1458. path=root / "data/keysyms/list.yaml",
  1459. )
  1460. s.test(config)
  1461. if write:
  1462. s.write(root)
  1463. MAPPING_TEMPLATE = """\
  1464. static const struct CaseMappings {prefix}data[{data_length}] = {{
  1465. {data}
  1466. }};
  1467. static const uint{offsets1_int_size}_t {prefix}offsets1[{offsets1_length}] = {{
  1468. {offsets1}
  1469. }};
  1470. static const uint{offsets2_int_size}_t {prefix}offsets2[{offsets2_length}] = {{
  1471. {offsets2}
  1472. }};
  1473. static inline const struct CaseMappings *
  1474. get_{prefix}entry(xkb_keysym_t ks)
  1475. {{
  1476. return &{prefix}data[{prefix}offsets1[{prefix}offsets2[ks >> {k12}] + ((ks >> {k1}) & 0x{mask2:0>2x})] + (ks & 0x{mask1:0>2x})];
  1477. }}\
  1478. """
  1479. TEMPLATE = """\
  1480. // NOTE: This file has been generated automatically by “{python_script}”.
  1481. // Do not edit manually!
  1482. /*
  1483. * Copyright © 2024 Pierre Le Marre
  1484. * SPDX-License-Identifier: MIT
  1485. */
  1486. /* Case mappings for Unicode {unicode_version}
  1487. *
  1488. {doc}
  1489. */
  1490. #include "config.h"
  1491. #include <stdint.h>
  1492. #include <stdbool.h>
  1493. #include <string.h>
  1494. #include "xkbcommon/xkbcommon.h"
  1495. #include "utils.h"
  1496. #include "keysym.h"
  1497. struct CaseMappings{{
  1498. bool lower:1;
  1499. bool upper:1;
  1500. int{data_int_size}_t offset:{data_int_size_with_offset};
  1501. }};
  1502. {legacy_keysyms_mappings}
  1503. {unicode_mappings}
  1504. xkb_keysym_t
  1505. xkb_keysym_to_lower(xkb_keysym_t ks)
  1506. {{
  1507. if (ks <= 0x{max_non_unicode:0>4x}) {{
  1508. const struct CaseMappings *m = get_legacy_keysym_entry(ks);
  1509. return (m->lower) ? ks + (xkb_keysym_t)m->offset : ks;
  1510. }} else if ({min_unicode} <= ks && ks <= 0x{max_unicode:0>8x}) {{
  1511. const struct CaseMappings *m = get_unicode_entry(ks - XKB_KEYSYM_UNICODE_OFFSET);
  1512. if (m->lower) {{
  1513. ks = ks + (xkb_keysym_t)m->offset;
  1514. return (ks < {min_unicode}) ? ks - XKB_KEYSYM_UNICODE_OFFSET : ks;
  1515. }} else {{
  1516. return ks;
  1517. }}
  1518. }} else {{
  1519. return ks;
  1520. }}
  1521. }}
  1522. xkb_keysym_t
  1523. xkb_keysym_to_upper(xkb_keysym_t ks)
  1524. {{
  1525. if (ks <= 0x{max_non_unicode:0>4x}) {{
  1526. const struct CaseMappings *m = get_legacy_keysym_entry(ks);
  1527. return (m->upper) ? ks - (xkb_keysym_t)m->offset : ks;
  1528. }} else if ({min_unicode} <= ks && ks <= 0x{max_unicode:0>8x}) {{
  1529. const struct CaseMappings *m = get_unicode_entry(ks - XKB_KEYSYM_UNICODE_OFFSET);
  1530. if (m->upper) {{
  1531. ks -= (xkb_keysym_t)m->offset;
  1532. return (ks < {min_unicode}) ? ks - XKB_KEYSYM_UNICODE_OFFSET : ks;
  1533. }} else {{
  1534. return ks;
  1535. }}
  1536. }} else {{
  1537. return ks;
  1538. }}
  1539. }}
  1540. bool
  1541. xkb_keysym_is_lower(xkb_keysym_t ks)
  1542. {{
  1543. /* This predicate matches keysyms with their corresponding Unicode code point
  1544. * having the Unicode property “Lowercase”.
  1545. *
  1546. * Here: a keysym is lower case if it has an upper case and no lower case.
  1547. * Note: title case letters may have both. Example for U+01F2:
  1548. * • U+01F1 DZ: upper case
  1549. * • U+01F2 Dz: title case
  1550. * • U+01F3 dz: lower case
  1551. */
  1552. if (ks <= 0x{max_non_unicode:0>4x}) {{
  1553. const struct CaseMappings *m = get_legacy_keysym_entry(ks);
  1554. return m->upper && !m->lower;
  1555. }} else if ({min_unicode} <= ks && ks <= 0x{max_unicode:0>8x}) {{
  1556. const struct CaseMappings *m = get_unicode_entry(ks - XKB_KEYSYM_UNICODE_OFFSET);
  1557. return m->upper && !m->lower;
  1558. }} else {{
  1559. return false;
  1560. }}
  1561. }}
  1562. bool
  1563. xkb_keysym_is_upper_or_title(xkb_keysym_t ks)
  1564. {{
  1565. /* This predicate matches keysyms with their corresponding Unicode code point
  1566. * having the Unicode properties “Uppercase” or General Category “Lt”.
  1567. *
  1568. * Here: a keysym is upper case or title case if it has a lower case. */
  1569. if (ks <= 0x{max_non_unicode:0>4x}) {{
  1570. return get_legacy_keysym_entry(ks)->lower;
  1571. }} else if ({min_unicode} <= ks && ks <= 0x{max_unicode:0>8x}) {{
  1572. return get_unicode_entry(ks - XKB_KEYSYM_UNICODE_OFFSET)->lower;
  1573. }} else {{
  1574. return false;
  1575. }}
  1576. }}
  1577. """
  1578. @staticmethod
  1579. def myHex(n: int, max_hex_length: int) -> str:
  1580. return (
  1581. f"""{"-" if n < 0 else " "}0x{hex(abs(n))[2:].rjust(max_hex_length, "0")}"""
  1582. )
  1583. @classmethod
  1584. def make_char_case_mappings(cls, e: Entry, max_hex_length: int) -> str:
  1585. # If the entry has both lower and upper mappings, they must be equal.
  1586. if e.lower and e.upper and e.lower != e.upper:
  1587. raise ValueError(e)
  1588. has_lower = e.is_upper or bool(e.lower)
  1589. has_upper = e.is_lower or bool(e.upper)
  1590. return f"{{{int(has_lower)}, {int(has_upper)},{cls.myHex(e.lower or e.upper, max_hex_length)}}}"
  1591. def generate_mapping(self, prefix: str, sol: SimpleSolutionCombinedMappings) -> str:
  1592. if not sol.offsets1_block_size_log2:
  1593. raise ValueError(f"generate_mapping: {prefix}")
  1594. max_data = max(max(abs(e.lower), abs(e.upper)) for e in sol.data)
  1595. max_hex_length = 1 + (int(math.log2(max_data)) >> 2)
  1596. return self.MAPPING_TEMPLATE.format(
  1597. prefix=prefix,
  1598. data_length=len(sol.data),
  1599. data=",\n ".join(
  1600. ", ".join(
  1601. "".join(self.make_char_case_mappings(x, max_hex_length)) for x in xs
  1602. )
  1603. for xs in itertools.batched(sol.data, 4)
  1604. ),
  1605. offsets1_int_size=sol.offsets1_int_size,
  1606. offsets1_length=len(sol.offsets1),
  1607. offsets1=",\n ".join(
  1608. ", ".join(f"0x{x:0>4x}" for x in xs)
  1609. for xs in itertools.batched(sol.offsets1, 10)
  1610. ),
  1611. offsets2_int_size=sol.offsets2_int_size,
  1612. offsets2_length=len(sol.offsets2),
  1613. offsets2=",\n ".join(
  1614. ", ".join(f"0x{x:0>4x}" for x in xs)
  1615. for xs in itertools.batched(sol.offsets2, 10)
  1616. ),
  1617. k1=sol.data_block_size_log2,
  1618. k2=sol.offsets1_block_size_log2,
  1619. k12=sol.data_block_size_log2 + sol.offsets1_block_size_log2,
  1620. mask1=(1 << sol.data_block_size_log2) - 1,
  1621. mask2=(1 << sol.offsets1_block_size_log2) - 1,
  1622. )
  1623. def write(self, root: Path) -> None:
  1624. path = root / "src/keysym-case-mappings.c"
  1625. keysyms = self.generate_mapping("legacy_keysym_", self.legacy_keysyms)
  1626. unicode = self.generate_mapping("unicode_", self.unicode)
  1627. assert self.legacy_keysyms.data_int_size == self.unicode.data_int_size
  1628. doc = textwrap.indent(
  1629. textwrap.indent(__doc__.strip(), prefix=" "),
  1630. prefix=" *",
  1631. predicate=lambda _: True,
  1632. )
  1633. content = self.TEMPLATE.format(
  1634. python_script=Path(__file__).name,
  1635. unicode_version=icu.UNICODE_VERSION,
  1636. doc=doc,
  1637. data_int_size=self.legacy_keysyms.data_int_size,
  1638. data_int_size_with_offset=self.legacy_keysyms.data_int_size - 2,
  1639. legacy_keysyms_mappings=keysyms,
  1640. unicode_mappings=unicode,
  1641. max_non_unicode=self.legacy_keysyms.max,
  1642. min_unicode="XKB_KEYSYM_UNICODE_MIN",
  1643. max_unicode=XKBCOMMON.XKB_KEYSYM_UNICODE_OFFSET + self.unicode.max,
  1644. keysym_unicode_offset="XKB_KEYSYM_UNICODE_OFFSET",
  1645. )
  1646. with path.open("wt", encoding="utf-8") as fd:
  1647. fd.write(content)
  1648. unicode_version_array = ", ".join(
  1649. (icu.UNICODE_VERSION + ".0.0.0").split(".")[:4]
  1650. )
  1651. path = root / "src/keysym.h.jinja"
  1652. with path.open("rt", encoding="utf-8") as fd:
  1653. content = fd.read()
  1654. pattern: re.Pattern[str] = re.compile(
  1655. r"^(#define\s+XKB_KEYSYM_UNICODE_VERSION\s+\{\s*)(?:\d+(?:,\s*\d+)*)*(\s*\})",
  1656. re.MULTILINE,
  1657. )
  1658. def replace(m: re.Match[str]) -> str:
  1659. return f"{m.group(1)}{unicode_version_array}{m.group(2)}"
  1660. content = pattern.sub(replace, content)
  1661. with path.open("wt", encoding="utf-8") as fd:
  1662. fd.write(content)
  1663. ################################################################################
  1664. # Strategy
  1665. ################################################################################
  1666. @unique
  1667. class Strategy(Enum):
  1668. Default = SeparateLegacyKeysymsAndUnicodeCombinedCaseMappings
  1669. def __str__(self) -> str:
  1670. return self.value.__name__
  1671. @classmethod
  1672. def parse(cls, str: str) -> Self:
  1673. for s in cls:
  1674. if s.name == str:
  1675. return s
  1676. else:
  1677. raise ValueError(str)
  1678. @classmethod
  1679. def run(
  1680. cls, root: Path, config: Config, strategies: list[Self], write: bool
  1681. ) -> None:
  1682. # FIXME: more generic type
  1683. results: list[SeparateLegacyKeysymsAndUnicodeCombinedCaseMappings] = []
  1684. best_total = math.inf
  1685. best_solution: SeparateLegacyKeysymsAndUnicodeCombinedCaseMappings | None = None
  1686. for strategy in strategies:
  1687. print(f" Optimizing using {strategy.name} ".center(90, "="))
  1688. sol = strategy.value.optimize(
  1689. config=config, path=root / "data/keysyms/list.yaml"
  1690. )
  1691. sol.test(config)
  1692. results.append(sol)
  1693. if sol.total < best_total:
  1694. best_total = sol.total
  1695. best_solution = sol
  1696. assert best_solution
  1697. assert best_solution
  1698. for strategy, sol in zip(strategies, results):
  1699. print(
  1700. f"{strategy.name}: {sol.total} ({sol.total // 8})",
  1701. "✨✨✨" if sol is best_solution else "",
  1702. )
  1703. best_solution.test(config)
  1704. if write:
  1705. best_solution.write(root)
  1706. cls.write_tests(root)
  1707. @classmethod
  1708. def write_tests(cls, root: Path) -> None:
  1709. # Configure Jinja
  1710. template_loader = jinja2.FileSystemLoader(root, encoding="utf-8")
  1711. jinja_env = jinja2.Environment(
  1712. loader=template_loader,
  1713. keep_trailing_newline=True,
  1714. trim_blocks=True,
  1715. lstrip_blocks=True,
  1716. )
  1717. def code_point_name_constant(c: str, padding: int = 0) -> str:
  1718. if not (name := unicodedata.name(c)):
  1719. raise ValueError(f"No Unicode name for code point: U+{ord(c):0>4X}")
  1720. name = name.replace("-", "_").replace(" ", "_").upper()
  1721. return name.ljust(padding)
  1722. jinja_env.filters["code_point"] = lambda c: f"0x{ord(c):0>4x}"
  1723. jinja_env.filters["code_point_name_constant"] = code_point_name_constant
  1724. path = root / "test/keysym-case-mapping.h"
  1725. template_path = path.with_suffix(f"{path.suffix}.jinja")
  1726. template = jinja_env.get_template(str(template_path.relative_to(root)))
  1727. with path.open("wt", encoding="utf-8") as fd:
  1728. fd.writelines(
  1729. template.generate(
  1730. upper_exceptions=Entry.to_upper_exceptions,
  1731. script=SCRIPT.relative_to(root),
  1732. )
  1733. )
  1734. def import_from_path(module_name: str, file_path: Path):
  1735. spec = importlib.util.spec_from_file_location(module_name, file_path)
  1736. module = importlib.util.module_from_spec(spec)
  1737. sys.modules[module_name] = module
  1738. spec.loader.exec_module(module)
  1739. return module
  1740. ################################################################################
  1741. # Main
  1742. ################################################################################
  1743. if __name__ == "__main__":
  1744. # Root of the project
  1745. ROOT = Path(__file__).parent.parent
  1746. # Parse commands
  1747. parser = argparse.ArgumentParser(description="Generate keysyms case mapping files")
  1748. parser.add_argument(
  1749. "--root",
  1750. type=Path,
  1751. default=ROOT,
  1752. help="Path to the root of the project (default: %(default)s)",
  1753. )
  1754. parser.add_argument(
  1755. "--ucd",
  1756. type=Path,
  1757. help="Path to UCD directory to replace ICU",
  1758. required=icu is None,
  1759. )
  1760. parser.add_argument(
  1761. "--ucd-version",
  1762. type=str,
  1763. help="Version of the UCD",
  1764. required=icu is None,
  1765. )
  1766. parser.add_argument(
  1767. "--strategy",
  1768. type=Strategy.parse,
  1769. default=list(Strategy),
  1770. nargs="+",
  1771. choices=Strategy,
  1772. help="Strategy (default: %(default)s)",
  1773. dest="strategies",
  1774. )
  1775. parser.add_argument(
  1776. "--dry",
  1777. action="store_true",
  1778. help="Do not write (default: %(default)s)",
  1779. )
  1780. parser.add_argument(
  1781. "--verbose",
  1782. action="store_true",
  1783. help="Verbose (default: %(default)s)",
  1784. )
  1785. parser.add_argument(
  1786. "--no-check",
  1787. action="store_true",
  1788. help="Do not check errors (default: %(default)s)",
  1789. )
  1790. args = parser.parse_args()
  1791. if ucd_path := args.ucd:
  1792. # Use a local UCD instead of ICU.
  1793. # Useful when ICU is not available or when it does not support the latest
  1794. # Unicode version.
  1795. # Data are available at: https://www.unicode.org/Public/<UNICODE_VERSION>/ucd/
  1796. icu = import_from_path(
  1797. module_name="icu", file_path=Path(__file__).with_stem("ucd")
  1798. )
  1799. if ucd_version := args.ucd_version:
  1800. icu.UNICODE_VERSION = ucd_version
  1801. icu.Char = icu.DB.parse_ucd(ucd_path)
  1802. config = Config(
  1803. check_error=not args.no_check,
  1804. verbose=args.verbose,
  1805. )
  1806. Strategy.run(
  1807. root=args.root, config=config, write=not args.dry, strategies=args.strategies
  1808. )