mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Streamline native string construction
- Combine bounded string concatenations while preserving evaluation and ownership. - Build array joins and raw strings directly in their final storage. - Support literal null and undefined string conversions with preserved effects.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -14210,6 +14210,12 @@
|
||||
"<repo>/tests/corpus/checked-dispatch-input-lifetimes.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/string-construction-order.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/string-construction-order.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<void> {
|
||||
let text = "before";
|
||||
const change = async (): Promise<string> => {
|
||||
await Promise.resolve();
|
||||
text = "after";
|
||||
return "middle";
|
||||
};
|
||||
console.log(`${text}/${await change()}/${text}`);
|
||||
}
|
||||
await suspendedParts();
|
||||
Reference in New Issue
Block a user