mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Borrow typed-array pages and literal-ending getter returns (#2421)
This commit is contained in:
@@ -130,3 +130,19 @@ test("function arms are nullable pointers compared by identity root", () => {
|
||||
expect(isPointerIdentityArm(single)).toBe(false);
|
||||
expect(unions.armTypes().get("fu")).toEqual(fn);
|
||||
});
|
||||
|
||||
test("typed array arms are nullable pointers compared by identity", () => {
|
||||
const i32: IrType = { kind: "bytes", elem: "i32" };
|
||||
const unions = new NullableUnions(
|
||||
module([
|
||||
{ id: "iu", arms: [i32, UNDEFINED_T] },
|
||||
{ id: "nin", arms: [NULL_T, i32, UNDEFINED_T] },
|
||||
{ id: "if", arms: [i32, F64] },
|
||||
]),
|
||||
);
|
||||
expect(unions.single("iu")).toMatchObject({ refTag: 0, unitTag: 1, nullTag: -1, arm: i32 });
|
||||
expect(unions.single("nin")).toMatchObject({ refTag: 1, unitTag: 2, nullTag: 0, arm: i32 });
|
||||
expect(unions.get("if")).toBeNull();
|
||||
expect(isObjectArm(unions.single("iu")!)).toBe(true);
|
||||
expect(isPointerIdentityArm(unions.single("iu")!)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -88,17 +88,17 @@ export const NULLABLE_NULL = "@sc_nullable_null";
|
||||
export const NULLABLE_ABSENT = "@sc_nullable_absent";
|
||||
|
||||
/** The reference arms whose instances are never NULL. Class and record
|
||||
* entry points all tolerate NULL; strings, Stats snapshots, arrays, maps and
|
||||
* sets retain through a NULL-skipping wrapper (shapes.ts), as do closures
|
||||
* (function arms). Object arms are always truthy and compare by identity
|
||||
* (function arms by identity root); string arms are truthy when non-empty
|
||||
* and compare bytes. */
|
||||
* entry points all tolerate NULL; strings, Stats snapshots, arrays, typed
|
||||
* arrays, maps and sets retain through a NULL-skipping wrapper (shapes.ts),
|
||||
* as do closures (function arms). Object arms are always truthy and compare
|
||||
* by identity (function arms by identity root); string arms are truthy when
|
||||
* non-empty and compare bytes. */
|
||||
export type NullableRefArm =
|
||||
| { kind: "object"; className: string }
|
||||
| { kind: "record"; shapeId: string }
|
||||
| { kind: "string" }
|
||||
| { kind: "stats" }
|
||||
| (IrType & { kind: "array" | "map" | "set" | "func" });
|
||||
| (IrType & { kind: "array" | "bytes" | "map" | "set" | "func" });
|
||||
|
||||
/** The arm is a JS object: always truthy. */
|
||||
export function isObjectArm(nullable: PointerUnion): boolean {
|
||||
@@ -273,7 +273,13 @@ function nullableShape(
|
||||
return { ...base, arm: { kind: "record", shapeId: arm.shapeId } };
|
||||
if (arm.kind === "string") return { ...base, arm: { kind: "string" } };
|
||||
if (arm.kind === "stats") return { ...base, arm: { kind: "stats" } };
|
||||
if (arm.kind === "array" || arm.kind === "map" || arm.kind === "set" || arm.kind === "func")
|
||||
if (
|
||||
arm.kind === "array" ||
|
||||
arm.kind === "bytes" ||
|
||||
arm.kind === "map" ||
|
||||
arm.kind === "set" ||
|
||||
arm.kind === "func"
|
||||
)
|
||||
return { ...base, arm };
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -139,3 +139,24 @@ test("returns of walks, including calls to borrowed-return callees, return borro
|
||||
);
|
||||
expect(analyzeWalks(owned, borrowedHost).returnsWalk).toBe(true);
|
||||
});
|
||||
|
||||
test("a string literal return keeps the body's other walks borrowed", () => {
|
||||
const ret = (value: IrExpr | null): IrStmt => ({ kind: "return", value, loc });
|
||||
const STRING: IrType = { kind: "string" };
|
||||
const text: IrExpr = {
|
||||
kind: "fieldGet",
|
||||
obj: ref("node"),
|
||||
className: "Node",
|
||||
field: "text",
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
const empty: IrExpr = { kind: "strLit", value: "", type: STRING, loc };
|
||||
const getter = fn([ret(text), ret(empty)], []);
|
||||
expect(analyzeWalks(getter, host).returnsWalk).toBe(true);
|
||||
// The literal needs no unwritten location; the field read still does.
|
||||
expect(analyzeWalks(getter, { ...host, unwritten: () => false }).returnsWalk).toBe(false);
|
||||
expect(analyzeWalks(fn([ret(empty)], []), { ...host, unwritten: () => false }).returnsWalk).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -42,8 +42,9 @@ const WALK_KINDS: readonly IrNodeKind[] = [
|
||||
|
||||
export interface WalkFacts {
|
||||
locals: ReadonlySet<string>;
|
||||
/** Every return yields a walk (or a throw), so the body can return its
|
||||
* pointer without a reference: no try statement, at least one return. */
|
||||
/** Every return yields a walk, a throw or a string literal (an interned
|
||||
* constant that nothing frees), so the body can return its pointer
|
||||
* without a reference: no try statement, at least one return. */
|
||||
returnsWalk: boolean;
|
||||
}
|
||||
|
||||
@@ -176,6 +177,7 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts {
|
||||
};
|
||||
const isThrow = (e: IrExpr): boolean => e.kind === "libCall" && e.fn === "error.nodeThrow";
|
||||
const walkOrThrow = (e: IrExpr): boolean => isThrow(e) || walk(e);
|
||||
const constant = (e: IrExpr): boolean => isThrow(e) || e.kind === "strLit";
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const id of candidates) {
|
||||
@@ -199,8 +201,7 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts {
|
||||
returns.every(
|
||||
(value) =>
|
||||
value !== null &&
|
||||
walkOrThrow(value) &&
|
||||
(isThrow(value) || host.unwritten?.(value, candidates) !== false),
|
||||
(constant(value) || (walk(value) && host.unwritten?.(value, candidates) !== false)),
|
||||
);
|
||||
return { locals: result, returnsWalk };
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/typed-array-or-undefined.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Typed arrays in `T | undefined` slots: paged bit sets, optional fields,
|
||||
// map values, comparisons, printing and JSON.
|
||||
class PagedBits {
|
||||
readonly pages: (Int32Array | undefined)[] = [];
|
||||
has(id: number): boolean {
|
||||
const page = this.pages[id >> 8];
|
||||
return page !== undefined && (page[(id >> 5) & 7]! & (1 << (id & 31))) !== 0;
|
||||
}
|
||||
add(id: number): void {
|
||||
const index = id >> 8;
|
||||
let page = this.pages[index];
|
||||
if (page === undefined) {
|
||||
page = new Int32Array(8);
|
||||
while (this.pages.length <= index) this.pages.push(undefined);
|
||||
this.pages[index] = page;
|
||||
}
|
||||
page[(id >> 5) & 7]! |= 1 << (id & 31);
|
||||
}
|
||||
clearPage(index: number): void {
|
||||
this.pages[index] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const bits = new PagedBits();
|
||||
for (let i = 0; i < 4000; i += 13) bits.add(i);
|
||||
for (let i = 2000; i < 2600; i += 7) bits.add(i);
|
||||
let count = 0;
|
||||
for (let i = 0; i < 5000; i++) if (bits.has(i)) count++;
|
||||
console.log("count", count, "pages", bits.pages.length);
|
||||
console.log(
|
||||
"present",
|
||||
bits.pages.map((p) => (p === undefined ? "-" : p.length)).join(","),
|
||||
);
|
||||
bits.clearPage(3);
|
||||
let after = 0;
|
||||
for (let i = 0; i < 5000; i++) if (bits.has(i)) after++;
|
||||
console.log("after clear", after, bits.pages[3] === undefined, bits.pages[99]);
|
||||
|
||||
// A page read past the end and from a hole.
|
||||
const sparse: (Uint8Array | undefined)[] = [];
|
||||
sparse[4] = Uint8Array.of(1, 2, 3);
|
||||
console.log(sparse.length, sparse[0], sparse[4], sparse[9]);
|
||||
console.log(sparse.findIndex((p) => p !== undefined), sparse.includes(undefined));
|
||||
|
||||
// Optional fields and parameters.
|
||||
class Buffered {
|
||||
data: Float64Array | undefined;
|
||||
constructor(n: number) {
|
||||
this.data = n > 0 ? new Float64Array(n).fill(0.5) : undefined;
|
||||
}
|
||||
total(): number {
|
||||
const d = this.data;
|
||||
if (!d) return -1;
|
||||
let s = 0;
|
||||
for (const x of d) s += x;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
const buffers = [new Buffered(4), new Buffered(0), new Buffered(2)];
|
||||
console.log(buffers.map((b) => b.total()));
|
||||
console.log(buffers.map((b) => b.data?.length ?? "none"));
|
||||
buffers[1]!.data = buffers[0]!.data;
|
||||
console.log(buffers[1]!.data === buffers[0]!.data, buffers[2]!.data === buffers[0]!.data);
|
||||
buffers[0]!.data![0] = 9;
|
||||
console.log(buffers[1]!.total(), typeof buffers[1]!.data, typeof buffers[2]!.data?.[5]);
|
||||
|
||||
function pick(a: Int16Array | undefined, b: Int16Array | undefined): Int16Array | undefined {
|
||||
return a ?? b;
|
||||
}
|
||||
const one = Int16Array.of(-1, 2);
|
||||
console.log(pick(undefined, one), pick(one, undefined) === one, pick(undefined, undefined));
|
||||
|
||||
// Map values and printing.
|
||||
const byName = new Map<string, Uint32Array | undefined>();
|
||||
byName.set("a", Uint32Array.of(7, 8));
|
||||
byName.set("b", undefined);
|
||||
for (const [k, v] of byName) console.log(k, v, v ? v[1] : "absent");
|
||||
console.log(byName.get("c"), byName.has("b"));
|
||||
const row: (Int32Array | undefined)[] = [Int32Array.of(5), undefined];
|
||||
console.log(row, JSON.stringify(row));
|
||||
const copy = [...row, Int32Array.of(6)];
|
||||
console.log(copy.filter((p) => p !== undefined).length, copy.indexOf(row[0]));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2800 }
|
||||
Reference in New Issue
Block a user