From ca2d5bd9c726b85350b024e7687e5611b12f428b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 19:58:24 -0700 Subject: [PATCH] Support String.prototype.at on strings held in untyped values Hono's mergePath reads base?.at(-1) on an untyped parameter. The checked-dynamic method dispatch answered at() on strings with a not-supported error. It now returns the UTF-16 code unit at the index, counting from the end for a negative index, and undefined outside the string, as String.prototype.at does. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_dyn_invoke.c | 15 ++++++++++++++- tests/corpus/7403-dyn-string-at.mjs | 9 +++++++++ .../corpus-costs/7403-dyn-string-at.mjs.json | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/7403-dyn-string-at.mjs create mode 100644 tests/harness/corpus-costs/7403-dyn-string-at.mjs.json diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 14d11dc67..a32689811 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -972,7 +972,20 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(piece); return r; } - if (dyn_name_is(method, "at") || dyn_name_is(method, "concat") || + if (dyn_name_is(method, "at")) { + /* String.prototype.at: one UTF-16 code unit, counted from the end + * for a negative index; undefined outside the string. */ + double index = dyn_index_arg(args, argc, 0, 0, what); + if (scr_exc_pending()) return NULL; + double len = (double)scr_str_utf16_len(s); + double k = index < 0 ? len + index : index; + if (k < 0 || k >= len) return scr_dyn_retain(scr_dyn_undefined()); + ScrStr *unit = scr_str_slice(s, k, k + 1); + ScrDyn *r = scr_dyn_new_str(unit); /* retains */ + scr_str_release(unit); + return r; + } + if (dyn_name_is(method, "concat") || dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || dyn_name_is(method, "includes")) { if ((dyn_name_is(method, "indexOf") || dyn_name_is(method, "lastIndexOf") || diff --git a/tests/corpus/7403-dyn-string-at.mjs b/tests/corpus/7403-dyn-string-at.mjs new file mode 100644 index 000000000..68e865000 --- /dev/null +++ b/tests/corpus/7403-dyn-string-at.mjs @@ -0,0 +1,9 @@ +// String.prototype.at on strings held in untyped (checked-dynamic) values, +// as Hono's mergePath reads `base?.at(-1)`. +const mergePath = (base, sub, ...rest) => { + if (rest.length) sub = mergePath(sub, ...rest); + return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`; +}; +console.log(mergePath("/api", "/users"), mergePath("api/", "/"), mergePath("/a", "b", "/c")); +const values = JSON.parse(`["hello", "", "a\\ud83d\\ude00b"]`); +for (const v of values) console.log(JSON.stringify([v.at(0), v.at(-1), v.at(1), v.at(99), v.at(-99), v.at(1.7), v.at()])); diff --git a/tests/harness/corpus-costs/7403-dyn-string-at.mjs.json b/tests/harness/corpus-costs/7403-dyn-string-at.mjs.json new file mode 100644 index 000000000..cd29a3461 --- /dev/null +++ b/tests/harness/corpus-costs/7403-dyn-string-at.mjs.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 15000 }