diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5892-buffer-write-utf16le-runs.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5892-buffer-write-utf16le-runs.ts.json new file mode 100644 index 000000000..012edf8c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5892-buffer-write-utf16le-runs.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5892-buffer-write-utf16le-runs.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index e0fb55464..df5b8a3c0 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -17,6 +17,11 @@ #include #include #include +#if defined(__SSE2__) +#include +#elif defined(__ARM_NEON) && defined(__aarch64__) +#include +#endif static bool scr_td_label_space(unsigned char c) { return c == 9 || c == 10 || c == 12 || c == 13 || c == 32; @@ -3302,13 +3307,30 @@ double scr_bytes_write_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc, while (i < s->len && budget - written >= width) { #if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ /* utf16le of an ASCII run: zero-extend each byte to a little-endian - * unit, eight per step with two word stores (the generic loop below - * stores byte by byte, because the source and destination may alias - * as far as the compiler knows). */ + * unit, sixteen per step with vector unpacks where the target has them + * and eight per step with two word stores otherwise (the generic loop + * below stores byte by byte, because the source and destination may + * alias as far as the compiler knows). */ if (utf16) { uint8_t *d = b->data + o + written; size_t n = 0, room = (budget - written) / 16, left = (s->len - i) / 8; size_t steps = room < left ? room : left; +#if defined(__SSE2__) + const __m128i zero = _mm_setzero_si128(); + for (; n + 2 <= steps; n += 2) { + __m128i v = _mm_loadu_si128((const __m128i *)(s->data + i + 8 * n)); + if (_mm_movemask_epi8(v)) break; + _mm_storeu_si128((__m128i *)(d + 16 * n), _mm_unpacklo_epi8(v, zero)); + _mm_storeu_si128((__m128i *)(d + 16 * n + 16), _mm_unpackhi_epi8(v, zero)); + } +#elif defined(__ARM_NEON) && defined(__aarch64__) + for (; n + 2 <= steps; n += 2) { + uint8x16_t v = vld1q_u8((const uint8_t *)s->data + i + 8 * n); + if (vmaxvq_u8(v) & 0x80) break; + vst1q_u8(d + 16 * n, vreinterpretq_u8_u16(vmovl_u8(vget_low_u8(v)))); + vst1q_u8(d + 16 * n + 16, vreinterpretq_u8_u16(vmovl_high_u8(v))); + } +#endif for (; n < steps; n++) { uint64_t w; memcpy(&w, s->data + i + 8 * n, 8); diff --git a/tests/corpus/5892-buffer-write-utf16le-runs.ts b/tests/corpus/5892-buffer-write-utf16le-runs.ts new file mode 100644 index 000000000..a69c0e8d5 --- /dev/null +++ b/tests/corpus/5892-buffer-write-utf16le-runs.ts @@ -0,0 +1,60 @@ +// Buffer.write(text, "utf16le") over ASCII runs of every length around the +// sixteen-byte widening steps (4732 covers the eight-byte ones): odd and even +// offsets, length limits that cut a run, and non-ASCII, astral and +// lone-surrogate units at every position of a 41-unit run. + +function hex(bytes: Uint8Array): string { + let out = ""; + for (let i = 0; i < bytes.length; i++) out += (bytes[i]! < 16 ? "0" : "") + bytes[i]!.toString(16); + return out; +} + +function digest(bytes: Uint8Array): number { + let h = 0x811c9dc5 | 0; + for (let i = 0; i < bytes.length; i++) h = Math.imul(h ^ bytes[i]!, 0x01000193); + return h >>> 0; +} + +const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$"; +function run(n: number): string { + let s = ""; + for (let i = 0; i < n; i++) s += alphabet[i % alphabet.length]; + return s; +} + +let summary = 0; +for (let n = 0; n <= 70; n++) { + const text = run(n); + for (const offset of [0, 1, 3, 16]) { + const buf = Buffer.alloc(offset + 2 * n + 5, 0xee); + const written = buf.write(text, offset, "utf16le"); + summary = (Math.imul(summary, 31) + written + digest(buf)) | 0; + for (const limit of [0, 1, 15, 16, 17, 31, 32, 33]) { + if (limit > 2 * n + 5) continue; + const cut = Buffer.alloc(offset + 2 * n + 5, 0xee); + const w = cut.write(text, offset, limit, "utf16le"); + summary = (Math.imul(summary, 31) + w + digest(cut)) | 0; + } + } +} +console.log("runs", summary); + +for (const insert of ["é", "€", "😀", "\ud800", "\udc00", "\u0080", "ÿ"]) { + let local = 0; + for (let at = 0; at <= 40; at++) { + const text = run(at) + insert + run(40 - at); + const buf = Buffer.alloc(2 * text.length + 3, 0xee); + const written = buf.write(text, 1, "utf16le"); + local = (Math.imul(local, 31) + written + digest(buf)) | 0; + if (at === 17) console.log(JSON.stringify(insert), written, hex(buf.subarray(33, 41))); + } + console.log("insert", JSON.stringify(insert), local); +} + +// A code-unit array the way a scanner builds one, then read back. +const source = run(1000) + "\n" + "const x = 1;\r\n".repeat(50); +const units = new Uint16Array(source.length + 4); +const n = Buffer.from(units.buffer).write(source, "utf16le"); +let same = n === 2 * source.length; +for (let i = 0; i < source.length; i++) if (units[i] !== source.charCodeAt(i)) same = false; +console.log("units", n, same, units[source.length], units[source.length + 3]);