@@ -1777,6 +1777,227 @@ char_is_identifier_utf8(const uint8_t *b, ptrdiff_t n) {
17771777 }
17781778}
17791779
1780+ /**
1781+ * Scan forward through ASCII identifier characters (a-z, A-Z, 0-9, _) using
1782+ * wide operations. Returns the number of leading ASCII identifier bytes.
1783+ * Callers must handle any remaining bytes (short tail or non-ASCII/UTF-8)
1784+ * with a byte-at-a-time loop.
1785+ *
1786+ * Up to four optimized implementations are selected at compile time, with a
1787+ * no-op fallback for unsupported platforms:
1788+ * 1. NEON — processes 16 bytes per iteration on aarch64.
1789+ * 2. SSE2 — processes 16 bytes per iteration on x86-64.
1790+ * 3. WASM SIMD — processes 16 bytes per iteration on WebAssembly.
1791+ * 4. SWAR — little-endian fallback, processes 8 bytes per iteration.
1792+ * 5. No-op — returns 0; the caller's byte-at-a-time loop handles everything.
1793+ */
1794+
1795+ #if defined(__aarch64__) && defined(__ARM_NEON)
1796+ #include <arm_neon.h>
1797+
1798+ static inline size_t
1799+ scan_identifier_ascii(const uint8_t *start, const uint8_t *end) {
1800+ const uint8_t *cursor = start;
1801+
1802+ // Nibble-based lookup tables for classifying [a-zA-Z0-9_].
1803+ // Each high nibble is assigned a unique bit; the low nibble table
1804+ // contains the OR of bits for all high nibbles that have an
1805+ // identifier character at that low nibble position. A byte is an
1806+ // identifier character iff (low_lut[lo] & high_lut[hi]) != 0.
1807+ const uint8x16_t low_lut = (uint8x16_t) {
1808+ 0x15, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F,
1809+ 0x1F, 0x1F, 0x1E, 0x0A, 0x0A, 0x0A, 0x0A, 0x0E
1810+ };
1811+ const uint8x16_t high_lut = (uint8x16_t) {
1812+ 0x00, 0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x10,
1813+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
1814+ };
1815+ const uint8x16_t mask_0f = vdupq_n_u8(0x0F);
1816+
1817+ while (cursor + 16 <= end) {
1818+ uint8x16_t v = vld1q_u8(cursor);
1819+
1820+ uint8x16_t lo_class = vqtbl1q_u8(low_lut, vandq_u8(v, mask_0f));
1821+ uint8x16_t hi_class = vqtbl1q_u8(high_lut, vshrq_n_u8(v, 4));
1822+ uint8x16_t ident = vandq_u8(lo_class, hi_class);
1823+
1824+ // Fast check: if the per-byte minimum is nonzero, every byte matched.
1825+ if (vminvq_u8(ident) != 0) {
1826+ cursor += 16;
1827+ continue;
1828+ }
1829+
1830+ // Find the first non-identifier byte (zero in ident).
1831+ uint8x16_t is_zero = vceqq_u8(ident, vdupq_n_u8(0));
1832+ uint64_t lo = vgetq_lane_u64(vreinterpretq_u64_u8(is_zero), 0);
1833+
1834+ if (lo != 0) {
1835+ cursor += pm_ctzll(lo) / 8;
1836+ } else {
1837+ uint64_t hi = vgetq_lane_u64(vreinterpretq_u64_u8(is_zero), 1);
1838+ cursor += 8 + pm_ctzll(hi) / 8;
1839+ }
1840+
1841+ return (size_t) (cursor - start);
1842+ }
1843+
1844+ return (size_t) (cursor - start);
1845+ }
1846+
1847+ #elif defined(__x86_64__) && defined(__SSE2__)
1848+ #include <emmintrin.h>
1849+
1850+ static inline size_t
1851+ scan_identifier_ascii(const uint8_t *start, const uint8_t *end) {
1852+ const uint8_t *cursor = start;
1853+
1854+ while (cursor + 16 <= end) {
1855+ __m128i v = _mm_loadu_si128((const __m128i *) cursor);
1856+ __m128i zero = _mm_setzero_si128();
1857+
1858+ // Unsigned range check via saturating subtraction:
1859+ // byte >= lo ⟺ saturate(lo - byte) == 0
1860+ // byte <= hi ⟺ saturate(byte - hi) == 0
1861+
1862+ // Fold case: OR with 0x20 maps A-Z to a-z.
1863+ __m128i lowered = _mm_or_si128(v, _mm_set1_epi8(0x20));
1864+ __m128i letter = _mm_and_si128(
1865+ _mm_cmpeq_epi8(_mm_subs_epu8(_mm_set1_epi8(0x61), lowered), zero),
1866+ _mm_cmpeq_epi8(_mm_subs_epu8(lowered, _mm_set1_epi8(0x7A)), zero));
1867+
1868+ __m128i digit = _mm_and_si128(
1869+ _mm_cmpeq_epi8(_mm_subs_epu8(_mm_set1_epi8(0x30), v), zero),
1870+ _mm_cmpeq_epi8(_mm_subs_epu8(v, _mm_set1_epi8(0x39)), zero));
1871+
1872+ __m128i underscore = _mm_cmpeq_epi8(v, _mm_set1_epi8(0x5F));
1873+
1874+ __m128i ident = _mm_or_si128(_mm_or_si128(letter, digit), underscore);
1875+ int mask = _mm_movemask_epi8(ident);
1876+
1877+ if (mask == 0xFFFF) {
1878+ cursor += 16;
1879+ continue;
1880+ }
1881+
1882+ cursor += pm_ctzll((uint64_t) (~mask & 0xFFFF));
1883+ return (size_t) (cursor - start);
1884+ }
1885+
1886+ return (size_t) (cursor - start);
1887+ }
1888+
1889+ #elif defined(__wasm_simd128__)
1890+ #include <wasm_simd128.h>
1891+
1892+ static inline size_t
1893+ scan_identifier_ascii(const uint8_t *start, const uint8_t *end) {
1894+ const uint8_t *cursor = start;
1895+
1896+ while (cursor + 16 <= end) {
1897+ v128_t v = wasm_v128_load(cursor);
1898+
1899+ // Range checks via subtract-and-unsigned-compare: (v - lo) < count
1900+ // is true iff v is in [lo, lo + count). One subtract + one compare
1901+ // per range instead of two comparisons + AND.
1902+
1903+ // Fold case: OR with 0x20 maps A-Z to a-z.
1904+ v128_t lowered = wasm_v128_or(v, wasm_u8x16_splat(0x20));
1905+ v128_t letter = wasm_u8x16_lt(
1906+ wasm_i8x16_sub(lowered, wasm_u8x16_splat(0x61)),
1907+ wasm_u8x16_splat(0x1A));
1908+
1909+ v128_t digit = wasm_u8x16_lt(
1910+ wasm_i8x16_sub(v, wasm_u8x16_splat(0x30)),
1911+ wasm_u8x16_splat(0x0A));
1912+
1913+ v128_t underscore = wasm_i8x16_eq(v, wasm_u8x16_splat(0x5F));
1914+
1915+ v128_t ident = wasm_v128_or(wasm_v128_or(letter, digit), underscore);
1916+
1917+ // Fast path: if all 16 bytes are identifier chars, advance.
1918+ if (wasm_i8x16_all_true(ident)) {
1919+ cursor += 16;
1920+ continue;
1921+ }
1922+
1923+ // Extract bitmask only on the exit path to find the first non-match.
1924+ uint32_t mask = wasm_i8x16_bitmask(ident);
1925+ cursor += pm_ctzll((uint64_t) (~mask & 0xFFFF));
1926+ return (size_t) (cursor - start);
1927+ }
1928+
1929+ return (size_t) (cursor - start);
1930+ }
1931+
1932+ // The SWAR path uses pm_ctzll to find the first non-matching byte within a
1933+ // word, which only yields the correct byte index on little-endian targets.
1934+ // We gate on a positive little-endian check so that unknown-endianness
1935+ // platforms safely fall through to the no-op fallback.
1936+ #elif defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
1937+
1938+ /**
1939+ * Portable SWAR fallback — processes 8 bytes per iteration.
1940+ *
1941+ * The byte-wise range checks avoid cross-byte borrows by pre-setting the high
1942+ * bit of each byte before subtraction: (byte | 0x80) - lo has a minimum value
1943+ * of 0x80 - 0x7F = 1, so underflow (and thus a borrow into the next byte) is
1944+ * impossible. The result has bit 7 set if and only if byte >= lo. The same
1945+ * reasoning applies to the upper-bound direction.
1946+ */
1947+ static inline size_t
1948+ scan_identifier_ascii(const uint8_t *start, const uint8_t *end) {
1949+ static const uint64_t ones = 0x0101010101010101ULL;
1950+ static const uint64_t highs = 0x8080808080808080ULL;
1951+ const uint8_t *cursor = start;
1952+
1953+ while (cursor + 8 <= end) {
1954+ uint64_t word;
1955+ memcpy(&word, cursor, 8);
1956+
1957+ // Bail on any non-ASCII byte.
1958+ if (word & highs) break;
1959+
1960+ uint64_t digit = ((word | highs) - ones * 0x30) & ((ones * 0x39 | highs) - word) & highs;
1961+
1962+ // Fold upper- and lowercase together by forcing bit 5 (OR 0x20),
1963+ // then check the lowercase range once. A-Z maps to a-z; the
1964+ // only non-letter byte that could alias into [0x61,0x7A] is one
1965+ // whose original value was in [0x41,0x5A] — which is exactly
1966+ // the uppercase letters we want to match.
1967+ uint64_t lowered = word | (ones * 0x20);
1968+ uint64_t letter = ((lowered | highs) - ones * 0x61) & ((ones * 0x7A | highs) - lowered) & highs;
1969+
1970+ // Standard SWAR "has zero byte" idiom on (word XOR 0x5F) to find
1971+ // bytes equal to underscore. Safe from cross-byte borrows because
1972+ // the ASCII guard above ensures all bytes are < 0x80.
1973+ uint64_t xor_us = word ^ (ones * 0x5F);
1974+ uint64_t underscore = (xor_us - ones) & ~xor_us & highs;
1975+
1976+ uint64_t ident = digit | letter | underscore;
1977+
1978+ if (ident == highs) {
1979+ cursor += 8;
1980+ continue;
1981+ }
1982+
1983+ // Find the first non-identifier byte. On little-endian the first
1984+ // byte sits in the least-significant position.
1985+ uint64_t not_ident = ~ident & highs;
1986+ cursor += pm_ctzll(not_ident) / 8;
1987+ return (size_t) (cursor - start);
1988+ }
1989+
1990+ return (size_t) (cursor - start);
1991+ }
1992+
1993+ #else
1994+
1995+ // No-op fallback for big-endian or other unsupported platforms.
1996+ // The caller's byte-at-a-time loop handles everything.
1997+ #define scan_identifier_ascii(start, end) ((size_t) 0)
1998+
1999+ #endif
2000+
17802001/**
17812002 * Like the above, this function is also used extremely frequently to lex all of
17822003 * the identifiers in a source file once the first character has been found. So
@@ -8155,6 +8376,10 @@ lex_identifier(pm_parser_t *parser, bool previous_command_start) {
81558376 current_end += width;
81568377 }
81578378 } else {
8379+ // Fast path: scan ASCII identifier bytes using wide operations.
8380+ current_end += scan_identifier_ascii(current_end, end);
8381+
8382+ // Byte-at-a-time fallback for the tail and any UTF-8 sequences.
81588383 while ((width = char_is_identifier_utf8(current_end, end - current_end)) > 0) {
81598384 current_end += width;
81608385 }
0 commit comments