/rust/registry/src/index.crates.io-1949cf8c6b5b557f/unicode-id-start-1.4.0/src/lib.rs
Line | Count | Source |
1 | | //! [![github]](https://github.com/Boshen/unicode-id-start) [![crates-io]](https://crates.io/crates/unicode-id-start) [![docs-rs]](https://docs.rs/unicode-id-start) |
2 | | //! |
3 | | //! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github |
4 | | //! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust |
5 | | //! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs |
6 | | //! |
7 | | //! <br> |
8 | | //! |
9 | | //! Implementation of [Unicode Standard Annex #31][tr31] for determining which |
10 | | //! `char` values are valid in programming language identifiers. |
11 | | //! |
12 | | //! [tr31]: https://www.unicode.org/reports/tr31/ |
13 | | //! |
14 | | //! This crate is a better optimized implementation of the older `unicode-id` |
15 | | //! crate. This crate uses less static storage, and is able to classify both |
16 | | //! ASCII and non-ASCII codepoints with better performance, 2–10× |
17 | | //! faster than `unicode-id`. |
18 | | //! |
19 | | //! <br> |
20 | | //! |
21 | | //! ## Comparison of performance |
22 | | //! |
23 | | //! The following table shows a comparison between five Unicode identifier |
24 | | //! implementations. |
25 | | //! |
26 | | //! - `unicode-id-start` is this crate, which is a fork of `unicode-ident`; |
27 | | //! - [`unicode-xid`] is a widely used crate run by the "unicode-rs" org; [`unicode-id`] is a fork of `unicode-xid`; |
28 | | //! - `ucd-trie` and `fst` are two data structures supported by the |
29 | | //! [`ucd-generate`] tool; |
30 | | //! - [`roaring`] is a Rust implementation of Roaring bitmap. |
31 | | //! |
32 | | //! The *static storage* column shows the total size of `static` tables that the |
33 | | //! crate bakes into your binary, measured in 1000s of bytes. |
34 | | //! |
35 | | //! The remaining columns show the **cost per call** to evaluate whether a |
36 | | //! single `char` has the ID\_Start or ID\_Continue Unicode property, |
37 | | //! comparing across different ratios of ASCII to non-ASCII codepoints in the |
38 | | //! input data. |
39 | | //! |
40 | | //! [`unicode-ident`]: https://github.com/dtolnay/unicode-ident |
41 | | //! [`unicode-xid`]: https://github.com/unicode-rs/unicode-xid |
42 | | //! [`unicode-id`]: https://github.com/Boshen/unicode-id |
43 | | //! [`ucd-generate`]: https://github.com/BurntSushi/ucd-generate |
44 | | //! [`roaring`]: https://github.com/RoaringBitmap/roaring-rs |
45 | | //! |
46 | | //! | | static storage | 0% nonascii | 1% | 10% | 100% nonascii | |
47 | | //! |---|---|---|---|---|---| |
48 | | //! | **`unicode-id-start`** | 9.68 K | 0.96 ns | 0.95 ns | 1.09 ns | 1.55 ns | |
49 | | //! | **`unicode-id`** | 11.23 K | 1.88 ns | 2.14 ns | 3.48 ns | 15.63 ns | |
50 | | //! | **`ucd-trie`** | 9.93 K | 1.29 ns | 1.28 ns | 1.36 ns | 2.15 ns | |
51 | | //! | **`fst`** | 131 K | 55.1 ns | 54.9 ns | 53.2 ns | 28.5 ns | |
52 | | //! | **`roaring`** | 66.1 K | 2.78 ns | 3.09 ns | 3.37 ns | 4.70 ns | |
53 | | //! | **`roaring`** | 66.1 K | 2.78 ns | 3.09 ns | 3.37 ns | 4.70 ns | |
54 | | //! |
55 | | //! Source code for the benchmark is provided in the *bench* directory of this |
56 | | //! repo and may be repeated by running `cargo criterion`. |
57 | | //! |
58 | | //! <br> |
59 | | //! |
60 | | //! ## Comparison of data structures |
61 | | //! |
62 | | //! #### unicode-id |
63 | | //! |
64 | | //! They use a sorted array of character ranges, and do a binary search to look |
65 | | //! up whether a given character lands inside one of those ranges. |
66 | | //! |
67 | | //! ```rust |
68 | | //! # const _: &str = stringify! { |
69 | | //! static ID_Continue_table: [(char, char); 763] = [ |
70 | | //! ('\u{30}', '\u{39}'), // 0-9 |
71 | | //! ('\u{41}', '\u{5a}'), // A-Z |
72 | | //! # " |
73 | | //! … |
74 | | //! # " |
75 | | //! ('\u{e0100}', '\u{e01ef}'), |
76 | | //! ]; |
77 | | //! # }; |
78 | | //! ``` |
79 | | //! |
80 | | //! The static storage used by this data structure scales with the number of |
81 | | //! contiguous ranges of identifier codepoints in Unicode. Every table entry |
82 | | //! consumes 8 bytes, because it consists of a pair of 32-bit `char` values. |
83 | | //! |
84 | | //! In some ranges of the Unicode codepoint space, this is quite a sparse |
85 | | //! representation – there are some ranges where tens of thousands of |
86 | | //! adjacent codepoints are all valid identifier characters. In other places, |
87 | | //! the representation is quite inefficient. A characater like `µ` (U+00B5) |
88 | | //! which is surrounded by non-identifier codepoints consumes 64 bits in the |
89 | | //! table, while it would be just 1 bit in a dense bitmap. |
90 | | //! |
91 | | //! On a system with 64-byte cache lines, binary searching the table touches 7 |
92 | | //! cache lines on average. Each cache line fits only 8 table entries. |
93 | | //! Additionally, the branching performed during the binary search is probably |
94 | | //! mostly unpredictable to the branch predictor. |
95 | | //! |
96 | | //! Overall, the crate ends up being about 10× slower on non-ASCII input |
97 | | //! compared to the fastest crate. |
98 | | //! |
99 | | //! A potential improvement would be to pack the table entries more compactly. |
100 | | //! Rust's `char` type is a 21-bit integer padded to 32 bits, which means every |
101 | | //! table entry is holding 22 bits of wasted space, adding up to 3.9 K. They |
102 | | //! could instead fit every table entry into 6 bytes, leaving out some of the |
103 | | //! padding, for a 25% improvement in space used. With some cleverness it may be |
104 | | //! possible to fit in 5 bytes or even 4 bytes by storing a low char and an |
105 | | //! extent, instead of low char and high char. I don't expect that performance |
106 | | //! would improve much but this could be the most efficient for space across all |
107 | | //! the libraries, needing only about 7 K to store. |
108 | | //! |
109 | | //! #### ucd-trie |
110 | | //! |
111 | | //! Their data structure is a compressed trie set specifically tailored for |
112 | | //! Unicode codepoints. The design is credited to Raph Levien in |
113 | | //! [rust-lang/rust#33098]. |
114 | | //! |
115 | | //! [rust-lang/rust#33098]: https://github.com/rust-lang/rust/pull/33098 |
116 | | //! |
117 | | //! ```rust |
118 | | //! pub struct TrieSet { |
119 | | //! tree1_level1: &'static [u64; 32], |
120 | | //! tree2_level1: &'static [u8; 992], |
121 | | //! tree2_level2: &'static [u64], |
122 | | //! tree3_level1: &'static [u8; 256], |
123 | | //! tree3_level2: &'static [u8], |
124 | | //! tree3_level3: &'static [u64], |
125 | | //! } |
126 | | //! ``` |
127 | | //! |
128 | | //! It represents codepoint sets using a trie to achieve prefix compression. The |
129 | | //! final states of the trie are embedded in leaves or "chunks", where each |
130 | | //! chunk is a 64-bit integer. Each bit position of the integer corresponds to |
131 | | //! whether a particular codepoint is in the set or not. These chunks are not |
132 | | //! just a compact representation of the final states of the trie, but are also |
133 | | //! a form of suffix compression. In particular, if multiple ranges of 64 |
134 | | //! contiguous codepoints have the same Unicode properties, then they all map to |
135 | | //! the same chunk in the final level of the trie. |
136 | | //! |
137 | | //! Being tailored for Unicode codepoints, this trie is partitioned into three |
138 | | //! disjoint sets: tree1, tree2, tree3. The first set corresponds to codepoints |
139 | | //! \[0, 0x800), the second \[0x800, 0x10000) and the third \[0x10000, |
140 | | //! 0x110000). These partitions conveniently correspond to the space of 1 or 2 |
141 | | //! byte UTF-8 encoded codepoints, 3 byte UTF-8 encoded codepoints and 4 byte |
142 | | //! UTF-8 encoded codepoints, respectively. |
143 | | //! |
144 | | //! Lookups in this data structure are significantly more efficient than binary |
145 | | //! search. A lookup touches either 1, 2, or 3 cache lines based on which of the |
146 | | //! trie partitions is being accessed. |
147 | | //! |
148 | | //! One possible performance improvement would be for this crate to expose a way |
149 | | //! to query based on a UTF-8 encoded string, returning the Unicode property |
150 | | //! corresponding to the first character in the string. Without such an API, the |
151 | | //! caller is required to tokenize their UTF-8 encoded input data into `char`, |
152 | | //! hand the `char` into `ucd-trie`, only for `ucd-trie` to undo that work by |
153 | | //! converting back into the variable-length representation for trie traversal. |
154 | | //! |
155 | | //! #### fst |
156 | | //! |
157 | | //! Uses a [finite state transducer][fst]. This representation is built into |
158 | | //! [ucd-generate] but I am not aware of any advantage over the `ucd-trie` |
159 | | //! representation. In particular `ucd-trie` is optimized for storing Unicode |
160 | | //! properties while `fst` is not. |
161 | | //! |
162 | | //! [fst]: https://github.com/BurntSushi/fst |
163 | | //! [ucd-generate]: https://github.com/BurntSushi/ucd-generate |
164 | | //! |
165 | | //! As far as I can tell, the main thing that causes `fst` to have large size |
166 | | //! and slow lookups for this use case relative to `ucd-trie` is that it does |
167 | | //! not specialize for the fact that only 21 of the 32 bits in a `char` are |
168 | | //! meaningful. There are some dense arrays in the structure with large ranges |
169 | | //! that could never possibly be used. |
170 | | //! |
171 | | //! #### roaring |
172 | | //! |
173 | | //! This crate is a pure-Rust implementation of [Roaring Bitmap], a data |
174 | | //! structure designed for storing sets of 32-bit unsigned integers. |
175 | | //! |
176 | | //! [Roaring Bitmap]: https://roaringbitmap.org/about/ |
177 | | //! |
178 | | //! Roaring bitmaps are compressed bitmaps which tend to outperform conventional |
179 | | //! compressed bitmaps such as WAH, EWAH or Concise. In some instances, they can |
180 | | //! be hundreds of times faster and they often offer significantly better |
181 | | //! compression. |
182 | | //! |
183 | | //! In this use case the performance was reasonably competitive but still |
184 | | //! substantially slower than the Unicode-optimized crates. Meanwhile the |
185 | | //! compression was significantly worse, requiring 6× as much storage for |
186 | | //! the data structure. |
187 | | //! |
188 | | //! I also benchmarked the [`croaring`] crate which is an FFI wrapper around the |
189 | | //! C reference implementation of Roaring Bitmap. This crate was consistently |
190 | | //! about 15% slower than pure-Rust `roaring`, which could just be FFI overhead. |
191 | | //! I did not investigate further. |
192 | | //! |
193 | | //! [`croaring`]: https://crates.io/crates/croaring |
194 | | //! |
195 | | //! #### unicode-ident |
196 | | //! |
197 | | //! This crate is most similar to the `ucd-trie` library, in that it's based on |
198 | | //! bitmaps stored in the leafs of a trie representation, achieving both prefix |
199 | | //! compression and suffix compression. |
200 | | //! |
201 | | //! The key differences are: |
202 | | //! |
203 | | //! - Uses a single 2-level trie, rather than 3 disjoint partitions of different |
204 | | //! depth each. |
205 | | //! - Uses significantly larger chunks: 512 bits rather than 64 bits. |
206 | | //! - Compresses the ID\_Start and ID\_Continue properties together |
207 | | //! simultaneously, rather than duplicating identical trie leaf chunks across |
208 | | //! the two. |
209 | | //! |
210 | | //! The following diagram show the ID\_Start and ID\_Continue Unicode boolean |
211 | | //! properties in uncompressed form, in row-major order: |
212 | | //! |
213 | | //! <table> |
214 | | //! <tr><th>ID_Start</th><th>ID_Continue</th></tr> |
215 | | //! <tr> |
216 | | //! <td><img alt="ID_Start bitmap" width="256" src="https://user-images.githubusercontent.com/1940490/168647353-c6eeb922-afec-49b2-9ef5-c03e9d1e0760.png"></td> |
217 | | //! <td><img alt="ID_Continue bitmap" width="256" src="https://user-images.githubusercontent.com/1940490/168647367-f447cca7-2362-4d7d-8cd7-d21c011d329b.png"></td> |
218 | | //! </tr> |
219 | | //! </table> |
220 | | //! |
221 | | //! Uncompressed, these would take 140 K to store, which is beyond what would be |
222 | | //! reasonable. However, as you can see there is a large degree of similarity |
223 | | //! between the two bitmaps and across the rows, which lends well to |
224 | | //! compression. |
225 | | //! |
226 | | //! This crate stores one 512-bit "row" of the above bitmaps in the leaf level |
227 | | //! of a trie, and a single additional level to index into the leafs. It turns |
228 | | //! out there are 124 unique 512-bit chunks across the two bitmaps so 7 bits are |
229 | | //! sufficient to index them. |
230 | | //! |
231 | | //! The chunk size of 512 bits is selected as the size that minimizes the total |
232 | | //! size of the data structure. A smaller chunk, like 256 or 128 bits, would |
233 | | //! achieve better deduplication but require a larger index. A larger chunk |
234 | | //! would increase redundancy in the leaf bitmaps. 512 bit chunks are the |
235 | | //! optimum for total size of the index plus leaf bitmaps. |
236 | | //! |
237 | | //! In fact since there are only 124 unique chunks, we can use an 8-bit index |
238 | | //! with a spare bit to index at the half-chunk level. This achieves an |
239 | | //! additional 8.5% compression by eliminating redundancies between the second |
240 | | //! half of any chunk and the first half of any other chunk. Note that this is |
241 | | //! not the same as using chunks which are half the size, because it does not |
242 | | //! necessitate raising the size of the trie's first level. |
243 | | //! |
244 | | //! In contrast to binary search or the `ucd-trie` crate, performing lookups in |
245 | | //! this data structure is straight-line code with no need for branching. |
246 | | |
247 | | #![no_std] |
248 | | #![doc(html_root_url = "https://docs.rs/unicode-id-start/1.1.0")] |
249 | | #![allow(clippy::doc_markdown, clippy::must_use_candidate)] |
250 | | |
251 | | #[rustfmt::skip] |
252 | | mod tables; |
253 | | |
254 | | use crate::tables::{ASCII_CONTINUE, ASCII_START, CHUNK, LEAF, TRIE_CONTINUE, TRIE_START}; |
255 | | |
256 | | /// Check ascii and unicode for id_start |
257 | | #[inline] |
258 | 0 | pub fn is_id_start(ch: char) -> bool { |
259 | 0 | if ch.is_ascii() { |
260 | 0 | return ASCII_START.0[ch as usize]; |
261 | 0 | } |
262 | 0 | is_id_start_unicode(ch) |
263 | 0 | } Unexecuted instantiation: unicode_id_start::is_id_start Unexecuted instantiation: unicode_id_start::is_id_start |
264 | | |
265 | | /// Check unicode only for id_start |
266 | | #[inline] |
267 | 382k | pub fn is_id_start_unicode(ch: char) -> bool { |
268 | 382k | let chunk = *TRIE_START.0.get(ch as usize / 8 / CHUNK).unwrap_or(&0); |
269 | 382k | let offset = chunk as usize * CHUNK / 2 + ch as usize / 8 % CHUNK; |
270 | 382k | unsafe { LEAF.0.get_unchecked(offset) }.wrapping_shr(ch as u32 % 8) & 1 != 0 |
271 | 382k | } Unexecuted instantiation: unicode_id_start::is_id_start_unicode Unexecuted instantiation: unicode_id_start::is_id_start_unicode unicode_id_start::is_id_start_unicode Line | Count | Source | 267 | 382k | pub fn is_id_start_unicode(ch: char) -> bool { | 268 | 382k | let chunk = *TRIE_START.0.get(ch as usize / 8 / CHUNK).unwrap_or(&0); | 269 | 382k | let offset = chunk as usize * CHUNK / 2 + ch as usize / 8 % CHUNK; | 270 | 382k | unsafe { LEAF.0.get_unchecked(offset) }.wrapping_shr(ch as u32 % 8) & 1 != 0 | 271 | 382k | } |
Unexecuted instantiation: unicode_id_start::is_id_start_unicode Unexecuted instantiation: unicode_id_start::is_id_start_unicode Unexecuted instantiation: unicode_id_start::is_id_start_unicode |
272 | | |
273 | | /// Check ascii and unicode for id_continue |
274 | | #[inline] |
275 | 0 | pub fn is_id_continue(ch: char) -> bool { |
276 | 0 | if ch.is_ascii() { |
277 | 0 | return ASCII_CONTINUE.0[ch as usize]; |
278 | 0 | } |
279 | 0 | is_id_continue_unicode(ch) |
280 | 0 | } Unexecuted instantiation: unicode_id_start::is_id_continue Unexecuted instantiation: unicode_id_start::is_id_continue |
281 | | |
282 | | /// Check and unicode only for id_continue |
283 | | #[inline] |
284 | 89.1k | pub fn is_id_continue_unicode(ch: char) -> bool { |
285 | 89.1k | let chunk = *TRIE_CONTINUE.0.get(ch as usize / 8 / CHUNK).unwrap_or(&0); |
286 | 89.1k | let offset = chunk as usize * CHUNK / 2 + ch as usize / 8 % CHUNK; |
287 | 89.1k | unsafe { LEAF.0.get_unchecked(offset) }.wrapping_shr(ch as u32 % 8) & 1 != 0 |
288 | 89.1k | } Unexecuted instantiation: unicode_id_start::is_id_continue_unicode Unexecuted instantiation: unicode_id_start::is_id_continue_unicode unicode_id_start::is_id_continue_unicode Line | Count | Source | 284 | 89.1k | pub fn is_id_continue_unicode(ch: char) -> bool { | 285 | 89.1k | let chunk = *TRIE_CONTINUE.0.get(ch as usize / 8 / CHUNK).unwrap_or(&0); | 286 | 89.1k | let offset = chunk as usize * CHUNK / 2 + ch as usize / 8 % CHUNK; | 287 | 89.1k | unsafe { LEAF.0.get_unchecked(offset) }.wrapping_shr(ch as u32 % 8) & 1 != 0 | 288 | 89.1k | } |
Unexecuted instantiation: unicode_id_start::is_id_continue_unicode Unexecuted instantiation: unicode_id_start::is_id_continue_unicode Unexecuted instantiation: unicode_id_start::is_id_continue_unicode |