Files
linux-stable-mirror/drivers/tty/vt/gen_ucs_width_table.py
T
Nicolas PitreandGreg Kroah-Hartman 255dc0ec0b vt: merge ucs_is_zero_width()/ucs_is_double_width() into ucs_get_width()
The hot path in vc_process_ucs() asks two independent questions about the
same code point -- "is it double-width?" and "is it zero-width?" -- and
was answering each with its own bsearch over its own table. For anything
past the leading bounds check that meant two scans of the BMP width
tables back to back for what is logically a single lookup.

Replace both with one ucs_get_width(cp) returning 0, 1, or 2 in a single
bsearch, while keeping the total table footprint at the same 2384 B as
before.

To do so, merge the zero-width and double-width ranges per region into
one sorted-by-`first` table. BMP entries stay 4 bytes; per-entry width
is hosted in spare bits of the non-BMP table's `last` field. Non-BMP
code points use only 20 of 32 bits, so each u32 has 12 unused high bits.
Store first/last shifted left by 12 and use the low 12 bits of `last`
for metadata: bit 11 is this entry's own width flag, bits 0..7 host an
8-bit chunk of the BMP double-width bitmap. Because the metadata bits
sit strictly below the lowest cp-scale bit, the bsearch comparator
remains a plain u32 compare on shifted keys with no masking.

In vc_process_ucs() the overwhelmingly common single-width path now
collapses to a single predicted branch:

	if (likely(w == 1))
		return 1;

Note: scripts/checkpatch.pl complains about "Macros with complex values
      should be enclosed in parentheses" for the BMP_*WIDTH and
      RANGE_*WIDTH macros. They are deliberately defined to expand to a
      comma-separated (first, last) pair so they can populate the two
      adjacent fields of a struct initializer; wrapping them in
      parentheses would turn that into a comma-expression and defeat
      the whole construction. Please ignore.

Signed-off-by: Nicolas Pitre <nico@fluxnic.net>
Link: https://patch.msgid.link/20260515034857.2514225-1-nico@fluxnic.net
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
2026-05-22 11:47:33 +02:00

