mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
Hold array and record elements whose serialization can call user code
JSON.stringify walked a dense native array through its borrowed storage and wrote record fields borrowed from the record. When an element's own serialization can run user code (it reaches an array that may hold foreign elements, whose getters and toJSON run, or a class instance or checked-dynamic value), that code could grow the array, reallocating the storage the walk still read, or replace the field it was writing. In programs that store foreign elements, such arrays now take the general walk, which reads each index afresh and holds its element, and such record fields are held across their write. Programs without foreign elements compile as before.
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
llFieldType,
|
||||
loadStructField,
|
||||
releaseSym,
|
||||
retainSym,
|
||||
traceAdapter,
|
||||
type ShapeHost,
|
||||
} from "./shapes.js";
|
||||
@@ -323,9 +324,7 @@ export class LlWalkers {
|
||||
if (i > 0) this.putc(B, "%b", "44"); // ','
|
||||
if (edgeable(f.type)) this.jbEdgeIdx(B, String(i));
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
B.line(
|
||||
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; [${llvmCommentText(f.name)}]`,
|
||||
);
|
||||
this.writeField(B, v, f.type, `[${llvmCommentText(f.name)}]`);
|
||||
});
|
||||
this.putc(B, "%b", "93"); // ']'
|
||||
if (cyclic) this.jbLeave(B);
|
||||
@@ -357,9 +356,7 @@ export class LlWalkers {
|
||||
this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`);
|
||||
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
B.line(
|
||||
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
this.writeField(B, v, f.type, `${llvmCommentText(f.name)}`);
|
||||
});
|
||||
} else {
|
||||
const first = B.slot();
|
||||
@@ -394,9 +391,7 @@ export class LlWalkers {
|
||||
comma();
|
||||
this.puts(B, "%b", jsonObjectKeyLabel(f.name));
|
||||
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
|
||||
B.line(
|
||||
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
this.writeField(B, v, f.type, `${llvmCommentText(f.name)}`);
|
||||
if (skip !== null) {
|
||||
B.br(skip);
|
||||
B.startBlock(skip);
|
||||
@@ -518,9 +513,7 @@ export class LlWalkers {
|
||||
B.startBlock(lKey);
|
||||
this.puts(B, "%b", jsonObjectKeyLabel(fieldName));
|
||||
if (cyclic && traceAdapter(this.host, f.type) !== null) this.jbEdgeProp(B, fieldName);
|
||||
B.line(
|
||||
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(fieldName)}`,
|
||||
);
|
||||
this.writeField(B, v, f.type, `${llvmCommentText(fieldName)}`);
|
||||
B.terminate("ret i1 true");
|
||||
});
|
||||
B.startBlock(lSkip);
|
||||
@@ -676,6 +669,47 @@ export class LlWalkers {
|
||||
B.line(`call void @scr_arr_release(ptr ${ks})`);
|
||||
}
|
||||
|
||||
/** Serializing a value of `t` can call user code that changes the
|
||||
* arrays or records holding it: it reaches an array that can hold
|
||||
* foreign elements (their toJSON and getters), or a value whose own
|
||||
* serialization may (a class instance, a checked-dynamic value), in a
|
||||
* program that has foreign elements at all. */
|
||||
private serializationMayCallUserCode(t: IrType, seen = new Set<string>()): boolean {
|
||||
const host = this.host;
|
||||
if (!host.mayHoldForeign({ kind: "f64" }) && !host.mayHoldForeign({ kind: "string" }))
|
||||
return false;
|
||||
const key = typeKey(t);
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
switch (t.kind) {
|
||||
case "dyn":
|
||||
case "object":
|
||||
return true;
|
||||
case "array":
|
||||
return host.mayHoldForeign(t.elem) || this.serializationMayCallUserCode(t.elem, seen);
|
||||
case "record":
|
||||
return (host.recordsById.get(t.shapeId)?.fields ?? []).some((field) =>
|
||||
this.serializationMayCallUserCode(field.type, seen),
|
||||
);
|
||||
case "union":
|
||||
return (host.unionsById.get(t.unionId)?.arms ?? []).some((arm) =>
|
||||
this.serializationMayCallUserCode(arm, seen),
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Write a field value loaded borrowed from its record. When its
|
||||
* serialization can call user code that replaces the field, the value
|
||||
* is held across the write. */
|
||||
private writeField(B: BlockBuilder, v: string, t: IrType, comment: string): void {
|
||||
const held = isRefCounted(t) && this.serializationMayCallUserCode(t);
|
||||
if (held) B.line(`call ptr ${retainSym(this.host, t)}(ptr ${v})`);
|
||||
B.line(`call void @${this.jsonWriteHelper(t)}(ptr %b, ${this.valTy(t)} ${v}) ; ${comment}`);
|
||||
if (held) B.line(`call void ${releaseSym(this.host, t)}(ptr ${v})`);
|
||||
}
|
||||
|
||||
private emitArrayWriter(B: BlockBuilder, elem: IrType, cyclic: boolean): void {
|
||||
const host = this.host;
|
||||
const w = this.jsonWriteHelper(elem);
|
||||
@@ -685,7 +719,12 @@ export class LlWalkers {
|
||||
if (cyclic) this.jbEnter(B, true);
|
||||
this.putc(B, "%b", "91"); // '['
|
||||
const finish = B.newLabel("jwa.end");
|
||||
if (elem.kind !== "bool") this.emitDenseArrayElements(B, elem, w, cyclic, finish);
|
||||
// The dense walk borrows the storage and each element. When an
|
||||
// element's serialization can call user code, which can grow or
|
||||
// shrink the array or replace the element, the general walk below
|
||||
// reads each index afresh and holds its element instead.
|
||||
if (elem.kind !== "bool" && !this.serializationMayCallUserCode(elem))
|
||||
this.emitDenseArrayElements(B, elem, w, cyclic, finish);
|
||||
const len = B.tmp();
|
||||
B.line(`${len} = call double @scr_arr_len(ptr %v)`);
|
||||
B.countedLoop(len, (i) => {
|
||||
|
||||
@@ -89,3 +89,28 @@ const cut: string[] = ["a", "b"];
|
||||
const cutView: unknown[] = cut;
|
||||
cutView.splice(1, 0, { toString: () => ((cut.length = 0), "x") });
|
||||
console.log(JSON.stringify(cut.join("-")), cut.length);
|
||||
|
||||
// JSON of elements whose getters change the array or record holding them.
|
||||
const fillers: number[][] = [];
|
||||
const rows: number[][] = [[1], [2]];
|
||||
const rowView: unknown[] = rows[0]!;
|
||||
rowView.push({
|
||||
get grown() {
|
||||
for (let i = 0; i < 64; i++) rows.push([i]);
|
||||
rows.length = 3;
|
||||
// Reuse the memory the old storage held.
|
||||
for (let i = 0; i < 64; i++) fillers.push([i, i]);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
console.log(JSON.stringify(rows), rows.length);
|
||||
const holder = { values: [5] as number[], name: "h" };
|
||||
const holderView: unknown[] = holder.values;
|
||||
holderView.push({
|
||||
get replaced() {
|
||||
holder.values = [9];
|
||||
for (let i = 0; i < 64; i++) fillers.push([i]);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
console.log(JSON.stringify(holder), JSON.stringify(holder));
|
||||
|
||||
Reference in New Issue
Block a user