From ba8849a269784ccfe9fbcaebf3da51bbad9d4a4a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 01:11:36 -0500 Subject: [PATCH] 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. --- .../src/backend/llvm/emit-statements.ts | 101 +++++++++++++++++- .../src/backend/llvm/expr-primitives.ts | 44 +++++++- .../src/backend/llvm/lib-filesystem.ts | 24 +++++ .../corpus/5607-string-char-compare.ts.json | 6 ++ .../corpus/5608-string-char-switch.ts.json | 6 ++ .../5609-string-switch-optional.ts.json | 6 ++ .../5750-string-from-char-codes.ts.json | 6 ++ .../corpus/5751-string-length-cache.ts.json | 6 ++ packages/runtime/src/scr_lib.c | 15 ++- packages/runtime/src/scr_number.h | 2 + packages/runtime/src/scr_string.c | 49 ++++++++- tests/corpus/5607-string-char-compare.ts | 37 +++++++ tests/corpus/5608-string-char-switch.ts | 95 ++++++++++++++++ tests/corpus/5609-string-switch-optional.ts | 57 ++++++++++ tests/corpus/5750-string-from-char-codes.ts | 27 +++++ tests/corpus/5751-string-length-cache.ts | 36 +++++++ 16 files changed, 511 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5607-string-char-compare.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5608-string-char-switch.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5609-string-switch-optional.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5750-string-from-char-codes.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5751-string-length-cache.ts.json create mode 100644 tests/corpus/5607-string-char-compare.ts create mode 100644 tests/corpus/5608-string-char-switch.ts create mode 100644 tests/corpus/5609-string-switch-optional.ts create mode 100644 tests/corpus/5750-string-from-char-codes.ts create mode 100644 tests/corpus/5751-string-length-cache.ts diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index e641f3ecd..01fa11035 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -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, + ), + }; +} diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 35e759110..9cbd8de74 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -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. diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 4798b1f09..358b21c4f 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -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 = diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5607-string-char-compare.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5607-string-char-compare.ts.json new file mode 100644 index 000000000..eef427a5e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5607-string-char-compare.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5607-string-char-compare.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5608-string-char-switch.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5608-string-char-switch.ts.json new file mode 100644 index 000000000..855057006 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5608-string-char-switch.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5608-string-char-switch.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5609-string-switch-optional.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5609-string-switch-optional.ts.json new file mode 100644 index 000000000..7f0bf061d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5609-string-switch-optional.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5609-string-switch-optional.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5750-string-from-char-codes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5750-string-from-char-codes.ts.json new file mode 100644 index 000000000..fab2fd504 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5750-string-from-char-codes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5750-string-from-char-codes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5751-string-length-cache.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5751-string-length-cache.ts.json new file mode 100644 index 000000000..0eebe2326 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5751-string-length-cache.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5751-string-length-cache.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index ce7de9654..54928f59f 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -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; } diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index f43195116..1b85fb0fe 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -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); diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 8358658b5..77c020ee0 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -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) { diff --git a/tests/corpus/5607-string-char-compare.ts b/tests/corpus/5607-string-char-compare.ts new file mode 100644 index 000000000..f80581c10 --- /dev/null +++ b/tests/corpus/5607-string-char-compare.ts @@ -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); diff --git a/tests/corpus/5608-string-char-switch.ts b/tests/corpus/5608-string-char-switch.ts new file mode 100644 index 000000000..8ea54f1f5 --- /dev/null +++ b/tests/corpus/5608-string-char-switch.ts @@ -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")); diff --git a/tests/corpus/5609-string-switch-optional.ts b/tests/corpus/5609-string-switch-optional.ts new file mode 100644 index 000000000..20e36b739 --- /dev/null +++ b/tests/corpus/5609-string-switch-optional.ts @@ -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([["k", "return"], ["e", ""]]); +console.log(["k", "e", "z"].map((key) => kind(table.get(key))).join(" ")); diff --git a/tests/corpus/5750-string-from-char-codes.ts b/tests/corpus/5750-string-from-char-codes.ts new file mode 100644 index 000000000..6229e6d81 --- /dev/null +++ b/tests/corpus/5750-string-from-char-codes.ts @@ -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); diff --git a/tests/corpus/5751-string-length-cache.ts b/tests/corpus/5751-string-length-cache.ts new file mode 100644 index 000000000..e0e5f8c67 --- /dev/null +++ b/tests/corpus/5751-string-length-cache.ts @@ -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);