[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

Select the types of activity you want to include in your feed.

perf(wcwidth): add BMP filter + fast path, use single table for binary search

Nate Moore (Jul 2, 2026, 12:16 AM EDT) 32e91018 bcaeb0aa

+261 -217
+105 -111
src/wcwidth.c
··· 1 1 /* wcwidth.c - Unicode character width lookup 2 - * Unicode 16.0 — generated by tasks/gen-wcwidth.ts on 2026-05-26 2 + * Unicode 16.0 — generated by tasks/gen-wcwidth.ts on 2026-05-28 3 3 * 4 4 * Only zero-width (combining marks + default ignorable) and double-width 5 5 * (wide/fullwidth) codepoints are stored. Everything else defaults to ··· 11 11 * DerivedCoreProperties.txt Default_Ignorable_Code_Point → width 0 12 12 * EastAsianWidth.txt W/F properties → width 2 13 13 * 14 - * Packed encoding (*_small_ranges arrays): 14 + * Combining (width 0) and wide (width 2) ranges are merged into a single 15 + * sorted table so wcwidth() needs only one binary search for any codepoint. 16 + * 17 + * BMP coarse filter (bmp_filter): 18 + * 64-byte bitmap, 1 bit per 128-codepoint BMP block. A 0-bit means no 19 + * special codepoints in that block — return width 1 without searching. 20 + * 21 + * Packed encoding (special_small_ranges): 15 22 * Each uint32_t entry packs one Unicode range as 16 - * bits 31–8 start codepoint (fits in 24 bits; Unicode max is U+10FFFF) 17 - * bits 7–0 count of additional codepoints beyond start (0 = single char) 18 - * Arrays are sorted by start so binary search operates on raw uint32_t values. 23 + * bits 31–11 start codepoint (fits in 21 bits; Unicode max is U+10FFFF) 24 + * bits 10–1 count of additional codepoints beyond start (max 1023) 25 + * bit 0 0 = width 0 (combining), 1 = width 2 (wide) 26 + * Array is sorted by start so binary search operates on raw uint32_t values. 19 27 * 20 - * Large-range encoding (*_large_starts / *_large_counts parallel arrays): 21 - * Used for ranges whose span exceeds 255 codepoints — large CJK blocks, 22 - * Hangul syllables, and the Unicode tag character plane. 28 + * Large-range encoding (special_large_* parallel arrays): 29 + * Used for ranges whose span exceeds 1023 codepoints. 23 30 */ 24 31 25 32 #include <stdint.h> 26 33 27 - static const uint32_t combining_small_ranges[] = { 28 - 0x0003006f, 0x00048306, 0x0005912c, 0x0005bf00, 0x0005c101, 0x0005c401, 0x0005c700, 0x0006100a, 29 - 0x00061c00, 0x00064b14, 0x00067000, 0x0006d606, 0x0006df05, 0x0006e701, 0x0006ea03, 0x00071100, 30 - 0x0007301a, 0x0007a60a, 0x0007eb08, 0x0007fd00, 0x00081603, 0x00081b08, 0x00082502, 0x00082904, 31 - 0x00085902, 0x00089708, 0x0008ca17, 0x0008e31f, 0x00093a00, 0x00093c00, 0x00094107, 0x00094d00, 32 - 0x00095106, 0x00096201, 0x00098100, 0x0009bc00, 0x0009c103, 0x0009cd00, 0x0009e201, 0x0009fe00, 33 - 0x000a0101, 0x000a3c00, 0x000a4101, 0x000a4701, 0x000a4b02, 0x000a5100, 0x000a7001, 0x000a7500, 34 - 0x000a8101, 0x000abc00, 0x000ac104, 0x000ac701, 0x000acd00, 0x000ae201, 0x000afa05, 0x000b0100, 35 - 0x000b3c00, 0x000b3f00, 0x000b4103, 0x000b4d00, 0x000b5501, 0x000b6201, 0x000b8200, 0x000bc000, 36 - 0x000bcd00, 0x000c0000, 0x000c0400, 0x000c3c00, 0x000c3e02, 0x000c4602, 0x000c4a03, 0x000c5501, 37 - 0x000c6201, 0x000c8100, 0x000cbc00, 0x000cbf00, 0x000cc600, 0x000ccc01, 0x000ce201, 0x000d0001, 38 - 0x000d3b01, 0x000d4103, 0x000d4d00, 0x000d6201, 0x000d8100, 0x000dca00, 0x000dd202, 0x000dd600, 39 - 0x000e3100, 0x000e3406, 0x000e4707, 0x000eb100, 0x000eb408, 0x000ec806, 0x000f1801, 0x000f3500, 40 - 0x000f3700, 0x000f3900, 0x000f710d, 0x000f8004, 0x000f8601, 0x000f8d0a, 0x000f9923, 0x000fc600, 41 - 0x00102d03, 0x00103205, 0x00103901, 0x00103d01, 0x00105801, 0x00105e02, 0x00107103, 0x00108200, 42 - 0x00108501, 0x00108d00, 0x00109d00, 0x00115f01, 0x00135d02, 0x00171202, 0x00173201, 0x00175201, 43 - 0x00177201, 0x0017b401, 0x0017b706, 0x0017c600, 0x0017c90a, 0x0017dd00, 0x00180b04, 0x00188501, 44 - 0x0018a900, 0x00192002, 0x00192701, 0x00193200, 0x00193902, 0x001a1701, 0x001a1b00, 0x001a5600, 45 - 0x001a5806, 0x001a6000, 0x001a6200, 0x001a6507, 0x001a7309, 0x001a7f00, 0x001ab01e, 0x001b0003, 46 - 0x001b3400, 0x001b3604, 0x001b3c00, 0x001b4200, 0x001b6b08, 0x001b8001, 0x001ba203, 0x001ba801, 47 - 0x001bab02, 0x001be600, 0x001be801, 0x001bed00, 0x001bef02, 0x001c2c07, 0x001c3601, 0x001cd002, 48 - 0x001cd40c, 0x001ce206, 0x001ced00, 0x001cf400, 0x001cf801, 0x001dc03f, 0x00200b04, 0x00202a04, 49 - 0x0020600f, 0x0020d020, 0x002cef02, 0x002d7f00, 0x002de01f, 0x00302a03, 0x00309901, 0x00316400, 50 - 0x00a66f03, 0x00a67409, 0x00a69e01, 0x00a6f001, 0x00a80200, 0x00a80600, 0x00a80b00, 0x00a82501, 51 - 0x00a82c00, 0x00a8c401, 0x00a8e011, 0x00a8ff00, 0x00a92607, 0x00a9470a, 0x00a98002, 0x00a9b300, 52 - 0x00a9b603, 0x00a9bc01, 0x00a9e500, 0x00aa2905, 0x00aa3101, 0x00aa3501, 0x00aa4300, 0x00aa4c00, 53 - 0x00aa7c00, 0x00aab000, 0x00aab202, 0x00aab701, 0x00aabe01, 0x00aac100, 0x00aaec01, 0x00aaf600, 54 - 0x00abe500, 0x00abe800, 0x00abed00, 0x00fb1e00, 0x00fe000f, 0x00fe200f, 0x00feff00, 0x00ffa000, 55 - 0x00fff008, 0x0101fd00, 0x0102e000, 0x01037604, 0x010a0102, 0x010a0501, 0x010a0c03, 0x010a3802, 56 - 0x010a3f00, 0x010ae501, 0x010d2403, 0x010d6904, 0x010eab01, 0x010efc03, 0x010f460a, 0x010f8203, 57 - 0x01100100, 0x0110380e, 0x01107000, 0x01107301, 0x01107f02, 0x0110b303, 0x0110b901, 0x0110c200, 58 - 0x01110002, 0x01112704, 0x01112d07, 0x01117300, 0x01118001, 0x0111b608, 0x0111c903, 0x0111cf00, 59 - 0x01122f02, 0x01123400, 0x01123601, 0x01123e00, 0x01124100, 0x0112df00, 0x0112e307, 0x01130001, 60 - 0x01133b01, 0x01134000, 0x01136606, 0x01137004, 0x0113bb05, 0x0113ce00, 0x0113d000, 0x0113d200, 61 - 0x0113e101, 0x01143807, 0x01144202, 0x01144600, 0x01145e00, 0x0114b305, 0x0114ba00, 0x0114bf01, 62 - 0x0114c201, 0x0115b203, 0x0115bc01, 0x0115bf01, 0x0115dc01, 0x01163307, 0x01163d00, 0x01163f01, 63 - 0x0116ab00, 0x0116ad00, 0x0116b005, 0x0116b700, 0x01171d00, 0x01171f00, 0x01172203, 0x01172704, 64 - 0x01182f08, 0x01183901, 0x01193b01, 0x01193e00, 0x01194300, 0x0119d403, 0x0119da01, 0x0119e000, 65 - 0x011a0109, 0x011a3305, 0x011a3b03, 0x011a4700, 0x011a5105, 0x011a5902, 0x011a8a0c, 0x011a9801, 66 - 0x011c3006, 0x011c3805, 0x011c3f00, 0x011c9215, 0x011caa06, 0x011cb201, 0x011cb501, 0x011d3105, 67 - 0x011d3a00, 0x011d3c01, 0x011d3f06, 0x011d4700, 0x011d9001, 0x011d9500, 0x011d9700, 0x011ef301, 68 - 0x011f0001, 0x011f3604, 0x011f4000, 0x011f4200, 0x011f5a00, 0x01344000, 0x0134470e, 0x01611e0b, 69 - 0x01612d02, 0x016af004, 0x016b3006, 0x016f4f00, 0x016f8f03, 0x016fe400, 0x01bc9d01, 0x01bca003, 70 - 0x01cf002d, 0x01cf3016, 0x01d16702, 0x01d1730f, 0x01d18506, 0x01d1aa03, 0x01d24202, 0x01da0036, 71 - 0x01da3b31, 0x01da7500, 0x01da8400, 0x01da9b04, 0x01daa10e, 0x01e00006, 0x01e00810, 0x01e01b06, 72 - 0x01e02301, 0x01e02604, 0x01e08f00, 0x01e13006, 0x01e2ae00, 0x01e2ec03, 0x01e4ec03, 0x01e5ee01, 73 - 0x01e8d006, 0x01e94406, 34 + static const uint32_t bmp_filter[16] = { 35 + 0xfffffa40, 0x0bf7c047, 0xee40f8c3, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 36 + 0xffffffff, 0xffffffff, 0xffbf33ff, 0xffffffff, 0xffffffff, 0x0000ffff, 0x00000000, 0xf07c0000, 74 37 }; 75 - #define COMBINING_SMALL_RANGE_COUNT 362 76 38 77 - static const uint32_t combining_large_starts[] = { 78 - 0x000e0000, 79 - }; 80 - static const uint16_t combining_large_counts[] = { 81 - 0x0fff, 39 + static const uint32_t special_small_ranges[] = { 40 + 0x001800de, 0x0024180c, 0x002c8858, 0x002df800, 0x002e0802, 0x002e2002, 0x002e3800, 0x00308014, 41 + 0x0030e000, 0x00325828, 0x00338000, 0x0036b00c, 0x0036f80a, 0x00373802, 0x00375006, 0x00388800, 42 + 0x00398034, 0x003d3014, 0x003f5810, 0x003fe800, 0x0040b006, 0x0040d810, 0x00412804, 0x00414808, 43 + 0x0042c804, 0x0044b810, 0x0046502e, 0x0047183e, 0x0049d000, 0x0049e000, 0x004a080e, 0x004a6800, 44 + 0x004a880c, 0x004b1002, 0x004c0800, 0x004de000, 0x004e0806, 0x004e6800, 0x004f1002, 0x004ff000, 45 + 0x00500802, 0x0051e000, 0x00520802, 0x00523802, 0x00525804, 0x00528800, 0x00538002, 0x0053a800, 46 + 0x00540802, 0x0055e000, 0x00560808, 0x00563802, 0x00566800, 0x00571002, 0x0057d00a, 0x00580800, 47 + 0x0059e000, 0x0059f800, 0x005a0806, 0x005a6800, 0x005aa802, 0x005b1002, 0x005c1000, 0x005e0000, 48 + 0x005e6800, 0x00600000, 0x00602000, 0x0061e000, 0x0061f004, 0x00623004, 0x00625006, 0x0062a802, 49 + 0x00631002, 0x00640800, 0x0065e000, 0x0065f800, 0x00663000, 0x00666002, 0x00671002, 0x00680002, 50 + 0x0069d802, 0x006a0806, 0x006a6800, 0x006b1002, 0x006c0800, 0x006e5000, 0x006e9004, 0x006eb000, 51 + 0x00718800, 0x0071a00c, 0x0072380e, 0x00758800, 0x0075a010, 0x0076400c, 0x0078c002, 0x0079a800, 52 + 0x0079b800, 0x0079c800, 0x007b881a, 0x007c0008, 0x007c3002, 0x007c6814, 0x007cc846, 0x007e3000, 53 + 0x00816806, 0x0081900a, 0x0081c802, 0x0081e802, 0x0082c002, 0x0082f004, 0x00838806, 0x00841000, 54 + 0x00842802, 0x00846800, 0x0084e800, 0x008800bd, 0x008af802, 0x009ae804, 0x00b89004, 0x00b99002, 55 + 0x00ba9002, 0x00bb9002, 0x00bda002, 0x00bdb80c, 0x00be3000, 0x00be4814, 0x00bee800, 0x00c05808, 56 + 0x00c42802, 0x00c54800, 0x00c90004, 0x00c93802, 0x00c99000, 0x00c9c804, 0x00d0b802, 0x00d0d800, 57 + 0x00d2b000, 0x00d2c00c, 0x00d30000, 0x00d31000, 0x00d3280e, 0x00d39812, 0x00d3f800, 0x00d5803c, 58 + 0x00d80006, 0x00d9a000, 0x00d9b008, 0x00d9e000, 0x00da1000, 0x00db5810, 0x00dc0002, 0x00dd1006, 59 + 0x00dd4002, 0x00dd5804, 0x00df3000, 0x00df4002, 0x00df6800, 0x00df7804, 0x00e1600e, 0x00e1b002, 60 + 0x00e68004, 0x00e6a018, 0x00e7100c, 0x00e76800, 0x00e7a000, 0x00e7c002, 0x00ee007e, 0x01005808, 61 + 0x01015008, 0x0103001e, 0x01068040, 0x0118d003, 0x01194803, 0x011f4807, 0x011f8001, 0x011f9801, 62 + 0x012fe803, 0x0130a003, 0x0131800f, 0x01324017, 0x0133f801, 0x0134500b, 0x01349801, 0x01350801, 63 + 0x01355003, 0x0135e803, 0x01362003, 0x01367001, 0x0136a001, 0x01375001, 0x01379003, 0x0137a801, 64 + 0x0137d001, 0x0137e801, 0x01382801, 0x01385003, 0x01394001, 0x013a6001, 0x013a7001, 0x013a9805, 65 + 0x013ab801, 0x013ca805, 0x013d8001, 0x013df801, 0x0158d803, 0x015a8001, 0x015aa801, 0x01677804, 66 + 0x016bf800, 0x016f003e, 0x01740033, 0x0174d8b1, 0x017801ab, 0x017f8073, 0x01815006, 0x01817021, 67 + 0x018208ab, 0x0184c802, 0x0184d8c9, 0x01882855, 0x01898865, 0x018b2000, 0x018b2853, 0x018c80ab, 68 + 0x018f785f, 0x0191004f, 0x0524806d, 0x05337806, 0x0533a012, 0x0534f002, 0x05378002, 0x05401000, 69 + 0x05403000, 0x05405800, 0x05412802, 0x05416000, 0x05462002, 0x05470022, 0x0547f800, 0x0549300e, 70 + 0x054a3814, 0x054b0039, 0x054c0004, 0x054d9800, 0x054db006, 0x054de002, 0x054f2800, 0x0551480a, 71 + 0x05518802, 0x0551a802, 0x05521800, 0x05526000, 0x0553e000, 0x05558000, 0x05559004, 0x0555b802, 72 + 0x0555f002, 0x05560800, 0x05576002, 0x0557b000, 0x055f2800, 0x055f4000, 0x055f6800, 0x07c803ff, 73 + 0x07d8f000, 0x07f0001e, 0x07f08013, 0x07f1001e, 0x07f18045, 0x07f2a025, 0x07f34007, 0x07f7f800, 74 + 0x07f808bf, 0x07fd0000, 0x07ff000d, 0x07ff8010, 0x080fe800, 0x08170000, 0x081bb008, 0x08500804, 75 + 0x08502802, 0x08506006, 0x0851c004, 0x0851f800, 0x08572802, 0x08692006, 0x086b4808, 0x08755802, 76 + 0x0877e006, 0x087a3014, 0x087c1006, 0x08800800, 0x0881c01c, 0x08838000, 0x08839802, 0x0883f804, 77 + 0x08859806, 0x0885c802, 0x08861000, 0x08880004, 0x08893808, 0x0889680e, 0x088b9800, 0x088c0002, 78 + 0x088db010, 0x088e4806, 0x088e7800, 0x08917804, 0x0891a000, 0x0891b002, 0x0891f000, 0x08920800, 79 + 0x0896f800, 0x0897180e, 0x08980002, 0x0899d802, 0x089a0000, 0x089b300c, 0x089b8008, 0x089dd80a, 80 + 0x089e7000, 0x089e8000, 0x089e9000, 0x089f0802, 0x08a1c00e, 0x08a21004, 0x08a23000, 0x08a2f000, 81 + 0x08a5980a, 0x08a5d000, 0x08a5f802, 0x08a61002, 0x08ad9006, 0x08ade002, 0x08adf802, 0x08aee002, 82 + 0x08b1980e, 0x08b1e800, 0x08b1f802, 0x08b55800, 0x08b56800, 0x08b5800a, 0x08b5b800, 0x08b8e800, 83 + 0x08b8f800, 0x08b91006, 0x08b93808, 0x08c17810, 0x08c1c802, 0x08c9d802, 0x08c9f000, 0x08ca1800, 84 + 0x08cea006, 0x08ced002, 0x08cf0000, 0x08d00812, 0x08d1980a, 0x08d1d806, 0x08d23800, 0x08d2880a, 85 + 0x08d2c804, 0x08d45018, 0x08d4c002, 0x08e1800c, 0x08e1c00a, 0x08e1f800, 0x08e4902a, 0x08e5500c, 86 + 0x08e59002, 0x08e5a802, 0x08e9880a, 0x08e9d000, 0x08e9e002, 0x08e9f80c, 0x08ea3800, 0x08ec8002, 87 + 0x08eca800, 0x08ecb800, 0x08f79802, 0x08f80002, 0x08f9b008, 0x08fa0000, 0x08fa1000, 0x08fad000, 88 + 0x09a20000, 0x09a2381c, 0x0b08f016, 0x0b096804, 0x0b578008, 0x0b59800c, 0x0b7a7800, 0x0b7c7806, 89 + 0x0b7f0007, 0x0b7f2000, 0x0b7f8003, 0x0c67f813, 0x0d7f8007, 0x0d7fa80d, 0x0d7fe803, 0x0d800245, 90 + 0x0d899001, 0x0d8a8005, 0x0d8aa801, 0x0d8b2007, 0x0d8b8317, 0x0de4e802, 0x0de50006, 0x0e78005a, 91 + 0x0e79802c, 0x0e8b3804, 0x0e8b981e, 0x0e8c280c, 0x0e8d5006, 0x0e921004, 0x0e9800ad, 0x0e9b002d, 92 + 0x0ed0006c, 0x0ed1d862, 0x0ed3a800, 0x0ed42000, 0x0ed4d808, 0x0ed5081c, 0x0f00000c, 0x0f004020, 93 + 0x0f00d80c, 0x0f011802, 0x0f013008, 0x0f047800, 0x0f09800c, 0x0f157000, 0x0f176006, 0x0f276006, 94 + 0x0f2f7002, 0x0f46800c, 0x0f4a200c, 0x0f802001, 0x0f867801, 0x0f8c7001, 0x0f8c8813, 0x0f900005, 95 + 0x0f908057, 0x0f920011, 0x0f928003, 0x0f93000b, 0x0f980041, 0x0f996811, 0x0f99b88b, 0x0f9bf02b, 96 + 0x0f9d0055, 0x0f9e7809, 0x0f9f0021, 0x0f9fa001, 0x0f9fc08d, 0x0fa20001, 0x0fa21175, 0x0fa7f87d, 97 + 0x0faa5807, 0x0faa802f, 0x0fabd001, 0x0faca803, 0x0fad2001, 0x0fafd8a9, 0x0fb4008b, 0x0fb66001, 98 + 0x0fb68005, 0x0fb6a805, 0x0fb6e007, 0x0fb75803, 0x0fb7a011, 0x0fbf0017, 0x0fbf8001, 0x0fc8605d, 99 + 0x0fc9e013, 0x0fca3971, 0x0fd38019, 0x0fd40013, 0x0fd4786f, 0x0fd6701d, 0x0fd6f815, 0x0fd78011, 82 100 }; 83 - #define COMBINING_LARGE_RANGE_COUNT 1 101 + #define SPECIAL_SMALL_COUNT 480 84 102 85 - static const uint32_t wide_small_ranges[] = { 86 - 0x0011005f, 0x00231a01, 0x00232901, 0x0023e903, 0x0023f000, 0x0023f300, 0x0025fd01, 0x00261401, 87 - 0x00263007, 0x0026480b, 0x00267f00, 0x00268a05, 0x00269300, 0x0026a100, 0x0026aa01, 0x0026bd01, 88 - 0x0026c401, 0x0026ce00, 0x0026d400, 0x0026ea00, 0x0026f201, 0x0026f500, 0x0026fa00, 0x0026fd00, 89 - 0x00270500, 0x00270a01, 0x00272800, 0x00274c00, 0x00274e00, 0x00275302, 0x00275700, 0x00279502, 90 - 0x0027b000, 0x0027bf00, 0x002b1b01, 0x002b5000, 0x002b5500, 0x002e8019, 0x002e9b58, 0x002f00d5, 91 - 0x002ff04e, 0x00304155, 0x00309966, 0x0031052a, 0x0031315d, 0x00319055, 0x0031ef2f, 0x00322027, 92 - 0x00a49036, 0x00a9601c, 0x00fe1009, 0x00fe3022, 0x00fe5412, 0x00fe6803, 0x00ff015f, 0x00ffe006, 93 - 0x016fe004, 0x016ff001, 0x018cff09, 0x01aff003, 0x01aff506, 0x01affd01, 0x01b13200, 0x01b15002, 94 - 0x01b15500, 0x01b16403, 0x01d30056, 0x01d36016, 0x01f00400, 0x01f0cf00, 0x01f18e00, 0x01f19109, 95 - 0x01f20002, 0x01f2102b, 0x01f24008, 0x01f25001, 0x01f26005, 0x01f30020, 0x01f32d08, 0x01f33745, 96 - 0x01f37e15, 0x01f3a02a, 0x01f3cf04, 0x01f3e010, 0x01f3f400, 0x01f3f846, 0x01f44000, 0x01f442ba, 97 - 0x01f4ff3e, 0x01f54b03, 0x01f55017, 0x01f57a00, 0x01f59501, 0x01f5a400, 0x01f5fb54, 0x01f68045, 98 - 0x01f6cc00, 0x01f6d002, 0x01f6d502, 0x01f6dc03, 0x01f6eb01, 0x01f6f408, 0x01f7e00b, 0x01f7f000, 99 - 0x01f90c2e, 0x01f93c09, 0x01f947b8, 0x01fa700c, 0x01fa8009, 0x01fa8f37, 0x01face0e, 0x01fadf0a, 100 - 0x01faf008, 103 + static const uint32_t special_large_starts[] = { 104 + 0x00003250, 0x0000ac00, 0x00017000, 0x00018800, 0x00020000, 0x00030000, 0x000e0000, 101 105 }; 102 - #define WIDE_SMALL_RANGE_COUNT 113 103 - 104 - static const uint32_t wide_large_starts[] = { 105 - 0x00003250, 0x0000ac00, 0x0000f900, 0x00017000, 0x00018800, 0x0001b000, 0x0001b170, 0x00020000, 106 - 0x00030000, 106 + static const uint16_t special_large_counts[] = { 107 + 0x723c, 0x2ba3, 0x17f7, 0x04d5, 0xfffd, 0xfffd, 0x0fff, 107 108 }; 108 - static const uint16_t wide_large_counts[] = { 109 - 0x723c, 0x2ba3, 0x01ff, 0x17f7, 0x04d5, 0x0122, 0x018b, 0xfffd, 110 - 0xfffd, 109 + static const uint8_t special_large_widths[] = { 110 + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x00, 111 111 }; 112 - #define WIDE_LARGE_RANGE_COUNT 9 112 + #define SPECIAL_LARGE_COUNT 7 113 113 114 - static int codepoint_in_range( 115 - const uint32_t *starts, const uint16_t *counts, int length, uint32_t codepoint 116 - ) { 117 - int left = 0, right = length - 1; 114 + static int codepoint_in_special(uint32_t codepoint) { 115 + if (codepoint <= 0xffff) { 116 + uint32_t block = codepoint >> 7; 117 + if (!((bmp_filter[block >> 5] >> (block & 31u)) & 1u)) return 1; 118 + } 119 + int left = 0, right = SPECIAL_SMALL_COUNT - 1; 118 120 while (left <= right) { 119 121 int mid = (left + right) / 2; 120 - if (codepoint < starts[mid]) right = mid - 1; 121 - else if (codepoint > starts[mid] + counts[mid]) left = mid + 1; 122 - else return 1; 122 + uint32_t entry = special_small_ranges[mid]; 123 + uint32_t start = entry >> 11; 124 + if (codepoint < start) right = mid - 1; 125 + else if (codepoint > start + ((entry >> 1) & 0x3FF)) left = mid + 1; 126 + else return (entry & 1) ? 2 : 0; 123 127 } 124 - return 0; 125 - } 126 - 127 - static int codepoint_in_packed_range(const uint32_t *ranges, int length, uint32_t codepoint) { 128 - int left = 0, right = length - 1; 128 + left = 0; right = SPECIAL_LARGE_COUNT - 1; 129 129 while (left <= right) { 130 - int mid = (left + right) / 2; 131 - uint32_t entry = ranges[mid]; 132 - uint32_t start = entry >> 8; 133 - if (codepoint < start) right = mid - 1; 134 - else if (codepoint > start + (entry & 0xff)) left = mid + 1; 135 - else return 1; 130 + int mid = (left + right) / 2; 131 + if (codepoint < special_large_starts[mid]) right = mid - 1; 132 + else if (codepoint > special_large_starts[mid] + special_large_counts[mid]) left = mid + 1; 133 + else return special_large_widths[mid]; 136 134 } 137 - return 0; 135 + return 1; 138 136 } 139 137 140 138 int wcwidth(uint32_t codepoint) { ··· 144 142 return 1; 145 143 if (codepoint < 0x20 || (codepoint > 0x7e && codepoint < 0xa0)) 146 144 return codepoint == 0 ? 0 : -1; 147 - if (codepoint_in_packed_range(combining_small_ranges, COMBINING_SMALL_RANGE_COUNT, codepoint)) return 0; 148 - if (codepoint_in_range(combining_large_starts, combining_large_counts, COMBINING_LARGE_RANGE_COUNT, codepoint)) return 0; 149 - if (codepoint_in_packed_range(wide_small_ranges, WIDE_SMALL_RANGE_COUNT, codepoint)) return 2; 150 - if (codepoint_in_range(wide_large_starts, wide_large_counts, WIDE_LARGE_RANGE_COUNT, codepoint)) return 2; 151 - return 1; 145 + return codepoint_in_special(codepoint); 152 146 } 153 147 154 148 int iswprint(uint32_t codepoint) { return wcwidth(codepoint) >= 0; }
+156 -106
tasks/gen-wcwidth.ts
··· 1 1 // gen-wcwidth.ts — generate src/wcwidth.c from Unicode 16.0 data 2 2 // Usage: deno task gen-wcwidth 3 3 // 4 - // Packed encoding (*_small_ranges): each uint32_t packs start codepoint in 5 - // bits 31–8 and count in bits 7–0. Large ranges (count > 255) use separate 6 - // starts[] + counts[] arrays. 4 + // Packed encoding (special_small_ranges): each uint32_t packs start codepoint 5 + // in bits 31–11, count in bits 10–1, and a wide bit in bit 0. Large ranges 6 + // (count > 1023) use separate starts[] + counts[] + widths[] arrays. 7 + // 8 + // BMP coarse filter: a 64-byte bitmap (1 bit per 128-codepoint block) gates 9 + // the binary search. Clean blocks return width 1 without touching the table. 7 10 8 11 const UNICODE_BASE = "https://www.unicode.org/Public/16.0.0/ucd"; 9 12 10 13 interface Interval { 11 14 start: number; 12 15 end: number; // inclusive 16 + } 17 + 18 + interface TaggedInterval extends Interval { 19 + width: 0 | 2; 13 20 } 14 21 15 22 async function fetchText(path: string): Promise<string> { ··· 95 102 return merged; 96 103 } 97 104 105 + /** 106 + * Subtracts `mask` intervals from `base`, returning codepoints in `base` that 107 + * are not covered by any interval in `mask`. Used to let combining marks take 108 + * priority over wide ranges when the same codepoint appears in both tables. 109 + */ 110 + function subtractIntervals(base: Interval[], mask: Interval[]): Interval[] { 111 + let result = [...base]; 112 + for (const m of mask) { 113 + const next: Interval[] = []; 114 + for (const b of result) { 115 + if (m.end < b.start || m.start > b.end) { 116 + next.push(b); 117 + } else { 118 + if (b.start < m.start) next.push({ start: b.start, end: m.start - 1 }); 119 + if (b.end > m.end) next.push({ start: m.end + 1, end: b.end }); 120 + } 121 + } 122 + result = next; 123 + } 124 + return result; 125 + } 126 + 98 127 function assertNoAdjacentRanges(intervals: Interval[], label: string): void { 99 128 for (let index = 1; index < intervals.length; index++) { 100 129 const previous = intervals[index - 1]; ··· 102 131 if (current.start <= previous.end + 1) { 103 132 throw new Error( 104 133 `${label}: adjacent ranges at index ${index}: ` + 105 - `[0x${previous.start.toString(16)}, 0x${ 106 - previous.end.toString(16) 107 - }] ` + 108 - `and [0x${current.start.toString(16)}, 0x${ 109 - current.end.toString(16) 110 - }]`, 134 + `[0x${previous.start.toString(16)}, 0x${previous.end.toString(16)}] ` + 135 + `and [0x${current.start.toString(16)}, 0x${current.end.toString(16)}]`, 111 136 ); 112 137 } 113 138 } ··· 119 144 120 145 function formatUint16Hex(value: number): string { 121 146 return `0x${value.toString(16).padStart(4, "0")}`; 147 + } 148 + 149 + function formatUint8Hex(value: number): string { 150 + return `0x${value.toString(16).padStart(2, "0")}`; 122 151 } 123 152 124 153 function formatUint32Array(values: number[], indent = " "): string { ··· 139 168 return lines.join("\n"); 140 169 } 141 170 171 + function formatUint8Array(values: number[], indent = " "): string { 172 + const lines: string[] = []; 173 + for (let index = 0; index < values.length; index += 8) { 174 + const chunk = values.slice(index, index + 8); 175 + lines.push(indent + chunk.map(formatUint8Hex).join(", ") + ","); 176 + } 177 + return lines.join("\n"); 178 + } 179 + 142 180 const [derivedCategoryText, derivedCorePropsText, eastAsianWidthText] = 143 181 await Promise.all([ 144 182 fetchText("extracted/DerivedGeneralCategory.txt"), ··· 172 210 const wideIntervals = mergeIntervals(parseWideEastAsian(eastAsianWidthText)); 173 211 assertNoAdjacentRanges(wideIntervals, "wide"); 174 212 175 - const combiningRanges = combiningIntervals.map((interval) => ({ 176 - start: interval.start, 177 - count: interval.end - interval.start, 178 - })); 213 + // Strip any codepoints that are in both tables — combining takes priority. 214 + // U+115F (Hangul Choseong Filler) is the known overlap in Unicode 16.0. 215 + const pureWideIntervals = subtractIntervals(wideIntervals, combiningIntervals); 216 + 217 + // Merge combining (width 0) and pure-wide (width 2) into a single sorted table. 218 + const allSpecialIntervals: TaggedInterval[] = [ 219 + ...combiningIntervals.map((i) => ({ ...i, width: 0 as const })), 220 + ...pureWideIntervals.map((i) => ({ ...i, width: 2 as const })), 221 + ].sort((a, b) => a.start - b.start); 222 + 223 + for (let index = 1; index < allSpecialIntervals.length; index++) { 224 + const prev = allSpecialIntervals[index - 1]; 225 + const curr = allSpecialIntervals[index]; 226 + if (curr.start <= prev.end) { 227 + throw new Error( 228 + `combining/wide overlap at 0x${curr.start.toString(16)} ` + 229 + `(prev width=${prev.width} ends at 0x${prev.end.toString(16)})`, 230 + ); 231 + } 232 + } 179 233 180 - const wideRanges = wideIntervals.map((interval) => ({ 181 - start: interval.start, 182 - count: interval.end - interval.start, 234 + const allRanges = allSpecialIntervals.map((i) => ({ 235 + start: i.start, 236 + count: i.end - i.start, 237 + width: i.width, 183 238 })); 184 239 185 - for (const range of [...combiningRanges, ...wideRanges]) { 186 - if (range.start > 0xffffff) { 240 + for (const range of allRanges) { 241 + if (range.start > 0x1fffff) { 187 242 throw new Error( 188 - `Range start 0x${ 189 - range.start.toString(16) 190 - } exceeds 24 bits — packed encoding broken`, 243 + `Range start 0x${range.start.toString(16)} exceeds 21 bits — packed encoding broken`, 191 244 ); 192 245 } 193 246 } ··· 195 248 // Unicode 16.0's DerivedCoreProperties.txt includes E0000..E0FFF as a single 196 249 // Default_Ignorable block (count = 4095), so combining ranges also need the 197 250 // small/large split — not just wide ranges. 198 - const combiningSmallRanges = combiningRanges.filter((range) => 199 - range.count <= 255 200 - ); 201 - const combiningLargeRanges = combiningRanges.filter((range) => 202 - range.count > 255 203 - ); 204 - const wideSmallRanges = wideRanges.filter((range) => range.count <= 255); 205 - const wideLargeRanges = wideRanges.filter((range) => range.count > 255); 251 + // Threshold is 1023 (10-bit count field) rather than 255 to absorb more wide ranges into small. 252 + const smallRanges = allRanges.filter((range) => range.count <= 1023); 253 + const largeRanges = allRanges.filter((range) => range.count > 1023); 206 254 207 - const combiningSmallPacked = combiningSmallRanges.map( 208 - (range) => (range.start << 8) | range.count, 209 - ); 210 - const wideSmallPacked = wideSmallRanges.map( 211 - (range) => (range.start << 8) | range.count, 255 + const smallPacked = smallRanges.map( 256 + (range) => (range.start << 11) | (range.count << 1) | (range.width === 2 ? 1 : 0), 212 257 ); 213 - const combiningLargeStarts = combiningLargeRanges.map((range) => range.start); 214 - const combiningLargeCounts = combiningLargeRanges.map((range) => range.count); 215 - const wideLargeStarts = wideLargeRanges.map((range) => range.start); 216 - const wideLargeCounts = wideLargeRanges.map((range) => range.count); 217 258 218 - for (let index = 1; index < combiningSmallPacked.length; index++) { 219 - if (combiningSmallPacked[index] <= combiningSmallPacked[index - 1]) { 259 + for (let index = 1; index < smallPacked.length; index++) { 260 + if (smallPacked[index] <= smallPacked[index - 1]) { 220 261 throw new Error( 221 - `combining_small_ranges not strictly increasing at index ${index}`, 262 + `special_small_ranges not strictly increasing at index ${index}`, 222 263 ); 223 264 } 224 265 } 225 - for (let index = 1; index < wideSmallPacked.length; index++) { 226 - if (wideSmallPacked[index] <= wideSmallPacked[index - 1]) { 227 - throw new Error( 228 - `wide_small_ranges not strictly increasing at index ${index}`, 229 - ); 266 + 267 + const largeStarts = largeRanges.map((range) => range.start); 268 + const largeCounts = largeRanges.map((range) => range.count); 269 + const largeWidths = largeRanges.map((range) => range.width); 270 + 271 + // BMP coarse filter: 64-byte bitmap, 1 bit per 128-codepoint block. 272 + // A 0-bit means no special codepoints in that block — skip the binary search. 273 + const bmpFilter = new Uint32Array(16); 274 + for (const range of allRanges) { 275 + if (range.start > 0xffff) continue; 276 + const endCp = Math.min(range.start + range.count, 0xffff); 277 + const startBlock = range.start >> 7; 278 + const endBlock = endCp >> 7; 279 + for (let block = startBlock; block <= endBlock; block++) { 280 + bmpFilter[block >> 5] |= (1 << (block & 31)); 230 281 } 282 + } 283 + let dirtyBlocks = 0; 284 + for (const w of bmpFilter) { 285 + let x = w; 286 + while (x) { x &= x - 1; dirtyBlocks++; } 231 287 } 232 288 233 - const tableBytes = combiningSmallPacked.length * 4 + 234 - combiningLargeStarts.length * 4 + 235 - combiningLargeCounts.length * 2 + 236 - wideSmallPacked.length * 4 + 237 - wideLargeStarts.length * 4 + 238 - wideLargeCounts.length * 2; 289 + const tableBytes = 290 + 16 * 4 + // bmp_filter 291 + smallPacked.length * 4 + 292 + largeStarts.length * 4 + 293 + largeCounts.length * 2 + 294 + largeWidths.length * 1; 239 295 240 - console.error(`combining_small_ranges: ${combiningSmallPacked.length} entries`); 241 - console.error(`combining_large_ranges: ${combiningLargeRanges.length} entries`); 242 - console.error(`wide_small_ranges: ${wideSmallPacked.length} entries`); 243 - console.error(`wide_large_ranges: ${wideLargeRanges.length} entries`); 244 - console.error(`Table data: ${tableBytes} bytes`); 296 + console.error(`special_small_ranges: ${smallPacked.length} entries`); 297 + console.error(`special_large_ranges: ${largeRanges.length} entries`); 298 + console.error(`BMP dirty blocks: ${dirtyBlocks} / 512`); 299 + console.error(`Table data: ${tableBytes} bytes`); 245 300 246 301 const date = new Date().toISOString().slice(0, 10); 247 302 ··· 259 314 * DerivedCoreProperties.txt Default_Ignorable_Code_Point → width 0 260 315 * EastAsianWidth.txt W/F properties → width 2 261 316 * 262 - * Packed encoding (*_small_ranges arrays): 317 + * Combining (width 0) and wide (width 2) ranges are merged into a single 318 + * sorted table so wcwidth() needs only one binary search for any codepoint. 319 + * 320 + * BMP coarse filter (bmp_filter): 321 + * 64-byte bitmap, 1 bit per 128-codepoint BMP block. A 0-bit means no 322 + * special codepoints in that block — return width 1 without searching. 323 + * 324 + * Packed encoding (special_small_ranges): 263 325 * Each uint32_t entry packs one Unicode range as 264 - * bits 31–8 start codepoint (fits in 24 bits; Unicode max is U+10FFFF) 265 - * bits 7–0 count of additional codepoints beyond start (0 = single char) 266 - * Arrays are sorted by start so binary search operates on raw uint32_t values. 326 + * bits 31–11 start codepoint (fits in 21 bits; Unicode max is U+10FFFF) 327 + * bits 10–1 count of additional codepoints beyond start (max 1023) 328 + * bit 0 0 = width 0 (combining), 1 = width 2 (wide) 329 + * Array is sorted by start so binary search operates on raw uint32_t values. 267 330 * 268 - * Large-range encoding (*_large_starts / *_large_counts parallel arrays): 269 - * Used for ranges whose span exceeds 255 codepoints — large CJK blocks, 270 - * Hangul syllables, and the Unicode tag character plane. 331 + * Large-range encoding (special_large_* parallel arrays): 332 + * Used for ranges whose span exceeds 1023 codepoints. 271 333 */ 272 334 273 335 #include <stdint.h> 274 336 275 - static const uint32_t combining_small_ranges[] = { 276 - ${formatUint32Array(combiningSmallPacked)} 337 + static const uint32_t bmp_filter[16] = { 338 + ${formatUint32Array(Array.from(bmpFilter))} 277 339 }; 278 - #define COMBINING_SMALL_RANGE_COUNT ${combiningSmallPacked.length} 279 340 280 - static const uint32_t combining_large_starts[] = { 281 - ${formatUint32Array(combiningLargeStarts)} 341 + static const uint32_t special_small_ranges[] = { 342 + ${formatUint32Array(smallPacked)} 282 343 }; 283 - static const uint16_t combining_large_counts[] = { 284 - ${formatUint16Array(combiningLargeCounts)} 285 - }; 286 - #define COMBINING_LARGE_RANGE_COUNT ${combiningLargeRanges.length} 344 + #define SPECIAL_SMALL_COUNT ${smallPacked.length} 287 345 288 - static const uint32_t wide_small_ranges[] = { 289 - ${formatUint32Array(wideSmallPacked)} 346 + static const uint32_t special_large_starts[] = { 347 + ${formatUint32Array(largeStarts)} 290 348 }; 291 - #define WIDE_SMALL_RANGE_COUNT ${wideSmallPacked.length} 292 - 293 - static const uint32_t wide_large_starts[] = { 294 - ${formatUint32Array(wideLargeStarts)} 349 + static const uint16_t special_large_counts[] = { 350 + ${formatUint16Array(largeCounts)} 295 351 }; 296 - static const uint16_t wide_large_counts[] = { 297 - ${formatUint16Array(wideLargeCounts)} 352 + static const uint8_t special_large_widths[] = { 353 + ${formatUint8Array(largeWidths)} 298 354 }; 299 - #define WIDE_LARGE_RANGE_COUNT ${wideLargeRanges.length} 355 + #define SPECIAL_LARGE_COUNT ${largeRanges.length} 300 356 301 - static int codepoint_in_range( 302 - const uint32_t *starts, const uint16_t *counts, int length, uint32_t codepoint 303 - ) { 304 - int left = 0, right = length - 1; 357 + static int codepoint_in_special(uint32_t codepoint) { 358 + if (codepoint <= 0xffff) { 359 + uint32_t block = codepoint >> 7; 360 + if (!((bmp_filter[block >> 5] >> (block & 31u)) & 1u)) return 1; 361 + } 362 + int left = 0, right = SPECIAL_SMALL_COUNT - 1; 305 363 while (left <= right) { 306 364 int mid = (left + right) / 2; 307 - if (codepoint < starts[mid]) right = mid - 1; 308 - else if (codepoint > starts[mid] + counts[mid]) left = mid + 1; 309 - else return 1; 365 + uint32_t entry = special_small_ranges[mid]; 366 + uint32_t start = entry >> 11; 367 + if (codepoint < start) right = mid - 1; 368 + else if (codepoint > start + ((entry >> 1) & 0x3FF)) left = mid + 1; 369 + else return (entry & 1) ? 2 : 0; 310 370 } 311 - return 0; 312 - } 313 - 314 - static int codepoint_in_packed_range(const uint32_t *ranges, int length, uint32_t codepoint) { 315 - int left = 0, right = length - 1; 371 + left = 0; right = SPECIAL_LARGE_COUNT - 1; 316 372 while (left <= right) { 317 - int mid = (left + right) / 2; 318 - uint32_t entry = ranges[mid]; 319 - uint32_t start = entry >> 8; 320 - if (codepoint < start) right = mid - 1; 321 - else if (codepoint > start + (entry & 0xff)) left = mid + 1; 322 - else return 1; 373 + int mid = (left + right) / 2; 374 + if (codepoint < special_large_starts[mid]) right = mid - 1; 375 + else if (codepoint > special_large_starts[mid] + special_large_counts[mid]) left = mid + 1; 376 + else return special_large_widths[mid]; 323 377 } 324 - return 0; 378 + return 1; 325 379 } 326 380 327 381 int wcwidth(uint32_t codepoint) { ··· 331 385 return 1; 332 386 if (codepoint < 0x20 || (codepoint > 0x7e && codepoint < 0xa0)) 333 387 return codepoint == 0 ? 0 : -1; 334 - if (codepoint_in_packed_range(combining_small_ranges, COMBINING_SMALL_RANGE_COUNT, codepoint)) return 0; 335 - if (codepoint_in_range(combining_large_starts, combining_large_counts, COMBINING_LARGE_RANGE_COUNT, codepoint)) return 0; 336 - if (codepoint_in_packed_range(wide_small_ranges, WIDE_SMALL_RANGE_COUNT, codepoint)) return 2; 337 - if (codepoint_in_range(wide_large_starts, wide_large_counts, WIDE_LARGE_RANGE_COUNT, codepoint)) return 2; 338 - return 1; 388 + return codepoint_in_special(codepoint); 339 389 } 340 390 341 391 int iswprint(uint32_t codepoint) { return wcwidth(codepoint) >= 0; }