@scriptc/cli-darwin-x64 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/THIRD_PARTY_NOTICES +1 -1
  2. package/dist/bin/scriptc +0 -0
  3. package/dist/bin/scriptc.json +1 -1
  4. package/dist/lib/declarations/scriptc-node-fallback.d.ts +915 -215
  5. package/dist/lib/declarations/scriptc-overrides.d.ts +5 -1
  6. package/dist/lib/declarations/scriptc.d.ts +22 -2
  7. package/dist/lib/llvm/bin/scriptc-llvm-codegen +0 -0
  8. package/dist/lib/llvm/package.json +1 -1
  9. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_array.o +0 -0
  10. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_bigint.o +0 -0
  11. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_bytes.o +0 -0
  12. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_bytes_io.o +0 -0
  13. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_copying.o +0 -0
  14. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_cycle.o +0 -0
  15. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_json.o +0 -0
  16. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_lib.o +0 -0
  17. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_map.o +0 -0
  18. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_number.o +0 -0
  19. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_regex.o +0 -0
  20. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_string.o +0 -0
  21. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/default/scr_util_compare.o +0 -0
  22. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_array.o +0 -0
  23. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_bigint.o +0 -0
  24. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_bytes.o +0 -0
  25. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_bytes_io.o +0 -0
  26. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_copying.o +0 -0
  27. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_cycle.o +0 -0
  28. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_json.o +0 -0
  29. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_lib.o +0 -0
  30. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_map.o +0 -0
  31. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_number.o +0 -0
  32. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_regex.o +0 -0
  33. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_string.o +0 -0
  34. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic/scr_util_compare.o +0 -0
  35. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/dynamic-text-decoder-legacy/scr_bytes.o +0 -0
  36. package/dist/lib/runtime-darwin-x64/artifacts/dev/runtime/text-decoder-legacy/scr_bytes.o +0 -0
  37. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_array.o +0 -0
  38. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_bigint.o +0 -0
  39. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_bytes.o +0 -0
  40. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_bytes_io.o +0 -0
  41. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_copying.o +0 -0
  42. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_cycle.o +0 -0
  43. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_json.o +0 -0
  44. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_lib.o +0 -0
  45. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_map.o +0 -0
  46. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_number.o +0 -0
  47. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_regex.o +0 -0
  48. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/default/scr_string.o +0 -0
  49. package/dist/lib/runtime-darwin-x64/artifacts/library-dev/runtime/text-decoder-legacy/scr_bytes.o +0 -0
  50. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_array.o +0 -0
  51. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_bigint.o +0 -0
  52. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_bytes.o +0 -0
  53. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_bytes_io.o +0 -0
  54. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_copying.o +0 -0
  55. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_cycle.o +0 -0
  56. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_json.o +0 -0
  57. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_lib.o +0 -0
  58. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_map.o +0 -0
  59. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_number.o +0 -0
  60. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_regex.o +0 -0
  61. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/default/scr_string.o +0 -0
  62. package/dist/lib/runtime-darwin-x64/artifacts/library-release/runtime/text-decoder-legacy/scr_bytes.o +0 -0
  63. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_array.o +0 -0
  64. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_bigint.o +0 -0
  65. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_bytes.o +0 -0
  66. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_bytes_io.o +0 -0
  67. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_copying.o +0 -0
  68. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_cycle.o +0 -0
  69. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_json.o +0 -0
  70. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_lib.o +0 -0
  71. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_map.o +0 -0
  72. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_number.o +0 -0
  73. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_regex.o +0 -0
  74. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/default/scr_string.o +0 -0
  75. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-dev/runtime/text-decoder-legacy/scr_bytes.o +0 -0
  76. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_array.o +0 -0
  77. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_bigint.o +0 -0
  78. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_bytes.o +0 -0
  79. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_bytes_io.o +0 -0
  80. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_copying.o +0 -0
  81. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_cycle.o +0 -0
  82. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_json.o +0 -0
  83. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_lib.o +0 -0
  84. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_map.o +0 -0
  85. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_number.o +0 -0
  86. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_regex.o +0 -0
  87. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/default/scr_string.o +0 -0
  88. package/dist/lib/runtime-darwin-x64/artifacts/library-thread-release/runtime/text-decoder-legacy/scr_bytes.o +0 -0
  89. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_array.o +0 -0
  90. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_bigint.o +0 -0
  91. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_bytes.o +0 -0
  92. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_bytes_io.o +0 -0
  93. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_copying.o +0 -0
  94. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_cycle.o +0 -0
  95. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_json.o +0 -0
  96. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_lib.o +0 -0
  97. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_map.o +0 -0
  98. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_number.o +0 -0
  99. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_regex.o +0 -0
  100. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_string.o +0 -0
  101. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/default/scr_util_compare.o +0 -0
  102. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_array.o +0 -0
  103. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_bigint.o +0 -0
  104. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_bytes.o +0 -0
  105. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_bytes_io.o +0 -0
  106. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_copying.o +0 -0
  107. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_cycle.o +0 -0
  108. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_json.o +0 -0
  109. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_lib.o +0 -0
  110. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_map.o +0 -0
  111. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_number.o +0 -0
  112. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_regex.o +0 -0
  113. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_string.o +0 -0
  114. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic/scr_util_compare.o +0 -0
  115. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/dynamic-text-decoder-legacy/scr_bytes.o +0 -0
  116. package/dist/lib/runtime-darwin-x64/artifacts/release/runtime/text-decoder-legacy/scr_bytes.o +0 -0
  117. package/dist/lib/runtime-darwin-x64/package.json +1 -1
  118. package/dist/lib/runtime-darwin-x64/runtime-pack.json +215 -215
  119. package/dist/lib/runtime-sources/package.json +1 -1
  120. package/dist/lib/runtime-sources/src/scr_array.c +317 -40
  121. package/dist/lib/runtime-sources/src/scr_async.test.ts +16 -6
  122. package/dist/lib/runtime-sources/src/scr_bigint.c +15 -0
  123. package/dist/lib/runtime-sources/src/scr_bytes.c +437 -216
  124. package/dist/lib/runtime-sources/src/scr_bytes.test.c +85 -0
  125. package/dist/lib/runtime-sources/src/scr_bytes_io.c +0 -34
  126. package/dist/lib/runtime-sources/src/scr_copying.c +12 -2
  127. package/dist/lib/runtime-sources/src/scr_cycle.c +84 -60
  128. package/dist/lib/runtime-sources/src/scr_json.c +494 -161
  129. package/dist/lib/runtime-sources/src/scr_json.test.c +323 -1
  130. package/dist/lib/runtime-sources/src/scr_json.test.ts +58 -25
  131. package/dist/lib/runtime-sources/src/scr_key.h +68 -0
  132. package/dist/lib/runtime-sources/src/scr_lib.c +177 -89
  133. package/dist/lib/runtime-sources/src/scr_map.c +268 -96
  134. package/dist/lib/runtime-sources/src/scr_number.c +2 -4
  135. package/dist/lib/runtime-sources/src/scr_numeric.h +25 -0
  136. package/dist/lib/runtime-sources/src/scr_numeric.test.c +37 -0
  137. package/dist/lib/runtime-sources/src/scr_numeric.test.ts +33 -0
  138. package/dist/lib/runtime-sources/src/scr_regex.c +255 -145
  139. package/dist/lib/runtime-sources/src/scr_runtime.h +76 -19
  140. package/dist/lib/runtime-sources/src/scr_string.c +184 -42
  141. package/package.json +1 -1
