1
0

update-keysyms-names-handling.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. #!/usr/bin/env python3
  2. """
  3. Generate C file to handle keysym names
  4. """
  5. import argparse
  6. import itertools
  7. import random
  8. import sys
  9. from pathlib import Path
  10. from typing import Generator, Iterable, Iterator
  11. import perfect_hash
  12. # Root of the project
  13. SCRIPT = Path(__file__)
  14. ROOT = SCRIPT.parent.parent
  15. sys.path.append(str(SCRIPT.parent))
  16. from keysyms import Keysym, Keysyms, Semantics # noqa: E402
  17. # libxkbcommon Keysyms names header
  18. KEYSYMS_NAMES_HEADER = ROOT / "src" / "keysym-names.h"
  19. if not KEYSYMS_NAMES_HEADER.is_file():
  20. raise FileNotFoundError(KEYSYMS_NAMES_HEADER)
  21. # Parse commands
  22. parser = argparse.ArgumentParser(description="Generate C file to handle keysym names")
  23. parser.add_argument(
  24. "c_header", type=Path, help="Path to the libxkbcommon keysym header"
  25. )
  26. parser.add_argument("gperf", type=Path, help="Path to the gperf file")
  27. args = parser.parse_args()
  28. # Set the seed explicitly, so we reduce diff
  29. random.seed(b"libxkbcommon")
  30. all_keysyms = Keysyms.parse_file(args.c_header)
  31. entries: tuple[Keysym, ...] = tuple(
  32. itertools.chain.from_iterable(all_keysyms.by_value.values())
  33. )
  34. # Sort based on the keysym name:
  35. # 1. Sort by the casefolded name: e.g. kana_ya < kana_YO.
  36. # 2. If same casefolded name, then sort by cased name, i.e for
  37. # ASCII: upper before lower: e.g kana_YA < kana_ya.
  38. # E.g. kana_YA < kana_ya < kana_YO < kana_yo
  39. # WARNING: this sort must not be changed, as some functions e.g.
  40. # xkb_keysym_from_name rely on upper case variant occuring first.
  41. entries_isorted = sorted(entries, key=lambda e: (e.name.casefold(), e.name))
  42. # Sort based on keysym value. Sort is stable so in case of duplicate, the first
  43. # keysym occurence stays first.
  44. entries_kssorted = sorted(entries, key=lambda e: e.value)
  45. print(
  46. f"""
  47. /**
  48. * This file comes from libxkbcommon and was generated by {SCRIPT.name}
  49. * You can always fetch the latest version from:
  50. * https://raw.github.com/xkbcommon/libxkbcommon/master/{KEYSYMS_NAMES_HEADER.relative_to(ROOT)}
  51. */
  52. #pragma once
  53. """
  54. )
  55. entry_offsets: dict[str, int] = {}
  56. UINT16_MAX = (1 << 16) - 1
  57. UNICODE_KEYSYM = UINT16_MAX - 1
  58. DEPRECATED_KEYSYM = UINT16_MAX
  59. MAX_EXPLICIT_DEPRECATED_ALIAS_INDEX_LOG2 = 8
  60. MAX_EXPLICIT_DEPRECATED_ALIAS_INDEX = 1 << MAX_EXPLICIT_DEPRECATED_ALIAS_INDEX_LOG2
  61. MAX_EXPLICIT_DEPRECATED_ALIAS_COUNT_LOG2 = 4
  62. MAX_EXPLICIT_DEPRECATED_ALIAS_COUNT = 1 << MAX_EXPLICIT_DEPRECATED_ALIAS_COUNT_LOG2
  63. MAX_OFFSET = UNICODE_KEYSYM - 1
  64. XKB_KEYSYM_UNICODE_MIN = 0x01000100
  65. XKB_KEYSYM_UNICODE_MAX = 0x0110FFFF
  66. print(
  67. """
  68. #include "config.h"
  69. #include <stddef.h>
  70. #include <stdint.h>
  71. #include "xkbcommon/xkbcommon.h"
  72. #include "utils.h"
  73. #ifdef __GNUC__
  74. #pragma GCC diagnostic push
  75. #pragma GCC diagnostic ignored "-Woverlength-strings"
  76. #endif
  77. static const char *keysym_names =
  78. """.strip()
  79. )
  80. offs = 0
  81. for keysym in entries_isorted:
  82. if offs >= MAX_OFFSET:
  83. raise ValueError(f"Offset must be kept under {MAX_OFFSET}, got: {offs}.")
  84. entry_offsets[keysym.name] = offs
  85. print(f' "{keysym.name}\\0"')
  86. offs += len(keysym.name) + 1
  87. print(
  88. """
  89. ;
  90. #ifdef __GNUC__
  91. #pragma GCC diagnostic pop
  92. #endif
  93. """.strip()
  94. )
  95. template = r"""
  96. static const uint16_t keysym_name_G[] = {
  97. $G
  98. };
  99. static inline size_t
  100. keysym_name_perfect_hash(const char *key)
  101. {
  102. const char *T1 = "$S1";
  103. const char *T2 = "$S2";
  104. size_t h1 = 0;
  105. size_t h2 = 0;
  106. for (size_t i = 0; key[i] != '\0'; i++) {
  107. h1 += (size_t) (T1[i % $NS] * key[i]);
  108. h2 += (size_t) (T2[i % $NS] * key[i]);
  109. }
  110. return (keysym_name_G[h1 % $NG] + keysym_name_G[h2 % $NG]) % $NG;
  111. }
  112. """
  113. print(
  114. perfect_hash.generate_code(
  115. keys=[keysym.name for keysym in entries_isorted],
  116. template=template,
  117. )
  118. )
  119. print(
  120. """
  121. struct name_keysym {
  122. xkb_keysym_t keysym;
  123. uint16_t offset;
  124. };\n"""
  125. )
  126. def print_entries(entries: Iterable[Keysym]):
  127. for entry in entries:
  128. print(
  129. " {{ 0x{value:08x}, {offs} }}, /* {name} */".format(
  130. offs=entry_offsets[entry.name], value=entry.value, name=entry.name
  131. )
  132. )
  133. print("static const struct name_keysym name_to_keysym[] = {")
  134. print_entries(entries_isorted)
  135. print("};\n")
  136. # *.sort() is stable so we always get the first keysym for duplicate
  137. print("static const struct name_keysym keysym_to_name[] = {")
  138. print_entries(
  139. next(g[1]) for g in itertools.groupby(entries_kssorted, key=lambda e: e.value)
  140. )
  141. print("};\n")
  142. def make_deprecated_entry(
  143. value,
  144. keysyms: list[Keysym],
  145. entry_offsets: dict[str, int],
  146. explicit_deprecated_aliases_index: int,
  147. ) -> tuple[str | None, tuple[int, ...]]:
  148. assert keysyms
  149. if all(not k.deprecated for k in keysyms):
  150. # No name is deprecated
  151. return None, ()
  152. canonical = keysyms[0]
  153. assert canonical.is_canonical
  154. ref = canonical.preferred
  155. non_deprecated_names = tuple(k for k in keysyms if not k.deprecated)
  156. deprecated_names = tuple(k for k in keysyms if k.deprecated)
  157. deprecated_names_indices: tuple[int, ...] = ()
  158. if non_deprecated_names:
  159. # Keysym is not deprecated, but some of its names are.
  160. assert not ref.deprecated, ref
  161. ref_name = f"Reference: {ref.name}. "
  162. ref_index = str(entry_offsets[ref.name])
  163. assert deprecated_names or ref.deprecated, keysyms
  164. if any(k is not ref for k in non_deprecated_names):
  165. # Keysym has both multiple valid names and some deprecated names
  166. deprecated_names_indices = tuple(
  167. entry_offsets[k.name] for k in deprecated_names
  168. )
  169. assert (
  170. explicit_deprecated_aliases_index < MAX_EXPLICIT_DEPRECATED_ALIAS_INDEX
  171. )
  172. assert len(deprecated_names_indices) < MAX_EXPLICIT_DEPRECATED_ALIAS_COUNT
  173. else:
  174. # Keysym has a single valid name and some deprecated names
  175. assert len(non_deprecated_names) == 1, non_deprecated_names
  176. assert deprecated_names
  177. # Do *not* use an explicit list of deprecated names
  178. explicit_deprecated_aliases_index = 0
  179. else:
  180. # Keysym is deprecated
  181. assert ref.deprecated, ref
  182. ref_name = ""
  183. ref_index = (
  184. "DEPRECATED_KEYSYM"
  185. if value < XKB_KEYSYM_UNICODE_MIN or value > XKB_KEYSYM_UNICODE_MAX
  186. else "UNICODE_KEYSYM"
  187. )
  188. # Do *not* use an explicit list of deprecated names
  189. explicit_deprecated_aliases_index = 0
  190. if non_deprecated_aliases := tuple(k for k in non_deprecated_names if k is not ref):
  191. non_deprecated = (
  192. "Non deprecated aliases: "
  193. + ", ".join(k.name for k in non_deprecated_aliases)
  194. + ". "
  195. )
  196. else:
  197. non_deprecated = ""
  198. deprecated = ", ".join(k.name for k in deprecated_names)
  199. comment = f"{ref_name}{non_deprecated}Deprecated: {deprecated}"
  200. return (
  201. f" {{ 0x{value:0>8x}, {ref_index: <17}, {explicit_deprecated_aliases_index}, {len(deprecated_names_indices)} }}, /* {comment} */",
  202. deprecated_names_indices,
  203. )
  204. def generate_deprecated_keysyms(
  205. all_keysyms: Keysyms, entry_offsets: dict[str, int]
  206. ) -> Generator[tuple[int, ...], None, None]:
  207. explicit_deprecated_aliases_index = 0
  208. for value, keysyms in sorted(all_keysyms.by_value.items(), key=lambda e: e[0]):
  209. assert keysyms
  210. c_entry, explicit_deprecated_aliases = make_deprecated_entry(
  211. value, keysyms, entry_offsets, explicit_deprecated_aliases_index
  212. )
  213. if c_entry is not None:
  214. print(c_entry)
  215. if explicit_deprecated_aliases:
  216. yield explicit_deprecated_aliases
  217. explicit_deprecated_aliases_index += len(explicit_deprecated_aliases)
  218. def generate_mixed_aliases(aliases: Iterable[Iterable[int]]):
  219. for xs in aliases:
  220. for x in xs:
  221. print(f" {x},")
  222. print(f"#define UNICODE_KEYSYM 0x{UNICODE_KEYSYM:x}")
  223. print(f"#define DEPRECATED_KEYSYM 0x{DEPRECATED_KEYSYM:x}")
  224. # NOTE: Alternative implementation, useful the day the indices do not fit uint16_t.
  225. # print(f"""
  226. # struct deprecated_keysym {{
  227. # xkb_keysym_t keysym;
  228. # union {{
  229. # uint32_t offset;
  230. # struct {{
  231. # uint32_t offset:{MAX_OFFSET_LOG2};
  232. # /* Explicit deprecated aliases start index & count */
  233. # uint8_t explicit_index:{MAX_EXPLICIT_DEPRECATED_ALIAS_INDEX_LOG2};
  234. # uint8_t explicit_count:{MAX_EXPLICIT_DEPRECATED_ALIAS_COUNT_LOG2};
  235. # }} details;
  236. # }};
  237. # }};
  238. # """)
  239. print("""
  240. struct deprecated_keysym {
  241. xkb_keysym_t keysym;
  242. uint16_t offset;
  243. /* Explicit deprecated aliases start index & count */
  244. uint8_t explicit_index;
  245. uint8_t explicit_count;
  246. };
  247. """)
  248. print("static const struct deprecated_keysym deprecated_keysyms[] = {")
  249. explicit_deprecated_aliases = tuple(
  250. generate_deprecated_keysyms(all_keysyms, entry_offsets)
  251. )
  252. print("};\n")
  253. print("static const uint32_t explicit_deprecated_aliases[] = {")
  254. generate_mixed_aliases(explicit_deprecated_aliases)
  255. print("};")
  256. print("""
  257. static_assert(ARRAY_SIZE(explicit_deprecated_aliases) < UINT8_MAX,
  258. "Cannot encode index and count in deprecated_keysym::explicit_*");\
  259. """)
  260. print(f"max name offset: {max(entry_offsets.values())}", file=sys.stderr)
  261. # Check that the keywords of our XKB parser that clash with keysyms are handled properly
  262. def parse_gperf_keywords(path: Path) -> Iterator[str]:
  263. with path.open("rt", encoding="utf-8") as fd:
  264. in_keyword_section = False
  265. for line in fd:
  266. if line.startswith(r"%%"):
  267. # This is a boundary of the keywords section
  268. if in_keyword_section:
  269. break
  270. in_keyword_section = True
  271. elif in_keyword_section:
  272. # Parse the keywords
  273. keyword, *_ = line.split(",")
  274. yield keyword.strip().casefold()
  275. # Skip any line until we reach the keywords
  276. else:
  277. raise ValueError("Parse error: keywords section boundary not found")
  278. SUPPORTED_KEYWORDS_CLASHES = {"section"}
  279. UNSUPPORTED_KEYWORDS_CLASHES = frozenset(parse_gperf_keywords(args.gperf)).difference(
  280. SUPPORTED_KEYWORDS_CLASHES
  281. )
  282. expected_clashes: set[str] = set()
  283. errors = 0
  284. for entry in entries:
  285. if entry.name.casefold() in UNSUPPORTED_KEYWORDS_CLASHES:
  286. print(
  287. f"ERROR: keysym “{entry.name}” (0x{entry.value:0>4x}) clashes with keywords",
  288. "and cannot be parsed properly.",
  289. file=sys.stderr,
  290. )
  291. errors += 1
  292. elif (lower := entry.name.lower()) in SUPPORTED_KEYWORDS_CLASHES:
  293. if not entry.name.islower():
  294. # Keywords’s atoms are registered in *lower* case, so the keysym will be
  295. # replaced by the keysym with the corresponding name, but they may not match.
  296. entry2: Keysym = Keysym(
  297. name="NoSymbol",
  298. value=0,
  299. char=None,
  300. char_aliases=[],
  301. char_semantics=Semantics.Default,
  302. deprecation=None,
  303. _canonical=None,
  304. _preferred=None,
  305. aliases=[],
  306. comment="",
  307. )
  308. if any(
  309. e.name == lower
  310. for e in all_keysyms.by_value[entry.value]
  311. if e.name != entry.value
  312. ):
  313. # There is a keysym in lower case that is an alias
  314. print(
  315. f"WARNING: keysym “{entry.name}”",
  316. f"will be parsed as “{lower}” (expected)",
  317. file=sys.stderr,
  318. )
  319. else:
  320. # Lookup the keysym mismatch
  321. for e in entries:
  322. if e.name == lower:
  323. entry2 = e
  324. break
  325. print(
  326. f"ERROR: keysym “{entry.name}” (0x{entry.value:0>4x})",
  327. r"clashes with keywords and will be replaced by",
  328. f"“{entry2.name}” (0x{entry2.value:0>4x}).",
  329. file=sys.stderr,
  330. )
  331. errors += 1
  332. else:
  333. print(
  334. f"WARNING: keysym “{entry.name}” clashing with keywords (expected)",
  335. file=sys.stderr,
  336. )
  337. expected_clashes.add(entry.name)
  338. if diff := SUPPORTED_KEYWORDS_CLASHES.difference(expected_clashes):
  339. print(f"ERROR: Unexpected missing clashing keysyms: {diff}", file=sys.stderr)
  340. errors += 1
  341. if errors:
  342. print(
  343. f" {errors} ERRORS ".center(80, "-"),
  344. "Please update the parser file `parser.y` to handle keysyms causing clashes.",
  345. "The relevant entries are:",
  346. "- Keysym",
  347. "- Element (for modmap, parsed via: Expr -> Term -> Lhs -> FieldSpec -> Element)",
  348. file=sys.stderr,
  349. sep="\n",
  350. )
  351. exit(1)