idna2008-1.0.0.0: internal/tools/genIdnaSimpleLower.py
#!/usr/bin/env python3
"""
genIdnaSimpleLower.py -- regenerate Text.IDNA2008.Internal.Case.Data
from the Unicode UCD's UnicodeData.txt.
The output module exposes a single function
toLower :: Char -> Char
implementing the Unicode @Simple_Lowercase_Mapping@ property (column
13 of @UnicodeData.txt@), pinned to the UCD version supplied as the
first argument. Codepoints with no defined lowercase mapping fall
through to identity.
The function is realised as a 'LambdaCase' lookup whose shape mirrors
@GHC.Internal.Unicode.Char.UnicodeData.SimpleLowerCaseMapping@ from
the GHC 9.14.1 @base@ library; GHC compiles a @\\case@ over @Char@
into a balanced decision tree on the codepoint integer, so the
runtime cost is @O(log n)@ with very small constants and no
indirection through 'ByteArray' / FFI machinery.
Usage:
python3 internal/tools/genIdnaSimpleLower.py <unicode-version> <UnicodeData.txt> \\
> internal/Text/IDNA2008/Internal/Case/Data.hs
UnicodeData.txt columns of interest:
0 Codepoint (hex, no prefix)
1 Character name
2 General_Category
...
12 Simple_Uppercase_Mapping
13 Simple_Lowercase_Mapping <-- consumed here
14 Simple_Titlecase_Mapping
Codepoint ranges denoted by @<..., First>@ / @<..., Last>@ pairs in
UnicodeData.txt (CJK ideographs, Hangul syllables, etc.) never carry
case mappings, so we don't attempt to expand them.
"""
from __future__ import annotations
import sys
def parse(path: str) -> list[tuple[int, int]]:
"""Return [(source_cp, target_cp)] for every row with a non-empty
Simple_Lowercase_Mapping field, sorted ascending by source."""
out: list[tuple[int, int]] = []
with open(path, encoding="utf-8") as f:
for raw in f:
line = raw.rstrip("\n")
if not line:
continue
cols = line.split(";")
if len(cols) < 14:
continue
slm = cols[13].strip()
if not slm:
continue
src = int(cols[0], 16)
tgt = int(slm, 16)
out.append((src, tgt))
out.sort(key=lambda p: p[0])
return out
HEADER = """\
-- DO NOT EDIT: This file is automatically generated by
-- internal/tools/genIdnaSimpleLower.py, with data from:
-- https://www.unicode.org/Public/{version}/ucd/UnicodeData.txt
--
-- Pinned to Unicode {version}; bump @x-unicode-version@ in
-- @idna2008.cabal@ and re-run @internal/tools/update@ to advance.
{{-# LANGUAGE NoImplicitPrelude, LambdaCase #-}}
{{-# OPTIONS_HADDOCK hide #-}}
-----------------------------------------------------------------------------
-- |
-- Module : Text.IDNA2008.Internal.Case.Data
-- Description : Unicode @Simple_Lowercase_Mapping@ table for the
-- UCD version pinned by the @idna2008@ library.
-- Copyright : (c) Viktor Dukhovni, 2026
-- License : BSD-3-Clause
--
-- Maintainer : ietf-dane@dukhovni.org
-- Stability : unstable
--
-- AUTOMATICALLY GENERATED. Do not edit by hand. Regenerate with:
--
-- python3 internal/tools/genIdnaSimpleLower.py <unicode-version> \\\\
-- <UnicodeData.txt>
--
-- Aligned with: Unicode {version}
-- Mapping entries: {count}
-----------------------------------------------------------------------------
module Text.IDNA2008.Internal.Case.Data
(toLower)
where
import Prelude (Char)
{{-# NOINLINE toLower #-}}
toLower :: Char -> Char
toLower = \\case
"""
FOOTER = """\
c -> c
"""
def main() -> None:
if len(sys.argv) != 3:
sys.stderr.write(__doc__)
sys.exit(1)
version = sys.argv[1]
ucd_path = sys.argv[2]
pairs = parse(ucd_path)
out = sys.stdout
out.write(HEADER.format(version=version, count=len(pairs)))
for src, tgt in pairs:
out.write(f" '\\x{src:x}' -> '\\x{tgt:x}'\n")
out.write(FOOTER)
if __name__ == "__main__":
main()