@@ -13,10 +13,9 @@
13
13
  * lre_compile rejects aborts with a clear
14
14
  * message (Node throws SyntaxError at parse time — documented divergence;
15
15
  * rare, since tsc's parser has already syntax-checked the literal).
16
- * - Subjects are matched as UTF-16: lre_exec runs over a per-call
17
- * conversion of the UTF-8 subject (cbuf_type 1), so every index libregexp
18
- * reports is a UTF-16 code-unit index — .length/slice/charCodeAt exact,
19
- * like Node. Correctness-first: the conversion is O(n) per call.
16
+ * - ASCII subjects borrow their existing bytes; other subjects use a
17
+ * per-call UTF-16 conversion. Both engine representations report UTF-16
18
+ * code-unit indices, matching .length/slice/charCodeAt.
20
19
  * - Match loops mirror quickjs.c's own (JS_RegExpDelete /
21
20
  * js_regexp_Symbol_replace): capture buffers are sized by
22
21
  * lre_get_alloc_count (NOT capture_count*2 — the executor writes
@@ -304,6 +303,8 @@ ScrStr *scr_regex_to_string(ScrRegex *re) {
304
303
  /* Subject as freshly-malloc'd UTF-16 code units (strings are well-formed
305
304
  * UTF-8, so no validation). Unit count ≤ byte count. */
306
305
  static uint16_t *scr_to_utf16(const ScrStr *s, int *plen) {
306
+ if (s->len > (size_t)INT32_MAX - 1 || s->len > SIZE_MAX / sizeof(uint16_t))
307
+ scr_regex_oom();
307
308
  uint16_t *u = malloc((s->len ? s->len : 1) * sizeof(uint16_t));
308
309
  if (!u) scr_regex_oom();
309
310
  int n = 0;
@@ -339,53 +340,98 @@ static uint16_t *scr_to_utf16(const ScrStr *s, int *plen) {
339
340
  return u;
340
341
  }
341
342
 
342
- /* Append u[start..end) as UTF-8. A lone surrogate (a non-/u match boundary
343
- * split an astral pair) becomes U+FFFD — the house rule for JS results that
344
- * are not well-formed UTF-16 (same divergence class as charAt/slice,
345
- * SEMANTICS.md). */
343
+ /* Decode one UTF-16 code point. A boundary that splits a surrogate pair
344
+ * produces U+FFFD, as in the runtime's string slicing contract. */
345
+ static uint32_t scr_regex_codepoint(const uint16_t *units, int *index, int end) {
346
+ uint32_t cp = units[(*index)++];
347
+ if (cp >= 0xD800 && cp < 0xDC00 && *index < end &&
348
+ units[*index] >= 0xDC00 && units[*index] < 0xE000)
349
+ return 0x10000 + ((cp - 0xD800) << 10) + (units[(*index)++] - 0xDC00);
350
+ return cp >= 0xD800 && cp < 0xE000 ? 0xFFFD : cp;
351
+ }
352
+
353
+ static size_t scr_regex_encode(char *out, uint32_t cp) {
354
+ if (cp < 0x80) {
355
+ out[0] = (char)cp;
356
+ return 1;
357
+ }
358
+ if (cp < 0x800) {
359
+ out[0] = (char)(0xC0 | (cp >> 6));
360
+ out[1] = (char)(0x80 | (cp & 0x3F));
361
+ return 2;
362
+ }
363
+ if (cp < 0x10000) {
364
+ out[0] = (char)(0xE0 | (cp >> 12));
365
+ out[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
366
+ out[2] = (char)(0x80 | (cp & 0x3F));
367
+ return 3;
368
+ }
369
+ out[0] = (char)(0xF0 | (cp >> 18));
370
+ out[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
371
+ out[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
372
+ out[3] = (char)(0x80 | (cp & 0x3F));
373
+ return 4;
374
+ }
375
+
346
376
  static void scr_jb_put_utf16(ScrJsonBuf *b, const uint16_t *u, int start, int end) {
347
- for (int i = start; i < end; i++) {
348
- uint32_t cp = u[i];
349
- if (cp >= 0xD800 && cp < 0xDC00 && i + 1 < end && u[i + 1] >= 0xDC00 &&
350
- u[i + 1] < 0xE000) {
351
- cp = 0x10000 + ((cp - 0xD800) << 10) + (u[i + 1] - 0xDC00);
352
- i++;
353
- } else if (cp >= 0xD800 && cp < 0xE000) {
354
- cp = 0xFFFD;
355
- }
356
- if (cp < 0x80) {
357
- scr_jb_putc(b, (char)cp);
358
- } else if (cp < 0x800) {
359
- scr_jb_putc(b, (char)(0xC0 | (cp >> 6)));
360
- scr_jb_putc(b, (char)(0x80 | (cp & 0x3F)));
361
- } else if (cp < 0x10000) {
362
- scr_jb_putc(b, (char)(0xE0 | (cp >> 12)));
363
- scr_jb_putc(b, (char)(0x80 | ((cp >> 6) & 0x3F)));
364
- scr_jb_putc(b, (char)(0x80 | (cp & 0x3F)));
365
- } else {
366
- scr_jb_putc(b, (char)(0xF0 | (cp >> 18)));
367
- scr_jb_putc(b, (char)(0x80 | ((cp >> 12) & 0x3F)));
368
- scr_jb_putc(b, (char)(0x80 | ((cp >> 6) & 0x3F)));
369
- scr_jb_putc(b, (char)(0x80 | (cp & 0x3F)));
377
+ char bytes[256];
378
+ size_t used = 0;
379
+ for (int i = start; i < end;) {
380
+ if (used > sizeof bytes - 4) {
381
+ scr_jb_write(b, bytes, used);
382
+ used = 0;
370
383
  }
384
+ used += scr_regex_encode(bytes + used, scr_regex_codepoint(u, &i, end));
371
385
  }
386
+ scr_jb_write(b, bytes, used);
372
387
  }
373
388
 
374
- static ScrStr *scr_str_from_utf16(const uint16_t *u, int start, int end) {
375
- ScrJsonBuf b;
376
- scr_jb_init(&b);
377
- scr_jb_put_utf16(&b, u, start, end);
378
- return scr_jb_finish(&b);
389
+ /* Matching owns only the converted subject and oversized register buffer.
390
+ * The caller keeps the original immutable string alive. ASCII bytes are
391
+ * already the engine's one-byte representation, even under /u or /i. */
392
+ typedef struct ScrRegexInput {
393
+ const uint8_t *data;
394
+ uint16_t *utf16;
395
+ int len;
396
+ int wide;
397
+ uint8_t **capture;
398
+ uint8_t *registers[32];
399
+ } ScrRegexInput;
400
+
401
+ static bool scr_text_ascii(const ScrStr *s) {
402
+ size_t i = 0;
403
+ for (; s->len - i >= sizeof(uint64_t); i += sizeof(uint64_t)) {
404
+ uint64_t word;
405
+ memcpy(&word, s->data + i, sizeof word);
406
+ if (word & UINT64_C(0x8080808080808080)) return false;
407
+ }
408
+ for (; i < s->len; i++) {
409
+ if ((unsigned char)s->data[i] >= 0x80) return false;
410
+ }
411
+ return true;
379
412
  }
380
413
 
381
- /* ── exec plumbing ────────────────────────────────────────────────────── */
414
+ static void scr_regex_input_init(ScrRegexInput *input, const uint8_t *bc,
415
+ const ScrStr *s) {
416
+ if (s->len > (size_t)INT32_MAX - 1) scr_regex_oom();
417
+ input->wide = !scr_text_ascii(s);
418
+ input->utf16 = input->wide ? scr_to_utf16(s, &input->len) : NULL;
419
+ input->data = input->wide ? (const uint8_t *)input->utf16 : (const uint8_t *)s->data;
420
+ if (!input->wide) input->len = (int)s->len;
421
+ /* The executor writes temporary registers beyond the capture pairs. */
422
+ int count = lre_get_alloc_count(bc);
423
+ input->capture = count <= 32 ? input->registers : malloc((size_t)count * sizeof(uint8_t *));
424
+ if (!input->capture) scr_regex_oom();
425
+ }
382
426
 
383
- /* One lre_exec over the UTF-16 subject. Returns 1 (match), 0 (no match);
384
- * engine-level failures (OOM, corrupt bytecode — no timeout handler is
385
- * installed) abort like every runtime OOM. */
386
- static int scr_exec(uint8_t **capture, const uint8_t *bc, const uint16_t *u,
387
- int index, int len) {
388
- int rc = lre_exec(capture, bc, (const uint8_t *)u, index, len, 1, lre_opaque());
427
+ static void scr_regex_input_dispose(ScrRegexInput *input) {
428
+ if (input->capture != input->registers) free(input->capture);
429
+ free(input->utf16);
430
+ }
431
+
432
+ static int scr_exec(ScrRegexInput *input, const uint8_t *bc, int index) {
433
+ int rc = lre_exec(input->capture, bc, input->data, index, input->len,
434
+ input->wide, lre_opaque());
389
435
  if (rc < 0) {
390
436
  fflush(stdout);
391
437
  scr_trap("scriptc: regular expression execution failed\n");
@@ -393,24 +439,43 @@ static int scr_exec(uint8_t **capture, const uint8_t *bc, const uint16_t *u,
393
439
  return rc;
394
440
  }
395
441
 
396
- /* Capture buffer sized by lre_get_alloc_count: the register-based executor
397
- * writes temporary registers past the capture pairs (quickjs.c documents
398
- * sizing by capture_count*2 as a silent heap overflow). */
399
- static uint8_t **scr_capture_alloc(const uint8_t *bc) {
400
- int n = lre_get_alloc_count(bc);
401
- uint8_t **cap = malloc((n > 0 ? (size_t)n : 1) * sizeof *cap);
402
- if (!cap) scr_regex_oom();
403
- return cap;
442
+ static int scr_capture_index(const ScrRegexInput *input, const uint8_t *capture) {
443
+ return (int)((capture - input->data) >> input->wide);
444
+ }
445
+
446
+ static void scr_regex_put_span(ScrJsonBuf *b, const ScrRegexInput *input,
447
+ int start, int end) {
448
+ if (input->wide) scr_jb_put_utf16(b, input->utf16, start, end);
449
+ else scr_jb_write(b, (const char *)input->data + start, (size_t)(end - start));
450
+ }
451
+
452
+ static ScrStr *scr_regex_span(const ScrRegexInput *input, int start, int end) {
453
+ if (!input->wide)
454
+ return scr_str_new((const char *)input->data + start, (size_t)(end - start));
455
+ /* Captures have a known span: allocate its exact UTF-8 length instead
456
+ * of inheriting a general builder's size hint from an earlier document. */
457
+ size_t bytes = 0;
458
+ for (int i = start; i < end;) {
459
+ uint32_t cp = scr_regex_codepoint(input->utf16, &i, end);
460
+ size_t width = cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
461
+ if (width > SIZE_MAX - sizeof(ScrStr) - 1 - bytes) scr_regex_oom();
462
+ bytes += width;
463
+ }
464
+ ScrStr *out = scr_str_alloc_raw(bytes, bytes);
465
+ size_t offset = 0;
466
+ for (int i = start; i < end;)
467
+ offset += scr_regex_encode(out->data + offset, scr_regex_codepoint(input->utf16, &i, end));
468
+ out->data[bytes] = '\0';
469
+ return out;
404
470
  }
405
471
 
406
472
  /* ECMA-262 AdvanceStringIndex: past a whole surrogate pair under /u, one
407
- * code unit otherwise — the rule that keeps zero-length matches from
408
- * looping forever. */
409
- static int scr_advance(const uint16_t *u, int len, int i, bool unicode) {
410
- if (unicode && i + 1 < len && u[i] >= 0xD800 && u[i] < 0xDC00 &&
411
- u[i + 1] >= 0xDC00 && u[i + 1] < 0xE000) {
473
+ * code unit otherwise. ASCII cannot contain a surrogate pair. */
474
+ static int scr_advance(const ScrRegexInput *input, int i, bool unicode) {
475
+ const uint16_t *u = input->utf16;
476
+ if (unicode && input->wide && i + 1 < input->len &&
477
+ u[i] >= 0xD800 && u[i] < 0xDC00 && u[i + 1] >= 0xDC00 && u[i + 1] < 0xE000)
412
478
  return i + 2;
413
- }
414
479
  return i + 1;
415
480
  }
416
481
 
@@ -420,13 +485,6 @@ void scr_regex_reset_last_index(ScrRegex *re, double index) {
420
485
  re->last_index = index;
421
486
  }
422
487
 
423
- bool scr_regex_test(ScrRegex *re, ScrStr *s) {
424
- ScrArr *result = scr_regex_exec(s, re);
425
- bool matched = result != NULL;
426
- scr_arr_release(result);
427
- return matched;
428
- }
429
-
430
488
  static int scr_regex_start(ScrRegex *re, int len) {
431
489
  double index = re->last_index;
432
490
  if (isnan(index) || index <= 0) return 0;
@@ -434,6 +492,20 @@ static int scr_regex_start(ScrRegex *re, int len) {
434
492
  return (int)floor(index);
435
493
  }
436
494
 
495
+ /* A predicate needs only boundaries; it never materializes result strings. */
496
+ bool scr_regex_test(ScrRegex *re, ScrStr *s) {
497
+ uint8_t *bc = scr_regex_bc(re);
498
+ ScrRegexInput input;
499
+ scr_regex_input_init(&input, bc, s);
500
+ bool stateful = (lre_get_flags(bc) & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) != 0;
501
+ int pos = stateful ? scr_regex_start(re, input.len) : 0;
502
+ int rc = pos < 0 ? 0 : scr_exec(&input, bc, pos);
503
+ if (stateful) re->last_index = rc == 1
504
+ ? (double)scr_capture_index(&input, input.capture[1]) : 0;
505
+ scr_regex_input_dispose(&input);
506
+ return rc == 1;
507
+ }
508
+
437
509
  /* ── match ────────────────────────────────────────────────────────────── */
438
510
 
439
511
  /* re.exec(s): a fresh string[] of [whole match, ...captures], or NULL
@@ -444,20 +516,20 @@ static int scr_regex_start(ScrRegex *re, int len) {
444
516
  ScrArr *scr_regex_exec(ScrStr *s, ScrRegex *re) {
445
517
  uint8_t *bc = scr_regex_bc(re);
446
518
  bool stateful = (lre_get_flags(bc) & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) != 0;
447
- int len;
448
- uint16_t *u = scr_to_utf16(s, &len);
449
- uint8_t **capture = scr_capture_alloc(bc);
519
+ ScrRegexInput input;
520
+ scr_regex_input_init(&input, bc, s);
521
+ int len = input.len;
522
+ uint8_t **capture = input.capture;
450
523
  int pos = stateful ? scr_regex_start(re, len) : 0;
451
- int rc = pos < 0 ? 0 : scr_exec(capture, bc, u, pos, len);
524
+ int rc = pos < 0 ? 0 : scr_exec(&input, bc, pos);
452
525
  if (stateful) re->last_index = rc == 1
453
- ? (double)((capture[1] - (const uint8_t *)u) >> 1) : 0;
526
+ ? (double)scr_capture_index(&input, capture[1]) : 0;
454
527
  ScrArr *out = NULL;
455
528
  if (rc == 1) {
456
- const uint8_t *ubase = (const uint8_t *)u;
457
529
  int count = lre_get_capture_count(bc);
458
530
  out = scr_arr_new(SCR_ELEM_STR, (size_t)count);
459
531
  out->metadata = scr_dyn_new_obj();
460
- scr_dyn_obj_set(out->metadata, "index", 5, scr_dyn_new_num((double)((capture[0] - ubase) >> 1)));
532
+ scr_dyn_obj_set(out->metadata, "index", 5, scr_dyn_new_num((double)scr_capture_index(&input, capture[0])));
461
533
  scr_dyn_obj_set(out->metadata, "input", 5, scr_dyn_new_str(s));
462
534
  for (int k = 0; k < count; k++) {
463
535
  const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1];
@@ -465,12 +537,11 @@ ScrArr *scr_regex_exec(ScrStr *s, ScrRegex *re) {
465
537
  scr_arr_set_undefined(out, (double)out->len);
466
538
  } else {
467
539
  scr_arr_push_ref(
468
- out, scr_str_from_utf16(u, (int)((cs - ubase) >> 1), (int)((ce - ubase) >> 1)));
540
+ out, scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce)));
469
541
  }
470
542
  }
471
543
  }
472
- free(capture);
473
- free(u);
544
+ scr_regex_input_dispose(&input);
474
545
  return out; /* +1, or NULL (no match) */
475
546
  }
476
547
 
@@ -485,22 +556,21 @@ ScrArr *scr_regex_match(ScrStr *s, ScrRegex *re) {
485
556
  if (!(flags & LRE_FLAG_GLOBAL)) return scr_regex_exec(s, re);
486
557
  re->last_index = 0;
487
558
  bool unicode = (flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0;
488
- int len;
489
- uint16_t *u = scr_to_utf16(s, &len);
490
- uint8_t **capture = scr_capture_alloc(bc);
491
- const uint8_t *ubase = (const uint8_t *)u;
559
+ ScrRegexInput input;
560
+ scr_regex_input_init(&input, bc, s);
561
+ int len = input.len;
562
+ uint8_t **capture = input.capture;
492
563
  ScrArr *out = NULL;
493
564
  int pos = scr_regex_start(re, len);
494
565
  if (pos < 0) pos = len + 1;
495
- while (pos <= len && scr_exec(capture, bc, u, pos, len) == 1) {
496
- int start = (int)((capture[0] - ubase) >> 1);
497
- int end = (int)((capture[1] - ubase) >> 1);
566
+ while (pos <= len && scr_exec(&input, bc, pos) == 1) {
567
+ int start = scr_capture_index(&input, capture[0]);
568
+ int end = scr_capture_index(&input, capture[1]);
498
569
  if (!out) out = scr_arr_new(SCR_ELEM_STR, 4);
499
- scr_arr_push_ref(out, scr_str_from_utf16(u, start, end));
500
- pos = start == end ? scr_advance(u, len, end, unicode) : end;
570
+ scr_arr_push_ref(out, scr_regex_span(&input, start, end));
571
+ pos = start == end ? scr_advance(&input, end, unicode) : end;
501
572
  }
502
- free(capture);
503
- free(u);
573
+ scr_regex_input_dispose(&input);
504
574
  return out;
505
575
  }
506
576
 
@@ -513,14 +583,13 @@ ScrArr *scr_regex_match(ScrStr *s, ScrRegex *re) {
513
583
  * bytecode omits the implicit forward scan) — exactly Node. */
514
584
  double scr_regex_search(ScrStr *s, ScrRegex *re) {
515
585
  uint8_t *bc = scr_regex_bc(re);
516
- int len;
517
- uint16_t *u = scr_to_utf16(s, &len);
518
- uint8_t **capture = scr_capture_alloc(bc);
519
- int rc = scr_exec(capture, bc, u, 0, len);
586
+ ScrRegexInput input;
587
+ scr_regex_input_init(&input, bc, s);
588
+ uint8_t **capture = input.capture;
589
+ int rc = scr_exec(&input, bc, 0);
520
590
  double out = -1;
521
- if (rc == 1) out = (double)((capture[0] - (const uint8_t *)u) >> 1);
522
- free(capture);
523
- free(u);
591
+ if (rc == 1) out = (double)scr_capture_index(&input, capture[0]);
592
+ scr_regex_input_dispose(&input);
524
593
  return out;
525
594
  }
526
595
 
@@ -544,17 +613,17 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices
544
613
  }
545
614
  bool unicode = (re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0;
546
615
  int capture_count = lre_get_capture_count(bc);
547
- int len;
548
- uint16_t *u = scr_to_utf16(s, &len);
549
- uint8_t **capture = scr_capture_alloc(bc);
550
- const uint8_t *ubase = (const uint8_t *)u;
616
+ ScrRegexInput input;
617
+ scr_regex_input_init(&input, bc, s);
618
+ int len = input.len;
619
+ uint8_t **capture = input.capture;
551
620
  ScrArr *out = scr_arr_new(SCR_ELEM_ARR, 4);
552
621
  int pos = scr_regex_start(re, len);
553
622
  if (pos < 0) pos = len + 1;
554
623
  while (pos <= len) {
555
- if (scr_exec(capture, bc, u, pos, len) != 1) break;
556
- int start = (int)((capture[0] - ubase) >> 1);
557
- int end = (int)((capture[1] - ubase) >> 1);
624
+ if (scr_exec(&input, bc, pos) != 1) break;
625
+ int start = scr_capture_index(&input, capture[0]);
626
+ int end = scr_capture_index(&input, capture[1]);
558
627
  ScrArr *row = scr_arr_new(SCR_ELEM_STR, (size_t)capture_count);
559
628
  for (int k = 0; k < capture_count; k++) {
560
629
  const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1];
@@ -562,15 +631,14 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices
562
631
  scr_arr_set_undefined(row, (double)row->len);
563
632
  } else {
564
633
  scr_arr_push_ref(
565
- row, scr_str_from_utf16(u, (int)((cs - ubase) >> 1), (int)((ce - ubase) >> 1)));
634
+ row, scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce)));
566
635
  }
567
636
  }
568
637
  scr_arr_push_ref(out, row);
569
638
  if (indices != NULL) scr_arr_push_f64(indices, (double)start);
570
- pos = end == start ? scr_advance(u, len, end, unicode) : end;
639
+ pos = end == start ? scr_advance(&input, end, unicode) : end;
571
640
  }
572
- free(capture);
573
- free(u);
641
+ scr_regex_input_dispose(&input);
574
642
  return out; /* +1 (possibly empty — Node's no-match drain is []) */
575
643
  }
576
644
 
@@ -598,17 +666,18 @@ ScrArr *scr_regex_match_all_into(ScrStr *s, ScrRegex *re, ScrArr *indices) {
598
666
  * — also Node's unterminated-'$<name' answer either way. The template is
599
667
  * scanned byte-wise: every '$' directive is ASCII, and UTF-8 continuation
600
668
  * bytes can never alias it. */
601
- static void scr_put_substitution(ScrJsonBuf *b, const uint16_t *u, int len,
669
+ static void scr_put_substitution(ScrJsonBuf *b, const ScrRegexInput *input,
602
670
  int start, int end, uint8_t **capture,
603
671
  int capture_count, const char *groupnames,
604
672
  const ScrStr *rep) {
605
- const uint8_t *ubase = (const uint8_t *)u;
606
673
  size_t i = 0;
607
674
  while (i < rep->len) {
608
675
  char c = rep->data[i];
609
676
  if (c != '$' || i + 1 >= rep->len) {
610
- scr_jb_putc(b, c);
611
- i++;
677
+ const char *next = memchr(rep->data + i + 1, '$', rep->len - i - 1);
678
+ size_t end = next ? (size_t)(next - rep->data) : rep->len;
679
+ scr_jb_write(b, rep->data + i, end - i);
680
+ i = end;
612
681
  continue;
613
682
  }
614
683
  char c1 = rep->data[i + 1];
@@ -616,13 +685,13 @@ static void scr_put_substitution(ScrJsonBuf *b, const uint16_t *u, int len,
616
685
  scr_jb_putc(b, '$');
617
686
  i += 2;
618
687
  } else if (c1 == '&') {
619
- scr_jb_put_utf16(b, u, start, end);
688
+ scr_regex_put_span(b, input, start, end);
620
689
  i += 2;
621
690
  } else if (c1 == '`') {
622
- scr_jb_put_utf16(b, u, 0, start);
691
+ scr_regex_put_span(b, input, 0, start);
623
692
  i += 2;
624
693
  } else if (c1 == '\'') {
625
- scr_jb_put_utf16(b, u, end, len);
694
+ scr_regex_put_span(b, input, end, input->len);
626
695
  i += 2;
627
696
  } else if (c1 >= '0' && c1 <= '9') {
628
697
  int k = c1 - '0';
@@ -637,7 +706,7 @@ static void scr_put_substitution(ScrJsonBuf *b, const uint16_t *u, int len,
637
706
  if (k >= 1 && k < capture_count) {
638
707
  const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1];
639
708
  if (cs && ce) {
640
- scr_jb_put_utf16(b, u, (int)((cs - ubase) >> 1), (int)((ce - ubase) >> 1));
709
+ scr_regex_put_span(b, input, scr_capture_index(input, cs), scr_capture_index(input, ce));
641
710
  }
642
711
  i += adv;
643
712
  } else {
@@ -663,7 +732,7 @@ static void scr_put_substitution(ScrJsonBuf *b, const uint16_t *u, int len,
663
732
  if (glen == name_len && memcmp(p, name, name_len) == 0) {
664
733
  const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1];
665
734
  if (cs && ce) {
666
- scr_jb_put_utf16(b, u, (int)((cs - ubase) >> 1), (int)((ce - ubase) >> 1));
735
+ scr_regex_put_span(b, input, scr_capture_index(input, cs), scr_capture_index(input, ce));
667
736
  break; /* at most one duplicate participates */
668
737
  }
669
738
  }
@@ -688,31 +757,34 @@ static ScrStr *scr_replace_impl(ScrStr *s, ScrRegex *re, ScrStr *rep) {
688
757
  bool unicode = (re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0;
689
758
  int capture_count = lre_get_capture_count(bc);
690
759
  const char *groupnames = lre_get_groupnames(bc);
691
- int len;
692
- uint16_t *u = scr_to_utf16(s, &len);
693
- uint8_t **capture = scr_capture_alloc(bc);
694
- const uint8_t *ubase = (const uint8_t *)u;
760
+ ScrRegexInput input;
761
+ scr_regex_input_init(&input, bc, s);
762
+ int len = input.len;
763
+ uint8_t **capture = input.capture;
695
764
  ScrJsonBuf b;
696
765
  scr_jb_init(&b);
697
766
  if (global) re->last_index = 0;
698
767
  int next = 0, pos = sticky ? scr_regex_start(re, len) : 0;
699
768
  if (pos < 0) pos = len + 1;
700
769
  if (sticky && !global) re->last_index = 0;
770
+ bool matched = false;
771
+ bool literal = memchr(rep->data, '$', rep->len) == NULL;
701
772
  while (pos <= len) {
702
- if (scr_exec(capture, bc, u, pos, len) != 1) break;
703
- int start = (int)((capture[0] - ubase) >> 1);
704
- int end = (int)((capture[1] - ubase) >> 1);
773
+ if (scr_exec(&input, bc, pos) != 1) break;
774
+ matched = true;
775
+ int start = scr_capture_index(&input, capture[0]);
776
+ int end = scr_capture_index(&input, capture[1]);
705
777
  if (sticky && !global) re->last_index = end;
706
- scr_jb_put_utf16(&b, u, next, start);
707
- scr_put_substitution(&b, u, len, start, end, capture, capture_count, groupnames, rep);
778
+ scr_regex_put_span(&b, &input, next, start);
779
+ if (literal) scr_jb_put_str(&b, rep);
780
+ else scr_put_substitution(&b, &input, start, end, capture, capture_count, groupnames, rep);
708
781
  next = end;
709
782
  if (!global) break;
710
- pos = end == start ? scr_advance(u, len, end, unicode) : end;
783
+ pos = end == start ? scr_advance(&input, end, unicode) : end;
711
784
  }
712
- scr_jb_put_utf16(&b, u, next, len);
713
- free(capture);
714
- free(u);
715
- return scr_jb_finish(&b);
785
+ if (matched) scr_regex_put_span(&b, &input, next, len);
786
+ scr_regex_input_dispose(&input);
787
+ return matched ? scr_jb_finish(&b) : scr_str_retain(s);
716
788
  }
717
789
 
718
790
  ScrStr *scr_regex_replace(ScrStr *s, ScrRegex *re, ScrStr *rep) {
@@ -795,10 +867,10 @@ ScrArr *scr_regex_split_limit(ScrStr *s, ScrRegex *re, double limit_num) {
795
867
  int re_flags = lre_get_flags(bc);
796
868
  bool unicode = (re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0;
797
869
  bool sticky = (re_flags & LRE_FLAG_STICKY) != 0;
798
- int len;
799
- uint16_t *u = scr_to_utf16(s, &len);
800
- uint8_t **capture = scr_capture_alloc(bc);
801
- const uint8_t *ubase = (const uint8_t *)u;
870
+ ScrRegexInput input;
871
+ scr_regex_input_init(&input, bc, s);
872
+ int len = input.len;
873
+ uint8_t **capture = input.capture;
802
874
  ScrArr *out = scr_arr_new(SCR_ELEM_STR, 0);
803
875
 
804
876
  /* ECMA-262 22.2.6.14 (Symbol.split). The spec probes every
@@ -808,31 +880,30 @@ ScrArr *scr_regex_split_limit(ScrStr *s, ScrRegex *re, double limit_num) {
808
880
  * loop. A genuinely sticky pattern keeps the spec's probe-and-advance. */
809
881
  if (len == 0) {
810
882
  /* Empty subject: one probe — a match yields [], no match yields [""]. */
811
- if (scr_exec(capture, bc, u, 0, len) != 1) {
883
+ if (scr_exec(&input, bc, 0) != 1) {
812
884
  scr_arr_push_ref(out, scr_str_new("", 0));
813
885
  }
814
- free(capture);
815
- free(u);
886
+ scr_regex_input_dispose(&input);
816
887
  return out;
817
888
  }
818
889
  int p = 0, q = 0;
819
890
  while (q < len) {
820
- if (scr_exec(capture, bc, u, q, len) != 1) {
891
+ if (scr_exec(&input, bc, q) != 1) {
821
892
  if (!sticky) break; /* no match anywhere right of q */
822
- q = scr_advance(u, len, q, unicode);
893
+ q = scr_advance(&input, q, unicode);
823
894
  continue;
824
895
  }
825
- int start = (int)((capture[0] - ubase) >> 1);
826
- int end = (int)((capture[1] - ubase) >> 1);
896
+ int start = scr_capture_index(&input, capture[0]);
897
+ int end = scr_capture_index(&input, capture[1]);
827
898
  /* The sticky algorithm never probes the position after the subject.
828
899
  * A forward search can find a zero-width match there; exclude it. */
829
900
  if (start == len) break;
830
901
  if (end == p) {
831
902
  /* Zero-length match adjacent to the previous split point: advance
832
903
  * (start == q == p here — the search cannot skip a match). */
833
- q = scr_advance(u, len, start, unicode);
904
+ q = scr_advance(&input, start, unicode);
834
905
  } else {
835
- scr_arr_push_ref(out, scr_str_from_utf16(u, p, start));
906
+ scr_arr_push_ref(out, scr_regex_span(&input, p, start));
836
907
  if (out->len == limit) goto done;
837
908
  for (int group = 1; group < capture_count; group++) {
838
909
  const uint8_t *from = capture[2 * group];
@@ -842,8 +913,8 @@ ScrArr *scr_regex_split_limit(ScrStr *s, ScrRegex *re, double limit_num) {
842
913
  // distinct from both empty strings and holes.
843
914
  scr_arr_set_undefined(out, (double)out->len);
844
915
  } else {
845
- scr_arr_push_ref(out, scr_str_from_utf16(u,
846
- (int)((from - ubase) >> 1), (int)((to - ubase) >> 1)));
916
+ scr_arr_push_ref(out, scr_regex_span(&input,
917
+ scr_capture_index(&input, from), scr_capture_index(&input, to)));
847
918
  }
848
919
  if (out->len == limit) goto done;
849
920
  }
@@ -851,10 +922,9 @@ ScrArr *scr_regex_split_limit(ScrStr *s, ScrRegex *re, double limit_num) {
851
922
  q = p;
852
923
  }
853
924
  }
854
- scr_arr_push_ref(out, scr_str_from_utf16(u, p, len));
925
+ scr_arr_push_ref(out, scr_regex_span(&input, p, len));
855
926
  done:
856
- free(capture);
857
- free(u);
927
+ scr_regex_input_dispose(&input);
858
928
  return out;
859
929
  }
860
930
 
@@ -955,10 +1025,38 @@ static void scr_case_put_cp(ScrJsonBuf *b, uint32_t cp) {
955
1025
  }
956
1026
 
957
1027
  static ScrStr *scr_str_case_conv(const ScrStr *s, int to_lower) {
1028
+ unsigned char first = to_lower ? 'A' : 'a';
1029
+ unsigned char last = to_lower ? 'Z' : 'z';
1030
+ if (scr_text_ascii(s)) {
1031
+ size_t changed = 0;
1032
+ while (changed < s->len &&
1033
+ ((unsigned char)s->data[changed] < first || (unsigned char)s->data[changed] > last))
1034
+ changed++;
1035
+ if (changed == s->len) return scr_str_retain((ScrStr *)s);
1036
+ ScrStr *out = scr_str_alloc_raw(s->len, s->len);
1037
+ memcpy(out->data, s->data, changed);
1038
+ for (size_t i = changed; i < s->len; i++) {
1039
+ unsigned char c = (unsigned char)s->data[i];
1040
+ out->data[i] = (char)(c >= first && c <= last ? c ^ 0x20 : c);
1041
+ }
1042
+ out->data[s->len] = '\0';
1043
+ return out;
1044
+ }
958
1045
  ScrJsonBuf b;
959
1046
  scr_jb_init(&b);
960
1047
  uint32_t res[LRE_CC_RES_LEN_MAX];
961
1048
  for (size_t i = 0; i < s->len;) {
1049
+ if ((unsigned char)s->data[i] < 0x80) {
1050
+ size_t start = i;
1051
+ while (i < s->len && (unsigned char)s->data[i] < 0x80) i++;
1052
+ size_t output = b.len;
1053
+ scr_jb_write(&b, s->data + start, i - start);
1054
+ for (size_t j = output; j < b.len; j++) {
1055
+ unsigned char c = (unsigned char)b.data[j];
1056
+ if (c >= first && c <= last) b.data[j] = (char)(c ^ 0x20);
1057
+ }
1058
+ continue;
1059
+ }
962
1060
  size_t adv;
963
1061
  uint32_t c = scr_case_decode(s->data + i, &adv);
964
1062
  int l;
@@ -994,6 +1092,9 @@ ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form) {
994
1092
  scr_throw_error(SCR_ERR_RANGE, scr_str_new(message, sizeof message - 1));
995
1093
  return NULL;
996
1094
  }
1095
+ /* Every ASCII string is already normalized in all four forms. Validate
1096
+ * the form first: even an empty subject must reject an invalid form. */
1097
+ if (scr_text_ascii(s)) return scr_str_retain((ScrStr *)s);
997
1098
  if (s->len > INT32_MAX / sizeof(uint32_t)) scr_regex_oom();
998
1099
  uint32_t *input = malloc((s->len + 1) * sizeof(uint32_t));
999
1100
  if (!input) scr_regex_oom();
@@ -1170,7 +1271,16 @@ ScrRegex *scr_regex_new(ScrStr *pattern, ScrStr *flags) {
1170
1271
  }
1171
1272
  re->rc = 1;
1172
1273
  re->source = pattern->len > 0 ? scr_str_retain(pattern) : scr_str_new("(?:)", 4);
1173
- re->flags = scr_str_retain(flags);
1274
+ /* Public flags use getter order, independently of constructor spelling.
1275
+ * Validate first so duplicates and invalid flags retain their errors. */
1276
+ char canonical[6];
1277
+ size_t flag_count = 0;
1278
+ const char *order = "gimsuy";
1279
+ for (unsigned i = 0; i < 6; i++) {
1280
+ if (seen_flags & (1u << i)) canonical[flag_count++] = order[i];
1281
+ }
1282
+ re->flags = flag_count == flags->len && memcmp(canonical, flags->data, flag_count) == 0
1283
+ ? scr_str_retain(flags) : scr_str_new(canonical, flag_count);
1174
1284
  /* Eager compile — the literal path stays lazy (its failure is an
1175
1285
  * abort; tsc already parsed those patterns). */
1176
1286
  int lre_flags = scr_lre_flags(re->flags);