Ruby 4.1.0dev (2026-03-20 revision eb04ab9117336f2b3613244cfe8a528c52faf6d6)
pm_strpbrk.c
2
6static inline void
7pm_strpbrk_invalid_multibyte_character(pm_parser_t *parser, uint32_t start, uint32_t length) {
8 pm_diagnostic_list_append_format(&parser->metadata_arena, &parser->error_list, start, length, PM_ERR_INVALID_MULTIBYTE_CHARACTER, parser->start[start]);
9}
10
14static inline void
15pm_strpbrk_explicit_encoding_set(pm_parser_t *parser, uint32_t start, uint32_t length) {
16 if (parser->explicit_encoding != NULL) {
17 if (parser->explicit_encoding == parser->encoding) {
18 // Okay, we already locked to this encoding.
19 } else if (parser->explicit_encoding == PM_ENCODING_UTF_8_ENTRY) {
20 // Not okay, we already found a Unicode escape sequence and this
21 // conflicts.
22 pm_diagnostic_list_append_format(&parser->metadata_arena, &parser->error_list, start, length, PM_ERR_MIXED_ENCODING, parser->encoding->name);
23 } else {
24 // Should not be anything else.
25 assert(false && "unreachable");
26 }
27 }
28
29 parser->explicit_encoding = parser->encoding;
30}
31
48#if defined(PRISM_HAS_NEON) || defined(PRISM_HAS_SSSE3) || defined(PRISM_HAS_SWAR)
49
60static inline void
61pm_strpbrk_cache_update(pm_parser_t *parser, const uint8_t *charset) {
62 // The cache key is the full charset buffer (PM_STRPBRK_CACHE_SIZE bytes).
63 // Since it is always NUL-padded, a fixed-size comparison covers both
64 // content and length.
65 if (memcmp(parser->strpbrk_cache.charset, charset, sizeof(parser->strpbrk_cache.charset)) == 0) return;
66
67 memset(parser->strpbrk_cache.low_lut, 0, sizeof(parser->strpbrk_cache.low_lut));
68 memset(parser->strpbrk_cache.high_lut, 0, sizeof(parser->strpbrk_cache.high_lut));
69 memset(parser->strpbrk_cache.table, 0, sizeof(parser->strpbrk_cache.table));
70
71 // Always include NUL in the tables. The slow path uses strchr, which
72 // always matches NUL (it finds the C string terminator), so NUL is
73 // effectively always a breakpoint. Replicating that here lets the fast
74 // scanner handle NUL at full speed instead of bailing to the slow path.
75 parser->strpbrk_cache.low_lut[0x00] |= (uint8_t) (1 << 0);
76 parser->strpbrk_cache.high_lut[0x00] = (uint8_t) (1 << 0);
77 parser->strpbrk_cache.table[0] |= (uint64_t) 1;
78
79 size_t charset_len = 0;
80 for (const uint8_t *c = charset; *c != '\0'; c++) {
81 parser->strpbrk_cache.low_lut[*c & 0x0F] |= (uint8_t) (1 << (*c >> 4));
82 parser->strpbrk_cache.high_lut[*c >> 4] = (uint8_t) (1 << (*c >> 4));
83 parser->strpbrk_cache.table[*c >> 6] |= (uint64_t) 1 << (*c & 0x3F);
84 charset_len++;
85 }
86
87 // Store the new charset key, NUL-padded to the full buffer size.
88 memcpy(parser->strpbrk_cache.charset, charset, charset_len + 1);
89 memset(parser->strpbrk_cache.charset + charset_len + 1, 0, sizeof(parser->strpbrk_cache.charset) - charset_len - 1);
90}
91
92#endif
93
94#if defined(PRISM_HAS_NEON)
95#include <arm_neon.h>
96
97static inline bool
98scan_strpbrk_ascii(pm_parser_t *parser, const uint8_t *source, size_t maximum, const uint8_t *charset, size_t *index) {
99 pm_strpbrk_cache_update(parser, charset);
100
101 uint8x16_t low_lut = vld1q_u8(parser->strpbrk_cache.low_lut);
102 uint8x16_t high_lut = vld1q_u8(parser->strpbrk_cache.high_lut);
103 uint8x16_t mask_0f = vdupq_n_u8(0x0F);
104 uint8x16_t mask_80 = vdupq_n_u8(0x80);
105
106 size_t idx = 0;
107
108 while (idx + 16 <= maximum) {
109 uint8x16_t v = vld1q_u8(source + idx);
110
111 // If any byte has the high bit set, we have non-ASCII data.
112 // Return to let the caller's encoding-aware loop handle it.
113 if (vmaxvq_u8(vandq_u8(v, mask_80)) != 0) break;
114
115 uint8x16_t lo_class = vqtbl1q_u8(low_lut, vandq_u8(v, mask_0f));
116 uint8x16_t hi_class = vqtbl1q_u8(high_lut, vshrq_n_u8(v, 4));
117 uint8x16_t matched = vtstq_u8(lo_class, hi_class);
118
119 if (vmaxvq_u8(matched) == 0) {
120 idx += 16;
121 continue;
122 }
123
124 // Find the position of the first matching byte.
125 uint64_t lo64 = vgetq_lane_u64(vreinterpretq_u64_u8(matched), 0);
126 if (lo64 != 0) {
127 *index = idx + pm_ctzll(lo64) / 8;
128 return true;
129 }
130 uint64_t hi64 = vgetq_lane_u64(vreinterpretq_u64_u8(matched), 1);
131 *index = idx + 8 + pm_ctzll(hi64) / 8;
132 return true;
133 }
134
135 // Scalar tail for remaining < 16 ASCII bytes.
136 while (idx < maximum && source[idx] < 0x80) {
137 uint8_t byte = source[idx];
138 if (parser->strpbrk_cache.table[byte >> 6] & ((uint64_t) 1 << (byte & 0x3F))) {
139 *index = idx;
140 return true;
141 }
142 idx++;
143 }
144
145 *index = idx;
146 return false;
147}
148
149#elif defined(PRISM_HAS_SSSE3)
150#include <tmmintrin.h>
151
152static inline bool
153scan_strpbrk_ascii(pm_parser_t *parser, const uint8_t *source, size_t maximum, const uint8_t *charset, size_t *index) {
154 pm_strpbrk_cache_update(parser, charset);
155
156 __m128i low_lut = _mm_loadu_si128((const __m128i *) parser->strpbrk_cache.low_lut);
157 __m128i high_lut = _mm_loadu_si128((const __m128i *) parser->strpbrk_cache.high_lut);
158 __m128i mask_0f = _mm_set1_epi8(0x0F);
159
160 size_t idx = 0;
161
162 while (idx + 16 <= maximum) {
163 __m128i v = _mm_loadu_si128((const __m128i *) (source + idx));
164
165 // If any byte has the high bit set, stop.
166 if (_mm_movemask_epi8(v) != 0) break;
167
168 // Nibble-based classification using pshufb (SSSE3), same as NEON
169 // vqtbl1q_u8. A byte matches iff (low_lut[lo_nib] & high_lut[hi_nib]) != 0.
170 __m128i lo_class = _mm_shuffle_epi8(low_lut, _mm_and_si128(v, mask_0f));
171 __m128i hi_class = _mm_shuffle_epi8(high_lut, _mm_and_si128(_mm_srli_epi16(v, 4), mask_0f));
172 __m128i matched = _mm_and_si128(lo_class, hi_class);
173
174 // Check if any byte matched.
175 int mask = _mm_movemask_epi8(_mm_cmpeq_epi8(matched, _mm_setzero_si128()));
176
177 if (mask == 0xFFFF) {
178 // All bytes were zero — no match in this chunk.
179 idx += 16;
180 continue;
181 }
182
183 // Find the first matching byte (first non-zero in matched).
184 *index = idx + pm_ctzll((uint64_t) (~mask & 0xFFFF));
185 return true;
186 }
187
188 // Scalar tail.
189 while (idx < maximum && source[idx] < 0x80) {
190 uint8_t byte = source[idx];
191 if (parser->strpbrk_cache.table[byte >> 6] & ((uint64_t) 1 << (byte & 0x3F))) {
192 *index = idx;
193 return true;
194 }
195 idx++;
196 }
197
198 *index = idx;
199 return false;
200}
201
202#elif defined(PRISM_HAS_SWAR)
203
204static inline bool
205scan_strpbrk_ascii(pm_parser_t *parser, const uint8_t *source, size_t maximum, const uint8_t *charset, size_t *index) {
206 pm_strpbrk_cache_update(parser, charset);
207
208 static const uint64_t highs = 0x8080808080808080ULL;
209 size_t idx = 0;
210
211 while (idx + 8 <= maximum) {
212 uint64_t word;
213 memcpy(&word, source + idx, 8);
214
215 // Bail on any non-ASCII byte.
216 if (word & highs) break;
217
218 // Check each byte against the charset table.
219 for (size_t j = 0; j < 8; j++) {
220 uint8_t byte = source[idx + j];
221 if (parser->strpbrk_cache.table[byte >> 6] & ((uint64_t) 1 << (byte & 0x3F))) {
222 *index = idx + j;
223 return true;
224 }
225 }
226
227 idx += 8;
228 }
229
230 // Scalar tail.
231 while (idx < maximum && source[idx] < 0x80) {
232 uint8_t byte = source[idx];
233 if (parser->strpbrk_cache.table[byte >> 6] & ((uint64_t) 1 << (byte & 0x3F))) {
234 *index = idx;
235 return true;
236 }
237 idx++;
238 }
239
240 *index = idx;
241 return false;
242}
243
244#else
245
246static inline bool
247scan_strpbrk_ascii(PRISM_ATTRIBUTE_UNUSED pm_parser_t *parser, PRISM_ATTRIBUTE_UNUSED const uint8_t *source, PRISM_ATTRIBUTE_UNUSED size_t maximum, PRISM_ATTRIBUTE_UNUSED const uint8_t *charset, size_t *index) {
248 *index = 0;
249 return false;
250}
251
252#endif
253
257static inline const uint8_t *
258pm_strpbrk_utf8(pm_parser_t *parser, const uint8_t *source, const uint8_t *charset, size_t index, size_t maximum, bool validate) {
259 while (index < maximum) {
260 if (strchr((const char *) charset, source[index]) != NULL) {
261 return source + index;
262 }
263
264 if (source[index] < 0x80) {
265 index++;
266 } else {
267 size_t width = pm_encoding_utf_8_char_width(source + index, (ptrdiff_t) (maximum - index));
268
269 if (width > 0) {
270 index += width;
271 } else if (!validate) {
272 index++;
273 } else {
274 // At this point we know we have an invalid multibyte character.
275 // We'll walk forward as far as we can until we find the next
276 // valid character so that we don't spam the user with a ton of
277 // the same kind of error.
278 const size_t start = index;
279
280 do {
281 index++;
282 } while (index < maximum && pm_encoding_utf_8_char_width(source + index, (ptrdiff_t) (maximum - index)) == 0);
283
284 pm_strpbrk_invalid_multibyte_character(parser, (uint32_t) ((source + start) - parser->start), (uint32_t) (index - start));
285 }
286 }
287 }
288
289 return NULL;
290}
291
295static inline const uint8_t *
296pm_strpbrk_ascii_8bit(pm_parser_t *parser, const uint8_t *source, const uint8_t *charset, size_t index, size_t maximum, bool validate) {
297 while (index < maximum) {
298 if (strchr((const char *) charset, source[index]) != NULL) {
299 return source + index;
300 }
301
302 if (validate && source[index] >= 0x80) pm_strpbrk_explicit_encoding_set(parser, (uint32_t) (source - parser->start), 1);
303 index++;
304 }
305
306 return NULL;
307}
308
312static inline const uint8_t *
313pm_strpbrk_multi_byte(pm_parser_t *parser, const uint8_t *source, const uint8_t *charset, size_t index, size_t maximum, bool validate) {
314 const pm_encoding_t *encoding = parser->encoding;
315
316 while (index < maximum) {
317 if (strchr((const char *) charset, source[index]) != NULL) {
318 return source + index;
319 }
320
321 if (source[index] < 0x80) {
322 index++;
323 } else {
324 size_t width = encoding->char_width(source + index, (ptrdiff_t) (maximum - index));
325 if (validate) pm_strpbrk_explicit_encoding_set(parser, (uint32_t) (source - parser->start), (uint32_t) width);
326
327 if (width > 0) {
328 index += width;
329 } else if (!validate) {
330 index++;
331 } else {
332 // At this point we know we have an invalid multibyte character.
333 // We'll walk forward as far as we can until we find the next
334 // valid character so that we don't spam the user with a ton of
335 // the same kind of error.
336 const size_t start = index;
337
338 do {
339 index++;
340 } while (index < maximum && encoding->char_width(source + index, (ptrdiff_t) (maximum - index)) == 0);
341
342 pm_strpbrk_invalid_multibyte_character(parser, (uint32_t) ((source + start) - parser->start), (uint32_t) (index - start));
343 }
344 }
345 }
346
347 return NULL;
348}
349
354static inline const uint8_t *
355pm_strpbrk_single_byte(pm_parser_t *parser, const uint8_t *source, const uint8_t *charset, size_t index, size_t maximum, bool validate) {
356 const pm_encoding_t *encoding = parser->encoding;
357
358 while (index < maximum) {
359 if (strchr((const char *) charset, source[index]) != NULL) {
360 return source + index;
361 }
362
363 if (source[index] < 0x80 || !validate) {
364 index++;
365 } else {
366 size_t width = encoding->char_width(source + index, (ptrdiff_t) (maximum - index));
367 pm_strpbrk_explicit_encoding_set(parser, (uint32_t) (source - parser->start), (uint32_t) width);
368
369 if (width > 0) {
370 index += width;
371 } else {
372 // At this point we know we have an invalid multibyte character.
373 // We'll walk forward as far as we can until we find the next
374 // valid character so that we don't spam the user with a ton of
375 // the same kind of error.
376 const size_t start = index;
377
378 do {
379 index++;
380 } while (index < maximum && encoding->char_width(source + index, (ptrdiff_t) (maximum - index)) == 0);
381
382 pm_strpbrk_invalid_multibyte_character(parser, (uint32_t) ((source + start) - parser->start), (uint32_t) (index - start));
383 }
384 }
385 }
386
387 return NULL;
388}
389
409const uint8_t *
410pm_strpbrk(pm_parser_t *parser, const uint8_t *source, const uint8_t *charset, ptrdiff_t length, bool validate) {
411 if (length <= 0) return NULL;
412
413 size_t maximum = (size_t) length;
414 size_t index = 0;
415 if (scan_strpbrk_ascii(parser, source, maximum, charset, &index)) return source + index;
416
417 if (!parser->encoding_changed) {
418 return pm_strpbrk_utf8(parser, source, charset, index, maximum, validate);
419 } else if (parser->encoding == PM_ENCODING_ASCII_8BIT_ENTRY) {
420 return pm_strpbrk_ascii_8bit(parser, source, charset, index, maximum, validate);
421 } else if (parser->encoding->multibyte) {
422 return pm_strpbrk_multi_byte(parser, source, charset, index, maximum, validate);
423 } else {
424 return pm_strpbrk_single_byte(parser, source, charset, index, maximum, validate);
425 }
426}
A custom strpbrk implementation.
#define pm_ctzll(v)
Platform detection for SIMD / fast-path implementations.
Definition defines.h:299
#define PRISM_ATTRIBUTE_UNUSED
GCC will warn if you specify a function or parameter that is unused at runtime.
Definition defines.h:81
#define PM_ENCODING_UTF_8_ENTRY
This is the default UTF-8 encoding.
Definition encoding.h:245
#define PM_ENCODING_ASCII_8BIT_ENTRY
This is the ASCII-8BIT encoding.
Definition encoding.h:259
This struct defines the functions necessary to implement the encoding interface so we can determine h...
Definition encoding.h:23
size_t(* char_width)(const uint8_t *b, ptrdiff_t n)
Return the number of bytes that the next character takes if it is valid in the encoding.
Definition encoding.h:29
bool multibyte
Return true if the encoding is a multibyte encoding.
Definition encoding.h:61
const char * name
The name of the encoding.
Definition encoding.h:56
This struct represents the overall parser.
Definition parser.h:652
const pm_encoding_t * explicit_encoding
When a string-like expression is being lexed, any byte or escape sequence that resolves to a value wh...
Definition parser.h:872
const pm_encoding_t * encoding
The encoding functions for the current file is attached to the parser as it's parsing so that it can ...
Definition parser.h:773
bool encoding_changed
Whether or not the encoding has been changed by a magic comment.
Definition parser.h:950
const uint8_t * start
The pointer to the start of the source.
Definition parser.h:709
pm_arena_t metadata_arena
The arena used for parser metadata (comments, diagnostics, etc.).
Definition parser.h:657
pm_list_t error_list
The list of errors that have been found while parsing.
Definition parser.h:752