mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Add object literal members through the store caches in untyped code (#2218)
This commit is contained in:
@@ -31,7 +31,8 @@ import { elemAccess, vAdapters } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { emitUnionWiden } from "./expr-records.js";
|
||||
import { emitJsIcMethod, jsIcMethods } from "./js-ic.js";
|
||||
import { emitJsIcLiteralMember, emitJsIcMethod, jsIcMethods } from "./js-ic.js";
|
||||
import { jsIcLiteralKey } from "../../ir/js-ic.js";
|
||||
import { DYN_BUFFER_METHODS } from "../../ir/runtime-features.js";
|
||||
|
||||
export function emitDynamicExpr(
|
||||
@@ -316,9 +317,14 @@ export function emitDynamicExpr(
|
||||
const obj = B.tmp();
|
||||
B.line(`${obj} = call ptr @scr_dyn_new_obj()`);
|
||||
const out = host.own({ name: obj, type: e.type });
|
||||
const cached = jsIcMethods(host);
|
||||
for (const f of e.fields ?? []) {
|
||||
const k = emitBorrowedInput(host, f.key);
|
||||
const v = emitBorrowedInput(host, f.value);
|
||||
if (cached && jsIcLiteralKey(f.key) !== null) {
|
||||
emitJsIcLiteralMember(host, obj, k.name, v.name);
|
||||
continue;
|
||||
}
|
||||
B.line(
|
||||
`call void @${f.key.type.kind === "dyn" ? "scr_dyn_key_set_computed" : "scr_dyn_key_set"}(ptr ${obj}, ptr ${k.name}, ptr ${v.name})`,
|
||||
);
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* keyed dense array element at an integral number key: inline;
|
||||
* otherwise scr_js_keyed_probe (string keys of ordinary objects)
|
||||
* method dynInvoke's method read: scr_js_load_probe
|
||||
* literal an object literal's constant-key member: scr_js_store_probe,
|
||||
* else scr_dyn_key_set
|
||||
*
|
||||
* A probe that cannot answer from plain data leaves everything untouched,
|
||||
* and the site calls its helper (or the generic runtime entry), which
|
||||
@@ -838,6 +840,27 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
|
||||
return host.own({ name: out, type: e.type });
|
||||
}
|
||||
|
||||
/** One member of an object literal (ir/js-ic.ts jsIcLiteralKey) added to
|
||||
* the fresh object `obj` through the site's store cache, else with
|
||||
* scr_dyn_key_set. The key and value are borrowed. */
|
||||
export function emitJsIcLiteralMember(host: Host, obj: string, key: string, value: string): void {
|
||||
const B = host.B;
|
||||
const ic = host.jsIcSite();
|
||||
host.declare(`declare zeroext i1 @scr_js_store_probe(ptr, ptr, ptr, ptr)`);
|
||||
const done = B.tmp();
|
||||
B.line(
|
||||
`${done} = call zeroext i1 @scr_js_store_probe(ptr ${obj}, ptr ${key}, ptr ${value}, ptr ${ic})`,
|
||||
);
|
||||
const slow = B.newLabel("icl.slow");
|
||||
const join = B.newLabel("icl.j");
|
||||
B.condBr(done, join, slow);
|
||||
B.startBlock(slow);
|
||||
B.line(`call void @scr_dyn_key_set(ptr ${obj}, ptr ${key}, ptr ${value})`);
|
||||
host.emitPendingCheck();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
}
|
||||
|
||||
/** dynInvoke's method read on an ordinary object through the site's
|
||||
* cache, else scr_dyn_prepare_method. Returns the owned method (or NULL,
|
||||
* which scr_dyn_invoke_prepared_id answers with the full dispatch). */
|
||||
|
||||
@@ -311,6 +311,23 @@ test("a function holds a site when it calls a classified helper or dispatches a
|
||||
expect(hasJsIcSite({ ...caller, name: "%init.0" }, sites)).toBe(true);
|
||||
});
|
||||
|
||||
test("a function holds a site when an object literal has a constant-key member", () => {
|
||||
const sites = sitesOf();
|
||||
const literal = (key: IrExpr): IrFunction =>
|
||||
helper(
|
||||
"f",
|
||||
["o"],
|
||||
[ret({ kind: "dynObjLit", fields: [{ key, value: ref("o") }], type: DYN, loc })],
|
||||
);
|
||||
expect(hasJsIcSite(literal(str("x")), sites)).toBe(true);
|
||||
// `__proto__` sets the prototype, and a computed key has no constant
|
||||
// name: both keep the generic member store.
|
||||
expect(hasJsIcSite(literal(str("__proto__")), sites)).toBe(false);
|
||||
expect(hasJsIcSite(literal(ref("k")), sites)).toBe(false);
|
||||
// Module initializers build their literals once.
|
||||
expect(hasJsIcSite({ ...literal(str("x")), name: "%init.0" }, sites)).toBe(false);
|
||||
});
|
||||
|
||||
test("bag helpers that exit early for values no class branch matches are still the receiver", () => {
|
||||
/** `if (!<test>(v)) return <value>`: the class tables' early exit. */
|
||||
const exit = (test: "classCapsule" | "funcKind", value: IrExpr, negated = true): IrStmt => ({
|
||||
|
||||
@@ -296,20 +296,33 @@ function unwrapFromDyn(e: IrExpr | null | undefined): IrExpr | null {
|
||||
return e;
|
||||
}
|
||||
|
||||
/** Whether a function holds a cache site: a call to a classified helper or
|
||||
* a method dispatch. */
|
||||
/** Whether a function holds a cache site: a call to a classified helper,
|
||||
* a method dispatch, or an object literal member with a constant key. */
|
||||
export function hasJsIcSite(fn: IrFunction, sites: JsIcSites): boolean {
|
||||
const methods = jsIcMethodSites(fn.name);
|
||||
return !everyStmtList(fn.body, {
|
||||
expr: (e) =>
|
||||
!(
|
||||
(methods && e.kind === "dynInvoke") ||
|
||||
(methods &&
|
||||
e.kind === "dynObjLit" &&
|
||||
(e.fields ?? []).some((f) => jsIcLiteralKey(f.key) !== null)) ||
|
||||
(e.kind === "call" && sites.site(e.callee) !== null)
|
||||
),
|
||||
stmt: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
/** The key of an object literal member that its site adds through the
|
||||
* store cache: a constant string other than `__proto__`, or null. The
|
||||
* literal starts as an empty object with the default prototype, so each
|
||||
* such member appends one data property, and the site caches the shape
|
||||
* transition and shares the key bytes the shape holds. Module
|
||||
* initializers have no literal sites (jsIcMethodSites). */
|
||||
export function jsIcLiteralKey(key: IrExpr): string | null {
|
||||
return key.kind === "strLit" && key.value !== "__proto__" ? key.value : null;
|
||||
}
|
||||
|
||||
/** Whether method dispatch in a function goes through caches. Module
|
||||
* initializers (`%init.N`) run once, and the methods they call at top level
|
||||
* are overwhelmingly registrations on native handles (`worker.on(...)`,
|
||||
|
||||
@@ -64,6 +64,37 @@ console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n);
|
||||
}
|
||||
});
|
||||
|
||||
test("object literal members with constant keys add through store caches", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-literal-"));
|
||||
try {
|
||||
const entry = join(dir, "main.js");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(
|
||||
entry,
|
||||
`
|
||||
function make(v, k) { return { a: v, b: 2, [k]: 3 }; }
|
||||
var top = { t: 1 };
|
||||
console.log(JSON.stringify(make(1, "c")), top.t);
|
||||
`,
|
||||
);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
||||
expect(moduleUsesDynTier(mod)).toBe(true);
|
||||
const llvm = emitLlvmModule(mod);
|
||||
const make = body(llvm, "make");
|
||||
// Each constant-key member tries its site's store cache first and
|
||||
// falls back to the generic member store; the computed key keeps the
|
||||
// generic store only.
|
||||
expect(make.match(/@scr_js_store_probe\(/g)).toHaveLength(2);
|
||||
expect(make.match(/@scr_dyn_key_set\(/g)).toHaveLength(2);
|
||||
expect(make).toContain("@scr_dyn_key_set_computed(");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("property sites keep their caches in programs that declare classes and subclasses", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-class-"));
|
||||
try {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/object-literal-members.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Object literals built in untyped functions, many times through the same
|
||||
// sites: key order (integer-like keys first), members named
|
||||
// like Object.prototype members and Error fields, __proto__ members, wide
|
||||
// literals, literals that differ only in a later member, and objects
|
||||
// changed after they are built.
|
||||
function point(x, y) {
|
||||
return { x, y, label: "p" + x };
|
||||
}
|
||||
function ordered(i) {
|
||||
return { b: i, 2: "two", a: i + 1, 1: "one", "01": "zero-one" };
|
||||
}
|
||||
function named(i) {
|
||||
return { constructor: i, toString: "s" + i, message: "m" + i, name: "n" + i, code: i, stack: "st" };
|
||||
}
|
||||
function withProto(proto, i) {
|
||||
if (proto === null) return { __proto__: null, own: i };
|
||||
const o = { own: i };
|
||||
Object.setPrototypeOf(o, proto);
|
||||
return o;
|
||||
}
|
||||
function wide(i) {
|
||||
return {
|
||||
k0: i, k1: i, k2: i, k3: i, k4: i, k5: i, k6: i, k7: i, k8: i, k9: i,
|
||||
k10: i, k11: i, k12: i, k13: i, k14: i, k15: i, k16: i, k17: i, k18: i, k19: i,
|
||||
k20: i, k21: i, k22: i, k23: i, k24: i, k25: i, k26: i, k27: i, k28: i, k29: i,
|
||||
k30: i, k31: i, k32: i, k33: i, k34: i, k35: i, k36: i, k37: i, k38: i, k39: i,
|
||||
k40: i, k41: i, k42: i, k43: i, k44: i, k45: i, k46: i, k47: i, k48: i, k49: i,
|
||||
k50: i, k51: i, k52: i, k53: i, k54: i, k55: i, k56: i, k57: i, k58: i, k59: i,
|
||||
k60: i, k61: i, k62: i, k63: i, k64: i, k65: i, k66: i, k67: i, k68: i, k69: i,
|
||||
};
|
||||
}
|
||||
function variant(i) {
|
||||
const head = { kind: "v", n: i };
|
||||
return i % 2 ? { ...head, odd: true } : { ...head, even: true, n: -i };
|
||||
}
|
||||
|
||||
let sum = 0;
|
||||
const points = [];
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
const p = point(i, i * 2);
|
||||
sum += p.x + p.y;
|
||||
if (i % 500 === 0) points.push(p);
|
||||
}
|
||||
points[0].z = 9;
|
||||
delete points[1].label;
|
||||
console.log(sum, JSON.stringify(points), Object.keys(points[2]).join());
|
||||
|
||||
const o = [ordered(1), ordered(2)];
|
||||
console.log(Object.keys(o[1]).join(), JSON.stringify(o[0]), o[1]["01"], o[1][1]);
|
||||
|
||||
const n = [named(1), named(2)];
|
||||
console.log(JSON.stringify(n[1]), typeof n[0].toString, n[1].constructor, n[1] instanceof Error);
|
||||
|
||||
const proto = { greet() { return "hi " + this.own; } };
|
||||
const w = [withProto(proto, 1), withProto(proto, 2), withProto(null, 3)];
|
||||
console.log(w[1].greet(), Object.getPrototypeOf(w[0]) === proto, Object.getPrototypeOf(w[2]), Object.keys(w[1]).join());
|
||||
|
||||
let wideSum = 0;
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const big = wide(i);
|
||||
wideSum += big.k0 + big.k69;
|
||||
}
|
||||
console.log(wideSum, Object.keys(wide(1)).length, Object.keys(wide(2))[69]);
|
||||
|
||||
const v = [];
|
||||
for (let i = 0; i < 6; i++) v.push(variant(i));
|
||||
console.log(JSON.stringify(v));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3565, "san": 6266 }
|
||||
@@ -386,7 +386,7 @@ globals: undefined fenced
|
||||
const run = runProbe(probe);
|
||||
expect(run.signal).toBeNull();
|
||||
expect(run.status).toBe(0);
|
||||
expect(run.stdout).toBe("run: 20000\n".repeat(3));
|
||||
expect(run.stdout).toBe("run: 40000\n".repeat(3));
|
||||
});
|
||||
|
||||
/* ── K5/K6/K7: the trap channel ──────────────────────────────────────── */
|
||||
@@ -614,7 +614,7 @@ survived, sink_calls=1
|
||||
const run = runProbe(probe);
|
||||
expect(run.signal).toBeNull();
|
||||
expect(run.status).toBe(0);
|
||||
expect(run.stdout).toBe("run: 20000\n".repeat(3));
|
||||
expect(run.stdout).toBe("run: 40000\n".repeat(3));
|
||||
});
|
||||
|
||||
platformTest("K10: K5/K7 under ASan", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Dynamic-tier property caches across library sessions: objects built empty
|
||||
// and filled through untyped stores, read back through untyped loads, and a
|
||||
// class instance at the same kind of sites. Each session must start with
|
||||
// and filled through untyped stores or built by object literals, read back
|
||||
// through untyped loads, and a class instance at the same kind of sites. Each session must start with
|
||||
// empty caches, since the shapes their entries named are freed when the
|
||||
// previous session ends.
|
||||
class Box {
|
||||
@@ -13,6 +13,9 @@ function fill(o, n) {
|
||||
o.b = n + 1;
|
||||
return o;
|
||||
}
|
||||
function pair(n) {
|
||||
return { a: n, b: n + 1 };
|
||||
}
|
||||
function sum(o) {
|
||||
return o.a + o.b;
|
||||
}
|
||||
@@ -21,6 +24,6 @@ function unbox(b) {
|
||||
}
|
||||
export function runAll() {
|
||||
let total = 0;
|
||||
for (let i = 0; i < 100; i++) total += sum(fill({}, i));
|
||||
for (let i = 0; i < 100; i++) total += sum(fill({}, i)) + sum(pair(i));
|
||||
return total + unbox(new Box(total));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user