From ca379c5d149683216d930fb9bbe308fcc0f9a690 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 19:38:31 -0500 Subject: [PATCH] Run exec on regular expressions held as any exec on a regular expression held as an untyped value threw "Native RegExp method has no lowering". It now returns Node's match array: the captures, with undefined for one that did not participate, then index, input and groups, or null without a match. Global and sticky expressions read and advance lastIndex and reset it when nothing matches, as the typed exec does. --- .../tests/corpus/6902-dyn-regexp-exec.js.json | 6 +++ packages/runtime/src/scr_regex.c | 18 +++++++ tests/corpus/6902-dyn-regexp-exec.js | 47 +++++++++++++++++++ .../corpus-costs/6902-dyn-regexp-exec.js.json | 1 + 4 files changed, 72 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6902-dyn-regexp-exec.js.json create mode 100644 tests/corpus/6902-dyn-regexp-exec.js create mode 100644 tests/harness/corpus-costs/6902-dyn-regexp-exec.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6902-dyn-regexp-exec.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6902-dyn-regexp-exec.js.json new file mode 100644 index 000000000..46ca41b60 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6902-dyn-regexp-exec.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6902-dyn-regexp-exec.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index e4867ddf3..73440c110 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -2784,6 +2784,24 @@ static ScrDyn *scr_native_regex_invoke(void *ptr, ScrDyn *self, const char *meth scr_str_release(subject); return scr_exc_pending() ? NULL : scr_dyn_new_bool(matches); } + if (strcmp(method, "exec") == 0) { + /* The match array: each capture (undefined when it did not + * participate), then index, input and groups; null without a match. + * The exec itself reads and advances lastIndex. */ + ScrStr *subject = scr_dyn_string_coerce_js(argc ? args[0] : scr_dyn_undefined()); + if (!subject) return NULL; + ScrArr *row = scr_regex_exec(subject, re); + scr_str_release(subject); + if (!row) return scr_exc_pending() ? NULL : scr_dyn_new_null(); + ScrDyn *match = scr_dyn_new_arr(); + for (size_t k = 0; k < row->len; k++) + scr_dyn_arr_push(match, *scr_arr_state_ptr(row, k) == SCR_ARR_VALUE + ? scr_dyn_new_str((ScrStr *)(uintptr_t)row->data[k]) + : scr_dyn_retain(scr_dyn_undefined())); + scr_arr_copy_metadata(row, match); + scr_arr_release(row); + return match; + } static const char message[] = "Native RegExp method has no lowering"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return NULL; diff --git a/tests/corpus/6902-dyn-regexp-exec.js b/tests/corpus/6902-dyn-regexp-exec.js new file mode 100644 index 000000000..a16a155c8 --- /dev/null +++ b/tests/corpus/6902-dyn-regexp-exec.js @@ -0,0 +1,47 @@ +// exec on a regular expression held as an untyped value returns Node's +// match array: the captures (undefined for one that did not participate), +// index, input and groups. Global and sticky expressions read and advance +// lastIndex, and reset it when nothing matches. +function dyn(value) { + return [value][0]; +} +function show(label, match) { + if (match === null) return console.log(label, null); + console.log(label, JSON.stringify([...match]), match.index, match.input, JSON.stringify(match.groups), match.length); +} + +const plain = dyn(/(\d+)-(x)?/); +show("plain", plain.exec("a 12- b")); +show("miss", plain.exec("none")); +console.log("plain lastIndex", plain.lastIndex); + +const global = dyn(/o(.)/g); +show("g1", global.exec("foo boo")); +console.log("after g1", global.lastIndex); +show("g2", global.exec("foo boo")); +console.log("after g2", global.lastIndex); +show("g3", global.exec("foo boo")); +console.log("after g3", global.lastIndex); +global.lastIndex = 5; +show("from 5", global.exec("foo boo")); + +const sticky = dyn(/b/y); +show("sticky miss", sticky.exec("ab")); +sticky.lastIndex = 1; +show("sticky hit", sticky.exec("ab")); +console.log("sticky after", sticky.lastIndex); + +const named = dyn(/(?\d{4})-(?\d{2})(?:-(?\d{2}))?/); +const date = named.exec("on 2024-05"); +show("named", date); +console.log("year", date.groups.year, "day", date.groups.day, Object.getPrototypeOf(date.groups) === null); + +const unicode = dyn(/\u{1F600}(.)/u); +show("unicode", unicode.exec("x\u{1F600}y")); +show("coerced", dyn(/1/).exec(123)); +const matches = []; +const all = dyn(/a(\d)/g); +let m; +while ((m = all.exec("a1 a2 a3")) !== null) matches.push(m[1] + "@" + m.index); +console.log("loop", matches.join(","), all.lastIndex); +console.log("Array.isArray", Array.isArray(dyn(/x/).exec("x"))); diff --git a/tests/harness/corpus-costs/6902-dyn-regexp-exec.js.json b/tests/harness/corpus-costs/6902-dyn-regexp-exec.js.json new file mode 100644 index 000000000..876c351cb --- /dev/null +++ b/tests/harness/corpus-costs/6902-dyn-regexp-exec.js.json @@ -0,0 +1 @@ +{ "plain": 2611, "san": 3084 }