mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Fold members stored after their namespace was created; skip the receiver read in JavaScript functions that never use this
- static-objects: a primitive member stored once by an unconditional top-level statement of the object's own file (app.math.EPSILON = 1e-9) reads as its literal where the read provably runs after the store: inside functions when the statements before the store run no code, and in later top-level statements of that file. SCRIPTC_SHAPE_FOLD_STORES=0 opts out. - JavaScript functions read the ambient receiver at entry only when this occurs in their parameters or body outside nested non-arrow functions. - npm case static-stores pins both, with the overlay on and off. nsbench (n=20000, overlay on): 64.4 -> 36 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
439f973dd9
commit
484fc2cb13
@@ -61,6 +61,29 @@ function declareCallReceiver(
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether a JavaScript function's own `this` can be read: a `this` in its
|
||||
* parameters or body, outside nested non-arrow functions (which bind their
|
||||
* own). Conservative inside classes and object literal methods. A function
|
||||
* that never reads it skips the ambient receiver read at entry. */
|
||||
function readsOwnThis(node: ts.FunctionLikeDeclaration): boolean {
|
||||
let found = false;
|
||||
const visit = (n: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (n.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
if (ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n)) return;
|
||||
ts.forEachChild(n, (c) => {
|
||||
visit(c);
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
for (const p of node.parameters.nodes) visit(p);
|
||||
if (node.body !== undefined) visit(node.body);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** True when the identifier resolves (through import aliases) to a
|
||||
* top-level function declaration of ANY program file (not merely a
|
||||
* same-named local shadowing one). Functions declared directly in a
|
||||
@@ -335,7 +358,7 @@ export function lowerLambda(
|
||||
try {
|
||||
const receiver =
|
||||
(checkedReceiver ||
|
||||
isJsSourceFile(ts.getSourceFileOfNode(node)) ||
|
||||
(isJsSourceFile(ts.getSourceFileOfNode(node)) && readsOwnThis(node)) ||
|
||||
node.parameters.nodes.some(isThisParameter)) &&
|
||||
!ts.isArrowFunction(node)
|
||||
? declareCallReceiver(lowerer, node, isGenerator)
|
||||
@@ -535,7 +558,8 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
lowerer.fnStack.push(ctx);
|
||||
try {
|
||||
const receiver =
|
||||
decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(ts.getSourceFileOfNode(decl))
|
||||
decl.parameters.nodes.some(isThisParameter) ||
|
||||
(isJsSourceFile(ts.getSourceFileOfNode(decl)) && readsOwnThis(decl))
|
||||
? declareCallReceiver(lowerer, decl, sig.generator !== undefined)
|
||||
: null;
|
||||
const { params, prologue } = lowerer.declareParams(decl.parameters.nodes, sig.params);
|
||||
|
||||
@@ -167,6 +167,45 @@ function ownLiteralMember(literal: Node, key: string): string | null {
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Whether a top-level statement runs no code but its own: no call, `new`,
|
||||
* tagged template, class (static blocks and field initializers run), getter
|
||||
* or setter, `await`, `yield` or `delete`. Function bodies do not run. */
|
||||
function runsNoCode(stmt: ts.Statement): boolean {
|
||||
if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) return true;
|
||||
if (!ts.isVariableStatement(stmt) && !ts.isExpressionStatement(stmt)) return false;
|
||||
let quiet = true;
|
||||
const visit = (n: Node): void => {
|
||||
if (!quiet) return;
|
||||
if (ts.isFunctionExpression(n) || ts.isArrowFunction(n)) return;
|
||||
if (
|
||||
ts.isCallExpression(n) ||
|
||||
ts.isNewExpression(n) ||
|
||||
ts.isTaggedTemplateExpression(n) ||
|
||||
ts.isClassExpression(n) ||
|
||||
ts.isGetAccessorDeclaration(n) ||
|
||||
ts.isSetAccessorDeclaration(n) ||
|
||||
ts.isAwaitExpression(n) ||
|
||||
ts.isYieldExpression(n) ||
|
||||
ts.isDeleteExpression(n) ||
|
||||
ts.isMethodDeclaration(n)
|
||||
) {
|
||||
quiet = false;
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(n, (c) => {
|
||||
visit(c);
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
visit(stmt);
|
||||
return quiet;
|
||||
}
|
||||
|
||||
function enclosingFunctionLike(n: Node): Node | null {
|
||||
for (let p = n.parent; p !== undefined; p = p.parent) if (ts.isFunctionLike(p)) return p;
|
||||
return null;
|
||||
}
|
||||
|
||||
function primitiveLiteralText(e: ts.Expression): string | null {
|
||||
if (
|
||||
ts.isNumericLiteral(e) ||
|
||||
@@ -758,19 +797,80 @@ export function planStaticObjectRewrites(
|
||||
// the object does and nothing writes it, so every read sees it. (A
|
||||
// member stored after the object was created could be read before the
|
||||
// store; those stay reads.)
|
||||
//
|
||||
// A member stored once by an unconditional top-level statement of the
|
||||
// object's own file (`app.math.EPSILON = 1e-9;`) folds too, at reads that
|
||||
// provably run after the store: reads inside functions, when no code can
|
||||
// run before the store (the statements before it call nothing, so no
|
||||
// function runs and no other module starts), and top-level reads after
|
||||
// it in the same file.
|
||||
const writesOf = new Map<Atom, Map<string, Node[]>>();
|
||||
for (const op of analysis.ops) {
|
||||
if (op.kind !== "write" || op.name === null) continue;
|
||||
for (const a of analysis.nodes[op.recv]!.atoms) {
|
||||
let byKey = writesOf.get(a);
|
||||
if (byKey === undefined) writesOf.set(a, (byKey = new Map()));
|
||||
let l = byKey.get(op.name);
|
||||
if (l === undefined) byKey.set(op.name, (l = []));
|
||||
if (!l.includes(op.node)) l.push(op.node);
|
||||
}
|
||||
}
|
||||
const quietPrefix = new Map<ts.Statement, boolean>();
|
||||
const storedLiteral = (
|
||||
atom: Atom,
|
||||
site: Node,
|
||||
key: string,
|
||||
): { literal: string; stmt: ts.ExpressionStatement; statements: readonly Node[] } | null => {
|
||||
const writes = writesOf.get(atom)?.get(key) ?? [];
|
||||
if (writes.length !== 1) return null;
|
||||
const target = writes[0]!;
|
||||
const assign = target.parent;
|
||||
if (
|
||||
assign === undefined ||
|
||||
!ts.isBinaryExpression(assign) ||
|
||||
assign.left !== target ||
|
||||
assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken
|
||||
)
|
||||
return null;
|
||||
const stmt = assign.parent;
|
||||
if (stmt === undefined || !ts.isExpressionStatement(stmt) || !ts.isSourceFile(stmt.parent))
|
||||
return null;
|
||||
if (stmt.getSourceFile() !== site.getSourceFile()) return null;
|
||||
const literal = primitiveLiteralText(assign.right);
|
||||
if (literal === null) return null;
|
||||
const statements = stmt.parent.statements.nodes;
|
||||
let quiet = quietPrefix.get(stmt);
|
||||
if (quiet === undefined) {
|
||||
quiet = statements.slice(0, statements.indexOf(stmt)).every(runsNoCode);
|
||||
quietPrefix.set(stmt, quiet);
|
||||
}
|
||||
return quiet ? { literal, stmt, statements } : null;
|
||||
};
|
||||
let foldedReads = 0;
|
||||
for (const { op, atoms } of mergedOps) {
|
||||
if (op.kind !== "read" || op.name === null) continue;
|
||||
if (atoms.size !== 1) continue;
|
||||
const o = constantAtoms.get([...atoms][0]!);
|
||||
const atom = [...atoms][0]!;
|
||||
const o = constantAtoms.get(atom);
|
||||
if (o === undefined || o.site.kind !== "literal") continue;
|
||||
const field = o.fields.find((f) => f.key === op.name);
|
||||
if (field === undefined || field.stores !== 1) continue;
|
||||
const literal = ownLiteralMember(o.site.node, op.name);
|
||||
if (literal === null) continue;
|
||||
const node = op.node;
|
||||
if (!ts.isPropertyAccessExpression(node) && !ts.isElementAccessExpression(node)) continue;
|
||||
if (!isPlainRead(node) || topStatement(node) === null) continue;
|
||||
let literal = ownLiteralMember(o.site.node, op.name);
|
||||
if (literal === null && process.env["SCRIPTC_SHAPE_FOLD_STORES"] !== "0") {
|
||||
const stored = storedLiteral(atom, o.site.node, op.name);
|
||||
if (stored === null) continue;
|
||||
const inFunction = enclosingFunctionLike(node) !== null;
|
||||
const top = topStatement(node)!;
|
||||
const after =
|
||||
top.getSourceFile() === stored.stmt.getSourceFile() &&
|
||||
stored.statements.indexOf(top) > stored.statements.indexOf(stored.stmt);
|
||||
if (!inFunction && !after) continue;
|
||||
literal = stored.literal;
|
||||
}
|
||||
if (literal === null) continue;
|
||||
// An element key needs no parentheses (and a parenthesized literal key
|
||||
// would read as a computed one).
|
||||
const key =
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/static-stores/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// The JavaScript consumer of static-stores (main.ts is the TypeScript one).
|
||||
const { lib, other } = require("static-stores");
|
||||
|
||||
let near = 0;
|
||||
for (let i = 0; i < 20; i++) if (lib.near((i - 10) / 10)) near++;
|
||||
console.log(near, lib.name(), lib.early, lib.after);
|
||||
console.log(lib.counter.inc(), lib.counter.inc(), lib.twice(21), lib.tagOf.call({ tag: "t" }));
|
||||
console.log(other.seen, other.check());
|
||||
@@ -0,0 +1,9 @@
|
||||
// Primitive members stored after their namespace was created, and functions
|
||||
// with and without a receiver (js-main.cjs is the JavaScript consumer).
|
||||
import { lib, other } from "static-stores";
|
||||
|
||||
let near = 0;
|
||||
for (let i = 0; i < 20; i++) if (lib.near((i - 10) / 10)) near++;
|
||||
console.log(near, lib.name(), lib.early, lib.after);
|
||||
console.log(lib.counter.inc(), lib.counter.inc(), lib.twice(21), lib.tagOf.call({ tag: "t" }));
|
||||
console.log(other.seen, other.check());
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export declare const lib: {
|
||||
early: number | undefined;
|
||||
after: number;
|
||||
near(x: number): boolean;
|
||||
name(): string;
|
||||
counter: { n: number; inc(): number };
|
||||
twice(a: number): number;
|
||||
tagOf(this: { tag: string }): string;
|
||||
};
|
||||
export declare const other: { seen: number; check(): boolean };
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// Primitive members stored after their namespace object was created. Reads
|
||||
// that provably run after the store read the stored literal; a read that can
|
||||
// run before it (top-level code before the store, or any read once code has
|
||||
// run before the store) stays a read.
|
||||
var lib = lib || {};
|
||||
lib.cfg = lib.cfg || {};
|
||||
lib.cfg.EPS = 0.5;
|
||||
lib.cfg.NAME = "v1";
|
||||
lib.early = lib.cfg.LATE;
|
||||
lib.cfg.LATE = 7;
|
||||
lib.after = lib.cfg.LATE + 1;
|
||||
lib.near = function (x) {
|
||||
return Math.abs(x) <= lib.cfg.EPS;
|
||||
};
|
||||
lib.name = function () {
|
||||
return lib.cfg.NAME + ":" + lib.cfg.LATE;
|
||||
};
|
||||
// Functions that read their own receiver, through an arrow, and not at all.
|
||||
lib.counter = {
|
||||
n: 0,
|
||||
inc: function () {
|
||||
this.n++;
|
||||
return this.n;
|
||||
},
|
||||
};
|
||||
lib.twice = function (a) {
|
||||
return a * 2;
|
||||
};
|
||||
lib.tagOf = function () {
|
||||
const read = () => this.tag;
|
||||
return read();
|
||||
};
|
||||
|
||||
// A store after a call: code ran before it, so no read folds.
|
||||
var other = {};
|
||||
other.log = [];
|
||||
other.note = function (s) {
|
||||
other.log.push(s);
|
||||
return other.log.length;
|
||||
};
|
||||
other.seen = other.note("start");
|
||||
other.flag = true;
|
||||
other.check = function () {
|
||||
return other.flag;
|
||||
};
|
||||
|
||||
module.exports = { lib, other };
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "static-stores", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "static-stores-case", "private": true, "type": "module" }
|
||||
@@ -0,0 +1,46 @@
|
||||
/* Static-object specialization (shape-inference/static-objects.ts): with the
|
||||
* shape overlay on, the npm case static-stores (through its JavaScript
|
||||
* consumer, js-main.cjs) reads primitive members stored after their
|
||||
* namespace object was created as literals where the read provably runs
|
||||
* after the store, keeps the reads that can run before it, and prints what
|
||||
* Node prints. Its functions with and without a receiver read `this` as
|
||||
* Node does. The npm suites run the TypeScript consumer with and without
|
||||
* the overlay. */
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "@scriptc/compiler";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = join(import.meta.dirname, "../..");
|
||||
const entry = join(repoRoot, "tests/fixtures/npm/cases/static-stores/js-main.cjs");
|
||||
const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/static-stores");
|
||||
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
||||
|
||||
test(
|
||||
"members stored after their namespace was created read as literals after the store",
|
||||
{ timeout: 600_000 },
|
||||
async () => {
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const result = await compile(entry, {
|
||||
outPath: join(outDir, "main"),
|
||||
outDir,
|
||||
sanitize,
|
||||
shapeOverlay: true,
|
||||
});
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const report = (result.warnings ?? []).find((w) => w.code === "SC6103");
|
||||
expect(report?.message).toMatch(/, 5 constant reads\)/);
|
||||
const pending = execFileAsync(result.binaryPath, [], { cwd: dirname(entry), encoding: "utf8" });
|
||||
pending.child.stdin?.end();
|
||||
const { stdout } = await pending;
|
||||
const node = await execFileAsync(process.execPath, [entry], {
|
||||
cwd: dirname(entry),
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(stdout).toBe(node.stdout);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user