FE 0.15.0
A C++23 toolkit for writing compiler/interpreter frontends.
Loading...
Searching...
No Matches
utf8.h
Go to the documentation of this file.
1#pragma once
2
3#include <istream>
4#include <ostream>
5#include <string_view>
6
7#include "fe/assert.h"
8
9/// UTF-8 helpers for decoding byte streams, encoding `char32_t` values, and running
10/// ASCII-style character classification on `char32_t`.
11///
12/// The central entry points are @ref decode and @ref encode.
13/// Decoding returns sentinel values such as @ref EoF and @ref Invalid instead of throwing.
14namespace fe::utf8 {
15
16static constexpr size_t Max = 4; ///< Maximal number of `char8_t`s of an UTF-8 byte sequence.
17static constexpr char32_t BOM = 0xfeff; ///< [Byte Order Mark](https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8).
18static constexpr std::string_view Bom = "\xef\xbb\xbf"; ///< BOM as UTF-8 bytes.
19static constexpr char32_t EoF
20 = (char32_t)std::istream::traits_type::eof(); ///< End of stream sentinel returned by @ref decode.
21static constexpr char32_t Null = 0; ///< U+0000 NULL returned unchanged by @ref decode.
22static constexpr char32_t Invalid = 0x110000; ///< Sentinel returned by @ref decode for malformed UTF-8.
23
24/// Returns the expected number of bytes for an UTF-8 char sequence by inspecting the first byte.
25/// Retuns @c 0 if invalid.
26constexpr size_t num_bytes(char8_t c) noexcept {
27 if ((c & char8_t(0b10000000)) == char8_t(0b00000000)) return 1;
28 if ((c & char8_t(0b11100000)) == char8_t(0b11000000)) return 2;
29 if ((c & char8_t(0b11110000)) == char8_t(0b11100000)) return 3;
30 if ((c & char8_t(0b11111000)) == char8_t(0b11110000)) return 4;
31 return 0;
32}
33
34/// Append @p b to @p c for converting UTF-8 to UTF-32.
35constexpr char32_t append(char32_t c, char8_t b) noexcept { return (c << 6) | (b & 0b00111111); }
36
37/// Get relevant bits of first UTF-8 byte @p c of a @em multi-byte sequence consisting of @p num bytes.
38constexpr char32_t first(char32_t c, char32_t num) noexcept { return c & (0b00011111 >> (num - 2)); }
39
40/// Minimum Unicode scalar value representable in an UTF-8 sequence of @p num bytes.
41constexpr char32_t min_code_point(size_t num) noexcept {
42 switch (num) {
43 case 1: return 0x000000;
44 case 2: return 0x000080;
45 case 3: return 0x000800;
46 case 4: return 0x010000;
47 default: return 0x110000;
48 }
49}
50
51/// Is the 2nd, 3rd, or 4th byte of an UTF-8 byte sequence valid?
52/// @returns the extracted `char8_t` or `char8_t(-1)` if invalid.
53constexpr char8_t is_valid234(char8_t c) noexcept {
54 return (c & char8_t(0b11000000)) == char8_t(0b10000000) ? (c & char8_t(0b00111111)) : char8_t(-1);
55}
56
57/// Is @p c a valid Unicode scalar value?
58constexpr bool is_scalar_value(char32_t c) noexcept { return c <= 0x10ffff && !(0xd800 <= c && c <= 0xdfff); }
59
60/// Decodes the next UTF-8 sequence from @p is into a single `char32_t`.
61///
62/// Returns @ref EoF when the stream is exhausted and @ref Invalid for malformed,
63/// overlong, surrogate, or otherwise non-scalar encodings.
64inline char32_t decode(std::istream& is) {
65 char32_t result = is.get();
66 if (result == EoF) return result;
67
68 switch (auto n = utf8::num_bytes(char8_t(result))) {
69 case 0: return Invalid;
70 case 1: return result;
71 default:
72 result = utf8::first(result, n);
73
74 for (size_t i = 1; i != n; ++i)
75 if (auto x = is_valid234(is.get()); x != char8_t(-1))
76 result = utf8::append(result, x);
77 else
78 return Invalid;
79
80 if (result < utf8::min_code_point(n) || !utf8::is_scalar_value(result)) return Invalid;
81 }
82
83 return result;
84}
85
86/// Decodes the UTF-8 sequence at @p i in @p str and advances @p i past it.
87///
88/// Returns @ref EoF at the end of @p str - leaving @p i alone - and @ref Invalid for malformed,
89/// overlong, surrogate, or otherwise non-scalar encodings.
90/// @note An @ref Invalid sequence advances @p i by a *single* byte, so the next @ref decode resynchronizes
91/// instead of swallowing bytes that may well start a valid sequence themselves.
92inline char32_t decode(std::string_view str, size_t& i) noexcept {
93 if (i >= str.size()) return EoF;
94
95 auto c8 = char8_t(str[i]);
96 auto n = utf8::num_bytes(c8);
97 char32_t result = char32_t(c8);
98 if (n == 0 || i + n > str.size()) return ++i, Invalid;
99 if (n == 1) return ++i, result;
100
101 result = utf8::first(result, n);
102 for (size_t j = 1; j != n; ++j) {
103 auto x = is_valid234(char8_t(str[i + j]));
104 if (x == char8_t(-1)) return ++i, Invalid;
105 result = utf8::append(result, x);
106 }
107
108 if (result < utf8::min_code_point(n) || !utf8::is_scalar_value(result)) return ++i, Invalid;
109
110 i += n;
111 return result;
112}
113
114/// Number of UTF-8 code points in @p str.
115/// @note A column is counted in code points, so this is what turns a byte offset into one.
116/// Counts via @ref decode, so a malformed sequence resynchronizes the same way the lexer does
117/// and a @p str truncated mid-sequence counts its final partial one.
118size_t num_code_points(std::string_view str) noexcept;
119
120/// Encodes @p c32 as UTF-8 and writes the resulting bytes to @p os.
121/// @returns `false` when @p c32 is outside the encodable range.
122bool encode(std::ostream& os, char32_t c32);
123
124/// Wrapper for `char32_t` with an `operator<<` that writes UTF-8.
125struct Char32 {
126 constexpr Char32(char32_t c) noexcept
127 : c(c) {}
128
129 friend std::ostream& operator<<(std::ostream& os, Char32 c) {
130 auto res = utf8::encode(os, c.c);
131 assert_unused(res);
132 return os;
133 }
134
135 char32_t c;
136};
137
138/// @name Character classification
139/// `char32_t`-style counterparts of the <[ctype](https://en.cppreference.com/w/cpp/header/cctype)>
140/// functions, for a code point of any width - everything above U+00FF belongs to no class.
141///@{
142constexpr bool isascii(char32_t c) noexcept { return c <= 0x7F; }
143constexpr bool isupper(char32_t c) noexcept { return 'A' <= c && c <= 'Z'; }
144constexpr bool islower(char32_t c) noexcept { return 'a' <= c && c <= 'z'; }
145constexpr bool isdigit(char32_t c) noexcept { return '0' <= c && c <= '9'; }
146constexpr bool isalpha(char32_t c) noexcept { return isupper(c) || islower(c); }
147constexpr bool isalnum(char32_t c) noexcept { return isalpha(c) || isdigit(c); }
148constexpr bool isxdigit(char32_t c) noexcept { return isdigit(c) || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f'); }
149constexpr bool iscntrl(char32_t c) noexcept { return c <= 0x1F || c == 0x7F; }
150constexpr bool isblank(char32_t c) noexcept { return c == ' ' || c == '\t'; }
151constexpr bool isspace(char32_t c) noexcept { return c == ' ' || ('\t' <= c && c <= '\r'); }
152constexpr bool isgraph(char32_t c) noexcept { return '!' <= c && c <= '~'; }
153constexpr bool isprint(char32_t c) noexcept { return ' ' <= c && c <= '~'; }
154constexpr bool ispunct(char32_t c) noexcept { return isgraph(c) && !isalnum(c); }
155constexpr char32_t tolower(char32_t c) noexcept { return isupper(c) ? c - 'A' + 'a' : c; }
156constexpr char32_t toupper(char32_t c) noexcept { return islower(c) ? c - 'a' + 'A' : c; }
157/// Is @p c within [begin, finis]?
158constexpr bool isrange(char32_t c, char32_t begin, char32_t finis) noexcept { return begin <= c && c <= finis; }
159constexpr auto isrange(char32_t begin, char32_t finis) noexcept {
160 return [=](char32_t c) { return isrange(c, begin, finis); };
161}
162constexpr bool isodigit(char32_t c) noexcept { return isrange(c, '0', '7'); } ///< Is octal digit?
163constexpr bool isbdigit(char32_t c) noexcept { return isrange(c, '0', '1'); } ///< Is binary digit?
164///@}
165
166namespace detail {
167
168constexpr bool any(char32_t c, char32_t d) noexcept { return c == d; }
169template<class... T>
170constexpr bool any(char32_t c, char32_t d, T... args) noexcept {
171 return c == d || any(c, args...);
172}
173
174} // namespace detail
175
176/// Build a predicate that checks whether a code point matches any of the given values.
177template<class... T>
178constexpr auto any(T... args) noexcept {
179 return [=](char32_t c) { return detail::any(c, args...); };
180}
181
182} // namespace fe::utf8
#define assert_unused(x)
Definition assert.h:39
UTF-8 helpers for decoding byte streams, encoding char32_t values, and running ASCII-style character ...
Definition utf8.h:14
constexpr bool ispunct(char32_t c) noexcept
Definition utf8.h:154
static constexpr char32_t Invalid
Sentinel returned by decode for malformed UTF-8.
Definition utf8.h:22
constexpr bool isprint(char32_t c) noexcept
Definition utf8.h:153
static constexpr char32_t BOM
Byte Order Mark.
Definition utf8.h:17
static constexpr std::string_view Bom
BOM as UTF-8 bytes.
Definition utf8.h:18
constexpr size_t num_bytes(char8_t c) noexcept
Returns the expected number of bytes for an UTF-8 char sequence by inspecting the first byte.
Definition utf8.h:26
constexpr bool isbdigit(char32_t c) noexcept
Is binary digit?
Definition utf8.h:163
constexpr char32_t first(char32_t c, char32_t num) noexcept
Get relevant bits of first UTF-8 byte c of a multi-byte sequence consisting of num bytes.
Definition utf8.h:38
constexpr bool iscntrl(char32_t c) noexcept
Definition utf8.h:149
constexpr bool isupper(char32_t c) noexcept
Definition utf8.h:143
char32_t decode(std::istream &is)
Decodes the next UTF-8 sequence from is into a single char32_t.
Definition utf8.h:64
constexpr bool isxdigit(char32_t c) noexcept
Definition utf8.h:148
bool encode(std::ostream &os, char32_t c32)
Encodes c32 as UTF-8 and writes the resulting bytes to os.
constexpr bool isblank(char32_t c) noexcept
Definition utf8.h:150
constexpr char8_t is_valid234(char8_t c) noexcept
Is the 2nd, 3rd, or 4th byte of an UTF-8 byte sequence valid?
Definition utf8.h:53
constexpr bool isalpha(char32_t c) noexcept
Definition utf8.h:146
constexpr bool isodigit(char32_t c) noexcept
Is octal digit?
Definition utf8.h:162
constexpr char32_t min_code_point(size_t num) noexcept
Minimum Unicode scalar value representable in an UTF-8 sequence of num bytes.
Definition utf8.h:41
static constexpr size_t Max
Maximal number of char8_ts of an UTF-8 byte sequence.
Definition utf8.h:16
constexpr bool isascii(char32_t c) noexcept
Definition utf8.h:142
static constexpr char32_t EoF
End of stream sentinel returned by decode.
Definition utf8.h:20
constexpr char32_t append(char32_t c, char8_t b) noexcept
Append b to c for converting UTF-8 to UTF-32.
Definition utf8.h:35
constexpr bool isgraph(char32_t c) noexcept
Definition utf8.h:152
constexpr bool isspace(char32_t c) noexcept
Definition utf8.h:151
constexpr bool isdigit(char32_t c) noexcept
Definition utf8.h:145
constexpr bool isrange(char32_t c, char32_t begin, char32_t finis) noexcept
Is c within [begin, finis]?
Definition utf8.h:158
constexpr bool is_scalar_value(char32_t c) noexcept
Is c a valid Unicode scalar value?
Definition utf8.h:58
constexpr bool islower(char32_t c) noexcept
Definition utf8.h:144
size_t num_code_points(std::string_view str) noexcept
Number of UTF-8 code points in str.
constexpr char32_t toupper(char32_t c) noexcept
Definition utf8.h:156
constexpr char32_t tolower(char32_t c) noexcept
Definition utf8.h:155
static constexpr char32_t Null
U+0000 NULL returned unchanged by decode.
Definition utf8.h:21
constexpr auto any(T... args) noexcept
Build a predicate that checks whether a code point matches any of the given values.
Definition utf8.h:178
constexpr bool isalnum(char32_t c) noexcept
Definition utf8.h:147
friend std::ostream & operator<<(std::ostream &os, Char32 c)
Definition utf8.h:129
char32_t c
Definition utf8.h:135
constexpr Char32(char32_t c) noexcept
Definition utf8.h:126