diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 849715ab0..55bbb828b 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -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})`, ); diff --git a/packages/compiler/src/backend/llvm/js-ic.ts b/packages/compiler/src/backend/llvm/js-ic.ts index b69d3199d..11b894a60 100644 --- a/packages/compiler/src/backend/llvm/js-ic.ts +++ b/packages/compiler/src/backend/llvm/js-ic.ts @@ -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). */ diff --git a/packages/compiler/src/ir/js-ic.test.ts b/packages/compiler/src/ir/js-ic.test.ts index f5391b83e..35de18085 100644 --- a/packages/compiler/src/ir/js-ic.test.ts +++ b/packages/compiler/src/ir/js-ic.test.ts @@ -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 (!(v)) return `: the class tables' early exit. */ const exit = (test: "classCapsule" | "funcKind", value: IrExpr, negated = true): IrStmt => ({ diff --git a/packages/compiler/src/ir/js-ic.ts b/packages/compiler/src/ir/js-ic.ts index 48019636a..5a483c585 100644 --- a/packages/compiler/src/ir/js-ic.ts +++ b/packages/compiler/src/ir/js-ic.ts @@ -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(...)`, diff --git a/packages/compiler/test/js-ic-emission.test.ts b/packages/compiler/test/js-ic-emission.test.ts index 15e8e92bc..79342e99d 100644 --- a/packages/compiler/test/js-ic-emission.test.ts +++ b/packages/compiler/test/js-ic-emission.test.ts @@ -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 { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-members.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-members.mjs.json new file mode 100644 index 000000000..f1f9017ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-literal-members.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/object-literal-members.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/object-literal-members.mjs b/tests/corpus/object-literal-members.mjs new file mode 100644 index 000000000..4bd211435 --- /dev/null +++ b/tests/corpus/object-literal-members.mjs @@ -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)); diff --git a/tests/harness/corpus-costs/object-literal-members.mjs.json b/tests/harness/corpus-costs/object-literal-members.mjs.json new file mode 100644 index 000000000..882d60088 --- /dev/null +++ b/tests/harness/corpus-costs/object-literal-members.mjs.json @@ -0,0 +1 @@ +{ "plain": 3565, "san": 6266 } diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index 4c1b96b37..4a3d4c390 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -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 () => { diff --git a/tests/library-mode/js-ic-sessions/work.mjs b/tests/library-mode/js-ic-sessions/work.mjs index 5c1afcd18..e6c4a2fc3 100644 --- a/tests/library-mode/js-ic-sessions/work.mjs +++ b/tests/library-mode/js-ic-sessions/work.mjs @@ -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)); }