packages feed

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()