Ruby 4.1.0dev (2026-09-05 revision a946c1bd8e5e7e03e938fe5bb4df942affa95ec6)
strpbrk.c
1#include "prism/internal/strpbrk.h"
2
6
7#include "prism/internal/bit.h"
8#include "prism/internal/diagnostic.h"
9#include "prism/internal/encoding.h"
10#include "prism/internal/parser.h"
11
12#include <assert.h>
13#include <stdbool.h>
14#include <string.h>
15
19static PRISM_INLINE void
20pm_strpbrk_invalid_multibyte_character(pm_parser_t *parser, uint32_t start, uint32_t length) {
21 pm_diagnostic_list_append_format(&parser->metadata_arena, &parser->error_list, start, length, PM_ERR_INVALID_MULTIBYTE_CHARACTER, parser->start[start]);
22}
23
27static PRISM_INLINE void
28pm_strpbrk_explicit_encoding_set(pm_parser_t *parser, uint32_t start, uint32_t length) {
29 if (parser->explicit_encoding != NULL) {
30 if (parser->explicit_encoding == parser->encoding) {
31 // Okay, we already locked to this encoding.
32 } else if (parser->explicit_encoding == PM_ENCODING_UTF_8_ENTRY) {
33 // Not okay, we already found a Unicode escape sequence and this
34 // conflicts.
35 pm_diagnostic_list_append_format(&parser->metadata_arena, &parser->error_list, start, length, PM_ERR_MIXED_ENCODING, parser->encoding->name);
36 } else {
37 // Should not be anything else.
38 assert(false && "unreachable");
39 }
40 }
41
42 parser->explicit_encoding = parser->encoding;
43}
44
61#if defined(PRISM_HAS_NEON) || defined(PRISM_HAS_SSSE3) || defined(PRISM_HAS_SWAR)
62
73static PRISM_INLINE void
74pm_strpbrk_cache_update(pm_parser_t *parser, const uint8_t *charset) {
75 // The cache key is the full charset buffer (PM_STRPBRK_CACHE_SIZE bytes).
76 // Since it is always NUL-padded, a fixed-size comparison covers both
77 // content and length.
78 if (memcmp(parser->strpbrk_cache.charset, charset, sizeof(parser->strpbrk_cache.charset)) == 0) return;
79
80 memset(parser->strpbrk_cache.low_lut, 0, sizeof(parser->strpbrk_cache.low_lut));
81 memset(parser->strpbrk_cache.high_lut, 0, sizeof(parser->strpbrk_cache.high_lut));
82 memset(parser->strpbrk_cache.table, 0, sizeof(parser->strpbrk_cache.table));
83
84 // Always include NUL in the tables. The slow path uses strchr, which
85 // always matches NUL (it finds the C string terminator), so NUL is
86 // effectively always a breakpoint. Replicating that here lets the fast
87 // scanner handle NUL at full speed instead of bailing to the slow path.
88 parser->strpbrk_cache.low_lut[0x00] |= (uint8_t) (1 << 0);
89 parser->strpbrk_cache.high_lut[0x00] = (uint8_t) (1 << 0);
90 parser->strpbrk_cache.table[0] |= (uint64_t) 1;
91
92 size_t charset_len = 0;
93 for (const uint8_t *c = charset; *c != '\0'; c++) {
94 parser->strpbrk_cache.low_lut[*c & 0x0F] |= (uint8_t) (1 << (*c >> 4));
95 parser->strpbrk_cache.high_lut[*c >> 4] = (uint8_t) (1 << (*c >> 4));
96 parser->strpbrk_cache.table[*c >> 6] |= (uint64_t) 1 << (*c & 0x3F);
97 charset_len++;
98 }
99
100 // Store the new charset key, NUL-padded to the full buffer size.
101 memcpy(parser->strpbrk_cache.charset, charset, charset_len + 1);
102 memset(parser->strpbrk_cache.charset + charset_len + 1, 0, sizeof(parser->strpbrk_cache.charset) - charset_len - 1);
103}
104
105#endif
106
107#if defined(PRISM_HAS_NEON)
108#include <arm_neon.h>
109
110static PRISM_INLINE bool
111scan_strpbrk_ascii(pm_parser_t *parser, const uint8_t *source, size_t maximum, const uint8_t *charset, size_t *index) {
112 pm_strpbrk_cache_update(parser, charset);
113
114 uint8x16_t low_lut = vld1q_u8(parser->strpbrk_cache.low_lut);
115 uint8x16_t high_lut = vld1q_u8(parser->strpbrk_cache.high_lut);
116 uint8x16_t mask_0f = vdupq_n_u8(0x0F);
117 uint8x16_t mask_80 = vdupq_n_u8(0x80);
118
119 size_t idx = 0;
120
121 while (idx + 16 <= maximum) {
122 uint8x16_t v = vld1q_u8(source + idx);
123
124 // If any byte has the high bit set, we have non-ASCII data.
125 // Return to let the caller's encoding-aware loop handle it.
126 if (vmaxvq_u8(vandq_u8(v, mask_80)) != 0) break;
127
128 uint8x16_t lo_class = vqtbl1q_u8(low_lut, vandq_u8(v, mask_0f));
129 uint8x16_t hi_class = vqtbl1q_u8(high_lut, vshrq_n_u8(v, 4));
130 uint8x16_t matched = vtstq_u8(lo_class, hi_class);
131
132 if (vmaxvq_u8(matched) == 0) {
133 idx += 16;
134 continue;
135 }
136
137 // Find the position of the first matching byte.
138 uint64_t lo64 = vgetq_lane_u64(vreinterpretq_u64_u8(matched), 0);
139 if (lo64 != 0) {
140 *index = idx + pm_ctzll(lo64) / 8;
141 return true;
142 }
143 uint64_t hi64 = vgetq_lane_u64(vreinterpretq_u64_u8(matched), 1);
144 *index = idx + 8 + pm_ctzll(hi64) / 8;
145 return true;
146 }
147
148 // Scalar tail for remaining < 16 ASCII bytes.
149 while (idx < maximum && source[idx] < 0x80) {
150 uint8_t byte = source[idx];
151 if (parser->strpbrk_cache.table[byte >> 6] & ((uint64_t) 1 << (byte & 0x3F))) {
152 *index = idx;
153 return true;
154 }
155 idx++;
156 }
157
158 *index = idx;
159 return false;
160}
161
162#elif defined(PRISM_HAS_SSSE3)
163#include <tmmintrin.h>
164
165static PRISM_INLINE bool
166scan_strpbrk_ascii(pm_parser_t *parser, const uint8_t *source, size_t maximum, const uint8_t *charset, size_t *index) {
167 pm_strpbrk_cache_update(parser, charset);
168
169 __m128i low_lut = _mm_loadu_si128((const __m128i *) parser->strpbrk_cache.low_lut);
170 __m128i high_lut = _mm_loadu_si128((const __m128i *) parser->strpbrk_cache.high_lut);
171 __m128i mask_0f = _mm_set1_epi8(0x0F);
172
173 size_t idx = 0;
174
175 while (idx + 16 <= maximum) {
176 __m128i v = _mm_loadu_si128((const __m128i *) (source + idx));
177
178 // If any byte has the high bit set, stop.
179 if (_mm_movemask_epi8(v) != 0) break;
180
181 // Nibble-based classification using pshufb (SSSE3), same as NEON
182 // vqtbl1q_u8. A byte matches iff (low_lut[lo_nib] & high_lut[hi_nib]) != 0.
183 __m128i lo_class = _mm_shuffle_epi8(low_lut, _mm_and_si128(v, mask_0f));
184 __m128i hi_class = _mm_shuffle_epi8(high_lut, _mm_and_si128(_mm_srli_epi16(v, 4), mask_0f));
185 __m128i matched = _mm_and_si128(lo_class, hi_class);
186
187 // Check if any byte matched.
188 int mask = _mm_movemask_epi8(_mm_cmpeq_epi8(matched, _mm_setzero_si128()));
189
190 if (mask == 0xFFFF) {
191 // All bytes were zero — no match in this chunk.
192 idx += 16;
193 continue;
194 }
195
196 // Find the first matching byte (first non-zero in matched).
197 *index = idx + pm_ctzll((uint64_t) (~mask & 0xFFFF));
198 return true;
199 }
200
201 // Scalar tail.
202 while (idx < maximum && source[idx] < 0x80) {
203 uint8_t byte = source[idx];
204 if (parser->strpbrk_cache.table[byte >> 6] & ((uint64_t) 1 << (byte & 0x3F))) {
205 *index = idx;
206 return true;
207 }
208 idx++;
209 }
210
211 *index = idx;
212 return false;
213}
214
215#elif defined(PRISM_HAS_SWAR)
216
217static PRISM_INLINE bool
218scan_strpbrk_ascii(pm_parser_t *parser, const uint8_t *source, size_t maximum, const uint8_t *charset, size_t *index) {
219 pm_strpbrk_cache_update(parser, charset);
220
221 static const uint64_t highs = 0x8080808080808080ULL;
222 size_t idx = 0;
223
224 while (idx + 8 <= maximum) {
225 uint64_t word;
226 memcpy(&word, source + idx, 8);
227
228 // Bail on any non-ASCII byte.
229 if (word & highs) break;
230
231 // Check each byte against the charset table.
232 for (size_t j = 0; j < 8; j++) {
233 uint8_t byte = source[idx + j];
234 if (parser->strpbrk_cache.table[byte >> 6] & ((uint64_t) 1 << (byte & 0x3F))) {
235 *index = idx + j;
236 return true;
237 }
238 }
239
240 idx += 8;
241 }
242
243 // Scalar tail.
244 while (idx < maximum && source[idx] < 0x80) {
245 uint8_t byte = source[idx];
246 if (parser->strpbrk_cache.table[byte >> 6] & ((uint64_t) 1 << (byte & 0x3F))) {
247 *index = idx;
248 return true;
249 }
250 idx++;
251 }
252
253 *index = idx;
254 return false;
255}
256
257#else
258
259static PRISM_INLINE bool
260scan_strpbrk_ascii(PRISM_UNUSED pm_parser_t *parser, PRISM_UNUSED const uint8_t *source, PRISM_UNUSED size_t maximum, PRISM_UNUSED const uint8_t *charset, size_t *index) {
261 *index = 0;
262 return false;
263}
264
265#endif
266
270static PRISM_INLINE const uint8_t *
271pm_strpbrk_utf8(pm_parser_t *parser, const uint8_t *source, const uint8_t *charset, size_t index, size_t maximum, bool validate) {
272 while (index < maximum) {
273 if (strchr((const char *) charset, source[index]) != NULL) {
274 return source + index;
275 }
276
277 if (source[index] < 0x80) {
278 index++;
279 } else {
280 size_t width = pm_encoding_utf_8_char_width(source + index, (ptrdiff_t) (maximum - index));
281
282 if (width > 0) {
283 index += width;
284 } else if (!validate) {
285 index++;
286 } else {
287 // At this point we know we have an invalid multibyte character.
288 // We'll walk forward as far as we can until we find the next
289 // valid character so that we don't spam the user with a ton of
290 // the same kind of error.
291 const size_t start = index;
292
293 do {
294 index++;
295 } while (index < maximum && pm_encoding_utf_8_char_width(source + index, (ptrdiff_t) (maximum - index)) == 0);
296
297 pm_strpbrk_invalid_multibyte_character(parser, (uint32_t) ((source + start) - parser->start), (uint32_t) (index - start));
298 }
299 }
300 }
301
302 return NULL;
303}
304
308static PRISM_INLINE const uint8_t *
309pm_strpbrk_ascii_8bit(pm_parser_t *parser, const uint8_t *source, const uint8_t *charset, size_t index, size_t maximum, bool validate) {
310 while (index < maximum) {
311 if (strchr((const char *) charset, source[index]) != NULL) {
312 return source + index;
313 }
314
315 if (validate && source[index] >= 0x80) pm_strpbrk_explicit_encoding_set(parser, (uint32_t) (source - parser->start), 1);
316 index++;
317 }
318
319 return NULL;
320}
321
325static PRISM_INLINE const uint8_t *
326pm_strpbrk_multi_byte(pm_parser_t *parser, const uint8_t *source, const uint8_t *charset, size_t index, size_t maximum, bool validate) {
327 const pm_encoding_t *encoding = parser->encoding;
328
329 while (index < maximum) {
330 if (strchr((const char *) charset, source[index]) != NULL) {
331 return source + index;
332 }
333
334 if (source[index] < 0x80) {
335 index++;
336 } else {
337 size_t width = encoding->char_width(source + index, (ptrdiff_t) (maximum - index));
338 if (validate) pm_strpbrk_explicit_encoding_set(parser, (uint32_t) (source - parser->start), (uint32_t) width);
339
340 if (width > 0) {
341 index += width;
342 } else if (!validate) {
343 index++;
344 } else {
345 // At this point we know we have an invalid multibyte character.
346 // We'll walk forward as far as we can until we find the next
347 // valid character so that we don't spam the user with a ton of
348 // the same kind of error.
349 const size_t start = index;
350
351 do {
352 index++;
353 } while (index < maximum && encoding->char_width(source + index, (ptrdiff_t) (maximum - index)) == 0);
354
355 pm_strpbrk_invalid_multibyte_character(parser, (uint32_t) ((source + start) - parser->start), (uint32_t) (index - start));
356 }
357 }
358 }
359
360 return NULL;
361}
362
367static PRISM_INLINE const uint8_t *
368pm_strpbrk_single_byte(pm_parser_t *parser, const uint8_t *source, const uint8_t *charset, size_t index, size_t maximum, bool validate) {
369 const pm_encoding_t *encoding = parser->encoding;
370
371 while (index < maximum) {
372 if (strchr((const char *) charset, source[index]) != NULL) {
373 return source + index;
374 }
375
376 if (source[index] < 0x80 || !validate) {
377 index++;
378 } else {
379 size_t width = encoding->char_width(source + index, (ptrdiff_t) (maximum - index));
380 pm_strpbrk_explicit_encoding_set(parser, (uint32_t) (source - parser->start), (uint32_t) width);
381
382 if (width > 0) {
383 index += width;
384 } else {
385 // At this point we know we have an invalid multibyte character.
386 // We'll walk forward as far as we can until we find the next
387 // valid character so that we don't spam the user with a ton of
388 // the same kind of error.
389 const size_t start = index;
390
391 do {
392 index++;
393 } while (index < maximum && encoding->char_width(source + index, (ptrdiff_t) (maximum - index)) == 0);
394
395 pm_strpbrk_invalid_multibyte_character(parser, (uint32_t) ((source + start) - parser->start), (uint32_t) (index - start));
396 }
397 }
398 }
399
400 return NULL;
401}
402
422const uint8_t *
423pm_strpbrk(pm_parser_t *parser, const uint8_t *source, const uint8_t *charset, ptrdiff_t length, bool validate) {
424 if (length <= 0) return NULL;
425
426 size_t maximum = (size_t) length;
427 size_t index = 0;
428 if (scan_strpbrk_ascii(parser, source, maximum, charset, &index)) return source + index;
429
430 if (!parser->encoding_changed) {
431 return pm_strpbrk_utf8(parser, source, charset, index, maximum, validate);
432 } else if (parser->encoding == PM_ENCODING_ASCII_8BIT_ENTRY) {
433 return pm_strpbrk_ascii_8bit(parser, source, charset, index, maximum, validate);
434 } else if (parser->encoding->multibyte) {
435 return pm_strpbrk_multi_byte(parser, source, charset, index, maximum, validate);
436 } else {
437 return pm_strpbrk_single_byte(parser, source, charset, index, maximum, validate);
438 }
439}
#define PRISM_INLINE
Old Visual Studio versions do not support the inline keyword, so we need to define it to be __inline.
Definition inline.h:12
C99 shim for <stdbool.h>
#define PRISM_UNUSED
GCC will warn if you specify a function or parameter that is unused at runtime.
Definition unused.h:13