355 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
#
# Leverage Python's unicodedata module to generate ucs_width_table.h
import unicodedata
import sys
import argparse
# This script's file name
from pathlib import Path
this_file = Path(__file__).name
# Default output file name
DEFAULT_OUT_FILE = "ucs_width_table.h"
# --- Global Constants for Width Assignments ---
# Known zero-width characters
KNOWN_ZERO_WIDTH = (
0x200B, # ZERO WIDTH SPACE
0x200C, # ZERO WIDTH NON-JOINER
0x200D, # ZERO WIDTH JOINER
0x2060, # WORD JOINER
0xFEFF # ZERO WIDTH NO-BREAK SPACE (BOM)
)
# Zero-width emoji modifiers and components
# NOTE: Some of these characters would normally be single-width according to
# East Asian Width properties, but we deliberately override them to be
# zero-width because they function as modifiers in emoji sequences.
EMOJI_ZERO_WIDTH = [
# Skin tone modifiers
(0x1F3FB, 0x1F3FF), # Emoji modifiers (skin tones)
# Variation selectors (note: VS16 is treated specially in vt.c)
(0xFE00, 0xFE0F), # Variation Selectors 1-16
# Gender and hair style modifiers
# These would be single-width by Unicode properties, but are zero-width
# when part of emoji
(0x2640, 0x2640), # Female sign
(0x2642, 0x2642), # Male sign
(0x26A7, 0x26A7), # Transgender symbol
(0x1F9B0, 0x1F9B3), # Hair components (red, curly, white, bald)
# Tag characters
(0xE0020, 0xE007E), # Tags
]
# Regional indicators (flag components)
REGIONAL_INDICATORS = (0x1F1E6, 0x1F1FF) # Regional indicator symbols A-Z
# Double-width emoji ranges
#
# Many emoji characters are classified as single-width according to Unicode
# Standard Annex #11 East Asian Width property (N or Neutral), but we
# deliberately override them to be double-width. References:
# 1. Unicode Technical Standard #51: Unicode Emoji
# (https://www.unicode.org/reports/tr51/)
# 2. Principle of "emoji presentation" in WHATWG CSS Text specification
# (https://drafts.csswg.org/css-text-3/#character-properties)
# 3. Terminal emulator implementations (iTerm2, Windows Terminal, etc.) which
# universally render emoji as double-width characters regardless of their
# Unicode EAW property
# 4. W3C Work Item: Requirements for Japanese Text Layout - Section 3.8.1
# Emoji width (https://www.w3.org/TR/jlreq/)
EMOJI_RANGES = [
(0x1F000, 0x1F02F), # Mahjong Tiles (EAW: N, but displayed as double-width)
(0x1F0A0, 0x1F0FF), # Playing Cards (EAW: N, but displayed as double-width)
(0x1F300, 0x1F5FF), # Miscellaneous Symbols and Pictographs
(0x1F600, 0x1F64F), # Emoticons
(0x1F680, 0x1F6FF), # Transport and Map Symbols
(0x1F700, 0x1F77F), # Alchemical Symbols
(0x1F780, 0x1F7FF), # Geometric Shapes Extended
(0x1F800, 0x1F8FF), # Supplemental Arrows-C
(0x1F900, 0x1F9FF), # Supplemental Symbols and Pictographs
(0x1FA00, 0x1FA6F), # Chess Symbols
(0x1FA70, 0x1FAFF), # Symbols and Pictographs Extended-A
]
def create_width_tables():
"""
Creates Unicode character width tables and returns the data structures.
Returns:
tuple: (zero_width_ranges, double_width_ranges)
"""
# Width data mapping
width_map = {} # Maps code points to width (0, 1, 2)
# Mark emoji modifiers as zero-width
for start, end in EMOJI_ZERO_WIDTH:
for cp in range(start, end + 1):
width_map[cp] = 0
# Mark all regional indicators as single-width as they are usually paired
# providing a combined width of 2 when displayed together.
start, end = REGIONAL_INDICATORS
for cp in range(start, end + 1):
width_map[cp] = 1
# Process all assigned Unicode code points (Basic Multilingual Plane +
# Supplementary Planes) Range 0x0 to 0x10FFFF (the full Unicode range)
for block_start in range(0, 0x110000, 0x1000):
block_end = block_start + 0x1000
for cp in range(block_start, block_end):
try:
char = chr(cp)
# Skip if already processed
if cp in width_map:
continue
# Check for combining marks and a format characters
category = unicodedata.category(char)
# Combining marks
if category.startswith('M'):
width_map[cp] = 0
continue
# Format characters
# Since we have no support for bidirectional text, all format
# characters (category Cf) can be treated with width 0 (zero)
# for simplicity, as they don't need to occupy visual space
# in a non-bidirectional text environment.
if category == 'Cf':
width_map[cp] = 0
continue
# Known zero-width characters
if cp in KNOWN_ZERO_WIDTH:
width_map[cp] = 0
continue
# Use East Asian Width property
eaw = unicodedata.east_asian_width(char)
if eaw in ('F', 'W'): # Fullwidth or Wide
width_map[cp] = 2
elif eaw in ('Na', 'H', 'N', 'A'): # Narrow, Halfwidth, Neutral, Ambiguous
width_map[cp] = 1
else:
# Default to single-width for unknown
width_map[cp] = 1
except (ValueError, OverflowError):
# Skip invalid code points
continue
# Process Emoji - generally double-width
for start, end in EMOJI_RANGES:
for cp in range(start, end + 1):
if cp not in width_map or width_map[cp] != 0: # Don't override zero-width
try:
char = chr(cp)
width_map[cp] = 2
except (ValueError, OverflowError):
continue
# Optimize to create range tables
def ranges_optimize(width_data, target_width):
points = sorted([cp for cp, width in width_data.items() if width == target_width])
if not points:
return []
# Group consecutive code points into ranges
ranges = []
start = points[0]
prev = start
for cp in points[1:]:
if cp > prev + 1:
ranges.append((start, prev))
start = cp
prev = cp
# Add the last range
ranges.append((start, prev))
return ranges
# Extract ranges for each width
zero_width_ranges = ranges_optimize(width_map, 0)
double_width_ranges = ranges_optimize(width_map, 2)
return zero_width_ranges, double_width_ranges
def write_tables(zero_width_ranges, double_width_ranges, out_file=DEFAULT_OUT_FILE):
"""
Write the generated tables to C header file.
The output uses a single sorted-by-`first` table per region (BMP and
non-BMP), with zero-width and double-width ranges merged together. The
non-BMP table also hosts the BMP double-width bitmap in spare bits of
`last`. See the encoding comment at the top of ucs.c for the layout.
Args:
zero_width_ranges: List of (start, end) ranges for zero-width characters
double_width_ranges: List of (start, end) ranges for double-width characters
out_file: Output file name (default: DEFAULT_OUT_FILE)
"""
# Bits per BMP-bitmap chunk hosted in one non-BMP entry's `last` field.
# 8 bits makes `idx / BITS_PER_CHUNK` / `idx % BITS_PER_CHUNK` compile to
# a cheap shift+mask in the lookup. The chunk size is also emitted as
# UCS_NONBMP_BMP_BITS in the generated header so ucs.c stays in sync.
BITS_PER_CHUNK = 8
# Function to split ranges into BMP (16-bit) and non-BMP (above 16-bit)
def split_ranges_by_size(ranges):
bmp_ranges = []
non_bmp_ranges = []
for start, end in ranges:
if end <= 0xFFFF:
bmp_ranges.append((start, end))
elif start > 0xFFFF:
non_bmp_ranges.append((start, end))
else:
# Split the range at 0xFFFF
bmp_ranges.append((start, 0xFFFF))
non_bmp_ranges.append((0x10000, end))
return bmp_ranges, non_bmp_ranges
# Split ranges into BMP and non-BMP
zero_width_bmp, zero_width_non_bmp = split_ranges_by_size(zero_width_ranges)
double_width_bmp, double_width_non_bmp = split_ranges_by_size(double_width_ranges)
# Merge zero- and double-width ranges per region, tagging each with its
# width, then sort by `first` so binary search works on the union.
bmp_entries = sorted(
[(s, e, 0) for s, e in zero_width_bmp] +
[(s, e, 2) for s, e in double_width_bmp],
key=lambda t: t[0])
nonbmp_entries = sorted(
[(s, e, 0) for s, e in zero_width_non_bmp] +
[(s, e, 2) for s, e in double_width_non_bmp],
key=lambda t: t[0])
# Build the BMP double-width bitmap: one bit per BMP entry (in sort
# order), set iff that entry is double-width. Pack into BITS_PER_CHUNK-
# wide chunks, with bit j of the chunk corresponding to entry
# (chunk_index * BITS_PER_CHUNK + j).
bmp_w2_bits = [1 if w == 2 else 0 for _, _, w in bmp_entries]
n_chunks = (len(bmp_w2_bits) + BITS_PER_CHUNK - 1) // BITS_PER_CHUNK
if n_chunks > len(nonbmp_entries):
raise RuntimeError(
f"BMP bitmap needs {n_chunks} host entries, "
f"but only {len(nonbmp_entries)} non-BMP entries are available")
chunks = [] # list of (base_index, end_index, packed_value)
for c in range(n_chunks):
base = c * BITS_PER_CHUNK
end_idx = min(base + BITS_PER_CHUNK - 1, len(bmp_w2_bits) - 1)
value = 0
for j in range(BITS_PER_CHUNK):
k = base + j
if k < len(bmp_w2_bits) and bmp_w2_bits[k]:
value |= 1 << j
chunks.append((base, end_idx, value))
# Function to generate code point description comments
def get_code_point_comment(start, end):
try:
start_char_desc = unicodedata.name(chr(start))
if start == end:
return f"/* {start_char_desc} */"
else:
end_char_desc = unicodedata.name(chr(end))
return f"/* {start_char_desc} - {end_char_desc} */"
except:
if start == end:
return f"/* U+{start:04X} */"
else:
return f"/* U+{start:04X} - U+{end:04X} */"
# Generate C tables
with open(out_file, 'w') as f:
f.write(f"""\
/* SPDX-License-Identifier: GPL-2.0 */
/*
* {out_file} - Unicode character width
*
* Auto-generated by {this_file}
*
* Unicode Version: {unicodedata.unidata_version}
*
* Zero-width and double-width ranges are merged into one sorted-by-`first`
* table per region. The non-BMP table additionally hosts the BMP
* double-width bitmap in the low {BITS_PER_CHUNK} bits of `last` of its
* first {n_chunks} entries (covering {len(bmp_w2_bits)} BMP entries).
* See ucs.c for the encoding details and the lookup code.
*/
/* Bits per BMP-bitmap chunk hosted in one non-BMP entry's `last` field. */
#define UCS_NONBMP_BMP_BITS {BITS_PER_CHUNK}
/* Combined zero- and double-width ranges
* (BMP - Basic Multilingual Plane, U+0000 to U+FFFF). */
static const struct ucs_width16 ucs_bmp_ranges[] = {{
""")
for s, e, w in bmp_entries:
macro = "BMP_0WIDTH" if w == 0 else "BMP_2WIDTH"
comment = get_code_point_comment(s, e)
f.write(f"\t{{ {macro}(0x{s:04X}, 0x{e:04X}) }}, {comment}\n")
f.write(f"""\
}};
/* Combined zero- and double-width ranges (non-BMP, U+10000 and above).
* The first {n_chunks} entries host the BMP double-width bitmap in the low
* {BITS_PER_CHUNK} bits of `last`. */
static const struct ucs_width32 ucs_nonbmp_ranges[] = {{
""")
for i, (s, e, w) in enumerate(nonbmp_entries):
macro = "RANGE_0WIDTH" if w == 0 else "RANGE_2WIDTH"
comment = get_code_point_comment(s, e)
if i < len(chunks):
base, end_idx, value = chunks[i]
f.write(
f"\t{{ {macro}(0x{s:05X}, 0x{e:05X}) {comment}\n"
f"\t | BMP_2W_BITS(0b{value:0{BITS_PER_CHUNK}b}) }},"
f" /* BMP entries [{base:>3}..{end_idx:>3}] */\n")
else:
f.write(f"\t{{ {macro}(0x{s:05X}, 0x{e:05X}) }}, {comment}\n")
f.write("};\n")
if __name__ == "__main__":
# Parse command line arguments
parser = argparse.ArgumentParser(description="Generate Unicode width tables")
parser.add_argument("-o", "--output", dest="output_file", default=DEFAULT_OUT_FILE,
help=f"Output file name (default: {DEFAULT_OUT_FILE})")
args = parser.parse_args()
# Write tables to header file
zero_width_ranges, double_width_ranges = create_width_tables()
write_tables(zero_width_ranges, double_width_ranges, out_file=args.output_file)
# Print summary
zero_width_count = sum(end - start + 1 for start, end in zero_width_ranges)
double_width_count = sum(end - start + 1 for start, end in double_width_ranges)
n_zero = len(zero_width_ranges)
n_double = len(double_width_ranges)
print(f"Generated {args.output_file} with:")
print(f"- {n_zero} zero-width ranges covering ~{zero_width_count} code points")
print(f"- {n_double} double-width ranges covering ~{double_width_count} code points")
print(f"- {n_zero + n_double} merged ranges total")
print(f"- Unicode Version: {unicodedata.unidata_version}")