mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
A whole-value read of a nullable-pointer field allocated a heap box per read, so list walks (`p = p.next`) regressed: 0.07 s -> 0.35 s on a 100k list walked 200 times. Such reads now retain the instance pointer into the current frame and wrap it in a private stack box with that independent owner: - local union storage accepts nullable field reads and copies of another binding of the same union as sources (the payload is retained; scalar and unit arms own nothing; retains branch on the tag because not every retain entry point is NULL-tolerant); - arguments to projection-only parameters of calls and constructors. The list walk now takes 0.05 s (baseline 0.07 s). Corpus 4061 adds a copy that outlives a store to the field and copies across string, number, -0 and undefined arms.
217 lines
7.0 KiB
TypeScript
217 lines
7.0 KiB
TypeScript
// Class fields typed `C | null` / `C | undefined` hold the instance pointer
|
|
// itself (no union box). This pins every way such a field is read and
|
|
// written: tests, narrowing, `??`, `?.`, equality, truthiness, whole-value
|
|
// escapes (arrays, calls, returns, closures), reassignment and self-
|
|
// assignment, constructor parameters that are and are not projection-only,
|
|
// subclass instances in base-typed fields, shared-field reads across a
|
|
// class union, `any` views (reads and writes through the dynamic bridge),
|
|
// JSON, cycles for the collector, and a long chain's teardown.
|
|
|
|
class Node {
|
|
next: Node | null = null;
|
|
prev?: Node;
|
|
readonly id: number;
|
|
constructor(id: number) {
|
|
this.id = id;
|
|
}
|
|
}
|
|
|
|
const a = new Node(1);
|
|
const b = new Node(2);
|
|
console.log("init", a.next === null, a.prev === undefined, a.next, a.prev);
|
|
a.next = b;
|
|
b.prev = a;
|
|
console.log("linked", a.next === b, a.next?.id, b.prev?.id, b.next?.id, a.prev?.id);
|
|
console.log("nullish", (a.next ?? b).id, (b.next ?? a).id, b.prev?.id ?? -1, a.prev?.id ?? -1);
|
|
console.log("truthy", !!a.next, !!b.next, !!b.prev, !!a.prev, a.next ? "yes" : "no", b.next ? "yes" : "no");
|
|
console.log("eq", a.next === b.next, a.next !== b, b.prev === a, a.prev === b.next as unknown);
|
|
|
|
// Whole-value escapes: arrays, calls, returns, closures.
|
|
function describe(n: Node | null): string {
|
|
return n === null ? "null" : `#${n.id}`;
|
|
}
|
|
function pick(n: Node, first: boolean): Node | null {
|
|
return first ? n.next : null;
|
|
}
|
|
const held: (Node | null)[] = [a.next, b.next, pick(a, true), pick(a, false)];
|
|
console.log("held", held.map(describe).join(","), describe(a.next), describe(b.next));
|
|
const getNext = (): Node | null => a.next;
|
|
a.next = null;
|
|
console.log("closure", describe(getNext()), describe(held[0]!), held[0] === b);
|
|
a.next = b;
|
|
console.log("closure2", describe(getNext()));
|
|
|
|
// Reassignment releases the old instance; self-assignment keeps it alive.
|
|
let chain = new Node(10);
|
|
chain.next = new Node(11);
|
|
chain.next.next = new Node(12);
|
|
chain.next = chain.next;
|
|
chain.next.next = chain.next.next;
|
|
console.log("self", chain.next?.id, chain.next?.next?.id);
|
|
chain.next = chain.next?.next ?? null;
|
|
console.log("skip", chain.next?.id, chain.next?.next);
|
|
chain = new Node(20);
|
|
console.log("fresh", chain.next, chain.id);
|
|
|
|
// Mutation inside `??` defaults and equality operands.
|
|
const m = new Node(30);
|
|
m.next = null;
|
|
const viaDefault = m.next ?? ((m.next = new Node(31)), m.next);
|
|
console.log("default", viaDefault.id, m.next?.id, viaDefault === m.next);
|
|
const old = m.next;
|
|
function swap(n: Node): Node | null {
|
|
n.next = new Node(32);
|
|
return n.next;
|
|
}
|
|
console.log("eq-mutation", m.next === swap(m), old === m.next, m.next?.id, old?.id);
|
|
|
|
// Constructor parameters: projection-only stores, and stores that also
|
|
// escape the parameter elsewhere.
|
|
class Tree {
|
|
readonly left: Tree | null;
|
|
readonly right: Tree | null;
|
|
constructor(left: Tree | null, right: Tree | null) {
|
|
this.left = left;
|
|
this.right = right;
|
|
}
|
|
count(): number {
|
|
return 1 + (this.left === null ? 0 : this.left.count()) + (this.right?.count() ?? 0);
|
|
}
|
|
}
|
|
const seen: (Tree | null)[] = [];
|
|
class Logged {
|
|
readonly child: Tree | null;
|
|
constructor(child: Tree | null) {
|
|
seen.push(child);
|
|
this.child = child;
|
|
}
|
|
}
|
|
function build(depth: number): Tree {
|
|
return depth === 0 ? new Tree(null, null) : new Tree(build(depth - 1), build(depth - 1));
|
|
}
|
|
let total = 0;
|
|
for (let i = 0; i < 200; i++) total += build(8).count();
|
|
const logged = [new Logged(build(2)), new Logged(null)];
|
|
console.log("trees", total, logged[0]!.child?.count(), logged[1]!.child, seen.length, seen[0] === logged[0]!.child);
|
|
|
|
// Subclass instances in base-typed fields; virtual calls through them.
|
|
class Shape {
|
|
parent: Shape | null = null;
|
|
area(): number {
|
|
return 0;
|
|
}
|
|
}
|
|
class Square extends Shape {
|
|
readonly side: number;
|
|
constructor(side: number) {
|
|
super();
|
|
this.side = side;
|
|
}
|
|
area(): number {
|
|
return this.side * this.side;
|
|
}
|
|
}
|
|
class Circle extends Shape {
|
|
readonly r: number;
|
|
constructor(r: number) {
|
|
super();
|
|
this.r = r;
|
|
}
|
|
area(): number {
|
|
return Math.round(Math.PI * this.r * this.r);
|
|
}
|
|
}
|
|
const sq = new Square(3);
|
|
const ci = new Circle(2);
|
|
sq.parent = ci;
|
|
ci.parent = new Square(5);
|
|
console.log("virtual", sq.parent.area(), sq.parent?.parent?.area(), ci.parent?.parent?.area() ?? "none");
|
|
|
|
// Shared-field reads across a union of classes.
|
|
class Leaf {
|
|
readonly kind = "leaf";
|
|
owner: Branch | null = null;
|
|
}
|
|
class Branch {
|
|
readonly kind = "branch";
|
|
owner: Branch | null = null;
|
|
}
|
|
const root = new Branch();
|
|
const owned = new Leaf();
|
|
const inner = new Branch();
|
|
owned.owner = root;
|
|
inner.owner = root;
|
|
const items: (Leaf | Branch)[] = [owned, inner, new Leaf()];
|
|
for (const item of items) console.log("owner", item.kind, item.owner === root, item.owner?.kind ?? "none");
|
|
|
|
// `any` views: reads, writes, JSON, and console output.
|
|
class Box {
|
|
inner: Box | null = null;
|
|
readonly tag: string;
|
|
constructor(tag: string) {
|
|
this.tag = tag;
|
|
}
|
|
}
|
|
const outer = new Box("outer");
|
|
outer.inner = new Box("inner");
|
|
const view: any = outer;
|
|
console.log("any-read", view.inner.tag, view.inner.inner);
|
|
// Read through a helper: TypeScript's assignment narrowing of `outer.inner`
|
|
// does not see writes made through the `any` view.
|
|
function innerOf(box: Box): Box | null {
|
|
return box.inner;
|
|
}
|
|
view.inner = new Box("replaced");
|
|
console.log("any-write", innerOf(outer)?.tag, innerOf(outer)?.inner);
|
|
view.inner = null;
|
|
console.log("any-null", innerOf(outer), innerOf(outer) === null);
|
|
outer.inner = new Box("again");
|
|
console.log("json", JSON.stringify(view));
|
|
console.log(outer);
|
|
|
|
// Local copies own their payload: a copied field read survives a later
|
|
// store to the field, and copies of other bindings keep scalar arms intact.
|
|
const keeper = new Node(40);
|
|
keeper.next = new Node(41);
|
|
let kept: Node | null = keeper.next;
|
|
keeper.next = null;
|
|
console.log("kept", describe(kept), keeper.next);
|
|
kept = keeper.next;
|
|
console.log("kept2", describe(kept));
|
|
function mixed(flag: number): string {
|
|
const first: string | number | undefined = flag > 1 ? "two" : flag > 0 ? 1 : undefined;
|
|
let copy = first;
|
|
let out = typeof copy === "string" ? copy.toUpperCase() : String(copy);
|
|
const second: string | number | undefined = flag > 0 ? -0 : `s${flag}`;
|
|
copy = second;
|
|
out += "|" + (typeof copy === "number" ? String(Object.is(copy, -0)) : copy);
|
|
copy = first;
|
|
return out + "|" + (copy ?? "none");
|
|
}
|
|
console.log("mixed", mixed(0), mixed(1), mixed(2));
|
|
|
|
// Cycles through nullable fields are collected; output stays stable.
|
|
let survivors = 0;
|
|
for (let i = 0; i < 20000; i++) {
|
|
const x = new Node(i);
|
|
const y = new Node(i + 1);
|
|
x.next = y;
|
|
y.next = x;
|
|
y.prev = x;
|
|
if (x.next.next === x) survivors++;
|
|
}
|
|
console.log("cycles", survivors);
|
|
|
|
// A long chain tears down without exhausting the native stack.
|
|
let head: Node | null = null;
|
|
for (let i = 0; i < 300000; i++) {
|
|
const n = new Node(i);
|
|
n.next = head;
|
|
head = n;
|
|
}
|
|
let length = 0;
|
|
for (let p: Node | null = head; p !== null; p = p.next) length++;
|
|
console.log("chain", length, head?.id, head?.next?.id);
|
|
head = null;
|
|
console.log("done", head);
|