Files
scriptc/tests/corpus/string-field-append.ts
T
Malte Ubl 2fa733b106 Append to string array elements and o.f = o.f + x fields in place
"Append to strings in place for fields, concat chains and character
codes" made `obj.f += s` linear by handing the field's reference to a
local before the concat. Two neighbouring forms still copied the whole
string on every append, so their loops stayed quadratic:

- `parts[i] += s` on a `string[]`: the old element reached the concat
  through a retained snapshot (the hidden %compoundOld local) while the
  slot still held it. 1.6M appends spread over four slots took 11 s.
- `o.f = o.f + s` written out: the field read retains a snapshot and
  the field keeps its reference until the store.

A statement `a[i] += rhs` on a `string[]` now lowers to
`a[i] = (hole ? "undefined" : a[i]) + rhs` with the array and index saved
first (evaluation order unchanged). The backend recognizes that store and
the direct `recv.f = recv.f + suffix` on a plain string class or record
field. After the snapshot and suffix are evaluated and the store guard
ran, a field unlinks and releases its current value; an array slot in
the hole-free prefix that still holds the snapshot is swapped for the
immortal empty string and released. The concat then appends in place
when nothing else holds the string, and the result is stored as before.
Both paths unlink only when the snapshot is frame-owned (or immortal).

Aliases, interned literals, throwing suffixes, suffixes that read or
replace the field or element, rebound receivers and indices, holes,
appends past the end and appends that grow the array keep their
semantics (tests/corpus/string-field-append.ts, which also pins the
field `+=` forms).
2026-10-16 07:00:37 -07:00

203 lines
5.0 KiB
TypeScript

// `recv.f += suffix` and `recv.f = recv.f + suffix` on string fields of
// class instances and object literals, and `a[i] += suffix` on string
// arrays, append in place when nothing else holds the string. Aliases must keep their value, interned literals are
// never mutated, a throwing suffix leaves the field unchanged, suffixes that
// read or write the field see JavaScript's values, a rebound receiver keeps
// the original target, and accessors and expression-position updates keep
// their semantics. Output must match Node.
class Writer {
buf = "";
count = 0;
write(s: string): void {
this.buf += s;
this.count++;
}
writeNumber(n: number): void {
this.buf += n;
}
writeParts(a: string, n: number): void {
this.buf += "[" + a + ":" + n + "]";
}
}
function writerLoop(): string {
const w = new Writer();
for (let i = 0; i < 2000; i++) w.write("<td>" + i + "</td>");
w.writeNumber(-0);
w.writeNumber(1e21);
w.writeNumber(0.1 + 0.2);
w.writeParts("p", 7);
return `${w.count} ${w.buf.length} ${w.buf.slice(0, 24)} ${w.buf.slice(-40)}`;
}
function aliases(): string {
const o = { text: "start" };
const seen: string[] = [];
for (let i = 0; i < 5; i++) {
const before = o.text; // shares the string: the append must copy
o.text += `-${i}`;
seen.push(before);
}
const other = { text: o.text };
o.text = o.text + "!";
seen.push(other.text, o.text);
return seen.join(" ");
}
class Pair {
a = "lit";
b = "lit";
}
function literals(): string {
const p = new Pair();
p.a += "eral";
const q = new Pair();
return `${p.a} ${p.b} ${q.a} ${"lit"}`;
}
function failing(n: number): string {
if (n > 2) throw new Error(`too big: ${n}`);
return `<${n}>`;
}
function guarded(): string {
const w = new Writer();
for (let i = 0; i < 5; i++) {
try {
w.buf += failing(i);
} catch (e) {
w.buf = w.buf + `!${(e as Error).message.length}`;
}
}
return w.buf;
}
function suffixWritesField(): string {
const w = new Writer();
w.buf = "old";
w.buf += ((w.buf = "replaced"), "+tail");
const o = { text: "x" };
o.text = o.text + ((o.text = "y"), "z");
return `${w.buf} ${o.text}`;
}
function suffixReadsField(): string {
const w = new Writer();
w.buf = "ab";
w.buf += w.buf;
w.buf += w.buf.length;
w.buf = w.buf + "|" + w.buf;
return w.buf;
}
function reboundReceiver(): string {
const first = new Writer();
const second = new Writer();
first.buf = "first";
second.buf = "second";
let target = first;
target.buf += ((target = second), "-appended");
let rec = { text: "one" };
const original = rec;
rec.text = rec.text + ((rec = { text: "two" }), "-x");
return `${first.buf} ${second.buf} ${target.buf} ${original.text} ${rec.text}`;
}
class Shout {
private inner = "";
get value(): string {
return this.inner;
}
set value(v: string) {
this.inner = v.toUpperCase();
}
}
function accessor(): string {
const s = new Shout();
s.value += "ab";
s.value += "cd";
return s.value;
}
function expressionPosition(): string {
const w = new Writer();
const r1 = (w.buf += "x");
const r2 = (w.buf += "y");
return `${r1} ${r2} ${w.buf}`;
}
function surrogates(): string {
const o = { text: "a\uD83D" };
o.text += "\uDE00b";
const w = new Writer();
w.buf = "\uD83D";
w.write("\uDE00");
return `${o.text.length} ${o.text.codePointAt(1)} ${w.buf.length} ${w.buf.codePointAt(0)}`;
}
function shared(): string {
const base = "shared";
const a = { text: base };
const b = { text: base };
for (let i = 0; i < 3; i++) a.text += i;
return `${a.text} ${b.text} ${base}`;
}
function arraySlots(): string {
const slots: string[] = ["", "", "", ""];
for (let i = 0; i < 4000; i++) slots[i % 4] += "v" + (i % 10);
const direct: string[] = ["a", "b"];
for (let i = 0; i < 3; i++) direct[1] = direct[1] + i;
return `${slots[0].length} ${slots[3].slice(-6)} ${direct.join(",")}`;
}
function arrayEdges(): string {
const holes: string[] = [];
holes[2] += "x"; // a hole reads as undefined
holes[2] += "y";
holes[5] += "tail";
const keep: string[] = ["base"];
const before = keep[0];
keep[0] += "+1";
const writes: string[] = ["old"];
writes[0] += ((writes[0] = "new"), "!");
const grows: string[] = ["g"];
for (let i = 0; i < 40; i++) grows[0] += ((grows[grows.length] = `${i}`), ".");
const idx: string[] = ["i0", "i1", "i2"];
let k = 0;
idx[k] += ((k = 2), "-k");
const shared: string[] = ["s"];
const other = shared;
shared[0] += "1";
other[0] += "2";
return [
holes.length,
String(holes[2]),
String(holes[5]),
before,
keep[0],
writes[0],
grows[0].length,
grows.length,
idx.join(","),
shared[0],
].join(" ");
}
console.log(writerLoop());
console.log(aliases());
console.log(literals());
console.log(guarded());
console.log(suffixWritesField());
console.log(suffixReadsField());
console.log(reboundReceiver());
console.log(accessor());
console.log(expressionPosition());
console.log(surrogates());
console.log(shared());
console.log(arraySlots());
console.log(arrayEdges());