From 4b8ed48a8a05aa4df5a1a89188ef6698caea0c8c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 27 Oct 2026 01:19:32 -0500 Subject: [PATCH] Spread a string into its code points array `[...text]` lowered to an array literal that spread the array of text's code points: the literal's array copied the code point strings, retaining each one, and the code point array then released them. An array literal whose only element is a spread of a string is now the code points array itself, as `Array.from(text)` already was. Both are fresh arrays. --- .../lowering/expressions/array-literals.ts | 11 ++++++++ .../tests/corpus/string-spread.ts.json | 6 +++++ tests/corpus/string-spread.ts | 25 +++++++++++++++++++ .../corpus-costs/string-spread.ts.json | 1 + 4 files changed, 43 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-spread.ts.json create mode 100644 tests/corpus/string-spread.ts create mode 100644 tests/harness/corpus-costs/string-spread.ts.json diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 5854a0ae8..e3088f4a2 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -652,6 +652,17 @@ export function lowerArrayLiteral( return lowered; }); if (!needsStateBuild) { + // `[...text]`: the code points call already builds a fresh array of the + // literal's type, so it is the literal's value, as `Array.from(text)` is. + const only = elems[0]; + if ( + elems.length === 1 && + spreads.length === 1 && + only?.kind === "libCall" && + only.fn === "str.codePoints" && + typeEquals(only.type, type) + ) + return { ...only, loc }; return { kind: "arrayLit", elems, ...(spreads.length > 0 ? { spreads } : {}), type, loc }; } const out = lowerer.declareHiddenLocal("%arrayLit", type); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-spread.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-spread.ts.json new file mode 100644 index 000000000..2c5cc69e8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/string-spread.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/string-spread.ts" + ], + "diags": [] +} diff --git a/tests/corpus/string-spread.ts b/tests/corpus/string-spread.ts new file mode 100644 index 000000000..6813806f4 --- /dev/null +++ b/tests/corpus/string-spread.ts @@ -0,0 +1,25 @@ +// Spreading strings into arrays: code points of each UTF-8 length, astral +// pairs, lone surrogates, the empty string, and the fresh array each spread +// returns. + +const samples = ["", "a", "abc", "é", "café", "日本語", "😀", "a😀b", "🚀🚀", "\ud800", "a\udc00b", "xé日😀"]; + +for (const s of samples) { + const chars = [...s]; + console.log(JSON.stringify(s), chars.length, JSON.stringify(chars), chars.join("|")); +} + +// Each spread is a new array: changing one leaves the others and the string. +const text = "héllo 😀"; +const first = [...text]; +const second = [...text]; +first.reverse(); +first.push("!"); +console.log(first.join(""), second.join(""), first === second, text); + +// The spread result is an ordinary array. +const letters = [..."naïve"]; +letters.sort(); +console.log(letters, letters.length, Array.isArray(letters)); +console.log([..."abc"].map((c) => c.toUpperCase()).join("")); +console.log([..."日本"].concat([..."語"]).join("-")); diff --git a/tests/harness/corpus-costs/string-spread.ts.json b/tests/harness/corpus-costs/string-spread.ts.json new file mode 100644 index 000000000..d7fbd510a --- /dev/null +++ b/tests/harness/corpus-costs/string-spread.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 4300 }