diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 548a04fd5..62a155bd1 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -706,31 +706,42 @@ static inline size_t scr_utf16_units_short(const char *data, size_t len) { } /* A fresh concatenation (no head, the template-literal shape): numbers - * format straight into the result, which reserves scr_f64_to_str's whole - * 32-byte buffer for each. */ + * format into a stack buffer first, so the result is allocated at its exact + * length. */ #define SCR_F64_TEXT_MAX 32 static __attribute__((noinline)) ScrStr *scr_str_concat_fresh(ScrStr *const *parts, const double *nums, size_t count) { - size_t reserve = 0; + char texts[SCR_STR_MIXED_MAX * SCR_F64_TEXT_MAX]; + size_t text_len[SCR_STR_MIXED_MAX]; + size_t len = 0, used = 0; bool ascii = true; for (size_t i = 0; i < count; i++) { - size_t n = parts[i] ? parts[i]->len : SCR_F64_TEXT_MAX; - if (n > SCR_STR_MAX_CAP - reserve) scr_oom(); - reserve += n; - if (parts[i]) ascii &= parts[i]->ascii; + size_t n; + if (parts[i]) { + n = parts[i]->len; + ascii &= parts[i]->ascii; + } else { + n = scr_f64_to_str(nums[i], texts + used); + text_len[i] = n; + used += n; + } + if (n > SCR_STR_MAX_CAP - len) scr_oom(); + len += n; } - ScrStr *out = scr_str_take_spare(reserve); - if (!out) out = scr_str_alloc(reserve, reserve); + ScrStr *out = scr_str_take_spare(len); + if (!out) out = scr_str_alloc(len, len); char *w = out->data; + const char *text = texts; for (size_t i = 0; i < count; i++) { if (parts[i]) { scr_copy_bytes(w, parts[i]->data, parts[i]->len); w += parts[i]->len; } else { - w += scr_f64_to_str(nums[i], w); + scr_copy_bytes(w, text, text_len[i]); + w += text_len[i]; + text += text_len[i]; } } - size_t len = (size_t)(w - out->data); out->len = len; out->data[len] = '\0'; out->ascii = ascii; diff --git a/packages/runtime/test/string.test.ts b/packages/runtime/test/string.test.ts index 0ca58aaf4..cc6d4fbb6 100644 --- a/packages/runtime/test/string.test.ts +++ b/packages/runtime/test/string.test.ts @@ -88,6 +88,13 @@ test("string methods match Node on committed oracle cases", async () => { expect(r.stderr.trim()).toMatch(/^(\d+)\/\1 cases passed$/); }); +// A template literal of strings and numbers allocates exactly its length: +// numbers format before the result is sized, not into a reserved maximum. +test("template-literal concatenation allocates its exact length", async () => { + const { stderr } = await execFileAsync(bin, ["--concat-capacity"]); + expect(stderr.trim()).toBe("0 concat capacity failures"); +}); + // repeat(count) with count < 0 or Infinity is Node's catchable RangeError // ("Invalid count value: -1"); the runtime leaves it pending. test.each([ diff --git a/packages/runtime/test/test_string.c b/packages/runtime/test/test_string.c index f41c2de50..d0dbebf0d 100644 --- a/packages/runtime/test/test_string.c +++ b/packages/runtime/test/test_string.c @@ -982,7 +982,42 @@ static void ordering_asserts(void) { } } +/* Template-literal concatenations (no head) of strings and numbers: the + * result holds exactly its bytes, whatever the numbers format to. */ +static int check_concat_capacity(void) { + ScrStr *a = scr_str_new("f", 1), *b = scr_str_new("_d", 2), *c = scr_str_new("", 0); + struct { + ScrStr *parts[4]; + double nums[4]; + size_t count; + const char *expected; + } cases[] = { + {{a, NULL, b, NULL}, {0, 7, 0, 2230}, 4, "f7_d2230"}, + {{NULL, a}, {-1.5e-7, 0}, 2, "-1.5e-7f"}, + {{NULL, NULL, NULL}, {1.7976931348623157e308, 0.1, -0.0}, 3, "1.7976931348623157e+3080.10"}, + {{c, NULL, c}, {0, 42, 0}, 3, "42"}, + }; + int failed = 0; + for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) { + ScrStr *r = scr_str_concat_mixed(NULL, cases[i].parts, cases[i].nums, cases[i].count); + size_t want = strlen(cases[i].expected); + if (r->len != want || memcmp(r->data, cases[i].expected, want) != 0 || r->data[want] != '\0' || + r->cap != r->len) { + fprintf(stderr, "concat case %zu: got \"%.*s\" len %zu cap %zu, want \"%s\" len %zu\n", i, + (int)r->len, r->data, r->len, (size_t)r->cap, cases[i].expected, want); + failed++; + } + scr_str_release(r); + } + scr_str_release(a); + scr_str_release(b); + scr_str_release(c); + fprintf(stderr, "%d concat capacity failures\n", failed); + return failed != 0; +} + int main(int argc, char **argv) { + if (argc > 1 && strcmp(argv[1], "--concat-capacity") == 0) return check_concat_capacity(); if (argc > 1 && strncmp(argv[1], "--crash-repeat", 14) == 0) { ScrStr *s = scr_str_new("ab", 2); double count = strcmp(argv[1], "--crash-repeat-inf") == 0