Speed up native string character tests, optional-string switches, fromCharCode and long-string length

`s.charAt(i) === "c"`, and `s[i] === "c"` where the index is proven in
bounds, built a one-character string to compare with the literal. When
the literal is one UTF-16 code unit, the comparison now reads
`s.charCodeAt(i)` and compares the number. charCodeAt converts the index
the same way charAt does, and an index out of range answers NaN, which
equals no unit, as "" equals no one-unit literal. A switch on
`s.charAt(i)` whose case tests are all one-unit literals switches on the
code unit the same way.

A switch on a `string | undefined` value with string literal cases, such
as an array element or an optional field, lowered to a chain of union
equalities. It now tests for undefined, which matches no case, and
dispatches the string through the string switch: by byte length, then by
the literal's leading bytes.

`String.fromCharCode(a, b, ...)` with up to sixteen arguments passes the
codes to the runtime in a stack array instead of a heap f64[], and up to
64 code units encode into a stack buffer instead of a heap scratch block.

`.length` of a string that is not ASCII counts its UTF-16 units. The
string index keeps four entries, so a program that read the lengths of
more long texts than that in turn (the text of every source file) walked
each whole string on every read. Lengths of non-ASCII strings longer than
64 bytes now go into a 1024-slot table keyed by address, allocated by
the first length it records. A slot is dropped when the string is freed
or reallocated and when an append changes it in place, which is where
the index drops its facts.
This commit is contained in:
Chris Tate
2026-10-20 01:11:36 -05:00
parent e134489edc
commit ba8849a269
16 changed files with 511 additions and 6 deletions
@@ -1,6 +1,6 @@
/* LlEmitter's statement emission: the statement switch, try/catch, switch, loop copies, speculative writes and string self-concatenation. */
import { type LlEmitter, type LlScopeEntry, adoptingAwaitOf } from "./emitter.js";
import { CAUGHT, type IrExpr, type IrStmt, STRING, isRefCounted } from "../../ir/ir.js";
import { CAUGHT, F64, type IrExpr, type IrStmt, STRING, isRefCounted } from "../../ir/ir.js";
import { emitStringSliceSnapshot } from "./string-slices.js";
import {
canDeferSplitPiece,
@@ -1657,6 +1657,8 @@ export function emitTryCatch(emitter: LlEmitter, s: IrStmt & { kind: "tryCatch"
* case, refcounted/boxed case-body locals are NULL-reset up front and
* the scope-exit releases rely on NULL tolerance. */
export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }): void {
const unitSwitch = charAtUnitSwitch(s);
if (unitSwitch !== null) return emitSwitch(emitter, unitSwitch);
const B = emitter.B;
const discKind = s.disc.type.kind;
if (discKind !== "f64" && discKind !== "string" && discKind !== "bool") {
@@ -1680,7 +1682,31 @@ export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }):
const caseLabels = s.cases.map(() => B.newLabel("sw.c"));
const defaultIdx = s.cases.findIndex((c) => c.test === null);
const fallback = defaultIdx >= 0 ? caseLabels[defaultIdx]! : end;
if (!emitLiteralSwitch(emitter, disc, s.disc, s.cases, caseLabels, fallback)) {
const nullableString = nullableStringSwitch(emitter, s);
if (nullableString !== null) {
// `undefined` matches no string literal; any other value is the
// string itself and dispatches as a string switch.
const value = emitter.emitExpr(nullableString.value);
const present = B.tmp();
const dispatch = B.newLabel("sw.string");
B.line(`${present} = icmp ne ptr ${value.name}, null`);
B.condBr(present, dispatch, fallback);
B.startBlock(dispatch);
const text: LlValue = { name: value.name, type: STRING };
if (!emitLiteralSwitch(emitter, text, undefined, nullableString.cases, caseLabels, fallback)) {
nullableString.cases.forEach((c, i) => {
if (c.test?.kind !== "strLit") return;
const hit = B.tmp();
B.line(
`${hit} = call zeroext i1 ${emitter.literalEquals(c.test.value)}(ptr ${value.name})`,
);
const next = B.newLabel("sw.t");
B.condBr(hit, caseLabels[i]!, next);
B.startBlock(next);
});
B.br(fallback);
}
} else if (!emitLiteralSwitch(emitter, disc, s.disc, s.cases, caseLabels, fallback)) {
s.cases.forEach((c, i) => {
if (c.test === null) return;
// Lazy source-order test evaluation (a test after the match never
@@ -1983,3 +2009,74 @@ export function emitStringSelfConcatAssign(
}
return result;
}
/** `switch (x)` on a `string | undefined` value whose case tests are all
* string literals: the frontend's `switch (true)` over `x === "lit"` union
* equalities against one switch local. The local and the cases with plain
* string literal tests. */
function nullableStringSwitch(
emitter: LlEmitter,
s: IrStmt & { kind: "switch" },
): { value: IrExpr; cases: (IrStmt & { kind: "switch" })["cases"] } | null {
if (s.disc.kind !== "boolLit" || !s.disc.value) return null;
let value: (IrExpr & { kind: "varRef" }) | null = null;
const cases: (IrStmt & { kind: "switch" })["cases"] = [];
for (const c of s.cases) {
const test = c.test;
if (test === null) {
cases.push(c);
continue;
}
if (
test.kind !== "unionEq" ||
test.negated ||
test.sameValue ||
test.left.kind !== "varRef" ||
test.right.kind !== "unionWrap" ||
test.right.value.kind !== "strLit"
)
return null;
const nullable = emitter.nullableUnions.get(test.unionId);
if (
nullable === null ||
nullable.repr !== "single" ||
nullable.nullTag !== -1 ||
nullable.arm.kind !== "string" ||
test.right.tag !== nullable.refTag
)
return null;
if (value !== null && value.localId !== test.left.localId) return null;
value = test.left;
cases.push({ ...c, test: test.right.value });
}
return value === null ? null : { value, cases };
}
/** `switch (s.charAt(i))` (or `switch (s[i])`) whose case tests are all
* one-unit string literals, as a switch on `s.charCodeAt(i)` with the units
* as cases: no character string is built. An index out of range answers
* NaN, which matches no case, as "" matches no one-unit literal. */
function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "switch" }) | null {
const disc = s.disc;
if (disc.kind !== "strIntrinsic" || disc.method !== "charAt" || disc.args.length !== 1)
return null;
if (!s.cases.some((c) => c.test !== null)) return null;
if (
!s.cases.every(
(c) => c.test === null || (c.test.kind === "strLit" && c.test.value.length === 1),
)
)
return null;
return {
...s,
disc: { ...disc, method: "charCodeAt", type: F64 },
cases: s.cases.map((c) =>
c.test?.kind === "strLit"
? {
...c,
test: { kind: "numLit", value: c.test.value.charCodeAt(0), type: F64, loc: c.test.loc },
}
: c,
),
};
}
@@ -1,7 +1,7 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
import { F64, isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
import { arrNewCall, elemAccess } from "./shapes.js";
import { LlvmUnsupportedError } from "./unsupported.js";
@@ -558,6 +558,28 @@ export function regexLiteralTemplate(
return `@${re.sym}`;
}
/** A string equality between `s.charAt(i)` and a literal of exactly one
* UTF-16 code unit: the charAt call and the unit. */
function charAtUnitTest(
e: ExprOf<"strEq">,
): { charAt: ExprOf<"strIntrinsic">; code: number } | null {
let text: string;
let other: IrExpr;
if (e.right.kind === "strLit") {
text = e.right.value;
other = e.left;
} else if (e.left.kind === "strLit") {
text = e.left.value;
other = e.right;
} else {
return null;
}
if (text.length !== 1) return null;
if (other.kind !== "strIntrinsic" || other.method !== "charAt" || other.args.length !== 1)
return null;
return { charAt: other, code: text.charCodeAt(0) };
}
export function emitStringExpr(
host: LlvmEmitterContext,
e: ExprOf<
@@ -600,6 +622,26 @@ export function emitStringExpr(
return host.own({ name: t, type: e.type });
}
case "strEq": {
// `s.charAt(i) === "c"` (and `s[i]`, which lowers to charAt) for a
// one-unit literal compares the code unit: charCodeAt converts the
// index the same way, and an index out of range answers NaN, which
// equals no unit, as "" equals no one-unit literal.
const unit = charAtUnitTest(e);
if (unit !== null) {
const code = host.emitExpr({
kind: "strIntrinsic",
method: "charCodeAt",
receiver: unit.charAt.receiver,
args: unit.charAt.args,
type: F64,
loc: unit.charAt.loc,
});
const t = B.tmp();
B.line(
`${t} = fcmp ${e.negated ? "une" : "oeq"} double ${code.name}, ${f64Lit(unit.code)}`,
);
return { name: t, type: e.type };
}
const [l, r] = emitStringInputs(host, [e.left, e.right]);
const eq = B.tmp();
// A literal side compares its length and bytes inline.
@@ -1097,6 +1097,30 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr):
B.line(`${t} = call ptr @scr_str_from_char_code_one(double ${code.name})`);
return host.own({ name: t, type: e.type });
}
if (
e.fn === "string.fromCharCode" &&
packed.kind === "arrayLit" &&
packed.elems.length <= 16 &&
!packed.spreads?.length
) {
// A short argument list passes its codes in a stack array.
const codes = packed.elems.map((elem) => host.emitExpr(elem));
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca [${Math.max(codes.length, 1)} x double]`);
codes.forEach((code, i) => {
const p = B.tmp();
B.line(
`${p} = getelementptr inbounds [${Math.max(codes.length, 1)} x double], ptr ${slot}, i64 0, i64 ${i}`,
);
B.line(`store double ${code.name}, ptr ${p}`);
});
host.declare(`declare ptr @scr_str_from_char_codes(ptr, ${host.sizeType})`);
const t = B.tmp();
B.line(
`${t} = call ptr @scr_str_from_char_codes(ptr ${slot}, ${host.sizeType} ${codes.length})`,
);
return host.own({ name: t, type: e.type });
}
// One packed f64[] (the frontend built it) or one bytes value (the
// spread-typed-array form); +1 string.
const base =
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5607-string-char-compare.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5608-string-char-switch.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5609-string-switch-optional.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5750-string-from-char-codes.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5751-string-length-cache.ts"
],
"diags": []
}
+13 -2
View File
@@ -10400,7 +10400,9 @@ ScrStr *scr_crypto_x509_valid_to_str(ScrStr *pem) {
* keep other surrogates lone, WTF-8 encode. */
static ScrStr *scr_str_from_units(size_t n, uint32_t (*unit)(void *, size_t), void *src) {
if (n == 1) return scr_str_from_char_code_one((double)unit(src, 0));
char *out = malloc(n * 3 + 1); /* worst case: 3 bytes per UTF-16 unit */
/* Up to 64 units encode on the stack (3 bytes per UTF-16 unit at most). */
char small[64 * 3 + 1];
char *out = n <= 64 ? small : malloc(n * 3 + 1);
if (!out) {
scr_trap("scriptc: out of memory\n");
}
@@ -10417,10 +10419,19 @@ static ScrStr *scr_str_from_units(size_t n, uint32_t (*unit)(void *, size_t), vo
o += scr_wtf8_put(out + o, cp);
}
ScrStr *s = scr_str_new(out, o);
free(out);
if (out != small) free(out);
return s;
}
static uint32_t scr_fcc_units_unit(void *src, size_t i) {
return scr_to_uint32(((const double *)src)[i]) & 0xFFFFu;
}
/* String.fromCharCode(c0, ..., cn-1) with the codes in a caller's array. */
ScrStr *scr_str_from_char_codes(const double *codes, size_t n) {
return scr_str_from_units(n, scr_fcc_units_unit, (void *)codes);
}
static uint32_t scr_fcc_arr_unit(void *src, size_t i) {
return scr_to_uint32(scr_arr_get_f64((ScrArr *)src, (double)i)) & 0xFFFFu;
}
+2
View File
@@ -52,6 +52,8 @@ ScrStr *scr_bool_to_scrstr(bool b); /* interned "true"/"false" */
* adjacent surrogate pairs combine, other surrogates stay lone. Borrowed
* args; the string result is +1; neither throws. */
ScrStr *scr_str_from_char_code(ScrArr *codes);
/* String.fromCharCode over n codes in a caller-owned array (no ScrArr). */
ScrStr *scr_str_from_char_codes(const double *codes, size_t n);
/* Single numeric argument; avoids argument packing. Defined in scr_string.c
* to share its character cache. Same ToUint16 and lone-surrogate behavior. */
ScrStr *scr_str_from_char_code_one(double code);
+48 -1
View File
@@ -115,6 +115,15 @@ static SCR_TL bool scr_sidx_cleanup_registered;
* moves one active entry; only clearing or initializing changes the count. */
static SCR_TL size_t scr_sidx_active;
/* UTF-16 length table (scr_str_utf16_len below). */
#define SCR_U16LEN_N 1024
typedef struct {
const ScrStr *s;
size_t units;
} ScrU16Len;
/* Allocated by the first length it records; freed with the index. */
static SCR_TL ScrU16Len *scr_u16len_tab;
static void scr_sidx_clear(ScrSidx *e, unsigned *live) {
if (e->s) {
scr_sidx_active--;
@@ -137,6 +146,8 @@ static void scr_sidx_reset_all(void) {
}
scr_sidx_sparse_clock = 0;
scr_sidx_cursor_clock = 0;
free(scr_u16len_tab);
scr_u16len_tab = NULL;
}
static void scr_sidx_register_cleanup(void) {
@@ -190,7 +201,27 @@ static __attribute__((noinline)) void scr_sidx_purge_active(const ScrStr *s) {
}
}
/* UTF-16 lengths of non-ASCII strings longer than 64 bytes, direct-mapped
* by address. The four index entries above evict each other when a
* program reads the lengths of more long texts than that in turn (every
* source file of a program), and each miss walks the whole string. A slot
* is dropped wherever the index drops its facts: when the string is freed
* or reallocated, and when an append changes it in place. */
/* The slot for s, or NULL before any length is recorded. */
static inline ScrU16Len *scr_u16len_slot(const ScrStr *s) {
if (!scr_u16len_tab) return NULL;
uint64_t h = ((uint64_t)(uintptr_t)s >> 4) * UINT64_C(0x9E3779B97F4A7C15);
return &scr_u16len_tab[h >> 54];
}
static inline void scr_u16len_forget(const ScrStr *s) {
ScrU16Len *slot = scr_u16len_slot(s);
if (slot && slot->s == s) slot->s = NULL;
}
static void scr_sidx_purge(const ScrStr *s) {
scr_u16len_forget(s);
if (scr_sidx_active) scr_sidx_purge_active(s);
}
@@ -288,6 +319,7 @@ static ScrSidx *scr_sidx_for(const ScrStr *s) {
* receiver may move from the cursor tier to the sparse tier on its next
* lookup, so invalidate either possible entry. */
static void scr_sidx_concat_append(const ScrStr *s, size_t oldlen) {
scr_u16len_forget(s);
if (scr_sidx_active == 0) return;
for (int i = 0; i < SCR_SIDX_N; i++) {
ScrSidx *entries[] = {&scr_sidx_sparse_tab[i], &scr_sidx_cursor_tab[i]};
@@ -1400,6 +1432,8 @@ static void scr_sidx_materialize_identity_prefix(const ScrStr *s, ScrSidx *e) {
static size_t scr_sidx_len(const ScrStr *s, ScrSidx *e) {
if (s->ascii) return s->len;
if (e->u16len != SCR_U16_UNKNOWN) return e->u16len;
ScrU16Len *slot = scr_u16len_slot(s);
if (slot && slot->s == s) return e->u16len = slot->units;
while (e->indexed_cb < s->len) scr_sidx_extend_one(s, e);
scr_sidx_finish(s, e);
return s->ascii ? s->len : e->u16len;
@@ -1899,7 +1933,20 @@ double scr_str_utf16_len(ScrStr *s) {
ScrSidx *last = scr_sidx_last; /* a scan's receiver, length known */
if (last && last->s == s && last->u16len != SCR_U16_UNKNOWN)
return (double)(int64_t)last->u16len; /* lengths fit int64_t */
return (double)scr_sidx_len(s, scr_sidx(s));
ScrU16Len *slot = scr_u16len_slot(s);
if (slot && slot->s == s) return (double)(int64_t)slot->units;
size_t units = scr_sidx_len(s, scr_sidx(s));
/* The walk may prove the string ASCII, which needs no slot. */
if (s->ascii) return (double)(int64_t)units;
if (!scr_u16len_tab) {
/* Without memory the lengths are counted on each read, as before. */
scr_u16len_tab = calloc(SCR_U16LEN_N, sizeof *scr_u16len_tab);
if (!scr_u16len_tab) return (double)(int64_t)units;
scr_sidx_register_cleanup();
slot = scr_u16len_slot(s);
}
*slot = (ScrU16Len){s, units};
return (double)(int64_t)units;
}
double scr_str_char_code_at(ScrStr *s, double i) {
+37
View File
@@ -0,0 +1,37 @@
// Comparing one character of a string with a one-unit literal: index and
// charAt forms, === and !==, the literal on either side, indexes out of
// range, negative, fractional and NaN, multi-byte and astral text, lone
// surrogates, and literals of other lengths.
function bits(s: string, i: number): string {
return [
s[i] === "_", s.charAt(i) === "_", "_" === s.charAt(i), s.charAt(i) !== "_", s[i] !== "_",
s[i] === "a", s.charAt(i) === "a", "a" === s.charAt(i), s.charAt(i) !== "a", s[i] !== "a",
s[i] === "é", s.charAt(i) === "é", "é" === s.charAt(i), s.charAt(i) !== "é", s[i] !== "é",
s[i] === "\uD83D", s.charAt(i) === "\uD83D", "\uD83D" === s.charAt(i), s.charAt(i) !== "\uD83D", s[i] !== "\uD83D",
s[i] === "\uDE00", s.charAt(i) === "\uDE00", "\uDE00" === s.charAt(i), s.charAt(i) !== "\uDE00", s[i] !== "\uDE00",
s[i] === "\uD800", s.charAt(i) === "\uD800", "\uD800" === s.charAt(i), s.charAt(i) !== "\uD800", s[i] !== "\uD800",
s[i] === "語", s.charAt(i) === "語", "語" === s.charAt(i), s.charAt(i) !== "語", s[i] !== "語",
s[i] === "", s.charAt(i) === "", "" === s.charAt(i), s.charAt(i) !== "", s[i] !== "",
s[i] === "ab", s.charAt(i) === "ab", "ab" === s.charAt(i), s.charAt(i) !== "ab", s[i] !== "ab",
]
.map((b) => (b ? "1" : "0"))
.join("");
}
const samples: string[] = ["a_b", "", "é_ü", "x😀y", "\uD800z", "日本_語", "ab"];
const indexes = [0, 1, 2, 3, 4, -1, 1.5, NaN, 100];
for (const s of samples) console.log(JSON.stringify(s), indexes.map((i) => bits(s, i)).join(" "));
function countUnderscores(text: string): number {
let n = 0;
for (let i = 0; i < text.length; i++) if (text[i] === "_") n++;
return n;
}
function firstSpace(text: string): number {
for (let i = 0; i < text.length; i++) if (text.charAt(i) === " ") return i;
return -1;
}
console.log(countUnderscores("a_b_c__d"), countUnderscores(""), firstSpace("type Foo = Bar"), firstSpace("none"));
let k = 0;
const s = "ab";
const at = (): number => k++;
console.log(s.charAt(at()) === "a", s.charAt(at()) === "a", k);
+95
View File
@@ -0,0 +1,95 @@
// switch on one character of a string with one-unit case literals: the
// index and charAt forms, indexes out of range, negative, fractional and
// NaN, astral text and lone surrogates, duplicate cases, fallthrough, a
// switch without default, few and many cases, and an index expression
// with a side effect.
function classify(ch: string): string {
return ch;
}
function kind(s: string, i: number): string {
switch (s[i]) {
case "(":
case ")":
return "paren";
case "{":
return "brace";
case "é":
return "e-acute";
case "\uD83D":
return "high";
case "\uDE00":
return "low";
case "\uD800":
return "lone";
case "(":
return "duplicate";
case " ":
return "space";
default:
return "other";
}
}
function few(s: string, i: number): number {
let n = 0;
switch (s.charAt(i)) {
case "a":
n += 1;
case "b":
n += 2;
break;
case "c":
n += 4;
}
return n;
}
const samples = ["(x) {é}", "😀", "\uD800", "", "abc", " "];
const indexes = [0, 1, 2, 3, -1, 0.5, NaN, 100];
for (const s of samples) {
console.log(
JSON.stringify(s),
indexes.map((i) => kind(s, i)).join(","),
indexes.map((i) => few(s, i)).join(","),
);
}
let calls = 0;
const text = "abc";
const next = (): number => calls++;
for (let r = 0; r < 4; r++) {
switch (text.charAt(next())) {
case "a":
console.log("a", calls);
break;
case "c":
console.log("c", calls);
break;
default:
console.log("default", calls);
}
}
function tokens(text: string): string {
const out: string[] = [];
for (let i = 0; i < text.length; i++) {
switch (text[i]) {
case "(":
case ")":
out.push("p");
break;
case "+":
case "-":
case "*":
case "/":
out.push("o");
break;
case "\uD83D":
out.push("h");
break;
case " ":
break;
default:
out.push(text[i]);
}
}
return out.join("");
}
console.log(tokens("(a + b) * c / d - é"), tokens("x😀y"), tokens(""));
console.log(classify("ok"));
@@ -0,0 +1,57 @@
// switch on a string | undefined value with string literal cases: array
// reads, optional fields and Map lookups that may be undefined, matching
// and missing values, the empty string, non-ASCII and long literals,
// duplicate cases, fallthrough, default in the middle, and few cases.
interface Token {
text?: string;
}
function kind(t: string | undefined): string {
switch (t) {
case "function":
case "class":
return "decl";
case "":
return "empty";
case "naïve":
return "unicode";
case "a-much-longer-keyword-than-eight-bytes":
return "long";
case "function":
return "duplicate";
default:
return t === undefined ? "missing" : "other";
case "return":
return "flow";
}
}
function fallthrough(t: string | undefined): number {
let n = 0;
switch (t) {
case "a":
n += 1;
case "b":
n += 2;
break;
default:
n += 8;
case "c":
n += 4;
}
return n;
}
function few(t: string | undefined): string {
switch (t) {
case "x":
return "x";
}
return "none";
}
const words = ["function", "class", "", "naïve", "a-much-longer-keyword-than-eight-bytes", "return", "other", "Function"];
for (let i = 0; i <= words.length; i++) {
const w: string | undefined = words[i];
console.log(i, kind(w), fallthrough(w), few(w));
}
const tokens: Token[] = [{ text: "a" }, {}, { text: "c" }, { text: "x" }];
console.log(tokens.map((t) => `${kind(t.text)}:${fallthrough(t.text)}:${few(t.text)}`).join(" "));
const table = new Map<string, string>([["k", "return"], ["e", ""]]);
console.log(["k", "e", "z"].map((key) => kind(table.get(key))).join(" "));
@@ -0,0 +1,27 @@
// String.fromCharCode with several arguments: ASCII and non-ASCII units,
// surrogate pairs that combine, lone and reversed surrogates, ToUint16 of
// large, negative, fractional and NaN codes, zero to seventeen arguments,
// and the spread form.
const show = (s: string): string =>
`${JSON.stringify(s)}:${s.length}:${Array.from(s, (c) => c.codePointAt(0)!.toString(16)).join(".")}`;
console.log(show(String.fromCharCode()));
console.log(show(String.fromCharCode(104, 105)));
console.log(show(String.fromCharCode(0xe9, 0x65e5, 0x41)));
console.log(show(String.fromCharCode(0xd83d, 0xde00)));
console.log(show(String.fromCharCode(0xde00, 0xd83d)));
console.log(show(String.fromCharCode(0xd800, 0x41, 0xdc00)));
console.log(show(String.fromCharCode(0x10041, -1, 65.9, NaN, 2 ** 32 + 66)));
console.log(
show(String.fromCharCode(97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112)),
);
console.log(
show(String.fromCharCode(97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113)),
);
const codes = [0x48, 0xd83d, 0xde00, 0x21];
console.log(show(String.fromCharCode(...codes)));
let out = "";
for (let i = 0; i < 26; i++) out += String.fromCharCode(97 + i, 65 + i);
console.log(out, out.length);
let calls = 0;
const next = (): number => 65 + calls++;
console.log(String.fromCharCode(next(), next(), next()), calls);
+36
View File
@@ -0,0 +1,36 @@
// .length of long non-ASCII strings: many texts read in turn, appends in
// place (including a lone high surrogate completed by a low one), strings
// freed and rebuilt at reused addresses, split pieces, and slices and
// searches on texts whose lengths were read before.
let unit = "";
while (unit.length < 300) unit += "naïve 日本語 ";
const texts: string[] = [];
for (let i = 0; i < 40; i++) texts.push(unit.repeat(1 + (i % 7)) + "é".repeat(i));
let sum = 0;
for (let r = 0; r < 3; r++) for (const t of texts) sum += t.length;
console.log(sum, texts[39].length, texts[0].length);
let grow = unit;
const lengths: number[] = [];
for (let i = 0; i < 20; i++) {
grow += i % 2 ? "ü" : "x";
lengths.push(grow.length);
}
console.log(lengths.join(","));
let pair = unit + "\uD83D";
const before = pair.length;
pair += "\uDE00";
console.log(before, pair.length, pair.codePointAt(pair.length - 2)!.toString(16));
const seen: number[] = [];
for (let i = 0; i < 50; i++) {
const temp = unit + "ß".repeat(i % 5) + i;
seen.push(temp.length);
}
console.log(seen.slice(0, 10).join(","), seen.reduce((a, b) => a + b, 0));
const parts = (unit + "|").repeat(30).split("|");
console.log(parts.length, parts[0].length, parts.map((p) => p.length).reduce((a, b) => a + b, 0));
const big = texts[39];
console.log(big.slice(big.length - 5), big.substring(10, 20).length, big.indexOf("日本"), big.lastIndexOf("é"), big.length);