diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 82d2229c2..9bf48ab5d 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -9,6 +9,7 @@ import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import { BYTES_ELEM_NUM, f64Lit } from "./common.js"; import { emitStringInputs } from "./string-lifetimes.js"; +import { emitStringParts, stringParts } from "./string-construction.js"; import { emitBorrowedInput } from "./borrowed-inputs.js"; import { exactInteger, widenInteger, integerNumber } from "./integer-values.js"; import { integerArithmeticRange } from "../../ir/integer-ranges.js"; @@ -282,6 +283,9 @@ export function emitStringExpr(host: LlvmEmitterContext, e: ExprOf<"strConcat" | const B = host.B; switch (e.kind) { case "strConcat": { + if (e.left.kind === "strConcat" || e.right.kind === "strConcat") { + return emitStringParts(host, stringParts(e)); + } // Concat may append to a uniquely owned left temporary. A retained // read protects an existing binding from that in-place operation. const l = host.emitExpr(e.left); diff --git a/packages/compiler/src/backend/llvm/string-construction.test.ts b/packages/compiler/src/backend/llvm/string-construction.test.ts new file mode 100644 index 000000000..b226b676c --- /dev/null +++ b/packages/compiler/src/backend/llvm/string-construction.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from "vitest"; +import { STRING, VOID, type IrExpr, type IrModule } from "../../ir/ir.js"; +import { validateModule } from "../../ir/validate.js"; +import { emitLlvmModule } from "./emitter.js"; +import { stringParts } from "./string-construction.js"; + +const loc = { file: "construction.ts", start: 0, end: 1 }; +const text = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING, loc }); +const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: STRING, loc }); +const concat = (left: IrExpr, right: IrExpr): IrExpr => ({ kind: "strConcat", left, right, type: STRING, loc }); + +function module(value: IrExpr): IrModule { + return { irVersion: 13, sourceFile: loc.file, entry: "main", functions: [ + { name: "main", loc, params: [], locals: [], returnType: VOID, body: [] }, + { name: "assemble", loc, params: [{ name: "value", localId: "value", type: STRING }], + locals: [{ id: "value", name: "value", type: STRING, mutable: true }], + returnType: STRING, body: [{ kind: "return", value, loc }] }, + ] }; +} + +test("concat grouping bounds each stack vector and preserves leaf order", () => { + const leaves = Array.from({ length: 100 }, (_, i) => text(String(i))); + for (const tree of [leaves.reduce(concat), leaves.reduceRight((right, left) => concat(left, right))]) { + const seen: IrExpr[] = []; + const walk = (value: IrExpr): void => { + if (value.kind !== "strConcat") { seen.push(value); return; } + const parts = stringParts(value); + expect(parts.length).toBeGreaterThanOrEqual(2); + expect(parts.length).toBeLessThanOrEqual(16); + for (const part of parts) walk(part); + }; + walk(tree); + expect(seen).toEqual(leaves); + } +}); + +test.each([32, 64] as const)("concat uses a borrowed stack vector with %i-bit size_t", (bits) => { + const mod = module(concat(concat(text("<"), ref("value")), text(">"))); + expect(validateModule(mod)).toEqual([]); + const ir = emitLlvmModule(mod, { pointerBits: bits }); + const body = /^define internal [^\n]*@sc_bf_assemble\([^]*?^}/m.exec(ir)![0]; + expect(body).toContain("alloca [3 x ptr]"); + expect(body).toMatch(new RegExp(`call ptr @scr_str_concat_parts\\(ptr %[^,]+, i${bits} 3\\)`)); + expect(body).not.toContain("@scr_str_retain_v"); + expect(body).not.toContain("@scr_str_release"); +}); + +test("later writes preserve an owned snapshot of earlier string parts", () => { + const changed: IrExpr = { kind: "assignExpr", localId: "value", value: text("new"), type: STRING, loc }; + const mod = module(concat(concat(ref("value"), changed), ref("value"))); + expect(validateModule(mod)).toEqual([]); + const body = /^define internal [^\n]*@sc_f_assemble\([^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const retain = body.indexOf("@scr_str_retain_v"); + const release = body.indexOf("@scr_str_release"); + const assemble = body.indexOf("@scr_str_concat_parts"); + expect(retain).toBeGreaterThanOrEqual(0); + expect(retain).toBeLessThan(release); + expect(release).toBeLessThan(assemble); + expect(body.slice(assemble)).toContain("@scr_str_release"); +}); + +test("two-part concatenation keeps the existing append path", () => { + const ir = emitLlvmModule(module(concat(ref("value"), text("suffix")))); + expect(ir).toContain("call ptr @scr_str_concat("); + expect(ir).not.toContain("@scr_str_concat_parts"); +}); diff --git a/packages/compiler/src/backend/llvm/string-construction.ts b/packages/compiler/src/backend/llvm/string-construction.ts new file mode 100644 index 000000000..1811b9c6c --- /dev/null +++ b/packages/compiler/src/backend/llvm/string-construction.ts @@ -0,0 +1,41 @@ +import type { IrExpr } from "../../ir/ir.js"; +import type { LlValue, LlvmEmitterContext } from "./expr-context.js"; +import { emitStringInputs } from "./string-lifetimes.js"; + +const MAX_PARTS = 16; + +/** Flatten only string concatenation, preserving conversion and operand + * order. Larger trees remain operands of bounded groups, so each call uses + * at most sixteen stack slots and does not need a heap argument array. */ +export function stringParts(value: IrExpr): IrExpr[] { + const pending = [value]; + const parts: IrExpr[] = []; + while (pending.length > 0) { + const part = pending.pop()!; + if (part.kind === "strConcat" && parts.length + pending.length < MAX_PARTS - 1) { + pending.push(part.right, part.left); + } else { + parts.push(part); + } + } + return parts; +} + +/** The runtime borrows every part without mutating it. Earlier operands + * therefore borrow only when their owners survive all later evaluations; + * other operands retain ordinary snapshots and exception cleanup. */ +export function emitStringParts(host: LlvmEmitterContext, parts: readonly IrExpr[]): LlValue { + const values = emitStringInputs(host, parts); + const B = host.B; + const storage = B.slot(); + B.entryAllocas.push(`${storage} = alloca [${parts.length} x ptr]`); + for (let i = 0; i < values.length; i++) { + const slot = B.tmp(); + B.line(`${slot} = getelementptr [${parts.length} x ptr], ptr ${storage}, i32 0, i32 ${i}`); + B.line(`store ptr ${values[i]!.name}, ptr ${slot}`); + } + host.declare(`declare ptr @scr_str_concat_parts(ptr, ${host.sizeType})`); + const result = B.tmp(); + B.line(`${result} = call ptr @scr_str_concat_parts(ptr ${storage}, ${host.sizeType} ${parts.length})`); + return host.own({ name: result, type: parts[0]!.type }); +} diff --git a/packages/compiler/src/frontend/lowering/lower-builtins.ts b/packages/compiler/src/frontend/lowering/lower-builtins.ts index be4bd6d03..b6a6581b2 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtins.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtins.ts @@ -9431,6 +9431,7 @@ function lowerStoredTextCodecCall(lowerer: Lowerer, call: ts.CallExpression, acc } const subs = call.arguments.slice(1).map((a): IrExpr => { const v = lowerer.lowerExpr(a); + if (isUnitType(v.type)) return lowerer.ensureString(v, a); if (v.type.kind === "string") return v; if (v.type.kind === "f64" || v.type.kind === "bool" || v.type.kind === "record") { return { kind: "toString", operand: v, type: STRING, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 95d0205c1..d9c25f260 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -6311,8 +6311,18 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr ); } if (isUnitType(e.type)) { - // `${undefined}` / "" + null: Node prints "undefined"/"null", but - // these only arise spelled literally — reject rather than special-case. + if (e.kind === "unitLit") { + return { kind: "strLit", value: e.unit, type: STRING, loc: e.loc }; + } + // Keep sequencing and branch effects even though the final unit's + // spelling is constant. Never turn an unknown producer into a literal. + if (e.kind === "seqExpr") { + return { ...e, result: lowerer.ensureString(e.result, node), type: STRING }; + } + if (e.kind === "ternary") { + return { ...e, then: lowerer.ensureString(e.then, node), + else_: lowerer.ensureString(e.else_, node), type: STRING }; + } lowerer.unsupported( "SC1090", node, diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index c09e08c44..02a200d4e 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -14210,6 +14210,12 @@ "/tests/corpus/checked-dispatch-input-lifetimes.js" ], "diags": [] + }, + "/tests/corpus/string-construction-order.ts": { + "order": [ + "/tests/corpus/string-construction-order.ts" + ], + "diags": [] } } } diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 38810d2fb..4420dcbba 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -1335,21 +1335,26 @@ bool scr_arr_includes_ref(ScrArr *a, void *v) { /* ── join ──────────────────────────────────────────────────────────────── */ -static void scr_join_append(char **buf, size_t *len, size_t *cap, - const char *bytes, size_t n) { - if (*len + n > *cap) { - size_t cap2 = *cap; - while (*len + n > cap2) { - if (cap2 > SIZE_MAX / 2) scr_arr_oom(); - cap2 *= 2; - } - char *grown = realloc(*buf, cap2); - if (!grown) scr_arr_oom(); - *buf = grown; - *cap = cap2; +static size_t scr_join_size(size_t len, size_t extra) { + if (extra > SIZE_MAX - sizeof(ScrStr) - 1 - len) scr_arr_oom(); + return len + extra; +} + +/* The builder owns the final string allocation throughout construction. + * It has no aliases or index-cache entries, and no user callbacks run while + * appending primitive array values. Finish by publishing the terminator. */ +static void scr_join_append(ScrStr **out, const char *bytes, size_t n) { + ScrStr *s = *out; + size_t need = scr_join_size(s->len, n); + if (need > s->cap) { + const size_t limit = SIZE_MAX - sizeof(ScrStr) - 1; + size_t cap = s->cap; + while (cap < need) cap = cap > limit / 2 ? need : cap * 2; + s = scr_str_regrow(s, cap); + *out = s; } - memcpy(*buf + *len, bytes, n); - *len += n; + memcpy(s->data + s->len, bytes, n); + s->len = need; } /* `a.slice(start?, end?)` — a fresh shallow copy of the index range, @@ -1392,11 +1397,24 @@ ScrArr *scr_arr_slice(ScrArr *a, double start, double end) { } ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep) { - size_t cap = 64, len = 0; - char *buf = malloc(cap); - if (!buf) scr_arr_oom(); + size_t cap = 64; + /* String widths are already known. Size once to avoid growth and copying + * even when the array holds holes, explicit undefined or sparse slots. + * Numeric formatting stays single-pass rather than formatting twice. */ + if (a->elem == SCR_ELEM_STR) { + const size_t limit = SIZE_MAX - sizeof(ScrStr) - 1; + if (a->len > 1 && sep->len > limit / (a->len - 1)) scr_arr_oom(); + cap = a->len > 1 ? (a->len - 1) * sep->len : 0; + for (size_t i = 0; i < a->len; i++) { + uint64_t slot; + if (scr_arr_state_at(a, i, &slot) != SCR_ARR_VALUE) continue; + const ScrStr *s = (const ScrStr *)scr_slot_to_ptr(slot); + if (s) cap = scr_join_size(cap, s->len); + } + } + ScrStr *out = scr_str_alloc_raw(0, cap); for (size_t i = 0; i < a->len; i++) { - if (i > 0) scr_join_append(&buf, &len, &cap, sep->data, sep->len); + if (i > 0) scr_join_append(&out, sep->data, sep->len); uint64_t slot; uint8_t state = scr_arr_state_at(a, i, &slot); if (state != SCR_ARR_VALUE) continue; @@ -1404,16 +1422,16 @@ ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep) { case SCR_ELEM_F64: { char nb[32]; size_t n = scr_f64_to_str(scr_slot_to_f64(slot), nb); - scr_join_append(&buf, &len, &cap, nb, n); + scr_join_append(&out, nb, n); break; } case SCR_ELEM_BOOL: - if (slot != 0) scr_join_append(&buf, &len, &cap, "true", 4); - else scr_join_append(&buf, &len, &cap, "false", 5); + if (slot != 0) scr_join_append(&out, "true", 4); + else scr_join_append(&out, "false", 5); break; case SCR_ELEM_STR: { const ScrStr *s = (const ScrStr *)scr_slot_to_ptr(slot); - if (s) scr_join_append(&buf, &len, &cap, s->data, s->len); + if (s) scr_join_append(&out, s->data, s->len); break; } case SCR_ELEM_ARR: @@ -1423,8 +1441,7 @@ ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep) { scr_trap("scriptc: internal error: join on a ref-element array\n"); } } - ScrStr *out = scr_str_new(buf, len); - free(buf); + out->data[out->len] = '\0'; return out; } @@ -1434,25 +1451,22 @@ ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep) { * missing ones skip, exactly the spec's loop. Both arrays are * SCR_ELEM_STR; borrows both; +1 result. Never throws. */ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) { - size_t cap = 64, len = 0; - char *buf = malloc(cap); - if (!buf) scr_arr_oom(); + ScrStr *out = scr_str_alloc_raw(0, 64); for (size_t i = 0; i < raw->len; i++) { uint64_t raw_slot; if (scr_arr_slot_at(raw, i, &raw_slot)) { const ScrStr *s = (const ScrStr *)scr_slot_to_ptr(raw_slot); - if (s) scr_join_append(&buf, &len, &cap, s->data, s->len); + if (s) scr_join_append(&out, s->data, s->len); } if (i + 1 < raw->len && i < subs->len) { uint64_t sub_slot; if (scr_arr_slot_at(subs, i, &sub_slot)) { const ScrStr *v = (const ScrStr *)scr_slot_to_ptr(sub_slot); - if (v) scr_join_append(&buf, &len, &cap, v->data, v->len); + if (v) scr_join_append(&out, v->data, v->len); } } } - ScrStr *out = scr_str_new(buf, len); - free(buf); + out->data[out->len] = '\0'; return out; } diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index df6015f6e..53efc952c 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -437,7 +437,7 @@ typedef struct ScrVt { * * cap is the usable byte capacity of data[] excluding the NUL (allocation * is sizeof(ScrStr) + cap + 1); cap == len for interned literals and plain - * allocations. Spare capacity (cap > len) exists only on concat results so + * allocations. Builders and concat results may have spare capacity so * scr_str_concat can append in place when the left operand is uniquely * owned (rc == 1) — observable immutability is preserved: a string with * rc > 1 or rc == SIZE_MAX is never mutated. @@ -454,7 +454,7 @@ ScrStr *scr_str_new(const char *bytes, size_t len); /* returns +1 */ * malformed subsequences with U+FFFD. NULL with len == 0 is an empty span. */ ScrStr *scr_str_from_utf8_lossy(const uint8_t *bytes, size_t len); /* +1 */ -/* Internal allocators for buffer builders (scr_json.c): a +1 string with +/* Internal allocators for buffer builders: a +1 string with * UNINITIALIZED data (the builder fills bytes, then len and the NUL), and * an rc==1-only realloc that grows capacity in place. Both keep the RC * audit's live count exact. */ @@ -470,6 +470,8 @@ void scr_str_release(ScrStr *s); /* NULL-tolerant (uninitialized locals) */ /* Borrow both args, return +1. */ ScrStr *scr_str_concat(ScrStr *a, ScrStr *b); +/* Borrow each part without mutation; return one independently owned result. */ +ScrStr *scr_str_concat_parts(ScrStr *const *parts, size_t count); bool scr_str_eq(ScrStr *a, ScrStr *b); diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index ee3c613f0..ac752c78d 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -271,6 +271,7 @@ ScrStr *scr_str_alloc_raw(size_t len, size_t cap) { } ScrStr *scr_str_regrow(ScrStr *s, size_t newcap) { + if (newcap < s->len || newcap > SIZE_MAX - sizeof(ScrStr) - 1) scr_oom(); scr_short_forget(s); scr_sidx_purge(s); /* realloc may move; the old address may be recycled */ ScrStr *r = realloc(s, sizeof(ScrStr) + newcap + 1); @@ -300,6 +301,8 @@ void scr_str_release(ScrStr *s) { } ScrStr *scr_str_concat(ScrStr *a, ScrStr *b) { + if (a->len == 0) return scr_str_retain(b); + if (b->len == 0) return scr_str_retain(a); if (a->len > SIZE_MAX - b->len - sizeof(ScrStr) - 1) scr_oom(); size_t newlen = a->len + b->len; /* In-place append: a is uniquely owned by the caller's borrow (rc == 1 — @@ -343,6 +346,32 @@ ScrStr *scr_str_concat(ScrStr *a, ScrStr *b) { return s; } +ScrStr *scr_str_concat_parts(ScrStr *const *parts, size_t count) { + const size_t limit = SIZE_MAX - sizeof(ScrStr) - 1; + size_t len = 0; + ScrStr *only = NULL; + size_t nonempty = 0; + for (size_t i = 0; i < count; i++) { + const size_t n = parts[i]->len; + if (n > limit - len) scr_oom(); + len += n; + if (n != 0) { + only = parts[i]; + nonempty++; + } + } + if (nonempty == 1) return scr_str_retain(only); + ScrStr *out = scr_str_alloc_raw(len, len); + size_t offset = 0; + for (size_t i = 0; i < count; i++) { + const ScrStr *part = parts[i]; + memcpy(out->data + offset, part->data, part->len); + offset += part->len; + } + out->data[len] = '\0'; + return out; +} + bool scr_str_eq(ScrStr *a, ScrStr *b) { return a == b || (a->len == b->len && memcmp(a->data, b->data, a->len) == 0); } diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 5665ae069..56432c500 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -549,6 +549,36 @@ static void test_join(void) { scr_str_release(sep2); scr_arr_release(s); + s = scr_arr_new(SCR_ELEM_STR, 1); + ScrStr *shared = scr_str_new("x\0y", 3); + scr_arr_set_ref(s, 0, scr_str_retain(shared)); + scr_arr_set_undefined(s, 1); + scr_arr_set_ref(s, 8192, scr_str_retain(shared)); + scr_arr_set_ref(s, -1, scr_str_new("ignored", 7)); + sep0 = scr_str_new("", 0); + strs = scr_arr_join(s, sep0); + check(strs->len == 6 && memcmp(strs->data, "x\0yx\0y", 6) == 0, + "join handles sparse values and embedded zero without properties"); + check(shared->rc == 3 && strs->data[strs->len] == '\0', + "join preserves duplicate input owners and terminates final storage"); + scr_str_release(strs); + strs = scr_arr_join(s, sep); + check(strs->len == 8198 && strs->data[3] == ',' && strs->data[8195] == 'x', + "join sizing includes separators for sparse holes and undefined"); + scr_str_release(strs); + scr_arr_release(s); + scr_str_release(shared); + scr_str_release(sep0); + + n = scr_arr_new(SCR_ELEM_F64, 0); + for (size_t i = 0; i < 300; i++) scr_arr_push_f64(n, 123.5); + nums = scr_arr_join(n, sep); + check(nums->len == 1799 && nums->data[1799] == '\0' && + memcmp(nums->data + 1794, "123.5", 5) == 0, + "numeric join grows final storage without losing its suffix"); + scr_str_release(nums); + scr_arr_release(n); + scr_str_release(sep); #ifdef SCR_RC_AUDIT check(scr_str_live_count() == strings0, "join: no strings leaked"); diff --git a/packages/runtime/test/test_string.c b/packages/runtime/test/test_string.c index 65c38e645..cf407db7e 100644 --- a/packages/runtime/test/test_string.c +++ b/packages/runtime/test/test_string.c @@ -171,6 +171,41 @@ static void handoff_append(ScrStr **binding, ScrStr *suffix) { scr_str_release(snapshot); } +static void construction_asserts(void) { + ScrStr *empty = scr_str_new("", 0); + ScrStr *value = scr_str_new("a\0\xE6\x97\xA5", 5); + ScrStr *parts[] = {empty, value, empty, value}; + ScrStr *out = scr_str_concat_parts(parts, 4); + total++; + if (out->len != 10 || memcmp(out->data, value->data, 5) != 0 || + memcmp(out->data + 5, value->data, 5) != 0 || out->data[10] != '\0' || + value->rc != 1 || value->len != 5) { + failed++; + fputs("CONSTRUCTION: duplicate inputs, embedded zero or ownership\n", stderr); + } + scr_str_release(out); + ScrStr *single = scr_str_concat_parts(parts, 3); + ScrStr *left = scr_str_concat(empty, value); + ScrStr *right = scr_str_concat(value, empty); + total++; + if (single != value || left != value || right != value || value->rc != 4) { + failed++; + fputs("CONSTRUCTION: empty operands must preserve independent owners\n", stderr); + } + scr_str_release(single); + scr_str_release(left); + scr_str_release(right); + out = scr_str_concat_parts(NULL, 0); + total++; + if (out->len != 0 || out->data[0] != '\0') { + failed++; + fputs("CONSTRUCTION: empty part list\n", stderr); + } + scr_str_release(out); + scr_str_release(value); + scr_str_release(empty); +} + static void accumulation_asserts(void) { ScrStr *piece = scr_str_new("x", 1); ScrStr *acc = scr_str_new("", 0); @@ -733,6 +768,7 @@ int main(int argc, char **argv) { if (in != stdin) fclose(in); divergence_asserts(); + construction_asserts(); accumulation_asserts(); short_string_asserts(); #ifdef SCR_SIDX_TEST diff --git a/tests/corpus/string-construction-order.ts b/tests/corpus/string-construction-order.ts new file mode 100644 index 000000000..70198190d --- /dev/null +++ b/tests/corpus/string-construction-order.ts @@ -0,0 +1,87 @@ +function changingParts(): void { + let text = "initial"; + const alias = text; + const replace = (): string => { + text = "replacement"; + return "middle"; + }; + console.log(text + "/" + replace() + "/" + text, alias); + text = "again"; + console.log(`${text}:${replace()}:${text}:${alias}`); + console.log((text + "-") + (replace() + "!") + text); +} +changingParts(); + +function projections(): void { + const holder = { text: "before" }; + const change = (): string => { + holder.text = "after"; + return "changed"; + }; + console.log(holder.text + " / " + change() + " / " + holder.text); +} +projections(); + +function throwingParts(): void { + let order = ""; + const part = (name: string): string => { + order += name; + if (name === "b") throw new Error("stopped"); + return name.repeat(3); + }; + try { + console.log(`${part("a")}:${part("b")}:${part("c")}`); + } catch (error) { + console.log((error as Error).message, order); + } +} +throwingParts(); + +const names = ["alpha", "日本語", "😀", "zero\0byte"]; +for (const name of names) { + console.log("[" + name + "]" + name + ":" + name); + console.log(`${name}|${-0}|${NaN}|${Infinity}|${false}|${null}|${undefined}`); + console.log("" + name, name + "", "" + name + ""); +} + +console.log(String(null), String(undefined), "null=" + null, undefined + "=missing"); +let effects = 0; +const condition = (): boolean => { effects++; return effects % 2 === 0; }; +console.log(`${condition() ? null : null}/${condition() ? null : null}/${undefined}`, effects); +console.log(String((condition(), null)), String((condition(), undefined)), effects); + +const words: string[] = ["first"]; +words[2] = "last"; +words.length = 5; +console.log(words.join("::"), words.join("")); +words[20] = "far"; +words[-1] = "property"; +console.log(words.join("|").length, words.join("") === "firstlastfar"); +console.log([true, false, true].join("/")); +console.log([NaN, Infinity, -Infinity, -0, 1e-7, 1e21].join(",")); + +const raw = ["<", ">", ""]; +console.log(String.raw({ raw }, "tag", "text", "tag")); +console.log(String.raw({ raw }, "tag")); +const empty: string[] = []; +console.log(String.raw({ raw: empty }, "unused")); +console.log(String.raw({ raw }, null, undefined, false)); + +let acc = ""; +const retained: string[] = []; +for (let i = 0; i < 6; i++) { + retained.push(acc); + acc = acc + ("[" + names[i % names.length]! + "]"); +} +console.log(acc, retained.join("|")); + +async function suspendedParts(): Promise { + let text = "before"; + const change = async (): Promise => { + await Promise.resolve(); + text = "after"; + return "middle"; + }; + console.log(`${text}/${await change()}/${text}`); +} +await suspendedParts();