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:
Malte Ubl
2026-10-25 13:10:12 -07:00
co-authored by Claude Opus 5.5
parent 439f973dd9
commit 484fc2cb13
10 changed files with 257 additions and 5 deletions
@@ -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 =
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/static-stores/main.ts"
],
"diags": []
}
+8
View File
@@ -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());
+9
View File
@@ -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());
@@ -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 };
@@ -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 };
@@ -0,0 +1 @@
{ "name": "static-stores", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
+1
View File
@@ -0,0 +1 @@
{ "name": "static-stores-case", "private": true, "type": "module" }
+46
View File
@@ -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);
},
);