Add object literal members through the store caches in untyped code (#2218)

This commit is contained in:
Chris Tate
2026-10-25 09:07:14 -05:00
committed by GitHub
10 changed files with 175 additions and 8 deletions
@@ -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). */
+17
View File
@@ -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 => ({
+15 -2
View File
@@ -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 {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/object-literal-members.mjs"
],
"diags": []
}
+67
View File
@@ -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 }
+2 -2
View File
@@ -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 () => {
+6 -3
View File
@@ -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));
}