Close nsbench's gap to its typed twin: store folds, receiver reads, exhaustive table dispatch (#2255)

This commit is contained in:
Chris Tate
2026-10-25 22:18:04 -05:00
committed by GitHub
10 changed files with 596 additions and 10 deletions
@@ -238,6 +238,149 @@ function ownLiteralMember(literal: Node, key: string): string | null {
return found;
}
/** Whether a top-level statement runs no code but its own. Its expressions
* are literals, identifiers, functions (not run), object and array literals
* without spreads or computed keys, `=` stores, `var` bindings without
* patterns, `&&`, `||`, `??`, `?:`, `,`, `!`, `typeof`, `void`, `===` and
* `!==`, none of which calls a function, and member reads and stores that
* `plainMember` accepts (no getter or setter runs). Every other form can
* run code: a call, `new` or tagged template, a class (static blocks and
* field initializers run), coercion (arithmetic, `+`, templates with
* substitutions, relational and loose comparisons, `in`, `instanceof`,
* computed keys), iteration (spreads, destructuring), `delete`, `await` and
* `yield`. */
function runsNoCode(stmt: ts.Statement, plainMember: (n: Node) => boolean): boolean {
if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) return true;
if (ts.isExpressionStatement(stmt)) return quietExpression(stmt.expression, plainMember);
if (!ts.isVariableStatement(stmt)) return false;
return stmt.declarationList.declarations.nodes.every(
(d) =>
ts.isIdentifier(d.name) &&
(d.initializer === undefined || quietExpression(d.initializer, plainMember)),
);
}
function quietExpression(e: ts.Expression, plainMember: (n: Node) => boolean): boolean {
const quiet = (x: ts.Expression): boolean => quietExpression(x, plainMember);
if (ts.isParenthesizedExpression(e)) return quiet(e.expression);
if (
ts.isIdentifier(e) ||
ts.isNumericLiteral(e) ||
ts.isBigIntLiteral(e) ||
ts.isStringLiteral(e) ||
ts.isNoSubstitutionTemplateLiteral(e) ||
ts.isRegularExpressionLiteral(e) ||
ts.isFunctionExpression(e) ||
ts.isArrowFunction(e) ||
e.kind === ts.SyntaxKind.TrueKeyword ||
e.kind === ts.SyntaxKind.FalseKeyword ||
e.kind === ts.SyntaxKind.NullKeyword ||
e.kind === ts.SyntaxKind.ThisKeyword
)
return true;
if (ts.isObjectLiteralExpression(e))
return e.properties.nodes.every((m) => {
if (ts.isShorthandPropertyAssignment(m)) return m.objectAssignmentInitializer === undefined;
if (!ts.isPropertyAssignment(m)) return false;
const name = m.name;
if (!ts.isIdentifier(name) && !ts.isStringLiteral(name) && !ts.isNumericLiteral(name))
return false;
return quiet(m.initializer);
});
if (ts.isArrayLiteralExpression(e))
return e.elements.nodes.every(
(x) => ts.isOmittedExpression(x) || (!ts.isSpreadElement(x) && quiet(x)),
);
if (ts.isTypeOfExpression(e) || ts.isVoidExpression(e)) return quiet(e.expression);
if (ts.isPrefixUnaryExpression(e))
return e.operator === ts.SyntaxKind.ExclamationToken && quiet(e.operand);
if (ts.isConditionalExpression(e))
return quiet(e.condition) && quiet(e.whenTrue) && quiet(e.whenFalse);
if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e))
return quietMember(e, plainMember);
if (!ts.isBinaryExpression(e)) return false;
switch (e.operatorToken.kind) {
case ts.SyntaxKind.EqualsToken:
if (ts.isIdentifier(e.left)) return quiet(e.right);
return (
(ts.isPropertyAccessExpression(e.left) || ts.isElementAccessExpression(e.left)) &&
quietMember(e.left, plainMember) &&
quiet(e.right)
);
case ts.SyntaxKind.AmpersandAmpersandToken:
case ts.SyntaxKind.BarBarToken:
case ts.SyntaxKind.QuestionQuestionToken:
case ts.SyntaxKind.CommaToken:
case ts.SyntaxKind.EqualsEqualsEqualsToken:
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
return quiet(e.left) && quiet(e.right);
default:
return false;
}
}
/** A member read or store target with a literal key on a receiver that
* runs no code, which `plainMember` accepts. */
function quietMember(
e: ts.PropertyAccessExpression | ts.ElementAccessExpression,
plainMember: (n: Node) => boolean,
): boolean {
if (e.questionDotToken !== undefined) return false;
if (
ts.isElementAccessExpression(e) &&
!ts.isStringLiteral(e.argumentExpression) &&
!ts.isNumericLiteral(e.argumentExpression)
)
return false;
return plainMember(e) && quietExpression(e.expression, plainMember);
}
/** Whether an identifier is written where it appears: an assignment or
* update target, a destructuring assignment or loop target (through array
* and object patterns, defaults and rests), or a `var`, `let`, `const` or
* parameter binding of the name (a nested binding shadows it, which only
* makes the answer conservative). */
function writesBinding(id: ts.Identifier): boolean {
let n: Node = id;
for (let p = n.parent; p !== undefined; n = p, p = p.parent) {
if (ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p))
return (
p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken
);
if (ts.isBinaryExpression(p)) {
const op = p.operatorToken.kind;
if (op >= ts.SyntaxKind.FirstAssignment && op <= ts.SyntaxKind.LastAssignment)
return p.left === n;
return false;
}
if (ts.isForOfStatement(p) || ts.isForInStatement(p)) return p.initializer === n;
if (ts.isVariableDeclaration(p) || ts.isParameter(p) || ts.isBindingElement(p))
return p.name === n;
if (
ts.isParenthesizedExpression(p) ||
ts.isArrayLiteralExpression(p) ||
ts.isObjectLiteralExpression(p) ||
ts.isSpreadElement(p) ||
ts.isSpreadAssignment(p) ||
ts.isShorthandPropertyAssignment(p) ||
ts.isArrayBindingPattern(p) ||
ts.isObjectBindingPattern(p)
)
continue;
if (ts.isPropertyAssignment(p)) {
if (p.initializer !== n) return false;
continue;
}
return false;
}
return false;
}
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) ||
@@ -1273,10 +1416,6 @@ export function planStaticObjectRewrites(
if (parent === undefined || ts.isDeleteExpression(parent)) continue;
if (!ts.isPropertyAccessExpression(node)) continue;
if (topStatement(node) === null) continue;
// The receiver's evaluation is dropped, and the binding exists from the
// store on (a function) or from its declaration on (a class).
if (!pureReceiver(node.expression, node)) continue;
if (!runsAfter(node, storeOf.get(field.values[0]!)!)) continue;
// The reference replaces the whole member expression: its receiver
// must be one that evaluating runs no code.
if (!effectFree(node.expression)) {
@@ -1287,6 +1426,10 @@ export function planStaticObjectRewrites(
kept.push(`reference ${node.getText()}: it may run before the member is stored`);
continue;
}
// The receiver's evaluation is dropped, and the binding exists from the
// store on (a function) or from its declaration on (a class).
if (!pureReceiver(node.expression, node)) continue;
if (!runsAfter(node, storeOf.get(field.values[0]!)!)) continue;
if (node.getSourceFile() !== field.values[0]!.node!.getSourceFile()) {
const text = crossFileReference(node, field.values[0]!.node!.getSourceFile(), name);
if (text === null) continue;
@@ -1319,16 +1462,84 @@ 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);
}
}
// Member reads and stores whose receiver can only be constant objects or
// primitives: neither has accessors of its own, so no getter or setter
// runs (a primitive receiver of a store throws, or reads its prototype).
const memberReceivers = new Map<Node, Set<Atom>>();
for (const op of analysis.ops) {
if ((op.kind !== "read" && op.kind !== "write") || op.name === null) continue;
let set = memberReceivers.get(op.node);
if (set === undefined) memberReceivers.set(op.node, (set = new Set()));
for (const a of analysis.nodes[op.recv]!.atoms) set.add(a);
}
const plainMember = (n: Node): boolean => {
const atoms = memberReceivers.get(n);
return (
atoms !== undefined &&
atoms.size > 0 &&
[...atoms].every((a) => a.kind === "prim" || constantAtoms.has(a))
);
};
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((s) => runsNoCode(s, plainMember));
quietPrefix.set(stmt, quiet);
}
return quiet ? { literal, stmt, statements } : null;
};
let foldedReads = 0;
for (const { op, atoms, nullish } 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;
@@ -1340,6 +1551,19 @@ export function planStaticObjectRewrites(
kept.push(`constant read ${node.getText()}: it may run before the object is created`);
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 =
@@ -1387,7 +1611,51 @@ export function planStaticObjectRewrites(
call: ts.CallExpression;
arms: [string, string][];
nullish: boolean;
exhaustive: boolean;
}[] = [];
// Whether a key parameter only ever holds one of the table's keys: its
// function never escapes, every call of it passes a string literal key
// of the table, and its body neither assigns the parameter nor uses
// `arguments` (which aliases parameters in sloppy code). The dispatch
// then needs no arm for other keys.
const literalKeysOnly = (keyDecl: Node, keys: ReadonlySet<string>): boolean => {
if (process.env["SCRIPTC_SHAPE_EXHAUSTIVE_TABLES"] === "0") return false;
if (!ts.isParameter(keyDecl) || !ts.isIdentifier(keyDecl.name)) return false;
const fn = keyDecl.parent;
if (fn === undefined || !ts.isFunctionLike(fn) || escapedFns.has(fn)) return false;
for (const a of analysis.atoms.atoms)
if (a.kind === "function" && a.node === fn && analysis.escaped.has(a)) return false;
const index = (fn as ts.FunctionLikeDeclaration).parameters.nodes.indexOf(keyDecl);
const name = keyDecl.name.text;
// Any write of the name in the function, its other parameters included,
// or a direct eval or `arguments` (which aliases parameters in sloppy
// code) can give the key another value.
let touched = false;
const visit = (n: Node): void => {
if (touched) return;
if (ts.isIdentifier(n) && (n.text === "arguments" || n.text === "eval")) touched = true;
else if (ts.isIdentifier(n) && n.text === name && n !== keyDecl.name && writesBinding(n))
touched = true;
ts.forEachChild(n, (c) => {
visit(c);
return undefined;
});
};
for (const p of (fn as ts.FunctionLikeDeclaration).parameters.nodes) visit(p);
const body = (fn as { body?: Node }).body;
if (body === undefined) return false;
visit(body);
if (touched) return false;
const edges = analysis.calls.filter((c) => c.callee.node === fn);
if (edges.length === 0) return false;
return edges.every((e) => {
if (!ts.isCallExpression(e.site)) return false;
const args = e.site.arguments.nodes;
if (args.slice(0, index + 1).some((a) => ts.isSpreadElement(a))) return false;
const arg = args[index];
return arg !== undefined && ts.isStringLiteral(arg) && keys.has(arg.text);
});
};
for (const { op, atoms, nullish } of mergedOps) {
if (op.kind !== "computedRead") continue;
const node = op.node;
@@ -1438,16 +1706,35 @@ export function planStaticObjectRewrites(
kept.push(`${where}: it may run before the table is created`);
continue;
}
tableCalls.push({ sf: call.getSourceFile(), call, arms, nullish });
tableCalls.push({
sf: call.getSourceFile(),
call,
arms,
nullish,
exhaustive: literalKeysOnly(keyDecl, new Set(arms.map(([k]) => k))),
});
}
for (const { sf, call, arms, nullish } of tableCalls) {
for (const { sf, call, arms, nullish, exhaustive } of tableCalls) {
const node = call.expression as ts.ElementAccessExpression;
const key = node.argumentExpression.getText();
const args = call.arguments.nodes.map((a) => sliceWithCalls(sf, a)).join(", ");
const table = sliceWithCalls(sf, node.expression);
const text = arms
const tested = exhaustive ? arms.slice(0, -1) : arms;
const text = tested
.map(([k, name]) => `${key} === ${JSON.stringify(k)} ? ${name}(${args}) : `)
.join("");
if (exhaustive) {
callEdits.push({
sf,
edit: {
start: ts.tokenStartOfNode(call),
end: call.end,
text: `(${nullish ? `${table}[${key}], ` : ""}${text}${arms[arms.length - 1]![1]}(${args}))`,
},
});
tableDispatches++;
continue;
}
callEdits.push({
sf,
edit: {
@@ -0,0 +1,148 @@
/* Static-object specialization (shape-inference/static-objects.ts) keeps two
* reads dynamic when the facts it would fold on can change:
* - a member stored after a statement that can run code (a coercion such as
* `+` can call toString or valueOf) keeps its read in functions, which
* that code could call before the store (the statements checked are all
* the file's top-level statements before the store);
* - a table call whose key parameter the function rebinds (destructuring,
* a loop target, another parameter's default or a `var` redeclaration)
* keeps reading the table for other keys, though every caller passes one
* of the table's keys.
* The checks read the rewritten package text SCRIPTC_DEBUG_SHAPE_OVERLAY
* prints. */
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { compile } from "../src/index.js";
const packageSource = `var quiet = quiet || {};
quiet.cfg = { U: 1 };
quiet.read = function () {
return quiet.cfg.V;
};
quiet.label = quiet.cfg.U;
quiet.cfg.V = 6;
var late = late || {};
late.cfg = { U: 1 };
late.read = function () {
return late.cfg.V;
};
late.label = "v" + late.cfg.U;
late.cfg.V = 5;
var t = t || {};
t.ops = {
a: function (v) {
return v + 1;
},
b: function (v) {
return v * 2;
},
};
t.plain = function (op, v) {
return t.ops[op](v);
};
t.pattern = function (op, v) {
[op] = ["c"];
return t.ops[op](v);
};
t.loop = function (op, v) {
for (op of ["c"]);
return t.ops[op](v);
};
t.initializer = function (op, v, w = (op = "c")) {
return t.ops[op](v);
};
t.redeclared = function (op, v) {
var op = "c";
return t.ops[op](v);
};
module.exports = { late, quiet, t };
`;
const consumer = `const { late, quiet, t } = require("guard-fixture");
console.log(late.read(), late.label, quiet.read(), quiet.label);
const tryCall = (call) => {
try {
return call();
} catch (e) {
return e.name;
}
};
console.log(t.plain("a", 1), t.plain("b", 2));
console.log(tryCall(() => t.pattern("a", 1)), tryCall(() => t.loop("b", 2)));
console.log(tryCall(() => t.initializer("a", 1)), tryCall(() => t.redeclared("b", 2)));
`;
let dir = "";
const saved: Record<string, string | undefined> = {};
beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), "scriptc-static-guards-"));
const pkg = join(dir, "node_modules/guard-fixture");
await mkdir(pkg, { recursive: true });
await writeFile(
join(pkg, "package.json"),
JSON.stringify({ name: "guard-fixture", version: "1.0.0", main: "index.js" }),
);
await writeFile(join(pkg, "index.js"), packageSource);
await writeFile(
join(dir, "package.json"),
JSON.stringify({ dependencies: { "guard-fixture": "1.0.0" } }),
);
await writeFile(join(dir, "main.cjs"), consumer);
for (const k of ["SCRIPTC_SHAPE_OVERLAY", "SCRIPTC_DEBUG_SHAPE_OVERLAY", "SCRIPTC_NO_CACHE"])
saved[k] = process.env[k];
});
afterEach(async () => {
for (const [k, v] of Object.entries(saved)) {
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
vi.restoreAllMocks();
await rm(dir, { recursive: true, force: true });
});
/** The package text after the structural rewrites. */
async function rewrittenPackage(): Promise<string> {
process.env["SCRIPTC_SHAPE_OVERLAY"] = "1";
process.env["SCRIPTC_DEBUG_SHAPE_OVERLAY"] = "1";
process.env["SCRIPTC_NO_CACHE"] = "1";
let text = "";
vi.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array) => {
text += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
return true;
});
const result = await compile(join(dir, "main.cjs"), {
outDir: dir,
outPath: join(dir, "main.ir.json"),
outputKind: "ir",
npmStatic: ["guard-fixture"],
});
vi.restoreAllMocks();
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
const start = text.indexOf("[static-objects] ");
const end = text.indexOf("[shape-overlay] ", start);
return start < 0 ? "" : text.slice(start, end < 0 ? undefined : end);
}
/** The body of the function the rewritten text assigns to or hoists as `name`. */
function body(text: string, name: string): string {
const at = text.search(new RegExp(`function [\\w$]*${name}\\b|${name} = function`));
return at < 0 ? "" : text.slice(at, text.indexOf("\n}", at));
}
test("a store after code that can run keeps the read; a quiet prefix folds it", async () => {
const text = await rewrittenPackage();
expect(body(text, "quiet_read")).toMatch(/return \(.*\b6\);/);
expect(body(text, "late_read")).toContain("return late.cfg.V");
});
test("a table call whose key parameter is rebound keeps reading the table", async () => {
const text = await rewrittenPackage();
// Every caller passes "a" or "b": the plain call needs no arm for other keys.
expect(body(text, "t_plain")).not.toContain("t.ops[op](v)");
for (const name of ["t_pattern", "t_loop", "t_initializer", "t_redeclared"])
expect(body(text, name), name).toContain("t.ops[op](v)");
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/static-stores/main.ts"
],
"diags": []
}
+10
View File
@@ -0,0 +1,10 @@
// 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" }));
const key = process.argv.length > 99 ? "dbl" : "inc";
console.log(lib.apply("inc", 1), lib.apply("dbl", 4), lib.applyAny("dbl", 5), lib.applyAny(key, 3));
console.log(other.seen, other.check());
+11
View File
@@ -0,0 +1,11 @@
// 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" }));
const key = process.argv.length > 99 ? "dbl" : "inc";
console.log(lib.apply("inc", 1), lib.apply("dbl", 4), lib.applyAny("dbl", 5), lib.applyAny(key, 3));
console.log(other.seen, other.check());
@@ -0,0 +1,12 @@
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;
apply(op: "inc" | "dbl", v: number): number;
applyAny(op: string, v: number): number | string;
};
export declare const other: { seen: number; check(): boolean };
@@ -0,0 +1,64 @@
// 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 constant function table: `apply` is only ever called with literal keys
// of the table, so its dispatch needs no arm for other keys; `applyAny` is
// called with a computed key and keeps reading the table for other keys.
lib.ops = {
inc: function (v) {
return v + 1;
},
dbl: function (v) {
return v * 2;
},
};
lib.apply = function (op, v) {
return lib.ops[op](v);
};
lib.applyAny = function (op, v) {
return lib.ops[op](v);
};
// 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(/2 function table calls, 4 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);
},
);